Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/helpers/esp32/SerialBLEInterface.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#include "SerialBLEInterface.h"
#include "esp_mac.h"
#include <esp_mac.h>

// See the following for generating UUIDs:
// https://www.uuidgenerator.net/
Expand Down
63 changes: 57 additions & 6 deletions src/helpers/ui/DisplayDriver.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,72 @@
#include <stdint.h>
#include <string.h>

// MarqueeScroller: Handles text scrolling for small displays (e.g., 64x48 OLED)
// Used by companion radio UI to display long messages that don't fit on screen
struct MarqueeScroller {
int offset = 0; // Current scroll offset in pixels
unsigned long next_time = 0; // Next scroll update time (millis)
bool paused = true; // Scroll paused state

// Reset scroller to initial state
void reset() {
offset = 0;
next_time = 0;
paused = true;
}

// Update scroll position - call once per loop iteration
// Parameters:
// textW - width of text in pixels
// displayW - width of display in pixels
// now - current time from millis()
// speed_ms - milliseconds between scroll steps (default: 150ms)
// pause_ms - milliseconds to pause before scrolling starts (default: 2000ms)
void update(int textW, int displayW, unsigned long now,
unsigned long speed_ms = 150, unsigned long pause_ms = 2000) {
int maxScroll = textW - displayW;
if (maxScroll <= 0) { offset = 0; return; }
if (now < next_time) return;
if (paused) {
paused = false;
next_time = now + pause_ms;
} else {
offset++;
if (offset >= maxScroll) {
offset = 0;
paused = true;
}
next_time = now + speed_ms;
}
}
};

// UIColor framework: Provides consistent color definitions across different display types
// Each display driver defines its own UIColor values based on its color capabilities
// For monochrome displays: 0 = BLACK, 1 = WHITE
// For color displays: 16-bit RGB values
using ColorVal = uint16_t;

class UIColor {
public:
// color definitions (by element _type_)
static ColorVal window_bkg, title_bkg, title_txt, primary_txt, secondary_txt, warning_txt, popup_bkg, popup_txt, corp_blue;
// UI element color definitions
// Each display driver implements these with appropriate values for its display
static ColorVal window_bkg; // Background color for windows/panels
static ColorVal title_bkg; // Background color for title bars
static ColorVal title_txt; // Text color for titles
static ColorVal primary_txt; // Primary text color (main content)
static ColorVal secondary_txt; // Secondary text color (metadata, timestamps)
static ColorVal warning_txt; // Warning/alert text color
static ColorVal popup_bkg; // Background color for popups/dialogs
static ColorVal popup_txt; // Text color for popups/dialogs
static ColorVal corp_blue; // Corporate/brand color (blue)
};

class DisplayDriver {
int _w, _h;
protected:
DisplayDriver(int w, int h) { _w = w; _h = h; }
public:
//enum Color { DARK=0, LIGHT, RED, GREEN, BLUE, YELLOW, ORANGE }; // on b/w screen, colors will be !=0 synonym of light

int width() const { return _w; }
int height() const { return _h; }

Expand Down Expand Up @@ -59,7 +110,7 @@ class DisplayDriver {
if (c >= 32 && c <= 126) {
dest[j++] = c; // ASCII printable
} else if (c >= 0x80) {
dest[j++] = '\xDB'; // CP437 full block
dest[j++] = '\xDB'; // CP437 full block
while (src[i+1] && (src[i+1] & 0xC0) == 0x80)
i++; // skip UTF-8 continuation bytes
}
Expand Down Expand Up @@ -106,4 +157,4 @@ class DisplayDriver {
}

virtual void endFrame() = 0;
};
};
131 changes: 131 additions & 0 deletions src/helpers/ui/SSD1306SPIDisplay.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
#include "SSD1306SPIDisplay.h"

// UIColor definitions for 64x48 OLED (black/white display)
ColorVal UIColor::window_bkg = 0; // BLACK
ColorVal UIColor::title_bkg = 0; // BLACK
ColorVal UIColor::title_txt = 1; // WHITE
ColorVal UIColor::primary_txt = 1; // WHITE
ColorVal UIColor::secondary_txt = 1; // WHITE
ColorVal UIColor::warning_txt = 1; // WHITE
ColorVal UIColor::popup_bkg = 0; // BLACK
ColorVal UIColor::popup_txt = 1; // WHITE
ColorVal UIColor::corp_blue = 1; // WHITE (B/W display)

