continued development, ready for public release

master
Sascha Nitsch 2022-03-12 21:36:54 +01:00
parent c6e4d17ffb
commit a239af8d0a
11 changed files with 1886 additions and 98 deletions

1
.gitignore vendored
View File

@ -7,3 +7,4 @@
/electronics/cimdit.kicad_prl
/electronics/fp-info-cache
/spec.d
/html

View File

@ -6,34 +6,18 @@
* Licensed under MIT license
*
*/
/// use a custom keyboard layout
#define HID_CUSTOM_LAYOUT
/// use german keyboard layout
#define LAYOUT_GERMAN
#define HAVE_DISPLAY
#ifdef HAVE_DISPLAY
/// no splash screen on OLED
#define SSD1306_NO_SPLASH
/// OLED display width, in pixels
#define SCREEN_WIDTH 128
/// OLED display height, in pixels
#define SCREEN_HEIGHT 32
/// OLED screen address
#define SCREEN_ADDRESS 0x3C
#endif
// defines
#include "defines.h"
// library includes
#include <HID-Project.h>
#ifdef HAVE_DISPLAY
#include <Adafruit_SSD1306.h>
#include "cimditssd1306.h"
#endif
// own includes
#include "cimdithal.h"
/// update usb devices every x ms if needed
#define UPDATE_INTERVAL 100
#include "cimditprofile.h"
/// our hardware abstraction layer
CimditHAL hal;
@ -48,29 +32,44 @@ void rotInt() {
/// next update time in ms
uint32_t nextUpdate = 0;
#ifdef HAVE_DISPLAY
/// our display
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
CimditSSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_ADDRESS);
#endif
/// display
///
// our profile handling class
CimditProfile profile;
/// initial setup
void setup() {
Serial.begin(115200);
hal.begin();
attachInterrupt(digitalPinToInterrupt(7), rotInt, RISING);
// initialize USB related things
// initialize USB related things
Keyboard.begin();
Mouse.begin();
Gamepad.begin();
Wire.begin();
Wire.setClock(400000);
#ifdef HAVE_DISPLAY
// init display
display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS);
display.begin();
display.clearDisplay();
display.setTextSize(2,2);
display.setCursor(28, 8);
display.write("cimdit");
display.display();
#endif
profile.begin();
}
/// protection string
const char protectString[] = "cimdit:";
/// current position in protection string
uint8_t protectPos = 0;
/// main loop
void loop() {
hal.readFromHardware(outstandingRotInterrupt);
@ -78,18 +77,62 @@ void loop() {
outstandingRotInterrupt = false;
return;
}
profile.tick();
if (Serial.available()) {
// incoming message
uint8_t in;
while ((in = Serial.read()) != 255) {
if (protectString[protectPos++] != in) {
protectPos = 0;
continue;
}
if (protectPos == sizeof(protectString) - 1) { // good, something for us
protectPos = 0;
// every line has to start with cimdit: or it will be ignored
uint8_t cmd = Serial.read();
switch (cmd) {
case 'd':
profile.userDisplay();
break;
case 'p': { // print something on display
uint8_t timeout = 0;
// read number for time to display
do {
cmd = Serial.read();
if (cmd != ',') {
timeout = timeout * 10 + cmd - '0';
}
} while (cmd != ',');
char tmp[43] = {0};
uint8_t pos = 0;
// now we expect the string, terminated by \n
do {
cmd = Serial.read();
if (cmd != '\n') {
tmp[pos++] = cmd;
}
} while (cmd != '\n' && pos < 43 && cmd != 255);
profile.setUserString(timeout, tmp);
}
break;
case 'f': // read flash
profile.printFlash();
break;
case 'F': // write flash
profile.writeFlash();
break;
}
}
}
}
if (hal.m_millis < nextUpdate && hal.m_millis > ROLLOVER_INTERVAL) return;
nextUpdate = hal.m_millis + UPDATE_INTERVAL;
uint8_t rotaryChanged = hal.rotaryChanged();
if (rotaryChanged) {
for (uint8_t i = 0; i < 7; ++i) {
for (uint8_t i = 0; i < 8; ++i) {
if (rotaryChanged & 1) {
// temporary debug output
Serial.print(F("rot "));
Serial.print(i);
Serial.print(F(" => "));
Serial.println(hal.getEncoder(i));
// end of temporary debug output
int8_t delta = hal.getEncoder(i);
profile.rotaryAction(i, delta);
}
rotaryChanged >>= 1;
}
@ -99,59 +142,24 @@ void loop() {
for (uint8_t i = 0; i < 4; ++i) {
if (analogChanged & 1) {
uint16_t analog = hal.getAnalog(i);
// temporary debug output
int16_t analogMapped16 = map(analog, 0, 1024, -32767, 32768);
int8_t analogMapped8 = map(analog, 0, 1024, -127, 127);
switch(i) {
case 0:
Gamepad.xAxis(analogMapped16);
break;
case 1:
Gamepad.yAxis(analogMapped16);
break;
case 2:
Gamepad.zAxis(analogMapped8);
break;
case 3:
Gamepad.rxAxis(analogMapped16);
break;
default:
break;
}
Serial.print(F("analog "));
Serial.print(i);
Serial.print(F(": "));
Serial.println(analogMapped16);
// end of temporary debug output
profile.axisAction(i, analog);
}
analogChanged >>= 1;
}
Gamepad.write();
}
uint64_t buttonsChanged = hal.buttonsChanged();
if (buttonsChanged) {
for (uint8_t i = 0; i < 64; ++i) {
if (buttonsChanged & 0xFF) { // quickcheck for 8 bits at a time
if (buttonsChanged & 1) {
// temporary debug output
Serial.print(F("button "));
Serial.print(i);
Serial.print(F(" "));
bool pressed = hal.getButton(i);
Serial.println(pressed);
if (i<32) {
if (pressed)
Gamepad.press(i+1);
else
Gamepad.release(i+1);
}
// end of temporary debug output
profile.buttonAction(i, pressed);
}
buttonsChanged >>= 1;
} else {
buttonsChanged >>= 8;
i += 8;
i += 7;
}
}
}

View File

@ -6,23 +6,9 @@
* Licensed under MIT license
*
*/
/// pin for analog mux bit 0
#define ANALOG_MUX0 6
/// pin for analog mux bit 1
#define ANALOG_MUX1 8
/// pin for analog mux bit 2
#define ANALOG_MUX2 9
/// pin for analog mux bit 3
#define ANALOG_MUX3 10
/// analog input pin
#define ANALOG_IN A1
/// rotary encoder interrupt pin
#define ROT_INT 7
/// scan analog every 12 milliseconds
#define ANALOG_INTERVAL 12
/// scan keys every 24 milliseconds
#define KEYSCAN_INTERVAL 24
// our defines
#include "defines.h"
// library includes
#include <Adafruit_MCP23X17.h>
@ -38,7 +24,7 @@ CimditHAL::CimditHAL() {
m_analogNum = 0;
m_analogChanged = 0;
for (uint8_t i = 0; i < 8; ++i) {
m_currentButtons[i] = 0;
m_currentButtons[i] = 0xff;
m_buttonsChanged[i] = 0;
}
m_rotChanged = 0;
@ -51,7 +37,7 @@ void CimditHAL::begin() {
pinMode(ANALOG_MUX2, OUTPUT);
pinMode(ANALOG_MUX3, OUTPUT);
m_keyMatrix.begin_I2C(0x20);
Serial.println(m_rotEncoders.begin_I2C(0x21));
m_rotEncoders.begin_I2C(0x21);
// combine the inputs to a single interrupt pin
m_rotEncoders.setupInterrupts(true, false, HIGH);
for (uint8_t i = 0; i < 8; ++i) {
@ -61,9 +47,8 @@ void CimditHAL::begin() {
m_rotEncoders.pinMode(i, INPUT_PULLUP);
m_rotEncoders.setupInterruptPin(i, CHANGE);
m_currentButtons[i] = 0xff;
m_rotaryEncoders[i].begin();
}
for (uint8_t i = 8; i < 15; ++i) {
for (uint8_t i = 8; i < 16; ++i) {
// set key matrix rows to input
m_keyMatrix.pinMode(i, INPUT_PULLUP);
// set encoders to input and setup interrupt to fire on change
@ -126,6 +111,7 @@ void CimditHAL::readFromHardware(bool interrupt) {
m_currentAnalogValues[m_analogNum] = analogRead(ANALOG_IN);
// set changed bits
if (lastAnalogValue != m_currentAnalogValues[m_analogNum]) {
//if (abs(lastAnalogValue - m_currentAnalogValues[m_analogNum]) > 1) {
m_analogChanged |= 1 << m_analogNum;
}
// set mux value for next round, wrap to 0 at 16
@ -151,6 +137,7 @@ void CimditHAL::readFromHardware(bool interrupt) {
m_keyMatrix.writeGPIO(0xff ^ (1 << m_keyRow), 0);
}
}
uint8_t CimditHAL::rotaryChanged() {
uint8_t ret = m_rotChanged;
m_rotChanged = 0;

791
cimditprofile.cpp Normal file
View File

@ -0,0 +1,791 @@
/**
* \file cimditprofile.h
* \brief Declaration of the CimditProfile class
* \author GrumpyDeveloper (Sascha Nitsch)
* \copyright 2022 Sascha Nitsch
* Licensed under MIT license
*
*/
#include "defines.h"
// system includes
#include <Arduino.h>
#include <string.h>
#include <stdlib.h>
#ifndef EXTERNAL_EEPROM
#include <EEPROM.h>
#endif
#ifdef HAVE_DISPLAY
#include "cimditssd1306.h"
#endif
// own includes
#include "cimditprofile.h"
extern CimditSSD1306 display;
extern CimditHAL hal;
/*
structure of profile in eeprom
uint16_t total size (including length bytes)
uint8_t number of profiles
for each profile:
uint16_t length length of profile in bytes (including length bytes)
char profile[] 0-terminated profile name
uint8_t custom image (0/1)
if custom image
uint8_t[512] image data
uint8_t button_mapping number
repeat for each mapped button
uint8_t button number
<mapping enum> uint8_t mapping type
int8_t value depending on mapping
uint8_t axis mapping number
repeated for each mapped axis
uint8_t axis number
<mapping enum> uint8_t mapping type
int8_t value depending on mapping
uint8_t rotary mapping number
repeated for each rotary encoder
uint8_t encoder number (note, each encoder has 2 buttons: counter-clockwise and clockwise)
<mapping enum> uint8_t mapping type
int8_t value depending on mapping
uint8_t number of macro sequences
repeated for every macro sequence
const char[] macro sequence, 0-terminated
*/
CimditProfile::CimditProfile() {
m_numProfiles = 0;
for (uint8_t i = 0; i < NUMBER_OF_PROFILES; ++i) {
m_profiles[i][0] = 0;
m_mappedRotary[i].m_number = -1;
}
m_numMappedButtons = 0;
m_numMappedAxis = 0;
m_numMappedRotary = 0;
m_activeProfile = 0;
m_selectingProfile = 0;
m_mouseMoveX = 0;
m_mouseMoveY = 0;
m_eepromPosition = 0;
m_stateTimeout = 0;
m_numMacros = 0;
m_macroStart = nullptr;
m_nextBarUpdate = 0;
m_dotX = 0;
m_dotY = 0;
m_relMouseX = 0;
m_relMouseY = 0;
m_displayState = DISPLAY_BLANK;
m_userTimeout = 0;
m_stateStarted = 0;
m_customImage = 0;
memset(m_userString, 0, sizeof(m_userString));
}
uint8_t CimditProfile::nextUInt8() {
uint8_t ret;
#ifdef EXTERNAL_EEPROM
Wire.beginTransmission(EXTERNAL_EEPROM);
Wire.write((uint8_t)(m_eepromPosition >> 8)); // MSB
Wire.write((uint8_t)(m_eepromPosition & 0xFF)); // LSB
Wire.endTransmission();
Wire.requestFrom(EXTERNAL_EEPROM, 1);
ret = Wire.read();
++m_eepromPosition;
return ret;
#else
return EEPROM.read(m_eepromPosition++);
#endif
}
uint16_t CimditProfile::nextUInt16() {
return ((uint16_t)(nextUInt8()) << 8) + nextUInt8();
}
void CimditProfile::nextString(unsigned char* destination) {
uint8_t next = nextUInt8();
while (next != 0) {
*destination = next;
next = nextUInt8();
++destination;
}
*destination = 0;
}
void CimditProfile::begin() {
initProfiles();
}
void CimditProfile::initProfiles() {
m_eepromPosition = 2;
// number of profiles
m_numProfiles = nextUInt8();
if (m_numProfiles > NUMBER_OF_PROFILES) m_numProfiles = 0;
for (uint8_t i = 0; i < m_numProfiles; ++i) {
uint16_t position = m_eepromPosition;
uint16_t size = nextUInt16();
nextString((unsigned char*)(m_profiles[i]));
m_eepromPosition = position + size; // we only scan the names here
}
if (m_numProfiles>0) {
load(0);
}
}
void CimditProfile::load(uint8_t num) {
m_activeProfile = num;
m_selectingProfile = num;
// skip number of profiles
m_eepromPosition = 3;
// seek to right profile
for (uint8_t i = 0; i < num; ++i) {
// get profile length
uint16_t size = nextUInt16();
m_eepromPosition += size - 2;
}
// ignore 2 bytes size
m_eepromPosition += 2;
// m_eepromPosition points to our profile to parse
// name is already saved, skip it
while (nextUInt8() != 0) {
}
// do we have a custom image?
m_customImage = nextUInt8() > 0 ? m_eepromPosition : 0;
if (m_customImage) {
// skip 512 bytes
m_eepromPosition += 512;
}
// mapped buttons
// number of mapped buttons
m_numMappedButtons = nextUInt8();
if (m_numMappedButtons) {
for (uint8_t i = 0; i < m_numMappedButtons ; ++i) {
m_mappedButtons[i].m_number = nextUInt8();
m_mappedButtons[i].m_type = (MappingType)(nextUInt8());
m_mappedButtons[i].m_value = nextUInt8();
}
}
// end of mapped buttons
// mapped axis
// number of mapped axis
m_numMappedAxis = nextUInt8();
if (m_numMappedAxis) {
for (uint8_t i = 0; i < m_numMappedAxis ; ++i) {
m_mappedAxis[i].m_number = nextUInt8();
m_mappedAxis[i].m_type = (MappingType)(nextUInt8());
m_mappedAxis[i].m_value = nextUInt8();
}
}
// end of mapped axis
// mapped rotary
// number of mapped rotary
m_numMappedRotary = nextUInt8();
if (m_numMappedRotary) {
for (uint8_t i = 0; i < m_numMappedRotary ; ++i) {
m_mappedRotary[i].m_number = nextUInt8();
m_mappedRotary[i].m_type = (MappingType)(nextUInt8());
m_mappedRotary[i].m_value = nextUInt8();
}
}
// end of mapped rotary
m_mouseMoveX = 0;
m_mouseMoveY = 0;
// parse macro
m_numMacros = nextUInt8();
if (m_macroStart) {
free(m_macroStart);
}
m_macroStart = reinterpret_cast<uint16_t*>(malloc(m_numMacros * sizeof(uint16_t)));
for (uint8_t macroNum = 0; macroNum < m_numMacros; ++macroNum) {
m_macroStart[macroNum] = m_eepromPosition;
scanOrExecuteMacro(macroNum, false);
}
showActiveProfile();
}
void CimditProfile::scanOrExecuteMacro(uint8_t macroNum, bool execute) {
m_eepromPosition = m_macroStart[macroNum];
uint8_t value8 = 0;
uint16_t delayTime = 0;
MACROCOMMANDS token;
do {
token = (MACROCOMMANDS)nextUInt8();
switch (token) {
case MACRO_NULL:
break;
case MACRO_SPEED: // read in 1 extra numberic value 8 bit
delayTime = 1000 / nextUInt8();
break;
case MACRO_JOY_PRESS: // read in 1 extra numberic value 8 bit
value8 = nextUInt8();
if (execute) {
Gamepad.press(value8);
Gamepad.write();
}
break;
case MACRO_JOY_RELEASE: // read in 1 extra numberic value 8 bit
value8 = nextUInt8();
if (execute) {
Gamepad.release(value8);
Gamepad.write();
}
break;
case MACRO_MOUSE_PRESS: // read in 1 extra numberic value 8 bit
value8 = nextUInt8();
if (execute) {
Mouse.press(value8);
}
break;
case MACRO_MOUSE_RELEASE: // read in 1 extra numberic value 8 bit
value8 = nextUInt8();
if (execute) {
Mouse.release(value8);
}
break;
case MACRO_MOUSE_WHEEL: // read in 1 extra numberic value 8 bit
value8 = nextUInt8();
if (execute) {
Mouse.move(0, 0, value8);
}
break;
case MACRO_DELAY: // read in 1 extra numberic value 16 bit
if (execute) {
delay(nextUInt16());
}
break;
case MACRO_KEY_PRESS: // one or more keys
while ((value8 = nextUInt8()) != MACRO_END) {
if (execute) {
Keyboard.press((KeyboardKeycode)value8);
if (delayTime) delay(delayTime);
}
}
break;
case MACRO_KEY_RELEASE:
while ((value8 = nextUInt8()) != MACRO_END) {
if (execute) {
Keyboard.release((KeyboardKeycode)value8);
if (delayTime) delay(delayTime);
}
}
break;
case MACRO_TYPE:
while ((value8 = nextUInt8()) != MACRO_END) {
if (execute) {
Keyboard.write((KeyboardKeycode)value8);
if (delayTime) delay(delayTime);
}
}
break;
case MACRO_MOUSE_REL_X: // read in 1 extra numberic value 8 bit
value8 = nextUInt8();
if (execute) {
Mouse.move(value8, 0, 0);
}
break;
case MACRO_MOUSE_REL_Y: // read in 1 extra numberic value 8 bit
value8 = nextUInt8();
if (execute) {
Mouse.move(0, value8, 0);
}
break;
case MACRO_MOUSE_REL_X_AXIS: { // one or 2 bytes"
int8_t axis = nextUInt8();
if (axis != -1) {
value8 = nextUInt8();
if (execute) {
// find previous mapping
bool found = false;
for (uint8_t i = 0; i < m_numMappedAxis; ++i) {
if (m_mappedAxis[i].m_number == axis) {
m_mappedAxis[i].m_type = MOUSE_REL_X_AXIS;
m_mappedAxis[i].m_value = value8;
found = true;
}
}
if (!found) {
m_mappedAxis[m_numMappedAxis].m_type = MOUSE_REL_X_AXIS;
m_mappedAxis[m_numMappedAxis].m_number = axis;
m_mappedAxis[m_numMappedAxis++].m_value = value8;
}
}
} else {
if (execute) {
// find axis
for (uint8_t i = 0; i < m_numMappedAxis; ++i) {
if (m_mappedAxis[i].m_number == axis) {
m_mappedAxis[i].m_type = MAPPING_NONE;
}
}
}
}
}
break;
case MACRO_MOUSE_REL_Y_AXIS: { // one or 2 bytes
int8_t axis = nextUInt8();
if (axis != -1) {
value8 = nextUInt8();
if (execute) {
// find previous mapping
bool found = false;
for (uint8_t i = 0; i < m_numMappedAxis; ++i) {
if (m_mappedAxis[i].m_number == axis) {
m_mappedAxis[i].m_type = MOUSE_REL_Y_AXIS;
m_mappedAxis[i].m_value = value8;
found = true;
}
}
if (!found) {
m_mappedAxis[m_numMappedAxis].m_type = MOUSE_REL_Y_AXIS;
m_mappedAxis[m_numMappedAxis].m_number = axis;
m_mappedAxis[m_numMappedAxis++].m_value = value8;
}
}
} else {
if (execute) {
// find axis
for (uint8_t i = 0; i < m_numMappedAxis; ++i) {
if (m_mappedAxis[i].m_number == axis) {
m_mappedAxis[i].m_type = MAPPING_NONE;
}
}
}
}
}
break;
}
} while (token != MACRO_NULL);
}
void CimditProfile::rotaryAction(uint8_t num, int8_t delta) {
if (m_displayState != DISPLAY_USER_STRING && m_displayState != DISPLAY_CONFIRMATION) {
showActiveProfile();
}
// find right action
// rotary has counter-clockwise and clockwise mapping
num = (num << 1) + (delta > 0);
for (uint8_t i = 0; i < m_numMappedRotary; ++i) {
if (m_mappedRotary[i].m_number == num) {
// rotary supports mouse rel axis, next/prev profile and macro
switch (m_mappedRotary[i].m_type) {
case MOUSE_REL_X_AXIS:
Mouse.move(m_mappedRotary[i].m_value * delta, 0, 0);
break;
case MOUSE_REL_Y_AXIS:
Mouse.move(0, m_mappedRotary[i].m_value * delta, 0);
break;
case MOUSE_REL_WHEEL:
Mouse.move(0, 0, m_mappedRotary[i].m_value * delta);
break;
case NEXT_PROFILE:
case PREV_PROFILE:
if (m_mappedRotary[i].m_type == NEXT_PROFILE) {
m_selectingProfile = (m_selectingProfile + 1) % m_numProfiles;
} else {
if (m_selectingProfile > 0) {
--m_selectingProfile;
} else {
m_selectingProfile = m_numProfiles - 1;
}
}
showActivate();
break;
default:
break;
}
}
}
}
void CimditProfile::showActiveProfile() {
#ifdef HAVE_DISPLAY
if (m_customImage) {
m_eepromPosition = m_customImage;
for (uint8_t y = 0; y < SCREEN_HEIGHT; ++y) {
for (uint8_t x = 0; x < SCREEN_WIDTH; x += 8) {
uint8_t img = nextUInt8();
for (uint8_t t = 0; t < 8; ++t) {
display.drawPixel(x + t, y, (img >> (7-t)) & 1);
}
}
}
} else {
printTextCentered(m_profiles[m_activeProfile]);
}
#else
Serial.println(m_profiles[m_activeProfile]);
#endif
m_displayState = DISPLAY_PROFILE;
m_stateTimeout = hal.m_millis + PROFILE_TIME;
m_stateStarted = hal.m_millis;
display.display();
}
void CimditProfile::showActivate() {
#ifdef HAVE_DISPLAY
display.clearDisplay();
display.setTextSize(2,2);
display.setCursor(16, 0);
display.write("activate");
// default font is 5x8 pixel
uint8_t w = (strlen(m_profiles[m_selectingProfile]) + 1) * 2 * 6;
uint8_t h = 16;
if (w > SCREEN_WIDTH) {
display.setTextSize(1,1);
w = w >> 1;
h = 8;
}
display.setCursor((SCREEN_WIDTH - w) >> 1, (SCREEN_HEIGHT >> 1) + ((SCREEN_HEIGHT - h)>> 1));
display.write(m_profiles[m_selectingProfile]);
display.write("?");
display.display();
#else
Serial.print(F("activate "));
Serial.print(m_profiles[m_selectingProfile]);
Serial.println(F("?"));
#endif
m_displayState = DISPLAY_CONFIRMATION;
m_stateTimeout = hal.m_millis + SELECTION_TIME;
m_stateStarted = hal.m_millis;
}
void CimditProfile::printTextCentered(const char* text) {
display.clearDisplay();
display.setTextSize(2,2);
// default font is 6x8 pixel
uint8_t w = strlen(text) * 12;
uint8_t h = 16;
if (w > SCREEN_WIDTH) {
display.setTextSize(1,1);
w = w >> 1;
h = 8;
}
display.setCursor((SCREEN_WIDTH - w) >> 1, (SCREEN_HEIGHT - h) >> 1);
display.write(text);
display.display();
}
void CimditProfile::showUserString() {
#ifdef HAVE_DISPLAY
printTextCentered(m_userString);
#endif
m_displayState = DISPLAY_USER_STRING;
m_stateTimeout = hal.m_millis + m_userTimeout;
m_stateStarted = hal.m_millis;
}
void CimditProfile::axisAction(uint8_t num, uint16_t state) {
for (uint8_t i = 0; i < m_numMappedAxis; ++i) {
if (m_mappedAxis[i].m_number == num) {
switch (m_mappedAxis[i].m_type) {
case JOYSTICK_AXIS:
switch (m_mappedAxis[i].m_value) {
case 1:
Gamepad.xAxis(map(state, 0, 1023, -32768, 32767));
break;
case 2:
Gamepad.yAxis(map(state, 0, 1023, -32768, 32767));
break;
case 3:
Gamepad.zAxis(map(state, 0, 1023, -128, 127));
break;
case 4:
Gamepad.rxAxis(map(state, 0, 1023, -32768, 32767));
break;
case 5:
Gamepad.ryAxis(map(state, 0, 1023, -32768, 32767));
break;
case 6:
Gamepad.rzAxis(map(state, 0, 1023, -128, 127));
break;
}
break;
case MOUSE_REL_X_AXIS: {
int16_t pos = (int16_t)state - 512;
if (pos > JOYSTICK_DEAD_ZONE_X) {
m_mouseMoveX = map(pos, JOYSTICK_DEAD_ZONE_X, 511, 0, m_mappedAxis[i].m_value);
} else if (pos < -JOYSTICK_DEAD_ZONE_X) {
m_mouseMoveX = map(pos, -512, -JOYSTICK_DEAD_ZONE_X, -m_mappedAxis[i].m_value, 0);
} else {
m_mouseMoveX = 0;
}
}
break;
case MOUSE_REL_Y_AXIS: {
int16_t pos = (int16_t)state - 512;
if (pos > JOYSTICK_DEAD_ZONE_Y) {
m_mouseMoveY = map(pos, JOYSTICK_DEAD_ZONE_Y, 511, 0, m_mappedAxis[i].m_value);
} else if (pos < -JOYSTICK_DEAD_ZONE_Y) {
m_mouseMoveY = map(pos, -512, -JOYSTICK_DEAD_ZONE_Y, -m_mappedAxis[i].m_value, 0);
} else {
m_mouseMoveY = 0;
}
}
break;
default:
break;
}
}
}
}
void CimditProfile::buttonAction(uint8_t num, bool state) {
if (m_displayState != DISPLAY_USER_STRING && m_displayState != DISPLAY_CONFIRMATION) {
showActiveProfile();
}
for (uint8_t i = 0; i < m_numMappedButtons; ++i) {
if (m_mappedButtons[i].m_number == num) {
switch (m_mappedButtons[i].m_type) {
case JOYSTICK_BUTTON:
if (state) {
Gamepad.press(m_mappedButtons[i].m_value);
} else {
Gamepad.release(m_mappedButtons[i].m_value);
}
Gamepad.write();
break;
case MOUSE_BUTTON:
if (state) {
Mouse.press(m_mappedButtons[i].m_value);
} else {
Mouse.release(m_mappedButtons[i].m_value);
}
break;
case MOUSE_REL_X_AXIS:
Mouse.move(m_mappedButtons[i].m_value, 0, 0);
break;
case MOUSE_REL_Y_AXIS:
Mouse.move(0, m_mappedButtons[i].m_value, 0);
break;
case MOUSE_REL_WHEEL:
Mouse.move(0, 0, m_mappedButtons[i].m_value);
break;
case NEXT_PROFILE:
case PREV_PROFILE:
if (m_mappedRotary[i].m_type == NEXT_PROFILE) {
m_selectingProfile = (m_selectingProfile + 1) % m_numProfiles;
} else {
if (m_selectingProfile > 0) {
--m_selectingProfile;
} else {
m_selectingProfile = m_numProfiles - 1;
}
}
showActivate();
break;
case SWITCH_PROFILE:
if (m_stateTimeout && state && m_displayState == DISPLAY_CONFIRMATION) {
load(m_selectingProfile);
}
break;
case MACRO_PRESS:
if (state)
scanOrExecuteMacro(m_mappedButtons[i].m_value, true);
break;
case MACRO_RELEASE:
if (!state)
scanOrExecuteMacro(m_mappedButtons[i].m_value, true);
break;
default:
break;
}
}
}
}
void CimditProfile::tick() {
if (m_stateTimeout && hal.m_millis >= m_stateTimeout) {
switch (m_displayState) {
case DISPLAY_BLANK: // ignored
break;
case DISPLAY_PROFILE: // go to blank display state
display.clearDisplay();
display.display();
m_displayState = DISPLAY_BLANK;
m_stateTimeout = 0;
m_stateStarted = hal.m_millis;
break;
case DISPLAY_CONFIRMATION:
case DISPLAY_USER_STRING:
// go back to profile name
showActiveProfile();
m_selectingProfile = m_activeProfile;
break;
}
}
if (hal.m_millis > m_nextBarUpdate) {
m_nextBarUpdate = hal.m_millis + BAR_UPDATE_TIME;
if (m_stateTimeout) {
uint8_t percentage = SCREEN_WIDTH * (hal.m_millis - m_stateStarted) / (m_stateTimeout - m_stateStarted);
// white bar from left to percentage
display.drawFastHLine(0, SCREEN_HEIGHT-1, percentage, true);
// black bar from percentage to right
display.drawFastHLine(percentage + 1, SCREEN_HEIGHT-1, SCREEN_WIDTH - percentage - 1, false);
display.displayPartial(3);
}
if (m_displayState == DISPLAY_BLANK) {
// clear old pixel
display.drawPixel(m_dotX, m_dotY, false);
uint8_t part1 = m_dotY/8;
uint16_t r = rand();
m_dotX = r & 127;
m_dotY = r & 31;
display.drawPixel(m_dotX, m_dotY, true);
uint8_t part2 = m_dotY/8;
if (part1 != part2) {
display.displayPartial(part2);
}
display.displayPartial(part1);
}
}
if (m_mouseMoveX || m_mouseMoveY) {
m_relMouseX += (float)m_mouseMoveX / 50;
m_relMouseY += (float)m_mouseMoveY / 50;
if (fabs(m_relMouseX) >= 1 || fabs(m_relMouseY) >= 1) {
Mouse.move(m_relMouseX, m_relMouseY);
m_relMouseX -= (int8_t)m_relMouseX;
m_relMouseY -= (int8_t)m_relMouseY;
}
}
}
void CimditProfile::printFlash() {
const uint8_t eepromPosition = m_eepromPosition;
m_eepromPosition = 0;
uint16_t len = nextUInt16();
m_eepromPosition = 0;
if (len < 1024) {
uint8_t in;
for (uint16_t i = 0; i < len; ++i) {
in = nextUInt8();
Serial.print(in >> 8, 16);
Serial.print(in & 255, 16);
}
Serial.println();
}
m_eepromPosition = eepromPosition;
}
#ifdef EXTERNAL_EEPROM
void CimditProfile::eepromWrite(uint16_t addr, uint8_t val) {
Wire.beginTransmission(EXTERNAL_EEPROM);
Wire.write((uint8_t)(addr >> 8)); // MSB
Wire.write((uint8_t)(addr & 0xFF)); // LSB
Wire.write(val);
Wire.endTransmission();
delay(5);
}
#endif
void CimditProfile::writeFlash() {
char buf[5]= {0};
Serial.readBytes(buf, 4);
uint16_t len = strtol(buf, NULL, 16);
buf[2] = 0;
m_eepromPosition = 0;
#ifdef EXTERNAL_EEPROM
eepromWrite(m_eepromPosition++, len >> 8);
eepromWrite(m_eepromPosition++, len & 255);
#else
EEPROM.write(m_eepromPosition++, len >> 8);
EEPROM.write(m_eepromPosition++, len & 255);
#endif
if (len < 1024) {
for (uint16_t i = 2; i < len; ++i) {
while ((buf[0] = Serial.read()) == -1) {
delay(100);
}
while ((buf[1] = Serial.read()) == -1) {
delay(100);
}
uint8_t in = strtol(buf, NULL, 16);
#ifdef EXTERNAL_EEPROM
eepromWrite(m_eepromPosition++, in);
#else
EEPROM.write(m_eepromPosition++, in);
#endif
}
Serial.println("written");
initProfiles();
}
}
void CimditProfile::setUserString(uint8_t timeout, const char* input) {
strncpy(m_userString, input, sizeof(m_userString) - 1);
m_userTimeout = timeout * 1000;
showUserString();
}
void CimditProfile::userDisplay() {
uint8_t timeout = 0;
char buf = 0;
// read number for time to display
do {
buf = Serial.read();
if (buf != '\n') {
timeout = timeout * 10 + buf - '0';
}
} while (buf != '\n');
Serial.println(F("ready for .pbm file"));
// read header
do {
buf = Serial.read();
} while (buf != '\n');
while (!Serial.available()) { delay(10); };
buf = Serial.read();
if (buf == '#') { // a comment
do {
buf = Serial.read();
} while (buf != '\n');
}
while (!Serial.available()) { delay(10); };
// width height
uint8_t width = 0;
do {
buf = Serial.read();
if (buf != ' ') {
width = width * 10 + buf - '0';
}
} while (buf != ' ');
uint8_t height = 0;
do {
buf = Serial.read();
if (buf != '\n') {
height = height * 10 + buf - '0';
}
} while (buf != '\n');
buf = 0;
uint16_t len = width * height;
if (width != SCREEN_WIDTH || SCREEN_HEIGHT != 32) {
for (uint8_t i = 0; i < width * height; ++i) {
buf = Serial.read();
if (buf == '\n') buf = Serial.read();
}
return;
}
for (uint8_t y = 0; y < SCREEN_HEIGHT; ++y) {
for (uint8_t x = 0; x < SCREEN_WIDTH; ++x) {
while ((buf = Serial.read()) == -1) {
delay(100);
}
while (buf != '0' && buf != '1') {
buf = Serial.read();
}
display.drawPixel(x, y, buf == '1');
}
}
display.display();
m_displayState = DISPLAY_USER_STRING;
uint32_t now = millis();
m_stateTimeout = now + (uint32_t)timeout * 1000;
m_stateStarted = now;
}

289
cimditprofile.h Normal file
View File

@ -0,0 +1,289 @@
/**
* \file cimditprofile.h
* \brief Declaration of the CimditProfile class
* \author GrumpyDeveloper (Sascha Nitsch)
* \copyright 2022 Sascha Nitsch
* Licensed under MIT license
*
*/
#ifndef CIMDITPROFILE_H_
#define CIMDITPROFILE_H_
// our defines
#include "defines.h"
// system includes
#include <inttypes.h>
// library include
#include <HID-Project.h>
// own include
#include "cimdithal.h"
/**
* \class CimditProfile
* \brief Profile storage and action class
*/
class CimditProfile {
public:
/// constructor
CimditProfile();
/// \brief initialize
void begin();
/// a button has been pressed or release
/// \param num which button number
/// \param state current button state (true if pressed)
void buttonAction(uint8_t num, bool state);
/// an axis was moved
/// \param num which axis number
/// \param state current axis state (absolute value)
void axisAction(uint8_t num, uint16_t state);
/// an axis was moved
/// \param num which axis number
/// \param delta rotary delta value
void rotaryAction(uint8_t num, int8_t delta);
/// periodic tick function, for timeouts, turning off display a.s.o.
void tick();
/// print flash(profile) content to serial
void printFlash();
/// write flash(profile) content from serial
void writeFlash();
/// set user string
/// \param timeout timout for string
/// \param input string to print out
void setUserString(uint8_t timeout, const char* input);
/// display user submitted graphic
void userDisplay();
/// mapping types
enum MappingType {
/// no mapping
MAPPING_NONE = 0,
/// mapped as joystick button (only works for buttons)
JOYSTICK_BUTTON,
/// mapped as joystick axis (only works for analog values)
JOYSTICK_AXIS,
/// mapped as mouse button (only works for buttons)
MOUSE_BUTTON,
/// mapped as mouse relative/incremental X axis (works for analog values, rotary encoder and buttons)
MOUSE_REL_X_AXIS,
/// mapped as mouse relative/incremental Y axis (works for analog values, rotary encoder and buttons)
MOUSE_REL_Y_AXIS,
/// mapped as mouse relative/incremental wheel (works for rotary encoder and buttons)
MOUSE_REL_WHEEL,
/// select to next profile
NEXT_PROFILE,
/// select to previous profile
PREV_PROFILE,
/// confirm selection
SWITCH_PROFILE,
/// macro key pressed
MACRO_PRESS,
/// macro key released
MACRO_RELEASE
};
/// enums for use in macros
enum MACROCOMMANDS {
/// null terminator
MACRO_NULL = 0,
/// end marker
MACRO_END = 0,
// time functions
/// speed for typing (chars per second)
MACRO_SPEED, // 1
/// wait a while (in ms)
MACRO_DELAY, // 2
// press/release functions
/// key press
MACRO_KEY_PRESS, // 3
/// key release
MACRO_KEY_RELEASE, // 4
/// joystick button press
MACRO_JOY_PRESS, // 5
/// joystick button release
MACRO_JOY_RELEASE, // 6
/// mouse press
MACRO_MOUSE_PRESS, // 7
/// mouse release
MACRO_MOUSE_RELEASE, // 8
/// mouse wheel rotation
MACRO_MOUSE_WHEEL, // 9
// typing functions
/// type a series of keys (press, delay, release)<repeated>
MACRO_TYPE, // 10
// axis mapping functions
/// move mouse X according to given value
MACRO_MOUSE_REL_X, // 11
/// move mouse Y according to given value
MACRO_MOUSE_REL_Y, // 12
/// move mouse X according to an analog axis
MACRO_MOUSE_REL_X_AXIS, // 13
/// move mouse Y according to an analog axis
MACRO_MOUSE_REL_Y_AXIS, // 14
};
private:
/// get unsigned int 8 from EEPROM and advance one byte
/// \return eeprom value
uint8_t nextUInt8();
/// get unsigned int 16 from EEPROM and advance two byte
/// \return eeprom value
uint16_t nextUInt16();
/// get 0-terminated string from EEPROM
/// \param destination destination pointer
void nextString(unsigned char* destination);
/// initialize profile structures
void initProfiles();
/// load profile
/// \param profileNum number of profile to load
void load(uint8_t);
/// scan macros and optionally execute fiven Macro
/// \param macroNum number of macro
/// \param execute should macro be executed (true) or just scanned (false)
void scanOrExecuteMacro(uint8_t macroNum, bool execute);
#ifdef EXTERNAL_EEPROM
/**
* \brief write to eeprom
* \param addr addr to write to
* \param val value to write
*/
void eepromWrite(uint16_t addr, uint8_t val);
#endif
/**
* \brief show activation confirmation
*/
void showActivate();
/**
* show currently active profile
*/
void showActiveProfile();
/**
* show user string
*/
void showUserString();
/**
* \brief print out text centered on display
* \param text
*/
void printTextCentered(const char* text);
/// list of profile names
char m_profiles[NUMBER_OF_PROFILES][16];
/// number of profiles
uint8_t m_numProfiles;
/// active profile
uint8_t m_activeProfile;
/// selecting profile
uint8_t m_selectingProfile;
/// Mapping structure
struct Map {
/// number of (button/axis/rotary)
uint8_t m_number;
/// mapping type
MappingType m_type;
/// optional value
int8_t m_value;
};
/// number of mapped buttons
uint8_t m_numMappedButtons;
/// actual button mappings
Map m_mappedButtons[64];
/// number of mapped axis
uint8_t m_numMappedAxis;
/// actual axis mappings
Map m_mappedAxis[16];
/// number of mapped rotary
uint8_t m_numMappedRotary;
/// actual rotary mappings
Map m_mappedRotary[8];
/// automatic mouse movement in X direction
int16_t m_mouseMoveX;
/// automatic mouse movement in Y direction
int16_t m_mouseMoveY;
/// current EEPROM position
uint16_t m_eepromPosition;
/// number of macros
uint8_t m_numMacros;
/// macro start position
uint16_t *m_macroStart;
/// different display states
enum DISPLAY_STATE {
/// almost blank display to protect oled
DISPLAY_BLANK = 0,
/// show current profile
DISPLAY_PROFILE,
/// show confirmation screen
DISPLAY_CONFIRMATION,
/// show user submitted (via USB) string
DISPLAY_USER_STRING
};
/// active display state
DISPLAY_STATE m_displayState;
/// time when state started
uint32_t m_stateStarted;
/// time when state should be timed out
uint32_t m_stateTimeout;
/// time for next bar update
uint32_t m_nextBarUpdate;
/// screen saver last dot X position
uint8_t m_dotX;
/// screen saver last dot Y position
uint8_t m_dotY;
/// user submitted string to output
char m_userString[43];
/// timeout of submitted string
uint32_t m_userTimeout;
float m_relMouseX, m_relMouseY;
uint16_t m_customImage;
};
#endif // CIMDITPROFILE_H_

View File

@ -6,7 +6,7 @@
* Licensed under MIT license
*
*/
#include "Arduino.h"
// own includes
#include "cimditrotary.h"
@ -15,9 +15,6 @@ CimditRotary::CimditRotary() {
m_delta = 0;
}
void CimditRotary::begin() {
}
bool CimditRotary::update(bool a, bool b) {
if (a == m_a) return false; // no change
if (a && !m_a) { // rising bit a

View File

@ -6,6 +6,7 @@
* Licensed under MIT license
*
*/
#ifndef CIMDITROTARY_H_
#define CIMDITROTARY_H_
@ -21,9 +22,6 @@ class CimditRotary {
/// constructor
CimditRotary();
/// initialize
void begin();
/// update internal state with new inputs
/// \param a first pin
/// \param b second pin

282
cimditssd1306.cpp Normal file
View File

@ -0,0 +1,282 @@
/**
* \file cimditssd1306.h
* \brief Declaration of the CimditSSD1306 class
* \author GrumpyDeveloper (Sascha Nitsch)
* \copyright 2022 Sascha Nitsch
* Licensed under MIT license
*
*/
// system includes
#include <avr/pgmspace.h>
#include <util/delay.h>
// our includes
#include "cimditssd1306.h"
#include "glcdfont.c"
CimditSSD1306::CimditSSD1306(uint8_t w, uint8_t h, uint8_t i2cAddr) {
m_width = w;
m_height = h;
m_buffer = nullptr;
m_cursorX = m_cursorY = 0;
m_textsizeX = m_textsizeY = 1;
m_state = true;
m_i2cAddr = i2cAddr;
}
CimditSSD1306::~CimditSSD1306() {
if (m_buffer) {
free(m_buffer);
m_buffer = nullptr;
}
}
void CimditSSD1306::commandStart() {
Wire.beginTransmission(m_i2cAddr);
Wire.write((uint8_t)0x00);
}
void CimditSSD1306::commandEnd() {
Wire.endTransmission();
}
void CimditSSD1306::commandListProgMem(const uint8_t *c, uint8_t n) {
uint16_t bytesOut = 1;
while (n--) {
if (bytesOut >= BUFFER_LENGTH) {
commandEnd();
commandStart();
bytesOut = 1;
}
Wire.write(pgm_read_byte(c++));
bytesOut++;
}
}
/// static intialization part 1
static const uint8_t PROGMEM init1[] = {
SSD1306_SET_DISPLAY_OFF,
SSD1306_SET_DISPLAY_CLOCK_DIVIDE_RATIO, 0x80,
SSD1306_SET_MULTIPLEX_RATIO
};
/// static initialization part 2
static const uint8_t PROGMEM init2[] = {
SSD1306_SET_DISPLAY_OFFSET, 0x0,
SSD1306_SET_DISPLAY_START_LINE_0,
SSD1306_CHARGE_PUMP_SETTING, 0x14,
SSD1306_SET_MEMORY_ADDRESSING_MODE, 0x00,
SSD1306_SET_SEGMENT_RE_MAP_127,
SSD1306_SET_COM_OUTPUT_SCAN_DIRECTION_REMAPPED,
SSD1306_SET_COM_PINS_HARDWARE_CONFIGURATION, 0x02,
SSD1306_SET_CONTRAST_CONTROL, 0x8F,
SSD1306_SET_PRE_CHARGE_PERIOD, 0xF1,
SSD1306_SET_VCOM_DESELECT_LEVEL, 0x20,
SSD1306_ENTIRE_DISPLAY_ON,
SSD1306_SET_NORMAL_DISPLAY,
SSD1306_DEACTIVATE_SCROLL,
SSD1306_SET_DISPLAY_ON
};
bool CimditSSD1306::begin() {
if ((!m_buffer) && !(m_buffer = reinterpret_cast<uint8_t *>(malloc(m_width * (m_height / 8)))))
return false;
clearDisplay();
// Init sequence
commandStart();
commandListProgMem(init1, sizeof(init1));
Wire.write(m_height - 1);
commandListProgMem(init2, sizeof(init2));
commandEnd();
return true; // Success
}
void CimditSSD1306::drawPixel(uint8_t x, uint8_t y, bool state) {
if (state) {
m_buffer[x + (y >> 3) * m_width] |= (1 << (y & 7));
} else {
m_buffer[x + (y >> 3) * m_width] &= ~(1 << (y & 7));
}
}
void CimditSSD1306::clearDisplay(void) {
memset(m_buffer, 0, m_width * ((m_height + 7) / 8));
}
void CimditSSD1306::drawFastHLine(uint8_t x, uint8_t y, uint8_t w, bool state) {
uint8_t *pBuf = m_buffer + (y / 8) * m_width + x;
uint8_t mask = 1 << (y & 7);
if (state) {
while (w--) {
*pBuf++ |= mask;
}
} else {
mask = ~mask;
while (w--) {
*pBuf++ &= mask;
}
}
}
void CimditSSD1306::drawFastVLine(uint8_t x, uint8_t y, uint8_t h, bool state) {
uint8_t *pBuf = m_buffer + (y / 8) * m_width + x;
// do the first partial byte, if necessary - this requires some masking
uint8_t mod = (y & 7);
if (mod) {
// mask off the high n bits we want to set
mod = 8 - mod;
// note - lookup table results in a nearly 10% performance
// improvement in fill* functions
// uint8_t mask = ~(0xFF >> mod);
static const uint8_t PROGMEM premask[8] = {0x00, 0x80, 0xC0, 0xE0,
0xF0, 0xF8, 0xFC, 0xFE};
uint8_t mask = pgm_read_byte(&premask[mod]);
// adjust the mask if we're not going to reach the end of this byte
if (h < mod) {
mask &= (0XFF >> (mod - h));
}
if (state) {
*pBuf |= mask;
} else {
*pBuf &= ~mask;
}
pBuf += m_width;
}
if (h >= mod) { // More to go?
h -= mod;
// Write solid bytes while we can - effectively 8 rows at a time
if (h >= 8) {
// store a local value to work with
uint8_t val = state ? 255 : 0;
do {
*pBuf = val; // Set byte
pBuf += m_width; // Advance pointer 8 rows
h -= 8; // Subtract 8 rows from height
} while (h >= 8);
}
if (h) { // Do the final partial byte, if necessary
mod = h & 7;
// this time we want to mask the low bits of the byte,
// vs the high bits we did above
// uint8_t mask = (1 << mod) - 1;
// note - lookup table results in a nearly 10% performance
// improvement in fill* functions
static const uint8_t PROGMEM postmask[8] = {0x00, 0x01, 0x03, 0x07,
0x0F, 0x1F, 0x3F, 0x7F};
uint8_t mask = pgm_read_byte(&postmask[mod]);
if (state) {
*pBuf |= mask;
} else {
*pBuf &= ~mask;
}
}
}
}
/// statric start command for transmitting screen content
static const uint8_t PROGMEM dlist1[] = {
SSD1306_SET_PAGE_ADDRESS, 0, 0x3,
SSD1306_SET_COLUMN_ADDRESS, 0
};
void CimditSSD1306::display(void) {
commandStart();
commandListProgMem(dlist1, sizeof(dlist1));
Wire.write(m_width - 1); // Column end address
commandEnd();
uint16_t count = m_width * (m_height >> 3);
uint8_t *ptr = m_buffer;
Wire.beginTransmission(m_i2cAddr);
Wire.write((uint8_t)0x40);
uint16_t bytesOut = 1;
while (count--) {
if (bytesOut >= BUFFER_LENGTH) {
Wire.endTransmission();
Wire.beginTransmission(m_i2cAddr);
Wire.write((uint8_t)0x40);
bytesOut = 1;
}
Wire.write(*ptr++);
bytesOut++;
}
Wire.endTransmission();
}
void CimditSSD1306::displayPartial(uint8_t part) {
commandStart();
Wire.write(SSD1306_SET_PAGE_ADDRESS);
Wire.write(part); // Page start address
Wire.write(part); // Page end address
Wire.write(SSD1306_SET_COLUMN_ADDRESS);
Wire.write(0);
Wire.write(m_width - 1); // Column end address
Wire.endTransmission();
uint16_t count = m_width * (m_height >> 5);
uint8_t *ptr = m_buffer + m_width * part;
Wire.beginTransmission(m_i2cAddr);
Wire.write((uint8_t)0x40);
uint16_t bytesOut = 1;
while (count--) {
if (bytesOut >= BUFFER_LENGTH) {
Wire.endTransmission();
Wire.beginTransmission(m_i2cAddr);
Wire.write((uint8_t)0x40);
bytesOut = 1;
}
Wire.write(*ptr++);
bytesOut++;
}
Wire.endTransmission();
}
void CimditSSD1306::setTextSize(uint8_t sX, uint8_t sY) {
m_textsizeX = sX;
m_textsizeY = sY;
}
size_t CimditSSD1306::write(uint8_t c) {
if (c == '\n' || m_cursorX + m_textsizeX * 6) { // a newline or text oveflow
m_cursorX = 0; // set X to zero
m_cursorY += m_textsizeY * 8; // go down one line
} else if (c == '\r') { // a carriage return
m_cursorX = 0; // set X to zero
} else { // something to print
drawChar(m_cursorX, m_cursorY, c, m_textsizeX, m_textsizeY);
m_cursorX += m_textsizeX * 6; // Advance x one char
}
return 1;
}
void CimditSSD1306::drawChar(uint8_t x, uint8_t y, unsigned char c, uint8_t sizeX, uint8_t sizeY) {
for (int8_t i = 0; i < 5; i++) { // one char is 5 pixel wide
uint8_t line = pgm_read_byte(&font[c * 5 + i]); // get pixel from flash/program memory
for (int8_t j = 0; j < 8; j++, line >>= 1) { // for each 8 lines
if (line & 1) {
if (sizeX == 1 && sizeY == 1) { // one pixel size, draw pixel direct
drawPixel(x + i, y + j, m_state);
} else {
fillRect(x + i * sizeX, y + j * sizeY, sizeX, sizeY, m_state); // draw rectangles with given size multiplicator
}
}
}
}
}
void CimditSSD1306::fillRect(uint8_t x, uint8_t y, uint8_t w, uint8_t h, bool state) {
if (y + h >= m_height) h = m_height - y;
if (x + w >= m_width) w = m_width - x;
for (int16_t i = x; i < x + w; i++) {
drawFastVLine(i, y, h, state);
}
}
void CimditSSD1306::setTextState(bool state) {
m_state = state;
}
void CimditSSD1306::setCursor(uint8_t x, uint8_t y) {
m_cursorX = x;
m_cursorY = y;
}

225
cimditssd1306.h Normal file
View File

@ -0,0 +1,225 @@
/**
* \file cimditssd1306.h
* \brief Declaration of the CimditSSD1306 class
* \author GrumpyDeveloper (Sascha Nitsch)
* \copyright 2022 Sascha Nitsch
* Licensed under MIT license
*
*/
#ifndef CIMDITSSD1306_H_
#define CIMDITSSD1306_H_
// system includes
#include <Wire.h>
/// memory adressing mode
#define SSD1306_SET_MEMORY_ADDRESSING_MODE 0x20
/// column address start and end
#define SSD1306_SET_COLUMN_ADDRESS 0x21
/// page start and end address
#define SSD1306_SET_PAGE_ADDRESS 0x22
/// deactivate scrolling
#define SSD1306_DEACTIVATE_SCROLL 0x2E
/// set display start line to 0
#define SSD1306_SET_DISPLAY_START_LINE_0 0x40
/// set contrast
#define SSD1306_SET_CONTRAST_CONTROL 0x81
/// charge pump setting
#define SSD1306_CHARGE_PUMP_SETTING 0x8D
/// set segment re-map to column 127
#define SSD1306_SET_SEGMENT_RE_MAP_127 0xA1
/// entire display on
#define SSD1306_ENTIRE_DISPLAY_ON 0xA4
/// set normal display
#define SSD1306_SET_NORMAL_DISPLAY 0xA6
/// set multiplex ratio
#define SSD1306_SET_MULTIPLEX_RATIO 0xA8
/// set display off
#define SSD1306_SET_DISPLAY_OFF 0xAE
/// set display on
#define SSD1306_SET_DISPLAY_ON 0xAF
/// set COM output scan direction to remapped
#define SSD1306_SET_COM_OUTPUT_SCAN_DIRECTION_REMAPPED 0xC8
/// set display offser
#define SSD1306_SET_DISPLAY_OFFSET 0xD3
/// set display clock divide ratio
#define SSD1306_SET_DISPLAY_CLOCK_DIVIDE_RATIO 0xD5
// set pre-charge period
#define SSD1306_SET_PRE_CHARGE_PERIOD 0xD9
// set com pins hardware configuration
#define SSD1306_SET_COM_PINS_HARDWARE_CONFIGURATION 0xDA
// set VCOM deselect level
#define SSD1306_SET_VCOM_DESELECT_LEVEL 0xDB
/**
* \brief Simple drawing class for SSD1306 OLED displays
*/
class CimditSSD1306 : public Print {
public:
/**
* \brief constructor
* \param w width of display in pixel
* \param h height of display in pixel
* \param i2cAddr i2c address
*/
CimditSSD1306(uint8_t w, uint8_t h, uint8_t i2cAddr);
/**
* \brief destructor
*
*/
~CimditSSD1306();
/**
* \brief initialize library, needs to be called before any other graphic functions
* \return true on success
*/
bool begin();
/**
* \brief send internal buffer to display
*
*/
void display(void);
/**
* \brief send part of internal buffer to display
* \param part which part of screen (each part is 8 pixel high, starting from top at 0
*/
void displayPartial(uint8_t part);
/**
* \brief clear display content in internal buffer, call display or displayPartial to take effect
*
*/
void clearDisplay(void);
/**
* \brief set a pixel to on or off
* \param x x coordinate of pixel
* \param y y coordinate of pixel
* \param state true for on, false for off
*/
void drawPixel(uint8_t x, uint8_t y, bool state);
/**
* \brief draw a horizontal line
* \param x start x position
* \param y start y position
* \param w width in pixel
* \param state true for on, false for off
*/
void drawFastHLine(uint8_t x, uint8_t y, uint8_t w, bool state);
/**
* \brief Draw a vertical line with a width and state.
* Functionality based on adafruit GFX library
* \param x x coordinate
* \param y top y coordinate
* \param h height of the line in pixels
* \param state true for pixel on, false for off
* \note Changes buffer contents only, no immediate effect on display.
Follow up with a call to display() or displayPartial()
*/
void drawFastVLine(uint8_t x, uint8_t y, uint8_t h, bool state);
/**
* \brief draw a filled rectangle
*
* \param x x start position
* \param y y start position
* \param w width in pixel
* \param h height in pixel
* \param state
*/
void fillRect(uint8_t x, uint8_t y, uint8_t w, uint8_t h, bool state);
/**
* \brief set text scaling factor
*
* \param sx X-scaling factor
* \param sy Y-scaling factor
*/
void setTextSize(uint8_t sx, uint8_t sy);
using Print::write;
/**
* \brief print a single character to screen
*
* \param c character to print
* \return 1
*/
size_t write(uint8_t c);
/**
* \brief set text cursor position
* \param x X coordinate
* \param y Y coordinate
*/
void setCursor(uint8_t x, uint8_t y);
/**
* \brief set text state
* \param state state to output
*/
void setTextState(bool state);
private:
/**
* \brief draw a single character c at x,y with scaling sX, sY
* \param x left coordinate of character
* \param y lower coordinate of character
* \param c character to print
* \param sX horizontal scaling factor
* \param sY veritcal scaling factor
*/
void drawChar(uint8_t x, uint8_t y, unsigned char c, uint8_t size_x, uint8_t size_y);
/**
* \brief start command transmission
*
*/
void commandStart();
/**
* \brief send command list from program memory
* \param c start pointer
* \param n number of items to output
*/
void commandListProgMem(const uint8_t *c, uint8_t n);
/**
* \brief finish command sending
*/
void commandEnd();
/// display width
uint8_t m_width;
/// display height
uint8_t m_height;
/// text cursor position X
uint8_t m_cursorX;
/// text cursor position Y
uint8_t m_cursorY;
/// text drawing state
bool m_state;
/// horizontal text scaling factor
uint8_t m_textsizeX;
/// vertical text scaling factor
uint8_t m_textsizeY;
/// internal buffer for screen pixel data
uint8_t *m_buffer;
/// i2c address of display
uint8_t m_i2cAddr;
};
#endif // CIMDITSSD1306_H_

67
defines.h Normal file
View File

@ -0,0 +1,67 @@
/// we have a display connected
#define HAVE_DISPLAY
/// use a custom keyboard layout
#define HID_CUSTOM_LAYOUT
/// use german keyboard layout
#define LAYOUT_GERMAN
#ifdef HAVE_DISPLAY
/// no splash screen on OLED
#define SSD1306_NO_SPLASH
/// OLED display width, in pixels
#define SCREEN_WIDTH 128
/// OLED display height, in pixels
#define SCREEN_HEIGHT 32
/// OLED screen address
#define SCREEN_ADDRESS 0x3C
#endif
/// how long does the profile selection wait for a confirmation
#define SELECTION_TIME 10000
/// how long to show the profile name/icon until going into screen saver
#define PROFILE_TIME 60000
/// update timeout bar every second
#define BAR_UPDATE_TIME 1000
/// update usb devices every x ms if needed
#define UPDATE_INTERVAL 10
/// dead zone (noise) around the center of the joystick X axis
#define JOYSTICK_DEAD_ZONE_X 20
/// dead zone (noise) around the center of the joystick Y axcs
#define JOYSTICK_DEAD_ZONE_Y 20
/// i2c address of the external EEPROM
#define EXTERNAL_EEPROM 0x50
/// pin for analog mux bit 0
#define ANALOG_MUX0 6
/// pin for analog mux bit 1
#define ANALOG_MUX1 8
/// pin for analog mux bit 2
#define ANALOG_MUX2 9
/// pin for analog mux bit 3
#define ANALOG_MUX3 10
/// analog input pin
#define ANALOG_IN A1
/// rotary encoder interrupt pin
#define ROT_INT 7
/// scan analog every 12 milliseconds
#define ANALOG_INTERVAL 12
/// scan keys every 4 milliseconds
#define KEYSCAN_INTERVAL 4
/// number of profiles
#define NUMBER_OF_PROFILES 8

143
glcdfont.c Normal file
View File

@ -0,0 +1,143 @@
// This is the 'classic' fixed-space bitmap font for Adafruit_GFX since 1.0.
// See gfxfont.h for newer custom bitmap font info.
#ifndef FONT5X7_H
#define FONT5X7_H
#ifdef __AVR__
#include <avr/io.h>
#include <avr/pgmspace.h>
#elif defined(ESP8266)
#include <pgmspace.h>
#elif defined(__IMXRT1052__) || defined(__IMXRT1062__)
// PROGMEM is defefind for T4 to place data in specific memory section
#undef PROGMEM
#define PROGMEM
#else
#define PROGMEM
#endif
// Standard ASCII 5x7 font
static const unsigned char font[] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x5B, 0x4F, 0x5B, 0x3E, 0x3E, 0x6B,
0x4F, 0x6B, 0x3E, 0x1C, 0x3E, 0x7C, 0x3E, 0x1C, 0x18, 0x3C, 0x7E, 0x3C,
0x18, 0x1C, 0x57, 0x7D, 0x57, 0x1C, 0x1C, 0x5E, 0x7F, 0x5E, 0x1C, 0x00,
0x18, 0x3C, 0x18, 0x00, 0xFF, 0xE7, 0xC3, 0xE7, 0xFF, 0x00, 0x18, 0x24,
0x18, 0x00, 0xFF, 0xE7, 0xDB, 0xE7, 0xFF, 0x30, 0x48, 0x3A, 0x06, 0x0E,
0x26, 0x29, 0x79, 0x29, 0x26, 0x40, 0x7F, 0x05, 0x05, 0x07, 0x40, 0x7F,
0x05, 0x25, 0x3F, 0x5A, 0x3C, 0xE7, 0x3C, 0x5A, 0x7F, 0x3E, 0x1C, 0x1C,
0x08, 0x08, 0x1C, 0x1C, 0x3E, 0x7F, 0x14, 0x22, 0x7F, 0x22, 0x14, 0x5F,
0x5F, 0x00, 0x5F, 0x5F, 0x06, 0x09, 0x7F, 0x01, 0x7F, 0x00, 0x66, 0x89,
0x95, 0x6A, 0x60, 0x60, 0x60, 0x60, 0x60, 0x94, 0xA2, 0xFF, 0xA2, 0x94,
0x08, 0x04, 0x7E, 0x04, 0x08, 0x10, 0x20, 0x7E, 0x20, 0x10, 0x08, 0x08,
0x2A, 0x1C, 0x08, 0x08, 0x1C, 0x2A, 0x08, 0x08, 0x1E, 0x10, 0x10, 0x10,
0x10, 0x0C, 0x1E, 0x0C, 0x1E, 0x0C, 0x30, 0x38, 0x3E, 0x38, 0x30, 0x06,
0x0E, 0x3E, 0x0E, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5F,
0x00, 0x00, 0x00, 0x07, 0x00, 0x07, 0x00, 0x14, 0x7F, 0x14, 0x7F, 0x14,
0x24, 0x2A, 0x7F, 0x2A, 0x12, 0x23, 0x13, 0x08, 0x64, 0x62, 0x36, 0x49,
0x56, 0x20, 0x50, 0x00, 0x08, 0x07, 0x03, 0x00, 0x00, 0x1C, 0x22, 0x41,
0x00, 0x00, 0x41, 0x22, 0x1C, 0x00, 0x2A, 0x1C, 0x7F, 0x1C, 0x2A, 0x08,
0x08, 0x3E, 0x08, 0x08, 0x00, 0x80, 0x70, 0x30, 0x00, 0x08, 0x08, 0x08,
0x08, 0x08, 0x00, 0x00, 0x60, 0x60, 0x00, 0x20, 0x10, 0x08, 0x04, 0x02,
0x3E, 0x51, 0x49, 0x45, 0x3E, 0x00, 0x42, 0x7F, 0x40, 0x00, 0x72, 0x49,
0x49, 0x49, 0x46, 0x21, 0x41, 0x49, 0x4D, 0x33, 0x18, 0x14, 0x12, 0x7F,
0x10, 0x27, 0x45, 0x45, 0x45, 0x39, 0x3C, 0x4A, 0x49, 0x49, 0x31, 0x41,
0x21, 0x11, 0x09, 0x07, 0x36, 0x49, 0x49, 0x49, 0x36, 0x46, 0x49, 0x49,
0x29, 0x1E, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x40, 0x34, 0x00, 0x00,
0x00, 0x08, 0x14, 0x22, 0x41, 0x14, 0x14, 0x14, 0x14, 0x14, 0x00, 0x41,
0x22, 0x14, 0x08, 0x02, 0x01, 0x59, 0x09, 0x06, 0x3E, 0x41, 0x5D, 0x59,
0x4E, 0x7C, 0x12, 0x11, 0x12, 0x7C, 0x7F, 0x49, 0x49, 0x49, 0x36, 0x3E,
0x41, 0x41, 0x41, 0x22, 0x7F, 0x41, 0x41, 0x41, 0x3E, 0x7F, 0x49, 0x49,
0x49, 0x41, 0x7F, 0x09, 0x09, 0x09, 0x01, 0x3E, 0x41, 0x41, 0x51, 0x73,
0x7F, 0x08, 0x08, 0x08, 0x7F, 0x00, 0x41, 0x7F, 0x41, 0x00, 0x20, 0x40,
0x41, 0x3F, 0x01, 0x7F, 0x08, 0x14, 0x22, 0x41, 0x7F, 0x40, 0x40, 0x40,
0x40, 0x7F, 0x02, 0x1C, 0x02, 0x7F, 0x7F, 0x04, 0x08, 0x10, 0x7F, 0x3E,
0x41, 0x41, 0x41, 0x3E, 0x7F, 0x09, 0x09, 0x09, 0x06, 0x3E, 0x41, 0x51,
0x21, 0x5E, 0x7F, 0x09, 0x19, 0x29, 0x46, 0x26, 0x49, 0x49, 0x49, 0x32,
0x03, 0x01, 0x7F, 0x01, 0x03, 0x3F, 0x40, 0x40, 0x40, 0x3F, 0x1F, 0x20,
0x40, 0x20, 0x1F, 0x3F, 0x40, 0x38, 0x40, 0x3F, 0x63, 0x14, 0x08, 0x14,
0x63, 0x03, 0x04, 0x78, 0x04, 0x03, 0x61, 0x59, 0x49, 0x4D, 0x43, 0x00,
0x7F, 0x41, 0x41, 0x41, 0x02, 0x04, 0x08, 0x10, 0x20, 0x00, 0x41, 0x41,
0x41, 0x7F, 0x04, 0x02, 0x01, 0x02, 0x04, 0x40, 0x40, 0x40, 0x40, 0x40,
0x00, 0x03, 0x07, 0x08, 0x00, 0x20, 0x54, 0x54, 0x78, 0x40, 0x7F, 0x28,
0x44, 0x44, 0x38, 0x38, 0x44, 0x44, 0x44, 0x28, 0x38, 0x44, 0x44, 0x28,
0x7F, 0x38, 0x54, 0x54, 0x54, 0x18, 0x00, 0x08, 0x7E, 0x09, 0x02, 0x18,
0xA4, 0xA4, 0x9C, 0x78, 0x7F, 0x08, 0x04, 0x04, 0x78, 0x00, 0x44, 0x7D,
0x40, 0x00, 0x20, 0x40, 0x40, 0x3D, 0x00, 0x7F, 0x10, 0x28, 0x44, 0x00,
0x00, 0x41, 0x7F, 0x40, 0x00, 0x7C, 0x04, 0x78, 0x04, 0x78, 0x7C, 0x08,
0x04, 0x04, 0x78, 0x38, 0x44, 0x44, 0x44, 0x38, 0xFC, 0x18, 0x24, 0x24,
0x18, 0x18, 0x24, 0x24, 0x18, 0xFC, 0x7C, 0x08, 0x04, 0x04, 0x08, 0x48,
0x54, 0x54, 0x54, 0x24, 0x04, 0x04, 0x3F, 0x44, 0x24, 0x3C, 0x40, 0x40,
0x20, 0x7C, 0x1C, 0x20, 0x40, 0x20, 0x1C, 0x3C, 0x40, 0x30, 0x40, 0x3C,
0x44, 0x28, 0x10, 0x28, 0x44, 0x4C, 0x90, 0x90, 0x90, 0x7C, 0x44, 0x64,
0x54, 0x4C, 0x44, 0x00, 0x08, 0x36, 0x41, 0x00, 0x00, 0x00, 0x77, 0x00,
0x00, 0x00, 0x41, 0x36, 0x08, 0x00, 0x02, 0x01, 0x02, 0x04, 0x02, 0x3C,
0x26, 0x23, 0x26, 0x3C, 0x1E, 0xA1, 0xA1, 0x61, 0x12, 0x3A, 0x40, 0x40,
0x20, 0x7A, 0x38, 0x54, 0x54, 0x55, 0x59, 0x21, 0x55, 0x55, 0x79, 0x41,
0x22, 0x54, 0x54, 0x78, 0x42, // a-umlaut
0x21, 0x55, 0x54, 0x78, 0x40, 0x20, 0x54, 0x55, 0x79, 0x40, 0x0C, 0x1E,
0x52, 0x72, 0x12, 0x39, 0x55, 0x55, 0x55, 0x59, 0x39, 0x54, 0x54, 0x54,
0x59, 0x39, 0x55, 0x54, 0x54, 0x58, 0x00, 0x00, 0x45, 0x7C, 0x41, 0x00,
0x02, 0x45, 0x7D, 0x42, 0x00, 0x01, 0x45, 0x7C, 0x40, 0x7D, 0x12, 0x11,
0x12, 0x7D, // A-umlaut
0xF0, 0x28, 0x25, 0x28, 0xF0, 0x7C, 0x54, 0x55, 0x45, 0x00, 0x20, 0x54,
0x54, 0x7C, 0x54, 0x7C, 0x0A, 0x09, 0x7F, 0x49, 0x32, 0x49, 0x49, 0x49,
0x32, 0x3A, 0x44, 0x44, 0x44, 0x3A, // o-umlaut
0x32, 0x4A, 0x48, 0x48, 0x30, 0x3A, 0x41, 0x41, 0x21, 0x7A, 0x3A, 0x42,
0x40, 0x20, 0x78, 0x00, 0x9D, 0xA0, 0xA0, 0x7D, 0x3D, 0x42, 0x42, 0x42,
0x3D, // O-umlaut
0x3D, 0x40, 0x40, 0x40, 0x3D, 0x3C, 0x24, 0xFF, 0x24, 0x24, 0x48, 0x7E,
0x49, 0x43, 0x66, 0x2B, 0x2F, 0xFC, 0x2F, 0x2B, 0xFF, 0x09, 0x29, 0xF6,
0x20, 0xC0, 0x88, 0x7E, 0x09, 0x03, 0x20, 0x54, 0x54, 0x79, 0x41, 0x00,
0x00, 0x44, 0x7D, 0x41, 0x30, 0x48, 0x48, 0x4A, 0x32, 0x38, 0x40, 0x40,
0x22, 0x7A, 0x00, 0x7A, 0x0A, 0x0A, 0x72, 0x7D, 0x0D, 0x19, 0x31, 0x7D,
0x26, 0x29, 0x29, 0x2F, 0x28, 0x26, 0x29, 0x29, 0x29, 0x26, 0x30, 0x48,
0x4D, 0x40, 0x20, 0x38, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
0x38, 0x2F, 0x10, 0xC8, 0xAC, 0xBA, 0x2F, 0x10, 0x28, 0x34, 0xFA, 0x00,
0x00, 0x7B, 0x00, 0x00, 0x08, 0x14, 0x2A, 0x14, 0x22, 0x22, 0x14, 0x2A,
0x14, 0x08, 0x55, 0x00, 0x55, 0x00, 0x55, // #176 (25% block) missing in old
// code
0xAA, 0x55, 0xAA, 0x55, 0xAA, // 50% block
0xFF, 0x55, 0xFF, 0x55, 0xFF, // 75% block
0x00, 0x00, 0x00, 0xFF, 0x00, 0x10, 0x10, 0x10, 0xFF, 0x00, 0x14, 0x14,
0x14, 0xFF, 0x00, 0x10, 0x10, 0xFF, 0x00, 0xFF, 0x10, 0x10, 0xF0, 0x10,
0xF0, 0x14, 0x14, 0x14, 0xFC, 0x00, 0x14, 0x14, 0xF7, 0x00, 0xFF, 0x00,
0x00, 0xFF, 0x00, 0xFF, 0x14, 0x14, 0xF4, 0x04, 0xFC, 0x14, 0x14, 0x17,
0x10, 0x1F, 0x10, 0x10, 0x1F, 0x10, 0x1F, 0x14, 0x14, 0x14, 0x1F, 0x00,
0x10, 0x10, 0x10, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x10, 0x10, 0x10,
0x10, 0x1F, 0x10, 0x10, 0x10, 0x10, 0xF0, 0x10, 0x00, 0x00, 0x00, 0xFF,
0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0xFF, 0x10, 0x00,
0x00, 0x00, 0xFF, 0x14, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x1F,
0x10, 0x17, 0x00, 0x00, 0xFC, 0x04, 0xF4, 0x14, 0x14, 0x17, 0x10, 0x17,
0x14, 0x14, 0xF4, 0x04, 0xF4, 0x00, 0x00, 0xFF, 0x00, 0xF7, 0x14, 0x14,
0x14, 0x14, 0x14, 0x14, 0x14, 0xF7, 0x00, 0xF7, 0x14, 0x14, 0x14, 0x17,
0x14, 0x10, 0x10, 0x1F, 0x10, 0x1F, 0x14, 0x14, 0x14, 0xF4, 0x14, 0x10,
0x10, 0xF0, 0x10, 0xF0, 0x00, 0x00, 0x1F, 0x10, 0x1F, 0x00, 0x00, 0x00,
0x1F, 0x14, 0x00, 0x00, 0x00, 0xFC, 0x14, 0x00, 0x00, 0xF0, 0x10, 0xF0,
0x10, 0x10, 0xFF, 0x10, 0xFF, 0x14, 0x14, 0x14, 0xFF, 0x14, 0x10, 0x10,
0x10, 0x1F, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x10, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFF, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x38, 0x44, 0x44,
0x38, 0x44, 0xFC, 0x4A, 0x4A, 0x4A, 0x34, // sharp-s or beta
0x7E, 0x02, 0x02, 0x06, 0x06, 0x02, 0x7E, 0x02, 0x7E, 0x02, 0x63, 0x55,
0x49, 0x41, 0x63, 0x38, 0x44, 0x44, 0x3C, 0x04, 0x40, 0x7E, 0x20, 0x1E,
0x20, 0x06, 0x02, 0x7E, 0x02, 0x02, 0x99, 0xA5, 0xE7, 0xA5, 0x99, 0x1C,
0x2A, 0x49, 0x2A, 0x1C, 0x4C, 0x72, 0x01, 0x72, 0x4C, 0x30, 0x4A, 0x4D,
0x4D, 0x30, 0x30, 0x48, 0x78, 0x48, 0x30, 0xBC, 0x62, 0x5A, 0x46, 0x3D,
0x3E, 0x49, 0x49, 0x49, 0x00, 0x7E, 0x01, 0x01, 0x01, 0x7E, 0x2A, 0x2A,
0x2A, 0x2A, 0x2A, 0x44, 0x44, 0x5F, 0x44, 0x44, 0x40, 0x51, 0x4A, 0x44,
0x40, 0x40, 0x44, 0x4A, 0x51, 0x40, 0x00, 0x00, 0xFF, 0x01, 0x03, 0xE0,
0x80, 0xFF, 0x00, 0x00, 0x08, 0x08, 0x6B, 0x6B, 0x08, 0x36, 0x12, 0x36,
0x24, 0x36, 0x06, 0x0F, 0x09, 0x0F, 0x06, 0x00, 0x00, 0x18, 0x18, 0x00,
0x00, 0x00, 0x10, 0x10, 0x00, 0x30, 0x40, 0xFF, 0x01, 0x01, 0x00, 0x1F,
0x01, 0x01, 0x1E, 0x00, 0x19, 0x1D, 0x17, 0x12, 0x00, 0x3C, 0x3C, 0x3C,
0x3C, 0x00, 0x00, 0x00, 0x00, 0x00 // #255 NBSP
};
// allow clean compilation with [-Wunused-const-variable=] and [-Wall]
static inline void avoid_unused_const_variable_compiler_warning(void) {
(void)font;
}
#endif // FONT5X7_H