mirror of
https://github.com/pelgraine/Meck.git
synced 2026-03-28 17:42:44 +01:00
Compare commits
27 Commits
firstbuild
...
txt-reader
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af9f41a541 | ||
|
|
0a746cdca5 | ||
|
|
3a5c48f440 | ||
|
|
e40d9ced4a | ||
|
|
b8de2d0d16 | ||
|
|
9fbc3202f6 | ||
|
|
9d91f48797 | ||
|
|
21eb385763 | ||
|
|
4b81e596d2 | ||
|
|
a5f2e8d055 | ||
|
|
462b1cb642 | ||
|
|
0b270c0e1a | ||
|
|
2730c05329 | ||
|
|
02d2fb08fb | ||
|
|
b0003e1896 | ||
|
|
0be77ef759 | ||
|
|
c5df40cefd | ||
|
|
5bdcbb25b6 | ||
|
|
53fe89b216 | ||
|
|
e194c2c48c | ||
|
|
9d401f76d3 | ||
|
|
15f392c80e | ||
|
|
621f9f9568 | ||
|
|
8c9106ca86 | ||
|
|
f4b9c89d9f | ||
|
|
a4f5328113 | ||
|
|
2ad02f49e6 |
139
README.md
139
README.md
@@ -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.
|
||||
@@ -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], ×tamp, 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,11 @@
|
||||
#define FIRMWARE_VER_CODE 8
|
||||
|
||||
#ifndef FIRMWARE_BUILD_DATE
|
||||
#define FIRMWARE_BUILD_DATE "28 Jan 2026"
|
||||
#define FIRMWARE_BUILD_DATE "7 Feb 2026"
|
||||
#endif
|
||||
|
||||
#ifndef FIRMWARE_VERSION
|
||||
#define FIRMWARE_VERSION "Meck v0.3"
|
||||
#define FIRMWARE_VERSION "Meck v0.6.3"
|
||||
#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;
|
||||
@@ -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
|
||||
292
examples/companion_radio/ui-new/ChannelScreen.h
Normal file
292
examples/companion_radio/ui-new/ChannelScreen.h
Normal 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;
|
||||
}
|
||||
};
|
||||
@@ -30,6 +30,7 @@
|
||||
#endif
|
||||
|
||||
#include "icons.h"
|
||||
#include "ChannelScreen.h"
|
||||
|
||||
class SplashScreen : public UIScreen {
|
||||
UITask* _task;
|
||||
@@ -103,29 +104,53 @@ class HomeScreen : public UIScreen {
|
||||
|
||||
|
||||
void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts) {
|
||||
// Convert millivolts to percentage
|
||||
const int minMilliVolts = 3000; // Minimum voltage (e.g., 3.0V)
|
||||
const int maxMilliVolts = 4200; // Maximum voltage (e.g., 4.2V)
|
||||
int batteryPercentage = ((batteryMilliVolts - minMilliVolts) * 100) / (maxMilliVolts - minMilliVolts);
|
||||
if (batteryPercentage < 0) batteryPercentage = 0; // Clamp to 0%
|
||||
if (batteryPercentage > 100) batteryPercentage = 100; // Clamp to 100%
|
||||
// Try to get accurate SOC from BQ27220 fuel gauge first
|
||||
uint8_t batteryPercentage = board.getBatteryPercent();
|
||||
|
||||
// Fall back to voltage-based estimation if fuel gauge returns 0
|
||||
if (batteryPercentage == 0 && batteryMilliVolts > 0) {
|
||||
const int minMilliVolts = 3000;
|
||||
const int maxMilliVolts = 4200;
|
||||
int pct = ((batteryMilliVolts - minMilliVolts) * 100) / (maxMilliVolts - minMilliVolts);
|
||||
if (pct < 0) pct = 0;
|
||||
if (pct > 100) pct = 100;
|
||||
batteryPercentage = (uint8_t)pct;
|
||||
}
|
||||
|
||||
// battery icon
|
||||
int iconWidth = 22;
|
||||
int iconHeight = 8;
|
||||
int iconX = display.width() - iconWidth - 5; // Position the icon near the top-right corner
|
||||
int iconY = 0;
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
|
||||
// battery icon dimensions (smaller to match tiny percentage text)
|
||||
int iconWidth = 16;
|
||||
int iconHeight = 6;
|
||||
|
||||
// measure percentage text width to position icon + text together at right edge
|
||||
display.setTextSize(0);
|
||||
char pctStr[5];
|
||||
sprintf(pctStr, "%d%%", batteryPercentage);
|
||||
uint16_t textWidth = display.getTextWidth(pctStr);
|
||||
|
||||
// layout: [icon 16px][cap 2px][gap 2px][text][margin 2px]
|
||||
int totalWidth = iconWidth + 2 + 2 + textWidth + 2;
|
||||
int iconX = display.width() - totalWidth;
|
||||
int iconY = 0; // vertically align with node name text
|
||||
|
||||
// battery outline
|
||||
display.drawRect(iconX, iconY, iconWidth, iconHeight);
|
||||
|
||||
// battery "cap"
|
||||
display.fillRect(iconX + iconWidth, iconY + (iconHeight / 4), 3, iconHeight / 2);
|
||||
display.fillRect(iconX + iconWidth, iconY + (iconHeight / 4), 2, iconHeight / 2);
|
||||
|
||||
// fill the battery based on the percentage
|
||||
int fillWidth = (batteryPercentage * (iconWidth - 4)) / 100;
|
||||
display.fillRect(iconX + 2, iconY + 2, fillWidth, iconHeight - 4);
|
||||
|
||||
// draw percentage text after the battery cap, offset upward to center with icon
|
||||
// (setCursor adds +5 internally for baseline, so compensate for the tiny font)
|
||||
int textX = iconX + iconWidth + 2 + 2; // after cap + gap
|
||||
int textY = iconY - 3; // offset up to vertically center with icon
|
||||
display.setCursor(textX, textY);
|
||||
display.print(pctStr);
|
||||
display.setTextSize(1); // restore default text size
|
||||
}
|
||||
|
||||
CayenneLPP sensors_lpp;
|
||||
@@ -580,6 +605,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 +654,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 +974,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);
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
};
|
||||
@@ -84,6 +84,10 @@ void GxEPDDisplay::startFrame(Color bkg) {
|
||||
void GxEPDDisplay::setTextSize(int sz) {
|
||||
display_crc.update<int>(sz);
|
||||
switch(sz) {
|
||||
case 0: // Tiny - built-in 6x8 pixel font
|
||||
display.setFont(NULL);
|
||||
display.setTextSize(1);
|
||||
break;
|
||||
case 1: // Small - use 9pt (was 9pt)
|
||||
display.setFont(&FreeSans9pt7b);
|
||||
break;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
290
variants/lilygo_tdeck_pro/Tca8418keyboard.h
Normal file
290
variants/lilygo_tdeck_pro/Tca8418keyboard.h
Normal 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;
|
||||
}
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user