// Check if SPI is ready (set by radio_init in target.cpp)
#if defined(P_LORA_SCLK)
extern bool spi_initialized;
#else
static bool spi_initialized = true; // Assume ready if no custom SPI
#endif

bool SSD1306SPIDisplay::begin() {
// Defer actual initialization - SPI may not be ready yet
// Real init happens in lazyInit() on first use (after radio_init)
return true;
}

bool SSD1306SPIDisplay::lazyInit() {
if (_initialized) return true;
if (!spi_initialized) {
Serial.println("SSD1306: SPI not initialized yet");
return false;
}

Serial.println("SSD1306: Attempting display init...");
#ifdef DISPLAY_ROTATION
display.setRotation(DISPLAY_ROTATION);
#endif
// SPI is now initialized by radio_init()
// Pass periphBegin=false to skip spi.begin() since radio already did it
if (!display.begin(SSD1306_SWITCHCAPVCC, 0, true, false)) {
Serial.println("SSD1306: display.begin() FAILED");
return false;
}
Serial.println("SSD1306: display.begin() OK");

// Fix for 64x48 displays: Adafruit library lacks this case and defaults
// to comPins=0x02 (sequential). Displays taller than 32px need 0x12
// (alternative COM pin config) or the output is garbled.
#if defined(DISPLAY_WIDTH) && defined(DISPLAY_HEIGHT)
#if (DISPLAY_WIDTH == 64) && (DISPLAY_HEIGHT == 48)
display.ssd1306_command(SSD1306_SETCOMPINS);
display.ssd1306_command(0x12);
#endif
#endif

// Clear any garbage in the display buffer
display.clearDisplay();
display.display();
_initialized = true;
return true;
}

void SSD1306SPIDisplay::turnOn() {
if (!lazyInit()) return;
display.ssd1306_command(SSD1306_DISPLAYON);
_isOn = true;
}

void SSD1306SPIDisplay::turnOff() {
if (!lazyInit()) return;
display.ssd1306_command(SSD1306_DISPLAYOFF);
_isOn = false;
}

void SSD1306SPIDisplay::clear() {
if (!lazyInit()) return;
display.clearDisplay();
display.display();
}

void SSD1306SPIDisplay::startFrame(ColorVal bkg) {
if (!lazyInit()) return;
display.clearDisplay(); // TODO: apply 'bkg'
_color = 1; // WHITE
display.setTextColor(_color);
display.setFont(NULL); // Default 6x8 font
display.setTextSize(1);
display.setTextWrap(false);
display.cp437(true);
}

void SSD1306SPIDisplay::setTextSize(int sz) {
display.setTextSize(sz);
}

void SSD1306SPIDisplay::setColor(ColorVal c) {
_color = (c != 0) ? 1 : 0; // WHITE or BLACK
display.setTextColor(_color);
}

void SSD1306SPIDisplay::setCursor(int x, int y) {
display.setCursor(x, y);
}

void SSD1306SPIDisplay::print(const char* str) {
display.print(str);
}

void SSD1306SPIDisplay::fillRect(int x, int y, int w, int h) {
display.fillRect(x, y, w, h, _color);
}

void SSD1306SPIDisplay::drawRect(int x, int y, int w, int h) {
display.drawRect(x, y, w, h, _color);
}

void SSD1306SPIDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) {
display.drawBitmap(x, y, bits, w, h, SSD1306_WHITE);
}

uint16_t SSD1306SPIDisplay::getTextWidth(const char* str) {
int16_t x1, y1;
uint16_t w, h;
display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h);
return w;
}

void SSD1306SPIDisplay::endFrame() {
if (!_initialized) return;
display.display();
}
38 changes: 38 additions & 0 deletions src/helpers/ui/SSD1306SPIDisplay.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#pragma once

#include "DisplayDriver.h"
#include <SPI.h>
#include <Adafruit_GFX.h>
#define SSD1306_NO_SPLASH
#include <Adafruit_SSD1306.h>

