Skip to content

Commit 21991cb

Browse files
TheRealHaoLiuclaude
andcommitted
Add dual-firmware OTA switching and display improvements for M5Stack Unit C6L
- Add OTA partition switching via BOOT button hold during startup, allowing switching between repeater and companion firmware without reflashing. Buzzer feedback indicates switch or error. - Fix SSD1306 64x48 display garbled output by setting correct COM pins configuration (0x12) after Adafruit library init, which lacks a 64x48 case. Disable text wrap to prevent long text from overflowing. - Fix ESP32-C6 BLE build by adding missing esp_mac.h include for esp_efuse_mac_get_default(). - Add display support to repeater build with C6L-specific UITask featuring boot screen, marquee scrolling for long node names, and I2C expander button wake. - Add boot screen to companion UITask showing MeshCore version and mode, with marquee scrolling and unread message count. - Reorganize variant UITask files into companion/ and repeater/ subdirectories for consistency. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3505d12 commit 21991cb

File tree

8 files changed

+360
-101
lines changed

8 files changed

+360
-101
lines changed

src/helpers/esp32/SerialBLEInterface.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include "SerialBLEInterface.h"
2+
#include <esp_mac.h>
23

34
// See the following for generating UUIDs:
45
// https://www.uuidgenerator.net/

src/helpers/ui/SSD1306SPIDisplay.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,17 @@ bool SSD1306SPIDisplay::lazyInit() {
3131
return false;
3232
}
3333
Serial.println("SSD1306: display.begin() OK");
34+
35+
// Fix for 64x48 displays: Adafruit library lacks this case and defaults
36+
// to comPins=0x02 (sequential). Displays taller than 32px need 0x12
37+
// (alternative COM pin config) or the output is garbled.
38+
#if defined(DISPLAY_WIDTH) && defined(DISPLAY_HEIGHT)
39+
#if (DISPLAY_WIDTH == 64) && (DISPLAY_HEIGHT == 48)
40+
display.ssd1306_command(SSD1306_SETCOMPINS);
41+
display.ssd1306_command(0x12);
42+
#endif
43+
#endif
44+
3445
// Clear any garbage in the display buffer
3546
display.clearDisplay();
3647
display.display();
@@ -63,6 +74,7 @@ void SSD1306SPIDisplay::startFrame(Color bkg) {
6374
display.setTextColor(_color);
6475
display.setFont(NULL); // Default 6x8 font
6576
display.setTextSize(1);
77+
display.setTextWrap(false);
6678
display.cp437(true);
6779
}
6880

variants/m5stack_unit_c6l/M5StackUnitC6LBoard.h

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#include <Arduino.h>
44
#include <Wire.h>
5+
#include <esp_ota_ops.h>
56
#include <helpers/ESP32Board.h>
67

78
// PI4IO I/O Expander (I2C address 0x43)
@@ -44,6 +45,52 @@ class M5StackUnitC6LBoard : public ESP32Board {
4445
return true;
4546
}
4647

