24 Commits

Author SHA1 Message Date
pelgraine
e40d9ced4a Merge branch 'dev' 2026-02-04 12:45:42 +11:00
pelgraine
b8de2d0d16 "updated mymesh h with firmware version details" 2026-02-04 12:45:06 +11:00
pelgraine
9fbc3202f6 "fixed reocurring BLE queue bug that popped up in v0.6.1. Improved keyboard responsiveness" 2026-02-04 12:44:17 +11:00
pelgraine
9d91f48797 Merge branch 'dev' 2026-02-02 21:28:48 +11:00
pelgraine
21eb385763 "Updated version date on mymesh.h and fixed modem_power_EN so 4G modem made inactive and annoying red LED Status light disabled when using firmware via Launcher mode" 2026-02-02 21:28:08 +11:00
pelgraine
4b81e596d2 "Fixed the queueSentChannelMessage BLE history ommission" 2026-02-01 20:26:27 +11:00
pelgraine
a5f2e8d055 "updated readme.md roadmap details" 2026-02-01 19:52:42 +11:00
pelgraine
462b1cb642 "Removed Preview Message overlay in favour of short popup that shows you which channel you've received a new message in" 2026-02-01 19:49:30 +11:00
pelgraine
0b270c0e1a "Changed word wrapping in channel view screen to boundary wrapping" 2026-02-01 19:38:26 +11:00
pelgraine
2730c05329 "Updated readme and firmware version" 2026-02-01 18:18:21 +11:00
pelgraine
02d2fb08fb "Added symbol capability" 2026-02-01 18:09:24 +11:00
pelgraine
b0003e1896 "Added additional channel compose functionality - can switch channels now. Minor ui changes for nav bar" 2026-02-01 17:51:17 +11:00
pelgraine
0be77ef759 "updated firmware version on mymesh" 2026-01-29 22:06:41 +11:00
pelgraine
c5df40cefd Added basic Public channel only view message history and compose - bugs still present 2026-01-29 21:33:14 +11:00
pelgraine
5bdcbb25b6 "Fix fix BLE shutdown on hibernate, update version in mymesh" 2026-01-29 19:01:38 +11:00
pelgraine
53fe89b216 "updated mymesh.h version type and date details" 2026-01-29 18:31:15 +11:00
pelgraine
e194c2c48c Replace Serial.print with MESH_DEBUG macros for cleaner debug output in main cpp, tdeckboard cpp and target cpp 2026-01-29 18:28:19 +11:00
pelgraine
9d401f76d3 Updated readme.md
Revised Roadmap / To-Do to goals specific to this repo
2026-01-28 21:43:41 +11:00
pelgraine
15f392c80e Update readme.md
Removed specific references to v1.1 now that I've confirmed it works on my T-Deck v1.0 (audio only) as well
2026-01-28 21:13:31 +11:00
pelgraine
621f9f9568 "added line to readme about purchase date of v1.1" 2026-01-28 20:18:34 +11:00
pelgraine
8c9106ca86 "update readme" 2026-01-28 20:16:57 +11:00
pelgraine
f4b9c89d9f "added and removed emoji variously to readme" 2026-01-28 20:15:44 +11:00
pelgraine
a4f5328113 "Updated readme" 2026-01-28 20:12:53 +11:00
pelgraine
2ad02f49e6 “Fixed the max contacts 400, max channels 20, pin back to 123456 for randomisation that somehow got lost in the merge from dev to main” 2026-01-28 20:05:19 +11:00
12 changed files with 1225 additions and 157 deletions

139
README.md
View File

