mirror of https://github.com/meshcore-dev/MeshCore
committed by
GitHub
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 |
#pragma once |
||||
|
|
||||
#include <Arduino.h> |
#include <Arduino.h> |
||||
|
#include <Wire.h> |
||||
#include <helpers/ESP32Board.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 { |
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: |
public: |
||||
void begin() { |
void begin() { |
||||
ESP32Board::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 { |
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