/* * 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 . */ /* * For further information, see * There you'll find documentation and how to update the device. */ #include #include #include #include #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 */