mirror of https://github.com/meshcore-dev/MeshCore
Browse Source
Add variant configuration for M5Stack Unit C6L (ESP32-C6, SX1262 LoRa, SSD1306 64x48 SPI display). Includes: - Board class with PI4IO I/O expander for LoRa RF control - DIO flash mode fix for ESP32-C6 bootloader compatibility - SSD1306 SPI display with custom minimal UITask for 64x48 - CustomSerialBLEInterface fix for correct BLE Connected status - All 4 firmware environments: companion (USB/BLE), repeater, room serverpull/2632/head
10 changed files with 542 additions and 71 deletions
@ -0,0 +1,120 @@ |
|||
#include "SSD1306SPIDisplay.h" |
|||
|
|||
// 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(Color bkg) { |
|||
if (!lazyInit()) return; |
|||
display.clearDisplay(); // TODO: apply 'bkg'
|
|||
_color = SSD1306_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(Color c) { |
|||
_color = (c != 0) ? SSD1306_WHITE : SSD1306_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(); |
|||
} |
|||
@ -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(Color bkg = DARK) override; |
|||
void setTextSize(int sz) override; |
|||
void setColor(Color 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; |
|||
}; |
|||
@ -1,15 +1,130 @@ |
|||
#pragma once |
|||
|
|||
#include <Arduino.h> |
|||
#include <Wire.h> |
|||
#include <helpers/ESP32Board.h> |
|||
|
|||
// PI4IO I/O Expander (I2C address 0x43)
|
|||
// Pin mapping:
|
|||
// P0 = Button (active low)
|
|||
// P1 = (unused input)
|
|||
// P5 = LNA_EN (LNA Enable)
|
|||
// P6 = ANT_SW (RF Switch)
|
|||
// P7 = NRST (LoRa Reset)
|
|||
#define PI4IO_ADDR 0x43 |
|||
|
|||
// PI4IO registers
|
|||
#define PI4IO_REG_CHIP_RESET 0x01 |
|||
#define PI4IO_REG_IO_DIR 0x03 |
|||
#define PI4IO_REG_OUT_SET 0x05 |
|||
#define PI4IO_REG_OUT_H_IM 0x07 |
|||
#define PI4IO_REG_IN_DEF_STA 0x09 |
|||
#define PI4IO_REG_PULL_EN 0x0B |
|||
#define PI4IO_REG_PULL_SEL 0x0D |
|||
#define PI4IO_REG_IN_STA 0x0F |
|||
#define PI4IO_REG_INT_MASK 0x11 |
|||
#define PI4IO_REG_IRQ_STA 0x13 |
|||
|
|||
class UnitC6LBoard : public ESP32Board { |
|||
private: |
|||
bool i2c_write_byte(uint8_t addr, uint8_t reg, uint8_t value) { |
|||
Wire.beginTransmission(addr); |
|||
Wire.write(reg); |
|||
Wire.write(value); |
|||
return Wire.endTransmission() == 0; |
|||
} |
|||
|
|||
bool i2c_read_byte(uint8_t addr, uint8_t reg, uint8_t *value) { |
|||
Wire.beginTransmission(addr); |
|||
Wire.write(reg); |
|||
if (Wire.endTransmission() != 0) return false; |
|||
if (Wire.requestFrom(addr, (uint8_t)1) != 1) return false; |
|||
if (!Wire.available()) return false; |
|||
*value = Wire.read(); |
|||
return true; |
|||
} |
|||
|
|||
void initIOExpander() { |
|||
uint8_t in_data; |
|||
|
|||
// Reset the I/O expander
|
|||
i2c_write_byte(PI4IO_ADDR, PI4IO_REG_CHIP_RESET, 0xFF); |
|||
delay(10); |
|||
|
|||
i2c_read_byte(PI4IO_ADDR, PI4IO_REG_CHIP_RESET, &in_data); |
|||
delay(10); |
|||
|
|||
// Set P5, P6, P7 as outputs (0: input, 1: output)
|
|||
i2c_write_byte(PI4IO_ADDR, PI4IO_REG_IO_DIR, 0b11100000); |
|||
delay(10); |
|||
|
|||
// Disable High-Impedance for used pins
|
|||
i2c_write_byte(PI4IO_ADDR, PI4IO_REG_OUT_H_IM, 0b00011100); |
|||
delay(10); |
|||
|
|||
// Pull up/down select (0: down, 1: up)
|
|||
i2c_write_byte(PI4IO_ADDR, PI4IO_REG_PULL_SEL, 0b11100011); |
|||
delay(10); |
|||
|
|||
// Pull up/down enable (0: disable, 1: enable)
|
|||
i2c_write_byte(PI4IO_ADDR, PI4IO_REG_PULL_EN, 0b11100011); |
|||
delay(10); |
|||
|
|||
// Default input state for P0, P1 (buttons)
|
|||
i2c_write_byte(PI4IO_ADDR, PI4IO_REG_IN_DEF_STA, 0b00000011); |
|||
delay(10); |
|||
|
|||
// Enable interrupts for P0, P1 (0: enable, 1: disable)
|
|||
i2c_write_byte(PI4IO_ADDR, PI4IO_REG_INT_MASK, 0b11111100); |
|||
delay(10); |
|||
|
|||
// Set P7 (Reset) high, P5 and P6 will be set after
|
|||
i2c_write_byte(PI4IO_ADDR, PI4IO_REG_OUT_SET, 0b10000000); |
|||
delay(10); |
|||
|
|||
// Clear IRQ status
|
|||
i2c_read_byte(PI4IO_ADDR, PI4IO_REG_IRQ_STA, &in_data); |
|||
|
|||
// Enable RF switch (P6 high) and LNA (P5 high)
|
|||
i2c_read_byte(PI4IO_ADDR, PI4IO_REG_OUT_SET, &in_data); |
|||
in_data |= (1 << 6); // P6 = RF Switch = HIGH
|
|||
in_data |= (1 << 5); // P5 = LNA Enable = HIGH
|
|||
i2c_write_byte(PI4IO_ADDR, PI4IO_REG_OUT_SET, in_data); |
|||
} |
|||
|
|||
public: |
|||
void begin() { |
|||
ESP32Board::begin(); |
|||
|
|||
// Initialize I/O expander for LoRa RF control
|
|||
initIOExpander(); |
|||
|
|||
#ifdef PIN_BUZZER |
|||
pinMode(PIN_BUZZER, OUTPUT); |
|||
digitalWrite(PIN_BUZZER, LOW); |
|||
#endif |
|||
} |
|||
|
|||
const char* getManufacturerName() const override { |
|||
return "Unit C6L"; |
|||
return "M5Stack Unit C6L"; |
|||
} |
|||
|
|||
// Read button state from I/O expander P0 (active low)
|
|||
bool isButtonPressed() { |
|||
uint8_t in_data = 0xFF; |
|||
if (!i2c_read_byte(PI4IO_ADDR, PI4IO_REG_IN_STA, &in_data)) { |
|||
return false; |
|||
} |
|||
return !(in_data & 0x01); |
|||
} |
|||
|
|||
#ifdef PIN_BUZZER |
|||
void playTone(uint16_t frequency, uint16_t duration_ms) { |
|||
tone(PIN_BUZZER, frequency, duration_ms); |
|||
} |
|||
|
|||
void stopTone() { |
|||
noTone(PIN_BUZZER); |
|||
} |
|||
#endif |
|||
}; |
|||
|
|||
@ -0,0 +1,111 @@ |
|||
#include "UITask.h" |
|||
#include <target.h> |
|||
#include "../../examples/companion_radio/MyMesh.h" |
|||
|
|||
#define AUTO_OFF_MILLIS 30000 |
|||
#define BOOT_SCREEN_MILLIS 4000 |
|||
#define SCROLL_SPEED_MS 150 |
|||
#define SCROLL_PAUSE_MS 2000 |
|||
|
|||
void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* node_prefs) { |
|||
_display = display; |
|||
_node_prefs = node_prefs; |
|||
_need_refresh = true; |
|||
_msgcount = 0; |
|||
_next_refresh = 0; |
|||
_auto_off = millis() + AUTO_OFF_MILLIS; |
|||
_scroller.reset(); |
|||
|
|||
if (_display != NULL) { |
|||
_display->turnOn(); |
|||
} |
|||
} |
|||
|
|||
void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) { |
|||
_msgcount = msgcount; |
|||
_need_refresh = true; |
|||
if (_display != NULL && !_display->isOn()) { |
|||
_display->turnOn(); |
|||
_auto_off = millis() + AUTO_OFF_MILLIS; |
|||
} |
|||
} |
|||
|
|||
void UITask::renderScreen() { |
|||
if (_display == NULL) return; |
|||
|
|||
int w = _display->width(); |
|||
char tmp[32]; |
|||
|
|||
if (millis() < BOOT_SCREEN_MILLIS) { |
|||
_display->setTextSize(1); |
|||
_display->drawTextCentered(w / 2, 3, "MeshCore"); |
|||
_display->drawTextCentered(w / 2, 20, FIRMWARE_VERSION); |
|||
_display->drawTextCentered(w / 2, 34, "Companion"); |
|||
return; |
|||
} |
|||
|
|||
_display->setTextSize(1); |
|||
|
|||
#ifdef BLE_PIN_CODE |
|||
uint32_t pin = the_mesh.getBLEPin(); |
|||
if (_connected) { |
|||
_display->drawTextCentered(w / 2, 0, "Connected"); |
|||
} else if (pin != 0) { |
|||
sprintf(tmp, "PIN:%06d", pin); |
|||
_display->drawTextCentered(w / 2, 0, tmp); |
|||
} else { |
|||
_display->drawTextCentered(w / 2, 0, "Ready"); |
|||
} |
|||
#else |
|||
_display->drawTextCentered(w / 2, 0, "USB Ready"); |
|||
#endif |
|||
|
|||
int nameW = _display->getTextWidth(_node_prefs->node_name); |
|||
if (nameW <= w) { |
|||
_display->setCursor(0, 10); |
|||
_display->print(_node_prefs->node_name); |
|||
} else { |
|||
_display->setCursor(-_scroller.offset, 10); |
|||
_display->print(_node_prefs->node_name); |
|||
} |
|||
|
|||
sprintf(tmp, "%.3f", _node_prefs->freq); |
|||
_display->setCursor(0, 20); |
|||
_display->print(tmp); |
|||
|
|||
if (_msgcount > 0) { |
|||
sprintf(tmp, "%d unread", _msgcount); |
|||
_display->setCursor(0, 30); |
|||
_display->print(tmp); |
|||
} |
|||
} |
|||
|
|||
void UITask::loop() { |
|||
if (_display == NULL) return; |
|||
|
|||
if (board.isButtonPressed()) { |
|||
if (!_display->isOn()) { |
|||
_display->turnOn(); |
|||
_need_refresh = true; |
|||
} |
|||
_auto_off = millis() + AUTO_OFF_MILLIS; |
|||
} |
|||
|
|||
if (_display->isOn()) { |
|||
if (_node_prefs != NULL) { |
|||
_scroller.update(_display->getTextWidth(_node_prefs->node_name), |
|||
_display->width(), millis(), SCROLL_SPEED_MS, SCROLL_PAUSE_MS); |
|||
} |
|||
|
|||
if (millis() >= _next_refresh) { |
|||
_display->startFrame(); |
|||
renderScreen(); |
|||
_display->endFrame(); |
|||
_next_refresh = millis() + 200; |
|||
} |
|||
|
|||
if (millis() > _auto_off) { |
|||
_display->turnOff(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
#pragma once |
|||
|
|||
#include <Arduino.h> |
|||
#include <helpers/ui/DisplayDriver.h> |
|||
#include <helpers/SensorManager.h> |
|||
#include "../../examples/companion_radio/NodePrefs.h" |
|||
#include "../../examples/companion_radio/AbstractUITask.h" |
|||
|
|||
class UITask : public AbstractUITask { |
|||
public: |
|||
UITask(mesh::MainBoard* board, BaseSerialInterface* serial) |
|||
: AbstractUITask(board, serial), _display(NULL) {} |
|||
|
|||
void begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* node_prefs); |
|||
void loop() override; |
|||
void notify(UIEventType t) override {} |
|||
void msgRead(int msgcount) override { _msgcount = msgcount; _need_refresh = true; } |
|||
void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) override; |
|||
|
|||
private: |
|||
void renderScreen(); |
|||
DisplayDriver* _display; |
|||
NodePrefs* _node_prefs; |
|||
int _msgcount; |
|||
bool _need_refresh; |
|||
uint32_t _next_refresh; |
|||
uint32_t _auto_off; |
|||
MarqueeScroller _scroller; |
|||
}; |
|||
Loading…
Reference in new issue