3 Commits

19 changed files with 1309 additions and 125 deletions

View File

@@ -13,6 +13,7 @@ This fork was created specifically to focus on enabling BLE companion firmware f
- [Sending a Direct Message](#sending-a-direct-message)
- [Repeater Admin Screen](#repeater-admin-screen)
- [Settings Screen](#settings-screen)
- [Serial Settings (USB)](Serial_Settings_Guide.md)
- [Compose Mode](#compose-mode)
- [Symbol Entry (Sym Key)](#symbol-entry-sym-key)
- [Emoji Picker](#emoji-picker)
@@ -51,6 +52,8 @@ The T-Deck Pro BLE companion firmware includes full keyboard support for standal
| B | Open web browser (BLE and 4G variants only) |
| T | Open SMS & Phone app (4G variant only) |
| P | Open audiobook player (audio variant only) |
| F | Open node discovery (search for nearby repeaters/nodes) |
| G | Open map screen (shows contacts with GPS positions) |
| Q | Back to home screen |
### Bluetooth (BLE)
@@ -165,6 +168,8 @@ When adding a hashtag channel, type the channel name and press Enter. The channe
If you've changed radio parameters, pressing Q will prompt you to apply changes before exiting.
> **Tip:** All device settings (plus mesh tuning parameters not available on-screen) can also be configured via USB serial. See the [Serial Settings Guide](Serial_Settings_Guide.md) for complete documentation.
### Compose Mode
| Key | Action |
@@ -349,4 +354,4 @@ However, this firmware links against libraries with different license terms. Bec
| [base64](https://github.com/Densaugeo/base64_arduino) | MIT | densaugeo |
| [Arduino Crypto](https://github.com/rweather/arduinolibs) | MIT | Rhys Weatherley |
Full license texts for each dependency are available in their respective repositories linked above.
Full license texts for each dependency are available in their respective repositories linked above.

View File

@@ -59,6 +59,12 @@ All commands follow a simple pattern: `get` to read, `set` to write.
| `get notify` | Keyboard flash notification (on/off) |
| `get gps` | GPS status and interval |
| `get pin` | BLE pairing PIN |
| `get path.hash.mode` | Path hash size (0=1-byte, 1=2-byte, 2=3-byte) |
| `get rxdelay` | Rx delay base (0=disabled) |
| `get af` | Airtime factor |
| `get multi.acks` | Redundant ACKs (0 or 1) |
| `get int.thresh` | Interference threshold (0=disabled) |
| `get gps.baud` | GPS baud rate (0=compile-time default) |
| `get channels` | List all channels with index numbers |
| `get presets` | List all radio presets with parameters |
| `get pubkey` | Device public key (hex) |
@@ -163,6 +169,78 @@ set notify off
set pin 123456
```
#### Path Hash Mode
Controls the byte size of each repeater's identity stamp in forwarded flood packets. Larger hashes reduce collisions at the cost of fewer maximum hops.
```
set path.hash.mode 1
```
| Mode | Bytes/hop | Max hops | Notes |
|------|-----------|----------|-------|
| 0 | 1 | 64 | Legacy — prone to hash collisions in larger networks |
| 1 | 2 | 32 | Recommended — effectively eliminates collisions |
| 2 | 3 | 21 | Maximum precision, rarely needed |
Nodes with different modes can coexist — the mode only affects packets your node originates. The hash size is encoded in each packet's header, so receiving nodes adapt automatically.
### Mesh Tuning
These settings control how the device participates in the mesh network. They take effect immediately — no reboot required (except `gps.baud`).
#### Rx Delay (rxdelay)
Delays processing of flood packets based on signal quality. Stronger signals are processed first; weaker copies wait longer and are typically discarded as duplicates. Direct messages are always processed immediately.
```
set rxdelay 3
```
Range: 020 (0 = disabled, default). Higher values create larger timing differences between strong and weak signals. Values below 1.0 have no practical effect. See the [MeshSydney wiki](https://meshsydney.com/wiki) for detailed tuning profiles.
#### Airtime Factor (af)
Adjusts how long certain internal timing windows remain open. Does not change the LoRa radio parameters (SF, BW, CR) — those remain as configured.
```
set af 1.0
```
Range: 09 (default: 1.0). Keep this value consistent across nodes in your mesh for best coherence.
#### Multiple Acknowledgments (multi.acks)
Sends redundant ACK packets for direct messages. When enabled, two ACKs are sent (a multi-ack first, then the standard ACK), improving delivery confirmation reliability.
```
set multi.acks 1
```
Values: 0 (single ACK) or 1 (redundant ACKs, default).
#### Interference Threshold (int.thresh)
Enables channel activity scanning before transmitting. Not recommended unless your device is in a high RF interference environment — specifically where the noise floor is low but shows significant fluctuations indicating interference. Enabling this adds approximately 4 seconds of receive delay per packet.
```
set int.thresh 14
set int.thresh 0
```
Values: 0 (disabled, default) or 14+ (14 is the typical setting). Values between 113 are not functional and will be rejected.
#### GPS Baud Rate (gps.baud)
Override the GPS serial baud rate. The default (0) uses the compile-time value of 38400. **Requires a reboot to take effect** — the GPS serial port is only configured at startup.
```
set gps.baud 9600
set gps.baud 0
```
Valid rates: 0 (default), 4800, 9600, 19200, 38400, 57600, 115200.
### Channel Management
#### List Channels

43
boards/crowpanel.json Normal file
View File

@@ -0,0 +1,43 @@
{
"build": {
"arduino": {
"ldscript": "esp32s3_out.ld",
"memory_type": "qio_opi",
"partitions": "default_16MB.csv"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_USB_CDC_ON_BOOT=1",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=0"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"hwids": [["0x303A", "0x1001"]],
"mcu": "esp32s3",
"variant": "esp32s3"
},
"connectivity": ["wifi", "bluetooth", "lora"],
"debug": {
"default_tool": "esp-builtin",
"onboard_tools": ["esp-builtin"],
"openocd_target": "esp32s3.cfg"
},
"frameworks": ["arduino", "espidf"],
"name": "Elecrow CrowPanel (ESP32-S3 16MB Flash, 8MB PSRAM)",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 524288,
"maximum_size": 16777216,
"require_upload_port": true,
"speed": 921600
},
"monitor": {
"speed": 115200
},
"url": "https://www.elecrow.com/crowpanel-advance-hmi-intelligent-screen-esp32-ai-display.html",
"vendor": "Elecrow"
}

View File

@@ -257,7 +257,7 @@ float MyMesh::getAirtimeBudgetFactor() const {
}
int MyMesh::getInterferenceThreshold() const {
return 0; // disabled for now, until currentRSSI() problem is resolved
return _prefs.interference_threshold;
}
int MyMesh::calcRxDelay(float score, uint32_t air_time) const {
@@ -1133,6 +1133,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
// defaults
memset(&_prefs, 0, sizeof(_prefs));
_prefs.airtime_factor = 1.0; // one half
_prefs.multi_acks = 1; // redundant ACKs on by default
strcpy(_prefs.node_name, "NONAME");
_prefs.freq = LORA_FREQ;
_prefs.sf = LORA_SF;
@@ -1183,6 +1184,17 @@ void MyMesh::begin(bool has_display) {
_prefs.gps_enabled = constrain(_prefs.gps_enabled, 0, 1); // Ensure boolean 0 or 1
_prefs.gps_interval = constrain(_prefs.gps_interval, 0, 86400); // Max 24 hours
_prefs.utc_offset_hours = constrain(_prefs.utc_offset_hours, -12, 14); // Valid timezone range
// gps_baudrate: 0 means use compile-time default; validate known rates
if (_prefs.gps_baudrate != 0 && _prefs.gps_baudrate != 4800 &&
_prefs.gps_baudrate != 9600 && _prefs.gps_baudrate != 19200 &&
_prefs.gps_baudrate != 38400 && _prefs.gps_baudrate != 57600 &&
_prefs.gps_baudrate != 115200) {
_prefs.gps_baudrate = 0; // reset to default if invalid
}
// interference_threshold: 0 = disabled, minimum functional value is 14
if (_prefs.interference_threshold > 0 && _prefs.interference_threshold < 14) {
_prefs.interference_threshold = 0;
}
#ifdef BLE_PIN_CODE // 123456 by default
if (_prefs.ble_pin == 0) {
@@ -2130,20 +2142,26 @@ void MyMesh::enterCLIRescue() {
void MyMesh::checkCLIRescueCmd() {
int len = strlen(cli_command);
bool line_complete = false;
while (Serial.available() && len < sizeof(cli_command)-1) {
char c = Serial.read();
if (c != '\n') {
cli_command[len++] = c;
cli_command[len] = 0;
if (c == '\r' || c == '\n') {
if (len > 0) {
line_complete = true;
Serial.println(); // echo newline
}
break; // stop reading — remaining LF (from CR+LF) is consumed next loop
}
cli_command[len++] = c;
cli_command[len] = 0;
Serial.print(c); // echo
}
if (len == sizeof(cli_command)-1) { // command buffer full
cli_command[sizeof(cli_command)-1] = '\r';
if (len == sizeof(cli_command)-1) { // buffer full — force processing
line_complete = true;
}
if (len > 0 && cli_command[len - 1] == '\r') { // received complete line
cli_command[len - 1] = 0; // replace newline with C string null terminator
if (line_complete && len > 0) {
cli_command[len] = 0; // ensure null terminated
// =====================================================================
// GET commands — read settings
@@ -2174,6 +2192,21 @@ void MyMesh::checkCLIRescueCmd() {
_prefs.gps_enabled ? "on" : "off", _prefs.gps_interval);
} else if (strcmp(key, "pin") == 0) {
Serial.printf(" > %06d\n", _prefs.ble_pin);
// --- Mesh tuning parameters ---
} else if (strcmp(key, "rxdelay") == 0) {
Serial.printf(" > %.1f\n", _prefs.rx_delay_base);
} else if (strcmp(key, "af") == 0) {
Serial.printf(" > %.1f\n", _prefs.airtime_factor);
} else if (strcmp(key, "multi.acks") == 0) {
Serial.printf(" > %d\n", _prefs.multi_acks);
} else if (strcmp(key, "int.thresh") == 0) {
Serial.printf(" > %d\n", _prefs.interference_threshold);
} else if (strcmp(key, "gps.baud") == 0) {
uint32_t effective = _prefs.gps_baudrate ? _prefs.gps_baudrate : GPS_BAUDRATE;
Serial.printf(" > %lu (effective: %lu)\n",
(unsigned long)_prefs.gps_baudrate, (unsigned long)effective);
} else if (strcmp(key, "radio") == 0) {
Serial.printf(" > freq=%.3f bw=%.1f sf=%d cr=%d tx=%d\n",
_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr, _prefs.tx_power_dbm);
@@ -2225,6 +2258,14 @@ void MyMesh::checkCLIRescueCmd() {
Serial.printf(" gps: %s (interval: %ds)\n",
_prefs.gps_enabled ? "on" : "off", _prefs.gps_interval);
Serial.printf(" pin: %06d\n", _prefs.ble_pin);
Serial.printf(" rxdelay: %.1f\n", _prefs.rx_delay_base);
Serial.printf(" af: %.1f\n", _prefs.airtime_factor);
Serial.printf(" multi.acks: %d\n", _prefs.multi_acks);
Serial.printf(" int.thresh: %d\n", _prefs.interference_threshold);
{
uint32_t eff_baud = _prefs.gps_baudrate ? _prefs.gps_baudrate : GPS_BAUDRATE;
Serial.printf(" gps.baud: %lu\n", (unsigned long)eff_baud);
}
#ifdef HAS_4G_MODEM
Serial.printf(" modem: %s\n", ModemManager::loadEnabledConfig() ? "on" : "off");
Serial.printf(" apn: %s\n", modemManager.getAPN());
@@ -2558,6 +2599,69 @@ void MyMesh::checkCLIRescueCmd() {
Serial.println(" > modem disabled");
#endif
// --- Mesh tuning parameters ---
} else if (memcmp(config, "rxdelay ", 8) == 0) {
float val = atof(&config[8]);
if (val >= 0.0f && val <= 20.0f) {
_prefs.rx_delay_base = val;
savePrefs();
Serial.printf(" > rxdelay = %.1f\n", _prefs.rx_delay_base);
} else {
Serial.println(" Error: rxdelay out of range (0-20)");
}
} else if (memcmp(config, "af ", 3) == 0) {
float val = atof(&config[3]);
if (val >= 0.0f && val <= 9.0f) {
_prefs.airtime_factor = val;
savePrefs();
Serial.printf(" > af = %.1f\n", _prefs.airtime_factor);
} else {
Serial.println(" Error: af out of range (0-9)");
}
} else if (memcmp(config, "multi.acks ", 11) == 0) {
int val = atoi(&config[11]);
if (val == 0 || val == 1) {
_prefs.multi_acks = (uint8_t)val;
savePrefs();
Serial.printf(" > multi.acks = %d\n", _prefs.multi_acks);
} else {
Serial.println(" Error: use 0 or 1");
}
// Interference threshold — not recommended unless the device is in a high
// RF interference environment (low noise floor with significant fluctuations).
// Enabling adds ~4s receive delay per packet for channel activity scanning.
} else if (memcmp(config, "int.thresh ", 11) == 0) {
int val = atoi(&config[11]);
if (val == 0) {
_prefs.interference_threshold = 0;
savePrefs();
Serial.println(" > int.thresh = 0 (disabled)");
} else if (val >= 14 && val <= 255) {
_prefs.interference_threshold = (uint8_t)val;
savePrefs();
Serial.printf(" > int.thresh = %d (enabled — adds ~4s rx delay)\n",
_prefs.interference_threshold);
Serial.println(" Note: only recommended for high RF interference environments");
} else {
Serial.println(" Error: use 0 (disabled) or 14+ (typical: 14)");
}
} else if (memcmp(config, "gps.baud ", 9) == 0) {
uint32_t val = (uint32_t)atol(&config[9]);
if (val == 0 || val == 4800 || val == 9600 || val == 19200 ||
val == 38400 || val == 57600 || val == 115200) {
_prefs.gps_baudrate = val;
savePrefs();
uint32_t effective = val ? val : GPS_BAUDRATE;
Serial.printf(" > gps.baud = %lu (effective: %lu, reboot to apply)\n",
(unsigned long)val, (unsigned long)effective);
} else {
Serial.println(" Error: use 0 (default), 4800, 9600, 19200, 38400, 57600, or 115200");
}
} else {
Serial.printf(" Error: unknown setting '%s' (try 'help')\n", config);
}
@@ -2574,6 +2678,13 @@ void MyMesh::checkCLIRescueCmd() {
Serial.println(" name, freq, bw, sf, cr, tx, utc, notify, pin");
Serial.println(" path.hash.mode Path hash size (0=1B, 1=2B, 2=3B)");
Serial.println("");
Serial.println(" Mesh tuning:");
Serial.println(" rxdelay <0-20> Rx delay base (0=disabled)");
Serial.println(" af <0-9> Airtime factor");
Serial.println(" multi.acks <0|1> Redundant ACKs (default: 1)");
Serial.println(" int.thresh <0|14+> Interference threshold dB (0=off, 14=typical)");
Serial.println(" gps.baud <rate> GPS baud (0=default, reboot to apply)");
Serial.println("");
Serial.println(" Compound commands:");
Serial.println(" get all Dump all settings");
Serial.println(" get radio Show all radio params");

View File

@@ -8,11 +8,11 @@
#define FIRMWARE_VER_CODE 10
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "7 March 2026"
#define FIRMWARE_BUILD_DATE "8 March 2026"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "Meck v0.9.9"
#define FIRMWARE_VERSION "Meck v1.0"
#endif
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)

View File

@@ -33,4 +33,6 @@ struct NodePrefs { // persisted to file
uint8_t ringtone_enabled; // Ringtone on incoming call (0=off, 1=on) — 4G only
uint8_t path_hash_mode; // 0=1-byte (legacy), 1=2-byte, 2=3-byte path hashes
uint8_t autoadd_max_hops; // 0=no limit, N=up to N-1 hops (max 64)
uint32_t gps_baudrate; // GPS baud rate (0 = use compile-time GPS_BAUDRATE default)
uint8_t interference_threshold; // Interference threshold in dB (0=disabled, 14+=enabled)
};

View File

@@ -343,6 +343,18 @@
}
#endif
// --- Non-T-Deck ESP32 targets (CrowPanel, etc.) ---
// Variables declared inside the LilyGo_TDeck_Pro block above that are
// referenced unconditionally in setup()/loop() need parallel declarations.
#if !defined(LilyGo_TDeck_Pro) && defined(ESP32)
CPUPowerManager cpuPower;
#define AGC_RESET_INTERVAL_MS 500
static unsigned long lastAGCReset = 0;
static bool readerMode = false;
static bool notesMode = false;
static bool audiobookMode = false;
#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;
@@ -730,8 +742,12 @@ void setup() {
// 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);
MESH_DEBUG_PRINTLN("setup() - Reinitialized Serial2 for GPS after sensors.begin()");
{
uint32_t gps_baud = the_mesh.getNodePrefs()->gps_baudrate;
if (gps_baud == 0) gps_baud = GPS_BAUDRATE;
Serial2.begin(gps_baud, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN);
MESH_DEBUG_PRINTLN("setup() - Reinitialized Serial2 for GPS at %lu baud", (unsigned long)gps_baud);
}
#endif
#ifdef DISPLAY_CLASS
@@ -745,8 +761,8 @@ void setup() {
initKeyboard();
#endif
// Initialize touch input (CST328)
#ifdef HAS_TOUCHSCREEN
// Initialize touch input (CST328 — T-Deck Pro only; CrowPanel uses GT911 via LovyanGFX)
#if defined(HAS_TOUCHSCREEN) && defined(CST328_PIN_INT)
if (touchInput.begin(CST328_PIN_INT)) {
MESH_DEBUG_PRINTLN("setup() - Touch input initialized");
} else {
@@ -906,7 +922,7 @@ void loop() {
cpuPower.loop();
// Audiobook: service audio decode regardless of which screen is active
#ifndef HAS_4G_MODEM
#if defined(LilyGo_TDeck_Pro) && !defined(HAS_4G_MODEM)
{
AudiobookPlayerScreen* abPlayer =
(AudiobookPlayerScreen*)ui_task.getAudiobookScreen();
@@ -1106,10 +1122,12 @@ void loop() {
#endif
rtc_clock.tick();
// Periodic AGC reset - re-assert boosted RX gain to prevent sensitivity drift
#if defined(LilyGo_TDeck_Pro)
if ((millis() - lastAGCReset) >= AGC_RESET_INTERVAL_MS) {
radio_reset_agc();
lastAGCReset = millis();
}
#endif
// Handle T-Deck Pro keyboard input
#if defined(LilyGo_TDeck_Pro)
handleKeyboardInput();

View File

@@ -1,107 +1,50 @@
#pragma once
#include <SPI.h>
#include <Wire.h>
// =============================================================================
// GxEPDDisplay STUB for CrowPanel (and other non-e-ink LGFX targets)
//
// This file shadows src/helpers/ui/GxEPDDisplay.h to prevent the LovyanGFX vs
// Adafruit_GFX GFXfont type collision at link time. MapScreen.h unconditionally
// includes GxEPDDisplay.h and uses a GxEPDDisplay* member — this stub provides
// the minimal API so that compilation and linking succeed.
//
// On CrowPanel the map screen is inert (no SD card when function switch is in
// WM mode, no keyboard to navigate to it). The _einkDisplay pointer in
// MapScreen will be a bad cast but is null-checked before every draw call.
// =============================================================================
#define ENABLE_GxEPD2_GFX 0
#include <helpers/ui/DisplayDriver.h>
#include <GxEPD2_BW.h>
#include <GxEPD2_3C.h>
#include <GxEPD2_4C.h>
#include <GxEPD2_7C.h>
#include <Fonts/FreeSans9pt7b.h>
#include <Fonts/FreeSansBold12pt7b.h>
#include <Fonts/FreeSans18pt7b.h>
// Inline CRC32 for frame change detection (replaces bakercp/CRC32
// to avoid naming collision with PNGdec's bundled CRC32.h)
class FrameCRC32 {
uint32_t _crc = 0xFFFFFFFF;
public:
void reset() { _crc = 0xFFFFFFFF; }
template<typename T> void update(T val) {
const uint8_t* p = (const uint8_t*)&val;
for (size_t i = 0; i < sizeof(T); i++) {
_crc ^= p[i];
for (int b = 0; b < 8; b++)
_crc = (_crc >> 1) ^ (0xEDB88320 & -(int32_t)(_crc & 1));
}
}
template<typename T> void update(const T* data, size_t len) {
const uint8_t* p = (const uint8_t*)data;
for (size_t i = 0; i < len * sizeof(T); i++) {
_crc ^= p[i];
for (int b = 0; b < 8; b++)
_crc = (_crc >> 1) ^ (0xEDB88320 & -(int32_t)(_crc & 1));
}
}
uint32_t finalize() { return _crc ^ 0xFFFFFFFF; }
};
#include "DisplayDriver.h"
// GxEPD color constants referenced by MapScreen.h
#ifndef GxEPD_BLACK
#define GxEPD_BLACK 0
#define GxEPD_WHITE 1
#endif
class GxEPDDisplay : public DisplayDriver {
#if defined(EINK_DISPLAY_MODEL)
GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT> display;
const float scale_x = EINK_SCALE_X;
const float scale_y = EINK_SCALE_Y;
const float offset_x = EINK_X_OFFSET;
const float offset_y = EINK_Y_OFFSET;
#else
GxEPD2_BW<GxEPD2_150_BN, 200> display;
const float scale_x = 1.5625f;
const float scale_y = 1.5625f;
const float offset_x = 0;
const float offset_y = 10;
#endif
bool _init = false;
bool _isOn = false;
uint16_t _curr_color;
FrameCRC32 display_crc;
int last_display_crc_value = 0;
public:
#if defined(EINK_DISPLAY_MODEL)
GxEPDDisplay() : DisplayDriver(128, 128), display(EINK_DISPLAY_MODEL(PIN_DISPLAY_CS, PIN_DISPLAY_DC, PIN_DISPLAY_RST, PIN_DISPLAY_BUSY)) {}
#else
GxEPDDisplay() : DisplayDriver(128, 128), display(GxEPD2_150_BN(DISP_CS, DISP_DC, DISP_RST, DISP_BUSY)) {}
#endif
GxEPDDisplay() : DisplayDriver(128, 128) {}
bool begin();
// --- MapScreen raw pixel API (stubs) ---
void drawPixelRaw(int16_t x, int16_t y, uint16_t color) {}
int16_t rawWidth() { return 0; }
int16_t rawHeight() { return 0; }
void drawTextRaw(int16_t x, int16_t y, const char* text, uint16_t color) {}
void invalidateFrameCRC() {}
bool isOn() override {return _isOn;};
void turnOn() override;
void turnOff() override;
void clear() override;
void startFrame(Color bkg = DARK) override;
void setTextSize(int sz) override;
void setColor(Color c) override;
void setCursor(int x, int y) override;
void print(const char* str) override;
void fillRect(int x, int y, int w, int h) override;
void drawRect(int x, int y, int w, int h) override;
void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override;
uint16_t getTextWidth(const char* str) override;
void endFrame() override;
// --- Raw pixel access for MapScreen (bypasses scaling) ---
void drawPixelRaw(int16_t x, int16_t y, uint16_t color) {
display.drawPixel(x, y, color);
}
int16_t rawWidth() { return display.width(); }
int16_t rawHeight() { return display.height(); }
// Draw text at raw (unscaled) physical coordinates using built-in 5x7 font
void drawTextRaw(int16_t x, int16_t y, const char* text, uint16_t color) {
display.setFont(NULL); // Built-in 5x7 font
display.setTextSize(1);
display.setTextColor(color);
display.setCursor(x, y);
display.print(text);
}
// Force endFrame() to push to display even if CRC unchanged
// (needed because drawPixelRaw bypasses CRC tracking)
void invalidateFrameCRC() { last_display_crc_value = 0; }
// --- DisplayDriver pure virtuals (no-op implementations) ---
bool isOn() override { return false; }
void turnOn() override {}
void turnOff() override {}
void clear() override {}
void startFrame(Color bkg = DARK) override {}
void setTextSize(int sz) override {}
void setColor(Color c) override {}
void setCursor(int x, int y) override {}
void print(const char* str) override {}
void fillRect(int x, int y, int w, int h) override {}
void drawRect(int x, int y, int w, int h) override {}
void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override {}
uint16_t getTextWidth(const char* str) override { return 0; }
void endFrame() override {}
};

View File

@@ -0,0 +1,74 @@
#pragma once
#include <Arduino.h>
#include <Wire.h>
#include <helpers/ESP32Board.h>
// CrowPanel 7.0" Advance Series
//
// V1.0: PCA9557/TCA9534 I/O expander at 0x18 for display power/reset/backlight
// V1.2: STC8H1K28 MCU at 0x30 (6-step brightness)
// V1.3: STC8H1K28 MCU at 0x30 (246-step brightness)
//
// I2C bus (shared between touch GT911, RTC at 0x51, and I/O controller)
// SDA = IO15, SCL = IO16
//
// Function select switch (active-low DIP switches on PCB rear):
// S1=0 S0=0 → MIC & SPK (IO4/5/6 to speaker, IO19/20 to mic)
// S1=0 S0=1 → WM (IO4/5/6 + IO19/20 to wireless module) ← REQUIRED FOR LORA
// S1=1 S0=1 → MIC & TF Card (IO4/5/6 to SD, IO19/20 to mic)
#define PIN_BOARD_SDA 15
#define PIN_BOARD_SCL 16
// Touch pins (GT911 at 0x5D)
#define PIN_TOUCH_SDA PIN_BOARD_SDA
#define PIN_TOUCH_SCL PIN_BOARD_SCL
#define PIN_TOUCH_INT 1 // IO1_TP_INT (active low pulse, not level-based)
#define PIN_TOUCH_RST -1 // Controlled via STC8H1K28 P1.7 (v1.3) or TCA9534 (v1.0)
// STC8H1K28 I2C address (v1.2/v1.3 only)
#define STC8H_ADDR 0x30
class CrowPanel70Board : public ESP32Board {
public:
void begin() {
// NOTE: Wire.begin(SDA, SCL) is called in target.cpp radio_init() BEFORE
// lcd.init(), to ensure correct init order for v1.3 STC8H1K28 backlight.
// Do NOT call Wire.begin() here — it would conflict with LovyanGFX's
// internal Wire usage for the GT911 touch controller.
ESP32Board::begin();
}
const char* getManufacturerName() const override {
#ifdef CROWPANEL_V13
return "CrowPanel 7.0 V1.3";
#else
return "CrowPanel 7.0";
#endif
}
// --- STC8H1K28 control (v1.3) ---
// These are safe to call on all versions; on v1.0 they'll talk to
// a nonexistent I2C device and silently fail.
void setBacklightBrightness(uint8_t level) {
// 0 = max, 244 = min, 245 = off
Wire.beginTransmission(STC8H_ADDR);
Wire.write(level);
Wire.endTransmission();
}
void buzzerOn() {
Wire.beginTransmission(STC8H_ADDR);
Wire.write(246);
Wire.endTransmission();
}
void buzzerOff() {
Wire.beginTransmission(STC8H_ADDR);
Wire.write(247);
Wire.endTransmission();
}
};

View File

@@ -0,0 +1,33 @@
#pragma once
#include <helpers/ui/LGFXDisplay.h>
#include "LGFX_CrowPanel70.h"
// Custom display class for CrowPanel 7.0" that handles native landscape touch coordinates
class CrowPanel70Display : public LGFXDisplay {
public:
CrowPanel70Display(int w, int h, LGFX_Device &disp) : LGFXDisplay(w, h, disp) {}
// Override getTouch for native landscape display (800x480)
// The 7" panel is natively landscape, unlike the 3.5" which is portrait rotated
//
// GT911 touch coords are in physical space (0-799, 0-479).
// We map them to logical space using:
// display->width()/height() = physical LGFX dimensions (800, 480)
// width()/height() = logical DisplayDriver dimensions (128, 64)
bool getTouch(int *x, int *y) override {
lgfx::v1::touch_point_t point;
int touch_count = display->getTouch(&point);
if (touch_count > 0) {
// Physical touch → logical coords
*x = point.x * width() / display->width();
*y = point.y * height() / display->height();
return true;
}
*x = -1;
*y = -1;
return false;
}
};

View File

@@ -0,0 +1,204 @@
#pragma once
#define LGFX_USE_V1
#include <LovyanGFX.hpp>
#ifndef CROWPANEL_V13
// V1.0 uses TCA9534 I/O expander at 0x18
#include <TCA9534.h>
#endif
#include <lgfx/v1/platforms/esp32s3/Panel_RGB.hpp>
#include <lgfx/v1/platforms/esp32s3/Bus_RGB.hpp>
// CrowPanel 7.0" uses RGB parallel interface (16-bit) with:
// V1.0: TCA9534 I/O expander at 0x18 for display power/reset/backlight
// V1.2/V1.3: STC8H1K28 MCU at 0x30 for backlight, buzzer, speaker control
//
// Display: 800x480 native landscape, ST7277 driver IC
// Touch: GT911 at 0x5D on I2C (SDA=15, SCL=16)
//
// STC8H1K28 (v1.3) command byte reference:
// 0 = backlight max brightness
// 1-244 = backlight dimmer (244 = dimmest)
// 245 = backlight off
// 246 = buzzer on
// 247 = buzzer off
// 248 = speaker amp on
// 249 = speaker amp off
#define STC8H_I2C_ADDR 0x30
class LGFX_CrowPanel70 : public lgfx::LGFX_Device {
lgfx::Bus_RGB _bus_instance;
lgfx::Panel_RGB _panel_instance;
lgfx::Touch_GT911 _touch_instance;
#ifndef CROWPANEL_V13
TCA9534 _ioex;
#endif
public:
static constexpr uint16_t SCREEN_WIDTH = 800;
static constexpr uint16_t SCREEN_HEIGHT = 480;
// --- STC8H1K28 helper (v1.3) ---
static void sendSTC8Command(uint8_t cmd) {
Wire.beginTransmission(STC8H_I2C_ADDR);
Wire.write(cmd);
Wire.endTransmission();
}
// Set backlight brightness: 0 = max, 244 = min, 245 = off
static void setBacklight(uint8_t brightness) {
#ifdef CROWPANEL_V13
sendSTC8Command(brightness);
#endif
}
static void buzzerOn() { sendSTC8Command(246); }
static void buzzerOff() { sendSTC8Command(247); }
static void speakerOn() { sendSTC8Command(248); }
static void speakerOff() { sendSTC8Command(249); }
bool init_impl(bool use_reset, bool use_clear) override {
#ifdef CROWPANEL_V13
// ---- V1.3: All I2C + GPIO init done externally in target.cpp ----
// Wire.begin(), STC8H backlight, and GPIO1 touch reset pulse are
// called BEFORE lcd.init() to avoid Wire double-init conflicts and
// ensure the display is powered before Panel_RGB allocates framebuffer.
#else
// ---- V1.0: TCA9534 init ----
_ioex.attach(Wire);
_ioex.setDeviceAddress(0x18);
// Configure TCA9534 pins as outputs
_ioex.config(1, TCA9534::Config::OUT); // Display power
_ioex.config(2, TCA9534::Config::OUT); // Display reset
_ioex.config(3, TCA9534::Config::OUT); // Not used
_ioex.config(4, TCA9534::Config::OUT); // Backlight
// Power on display
_ioex.output(1, TCA9534::Level::H);
// Reset sequence
pinMode(1, OUTPUT);
digitalWrite(1, LOW);
_ioex.output(2, TCA9534::Level::L);
delay(20);
_ioex.output(2, TCA9534::Level::H);
delay(100);
pinMode(1, INPUT);
// Turn on backlight
_ioex.output(4, TCA9534::Level::H);
#endif
return LGFX_Device::init_impl(use_reset, use_clear);
}
LGFX_CrowPanel70(void) {
// Panel configuration
{
auto cfg = _panel_instance.config();
cfg.memory_width = SCREEN_WIDTH;
cfg.memory_height = SCREEN_HEIGHT;
cfg.panel_width = SCREEN_WIDTH;
cfg.panel_height = SCREEN_HEIGHT;
cfg.offset_x = 0;
cfg.offset_y = 0;
cfg.offset_rotation = 0; // Panel_RGB: rotation not supported via offset_rotation
// Display renders in portrait (480x800 physical). Landscape rotation will
// be handled by swapping the CrowPanel70Display coordinate mapping.
_panel_instance.config(cfg);
}
// Panel detail configuration
{
auto cfg = _panel_instance.config_detail();
cfg.use_psram = 1; // Use PSRAM for frame buffer
_panel_instance.config_detail(cfg);
}
// RGB bus configuration
// Pin mapping is identical across all CrowPanel 7" hardware versions
{
auto cfg = _bus_instance.config();
cfg.panel = &_panel_instance;
// Blue (B3-B7 on panel, 5 bits)
cfg.pin_d0 = 21; // B3
cfg.pin_d1 = 47; // B4
cfg.pin_d2 = 48; // B5
cfg.pin_d3 = 45; // B6
cfg.pin_d4 = 38; // B7
// Green (G2-G7 on panel, 6 bits)
cfg.pin_d5 = 9; // G2
cfg.pin_d6 = 10; // G3
cfg.pin_d7 = 11; // G4
cfg.pin_d8 = 12; // G5
cfg.pin_d9 = 13; // G6
cfg.pin_d10 = 14; // G7
// Red (R3-R7 on panel, 5 bits)
cfg.pin_d11 = 7; // R3
cfg.pin_d12 = 17; // R4
cfg.pin_d13 = 18; // R5
cfg.pin_d14 = 3; // R6
cfg.pin_d15 = 46; // R7
// Control pins
cfg.pin_henable = 42; // DE (Data Enable)
cfg.pin_vsync = 41; // VSYNC
cfg.pin_hsync = 40; // HSYNC
cfg.pin_pclk = 39; // Pixel clock (DCLK)
// Timing configuration (14MHz pixel clock for 7" display)
cfg.freq_write = 14000000;
// Horizontal timing
cfg.hsync_polarity = 0;
cfg.hsync_front_porch = 8;
cfg.hsync_pulse_width = 4;
cfg.hsync_back_porch = 8;
// Vertical timing
cfg.vsync_polarity = 0;
cfg.vsync_front_porch = 8;
cfg.vsync_pulse_width = 4;
cfg.vsync_back_porch = 8;
// Clock configuration
cfg.pclk_idle_high = 1;
cfg.pclk_active_neg = 0;
_bus_instance.config(cfg);
}
_panel_instance.setBus(&_bus_instance);
// Touch configuration (GT911 at 0x5D)
{
auto cfg = _touch_instance.config();
cfg.x_min = 0;
cfg.x_max = SCREEN_WIDTH - 1;
cfg.y_min = 0;
cfg.y_max = SCREEN_HEIGHT - 1;
cfg.pin_int = -1; // IO1 is TP_INT but we poll, not interrupt-driven
cfg.pin_rst = -1; // Reset via STC8H1K28 (v1.3) or TCA9534 (v1.0)
cfg.bus_shared = true;
cfg.offset_rotation = 0; // Match panel config
cfg.i2c_port = 0;
cfg.i2c_addr = 0x5D;
cfg.pin_sda = 15;
cfg.pin_scl = 16;
cfg.freq = 400000;
_touch_instance.config(cfg);
_panel_instance.setTouch(&_touch_instance);
}
setPanel(&_panel_instance);
}
};

View File

@@ -0,0 +1,70 @@
#pragma once
#include <Arduino.h>
// CPU Frequency Scaling for ESP32-S3
//
// Typical current draw (CPU only, rough):
// 240 MHz ~70-80 mA
// 160 MHz ~50-60 mA
// 80 MHz ~30-40 mA
//
// SPI peripherals and UART use their own clock dividers from the APB clock,
// so LoRa, e-ink, and GPS serial all work fine at 80MHz.
#ifdef ESP32
#ifndef CPU_FREQ_IDLE
#define CPU_FREQ_IDLE 80 // MHz — normal mesh listening
#endif
#ifndef CPU_FREQ_BOOST
#define CPU_FREQ_BOOST 240 // MHz — heavy processing
#endif
#ifndef CPU_BOOST_TIMEOUT_MS
#define CPU_BOOST_TIMEOUT_MS 10000 // 10 seconds
#endif
class CPUPowerManager {
public:
CPUPowerManager() : _boosted(false), _boost_started(0) {}
void begin() {
setCpuFrequencyMhz(CPU_FREQ_IDLE);
_boosted = false;
MESH_DEBUG_PRINTLN("CPU power: idle at %d MHz", CPU_FREQ_IDLE);
}
void loop() {
if (_boosted && (millis() - _boost_started >= CPU_BOOST_TIMEOUT_MS)) {
setIdle();
}
}
void setBoost() {
if (!_boosted) {
setCpuFrequencyMhz(CPU_FREQ_BOOST);
_boosted = true;
MESH_DEBUG_PRINTLN("CPU power: boosted to %d MHz", CPU_FREQ_BOOST);
}
_boost_started = millis();
}
void setIdle() {
if (_boosted) {
setCpuFrequencyMhz(CPU_FREQ_IDLE);
_boosted = false;
MESH_DEBUG_PRINTLN("CPU power: idle at %d MHz", CPU_FREQ_IDLE);
}
}
bool isBoosted() const { return _boosted; }
uint32_t getFrequencyMHz() const { return getCpuFrequencyMhz(); }
private:
bool _boosted;
unsigned long _boost_started;
};
#endif // ESP32

View File

@@ -0,0 +1,44 @@
#ifndef Pins_Arduino_h
#define Pins_Arduino_h
#include <stdint.h>
// USB Serial (native USB CDC on ESP32-S3)
static const uint8_t TX = 43;
static const uint8_t RX = 44;
// I2C (shared between touch GT911, RTC PCF85063 at 0x51, STC8H1K28 at 0x30)
static const uint8_t SDA = 15;
static const uint8_t SCL = 16;
// Default SPI — mapped to wireless module slot (active when function switch = WM)
// V1.3 LoRa pin mapping (confirmed from board silkscreen):
// NSS=19, DIO1=20, RESET=8, BUSY=2, SPI on 4/5/6
// V1.0 LoRa pin mapping (original):
// NSS=0, DIO1=20, RESET=19, BUSY=2, SPI on 4/5/6
#ifdef CROWPANEL_V13
static const uint8_t SS = 20; // LoRa NSS (V1.3: GPIO20, confirmed by SPI probe)
#else
static const uint8_t SS = 0; // LoRa NSS (V1.0: on boot strapping pin)
#endif
static const uint8_t MOSI = 6; // LoRa MOSI / SD MOSI / I2S LRCLK (shared)
static const uint8_t MISO = 4; // LoRa MISO / SD MISO / I2S SDIN (shared)
static const uint8_t SCK = 5; // LoRa SCK / SD CLK / I2S BCLK (shared)
// Analog pins
static const uint8_t A0 = 1;
static const uint8_t A1 = 2;
static const uint8_t A2 = 3;
static const uint8_t A3 = 4;
static const uint8_t A4 = 5;
static const uint8_t A5 = 6;
// Touch pins
static const uint8_t T1 = 1;
static const uint8_t T2 = 2;
static const uint8_t T3 = 3;
static const uint8_t T4 = 4;
static const uint8_t T5 = 5;
static const uint8_t T6 = 6;
#endif /* Pins_Arduino_h */

View File

@@ -0,0 +1,143 @@
; =============================================================================
; CrowPanel 7.0" Advance Series — shared base configuration
; =============================================================================
; Display: 800x480 IPS, ST7277 driver, RGB parallel 16-bit
; Touch: GT911 at I2C 0x5D
; MCU: ESP32-S3-WROOM-1-N16R8 (16MB flash, 8MB OPSRAM)
;
; IMPORTANT: The PCB function select DIP switch (rear of board) MUST be set
; to WM mode (S0=1, S1=0) for LoRa wireless module operation.
; =============================================================================
[crowpanel_70_base]
extends = esp32_base
board = crowpanel
build_flags =
${esp32_base.build_flags}
-I variants/crowpanel_70
-D CROWPANEL_70
; Route Arduino Serial to UART0 (CH340 USB bridge) instead of native USB CDC.
; The crowpanel.json board def sets CDC_ON_BOOT=1 but the user monitors via
; the CH340 port (/dev/cu.wchusbserial*), not the native USB-C port.
-D ARDUINO_USB_CDC_ON_BOOT=0
; I2C pins (shared: touch, RTC, I/O controller)
-D PIN_BOARD_SDA=15
-D PIN_BOARD_SCL=16
; Radio configuration (common across versions)
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D SX126X_DIO2_AS_RF_SWITCH=true
-D SX126X_DIO3_TCXO_VOLTAGE=3.3
-D SX126X_CURRENT_LIMIT=140
-D SX126X_RX_BOOSTED_GAIN=1
-D LORA_TX_POWER=22
; SPI bus pins (same all versions — active when function switch = WM)
-D P_LORA_MOSI=6
-D P_LORA_MISO=4
-D P_LORA_SCLK=5
build_src_filter = ${esp32_base.build_src_filter}
+<../variants/crowpanel_70>
+<helpers/sensors>
lib_deps =
${esp32_base.lib_deps}
; CRITICAL: Use LovyanGFX 1.2.0 — v1.2.7 breaks 7" RGB panel init!
lovyan03/LovyanGFX@1.2.0
; PNGdec for map tile rendering
bitbank2/PNGdec@^1.1.1
; NOTE: GxEPD2 + Adafruit GFX NOT needed — CrowPanel variant shadows
; GxEPDDisplay.h with a stub to avoid LovyanGFX/Adafruit_GFX type collision
; =============================================================================
; V1.3 hardware (current production — STC8H1K28 at 0x30)
; =============================================================================
; LoRa pins confirmed from V1.3 board silkscreen (wireless module connector):
; IO19=NSS, IO20=DIO1, IO8=RESET, IO2=BUSY
; IO8 was freed when buzzer moved from direct GPIO to STC8H1K28
; =============================================================================
[crowpanel_70_v13]
extends = crowpanel_70_base
build_flags =
${crowpanel_70_base.build_flags}
-D CROWPANEL_V13
; V1.3 LoRa pin mapping (confirmed by SPI probe: NSS=GPIO20, DIO1=GPIO19)
-D P_LORA_NSS=20
-D P_LORA_DIO_1=19
-D P_LORA_RESET=8
-D P_LORA_BUSY=2
lib_deps =
${crowpanel_70_base.lib_deps}
; No TCA9534 needed — V1.3 uses STC8H1K28 controlled via raw I2C writes
[env:crowpanel_70_v13_companion_radio_ble]
extends = crowpanel_70_v13
build_flags =
${crowpanel_70_v13.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D BLE_PIN_CODE=123456
-D OFFLINE_QUEUE_SIZE=256
-D DISPLAY_CLASS=CrowPanel70Display
; Scale factors: 800x480 physical -> 128x64 logical
; 800/128 = 6.25, 480/64 = 7.5
-D DISPLAY_SCALE_X=6.25
-D DISPLAY_SCALE_Y=7.5
-D AUTO_OFF_MILLIS=30000
-D HAS_TOUCH_SCREEN=1
-D NO_BATTERY_INDICATOR=1
-D MESH_DEBUG=1
build_src_filter = ${crowpanel_70_v13.build_src_filter}
+<helpers/esp32/*.cpp>
+<helpers/ui/LGFXDisplay.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${crowpanel_70_v13.lib_deps}
densaugeo/base64 @ ~1.4.0
; =============================================================================
; V1.0 hardware (original — TCA9534/PCA9557 at 0x18)
; =============================================================================
; LoRa pins for V1.0:
; IO0=NSS (boot pin!), IO20=DIO1, IO19=RESET, IO2=BUSY
; =============================================================================
[crowpanel_70_v10]
extends = crowpanel_70_base
build_flags =
${crowpanel_70_base.build_flags}
; V1.0 LoRa pin mapping
-D P_LORA_NSS=0
-D P_LORA_DIO_1=20
-D P_LORA_RESET=19
-D P_LORA_BUSY=2
lib_deps =
${crowpanel_70_base.lib_deps}
; TCA9534 I/O expander for display power control (V1.0 only)
hideakitai/TCA9534@0.1.1
[env:crowpanel_70_v10_companion_radio_ble]
extends = crowpanel_70_v10
build_flags =
${crowpanel_70_v10.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D BLE_PIN_CODE=123456
-D OFFLINE_QUEUE_SIZE=256
-D DISPLAY_CLASS=CrowPanel70Display
-D DISPLAY_SCALE_X=6.25
-D DISPLAY_SCALE_Y=7.5
-D AUTO_OFF_MILLIS=30000
-D HAS_TOUCH_SCREEN=1
-D NO_BATTERY_INDICATOR=1
-D MESH_DEBUG=1
build_src_filter = ${crowpanel_70_v10.build_src_filter}
+<helpers/esp32/*.cpp>
+<helpers/ui/LGFXDisplay.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${crowpanel_70_v10.lib_deps}
densaugeo/base64 @ ~1.4.0

View File

@@ -0,0 +1,227 @@
#include <Arduino.h>
#include "target.h"
CrowPanel70Board board;
// SPI bus for LoRa — use HSPI (SPI3_HOST). Pass -1 for SS so RadioLib
// can manually toggle NSS via digitalWrite (hardware CS conflicts with this).
static SPIClass spi(HSPI);
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi);
WRAPPER_CLASS radio_driver(radio, board);
ESP32RTCClock fallback_clock;
AutoDiscoverRTCClock rtc_clock(fallback_clock);
EnvironmentSensorManager sensors;
#ifdef DISPLAY_CLASS
LGFX_CrowPanel70 lcd;
CrowPanel70Display display(800, 480, lcd);
#ifndef HAS_TOUCH_SCREEN
TouchButton user_btn(&display);
#endif
#endif
bool radio_init() {
delay(1000);
#ifdef CROWPANEL_V13
Serial.println("\n\n=== CrowPanel 7.0 V1.3 MeshCore ===");
#else
Serial.println("\n\n=== CrowPanel 7.0 MeshCore ===");
#endif
Serial.println("Initializing...");
Serial.printf(" LoRa pins: NSS=%d DIO1=%d RST=%d BUSY=%d\n",
P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY);
Serial.printf(" SPI pins: MOSI=%d MISO=%d SCK=%d\n",
P_LORA_MOSI, P_LORA_MISO, P_LORA_SCLK);
#ifdef DISPLAY_CLASS
#ifdef CROWPANEL_V13
Serial.println("Step 1: STC8H1K28 backlight ON...");
Wire.beginTransmission(0x30);
Wire.write(0);
Wire.endTransmission();
#endif
Serial.println("Step 2: Touch reset pulse...");
pinMode(1, OUTPUT);
digitalWrite(1, LOW);
delay(120);
pinMode(1, INPUT);
// NO ST7277 SPI init — MADCTL breaks RGB mode on this panel.
// Rotation handled by LovyanGFX offset_rotation in constructor.
Serial.println("Step 3: lcd.init()...");
lcd.init();
Serial.printf(" LGFX: %dx%d\n", lcd.width(), lcd.height());
Serial.println("Step 4: display.begin()...");
display.begin();
#ifndef HAS_TOUCH_SCREEN
user_btn.begin();
#endif
Serial.println("Display ready");
// Orientation test
lcd.fillScreen(0x0000);
int w = lcd.width();
int h = lcd.height();
lcd.fillRect(0, 0, 60, 40, 0xF800);
lcd.fillRect(w-60, 0, 60, 40, 0x07E0);
lcd.fillRect(0, h-40, 60, 40, 0x001F);
lcd.fillRect(w-60, h-40, 60, 40, 0xFFE0);
lcd.setTextColor(0xFFFF);
lcd.setTextSize(2);
lcd.setCursor(80, 10);
lcd.printf("LGFX: %dx%d", w, h);
lcd.setCursor(80, 40);
lcd.print("CrowPanel 7.0 V1.3 + Meck");
lcd.setCursor(80, 70);
lcd.print("RED=TL GRN=TR BLU=BL YEL=BR");
lcd.setCursor(80, 100);
lcd.print("Testing LoRa...");
#endif
Serial.println("Initializing RTC...");
fallback_clock.begin();
rtc_clock.begin(Wire);
// --- LoRa SPI diagnostic: bitbang to bypass peripheral ---
Serial.println("Initializing LoRa radio...");
Serial.println(" Hardware SPI returned all zeros — trying bitbang to test physical wiring\n");
// Reset radio
pinMode(P_LORA_RESET, OUTPUT);
digitalWrite(P_LORA_RESET, LOW);
delay(20);
digitalWrite(P_LORA_RESET, HIGH);
delay(100);
pinMode(P_LORA_BUSY, INPUT);
unsigned long bs = millis();
while (digitalRead(P_LORA_BUSY) && (millis() - bs < 1000)) delay(1);
Serial.printf(" BUSY after reset: %s (%ldms)\n",
digitalRead(P_LORA_BUSY) ? "HIGH" : "LOW", millis() - bs);
// Configure pins as GPIO (not SPI peripheral)
pinMode(P_LORA_SCLK, OUTPUT); // GPIO5 = SCK
pinMode(P_LORA_MOSI, OUTPUT); // GPIO6 = MOSI
pinMode(P_LORA_MISO, INPUT); // GPIO4 = MISO
pinMode(P_LORA_NSS, OUTPUT); // GPIO20 = NSS
digitalWrite(P_LORA_SCLK, LOW);
digitalWrite(P_LORA_NSS, HIGH);
// Bitbang SPI Mode 0: CPOL=0, CPHA=0
// Clock idle LOW, data sampled on rising edge
auto bbTransfer = [](uint8_t tx) -> uint8_t {
uint8_t rx = 0;
for (int i = 7; i >= 0; i--) {
// Set MOSI
digitalWrite(P_LORA_MOSI, (tx >> i) & 1);
delayMicroseconds(2);
// Rising edge — slave clocks in MOSI, we read MISO
digitalWrite(P_LORA_SCLK, HIGH);
delayMicroseconds(2);
if (digitalRead(P_LORA_MISO)) rx |= (1 << i);
// Falling edge
digitalWrite(P_LORA_SCLK, LOW);
delayMicroseconds(2);
}
return rx;
};
// Read register 0x0320 via bitbang
Serial.println(" Bitbang SPI read of reg 0x0320:");
digitalWrite(P_LORA_NSS, LOW);
delayMicroseconds(50);
uint8_t b0 = bbTransfer(0x1D); // ReadRegister
uint8_t b1 = bbTransfer(0x03); // Addr high
uint8_t b2 = bbTransfer(0x20); // Addr low
uint8_t b3 = bbTransfer(0x00); // NOP (status)
uint8_t b4 = bbTransfer(0x00); // Register value
digitalWrite(P_LORA_NSS, HIGH);
Serial.printf(" SCK=%d MOSI=%d MISO=%d NSS=%d\n",
P_LORA_SCLK, P_LORA_MOSI, P_LORA_MISO, P_LORA_NSS);
Serial.printf(" bytes: %02X %02X %02X %02X %02X\n", b0, b1, b2, b3, b4);
Serial.printf(" val=0x%02X %s\n", b4,
b4 == 0x58 ? "<<< SX1262 FOUND via bitbang!" :
(b4 == 0xFF ? "(no response)" :
(b4 == 0x00 ? "(zeros — physical wiring issue)" : "")));
// Also try with MISO and MOSI swapped
Serial.println("\n Bitbang with MISO=6, MOSI=4 (swapped):");
pinMode(6, INPUT); // Now MISO
pinMode(4, OUTPUT); // Now MOSI
digitalWrite(4, LOW);
// Reset again
digitalWrite(P_LORA_RESET, LOW);
delay(20);
digitalWrite(P_LORA_RESET, HIGH);
delay(100);
bs = millis();
while (digitalRead(P_LORA_BUSY) && (millis() - bs < 500)) delay(1);
auto bbTransfer2 = [](uint8_t tx) -> uint8_t {
uint8_t rx = 0;
for (int i = 7; i >= 0; i--) {
digitalWrite(4, (tx >> i) & 1); // MOSI on GPIO4
delayMicroseconds(2);
digitalWrite(5, HIGH); // SCK
delayMicroseconds(2);
if (digitalRead(6)) rx |= (1 << i); // MISO on GPIO6
digitalWrite(5, LOW);
delayMicroseconds(2);
}
return rx;
};
digitalWrite(P_LORA_NSS, LOW);
delayMicroseconds(50);
b0 = bbTransfer2(0x1D);
b1 = bbTransfer2(0x03);
b2 = bbTransfer2(0x20);
b3 = bbTransfer2(0x00);
b4 = bbTransfer2(0x00);
digitalWrite(P_LORA_NSS, HIGH);
Serial.printf(" SCK=5 MOSI=4 MISO=6 NSS=%d\n", P_LORA_NSS);
Serial.printf(" bytes: %02X %02X %02X %02X %02X\n", b0, b1, b2, b3, b4);
Serial.printf(" val=0x%02X %s\n", b4,
b4 == 0x58 ? "<<< SX1262 FOUND (MISO/MOSI swapped)!" :
(b4 == 0xFF ? "(no response)" :
(b4 == 0x00 ? "(zeros)" : "")));
// Restore pins for hardware SPI attempt
spi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI, -1);
bool result = radio.std_init(&spi);
if (result) {
Serial.println("LoRa: OK!");
#ifdef DISPLAY_CLASS
lcd.setTextColor(0x07E0);
lcd.setCursor(80, 130);
lcd.print("LoRa: OK!");
#endif
} else {
Serial.println("LoRa: FAILED");
#ifdef DISPLAY_CLASS
lcd.setTextColor(0xF800);
lcd.setCursor(80, 130);
lcd.print("LoRa: FAILED");
#endif
}
return true;
}
uint32_t radio_get_rng_seed() { return radio.random(0x7FFFFFFF); }
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setFrequency(freq); radio.setSpreadingFactor(sf);
radio.setBandwidth(bw); radio.setCodingRate(cr);
}
void radio_set_tx_power(uint8_t dbm) { radio.setOutputPower(dbm); }
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio); return mesh::LocalIdentity(&rng);
}

View File

@@ -0,0 +1,39 @@
#pragma once
#include "variant.h" // Board-specific defines (HAS_GPS, GPS_BAUDRATE, etc.)
#define RADIOLIB_STATIC_ONLY 1
#include <RadioLib.h>
#include <helpers/radiolib/RadioLibWrappers.h>
#include <CrowPanel70Board.h>
#include <helpers/radiolib/CustomSX1262Wrapper.h>
#include <helpers/AutoDiscoverRTCClock.h>
#include <helpers/SensorManager.h>
#include <helpers/sensors/EnvironmentSensorManager.h>
#ifdef DISPLAY_CLASS
#include <LGFX_CrowPanel70.h>
#include <CrowPanel70Display.h>
#ifndef HAS_TOUCH_SCREEN
#include <TouchButton.h>
#endif
#endif
extern CrowPanel70Board board;
extern WRAPPER_CLASS radio_driver;
extern AutoDiscoverRTCClock rtc_clock;
extern EnvironmentSensorManager sensors;
#ifdef DISPLAY_CLASS
extern LGFX_CrowPanel70 lcd;
extern CrowPanel70Display display;
#ifndef HAS_TOUCH_SCREEN
extern TouchButton user_btn;
#endif
#endif
bool radio_init();
uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(uint8_t dbm);
mesh::LocalIdentity radio_new_identity();

View File

@@ -0,0 +1,150 @@
#pragma once
// =============================================================================
// CrowPanel 7.0" Advance Series (V1.0 / V1.3)
// ESP32-S3-WROOM-1-N16R8 (16MB Flash, 8MB OPI-PSRAM)
// 800x480 IPS display, ST7277 driver, RGB parallel 16-bit
// GT911 capacitive touch at I2C 0x5D
// =============================================================================
// -----------------------------------------------------------------------------
// I2C Bus (shared: touch GT911, RTC PCF85063 at 0x51, STC8H1K28 at 0x30)
// -----------------------------------------------------------------------------
#define I2C_SDA 15
#define I2C_SCL 16
#define PIN_BOARD_SDA I2C_SDA
#define PIN_BOARD_SCL I2C_SCL
// -----------------------------------------------------------------------------
// Display
// -----------------------------------------------------------------------------
#define LCD_HOR_SIZE 800
#define LCD_VER_SIZE 480
// -----------------------------------------------------------------------------
// LoRa Radio (SX1262) — via wireless module connector
// Pin mapping depends on hardware version (defined in platformio.ini):
// V1.3: NSS=19, DIO1=20, RESET=8, BUSY=2, SPI 4/5/6
// V1.0: NSS=0, DIO1=20, RESET=19, BUSY=2, SPI 4/5/6
//
// IMPORTANT: Function select DIP switch must be set to WM mode (S0=1, S1=0)
// for the SPI bus to be routed to the wireless module connector.
// -----------------------------------------------------------------------------
#define USE_SX1262
#define USE_SX1268
// P_LORA_* pins are defined in platformio.ini per hardware version
// RadioLib/MeshCore compat aliases (only define if not already set by platformio)
#ifndef P_LORA_NSS
#ifdef CROWPANEL_V13
#define P_LORA_NSS 20
#define P_LORA_DIO_1 19
#define P_LORA_RESET 8
#define P_LORA_BUSY 2
#else
#define P_LORA_NSS 0
#define P_LORA_DIO_1 20
#define P_LORA_RESET 19
#define P_LORA_BUSY 2
#endif
#endif
#ifndef P_LORA_SCLK
#define P_LORA_SCLK 5
#define P_LORA_MISO 4
#define P_LORA_MOSI 6
#endif
// -----------------------------------------------------------------------------
// GPS — not present on CrowPanel
// Define a fallback GPS_BAUDRATE so that serial CLI code compiles cleanly
// (the CLI GPS commands will be inert since HAS_GPS is not defined)
// -----------------------------------------------------------------------------
// #define HAS_GPS — intentionally NOT defined
#ifndef GPS_BAUDRATE
#define GPS_BAUDRATE 9600
#endif
// -----------------------------------------------------------------------------
// SD Card — shares SPI bus with LoRa and speaker via function switch
// When function switch is in WM mode, SD card is NOT accessible.
// SD support can be enabled for standalone (non-LoRa) use with S1=1,S0=1
// -----------------------------------------------------------------------------
// #define HAS_SDCARD — not defined by default (conflicts with LoRa SPI)
// SDCARD_CS dummy: NotesScreen, TextReaderScreen, EpubZipReader reference this
// unconditionally. -1 makes digitalWrite() a no-op on ESP32.
#ifndef SDCARD_CS
#define SDCARD_CS -1
#endif
// E-ink display: not present. GxEPDDisplay.h is shadowed by a stub in the
// variant include path to avoid LovyanGFX/Adafruit_GFX type collision.
// -----------------------------------------------------------------------------
// Battery — CrowPanel has a battery connector but no fuel gauge IC
// Battery charging is handled by onboard circuit with CHG LED indicator
// -----------------------------------------------------------------------------
// #define HAS_BQ27220 — not present
#define NO_BATTERY_INDICATOR 1
// -----------------------------------------------------------------------------
// Audio — I2S speaker (shared pins, only when function switch = MIC&SPK)
// Not usable simultaneously with LoRa — commented out for reference
// -----------------------------------------------------------------------------
// #define BOARD_I2S_SDIN 4 // IO4 (shared with SPI MISO)
// #define BOARD_I2S_BCLK 5 // IO5 (shared with SPI SCK)
// #define BOARD_I2S_LRCLK 6 // IO6 (shared with SPI MOSI)
// MIC (v1.3: LMD3526B261 PDM mic)
// #define BOARD_MIC_DATA 20 // IO20 (shared with LoRa DIO1)
// #define BOARD_MIC_CLOCK 19 // IO19 (shared with LoRa NSS)
// -----------------------------------------------------------------------------
// Touch Screen
// -----------------------------------------------------------------------------
#define HAS_TOUCHSCREEN 1
// Touch controller: GT911 at 0x5D
// INT = IO1 (active-low pulse, used for GT911 address selection at boot)
// RST = STC8H1K28 P1.7 (v1.3) or TCA9534 pin 2 (v1.0)
// -----------------------------------------------------------------------------
// Keyboard — not present (touchscreen-only device)
// -----------------------------------------------------------------------------
// #define HAS_PHYSICAL_KEYBOARD — not present
// -----------------------------------------------------------------------------
// Buttons — CrowPanel is touchscreen-only
// -----------------------------------------------------------------------------
// BOOT button (GPIO0) is for programming only, not a user button.
// Do NOT define PIN_USER_BTN — it would enable TouchButton code in UITask.cpp
// but the TouchButton object is not instantiated (HAS_TOUCH_SCREEN suppresses it).
// #define BUTTON_PIN 0
// #define PIN_USER_BTN 0
// -----------------------------------------------------------------------------
// UART
// -----------------------------------------------------------------------------
// UART0: TX=IO43, RX=IO44 (also USB CDC)
// UART1: TX=IO20, RX=IO19 (shared with LoRa DIO1/NSS when in WM mode!)
// -----------------------------------------------------------------------------
// RTC — PCF85063 at I2C 0x51 (confirmed from board silkscreen)
// -----------------------------------------------------------------------------
// AutoDiscoverRTCClock will find it automatically on the I2C bus
// -----------------------------------------------------------------------------
// STC8H1K28 I/O Controller (V1.2/V1.3 only)
// I2C slave at address 0x30
// Command bytes:
// 0 = backlight max brightness
// 1-244 = backlight dimmer
// 245 = backlight off
// 246 = buzzer on
// 247 = buzzer off
// 248 = speaker amp on
// 249 = speaker amp off
// -----------------------------------------------------------------------------
#ifdef CROWPANEL_V13
#define STC8H_I2C_ADDR 0x30
#endif

View File

@@ -142,7 +142,7 @@ build_flags =
-D OFFLINE_QUEUE_SIZE=256
-D MECK_AUDIO_VARIANT
-D MECK_WEB_READER=1
-D FIRMWARE_VERSION='"Meck v0.9.9WiFi"'
-D FIRMWARE_VERSION='"Meck v1.0.WiFi"'
build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
+<helpers/esp32/*.cpp>
+<helpers/ui/MomentaryButton.cpp>
@@ -192,7 +192,7 @@ build_flags =
-D OFFLINE_QUEUE_SIZE=256
-D HAS_4G_MODEM=1
-D MECK_WEB_READER=1
-D FIRMWARE_VERSION='"Meck v0.9.94G"'
-D FIRMWARE_VERSION='"Meck v1.0.4G"'
build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
+<helpers/esp32/*.cpp>
+<helpers/ui/MomentaryButton.cpp>
@@ -222,7 +222,7 @@ build_flags =
-D OFFLINE_QUEUE_SIZE=256
-D HAS_4G_MODEM=1
-D MECK_WEB_READER=1
-D FIRMWARE_VERSION='"Meck v0.9.94G.WiFi"'
-D FIRMWARE_VERSION='"Meck v1.0.4G.WiFi"'
build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
+<helpers/esp32/*.cpp>
+<helpers/ui/MomentaryButton.cpp>
@@ -248,7 +248,7 @@ build_flags =
-D OFFLINE_QUEUE_SIZE=256
-D HAS_4G_MODEM=1
-D MECK_WEB_READER=1
-D FIRMWARE_VERSION='"Meck v0.9.94G.SA"'
-D FIRMWARE_VERSION='"Meck v1.0.4G.SA"'
build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
+<helpers/esp32/*.cpp>
+<helpers/ui/MomentaryButton.cpp>

View File

@@ -49,19 +49,11 @@ bool radio_init() {
loraSpi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI, P_LORA_NSS);
MESH_DEBUG_PRINTLN("radio_init() - SPI initialized, calling radio.std_init()...");
bool result = radio.std_init(&loraSpi);
if (result) {
radio.setPreambleLength(32);
MESH_DEBUG_PRINTLN("radio_init() - preamble set to 32 symbols");
}
MESH_DEBUG_PRINTLN("radio_init() - radio.std_init() returned: %s", result ? "SUCCESS" : "FAILED");
return result;
#else
MESH_DEBUG_PRINTLN("radio_init() - calling radio.std_init() without custom SPI...");
bool result = radio.std_init();
if (result) {
radio.setPreambleLength(32);
MESH_DEBUG_PRINTLN("radio_init() - preamble set to 32 symbols");
}
return result;
#endif
}
@@ -75,6 +67,14 @@ void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setSpreadingFactor(sf);
radio.setBandwidth(bw);
radio.setCodingRate(cr);
// Longer preamble for low SF improves reliability — each symbol is shorter
// at low SF, so more symbols are needed for reliable detection.
// SF <= 8 gets 32 symbols (~65ms at SF7/62.5kHz); SF >= 9 keeps 16 (already ~131ms+).
// See: https://github.com/meshcore-dev/MeshCore/pull/1954
uint16_t preamble = (sf <= 8) ? 32 : 16;
radio.setPreambleLength(preamble);
MESH_DEBUG_PRINTLN("radio_set_params() - bw=%.1f sf=%u preamble=%u", bw, sf, preamble);
}
void radio_set_tx_power(uint8_t dbm) {