diff --git a/.github/workflows/spell-check.yml b/.github/workflows/spell-check.yml index 778377b..86d5080 100644 --- a/.github/workflows/spell-check.yml +++ b/.github/workflows/spell-check.yml @@ -16,4 +16,4 @@ jobs: - name: Spell check uses: codespell-project/actions-codespell@master with: - ignore_words_list: ned + ignore_words_list: ned, ser diff --git a/.gitignore b/.gitignore index beaaf8a..d313360 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ .vscode/launch.json .vscode/ipch .vscode/extensions.json +screenshot/*.png diff --git a/README.md b/README.md index b064a41..203fa54 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ -# LILYGO T-Beam Mapper for the Helium or TTN LoRaWAN Network. - +# LILYGO T-Beam Mapper for the Helium or TTN LoRaWAN Network This code loads onto LilyGo TTGO T-Beam v1.2 board with AXP2101 and SX1262 or SX1276 to make a LoraWan (Helium/TTN) Network Mapper. To build one: download this build, configure some files, and upload it to your device. Go travel the world to contribute to the TTN/Helium Network Coverage Maps! @@ -16,21 +15,57 @@ Details for the Mapper project can be found [TTN Mapper](https://ttnmapper.org/) The Mapper is intended to be highly active while the vehicle is in motion, and quieter when the vehicle is stationary. By default, it is not miserly with Data Credits. If you want to conserve Data Credits or battery power, tune the configuration to send packets less frequently. -### But do I get PAID for Mapping? +## Supported Hardware -No, you do not. I put this here because it seems to be the #1 FAQ. You do not earn HNT or Data Credits by mapping. Mapping costs you very little -- One Penny (USD $0.01) for every thousand mapping packets. It helps the Helium network by providing a coverage map, and it helps you by providing clarity on your own local Helium environment. It's all volunteer. +I tested this software on LilyGo [TTGO T-Beam v1.2](https://www.lilygo.cc/products/t-beam-softrf?variant=43170155692213) devices, all on **EU868**. Others have enjoyed success on other worldwide bands, with the matching device. These are commonly available as "Meshtastic" devices from AliExpress, Amazon, Banggood, eBay, etc, usually as a kit with an unsoldered OLED screen and SMA antenna for around USD $30.00. -### But do I get to flag and delist spoofing gamer Hotspots?! +## Quick Setup -No, you do not. It's the #2 FAQ. The Mapper data and coverage maps are not involved in any POC challenges or used for gaming denylists. +Settings for the console and in `credentials.h` -## Supported Hardware +### The Things Network TTN + +Console: + +- Frequency plan: Europe 863-870 MHz (SF9 for RX2 - recommended) +- LoRaWAN version: LoRaWAN Specification 1.1.0 +- Regional Parameters version: RP001 Regional Parameters 1.1 revision A +- Payload formatters: Uplink: JavaScript functions and paste: /console-decoders/unified_decoder.js +- End devices: General settings: Join settings: Resets join nonces: Enabled + +credentials.h: + +- uncomment #define USE_NWK_KEY +- RADIOLIB_LORAWAN_JOIN_EUI: all 0 +- RADIOLIB_LORAWAN_DEV_EUI: copy msb from console +- RADIOLIB_LORAWAN_JOIN_EUI: copy msb from console +- RADIOLIB_LORAWAN_APP_KEY: copy msb from console +- RADIOLIB_LORAWAN_NWK_KEY: copy msb from console + +### Helium + +Console: + +- Region: EU868 +- MAC version: LoRaWAN 1.0.4 +- Regional Parameters revision: A +- ADR algorithm: Default ADR algorithm (LoRa only) +- Join OTAA/ABP: Device supports OTAA +- Codec: JavaScript functions and paste: /console-decoders/unified_decoder.js +- Device Configuration: Disable frame-counter validation + +credentials.h: -I tested this software on (many) LilyGo [TTGO T-Beam v1.2](https://www.lilygo.cc/products/t-beam-softrf?variant=43170155692213) devices, all on **EU868**. Others have enjoyed success on **EU688** and other worldwide bands, with the matching device. These are commonly available as "Meshtastic" devices from AliExpress, Amazon, Banggood, eBay, etc, usually as a kit with an unsoldered OLED screen and SMA antenna for around USD $30.00. +- comment #define USE_NWK_KEY +- RADIOLIB_LORAWAN_JOIN_EUI: copy msb from console +- RADIOLIB_LORAWAN_DEV_EUI: copy msb from console +- RADIOLIB_LORAWAN_JOIN_EUI: copy msb from console +- RADIOLIB_LORAWAN_APP_KEY: copy msb from console +- RADIOLIB_LORAWAN_NWK_KEY: will not be used ### Semtech LoRa Radio -This build uses the [RadioLib Library](https://github.com/jgromes/RadioLib/) for LoRaWAN on the Semtech SX1262 or SX1276 radio modules. +This Fork uses the [RadioLib Library](https://github.com/jgromes/RadioLib/) for LoRaWAN on the Semtech SX1262 or SX1276 radio modules. ### OLED Display @@ -47,12 +82,15 @@ If you incorrectly power the OLED, short connections, or damage the Pin 21/22 co ## Mandatory Configuration Before Building and Uploading, you will probably want to inspect or change some items in these three files: - - `platformio.ini` - - `main/configuration.h` - - `main/credentials.h` + +- `main/configuration.h` +- `main/credentials.h` +- `platformio.ini` + The comments and text below will guide you on what values to look out for. ### Geographic Region, and Frequency + By default, this build is for the **EU868** region. Change the declaration in `credentials.h` for a different locale, to select the correct operating rules and frequency for your country. ### PlatformIO Communication port @@ -63,11 +101,11 @@ On MacOS, it can be significantly more complicated to connect PlatformIO to your ### Device IDs -Each LoRaWAN device is identified by the three OTAA values used in Joining the network: `DevEUI`, `AppEUI`, and `AppKey`. +Each LoRaWAN device is identified by the three OTAA values used in Joining the network: `DevEUI`, `NwkKey`, and `AppKey`. -You should choose your own private `AppKey` value in `credentials.cpp`. Either take the random value generated by the new Console Device entry, or make up one of your own. Read the notes in `credentials.cpp` for details. The value in the build must match the value in Console, regardless of how you achieve that. +You should choose your own private `AppKey` value in `credentials.h`. Either take the random value generated by the new Console Device entry, or make up one of your own. Read the notes in `credentials.h` for details. The value in the build must match the value in Console, regardless of how you achieve that. -By default, the `DevEUI` is generated automatically to be unique to each unit, but you may want to hardcode it in `credentials.cpp` instead. There is an explanation there of why you might want to go either way. +By default, the `DevEUI` is generated automatically to be unique to each unit, but you may want to hardcode it in `credentials.h` instead. There is an explanation there of why you might want to go either way. ### Mapper uplink period and behavior @@ -164,6 +202,7 @@ Regardless of battery or sleep state, the Mapper will power on and resume when U ### Buttons The TTGO T-Beam has three buttons on the underside: + 1. Power: Nearest the USB connector is the Power button. - Menu: **short press** while on will enter the Menu display. Use the Power button to step through options, and the **Middle** button to select a menu entry. - Off: **long press** on this button will turn the unit completely off (5 seconds). @@ -181,6 +220,7 @@ The device outputs debugging information on the USB Serial connection at 115200b #### ESP32 Bootloader On powerup or reset, the very first messages will be from the Bootloader built into the ESP system. This is before any Mapper software runs and should look something like this: + ``` ets Jul 29 2019 12:21:46 @@ -218,7 +258,9 @@ On startup, the USB Serial port will print the DevEUI, AppID, and AppKey values, For some, this is the easiest way to configure a new device. Upload the software, monitor the first boot, then cut & paste the values from the messages into the Console "New Device" setup. ##### Saved Preferences + The Mapper will retain certain settings across power cycles. + * Minimum distance * Stationary Tx Interval (min time) * Rest Wait (time until slower reporting) @@ -299,9 +341,11 @@ The T-Beam usually comes as a kit with a 0.96" SSD1306 OLED screen that you must The OLED screen is always on when operating, as it uses only 10mA. #### Status Bar + Operating Status is shown in the top two rows, with a running 4-line message log in the region below the line. The top status line alternates between two displays every few seconds: + - `#ABC` is the last three hex digits of your DevEUI, so you can match it to the correct device in Console. Handy if you have several Mappers that look the same. - `4.10v` is the battery voltage - `80%` is the charge level of the the battery. @@ -337,16 +381,17 @@ The Payload Port and byte content have been selected to match the format used by A custom Decoder Function translates the payload bytes into a set of JSON values required by the Integrations for both Mapper and Cargo. This turns the Base64 Payload into values for Lat, Long, Altitude, Speed, Battery, and Sats. -This [Decoder Function](https://github.com/designer2k2/tbeam-lorawan-mapper/blob/RadioLib_SX1262/console-decoders/unified_decoder.js)) can be pasted directly into the Console custom function. Do not use Decoder functions from other builds or instructions! The Uplink decoding is specific to the software that made the packet, so it has to match. (Note that HDOP is not sent in this data.) +This [Decoder Function](https://github.com/designer2k2/tbeam-lorawan-mapper/blob/main/console-decoders/unified_decoder.js) can be pasted directly into the Console custom function. Do not use Decoder functions from other builds or instructions! The Uplink decoding is specific to the software that made the packet, so it has to match. (Note that HDOP is not sent in this data.) ### Grafana integration for custom maps If you want to maintain your own device map, there is an excellent [Grafana guide](https://github.com/takeabyte/helium_mapper_grafana) by @takeabyte (`@friends just call me bob`) available. ## Downlink + This builds adds the option to reconfigure the Mapper remotely via Helium Downlink (network to device). You can change the maximum Time Interval, Distance, and Battery Cut-off voltage remotely. -### Format your Downlink Payload. +### Format your Downlink Payload You can use the `console-decoders/downlink_encoder.py` Python script to convert your intent into a Base64 Payload. ``` @@ -395,12 +440,11 @@ This build is a modification of work by many experts, with input from the [Heliu The Fork history here in Github shows the lineage and prior work, including https://github.com/helium/longfi-arduino/tree/master/TTGO-TBeam-Tracker -This code was originally developed for use on The Things Network (TTN) it has been edited/repurposed for use with the Helium Network. +This code was originally developed for use on The Things Network (TTN) it has been edited/repurposed for use with the Helium Network. And then back to be universal for both. -This version is based on a forked repo from github user [kizniche] https://github.com/kizniche/ttgo-tbeam-ttn-tracker. Which in turn is based on the code from [xoseperez/ttgo-beam-tracker](https://github.com/xoseperez/ttgo-beam-tracker), with excerpts from [dermatthias/Lora-TTNMapper-T-Beam](https://github.com/dermatthias/Lora-TTNMapper-T-Beam) to fix an issue with incorrect GPS data being transmitted to the network. Support was also added for the 915 MHz frequency (North and South America). +This version is based on a forked repo from github user [Max-Plastix](https://github.com/Max-Plastix/tbeam-helium-mapper). Which is based on [kizniche](https://github.com/kizniche/ttgo-tbeam-ttn-tracker). Which in turn is based on the code from [xoseperez/ttgo-beam-tracker](https://github.com/xoseperez/ttgo-beam-tracker), with excerpts from [dermatthias/Lora-TTNMapper-T-Beam](https://github.com/dermatthias/Lora-TTNMapper-T-Beam) to fix an issue with incorrect GPS data being transmitted to the network. Support was also added for the 915 MHz frequency (North and South America). -This is a LoRaWAN node based on the [TTGO T-Beam](https://github.com/LilyGO/TTGO-T-Beam) development platform using the SSD1306 I2C OLED display. -It uses a RFM95 by HopeRF and the MCCI LoRaWAN LMIC stack. This sample code is configured to connect to The LoRaWan network using the US 915 MHz frequency by default, but can be changed to EU 868 MHz. +This is a LoRaWAN node based on the [TTGO T-Beam](https://github.com/LilyGO/TTGO-T-Beam) development platform using the SSD1306 I2C OLED display. This sample code is configured to connect to The LoRaWan network using the US 915 MHz frequency by default, but can be changed to EU 868 MHz. NOTE: There are now 2 versions of the TTGO T-BEAM, the first version (Rev0) and a newer version (Rev1). The GPS module on Rev1 is connected to different pins than Rev0. This code has been successfully tested on REV0, and is in the process of being tested on REV1. See the end of this README for photos of each board. diff --git a/main/credentials.h b/main/credentials.h index d4d946e..44f140d 100644 --- a/main/credentials.h +++ b/main/credentials.h @@ -6,39 +6,34 @@ #include /* -This is where you define the three key values that map your Device to the Helium Console. +This is where you define the three key values that map your Device to the LoRaWAN Console. All three values must match between the code and the Console. -There are two general ways to go about this: -1) Let the Console pick random values for one or all of them, and copy them here in the code. --or- -2) Define them here in the code, and then copy them to the Console to match these values. - -When the Mapper boots, it will show all three values in the Monitor console, like this: - -DevEUI (msb): AABBCCDDEEFEFF -APPEUI (msb): 6081F9BF908E2EA0 -APPKEY (msb): CF4B3E8F8FCB779C8E1CAEE311712AE5 - -This format is suitable for copying from Terminal/Monitor and pasting directly into the console as-is. - If you want to take the random Console values for a new device, and use them here, be sure to select: Device EUI: msb App Key: msb NwK Key: msb in the Console, then click the arrows to expand the values with comma separators, then paste them below. */ + +/* +NwkKey option for LoRaWAN 1.1.x +- For LoRaWAN 1.0.x, comment out the #define line. +- For LoRaWAN 1.1.x, uncomment it and provide your NwkKey. +*/ +//#define USE_NWK_KEY + // joinEUI - previous versions of LoRaWAN called this AppEUI // for development purposes you can use all zeros - see wiki for details -#define RADIOLIB_LORAWAN_JOIN_EUI 0x0000000000000000 +#define RADIOLIB_LORAWAN_JOIN_EUI 0xa09284515663b1a5 // the Device EUI & two keys can be generated on the TTN console #ifndef RADIOLIB_LORAWAN_DEV_EUI // Replace with your Device EUI -#define RADIOLIB_LORAWAN_DEV_EUI 0x70B3D57ED0066B6E +#define RADIOLIB_LORAWAN_DEV_EUI 0x044e31696f7f04de #endif #ifndef RADIOLIB_LORAWAN_APP_KEY // Replace with your App Key #define RADIOLIB_LORAWAN_APP_KEY \ - 0x74, 0x5D, 0x28, 0x7B, 0xEF, 0xFB, 0x51, 0xFF, 0x4A, 0x89, 0xDC, 0xF7, 0x95, 0x3B, 0x16, 0x4D + 0x4B, 0x8F, 0xA9, 0x31, 0xAB, 0x2C, 0x68, 0x5B, 0x14, 0x3C, 0x49, 0xB0, 0x7B, 0xFD, 0x35, 0xE3 #endif #ifndef RADIOLIB_LORAWAN_NWK_KEY // Put your Nwk Key here #define RADIOLIB_LORAWAN_NWK_KEY \ @@ -56,8 +51,13 @@ const uint8_t subBand = 0; // For US915, change this to 2, otherwise leave on 0 uint64_t joinEUI = RADIOLIB_LORAWAN_JOIN_EUI; uint64_t devEUI = RADIOLIB_LORAWAN_DEV_EUI; uint8_t appKey[] = {RADIOLIB_LORAWAN_APP_KEY}; -uint8_t nwkKey[] = {RADIOLIB_LORAWAN_NWK_KEY}; +// Conditionally define nwkKey. If the macro doesn't exist, it becomes a null pointer. +#ifdef USE_NWK_KEY + uint8_t nwkKey[] = {RADIOLIB_LORAWAN_NWK_KEY}; +#else + uint8_t* nwkKey = NULL; +#endif // do not modify below easily, switch between radios in the platformio.ini file build_flags section. diff --git a/main/main.cpp b/main/main.cpp index 703e770..d1f76ce 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -1,14 +1,16 @@ /** * LoRaWan Mapper build for LilyGo TTGO T-Beam v1.2 boards. + * + * Copyright (C) 2025 designer2k2 Stephan M. * Copyright (C) 2021-2022 by Max-Plastix * - * This is a development fork by Max-Plastix hosted here: - * https://github.com/Max-Plastix/tbeam-helium-mapper/ + * This is a fork by designer2k2 hosted here: + * https://github.com/designer2k2/tbeam-lorawan-mapper * * This code comes from a number of developers and earlier efforts, visible in * the full lineage on Github, including: * - * Fizzy, longfi-arduino, Kyle T. Gabriel, and Xose Pérez + * Max-Plastix, Fizzy, longfi-arduino, Kyle T. Gabriel, and Xose Pérez * * GPL makes this all possible -- continue to modify, extend, and share! */ @@ -139,8 +141,6 @@ esp_sleep_source_t wakeCause; // the reason we booted this time char buffer[40]; // Screen buffer - -String lorawanServer; uint8_t lorawanAck = false; uint8_t lorawan_sf; // prefs LORAWAN_SF uint8_t lorawan_tx_power; @@ -573,7 +573,6 @@ void lorawan_restore_prefs(void) { if (p.begin("lora", true)) { // Read-only lorawanAck = p.getUChar("ack", LORAWAN_CONFIRMED_EVERY); lorawan_sf = p.getUChar("sf", LORAWAN_SF); - lorawanServer = p.getString("server", "helium"); lorawan_tx_power = p.getUChar("tx_power", 16); // a buffer that holds all LW base parameters that should persist at all times! uint8_t BbufferNonces[RADIOLIB_LORAWAN_NONCES_BUF_SIZE]; @@ -612,7 +611,6 @@ void lorawan_save_prefs(void) { Preferences p; Serial.println("Saving lorawan prefs."); if (p.begin("lora", false)) { - p.putString("server", lorawanServer); p.putUChar("sf", lorawan_sf); p.putUChar("ack", lorawanAck); p.putUChar("tx_power", lorawan_tx_power); diff --git a/main/screen.cpp b/main/screen.cpp index 3bc0797..0a4005c 100644 --- a/main/screen.cpp +++ b/main/screen.cpp @@ -2,8 +2,10 @@ * * SSD1306 - Screen module * + * Copyright (C) 2025 designer2k2 Stephan M. * Copyright (C) 2018 by Xose Pérez * + * Based on the work from Xose Pérez * * 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 @@ -33,6 +35,26 @@ #include "gps.h" #include "images.h" +// --- Screenshot Helper Classes --- +// These simple subclasses expose the protected 'buffer' from the base library +// so we can access it for screen capture functionality without modifying the library. + +class ScreenCaptureSSD1306 : public SSD1306Wire { +public: + ScreenCaptureSSD1306(uint8_t addr, uint8_t sda, uint8_t scl) : SSD1306Wire(addr, sda, scl) {} + uint8_t* getBuffer() { + return this->buffer; + } +}; + +class ScreenCaptureSH1106 : public SH1106Wire { +public: + ScreenCaptureSH1106(uint8_t addr, uint8_t sda, uint8_t scl) : SH1106Wire(addr, sda, scl) {} + uint8_t* getBuffer() { + return this->buffer; + } +}; + #define SCREEN_HEADER_HEIGHT 23 const uint16_t logBufferLineLen = 30; const uint8_t logBufferMaxLines = 4; @@ -185,6 +207,7 @@ void screen_buffer_print() { void screen_update() { if (display) display->display(); + // screen_serial_dump_compressed(); // Send screenshot over serial } /** @@ -275,13 +298,14 @@ void screen_setup(uint8_t addr) { if (display_type == E_DISPLAY_UNKNOWN) display_type = display_get_type(addr); - // Display instance + // Display instance - CHANGED TO USE OUR WRAPPER CLASSES if (display_type == E_DISPLAY_SSD1306) - display = new SSD1306Wire(addr, I2C_SDA, I2C_SCL); + display = new ScreenCaptureSSD1306(addr, I2C_SDA, I2C_SCL); // Use our class else if (display_type == E_DISPLAY_SH1106) - display = new SH1106Wire(addr, I2C_SDA, I2C_SCL); + display = new ScreenCaptureSH1106(addr, I2C_SDA, I2C_SCL); // Use our class else return; + display->init(); display->flipScreenVertically(); display->setFont(Custom_Font); @@ -382,4 +406,107 @@ void screen_body(boolean in_menu, const char *menu_prev, const char *menu_cur, c screen_buffer_print(); } display->display(); + // screen_serial_dump_compressed(); // Send screenshot over serial +} + +/** + * @brief Gets the state of a single pixel directly from the display's memory buffer. + * This version uses a subclassing trick to access the protected buffer. + * @param x The x-coordinate of the pixel. + * @param y The y-coordinate of the pixel. + * @return 1 if the pixel is on (white), 0 if it is off (black). + */ +int getPixelFromBuffer(int16_t x, int16_t y) { + if (!display) return 0; + + uint8_t* buffer = nullptr; + + // Cast the display pointer to our specific subclass to access getBuffer() + if (display_type == E_DISPLAY_SSD1306) { + buffer = static_cast(display)->getBuffer(); + } else if (display_type == E_DISPLAY_SH1106) { + buffer = static_cast(display)->getBuffer(); + } + + if (!buffer) return 0; // Safety check + + // Check if coordinates are out of bounds + if (x < 0 || x >= display->getWidth() || y < 0 || y >= display->getHeight()) { + return 0; + } + + // The rest of the logic is the same as before + int byte_index = x + (y / 8) * display->getWidth(); + int bit_index = y % 8; + + if ((buffer[byte_index] >> bit_index) & 1) { + return 1; + } + + return 0; +} + +/** + * @brief Dumps the current screen buffer to the Serial port as ASCII art. + * Useful for debugging without having physical access to the screen. + */ +void screen_serial_dump() { + if (!display) { + return; + } + + Serial.println(F("\n--- SCREEN DUMP BEGIN ---")); + for (int16_t y = 0; y < display->getHeight(); y++) { + for (int16_t x = 0; x < display->getWidth(); x++) { + // Use the new helper function to read from the buffer + Serial.print(getPixelFromBuffer(x, y) ? "#" : "."); + } + Serial.println(); // Newline after each row + } + Serial.println(F("--- SCREEN DUMP END ---")); +} + + +/** + * @brief Dumps the current screen buffer to Serial using Run-Length Encoding (RLE). + * This is much faster than the uncompressed dump. + * Format: B W ... (e.g., B128 W15 B1000) + */ +void screen_serial_dump_compressed() { + if (!display) { + return; + } + + Serial.println(F("\n--- RLE DUMP BEGIN ---")); + + // Get the state of the very first pixel to start the first run + int currentRunState = getPixelFromBuffer(0, 0); + int runLength = 0; + + for (int16_t y = 0; y < display->getHeight(); y++) { + for (int16_t x = 0; x < display->getWidth(); x++) { + // Use the new helper function here as well + int pixelState = getPixelFromBuffer(x, y); + if (pixelState == currentRunState) { + // If the pixel is the same, extend the current run + runLength++; + } else { + // The pixel changed, so the run has ended. Print it. + Serial.print(currentRunState ? 'W' : 'B'); + Serial.print(runLength); + Serial.print(' '); + + // Start a new run + currentRunState = pixelState; + runLength = 1; + } + } + } + + // After the loops, print the very last run + Serial.print(currentRunState ? 'W' : 'B'); + Serial.print(runLength); + Serial.println(); // Final newline + + Serial.println(F("--- RLE DUMP END ---")); } \ No newline at end of file diff --git a/main/screen.h b/main/screen.h index f18054d..5bce6e3 100644 --- a/main/screen.h +++ b/main/screen.h @@ -33,3 +33,6 @@ void screen_on(void); void screen_show_logo(void); void screen_setup(uint8_t addr); void screen_end(void); + +void screen_serial_dump(); +void screen_serial_dump_compressed(); \ No newline at end of file diff --git a/screenshot/screenshotreceiver.py b/screenshot/screenshotreceiver.py new file mode 100644 index 0000000..041822a --- /dev/null +++ b/screenshot/screenshotreceiver.py @@ -0,0 +1,157 @@ +# +# Screenshot receiver +# +# Copyright (C) 2025 designer2k2 Stephan M. +# +# 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 +# (at your option) 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 . +# + +import serial +import argparse +from PIL import Image +import sys +import os +from datetime import datetime +import re + +# --- Constants for different screen sizes --- +# If you use a different display, change the width and height here. +DISPLAY_WIDTH = 128 +DISPLAY_HEIGHT = 64 + +def screenshot_listener(port, baud, base_output_file): + """ + Listens on a serial port for screen dumps (compressed or uncompressed) + and saves each as a timestamped PNG image. + """ + print(f"--- Listening on port {port} at {baud} bps ---") + try: + ser = serial.Serial(port, baud, timeout=2) + print("--- Port opened. Press Ctrl+C to exit. ---") + except serial.SerialException as e: + print(f"Error: Could not open port {port}. {e}") + sys.exit(1) + + try: + while True: + print(f"\n--- Waiting for screenshot dump (Format: Auto-Detect) ---") + line_bytes = ser.readline() + if not line_bytes: + continue + + line = line_bytes.decode('utf-8', errors='ignore').strip() + + # --- Auto-detect format --- + if "--- RLE DUMP BEGIN ---" in line: + print("--- Compressed (RLE) dump detected. Capturing... ---") + rle_data_line = ser.readline().decode('utf-8', errors='ignore').strip() + ser.readline() # Consume the END marker + print("--- Capture complete. ---") + process_rle_and_save(rle_data_line, base_output_file) + + elif "--- SCREEN DUMP BEGIN ---" in line: + print("--- Uncompressed dump detected. Capturing... ---") + screenshot_lines = [] + while True: + line_bytes = ser.readline() + if not line_bytes or b"--- SCREEN DUMP END ---" in line_bytes: + break + screenshot_lines.append(line_bytes.decode('utf-8', errors='ignore').strip()) + print("--- Capture complete. ---") + process_uncompressed_and_save(screenshot_lines, base_output_file) + + except KeyboardInterrupt: + print("\n--- Program interrupted by user. Exiting. ---") + except serial.SerialException as e: + print(f"\n--- Serial port error: {e}. Exiting. ---") + finally: + if ser.is_open: + ser.close() + print("--- Serial port closed. ---") + +def process_rle_and_save(rle_data, base_output_file): + """ + Decodes RLE data and saves it as a white-on-black PNG image. + """ + if not rle_data: + print("--- RLE data is empty. Cannot create image. ---") + return + + print(f"--- Creating a {DISPLAY_WIDTH}x{DISPLAY_HEIGHT} image from RLE data... ---") + img = Image.new('1', (DISPLAY_WIDTH, DISPLAY_HEIGHT), 0) # Black background + pixels = img.load() + + x, y = 0, 0 + runs = re.findall(r'([WB])(\d+)', rle_data) + + for color_char, length_str in runs: + length = int(length_str) + pixel_color = 255 if color_char == 'W' else 0 # White or Black + + for _ in range(length): + if x < DISPLAY_WIDTH and y < DISPLAY_HEIGHT: + pixels[x, y] = pixel_color + + x += 1 + if x >= DISPLAY_WIDTH: + x = 0 + y += 1 + + save_image(img, base_output_file) + +def process_uncompressed_and_save(lines, base_output_file): + """ + Processes uncompressed ASCII art and saves it as a PNG image. + """ + if not lines: + print("--- No data captured. Cannot create image. ---") + return + + height = len(lines) + width = len(lines[0]) if height > 0 else 0 + + print(f"--- Creating a {width}x{height} image from uncompressed data... ---") + img = Image.new('1', (width, height), 0) # Black background + pixels = img.load() + + for y, row_str in enumerate(lines): + for x, char in enumerate(row_str): + if char == '#': + pixels[x, y] = 255 # White pixel + + save_image(img, base_output_file) + +def save_image(img, base_output_file): + """ + Saves a PIL image object with a timestamped filename. + """ + name, ext = os.path.splitext(base_output_file) + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + final_output_file = f"{name}_{timestamp}{ext}" + try: + img.save(final_output_file) + print(f"--- Screenshot successfully saved to '{final_output_file}' ---") + except IOError as e: + print(f"Error: Could not save image file. {e}") + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Capture screen dumps from serial and save as PNGs. Auto-detects RLE compression.", + ) + parser.add_argument('-p', '--port', required=True, help="Serial port name (e.g., COM3, /dev/ttyUSB0)") + parser.add_argument('-b', '--baud', type=int, default=115200, help="Baud rate (default: 115200)") + parser.add_argument('-o', '--output', default='screenshot.png', help="Base name for output files (default: screenshot.png)") + + args = parser.parse_args() + screenshot_listener(args.port, args.baud, args.output) \ No newline at end of file