48+
void checkOTASwitch() {
49+
// If BOOT button (P0) is held during startup, switch to the other OTA partition
50+
if (!isButtonPressed()) return;
51+
52+
// Button is pressed — wait a moment and confirm it's still held
53+
delay(500);
54+
if (!isButtonPressed()) return;
55+
56+
const esp_partition_t* running = esp_ota_get_running_partition();
57+
const esp_partition_t* next = esp_ota_get_next_update_partition(running);
58+
if (next == NULL) {
59+
Serial.println("OTA switch: no other partition found");
60+
return;
61+
}
62+
63+
// Verify the other partition has valid firmware
64+
esp_app_desc_t app_desc;
65+
if (esp_ota_get_partition_description(next, &app_desc) != ESP_OK) {
66+
Serial.println("OTA switch: other partition has no valid firmware");
67+
#ifdef PIN_BUZZER
68+
// Error buzzer: three short beeps
69+
for (int i = 0; i < 3; i++) {
70+
tone(PIN_BUZZER, 200, 100);
71+
delay(200);
72+
}
73+
noTone(PIN_BUZZER);
74+
#endif
75+
return;
76+
}
77+
78+
Serial.printf("OTA switch: %s -> %s\n", running->label, next->label);
79+
80+
#ifdef PIN_BUZZER
81+
// Confirmation buzzer: ascending two-tone
82+
tone(PIN_BUZZER, 1000, 150);
83+
delay(200);
84+
tone(PIN_BUZZER, 1500, 150);
85+
delay(200);
86+
noTone(PIN_BUZZER);
87+
#endif
88+
89+
esp_ota_set_boot_partition(next);
90+
delay(200);
91+
ESP.restart();
92+
}
93+
4794
void initIOExpander() {
4895
// Initialize PI4IO I/O expander for LoRa control pins
4996
// P5: LNA Enable, P6: RF Switch, P7: LoRa Reset
@@ -105,6 +152,9 @@ class M5StackUnitC6LBoard : public ESP32Board {
105152
pinMode(PIN_BUZZER, OUTPUT);
106153
digitalWrite(PIN_BUZZER, LOW);
107154
#endif
155+
156+
// Check if BOOT button is held to switch firmware
157+
checkOTASwitch();
108158
}
109159

110160
const char* getManufacturerName() const override {

variants/m5stack_unit_c6l/UITask.cpp

Lines changed: 0 additions & 94 deletions
This file was deleted.
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
#include "UITask.h"
2+
#include <target.h>
3+
#include "../../examples/companion_radio/MyMesh.h"
4+
5+
#define AUTO_OFF_MILLIS 30000 // 30 seconds
6+
#define BOOT_SCREEN_MILLIS 4000 // 4 seconds
7+
#define SCROLL_SPEED_MS 150 // pixels per tick
8+
#define SCROLL_PAUSE_MS 2000 // pause at start/end
9+
10+
void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* node_prefs) {
11+
_display = display;
12+
_node_prefs = node_prefs;
13+
_need_refresh = true;
14+
_msgcount = 0;
15+
_next_refresh = 0;
16+
_auto_off = millis() + AUTO_OFF_MILLIS;
17+
_scroll_offset = 0;
18+
_scroll_next = 0;
19+
_scroll_paused = true;
20+
21+
Serial.println("UITask: begin()");
22+
if (_display != NULL) {
23+
Serial.println("UITask: calling turnOn()");
24+
_display->turnOn();
25+
Serial.print("UITask: isOn() = ");
26+
Serial.println(_display->isOn());
27+
} else {
28+
Serial.println("UITask: display is NULL");
29+
}
30+
}
31+
32+
void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) {
33+
_msgcount = msgcount;
34+
_need_refresh = true;
35+
if (_display != NULL && !_display->isOn()) {
36+
_display->turnOn();
37+
_auto_off = millis() + AUTO_OFF_MILLIS;
38+
}
39+
}
40+
41+
void UITask::renderScreen() {
42+
if (_display == NULL) return;
43+
44+
int w = _display->width();
45+
char tmp[32];
46+
47+
if (millis() < BOOT_SCREEN_MILLIS) {
48+
// Boot screen
49+
_display->setTextSize(1);
50+
_display->drawTextCentered(w / 2, 3, "MeshCore");
51+
_display->drawTextCentered(w / 2, 20, FIRMWARE_VERSION);
52+
_display->drawTextCentered(w / 2, 34, "Companion");
53+
return;
54+
}
55+
56+
uint32_t pin = the_mesh.getBLEPin();
57+
58+
// 64x48 display, 6x8 default font = 10 chars x 6 lines
59+
60+
// 48px tall, 6x8 font → 4 lines at 10px spacing + 8px last
61+
_display->setTextSize(1);
62+
63+
// Line 1: PIN or status (y=0)
64+
if (_connected) {
65+
_display->drawTextCentered(w / 2, 0, "Connected");
66+
} else if (pin != 0) {
67+
sprintf(tmp, "PIN:%06d", pin);
68+
_display->drawTextCentered(w / 2, 0, tmp);
69+
} else {
70+
_display->drawTextCentered(w / 2, 0, "Ready");
71+
}
72+
73+
// Line 2: Node name with marquee scroll (y=10)
74+
int nameW = _display->getTextWidth(_node_prefs->node_name);
75+
if (nameW <= w) {
76+
// Fits on screen, no scroll needed
77+
_display->setCursor(0, 10);
78+
_display->print(_node_prefs->node_name);
79+
} else {
80+
// Marquee: draw at negative offset
81+
_display->setCursor(-_scroll_offset, 10);
82+
_display->print(_node_prefs->node_name);
83+
}
84+
85+
// Line 3: Frequency (y=20)
86+
sprintf(tmp, "%.3f", _node_prefs->freq);
87+
_display->setCursor(0, 20);
88+
_display->print(tmp);
89+
90+
// Line 4: Unread messages (y=30)
91+
if (_msgcount > 0) {
92+
sprintf(tmp, "%d unread", _msgcount);
93+
_display->setCursor(0, 30);
94+
_display->print(tmp);
95+
}
96+
}
97+
98+
void UITask::loop() {
99+
if (_display == NULL) return;
100+
101+
// Check button press to wake display
102+
if (board.isButtonPressed()) {
103+
if (!_display->isOn()) {
104+
_display->turnOn();
105+
_need_refresh = true;
106+
}
107+
_auto_off = millis() + AUTO_OFF_MILLIS;
108+
}
109+
110+
if (_display->isOn()) {
111+
// Update marquee scroll
112+
if (_node_prefs != NULL && millis() >= _scroll_next) {
113+
int nameW = _display->getTextWidth(_node_prefs->node_name);
114+
int w = _display->width();
115+
int maxScroll = nameW - w;
116+
if (maxScroll > 0) {
117+
if (_scroll_paused) {
118+
_scroll_paused = false;
119+
_scroll_next = millis() + SCROLL_PAUSE_MS;
120+
} else {
121+
_scroll_offset++;
122+
if (_scroll_offset >= maxScroll) {
123+
_scroll_offset = 0;
124+
_scroll_paused = true;
125+
}
126+
_scroll_next = millis() + SCROLL_SPEED_MS;
127+
}
128+
}
129+
}
130+
131+
if (millis() >= _next_refresh) {
132+
_display->startFrame();
133+
renderScreen();
134+
_display->endFrame();
135+
_next_refresh = millis() + 200; // faster refresh for smooth scroll
136+
}
137+
138+
if (millis() > _auto_off) {
139+
_display->turnOff();
140+
}
141+
}
142+
}

variants/m5stack_unit_c6l/UITask.h renamed to variants/m5stack_unit_c6l/companion/UITask.h

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
#include <Arduino.h>
44
#include <helpers/ui/DisplayDriver.h>
55
#include <helpers/SensorManager.h>
6-
#include "../examples/companion_radio/NodePrefs.h"
7-
#include "../examples/companion_radio/AbstractUITask.h"
6+
#include "../../examples/companion_radio/NodePrefs.h"
7+
#include "../../examples/companion_radio/AbstractUITask.h"
88

99
class M5StackUnitC6LBoard;
1010

@@ -30,4 +30,7 @@ class UITask : public AbstractUITask {
3030
bool _need_refresh;
3131
uint32_t _next_refresh;
3232
uint32_t _auto_off;
33+
int _scroll_offset;
34+
uint32_t _scroll_next;
35+
bool _scroll_paused;
3336
};

0 commit comments

Comments
 (0)