@@ -1,24 +1,86 @@
## Meshcore + Fork = Meck
This fork was created specifically to focus on enabling BLE companion firmware for the LilyGo T-Deck Pro. Created with the assistance of Claude AI using Meshcore v1.11 code.
***Please note as of 1 Feb 2026, the T-Deck Pro repeater & usb firmware has not been finalised nor confirmed as functioning.*** ⭐
## T-Deck Pro Keyboard Controls
The T-Deck Pro BLE companion firmware includes full keyboard support for standalone messaging without a phone.
### Navigation (Home Screen)
| Key | Action |
|-----|--------|
| W / A | Previous page |
| S / D | Next page |
| Enter | Select / Confirm |
| M | Open channel messages |
| Q | Back to home screen |
### Channel Message Screen
| Key | Action |
|-----|--------|
| W / S | Scroll messages up/down |
| A / D | Switch between channels |
| C | Compose new message |
| Q | Back to home screen |
### Compose Mode
| Key | Action |
|-----|--------|
| A / D | Switch destination channel (when message is empty) |
| Enter | Send message |
| Backspace | Delete last character |
| Shift + Backspace | Cancel and exit compose mode |
### Symbol Entry (Sym Key)
Press the **Sym** key then the letter key to enter numbers and symbols:
| Key | Sym+ | | Key | Sym+ | | Key | Sym+ |
|-----|------|-|-----|------|-|-----|------|
| Q | # | | A | * | | Z | 7 |
| W | 1 | | S | 4 | | X | 8 |
| E | 2 | | D | 5 | | C | 9 |
| R | 3 | | F | 6 | | V | ? |
| T | ( | | G | / | | B | ! |
| Y | ) | | H | : | | N | , |
| U | _ | | J | ; | | M | . |
| I | - | | K | ' | | Mic | 0 |
| O | + | | L | " | | $ | (dedicated) |
| P | @ | | | | | | |
### Other Keys
| Key | Action |
|-----|--------|
| Shift | Uppercase next letter |
| Alt | Same as Sym (for numbers/symbols) |
| Space | Space character / Next in navigation |
## About MeshCore
MeshCore is a lightweight, portable C++ library that enables multi-hop packet routing for embedded projects using LoRa and other packet radios. It is designed for developers who want to create resilient, decentralized communication networks that work without the internet.
## 🔍 What is MeshCore?
## What is MeshCore?
MeshCore now supports a range of LoRa devices, allowing for easy flashing without the need to compile firmware manually. Users can flash a pre-built binary using tools like Adafruit ESPTool and interact with the network through a serial console.
MeshCore provides the ability to create wireless mesh networks, similar to Meshtastic and Reticulum but with a focus on lightweight multi-hop packet routing for embedded projects. Unlike Meshtastic, which is tailored for casual LoRa communication, or Reticulum, which offers advanced networking, MeshCore balances simplicity with scalability, making it ideal for custom embedded solutions., where devices (nodes) can communicate over long distances by relaying messages through intermediate nodes. This is especially useful in off-grid, emergency, or tactical situations where traditional communication infrastructure is unavailable.
## Key Features
## Key Features
* Multi-Hop Packet Routing
* Devices can forward messages across multiple nodes, extending range beyond a single radio's reach.
* Supports up to a configurable number of hops to balance network efficiency and prevent excessive traffic.
* Nodes use fixed roles where "Companion" nodes are not repeating messages at all to prevent adverse routing paths from being used.
* Supports LoRa Radios Works with Heltec, RAK Wireless, and other LoRa-based hardware.
* Decentralized & Resilient No central server or internet required; the network is self-healing.
* Low Power Consumption Ideal for battery-powered or solar-powered devices.
* Simple to Deploy Pre-built example applications make it easy to get started.
* Supports LoRa Radios Works with Heltec, RAK Wireless, and other LoRa-based hardware.
* Decentralized & Resilient No central server or internet required; the network is self-healing.
* Low Power Consumption Ideal for battery-powered or solar-powered devices.
* Simple to Deploy Pre-built example applications make it easy to get started.
## 🎯 What Can You Use MeshCore For?
## What Can You Use MeshCore For?
* Off-Grid Communication: Stay connected even in remote areas.
* Emergency Response & Disaster Recovery: Set up instant networks where infrastructure is down.
@@ -26,7 +88,7 @@ MeshCore provides the ability to create wireless mesh networks, similar to Mesht
* Tactical & Security Applications: Military, law enforcement, and private security use cases.
* IoT & Sensor Networks: Collect data from remote sensors and relay it back to a central location.
## 🚀 How to Get Started
## How to Get Started
- Watch the [MeshCore Intro Video](https://www.youtube.com/watch?v=t1qne8uJBAc) by Andy Kirby.
- Read through our [Frequently Asked Questions](./docs/faq.md) section.
@@ -39,27 +101,22 @@ For developers;
- Clone and open the MeshCore repository in Visual Studio Code.
- See the example applications you can modify and run:
- [Companion Radio](./examples/companion_radio) - For use with an external chat app, over BLE, USB or WiFi.
- [Simple Repeater](./examples/simple_repeater) - Extends network coverage by relaying messages.
- [Simple Room Server](./examples/simple_room_server) - A simple BBS server for shared Posts.
- [Simple Secure Chat](./examples/simple_secure_chat) - Secure terminal based text communication between devices.
The Simple Secure Chat example can be interacted with through the Serial Monitor in Visual Studio Code, or with a Serial USB Terminal on Android.
## MeshCore Flasher
## ⚡️ MeshCore Flasher
We have prebuilt firmware ready to flash on supported devices.
Download a copy of the Meck firmware bin from https://github.com/pelgraine/Meck/releases, then:
- Launch https://flasher.meshcore.co.uk
- Select a supported device
- Flash one of the firmware types:
- Companion, Repeater or Room Server
- Select Custom Firmware
- Select the .bin file you just downloaded, and click Open or press Enter.
- Click Flash, then select your device in the popup window (eg. USB JTAG/serial debug unit cu.usbmodem101 as an example), then click Connect.
- Once flashing is complete, you can connect with one of the MeshCore clients below.
## 📱 MeshCore Clients
## MeshCore Clients
**Companion Firmware**
The companion firmware can be connected to via BLE, USB or WiFi depending on the firmware type you flashed.
The companion firmware can be connected to via BLE. USB is planned for a future update.
- Web: https://app.meshcore.nz
- Android: https://play.google.com/store/apps/details?id=com.liamcottle.meshcore.android
@@ -67,14 +124,6 @@ The companion firmware can be connected to via BLE, USB or WiFi depending on the
- NodeJS: https://github.com/liamcottle/meshcore.js
- Python: https://github.com/fdlamotte/meshcore-cli
**Repeater and Room Server Firmware**
The repeater and room server firmwares can be setup via USB in the web config tool.
- https://config.meshcore.dev
They can also be managed via LoRa in the mobile app by using the Remote Management feature.
## 🛠 Hardware Compatibility
MeshCore is designed for devices listed in the [MeshCore Flasher](https://flasher.meshcore.co.uk)
@@ -95,30 +144,16 @@ Here are some general principals you should try to adhere to:
## Road-Map / To-Do
There are a number of fairly major features in the pipeline, with no particular time-frames attached yet. In very rough chronological order:
- [X] Companion radio: UI redesign
- [ ] Repeater + Room Server: add ACL's (like Sensor Node has)
- [ ] Standardise Bridge mode for repeaters
- [ ] Repeater/Bridge: Standardise the Transport Codes for zoning/filtering
- [ ] Core + Repeater: enhanced zero-hop neighbour discovery
- [ ] Core: round-trip manual path support
- [ ] Companion + Apps: support for multiple sub-meshes (and 'off-grid' client repeat mode)
- [ ] Core + Apps: support for LZW message compression
- [ ] Core: dynamic CR (Coding Rate) for weak vs strong hops
- [ ] Core: new framework for hosting multiple virtual nodes on one physical device
- [ ] V2 protocol spec: discussion and consensus around V2 packet protocol, including path hashes, new encryption specs, etc
There are a number of fairly major features in the pipeline, with no particular time-frames attached yet. In partly chronological order:
- [X] Companion radio: BLE
- [X] Text entry for Public channel messages Companion BLE firmware
- [X] View and compose all channel messages Companion BLE firmware
- [ ] Standalone DM functionality for Companion BLE firmware
- [ ] Companion radio: USB
- [ ] Simple Repeater firmware for the T-Deck Pro
- [ ] Get pin 45 with the screen backlight functioning for the T-Deck Pro v1.1
- [ ] Canned messages function for Companion BLE firmware
## 📞 Get Support
- Report bugs and request features on the [GitHub Issues](https://github.com/ripplebiz/MeshCore/issues) page.
- Find additional guides and components on [my site](https://buymeacoffee.com/ripplebiz).
- Join [MeshCore Discord](https://discord.gg/BMwCtwHj5V) to chat with the developers and get help from the community.
## RAK Wireless Board Support in PlatformIO
Before building/flashing the RAK4631 targets in this project, there is, unfortunately, some patching you have to do to your platformIO packages to make it work. There is a guide here on the process:
[RAK Wireless: How to Perform Installation of Board Support Package in PlatformIO](https://learn.rakwireless.com/hc/en-us/articles/26687276346775-How-To-Perform-Installation-of-Board-Support-Package-in-PlatformIO)
After building, you will need to convert the output firmware.hex file into a .uf2 file you can copy over to your RAK4631 device (after doing a full erase) by using the command `uf2conv.py -f 0xADA52840 -c firmware.hex` with the python script available from:
[GitHub: Microsoft - uf2](https://github.com/Microsoft/uf2/blob/master/utils/uf2conv.py)
- Join [MeshCore Discord](https://discord.gg/BMwCtwHj5V) to chat with the developers and get help from the community.

View File

@@ -541,6 +541,46 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe
#endif
}
void MyMesh::queueSentChannelMessage(uint8_t channel_idx, uint32_t timestamp, const char* sender, const char* text) {
// Format message the same way as onChannelMessageRecv for BLE app sync
// This allows sent messages from device keyboard to appear in the app
int i = 0;
if (app_target_ver >= 3) {
out_frame[i++] = RESP_CODE_CHANNEL_MSG_RECV_V3;
out_frame[i++] = 0; // SNR not applicable for sent messages
out_frame[i++] = 0; // reserved1
out_frame[i++] = 0; // reserved2
} else {
out_frame[i++] = RESP_CODE_CHANNEL_MSG_RECV;
}
out_frame[i++] = channel_idx;
out_frame[i++] = 0; // path_len = 0 indicates local/sent message
out_frame[i++] = TXT_TYPE_PLAIN;
memcpy(&out_frame[i], &timestamp, 4);
i += 4;
// Format as "sender: text" like the app expects
char formatted[MAX_FRAME_SIZE];
snprintf(formatted, sizeof(formatted), "%s: %s", sender, text);
int tlen = strlen(formatted);
if (i + tlen > MAX_FRAME_SIZE) {
tlen = MAX_FRAME_SIZE - i;
}
memcpy(&out_frame[i], formatted, tlen);
i += tlen;
addToOfflineQueue(out_frame, i);
// If app is connected, send push notification
if (_serial->isConnected()) {
uint8_t frame[1];
frame[0] = PUSH_CODE_MSG_WAITING;
_serial->writeFrame(frame, 1);
}
}
uint8_t MyMesh::onContactRequest(const ContactInfo &contact, uint32_t sender_timestamp, const uint8_t *data,
uint8_t len, uint8_t *reply) {
if (data[0] == REQ_TYPE_GET_TELEMETRY_DATA) {
@@ -1979,4 +2019,4 @@ bool MyMesh::advert() {
} else {
return false;
}
}
}

View File

@@ -8,11 +8,11 @@
#define FIRMWARE_VER_CODE 8
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "28 Jan 2026"
#define FIRMWARE_BUILD_DATE "4 Feb 2026"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "Meck v0.3"
#define FIRMWARE_VERSION "Meck v0.6.2"
#endif
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
@@ -101,6 +101,9 @@ public:
void enterCLIRescue();
int getRecentlyHeard(AdvertPath dest[], int max_num);
// Queue a sent channel message for BLE app sync
void queueSentChannelMessage(uint8_t channel_idx, uint32_t timestamp, const char* sender, const char* text);
protected:
float getAirtimeBudgetFactor() const override;
@@ -230,4 +233,4 @@ private:
AdvertPath advert_paths[ADVERT_PATH_TABLE_SIZE]; // circular table
};
extern MyMesh the_mesh;
extern MyMesh the_mesh;

View File

@@ -4,6 +4,26 @@
#include "variant.h" // Board-specific defines (HAS_GPS, etc.)
#include "target.h" // For sensors, board, etc.
// T-Deck Pro Keyboard support
#if defined(LilyGo_TDeck_Pro)
#include "TCA8418Keyboard.h"
TCA8418Keyboard keyboard(I2C_ADDR_KEYBOARD, &Wire);
// Compose mode state
static bool composeMode = false;
static char composeBuffer[138]; // 137 chars max + null terminator
static int composePos = 0;
static uint8_t composeChannelIdx = 0; // Which channel to send to
static unsigned long lastComposeRefresh = 0;
static bool composeNeedsRefresh = false;
#define COMPOSE_REFRESH_INTERVAL 600 // ms between e-ink refreshes while typing (refresh takes ~650ms)
void initKeyboard();
void handleKeyboardInput();
void drawComposeScreen();
void sendComposedMessage();
#endif
// Believe it or not, this std C function is busted on some platforms!
static uint32_t _atoi(const char* sp) {
uint32_t n = 0;
@@ -110,14 +130,14 @@ void halt() {
void setup() {
Serial.begin(115200);
delay(100); // Give serial time to initialize
Serial.println("=== setup() - STARTING ===");
MESH_DEBUG_PRINTLN("=== setup() - STARTING ===");
board.begin();
Serial.println("setup() - board.begin() done");
MESH_DEBUG_PRINTLN("setup() - board.begin() done");
#ifdef DISPLAY_CLASS
DisplayDriver* disp = NULL;
Serial.println("setup() - about to call display.begin()");
MESH_DEBUG_PRINTLN("setup() - about to call display.begin()");
// =========================================================================
// T-Deck Pro V1.1: Initialize E-Ink reset pin BEFORE display.begin()
@@ -127,7 +147,7 @@ void setup() {
// Initialize E-Ink reset pin (GPIO 16)
pinMode(PIN_DISPLAY_RST, OUTPUT);
digitalWrite(PIN_DISPLAY_RST, HIGH);
Serial.println("setup() - E-Ink reset pin initialized");
MESH_DEBUG_PRINTLN("setup() - E-Ink reset pin initialized");
// Initialize Touch reset pin (GPIO 38)
#ifdef CST328_PIN_RST
@@ -138,13 +158,13 @@ void setup() {
delay(80);
digitalWrite(CST328_PIN_RST, HIGH);
delay(20);
Serial.println("setup() - Touch reset pin initialized");
MESH_DEBUG_PRINTLN("setup() - Touch reset pin initialized");
#endif
#endif
// =========================================================================
if (display.begin()) {
Serial.println("setup() - display.begin() returned true");
MESH_DEBUG_PRINTLN("setup() - display.begin() returned true");
disp = &display;
disp->startFrame();
#ifdef ST7789
@@ -152,25 +172,25 @@ void setup() {
#endif
disp->drawTextCentered(disp->width() / 2, 28, "Loading...");
disp->endFrame();
Serial.println("setup() - Loading screen drawn");
MESH_DEBUG_PRINTLN("setup() - Loading screen drawn");
} else {
Serial.println("setup() - display.begin() returned false!");
MESH_DEBUG_PRINTLN("setup() - display.begin() returned false!");
}
#endif
Serial.println("setup() - about to call radio_init()");
MESH_DEBUG_PRINTLN("setup() - about to call radio_init()");
if (!radio_init()) {
Serial.println("setup() - radio_init() FAILED! Halting.");
MESH_DEBUG_PRINTLN("setup() - radio_init() FAILED! Halting.");
halt();
}
Serial.println("setup() - radio_init() done");
MESH_DEBUG_PRINTLN("setup() - radio_init() done");
Serial.println("setup() - about to call fast_rng.begin()");
MESH_DEBUG_PRINTLN("setup() - about to call fast_rng.begin()");
fast_rng.begin(radio_get_rng_seed());
Serial.println("setup() - fast_rng.begin() done");
MESH_DEBUG_PRINTLN("setup() - fast_rng.begin() done");
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
Serial.println("setup() - NRF52/STM32 filesystem init");
MESH_DEBUG_PRINTLN("setup() - NRF52/STM32 filesystem init");
InternalFS.begin();
#if defined(QSPIFLASH)
if (!QSPIFlash.begin()) {
@@ -183,11 +203,11 @@ void setup() {
ExtraFS.begin();
#endif
#endif
Serial.println("setup() - about to call store.begin()");
MESH_DEBUG_PRINTLN("setup() - about to call store.begin()");
store.begin();
Serial.println("setup() - store.begin() done");
MESH_DEBUG_PRINTLN("setup() - store.begin() done");
Serial.println("setup() - about to call the_mesh.begin()");
MESH_DEBUG_PRINTLN("setup() - about to call the_mesh.begin()");
the_mesh.begin(
#ifdef DISPLAY_CLASS
disp != NULL
@@ -195,27 +215,27 @@ void setup() {
false
#endif
);
Serial.println("setup() - the_mesh.begin() done");
MESH_DEBUG_PRINTLN("setup() - the_mesh.begin() done");
#ifdef BLE_PIN_CODE
Serial.println("setup() - about to call serial_interface.begin() with BLE");
MESH_DEBUG_PRINTLN("setup() - about to call serial_interface.begin() with BLE");
serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin());
Serial.println("setup() - serial_interface.begin() done");
MESH_DEBUG_PRINTLN("setup() - serial_interface.begin() done");
#else
serial_interface.begin(Serial);
#endif
Serial.println("setup() - about to call the_mesh.startInterface()");
MESH_DEBUG_PRINTLN("setup() - about to call the_mesh.startInterface()");
the_mesh.startInterface(serial_interface);
Serial.println("setup() - the_mesh.startInterface() done");
MESH_DEBUG_PRINTLN("setup() - the_mesh.startInterface() done");
#elif defined(RP2040_PLATFORM)
Serial.println("setup() - RP2040 filesystem init");
MESH_DEBUG_PRINTLN("setup() - RP2040 filesystem init");
LittleFS.begin();
Serial.println("setup() - about to call store.begin()");
MESH_DEBUG_PRINTLN("setup() - about to call store.begin()");
store.begin();
Serial.println("setup() - store.begin() done");
MESH_DEBUG_PRINTLN("setup() - store.begin() done");
Serial.println("setup() - about to call the_mesh.begin()");
MESH_DEBUG_PRINTLN("setup() - about to call the_mesh.begin()");
the_mesh.begin(
#ifdef DISPLAY_CLASS
disp != NULL
@@ -223,7 +243,7 @@ void setup() {
false
#endif
);
Serial.println("setup() - the_mesh.begin() done");
MESH_DEBUG_PRINTLN("setup() - the_mesh.begin() done");
//#ifdef WIFI_SSID
// WiFi.begin(WIFI_SSID, WIFI_PWD);
@@ -239,20 +259,20 @@ void setup() {
#else
serial_interface.begin(Serial);
#endif
Serial.println("setup() - about to call the_mesh.startInterface()");
MESH_DEBUG_PRINTLN("setup() - about to call the_mesh.startInterface()");
the_mesh.startInterface(serial_interface);
Serial.println("setup() - the_mesh.startInterface() done");
MESH_DEBUG_PRINTLN("setup() - the_mesh.startInterface() done");
#elif defined(ESP32)
Serial.println("setup() - ESP32 filesystem init - calling SPIFFS.begin()");
MESH_DEBUG_PRINTLN("setup() - ESP32 filesystem init - calling SPIFFS.begin()");
SPIFFS.begin(true);
Serial.println("setup() - SPIFFS.begin() done");
MESH_DEBUG_PRINTLN("setup() - SPIFFS.begin() done");
Serial.println("setup() - about to call store.begin()");
MESH_DEBUG_PRINTLN("setup() - about to call store.begin()");
store.begin();
Serial.println("setup() - store.begin() done");
MESH_DEBUG_PRINTLN("setup() - store.begin() done");
Serial.println("setup() - about to call the_mesh.begin()");
MESH_DEBUG_PRINTLN("setup() - about to call the_mesh.begin()");
the_mesh.begin(
#ifdef DISPLAY_CLASS
disp != NULL
@@ -260,16 +280,16 @@ void setup() {
false
#endif
);
Serial.println("setup() - the_mesh.begin() done");
MESH_DEBUG_PRINTLN("setup() - the_mesh.begin() done");
#ifdef WIFI_SSID
Serial.println("setup() - WiFi mode");
MESH_DEBUG_PRINTLN("setup() - WiFi mode");
WiFi.begin(WIFI_SSID, WIFI_PWD);
serial_interface.begin(TCP_PORT);
#elif defined(BLE_PIN_CODE)
Serial.println("setup() - about to call serial_interface.begin() with BLE");
MESH_DEBUG_PRINTLN("setup() - about to call serial_interface.begin() with BLE");
serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin());
Serial.println("setup() - serial_interface.begin() done");
MESH_DEBUG_PRINTLN("setup() - serial_interface.begin() done");
#elif defined(SERIAL_RX)
companion_serial.setPins(SERIAL_RX, SERIAL_TX);
companion_serial.begin(115200);
@@ -277,69 +297,381 @@ void setup() {
#else
serial_interface.begin(Serial);
#endif
Serial.println("setup() - about to call the_mesh.startInterface()");
MESH_DEBUG_PRINTLN("setup() - about to call the_mesh.startInterface()");
the_mesh.startInterface(serial_interface);
Serial.println("setup() - the_mesh.startInterface() done");
MESH_DEBUG_PRINTLN("setup() - the_mesh.startInterface() done");
#else
#error "need to define filesystem"
#endif
Serial.println("setup() - about to call sensors.begin()");
MESH_DEBUG_PRINTLN("setup() - about to call sensors.begin()");
sensors.begin();
Serial.println("setup() - sensors.begin() done");
MESH_DEBUG_PRINTLN("setup() - sensors.begin() done");
// IMPORTANT: sensors.begin() calls initBasicGPS() which steals the GPS pins for Serial1
// We need to reinitialize Serial2 to reclaim them
#if HAS_GPS
Serial2.end(); // Close any existing Serial2
Serial2.begin(GPS_BAUDRATE, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN);
Serial.println("setup() - Reinitialized Serial2 for GPS after sensors.begin()");
MESH_DEBUG_PRINTLN("setup() - Reinitialized Serial2 for GPS after sensors.begin()");
#endif
#ifdef DISPLAY_CLASS
Serial.println("setup() - about to call ui_task.begin()");
MESH_DEBUG_PRINTLN("setup() - about to call ui_task.begin()");
ui_task.begin(disp, &sensors, the_mesh.getNodePrefs());
Serial.println("setup() - ui_task.begin() done");
MESH_DEBUG_PRINTLN("setup() - ui_task.begin() done");
#endif
// Initialize T-Deck Pro keyboard
#if defined(LilyGo_TDeck_Pro)
initKeyboard();
#endif
// Enable GPS by default on T-Deck Pro
#if HAS_GPS
// Set GPS enabled in both sensor manager and node prefs
sensors.setSettingValue("gps", "1");
the_mesh.getNodePrefs()->gps_enabled = 1;
the_mesh.savePrefs();
Serial.println("setup() - GPS enabled by default");
MESH_DEBUG_PRINTLN("setup() - GPS enabled by default");
#endif
Serial.println("=== setup() - COMPLETE ===");
MESH_DEBUG_PRINTLN("=== setup() - COMPLETE ===");
}
void loop() {
the_mesh.loop();
sensors.loop();
#ifdef DISPLAY_CLASS
// Skip UITask rendering when in compose mode to prevent flickering
#if defined(LilyGo_TDeck_Pro)
if (!composeMode) {
ui_task.loop();
} else {
// Handle debounced compose screen refresh
if (composeNeedsRefresh && (millis() - lastComposeRefresh) >= COMPOSE_REFRESH_INTERVAL) {
drawComposeScreen();
lastComposeRefresh = millis();
composeNeedsRefresh = false;
}
}
#else
ui_task.loop();
#endif
#endif
rtc_clock.tick();
// Debug: Check for GPS data on Serial2
#if HAS_GPS
static unsigned long lastGpsDebug = 0;
if (millis() - lastGpsDebug > 5000) { // Every 5 seconds
lastGpsDebug = millis();
Serial.print("GPS Debug - Serial2 available: ");
Serial.print(Serial2.available());
Serial.print(" bytes");
LocationProvider* loc = sensors.getLocationProvider();
if (loc) {
Serial.print(", valid: ");
Serial.print(loc->isValid() ? "YES" : "NO");
Serial.print(", sats: ");
Serial.println(loc->satellitesCount());
} else {
Serial.println(", LocationProvider: NULL");
// Handle T-Deck Pro keyboard input
#if defined(LilyGo_TDeck_Pro)
handleKeyboardInput();
#endif
}
// ============================================================================
// T-DECK PRO KEYBOARD FUNCTIONS
// ============================================================================
#if defined(LilyGo_TDeck_Pro)
void initKeyboard() {
// Keyboard uses the same I2C bus as other peripherals (already initialized)
if (keyboard.begin()) {
MESH_DEBUG_PRINTLN("setup() - Keyboard initialized");
composeBuffer[0] = '\0';
composePos = 0;
composeMode = false;
composeNeedsRefresh = false;
lastComposeRefresh = 0;
} else {
MESH_DEBUG_PRINTLN("setup() - Keyboard initialization failed!");
}
}
void handleKeyboardInput() {
if (!keyboard.isReady()) return;
char key = keyboard.readKey();
if (key == 0) return;
Serial.printf("handleKeyboardInput: key='%c' (0x%02X) composeMode=%d\n",
key >= 32 ? key : '?', key, composeMode);
if (composeMode) {
// In compose mode - handle text input
if (key == '\r') {
// Enter - send the message
Serial.println("Compose: Enter pressed, sending...");
if (composePos > 0) {
sendComposedMessage();
}
composeMode = false;
composeBuffer[0] = '\0';
composePos = 0;
ui_task.gotoHomeScreen();
return;
}
if (key == '\b') {
// Backspace - check if shift was recently pressed for cancel combo
if (keyboard.wasShiftRecentlyPressed(500)) {
// Shift+Backspace = Cancel (works anytime)
Serial.println("Compose: Shift+Backspace, cancelling...");
composeMode = false;
composeBuffer[0] = '\0';
composePos = 0;
ui_task.gotoHomeScreen();
return;
}
// Regular backspace - delete last character
if (composePos > 0) {
composePos--;
composeBuffer[composePos] = '\0';
Serial.printf("Compose: Backspace, pos now %d\n", composePos);
composeNeedsRefresh = true; // Use debounced refresh
}
return;
}
// A/D keys switch channels (only when buffer is empty or as special function)
if ((key == 'a' || key == 'A') && composePos == 0) {
// Previous channel
if (composeChannelIdx > 0) {
composeChannelIdx--;
} else {
// Wrap to last valid channel
for (uint8_t i = MAX_GROUP_CHANNELS - 1; i > 0; i--) {
ChannelDetails ch;
if (the_mesh.getChannel(i, ch) && ch.name[0] != '\0') {
composeChannelIdx = i;
break;
}
}
}
Serial.printf("Compose: Channel switched to %d\n", composeChannelIdx);
drawComposeScreen();
return;
}
if ((key == 'd' || key == 'D') && composePos == 0) {
// Next channel
ChannelDetails ch;
uint8_t nextIdx = composeChannelIdx + 1;
if (the_mesh.getChannel(nextIdx, ch) && ch.name[0] != '\0') {
composeChannelIdx = nextIdx;
} else {
composeChannelIdx = 0; // Wrap to first channel
}
Serial.printf("Compose: Channel switched to %d\n", composeChannelIdx);
drawComposeScreen();
return;
}
// Regular character input
if (key >= 32 && key < 127 && composePos < 137) {
composeBuffer[composePos++] = key;
composeBuffer[composePos] = '\0';
Serial.printf("Compose: Added '%c', pos now %d\n", key, composePos);
composeNeedsRefresh = true; // Use debounced refresh
}
return;
}
// Normal mode - not composing
switch (key) {
case 'c':
case 'C':
// Enter compose mode
composeMode = true;
composeBuffer[0] = '\0';
composePos = 0;
// If on channel screen, sync compose channel with viewed channel
if (ui_task.isOnChannelScreen()) {
composeChannelIdx = ui_task.getChannelScreenViewIdx();
}
Serial.printf("Entering compose mode, channel %d\n", composeChannelIdx);
drawComposeScreen();
break;
case 'm':
case 'M':
// Go to channel message screen
Serial.println("Opening channel messages");
ui_task.gotoChannelScreen();
break;
case 'w':
case 'W':
// Navigate up/previous (scroll on channel screen)
if (ui_task.isOnChannelScreen()) {
ui_task.injectKey('w'); // Pass directly for channel switching
} else {
Serial.println("Nav: Previous");
ui_task.injectKey(0xF2); // KEY_PREV
}
break;
case 's':
case 'S':
// Navigate down/next (scroll on channel screen)
if (ui_task.isOnChannelScreen()) {
ui_task.injectKey('s'); // Pass directly for channel switching
} else {
Serial.println("Nav: Next");
ui_task.injectKey(0xF1); // KEY_NEXT
}
break;
case 'a':
case 'A':
// Navigate left or switch channel (on channel screen)
if (ui_task.isOnChannelScreen()) {
ui_task.injectKey('a'); // Pass directly for channel switching
} else {
Serial.println("Nav: Previous");
ui_task.injectKey(0xF2); // KEY_PREV
}
break;
case 'd':
case 'D':
// Navigate right or switch channel (on channel screen)
if (ui_task.isOnChannelScreen()) {
ui_task.injectKey('d'); // Pass directly for channel switching
} else {
Serial.println("Nav: Next");
ui_task.injectKey(0xF1); // KEY_NEXT
}
break;
case '\r':
// Select/Enter
Serial.println("Nav: Enter/Select");
ui_task.injectKey(13); // KEY_ENTER
break;
case 'q':
case 'Q':
case '\b':
// Go back to home screen
Serial.println("Nav: Back to home");
ui_task.gotoHomeScreen();
break;
case ' ':
// Space - also acts as next/select
Serial.println("Nav: Space (Next)");
ui_task.injectKey(0xF1); // KEY_NEXT
break;
default:
Serial.printf("Unhandled key in normal mode: '%c' (0x%02X)\n", key, key);
break;
}
}
void drawComposeScreen() {
#ifdef DISPLAY_CLASS
display.startFrame();
display.setTextSize(1);
display.setColor(DisplayDriver::GREEN);
display.setCursor(0, 0);
// Get the channel name for display
ChannelDetails channel;
char headerBuf[40];
if (the_mesh.getChannel(composeChannelIdx, channel)) {
snprintf(headerBuf, sizeof(headerBuf), "To: %s", channel.name);
} else {
snprintf(headerBuf, sizeof(headerBuf), "To: Channel %d", composeChannelIdx);
}
display.print(headerBuf);
display.setColor(DisplayDriver::LIGHT);
display.drawRect(0, 11, display.width(), 1);
display.setCursor(0, 14);
display.setColor(DisplayDriver::LIGHT);
// Word wrap the compose buffer - calculate chars per line based on actual font width
int x = 0;
int y = 14;
uint16_t testWidth = display.getTextWidth("MMMMMMMMMM"); // 10 wide chars
int charsPerLine = (testWidth > 0) ? (display.width() * 10) / testWidth : 20;
if (charsPerLine < 12) charsPerLine = 12;
if (charsPerLine > 40) charsPerLine = 40;
char charStr[2] = {0, 0}; // Buffer for single character as string
for (int i = 0; i < composePos; i++) {
charStr[0] = composeBuffer[i];
display.print(charStr);
x++;
if (x >= charsPerLine) {
x = 0;
y += 11;
display.setCursor(0, y);
}
}
// Show cursor
display.print("_");
// Status bar
int statusY = display.height() - 12;
display.setColor(DisplayDriver::LIGHT);
display.drawRect(0, statusY - 2, display.width(), 1);
display.setCursor(0, statusY);
display.setColor(DisplayDriver::YELLOW);
char status[40];
if (composePos == 0) {
// Empty buffer - show channel switching hint
display.print("A/D:Ch");
sprintf(status, "Sh+Del:X");
display.setCursor(display.width() - display.getTextWidth(status) - 2, statusY);
display.print(status);
} else {
// Has text - show send/cancel hint
sprintf(status, "%d/137 Ent:Send", composePos);
display.print(status);
sprintf(status, "Sh+Del:X");
display.setCursor(display.width() - display.getTextWidth(status) - 2, statusY);
display.print(status);
}
display.endFrame();
#endif
}
}
void sendComposedMessage() {
if (composePos == 0) return;
// Get the selected channel
ChannelDetails channel;
if (the_mesh.getChannel(composeChannelIdx, channel)) {
uint32_t timestamp = rtc_clock.getCurrentTime();
// Send to channel
if (the_mesh.sendGroupMessage(timestamp, channel.channel,
the_mesh.getNodePrefs()->node_name,
composeBuffer, composePos)) {
// Add the sent message to local channel history so we can see what we sent
ui_task.addSentChannelMessage(composeChannelIdx,
the_mesh.getNodePrefs()->node_name,
composeBuffer);
// Queue message for BLE app sync (so sent messages appear in companion app)
the_mesh.queueSentChannelMessage(composeChannelIdx, timestamp,
the_mesh.getNodePrefs()->node_name,
composeBuffer);
ui_task.showAlert("Sent!", 1500);
} else {
ui_task.showAlert("Send failed!", 1500);
}
} else {
ui_task.showAlert("No channel!", 1500);
}
}
#endif // LilyGo_TDeck_Pro

View File

@@ -0,0 +1,292 @@
#pragma once
#include <helpers/ui/UIScreen.h>
#include <helpers/ui/DisplayDriver.h>
#include <helpers/ChannelDetails.h>
#include <MeshCore.h>
// Maximum messages to store in history
#define CHANNEL_MSG_HISTORY_SIZE 20
#define CHANNEL_MSG_TEXT_LEN 160
#ifndef MAX_GROUP_CHANNELS
#define MAX_GROUP_CHANNELS 20
#endif
class UITask; // Forward declaration
class MyMesh; // Forward declaration
extern MyMesh the_mesh;
class ChannelScreen : public UIScreen {
public:
struct ChannelMessage {
uint32_t timestamp;
uint8_t path_len;
uint8_t channel_idx; // Which channel this message belongs to
char text[CHANNEL_MSG_TEXT_LEN];
bool valid;
};
private:
UITask* _task;
mesh::RTCClock* _rtc;
ChannelMessage _messages[CHANNEL_MSG_HISTORY_SIZE];
int _msgCount; // Total messages stored
int _newestIdx; // Index of newest message (circular buffer)
int _scrollPos; // Current scroll position (0 = newest)
int _msgsPerPage; // Messages that fit on screen
uint8_t _viewChannelIdx; // Which channel we're currently viewing
public:
ChannelScreen(UITask* task, mesh::RTCClock* rtc)
: _task(task), _rtc(rtc), _msgCount(0), _newestIdx(-1), _scrollPos(0),
_msgsPerPage(3), _viewChannelIdx(0) {
// Initialize all messages as invalid
for (int i = 0; i < CHANNEL_MSG_HISTORY_SIZE; i++) {
_messages[i].valid = false;
}
}
// Add a new message to the history
void addMessage(uint8_t channel_idx, uint8_t path_len, const char* sender, const char* text) {
// Move to next slot in circular buffer
_newestIdx = (_newestIdx + 1) % CHANNEL_MSG_HISTORY_SIZE;
ChannelMessage* msg = &_messages[_newestIdx];
msg->timestamp = _rtc->getCurrentTime();
msg->path_len = path_len;
msg->channel_idx = channel_idx;
msg->valid = true;
// The text already contains "Sender: message" format, just store it
strncpy(msg->text, text, CHANNEL_MSG_TEXT_LEN - 1);
msg->text[CHANNEL_MSG_TEXT_LEN - 1] = '\0';
if (_msgCount < CHANNEL_MSG_HISTORY_SIZE) {
_msgCount++;
}
// Reset scroll to show newest message
_scrollPos = 0;
}
// Get count of messages for the currently viewed channel
int getMessageCountForChannel() const {
int count = 0;
for (int i = 0; i < CHANNEL_MSG_HISTORY_SIZE; i++) {
if (_messages[i].valid && _messages[i].channel_idx == _viewChannelIdx) {
count++;
}
}
return count;
}
int getMessageCount() const { return _msgCount; }
uint8_t getViewChannelIdx() const { return _viewChannelIdx; }
void setViewChannelIdx(uint8_t idx) { _viewChannelIdx = idx; _scrollPos = 0; }
int render(DisplayDriver& display) override {
char tmp[40];
// Header - show current channel name
display.setCursor(0, 0);
display.setTextSize(1);
display.setColor(DisplayDriver::GREEN);
// Get channel name
ChannelDetails channel;
if (the_mesh.getChannel(_viewChannelIdx, channel)) {
display.print(channel.name);
} else {
sprintf(tmp, "Channel %d", _viewChannelIdx);
display.print(tmp);
}
// Message count for this channel on right
int channelMsgCount = getMessageCountForChannel();
sprintf(tmp, "[%d]", channelMsgCount);
display.setCursor(display.width() - display.getTextWidth(tmp) - 2, 0);
display.print(tmp);
// Divider line
display.drawRect(0, 11, display.width(), 1);
if (channelMsgCount == 0) {
display.setCursor(0, 25);
display.setColor(DisplayDriver::LIGHT);
display.print("No messages yet");
display.setCursor(0, 40);
display.print("A/D: Switch channel");
display.setCursor(0, 52);
display.print("C: Compose message");
} else {
int lineHeight = 10;
int headerHeight = 14;
int footerHeight = 14;
// Calculate chars per line based on actual font width (not assumed 6px)
// Measure a test string and scale accordingly
uint16_t testWidth = display.getTextWidth("MMMMMMMMMM"); // 10 wide chars
int charsPerLine = (testWidth > 0) ? (display.width() * 10) / testWidth : 20;
if (charsPerLine < 12) charsPerLine = 12; // Minimum reasonable
if (charsPerLine > 40) charsPerLine = 40; // Maximum reasonable
int y = headerHeight;
// Build list of messages for this channel (newest first)
int channelMsgs[CHANNEL_MSG_HISTORY_SIZE];
int numChannelMsgs = 0;
for (int i = 0; i < _msgCount && numChannelMsgs < CHANNEL_MSG_HISTORY_SIZE; i++) {
int idx = _newestIdx - i;
while (idx < 0) idx += CHANNEL_MSG_HISTORY_SIZE;
idx = idx % CHANNEL_MSG_HISTORY_SIZE;
if (_messages[idx].valid && _messages[idx].channel_idx == _viewChannelIdx) {
channelMsgs[numChannelMsgs++] = idx;
}
}
// Display messages from scroll position
int msgsDrawn = 0;
for (int i = _scrollPos; i < numChannelMsgs && y < display.height() - footerHeight - lineHeight; i++) {
int idx = channelMsgs[i];
ChannelMessage* msg = &_messages[idx];
// Time indicator with hop count
display.setCursor(0, y);
display.setColor(DisplayDriver::YELLOW);
uint32_t age = _rtc->getCurrentTime() - msg->timestamp;
if (age < 60) {
sprintf(tmp, "(%d) %ds", msg->path_len == 0xFF ? 0 : msg->path_len, age);
} else if (age < 3600) {
sprintf(tmp, "(%d) %dm", msg->path_len == 0xFF ? 0 : msg->path_len, age / 60);
} else if (age < 86400) {
sprintf(tmp, "(%d) %dh", msg->path_len == 0xFF ? 0 : msg->path_len, age / 3600);
} else {
sprintf(tmp, "(%d) %dd", msg->path_len == 0xFF ? 0 : msg->path_len, age / 86400);
}
display.print(tmp);
y += lineHeight;
// Message text with character wrapping (like compose screen - fills full width)
display.setColor(DisplayDriver::LIGHT);
int textLen = strlen(msg->text);
int pos = 0;
int linesForThisMsg = 0;
int maxLinesPerMsg = 6; // Allow more lines per message
int x = 0;
char charStr[2] = {0, 0};
display.setCursor(0, y);
while (pos < textLen && linesForThisMsg < maxLinesPerMsg && y < display.height() - footerHeight - 2) {
charStr[0] = msg->text[pos];
display.print(charStr);
x++;
pos++;
if (x >= charsPerLine) {
x = 0;
linesForThisMsg++;
y += lineHeight;
if (linesForThisMsg < maxLinesPerMsg && y < display.height() - footerHeight - 2) {
display.setCursor(0, y);
}
}
}
// If we didn't end on a full line, still count it
if (x > 0) {
y += lineHeight;
}
y += 2;
msgsDrawn++;
_msgsPerPage = msgsDrawn;
}
}
// Footer with controls
int footerY = display.height() - 12;
display.drawRect(0, footerY - 2, display.width(), 1);
display.setCursor(0, footerY);
display.setColor(DisplayDriver::YELLOW);
// Left side: Q:Back A/D:Ch
display.print("Q:Back A/D:Ch");
// Right side: C:New
const char* rightText = "C:New";
display.setCursor(display.width() - display.getTextWidth(rightText) - 2, footerY);
display.print(rightText);
#if AUTO_OFF_MILLIS == 0 // e-ink
return 5000;
#else
return 1000;
#endif
}
bool handleInput(char c) override {
int channelMsgCount = getMessageCountForChannel();
// W or KEY_PREV - scroll up (older messages)
if (c == 0xF2 || c == 'w' || c == 'W') {
if (_scrollPos + _msgsPerPage < channelMsgCount) {
_scrollPos++;
return true;
}
}
// S or KEY_NEXT - scroll down (newer messages)
if (c == 0xF1 || c == 's' || c == 'S') {
if (_scrollPos > 0) {
_scrollPos--;
return true;
}
}
// A - previous channel
if (c == 'a' || c == 'A') {
if (_viewChannelIdx > 0) {
_viewChannelIdx--;
} else {
// Wrap to last valid channel
for (uint8_t i = MAX_GROUP_CHANNELS - 1; i > 0; i--) {
ChannelDetails ch;
if (the_mesh.getChannel(i, ch) && ch.name[0] != '\0') {
_viewChannelIdx = i;
break;
}
}
}
_scrollPos = 0;
return true;
}
// D - next channel
if (c == 'd' || c == 'D') {
ChannelDetails ch;
uint8_t nextIdx = _viewChannelIdx + 1;
if (the_mesh.getChannel(nextIdx, ch) && ch.name[0] != '\0') {
_viewChannelIdx = nextIdx;
} else {
_viewChannelIdx = 0;
}
_scrollPos = 0;
return true;
}
return false;
}
// Reset scroll position to newest
void resetScroll() {
_scrollPos = 0;
}
};

View File

@@ -30,6 +30,7 @@
#endif
#include "icons.h"
#include "ChannelScreen.h"
class SplashScreen : public UIScreen {
UITask* _task;
@@ -580,6 +581,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
splash = new SplashScreen(this);
home = new HomeScreen(this, &rtc_clock, sensors, node_prefs);
msg_preview = new MsgPreviewScreen(this, &rtc_clock);
channel_screen = new ChannelScreen(this, &rtc_clock);
setCurrScreen(splash);
}
@@ -628,8 +630,34 @@ void UITask::msgRead(int msgcount) {
void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) {
_msgcount = msgcount;
// Add to preview screen (for notifications on non-keyboard devices)
((MsgPreviewScreen *) msg_preview)->addPreview(path_len, from_name, text);
// Determine channel index by looking up the channel name
// For channel messages, from_name is the channel name
// For contact messages, from_name is the contact name (channel_idx = 0xFF)
uint8_t channel_idx = 0xFF; // Default: unknown/contact message
for (uint8_t i = 0; i < MAX_GROUP_CHANNELS; i++) {
ChannelDetails ch;
if (the_mesh.getChannel(i, ch) && strcmp(ch.name, from_name) == 0) {
channel_idx = i;
break;
}
}
// Add to channel history screen with channel index
((ChannelScreen *) channel_screen)->addMessage(channel_idx, path_len, from_name, text);
#if defined(LilyGo_TDeck_Pro)
// T-Deck Pro: Don't interrupt user with popup - just show brief notification
// Messages are stored in channel history, accessible via 'M' key
char alertBuf[40];
snprintf(alertBuf, sizeof(alertBuf), "New: %s", from_name);
showAlert(alertBuf, 2000);
#else
// Other devices: Show full preview screen (legacy behavior)
setCurrScreen(msg_preview);
#endif
if (_display != NULL) {
if (!_display->isOn() && !hasConnection()) {
@@ -922,3 +950,38 @@ void UITask::toggleBuzzer() {
_next_refresh = 0; // trigger refresh
#endif
}
void UITask::injectKey(char c) {
if (c != 0 && curr) {
// Turn on display if it's off
if (_display != NULL && !_display->isOn()) {
_display->turnOn();
}
curr->handleInput(c);
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
_next_refresh = 100; // trigger refresh
}
}
void UITask::gotoChannelScreen() {
((ChannelScreen *) channel_screen)->resetScroll();
setCurrScreen(channel_screen);
if (_display != NULL && !_display->isOn()) {
_display->turnOn();
}
_auto_off = millis() + AUTO_OFF_MILLIS;
_next_refresh = 100;
}
uint8_t UITask::getChannelScreenViewIdx() const {
return ((ChannelScreen *) channel_screen)->getViewChannelIdx();
}
void UITask::addSentChannelMessage(uint8_t channel_idx, const char* sender, const char* text) {
// Format the message as "Sender: message"
char formattedMsg[CHANNEL_MSG_TEXT_LEN];
snprintf(formattedMsg, sizeof(formattedMsg), "%s: %s", sender, text);
// Add to channel history with path_len=0 (local message)
((ChannelScreen *) channel_screen)->addMessage(channel_idx, 0, sender, formattedMsg);
}

View File

@@ -51,6 +51,7 @@ class UITask : public AbstractUITask {
UIScreen* splash;
UIScreen* home;
UIScreen* msg_preview;
UIScreen* channel_screen; // Channel message history screen
UIScreen* curr;
void userLedHandler();
@@ -73,15 +74,27 @@ public:
void begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* node_prefs);
void gotoHomeScreen() { setCurrScreen(home); }
void gotoChannelScreen(); // Navigate to channel message screen
void showAlert(const char* text, int duration_millis);
int getMsgCount() const { return _msgcount; }
bool hasDisplay() const { return _display != NULL; }
bool isButtonPressed() const;
bool isOnChannelScreen() const { return curr == channel_screen; }
uint8_t getChannelScreenViewIdx() const;
void toggleBuzzer();
bool getGPSState();
void toggleGPS();
// Inject a key press from external source (e.g., keyboard)
void injectKey(char c);
// Add a sent message to the channel screen history
void addSentChannelMessage(uint8_t channel_idx, const char* sender, const char* text);
// Get current screen for checking state
UIScreen* getCurrentScreen() const { return curr; }
UIScreen* getMsgPreviewScreen() const { return msg_preview; }
// from AbstractUITask
void msgRead(int msgcount) override;
@@ -90,4 +103,4 @@ public:
void loop() override;
void shutdown(bool restart = false);
};
};

View File

@@ -1,24 +1,25 @@
#include <Arduino.h>
#include "variant.h"
#include "TDeckBoard.h"
#include <Mesh.h> // For MESH_DEBUG_PRINTLN
uint32_t deviceOnline = 0x00;
void TDeckBoard::begin() {
Serial.println("TDeckBoard::begin() - starting");
MESH_DEBUG_PRINTLN("TDeckBoard::begin() - starting");
// Enable peripheral power (keyboard, sensors, etc.) FIRST
// This powers the BQ27220 fuel gauge and other I2C devices
pinMode(PIN_PERF_POWERON, OUTPUT);
digitalWrite(PIN_PERF_POWERON, HIGH);
delay(50); // Allow peripherals to power up before I2C init
Serial.println("TDeckBoard::begin() - peripheral power enabled");
MESH_DEBUG_PRINTLN("TDeckBoard::begin() - peripheral power enabled");
// Initialize I2C with correct pins for T-Deck Pro
Wire.begin(I2C_SDA, I2C_SCL);
Wire.setClock(100000); // 100kHz for reliable fuel gauge communication
Serial.println("TDeckBoard::begin() - I2C initialized");
MESH_DEBUG_PRINTLN("TDeckBoard::begin() - I2C initialized");
// Now call parent class begin (after power and I2C are ready)
ESP32Board::begin();
@@ -28,7 +29,7 @@ void TDeckBoard::begin() {
pinMode(P_LORA_EN, OUTPUT);
digitalWrite(P_LORA_EN, HIGH);
delay(10); // Allow module to power up
Serial.println("TDeckBoard::begin() - LoRa power enabled");
MESH_DEBUG_PRINTLN("TDeckBoard::begin() - LoRa power enabled");
#endif
// Enable GPS module power and initialize Serial2
@@ -37,14 +38,20 @@ void TDeckBoard::begin() {
pinMode(PIN_GPS_EN, OUTPUT);
digitalWrite(PIN_GPS_EN, GPS_EN_ACTIVE); // GPS_EN_ACTIVE is 1 (HIGH)
delay(100); // Allow GPS to power up
Serial.println("TDeckBoard::begin() - GPS power enabled");
MESH_DEBUG_PRINTLN("TDeckBoard::begin() - GPS power enabled");
#endif
// Initialize Serial2 for GPS with correct pins
Serial2.begin(GPS_BAUDRATE, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN);
Serial.print("TDeckBoard::begin() - GPS Serial2 initialized at ");
Serial.print(GPS_BAUDRATE);
Serial.println(" baud");
MESH_DEBUG_PRINTLN("TDeckBoard::begin() - GPS Serial2 initialized at %d baud", GPS_BAUDRATE);
#endif
// Disable 4G modem power (only present on 4G version, not audio version)
// This turns off the red status LED on the modem module
#ifdef MODEM_POWER_EN
pinMode(MODEM_POWER_EN, OUTPUT);
digitalWrite(MODEM_POWER_EN, LOW); // Cut power to modem
MESH_DEBUG_PRINTLN("TDeckBoard::begin() - 4G modem power disabled");
#endif
// Configure user button
@@ -68,12 +75,10 @@ void TDeckBoard::begin() {
// Test BQ27220 communication
#if HAS_BQ27220
uint16_t voltage = getBattMilliVolts();
Serial.print("TDeckBoard::begin() - Battery voltage: ");
Serial.print(voltage);
Serial.println(" mV");
MESH_DEBUG_PRINTLN("TDeckBoard::begin() - Battery voltage: %d mV", voltage);
#endif
Serial.println("TDeckBoard::begin() - complete");
MESH_DEBUG_PRINTLN("TDeckBoard::begin() - complete");
}
uint16_t TDeckBoard::getBattMilliVolts() {
@@ -81,13 +86,13 @@ uint16_t TDeckBoard::getBattMilliVolts() {
Wire.beginTransmission(BQ27220_I2C_ADDR);
Wire.write(BQ27220_REG_VOLTAGE);
if (Wire.endTransmission(false) != 0) {
Serial.println("BQ27220: I2C error reading voltage");
MESH_DEBUG_PRINTLN("BQ27220: I2C error reading voltage");
return 0;
}
uint8_t count = Wire.requestFrom((uint8_t)BQ27220_I2C_ADDR, (uint8_t)2);
if (count != 2) {
Serial.println("BQ27220: Read error - wrong byte count");
MESH_DEBUG_PRINTLN("BQ27220: Read error - wrong byte count");
return 0;
}

View File

@@ -16,6 +16,13 @@ class TDeckBoard : public ESP32Board {
public:
void begin();
void powerOff() override {
// Stop Bluetooth before power off
btStop();
// Don't call parent or enterDeepSleep - let normal shutdown continue
// Display will show "hibernating..." text
}
void enterDeepSleep(uint32_t secs, int pin_wake_btn) {
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);

View File

@@ -0,0 +1,290 @@
#pragma once
#include <Arduino.h>
#include <Wire.h>
// TCA8418 Register addresses
#define TCA8418_REG_CFG 0x01
#define TCA8418_REG_INT_STAT 0x02
#define TCA8418_REG_KEY_LCK_EC 0x03
#define TCA8418_REG_KEY_EVENT_A 0x04
#define TCA8418_REG_KP_GPIO1 0x1D
#define TCA8418_REG_KP_GPIO2 0x1E
#define TCA8418_REG_KP_GPIO3 0x1F
#define TCA8418_REG_DEBOUNCE 0x29
#define TCA8418_REG_GPI_EM1 0x20
#define TCA8418_REG_GPI_EM2 0x21
#define TCA8418_REG_GPI_EM3 0x22
// Key codes for special keys
#define KB_KEY_NONE 0
#define KB_KEY_BACKSPACE '\b'
#define KB_KEY_ENTER '\r'
#define KB_KEY_SPACE ' '
class TCA8418Keyboard {
private:
uint8_t _addr;
TwoWire* _wire;
bool _initialized;
bool _shiftActive; // Sticky shift (one-shot)
bool _altActive; // Sticky alt (one-shot)
bool _symActive; // Sticky sym (one-shot)
unsigned long _lastShiftTime; // For Shift+key combos
uint8_t readReg(uint8_t reg) {
_wire->beginTransmission(_addr);
_wire->write(reg);
_wire->endTransmission();
_wire->requestFrom(_addr, (uint8_t)1);
return _wire->available() ? _wire->read() : 0;
}
void writeReg(uint8_t reg, uint8_t val) {
_wire->beginTransmission(_addr);
_wire->write(reg);
_wire->write(val);
_wire->endTransmission();
}
// Map raw key codes to characters (from working reader firmware)
char getKeyChar(uint8_t keyCode) {
switch (keyCode) {
// Row 1 - QWERTYUIOP
case 10: return 'q'; // Q (was 97 on different hardware)
case 9: return 'w';
case 8: return 'e';
case 7: return 'r';
case 6: return 't';
case 5: return 'y';
case 4: return 'u';
case 3: return 'i';
case 2: return 'o';
case 1: return 'p';
// Row 2 - ASDFGHJKL + Backspace
case 20: return 'a'; // A (was 98 on different hardware)
case 19: return 's';
case 18: return 'd';
case 17: return 'f';
case 16: return 'g';
case 15: return 'h';
case 14: return 'j';
case 13: return 'k';
case 12: return 'l';
case 11: return '\b'; // Backspace
// Row 3 - Alt ZXCVBNM Sym Enter
case 30: return 0; // Alt - handled separately
case 29: return 'z';
case 28: return 'x';
case 27: return 'c';
case 26: return 'v';
case 25: return 'b';
case 24: return 'n';
case 23: return 'm';
case 22: return 0; // Symbol key - handled separately
case 21: return '\r'; // Enter
// Row 4 - Shift Mic Space Sym Shift
case 35: return 0; // Left shift - handled separately
case 34: return 0; // Mic
case 33: return ' '; // Space
case 32: return 0; // Sym - handled separately
case 31: return 0; // Right shift - handled separately
default: return 0;
}
}
// Map key with Alt modifier - same as Sym for this keyboard
char getAltChar(uint8_t keyCode) {
return getSymChar(keyCode); // Alt does same as Sym
}
// Map key with Sym modifier - based on actual T-Deck Pro keyboard silk-screen
char getSymChar(uint8_t keyCode) {
switch (keyCode) {
// Row 1: Q W E R T Y U I O P
case 10: return '#'; // Q -> #
case 9: return '1'; // W -> 1
case 8: return '2'; // E -> 2
case 7: return '3'; // R -> 3
case 6: return '('; // T -> (
case 5: return ')'; // Y -> )
case 4: return '_'; // U -> _
case 3: return '-'; // I -> -
case 2: return '+'; // O -> +
case 1: return '@'; // P -> @
// Row 2: A S D F G H J K L
case 20: return '*'; // A -> *
case 19: return '4'; // S -> 4
case 18: return '5'; // D -> 5
case 17: return '6'; // F -> 6
case 16: return '/'; // G -> /
case 15: return ':'; // H -> :
case 14: return ';'; // J -> ;
case 13: return '\''; // K -> '
case 12: return '"'; // L -> "
// Row 3: Z X C V B N M
case 29: return '7'; // Z -> 7
case 28: return '8'; // X -> 8
case 27: return '9'; // C -> 9
case 26: return '?'; // V -> ?
case 25: return '!'; // B -> !
case 24: return ','; // N -> ,
case 23: return '.'; // M -> .
// Row 4: Mic key -> 0
case 34: return '0'; // Mic -> 0
default: return 0;
}
}
public:
TCA8418Keyboard(uint8_t addr = 0x34, TwoWire* wire = &Wire)
: _addr(addr), _wire(wire), _initialized(false),
_shiftActive(false), _altActive(false), _symActive(false), _lastShiftTime(0) {}
bool begin() {
// Check if device responds
_wire->beginTransmission(_addr);
if (_wire->endTransmission() != 0) {
Serial.println("TCA8418: Device not found");
return false;
}
// Configure keyboard matrix (8 rows x 10 cols)
writeReg(TCA8418_REG_KP_GPIO1, 0xFF); // Rows 0-7 as keypad
writeReg(TCA8418_REG_KP_GPIO2, 0xFF); // Cols 0-7 as keypad
writeReg(TCA8418_REG_KP_GPIO3, 0x03); // Cols 8-9 as keypad
// Enable keypad with FIFO overflow detection
writeReg(TCA8418_REG_CFG, 0x11); // KE_IEN + INT_CFG
// Set debounce
writeReg(TCA8418_REG_DEBOUNCE, 0x03);
// Clear any pending interrupts
writeReg(TCA8418_REG_INT_STAT, 0x1F);
// Flush the FIFO
while (readReg(TCA8418_REG_KEY_LCK_EC) & 0x0F) {
readReg(TCA8418_REG_KEY_EVENT_A);
}
_initialized = true;
Serial.println("TCA8418: Keyboard initialized OK");
return true;
}
// Read a key press - returns character or 0 if no key
char readKey() {
if (!_initialized) return 0;
// Check for key events in FIFO
uint8_t keyCount = readReg(TCA8418_REG_KEY_LCK_EC) & 0x0F;
if (keyCount == 0) return 0;
// Read key event from FIFO
uint8_t keyEvent = readReg(TCA8418_REG_KEY_EVENT_A);
// Bit 7: 1 = press, 0 = release
bool pressed = (keyEvent & 0x80) != 0;
uint8_t keyCode = keyEvent & 0x7F;
// Clear interrupt
writeReg(TCA8418_REG_INT_STAT, 0x1F);
Serial.printf("KB raw: event=0x%02X code=%d pressed=%d count=%d\n",
keyEvent, keyCode, pressed, keyCount);
// Only act on key press, not release
if (!pressed || keyCode == 0) {
return 0;
}
// Handle modifier keys - set sticky state and return 0
if (keyCode == 35 || keyCode == 31) { // Shift keys
_shiftActive = true;
_lastShiftTime = millis();
Serial.println("KB: Shift activated");
return 0;
}
if (keyCode == 30) { // Alt key
_altActive = true;
Serial.println("KB: Alt activated");
return 0;
}
if (keyCode == 32) { // Sym key (bottom row)
_symActive = true;
Serial.println("KB: Sym activated");
return 0;
}
// Handle dedicated $ key (key code 22, next to M)
if (keyCode == 22) {
Serial.println("KB: $ key pressed");
return '$';
}
// Handle Mic key - produces 0 with Sym, otherwise ignore
if (keyCode == 34) {
if (_symActive) {
_symActive = false;
Serial.println("KB: Sym+Mic -> '0'");
return '0';
}
return 0; // Ignore mic without Sym
}
// Get the character
char c = 0;
if (_altActive) {
c = getAltChar(keyCode);
_altActive = false; // Reset sticky alt
if (c != 0) {
Serial.printf("KB: Alt+key -> '%c'\n", c);
return c;
}
}
if (_symActive) {
c = getSymChar(keyCode);
_symActive = false; // Reset sticky sym
if (c != 0) {
Serial.printf("KB: Sym+key -> '%c'\n", c);
return c;
}
}
c = getKeyChar(keyCode);
if (c != 0 && _shiftActive) {
// Apply shift - uppercase letters
if (c >= 'a' && c <= 'z') {
c = c - 'a' + 'A';
}
_shiftActive = false; // Reset sticky shift
}
if (c != 0) {
Serial.printf("KB: code %d -> '%c' (0x%02X)\n", keyCode, c >= 32 ? c : '?', c);
} else {
Serial.printf("KB: code %d -> UNMAPPED\n", keyCode);
}
return c;
}
bool isReady() const { return _initialized; }
// Check if shift was pressed within the last N milliseconds
bool wasShiftRecentlyPressed(unsigned long withinMs = 500) const {
return (millis() - _lastShiftTime) < withinMs;
}
};

View File

@@ -113,9 +113,9 @@ extends = LilyGo_TDeck_Pro
build_flags =
${LilyGo_TDeck_Pro.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D BLE_PIN_CODE=234567
-D MAX_CONTACTS=400
-D MAX_GROUP_CHANNELS=20
-D BLE_PIN_CODE=123456
-D OFFLINE_QUEUE_SIZE=256
build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
+<helpers/esp32/*.cpp>

View File

@@ -19,10 +19,8 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock);
#if HAS_GPS
MicroNMEALocationProvider gps(Serial2, &rtc_clock);
EnvironmentSensorManager sensors(gps);
#pragma message "GPS enabled - using EnvironmentSensorManager with MicroNMEALocationProvider"
#else
SensorManager sensors;
#pragma message "GPS disabled - using basic SensorManager"
#endif
#ifdef DISPLAY_CLASS
@@ -31,37 +29,27 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock);
#endif
bool radio_init() {
Serial.println("radio_init() - starting");
MESH_DEBUG_PRINTLN("radio_init() - starting");
// NOTE: board.begin() is called by main.cpp setup() before radio_init()
// I2C is already initialized there with correct pins
fallback_clock.begin();
Serial.println("radio_init() - fallback_clock started");
MESH_DEBUG_PRINTLN("radio_init() - fallback_clock started");
// Wire already initialized in board.begin() - just use it for RTC
rtc_clock.begin(Wire);
Serial.println("radio_init() - rtc_clock started");
// Debug GPS status
#if HAS_GPS
Serial.println("radio_init() - HAS_GPS is defined");
Serial.print("radio_init() - gps object address: ");
Serial.println((uint32_t)&gps, HEX);
#else
Serial.println("radio_init() - HAS_GPS is NOT defined");
#endif
MESH_DEBUG_PRINTLN("radio_init() - rtc_clock started");
#if defined(P_LORA_SCLK)
Serial.println("radio_init() - initializing LoRa SPI...");
MESH_DEBUG_PRINTLN("radio_init() - initializing LoRa SPI...");
loraSpi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI, P_LORA_NSS);
Serial.println("radio_init() - SPI initialized, calling radio.std_init()...");
MESH_DEBUG_PRINTLN("radio_init() - SPI initialized, calling radio.std_init()...");
bool result = radio.std_init(&loraSpi);
Serial.print("radio_init() - radio.std_init() returned: ");
Serial.println(result ? "SUCCESS" : "FAILED");
MESH_DEBUG_PRINTLN("radio_init() - radio.std_init() returned: %s", result ? "SUCCESS" : "FAILED");
return result;
#else
Serial.println("radio_init() - calling radio.std_init() without custom SPI...");
MESH_DEBUG_PRINTLN("radio_init() - calling radio.std_init() without custom SPI...");
return radio.std_init();
#endif
}