class SSD1306SPIDisplay : public DisplayDriver {
Adafruit_SSD1306 display;
bool _isOn;
bool _initialized;
uint8_t _color;

bool lazyInit(); // Deferred init for SPI bus sharing

public:
// Accept pre-initialized SPI - do NOT call spi.begin()
SSD1306SPIDisplay(SPIClass* spi, int16_t w, int16_t h, int8_t dc, int8_t rst, int8_t cs)
: DisplayDriver(w, h), display(w, h, spi, dc, rst, cs) { _isOn = false; _initialized = false; }

bool begin();

bool isOn() override { return _isOn; }
void turnOn() override;
void turnOff() override;
void clear() override;
void startFrame(ColorVal bkg = UIColor::window_bkg) override;
void setTextSize(int sz) override;
void setColor(ColorVal c) override;
void setCursor(int x, int y) override;
void print(const char* str) override;
void fillRect(int x, int y, int w, int h) override;
void drawRect(int x, int y, int w, int h) override;
void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override;
uint16_t getTextWidth(const char* str) override;
void endFrame() override;
};
54 changes: 54 additions & 0 deletions variants/m5stack_unit_c6l/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# M5Stack Unit C6L - MeshCore Variant

## Overview

Compact ESP32-C6 LoRa module with integrated SX1262 radio, 64x48 OLED display, and PI4IO I/O expander.

## Hardware

- **MCU:** ESP32-C6 (WiFi 6 + Bluetooth 5 LE)
- **LoRa:** SX1262 (SPI)
- **Display:** 64x48 SSD1306 OLED (SPI)
- **I/O Expander:** PI4IO (I2C)
- **LED:** NeoPixel (GPIO 2)
- **Buzzer:** GPIO 11

## Pin Configuration

| Function | GPIO |
|----------|------|
| LoRa SCLK | 20 |
| LoRa MISO | 22 |
| LoRa MOSI | 21 |
| LoRa NSS | 23 |
| LoRa DIO1 | 7 |
| Display CS | 6 |
| Display DC | 18 |
| Display RST | 15 |
| I2C SDA | 10 |
| I2C SCL | 8 |
| NeoPixel | 2 |
| Buzzer | 11 |
| GPS RX | 4 |
| GPS TX | 5 |

## Firmware Environments

| Environment | Interface | Use Case |
|-------------|-----------|----------|
| `m5stack_unit_c6l_companion_radio_usb` | USB Serial | Connect via USB |
| `m5stack_unit_c6l_companion_radio_ble` | BLE | Connect via Bluetooth |
| `m5stack_unit_c6l_repeater` | LoRa | Network extender |
| `m5stack_unit_c6l_room_server` | LoRa | BBS server |
| `m5stack_unit_c6l_kiss_modem` | USB Serial | KISS TNC |

## Build

```bash
pio run -e m5stack_unit_c6l_companion_radio_usb
```

## Resources

- [M5Stack Unit C6L Product Page](https://docs.m5stack.com/en/unit/Unit_C6L)
- [MeshCore Documentation](https://docs.meshcore.io)
11 changes: 8 additions & 3 deletions variants/m5stack_unit_c6l/UnitC6LBoard.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
UnitC6LBoard board;

#if defined(P_LORA_SCLK)
static SPIClass spi(0);
SPIClass spi(0);
bool spi_initialized = false;
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi);
#else
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY);
Expand All @@ -16,12 +17,17 @@ ESP32RTCClock fallback_clock;
AutoDiscoverRTCClock rtc_clock(fallback_clock);
SensorManager sensors;

#ifdef DISPLAY_CLASS
DISPLAY_CLASS display(&spi, DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_DC, DISPLAY_RST, DISPLAY_CS);
#endif

bool radio_init() {
fallback_clock.begin();
rtc_clock.begin(Wire);

#if defined(P_LORA_SCLK)
spi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI);
spi_initialized = true;
return radio.std_init(&spi);
#else
return radio.std_init();
Expand All @@ -30,6 +36,5 @@ bool radio_init() {

mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity
return mesh::LocalIdentity(&rng);
}

Loading