diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000..29b3030fc --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,20 @@ +- [x] Verify that the copilot-instructions.md file in the .github directory is created. + +- [x] Clarify Project Requirements + +- [x] Scaffold the Project + +- [x] Customize the Project + +- [x] Install Required Extensions + +- [x] Compile the Project + +- [x] Create and Run Task + +- [x] Launch the Project + +- [x] Ensure Documentation is Complete +- Work through each checklist item systematically. +- Keep communication concise and focused. +- Follow development best practices. diff --git a/README.md b/README.md index da6d9d7d1..a88c7834c 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ # :shark: Bruce Bruce is a versatile ESP32 firmware that supports a ton of offensive features focusing on facilitating Red Team operations. -It also supports M5stack and Lilygo products and works great with Cardputer, Sticks, M5Cores, T-Decks and T-Embeds. +It also supports M5stack and Lilygo products and works great with Cardputer, Sticks, M5Cores, CoreInk, T-Decks and T-Embeds. **Check our fully open-source hardware too:** https://bruce.computer/boards @@ -221,6 +221,7 @@ Also, [read our FAQ](https://github.com/pr3y/Bruce/wiki/FAQ) | [M5Stack M5Core BASIC](https://shop.m5stack.com/products/basic-core-iot-development-kit) | :ok: | :ok: | :ok: | :ok: | :ok: | :ok:¹ | :x: | Tone | :x: | :x: | | [M5Stack M5Core2](https://shop.m5stack.com/products/m5stack-core2-esp32-iot-development-kit-v1-1) | :ok: | :ok: | :ok: | :ok: | :ok: | :ok:¹ | :x: | :x: | :x: | :x: | | [M5Stack M5CoreS3](https://shop.m5stack.com/products/m5stack-cores3-esp32s3-lotdevelopment-kit)/[SE](https://shop.m5stack.com/products/m5stack-cores3-se-iot-controller-w-o-battery-bottom) | :ok: | :ok: | :ok: | :ok: | :x: | :ok: | :x: | :x: | :x: | :x: | +| [M5Stack CoreInk](https://shop.m5stack.com/products/m5stack-esp32-core-ink-development-kit1-54-elnk-display) | :ok: | :ok: | :ok: | :ok: | :x: | :ok:¹ | :x: | :x: | :ok: | :x: | | [JCZN CYD‑2432S028](https://www.aliexpress.us/item/3256804774970998.html) | :ok: | :ok: | :ok: | :ok: | :x: | :ok:¹ | :x: | :x: | :x: | :x:² | | [Lilygo T‑Embed CC1101](https://lilygo.cc/products/t-embed-cc1101) | :ok: | :ok: | :ok: | :ok: | :ok: | :ok: | :ok: | :ok: | :ok: | :x: | | [Lilygo T‑Embed](https://lilygo.cc/products/t-embed) | :ok: | :ok: | :ok: | :ok: | :ok: | :ok: | :ok: | :ok: | :x: | :x: | @@ -238,6 +239,44 @@ Also, [read our FAQ](https://github.com/pr3y/Bruce/wiki/FAQ) *LITE_VERSION*: TelNet, SSH, WireGuard, ScanHosts, RawSniffer, Brucegotchi, BLEBacon, BLEScan and Interpreter are NOT available for M5Launcher Compatibility +## :desktop_computer: E-Ink Display Optimizations (M5Stack CoreInk) + +The M5Stack CoreInk features a 200x200 pixel e-ink display that requires special optimizations for an optimal user experience. This fork includes several key enhancements specifically designed for e-ink displays: + +### Display & Power Management +- **No Backlight Control**: E-ink displays don't have backlights, so brightness controls are disabled +- **Screen Timeout Disabled**: Screen dimming and auto-off features are bypassed as they don't apply to e-ink +- **Optimized Sleep Mode**: Power saving mode is adapted to work without display backlight control +- **LED Feedback**: Input button presses trigger brief LED pulses to provide immediate feedback (since display updates are slower) + +### Refresh Rate Control +- **Configurable Refresh Intervals**: Choose from manual, 15s, 30s, 60s, or 5-minute automatic refresh rates + - Access via: Config → Display & UI → Refresh + - Manual mode (0ms): Display only updates when explicitly flushed by the application + - Timed modes: Display automatically flushes at specified intervals to reduce ghosting +- **Intelligent Flush System**: Menu navigation uses a 300ms minimum flush interval to balance responsiveness with display longevity + +### Visual Optimizations +- **Black & White Theme**: Colors are automatically forced to pure black (0x0000) on white (0xFFFF) background for maximum contrast +- **Simplified Time Display**: Clock shows HH:MM format instead of HH:MM:SS to reduce unnecessary refreshes +- **Disabled Scrolling Text**: Text scrolling animations are disabled as they consume excessive refresh cycles +- **Inverted Color Default**: Color inversion is disabled by default (compared to stock Bruce) for natural e-ink appearance + +### Input Handling +- **Rocker Navigation**: Uses the 3-button rocker switch (left/right for navigation, center for select) + - Short press center button: Select + - Long press center button (800ms+): Back/Escape + - Configurable inversion via Config → Display & UI → Rocker Invert + +### Hardware Support +- **Battery Monitoring**: Full battery level reporting and charging status detection +- **I2C Grove Connector**: Pin 21 (SDA) and Pin 22 (SCL) for external modules +- **BadUSB Support**: Via Grove connector pins +- **No Onboard SD Card**: External SD card adapters can be connected via I2C or SPI + +These optimizations ensure Bruce runs efficiently on the CoreInk while preserving battery life and extending the lifespan of the e-ink display. + + ## :sparkles: Why and how does it look? Bruce stems from a keen observation within the community focused on devices like Flipper Zero. While these devices offered a glimpse into the world of offensive security, there was a palpable sense that something more could be achieved without being that overpriced, particularly with the robust and modular hardware ecosystem provided by ESP32 Devices, Lilygo and M5Stack products. diff --git a/boards/_boards_json/m5stack-coreink.json b/boards/_boards_json/m5stack-coreink.json new file mode 100644 index 000000000..027ba49b3 --- /dev/null +++ b/boards/_boards_json/m5stack-coreink.json @@ -0,0 +1,38 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32_out.ld", + "partitions": "default_4MB.csv" + }, + "core": "esp32", + "extra_flags": [ + "-DARDUINO_M5STACK_COREINK", + "-DM5STACK" + ], + "f_cpu": "240000000L", + "f_flash": "40000000L", + "flash_mode": "dio", + "mcu": "esp32", + "variant": "pinouts" + }, + "connectivity": [ + "wifi", + "bluetooth", + "ethernet", + "can" + ], + "frameworks": [ + "arduino", + "espidf" + ], + "name": "M5Stack CoreInk", + "upload": { + "flash_size": "4MB", + "maximum_ram_size": 327680, + "maximum_size": 4194304, + "require_upload_port": true, + "speed": 460800 + }, + "url": "http://www.m5stack.com", + "vendor": "M5Stack" +} diff --git a/boards/m5stack-coreink/interface.cpp b/boards/m5stack-coreink/interface.cpp new file mode 100644 index 000000000..fdf7f733a --- /dev/null +++ b/boards/m5stack-coreink/interface.cpp @@ -0,0 +1,119 @@ +#include "core/powerSave.h" +#include +#include + +namespace { +constexpr uint8_t ROCKER_LEFT_PIN = 39; +constexpr uint8_t ROCKER_CENTER_PIN = 38; +constexpr uint8_t ROCKER_RIGHT_PIN = 37; +constexpr bool ROCKER_ACTIVE_LOW = true; + +bool readRockerPin(uint8_t pin) { + int level = digitalRead(pin); + return ROCKER_ACTIVE_LOW ? (level == LOW) : (level == HIGH); +} +} // namespace + +/*************************************************************************************** +** Function name: _setup_gpio() +** Location: main.cpp +** Description: initial setup for the device +***************************************************************************************/ +void _setup_gpio() { + M5.begin(); + pinMode(ROCKER_LEFT_PIN, INPUT); + pinMode(ROCKER_CENTER_PIN, INPUT); + pinMode(ROCKER_RIGHT_PIN, INPUT); +} + +/*************************************************************************************** +** Function name: getBattery() +** location: display.cpp +** Description: Delivers the battery value from 1-100 +***************************************************************************************/ +int getBattery() { + int percent = M5.Power.getBatteryLevel(); + return (percent < 0) ? 1 : (percent >= 100) ? 100 : percent; +} + +/********************************************************************* +** Function: setBrightness +** location: settings.cpp +** set brightness value +**********************************************************************/ +void _setBrightness(uint8_t brightval) { (void)brightval; } + +/********************************************************************* +** Function: InputHandler +** Handles the variables PrevPress, NextPress, SelPress, AnyKeyPress and EscPress +**********************************************************************/ +void InputHandler(void) { + static unsigned long tm = 0; + if (millis() - tm < 200 && !LongPress) return; + + M5.update(); + + // Rocker button: left/right = next/prev (inverted for intuitive nav), center = select/back + static bool lastLeft = false; + static bool lastRight = false; + static bool lastCenter = false; + static unsigned long centerPressTime = 0; + + bool leftNow = readRockerPin(ROCKER_LEFT_PIN); + bool rightNow = readRockerPin(ROCKER_RIGHT_PIN); + bool centerNow = readRockerPin(ROCKER_CENTER_PIN); + + bool leftPressed = leftNow && !lastLeft; + bool rightPressed = rightNow && !lastRight; + bool centerPressed = centerNow && !lastCenter; + + // Track center button hold time for long press detection + if (centerNow && !lastCenter) { centerPressTime = millis(); } + bool centerLongPress = centerNow && (millis() - centerPressTime > 800); + + lastLeft = leftNow; + lastRight = rightNow; + lastCenter = centerNow; + + if (centerPressed) { + leftPressed = false; + rightPressed = false; + } + + bool any = leftPressed || rightPressed || centerPressed; + if (any) tm = millis(); + if (any && wakeUpScreen()) return; + + AnyKeyPress = any; + UpPress = false; + DownPress = false; + SelPress = centerPressed && !centerLongPress; // Select on short press + NextPress = leftPressed; // Inverted: left = next (right direction) + PrevPress = rightPressed; // Inverted: right = prev (left direction) + EscPress = centerLongPress; // Back on long press (800ms+) + LongPress = false; +} + +/********************************************************************* +** Function: powerOff +** location: mykeyboard.cpp +** Turns off the device (or try to) +**********************************************************************/ +void powerOff() { M5.Power.powerOff(); } +void goToDeepSleep() { M5.Power.deepSleep(); } + +/********************************************************************* +** Function: checkReboot +** location: mykeyboard.cpp +** Btn logic to turn off the device (name is odd btw) +**********************************************************************/ +void checkReboot() {} + +/*************************************************************************************** +** Function name: isCharging() +** Description: Determines if the device is charging +***************************************************************************************/ +bool isCharging() { + if (M5.Power.getBatteryCurrent() > 0 || M5.Power.getBatteryCurrent()) return true; + else return false; +} diff --git a/boards/m5stack-coreink/m5stack-coreink.ini b/boards/m5stack-coreink/m5stack-coreink.ini new file mode 100644 index 000000000..7b52f9ea5 --- /dev/null +++ b/boards/m5stack-coreink/m5stack-coreink.ini @@ -0,0 +1,45 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:m5stack-coreink] +board = m5stack-coreink +monitor_speed = 115200 +board_build.partitions = custom_4Mb_full.csv +build_src_filter =${env.build_src_filter} +<../boards/m5stack-coreink> +build_flags = + ${env.build_flags} + -Iboards/m5stack-coreink + -DCORE_DEBUG_LEVEL=0 + -DUSE_M5GFX=1 + -DHAS_SCREEN=1 + -DHAS_EINK=1 + -DROTATION=1 + -DFP=1 + -DFM=2 + -DFG=3 + -DTFT_WIDTH=200 + -DTFT_HEIGHT=200 + -DDEVICE_NAME='"M5Stack CoreInk"' + -DBAD_TX=GROVE_SDA + -DBAD_RX=GROVE_SCL + + ; Default I2C port + -DGROVE_SDA=21 + -DGROVE_SCL=22 + + ; SD Card pins (CoreInk has no onboard SD) + -DSDCARD_CS=-1 + -DSDCARD_SCK=-1 + -DSDCARD_MISO=-1 + -DSDCARD_MOSI=-1 + +lib_deps = + ${env.lib_deps} + m5stack/M5Unified @ ^0.2.11 diff --git a/boards/m5stack-coreink/pins_arduino.h b/boards/m5stack-coreink/pins_arduino.h new file mode 100644 index 000000000..65263f802 --- /dev/null +++ b/boards/m5stack-coreink/pins_arduino.h @@ -0,0 +1,55 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +static const uint8_t TX = 1; +static const uint8_t RX = 3; + +static const uint8_t SDA = 21; +static const uint8_t SCL = 22; + +static const uint8_t SS = 5; +static const uint8_t MOSI = 23; +static const uint8_t MISO = 19; +static const uint8_t SCK = 18; + +static const uint8_t G0 = 0; +static const uint8_t G1 = 1; +static const uint8_t G2 = 2; +static const uint8_t G3 = 3; +static const uint8_t G4 = 4; +static const uint8_t G5 = 5; +static const uint8_t G12 = 12; +static const uint8_t G13 = 13; +static const uint8_t G14 = 14; +static const uint8_t G15 = 15; +static const uint8_t G16 = 16; +static const uint8_t G17 = 17; +static const uint8_t G18 = 18; +static const uint8_t G19 = 19; +static const uint8_t G21 = 21; +static const uint8_t G22 = 22; +static const uint8_t G23 = 23; +static const uint8_t G25 = 25; +static const uint8_t G26 = 26; +static const uint8_t G27 = 27; +static const uint8_t G32 = 32; +static const uint8_t G33 = 33; +static const uint8_t G34 = 34; +static const uint8_t G35 = 35; +static const uint8_t G36 = 36; +static const uint8_t G39 = 39; + +static const uint8_t DAC1 = 25; +static const uint8_t DAC2 = 26; + +static const uint8_t ADC1 = 35; +static const uint8_t ADC2 = 36; + +#define SPI_SCK_PIN SCK +#define SPI_MOSI_PIN MOSI +#define SPI_MISO_PIN MISO +#define SPI_SS_PIN SS + +#endif /* Pins_Arduino_h */ diff --git a/boards/m5stack-coreink/readme.md b/boards/m5stack-coreink/readme.md new file mode 100644 index 000000000..d45b5345f --- /dev/null +++ b/boards/m5stack-coreink/readme.md @@ -0,0 +1,103 @@ +# M5Stack CoreInk - Bruce Firmware + +This directory contains the board-specific configuration and drivers for the M5Stack CoreInk e-ink device. + +## Device Overview + +The M5Stack CoreInk is an ESP32-based development kit featuring: +- **Display**: 200x200 pixel e-ink display (1.54" diagonal) +- **CPU**: ESP32-PICO-D4 (dual-core, 240MHz) +- **Memory**: 4MB Flash +- **Battery**: Built-in 390mAh rechargeable lithium battery with power management +- **Input**: 3-button rocker switch +- **Connectivity**: Grove I2C port (Pin 21 SDA, Pin 22 SCL) +- **LED**: Power status LED with programmable control +- **Size**: Compact 56mm × 40mm × 16mm form factor + +## Bruce Firmware Adaptations + +### E-Ink Display Optimizations + +The e-ink display requires special handling compared to traditional LCD/OLED displays: + +1. **Refresh Rate Management** + - Configurable auto-refresh intervals: Manual, 15s, 30s, 60s, or 5 minutes + - Intelligent flush system prevents excessive refreshes + - Menu updates use 300ms minimum interval for responsiveness + +2. **Visual Rendering** + - Forced black & white color scheme (no grayscale) + - Disabled text scrolling animations + - Simplified clock display (HH:MM only) + - No backlight control (not applicable to e-ink) + +3. **Power Management** + - Screen timeout features disabled + - Display sleep mode bypassed + - LED feedback for button presses (immediate user feedback) + - Optimized power consumption for extended battery life + +### Input System + +The rocker switch provides navigation: +- **Left/Right**: Previous/Next menu item (configurable inversion) +- **Center (short press)**: Select/Confirm +- **Center (long press, 800ms+)**: Back/Escape +- **LED Pulse**: 35ms flash on any button press for tactile feedback + +### Pin Configuration + +| Function | Pin | Notes | +|----------|-----|-------| +| Grove SDA | 21 | I2C Data (external modules) | +| Grove SCL | 22 | I2C Clock (external modules) | +| Rocker Left | 39 | Navigation input | +| Rocker Center | 38 | Select/Back input | +| Rocker Right | 37 | Navigation input | +| BadUSB TX | 21 | Via Grove SDA | +| BadUSB RX | 22 | Via Grove SCL | + +### External Module Support + +Via the Grove I2C connector, the CoreInk supports: +- **CC1101**: Sub-GHz RF transceiver +- **NRF24**: 2.4GHz wireless module +- **PN532**: RFID/NFC reader +- **FM Radio**: RDA5807M or similar I2C modules +- **SD Card**: Via I2C or external SPI adapter + +## Files in This Directory + +- **`m5stack-coreink.ini`**: PlatformIO build configuration with compiler flags and pin definitions +- **`interface.cpp`**: Hardware abstraction layer for buttons, battery, LED, and power management +- **`pins_arduino.h`**: Arduino pin definitions for the CoreInk board +- **`readme.md`**: This file + +## Building for CoreInk + +1. Edit `platformio.ini` in the project root +2. Set `default_envs = m5stack-coreink` +3. Build: `pio run -e m5stack-coreink` +4. Flash: `pio run -e m5stack-coreink -t upload` + +Or use the web flasher at https://bruce.computer/flasher + +## Configuration Notes + +- No onboard SD card slot (use external adapter) +- Default rotation: 3 (270 degrees) +- 4MB partition scheme with full LittleFS support +- USB CDC for serial communication + +## Known Limitations + +- No built-in microphone +- No RGB LED strip support +- No onboard speaker (tone generation not available) +- E-ink ghosting may occur with frequent refreshes (mitigated by configurable intervals) + +## Additional Resources + +- [M5Stack CoreInk Product Page](https://shop.m5stack.com/products/m5stack-esp32-core-ink-development-kit1-54-elnk-display) +- [Bruce Wiki](https://github.com/pr3y/Bruce/wiki) +- [Bruce Discord](https://discord.gg/WJ9XF9czVT) diff --git a/include/tftLogger.h b/include/tftLogger.h index fe1ee4109..f384f30f2 100644 --- a/include/tftLogger.h +++ b/include/tftLogger.h @@ -59,6 +59,10 @@ class tft_logger : public BRUCE_TFT_DRIVER { bool isSleeping = false; bool logging = false; bool _logging = false; +#if defined(HAS_EINK) + bool einkDirty = false; + uint32_t lastEinkFlushMs = 0; +#endif void clearLog(); bool async_serial = false; TaskHandle_t asyncSerialTask = NULL; @@ -85,6 +89,10 @@ class tft_logger : public BRUCE_TFT_DRIVER { return textbgcolor; #endif } + String sanitizeText(const String &s) const; + + void markDirty(); + static uint16_t mapColor(uint32_t color); public: tft_logger(int16_t w = TFT_WIDTH, int16_t h = TFT_HEIGHT); @@ -102,10 +110,14 @@ class tft_logger : public BRUCE_TFT_DRIVER { void removeOverlappedImages(int x, int y, int center, int ms); void fillScreen(int32_t color); + void drawPixel(int32_t x, int32_t y, int32_t color); + void setTextColor(uint16_t fg); + void setTextColor(uint16_t fg, uint16_t bg); void startAsyncSerial(); void stopAsyncSerial(); void getTftInfo(); void imageToBin(uint8_t fs, String file, int x, int y, bool center, int Ms); + bool flushEinkIfDirty(uint32_t minIntervalMs = 250); void drawLine(int32_t x, int32_t y, int32_t x1, int32_t y1, int32_t color); void drawRect(int32_t x, int32_t y, int32_t w, int32_t h, int32_t color); diff --git a/lib/HAL/display/m5gfx.cpp b/lib/HAL/display/m5gfx.cpp index 9874e18ad..5f9c2edfa 100644 --- a/lib/HAL/display/m5gfx.cpp +++ b/lib/HAL/display/m5gfx.cpp @@ -7,9 +7,9 @@ #include "freertos/semphr.h" #include "freertos/task.h" // static SemaphoreHandle_t tftMutex; - // #define RUN_ON_MUTEX(fn) \ +// #define RUN_ON_MUTEX(fn) \ // xSemaphoreTakeRecursive(tftMutex, portMAX_DELAY); \ -// fn; \ + // fn; \ xSemaphoreGiveRecursive(tftMutex); #define RUN_ON_MUTEX(fn) fn; @@ -22,6 +22,9 @@ void tft_display::init(uint8_t tc) { (void)tc; M5.begin(); } +void tft_display::display() { M5.Display.display(); } + +void tft_display::setAutoDisplay(bool enabled) { M5.Display.setAutoDisplay(enabled); } void tft_display::setRotation(uint8_t r) { M5.Display.setRotation(r); @@ -29,79 +32,79 @@ void tft_display::setRotation(uint8_t r) { } void tft_display::drawPixel(int32_t x, int32_t y, uint32_t color) { - RUN_ON_MUTEX(M5.Display.drawPixel(x, y, (uint16_t)color)); + RUN_ON_MUTEX(M5.Display.drawPixel(x, y, color)); } void tft_display::drawLine(int32_t x0, int32_t y0, int32_t x1, int32_t y1, uint32_t color) { - RUN_ON_MUTEX(M5.Display.drawLine(x0, y0, x1, y1, (uint16_t)color)); + RUN_ON_MUTEX(M5.Display.drawLine(x0, y0, x1, y1, color)); } void tft_display::drawFastHLine(int32_t x, int32_t y, int32_t w, uint32_t color) { - RUN_ON_MUTEX(M5.Display.drawFastHLine(x, y, w, (uint16_t)color)); + RUN_ON_MUTEX(M5.Display.drawFastHLine(x, y, w, color)); } void tft_display::drawFastVLine(int32_t x, int32_t y, int32_t h, uint32_t color) { - RUN_ON_MUTEX(M5.Display.drawFastVLine(x, y, h, (uint16_t)color)); + RUN_ON_MUTEX(M5.Display.drawFastVLine(x, y, h, color)); } void tft_display::drawRect(int32_t x, int32_t y, int32_t w, int32_t h, uint32_t color) { - RUN_ON_MUTEX(M5.Display.drawRect(x, y, w, h, (uint16_t)color)); + RUN_ON_MUTEX(M5.Display.drawRect(x, y, w, h, color)); } void tft_display::fillRect(int32_t x, int32_t y, int32_t w, int32_t h, uint32_t color) { - RUN_ON_MUTEX(M5.Display.fillRect(x, y, w, h, (uint16_t)color)); + RUN_ON_MUTEX(M5.Display.fillRect(x, y, w, h, color)); } void tft_display::fillRectHGradient( int16_t x, int16_t y, int16_t w, int16_t h, uint32_t color1, uint32_t color2 ) { (void)color2; - fillRect(x, y, w, h, (uint16_t)color1); + fillRect(x, y, w, h, color1); } void tft_display::fillRectVGradient( int16_t x, int16_t y, int16_t w, int16_t h, uint32_t color1, uint32_t color2 ) { (void)color2; - fillRect(x, y, w, h, (uint16_t)color1); + fillRect(x, y, w, h, color1); } -void tft_display::fillScreen(uint32_t color) { RUN_ON_MUTEX(M5.Display.fillScreen((uint16_t)color)); } +void tft_display::fillScreen(uint32_t color) { RUN_ON_MUTEX(M5.Display.fillScreen(color)); } void tft_display::drawRoundRect(int32_t x, int32_t y, int32_t w, int32_t h, int32_t r, uint32_t color) { - RUN_ON_MUTEX(M5.Display.drawRoundRect(x, y, w, h, r, (uint16_t)color)); + RUN_ON_MUTEX(M5.Display.drawRoundRect(x, y, w, h, r, color)); } void tft_display::fillRoundRect(int32_t x, int32_t y, int32_t w, int32_t h, int32_t r, uint32_t color) { - RUN_ON_MUTEX(M5.Display.fillRoundRect(x, y, w, h, r, (uint16_t)color)); + RUN_ON_MUTEX(M5.Display.fillRoundRect(x, y, w, h, r, color)); } void tft_display::drawCircle(int32_t x0, int32_t y0, int32_t r, uint32_t color) { - RUN_ON_MUTEX(M5.Display.drawCircle(x0, y0, r, (uint16_t)color)); + RUN_ON_MUTEX(M5.Display.drawCircle(x0, y0, r, color)); } void tft_display::fillCircle(int32_t x0, int32_t y0, int32_t r, uint32_t color) { - RUN_ON_MUTEX(M5.Display.fillCircle(x0, y0, r, (uint16_t)color)); + RUN_ON_MUTEX(M5.Display.fillCircle(x0, y0, r, color)); } void tft_display::drawTriangle( int32_t x0, int32_t y0, int32_t x1, int32_t y1, int32_t x2, int32_t y2, uint32_t color ) { - RUN_ON_MUTEX(M5.Display.drawTriangle(x0, y0, x1, y1, x2, y2, (uint16_t)color)); + RUN_ON_MUTEX(M5.Display.drawTriangle(x0, y0, x1, y1, x2, y2, color)); } void tft_display::fillTriangle( int32_t x0, int32_t y0, int32_t x1, int32_t y1, int32_t x2, int32_t y2, uint32_t color ) { - RUN_ON_MUTEX(M5.Display.fillTriangle(x0, y0, x1, y1, x2, y2, (uint16_t)color)); + RUN_ON_MUTEX(M5.Display.fillTriangle(x0, y0, x1, y1, x2, y2, color)); } void tft_display::drawEllipse(int16_t x0, int16_t y0, int32_t rx, int32_t ry, uint16_t color) { - RUN_ON_MUTEX(M5.Display.drawEllipse(x0, y0, rx, ry, (uint16_t)color)); + RUN_ON_MUTEX(M5.Display.drawEllipse(x0, y0, rx, ry, color)); } void tft_display::fillEllipse(int16_t x0, int16_t y0, int32_t rx, int32_t ry, uint16_t color) { - RUN_ON_MUTEX(M5.Display.fillEllipse(x0, y0, rx, ry, (uint16_t)color)); + RUN_ON_MUTEX(M5.Display.fillEllipse(x0, y0, rx, ry, color)); } void tft_display::drawArc( @@ -110,36 +113,44 @@ void tft_display::drawArc( ) { (void)bg_color; (void)smoothArc; - RUN_ON_MUTEX(M5.Display.fillArc(x, y, r, ir, startAngle + 90, endAngle + 90, (uint16_t)fg_color)); + RUN_ON_MUTEX(M5.Display.fillArc(x, y, r, ir, startAngle + 90, endAngle + 90, fg_color)); } void tft_display::drawWideLine( float ax, float ay, float bx, float by, float wd, uint32_t fg_color, uint32_t bg_color ) { (void)bg_color; - RUN_ON_MUTEX(M5.Display.drawWideLine(ax, ay, bx, by, wd, (uint16_t)fg_color)); + RUN_ON_MUTEX(M5.Display.drawWideLine(ax, ay, bx, by, wd, fg_color)); } void tft_display::drawXBitmap( int16_t x, int16_t y, const uint8_t *bitmap, int16_t w, int16_t h, uint16_t color ) { if (!bitmap) return; - RUN_ON_MUTEX(M5.Display.drawXBitmap(x, y, bitmap, w, h, (uint16_t)color)); + RUN_ON_MUTEX(M5.Display.drawXBitmap(x, y, bitmap, w, h, color)); } void tft_display::drawXBitmap( int16_t x, int16_t y, const uint8_t *bitmap, int16_t w, int16_t h, uint16_t color, uint16_t bg ) { if (!bitmap) return; - RUN_ON_MUTEX(M5.Display.drawXBitmap(x, y, bitmap, w, h, (uint16_t)color, (uint16_t)bg)); + RUN_ON_MUTEX(M5.Display.drawXBitmap(x, y, bitmap, w, h, color, bg)); } void tft_display::pushImage(int32_t x, int32_t y, int32_t w, int32_t h, const uint16_t *data) { +#if defined(HAS_EINK) + pushImageFallback(x, y, w, h, data); +#else RUN_ON_MUTEX(M5.Display.pushImage(x, y, w, h, data)); +#endif } void tft_display::pushImage(int32_t x, int32_t y, int32_t w, int32_t h, uint16_t *data) { +#if defined(HAS_EINK) + pushImageFallback(x, y, w, h, data); +#else RUN_ON_MUTEX(M5.Display.pushImage(x, y, w, h, data)); +#endif } void tft_display::pushImage( @@ -200,14 +211,14 @@ void tft_display::setTextSize(uint8_t s) { void tft_display::setTextColor(uint16_t c) { _textColor = c; - M5.Display.setTextColor((uint16_t)c); + M5.Display.setTextColor(static_cast(_textColor)); } void tft_display::setTextColor(uint16_t c, uint16_t b, bool bgfill) { (void)bgfill; _textColor = c; _textBgColor = b; - M5.Display.setTextColor((uint16_t)c, (uint16_t)b); + M5.Display.setTextColor(static_cast(_textColor), static_cast(_textBgColor)); } void tft_display::setTextDatum(uint8_t d) { _textDatum = d; } @@ -330,6 +341,8 @@ int16_t tft_display::drawAlignedString(const String &s, int32_t x, int32_t y, ui return w; } +uint16_t tft_display::normalizeColor(uint32_t color) const { return static_cast(color); } + tft_sprite::tft_sprite(tft_display *parent) : lgfx::LGFX_Sprite(parent ? &M5.Display : nullptr) {} void *tft_sprite::createSprite(int16_t w, int16_t h, uint8_t frames) { @@ -341,36 +354,36 @@ void tft_sprite::deleteSprite() { lgfx::LGFX_Sprite::deleteSprite(); } void tft_sprite::setColorDepth(uint8_t depth) { lgfx::LGFX_Sprite::setColorDepth(depth); } -void tft_sprite::fillScreen(uint32_t color) { lgfx::LGFX_Sprite::fillScreen((uint16_t)color); } +void tft_sprite::fillScreen(uint32_t color) { lgfx::LGFX_Sprite::fillScreen(color); } void tft_sprite::fillRect(int32_t x, int32_t y, int32_t w, int32_t h, uint32_t color) { - lgfx::LGFX_Sprite::fillRect(x, y, w, h, (uint16_t)color); + lgfx::LGFX_Sprite::fillRect(x, y, w, h, color); } void tft_sprite::fillCircle(int32_t x, int32_t y, int32_t r, uint32_t color) { - lgfx::LGFX_Sprite::fillCircle(x, y, r, (uint16_t)color); + lgfx::LGFX_Sprite::fillCircle(x, y, r, color); } void tft_sprite::fillEllipse(int16_t x, int16_t y, int32_t rx, int32_t ry, uint16_t color) { - lgfx::LGFX_Sprite::fillEllipse(x, y, rx, ry, (uint16_t)color); + lgfx::LGFX_Sprite::fillEllipse(x, y, rx, ry, color); } void tft_sprite::fillTriangle( int32_t x0, int32_t y0, int32_t x1, int32_t y1, int32_t x2, int32_t y2, uint32_t color ) { - lgfx::LGFX_Sprite::fillTriangle(x0, y0, x1, y1, x2, y2, (uint16_t)color); + lgfx::LGFX_Sprite::fillTriangle(x0, y0, x1, y1, x2, y2, color); } void tft_sprite::drawFastVLine(int32_t x, int32_t y, int32_t h, uint32_t color) { - lgfx::LGFX_Sprite::drawFastVLine(x, y, h, (uint16_t)color); + lgfx::LGFX_Sprite::drawFastVLine(x, y, h, color); } void tft_sprite::pushSprite(int32_t x, int32_t y, uint32_t transparent) { - RUN_ON_MUTEX(lgfx::LGFX_Sprite::pushSprite(x, y, (uint16_t)transparent)); + RUN_ON_MUTEX(lgfx::LGFX_Sprite::pushSprite(x, y, transparent)); } void tft_sprite::pushToSprite(tft_sprite *dest, int32_t x, int32_t y, uint32_t transparent) { - lgfx::LGFX_Sprite::pushSprite(static_cast(dest), x, y, (uint16_t)transparent); + lgfx::LGFX_Sprite::pushSprite(static_cast(dest), x, y, transparent); } void tft_sprite::pushImage( @@ -378,7 +391,9 @@ void tft_sprite::pushImage( ) { if (!data || !bpp8 || !cmap) return; for (int32_t row = 0; row < h; ++row) { - for (int32_t col = 0; col < w; ++col) { drawPixel(x + col, y + row, cmap[data[row * w + col]]); } + for (int32_t col = 0; col < w; ++col) { + drawPixel(x + col, y + row, cmap[data[row * w + col]]); + } } } @@ -392,13 +407,13 @@ void tft_sprite::fillRectHGradient( int16_t x, int16_t y, int16_t w, int16_t h, uint32_t color1, uint32_t color2 ) { (void)color2; - lgfx::LGFX_Sprite::fillRect(x, y, w, h, (uint16_t)color1); + lgfx::LGFX_Sprite::fillRect(x, y, w, h, color1); } void tft_sprite::fillRectVGradient( int16_t x, int16_t y, int16_t w, int16_t h, uint32_t color1, uint32_t color2 ) { - lgfx::LGFX_Sprite::fillRect(x, y, w, h, (uint16_t)color1); + lgfx::LGFX_Sprite::fillRect(x, y, w, h, color1); } int16_t tft_sprite::width() const { return static_cast(lgfx::LGFX_Sprite::width()); } diff --git a/lib/HAL/display/m5gfx.h b/lib/HAL/display/m5gfx.h index 80ae77e54..97ef26864 100644 --- a/lib/HAL/display/m5gfx.h +++ b/lib/HAL/display/m5gfx.h @@ -90,6 +90,9 @@ class tft_display { SPIClass &getSPIinstance() const; void writecommand(uint8_t c); + void display(); + void setAutoDisplay(bool enabled); + uint32_t getTextColor() const; uint32_t getTextBgColor() const; uint8_t getTextSize() const; @@ -102,7 +105,7 @@ class tft_display { if (!data) return; for (int32_t row = 0; row < h; ++row) { for (int32_t col = 0; col < w; ++col) { - uint16_t color = data[row * w + col]; + uint16_t color = normalizeColor(data[row * w + col]); if (_swapBytes) color = static_cast((color >> 8) | (color << 8)); M5.Display.drawPixel(x + col, y + row, color); } @@ -110,6 +113,7 @@ class tft_display { } int16_t drawAlignedString(const String &s, int32_t x, int32_t y, uint8_t datum); + uint16_t normalizeColor(uint32_t color) const; uint16_t _height = TFT_HEIGHT; uint16_t _width = TFT_WIDTH; diff --git a/sd_files/themes/README.md b/sd_files/themes/README.md deleted file mode 100644 index 67b7b1a92..000000000 --- a/sd_files/themes/README.md +++ /dev/null @@ -1,66 +0,0 @@ -# THEME pack - -Themes are supposed to change the UI experience of the User by allowing you to choose: -* Different sets of images of the Main Menu -* If the UI will display borders or not -* If displaying the Labels on the Main Menu -* Setting the Primary Font color -* Setting the Secondary Font Color (Non selected submenu item) -* Setting the Background Color -* Setting the LED color and effects - -## Images -Bruce accepts **.bmp** **.jpg** **.gif** and **.png** (not available on LITE_VERSION) to be used in the Main menu, but the bigger they are, greater will be the time to draw them on screen, **please, try to keep them small**. - -The recommended height of the images is: - -| Device | Display size | Height | -| --- | --- | --- | -| T-Embed | 320x170 | 140px | -| Cardputer StickCPlus | 240x135 | 105px | -| Core / CYD | 320x240 | 180 | - -## Theme file -Theme settings are stored in a **.json** file, following this structure: -``` -{ - "wifi":"wifi.png", - "ble":"ble.png", - "rf":"rf.png", - "rfid":"rfid.png", - "fm":"fm.png", - "ir":"ir.png", - "files":"files.png", - "gps":"gps.png", - "nrf":"nrf.png", - "interpreter":"interpreter.png", - "clock":"clock.png", - "others":"others.png", - "connect":"connect.png", - "config":"config.png", - "priColor":"ffff", - "secColor":"aaaa", - "bgColor":"0", - "border":0, - "label":0, - "boot_img":"boot.gif", - "boot_sound":"boot.wav", - "ledBright": 100, - "ledColor": "960064", - "ledEffect": 0, - "ledEffectSpeed": 3, - "ledEffectDirection": 1 -} -``` -* Colors (excluding LED): Codes are in **RGB565**, 16bit pattern, so you need to convert it using [this tool](https://rgbcolorpicker.com/565). -* LED Colors: Codes are in HEX. -* border: 0 or 1, 1 to keep UI Borders -* label: 0 or 1, 1 to use UI Labels (Use 50px smaller images in this case) - -## Theme creator -You can use [Bruce Theme Builder](https://bruce.computer/build_theme.html) to setup and prepare your images, settings and .json file, it will give you a .zip file that you **need to unzip** somewhere in your device, LittleFS or SD Card. - -## Setting a Theme -Config > UI Theme > (Choose FS) > select the .json file and the theme will be set. - - diff --git a/sd_files/themes/Theme_Builder.html b/sd_files/themes/Theme_Builder.html deleted file mode 100644 index 62053ccb5..000000000 --- a/sd_files/themes/Theme_Builder.html +++ /dev/null @@ -1,898 +0,0 @@ - - - - - - Bruce Theme Builder - - - -
-
-

