cimdit/cimditprofile.cpp

813 lines
23 KiB
C++

/**
* \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;
case MACRO_PRESS:
scanOrExecuteMacro(m_mappedRotary[i].m_value, true);
break;
default:
break;
}
}
}
}
void CimditProfile::showActiveProfile() {
if (m_displayState != DISPLAY_PROFILE) {
#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;
display.display();
}
m_stateTimeout = hal.m_millis + PROFILE_TIME;
m_stateStarted = hal.m_millis;
}
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- h);
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;
case KEYBOARD_BUTTON:
if (state) {
Keyboard.press((KeyboardKeycode)m_mappedButtons[i].m_value);
} else {
Keyboard.release((KeyboardKeycode)m_mappedButtons[i].m_value);
}
break;
case MAPPING_NONE:
case JOYSTICK_AXIS:
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;
#ifdef EXTERNAL_EEPROM
if (len < 32768) {
#else
if (len < 1024) {
#endif
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);
if (len < 32768) {
#else
EEPROM.write(m_eepromPosition++, len >> 8);
EEPROM.write(m_eepromPosition++, len & 255);
if (len < 1024) {
#endif
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();
}
m_displayState = DISPLAY_BLANK; // to enforce a redraw
showActiveProfile();
return false;
}
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;
}