diff --git a/.gitignore b/.gitignore index 2f4c1e7..0babea1 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ /electronics/cimdit.kicad_prl /electronics/fp-info-cache /spec.d +/html diff --git a/cimdit.ino b/cimdit.ino index 118cf41..88509f4 100644 --- a/cimdit.ino +++ b/cimdit.ino @@ -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 #ifdef HAVE_DISPLAY -#include +#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; } } } diff --git a/cimdithal.cpp b/cimdithal.cpp index c394d56..ce94d00 100644 --- a/cimdithal.cpp +++ b/cimdithal.cpp @@ -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 @@ -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; diff --git a/cimditprofile.cpp b/cimditprofile.cpp new file mode 100644 index 0000000..d5a8b3d --- /dev/null +++ b/cimditprofile.cpp @@ -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 +#include +#include +#ifndef EXTERNAL_EEPROM +#include +#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 + uint8_t mapping type + int8_t value depending on mapping + uint8_t axis mapping number + repeated for each mapped axis + uint8_t axis number + 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) + 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(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; +} diff --git a/cimditprofile.h b/cimditprofile.h new file mode 100644 index 0000000..a5d093c --- /dev/null +++ b/cimditprofile.h @@ -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 + +// library include +#include + +// 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) + 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_ diff --git a/cimditrotary.cpp b/cimditrotary.cpp index b5eea30..5d9897e 100644 --- a/cimditrotary.cpp +++ b/cimditrotary.cpp @@ -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 diff --git a/cimditrotary.h b/cimditrotary.h index 4ba2986..cf2b7d3 100644 --- a/cimditrotary.h +++ b/cimditrotary.h @@ -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 diff --git a/cimditssd1306.cpp b/cimditssd1306.cpp new file mode 100644 index 0000000..97a936f --- /dev/null +++ b/cimditssd1306.cpp @@ -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 +#include + +// 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(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; +} diff --git a/cimditssd1306.h b/cimditssd1306.h new file mode 100644 index 0000000..e4c75b7 --- /dev/null +++ b/cimditssd1306.h @@ -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 + +/// 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_ diff --git a/defines.h b/defines.h new file mode 100644 index 0000000..06bd3b2 --- /dev/null +++ b/defines.h @@ -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 diff --git a/glcdfont.c b/glcdfont.c new file mode 100644 index 0000000..535da3a --- /dev/null +++ b/glcdfont.c @@ -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 +#include +#elif defined(ESP8266) +#include +#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