Bruce Theme Builder

-
- -
-
Primary Color | Secondary Color
-
-
- -
-
-
- - -
-
- - -
0.8
-
-
- -
-
-
- - -
-
- - -
-
- - -
-
-
- - -
-
- - -
-
-
- -
-
-
- Drag multiple images here or click to select -
- -
-
-
- -
- - -
- - - - - diff --git a/sd_files/themes/example/Pirata_BW_jpg.zip b/sd_files/themes/example/Pirata_BW_jpg.zip deleted file mode 100644 index 0339364a5..000000000 Binary files a/sd_files/themes/example/Pirata_BW_jpg.zip and /dev/null differ diff --git a/sd_files/themes/example/Pirata_png.zip b/sd_files/themes/example/Pirata_png.zip deleted file mode 100644 index 462b1c3e1..000000000 Binary files a/sd_files/themes/example/Pirata_png.zip and /dev/null differ diff --git a/sd_files/themes/example/README.md b/sd_files/themes/example/README.md deleted file mode 100644 index b45e72304..000000000 --- a/sd_files/themes/example/README.md +++ /dev/null @@ -1,6 +0,0 @@ -## Instructions - -* Download the .zip file -* Unzip it into somewhere -* Copy the folder into LittleFS (WebUI) or into your SD Card -* Config > UI Theme > (Find the .json file) diff --git a/sd_files/themes/example/Sharky.zip b/sd_files/themes/example/Sharky.zip deleted file mode 100644 index 1786001d7..000000000 Binary files a/sd_files/themes/example/Sharky.zip and /dev/null differ diff --git a/src/core/config.cpp b/src/core/config.cpp index 81b918b09..310d47d63 100644 --- a/src/core/config.cpp +++ b/src/core/config.cpp @@ -14,6 +14,7 @@ JsonDocument BruceConfig::toJson() const { setting["dimmerSet"] = dimmerSet; setting["bright"] = bright; + setting["einkRefreshMs"] = einkRefreshMs; setting["automaticTimeUpdateViaNTP"] = automaticTimeUpdateViaNTP; setting["tmz"] = tmz; setting["dst"] = dst; @@ -160,6 +161,12 @@ void BruceConfig::fromFile(bool checkFS) { count++; log_e("Fail"); } + if (!setting["einkRefreshMs"].isNull()) { + einkRefreshMs = setting["einkRefreshMs"].as(); + } else { + count++; + log_e("Fail"); + } if (!setting["automaticTimeUpdateViaNTP"].isNull()) { automaticTimeUpdateViaNTP = setting["automaticTimeUpdateViaNTP"].as(); } else { @@ -444,6 +451,7 @@ void BruceConfig::factoryReset() { void BruceConfig::validateConfig() { validateDimmerValue(); validateBrightValue(); + validateEinkRefreshMs(); validateTmzValue(); validateSoundEnabledValue(); validateSoundVolumeValue(); @@ -464,6 +472,12 @@ void BruceConfig::validateConfig() { validateEvilEndpointCreds(); validateEvilEndpointSsid(); validateEvilPasswordMode(); +#if defined(HAS_EINK) + priColor = 0xFFFF; + secColor = 0xFFFF; + bgColor = 0x0000; + colorInverted = 0; +#endif } void BruceConfig::setUiColor(uint16_t primary, uint16_t *secondary, uint16_t *background) { @@ -492,6 +506,17 @@ void BruceConfig::validateBrightValue() { if (bright > 100) bright = 100; } +void BruceConfig::setEinkRefreshMs(int value) { + einkRefreshMs = value; + validateEinkRefreshMs(); + saveFile(); +} + +void BruceConfig::validateEinkRefreshMs() { + if (einkRefreshMs < 0) einkRefreshMs = 0; + if (einkRefreshMs > 600000) einkRefreshMs = 600000; +} + void BruceConfig::setAutomaticTimeUpdateViaNTP(bool value) { automaticTimeUpdateViaNTP = value; saveFile(); @@ -735,6 +760,7 @@ void BruceConfig::setColorInverted(int value) { void BruceConfig::validateColorInverted() { if (colorInverted > 1) colorInverted = 1; + if (colorInverted < 0) colorInverted = 0; } void BruceConfig::setBadUSBBLEKeyboardLayout(int value) { diff --git a/src/core/config.h b/src/core/config.h index 04fa143dc..101579dd4 100644 --- a/src/core/config.h +++ b/src/core/config.h @@ -38,6 +38,7 @@ class BruceConfig : public BruceTheme { // Settings int dimmerSet = 10; int bright = 100; + int einkRefreshMs = 15000; bool automaticTimeUpdateViaNTP = true; float tmz = 0; bool dst = false; @@ -119,6 +120,8 @@ class BruceConfig : public BruceTheme { void validateDimmerValue(); void setBright(uint8_t value); void validateBrightValue(); + void setEinkRefreshMs(int value); + void validateEinkRefreshMs(); void setAutomaticTimeUpdateViaNTP(bool value); void setTmz(float value); void validateTmzValue(); diff --git a/src/core/display.cpp b/src/core/display.cpp index 8c14fadfa..4768d2a5b 100644 --- a/src/core/display.cpp +++ b/src/core/display.cpp @@ -26,11 +26,27 @@ void panelSleep(bool on) { } bool __attribute__((weak)) isCharging() { return false; } + +void einkFlushIfDirty(uint32_t minIntervalMs) { +#if defined(HAS_EINK) + uint32_t interval = minIntervalMs; + if (minIntervalMs == 0xFFFFFFFFu) { + if (bruceConfig.einkRefreshMs == 0) return; + interval = static_cast(bruceConfig.einkRefreshMs); + } + tft.flushEinkIfDirty(interval); +#endif +} /*************************************************************************************** ** Function name: displayScrollingText ** Description: Scroll large texts into screen ***************************************************************************************/ void displayScrollingText(const String &text, Opt_Coord &coord) { +#if defined(HAS_EINK) + (void)text; + (void)coord; + return; +#endif int len = text.length(); String displayText = text + " "; // Add spaces for smooth looping int scrollLen = len + 8; // Full text plus space buffer @@ -115,14 +131,6 @@ bool wakeUpScreen() { previousMillis = millis(); if (isScreenOff) { isScreenOff = false; - dimmer = false; - getBrightness(); - vTaskDelay(pdMS_TO_TICKS(200)); - return true; - } else if (dimmer) { - dimmer = false; - getBrightness(); - vTaskDelay(pdMS_TO_TICKS(200)); return true; } return false; @@ -156,6 +164,7 @@ void displayRedStripe(String text, uint16_t fgcolor, uint16_t bgcolor) { tft.drawCentreString(text.substring(text_size / 2), tftWidth / 2, tftHeight / 2 + 1); } } + einkFlushIfDirty(0); } void drawButton( @@ -251,6 +260,7 @@ int8_t displayMessage( ); } redraw = false; + einkFlushIfDirty(0); } delay(10); @@ -539,6 +549,8 @@ int loopOptions( displayScrollingText(txt, coord); } + einkFlushIfDirty(0); + // Checks ESC Press first, to not exit after PrevPress is processed // PrevPress condition is a StickCPlus workaround, as it uses the same button for Prev and Esc // Same happens to Core and some other boards @@ -560,6 +572,7 @@ int loopOptions( #ifndef HAS_ENCODER // T-Embed doesn't need it LongPress = true; while (PrevPress && menuType != MENU_TYPE_MAIN) { +#if !defined(HAS_EINK) if (millis() - _tmp > 200) tft.drawArc( tftWidth / 2, @@ -571,11 +584,14 @@ int loopOptions( getColorVariation(bruceConfig.priColor), bruceConfig.bgColor ); +#endif vTaskDelay(10 / portTICK_RATE_MS); } +#if !defined(HAS_EINK) tft.drawArc( tftWidth / 2, tftHeight / 2, 25, 15, 0, 360, bruceConfig.bgColor, bruceConfig.bgColor ); +#endif LongPress = false; #endif if (millis() - _tmp > 700) { // longpress detected to exit @@ -808,13 +824,20 @@ void drawStatusBar() { if (clock_set) { int clock_fontsize = 1; // Font size of the clock / BRUCE + BRUCE_VERSION - setTftDisplay(12, 12, bruceConfig.priColor, clock_fontsize, bruceConfig.bgColor); - tft.fillRect(12, 12, 100, clock_fontsize * LH, bruceConfig.bgColor); + int clock_x = 12; + int clock_y = 12; +#if defined(HAS_EINK) + clock_fontsize = 2; + clock_y = 8; +#endif + setTftDisplay(clock_x, clock_y, bruceConfig.priColor, clock_fontsize, bruceConfig.bgColor); #if defined(HAS_RTC) updateTimeStr(_rtc.getTimeStruct()); #else updateTimeStr(rtc.getTimeStruct()); #endif + int clock_width = tft.textWidth(timeStr); + tft.fillRect(clock_x, clock_y, clock_width + 4, clock_fontsize * LH, bruceConfig.bgColor); tft.print(timeStr); } else { setTftDisplay(12, 12, bruceConfig.priColor, 1, bruceConfig.bgColor); diff --git a/src/core/display.h b/src/core/display.h index b7c1b1907..54dc64c2d 100644 --- a/src/core/display.h +++ b/src/core/display.h @@ -190,6 +190,7 @@ void printTitle(String title); void printSubtitle(String subtitle, bool withLine = true); void printFootnote(String text); void printCenterFootnote(String text); +void einkFlushIfDirty(uint32_t minIntervalMs = 0xFFFFFFFFu); Opt_Coord listFiles(int index, std::vector fileList); diff --git a/src/core/menu_items/ConfigMenu.cpp b/src/core/menu_items/ConfigMenu.cpp index 53cdf25e9..7d0ae09d9 100644 --- a/src/core/menu_items/ConfigMenu.cpp +++ b/src/core/menu_items/ConfigMenu.cpp @@ -59,11 +59,17 @@ void ConfigMenu::optionsMenu() { void ConfigMenu::displayUIMenu() { while (true) { std::vector