Files
SSUP/Client/Client.ino

780 lines
21 KiB
C++

/*
* SSUP (Super Spiker Ultra Plus)
* Copyright (C) 2024-2026 Gabriel Weingardt
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License,
* or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* For further information, see <https://weingardt.dev/ssup/>
* There you'll find documentation and how to update the device.
*/
#include <Wire.h>
#include <EEPROM.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include "assets.h"
/* Screen dimension defines */
#define IMAGEX 128
#define IMAGEY 32
#define TEXTX 21
#define TEXTY 4
Adafruit_SSD1306 display(IMAGEX, IMAGEY, &Wire, -1);
const char PROGMEM HELP[] = \
"--- SSUP Help---\n\
Please see https://weingardt.dev/ssup for more info\n\
There you'll also find a manual and instructions\n\n\
g = Print System Info\n\
r = Reload Standart Configuration\n\
s = Enter Data Transfer Mode\n\
x = Exit Data Transfer Mode\n\
f = TOC Clear\n\
c = Clear EEPROM\n\
p = Print Page Content\n\
t = Print TOC Content\n\
h = Help\n\
u = Unlock Device\n\
l = Lock Device";
/* Serial BAUD rate */
#define BAUD 9600
/* EEPROM addresses for settings */
#define E_PAGE 0
#define E_CONTRAST 1
#define E_LOCK 2
#define E_EEPROM_SIZE 3
#define E_TOC_SIZE 4
#define E_SCREEN_ADDR 5
#define E_EEPROM_ADDR 6
#define E_THRESHHOLD 7
/* Calculator button pins */
#define ROW 3
#define D1 A3
#define D2 A2
#define D3 A0
#define D4 A1
#define LEFT 0b1000
#define SUPER 0b0100
#define TOGGLE 0b0010
#define RIGHT 0b0001
/* Serial flow control values and device info */
#define WAIT 0xFA
#define RELEASE 0xFB
#define MODEL 1
#define FIRMWARE 4
/* Setting variables */
uint8_t S_EEPROM_Address;
uint8_t S_SCREEN_Address;
uint16_t S_EEPROM_Size;
uint16_t S_TOC_SIZE;
uint8_t S_THRESHHOLD;
/* Variables */
uint8_t currentIndex = 0;
uint8_t currentPress = 4;
uint8_t fetchTrack = 0;
bool contrast = true;
bool locked = false;
bool dataTransferMode = false;
bool displayOn = true;
bool nextPage = false;
void setup() {
/* Begin serial and I2C wire for EEPROM */
Serial.begin(BAUD);
Wire.begin();
Wire.setClock(400000);
/* Init settings */
loadSettings();
/* Say hello and lock status */
Serial.println("#SSUP\nType 'h' for help");
if (locked) Serial.println("!SSUP LOCK\n#This device is locked! Send 'u' to unlock it");
/* Init calculator scan pins */
pinMode(D1, INPUT_PULLUP);
pinMode(D2, INPUT_PULLUP);
pinMode(D3, INPUT_PULLUP);
pinMode(D4, INPUT_PULLUP);
pinMode(ROW, OUTPUT);
/* Init display
* Note: If less than ~768 bytes of dynamic memory is avaiable,
* the display will fail to init. The screen library needs 1k
* of dynamic memory during runtime!
*/
if (!display.begin(SSD1306_SWITCHCAPVCC, S_SCREEN_Address)) {
Serial.print("!DISPLAYBAD: ");
Serial.println(display.getWriteError());
Serial.println("#See manual");
}
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.cp437(true);
/* Print welcome screen */
display.clearDisplay();
display.setCursor(38, 12);
display.write("@");
display.print(BAUD);
display.write(" Baud");
scanButtons();
if ((currentPress & RIGHT) != 0) {
//display.fillRect(102, 12, 16, 16, SSD1306_BLACK);
display.drawBitmap(0, 0, LOGO, 128, 32, 1);
//display.drawBitmap(102, 12, WHAT, 16, 16, 1);
display.display();
delay(4000);
if (currentPress == (RIGHT | LEFT))
for (;;);
} else if ((currentPress & LEFT) != 0) {
/* Restore settings on left press */
display.setCursor(0, 0);
display.print("Loading Default Settings");
resetSettings();
}
display.display();
Wire.begin();
delay(6000);
/* Display first page */
currentIndex = 0;
displayPage();
}
void loop() {
scanButtons();
static float datCount = 0;
static uint8_t recieved = 0;
/* Look while data is in seral buffer */
while (Serial.available()) {
static bool indexMode = false;
static bool contentMode = false;
static bool image = false;
static uint8_t newIndex;
static uint16_t startAddress;
static uint16_t endAddress;
/* Get recieved data */
recieved = (uint8_t)Serial.read();
datCount += (recieved / 100.f);
if (contentMode) {
/* If in content mode (data upload mode) */
/* Stop on character ']' */
if (recieved == ']') {
Serial.println("!DONE");
contentMode = false;
} else {
/* Else, interpret the send data */
static int count = 0;
count++;
/* Send '.' every 8 counts for the master to flow control data */
if (count > 8) {
count = 0;
Serial.print(".");
}
if (image) {
/* For the image, decode 2 byte ASCII HEX into a byte */
static uint8_t num = 0;
/* Stop digging byte on ';' */
if (recieved == ';') {
writeEEPROM(startAddress++, num);
num = 0;
} else if (isalnum(recieved)) {
/* Fetch ASCII hex digit (0..F) into a nibble and construct the byte */
recieved = tolower(recieved);
uint8_t fetch = ((recieved - ((recieved >= '0' && recieved <= '9') ? '0' : 'a')) & 0xF);
/* First, shift previous fetch and then OR the new nibble */
num = num << 4;
num |= fetch;
}
/* Else, just write the ASCII data */
} else if (recieved >= 0x20) writeEEPROM(startAddress++, recieved);
/* On overflow, request temporary pause from master */
if (startAddress > endAddress) Serial.println("!");
}
} else if (indexMode) {
/* Index mode fetches the allocation mode for the next entry */
if (recieved == ';') {
/* If ';' was recieved, alloc entry and reset index mode, now comes the data */
indexMode = false;
contentMode = true;
newIndex = allocPage(image);
startAddress = (getPageAddress(newIndex) & 0x7FFF);
endAddress = (startAddress + getPageSize(newIndex));
} else image = (recieved != 't'); // Determine image or text
} else {
/* Interpret the recieved serial characters */
switch (recieved) {
case 'g':
/* Print system info to serial */
printSystemInfo();
break;
case 'r':
/* Load default values and reset */
Serial.println("!RESETTING");
resetSettings();
setup();
break;
case 's':
/* Set data transfer mode */
dataTransferMode = true;
break;
case 'x':
/* Exit data transfer mode */
dataTransferMode = false;
break;
case 'f':
/* Clear fast, only TOC */
clearTOC();
break;
case 'c':
/* Clear entire EEPROM */
clearEEPROM();
break;
case 'p':
/* Print all page content to serial */
for (int i = 0; i < 64; i++) {
uint16_t addr = getPageAddress(i);
if (addr != 0) printPageData(i);
}
break;
case 't':
/* Print TOC entries (addresses and size) */
for (int i = 0; i < 64; i++) {
printTOC(i);
}
break;
case 'h':
/* Print help dialog */
Serial.println(HELP);
break;
case 'u':
/* Unlock device */
EEPROM.write(E_LOCK, 0);
Serial.println("!UNLOCKED");
setup();
case 'l':
/* Lock device */
EEPROM.write(E_LOCK, 1);
Serial.println("!LOCKED");
break;
case '[':
/* Start index mode (new page entry) */
indexMode = true;
break;
default:
Serial.println("?");
break;
}
}
}
if (dataTransferMode) {
/* If in data transfer mode, show data transfer screen animation */
static float rotCount = 0;
display.clearDisplay();
display.setCursor(0, 0);
display.print("Transfer mode\nHold D2+D3 to \nforcefully abort");
/* Nice little animation */
display.drawLine(108, 16, 108 + (cosf(rotCount) * 10), 16 + (sinf(rotCount) * 10), SSD1306_WHITE);
display.drawLine(108, 16, 108 + (sinf(datCount + rotCount) * 10), 16 + (cosf(datCount + rotCount) * 10), SSD1306_WHITE);
display.display();
//delay(3);
rotCount += 0.125;
scanButtons();
if (currentPress == (SUPER | TOGGLE)) dataTransferMode = false;
} else {
/* If not in data transfer mode, we are in display/normal mode */
bool wait = true;
/* Test the current key press */
switch (currentPress) {
case TOGGLE:
/* Toggle, toggles the display on/off state */
displayOn = !displayOn;
display.ssd1306_command(displayOn ? SSD1306_DISPLAYON : SSD1306_DISPLAYOFF);
delay(200);
break;
case LEFT:
/* Left, decrements the page */
incrementPage(false);
break;
case RIGHT:
/* Right, increments the page */
incrementPage(true);
break;
case SUPER | TOGGLE:
/* Super+Toggle toggles the display contrast */
contrast = !contrast;
display.ssd1306_command(SSD1306_SETCONTRAST);
display.ssd1306_command(contrast ? 255 : 0);
delay(200);
break;
case SUPER | RIGHT:
/* Super+Right will lock the calculator */
bool abort = false;
for (int i = 5; i >= 0; i--) {
display.clearDisplay();
display.setCursor(0, 0);
display.print("Device will be locked\nHold D1 to abort!\nHold D4 to lock NOW\n -> Locking in: ");
display.print(i);
display.display();
delay(1000);
/* On left press abort, on right press skip counter */
scanButtons();
if (currentPress == LEFT) {
abort = true;
break;
} else if (currentPress == RIGHT) break;
}
/* Write lock and reset */
if (!abort) {
EEPROM.write(E_LOCK, 1);
setup();
}
case SUPER | LEFT:
/* Super+Left resets the index to 0 */
currentIndex = 0;
displayPage();
break;
default:
wait = false;
break;
}
if (wait && currentPress != 0) delay(100);
}
}
void writeEEPROM(uint16_t address, char writebyte) {
/* Write the given value to EEPROM from desired address */
Wire.beginTransmission(S_EEPROM_Address);
Wire.write((byte)(address >> 8));
Wire.write((byte)(address & 0xFF));
Wire.write(writebyte);
Wire.endTransmission();
delay(2); // wait to complete the write cycle
}
int readEEPROM(uint16_t address) {
/* Read one byte from EEPROM from desired address */
Wire.beginTransmission(S_EEPROM_Address);
Wire.write((byte)(address >> 8));
Wire.write((byte)(address & 0xFF));
Wire.endTransmission();
Wire.requestFrom(S_EEPROM_Address, (uint8_t)1);
return Wire.read();
}
void clearEEPROM() {
/* Clearing the entire EEPROM (fill with zeros) */
Serial.print("!EEPROM CLEAR ");
Serial.print((S_EEPROM_Size / 1024) + 1);
Serial.println("k\n# '.' = 1k");
for (uint32_t i = 0; i < S_EEPROM_Size; i++) {
writeEEPROM(i, 0);
/* For every 256 bytes, refresh display */
if ((i & 0xFF) == 0) {
display.clearDisplay();
display.setCursor(0, 0);
display.print("Clearing EEPROM ");
display.print((S_EEPROM_Size / 1024) + 1);
display.println('k');
display.print(String(i / 1024));
display.println("KB cleared");
display.print((uint8_t)(((float)i / S_EEPROM_Size) * 100));
display.print("% done ");
display.println(i < S_TOC_SIZE ? "(TOC)" : "(Data)");
display.println("Hold D4 to abort");
display.display();
/* If RIGHT was pressed, abort */
scanButtons();
if (currentPress == RIGHT) break;
/* Print status */
Serial.print(".");
}
}
Serial.println("\n!EEPROM CLEAR DONE");
displayPage();
}
void displayPage() {
/* Display page at current index */
display.clearDisplay();
display.setCursor(0, 0);
/* Get TOC address and size */
uint16_t start = (getPageAddress(currentIndex) & 0x7FFF);
uint16_t end = start + getPageSize(currentIndex);
/* If text mode, just print the characters */
if ((getPageAddress(currentIndex) & 0x8000) == 0) {
for (int16_t i = start; i < end; i++) {
display.print((char)readEEPROM(i));
}
} else {
/* If it is an image, read bytes and put them into a line buffer and print them out */
uint8_t buffer[(IMAGEX / 8)];
uint8_t y = 0;
uint8_t c = 0;
for (uint16_t i = start; i < end; i++) {
/* Fill buffer with values */
buffer[c++] = readEEPROM(i);
if (c >= (IMAGEX / 8)) {
/* Draw a 1 line bitmap from the buffer to the S_SCREEN_Address
* This draws the image line by line, without needing a big buffer
*/
display.drawBitmap(0, y++, buffer, IMAGEX - 1, 1, 1);
c = 0;
scanButtons();
if ((currentPress & (LEFT | RIGHT)) != 0) {
nextPage = true;
break;
}
}
/* For every 8 lines, print the page */
if ((y % 8) == 0) display.display();
}
}
display.display();
}
void scanButtons() {
/* Performs a scan on the calculator button matrix */
/* Set the ROW pin to OUTPUT and set it LOW */
pinMode(ROW, OUTPUT);
digitalWrite(ROW, LOW);
/* Wait a bit */
delay(3);
currentPress = 0;
/* Read inputs, if analog read exceeds the threshhold,
* OR the pressed button to currentPress
*/
if (analogRead(D1) < S_THRESHHOLD) currentPress |= LEFT;
if (analogRead(D2) < S_THRESHHOLD) currentPress |= SUPER;
if (analogRead(D3) < S_THRESHHOLD) currentPress |= TOGGLE;
if (analogRead(D4) < S_THRESHHOLD) currentPress |= RIGHT;
/* Set ROW to INPUT, this set's the pin in tri state mode */
pinMode(ROW, INPUT);
}
void incrementPage(bool increment) {
/* Increments or decrements the page */
/* Dont't modify, if index is 0 or 255 or if next TOC entry is empty */
if (increment && currentIndex == 255) return;
else if (!increment && currentIndex == 0) return;
else if (increment && (getPageAddress((currentIndex + 1)) & 0x7FFF) == 0) return;
currentIndex += (increment ? 1 : -1);
nextPage = false;
displayPage();
if (nextPage) incrementPage(increment);
}
void printPageData(uint8_t pageIndex) {
/* Print the page data at the requested index to serial */
int i = getPageAddress(pageIndex) & 0x7FFF;
int to = i + getPageSize(pageIndex);
bool graphicsMode = ((getPageAddress(pageIndex) & 0x8000) != 0);
Serial.print("!DATA ");
Serial.println(graphicsMode ? "IMAGE:" : "TEXT:");
for (; i < to; i++) {
uint8_t val = readEEPROM(i);
if (graphicsMode) {
Serial.print(numToHex(val));
Serial.print(numToHex(val >> 4));
Serial.print(';');
} else {
Serial.print((char)val);
}
}
Serial.println("\n!DONE");
}
void printTOC(int index) {
/* Print TOC index information to serial */
uint16_t addr = getPageAddress(index);
uint16_t size = getPageSize(index);
/* Don't print if address is empty */
if (addr != 0) {
Serial.print("!TOC INFO: ");
Serial.print(index);
Serial.print("\tADDR: ");
Serial.print((addr & 0x7FFF));
Serial.print("\tSIZE: ");
Serial.print(size);
Serial.print((addr & 0x8000) ? "\tIMAGE" : "\tTEXT");
Serial.println();
}
}
char numToHex(uint8_t val) {
/* Convert the lower section of a byte to a hex char */
val &= 0xF;
return (val < 0xA) ? ('0' + val) : ('A' + (val - 0xA));
}
uint8_t allocPage(bool image) {
/* Allocate new Memory Block returning the allocated Index
* Input specifies the allocation size. A Text Entry (image = false)
* will allocate 84 bytes, while a Image (image = true) will use 512 bytes
*/
/* Get free index */
uint8_t newIndex = findNewTOCIndex();
uint16_t resultAddress = 0xFFFF;
/* If the index was 0 (first TOC entry), use the address, after the TOC size
* Addresses after the TOC size is the usable data
*/
Serial.println("!TOC ALLOC");
if (newIndex == 0) resultAddress = S_TOC_SIZE;
else {
/* If not, search for a suitable address based of TOC Entries */
uint16_t lastAddress = 0;
uint16_t end = 0;
for (int i = 0; i < S_TOC_SIZE / 2; i++) {
lastAddress = end;
uint16_t addrs = getPageAddress(i);
end = (addrs & 0x7FFF) + getPageSize(i);
if (i != 0 && (addrs & 0x7FFF) == 0) {
/* If we reached end of TOC, break */
resultAddress = lastAddress;
Serial.println("!TOC FULL");
break;
}
}
}
/* Update TOC index to found address and set the image/text bit */
setPageAddress(newIndex, (resultAddress | (image ? 0x8000 : 0)));
/* Randomizing of new allocated space (optional) */
/*
uint16_t end = (getPageSize(newIndex) + resultAddress);
for (int i = resultAddress; i < end; i++) {
if (image) writeEEPROM(i, random(255));
else writeEEPROM(i, random(20, 127));
}
*/
Serial.print("!TOC INDEX ");
Serial.print(newIndex);
Serial.print(" IS ADDR ");
Serial.println(resultAddress);
return newIndex;
}
uint8_t findNewTOCIndex() {
/* Get next free TOC Index */
Serial.println("!TOC SCAN");
int foundIndex = 0;
for (; foundIndex < S_TOC_SIZE / 2; foundIndex++) {
uint16_t addrs = getPageAddress(foundIndex);
if (addrs == 0) break;
}
Serial.println("!TOC FOUND INDEX" + String(foundIndex));
setPageAddress(foundIndex, 0);
return foundIndex;
}
uint16_t getPageSize(uint8_t pageIndex) {
/* Get the TOC page size from TOC index
* Here, the most significant bit of the address byte
* is used to determine the allocated space.
* For text, it's just TEXTX * TEXTY (for 21x4 Text = 84 bytes),
* for an image it's (IMAGEX / 8) * IMAGEY, because 1 byte can hold 8
* horizontal pixels (for 128x32 Image = 512 bytes)
*/
return ((getPageAddress(pageIndex) & 0x8000) == 0 ? (TEXTX * TEXTY) : ((IMAGEX / 8) * IMAGEY));
}
uint16_t getPageAddress(uint8_t pageIndex) {
/* Get the page address from the TOC index */
uint16_t retAddrs = readEEPROM(pageIndex * 2);
retAddrs |= (readEEPROM((pageIndex * 2) + 1) << 8);
return retAddrs;
}
void setPageAddress(uint8_t pageIndex, uint16_t address) {
/* Write the given Address to the desired TOC Location */
writeEEPROM(pageIndex * 2, (byte)(address & 0xFF));
writeEEPROM((pageIndex * 2) + 1, (byte)(address >> 8));
}
void clearTOC() {
/* Clear table of contents (TOC)
* This is a quick erase, deleting just the address entries
*/
Serial.println("!TOC CLEAR");
for (int i = 0; i < S_TOC_SIZE; i++) {
writeEEPROM(i, 0);
}
Serial.println("!TOC CLEAR DONE");
}
void printSystemInfo() {
/* Print system info as JSON to serial */
Serial.print("!SYSINFO:{");
Serial.print("\"FIRMWARE\":" + String(FIRMWARE));
Serial.print(",\"MODEL\":" + String(MODEL));
Serial.print(",\"TEXTX\":" + String(TEXTX));
Serial.print(",\"TEXTY\":" + String(TEXTY));
Serial.print(",\"IMAGEX\":" + String(IMAGEX));
Serial.print(",\"IMAGEY\":" + String(IMAGEY));
Serial.println("}");
getSettings();
}
void loadSettings() {
/* Loads configuration from EEPROM */
S_EEPROM_Address = EEPROM.read(E_EEPROM_ADDR);
S_SCREEN_Address = EEPROM.read(E_SCREEN_ADDR);
S_EEPROM_Size = (((uint16_t)EEPROM.read(E_EEPROM_SIZE)) * 1024) - 1;
S_TOC_SIZE = ((uint16_t)EEPROM.read(E_TOC_SIZE)) * 64;
S_THRESHHOLD = EEPROM.read(E_THRESHHOLD);
currentIndex = EEPROM.read(E_PAGE);
contrast = (bool)EEPROM.read(E_CONTRAST);
locked = (bool)EEPROM.read(E_LOCK);
}
void getSettings() {
/* Print settings in JSON format to serial */
Serial.print("!SETTINGS:{");
Serial.print("\"EEPROM_ADDR\":" + String(EEPROM.read(E_EEPROM_ADDR)));
Serial.print(",\"SCREEN_ADDR\":" + String(EEPROM.read(E_SCREEN_ADDR)));
Serial.print(",\"EEPROM_SIZE\":" + String(EEPROM.read(E_EEPROM_SIZE)));
Serial.print(",\"TOC_SIZE\":" + String(EEPROM.read(E_TOC_SIZE)));
Serial.print(",\"THRESHHOLD\":" + String(EEPROM.read(E_THRESHHOLD)));
Serial.print(",\"PAGE\":" + String(EEPROM.read(E_PAGE)));
Serial.print(",\"CONTRAST\":" + String(EEPROM.read(E_CONTRAST)));
Serial.print(",\"LOCKED\":" + String(EEPROM.read(E_LOCK)));
Serial.println("}");
}
void resetSettings() {
/* Restore settings to default values */
EEPROM.write(E_PAGE, 0);
EEPROM.write(E_CONTRAST, 255);
EEPROM.write(E_LOCK, 0);
EEPROM.write(E_EEPROM_SIZE, 64); // x * 1024 = EEPROM SIZE
EEPROM.write(E_TOC_SIZE, 8); // x * 64 = TOCSIZE
EEPROM.write(E_EEPROM_ADDR, 0x50);
EEPROM.write(E_SCREEN_ADDR, 0x3C);
EEPROM.write(E_THRESHHOLD, 80);
}
/* Ein geschlossener Regenschirm ist ebenso elegant wie ein offener hässlich ist */