/* Walkie-Textie - see http://www.technoblogy.com/show?2AON David Johnson-Davies - www.technoblogy.com - 29th July 2025 ATtiny814 @ 20MHz (external crystal; BOD disabled) CC BY 4.0 Licensed under a Creative Commons Attribution 4.0 International license: http://creativecommons.org/licenses/by/4.0/ */ #include #include #include #include // Full battery voltage in hundredths of a volt - uncomment one option int const FullBattery = 420; // LiPo battery // int const FullBattery = 320; // 2 x 1.5V Alkaline batteries eg Energizer Max // int const FullBattery = 360; // 2 x 1.5V Energizer Ultimate Lithium batteries bool Beep = true; const int BeepSave = 1; // EEPROM address for beep flag // Pins const int Matrix = PIN_PA6; const int Piezo = PIN_PA5; // Also voltage divider to read battery voltage // Matrix keypad ******************************************************* const int nButtons = 12; const int SmallestGap = 40; int AnalogVals[] = {1023, 680, 640, 590, 547, 507, 464, 411, 351, 273, 180, 133, 0, -100}; int Buttons[] = {-1, 1, 4, 7, 10, 2, 3, 5, 6, 8, 9, 0, 11}; // Returns the keypad character or -1 if no button pressed int ReadKeypad () { int val, lastval=0, count = 0; do { val = analogRead(Matrix); if (abs(val-lastval)<2) count++; else { lastval = val; count = 0; } } while (count < 3); int i = 0; val = val - SmallestGap/2; while (val < AnalogVals[i]) { i++; } return Buttons[i - 1]; } // OLED display ********************************************** int const SH1106 = 1; // Set to 0 for SSD1306 or 1 for SH1106 // SH1106/SSD1306 display commands int const address = 60; int const commands = 0x00; int const onecommand = 0x80; int const data = 0x40; int const onedata = 0xC0; // Character set - stored in program memory const uint8_t CharMap[][6] PROGMEM = { { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, { 0x00, 0x00, 0x5F, 0x00, 0x00, 0x00 }, { 0x00, 0x07, 0x00, 0x07, 0x00, 0x00 }, { 0x14, 0x7F, 0x14, 0x7F, 0x14, 0x00 }, { 0x24, 0x2A, 0x7F, 0x2A, 0x12, 0x00 }, { 0x23, 0x13, 0x08, 0x64, 0x62, 0x00 }, { 0x36, 0x49, 0x56, 0x20, 0x50, 0x00 }, { 0x00, 0x08, 0x07, 0x03, 0x00, 0x00 }, { 0x00, 0x1C, 0x22, 0x41, 0x00, 0x00 }, { 0x00, 0x41, 0x22, 0x1C, 0x00, 0x00 }, { 0x2A, 0x1C, 0x7F, 0x1C, 0x2A, 0x00 }, { 0x08, 0x08, 0x3E, 0x08, 0x08, 0x00 }, { 0x00, 0x80, 0x70, 0x30, 0x00, 0x00 }, { 0x08, 0x08, 0x08, 0x08, 0x08, 0x00 }, { 0x00, 0x00, 0x60, 0x60, 0x00, 0x00 }, { 0x20, 0x10, 0x08, 0x04, 0x02, 0x00 }, { 0x3E, 0x51, 0x49, 0x45, 0x3E, 0x00 }, { 0x00, 0x42, 0x7F, 0x40, 0x00, 0x00 }, { 0x72, 0x49, 0x49, 0x49, 0x46, 0x00 }, { 0x21, 0x41, 0x49, 0x4D, 0x33, 0x00 }, { 0x18, 0x14, 0x12, 0x7F, 0x10, 0x00 }, { 0x27, 0x45, 0x45, 0x45, 0x39, 0x00 }, { 0x3C, 0x4A, 0x49, 0x49, 0x31, 0x00 }, { 0x41, 0x21, 0x11, 0x09, 0x07, 0x00 }, { 0x36, 0x49, 0x49, 0x49, 0x36, 0x00 }, { 0x46, 0x49, 0x49, 0x29, 0x1E, 0x00 }, { 0x00, 0x36, 0x36, 0x00, 0x00, 0x00 }, { 0x00, 0x56, 0x36, 0x00, 0x00, 0x00 }, { 0x00, 0x08, 0x14, 0x22, 0x41, 0x00 }, { 0x14, 0x14, 0x14, 0x14, 0x14, 0x00 }, { 0x00, 0x41, 0x22, 0x14, 0x08, 0x00 }, { 0x02, 0x01, 0x59, 0x09, 0x06, 0x00 }, { 0x3E, 0x41, 0x5D, 0x59, 0x4E, 0x00 }, { 0x7C, 0x12, 0x11, 0x12, 0x7C, 0x00 }, { 0x7F, 0x49, 0x49, 0x49, 0x36, 0x00 }, { 0x3E, 0x41, 0x41, 0x41, 0x22, 0x00 }, { 0x7F, 0x41, 0x41, 0x41, 0x3E, 0x00 }, { 0x7F, 0x49, 0x49, 0x49, 0x41, 0x00 }, { 0x7F, 0x09, 0x09, 0x09, 0x01, 0x00 }, { 0x3E, 0x41, 0x41, 0x51, 0x73, 0x00 }, { 0x7F, 0x08, 0x08, 0x08, 0x7F, 0x00 }, { 0x00, 0x41, 0x7F, 0x41, 0x00, 0x00 }, { 0x20, 0x40, 0x41, 0x3F, 0x01, 0x00 }, { 0x7F, 0x08, 0x14, 0x22, 0x41, 0x00 }, { 0x7F, 0x40, 0x40, 0x40, 0x40, 0x00 }, { 0x7F, 0x02, 0x1C, 0x02, 0x7F, 0x00 }, { 0x7F, 0x04, 0x08, 0x10, 0x7F, 0x00 }, { 0x3E, 0x41, 0x41, 0x41, 0x3E, 0x00 }, { 0x7F, 0x09, 0x09, 0x09, 0x06, 0x00 }, { 0x3E, 0x41, 0x51, 0x21, 0x5E, 0x00 }, { 0x7F, 0x09, 0x19, 0x29, 0x46, 0x00 }, { 0x26, 0x49, 0x49, 0x49, 0x32, 0x00 }, { 0x03, 0x01, 0x7F, 0x01, 0x03, 0x00 }, { 0x3F, 0x40, 0x40, 0x40, 0x3F, 0x00 }, { 0x1F, 0x20, 0x40, 0x20, 0x1F, 0x00 }, { 0x3F, 0x40, 0x38, 0x40, 0x3F, 0x00 }, { 0x63, 0x14, 0x08, 0x14, 0x63, 0x00 }, { 0x03, 0x04, 0x78, 0x04, 0x03, 0x00 }, { 0x61, 0x59, 0x49, 0x4D, 0x43, 0x00 }, { 0x00, 0x7F, 0x41, 0x41, 0x41, 0x00 }, { 0x02, 0x04, 0x08, 0x10, 0x20, 0x00 }, { 0x00, 0x41, 0x41, 0x41, 0x7F, 0x00 }, { 0x04, 0x02, 0x01, 0x02, 0x04, 0x00 }, { 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0 }, { 0x00, 0x03, 0x07, 0x08, 0x00, 0x00 }, { 0x20, 0x54, 0x54, 0x78, 0x40, 0x00 }, { 0x7F, 0x28, 0x44, 0x44, 0x38, 0x00 }, { 0x38, 0x44, 0x44, 0x44, 0x28, 0x00 }, { 0x38, 0x44, 0x44, 0x28, 0x7F, 0x00 }, { 0x38, 0x54, 0x54, 0x54, 0x18, 0x00 }, { 0x00, 0x08, 0x7E, 0x09, 0x02, 0x00 }, { 0x18, 0xA4, 0xA4, 0x9C, 0x78, 0x00 }, { 0x7F, 0x08, 0x04, 0x04, 0x78, 0x00 }, { 0x00, 0x44, 0x7D, 0x40, 0x00, 0x00 }, { 0x20, 0x40, 0x40, 0x3D, 0x00, 0x00 }, { 0x7F, 0x10, 0x28, 0x44, 0x00, 0x00 }, { 0x00, 0x41, 0x7F, 0x40, 0x00, 0x00 }, { 0x7C, 0x04, 0x78, 0x04, 0x78, 0x00 }, { 0x7C, 0x08, 0x04, 0x04, 0x78, 0x00 }, { 0x38, 0x44, 0x44, 0x44, 0x38, 0x00 }, { 0xFC, 0x18, 0x24, 0x24, 0x18, 0x00 }, { 0x18, 0x24, 0x24, 0x18, 0xFC, 0x00 }, { 0x7C, 0x08, 0x04, 0x04, 0x08, 0x00 }, { 0x48, 0x54, 0x54, 0x54, 0x24, 0x00 }, { 0x04, 0x04, 0x3F, 0x44, 0x24, 0x00 }, { 0x3C, 0x40, 0x40, 0x20, 0x7C, 0x00 }, { 0x1C, 0x20, 0x40, 0x20, 0x1C, 0x00 }, { 0x3C, 0x40, 0x30, 0x40, 0x3C, 0x00 }, { 0x44, 0x28, 0x10, 0x28, 0x44, 0x00 }, { 0x4C, 0x90, 0x90, 0x90, 0x7C, 0x00 }, { 0x44, 0x64, 0x54, 0x4C, 0x44, 0x00 }, { 0x00, 0x08, 0x36, 0x41, 0x00, 0x00 }, { 0x00, 0x00, 0x77, 0x00, 0x00, 0x00 }, { 0x00, 0x41, 0x36, 0x08, 0x00, 0x00 }, { 0x02, 0x01, 0x02, 0x04, 0x02, 0x00 }, { 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x7F }, // 7F Black { 0x7F, 0x7F, 0x7F, 0x7F, 0x7F, 0x41 }, // 80 Left cap { 0x41, 0x41, 0x41, 0x41, 0x41, 0x41 }, // 81 Empty { 0x5D, 0x5D, 0x41, 0x41, 0x41, 0x41 }, // 82 1/3 full { 0x5D, 0x5D, 0x5D, 0x5D, 0x41, 0x41 }, // 83 2/3 full { 0x5D, 0x5D, 0x5D, 0x5D, 0x5D, 0x5D }, // 84 Full { 0x41, 0x63, 0x63, 0x7F, 0x7F, 0x7F }, // 85 Right cap { 0x41, 0x41, 0x7F, 0x7F, 0x7F, 0x7F }, // 86 Signal { 0x4F, 0x41, 0x7D, 0x7D, 0x65, 0x61 }, // 87 Beep }; const int Black = 0x7F; const int Signal = 0x86; const int BeepIcon = 0x87; const int LeftCap = 0x80; const int RtCap = 0x85; // Write a single command void Single (uint8_t x) { Wire.write(onecommand); Wire.write(x); } void InitDisplay () { Wire.beginTransmission(address); Wire.write(commands); Wire.write(0xA0); // Don't flip horizontal Wire.write(0xC0); // Don't flip vertical Wire.write(0xD3); // Reset scroll Wire.write(0); Wire.endTransmission(); } void DisplayOn () { Wire.beginTransmission(address); Wire.write(commands); Wire.write(0xAF); // Display on Wire.endTransmission(); } void ClearDisplay () { for (int p = 0 ; p < 8; p++) { Wire.beginTransmission(address); Single(0xB0 + p); Wire.endTransmission(); for (int q = 0 ; q < 8; q++) { Wire.beginTransmission(address); Wire.write(data); for (int i = 0 ; i < 20; i++) Wire.write(0); Wire.endTransmission(); } } } // Character terminal ********************************************** uint8_t Scroll = 0, Line = 0, Column = 0; void ClearLine (uint8_t line) { Wire.beginTransmission(address); Wire.write(commands); Wire.write(0xB0 + ((Scroll + line) & 0x07)); Wire.write(0x00); // Column start low Wire.write(0x00); // Column start high Wire.endTransmission(); for (uint8_t a = 0 ; a < 6; a++) { // Do 6 lots of 22 = 132 Wire.beginTransmission(address); Wire.write(data); for (uint8_t b = 0 ; b < 22; b++) Wire.write(0);; Wire.endTransmission(); } } // Clears the top line, then scrolls the display up by one line void ScrollDisplay () { ClearLine(0); Scroll = (Scroll + 1) & 0x07; Wire.beginTransmission(address); Wire.write(commands); Wire.write(0xD3); Wire.write(Scroll << 3); Wire.endTransmission(); } // Plots a character; line = 0 to 7; column = 0 to 20 void PlotChar (uint8_t c, int line, int column) { column = column*6+2*SH1106; Wire.beginTransmission(address); Wire.write(commands); Wire.write(0xB0 + ((line + Scroll) & 0x07)); Wire.write(0x00 + (column & 0x0F)); // Column start low Wire.write(0x10 + (column >> 4)); // Column start high Wire.endTransmission(); Wire.beginTransmission(address); Wire.write(data); for (uint8_t col = 0 ; col < 6; col++) { Wire.write(pgm_read_byte(&CharMap[c-32][col])); } Wire.endTransmission(); } void DrawDivider () { for (uint8_t c=0; c < 21; c++) PlotChar('-', 6, c); } // Control codes const int ShowCursor = 2; const int HideCursor = 3; const int Clear = 12; const int Delete = 0x7F; // Prints a character to display, with cursor, handling control characters void Display (uint8_t c) { if (c == 8) { // Backspace if (Column != 0) Column--; } else if (c == ShowCursor) { // Show Cursor PlotChar('_', Line+Scroll, Column); } else if (c == HideCursor) { // Hide Cursor PlotChar(' ', Line+Scroll, Column); } else if (c == Delete) { // Delete if (Column > 0) { PlotChar(' ', Line+Scroll, Column); // Hide cursor if there Column--; } } else if ((c & 0x7f) >= 32) { // Normal character if (Column < 21) PlotChar(c, Line+Scroll, Column++); } else if (c == Clear) { // Clear display for (uint8_t p=0; p < 8; p++) ClearLine(p); DrawDivider(); MoveTo(7, 0); } else if (c == '\n') { // Newline Column = 0; } } void PrintString (char *string) { while (*string) { Display(*string++); } } // Multitap ********************************************** const char *Multitap[12] = { " 0", ".1!\"#$%&'()*+,-/:;<=>?@[\\]^_`{|}~", "ABC2abc", "DEF3def", "GHI4ghi", "JKL5jkl", "MNO6mno", "PQRS7pqrs", "TUV8tuv", "WXYZ9wxyz", "*", "#" }; const int MessageMax = 20; char Message[MessageMax+1]; char Received[MessageMax+1]; volatile bool HaveReceived = false; // Display ********************************************** void DrawStatus () { // Signal strength goes from -120 (weak) to -30 (strong) int strength = (LoRa.packetRssi() + 120) / 10; PlotChar(Black, 0, 0); for (int i=1; i= FullBattery - i*30) PlotChar(0x84, 0, 19-i); else if (battery >= FullBattery - i*30 - 10) PlotChar(0x83, 0, 19-i); else if (battery >= FullBattery - i*30 - 20) PlotChar(0x82, 0, 19-i); else PlotChar(0x81, 0, 19-i); } PlotChar(RtCap, 0, 20); } void UpdateDisplayReceived () { // Update Display with received packet digitalWrite(LED_BUILTIN, HIGH); if (Beep) tone(Piezo, 2400); ClearLine(0); // Status ClearLine(6); // Divider ClearLine(7); // Entry area ScrollDisplay(); delay(100); noTone(Piezo); pinMode(Piezo, INPUT); // So can read battery voltage DrawDivider(); DrawStatus(); MoveTo(5,0); PrintString((char*)Received); // Display received message digitalWrite(LED_BUILTIN, LOW); MoveTo(7,0); PrintString((char*)Message); // Restore entry area Display(ShowCursor); } void UpdateDisplaySent () { // Update Display with sent packet ClearLine(6); // Divider ClearLine(7); // Entry area ScrollDisplay(); DrawDivider(); DrawStatus(); MoveTo(5, 21-strlen(Message)); PrintString((char*)Message); } // Main interface ********************************************** int const SyncWord = 0x78; void Receiver (int packetSize) { int i; if (packetSize > MessageMax) packetSize = MessageMax; for (i=0; i 1000 && !timeout) { Display(ShowCursor); // MultiTap timed out - display cursor timeout = true; } if (HaveReceived) { Message[m] = 0; UpdateDisplayReceived(); HaveReceived = 0; } } while (key == -1); // Wait for key up while (ReadKeypad() != -1); // Multitap if (key == 11) { if (Column != 0) { Display(Delete); m--; } Display(ShowCursor); key0 = -1; } else if (key == 10) { // Send message Message[m++] = 0; // Terminate message LoRa.beginPacket(); LoRa.print(Message); LoRa.endPacket(); // Send message LoRa.receive(); // Back into receive mode UpdateDisplaySent(); // Display it MoveTo(7,0); m = 0; key0 = -1; } else if (key == key0 && !timeout) { Display(8); // Backspace i++; m--; c = pgm_read_byte(&Multitap[key][i]); if (c == 0) { i = 0; c = pgm_read_byte(&Multitap[key][i]); } Message[m++] = c; Display(c); } else if (Column < 20) { i = 0; c = pgm_read_byte(&Multitap[key][i]); Message[m++] = c; Display(c); key0 = key; } } } void MoveTo (uint8_t line, uint8_t column) { Line = (line + 8 - Scroll) & 0x07; Column = column; } // Beep ********************************************** void ToggleBeep () { Beep = !Beep; EEPROM.write(BeepSave, Beep); } // Setup ********************************************** void setup (void) { Wire.begin(); delay(500); InitDisplay(); delay(1000); ClearDisplay(); DisplayOn(); Beep = EEPROM.read(BeepSave); // # at startup to toggle beep if (digitalRead(Matrix) == LOW) ToggleBeep(); LoRa.setPins(PIN_PA4, PIN_PB3, PIN_PB2); if (!LoRa.begin(868E6)) { PrintString((char*)"Starting LoRa failed!"); while (1); } LoRa.setSyncWord(SyncWord); pinMode(LED_BUILTIN, OUTPUT); digitalWrite(LED_BUILTIN, HIGH); delay(500); digitalWrite(LED_BUILTIN, LOW); DrawDivider(); DrawStatus(); MoveTo(7, 0); LoRa.onReceive(Receiver); // Register callback LoRa.receive(); // Start listening } void loop (void) { Interface(); }