Unify Heltec V4 OLED, TFT and headless display selection
Detect OLED or the original stock TFT touch assembly in ordinary V4.2/V4.3 images. Add saved live display.type auto/oled/tft/off selection and report the active selection in board output. Keep sensors on primary I2C, preserve shared power, and isolate TFT FSPI from radio HSPI through sleep and changes. Consolidate future picker/build choices only with application capability proof. Retain exact legacy OTA identities and separate R8 hardware images. Document controls and physical qualification limits. Validation: five serialized Full builds with capability/size/RAM checks; focused host tests, existing R8/T-Deck/OLED regressions, build-profile suite, and picker/provider tests. V4.3 OLED live changes and off/auto reboot persistence passed on Mercerwood. Physical TFT qualification remains pending.
This commit is contained in:
1 parent
5b5708283d
commit
b00d346df7
43 files changed
+2123
-76
No files matched your search
@@ -1187,6 +1187,8 @@ jobs:
|
||||
python3 -B test/test_wireless_tracker_display_detection.py -v
|
||||
python3 -B test/test_heltec_r8_display_detection.py -v
|
||||
python3 -B test/test_heltec_r8_display_rotation.py -v
|
||||
python3 -B test/test_heltec_v4_display_detection.py -v
|
||||
python3 -B test/test_heltec_v4_unified_catalog.py -v
|
||||
python3 -B test/test_tdeck_display_cooperation.py -v
|
||||
python3 -B test/test_muzi_accessory_detection.py -v
|
||||
python3 -B test/test_techo_lite_display_detection.py -v
|
||||
|
||||
+4
-5
@@ -5789,9 +5789,9 @@ get_esp32_full_companion_replacement() {
|
||||
|
||||
# Full Companion includes USB, BLE, and ordinary WiFi Companion. Map legacy
|
||||
# FEM-default aliases to the one runtime-configurable image for each physical
|
||||
# V4 display/radio layout. The expansion-kit TFT remains a distinct Full
|
||||
# recipe because it has different sensor wiring from both base display
|
||||
# layouts.
|
||||
# V4 display/radio layout. Unified display consolidation below omits the
|
||||
# older TFT/expansion aliases only when the canonical driver is configured.
|
||||
# Their exact recipes remain available for deployed update identities.
|
||||
case "$env_name" in
|
||||
heltec_v3_companion_radio_wifi_mqtt)
|
||||
full_env=Heltec_v3_companion_radio_full
|
||||
@@ -6079,9 +6079,8 @@ get_consolidated_hardware_replacement() {
|
||||
# Explicit builds still retain the requested historical OTA identity.
|
||||
case "${1,,}" in
|
||||
picow_companion_radio_usb|picow_companion_radio_ble|picow_companion_radio_wifi|\
|
||||
heltec_v4_expansionkit_repeater|heltec_v4_expansionkit_repeater_observer_mqtt|heltec_v4_expansionkit_room_server_observer_mqtt|\
|
||||
heltec_v4_tft_*|heltec_v4_3_tft_*|heltec_v4_expansionkit_*|heltec_v4_3_expansionkit_*|\
|
||||
heltec_v4_r8_tft_portrait_repeater_observer_mqtt|heltec_v4_r8_tft_portrait_room_server_observer_mqtt|\
|
||||
heltec_v4_expansionkit_tft_companion_radio_*|heltec_v4_3_expansionkit_tft_companion_radio_*|\
|
||||
muzi_base_duo_superio_companion_radio_*|muzi_base_uno_superio_companion_radio_*|lilygo_t-echo-lite_non_shell_companion_radio_*|\
|
||||
heltec_rc32_without_display_*) ;;
|
||||
*) return 1 ;;
|
||||
|
||||
@@ -822,8 +822,20 @@
|
||||
releaseBoundControl(profile, "combinedCompanion"));
|
||||
}
|
||||
|
||||
function unifiedV4DisplayProfile(profile) {
|
||||
return Boolean(profile && profile.hardware === "heltec_v4" &&
|
||||
profile.controls && profile.controls.platform === "ESP32_PLATFORM" &&
|
||||
profile.controls.display === true && releaseBoundControl(profile, "unifiedV4Display"));
|
||||
}
|
||||
|
||||
function consolidationSuccessor(profile, candidate) {
|
||||
if (!profile || !candidate || profile.role !== candidate.role) return false;
|
||||
if (/^heltec_v4(?:_3)?(?:_expansionkit)?(?:_tft)?$/.test(profile.hardware) &&
|
||||
profile.hardware !== "heltec_v4" && unifiedV4DisplayProfile(candidate)) {
|
||||
return candidate.feature === profile.feature && candidate.ota === profile.ota &&
|
||||
(!profile.sensorProfile || profile.sensorProfile === candidate.sensorProfile) &&
|
||||
(profile.role !== "companion" || isFullCompanion(candidate));
|
||||
}
|
||||
if (/^PicoW_companion_radio_(usb|ble|wifi)$/.test(profile.target)) {
|
||||
return combinedPicoCompanion(candidate);
|
||||
}
|
||||
@@ -1725,6 +1737,10 @@
|
||||
requested[field] = params.get(field) || "";
|
||||
});
|
||||
[
|
||||
...["heltec_v4_3", "heltec_v4_tft", "heltec_v4_3_tft", "heltec_v4_expansionkit",
|
||||
"heltec_v4_expansionkit_tft", "heltec_v4_3_expansionkit_tft"].map(function (hardware) {
|
||||
return [hardware, "heltec_v4", unifiedV4DisplayProfile];
|
||||
}),
|
||||
["heltec_rc32_without_display", "heltec_rc32", function (profile) { return optionalAutoDisplayProfile(profile); }],
|
||||
["heltec_v4_expansionkit", "heltec_v4", function (profile) { return ["repeater", "room"].includes(profile.role) && releaseBoundControl(profile, "oledExpansionCompatible"); }],
|
||||
["heltec_v4_r8_tft_portrait", "heltec_v4_r8_tft", function (profile) { return ["repeater", "room"].includes(profile.role) && releaseBoundControl(profile, "displayRotation"); }],
|
||||
@@ -2011,6 +2027,10 @@
|
||||
if (combinedPicoCompanion(profile)) {
|
||||
notes.push("This Companion image includes USB, Bluetooth LE and WiFi together. Separate transport firmware is unnecessary; this image does not add a wireless firmware updater.");
|
||||
}
|
||||
if (unifiedV4DisplayProfile(profile)) {
|
||||
notes.push("One ordinary Heltec V4 image supports V4.2/V4.3 with OLED, the stock touch TFT kit, or no screen. The saved display.type setting selects auto, oled, tft or off; board reports the detected revision and active display. R8 uses its separate hardware image.");
|
||||
notes.push("When changing from a legacy TFT or expansion-kit target, use a local USB installation or a supported WiFi application update. Their LoRa OTA target IDs remain distinct; never force a different target's LoRa OTA package.");
|
||||
}
|
||||
if (releaseBoundControl(profile, "oledExpansionCompatible")) {
|
||||
notes.push("The same OLED firmware supports the base Heltec V4 and its OLED expansion kit. Existing expansion-kit OTA identities still require their exact historical update or a local USB migration.");
|
||||
}
|
||||
@@ -2375,6 +2395,9 @@
|
||||
if (infrastructure && (info.updateMethods || []).includes("bluetooth")) section("Bluetooth firmware update", [
|
||||
{ label: "Start DFU", commands: ["start ota"], text: "Use the exact board's application DFU ZIP with the matching Bluetooth-capable bootloader." },
|
||||
]);
|
||||
if (unifiedV4DisplayProfile(profile)) section("Display type", ["auto", "oled", "tft", "off"].map(function (type) {
|
||||
return {label: type === "auto" ? "Automatic" : type === "off" ? "Screen off" : type.toUpperCase(), commands: ["set display.type " + type]};
|
||||
}), "Saved choice applies immediately. Check with get display.type and board. Automatic mode continues without a screen if no supported display is detected.");
|
||||
if ((full || releaseBoundControl(profile, "displayRotation")) && info.display) section("Display rotation", [0, 90, 180, 270].map(function (angle) {
|
||||
return { label: angle === 0 ? "Board default" : angle + " degrees", commands: ["set display.rotation " + angle] };
|
||||
}), "Use on supported displays; 0 restores the board default. Check with get display.rotation.");
|
||||
|
||||
@@ -1310,6 +1310,40 @@ build flag is needed.
|
||||
|
||||
---
|
||||
|
||||
## Select the original Heltec V4 display
|
||||
|
||||
New unified original-V4 images support V4.2/V4.3 OLED, the original TFT
|
||||
touchscreen expansion kit, and operation without a screen. This feature is
|
||||
not present in the older 1.17.1.1 or 1.17.1.5 images. V4 R8 remains a separate
|
||||
hardware target.
|
||||
|
||||
```text
|
||||
get display.type
|
||||
set display.type auto
|
||||
set display.type oled
|
||||
set display.type tft
|
||||
set display.type off
|
||||
board
|
||||
```
|
||||
|
||||
The default `auto` requires an OLED I2C response or a stable identity from the
|
||||
stock TFT assembly's touch controller. No OLED response alone is not evidence
|
||||
of a TFT. When neither responds, the node runs without a screen and `board`
|
||||
reports `none detected`. Touch identification establishes the stock assembly,
|
||||
not LCD health. A damaged or nonstandard panel can require an override.
|
||||
|
||||
`oled` explicitly selects and checks the OLED driver. `tft` explicitly selects
|
||||
the TFT driver and reports `TFT (forced)` rather than claiming detection.
|
||||
`off` disables display initialization and reports `off`, distinct from
|
||||
`none detected`. Selections apply immediately and survive reboot in the checked
|
||||
`/display_type` file. A failed save leaves the live selection unchanged.
|
||||
|
||||
`board` combines the detected radio front end with the live display status,
|
||||
for example `Heltec V4.3 OLED`, `Heltec V4.2 TFT`, or `Heltec V4.3 off`.
|
||||
The radio front end identifies the V4.2/V4.3 hardware generation; it is not a
|
||||
read of a PCB revision register. Use `display.mode` below for normal screen
|
||||
sleep/wake policy without changing the selected hardware driver.
|
||||
|
||||
## Set display modes and timeouts
|
||||
|
||||
Companion message sound, vibration, LED, screen and GPIO rules have separate
|
||||
|
||||
@@ -338,6 +338,14 @@ SSD1306 Full Companion builds use `set display.rotation 90|180|270`; `0`
|
||||
restores the board default, so a separate rotated release image is not
|
||||
recommended.
|
||||
|
||||
Newly built ordinary Heltec V4 images combine V4.2/V4.3, OLED, the stock touch
|
||||
TFT kit, and no screen in one image per role. A qualified image offers saved
|
||||
`display.type auto|oled|tft|off` controls; selection applies immediately, and
|
||||
`board` reports the detected revision and active display. R8 remains separate.
|
||||
Older TFT/expansion-kit target names retain their distinct LoRa OTA identities;
|
||||
use USB or supported WiFi updating when moving to the canonical image. Older
|
||||
published downloads keep their original behavior. See [optional screens](optional_screens.md#heltec-v42-v43).
|
||||
|
||||
New display-capable RAK4631 and RAK3401 images with OLED controller detection
|
||||
also offer saved `display.controller` choices: `auto`, `ssd1306` or `sh1106`.
|
||||
This applies after restart and avoids a custom image when a panel needs an
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
Use the normal display-capable firmware for your exact board and role. When
|
||||
automatic detection is included, that image starts the supported screen if
|
||||
it responds, or continues headless if it is absent. There is no screen-presence
|
||||
setting. Power the board off before attaching or removing an optional screen.
|
||||
setting on most boards. The unified ordinary Heltec V4 image also offers a
|
||||
saved display override, described below. Power the board off before attaching
|
||||
or removing an optional screen.
|
||||
|
||||
The [firmware picker](firmware_picker.md) describes automatic screen detection
|
||||
only when the selected application has been verified to contain it. Previously
|
||||
@@ -31,10 +33,34 @@ add detection to an older download.
|
||||
| Muzi Base Uno/Duo with SuperIO | [SuperIO](https://muzi.works/products/super-io) adds the OLED, GPS, buttons and buzzer to the matching Base radio. | New Companion images probe the SH1107 display, leave its 12 V supply off when absent, and discover GPS independently. Uno and Duo retain their different radio drivers. |
|
||||
| LilyGo T-Echo Lite core/display assembly | [LilyGo](https://lilygo.cc/en-us/products/t-echo-lite) documents the split core/base design. | New Companion images check SSD1681 status readback before e-paper initialization and continue headless when the panel does not respond. The old non-shell target names remain compatibility recipes. |
|
||||
|
||||
The older V4 TFT expansion configuration is also optional, but its current
|
||||
recipe has no verified panel readback connection. Detection is not enabled for
|
||||
that wiring. A touchscreen acknowledgement alone does not establish that the
|
||||
LCD responds. The V4 R8 TFT and older V4 TFT pin mappings are distinct.
|
||||
## Heltec V4.2 / V4.3
|
||||
|
||||
New ordinary V4 images combine OLED, the original stock touch TFT expansion
|
||||
kit, and operation without a screen in one image per role. The OLED probe uses
|
||||
its own I2C bus; a verified CHSC6xxx touch-controller identity selects the stock
|
||||
TFT assembly. This identifies the touchscreen assembly, not a working LCD.
|
||||
With neither display detected, the firmware continues without a screen.
|
||||
|
||||
`get display.type` reports the saved choice and active display. Use
|
||||
`set display.type auto|oled|tft|off` to change it immediately and save the choice.
|
||||
`auto` is the default. `off` explicitly disables rendering; it is distinct from
|
||||
an absent or unrecognized screen. `board` reports the radio front-end revision
|
||||
and active display, including the off/headless states. V4.2/V4.3 is inferred
|
||||
from the detected radio front end, rather than reading a PCB revision code.
|
||||
|
||||
This change applies to newly built and qualified unified images. Existing
|
||||
releases retain their compiled OLED/TFT behavior. The picker advertises the
|
||||
combined image only when its application and source-bound metadata prove that
|
||||
it includes the new driver. V4.3 OLED detection, live selection and reboot
|
||||
persistence passed on the Mercerwood test board. Physical TFT detection and
|
||||
display operation remain unqualified. See the [validation record](test-results/firmware_variant_consolidation.md#unified-ordinary-v4-update-2026-10-26).
|
||||
|
||||
The V4 R8 retains its separate hardware image because its memory configuration
|
||||
and display wiring differ. Older TFT/expansion-kit target names remain explicit
|
||||
compatibility builds with their original LoRa OTA identity. Moving to the
|
||||
canonical ordinary V4 image requires a local USB installation or an updater
|
||||
that accepts the matching application over WiFi. Do not force a different
|
||||
LoRa OTA target ID. No settings erase is required for display selection.
|
||||
|
||||
RAK4631 and RAK3401 images with OLED controller detection support
|
||||
`set display.controller auto`, `ssd1306` or `sh1106` in all display-enabled
|
||||
|
||||
@@ -67,3 +67,45 @@ The production controls generator and picker used release metadata assembled fro
|
||||
These checks are required before describing the hardware combinations as physically qualified.
|
||||
|
||||
The earlier [optional-screen record](../optional_screens.md#validation) covers separate RC32 and Tracker smoke packages; those are not part of this ten-profile matrix.
|
||||
|
||||
## Unified ordinary V4 update - 2026-10-26
|
||||
|
||||
New original-V4 builds combine OLED, the stock touch TFT kit and headless
|
||||
operation. `display.type` defaults to `auto`; saved `oled`, `tft` and `off`
|
||||
overrides apply immediately. `board` reports the radio front end's V4.2/V4.3
|
||||
generation and live display selection. R8 retains its separate hardware image.
|
||||
|
||||
All five Full packages below passed serialized compilation, application size,
|
||||
capability-marker and runtime RAM admission checks. Test version:
|
||||
`v1.18.0.2-display-test-a87b2a33`, built from the working implementation based
|
||||
on `a87b2a33a`. The application hashes distinguish these local test builds.
|
||||
|
||||
| Target | Application bytes | RAM admission margin (bytes) | SHA-256 prefix |
|
||||
| --- | ---: | ---: | --- |
|
||||
| V4 Full Companion | 2,545,128 | 25,328 | `d90b050b9892` |
|
||||
| V4 Full Repeater | 2,140,920 | 3,912 | `9e1bf8b7c31a` |
|
||||
| V4 Full Room Server | 2,139,400 | 18,040 | `8d3cddf67659` |
|
||||
| V4 Full Sensor | 1,524,472 | 123,112 | `6bd02f499a4a` |
|
||||
| Legacy TFT Repeater identity | 1,784,936 | 50,936 | `7c47a7825100` |
|
||||
|
||||
The ten focused detection/integration tests pass, including absent and invalid
|
||||
panels, saved-setting recovery, live switching, clock wrap and headless
|
||||
Companion notifications/battery checks. They also verify that display sleep
|
||||
keeps the sensor power rail intact, sensors retain primary I2C, and TFT FSPI
|
||||
cleanup cannot stop the radio's separate HSPI controller. Shared R8, T-Deck
|
||||
and OLED regressions pass, as do build-profile and picker/provider checks.
|
||||
|
||||
The Mercerwood V4.3 OLED passed automatic detection, live off/auto/oled changes,
|
||||
off and auto persistence across reboot, accurate `board` output and USB CLI
|
||||
responses. Its original display power preferences were retained. Only the
|
||||
active application was flashed; partitions and stored device data were kept.
|
||||
The final image also passed a save/logging smoke check: no false missing-file
|
||||
errors. Native USB needed one additional monitored RTS reset after esptool's
|
||||
flash/reset handoff before the terminal responded; later application reboots
|
||||
passed the persistence checks.
|
||||
|
||||
Physical TFT identity/readback, visible rendering and sleep/wake still require
|
||||
the original stock kit. Screen-absent hardware and V4.2 hardware were covered
|
||||
by software tests, not a physical run. These display checks do not qualify
|
||||
maximum MQTT/TLS load, radio throughput or sleep current. Existing published
|
||||
images and LoRa update identities were not changed.
|
||||
@@ -5,6 +5,9 @@
|
||||
#endif
|
||||
#include <helpers/LedSettings.h>
|
||||
#include <helpers/ui/DisplayPowerSettings.h>
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED && defined(DISPLAY_CLASS)
|
||||
#include <helpers/ui/HeltecV4DisplaySettings.h>
|
||||
#endif
|
||||
#include <helpers/ui/DisplayNightLocation.h>
|
||||
#if defined(OLED_AUTO_DETECT) && OLED_AUTO_DETECT && defined(DISPLAY_CLASS)
|
||||
#include <helpers/ui/OLEDControllerSettings.h>
|
||||
@@ -4526,6 +4529,9 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
|
||||
return true;
|
||||
}
|
||||
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED && defined(DISPLAY_CLASS)
|
||||
if (mesh::ui::handleHeltecV4DisplayCommand(command, reply, reply_size, display)) return true;
|
||||
#endif
|
||||
if (mesh::ui::handleDisplayPowerCommand(command, reply, reply_size,
|
||||
_ui != nullptr && _ui->supportsInboxModes())) {
|
||||
if (_ui) _ui->inboxModeChanged();
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
#include <Arduino.h> // needed for PlatformIO
|
||||
#include <helpers/ui/StartupScreen.h>
|
||||
#include <helpers/ui/DisplayPowerSettings.h>
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED && defined(DISPLAY_CLASS)
|
||||
#include <helpers/ui/HeltecV4DisplaySettings.h>
|
||||
#endif
|
||||
#if defined(OLED_AUTO_DETECT) && OLED_AUTO_DETECT
|
||||
#include <helpers/ui/OLEDControllerSettings.h>
|
||||
#endif
|
||||
@@ -2080,6 +2083,12 @@ void halt() {
|
||||
|
||||
void formatCompanionWiFiDisplayStatus(char* reply, size_t reply_size) {
|
||||
if (reply == nullptr || reply_size == 0) return;
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED \
|
||||
&& defined(DISPLAY_CLASS)
|
||||
companion_display_available =
|
||||
display.selectedType() != mesh::ui::HeltecV4DisplayType::None;
|
||||
companion_display_begin_status = companion_display_available ? 1 : 0;
|
||||
#endif
|
||||
static const char* const names[] = {
|
||||
"not-rendered", "setup", "off", "ready", "not-configured",
|
||||
"connecting",
|
||||
@@ -3691,6 +3700,9 @@ void setup() {
|
||||
if (!mesh::storage::internalPrimaryFilesystemReady(primary_fs_boot)
|
||||
&& !store.isVolatilePrimaryFS()) display_prefs_fs = nullptr;
|
||||
#endif
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED
|
||||
mesh::ui::loadHeltecV4DisplaySettings(regulator_prefs_fs, display);
|
||||
#endif
|
||||
#if defined(OLED_AUTO_DETECT) && OLED_AUTO_DETECT
|
||||
mesh::ui::loadOLEDControllerSettings(regulator_prefs_fs);
|
||||
#endif
|
||||
@@ -4153,6 +4165,11 @@ void setup() {
|
||||
#endif
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED
|
||||
// Retain the wrapper dependency in headless mode so live selection can
|
||||
// enable a screen. The UI skips rendering while no backend is selected.
|
||||
disp = &display;
|
||||
#endif
|
||||
ui_task.begin(disp, &sensors, the_mesh.getNodePrefs()); // still want to pass this in as dependency, as prefs might be moved
|
||||
#endif
|
||||
#if defined(WIO_E5_MINI_HEADLESS_COMPANION_UI) && defined(PIN_USER_BTN)
|
||||
|
||||
@@ -59,6 +59,15 @@
|
||||
|
||||
#define LONG_PRESS_MILLIS 1200
|
||||
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED \
|
||||
&& !defined(AUTO_SHUTDOWN_MILLIVOLTS)
|
||||
// The original TFT Full recipe includes this battery protection. Retain it
|
||||
// when the canonical V4 image detects that assembly without changing the
|
||||
// historical OLED/headless Full policy.
|
||||
#define AUTO_SHUTDOWN_MILLIVOLTS 3400
|
||||
#define HELTEC_V4_TFT_SHUTDOWN_ONLY 1
|
||||
#endif
|
||||
|
||||
static uint64_t companionMessageNowMillis() {
|
||||
#if defined(ESP32)
|
||||
// Unlike Arduino millis(), this does not wrap after roughly 49.7 days.
|
||||
@@ -2767,6 +2776,18 @@ void UITask::syncAlertPreferences() {
|
||||
|
||||
void UITask::loop() {
|
||||
syncAlertPreferences();
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED
|
||||
static uint32_t display_generation = 0;
|
||||
if (display_generation != display.generation()) {
|
||||
display_generation = display.generation();
|
||||
display.setRotationDegrees(_node_prefs->display_rotation_degrees);
|
||||
_next_refresh = millis();
|
||||
}
|
||||
// Preserve normal headless button, notification and battery behavior while
|
||||
// retaining the wrapper for a later live display.type change.
|
||||
_display = display.selectedType() == mesh::ui::HeltecV4DisplayType::None
|
||||
? nullptr : &display;
|
||||
#endif
|
||||
// Observe startup expiry even while display/Morse/input defers rendering.
|
||||
if (!_startup_complete && static_cast<uint32_t>(millis() - ui_started_at) >= 8000u) {
|
||||
_startup_complete = true;
|
||||
@@ -3037,7 +3058,11 @@ void UITask::loop() {
|
||||
#ifdef AUTO_SHUTDOWN_MILLIVOLTS
|
||||
if (static_cast<int32_t>(millis() - next_batt_chck) >= 0) {
|
||||
uint16_t milliVolts = getBattMilliVolts();
|
||||
if (milliVolts > 0 && milliVolts < AUTO_SHUTDOWN_MILLIVOLTS) {
|
||||
if (milliVolts > 0 && milliVolts < AUTO_SHUTDOWN_MILLIVOLTS
|
||||
#ifdef HELTEC_V4_TFT_SHUTDOWN_ONLY
|
||||
&& display.selectedType() == mesh::ui::HeltecV4DisplayType::Tft
|
||||
#endif
|
||||
) {
|
||||
if(!board.isExternalPowered()) {
|
||||
if (_display != NULL) {
|
||||
_display->startFrame();
|
||||
|
||||
@@ -867,6 +867,19 @@ void UITask::toggleDisplay(const char* source) {
|
||||
#endif
|
||||
|
||||
void UITask::loop() {
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED
|
||||
static uint32_t display_generation = 0;
|
||||
if (display_generation != display.generation()) {
|
||||
display_generation = display.generation();
|
||||
_next_refresh = millis();
|
||||
#ifdef DISPLAY_REDRAW_ON_CHANGE
|
||||
_frame_valid = false;
|
||||
#endif
|
||||
#ifdef DISPLAY_ACTIVITY_DASHBOARD
|
||||
_rows_valid = false;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
#if defined(HELTEC_V4_R8_TFT) && defined(HELTEC_TFT_RUNTIME_ROTATION) && HELTEC_TFT_RUNTIME_ROTATION == 1
|
||||
applyDisplayRotation();
|
||||
#endif
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
#include <helpers/IdentityGeneration.h>
|
||||
#include <helpers/ui/StartupScreen.h>
|
||||
#include <helpers/ui/DisplayPowerSettings.h>
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED && defined(DISPLAY_CLASS)
|
||||
#include <helpers/ui/HeltecV4DisplaySettings.h>
|
||||
#endif
|
||||
#if defined(OLED_AUTO_DETECT) && OLED_AUTO_DETECT
|
||||
#include <helpers/ui/OLEDControllerSettings.h>
|
||||
#endif
|
||||
@@ -229,6 +232,9 @@ void setup() {
|
||||
#ifdef DISPLAY_CLASS
|
||||
mesh::hilStartupTrace("display_begin");
|
||||
mesh::ui::loadDisplayPowerSettings(fs, false);
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED
|
||||
mesh::ui::loadHeltecV4DisplaySettings(regulator_prefs_fs, display);
|
||||
#endif
|
||||
#if defined(OLED_AUTO_DETECT) && OLED_AUTO_DETECT
|
||||
mesh::ui::loadOLEDControllerSettings(regulator_prefs_fs);
|
||||
#endif
|
||||
@@ -405,6 +411,11 @@ void setup() {
|
||||
#endif
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED
|
||||
// Initialize the UI dependency even when auto detection found no screen.
|
||||
// A later saved display.type change can enable it without restarting.
|
||||
display_ready = true;
|
||||
#endif
|
||||
if (display_ready) {
|
||||
mesh::hilStartupTrace("display_task_begin");
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
@@ -641,7 +652,11 @@ void loop() {
|
||||
const bool morse_timing = mesh::morseTimingActive(&radio_driver);
|
||||
if (!morse_timing) sensors.loop();
|
||||
#ifdef DISPLAY_CLASS
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED
|
||||
if (display.selectedType() != mesh::ui::HeltecV4DisplayType::None) ui_task.loop();
|
||||
#else
|
||||
if (display_ready) ui_task.loop();
|
||||
#endif
|
||||
#endif
|
||||
rtc_clock.tick();
|
||||
if (mesh::serviceUsbLoggingWatchdog(usbLoggingRecoverySafe)) board.reboot();
|
||||
|
||||
@@ -518,6 +518,19 @@ void UITask::toggleDisplay(const char* source) {
|
||||
#endif
|
||||
|
||||
void UITask::loop() {
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED
|
||||
static uint32_t display_generation = 0;
|
||||
if (display_generation != display.generation()) {
|
||||
display_generation = display.generation();
|
||||
_next_refresh = millis();
|
||||
#ifdef DISPLAY_REDRAW_ON_CHANGE
|
||||
_frame_valid = false;
|
||||
#endif
|
||||
#ifdef DISPLAY_ACTIVITY_DASHBOARD
|
||||
_rows_valid = false;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
#if defined(HELTEC_V4_R8_TFT) && defined(HELTEC_TFT_RUNTIME_ROTATION) && HELTEC_TFT_RUNTIME_ROTATION == 1
|
||||
applyDisplayRotation();
|
||||
#endif
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
#include <helpers/IdentityGeneration.h>
|
||||
#include <helpers/ui/StartupScreen.h>
|
||||
#include <helpers/ui/DisplayPowerSettings.h>
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED && defined(DISPLAY_CLASS)
|
||||
#include <helpers/ui/HeltecV4DisplaySettings.h>
|
||||
#endif
|
||||
#if defined(OLED_AUTO_DETECT) && OLED_AUTO_DETECT
|
||||
#include <helpers/ui/OLEDControllerSettings.h>
|
||||
#endif
|
||||
@@ -161,6 +164,9 @@ void setup() {
|
||||
mesh::RadioProfileCLI::configureBootTuning(regulator_prefs_fs, radio_driver);
|
||||
#ifdef DISPLAY_CLASS
|
||||
mesh::ui::loadDisplayPowerSettings(fs, false);
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED
|
||||
mesh::ui::loadHeltecV4DisplaySettings(regulator_prefs_fs, display);
|
||||
#endif
|
||||
#if defined(OLED_AUTO_DETECT) && OLED_AUTO_DETECT
|
||||
mesh::ui::loadOLEDControllerSettings(regulator_prefs_fs);
|
||||
#endif
|
||||
@@ -279,6 +285,11 @@ void setup() {
|
||||
#endif
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED
|
||||
// Initialize the UI dependency even when auto detection found no screen.
|
||||
// A later saved display.type change can enable it without restarting.
|
||||
display_ready = true;
|
||||
#endif
|
||||
if (display_ready) {
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
ui_task.setObserverPrefs(the_mesh.getObserverPrefs());
|
||||
@@ -415,7 +426,11 @@ void loop() {
|
||||
const bool morse_timing = mesh::morseTimingActive(&radio_driver);
|
||||
if (!morse_timing) sensors.loop();
|
||||
#ifdef DISPLAY_CLASS
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED
|
||||
if (display.selectedType() != mesh::ui::HeltecV4DisplayType::None) ui_task.loop();
|
||||
#else
|
||||
if (display_ready) ui_task.loop();
|
||||
#endif
|
||||
#endif
|
||||
rtc_clock.tick();
|
||||
mesh::power::serviceRegulatorSettings();
|
||||
|
||||
@@ -226,6 +226,19 @@ void UITask::renderCurrScreen() {
|
||||
}
|
||||
|
||||
void UITask::loop() {
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED
|
||||
static uint32_t display_generation = 0;
|
||||
if (display_generation != display.generation()) {
|
||||
display_generation = display.generation();
|
||||
_last_refresh = millis() - 1000u;
|
||||
#ifdef DISPLAY_REDRAW_ON_CHANGE
|
||||
_frame_valid = false;
|
||||
#endif
|
||||
#ifdef DISPLAY_ACTIVITY_DASHBOARD
|
||||
_rows_valid = false;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
#if defined(HELTEC_V4_R8_TFT) && defined(HELTEC_TFT_RUNTIME_ROTATION) && HELTEC_TFT_RUNTIME_ROTATION == 1
|
||||
applyDisplayRotation();
|
||||
#endif
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
#include <helpers/IdentityGeneration.h>
|
||||
#include <helpers/ui/StartupScreen.h>
|
||||
#include <helpers/ui/DisplayPowerSettings.h>
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED && defined(DISPLAY_CLASS)
|
||||
#include <helpers/ui/HeltecV4DisplaySettings.h>
|
||||
#endif
|
||||
#if defined(OLED_AUTO_DETECT) && OLED_AUTO_DETECT
|
||||
#include <helpers/ui/OLEDControllerSettings.h>
|
||||
#endif
|
||||
@@ -179,6 +182,9 @@ void setup() {
|
||||
mesh::RadioProfileCLI::configureBootTuning(regulator_prefs_fs, radio_driver);
|
||||
#ifdef DISPLAY_CLASS
|
||||
mesh::ui::loadDisplayPowerSettings(fs, false);
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED
|
||||
mesh::ui::loadHeltecV4DisplaySettings(regulator_prefs_fs, display);
|
||||
#endif
|
||||
#if defined(OLED_AUTO_DETECT) && OLED_AUTO_DETECT
|
||||
mesh::ui::loadOLEDControllerSettings(regulator_prefs_fs);
|
||||
#endif
|
||||
@@ -294,6 +300,11 @@ void setup() {
|
||||
#endif
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED
|
||||
// Initialize the UI dependency even when auto detection found no screen.
|
||||
// A later saved display.type change can enable it without restarting.
|
||||
display_ready = true;
|
||||
#endif
|
||||
if (display_ready) {
|
||||
ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION);
|
||||
}
|
||||
@@ -376,7 +387,11 @@ void loop() {
|
||||
const bool morse_timing = mesh::morseTimingActive(&radio_driver);
|
||||
if (!morse_timing) sensors.loop();
|
||||
#ifdef DISPLAY_CLASS
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED
|
||||
if (display.selectedType() != mesh::ui::HeltecV4DisplayType::None) ui_task.loop();
|
||||
#else
|
||||
if (display_ready) ui_task.loop();
|
||||
#endif
|
||||
#endif
|
||||
rtc_clock.tick();
|
||||
if (mesh::serviceUsbLoggingWatchdog(usbLoggingRecoverySafe)) board.reboot();
|
||||
|
||||
@@ -10,6 +10,11 @@ import sys
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote
|
||||
|
||||
# Shared declarations describe future source capabilities, never historical
|
||||
# filename aliases. The resolver below also inspects the actual application.
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
|
||||
from consolidated_profiles import SPECS as CONSOLIDATED_SPECS, proven, unified_v4_replacements
|
||||
|
||||
|
||||
FIRMWARE_EXTENSIONS = {".bin", ".hex", ".uf2", ".zip"}
|
||||
FILENAME_VERSION_RE = re.compile(
|
||||
@@ -637,9 +642,38 @@ def release_identity_is_full_companion(
|
||||
return identity in release_files and "_companion_radio_full" in identity.lower()
|
||||
|
||||
|
||||
def qualified_unified_v4_identity(identity: str, paths: list[Path]) -> bool:
|
||||
spec = next(spec for spec in CONSOLIDATED_SPECS if spec["key"] == "unifiedV4Display")
|
||||
for path in paths:
|
||||
if path.suffix.lower() != ".bin" or path.stem.endswith("-merged"):
|
||||
continue
|
||||
proof_path = path.with_suffix(".capabilities.json")
|
||||
if not proof_path.is_file():
|
||||
continue
|
||||
manifest = json.loads(proof_path.read_text())
|
||||
if (manifest.get("artifact_target", manifest.get("target")) != identity
|
||||
or not proven(manifest, spec)):
|
||||
continue
|
||||
if spec["marker"].encode("ascii") in path.read_bytes():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def unified_v4_release_identity(old_identity: str, release_files: dict[str, list[Path]]) -> str | None:
|
||||
base = re.sub(r"-(?:full-usb-wifi-ota|full-logging-ota|full-ota|ota|logging)$", "", old_identity)
|
||||
for target, _key in unified_v4_replacements(base):
|
||||
for identity in (target + "-full-usb-wifi-ota", target + "-full-logging-ota", target):
|
||||
if identity in release_files and qualified_unified_v4_identity(identity, release_files[identity]):
|
||||
return identity
|
||||
return None
|
||||
|
||||
|
||||
def resolve_release_identity(
|
||||
old_identity: str, release_files: dict[str, list[Path]]
|
||||
) -> tuple[str, bool]:
|
||||
unified_v4 = unified_v4_release_identity(old_identity, release_files)
|
||||
if unified_v4 is not None:
|
||||
return unified_v4, unified_v4.endswith("-full-usb-wifi-ota")
|
||||
candidates = release_identity_candidates(old_identity)
|
||||
for candidate in candidates:
|
||||
if candidate in release_files:
|
||||
@@ -1121,6 +1155,16 @@ def update_catalog(catalog: dict, release_files: dict[str, list[Path]], args: ar
|
||||
firmware, notes
|
||||
)
|
||||
|
||||
if any(qualified_unified_v4_identity(identity, release_files[identity])
|
||||
for identity in resolved_identities if identity.startswith("heltec_v4_")):
|
||||
firmware["subTitle"] = "V4.2/V4.3 automatic OLED / TFT / no screen"
|
||||
notes += (
|
||||
"\n\nDISPLAY - One ordinary V4 image detects OLED, the stock touch TFT "
|
||||
"kit, or no screen. Saved display.type accepts auto, oled, tft and off. "
|
||||
"R8 remains separate. Changing from a legacy TFT/expansion-kit target "
|
||||
"requires USB or supported WiFi updating; LoRa OTA target IDs stay distinct."
|
||||
)
|
||||
|
||||
firmware["version"] = {version_key: {"notes": notes, "files": files}}
|
||||
updated_entries += 1
|
||||
updated_files += len(files)
|
||||
|
||||
@@ -66,6 +66,7 @@ build_src_filter =
|
||||
+<helpers/ui/DisplayNightLocation.cpp>
|
||||
+<helpers/ui/DisplayRotationSettings.cpp>
|
||||
+<helpers/ui/OLEDControllerSettings.cpp>
|
||||
+<helpers/ui/HeltecV4DisplaySettings.cpp>
|
||||
+<helpers/ota/OtaTinf.c>
|
||||
; MQTT-only sources (need Timezone/NTPClient); observer envs add them back.
|
||||
-<helpers/MQTTMessageBuilder.cpp>
|
||||
|
||||
@@ -162,6 +162,15 @@ def requirements(platform, defines, target):
|
||||
if platform != "ESP32_PLATFORM":
|
||||
raise ValueError("Wio Tracker L2 display budget requires ESP32")
|
||||
display_heap = wio_tracker_l2_display_heap(defines)
|
||||
elif display == "HeltecV4Display":
|
||||
if platform != "ESP32_PLATFORM" or not integer(defines, "HELTEC_V4_DISPLAY_UNIFIED", 0):
|
||||
raise ValueError("unified Heltec V4 display budget requires ESP32 and its unified recipe")
|
||||
# Both backend objects and the private TwoWire instance are linked
|
||||
# globals. The OLED nevertheless retains its one 1024-byte framebuffer
|
||||
# after a live switch to TFT. The source assertion conservatively bounds
|
||||
# the complete wrapper plus that allocation and allocator metadata to
|
||||
# 2 KiB. TFT draws directly; it allocates no RGB framebuffer/canvas.
|
||||
display_heap = 2048
|
||||
elif display == "ST7789Display":
|
||||
display_heap = st7789_display_heap(defines)
|
||||
elif display == "ST7735Display":
|
||||
@@ -187,6 +196,14 @@ def requirements(platform, defines, target):
|
||||
# (up to 320 rows, including overlap while growing) and SDK bus/touch
|
||||
# driver metadata. These allocations coexist with the UI sprite.
|
||||
parts["display_dma_and_bus"] = 16384
|
||||
if display == "HeltecV4Display":
|
||||
# Pinned Arduino-ESP32 2.x: the private I2C controller owns two bounded
|
||||
# 128-byte buffers, HAL/IDF locks, master state and transient command
|
||||
# links. Switching to TFT can retain its SPI/parameter mutexes while
|
||||
# the OLED framebuffer remains allocated. Reserve those SDK/bus owners
|
||||
# together, even on a headless/off boot: a saved runtime choice can
|
||||
# activate either panel later. Keep core/transient margins unchanged.
|
||||
parts["display_i2c_spi_and_probe"] = 4096
|
||||
mqtt = None
|
||||
combined_rak_ethernet = "RAK4631_COMBINED_ETHERNET" in defines
|
||||
cooperative_rak_ethernet = not companion and (combined_rak_ethernet or (
|
||||
@@ -411,6 +428,7 @@ def requirements(platform, defines, target):
|
||||
# including worst-case 8-byte Entry alignment.
|
||||
small_display = display in {
|
||||
"SSD1306Display", "SH1106Display", "ST7735Display", "U8g2Display",
|
||||
"HeltecV4Display",
|
||||
}
|
||||
small_font = integer(defines, "UI_SMALL_MESSAGE_FONT", int(small_display))
|
||||
default_preview = 161 if small_font else 78
|
||||
|
||||
@@ -6,6 +6,13 @@ import sys
|
||||
from optional_display_profiles import resolved_defines, spec_for_defines
|
||||
|
||||
SPECS = (
|
||||
dict(key="unifiedV4Display", capability="display.heltec_v4.unified",
|
||||
marker="meshcore.capability.display.heltec_v4.unified.v1",
|
||||
platform="ESP32_PLATFORM", board="heltec_v4",
|
||||
target=(r"heltec_v4(?:_2_v4_3|(?:_3)?(?:_expansionkit)?(?:_tft)?)_"
|
||||
r"(?:(?:repeater(?:_bridge_espnow)?|room_server)(?:_observer_mqtt)?|sensor|"
|
||||
r"companion_radio_(?:usb|ble|wifi|full)(?:_mqtt)?(?:_ps)?(?:_fem(?:on|off))?)"),
|
||||
defines={"HELTEC_V4_DISPLAY_UNIFIED": "1", "DISPLAY_CLASS": "HeltecV4Display"}),
|
||||
dict(key="combinedCompanion", capability="companion.picow.combined",
|
||||
marker="meshcore.capability.companion.picow.combined.v1",
|
||||
platform="RP2040_PLATFORM", board="rpipicow", target=r"PicoW_companion_radio",
|
||||
@@ -62,12 +69,34 @@ def runtime_metadata(manifest, env, release_source):
|
||||
return result
|
||||
|
||||
|
||||
def unified_v4_replacements(target):
|
||||
"""Ordinary V4 display successors; R8 and deployed mOTA IDs stay separate."""
|
||||
match = re.fullmatch(
|
||||
r"heltec_v4(?:_3)?(?:_expansionkit)?(?:_tft)?_"
|
||||
r"(repeater(?:_bridge_espnow)?|room_server|sensor)(_observer_mqtt)?", target, re.I)
|
||||
if match:
|
||||
role, observer = match.groups()
|
||||
base = "heltec_v4_" + role.lower()
|
||||
choices = [base + (observer or ""), base]
|
||||
if role.lower() == "repeater_bridge_espnow":
|
||||
choices.append("heltec_v4_repeater")
|
||||
return [(name, "unifiedV4Display") for name in dict.fromkeys(choices)
|
||||
if name.lower() != target.lower()]
|
||||
if re.fullmatch(
|
||||
r"heltec_v4(?:_3)?(?:_expansionkit)?(?:_tft)?_companion_radio_"
|
||||
r"(?:usb|ble|wifi|full)(?:_ps)?(?:_fem(?:on|off))?", target, re.I):
|
||||
canonical = "heltec_v4_2_v4_3_companion_radio_full_femon"
|
||||
return [(canonical, "unifiedV4Display")]
|
||||
return []
|
||||
|
||||
|
||||
def replacements(target):
|
||||
"""Known functionality successors, never aliases for deployed update IDs."""
|
||||
unified = unified_v4_replacements(target)
|
||||
if re.fullmatch(r"PicoW_companion_radio_(?:usb|ble|wifi)", target, re.I):
|
||||
return [("PicoW_companion_radio", "combinedCompanion")]
|
||||
if re.fullmatch(r"heltec_v4(?:_3)?_expansionkit_tft_companion_radio_(?:usb|ble|wifi|full)(?:_ps)?(?:_fem(?:on|off))?", target, re.I):
|
||||
return [("heltec_v4_tft_companion_radio_full_femon", "tftExpansionCompatible")]
|
||||
return unified + [("heltec_v4_tft_companion_radio_full_femon", "tftExpansionCompatible")]
|
||||
match = re.fullmatch(r"muzi_base_(uno|duo)_superio_companion_radio_(?:usb|ble|full)", target, re.I)
|
||||
if match:
|
||||
return [("muzi_base_" + match[1].lower() + "_companion_radio_full", "muziAccessories")]
|
||||
@@ -76,7 +105,7 @@ def replacements(target):
|
||||
match = re.fullmatch(r"heltec_v4_expansionkit_(repeater|room_server)(_observer_mqtt)?", target, re.I)
|
||||
if match:
|
||||
base = "heltec_v4_" + match[1].lower()
|
||||
return [(base + (match[2] or ""), "oledExpansionCompatible"), (base, "oledExpansionCompatible")]
|
||||
return unified + [(base + (match[2] or ""), "oledExpansionCompatible"), (base, "oledExpansionCompatible")]
|
||||
match = re.fullmatch(r"heltec_v4_r8_tft_portrait_(repeater|room_server)_observer_mqtt", target, re.I)
|
||||
if match:
|
||||
base = "heltec_v4_r8_tft_" + match[1].lower()
|
||||
@@ -84,7 +113,7 @@ def replacements(target):
|
||||
match = re.fullmatch(r"heltec_rc32_without_display_(repeater|room_server|sensor|companion_radio_(?:usb|ble|wifi|full))(?:_observer_mqtt)?", target, re.I)
|
||||
if match:
|
||||
return [(target.lower().replace("_without_display_", "_", 1), "displayAuto")]
|
||||
return []
|
||||
return unified
|
||||
|
||||
|
||||
def configuration_replacement(target, envs):
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
#include <helpers/esp32/PowerTestControls.h>
|
||||
#endif
|
||||
#include <helpers/ui/DisplayPowerSettings.h>
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED && defined(DISPLAY_CLASS)
|
||||
#include <helpers/ui/HeltecV4DisplaySettings.h>
|
||||
#endif
|
||||
#if defined(OLED_AUTO_DETECT) && OLED_AUTO_DETECT && defined(DISPLAY_CLASS)
|
||||
#include <helpers/ui/OLEDControllerSettings.h>
|
||||
#endif
|
||||
@@ -3553,6 +3556,9 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
|
||||
}
|
||||
return;
|
||||
}
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED && defined(DISPLAY_CLASS)
|
||||
if (mesh::ui::handleHeltecV4DisplayCommand(command, reply, 160, display)) return;
|
||||
#endif
|
||||
if (mesh::ui::handleDisplayPowerCommand(command, reply, 160)) return;
|
||||
#if defined(OLED_AUTO_DETECT) && OLED_AUTO_DETECT && defined(DISPLAY_CLASS)
|
||||
if (mesh::ui::handleOLEDControllerCommand(command, reply, 160, true)) return;
|
||||
|
||||
@@ -5,7 +5,15 @@
|
||||
// disconnected-pin sentinel. Keep every consumer on the same validity rule.
|
||||
#if defined(ENV_PIN_SDA) && defined(ENV_PIN_SCL) \
|
||||
&& (ENV_PIN_SDA >= 0) && (ENV_PIN_SCL >= 0)
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED \
|
||||
&& defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL) \
|
||||
&& ENV_PIN_SDA == PIN_BOARD_SDA && ENV_PIN_SCL == PIN_BOARD_SCL
|
||||
// Unified V4 reserves controller 1 for the display/touch probe. Expansion
|
||||
// sensors share the primary board bus, not a second controller on those pins.
|
||||
#define ENV_HAS_SECONDARY_I2C 0
|
||||
#else
|
||||
#define ENV_HAS_SECONDARY_I2C 1
|
||||
#endif
|
||||
#else
|
||||
#define ENV_HAS_SECONDARY_I2C 0
|
||||
#endif
|
||||
@@ -6,6 +6,7 @@
|
||||
#define MESHCORE_NULL_DISPLAY_CLASS_NullDisplayDriver 1
|
||||
#define MESHCORE_SMALL_DISPLAY_CLASS_SH1106Display 1
|
||||
#define MESHCORE_SMALL_DISPLAY_CLASS_SSD1306Display 1
|
||||
#define MESHCORE_SMALL_DISPLAY_CLASS_HeltecV4Display 1
|
||||
#define MESHCORE_SMALL_DISPLAY_CLASS_ST7735Display 1
|
||||
#define MESHCORE_SMALL_DISPLAY_CLASS_U8g2Display 1
|
||||
#define MESHCORE_EINK_DISPLAY_CLASS_GxEPDDisplay 1
|
||||
@@ -14,6 +15,7 @@
|
||||
#define MESHCORE_COLOR_DISPLAY_CLASS_ST7735Display 1
|
||||
#define MESHCORE_COLOR_DISPLAY_CLASS_ST7789Display 1
|
||||
#define MESHCORE_COLOR_DISPLAY_CLASS_ST7789LCDDisplay 1
|
||||
#define MESHCORE_COLOR_DISPLAY_CLASS_HeltecV4Display 1
|
||||
#define MESHCORE_COLOR_DISPLAY_CLASS_NV3001BDisplay 1
|
||||
#define MESHCORE_COLOR_DISPLAY_CLASS_LGFXDisplay 1
|
||||
#define MESHCORE_COLOR_DISPLAY_CLASS_SCIndicatorDisplay 1
|
||||
@@ -63,11 +65,13 @@
|
||||
#undef MESHCORE_COLOR_DISPLAY_CLASS_LGFXDisplay
|
||||
#undef MESHCORE_COLOR_DISPLAY_CLASS_NV3001BDisplay
|
||||
#undef MESHCORE_COLOR_DISPLAY_CLASS_ST7789LCDDisplay
|
||||
#undef MESHCORE_COLOR_DISPLAY_CLASS_HeltecV4Display
|
||||
#undef MESHCORE_COLOR_DISPLAY_CLASS_ST7789Display
|
||||
#undef MESHCORE_COLOR_DISPLAY_CLASS_ST7735Display
|
||||
#undef MESHCORE_SMALL_DISPLAY_CLASS_U8g2Display
|
||||
#undef MESHCORE_SMALL_DISPLAY_CLASS_ST7735Display
|
||||
#undef MESHCORE_SMALL_DISPLAY_CLASS_SSD1306Display
|
||||
#undef MESHCORE_SMALL_DISPLAY_CLASS_HeltecV4Display
|
||||
#undef MESHCORE_SMALL_DISPLAY_CLASS_SH1106Display
|
||||
#undef MESHCORE_EINK_DISPLAY_CLASS_E290Display
|
||||
#undef MESHCORE_EINK_DISPLAY_CLASS_E213Display
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED
|
||||
|
||||
#include "HeltecV4Display.h"
|
||||
#include "HeltecV4DisplayProbe.h"
|
||||
#include "ColorTheme.h"
|
||||
|
||||
// Runtime budget guard: both concrete drivers/private Wire are linked globals,
|
||||
// but count them conservatively with the one retained OLED heap framebuffer.
|
||||
// The separate 4 KiB SDK/bus allowance covers I2C/SPI locks, I2C driver state,
|
||||
// command links and both Wire buffers. TFT has no dynamic pixel canvas.
|
||||
static_assert(SSD1306Display::FRAMEBUFFER_BYTES == 1024,
|
||||
"Update the unified Heltec V4 OLED framebuffer budget");
|
||||
static_assert(sizeof(HeltecV4Display) + SSD1306Display::FRAMEBUFFER_BYTES + 32 <= 2048,
|
||||
"Update the unified Heltec V4 display runtime budget");
|
||||
static_assert(I2C_BUFFER_LENGTH <= 128,
|
||||
"Update the unified Heltec V4 private I2C runtime budget");
|
||||
|
||||
extern const char meshcore_capability_display_heltec_v4_unified[] =
|
||||
"meshcore.capability.display.heltec_v4.unified.v1";
|
||||
extern const char meshcore_capability_display_autodetect_heltec_v4[] =
|
||||
"meshcore.capability.display.autodetect.heltec_v4.v1";
|
||||
|
||||
// Both backends are linked into this image. Only the wrapper owns the shared
|
||||
// palette; monochrome OLED and RGB TFT colors are selected at activation.
|
||||
ColorVal UIColor::window_bkg = 0;
|
||||
ColorVal UIColor::title_bkg = 0;
|
||||
ColorVal UIColor::title_txt = 1;
|
||||
ColorVal UIColor::primary_txt = 1;
|
||||
ColorVal UIColor::secondary_txt = 1;
|
||||
ColorVal UIColor::warning_txt = 1;
|
||||
ColorVal UIColor::popup_bkg = 0;
|
||||
ColorVal UIColor::popup_txt = 1;
|
||||
ColorVal UIColor::corp_blue = 1;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kOledSda = 17;
|
||||
constexpr int kOledScl = 18;
|
||||
constexpr int kTouchSda = 47;
|
||||
constexpr int kTouchScl = 48;
|
||||
constexpr int kTouchReset = 44;
|
||||
constexpr uint8_t kTouchAddress = 0x2E;
|
||||
|
||||
// The original expansion kit routes the LCD's SDO to ground, so its LCD
|
||||
// cannot identify itself through SPI. Read the stock CHSC touch assembly's
|
||||
// documented identity instead. This identifies the assembly, not LCD health.
|
||||
struct TouchIo {
|
||||
TwoWire& wire;
|
||||
|
||||
void reset() {
|
||||
digitalWrite(kTouchReset, LOW);
|
||||
pinMode(kTouchReset, OUTPUT);
|
||||
delay(20);
|
||||
digitalWrite(kTouchReset, HIGH);
|
||||
}
|
||||
void delayMs(unsigned ms) { delay(ms); }
|
||||
bool write(const uint8_t* data, size_t length) {
|
||||
wire.beginTransmission(kTouchAddress);
|
||||
const bool complete = wire.write(data, length) == length;
|
||||
return wire.endTransmission() == 0 && complete;
|
||||
}
|
||||
bool readRegister(uint16_t address, uint8_t* data, size_t length) {
|
||||
wire.beginTransmission(kTouchAddress);
|
||||
const uint8_t reg[] = {uint8_t(address >> 8), uint8_t(address)};
|
||||
const bool complete = wire.write(reg, sizeof(reg)) == sizeof(reg);
|
||||
// The manufacturer's DMA-read sequence uses STOP between the address
|
||||
// write and the read, rather than relying on repeated-start support.
|
||||
if (wire.endTransmission() != 0 || !complete) return false;
|
||||
const size_t received = wire.requestFrom(kTouchAddress, uint8_t(length));
|
||||
const bool ok = received == length && wire.available() >= int(length);
|
||||
for (size_t i = 0; i < length; ++i) {
|
||||
data[i] = wire.available() ? uint8_t(wire.read()) : 0xFF;
|
||||
}
|
||||
while (wire.available()) wire.read();
|
||||
return ok;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
void HeltecV4Display::claimPower() {
|
||||
if (_power && !_power_claimed) {
|
||||
_power->claim();
|
||||
_power_claimed = true;
|
||||
delay(10);
|
||||
}
|
||||
}
|
||||
|
||||
void HeltecV4Display::releasePower() {
|
||||
if (_power && _power_claimed) {
|
||||
_power->release();
|
||||
_power_claimed = false;
|
||||
}
|
||||
}
|
||||
|
||||
void HeltecV4Display::releasePins() {
|
||||
if (_wire_started) {
|
||||
_display_wire.end();
|
||||
_wire_started = false;
|
||||
}
|
||||
// No inactive driver may back-power a disconnected panel. Wire on GPIO4/3
|
||||
// belongs to RTC/environmental sensors and is never stopped or remapped.
|
||||
const int pins[] = {PIN_TFT_CS, PIN_TFT_DC, PIN_TFT_SDA, PIN_TFT_SCL,
|
||||
PIN_TFT_RST, PIN_TFT_LEDA_CTL, PIN_OLED_RESET,
|
||||
kTouchSda, kTouchScl, kTouchReset};
|
||||
for (int pin : pins) if (pin >= 0) pinMode(pin, INPUT);
|
||||
if (_known_tft) {
|
||||
// The original kit's LCD has an independent supply. Keep its backlight
|
||||
// and reset positively off just like the dedicated TFT driver; floating
|
||||
// either output is not an off command. Cold headless/off never does this.
|
||||
digitalWrite(PIN_TFT_LEDA_CTL, !PIN_TFT_LEDA_CTL_ACTIVE);
|
||||
pinMode(PIN_TFT_LEDA_CTL, OUTPUT);
|
||||
digitalWrite(PIN_TFT_RST, LOW);
|
||||
pinMode(PIN_TFT_RST, OUTPUT);
|
||||
}
|
||||
}
|
||||
|
||||
void HeltecV4Display::deactivate() {
|
||||
if (_active && _active->isOn()) _active->turnOff();
|
||||
if (_type == Type::Tft) _tft.releaseBus();
|
||||
_active = nullptr;
|
||||
_type = Type::None;
|
||||
releasePins();
|
||||
releasePower();
|
||||
}
|
||||
|
||||
bool HeltecV4Display::startOledBus() {
|
||||
if (_wire_started) return true;
|
||||
_wire_started = _display_wire.begin(kOledSda, kOledScl, 100000);
|
||||
if (_wire_started) _display_wire.setTimeOut(20);
|
||||
return _wire_started;
|
||||
}
|
||||
|
||||
bool HeltecV4Display::probeOled() {
|
||||
// Keep the TFT deselected while the same GPIO17/18 pair is briefly used
|
||||
// as OLED I2C. GPIO21 is OLED reset / TFT backlight; release it before ACK.
|
||||
digitalWrite(PIN_TFT_CS, HIGH);
|
||||
pinMode(PIN_TFT_CS, OUTPUT);
|
||||
digitalWrite(PIN_OLED_RESET, LOW);
|
||||
pinMode(PIN_OLED_RESET, OUTPUT);
|
||||
delay(10);
|
||||
digitalWrite(PIN_OLED_RESET, HIGH);
|
||||
delay(10);
|
||||
if (!startOledBus()) return false;
|
||||
_display_wire.beginTransmission(DISPLAY_ADDRESS);
|
||||
return _display_wire.endTransmission() == 0;
|
||||
}
|
||||
|
||||
bool HeltecV4Display::probeTouchAssembly() {
|
||||
if (_wire_started) {
|
||||
_display_wire.end();
|
||||
_wire_started = false;
|
||||
}
|
||||
pinMode(kOledSda, INPUT);
|
||||
pinMode(kOledScl, INPUT);
|
||||
// Avoid flashing the backlight while resetting and identifying touch.
|
||||
digitalWrite(PIN_TFT_LEDA_CTL, !PIN_TFT_LEDA_CTL_ACTIVE);
|
||||
pinMode(PIN_TFT_LEDA_CTL, OUTPUT);
|
||||
if (!_display_wire.begin(kTouchSda, kTouchScl, 100000)) return false;
|
||||
_display_wire.setTimeOut(20);
|
||||
TouchIo io{_display_wire};
|
||||
const bool present = mesh::ui::probeHeltecV4Touch(io);
|
||||
_display_wire.end();
|
||||
pinMode(kTouchSda, INPUT);
|
||||
pinMode(kTouchScl, INPUT);
|
||||
if (!present) pinMode(kTouchReset, INPUT);
|
||||
return present;
|
||||
}
|
||||
|
||||
void HeltecV4Display::setPalette() {
|
||||
using namespace mesh::ui::color_theme;
|
||||
const bool color = _type == Type::Tft;
|
||||
UIColor::window_bkg = color ? WINDOW_BACKGROUND : 0;
|
||||
UIColor::title_bkg = color ? TITLE_BACKGROUND : 0;
|
||||
UIColor::title_txt = color ? TEXT : 1;
|
||||
UIColor::primary_txt = color ? TEXT : 1;
|
||||
UIColor::secondary_txt = color ? SECONDARY_TEXT : 1;
|
||||
UIColor::warning_txt = color ? WARNING_TEXT : 1;
|
||||
UIColor::popup_bkg = color ? POPUP_BACKGROUND : 0;
|
||||
UIColor::popup_txt = color ? TEXT : 1;
|
||||
UIColor::corp_blue = color ? ACCENT : 1;
|
||||
}
|
||||
|
||||
void HeltecV4Display::updateDimensions() {
|
||||
setDimensions(_active ? _active->width() : 128,
|
||||
_active ? _active->height() : 64);
|
||||
}
|
||||
|
||||
bool HeltecV4Display::startSelected(Type type) {
|
||||
bool ready = false;
|
||||
if (type == Type::Oled && startOledBus()) {
|
||||
_oled.setRotationDegrees(_rotation);
|
||||
_oled.setFlipped(_flipped);
|
||||
ready = _oled.begin();
|
||||
if (ready) {
|
||||
_active = &_oled;
|
||||
_known_tft = false;
|
||||
}
|
||||
} else if (type == Type::Tft) {
|
||||
_known_tft = true;
|
||||
if (_wire_started) {
|
||||
_display_wire.end();
|
||||
_wire_started = false;
|
||||
}
|
||||
_tft.setRotationDegrees(_rotation);
|
||||
_tft.setFlipped(_flipped);
|
||||
ready = _tft.begin();
|
||||
if (ready) _active = &_tft;
|
||||
}
|
||||
_type = ready ? type : Type::None;
|
||||
if (!ready) {
|
||||
releasePins();
|
||||
releasePower();
|
||||
}
|
||||
setPalette();
|
||||
updateDimensions();
|
||||
return ready;
|
||||
}
|
||||
|
||||
bool HeltecV4Display::begin() {
|
||||
const volatile char* unified = meshcore_capability_display_heltec_v4_unified;
|
||||
(void)*unified;
|
||||
// A boot with display.type=off must not enable any display/probe outputs.
|
||||
if (_started) deactivate();
|
||||
_started = true;
|
||||
++_generation;
|
||||
if (_mode == Mode::Off) {
|
||||
_status = "off";
|
||||
setPalette();
|
||||
updateDimensions();
|
||||
return false;
|
||||
}
|
||||
claimPower();
|
||||
bool oled = false, tft = false;
|
||||
if (_mode == Mode::Auto) {
|
||||
const volatile char* autodetect = meshcore_capability_display_autodetect_heltec_v4;
|
||||
(void)*autodetect;
|
||||
oled = probeOled();
|
||||
if (!oled) tft = probeTouchAssembly();
|
||||
}
|
||||
const Type selected = mesh::ui::selectHeltecV4Display(_mode, oled, tft);
|
||||
if (selected == Type::None) {
|
||||
_known_tft = false; // A rescan found no assembly; leave header pins free.
|
||||
releasePins();
|
||||
releasePower();
|
||||
_status = "none detected";
|
||||
setPalette();
|
||||
updateDimensions();
|
||||
return false;
|
||||
}
|
||||
const bool ready = startSelected(selected);
|
||||
_status = !ready ? "display initialization failed"
|
||||
: selected == Type::Oled ? "OLED"
|
||||
: _mode == Mode::Tft ? "TFT (forced)" : "TFT";
|
||||
return ready;
|
||||
}
|
||||
|
||||
bool HeltecV4Display::setMode(Mode mode) {
|
||||
if (unsigned(mode) > unsigned(Mode::Off)) return false;
|
||||
_mode = mode;
|
||||
if (_started) begin();
|
||||
// A valid off/headless choice is successful even though begin() reports no
|
||||
// active screen. The CLI can report status() independently of setting save.
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HeltecV4Display::setRotationDegrees(uint16_t degrees) {
|
||||
if (degrees > 270 || degrees % 90 != 0) return false;
|
||||
_rotation = degrees;
|
||||
// Only touch the selected backend: a dormant OLED may retain an allocated
|
||||
// framebuffer and must not write into a bus now owned by the TFT.
|
||||
if (isOn()) _active->setRotationDegrees(degrees);
|
||||
updateDimensions();
|
||||
return true;
|
||||
}
|
||||
|
||||
void HeltecV4Display::setFlipped(bool flipped) {
|
||||
_flipped = flipped;
|
||||
if (isOn()) _active->setFlipped(flipped);
|
||||
updateDimensions();
|
||||
}
|
||||
|
||||
void HeltecV4Display::turnOn() {
|
||||
if (!_active || _active->isOn()) return;
|
||||
claimPower();
|
||||
// The wrapper owns the rail. Reinitialize after it was cut, even though
|
||||
// each embedded driver's nullptr power owner would otherwise skip reset.
|
||||
const Type type = _type;
|
||||
_active = nullptr;
|
||||
if (!startSelected(type)) _status = "display initialization failed";
|
||||
}
|
||||
|
||||
void HeltecV4Display::turnOff() {
|
||||
if (_active && _active->isOn()) _active->turnOff();
|
||||
if (_type == Type::Tft) _tft.releaseBus();
|
||||
releasePins();
|
||||
releasePower();
|
||||
}
|
||||
|
||||
ColorVal HeltecV4Display::mapColor(ColorVal color) const {
|
||||
if (_type != Type::Oled) return color;
|
||||
// SSD1306 uses literal0/1/2, not RGB565 or just the RGB value's low bit.
|
||||
return mesh::ui::color_theme::isBackground(color) ? 0 : 1;
|
||||
}
|
||||
|
||||
int HeltecV4Display::textLineHeight() { return _active ? _active->textLineHeight() : 8; }
|
||||
int HeltecV4Display::renderWidth() const { return _active ? _active->renderWidth() : width(); }
|
||||
int HeltecV4Display::renderHeight() const { return _active ? _active->renderHeight() : height(); }
|
||||
bool HeltecV4Display::setRenderScale(uint8_t scale, float zoom) {
|
||||
return _active && _active->setRenderScale(scale, zoom);
|
||||
}
|
||||
void HeltecV4Display::clear() { if (isOn()) _active->clear(); }
|
||||
void HeltecV4Display::startFrame(ColorVal bkg) { if (isOn()) _active->startFrame(mapColor(bkg)); }
|
||||
void HeltecV4Display::setTextSize(int size) { if (_active) _active->setTextSize(size); }
|
||||
void HeltecV4Display::setCompactText(bool compact) { if (_active) _active->setCompactText(compact); }
|
||||
void HeltecV4Display::setColor(ColorVal color) { if (_active) _active->setColor(mapColor(color)); }
|
||||
void HeltecV4Display::setCursor(int x, int y) { if (isOn()) _active->setCursor(x, y); }
|
||||
void HeltecV4Display::print(const char* text) { if (isOn()) _active->print(text); }
|
||||
void HeltecV4Display::printWordWrap(const char* text, int max_width) {
|
||||
if (isOn()) _active->printWordWrap(text, max_width);
|
||||
}
|
||||
void HeltecV4Display::fillRect(int x, int y, int w, int h) { if (isOn()) _active->fillRect(x, y, w, h); }
|
||||
void HeltecV4Display::drawRect(int x, int y, int w, int h) { if (isOn()) _active->drawRect(x, y, w, h); }
|
||||
void HeltecV4Display::drawXbm(int x, int y, const uint8_t* bits, int w, int h) {
|
||||
if (isOn()) _active->drawXbm(x, y, bits, w, h);
|
||||
}
|
||||
uint16_t HeltecV4Display::getTextWidth(const char* text) { return _active ? _active->getTextWidth(text) : 0; }
|
||||
void HeltecV4Display::endFrame() { if (isOn()) _active->endFrame(); }
|
||||
|
||||
#endif // HELTEC_V4_DISPLAY_UNIFIED
|
||||
@@ -0,0 +1,77 @@
|
||||
#pragma once
|
||||
|
||||
#include "DisplayDriver.h"
|
||||
#include "HeltecV4DisplayPolicy.h"
|
||||
#include "SSD1306Display.h"
|
||||
#include "ST7789LCDDisplay.h"
|
||||
|
||||
// One display interface for the original V4/V4.3 base board and its original
|
||||
// touch expansion kit. R8 uses a different MCU memory/pin configuration.
|
||||
class HeltecV4Display : public DisplayDriver {
|
||||
using Mode = mesh::ui::HeltecV4DisplayMode;
|
||||
using Type = mesh::ui::HeltecV4DisplayType;
|
||||
|
||||
TwoWire _display_wire;
|
||||
SSD1306Display _oled;
|
||||
ST7789LCDDisplay _tft;
|
||||
RefCountedDigitalPin* _power;
|
||||
DisplayDriver* _active = nullptr;
|
||||
Mode _mode = Mode::Auto;
|
||||
Type _type = Type::None;
|
||||
const char* _status = "not initialized";
|
||||
bool _started = false;
|
||||
bool _wire_started = false;
|
||||
bool _power_claimed = false;
|
||||
bool _known_tft = false;
|
||||
bool _flipped = false;
|
||||
uint16_t _rotation = 0;
|
||||
uint32_t _generation = 0;
|
||||
|
||||
void claimPower();
|
||||
void releasePower();
|
||||
void releasePins();
|
||||
void deactivate();
|
||||
void setPalette();
|
||||
void updateDimensions();
|
||||
bool startOledBus();
|
||||
bool probeOled();
|
||||
bool probeTouchAssembly();
|
||||
bool startSelected(Type type);
|
||||
ColorVal mapColor(ColorVal color) const;
|
||||
|
||||
public:
|
||||
explicit HeltecV4Display(RefCountedDigitalPin* power = nullptr)
|
||||
: DisplayDriver(128, 64), _display_wire(1),
|
||||
_oled(nullptr, &_display_wire), _tft(nullptr), _power(power) {}
|
||||
|
||||
bool begin();
|
||||
bool setMode(Mode mode);
|
||||
Mode mode() const { return _mode; }
|
||||
Type selectedType() const { return _type; }
|
||||
const char* status() const { return _status; }
|
||||
uint32_t generation() const { return _generation; }
|
||||
|
||||
bool isOn() override { return _active && _active->isOn(); }
|
||||
bool supportsRotation() const override { return true; }
|
||||
bool setRotationDegrees(uint16_t degrees) override;
|
||||
void setFlipped(bool flipped) override;
|
||||
void turnOn() override;
|
||||
void turnOff() override;
|
||||
int textLineHeight() override;
|
||||
int renderWidth() const override;
|
||||
int renderHeight() const override;
|
||||
bool setRenderScale(uint8_t scale, float zoom) override;
|
||||
void clear() override;
|
||||
void startFrame(ColorVal bkg = UIColor::window_bkg) override;
|
||||
void setTextSize(int size) override;
|
||||
void setCompactText(bool compact) override;
|
||||
void setColor(ColorVal color) override;
|
||||
void setCursor(int x, int y) override;
|
||||
void print(const char* text) override;
|
||||
void printWordWrap(const char* text, int max_width) 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* text) override;
|
||||
void endFrame() override;
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace mesh { namespace ui {
|
||||
|
||||
enum class HeltecV4DisplayMode : uint8_t { Auto, Oled, Tft, Off };
|
||||
enum class HeltecV4DisplayType : uint8_t { None, Oled, Tft };
|
||||
|
||||
inline const char* heltecV4DisplayModeName(HeltecV4DisplayMode mode) {
|
||||
switch (mode) {
|
||||
case HeltecV4DisplayMode::Oled: return "oled";
|
||||
case HeltecV4DisplayMode::Tft: return "tft";
|
||||
case HeltecV4DisplayMode::Off: return "off";
|
||||
default: return "auto";
|
||||
}
|
||||
}
|
||||
|
||||
inline const char* heltecV4DisplayTypeName(HeltecV4DisplayType type) {
|
||||
switch (type) {
|
||||
case HeltecV4DisplayType::Oled: return "oled";
|
||||
case HeltecV4DisplayType::Tft: return "tft";
|
||||
default: return "none";
|
||||
}
|
||||
}
|
||||
|
||||
inline bool parseHeltecV4DisplayMode(const char* text, HeltecV4DisplayMode& mode) {
|
||||
if (!text) return false;
|
||||
if (strcmp(text, "auto") == 0) mode = HeltecV4DisplayMode::Auto;
|
||||
else if (strcmp(text, "oled") == 0) mode = HeltecV4DisplayMode::Oled;
|
||||
else if (strcmp(text, "tft") == 0) mode = HeltecV4DisplayMode::Tft;
|
||||
else if (strcmp(text, "off") == 0) mode = HeltecV4DisplayMode::Off;
|
||||
else return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
inline HeltecV4DisplayType selectHeltecV4Display(HeltecV4DisplayMode mode,
|
||||
bool oled_present,
|
||||
bool tft_present) {
|
||||
if (mode == HeltecV4DisplayMode::Off) return HeltecV4DisplayType::None;
|
||||
if (mode == HeltecV4DisplayMode::Oled) return HeltecV4DisplayType::Oled;
|
||||
if (mode == HeltecV4DisplayMode::Tft) return HeltecV4DisplayType::Tft;
|
||||
if (oled_present) return HeltecV4DisplayType::Oled;
|
||||
return tft_present ? HeltecV4DisplayType::Tft : HeltecV4DisplayType::None;
|
||||
}
|
||||
|
||||
}} // namespace mesh::ui
|
||||
@@ -0,0 +1,77 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
namespace mesh { namespace ui {
|
||||
|
||||
struct HeltecV4TouchIdentity {
|
||||
uint8_t address = 0;
|
||||
uint8_t chip = 0;
|
||||
uint32_t config = 0;
|
||||
uint16_t width = 0;
|
||||
uint16_t height = 0;
|
||||
};
|
||||
|
||||
inline bool validHeltecV4TouchIdentity(const HeltecV4TouchIdentity& identity) {
|
||||
// The application note gives the chip code only as a five-bit field; it
|
||||
// does not reserve code zero. Require independently read configuration,
|
||||
// address and plausible touch coordinates instead of guessing chip IDs.
|
||||
// Touch resolution is independent of LCD resolution: the manufacturer's
|
||||
// own example is 360x360, even for a rectangular display assembly.
|
||||
return (identity.address == 0x2E || identity.address == 0x5C)
|
||||
&& identity.chip != 0xFF
|
||||
&& identity.config != 0 && identity.config != 0xFFFFFFFFu
|
||||
&& identity.width >= 160 && identity.width <= 4095
|
||||
&& identity.height >= 160 && identity.height <= 4095;
|
||||
}
|
||||
|
||||
inline bool equalHeltecV4TouchIdentity(const HeltecV4TouchIdentity& a,
|
||||
const HeltecV4TouchIdentity& b) {
|
||||
return a.address == b.address && a.chip == b.chip && a.config == b.config
|
||||
&& a.width == b.width && a.height == b.height;
|
||||
}
|
||||
|
||||
template <typename Io>
|
||||
bool readHeltecV4TouchIdentity(Io& io, HeltecV4TouchIdentity& identity) {
|
||||
uint8_t config[4], resolution[4];
|
||||
if (!io.readRegister(0x0001, &identity.address, 1)
|
||||
|| !io.readRegister(0x9E6B, &identity.chip, 1)
|
||||
|| !io.readRegister(0x9E00, config, sizeof(config))
|
||||
|| !io.readRegister(0x9E4C, resolution, sizeof(resolution))) return false;
|
||||
identity.config = uint32_t(config[0]) | uint32_t(config[1]) << 8
|
||||
| uint32_t(config[2]) << 16 | uint32_t(config[3]) << 24;
|
||||
identity.width = uint16_t(resolution[0]) | uint16_t(resolution[1]) << 8;
|
||||
identity.height = uint16_t(resolution[2]) | uint16_t(resolution[3]) << 8;
|
||||
return validHeltecV4TouchIdentity(identity);
|
||||
}
|
||||
|
||||
// CHSC6xxx application note, sections 3.1 and 3.1.1-3.1.5:
|
||||
// https://resource.heltec.cn/download/WiFi_LoRa_32_Expansion_Kit/Touchscreen-Data/
|
||||
// Application%C2%A0Note%C2%A0for%C2%A0CTPM_CHSC6xxx(1).pdf
|
||||
// Only the documented temporary DMA-mode command and identity reads are used.
|
||||
// There are no controller firmware/flash writes. Reset always exits DMA mode,
|
||||
// including after a NACK, short read or rejected identity. Io provides reset(),
|
||||
// delayMs(), write(bytes,length), and readRegister(address,bytes,length).
|
||||
template <typename Io>
|
||||
bool probeHeltecV4Touch(Io& io, HeltecV4TouchIdentity* identity = nullptr) {
|
||||
static const uint8_t enter_dma[] = {0x42, 0xBD, 0x28, 0x35,
|
||||
0xC1, 0x00, 0x35, 0xAE};
|
||||
bool present = false;
|
||||
HeltecV4TouchIdentity first, second;
|
||||
for (uint8_t attempt = 0; attempt < 2 && !present; ++attempt) {
|
||||
io.reset();
|
||||
io.delayMs(30);
|
||||
if (!io.write(enter_dma, sizeof(enter_dma))) continue;
|
||||
io.delayMs(20);
|
||||
present = readHeltecV4TouchIdentity(io, first)
|
||||
&& readHeltecV4TouchIdentity(io, second)
|
||||
&& equalHeltecV4TouchIdentity(first, second);
|
||||
}
|
||||
io.reset();
|
||||
io.delayMs(30);
|
||||
if (present && identity) *identity = first;
|
||||
return present;
|
||||
}
|
||||
|
||||
}} // namespace mesh::ui
|
||||
@@ -0,0 +1,125 @@
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED
|
||||
|
||||
#include "HeltecV4DisplaySettings.h"
|
||||
#include <helpers/CheckedSettingsFile.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace mesh { namespace ui {
|
||||
namespace {
|
||||
FILESYSTEM* settings_fs = nullptr;
|
||||
bool saved_mode = false;
|
||||
constexpr char path[] = "/display_type";
|
||||
constexpr char backup[] = "/display_type.bak";
|
||||
constexpr char temporary[] = "/display_type.tmp";
|
||||
|
||||
void encode(HeltecV4DisplayMode mode, uint8_t (&data)[6]) {
|
||||
data[0] = 'D'; data[1] = 'T'; data[2] = 1;
|
||||
data[3] = uint8_t(mode); data[4] = 0; data[5] = 0;
|
||||
for (unsigned i = 0; i < 5; ++i) data[5] ^= data[i];
|
||||
}
|
||||
|
||||
bool read(const char* name, HeltecV4DisplayMode& mode) {
|
||||
uint8_t data[6], expected[6];
|
||||
if (!storage::readFixedSettingsFile(settings_fs, name, data, sizeof(data))
|
||||
|| data[3] > uint8_t(HeltecV4DisplayMode::Off)) return false;
|
||||
const auto candidate = HeltecV4DisplayMode(data[3]);
|
||||
encode(candidate, expected);
|
||||
if (memcmp(data, expected, sizeof(data)) != 0) return false;
|
||||
mode = candidate;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool recover() {
|
||||
HeltecV4DisplayMode mode;
|
||||
if (read(path, mode) || !read(backup, mode)) return true;
|
||||
if (settings_fs->exists(path) && !settings_fs->remove(path)) return false;
|
||||
return settings_fs->rename(backup, path) && read(path, mode);
|
||||
}
|
||||
|
||||
bool save(HeltecV4DisplayMode mode) {
|
||||
if (!settings_fs || !recover()) return false;
|
||||
if (settings_fs->exists(temporary) && !settings_fs->remove(temporary)) return false;
|
||||
if (settings_fs->exists(temporary)) return false;
|
||||
File file = settings_fs->open(temporary, "w", true);
|
||||
if (!file) return false;
|
||||
uint8_t data[6];
|
||||
encode(mode, data);
|
||||
const bool written = file.write(data, sizeof(data)) == sizeof(data);
|
||||
file.close();
|
||||
HeltecV4DisplayMode checked;
|
||||
if (!written || !read(temporary, checked) || checked != mode) return false;
|
||||
if (settings_fs->exists(backup) && !settings_fs->remove(backup)) return false;
|
||||
if (settings_fs->exists(path) && !settings_fs->rename(path, backup)) return false;
|
||||
if (!settings_fs->rename(temporary, path)) {
|
||||
// A failed publish must not discard the last complete preference record.
|
||||
if (settings_fs->exists(backup) && !settings_fs->exists(path))
|
||||
settings_fs->rename(backup, path);
|
||||
return false;
|
||||
}
|
||||
if (!read(path, checked) || checked != mode) return false;
|
||||
if (settings_fs->exists(backup)) settings_fs->remove(backup);
|
||||
return true;
|
||||
}
|
||||
|
||||
const char* modeName(HeltecV4DisplayMode mode) {
|
||||
switch (mode) {
|
||||
case HeltecV4DisplayMode::Oled: return "oled";
|
||||
case HeltecV4DisplayMode::Tft: return "tft";
|
||||
case HeltecV4DisplayMode::Off: return "off";
|
||||
default: return "auto";
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool loadHeltecV4DisplaySettings(FILESYSTEM* fs, HeltecV4Display& display) {
|
||||
settings_fs = fs;
|
||||
saved_mode = false;
|
||||
HeltecV4DisplayMode mode = HeltecV4DisplayMode::Auto;
|
||||
if (fs) {
|
||||
recover();
|
||||
saved_mode = read(path, mode) || read(backup, mode);
|
||||
}
|
||||
display.setMode(mode);
|
||||
return saved_mode;
|
||||
}
|
||||
|
||||
bool handleHeltecV4DisplayCommand(const char* command, char* reply,
|
||||
size_t reply_size, HeltecV4Display& display) {
|
||||
const bool get = strncmp(command, "get ", 4) == 0;
|
||||
if (!get && strncmp(command, "set ", 4) != 0) return false;
|
||||
constexpr char key[] = "display.type";
|
||||
constexpr size_t length = sizeof(key) - 1;
|
||||
if (strncmp(command + 4, key, length) != 0
|
||||
|| (command[4 + length] != 0 && command[4 + length] != ' ')) return false;
|
||||
const char* value = command + 4 + length;
|
||||
if (get) {
|
||||
if (*value) snprintf(reply, reply_size, "Error: unexpected argument");
|
||||
else snprintf(reply, reply_size, "display.type %s (active %s)",
|
||||
modeName(display.mode()), display.status());
|
||||
return true;
|
||||
}
|
||||
while (*value == ' ') ++value;
|
||||
HeltecV4DisplayMode mode;
|
||||
if (!strcmp(value, "auto")) mode = HeltecV4DisplayMode::Auto;
|
||||
else if (!strcmp(value, "oled")) mode = HeltecV4DisplayMode::Oled;
|
||||
else if (!strcmp(value, "tft")) mode = HeltecV4DisplayMode::Tft;
|
||||
else if (!strcmp(value, "off")) mode = HeltecV4DisplayMode::Off;
|
||||
else {
|
||||
snprintf(reply, reply_size, "Error: use set display.type <auto|oled|tft|off>");
|
||||
return true;
|
||||
}
|
||||
if ((!saved_mode || display.mode() != mode) && !save(mode)) {
|
||||
snprintf(reply, reply_size, "Error: display type save failed");
|
||||
return true;
|
||||
}
|
||||
saved_mode = true;
|
||||
display.setMode(mode);
|
||||
snprintf(reply, reply_size, "OK - display.type %s (active %s)",
|
||||
modeName(mode), display.status());
|
||||
return true;
|
||||
}
|
||||
|
||||
}} // namespace mesh::ui
|
||||
|
||||
#endif // HELTEC_V4_DISPLAY_UNIFIED
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <helpers/IdentityStore.h>
|
||||
#include "HeltecV4Display.h"
|
||||
|
||||
namespace mesh { namespace ui {
|
||||
|
||||
// Load before the first display probe, including a saved Off selection.
|
||||
bool loadHeltecV4DisplaySettings(FILESYSTEM* fs, HeltecV4Display& display);
|
||||
bool handleHeltecV4DisplayCommand(const char* command, char* reply,
|
||||
size_t reply_size, HeltecV4Display& display);
|
||||
|
||||
}} // namespace mesh::ui
|
||||
@@ -21,6 +21,7 @@ bool SSD1306Display::i2c_probe(TwoWire& wire, uint8_t addr) {
|
||||
}
|
||||
|
||||
// Color scheme
|
||||
#if !defined(HELTEC_V4_DISPLAY_UNIFIED) || !HELTEC_V4_DISPLAY_UNIFIED
|
||||
ColorVal UIColor::window_bkg = SSD1306_BLACK;
|
||||
ColorVal UIColor::title_bkg = SSD1306_BLACK;
|
||||
ColorVal UIColor::title_txt = SSD1306_WHITE;
|
||||
@@ -30,6 +31,7 @@ ColorVal UIColor::warning_txt = SSD1306_WHITE;
|
||||
ColorVal UIColor::popup_bkg = SSD1306_BLACK;
|
||||
ColorVal UIColor::popup_txt = SSD1306_WHITE;
|
||||
ColorVal UIColor::corp_blue = SSD1306_WHITE;
|
||||
#endif
|
||||
|
||||
bool SSD1306Display::begin() {
|
||||
if (!_isOn) {
|
||||
@@ -42,7 +44,7 @@ bool SSD1306Display::begin() {
|
||||
, mesh::ui::oledControllerMode()
|
||||
#endif
|
||||
)
|
||||
&& i2c_probe(Wire, DISPLAY_ADDRESS);
|
||||
&& i2c_probe(*_wire, DISPLAY_ADDRESS);
|
||||
if (started) {
|
||||
_panel_ready = true;
|
||||
applyRotation();
|
||||
|
||||
@@ -40,13 +40,14 @@ private:
|
||||
uint8_t _color;
|
||||
int _text_size = 1;
|
||||
RefCountedDigitalPin* _peripher_power;
|
||||
TwoWire* _wire;
|
||||
|
||||
bool i2c_probe(TwoWire& wire, uint8_t addr);
|
||||
void applyRotation();
|
||||
public:
|
||||
SSD1306Display(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(PANEL_WIDTH, PANEL_HEIGHT),
|
||||
display(PANEL_WIDTH, PANEL_HEIGHT, &Wire, PIN_OLED_RESET),
|
||||
_peripher_power(peripher_power)
|
||||
SSD1306Display(RefCountedDigitalPin* peripher_power=NULL, TwoWire* wire=&Wire) : DisplayDriver(PANEL_WIDTH, PANEL_HEIGHT),
|
||||
display(PANEL_WIDTH, PANEL_HEIGHT, wire, PIN_OLED_RESET),
|
||||
_peripher_power(peripher_power), _wire(wire)
|
||||
{
|
||||
_isOn = false;
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ const char meshcore_capability_display_autodetect_heltec_r8[] =
|
||||
// display.flip remains an additive 180-degree mounting adjustment, independent
|
||||
// of the selected orientation. Other boards keep their compiled orientation.
|
||||
uint8_t ST7789LCDDisplay::effectiveRotation() const {
|
||||
#ifdef MESHCORE_HELTEC_R8_RUNTIME_ROTATION
|
||||
#ifdef MESHCORE_HELTEC_TFT_RUNTIME_ROTATION
|
||||
return (uint8_t)((_base_rotation + (_flipped ? 2 : 0)) & 3);
|
||||
#else
|
||||
return (uint8_t)((DISPLAY_ROTATION + (_flipped ? 2 : 0)) & 3);
|
||||
@@ -183,7 +183,7 @@ uint8_t ST7789LCDDisplay::effectiveRotation() const {
|
||||
}
|
||||
|
||||
void ST7789LCDDisplay::applyRotation() {
|
||||
#ifdef HELTEC_V4_R8_TFT
|
||||
#if defined(HELTEC_V4_R8_TFT) || defined(HELTEC_V4_DISPLAY_UNIFIED)
|
||||
const auto rotation = effectiveRotation();
|
||||
// Layout follows saved intent immediately, including before initialization
|
||||
// and while the backlight is off. Defer all panel traffic until awake.
|
||||
@@ -198,9 +198,11 @@ void ST7789LCDDisplay::applyRotation() {
|
||||
}
|
||||
|
||||
bool ST7789LCDDisplay::setRotationDegrees(uint16_t degrees) {
|
||||
#ifdef MESHCORE_HELTEC_TFT_RUNTIME_ROTATION
|
||||
#ifdef MESHCORE_HELTEC_R8_RUNTIME_ROTATION
|
||||
const volatile char* capability = meshcore_capability_display_rotation_heltec_r8;
|
||||
(void)*capability;
|
||||
#endif
|
||||
if (degrees > 270 || degrees % 90 != 0) return false;
|
||||
const uint8_t rotation = (DISPLAY_ROTATION + degrees / 90) & 3;
|
||||
if (_base_rotation != rotation) {
|
||||
@@ -225,6 +227,7 @@ bool ST7789LCDDisplay::i2c_probe(TwoWire& wire, uint8_t addr) {
|
||||
}
|
||||
|
||||
// Color scheme
|
||||
#if !defined(HELTEC_V4_DISPLAY_UNIFIED) || !HELTEC_V4_DISPLAY_UNIFIED
|
||||
ColorVal UIColor::window_bkg = mesh::ui::color_theme::WINDOW_BACKGROUND;
|
||||
ColorVal UIColor::title_bkg = mesh::ui::color_theme::TITLE_BACKGROUND;
|
||||
ColorVal UIColor::title_txt = mesh::ui::color_theme::TEXT;
|
||||
@@ -234,6 +237,7 @@ ColorVal UIColor::warning_txt = mesh::ui::color_theme::WARNING_TEXT;
|
||||
ColorVal UIColor::popup_bkg = mesh::ui::color_theme::POPUP_BACKGROUND;
|
||||
ColorVal UIColor::popup_txt = mesh::ui::color_theme::TEXT;
|
||||
ColorVal UIColor::corp_blue = mesh::ui::color_theme::ACCENT;
|
||||
#endif
|
||||
|
||||
bool ST7789LCDDisplay::begin() {
|
||||
#ifdef MESHCORE_HELTEC_R8_TFT_DETECTION
|
||||
@@ -308,14 +312,26 @@ bool ST7789LCDDisplay::begin() {
|
||||
#if defined(LILYGO_TDECK) || defined(HELTEC_LORA_V4_TFT) || defined(HELTEC_V4_R8_TFT)
|
||||
displaySPI.begin(PIN_TFT_SCL, PIN_TFT_MISO, PIN_TFT_SDA, PIN_TFT_CS);
|
||||
#endif
|
||||
#ifdef MESHCORE_HELTEC_R8_TFT_DETECTION
|
||||
#if defined(MESHCORE_HELTEC_R8_TFT_DETECTION) || \
|
||||
(defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED)
|
||||
// Arduino-ESP32 2.x begin() returns void, so inspect its public bus handle
|
||||
// before handing an unavailable SPI peripheral to the panel library.
|
||||
if (!displaySPI.bus()) {
|
||||
displaySPI.end();
|
||||
#ifdef MESHCORE_HELTEC_R8_TFT_DETECTION
|
||||
_present = false; // Latch failure just like an absent LCD.
|
||||
mesh::ui::heltec_r8::backlightOff();
|
||||
mesh::ui::heltec_r8::releaseBus();
|
||||
#else
|
||||
digitalWrite(PIN_TFT_LEDA_CTL, !PIN_TFT_LEDA_CTL_ACTIVE);
|
||||
digitalWrite(PIN_TFT_RST, LOW);
|
||||
pinMode(PIN_TFT_RST, OUTPUT);
|
||||
pinMode(PIN_TFT_CS, INPUT);
|
||||
pinMode(PIN_TFT_DC, INPUT);
|
||||
pinMode(PIN_TFT_SDA, INPUT);
|
||||
pinMode(PIN_TFT_SCL, INPUT);
|
||||
_panel_ready = false;
|
||||
#endif
|
||||
if (_peripher_power) _peripher_power->release();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,11 @@ class TDeckCanvas;
|
||||
#define MESHCORE_HELTEC_R8_RUNTIME_ROTATION 1
|
||||
#endif
|
||||
|
||||
#if defined(MESHCORE_HELTEC_R8_RUNTIME_ROTATION) || \
|
||||
(defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED)
|
||||
#define MESHCORE_HELTEC_TFT_RUNTIME_ROTATION 1
|
||||
#endif
|
||||
|
||||
class ST7789LCDDisplay : public DisplayDriver {
|
||||
#if defined(LILYGO_TDECK) || defined(HELTEC_LORA_V4_TFT) || defined(HELTEC_V4_R8_TFT)
|
||||
SPIClass displaySPI;
|
||||
@@ -36,8 +41,8 @@ class ST7789LCDDisplay : public DisplayDriver {
|
||||
bool _probe_complete = false;
|
||||
bool _present = false;
|
||||
#endif
|
||||
#ifdef HELTEC_V4_R8_TFT
|
||||
#ifdef MESHCORE_HELTEC_R8_RUNTIME_ROTATION
|
||||
#if defined(HELTEC_V4_R8_TFT) || defined(HELTEC_V4_DISPLAY_UNIFIED)
|
||||
#ifdef MESHCORE_HELTEC_TFT_RUNTIME_ROTATION
|
||||
uint8_t _base_rotation = DISPLAY_ROTATION & 3;
|
||||
#endif
|
||||
bool _rotation_pending = false;
|
||||
@@ -78,7 +83,13 @@ public:
|
||||
}
|
||||
#elif defined(LILYGO_TDECK) || defined(HELTEC_LORA_V4_TFT) || defined(HELTEC_V4_R8_TFT)
|
||||
ST7789LCDDisplay(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(kWidth, kHeight),
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED
|
||||
// The V4 radio uses SPIClass's default HSPI controller. A separate
|
||||
// controller is required so TFT setup/sleep cannot remap or stop it.
|
||||
displaySPI(FSPI),
|
||||
#else
|
||||
displaySPI(HSPI),
|
||||
#endif
|
||||
display(&displaySPI, PIN_TFT_CS, PIN_TFT_DC, PIN_TFT_RST),
|
||||
_peripher_power(peripher_power)
|
||||
{
|
||||
@@ -106,8 +117,17 @@ public:
|
||||
#endif
|
||||
|
||||
bool isOn() override { return _isOn; }
|
||||
#ifdef MESHCORE_HELTEC_R8_RUNTIME_ROTATION
|
||||
#ifdef MESHCORE_HELTEC_TFT_RUNTIME_ROTATION
|
||||
bool supportsRotation() const override { return true; }
|
||||
#endif
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED
|
||||
// The combined V4 image releases the LCD SPI peripheral before the shared
|
||||
// GPIO17/18 pair is used by the OLED bus again.
|
||||
void releaseBus() {
|
||||
displaySPI.end();
|
||||
_panel_ready = false;
|
||||
_isOn = false;
|
||||
}
|
||||
#endif
|
||||
bool setRotationDegrees(uint16_t degrees) override;
|
||||
void turnOn() override;
|
||||
|
||||
@@ -319,24 +319,26 @@ require("meshnology_w12_sensor", "build_flags", "PIN_BOARD_SDA=17")
|
||||
require("meshnology_w12_sensor", "build_flags", "PIN_BOARD_SCL=18")
|
||||
reject("meshnology_w12_sensor", "build_flags", "ENV_PIN_")
|
||||
|
||||
# Unified ordinary V4 keeps the sensor/RTC bus on 4/3. OLED uses a private
|
||||
# controller on 17/18, so changing display type cannot move the sensor bus.
|
||||
for env_name in (
|
||||
"heltec_v4_repeater",
|
||||
"heltec_v4_room_server",
|
||||
"heltec_v4_sensor",
|
||||
"heltec_v4_2_v4_3_companion_radio_full_femon",
|
||||
):
|
||||
require(env_name, "build_flags", "PIN_BOARD_SDA=17")
|
||||
require(env_name, "build_flags", "PIN_BOARD_SCL=18")
|
||||
require(env_name, "build_flags", "ENV_PIN_SDA=4")
|
||||
require(env_name, "build_flags", "ENV_PIN_SCL=3")
|
||||
|
||||
for env_name in (
|
||||
"heltec_v4_tft_sensor",
|
||||
"heltec_v4_expansionkit_tft_companion_radio_ble_femon",
|
||||
"heltec_v4_expansionkit_tft_companion_radio_full_femon",
|
||||
"heltec_v4_tft_companion_radio_full_femon",
|
||||
):
|
||||
require(env_name, "build_flags", "HELTEC_V4_DISPLAY_UNIFIED=1")
|
||||
require(env_name, "build_flags", "DISPLAY_CLASS=HeltecV4Display")
|
||||
require(env_name, "build_flags", "PIN_BOARD_SDA=4")
|
||||
require(env_name, "build_flags", "PIN_BOARD_SCL=3")
|
||||
reject(env_name, "build_flags", "ENV_PIN_")
|
||||
require(env_name, "build_flags", "ENV_PIN_SDA=4")
|
||||
require(env_name, "build_flags", "ENV_PIN_SCL=3")
|
||||
for driver in ("HeltecV4Display", "SSD1306Display", "ST7789LCDDisplay"):
|
||||
require(env_name, "build_src_filter", "helpers/ui/" + driver + ".cpp")
|
||||
|
||||
for env_name in (
|
||||
"RAK_3112_sensor",
|
||||
|
||||
@@ -102,3 +102,41 @@ console.log("Muzi per-radio accessories and T-Echo Lite shell/core folding requi
|
||||
assert.strictEqual(p.buildCatalog([release([legacy, canonical])], metadata).profiles.length, 2);
|
||||
}
|
||||
console.log("V4 TFT base/kit consolidation requires its own exact qualified Full image");
|
||||
|
||||
{
|
||||
const canonical = "heltec_v4_repeater-full-usb-wifi";
|
||||
const aliases = ["heltec_v4_tft_repeater-full-usb-wifi",
|
||||
"heltec_v4_expansionkit_repeater-full-usb-wifi",
|
||||
"heltec_v4_3_tft_repeater-full-usb-wifi"];
|
||||
const r8 = "heltec_v4_r8_tft_repeater-full-usb-wifi";
|
||||
const proof = {platform: "ESP32_PLATFORM", display: true,
|
||||
unifiedV4Display: true, unifiedV4DisplaySource: source};
|
||||
const metadata = {familyTag: family, source, profiles: {[canonical]: proof}};
|
||||
const names = [canonical, ...aliases, r8];
|
||||
const snapshots = [release(names)];
|
||||
const before = JSON.stringify(snapshots);
|
||||
const catalog = p.buildCatalog(snapshots, metadata);
|
||||
assert.deepStrictEqual(catalog.profiles.map(x => x.target).sort(), [canonical, r8].sort());
|
||||
assert.strictEqual(catalog.rows.length, names.length, "historical download identities are retained");
|
||||
assert.strictEqual(JSON.stringify(snapshots), before);
|
||||
const base = catalog.profiles.find(x => x.target === canonical);
|
||||
assert(p.boardNotes(base).some(note => note.includes("OLED") && note.includes("no screen")));
|
||||
assert(p.boardNotes(base).some(note => note.includes("LoRa OTA target IDs remain distinct")));
|
||||
const displayControls = p.runtimeDirections(base).find(section => section.title === "Display type");
|
||||
assert.deepStrictEqual(displayControls.actions.map(action => action.commands[0]),
|
||||
["set display.type auto", "set display.type oled", "set display.type tft", "set display.type off"]);
|
||||
assert(displayControls.note.includes("immediately"));
|
||||
for (const alias of aliases) {
|
||||
const hardware = p.parseTargetProfile(alias).hardware;
|
||||
const restored = p.selectionFromUrl("https://example.org/?hardware=" + hardware + "&role=repeater", catalog.profiles);
|
||||
assert.strictEqual(restored.filters.hardware, "heltec_v4");
|
||||
assert.deepStrictEqual(restored.unavailable, []);
|
||||
}
|
||||
for (const fault of [{unifiedV4Display: false}, {unifiedV4DisplaySource: "b".repeat(40)},
|
||||
{platform: "NRF52_PLATFORM"}, {display: false}]) {
|
||||
const bad = {familyTag: family, source, profiles: {[canonical]: {...proof, ...fault}}};
|
||||
assert.strictEqual(p.buildCatalog(snapshots, bad).profiles.length, names.length,
|
||||
"historical V4 display labels require exact newly qualified proof");
|
||||
}
|
||||
}
|
||||
console.log("Unified ordinary V4 OLED/TFT/headless choices preserve historical downloads and R8");
|
||||
@@ -16,6 +16,11 @@ import consolidated_profiles as consolidated
|
||||
|
||||
SOURCE = "a" * 40
|
||||
CASES = (
|
||||
dict(target="heltec_v4_sensor", legacy="heltec_v4_tft_sensor",
|
||||
board="heltec_v4", platform="ESP32_PLATFORM", key="unifiedV4Display",
|
||||
capability="display.heltec_v4.unified",
|
||||
marker="meshcore.capability.display.heltec_v4.unified.v1",
|
||||
flags=["-DHELTEC_V4_DISPLAY_UNIFIED=1", "-DDISPLAY_CLASS=HeltecV4Display"]),
|
||||
dict(target="PicoW_companion_radio", legacy="PicoW_companion_radio_wifi",
|
||||
board="rpipicow", platform="RP2040_PLATFORM", key="combinedCompanion",
|
||||
capability="companion.picow.combined",
|
||||
@@ -96,7 +101,7 @@ class ConsolidatedProfileTests(unittest.TestCase):
|
||||
legacy = "env:heltec_v4_expansionkit_tft_companion_radio_full_femon"
|
||||
flags = defines(recipes.value(canonical, "build_flags"))
|
||||
self.assertEqual(flags, defines(recipes.value(legacy, "build_flags")))
|
||||
for key, value in dict(HELTEC_V4_TFT_KIT_COMPAT="1", DISPLAY_CLASS="ST7789LCDDisplay",
|
||||
for key, value in dict(HELTEC_V4_TFT_KIT_COMPAT="1", HELTEC_V4_DISPLAY_UNIFIED="1", DISPLAY_CLASS="HeltecV4Display",
|
||||
AUTO_SHUTDOWN_MILLIVOLTS="3400", MESHCORE_REQUIRES_COMPANION_RADIO_FULL="1",
|
||||
PIN_BOARD_SDA="4", PIN_BOARD_SCL="3", PIN_TFT_RST="18", PIN_TFT_LEDA_CTL="21",
|
||||
PIN_TFT_CS="15", PIN_TFT_DC="16", PIN_TFT_SCL="17", PIN_TFT_SDA="33").items():
|
||||
@@ -153,9 +158,9 @@ class ConsolidatedProfileTests(unittest.TestCase):
|
||||
def test_replacements_are_limited_to_known_exact_legacy_targets(self):
|
||||
for case in CASES:
|
||||
self.assertIn((case["target"], case["key"]), consolidated.replacements(case["legacy"]))
|
||||
self.assertEqual(consolidated.replacements(case["target"]), [])
|
||||
self.assertNotIn((case["target"], case["key"]), consolidated.replacements(case["target"]))
|
||||
self.assertEqual(consolidated.replacements(case["legacy"] + "_custom"), [])
|
||||
for target in ("other_PicoW_companion_radio_usb", "heltec_v4_expansionkit_sensor",
|
||||
for target in ("other_PicoW_companion_radio_usb", "heltec_v4_r8_expansionkit_sensor",
|
||||
"heltec_v4_r8_tft_portrait_sensor_observer_mqtt", "other_without_display_repeater"):
|
||||
with self.subTest(target=target):
|
||||
self.assertEqual(consolidated.replacements(target), [])
|
||||
|
||||
@@ -502,6 +502,63 @@ int main() {
|
||||
}, "v4_companion")
|
||||
self.assertEqual(default, expanded)
|
||||
|
||||
def test_unified_v4_display_reserves_retained_oled_and_both_bus_owners(self):
|
||||
defines = {"HELTEC_V4_DISPLAY_UNIFIED": 1, "HELTEC_LORA_V4": 1}
|
||||
for role in ("heltec_v4_repeater", "heltec_v4_room_server",
|
||||
"heltec_v4_sensor", "heltec_v4_companion_radio_full"):
|
||||
with self.subTest(role=role):
|
||||
oled = ram.requirements("ESP32_PLATFORM", {
|
||||
**defines, "DISPLAY_CLASS": "SSD1306Display",
|
||||
}, role)
|
||||
unified = ram.requirements("ESP32_PLATFORM", {
|
||||
**defines, "DISPLAY_CLASS": "HeltecV4Display",
|
||||
}, role)
|
||||
parts = unified["components"]
|
||||
self.assertEqual(parts["display_pixels_and_driver"], 2048)
|
||||
self.assertEqual(parts["display_i2c_spi_and_probe"], 4096)
|
||||
self.assertEqual(parts["allocation_and_transient_margin"], 16384)
|
||||
# No common reserve is reduced to pay for the additional bus.
|
||||
self.assertEqual(parts, {
|
||||
**oled["components"], "display_i2c_spi_and_probe": 4096,
|
||||
})
|
||||
self.assertEqual(unified["required_heap_bytes"],
|
||||
oled["required_heap_bytes"] + 4096)
|
||||
if "companion" in role:
|
||||
# This class can become a small OLED after a saved runtime
|
||||
# setting change, even when it initially selects TFT/off.
|
||||
self.assertEqual(parts["expanded_message_previews"], 2816)
|
||||
self.assertGreaterEqual(unified["required_contiguous_bytes"], 11008)
|
||||
for platform, extra in (("NRF52_PLATFORM", defines),
|
||||
("ESP32_PLATFORM", {})):
|
||||
with self.subTest(platform=platform, extra=extra):
|
||||
with self.assertRaisesRegex(ValueError, "unified Heltec V4"):
|
||||
ram.requirements(platform, {
|
||||
**extra, "DISPLAY_CLASS": "HeltecV4Display",
|
||||
}, "companion")
|
||||
|
||||
def test_unified_v4_display_source_guard_rejects_growth_at_budget_boundary(self):
|
||||
source = (ROOT / "src/helpers/ui/HeltecV4Display.cpp").read_text()
|
||||
guard = source.split("// Runtime budget guard:", 1)[1].split(
|
||||
"extern const char meshcore_capability_display_heltec_v4_unified", 1)[0]
|
||||
# Compile the actual assertions, including negative boundaries. The
|
||||
# firmware build substitutes real wrapper/Wire/library definitions.
|
||||
declarations = (
|
||||
"struct HeltecV4Display { unsigned char bytes[OBJECT_BYTES]; };\n"
|
||||
"struct SSD1306Display { static constexpr unsigned FRAMEBUFFER_BYTES = FRAME_BYTES; };\n"
|
||||
)
|
||||
# The split removes the comment opener but leaves its first line.
|
||||
guard = "// Runtime budget guard:" + guard
|
||||
for objects, framebuffer, wire, accepted in (
|
||||
(992, 1024, 128, True), (993, 1024, 128, False),
|
||||
(992, 1025, 128, False), (992, 1024, 129, False)):
|
||||
with self.subTest(objects=objects, framebuffer=framebuffer, wire=wire):
|
||||
result = subprocess.run([
|
||||
"c++", "-std=c++17", "-fsyntax-only", "-x", "c++", "-",
|
||||
f"-DOBJECT_BYTES={objects}", f"-DFRAME_BYTES={framebuffer}",
|
||||
f"-DI2C_BUFFER_LENGTH={wire}",
|
||||
], input=declarations + guard, text=True, capture_output=True)
|
||||
self.assertEqual(result.returncode == 0, accepted, result.stderr)
|
||||
|
||||
def test_combined_uart_reserves_heap_beside_mqtt_before_enable(self):
|
||||
defines = {"WITH_MQTT_BRIDGE": 1, "WIFI_OTA_SEEDER": 1}
|
||||
mqtt = ram.requirements("ESP32_PLATFORM", defines, "v3_repeater")
|
||||
|
||||
@@ -0,0 +1,709 @@
|
||||
"""Exercise the unified base-V4 display selector and hardware probes."""
|
||||
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from test_serial_wifi_sessions import SANITIZER_FLAGS
|
||||
from test_display_power import FS_STUB
|
||||
from cpp_source import body
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
UI = ROOT / "src/helpers/ui"
|
||||
|
||||
POLICY_TEST = r'''
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <initializer_list>
|
||||
#include <helpers/ui/HeltecV4DisplayPolicy.h>
|
||||
using namespace mesh::ui;
|
||||
int main() {
|
||||
const HeltecV4DisplayMode modes[] = {HeltecV4DisplayMode::Auto,
|
||||
HeltecV4DisplayMode::Oled, HeltecV4DisplayMode::Tft, HeltecV4DisplayMode::Off};
|
||||
const char* names[] = {"auto", "oled", "tft", "off"};
|
||||
for (unsigned index = 0; index < 4; ++index) {
|
||||
assert(strcmp(heltecV4DisplayModeName(modes[index]), names[index]) == 0);
|
||||
HeltecV4DisplayMode parsed = HeltecV4DisplayMode::Off;
|
||||
assert(parseHeltecV4DisplayMode(names[index], parsed) && parsed == modes[index]);
|
||||
for (bool oled : {false, true}) {
|
||||
for (bool tft : {false, true}) {
|
||||
auto expected = HeltecV4DisplayType::None;
|
||||
if (modes[index] == HeltecV4DisplayMode::Oled) expected = HeltecV4DisplayType::Oled;
|
||||
if (modes[index] == HeltecV4DisplayMode::Tft) expected = HeltecV4DisplayType::Tft;
|
||||
if (modes[index] == HeltecV4DisplayMode::Auto)
|
||||
expected = oled ? HeltecV4DisplayType::Oled
|
||||
: tft ? HeltecV4DisplayType::Tft : HeltecV4DisplayType::None;
|
||||
assert(selectHeltecV4Display(modes[index], oled, tft) == expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const char* invalid : {"", "Auto", "OLED", "none", "on", "1", "tft extra", "oled ", " auto"}) {
|
||||
auto unchanged = HeltecV4DisplayMode::Tft;
|
||||
assert(!parseHeltecV4DisplayMode(invalid, unchanged));
|
||||
assert(unchanged == HeltecV4DisplayMode::Tft && "invalid values must not mutate the saved choice");
|
||||
}
|
||||
assert(strcmp(heltecV4DisplayTypeName(HeltecV4DisplayType::Oled), "oled") == 0);
|
||||
assert(strcmp(heltecV4DisplayTypeName(HeltecV4DisplayType::Tft), "tft") == 0);
|
||||
assert(strcmp(heltecV4DisplayTypeName(HeltecV4DisplayType::None), "none") == 0);
|
||||
auto unchanged = HeltecV4DisplayMode::Off;
|
||||
assert(!parseHeltecV4DisplayMode(nullptr, unchanged) && unchanged==HeltecV4DisplayMode::Off);
|
||||
}
|
||||
'''
|
||||
|
||||
TOUCH_TEST = r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <initializer_list>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <helpers/ui/HeltecV4DisplayProbe.h>
|
||||
using namespace mesh::ui;
|
||||
struct Io {
|
||||
std::string scenario;
|
||||
unsigned resets=0, writes=0, reads=0, delay=0, index=0;
|
||||
std::vector<unsigned> waits;
|
||||
explicit Io(const char* value) : scenario(value) {}
|
||||
void reset() { ++resets; index=0; }
|
||||
void delayMs(unsigned value) { waits.push_back(value); delay+=value; }
|
||||
bool write(const uint8_t* bytes, size_t count) {
|
||||
static const uint8_t expected[] = {0x42,0xBD,0x28,0x35,0xC1,0x00,0x35,0xAE};
|
||||
assert(count==sizeof(expected) && memcmp(bytes,expected,count)==0);
|
||||
++writes;
|
||||
return scenario!="write-failed";
|
||||
}
|
||||
bool readRegister(uint16_t reg, uint8_t* bytes, size_t count) {
|
||||
static const uint16_t registers[] = {0x0001,0x9E6B,0x9E00,0x9E4C};
|
||||
static const size_t sizes[] = {1,1,4,4};
|
||||
assert(reg==registers[index%4] && count==sizes[index%4]);
|
||||
const unsigned sample=index/4;
|
||||
++reads; ++index;
|
||||
if(scenario=="read-failed" || (scenario=="recover" && writes==1)) return false;
|
||||
memset(bytes,0,count);
|
||||
if(reg==0x0001) bytes[0]=scenario=="alternate-address" ? 0x5c : 0x2e;
|
||||
if(reg==0x9E6B) bytes[0]=scenario=="zero-chip" ? 0 : 0x20;
|
||||
if(reg==0x9E00) { bytes[0]=0x81;bytes[1]=0x52;bytes[2]=0x19;bytes[3]=0x03; }
|
||||
if(reg==0x9E4C) {
|
||||
const bool landscape=scenario=="landscape";
|
||||
const unsigned width=scenario=="independent-touch-resolution" ? 360 : landscape ? 320 : 240;
|
||||
const unsigned height=scenario=="independent-touch-resolution" ? 360 : landscape ? 240 : 320;
|
||||
bytes[0]=width&255;bytes[1]=width>>8;bytes[2]=height&255;bytes[3]=height>>8;
|
||||
}
|
||||
if(scenario=="zero") memset(bytes,0,count);
|
||||
if(scenario=="high") memset(bytes,255,count);
|
||||
if(scenario=="wrong-address" && reg==0x0001) bytes[0]=0x3c;
|
||||
if(scenario=="invalid-chip" && reg==0x9E6B) bytes[0]=255;
|
||||
if(scenario=="zero-config" && reg==0x9E00) memset(bytes,0,count);
|
||||
if(scenario=="high-config" && reg==0x9E00) memset(bytes,255,count);
|
||||
if(scenario=="wrong-size" && reg==0x9E4C) bytes[0]=128;
|
||||
if(scenario=="noise" && reg==0x9E00 && sample%2) bytes[0]^=1;
|
||||
if(scenario=="missing-read" && reg==0x9E4C) return false;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
int main() {
|
||||
for(const char* scenario : {"valid","alternate-address","landscape","recover","zero-chip",
|
||||
"independent-touch-resolution",
|
||||
"write-failed","read-failed","missing-read","zero","high","wrong-address",
|
||||
"invalid-chip","zero-config","high-config","wrong-size","noise"}) {
|
||||
Io io{scenario};
|
||||
HeltecV4TouchIdentity identity{};
|
||||
identity.address=0x91;
|
||||
const bool valid=io.scenario=="valid" || io.scenario=="alternate-address"
|
||||
|| io.scenario=="landscape" || io.scenario=="recover" || io.scenario=="zero-chip"
|
||||
|| io.scenario=="independent-touch-resolution";
|
||||
assert(probeHeltecV4Touch(io,&identity)==valid);
|
||||
assert(io.resets>=2 && io.resets<=3 && io.writes>=1 && io.writes<=2);
|
||||
assert(io.waits.back()>=30 && "DMA access must be exited by reset on every path");
|
||||
assert(io.reads<=16 && io.delay<250 && "startup probe must be bounded when absent");
|
||||
if(valid) {
|
||||
assert(identity.address==(io.scenario=="alternate-address" ? 0x5c : 0x2e));
|
||||
assert(identity.width==(io.scenario=="independent-touch-resolution" ? 360
|
||||
: io.scenario=="landscape" ? 320 : 240));
|
||||
assert(identity.height==(io.scenario=="independent-touch-resolution" ? 360
|
||||
: io.scenario=="landscape" ? 240 : 320));
|
||||
assert(identity.config==0x03195281u && identity.chip==(io.scenario=="zero-chip" ? 0 : 0x20));
|
||||
} else assert(identity.address==0x91 && "failed detection must not publish a partial identity");
|
||||
}
|
||||
Io no_output{"valid"};
|
||||
assert(probeHeltecV4Touch(no_output));
|
||||
}
|
||||
'''
|
||||
|
||||
SETTINGS_DISPLAY = r'''
|
||||
#pragma once
|
||||
#include <helpers/ui/HeltecV4DisplayPolicy.h>
|
||||
class HeltecV4Display {
|
||||
mesh::ui::HeltecV4DisplayMode _mode=mesh::ui::HeltecV4DisplayMode::Auto;
|
||||
public:
|
||||
unsigned applied=0;
|
||||
bool oled=true,tft=false;
|
||||
auto mode() const { return _mode; }
|
||||
void setMode(mesh::ui::HeltecV4DisplayMode mode) { _mode=mode; ++applied; }
|
||||
const char* status() const {
|
||||
if(_mode==mesh::ui::HeltecV4DisplayMode::Off) return "off";
|
||||
const auto type=mesh::ui::selectHeltecV4Display(_mode,oled,tft);
|
||||
if(type==mesh::ui::HeltecV4DisplayType::Oled) return "OLED";
|
||||
if(type==mesh::ui::HeltecV4DisplayType::Tft) return "TFT";
|
||||
return "none detected";
|
||||
}
|
||||
};
|
||||
'''
|
||||
|
||||
SETTINGS_TEST = r'''
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <initializer_list>
|
||||
#include <HeltecV4DisplaySettings.h>
|
||||
using namespace mesh::ui;
|
||||
int main() {
|
||||
FakeFS fs; HeltecV4Display display; char reply[160];
|
||||
const auto command=[&](const char* text) {
|
||||
memset(reply,0,sizeof(reply));
|
||||
return handleHeltecV4DisplayCommand(text,reply,sizeof(reply),display);
|
||||
};
|
||||
assert(!loadHeltecV4DisplaySettings(&fs,display));
|
||||
assert(display.mode()==HeltecV4DisplayMode::Auto);
|
||||
assert(command("get display.type") && strstr(reply,"auto") && strstr(reply,"active OLED"));
|
||||
assert(!command("get display.type.extra") && !command("set display.mode off"));
|
||||
for(const char* invalid : {"set display.type", "set display.type ", "set display.type none",
|
||||
"set display.type OLED", "set display.type tft extra", "set display.type off ",
|
||||
"get display.type off"}) {
|
||||
assert(command(invalid) && strstr(reply,"Error:"));
|
||||
assert(display.mode()==HeltecV4DisplayMode::Auto && fs.files.empty());
|
||||
}
|
||||
for(const char* mode : {"oled","tft","off","auto"}) {
|
||||
char input[80]; snprintf(input,sizeof(input),"set display.type %s",mode);
|
||||
assert(command(input) && strstr(reply,"OK"));
|
||||
assert(fs.files["/display_type"].size()==6);
|
||||
HeltecV4DisplayMode parsed;
|
||||
assert(parseHeltecV4DisplayMode(mode,parsed) && display.mode()==parsed);
|
||||
HeltecV4Display rebooted;
|
||||
assert(loadHeltecV4DisplaySettings(&fs,rebooted) && rebooted.mode()==parsed);
|
||||
}
|
||||
assert(command("set display.type off") && strstr(reply,"active off"));
|
||||
auto original=fs.files["/display_type"];
|
||||
const auto before=display.applied;
|
||||
fs.fail_write=true;
|
||||
assert(command("set display.type tft") && strstr(reply,"save failed"));
|
||||
assert(display.mode()==HeltecV4DisplayMode::Off && display.applied==before);
|
||||
assert(fs.files["/display_type"]==original);
|
||||
fs.fail_write=false; fs.corrupt_write=true;
|
||||
assert(command("set display.type tft") && strstr(reply,"save failed"));
|
||||
assert(display.mode()==HeltecV4DisplayMode::Off && fs.files["/display_type"]==original);
|
||||
fs.corrupt_write=false; fs.fail_publish=true;
|
||||
assert(command("set display.type tft") && strstr(reply,"save failed"));
|
||||
assert(display.mode()==HeltecV4DisplayMode::Off && fs.files["/display_type"]==original);
|
||||
fs.fail_publish=false;
|
||||
assert(loadHeltecV4DisplaySettings(&fs,display) && display.mode()==HeltecV4DisplayMode::Off);
|
||||
fs.files["/display_type.bak"]=original;
|
||||
fs.files["/display_type"][5]^=1;
|
||||
assert(loadHeltecV4DisplaySettings(&fs,display) && display.mode()==HeltecV4DisplayMode::Off);
|
||||
assert(fs.files["/display_type"]==original && !fs.exists("/display_type.bak"));
|
||||
for(unsigned field : {2,3,4,5}) {
|
||||
auto invalid=original;
|
||||
invalid[field]=field==3 ? 255 : invalid[field]^0x80;
|
||||
if(field!=5) { invalid[5]=0; for(unsigned i=0;i<5;++i) invalid[5]^=invalid[i]; }
|
||||
fs.files["/display_type"]=invalid;
|
||||
assert(!loadHeltecV4DisplaySettings(&fs,display));
|
||||
assert(display.mode()==HeltecV4DisplayMode::Auto);
|
||||
}
|
||||
assert(!loadHeltecV4DisplaySettings(nullptr,display));
|
||||
const auto no_fs=display.applied;
|
||||
assert(command("set display.type off") && strstr(reply,"save failed"));
|
||||
assert(display.mode()==HeltecV4DisplayMode::Auto && display.applied==no_fs);
|
||||
assert(!loadHeltecV4DisplaySettings(&fs,display));
|
||||
assert(command("set display.type off") && display.mode()==HeltecV4DisplayMode::Off);
|
||||
fs.files.clear(); // Factory reset erases this file with the rest of SPIFFS.
|
||||
assert(!loadHeltecV4DisplaySettings(&fs,display) && display.mode()==HeltecV4DisplayMode::Auto);
|
||||
}
|
||||
'''
|
||||
|
||||
WRAPPER_ARDUINO = r'''
|
||||
#pragma once
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#define LOW 0
|
||||
#define HIGH 1
|
||||
#define INPUT 0
|
||||
#define OUTPUT 1
|
||||
#define PIN_TFT_CS 15
|
||||
#define PIN_TFT_DC 16
|
||||
#define PIN_TFT_SDA 33
|
||||
#define PIN_TFT_SCL 17
|
||||
#define PIN_TFT_RST 18
|
||||
#define PIN_TFT_LEDA_CTL 21
|
||||
#define PIN_TFT_LEDA_CTL_ACTIVE HIGH
|
||||
#define PIN_OLED_RESET 21
|
||||
#define DISPLAY_ADDRESS 0x3C
|
||||
inline int modes[64]={},levels[64]={};
|
||||
inline unsigned gpio_events=0,output_events=0,wait_ms=0,oled_begins=0,tft_begins=0,draws=0,bus_releases=0;
|
||||
inline unsigned last_color=0,last_background=0;
|
||||
inline bool oled_present=false,tft_present=false,fail_oled=false,fail_tft=false;
|
||||
inline std::string scenario;
|
||||
inline void pinMode(int pin,int mode) {
|
||||
assert((pin==15 || pin==16 || pin==33 || pin==17 || pin==18 || pin==21
|
||||
|| pin==47 || pin==48 || pin==44 || pin==36) && "never drive radio/GNSS/global-I2C pins");
|
||||
++gpio_events;if(mode==OUTPUT) ++output_events; modes[pin]=mode;
|
||||
}
|
||||
inline void digitalWrite(int pin,int value) { ++gpio_events; ++output_events; levels[pin]=value; }
|
||||
inline void delay(unsigned milliseconds) { wait_ms+=milliseconds; }
|
||||
'''
|
||||
|
||||
WRAPPER_WIRE = r'''
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
#include <vector>
|
||||
#define I2C_BUFFER_LENGTH 128
|
||||
class TwoWire {
|
||||
int id,sda=-1,scl=-1;
|
||||
uint8_t address=0;
|
||||
uint16_t reg=0;
|
||||
std::vector<uint8_t> output,input;
|
||||
size_t cursor=0;
|
||||
public:
|
||||
inline static unsigned begins=0,ends=0,global_events=0,touch_reads=0;
|
||||
explicit TwoWire(int bus=0) : id(bus) {}
|
||||
bool begin(int data,int clock,uint32_t hz) {
|
||||
assert(id==1 && "leave the RTC/environment Wire(0) untouched");
|
||||
assert((data==17 && clock==18) || (data==47 && clock==48));
|
||||
assert(hz==100000); ++begins; sda=data;scl=clock;
|
||||
if(scenario=="bus-failed") return false;
|
||||
modes[sda]=modes[scl]=OUTPUT;
|
||||
return true;
|
||||
}
|
||||
void end() { assert(id==1); ++ends; modes[sda]=modes[scl]=INPUT; }
|
||||
void setTimeOut(unsigned value) { assert(value<=20); }
|
||||
void beginTransmission(uint8_t value) { assert(id==1); address=value; output.clear(); }
|
||||
size_t write(const uint8_t* bytes,size_t count) {
|
||||
output.assign(bytes,bytes+count);
|
||||
return scenario=="short-write" ? count-1 : count;
|
||||
}
|
||||
uint8_t endTransmission(bool stop=true) {
|
||||
assert(id==1);
|
||||
assert(stop && "CHSC DMA reads require STOP after the register address");
|
||||
if(address==0x3c) { assert(sda==17 && scl==18); return oled_present ? 0 : 2; }
|
||||
assert(address==0x2e && sda==47 && scl==48);
|
||||
if(output.size()==2) reg=unsigned(output[0])<<8 | output[1];
|
||||
if(output.size()==8) {
|
||||
static const uint8_t dma[]={0x42,0xbd,0x28,0x35,0xc1,0,0x35,0xae};
|
||||
assert(memcmp(output.data(),dma,8)==0);
|
||||
}
|
||||
return tft_present || scenario=="ack-only" ? 0 : 2;
|
||||
}
|
||||
size_t requestFrom(uint8_t address,uint8_t count) {
|
||||
assert(address==0x2e); ++touch_reads; input.assign(count,0);cursor=0;
|
||||
if(scenario!="ack-only") {
|
||||
if(reg==1) input[0]=0x2e;
|
||||
if(reg==0x9e6b) input[0]=0x10;
|
||||
if(reg==0x9e00) input[0]=0x81;
|
||||
if(reg==0x9e4c) { input[0]=0xf0;input[1]=0;input[2]=0x40;input[3]=1; }
|
||||
}
|
||||
return scenario=="short-read" ? count-1 : count;
|
||||
}
|
||||
int available() { return input.size()-cursor; }
|
||||
int read() { assert(cursor<input.size()); return input[cursor++]; }
|
||||
};
|
||||
inline TwoWire Wire(0);
|
||||
'''
|
||||
|
||||
WRAPPER_BACKEND = r'''
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
#include <helpers/RefCountedDigitalPin.h>
|
||||
#include "DisplayDriver.h"
|
||||
class RecordingDisplay : public DisplayDriver {
|
||||
protected:
|
||||
bool on=false;
|
||||
int default_w,default_h;
|
||||
public:
|
||||
RecordingDisplay(int w,int h) : DisplayDriver(w,h),default_w(w),default_h(h) {}
|
||||
bool isOn() override { return on; }
|
||||
bool supportsRotation() const override { return true; }
|
||||
bool setRotationDegrees(uint16_t degrees) override {
|
||||
setDimensions(degrees%180 ? default_h : default_w,degrees%180 ? default_w : default_h); return true;
|
||||
}
|
||||
void turnOn() override { on=true; }
|
||||
void turnOff() override { on=false; }
|
||||
void clear() override { assert(on); ++draws; }
|
||||
void startFrame(ColorVal bkg) override { assert(on); last_background=bkg; ++draws; }
|
||||
void setTextSize(int) override {}
|
||||
void setColor(ColorVal color) override { last_color=color; }
|
||||
void setCursor(int,int) override { assert(on); ++draws; }
|
||||
void print(const char*) override { assert(on); ++draws; }
|
||||
void fillRect(int,int,int,int) override { assert(on); ++draws; }
|
||||
void drawRect(int,int,int,int) override { assert(on); ++draws; }
|
||||
void drawXbm(int,int,const uint8_t*,int,int) override { assert(on); ++draws; }
|
||||
uint16_t getTextWidth(const char* text) override { return strlen(text)*6; }
|
||||
void endFrame() override { assert(on); ++draws; }
|
||||
};
|
||||
class SSD1306Display : public RecordingDisplay {
|
||||
TwoWire* wire;
|
||||
public:
|
||||
static constexpr size_t FRAMEBUFFER_BYTES=1024;
|
||||
SSD1306Display(RefCountedDigitalPin*,TwoWire* value) : RecordingDisplay(128,64),wire(value) {}
|
||||
bool begin() { ++oled_begins; wire->beginTransmission(0x3c); on=wire->endTransmission()==0&&!fail_oled; return on; }
|
||||
};
|
||||
class ST7789LCDDisplay : public RecordingDisplay {
|
||||
public:
|
||||
explicit ST7789LCDDisplay(RefCountedDigitalPin*) : RecordingDisplay(320,240) {}
|
||||
bool begin() { ++tft_begins; on=!fail_tft; return on; }
|
||||
void releaseBus() { ++bus_releases; on=false; }
|
||||
};
|
||||
'''
|
||||
|
||||
WRAPPER_TEST = r'''
|
||||
#include <cassert>
|
||||
#include <initializer_list>
|
||||
#include "HeltecV4Display.h"
|
||||
using namespace mesh::ui;
|
||||
void frame(HeltecV4Display& display) {
|
||||
static const uint8_t bits[]={128};
|
||||
display.clear();display.startFrame();display.setTextSize(1);display.setColor(1);
|
||||
display.setCursor(0,0);display.print("hello");display.fillRect(0,0,1,1);
|
||||
display.drawRect(0,0,1,1);display.drawXbm(0,0,bits,1,1);display.endFrame();
|
||||
}
|
||||
void parked(bool known_tft=false) {
|
||||
for(int pin : {15,16,33,17,18,21,47,48,44}) {
|
||||
if(known_tft && (pin==18 || pin==21))
|
||||
assert(modes[pin]==OUTPUT && levels[pin]==LOW && "known TFT must retain reset/backlight LOW");
|
||||
else assert(modes[pin]==INPUT);
|
||||
}
|
||||
}
|
||||
int main() {
|
||||
{
|
||||
// A saved Off choice must never probe or enable any display GPIO at boot.
|
||||
HeltecV4Display display;
|
||||
assert(display.setMode(HeltecV4DisplayMode::Off));
|
||||
assert(!display.begin() && strcmp(display.status(),"off")==0);
|
||||
assert(gpio_events==0 && TwoWire::begins==0);
|
||||
frame(display);display.turnOn();display.turnOff();
|
||||
assert(display.setMode(HeltecV4DisplayMode::Off));
|
||||
assert(output_events==0 && TwoWire::begins==0 && !display.isOn());
|
||||
}
|
||||
for(const char* input : {"absent","ack-only","short-read","short-write","bus-failed",
|
||||
"oled","both","tft","oled-init-failed","tft-init-failed"}) {
|
||||
scenario=input; oled_present=scenario=="oled" || scenario=="both" || scenario=="oled-init-failed";
|
||||
tft_present=scenario=="tft" || scenario=="both" || scenario=="short-read"
|
||||
|| scenario=="short-write" || scenario=="tft-init-failed";
|
||||
fail_oled=scenario=="oled-init-failed";fail_tft=scenario=="tft-init-failed";
|
||||
RefCountedDigitalPin rail(36);rail.begin();rail.claim();
|
||||
HeltecV4Display display(&rail);
|
||||
const bool ready=scenario=="oled" || scenario=="both" || scenario=="tft";
|
||||
const auto touch_before=TwoWire::touch_reads,oled_before=oled_begins,tft_before=tft_begins;
|
||||
assert(display.begin()==ready && display.isOn()==ready);
|
||||
if(!ready) {
|
||||
assert(display.selectedType()==HeltecV4DisplayType::None);
|
||||
assert(strstr(display.status(),"none") || strstr(display.status(),"failed"));
|
||||
parked(scenario=="tft-init-failed"); const auto painted=draws;frame(display);display.turnOn();
|
||||
assert(draws==painted && !display.isOn());
|
||||
assert(levels[36]==HIGH && "failed probe must preserve another owner's rail claim");
|
||||
} else {
|
||||
assert(display.selectedType()==(oled_present ? HeltecV4DisplayType::Oled : HeltecV4DisplayType::Tft));
|
||||
assert(strcmp(display.status(),oled_present ? "OLED" : "TFT")==0);
|
||||
if(oled_present) {
|
||||
assert(TwoWire::touch_reads==touch_before && tft_begins==tft_before && oled_begins==oled_before+1);
|
||||
display.setColor(0xf800);assert(last_color==1);
|
||||
display.startFrame(0);assert(last_background==0);
|
||||
} else {
|
||||
assert(TwoWire::touch_reads>touch_before && tft_begins==tft_before+1 && oled_begins==oled_before);
|
||||
display.setColor(0xf800);assert(last_color==0xf800);
|
||||
assert(modes[47]==INPUT && modes[48]==INPUT && levels[44]==HIGH);
|
||||
}
|
||||
assert(display.setRotationDegrees(90));
|
||||
assert(display.width()==(oled_present ? 64 : 240) && display.height()==(oled_present ? 128 : 320));
|
||||
assert(!display.setRotationDegrees(45));
|
||||
frame(display);
|
||||
display.turnOff();const auto painted=draws;frame(display);assert(draws==painted);
|
||||
parked(!oled_present);
|
||||
const auto begins_before_wake=oled_begins+tft_begins;
|
||||
display.turnOn();
|
||||
assert(display.isOn() && oled_begins+tft_begins==begins_before_wake+1);
|
||||
assert(display.width()==(oled_present ? 64 : 240) && display.height()==(oled_present ? 128 : 320));
|
||||
frame(display);
|
||||
assert(display.setMode(HeltecV4DisplayMode::Off));
|
||||
assert(strcmp(display.status(),"off")==0 && !display.isOn());parked(!oled_present);
|
||||
assert(display.setMode(HeltecV4DisplayMode::Off));parked(!oled_present);
|
||||
}
|
||||
rail.release();assert(levels[36]==LOW && "wrapper must balance every temporary power claim");
|
||||
}
|
||||
// Manual override is explicit, and returning to Auto can find another type.
|
||||
scenario="tft";oled_present=false;tft_present=true;fail_oled=fail_tft=false;
|
||||
HeltecV4Display display;
|
||||
assert(display.setMode(HeltecV4DisplayMode::Tft) && display.begin());
|
||||
assert(strstr(display.status(),"TFT") && strstr(display.status(),"forced"));
|
||||
const auto before=bus_releases;
|
||||
oled_present=true;tft_present=false;
|
||||
assert(display.setMode(HeltecV4DisplayMode::Auto));
|
||||
assert(display.selectedType()==HeltecV4DisplayType::Oled && strcmp(display.status(),"OLED")==0);
|
||||
assert(bus_releases>before && "release TFT SPI before remapping shared OLED pins");
|
||||
const auto mode=display.mode();
|
||||
assert(!display.setMode(HeltecV4DisplayMode(99)) && display.mode()==mode);
|
||||
assert(display.setMode(HeltecV4DisplayMode::Off));parked();
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class HeltecV4DisplayDetectionTests(unittest.TestCase):
|
||||
def compile_run(self, work, main, extra=(), files=None, flags=()):
|
||||
for name, contents in (files or {}).items():
|
||||
destination = work / name
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
destination.write_text(contents, encoding="ascii")
|
||||
source = work / "main.cpp"
|
||||
source.write_text(main, encoding="ascii")
|
||||
binary = work / "check"
|
||||
compiled = subprocess.run([
|
||||
os.environ.get("CXX", "g++"), "-std=c++17", "-Wall", "-Wextra", "-Werror",
|
||||
*SANITIZER_FLAGS, *flags, "-I" + str(work), "-I" + str(ROOT / "src"),
|
||||
str(source), *map(str, extra), "-o", str(binary),
|
||||
], capture_output=True, text=True, timeout=60)
|
||||
self.assertEqual(compiled.returncode, 0, compiled.stdout + compiled.stderr)
|
||||
ran = subprocess.run([str(binary)], capture_output=True, text=True, timeout=15)
|
||||
self.assertEqual(ran.returncode, 0, ran.stdout + ran.stderr)
|
||||
return binary
|
||||
|
||||
def test_auto_presence_matrix_manual_choices_and_invalid_values(self):
|
||||
with tempfile.TemporaryDirectory(prefix="heltec-v4-display-policy-") as directory:
|
||||
self.compile_run(Path(directory), POLICY_TEST)
|
||||
|
||||
def test_touch_requires_stable_identity_and_resets_on_every_exit(self):
|
||||
with tempfile.TemporaryDirectory(prefix="heltec-v4-touch-probe-") as directory:
|
||||
self.compile_run(Path(directory), TOUCH_TEST)
|
||||
|
||||
def test_saved_type_validation_live_apply_and_failed_save_rollback(self):
|
||||
with tempfile.TemporaryDirectory(prefix="heltec-v4-display-settings-") as directory:
|
||||
work = Path(directory)
|
||||
files = {"HeltecV4Display.h": SETTINGS_DISPLAY,
|
||||
"helpers/IdentityStore.h": FS_STUB.replace("/display_prefs", "/display_type")}
|
||||
for name in ("HeltecV4DisplaySettings.h", "HeltecV4DisplaySettings.cpp"):
|
||||
files[name] = (UI / name).read_text()
|
||||
self.compile_run(work, SETTINGS_TEST, [work / "HeltecV4DisplaySettings.cpp"], files,
|
||||
["-DHELTEC_V4_DISPLAY_UNIFIED=1"])
|
||||
|
||||
def test_board_name_uses_live_display_status_and_detected_fem_revision(self):
|
||||
board = (ROOT / "variants/heltec_v4/HeltecV4Board.cpp").read_text()
|
||||
method = body(board, "const char* HeltecV4Board::getManufacturerName() const")
|
||||
main = r'''
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <initializer_list>
|
||||
#include "HeltecV4Display.h"
|
||||
constexpr int KCT8103L_PA=3;
|
||||
HeltecV4Display display;
|
||||
class HeltecV4Board {
|
||||
public:
|
||||
struct Fem { int type=0; int getFEMType() const { return type; } } loRaFEMControl;
|
||||
const char* getManufacturerName() const;
|
||||
};
|
||||
@METHOD@
|
||||
int main() {
|
||||
using namespace mesh::ui;
|
||||
HeltecV4Board board;
|
||||
for(int fem : {0,KCT8103L_PA}) {
|
||||
board.loRaFEMControl.type=fem;
|
||||
const std::string prefix=fem==KCT8103L_PA ? "Heltec V4.3 " : "Heltec V4.2 ";
|
||||
for(auto mode : {HeltecV4DisplayMode::Auto,HeltecV4DisplayMode::Off,
|
||||
HeltecV4DisplayMode::Oled,HeltecV4DisplayMode::Tft}) {
|
||||
for(bool oled : {false,true}) for(bool tft : {false,true}) {
|
||||
display.oled=oled; display.tft=tft; display.setMode(mode);
|
||||
assert(board.getManufacturerName()==prefix+display.status());
|
||||
}
|
||||
}
|
||||
display.oled=display.tft=false;
|
||||
display.setMode(HeltecV4DisplayMode::Auto);
|
||||
assert(board.getManufacturerName()==prefix+"none detected");
|
||||
display.setMode(HeltecV4DisplayMode::Off);
|
||||
assert(board.getManufacturerName()==prefix+"off");
|
||||
}
|
||||
}
|
||||
'''.replace("@METHOD@", method)
|
||||
with tempfile.TemporaryDirectory(prefix="heltec-v4-live-board-name-") as directory:
|
||||
self.compile_run(Path(directory), main, files={"HeltecV4Display.h": SETTINGS_DISPLAY},
|
||||
flags=["-DHELTEC_V4_DISPLAY_UNIFIED=1", "-DDISPLAY_CLASS=HeltecV4Display"])
|
||||
|
||||
def test_real_wrapper_releases_pins_preserves_other_power_and_switches_live(self):
|
||||
with tempfile.TemporaryDirectory(prefix="heltec-v4-display-driver-") as directory:
|
||||
work = Path(directory)
|
||||
files = {"Arduino.h": WRAPPER_ARDUINO, "Wire.h": WRAPPER_WIRE,
|
||||
"SSD1306Display.h": WRAPPER_BACKEND,
|
||||
"ST7789LCDDisplay.h": '#pragma once\n#include "SSD1306Display.h"\n'}
|
||||
for name in ("HeltecV4Display.h", "HeltecV4Display.cpp", "HeltecV4DisplayPolicy.h",
|
||||
"HeltecV4DisplayProbe.h", "DisplayDriver.h", "DisplayPowerPolicy.h",
|
||||
"DisplayBuildFlags.h", "ColorTheme.h", "DisplayNightMode.h"):
|
||||
files[name] = (UI / name).read_text()
|
||||
self.compile_run(work, WRAPPER_TEST, [work / "HeltecV4Display.cpp"], files,
|
||||
["-DHELTEC_V4_DISPLAY_UNIFIED=1", "-Wno-unused-parameter"])
|
||||
|
||||
def test_every_role_loads_before_probe_and_base_v4_retains_external_rail_ownership(self):
|
||||
for role in ("companion_radio", "simple_repeater", "simple_room_server", "simple_sensor"):
|
||||
setup = body((ROOT / "examples" / role / "main.cpp").read_text(), "void setup()")
|
||||
with self.subTest(role=role):
|
||||
self.assertLess(setup.index("loadHeltecV4DisplaySettings("), setup.index("display.begin()"))
|
||||
target = (ROOT / "variants/heltec_v4/target.cpp").read_text()
|
||||
# The original V4 screen has never owned the shared external-power
|
||||
# rail. Turning a screen off must not cut power to expansion sensors.
|
||||
self.assertNotIn("display(&board.periph_power)", target)
|
||||
self.assertRegex(target, r"DISPLAY_CLASS\s+display\((?:NULL|nullptr)\)")
|
||||
|
||||
def test_actual_companion_loop_keeps_headless_services_and_live_selection(self):
|
||||
fixture = (ROOT / "test/fixtures/companion_ui_deadlines.cpp").read_text()
|
||||
prefix = fixture[:fixture.index("@LOOP@")]
|
||||
prefix = prefix.replace("namespace ui { enum class DisplayWake { Button }; }",
|
||||
"namespace ui { enum class DisplayWake { Button }; "
|
||||
"enum class HeltecV4DisplayType { None,Oled,Tft }; }")
|
||||
prefix = prefix.replace("struct Display {", r'''
|
||||
struct Display {
|
||||
mesh::ui::HeltecV4DisplayType type=mesh::ui::HeltecV4DisplayType::None;
|
||||
unsigned epoch=1,rotations=0,angle=0;
|
||||
unsigned generation() const { return epoch; }
|
||||
auto selectedType() const { return type; }
|
||||
bool setRotationDegrees(unsigned value) { ++rotations;angle=value;return true; }
|
||||
''')
|
||||
prefix = prefix.replace('const char* node_name = "test";',
|
||||
'const char* node_name = "test"; unsigned display_rotation_degrees=90;')
|
||||
dependencies = r'''
|
||||
void UITask::setCurrScreen(UIScreen* screen) { curr=screen; }
|
||||
struct Buzzer {
|
||||
unsigned serviced=0;
|
||||
bool isPlaying() const { return true; }
|
||||
void loop() { ++serviced; }
|
||||
} buzzer;
|
||||
struct Vibration { unsigned serviced=0; void loop() { ++serviced; } } vibration;
|
||||
'''
|
||||
loop = body((ROOT / "examples/companion_radio/ui-new/UITask.cpp").read_text(),
|
||||
"void UITask::loop()")
|
||||
main = r'''
|
||||
int main() {
|
||||
UITask ui;
|
||||
now_ms=10000;ui.voltage=3300;
|
||||
ui.loop();
|
||||
assert(ui._display==nullptr && ui.display.frames==0);
|
||||
assert(ui.shutdowns==1 && ui.battery_reads==1 && "headless mode must preserve battery protection");
|
||||
assert(buzzer.serviced==1 && vibration.serviced==1 && ui.screen.polls==1);
|
||||
ui.voltage=4000;now_ms=11000;
|
||||
ui.display.type=mesh::ui::HeltecV4DisplayType::Oled;++ui.display.epoch;
|
||||
ui.loop();
|
||||
assert(ui._display==&ui.display && ui.display.frames==1);
|
||||
assert(ui.display.angle==90 && ui.display.rotations==2);
|
||||
assert(buzzer.serviced==2 && vibration.serviced==2);
|
||||
now_ms=12000;ui.display.type=mesh::ui::HeltecV4DisplayType::None;++ui.display.epoch;
|
||||
ui.loop();
|
||||
assert(ui._display==nullptr && ui.display.frames==1);
|
||||
assert(buzzer.serviced==3 && vibration.serviced==3 && ui.screen.polls==3);
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix="heltec-v4-headless-ui-services-") as directory:
|
||||
self.compile_run(Path(directory), prefix + dependencies + loop + main,
|
||||
flags=["-DHELTEC_V4_DISPLAY_UNIFIED=1", "-DAUTO_SHUTDOWN_MILLIVOLTS=3400",
|
||||
"-DCOMPANION_FEATURE_READER=0", "-DPIN_BUZZER=1", "-DPIN_VIBRATION=2",
|
||||
"-DAUTO_OFF_MILLIS=0", "-Wno-unused-function"])
|
||||
|
||||
def test_infrastructure_generation_forces_redraw_across_clock_wrap(self):
|
||||
for role in ("simple_repeater", "simple_room_server", "simple_sensor"):
|
||||
source = (ROOT / "examples" / role / "UITask.cpp").read_text()
|
||||
loop = body(source, "void UITask::loop()")
|
||||
prefix = loop[:loop.index("#if defined(HELTEC_V4_R8_TFT)")]
|
||||
due = ("assert(uint32_t(now-ui._last_refresh)>=1000u);" if role == "simple_sensor"
|
||||
else "assert(int32_t(now-ui._next_refresh)>=0 && \"live backend change must redraw now\");")
|
||||
main = r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <initializer_list>
|
||||
uint32_t now=0;
|
||||
uint32_t millis() { return now; }
|
||||
struct Display { unsigned epoch=1; unsigned generation() const { return epoch; } } display;
|
||||
class UITask {
|
||||
public:
|
||||
uint32_t _next_refresh=0,_last_refresh=0;
|
||||
bool _frame_valid=true,_rows_valid=true;
|
||||
void loop();
|
||||
};
|
||||
@PREFIX@
|
||||
}
|
||||
int main() {
|
||||
UITask ui;
|
||||
for(uint32_t clock : {10000u,0x90000000u,0xfffffff0u,17u}) {
|
||||
now=clock;++display.epoch;
|
||||
ui._frame_valid=ui._rows_valid=true;
|
||||
ui._next_refresh=now+5000u;ui._last_refresh=now;
|
||||
ui.loop();
|
||||
assert(!ui._frame_valid && !ui._rows_valid);
|
||||
@DUE@
|
||||
}
|
||||
}
|
||||
'''.replace("@PREFIX@", prefix).replace("@DUE@", due)
|
||||
with self.subTest(role=role), tempfile.TemporaryDirectory(prefix="heltec-v4-display-generation-") as directory:
|
||||
self.compile_run(Path(directory), main,
|
||||
flags=["-DHELTEC_V4_DISPLAY_UNIFIED=1", "-DDISPLAY_REDRAW_ON_CHANGE=1",
|
||||
"-DDISPLAY_ACTIVITY_DASHBOARD=1"])
|
||||
|
||||
def test_unified_environment_uses_primary_i2c_and_preserves_other_board_policy(self):
|
||||
main = r'''
|
||||
#include <helpers/sensors/EnvironmentI2CConfig.h>
|
||||
static_assert(ENV_HAS_SECONDARY_I2C==EXPECTED_SECONDARY,
|
||||
"unified display owns controller 1; same-pin sensors must use primary Wire");
|
||||
int main() {}
|
||||
'''
|
||||
cases = [(1, 4, 3, 0), (0, 4, 3, 1), (1, 6, 7, 1)]
|
||||
for unified, sda, scl, expected in cases:
|
||||
with self.subTest(unified=unified, pins=(sda, scl)), tempfile.TemporaryDirectory(
|
||||
prefix="heltec-v4-i2c-ownership-") as directory:
|
||||
self.compile_run(Path(directory), main,
|
||||
flags=[f"-DHELTEC_V4_DISPLAY_UNIFIED={unified}", "-DPIN_BOARD_SDA=4",
|
||||
"-DPIN_BOARD_SCL=3", f"-DENV_PIN_SDA={sda}", f"-DENV_PIN_SCL={scl}",
|
||||
f"-DEXPECTED_SECONDARY={expected}"])
|
||||
|
||||
def test_real_tft_rejects_spi_allocation_failure_before_panel_initialization(self):
|
||||
import test_heltec_r8_display_detection as fixture
|
||||
main = r'''
|
||||
#include "ST7789LCDDisplay.h"
|
||||
ColorVal UIColor::window_bkg=0,UIColor::title_bkg=0,UIColor::title_txt=1;
|
||||
ColorVal UIColor::primary_txt=1,UIColor::secondary_txt=1,UIColor::warning_txt=1;
|
||||
ColorVal UIColor::popup_bkg=0,UIColor::popup_txt=1,UIColor::corp_blue=1;
|
||||
int main(int argc,char** argv) {
|
||||
assert(argc==2);scenario=argv[1];
|
||||
SPIClass radio(HSPI);bus_live[HSPI]=true;
|
||||
ST7789LCDDisplay display;
|
||||
assert(last_spi_host==FSPI && "unified TFT must use a different SPI controller from the radio");
|
||||
const bool fail=scenario=="spi-fail";
|
||||
assert(display.begin()==!fail);
|
||||
assert(display.isOn()==!fail && spi_begins==1);
|
||||
assert(radio.bus() && "TFT startup/failure must preserve the active radio controller");
|
||||
if(fail) {
|
||||
assert(init_calls==0 && draws==0 && spi_ends==1);
|
||||
assert(levels[PIN_TFT_LEDA_CTL]==LOW && levels[PIN_TFT_RST]==LOW);
|
||||
} else assert(init_calls==1 && draws>0);
|
||||
display.releaseBus();
|
||||
assert(radio.bus() && "TFT sleep must not stop the radio SPI controller");
|
||||
}
|
||||
'''
|
||||
tester = fixture.HeltecR8DisplayDetectionTest()
|
||||
spi = fixture.SPI.replace("struct SPIClass {", "inline bool bus_live[2]={};\ninline int last_spi_host=-1;\nstruct SPIClass {")
|
||||
spi = spi.replace("explicit SPIClass(int=0) {}", "int host;\n explicit SPIClass(int value=HSPI) : host(value) { last_spi_host=value; }")
|
||||
spi = spi.replace('live=scenario!="spi-fail";', 'live=scenario!="spi-fail"; bus_live[host]=live;')
|
||||
spi = spi.replace("return live ? this : nullptr;", "return bus_live[host] ? this : nullptr;")
|
||||
spi = spi.replace("++spi_ends; live=false;", "++spi_ends; live=false; bus_live[host]=false;")
|
||||
with tempfile.TemporaryDirectory(prefix="heltec-v4-tft-spi-allocation-") as directory:
|
||||
with patch.object(fixture, "MAIN", main), patch.object(fixture, "SPI", spi), \
|
||||
patch.object(fixture, "ARDUINO", fixture.ARDUINO + "\n#define FSPI 0\n"):
|
||||
binary = tester.build(Path(directory) / "driver",
|
||||
["-DHELTEC_V4_DISPLAY_UNIFIED=1", "-DHELTEC_LORA_V4_TFT",
|
||||
"-DDISPLAY_WIDTH=240", "-DDISPLAY_HEIGHT=320"])
|
||||
for scenario in ("valid", "spi-fail"):
|
||||
with self.subTest(scenario=scenario):
|
||||
tester.run_case(binary, scenario)
|
||||
radio = (ROOT / "variants/heltec_v4/target.cpp").read_text()
|
||||
self.assertRegex(radio, r"static\s+SPIClass\s+spi\(HSPI\)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,112 @@
|
||||
"""Keep unified V4 releases distinct from historical display and OTA targets."""
|
||||
from copy import deepcopy
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
import consolidated_profiles as consolidated
|
||||
|
||||
SPEC = next(spec for spec in consolidated.SPECS if spec["key"] == "unifiedV4Display")
|
||||
SOURCE = "a" * 40
|
||||
FLAGS = ["-DHELTEC_V4_DISPLAY_UNIFIED=1", "-DDISPLAY_CLASS=HeltecV4Display"]
|
||||
PROVIDER_SPEC = importlib.util.spec_from_file_location(
|
||||
"unified_v4_provider", ROOT / "mesh-america/update-provider-release.py")
|
||||
provider = importlib.util.module_from_spec(PROVIDER_SPEC)
|
||||
PROVIDER_SPEC.loader.exec_module(provider)
|
||||
|
||||
|
||||
def environment():
|
||||
return dict(board="heltec_v4", build_flags=list(FLAGS))
|
||||
|
||||
|
||||
def manifest(target):
|
||||
return dict(target=target, artifact_target=target, verified=True,
|
||||
platform="ESP32_PLATFORM", build_profile="full", source_commit=SOURCE,
|
||||
capabilities=[SPEC["capability"]], verification=[dict(
|
||||
capability=SPEC["capability"], present=True,
|
||||
source="packaged application", evidence=SPEC["marker"])])
|
||||
|
||||
|
||||
class UnifiedV4CatalogTests(unittest.TestCase):
|
||||
def test_all_display_variants_require_exact_new_driver_and_source_proof(self):
|
||||
for target in ("heltec_v4_repeater", "heltec_v4_room_server", "heltec_v4_sensor",
|
||||
"heltec_v4_2_v4_3_companion_radio_full_femon",
|
||||
"heltec_v4_tft_repeater", "heltec_v4_expansionkit_repeater",
|
||||
"heltec_v4_expansionkit_tft_companion_radio_full_femon"):
|
||||
row = manifest(target)
|
||||
with self.subTest(target=target):
|
||||
self.assertEqual(consolidated.runtime_metadata(row, environment(), SOURCE),
|
||||
dict(unifiedV4Display=True, unifiedV4DisplaySource=SOURCE))
|
||||
self.assertEqual(consolidated.runtime_metadata(
|
||||
{**row, "capabilities": []}, environment(), SOURCE), {})
|
||||
for changed in ({"verification": []}, {"platform": "NRF52_PLATFORM"},
|
||||
{"source_commit": "b" * 40}, {"verified": False}):
|
||||
with self.assertRaises(ValueError):
|
||||
consolidated.runtime_metadata({**row, **changed}, environment(), SOURCE)
|
||||
with self.assertRaises(ValueError):
|
||||
consolidated.runtime_metadata(row, {**environment(), "board": "heltec_v4_r8"}, SOURCE)
|
||||
for override in ("-UHELTEC_V4_DISPLAY_UNIFIED", "-DDISPLAY_CLASS=SSD1306Display"):
|
||||
with self.assertRaises(ValueError):
|
||||
consolidated.runtime_metadata(row, {**environment(), "build_flags": FLAGS + [override]}, SOURCE)
|
||||
|
||||
def test_roles_and_r8_hardware_cannot_be_interchanged(self):
|
||||
for prefix in ("heltec_v4_tft", "heltec_v4_3_tft", "heltec_v4_expansionkit"):
|
||||
for role in ("repeater", "room_server", "sensor"):
|
||||
old, new = prefix + "_" + role, "heltec_v4_" + role
|
||||
with self.subTest(target=old):
|
||||
self.assertIn((new, "unifiedV4Display"), consolidated.replacements(old))
|
||||
envs = {"env:" + old: environment(), "env:" + new: environment()}
|
||||
self.assertEqual(consolidated.configuration_replacement(old, envs), new)
|
||||
envs["env:" + old]["board"] = "heltec_v4_r8"
|
||||
self.assertIsNone(consolidated.configuration_replacement(old, envs))
|
||||
for target in ("heltec_v4_r8_tft_repeater", "heltec_v4_r8_sensor",
|
||||
"heltec_v4_partition_expander", "heltec_v4_tft_repeater_custom"):
|
||||
with self.subTest(target=target):
|
||||
self.assertEqual(consolidated.unified_v4_replacements(target), [])
|
||||
with self.assertRaises(ValueError):
|
||||
consolidated.runtime_metadata(manifest(target), environment(), SOURCE)
|
||||
|
||||
def test_default_consolidation_does_not_change_deployed_target_names(self):
|
||||
old, new = "heltec_v4_tft_repeater", "heltec_v4_repeater"
|
||||
legacy = dict(manifest={**manifest(old), "capabilities": [], "verification": []})
|
||||
canonical = dict(manifest=manifest(new))
|
||||
inputs = [legacy, canonical]
|
||||
before = deepcopy(inputs)
|
||||
self.assertEqual(consolidated.omit_redundant_records(inputs), [canonical])
|
||||
self.assertEqual(inputs, before)
|
||||
canonical["manifest"]["verification"] = []
|
||||
self.assertEqual(consolidated.omit_redundant_records(inputs), inputs)
|
||||
|
||||
def test_provider_requires_the_downloadable_application_not_only_sidecar_claims(self):
|
||||
old, new = "heltec_v4_tft_repeater", "heltec_v4_repeater-full-usb-wifi-ota"
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
app = Path(folder) / (new + "-v1.18.0.2-aaaaaaaa.bin")
|
||||
proof = app.with_suffix(".capabilities.json")
|
||||
row = {**manifest("heltec_v4_repeater"), "artifact_target": new}
|
||||
proof.write_text(json.dumps(row))
|
||||
app.write_bytes(b"old application without unified display")
|
||||
files = {new: [app]}
|
||||
self.assertIsNone(provider.unified_v4_release_identity(old, files))
|
||||
app.write_bytes(b"prefix\0" + SPEC["marker"].encode("ascii") + b"\0suffix")
|
||||
self.assertEqual(provider.unified_v4_release_identity(old, files), new)
|
||||
self.assertEqual(provider.resolve_release_identity(old, files), (new, True))
|
||||
for changed in ({"verified": False}, {"verification": []},
|
||||
{"artifact_target": "heltec_v4_r8_repeater-full-usb-wifi-ota"}):
|
||||
proof.write_text(json.dumps({**row, **changed}))
|
||||
self.assertIsNone(provider.unified_v4_release_identity(old, files))
|
||||
proof.unlink()
|
||||
self.assertIsNone(provider.unified_v4_release_identity(old, files))
|
||||
|
||||
def test_historical_provider_artifacts_and_r8_keep_their_exact_identity(self):
|
||||
for old in ("heltec_v4_tft_repeater", "heltec_v4_r8_tft_repeater"):
|
||||
files = {old: [Path(old + "-v1.18.0.1-aaaaaaaa.bin")]}
|
||||
self.assertEqual(provider.resolve_release_identity(old, files), (old, False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,5 +1,9 @@
|
||||
#include "HeltecV4Board.h"
|
||||
#include <helpers/ESP32GpioHold.h>
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED && defined(DISPLAY_CLASS)
|
||||
#include <helpers/ui/HeltecV4DisplaySettings.h>
|
||||
extern HeltecV4Display display;
|
||||
#endif
|
||||
|
||||
void HeltecV4Board::begin() {
|
||||
ESP32Board::begin();
|
||||
@@ -56,7 +60,13 @@ void HeltecV4Board::begin() {
|
||||
}
|
||||
|
||||
const char* HeltecV4Board::getManufacturerName() const {
|
||||
#ifdef HELTEC_LORA_V4_TFT
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED && defined(DISPLAY_CLASS)
|
||||
static char name[64];
|
||||
snprintf(name, sizeof(name), "Heltec V4.%u %s",
|
||||
loRaFEMControl.getFEMType() == KCT8103L_PA ? 3u : 2u,
|
||||
display.status());
|
||||
return name;
|
||||
#elif defined(HELTEC_LORA_V4_TFT)
|
||||
return loRaFEMControl.getFEMType() == KCT8103L_PA ? "Heltec V4.3 TFT" : "Heltec V4 TFT";
|
||||
#else
|
||||
return loRaFEMControl.getFEMType() == KCT8103L_PA ? "Heltec V4.3 OLED" : "Heltec V4 OLED";
|
||||
@@ -98,6 +108,9 @@ bool HeltecV4Board::handleCommand(const char* command, uint32_t sender_timestamp
|
||||
if (command == nullptr || reply == nullptr) {
|
||||
return false;
|
||||
}
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED && defined(DISPLAY_CLASS)
|
||||
if (mesh::ui::handleHeltecV4DisplayCommand(command, reply, 160, display)) return true;
|
||||
#endif
|
||||
|
||||
if (strcmp(command, "get radio.fem.rxgain") == 0) {
|
||||
if (!loRaFEMControl.isLnaCanControl()) {
|
||||
|
||||
@@ -51,29 +51,21 @@ lib_deps =
|
||||
${esp32_base.lib_deps}
|
||||
${sensor_base.lib_deps}
|
||||
|
||||
[heltec_v4_oled]
|
||||
[heltec_v4_display]
|
||||
extends = Heltec_lora32_v4
|
||||
build_flags =
|
||||
${Heltec_lora32_v4.build_flags}
|
||||
-D HELTEC_LORA_V4_OLED
|
||||
-D HELTEC_V4_DISPLAY_UNIFIED=1
|
||||
-D HELTEC_LORA_V4_TFT
|
||||
-D HELTEC_V4_OLED_KIT_COMPAT=1
|
||||
-D PIN_BOARD_SDA=17
|
||||
-D PIN_BOARD_SCL=18
|
||||
; The OLED uses Wire above; the expansion/general sensor connector is 4/3.
|
||||
-D HELTEC_V4_TFT_KIT_COMPAT=1
|
||||
; Keep sensors/RTC on the expansion bus. The display wrapper owns a
|
||||
; separate I2C controller for OLED 17/18 and TFT touch identification.
|
||||
-D PIN_BOARD_SDA=4
|
||||
-D PIN_BOARD_SCL=3
|
||||
-D ENV_PIN_SDA=4
|
||||
-D ENV_PIN_SCL=3
|
||||
-D PIN_OLED_RESET=21
|
||||
build_src_filter= ${Heltec_lora32_v4.build_src_filter}
|
||||
lib_deps = ${Heltec_lora32_v4.lib_deps}
|
||||
|
||||
[heltec_v4_tft]
|
||||
extends = Heltec_lora32_v4
|
||||
build_flags =
|
||||
${Heltec_lora32_v4.build_flags}
|
||||
-D HELTEC_LORA_V4_TFT
|
||||
-D HELTEC_V4_TFT_KIT_COMPAT=1
|
||||
-D PIN_BOARD_SDA=4
|
||||
-D PIN_BOARD_SCL=3
|
||||
-D PIN_TFT_RST=18
|
||||
-D PIN_TFT_VDD_CTL=-1
|
||||
-D PIN_TFT_LEDA_CTL=21
|
||||
@@ -82,16 +74,33 @@ build_flags =
|
||||
-D PIN_TFT_DC=16
|
||||
-D PIN_TFT_SCL=17
|
||||
-D PIN_TFT_SDA=33
|
||||
build_src_filter= ${Heltec_lora32_v4.build_src_filter}
|
||||
lib_deps =
|
||||
build_src_filter = ${Heltec_lora32_v4.build_src_filter}
|
||||
+<helpers/ui/HeltecV4Display.cpp>
|
||||
+<helpers/ui/SSD1306Display.cpp>
|
||||
+<helpers/ui/ST7789LCDDisplay.cpp>
|
||||
lib_deps =
|
||||
${Heltec_lora32_v4.lib_deps}
|
||||
adafruit/Adafruit ST7735 and ST7789 Library @ ^1.11.0
|
||||
|
||||
; Keep explicit historical environments/OTA identities available. Both display
|
||||
; aliases now compile the same auto/OLED/TFT/headless implementation.
|
||||
[heltec_v4_oled]
|
||||
extends = heltec_v4_display
|
||||
build_flags = ${heltec_v4_display.build_flags}
|
||||
build_src_filter = ${heltec_v4_display.build_src_filter}
|
||||
lib_deps = ${heltec_v4_display.lib_deps}
|
||||
|
||||
[heltec_v4_tft]
|
||||
extends = heltec_v4_display
|
||||
build_flags = ${heltec_v4_display.build_flags}
|
||||
build_src_filter = ${heltec_v4_display.build_src_filter}
|
||||
lib_deps = ${heltec_v4_display.lib_deps}
|
||||
|
||||
[env:heltec_v4_repeater]
|
||||
extends = heltec_v4_oled
|
||||
build_flags =
|
||||
${heltec_v4_oled.build_flags}
|
||||
-D DISPLAY_CLASS=SSD1306Display
|
||||
-D DISPLAY_CLASS=HeltecV4Display
|
||||
-D ADVERT_NAME='"Heltec Repeater"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
@@ -114,7 +123,7 @@ extends = heltec_v4_oled
|
||||
board_build.partitions = default.csv
|
||||
build_flags =
|
||||
${heltec_v4_oled.build_flags}
|
||||
-D DISPLAY_CLASS=SSD1306Display
|
||||
-D DISPLAY_CLASS=HeltecV4Display
|
||||
-D ADVERT_NAME='"Heltec Repeater"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
@@ -132,7 +141,7 @@ lib_deps =
|
||||
extends = heltec_v4_oled
|
||||
build_flags =
|
||||
${heltec_v4_oled.build_flags}
|
||||
-D DISPLAY_CLASS=SSD1306Display
|
||||
-D DISPLAY_CLASS=HeltecV4Display
|
||||
-D ADVERT_NAME='"Heltec Repeater"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
@@ -154,7 +163,7 @@ lib_deps =
|
||||
extends = heltec_v4_oled
|
||||
build_flags =
|
||||
${heltec_v4_oled.build_flags}
|
||||
-D DISPLAY_CLASS=SSD1306Display
|
||||
-D DISPLAY_CLASS=HeltecV4Display
|
||||
-D ADVERT_NAME='"ESPNow Bridge"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
@@ -181,7 +190,7 @@ board_ssl_cert_source = adafruit-full
|
||||
board_build.embed_files = src/certs/x509_crt_bundle.bin
|
||||
build_flags =
|
||||
${heltec_v4_oled.build_flags}
|
||||
-D DISPLAY_CLASS=SSD1306Display
|
||||
-D DISPLAY_CLASS=HeltecV4Display
|
||||
-D ADVERT_NAME='"MQTT Observer"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
@@ -231,7 +240,7 @@ board_ssl_cert_source = adafruit-full
|
||||
board_build.embed_files = src/certs/x509_crt_bundle.bin
|
||||
build_flags =
|
||||
${heltec_v4_oled.build_flags}
|
||||
-D DISPLAY_CLASS=SSD1306Display
|
||||
-D DISPLAY_CLASS=HeltecV4Display
|
||||
-D ADVERT_NAME='"MQTT Observer"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
@@ -332,7 +341,7 @@ board_ssl_cert_source = adafruit-full
|
||||
board_build.embed_files = src/certs/x509_crt_bundle.bin
|
||||
build_flags =
|
||||
${heltec_v4_oled.build_flags}
|
||||
-D DISPLAY_CLASS=SSD1306Display
|
||||
-D DISPLAY_CLASS=HeltecV4Display
|
||||
-D ADVERT_NAME='"Heltec Room Observer"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
@@ -374,7 +383,7 @@ board_ssl_cert_source = adafruit-full
|
||||
board_build.embed_files = src/certs/x509_crt_bundle.bin
|
||||
build_flags =
|
||||
${heltec_v4_oled.build_flags}
|
||||
-D DISPLAY_CLASS=SSD1306Display
|
||||
-D DISPLAY_CLASS=HeltecV4Display
|
||||
-D ADVERT_NAME='"Heltec Room Observer"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
@@ -413,7 +422,7 @@ lib_deps =
|
||||
extends = heltec_v4_oled
|
||||
build_flags =
|
||||
${heltec_v4_oled.build_flags}
|
||||
-D DISPLAY_CLASS=SSD1306Display
|
||||
-D DISPLAY_CLASS=HeltecV4Display
|
||||
-D ADVERT_NAME='"Heltec Room"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
@@ -449,7 +458,7 @@ build_flags =
|
||||
-I examples/companion_radio/ui-new
|
||||
-D MAX_CONTACTS=350
|
||||
-D MAX_GROUP_CHANNELS=40
|
||||
-D DISPLAY_CLASS=SSD1306Display
|
||||
-D DISPLAY_CLASS=HeltecV4Display
|
||||
-D ENABLE_USB_INTERFACE
|
||||
; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1
|
||||
; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1
|
||||
@@ -485,7 +494,7 @@ build_flags =
|
||||
-I examples/companion_radio/ui-new
|
||||
-D MAX_CONTACTS=350
|
||||
-D MAX_GROUP_CHANNELS=40
|
||||
-D DISPLAY_CLASS=SSD1306Display
|
||||
-D DISPLAY_CLASS=HeltecV4Display
|
||||
-D BLE_PIN_CODE=123456 ; dynamic, random PIN
|
||||
-D AUTO_SHUTDOWN_MILLIVOLTS=3400
|
||||
-D BLE_DEBUG_LOGGING=1
|
||||
@@ -532,7 +541,7 @@ build_flags =
|
||||
-D ENABLE_WIFI_INTERFACE
|
||||
-D MAX_CONTACTS=350
|
||||
-D MAX_GROUP_CHANNELS=40
|
||||
-D DISPLAY_CLASS=SSD1306Display
|
||||
-D DISPLAY_CLASS=HeltecV4Display
|
||||
-D WIFI_DEBUG_LOGGING=1
|
||||
-D WIFI_SSID='"myssid"'
|
||||
-D WIFI_PWD='"mypwd"'
|
||||
@@ -618,7 +627,7 @@ build_flags =
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D DISPLAY_CLASS=SSD1306Display
|
||||
-D DISPLAY_CLASS=HeltecV4Display
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
build_src_filter = ${heltec_v4_oled.build_src_filter}
|
||||
@@ -633,7 +642,7 @@ lib_deps =
|
||||
extends = heltec_v4_tft
|
||||
build_flags =
|
||||
${heltec_v4_tft.build_flags}
|
||||
-D DISPLAY_CLASS=ST7789LCDDisplay
|
||||
-D DISPLAY_CLASS=HeltecV4Display
|
||||
-D ADVERT_NAME='"Heltec Repeater"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
@@ -654,7 +663,7 @@ lib_deps =
|
||||
extends = heltec_v4_tft
|
||||
build_flags =
|
||||
${heltec_v4_tft.build_flags}
|
||||
-D DISPLAY_CLASS=ST7789LCDDisplay
|
||||
-D DISPLAY_CLASS=HeltecV4Display
|
||||
-D ADVERT_NAME='"ESPNow Bridge"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
@@ -676,7 +685,7 @@ lib_deps =
|
||||
extends = heltec_v4_tft
|
||||
build_flags =
|
||||
${heltec_v4_tft.build_flags}
|
||||
-D DISPLAY_CLASS=ST7789LCDDisplay
|
||||
-D DISPLAY_CLASS=HeltecV4Display
|
||||
-D ADVERT_NAME='"Heltec Room"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
@@ -712,7 +721,7 @@ build_flags =
|
||||
-I examples/companion_radio/ui-new
|
||||
-D MAX_CONTACTS=350
|
||||
-D MAX_GROUP_CHANNELS=40
|
||||
-D DISPLAY_CLASS=ST7789LCDDisplay
|
||||
-D DISPLAY_CLASS=HeltecV4Display
|
||||
-D ENABLE_USB_INTERFACE
|
||||
; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1
|
||||
; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1
|
||||
@@ -736,7 +745,7 @@ extends = heltec_v4_tft
|
||||
build_flags =
|
||||
${heltec_v4_tft.build_flags}
|
||||
-I examples/companion_radio/ui-new
|
||||
-D DISPLAY_CLASS=ST7789LCDDisplay
|
||||
-D DISPLAY_CLASS=HeltecV4Display
|
||||
-D MAX_CONTACTS=350
|
||||
-D MAX_GROUP_CHANNELS=40
|
||||
-D BLE_PIN_CODE=123456 ; dynamic, random PIN
|
||||
@@ -793,7 +802,7 @@ build_flags =
|
||||
-D MAX_CONTACTS=350
|
||||
-D MAX_GROUP_CHANNELS=40
|
||||
-D OFFLINE_QUEUE_SIZE=512
|
||||
-D DISPLAY_CLASS=ST7789LCDDisplay
|
||||
-D DISPLAY_CLASS=HeltecV4Display
|
||||
-D WIFI_DEBUG_LOGGING=1
|
||||
-D WIFI_SSID='"myssid"'
|
||||
-D WIFI_PWD='"mypwd"'
|
||||
@@ -839,7 +848,7 @@ build_flags =
|
||||
-D ADVERT_LON=0.0
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
; Environmental sensors use the TFT profile's Wire bus at GPIO4/3.
|
||||
-D DISPLAY_CLASS=ST7789LCDDisplay
|
||||
-D DISPLAY_CLASS=HeltecV4Display
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
build_src_filter = ${heltec_v4_tft.build_src_filter}
|
||||
|
||||
@@ -29,7 +29,11 @@ const char meshcore_capability_heltec_v4_tft_kit[] =
|
||||
#endif
|
||||
|
||||
#if defined(P_LORA_SCLK)
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED
|
||||
static SPIClass spi(HSPI); // TFT owns FSPI; never share its reset/remap.
|
||||
#else
|
||||
static SPIClass spi;
|
||||
#endif
|
||||
static SX126xBufferedRadioHal radio_hal(spi);
|
||||
RADIO_CLASS radio = new Module(&radio_hal, P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY);
|
||||
#else
|
||||
@@ -51,6 +55,8 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock);
|
||||
#endif
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
// VEXT is shared with the expansion sensors. Display selection must not
|
||||
// acquire or switch that rail when entering headless/off mode.
|
||||
DISPLAY_CLASS display(NULL);
|
||||
MomentaryButton user_btn(PIN_USER_BTN, 1000, true);
|
||||
#endif
|
||||
@@ -61,7 +67,11 @@ bool radio_init() {
|
||||
(void)*capability;
|
||||
#endif
|
||||
#if defined(MESHCORE_HELTEC_V4_TFT_KIT_COMPAT)
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED
|
||||
{
|
||||
#else
|
||||
if (std::is_same<DISPLAY_CLASS, ST7789LCDDisplay>::value) {
|
||||
#endif
|
||||
const volatile char* capability = meshcore_capability_heltec_v4_tft_kit;
|
||||
(void)*capability;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
|
||||
// GPIO40 and GPIO41 are hard-wired to GNSS WAKE and PPS respectively.
|
||||
// Keep them unavailable even when no GNSS data is detected at startup.
|
||||
#define USER_GPIO_RESERVED_PINS 40, 41
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED && defined(DISPLAY_CLASS)
|
||||
// The stock TFT touch controller is probed on 47/48, with reset on 44.
|
||||
#define USER_GPIO_RESERVED_PINS 40, 41, 44, 47, 48
|
||||
#else
|
||||
#define USER_GPIO_RESERVED_PINS 40, 41
|
||||
#endif
|
||||
|
||||
#define RADIOLIB_STATIC_ONLY 1
|
||||
#include <RadioLib.h>
|
||||
@@ -13,7 +18,9 @@
|
||||
#include <helpers/SensorManager.h>
|
||||
#include <helpers/sensors/EnvironmentSensorManager.h>
|
||||
#ifdef DISPLAY_CLASS
|
||||
#ifdef HELTEC_LORA_V4_OLED
|
||||
#if defined(HELTEC_V4_DISPLAY_UNIFIED) && HELTEC_V4_DISPLAY_UNIFIED
|
||||
#include <helpers/ui/HeltecV4Display.h>
|
||||
#elif defined(HELTEC_LORA_V4_OLED)
|
||||
#include <helpers/ui/SSD1306Display.h>
|
||||
#elif defined(HELTEC_LORA_V4_TFT)
|
||||
#include <helpers/ui/ST7789LCDDisplay.h>
|
||||
|
||||
Reference in new issue
Block a user