Detect manufacturer-supported optional screens on RC32, Tracker V1 and V4 R8
This commit is contained in:
1 parent
3a425db6a9
commit
4f746d1e3b
25 files changed
+1808
-4
No files matched your search
@@ -66,6 +66,28 @@ jobs:
|
||||
- name: Compile optional RAK15002 SD archive (no hardware qualification)
|
||||
run: pio run -e RAK_4631_repeater_rak15002_sd_seeder_only
|
||||
|
||||
optional-display-smoke:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Build Environment
|
||||
uses: ./.github/actions/setup-build-environment
|
||||
- name: Qualify RC32 Full Companion with optional T108 detection
|
||||
run: >-
|
||||
bash build.sh build-firmware heltec_rc32_companion_radio_full
|
||||
--firmware-version v1.18.0.1-halo-keymind-cascade-dev
|
||||
--radio-preset usa-cascade-fixed --profile cascade --require-ota
|
||||
- name: Qualify Wireless Tracker V1 with optional screen detection
|
||||
run: >-
|
||||
bash build.sh build-firmware Heltec_Wireless_Tracker_repeater
|
||||
--firmware-version v1.18.0.1-halo-keymind-cascade-dev
|
||||
--radio-preset usa-cascade-fixed --profile cascade --require-ota
|
||||
- name: Qualify V4 R8 TFT Full Companion with optional screen detection
|
||||
run: >-
|
||||
bash build.sh build-firmware heltec_v4_r8_tft_companion_radio_full
|
||||
--firmware-version v1.18.0.1-halo-keymind-cascade-dev
|
||||
--radio-preset usa-cascade-fixed --profile cascade --require-ota
|
||||
|
||||
esp32-sensor-smoke:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -798,7 +820,9 @@ jobs:
|
||||
python3 -B test/test_firmware_picker_package.py
|
||||
node test/test_firmware_picker.js
|
||||
node test/test_t114_picker.js
|
||||
node test/test_optional_display_picker.js
|
||||
python3 -B test/test_t114_release_profiles.py -v
|
||||
python3 -B test/test_optional_display_profiles.py -v
|
||||
|
||||
- name: Verify Full logging compiler flags and packaged capabilities
|
||||
run: |
|
||||
@@ -933,6 +957,9 @@ jobs:
|
||||
python3 -B test/test_rgb_led_rollover.py -v
|
||||
python3 -B test/test_display_power.py
|
||||
python3 -B test/test_t114_display_detection.py -v
|
||||
python3 -B test/test_rc32_display_detection.py -v
|
||||
python3 -B test/test_wireless_tracker_display_detection.py -v
|
||||
python3 -B test/test_heltec_r8_display_detection.py -v
|
||||
python3 -B test/test_companion_headless_buttons.py -v
|
||||
python3 -B test/test_st7735_display_wake_profile.py -v
|
||||
python3 -B test/test_legacy_ui_version_bounds.py -v
|
||||
|
||||
@@ -3551,6 +3551,14 @@ declare_build_capability_contract() {
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$env_platform" = "ESP32_PLATFORM" ] && ! is_kiss_modem_target "$env_name"; then
|
||||
local optional_display_marker
|
||||
if [ -n "$PIO_CONFIG_JSON" ] \
|
||||
&& optional_display_marker=$(python3 scripts/optional_display_profiles.py "$pio_env_name" <<<"$PIO_CONFIG_JSON"); then
|
||||
record_application_expectation "display.autodetect" "$optional_display_marker"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$env_platform" = "NRF52_PLATFORM" ] && ! is_kiss_modem_target "$env_name"; then
|
||||
if is_t114_display_autodetect_build "$pio_env_name"; then
|
||||
record_application_expectation "display.autodetect" \
|
||||
|
||||
@@ -742,6 +742,22 @@
|
||||
}));
|
||||
}
|
||||
|
||||
function optionalAutoDisplayProfile(profile) {
|
||||
const controls = profile && profile.controls;
|
||||
const target = String(profile && profile.target || "").toLowerCase();
|
||||
const controller = /^heltec_rc32_(?!without_display_)/.test(target) ? "nv3001b"
|
||||
: /^heltec_(?:wireless_tracker|tracker_v1_1)_/.test(target) ? "st7735"
|
||||
: /^heltec_v4_r8_tft_/.test(target) ? "st7789" : "";
|
||||
return Boolean(controller && controls && controls.platform === "ESP32_PLATFORM" &&
|
||||
controls.display === true && controls.displayAuto === true && controls.displayController === controller &&
|
||||
/^[0-9a-f]{40}$/.test(controls.displaySource || "") &&
|
||||
(!profile.controlsSource || controls.displaySource === profile.controlsSource) &&
|
||||
Array.isArray(profile.files) && profile.files.length > 0 && profile.files.every(function (file) {
|
||||
const hash = String(file.name || "").match(/-([0-9a-f]{8,40})(?:-merged)?\.(?:bin|hex|uf2|zip)$/i);
|
||||
return hash && controls.displaySource.startsWith(hash[1].toLowerCase());
|
||||
}));
|
||||
}
|
||||
|
||||
function omitT114DisplayDuplicates(profiles) {
|
||||
const families = new Map();
|
||||
function role(profile) { return profile.role || parseTargetProfile(profile.target).role; }
|
||||
@@ -783,6 +799,9 @@
|
||||
}
|
||||
if (String(value).toLowerCase() === "heltec_t114" && matches.length > 0 &&
|
||||
matches.every(t114AutoDisplayProfile)) return "Heltec T114 (automatic screen detection)";
|
||||
if (field === "hardware" && matches.length > 0 && matches.every(optionalAutoDisplayProfile)) {
|
||||
return (family ? humanizeHardwareVariant(value, family) : humanizeHardware(value)) + " (automatic screen detection)";
|
||||
}
|
||||
return field === "hardware" && family
|
||||
? humanizeHardwareVariant(value, family) : humanizeHardware(value);
|
||||
}
|
||||
@@ -1759,6 +1778,9 @@
|
||||
notes.push("One T114 image detects the supported ST7789 screen at startup and also runs without a screen.");
|
||||
notes.push("Switching from an older without_display image requires a local USB update; do not use a different target's LoRa OTA package. Keep your existing identity and settings; no erase is required.");
|
||||
}
|
||||
if (optionalAutoDisplayProfile(profile)) {
|
||||
notes.push("This image detects the manufacturer's optional screen at startup and also runs without that screen. Power the board off before attaching or removing the display.");
|
||||
}
|
||||
if (identity.hardware === "Station_G3_ESP32" && stationG3SharedSlotProfile(profile)) {
|
||||
notes.push("Automatically selects physical radio slot 1 or 2. Use this same image for either slot.");
|
||||
}
|
||||
@@ -2884,6 +2906,7 @@
|
||||
omitLegacyObserverCompatibilityProfiles,
|
||||
omitStationG3SlotDuplicates: omitStationG3SlotDuplicates,
|
||||
t114AutoDisplayProfile: t114AutoDisplayProfile,
|
||||
optionalAutoDisplayProfile: optionalAutoDisplayProfile,
|
||||
omitT114DisplayDuplicates: omitT114DisplayDuplicates,
|
||||
omitTransportsReplacedByDualCdcFull:
|
||||
omitTransportsReplacedByFull,
|
||||
|
||||
@@ -579,6 +579,9 @@ partition signature.
|
||||
|
||||
## Maintaining the runtime directions
|
||||
|
||||
See [optional screens](optional_screens.md) for manufacturer-confirmed screen
|
||||
options, automatic detection support, and integral-screen exclusions.
|
||||
|
||||
The online picker and downloadable HTML use the same command renderer.
|
||||
Hardware-specific controls are enabled only when `_data/firmware_controls.json`
|
||||
matches the selected release family and exact target. If that metadata is
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
<!-- meshcore-hosted-doc-link:start -->
|
||||
<p class="meshcore-hosted-doc-link"><a href="https://mikecarper.github.io/MeshCore/optional_screens/">View this page on MeshCore Docs</a>.</p>
|
||||
<!-- meshcore-hosted-doc-link:end -->
|
||||
|
||||
# Optional screens
|
||||
|
||||
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.
|
||||
|
||||
The [firmware picker](firmware_picker.md) describes automatic screen detection
|
||||
only when the selected application has been verified to contain it. Previously
|
||||
published firmware retains its original behavior. These additions become
|
||||
available as new firmware is built and qualified; changing the picker does not
|
||||
add detection to an older download.
|
||||
|
||||
## Manufacturer-confirmed optional screens
|
||||
|
||||
| Hardware | Manufacturer evidence | Firmware behavior |
|
||||
| --- | --- | --- |
|
||||
| Heltec T114 | [Heltec](https://heltec.org/project/mesh-node-t114/) offers the optional stock screen. | Automatic ST7789 detection; see the [T114 guide](heltec_t114.md). |
|
||||
| Heltec RC32 with T108 | The [T108](https://heltec.org/project/t108/) is a separate pin-header display module. The [manufacturer example](https://github.com/HelTecAutomation/RadioCore_Library/blob/45f20ff79f66d84022497040fda7cb7d0824639b/examples/NV3001B_Display/README.md) documents its pins and power controls. | New display-capable images detect the NV3001B panel. Legacy `without_display` targets remain headless compatibility builds. |
|
||||
| Heltec Wireless Tracker V1/V1.1 | The [manufacturer listing](https://old.heltec.org/project/wireless-tracker/) explicitly offers Screen Yes/No. | New display-capable images detect the ST7735 panel. Its absence releases only the display's power claim; GPS retains its own claim on the shared supply. |
|
||||
| Heltec V4 R8 Expansion Kit | The [manufacturer kit](https://heltec.org/project/v4-r8-ex/) offers front-panel configurations; its V2 expansion schematic connects the display's separate return line. | New R8 TFT images detect the panel through the documented MISO pin. Existing touch reset and backlight-only sleep/wake behavior are retained. |
|
||||
| Heltec V4/V4 R8 OLED | The [manufacturer listing](https://heltec.org/project/wifi-lora-32-v4/) offers OLED and No Display configurations. | Existing SSD1306 images already check whether the OLED responds. |
|
||||
| RAK WisBlock | The [RAK4631 kit guide](https://downloads.rakwireless.com/Meshtastic/Quick%20Start%20Guide_WisMesh%20RAK4631%20Starter%20Kit.pdf) labels the RAK1921 OLED optional. | Existing display-capable SSD1306 images already check presence. |
|
||||
| iKoka Stick and Handheld | The designer documents an [optional Stick OLED](https://github.com/ndoo/ikoka-stick-meshtastic-device#oled-display-optional) and [optional Handheld OLED](https://github.com/ndoo/ikoka-handheld-meshtastic-device#oled-display-optional). | Existing display-capable images already check presence. Handheld Companion supports the screen; currently headless infrastructure recipes remain headless. |
|
||||
| Original LilyGo T-Beam | [LilyGo](https://lilygo.cc/products/t-beam-v1-1-esp32-lora-module) offers soldered/non-soldered OLED configurations. | Existing display-capable SSD1306 images already check presence. |
|
||||
| XIAO ESP32-S3 with Wio-SX1262 | [Seeed](https://wiki.seeedstudio.com/wio_sx1262_with_xiao_esp32s3_kit/#recommended-accessories) lists the expansion board as an accessory for adding screens and sensors. | Existing display-capable Companion images already check presence. |
|
||||
|
||||
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.
|
||||
|
||||
The R8 readback route is documented in Heltec's
|
||||
[Expansion Kit V2.03 schematic](https://resource.heltec.cn/download/WiFi_LoRa_32_Expansion_Kit-V2/schematic/Expansion_board_V2.03.pdf).
|
||||
Tracker V1 wiring is documented in the
|
||||
[V1.1 schematic](https://resource.heltec.cn/download/Wireless_Tracker/Wireless_Tacker1.1/HTIT-Tracker_V0.5.pdf).
|
||||
|
||||
Screen presence does not establish controller type, dimensions, physical
|
||||
orientation, or board revision. Continue choosing the exact hardware profile.
|
||||
Use USB when changing an old firmware target name; a different target's LoRa
|
||||
update package is not interchangeable. Saved identity and settings do not need
|
||||
to be erased for screen detection.
|
||||
|
||||
## Integral screens
|
||||
|
||||
Manufacturer documentation identifies built-in screens on the
|
||||
[LilyGo T-Echo Card](https://lilygo.cc/products/t-echo-card),
|
||||
[Heltec T190](https://wiki.heltec.org/docs/devices/open-source-hardware/esp32-series/vision-master/vison-master-t190/),
|
||||
[T1](https://heltec.org/project/mesh-node-t1/),
|
||||
[T096](https://heltec.org/project/t096/),
|
||||
[Wireless Tracker V2](https://heltec.org/project/wireless-tracker-v2/),
|
||||
[ThinkNode M9](https://www.elecrow.com/wiki/ThinkNode_M9_MeshCore_Communication_Terminal_with_Full_Keyboard.html)
|
||||
and [T-Deck](https://wiki.lilygo.cc/products/t-deck-series/t-deck/).
|
||||
No new optional-screen detection is enabled for these boards. In particular,
|
||||
the T190's optional module is its LoRa radio, not its screen.
|
||||
|
||||
## Validation
|
||||
|
||||
The new probes run before display SPI initialization and framebuffer
|
||||
allocation, with the backlight off. Bounded repeated reads under opposite
|
||||
pull directions reject floating, stuck and unstable return lines. An absent
|
||||
screen stays disabled until reboot; later drawing or wake requests cannot
|
||||
power it back up. Shared supplies retain other peripherals' ownership.
|
||||
|
||||
Native tests cover controller replies, timing, pin turnaround, power ownership,
|
||||
headless behavior, rendering, sleep/wake, and exclusions for integral screens.
|
||||
Release checks require the detection marker in the uploadable application,
|
||||
the exact resolved board/driver flags and the matching source commit before
|
||||
the picker can advertise detection.
|
||||
|
||||
| Check | Result |
|
||||
| --- | --- |
|
||||
| Native detection, rendering, power and headless regressions | PASS, 31 test methods, including 12 new detection methods with sanitizers and negative controls |
|
||||
| Recipe, release-proof, controls and offline-picker regressions | PASS, 41 test methods |
|
||||
| Resolved PlatformIO configuration | PASS, 31 optional-screen environments across the three hardware families; integral-screen recipes excluded |
|
||||
| Picker JavaScript regressions | PASS, 4 suites |
|
||||
| Complete build-profile selection suite | PASS |
|
||||
| Hosted documentation links | PASS, 7 test methods |
|
||||
| Packaged ESP32 firmware smoke builds | Pending |
|
||||
|
||||
Physical verification on RC32, Tracker V1 and V4 R8 with and without their
|
||||
manufacturer-supported screen has not been performed. Hardware qualification
|
||||
requires cold USB/battery boots, screen sleep/wake, headless input, normal radio
|
||||
transmit/receive, and GPS or touch operation where fitted.
|
||||
@@ -12,7 +12,7 @@ from pathlib import Path
|
||||
from package_esp32_partition_migration import BOARDS
|
||||
from package_cascade_release import nrf52_sensor_profile
|
||||
from station_g3_profiles import runtime_metadata as station_g3_metadata
|
||||
from t114_profiles import runtime_metadata as t114_metadata
|
||||
from optional_display_profiles import runtime_metadata as display_metadata
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
@@ -163,7 +163,7 @@ def generate(stage, config):
|
||||
}
|
||||
controls.update(runtime_metadata(manifest, controls['mqtt'], plan['source']))
|
||||
controls.update(station_g3_metadata(manifest, defines, plan['source']))
|
||||
controls.update(t114_metadata(manifest, defines, plan['source']))
|
||||
controls.update(display_metadata(manifest, defines, plan['source'], env.get('board')))
|
||||
if manifest.get('ota_update_requirements'):
|
||||
controls['updateRequirements'] = manifest['ota_update_requirements']
|
||||
note = capacity_note(manifest.get('reductions', []), defines)
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
"""Bind optional-screen directions to the exact qualified application."""
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
|
||||
from t114_profiles import runtime_metadata as t114_metadata
|
||||
|
||||
CAPABILITY = "display.autodetect"
|
||||
SPECS = (
|
||||
dict(flag="RC32_DISPLAY_AUTODETECT", board="heltec-rc32", guard="HELTEC_RC32",
|
||||
driver="NV3001BDisplay", controller="nv3001b", marker="rc32",
|
||||
targets=r"heltec_rc32_(?!without_display_).+"),
|
||||
dict(flag="WIRELESS_TRACKER_DISPLAY_AUTODETECT", board="esp32-s3-devkitc-1",
|
||||
guard="HELTEC_LORA_V3", driver="ST7735Display", controller="st7735", marker="tracker",
|
||||
targets=r"heltec_wireless_tracker_.+"),
|
||||
dict(flag="WIRELESS_TRACKER_DISPLAY_AUTODETECT", board="heltec_tracker_v1_1",
|
||||
guard="HELTEC_TRACKER_V1_1", driver="ST7735Display", controller="st7735", marker="tracker",
|
||||
targets=r"heltec_tracker_v1_1_.+"),
|
||||
dict(flag="HELTEC_TFT_DISPLAY_AUTODETECT", board="heltec_v4_r8",
|
||||
guard="HELTEC_V4_R8_TFT", driver="ST7789LCDDisplay", controller="st7789", marker="heltec_r8",
|
||||
targets=r"heltec_v4_r8_tft_.+"),
|
||||
)
|
||||
|
||||
|
||||
def marker(spec):
|
||||
return "meshcore.capability.display.autodetect." + spec["marker"] + ".v1"
|
||||
|
||||
|
||||
def spec_for_defines(defines, board=None):
|
||||
for spec in SPECS:
|
||||
if (defines.get(spec["flag"]) == "1"
|
||||
and defines.get(spec["guard"], "0") not in ("0", "false")
|
||||
and defines.get("DISPLAY_CLASS") == spec["driver"]
|
||||
and (board is None or board == spec["board"])):
|
||||
return spec
|
||||
return None
|
||||
|
||||
|
||||
def runtime_metadata(manifest, defines, release_source=None, board=None):
|
||||
if CAPABILITY not in (manifest.get("capabilities") or []):
|
||||
return {}
|
||||
if str(manifest.get("target", "")).lower().startswith("heltec_t114_"):
|
||||
return t114_metadata(manifest, defines, release_source)
|
||||
spec = spec_for_defines(defines, board)
|
||||
if (not spec or board is None or manifest.get("verified") is not True
|
||||
or manifest.get("platform") != "ESP32_PLATFORM"
|
||||
or not re.fullmatch(spec["targets"], str(manifest.get("target", "")).lower())
|
||||
or not any(row.get("capability") == CAPABILITY and row.get("present") is True
|
||||
and row.get("source") == "packaged application"
|
||||
and row.get("evidence") == marker(spec)
|
||||
for row in manifest.get("verification") or [])):
|
||||
raise ValueError("Optional display metadata requires exact board, flags and packaged detection proof")
|
||||
source = manifest.get("source_commit", release_source)
|
||||
if not isinstance(source, str) or not re.fullmatch(r"[0-9a-f]{40}", source):
|
||||
raise ValueError("Optional display metadata requires an exact source commit")
|
||||
if release_source is not None and source != release_source:
|
||||
raise ValueError("Optional display metadata source must match its release")
|
||||
return dict(displayAuto=True, displayController=spec["controller"], displaySource=source)
|
||||
|
||||
|
||||
def resolved_defines(flags):
|
||||
result = {}
|
||||
for flag in flags:
|
||||
match = re.fullmatch(r"-([DU])\s*(\w+)(?:=(.*))?", str(flag).strip())
|
||||
if match:
|
||||
kind, key, value = match.groups()
|
||||
if kind == "U":
|
||||
result.pop(key, None)
|
||||
else:
|
||||
result[key] = "1" if value is None else value
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Configuration resolution is owned by build.sh; this command never runs PIO.
|
||||
envs = {name: dict(options) for name, options in json.load(sys.stdin)}
|
||||
env = envs.get("env:" + sys.argv[1], {})
|
||||
spec = spec_for_defines(resolved_defines(env.get("build_flags", [])), env.get("board"))
|
||||
if not spec:
|
||||
sys.exit(1)
|
||||
print(marker(spec))
|
||||
@@ -0,0 +1,108 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
namespace mesh { namespace ui { namespace heltec_r8 {
|
||||
|
||||
// Expansion Kit V2 routes the LCD's separate SDO to GPIO 45 (LCD_MISO).
|
||||
// This is not the T114's bidirectional SDA interface. Probe before the SPI
|
||||
// peripheral owns these pads, and never use the touch controller as proof of
|
||||
// LCD presence. The LCD/TP reset is shared and must finish HIGH even on failure.
|
||||
static_assert(PIN_TFT_MISO >= 0 && PIN_TFT_RST >= 0,
|
||||
"R8 display detection requires LCD SDO and shared reset");
|
||||
static_assert(SDCARD_CS >= 0 && SDCARD_CS != PIN_TFT_CS
|
||||
&& SDCARD_CS != PIN_TFT_SDA && SDCARD_CS != PIN_TFT_SCL
|
||||
&& SDCARD_CS != PIN_TFT_MISO && SDCARD_CS != PIN_TFT_RST,
|
||||
"R8 LCD probe requires the separate expansion microSD chip select");
|
||||
static_assert(PIN_TFT_MISO != PIN_TFT_SDA && PIN_TFT_MISO != PIN_TFT_SCL
|
||||
&& PIN_TFT_MISO != PIN_TFT_CS && PIN_TFT_MISO != PIN_TFT_DC
|
||||
&& PIN_TFT_MISO != PIN_TFT_RST,
|
||||
"R8 LCD SDO must be a separate input");
|
||||
|
||||
inline void backlightOff() {
|
||||
if (PIN_TFT_LEDA_CTL >= 0) {
|
||||
// Preload the inactive latch before enabling the output to avoid a flash.
|
||||
digitalWrite(PIN_TFT_LEDA_CTL, !PIN_TFT_LEDA_CTL_ACTIVE);
|
||||
pinMode(PIN_TFT_LEDA_CTL, OUTPUT);
|
||||
}
|
||||
}
|
||||
|
||||
inline uint32_t readDisplayId(uint8_t bias) {
|
||||
pinMode(PIN_TFT_MISO, bias);
|
||||
delayMicroseconds(5);
|
||||
digitalWrite(PIN_TFT_CS, LOW);
|
||||
digitalWrite(PIN_TFT_DC, LOW);
|
||||
for (uint8_t mask = 0x80; mask; mask >>= 1) {
|
||||
digitalWrite(PIN_TFT_SDA, (0x04 & mask) ? HIGH : LOW); // RDDID
|
||||
delayMicroseconds(5);
|
||||
digitalWrite(PIN_TFT_SCL, HIGH);
|
||||
delayMicroseconds(5);
|
||||
digitalWrite(PIN_TFT_SCL, LOW);
|
||||
}
|
||||
digitalWrite(PIN_TFT_DC, HIGH);
|
||||
// ST7789 RDDID has one dummy clock followed by three ID bytes on SDO.
|
||||
delayMicroseconds(5);
|
||||
digitalWrite(PIN_TFT_SCL, HIGH);
|
||||
delayMicroseconds(5);
|
||||
digitalWrite(PIN_TFT_SCL, LOW);
|
||||
uint32_t id = 0;
|
||||
for (uint8_t bit = 0; bit < 24; ++bit) {
|
||||
delayMicroseconds(5);
|
||||
id = (id << 1) | (digitalRead(PIN_TFT_MISO) ? 1u : 0u);
|
||||
digitalWrite(PIN_TFT_SCL, HIGH);
|
||||
delayMicroseconds(5);
|
||||
digitalWrite(PIN_TFT_SCL, LOW);
|
||||
}
|
||||
digitalWrite(PIN_TFT_CS, HIGH);
|
||||
return id;
|
||||
}
|
||||
|
||||
inline void releaseBus() {
|
||||
pinMode(PIN_TFT_CS, INPUT);
|
||||
pinMode(PIN_TFT_DC, INPUT);
|
||||
pinMode(PIN_TFT_SDA, INPUT);
|
||||
pinMode(PIN_TFT_SCL, INPUT);
|
||||
pinMode(PIN_TFT_MISO, INPUT);
|
||||
}
|
||||
|
||||
inline bool probe() {
|
||||
backlightOff();
|
||||
// V2.03 shares LCD SCK/MOSI/MISO with microSD; GPIO 3 selects the card.
|
||||
// A warm reset can leave it selected. Deselect before any probe clock and
|
||||
// retain that level when parking the LCD bus, including the failure path.
|
||||
digitalWrite(SDCARD_CS, HIGH);
|
||||
pinMode(SDCARD_CS, OUTPUT);
|
||||
digitalWrite(PIN_TFT_CS, HIGH);
|
||||
pinMode(PIN_TFT_CS, OUTPUT);
|
||||
digitalWrite(PIN_TFT_SCL, LOW);
|
||||
pinMode(PIN_TFT_SCL, OUTPUT);
|
||||
pinMode(PIN_TFT_SDA, OUTPUT);
|
||||
pinMode(PIN_TFT_DC, OUTPUT);
|
||||
digitalWrite(PIN_TFT_RST, HIGH);
|
||||
pinMode(PIN_TFT_RST, OUTPUT);
|
||||
|
||||
for (uint8_t attempt = 0; attempt < 2; ++attempt) {
|
||||
digitalWrite(PIN_TFT_RST, LOW);
|
||||
delay(10);
|
||||
digitalWrite(PIN_TFT_RST, HIGH);
|
||||
delay(120); // Reset/NVM settling, including the sleep-out interval.
|
||||
uint32_t ids[2];
|
||||
bool stable = true;
|
||||
for (uint8_t bias = 0; bias < 2; ++bias) {
|
||||
const uint8_t mode = bias == 0 ? INPUT_PULLUP : INPUT_PULLDOWN;
|
||||
(void)readDisplayId(mode); // Discard the first read after changing bias.
|
||||
ids[bias] = readDisplayId(mode);
|
||||
stable = (readDisplayId(mode) == ids[bias]) && stable;
|
||||
}
|
||||
// IDs are programmable: require stable, actively driven data rather than
|
||||
// guessing one factory ID. Opposite pulls expose a floating absent panel.
|
||||
if (stable && ids[0] == ids[1] && ids[0] != 0 && ids[0] != 0xFFFFFFu) {
|
||||
pinMode(PIN_TFT_MISO, INPUT);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
releaseBus();
|
||||
return false;
|
||||
}
|
||||
|
||||
}}} // namespace mesh::ui::heltec_r8
|
||||
@@ -51,6 +51,13 @@
|
||||
#define DISPLAY_ROTATION 0
|
||||
#endif
|
||||
|
||||
#if defined(HELTEC_RC32) && defined(RC32_DISPLAY_AUTODETECT) && RC32_DISPLAY_AUTODETECT
|
||||
#include "RC32DisplayProbe.h"
|
||||
extern const char meshcore_capability_display_autodetect_rc32[];
|
||||
const char meshcore_capability_display_autodetect_rc32[] =
|
||||
"meshcore.capability.display.autodetect.rc32.v1";
|
||||
#endif
|
||||
|
||||
#define NV3001B_SWRESET 0x01
|
||||
#define NV3001B_SLPOUT 0x11
|
||||
#define NV3001B_DISPON 0x29
|
||||
@@ -364,8 +371,23 @@ void NV3001BDisplay::drawChar(int x, int y, char ch) {
|
||||
bool NV3001BDisplay::begin() {
|
||||
if (is_on) return true;
|
||||
|
||||
#if defined(HELTEC_RC32) && defined(RC32_DISPLAY_AUTODETECT) && RC32_DISPLAY_AUTODETECT
|
||||
if (probe_complete && !panel_present) return false;
|
||||
#endif
|
||||
if (periph_power) periph_power->claim();
|
||||
|
||||
#if defined(HELTEC_RC32) && defined(RC32_DISPLAY_AUTODETECT) && RC32_DISPLAY_AUTODETECT
|
||||
if (!probe_complete) {
|
||||
const volatile char* capability = meshcore_capability_display_autodetect_rc32;
|
||||
(void)*capability; // Retain proof in the executed optional-screen path.
|
||||
probe_complete = true;
|
||||
panel_present = mesh::ui::rc32::probe();
|
||||
if (!panel_present) {
|
||||
if (periph_power) periph_power->release();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
setupOptionalOutput(PIN_TFT_EN, PIN_TFT_EN_ACTIVE);
|
||||
setupOptionalOutput(PIN_TFT_BL, !PIN_TFT_BL_ACTIVE);
|
||||
pinMode(PIN_TFT_CS, OUTPUT);
|
||||
@@ -374,7 +396,17 @@ bool NV3001BDisplay::begin() {
|
||||
digitalWrite(PIN_TFT_DC, HIGH);
|
||||
delay(20);
|
||||
|
||||
#if defined(HELTEC_RC32) && defined(RC32_DISPLAY_AUTODETECT) && RC32_DISPLAY_AUTODETECT
|
||||
if (!spi.begin(PIN_TFT_SCL, PIN_TFT_MISO, PIN_TFT_SDA, PIN_TFT_CS)) {
|
||||
panel_present = false;
|
||||
spi.end();
|
||||
mesh::ui::rc32::powerOff();
|
||||
if (periph_power) periph_power->release();
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
spi.begin(PIN_TFT_SCL, PIN_TFT_MISO, PIN_TFT_SDA, PIN_TFT_CS);
|
||||
#endif
|
||||
if (PIN_TFT_RST >= 0) {
|
||||
pinMode(PIN_TFT_RST, OUTPUT);
|
||||
digitalWrite(PIN_TFT_RST, HIGH);
|
||||
@@ -398,6 +430,9 @@ bool NV3001BDisplay::begin() {
|
||||
}
|
||||
|
||||
void NV3001BDisplay::turnOn() {
|
||||
#if defined(HELTEC_RC32) && defined(RC32_DISPLAY_AUTODETECT) && RC32_DISPLAY_AUTODETECT
|
||||
if (!panel_present) return;
|
||||
#endif
|
||||
begin();
|
||||
}
|
||||
|
||||
@@ -405,7 +440,12 @@ void NV3001BDisplay::turnOff() {
|
||||
if (!is_on) return;
|
||||
|
||||
writeOptionalPin(PIN_TFT_BL, !PIN_TFT_BL_ACTIVE);
|
||||
#if defined(HELTEC_RC32) && defined(RC32_DISPLAY_AUTODETECT) && RC32_DISPLAY_AUTODETECT
|
||||
spi.end();
|
||||
mesh::ui::rc32::powerOff();
|
||||
#else
|
||||
writeOptionalPin(PIN_TFT_EN, !PIN_TFT_EN_ACTIVE);
|
||||
#endif
|
||||
is_on = false;
|
||||
if (periph_power) periph_power->release();
|
||||
}
|
||||
|
||||
@@ -28,6 +28,10 @@ class NV3001BDisplay : public DisplayDriver {
|
||||
SPIClass spi;
|
||||
RefCountedDigitalPin* periph_power;
|
||||
bool is_on = false;
|
||||
#if defined(HELTEC_RC32) && defined(RC32_DISPLAY_AUTODETECT) && RC32_DISPLAY_AUTODETECT
|
||||
bool probe_complete = false;
|
||||
bool panel_present = false;
|
||||
#endif
|
||||
uint16_t color = 0xffff;
|
||||
uint8_t text_size = 1;
|
||||
int cursor_x = 0;
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
namespace mesh { namespace ui { namespace rc32 {
|
||||
|
||||
// Heltec's optional T108 uses NV3001B on the shared, bidirectional SDA pin.
|
||||
// Read commands/ID: meshtastic/firmware src/mesh/NodeDB.cpp, TFT_NV3001B_DETECT.
|
||||
// Pins/power: HelTecAutomation/RadioCore_Library examples/NV3001B_Display.
|
||||
// Probe before claiming the SPI peripheral; no independent MISO is connected.
|
||||
inline uint32_t readIdCommand(uint8_t command, uint8_t bits, uint8_t dummy, uint8_t bias) {
|
||||
digitalWrite(PIN_TFT_CS, LOW);
|
||||
digitalWrite(PIN_TFT_DC, LOW);
|
||||
pinMode(PIN_TFT_SDA, OUTPUT);
|
||||
for (uint8_t mask = 0x80; mask; mask >>= 1) {
|
||||
digitalWrite(PIN_TFT_SDA, (command & mask) ? HIGH : LOW);
|
||||
delayMicroseconds(5);
|
||||
digitalWrite(PIN_TFT_SCL, HIGH);
|
||||
delayMicroseconds(5);
|
||||
// Release after the final command bit is latched, before the panel can
|
||||
// drive its first response bit on the falling edge.
|
||||
if (mask == 1) pinMode(PIN_TFT_SDA, bias);
|
||||
digitalWrite(PIN_TFT_SCL, LOW);
|
||||
}
|
||||
digitalWrite(PIN_TFT_DC, HIGH);
|
||||
for (uint8_t i = 0; i < dummy; ++i) {
|
||||
delayMicroseconds(5);
|
||||
digitalWrite(PIN_TFT_SCL, HIGH);
|
||||
delayMicroseconds(5);
|
||||
digitalWrite(PIN_TFT_SCL, LOW);
|
||||
}
|
||||
uint32_t id = 0;
|
||||
for (uint8_t i = 0; i < bits; ++i) {
|
||||
delayMicroseconds(5);
|
||||
id = (id << 1) | (digitalRead(PIN_TFT_SDA) ? 1u : 0u);
|
||||
digitalWrite(PIN_TFT_SCL, HIGH);
|
||||
delayMicroseconds(5);
|
||||
digitalWrite(PIN_TFT_SCL, LOW);
|
||||
}
|
||||
digitalWrite(PIN_TFT_CS, HIGH);
|
||||
return id;
|
||||
}
|
||||
|
||||
inline uint32_t readId(uint8_t bias, bool individual_bytes) {
|
||||
if (!individual_bytes) return readIdCommand(0x04, 24, 1, bias);
|
||||
const uint32_t first = readIdCommand(0xDA, 8, 0, bias);
|
||||
const uint32_t second = readIdCommand(0xDB, 8, 0, bias);
|
||||
return (first << 16) | (second << 8) | readIdCommand(0xDC, 8, 0, bias);
|
||||
}
|
||||
|
||||
inline bool stableId(bool individual_bytes) {
|
||||
uint32_t ids[2];
|
||||
bool stable = true;
|
||||
for (uint8_t i = 0; i < 2; ++i) {
|
||||
const uint8_t bias = i == 0 ? INPUT_PULLUP : INPUT_PULLDOWN;
|
||||
(void)readId(bias, individual_bytes); // Discard stale data after changing pull.
|
||||
ids[i] = readId(bias, individual_bytes);
|
||||
stable = (readId(bias, individual_bytes) == ids[i]) && stable;
|
||||
}
|
||||
// Recognize this controller, not merely nonzero data from another header
|
||||
// peripheral. Opposing pulls also reject an absent or weakly driven line.
|
||||
return stable && ids[0] == ids[1] && ids[0] == 0x300101;
|
||||
}
|
||||
|
||||
inline void powerOff() {
|
||||
digitalWrite(PIN_TFT_BL, !PIN_TFT_BL_ACTIVE);
|
||||
digitalWrite(PIN_TFT_RST, LOW);
|
||||
// Do not back-power an unpowered panel through GPIO or the SPI peripheral.
|
||||
pinMode(PIN_TFT_CS, INPUT);
|
||||
pinMode(PIN_TFT_DC, INPUT);
|
||||
pinMode(PIN_TFT_SDA, INPUT);
|
||||
pinMode(PIN_TFT_SCL, INPUT);
|
||||
digitalWrite(PIN_TFT_EN, !PIN_TFT_EN_ACTIVE);
|
||||
}
|
||||
|
||||
inline bool probe() {
|
||||
digitalWrite(PIN_TFT_BL, !PIN_TFT_BL_ACTIVE);
|
||||
pinMode(PIN_TFT_BL, OUTPUT);
|
||||
digitalWrite(PIN_TFT_EN, !PIN_TFT_EN_ACTIVE);
|
||||
pinMode(PIN_TFT_EN, OUTPUT);
|
||||
digitalWrite(PIN_TFT_CS, HIGH);
|
||||
pinMode(PIN_TFT_CS, OUTPUT);
|
||||
digitalWrite(PIN_TFT_SCL, LOW);
|
||||
pinMode(PIN_TFT_SCL, OUTPUT);
|
||||
pinMode(PIN_TFT_DC, OUTPUT);
|
||||
digitalWrite(PIN_TFT_RST, LOW);
|
||||
pinMode(PIN_TFT_RST, OUTPUT);
|
||||
digitalWrite(PIN_TFT_EN, PIN_TFT_EN_ACTIVE);
|
||||
delay(120);
|
||||
for (uint8_t attempt = 0; attempt < 2; ++attempt) {
|
||||
digitalWrite(PIN_TFT_RST, LOW);
|
||||
delay(120);
|
||||
digitalWrite(PIN_TFT_RST, HIGH);
|
||||
delay(120);
|
||||
// Some T108 panels answer only the individual ID-byte commands.
|
||||
if (stableId(false) || stableId(true)) return true;
|
||||
}
|
||||
powerOff();
|
||||
return false;
|
||||
}
|
||||
|
||||
}}} // namespace mesh::ui::rc32
|
||||
@@ -5,6 +5,11 @@
|
||||
#endif
|
||||
#include <TFT_eSPI.h>
|
||||
|
||||
#ifdef MESHCORE_TRACKER_DISPLAY_AUTODETECT
|
||||
extern const char meshcore_capability_display_autodetect_tracker[] =
|
||||
"meshcore.capability.display.autodetect.tracker.v1";
|
||||
#endif
|
||||
|
||||
// Most ST7735 recipes keep the original RGB565 canvas. RAM-constrained
|
||||
// targets can retain the complete UI in RGB332 and expand only one scanline
|
||||
// while sending it to the panel; the panel still receives RGB565 pixels.
|
||||
@@ -448,6 +453,10 @@ bool ST7735Display::i2c_probe(TwoWire& wire, uint8_t addr) {
|
||||
#define PIN_TFT_LEDA_CTL_ACTIVE HIGH
|
||||
#endif
|
||||
|
||||
#ifdef MESHCORE_TRACKER_DISPLAY_AUTODETECT
|
||||
#include "WirelessTrackerDisplayProbe.h"
|
||||
#endif
|
||||
|
||||
// Dark colors reduce panel current on the PowerSaving builds.
|
||||
ColorVal UIColor::window_bkg = ST77XX_BLACK;
|
||||
ColorVal UIColor::title_bkg = ST77XX_BLACK;
|
||||
@@ -460,11 +469,34 @@ ColorVal UIColor::popup_txt = ST77XX_WHITE;
|
||||
ColorVal UIColor::corp_blue = ST77XX_BLUE;
|
||||
|
||||
bool ST7735Display::begin() {
|
||||
#ifdef MESHCORE_TRACKER_DISPLAY_AUTODETECT
|
||||
bool probePowerClaimed = false;
|
||||
if (!_probeComplete) {
|
||||
if (_peripher_power) {
|
||||
_peripher_power->claim();
|
||||
probePowerClaimed = true;
|
||||
}
|
||||
delay(100);
|
||||
const volatile char* capability = meshcore_capability_display_autodetect_tracker;
|
||||
(void)*capability;
|
||||
_probeComplete = true;
|
||||
_present = mesh::ui::wireless_tracker::probe();
|
||||
if (!_present) {
|
||||
if (probePowerClaimed) _peripher_power->release();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!_present) return false;
|
||||
#endif
|
||||
if (!sprite) {
|
||||
// alloc offscreen canvas
|
||||
sprite = new TFT_eSprite(&lcd);
|
||||
if (!sprite) {
|
||||
mesh::usbLoggingPort().println("ST7735Display: failed to alloc canvas");
|
||||
#ifdef MESHCORE_TRACKER_DISPLAY_AUTODETECT
|
||||
mesh::ui::wireless_tracker::parkAbsentPanel();
|
||||
if (probePowerClaimed) _peripher_power->release();
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
sprite->setColorDepth(ST7735_BUFFER_COLOR_DEPTH);
|
||||
@@ -473,16 +505,29 @@ bool ST7735Display::begin() {
|
||||
"ST7735Display: failed to alloc canvas pixels");
|
||||
delete sprite;
|
||||
sprite = NULL;
|
||||
#ifdef MESHCORE_TRACKER_DISPLAY_AUTODETECT
|
||||
mesh::ui::wireless_tracker::parkAbsentPanel();
|
||||
if (probePowerClaimed) _peripher_power->release();
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
sprite->fillScreen(ST77XX_BLACK);
|
||||
sprite->setTextColor(curr_color = ST77XX_WHITE);
|
||||
}
|
||||
|
||||
if (!spriteReady()) return false;
|
||||
if (!spriteReady()) {
|
||||
#ifdef MESHCORE_TRACKER_DISPLAY_AUTODETECT
|
||||
if (probePowerClaimed) _peripher_power->release();
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_isOn) {
|
||||
#ifdef MESHCORE_TRACKER_DISPLAY_AUTODETECT
|
||||
if (_peripher_power && !probePowerClaimed) _peripher_power->claim();
|
||||
#else
|
||||
if (_peripher_power) _peripher_power->claim();
|
||||
#endif
|
||||
|
||||
delay(100); // allow the peripheral rail to stabilize
|
||||
pinMode(PIN_TFT_RST, OUTPUT);
|
||||
|
||||
@@ -5,11 +5,23 @@
|
||||
#include <SPI.h>
|
||||
#include <helpers/RefCountedDigitalPin.h>
|
||||
|
||||
// The optional-display hardware is Tracker V1/V1.1. Other boards using this
|
||||
// renderer (including Tracker V2, T096 and T1) keep their existing behavior.
|
||||
#if defined(WIRELESS_TRACKER_DISPLAY_AUTODETECT) && WIRELESS_TRACKER_DISPLAY_AUTODETECT \
|
||||
&& (defined(HELTEC_LORA_V3) || defined(HELTEC_TRACKER_V1_1)) \
|
||||
&& !defined(HELTEC_TRACKER_V2) && !defined(HELTEC_T096) && !defined(HELTEC_T1)
|
||||
#define MESHCORE_TRACKER_DISPLAY_AUTODETECT 1
|
||||
#endif
|
||||
|
||||
class ST7735Display : public DisplayDriver {
|
||||
// One UI coordinate is one pixel on every 160x80 ST7735 panel.
|
||||
static constexpr int kLogicalWidth = 160, kLogicalHeight = 80;
|
||||
bool _isOn;
|
||||
RefCountedDigitalPin* _peripher_power;
|
||||
#ifdef MESHCORE_TRACKER_DISPLAY_AUTODETECT
|
||||
bool _probeComplete = false;
|
||||
bool _present = false;
|
||||
#endif
|
||||
|
||||
bool i2c_probe(TwoWire& wire, uint8_t addr);
|
||||
public:
|
||||
|
||||
@@ -5,6 +5,13 @@
|
||||
#define PIN_TFT_MISO -1
|
||||
#endif
|
||||
|
||||
#ifdef MESHCORE_HELTEC_R8_TFT_DETECTION
|
||||
#include "HeltecR8DisplayProbe.h"
|
||||
extern const char meshcore_capability_display_autodetect_heltec_r8[];
|
||||
const char meshcore_capability_display_autodetect_heltec_r8[] =
|
||||
"meshcore.capability.display.autodetect.heltec_r8.v1";
|
||||
#endif
|
||||
|
||||
#ifndef DISPLAY_ROTATION
|
||||
#define DISPLAY_ROTATION 3
|
||||
#endif
|
||||
@@ -22,7 +29,15 @@ uint8_t ST7789LCDDisplay::effectiveRotation() const {
|
||||
void ST7789LCDDisplay::setFlipped(bool flipped) {
|
||||
if (_flipped == flipped) return;
|
||||
_flipped = flipped;
|
||||
if (_panel_ready) display.setRotation(effectiveRotation());
|
||||
if (_panel_ready) {
|
||||
#ifdef MESHCORE_HELTEC_R8_TFT_DETECTION
|
||||
if (!_isOn) {
|
||||
_rotation_pending = true;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
display.setRotation(effectiveRotation());
|
||||
}
|
||||
}
|
||||
|
||||
bool ST7789LCDDisplay::i2c_probe(TwoWire& wire, uint8_t addr) {
|
||||
@@ -41,6 +56,10 @@ ColorVal UIColor::popup_txt = mesh::ui::color_theme::TEXT;
|
||||
ColorVal UIColor::corp_blue = mesh::ui::color_theme::ACCENT;
|
||||
|
||||
bool ST7789LCDDisplay::begin() {
|
||||
#ifdef MESHCORE_HELTEC_R8_TFT_DETECTION
|
||||
if (_probe_complete && !_present) return false;
|
||||
if (!_probe_complete) mesh::ui::heltec_r8::backlightOff();
|
||||
#endif
|
||||
if (!_isOn) {
|
||||
#ifdef HELTEC_V4_R8_TFT
|
||||
// turnOff() leaves this panel configured and powered - its reset line is
|
||||
@@ -50,6 +69,14 @@ bool ST7789LCDDisplay::begin() {
|
||||
// and stalling the UI loop for ~500 ms of reset delays.
|
||||
if (_panel_ready) {
|
||||
if (_peripher_power) _peripher_power->claim();
|
||||
#ifdef MESHCORE_HELTEC_R8_TFT_DETECTION
|
||||
// A preference may change while the backlight is off. Apply only that
|
||||
// deferred change; ordinary wakes remain free of SPI/reset work.
|
||||
if (_rotation_pending) {
|
||||
display.setRotation(effectiveRotation());
|
||||
_rotation_pending = false;
|
||||
}
|
||||
#endif
|
||||
if (PIN_TFT_LEDA_CTL != -1) {
|
||||
digitalWrite(PIN_TFT_LEDA_CTL, PIN_TFT_LEDA_CTL_ACTIVE);
|
||||
}
|
||||
@@ -65,6 +92,21 @@ bool ST7789LCDDisplay::begin() {
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef MESHCORE_HELTEC_R8_TFT_DETECTION
|
||||
if (!_probe_complete) {
|
||||
const volatile char* capability = meshcore_capability_display_autodetect_heltec_r8;
|
||||
(void)*capability; // Keep the marker bound to the executed probe path.
|
||||
_probe_complete = true;
|
||||
_present = mesh::ui::heltec_r8::probe();
|
||||
if (!_present) {
|
||||
// Release exactly this display's claim. VEXT also powers the radio
|
||||
// and other peripherals; never turn the shared rail off directly.
|
||||
if (_peripher_power) _peripher_power->release();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (PIN_TFT_LEDA_CTL != -1) {
|
||||
pinMode(PIN_TFT_LEDA_CTL, OUTPUT);
|
||||
#ifdef HELTEC_V4_R8_TFT
|
||||
@@ -78,6 +120,18 @@ 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
|
||||
// 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();
|
||||
_present = false; // Latch failure just like an absent LCD.
|
||||
mesh::ui::heltec_r8::backlightOff();
|
||||
mesh::ui::heltec_r8::releaseBus();
|
||||
if (_peripher_power) _peripher_power->release();
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
display.init(DISPLAY_WIDTH, DISPLAY_HEIGHT);
|
||||
display.setRotation(effectiveRotation());
|
||||
@@ -137,10 +191,16 @@ void ST7789LCDDisplay::turnOff() {
|
||||
}
|
||||
|
||||
void ST7789LCDDisplay::clear() {
|
||||
#ifdef MESHCORE_HELTEC_R8_TFT_DETECTION
|
||||
if (!_isOn) return;
|
||||
#endif
|
||||
display.fillScreen(ST77XX_BLACK);
|
||||
}
|
||||
|
||||
void ST7789LCDDisplay::startFrame(ColorVal bkg) {
|
||||
#ifdef MESHCORE_HELTEC_R8_TFT_DETECTION
|
||||
if (!_isOn) return;
|
||||
#endif
|
||||
display.fillScreen(bkg);
|
||||
display.setTextColor(_color = UIColor::primary_txt);
|
||||
display.setTextSize(1);
|
||||
@@ -148,35 +208,59 @@ void ST7789LCDDisplay::startFrame(ColorVal bkg) {
|
||||
}
|
||||
|
||||
void ST7789LCDDisplay::setTextSize(int sz) {
|
||||
#ifdef MESHCORE_HELTEC_R8_TFT_DETECTION
|
||||
if (!_isOn) return;
|
||||
#endif
|
||||
display.setTextSize(sz > 0 ? sz : 1);
|
||||
}
|
||||
|
||||
void ST7789LCDDisplay::setColor(ColorVal c) {
|
||||
#ifdef MESHCORE_HELTEC_R8_TFT_DETECTION
|
||||
if (!_isOn) return;
|
||||
#endif
|
||||
display.setTextColor(_color = c);
|
||||
}
|
||||
|
||||
void ST7789LCDDisplay::setCursor(int x, int y) {
|
||||
#ifdef MESHCORE_HELTEC_R8_TFT_DETECTION
|
||||
if (!_isOn) return;
|
||||
#endif
|
||||
display.setCursor(x, y);
|
||||
}
|
||||
|
||||
void ST7789LCDDisplay::print(const char* str) {
|
||||
#ifdef MESHCORE_HELTEC_R8_TFT_DETECTION
|
||||
if (!_isOn) return;
|
||||
#endif
|
||||
display.print(str);
|
||||
}
|
||||
|
||||
void ST7789LCDDisplay::fillRect(int x, int y, int w, int h) {
|
||||
#ifdef MESHCORE_HELTEC_R8_TFT_DETECTION
|
||||
if (!_isOn) return;
|
||||
#endif
|
||||
display.fillRect(x, y, w, h, _color);
|
||||
}
|
||||
|
||||
void ST7789LCDDisplay::drawRect(int x, int y, int w, int h) {
|
||||
#ifdef MESHCORE_HELTEC_R8_TFT_DETECTION
|
||||
if (!_isOn) return;
|
||||
#endif
|
||||
display.drawRect(x, y, w, h, _color);
|
||||
}
|
||||
|
||||
void ST7789LCDDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) {
|
||||
#ifdef MESHCORE_HELTEC_R8_TFT_DETECTION
|
||||
if (!_isOn) return;
|
||||
#endif
|
||||
// The common UI bitmaps are MSB-first, as expected by drawBitmap.
|
||||
display.drawBitmap(x, y, bits, w, h, _color);
|
||||
}
|
||||
|
||||
uint16_t ST7789LCDDisplay::getTextWidth(const char* str) {
|
||||
#ifdef MESHCORE_HELTEC_R8_TFT_DETECTION
|
||||
if (!_isOn) return 0;
|
||||
#endif
|
||||
int16_t x1, y1;
|
||||
uint16_t w, h;
|
||||
display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h);
|
||||
|
||||
@@ -11,6 +11,11 @@
|
||||
#define DISPLAY_ROTATION 3
|
||||
#endif
|
||||
|
||||
#if defined(HELTEC_V4_R8_TFT) && defined(HELTEC_TFT_DISPLAY_AUTODETECT) \
|
||||
&& HELTEC_TFT_DISPLAY_AUTODETECT == 1
|
||||
#define MESHCORE_HELTEC_R8_TFT_DETECTION 1
|
||||
#endif
|
||||
|
||||
class ST7789LCDDisplay : public DisplayDriver {
|
||||
#if defined(LILYGO_TDECK) || defined(HELTEC_LORA_V4_TFT) || defined(HELTEC_V4_R8_TFT)
|
||||
SPIClass displaySPI;
|
||||
@@ -18,6 +23,11 @@ class ST7789LCDDisplay : public DisplayDriver {
|
||||
Adafruit_ST7789 display;
|
||||
bool _isOn;
|
||||
bool _panel_ready = false; // panel has been configured at least once
|
||||
#ifdef MESHCORE_HELTEC_R8_TFT_DETECTION
|
||||
bool _probe_complete = false;
|
||||
bool _present = false;
|
||||
bool _rotation_pending = false;
|
||||
#endif
|
||||
bool _flipped = false;
|
||||
uint16_t _color;
|
||||
RefCountedDigitalPin* _peripher_power;
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
namespace mesh { namespace ui { namespace wireless_tracker {
|
||||
|
||||
// Wireless Tracker V1's optional ST7735 panel is connected directly to SDA
|
||||
// (GPIO42), not radio MISO. The ST7735 serial read protocol returns data on
|
||||
// that same pin: release it before the last command clock's falling edge.
|
||||
// The caller owns a reference to the shared TFT/GNSS peripheral power rail.
|
||||
inline uint32_t readDisplayId(uint8_t bias) {
|
||||
digitalWrite(PIN_TFT_CS, LOW);
|
||||
digitalWrite(PIN_TFT_DC, LOW);
|
||||
pinMode(PIN_TFT_SDA, OUTPUT);
|
||||
for (uint8_t mask = 0x80; mask; mask >>= 1) {
|
||||
digitalWrite(PIN_TFT_SDA, (0x04 & mask) ? HIGH : LOW);
|
||||
delayMicroseconds(5);
|
||||
digitalWrite(PIN_TFT_SCL, HIGH);
|
||||
delayMicroseconds(5);
|
||||
if (mask == 1) pinMode(PIN_TFT_SDA, bias);
|
||||
digitalWrite(PIN_TFT_SCL, LOW);
|
||||
}
|
||||
digitalWrite(PIN_TFT_DC, HIGH);
|
||||
// RDDID returns 24 bits after one turnaround/dummy clock.
|
||||
delayMicroseconds(5);
|
||||
digitalWrite(PIN_TFT_SCL, HIGH);
|
||||
delayMicroseconds(5);
|
||||
digitalWrite(PIN_TFT_SCL, LOW);
|
||||
uint32_t id = 0;
|
||||
for (uint8_t bit = 0; bit < 24; ++bit) {
|
||||
delayMicroseconds(5);
|
||||
id = (id << 1) | (digitalRead(PIN_TFT_SDA) ? 1u : 0u);
|
||||
digitalWrite(PIN_TFT_SCL, HIGH);
|
||||
delayMicroseconds(5);
|
||||
digitalWrite(PIN_TFT_SCL, LOW);
|
||||
}
|
||||
digitalWrite(PIN_TFT_CS, HIGH);
|
||||
return id;
|
||||
}
|
||||
|
||||
inline void parkAbsentPanel() {
|
||||
digitalWrite(PIN_TFT_LEDA_CTL, !PIN_TFT_LEDA_CTL_ACTIVE);
|
||||
digitalWrite(PIN_TFT_RST, LOW);
|
||||
pinMode(PIN_TFT_CS, INPUT);
|
||||
pinMode(PIN_TFT_DC, INPUT);
|
||||
pinMode(PIN_TFT_SDA, INPUT);
|
||||
pinMode(PIN_TFT_SCL, INPUT);
|
||||
// Do not drive Vext: the caller releases only the display's power claim,
|
||||
// allowing GNSS or another peripheral to retain its independent claim.
|
||||
}
|
||||
|
||||
inline bool probe() {
|
||||
digitalWrite(PIN_TFT_LEDA_CTL, !PIN_TFT_LEDA_CTL_ACTIVE);
|
||||
pinMode(PIN_TFT_LEDA_CTL, OUTPUT);
|
||||
digitalWrite(PIN_TFT_CS, HIGH);
|
||||
pinMode(PIN_TFT_CS, OUTPUT);
|
||||
digitalWrite(PIN_TFT_SCL, LOW);
|
||||
pinMode(PIN_TFT_SCL, OUTPUT);
|
||||
pinMode(PIN_TFT_DC, OUTPUT);
|
||||
digitalWrite(PIN_TFT_RST, LOW);
|
||||
pinMode(PIN_TFT_RST, OUTPUT);
|
||||
|
||||
for (uint8_t attempt = 0; attempt < 2; ++attempt) {
|
||||
digitalWrite(PIN_TFT_RST, LOW);
|
||||
delay(10);
|
||||
digitalWrite(PIN_TFT_RST, HIGH);
|
||||
delay(120);
|
||||
uint32_t ids[2];
|
||||
bool stable = true;
|
||||
for (uint8_t bias = 0; bias < 2; ++bias) {
|
||||
const uint8_t mode = bias == 0 ? INPUT_PULLUP : INPUT_PULLDOWN;
|
||||
(void)readDisplayId(mode);
|
||||
ids[bias] = readDisplayId(mode);
|
||||
stable = (readDisplayId(mode) == ids[bias]) && stable;
|
||||
}
|
||||
// IDs are programmable; require stable driven data under opposite pulls.
|
||||
if (stable && ids[0] == ids[1] && ids[0] != 0 && ids[0] != 0xFFFFFFu) {
|
||||
pinMode(PIN_TFT_SDA, INPUT);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
parkAbsentPanel();
|
||||
return false;
|
||||
}
|
||||
|
||||
}}} // namespace mesh::ui::wireless_tracker
|
||||
@@ -0,0 +1,313 @@
|
||||
"""Run the real R8 LCD driver/probe against recording GPIO and panel libraries."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
UI = ROOT / "src/helpers/ui"
|
||||
|
||||
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 INPUT_PULLUP 2
|
||||
#define INPUT_PULLDOWN 3
|
||||
#define HSPI 1
|
||||
#define PIN_TFT_CS 47
|
||||
#define PIN_TFT_DC 48
|
||||
#define PIN_TFT_SCL 16
|
||||
#define PIN_TFT_SDA 15
|
||||
#ifndef PIN_TFT_MISO
|
||||
#define PIN_TFT_MISO 45
|
||||
#endif
|
||||
#define PIN_TFT_RST 21
|
||||
#define PIN_TFT_LEDA_CTL 44
|
||||
#define PIN_TFT_LEDA_CTL_ACTIVE HIGH
|
||||
#define SDCARD_CS 3
|
||||
inline int modes[64] = {}, levels[64] = {};
|
||||
inline uint64_t now_us=0, reset_low_at=0, reset_high_at=0, reset_low_us=0;
|
||||
inline unsigned events=0, transactions=0, reads=0, resets=0, spi_begins=0, init_calls=0, draws=0;
|
||||
inline unsigned spi_ends=0;
|
||||
inline unsigned rotations=0, last_rotation=0;
|
||||
inline unsigned command_bits=0, clocks=0, data_bits=0, command=0;
|
||||
inline uint32_t response=0;
|
||||
inline bool transaction=false, initialized=false;
|
||||
inline std::string scenario;
|
||||
inline bool automatic() {
|
||||
#if defined(HELTEC_V4_R8_TFT) && defined(HELTEC_TFT_DISPLAY_AUTODETECT) && HELTEC_TFT_DISPLAY_AUTODETECT == 1
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
inline void check_pin(int pin) {
|
||||
assert(pin==PIN_TFT_CS || pin==PIN_TFT_DC || pin==PIN_TFT_SCL || pin==PIN_TFT_SDA
|
||||
|| pin==PIN_TFT_MISO || pin==PIN_TFT_RST || pin==PIN_TFT_LEDA_CTL || pin==40 || pin==SDCARD_CS);
|
||||
}
|
||||
inline void pinMode(int pin,int mode) {
|
||||
check_pin(pin); ++events;
|
||||
if (pin==PIN_TFT_MISO) assert(mode!=OUTPUT && "LCD SDO must never be driven");
|
||||
if (pin==PIN_TFT_LEDA_CTL && automatic() && !initialized)
|
||||
assert(levels[pin]==LOW && "preload inactive backlight before enabling output");
|
||||
if (pin==SDCARD_CS && mode==OUTPUT && automatic())
|
||||
assert(levels[pin]==HIGH && "preload microSD deselect before enabling output");
|
||||
modes[pin]=mode;
|
||||
}
|
||||
inline void digitalWrite(int pin,int value) {
|
||||
check_pin(pin); ++events;
|
||||
if (pin==PIN_TFT_LEDA_CTL && automatic() && value==HIGH)
|
||||
assert(initialized && "backlight must remain dark until LCD init completes");
|
||||
if (pin==PIN_TFT_RST) {
|
||||
if (value==LOW && levels[pin]==HIGH) reset_low_at=now_us;
|
||||
if (value==HIGH && levels[pin]==LOW) {
|
||||
reset_low_us=now_us-reset_low_at; reset_high_at=now_us; ++resets;
|
||||
}
|
||||
}
|
||||
if (pin==PIN_TFT_CS && value==LOW && levels[pin]==HIGH && automatic() && spi_begins==0) {
|
||||
assert(now_us-reset_high_at>=120000 && "wait for reset/NVM before RDDID");
|
||||
assert(reset_low_us>=10000 && levels[PIN_TFT_LEDA_CTL]==LOW);
|
||||
assert(levels[40]==HIGH && "shared peripheral rail must be claimed");
|
||||
transaction=true; ++transactions; command_bits=clocks=data_bits=command=0;
|
||||
const unsigned within=(transactions-1)%6;
|
||||
response=0x858552;
|
||||
if (scenario=="floating") response=modes[PIN_TFT_MISO]==INPUT_PULLUP ? 0xFFFFFF : 0;
|
||||
if (scenario=="high") response=0xFFFFFF;
|
||||
if (scenario=="low") response=0;
|
||||
if (scenario=="biased") response=modes[PIN_TFT_MISO]==INPUT_PULLUP ? 0x858552 : 0x123456;
|
||||
if (scenario=="noise") response=within==2 ? 0x858553 : 0x858552;
|
||||
if (scenario=="recover" && transactions<=6) response=0;
|
||||
if (scenario=="prime" && within%3==0) response=0;
|
||||
if (scenario=="programmable") response=0x123456;
|
||||
}
|
||||
if (pin==PIN_TFT_SCL && value==HIGH && transaction) {
|
||||
assert(levels[SDCARD_CS]==HIGH && modes[SDCARD_CS]==OUTPUT && "deselect shared microSD before LCD clocks");
|
||||
++clocks;
|
||||
if (clocks<=8) {
|
||||
assert(levels[PIN_TFT_DC]==LOW && modes[PIN_TFT_SDA]==OUTPUT);
|
||||
command=(command<<1)|levels[PIN_TFT_SDA]; ++command_bits;
|
||||
}
|
||||
}
|
||||
if (pin==PIN_TFT_CS && value==HIGH && transaction) {
|
||||
assert(command==0x04 && command_bits==8 && data_bits==24 && clocks==33);
|
||||
transaction=false;
|
||||
}
|
||||
levels[pin]=value;
|
||||
}
|
||||
inline int digitalRead(int pin) {
|
||||
assert(pin==PIN_TFT_MISO && "read dedicated LCD SDO, never MOSI/touch/I2C");
|
||||
assert(transaction && command==0x04 && levels[PIN_TFT_DC]==HIGH);
|
||||
assert(modes[pin]==((transactions-1)%6<3 ? INPUT_PULLUP : INPUT_PULLDOWN));
|
||||
assert(clocks==9+data_bits && levels[PIN_TFT_SCL]==LOW && data_bits<24);
|
||||
++reads; return (response>>(23-data_bits++))&1;
|
||||
}
|
||||
inline void delay(unsigned ms) { now_us+=uint64_t(ms)*1000; }
|
||||
inline void delayMicroseconds(unsigned us) { now_us+=us; }
|
||||
'''
|
||||
|
||||
SPI = r'''
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
struct SPIClass {
|
||||
bool live=false;
|
||||
explicit SPIClass(int=0) {}
|
||||
#ifdef TEST_BOOL_SPI_BEGIN
|
||||
bool begin(int clk,int miso,int mosi,int cs) {
|
||||
#else
|
||||
void begin(int clk,int miso,int mosi,int cs) {
|
||||
#endif
|
||||
assert(clk==PIN_TFT_SCL && miso==PIN_TFT_MISO && mosi==PIN_TFT_SDA && cs==PIN_TFT_CS);
|
||||
if (automatic()) assert(transactions>=6 && levels[PIN_TFT_LEDA_CTL]==LOW);
|
||||
++spi_begins;
|
||||
live=scenario!="spi-fail";
|
||||
#ifdef TEST_BOOL_SPI_BEGIN
|
||||
return live;
|
||||
#endif
|
||||
}
|
||||
void* bus() { return live ? this : nullptr; }
|
||||
void end() { ++spi_ends; live=false; }
|
||||
};
|
||||
inline SPIClass SPI;
|
||||
'''
|
||||
|
||||
PANEL = r'''
|
||||
#pragma once
|
||||
#include <SPI.h>
|
||||
#define ST77XX_BLACK 0
|
||||
#define ST77XX_WHITE 0xffff
|
||||
class Adafruit_ST7789 {
|
||||
int _w=240, _h=320;
|
||||
public:
|
||||
template<class... T> explicit Adafruit_ST7789(T...) {}
|
||||
void init(int w,int h) {
|
||||
if (automatic()) assert(spi_begins==1 && transactions>=6 && levels[PIN_TFT_LEDA_CTL]==LOW);
|
||||
++init_calls; initialized=true; _w=w; _h=h;
|
||||
}
|
||||
void setRotation(int rotation) { assert(initialized); _w=rotation&1 ? 320 : 240; _h=rotation&1 ? 240 : 320; ++draws; ++rotations; last_rotation=rotation; }
|
||||
int width() const { return _w; }
|
||||
int height() const { return _h; }
|
||||
void setSPISpeed(double) { assert(initialized); }
|
||||
void fillScreen(uint16_t) { assert(initialized); ++draws; }
|
||||
void setTextColor(uint16_t) { assert(initialized); ++draws; }
|
||||
void setTextSize(int) { assert(initialized); ++draws; }
|
||||
void setTextWrap(bool) { assert(initialized); ++draws; }
|
||||
void cp437(bool) { assert(initialized); ++draws; }
|
||||
void setCursor(int,int) { assert(initialized); ++draws; }
|
||||
void print(const char*) { assert(initialized); ++draws; }
|
||||
void fillRect(int,int,int,int,uint16_t) { assert(initialized); ++draws; }
|
||||
void drawRect(int,int,int,int,uint16_t) { assert(initialized); ++draws; }
|
||||
void drawBitmap(int,int,const uint8_t*,int,int,uint16_t) { assert(initialized); ++draws; }
|
||||
void getTextBounds(const char* s,int,int,int16_t* x,int16_t* y,uint16_t* w,uint16_t* h) {
|
||||
assert(initialized); ++draws; *x=*y=0; *w=strlen(s)*6; *h=8;
|
||||
}
|
||||
};
|
||||
'''
|
||||
|
||||
MAIN = r'''
|
||||
#include "ST7789LCDDisplay.h"
|
||||
void frame(ST7789LCDDisplay& d) {
|
||||
const uint8_t bits[]={0x80};
|
||||
d.clear(); d.startFrame(); d.setTextSize(2); d.setColor(1); d.setCursor(0,0);
|
||||
d.print("text"); d.fillRect(0,0,1,1); d.drawRect(0,0,1,1);
|
||||
d.drawXbm(0,0,bits,1,1); d.endFrame(); (void)d.getTextWidth("text");
|
||||
}
|
||||
int main(int argc,char** argv) {
|
||||
assert(argc==2); scenario=argv[1];
|
||||
RefCountedDigitalPin rail(40); rail.begin();
|
||||
const bool other_owner=scenario!="unshared";
|
||||
if (other_owner) rail.claim();
|
||||
ST7789LCDDisplay d(&rail);
|
||||
if (!automatic()) {
|
||||
assert(d.begin() && d.isOn()); frame(d);
|
||||
assert(reads==0 && transactions==0 && init_calls==1);
|
||||
return 0;
|
||||
}
|
||||
const auto before=events; frame(d); d.setFlipped(true);
|
||||
assert(events==before && draws==0 && init_calls==0);
|
||||
// Simulate a warm reset after a card transaction left DAT3/CS asserted.
|
||||
if (scenario=="sd-selected") { modes[SDCARD_CS]=OUTPUT; levels[SDCARD_CS]=LOW; }
|
||||
const bool absent=scenario=="floating" || scenario=="high" || scenario=="low"
|
||||
|| scenario=="noise" || scenario=="biased" || scenario=="unshared";
|
||||
if (scenario=="unshared") scenario="floating";
|
||||
const bool begun=d.begin();
|
||||
if (absent || scenario=="spi-fail") {
|
||||
assert(!begun && !d.isOn() && "invalid LCD must remain headless");
|
||||
assert(init_calls==0 && spi_begins==(scenario=="spi-fail" ? 1u : 0u) && draws==0);
|
||||
assert(spi_ends==(scenario=="spi-fail" ? 1u : 0u));
|
||||
assert(levels[PIN_TFT_RST]==HIGH && "shared touch reset must not remain LOW");
|
||||
assert(levels[PIN_TFT_LEDA_CTL]==LOW && levels[40]==other_owner);
|
||||
for (int pin:{PIN_TFT_CS,PIN_TFT_DC,PIN_TFT_SDA,PIN_TFT_SCL,PIN_TFT_MISO}) assert(modes[pin]==INPUT);
|
||||
const auto count=events; frame(d); d.turnOn(); d.turnOff(); d.setFlipped(false);
|
||||
assert(!d.begin() && events==count && draws==0 && reads==24*transactions);
|
||||
assert(transactions==(scenario=="spi-fail" ? 6u : 12u) && now_us<450000);
|
||||
assert(levels[SDCARD_CS]==HIGH && modes[SDCARD_CS]==OUTPUT);
|
||||
if (other_owner) { rail.release(); assert(levels[40]==LOW && "probe must not leak its power claim"); }
|
||||
return 0;
|
||||
}
|
||||
assert(begun && d.isOn() && init_calls==1 && spi_begins==1 && initialized);
|
||||
assert(last_rotation==((DISPLAY_ROTATION+2)&3));
|
||||
assert(transactions==(scenario=="recover" ? 12u : 6u));
|
||||
assert(reads==24*transactions && levels[PIN_TFT_LEDA_CTL]==HIGH);
|
||||
assert(levels[SDCARD_CS]==HIGH && modes[SDCARD_CS]==OUTPUT);
|
||||
frame(d); const auto painted=draws, reset_count=resets, probes=transactions;
|
||||
assert(d.begin() && init_calls==1 && transactions==probes);
|
||||
d.turnOff(); assert(!d.isOn() && levels[PIN_TFT_RST]==HIGH && levels[PIN_TFT_LEDA_CTL]==LOW);
|
||||
assert(levels[40]==HIGH && "display off must retain the other peripheral claim");
|
||||
frame(d); d.setFlipped(false); assert(draws==painted);
|
||||
for (unsigned i=0;i<100;++i) { d.turnOn(); assert(d.isOn()); d.turnOff(); }
|
||||
assert(resets==reset_count && init_calls==1 && spi_begins==1 && transactions==probes);
|
||||
assert(rotations==2 && last_rotation==(DISPLAY_ROTATION&3) && "apply deferred flip once on wake");
|
||||
d.turnOn(); frame(d); assert(draws>painted);
|
||||
d.turnOff(); rail.release(); assert(levels[40]==LOW && "wake/sleep claims must remain balanced");
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class HeltecR8DisplayDetectionTest(unittest.TestCase):
|
||||
def build(self, directory, flags=(), mutation=None, expect_failure=False):
|
||||
directory.mkdir()
|
||||
for name in ("ST7789LCDDisplay.h", "ST7789LCDDisplay.cpp", "HeltecR8DisplayProbe.h"):
|
||||
source = (UI / name).read_text()
|
||||
if name == "HeltecR8DisplayProbe.h" and mutation:
|
||||
old, new = mutation
|
||||
self.assertIn(old, source)
|
||||
source = source.replace(old, new)
|
||||
(directory / name).write_text(source)
|
||||
for name, source in {"Arduino.h": ARDUINO, "SPI.h": SPI,
|
||||
"Wire.h": "#pragma once\nclass TwoWire {};\n",
|
||||
"Adafruit_GFX.h": "#pragma once\n",
|
||||
"Adafruit_ST7789.h": PANEL, "main.cpp": MAIN}.items():
|
||||
(directory / name).write_text(source)
|
||||
binary = directory / "probe"
|
||||
sanitizer = (["-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie"]
|
||||
if sys.platform.startswith("linux") else [])
|
||||
compiled = subprocess.run([
|
||||
os.environ.get("CXX", "g++"), "-std=c++17", "-Wall", "-Wextra", "-Werror",
|
||||
"-Wno-unused-parameter", *sanitizer, *flags,
|
||||
"-I" + str(directory), "-I" + str(UI), "-I" + str(ROOT / "src"),
|
||||
str(directory / "main.cpp"), str(directory / "ST7789LCDDisplay.cpp"), "-o", str(binary)
|
||||
], capture_output=True, text=True, timeout=60)
|
||||
if expect_failure:
|
||||
self.assertNotEqual(compiled.returncode, 0)
|
||||
self.assertIn("R8 display detection requires LCD SDO", compiled.stderr)
|
||||
else:
|
||||
self.assertEqual(compiled.returncode, 0, compiled.stdout + compiled.stderr)
|
||||
return binary
|
||||
|
||||
def run_case(self, binary, scenario, negative=None):
|
||||
ran = subprocess.run([str(binary), scenario], capture_output=True, text=True, timeout=10)
|
||||
if negative:
|
||||
self.assertNotEqual(ran.returncode, 0)
|
||||
self.assertIn(negative, ran.stderr)
|
||||
else:
|
||||
self.assertEqual(ran.returncode, 0, ran.stdout + ran.stderr)
|
||||
|
||||
def test_absent_floating_stuck_unstable_and_power_ownership(self):
|
||||
with tempfile.TemporaryDirectory(prefix="heltec-r8-display-") as temp:
|
||||
binary = self.build(Path(temp) / "auto", ["-DHELTEC_V4_R8_TFT", "-DHELTEC_TFT_DISPLAY_AUTODETECT=1"])
|
||||
self.assertIn(b"meshcore.capability.display.autodetect.heltec_r8.v1", binary.read_bytes())
|
||||
for scenario in ("floating", "high", "low", "noise", "biased", "unshared", "spi-fail"):
|
||||
with self.subTest(scenario=scenario): self.run_case(binary, scenario)
|
||||
|
||||
def test_panel_ids_retry_and_shared_touch_reset_sleep_wake(self):
|
||||
with tempfile.TemporaryDirectory(prefix="heltec-r8-display-") as temp:
|
||||
for api in ("void", "bool"):
|
||||
flags = ["-DHELTEC_V4_R8_TFT", "-DHELTEC_TFT_DISPLAY_AUTODETECT=1"]
|
||||
if api == "bool": flags.append("-DTEST_BOOL_SPI_BEGIN")
|
||||
binary = self.build(Path(temp) / api, flags)
|
||||
for scenario in ("valid", "programmable", "prime", "recover", "sd-selected", "spi-fail"):
|
||||
with self.subTest(api=api, scenario=scenario): self.run_case(binary, scenario)
|
||||
|
||||
def test_other_boards_and_explicitly_disabled_r8_keep_existing_behavior(self):
|
||||
with tempfile.TemporaryDirectory(prefix="heltec-r8-display-") as temp:
|
||||
for index, flags in enumerate(([], ["-DHELTEC_V4_R8_TFT"],
|
||||
["-DHELTEC_V4_R8_TFT", "-DHELTEC_TFT_DISPLAY_AUTODETECT=0"],
|
||||
["-DHELTEC_LORA_V4_TFT", "-DHELTEC_TFT_DISPLAY_AUTODETECT=1"],
|
||||
["-DLILYGO_TDECK", "-DHELTEC_TFT_DISPLAY_AUTODETECT=1"])):
|
||||
binary = self.build(Path(temp) / str(index), flags)
|
||||
self.run_case(binary, "valid")
|
||||
self.assertNotIn(b"meshcore.capability.display.autodetect.heltec_r8.v1", binary.read_bytes())
|
||||
|
||||
def test_negative_controls_detect_bias_settle_and_wrong_read_pin(self):
|
||||
flags = ["-DHELTEC_V4_R8_TFT", "-DHELTEC_TFT_DISPLAY_AUTODETECT=1"]
|
||||
with tempfile.TemporaryDirectory(prefix="heltec-r8-display-") as temp:
|
||||
cases = [("bias", ("ids[0] == ids[1]", "true"), "biased", "invalid LCD must remain headless"),
|
||||
("settle", ("delay(120);", "delay(0);"), "valid", "wait for reset/NVM before RDDID"),
|
||||
("pin", ("digitalRead(PIN_TFT_MISO)", "digitalRead(PIN_TFT_SDA)"), "valid", "read dedicated LCD SDO"),
|
||||
("sd", (" digitalWrite(SDCARD_CS, HIGH);\n pinMode(SDCARD_CS, OUTPUT);", ""), "sd-selected", "deselect shared microSD before LCD clocks")]
|
||||
for name, mutation, scenario, message in cases:
|
||||
binary = self.build(Path(temp) / name, flags, mutation)
|
||||
self.run_case(binary, scenario, negative=message)
|
||||
self.build(Path(temp) / "no-miso", flags + ["-DPIN_TFT_MISO=-1"], expect_failure=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,35 @@
|
||||
"use strict";
|
||||
const assert = require("assert");
|
||||
const picker = require("../docs/_javascript/firmware_picker.js");
|
||||
const source = "a".repeat(40);
|
||||
for (const [target, controller] of [
|
||||
["heltec_rc32_companion_radio_full", "nv3001b"],
|
||||
["Heltec_Wireless_Tracker_repeater", "st7735"],
|
||||
["heltec_tracker_v1_1_repeater_observer_mqtt", "st7735"],
|
||||
["heltec_v4_r8_tft_portrait_repeater_observer_mqtt", "st7789"]
|
||||
]) {
|
||||
const profile = {...picker.parseTargetProfile(target), target,
|
||||
controls: {platform: "ESP32_PLATFORM", display: true, displayAuto: true,
|
||||
displayController: controller, displaySource: source},
|
||||
files: [".bin", "-merged.bin"].map(suffix => ({name: target + "-v1.18.0.1-dev-aaaaaaaa" + suffix}))};
|
||||
assert(picker.optionalAutoDisplayProfile(profile), target);
|
||||
assert(picker.boardNotes(profile).some(note => note.includes("optional screen")), target);
|
||||
assert(picker.hardwareChoiceLabel("hardware", profile.hardware, [profile]).includes("automatic screen detection"));
|
||||
for (const change of [{display: false}, {displayAuto: false}, {displayController: "unknown"},
|
||||
{displaySource: source.slice(0, 8)}, {displaySource: "b".repeat(40)}, {platform: "NRF52_PLATFORM"}]) {
|
||||
const old = {...profile, controls: {...profile.controls, ...change}};
|
||||
assert(!picker.optionalAutoDisplayProfile(old));
|
||||
assert(!picker.boardNotes(old).some(note => note.includes("optional screen")));
|
||||
}
|
||||
const mixed = {...profile, files: [...profile.files, {name: target + "-v1.18.0.1-dev-bbbbbbbb.bin"}]};
|
||||
assert(!picker.optionalAutoDisplayProfile(mixed), "every download must match the detection source");
|
||||
assert(!picker.optionalAutoDisplayProfile({...profile, controlsSource: "b".repeat(40)}),
|
||||
"the detection source must match attached metadata too");
|
||||
}
|
||||
for (const target of ["heltec_rc32_without_display_repeater", "heltec_tracker_v2_repeater",
|
||||
"Heltec_t096_repeater", "Heltec_T190_repeater_", "LilyGo_T-Echo_Card_repeater", "heltec_v4_tft_repeater"]) {
|
||||
assert(!picker.optionalAutoDisplayProfile({target, controls: {platform: "ESP32_PLATFORM",
|
||||
display: true, displayAuto: true, displayController: "st7789", displaySource: source},
|
||||
files: [{name: target + "-v1.18.0.1-dev-aaaaaaaa.bin"}]}));
|
||||
}
|
||||
console.log("Optional display picker directions require the exact supported board, controller and application source");
|
||||
@@ -0,0 +1,125 @@
|
||||
"""Optional-screen defaults and exact packaged detection evidence."""
|
||||
from copy import deepcopy
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
import optional_display_profiles as displays
|
||||
import generate_picker_controls as generator
|
||||
from test_upstream_observer_recipes import Recipes
|
||||
from test_t114_autodisplay_recipes import defines
|
||||
|
||||
SOURCE = "a" * 40
|
||||
TARGETS = ("heltec_rc32_repeater", "Heltec_Wireless_Tracker_repeater",
|
||||
"heltec_tracker_v1_1_repeater_observer_mqtt", "heltec_v4_r8_tft_repeater")
|
||||
|
||||
|
||||
def manifest(spec, target):
|
||||
return dict(target=target, platform="ESP32_PLATFORM", verified=True, source_commit=SOURCE,
|
||||
capabilities=[displays.CAPABILITY], verification=[dict(capability=displays.CAPABILITY,
|
||||
present=True, source="packaged application", evidence=displays.marker(spec))])
|
||||
|
||||
|
||||
class OptionalDisplayProfilesTest(unittest.TestCase):
|
||||
def test_optional_hardware_defaults_and_integral_screens_are_separate(self):
|
||||
recipes = Recipes()
|
||||
for spec, target in zip(displays.SPECS, TARGETS):
|
||||
with self.subTest(target=target):
|
||||
flags = defines(recipes.value("env:" + target, "build_flags"))
|
||||
self.assertEqual(displays.spec_for_defines(flags, recipes.value("env:" + target, "board")), spec)
|
||||
if spec["marker"] == "heltec_r8":
|
||||
self.assertEqual(flags["SDCARD_CS"], "3")
|
||||
for target in ("Heltec_T190_repeater_", "LilyGo_T-Echo_Card_repeater",
|
||||
"Heltec_t096_repeater", "Heltec_t1_repeater", "heltec_tracker_v2_repeater",
|
||||
"ThinkNode_M9_repeater_", "heltec_v4_repeater", "heltec_v4_tft_repeater",
|
||||
"heltec_rc32_without_display_repeater", "heltec_rc32_kiss_modem"):
|
||||
with self.subTest(target=target):
|
||||
flags = defines(recipes.value("env:" + target, "build_flags"))
|
||||
self.assertIsNone(displays.spec_for_defines(flags))
|
||||
|
||||
def test_exact_flags_application_marker_controller_and_source_are_required(self):
|
||||
recipes = Recipes()
|
||||
for spec, target in zip(displays.SPECS, TARGETS):
|
||||
flags = defines(recipes.value("env:" + target, "build_flags"))
|
||||
row = manifest(spec, target)
|
||||
with self.subTest(target=target):
|
||||
self.assertEqual(displays.runtime_metadata(row, flags, board=spec["board"]), dict(displayAuto=True,
|
||||
displayController=spec["controller"], displaySource=SOURCE))
|
||||
old = {**row, "capabilities": []}
|
||||
self.assertEqual(displays.runtime_metadata(old, flags), {})
|
||||
for changed in ({spec["flag"]: "0"}, {spec["guard"]: "0"}, {"DISPLAY_CLASS": "NullDisplayDriver"}):
|
||||
with self.assertRaises(ValueError):
|
||||
displays.runtime_metadata(row, {**flags, **changed}, board=spec["board"])
|
||||
for changed in ({"verified": False}, {"platform": "NRF52_PLATFORM"},
|
||||
{"source_commit": SOURCE[:8]}, {"verification": []}, {"target": "unrelated_repeater"}):
|
||||
with self.assertRaises(ValueError):
|
||||
displays.runtime_metadata({**row, **changed}, flags, board=spec["board"])
|
||||
for key, value in (("present", False), ("source", "linked image"), ("evidence", "unrelated")):
|
||||
bad = deepcopy(row)
|
||||
bad["verification"][0][key] = value
|
||||
with self.assertRaises(ValueError):
|
||||
displays.runtime_metadata(bad, flags, board=spec["board"])
|
||||
for board in (None, "different_board"):
|
||||
with self.assertRaises(ValueError):
|
||||
displays.runtime_metadata(row, flags, board=board)
|
||||
with self.assertRaisesRegex(ValueError, "source must match"):
|
||||
displays.runtime_metadata(row, flags, "b" * 40, board=spec["board"])
|
||||
|
||||
def test_builder_uses_ordered_flags_and_exact_board_without_starting_platformio(self):
|
||||
spec = displays.SPECS[0]
|
||||
flags = ["-D HELTEC_RC32", "-DDISPLAY_CLASS=NV3001BDisplay", "-D RC32_DISPLAY_AUTODETECT=1"]
|
||||
for board, extra, expected in ((spec["board"], [], 0), (spec["board"], ["-U RC32_DISPLAY_AUTODETECT"], 1),
|
||||
(spec["board"], ["-D RC32_DISPLAY_AUTODETECT=0"], 1), ("other_board", [], 1)):
|
||||
config = [["env:sample", [["board", board], ["build_flags", flags + extra]]]]
|
||||
result = subprocess.run([sys.executable, str(ROOT / "scripts/optional_display_profiles.py"), "sample"],
|
||||
input=json.dumps(config), text=True, capture_output=True, check=False)
|
||||
self.assertEqual(result.returncode, expected, result.stderr)
|
||||
if not expected:
|
||||
self.assertEqual(result.stdout.strip(), displays.marker(spec))
|
||||
|
||||
def test_real_generator_binds_each_optional_board_to_uploadable_files(self):
|
||||
recipes = Recipes()
|
||||
for spec, target in zip(displays.SPECS, TARGETS):
|
||||
with self.subTest(target=target), tempfile.TemporaryDirectory() as folder:
|
||||
stage = Path(folder)
|
||||
(stage / "companion").mkdir()
|
||||
family = "v1.18.0.1-dev-aaaaaaaa"
|
||||
(stage / "release-plan.json").write_text(json.dumps(dict(source=SOURCE,
|
||||
groups=[dict(key="companion", tag=family)])))
|
||||
row = manifest(spec, target)
|
||||
row.update(platformio_env="sample", files=[target + "-" + family + suffix for suffix in (".bin", "-merged.bin")])
|
||||
(stage / "companion/TARGET-MANIFEST.json").write_text(json.dumps([row]))
|
||||
flags = defines(recipes.value("env:" + target, "build_flags"))
|
||||
result = generator.generate(stage, [["env:sample", [["board", spec["board"]],
|
||||
["build_flags", ["-D" + k + "=" + v for k, v in flags.items()]]]]])
|
||||
self.assertEqual(result["profiles"][target]["displaySource"], SOURCE)
|
||||
self.assertEqual(result["profiles"][target]["displayController"], spec["controller"])
|
||||
self.assertTrue(result["profiles"][target]["displayAuto"])
|
||||
|
||||
def test_elf_marker_alone_cannot_qualify_the_downloaded_esp32_application(self):
|
||||
spec = displays.SPECS[0]
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
root = Path(folder)
|
||||
elf, app, output = root / "firmware.elf", root / "firmware.bin", root / "capabilities.json"
|
||||
elf.write_bytes(displays.marker(spec).encode())
|
||||
command = [sys.executable, "-B", str(ROOT / "scripts/check_firmware_capabilities.py"),
|
||||
"--image", str(elf), "--firmware-bin", str(app), "--output", str(output),
|
||||
"--target", TARGETS[0], "--platform", "ESP32_PLATFORM", "--build-profile", "full",
|
||||
"--expect-application", displays.CAPABILITY + "=" + displays.marker(spec)]
|
||||
app.write_bytes(b"old display firmware without detection")
|
||||
result = subprocess.run(command, capture_output=True, text=True, check=False)
|
||||
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
|
||||
self.assertFalse(json.loads(output.read_text())["verified"])
|
||||
app.write_bytes(b"prefix\0" + displays.marker(spec).encode() + b"\0suffix")
|
||||
result = subprocess.run(command, capture_output=True, text=True, check=False)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertTrue(json.loads(output.read_text())["verified"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,311 @@
|
||||
"""Exercise the RC32 optional T108 probe and actual NV3001B driver lifecycle."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
UI = ROOT / "src/helpers/ui"
|
||||
|
||||
ARDUINO = r'''
|
||||
#pragma once
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <initializer_list>
|
||||
#define LOW 0
|
||||
#define HIGH 1
|
||||
#define INPUT 0
|
||||
#define OUTPUT 1
|
||||
#define INPUT_PULLUP 2
|
||||
#define INPUT_PULLDOWN 3
|
||||
#define PROGMEM
|
||||
#define pgm_read_byte(p) (*(p))
|
||||
#define PIN_TFT_SCL 17
|
||||
#define PIN_TFT_SDA 38
|
||||
#define PIN_TFT_CS 39
|
||||
#define PIN_TFT_DC 16
|
||||
#define PIN_TFT_RST 4
|
||||
#define PIN_TFT_EN 6
|
||||
#define PIN_TFT_BL 5
|
||||
#ifndef PIN_TFT_EN_ACTIVE
|
||||
#define PIN_TFT_EN_ACTIVE LOW
|
||||
#endif
|
||||
#ifndef PIN_TFT_BL_ACTIVE
|
||||
#define PIN_TFT_BL_ACTIVE HIGH
|
||||
#endif
|
||||
inline int modes[48] = {}, levels[48] = {};
|
||||
inline uint64_t now_us = 0, reset_high_at = 0, reset_low_at = 0, reset_low_us = 0;
|
||||
inline unsigned gpio_events = 0, reads = 0, transactions = 0, spi_begins = 0, spi_ends = 0;
|
||||
inline unsigned spi_transfers = 0, pixel_bytes = 0, reset_count = 0;
|
||||
inline unsigned clock_edges = 0, command_bits = 0, data_bits = 0;
|
||||
inline unsigned per_command[2][4] = {};
|
||||
inline uint8_t command_value = 0, spi_command = 0;
|
||||
inline bool transaction = false, spi_active = false, init_done = false, rail_on = false;
|
||||
inline const char* scenario = "valid";
|
||||
inline uint32_t read_value = 0;
|
||||
inline bool is(const char* value) { return strcmp(scenario, value) == 0; }
|
||||
inline bool automatic() {
|
||||
#if defined(HELTEC_RC32) && defined(RC32_DISPLAY_AUTODETECT) && RC32_DISPLAY_AUTODETECT
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
inline void panelPin(int pin) {
|
||||
assert((pin == PIN_TFT_CS || pin == PIN_TFT_DC || pin == PIN_TFT_SDA || pin == PIN_TFT_SCL
|
||||
|| pin == PIN_TFT_RST || pin == PIN_TFT_EN || pin == PIN_TFT_BL || pin == 30)
|
||||
&& "touch only panel pins and explicitly shared power");
|
||||
}
|
||||
inline int commandIndex() {
|
||||
if (command_value == 0x04) return 0;
|
||||
if (command_value == 0xDA) return 1;
|
||||
if (command_value == 0xDB) return 2;
|
||||
assert(command_value == 0xDC); return 3;
|
||||
}
|
||||
inline unsigned responseBits() { return command_value == 0x04 ? 24 : 8; }
|
||||
inline void pinMode(int pin, int mode) {
|
||||
panelPin(pin); ++gpio_events; modes[pin] = mode;
|
||||
if (pin == PIN_TFT_SDA && (mode == INPUT_PULLUP || mode == INPUT_PULLDOWN)) {
|
||||
assert(transaction && command_bits == 8);
|
||||
const unsigned occurrence = per_command[mode == INPUT_PULLUP ? 0 : 1][commandIndex()]++;
|
||||
uint32_t id = 0x300101;
|
||||
if (is("wrong-id")) id = 0x123456;
|
||||
if (is("biased") && mode == INPUT_PULLDOWN) id = 0x300102;
|
||||
if (is("noise") && occurrence % 3 == 2) id = 0x300102;
|
||||
if (is("prime") && occurrence % 3 == 0) id = 0;
|
||||
if (is("recover") && reset_count == 1) id = 0;
|
||||
if ((is("fallback") || is("bad-fallback")) && command_value == 0x04) id = 0;
|
||||
if (is("bad-fallback") && command_value == 0xDC) id = 0x300102;
|
||||
if (is("floating")) id = mode == INPUT_PULLUP ? 0xFFFFFF : 0;
|
||||
if (is("stuck-high")) id = 0xFFFFFF;
|
||||
if (is("stuck-low")) id = 0;
|
||||
if (command_value == 0x04) read_value = id;
|
||||
else read_value = (id >> ((3-commandIndex())*8)) & 0xFF;
|
||||
}
|
||||
}
|
||||
inline void digitalWrite(int pin, int value) {
|
||||
panelPin(pin); ++gpio_events;
|
||||
if (automatic() && pin == PIN_TFT_BL && value == PIN_TFT_BL_ACTIVE)
|
||||
assert(init_done && pixel_bytes >= 220u*128u*2u && "backlight only after initialization and clear");
|
||||
if (pin == PIN_TFT_EN) {
|
||||
if (automatic() && rail_on && value != PIN_TFT_EN_ACTIVE) {
|
||||
assert(!spi_active);
|
||||
for (int p : {PIN_TFT_CS, PIN_TFT_DC, PIN_TFT_SDA, PIN_TFT_SCL})
|
||||
assert(modes[p] == INPUT && "park bus before removing panel power");
|
||||
}
|
||||
rail_on = value == PIN_TFT_EN_ACTIVE;
|
||||
}
|
||||
if (pin == PIN_TFT_RST && value == LOW) reset_low_at = now_us;
|
||||
if (pin == PIN_TFT_RST && value == HIGH && levels[pin] == LOW) {
|
||||
reset_low_us = now_us-reset_low_at; reset_high_at = now_us; ++reset_count;
|
||||
}
|
||||
if (pin == PIN_TFT_CS && value == LOW && levels[pin] == HIGH && !spi_active) {
|
||||
assert(automatic());
|
||||
assert(rail_on && levels[PIN_TFT_BL] != PIN_TFT_BL_ACTIVE);
|
||||
assert(now_us-reset_high_at >= 120000 && "probe reset settle");
|
||||
assert(reset_low_us >= 120000 && "probe reset pulse");
|
||||
transaction = true; ++transactions;
|
||||
clock_edges = command_bits = data_bits = 0; command_value = 0;
|
||||
}
|
||||
if (pin == PIN_TFT_SCL && value == HIGH && transaction) {
|
||||
++clock_edges;
|
||||
if (modes[PIN_TFT_SDA] == OUTPUT) {
|
||||
assert(levels[PIN_TFT_DC] == LOW && command_bits < 8);
|
||||
command_value = uint8_t((command_value << 1) | levels[PIN_TFT_SDA]);
|
||||
++command_bits;
|
||||
}
|
||||
}
|
||||
if (pin == PIN_TFT_SCL && value == LOW && transaction && clock_edges == 8)
|
||||
assert(modes[PIN_TFT_SDA] != OUTPUT && "release SDA before command falling edge");
|
||||
if (pin == PIN_TFT_CS && value == HIGH && transaction) {
|
||||
assert(command_bits == 8 && data_bits == responseBits());
|
||||
assert(clock_edges == 8+responseBits()+(command_value == 0x04 ? 1 : 0));
|
||||
transaction = false;
|
||||
}
|
||||
levels[pin] = value;
|
||||
}
|
||||
inline int digitalRead(int pin) {
|
||||
assert(pin == PIN_TFT_SDA && transaction && levels[PIN_TFT_DC] == HIGH);
|
||||
assert(modes[pin] == INPUT_PULLUP || modes[pin] == INPUT_PULLDOWN);
|
||||
assert(clock_edges == 8+data_bits+(command_value == 0x04 ? 1 : 0));
|
||||
assert(data_bits < responseBits() && levels[PIN_TFT_SCL] == LOW && "sample after turnaround falling edge");
|
||||
++reads;
|
||||
return (read_value >> (responseBits()-1-data_bits++)) & 1;
|
||||
}
|
||||
inline void delay(unsigned ms) { now_us += uint64_t(ms)*1000; }
|
||||
inline void delayMicroseconds(unsigned us) { now_us += us; }
|
||||
'''
|
||||
|
||||
SPI = r'''
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
#define HSPI 1
|
||||
#define MSBFIRST 1
|
||||
#define SPI_MODE0 0
|
||||
struct SPISettings { SPISettings(int, int, int) {} };
|
||||
struct SPIClass {
|
||||
explicit SPIClass(int) {}
|
||||
bool begin(int sck, int miso, int mosi, int cs) {
|
||||
assert(sck == PIN_TFT_SCL && miso == -1 && mosi == PIN_TFT_SDA && cs == PIN_TFT_CS);
|
||||
++spi_begins; spi_active = true; init_done = false; pixel_bytes = 0;
|
||||
modes[sck] = modes[mosi] = OUTPUT;
|
||||
return !is("init-fail") && !(is("wake-fail") && spi_begins == 2);
|
||||
}
|
||||
void end() { assert(spi_active); ++spi_ends; spi_active = false; }
|
||||
void beginTransaction(SPISettings) { assert(spi_active); }
|
||||
void endTransaction() { assert(spi_active); }
|
||||
uint8_t transfer(uint8_t data) {
|
||||
assert(spi_active && rail_on);
|
||||
assert(levels[PIN_TFT_CS] == LOW); ++spi_transfers;
|
||||
if (levels[PIN_TFT_DC] == LOW) {
|
||||
assert(now_us-reset_high_at >= 120000);
|
||||
spi_command = data;
|
||||
if (data == 0x29) init_done = true;
|
||||
} else if (spi_command == 0x2C) {
|
||||
assert(init_done); ++pixel_bytes;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
'''
|
||||
|
||||
MAIN = r'''
|
||||
#include "NV3001BDisplay.h"
|
||||
void frame(NV3001BDisplay& d) {
|
||||
const uint8_t bits[] = {0x80};
|
||||
d.clear(); d.startFrame(); d.setTextSize(1); d.setColor(1); d.setCursor(0,0);
|
||||
d.print("a"); d.printWordWrap("b",10); d.fillRect(0,0,1,1); d.drawRect(0,0,1,1);
|
||||
d.drawXbm(0,0,bits,1,1); d.endFrame(); assert(d.getTextWidth("a") == 6);
|
||||
}
|
||||
int main(int argc, char** argv) {
|
||||
assert(argc == 2); scenario = argv[1];
|
||||
RefCountedDigitalPin power(30, HIGH); power.begin();
|
||||
NV3001BDisplay d(&power);
|
||||
if (automatic()) {
|
||||
const auto events = gpio_events; d.turnOn(); frame(d); d.turnOff();
|
||||
assert(gpio_events == events && spi_begins == 0 && spi_transfers == 0);
|
||||
}
|
||||
const bool absent = is("floating") || is("stuck-high") || is("stuck-low")
|
||||
|| is("noise") || is("biased") || is("wrong-id") || is("bad-fallback");
|
||||
const bool begun = d.begin();
|
||||
if (automatic() && is("init-fail")) {
|
||||
assert(!begun && !d.isOn() && spi_begins == 1 && spi_transfers == 0 && spi_ends == 1);
|
||||
assert(transactions == 6 && levels[30] == LOW && !rail_on);
|
||||
assert(levels[PIN_TFT_BL] != PIN_TFT_BL_ACTIVE && levels[PIN_TFT_RST] == LOW);
|
||||
const auto events=gpio_events; d.turnOn(); d.turnOff(); frame(d); assert(!d.begin());
|
||||
assert(gpio_events == events);
|
||||
return 0;
|
||||
}
|
||||
if (automatic() && absent) {
|
||||
assert(!begun && !d.isOn() && "invalid panel must remain headless");
|
||||
assert(spi_begins == 0 && spi_transfers == 0 && spi_ends == 0);
|
||||
assert(transactions == 48 && reads == 576 && now_us < 800000);
|
||||
assert(levels[PIN_TFT_EN] != PIN_TFT_EN_ACTIVE && levels[PIN_TFT_RST] == LOW);
|
||||
assert(levels[PIN_TFT_BL] != PIN_TFT_BL_ACTIVE && levels[30] == LOW);
|
||||
for (int p : {PIN_TFT_CS, PIN_TFT_DC, PIN_TFT_SDA, PIN_TFT_SCL}) assert(modes[p] == INPUT);
|
||||
const auto events=gpio_events; d.turnOn(); d.turnOff(); frame(d); assert(!d.begin());
|
||||
assert(events == gpio_events && spi_begins == 0 && transactions == 48);
|
||||
return 0;
|
||||
}
|
||||
assert(begun && d.isOn() && init_done && spi_begins == 1);
|
||||
assert(levels[PIN_TFT_BL] == PIN_TFT_BL_ACTIVE && levels[30] == HIGH);
|
||||
if (automatic()) assert(transactions == (is("fallback") ? 24u : is("recover") ? 30u : 6u));
|
||||
else assert(transactions == 0 && reads == 0);
|
||||
frame(d); const auto count=transactions, tx=spi_transfers;
|
||||
assert(d.begin() && spi_begins == 1 && spi_transfers == tx);
|
||||
d.turnOff(); assert(!d.isOn() && levels[PIN_TFT_EN] != PIN_TFT_EN_ACTIVE && levels[30] == LOW);
|
||||
frame(d); assert(spi_transfers == tx);
|
||||
if (automatic()) {
|
||||
assert(spi_ends == 1 && !spi_active);
|
||||
for (int p : {PIN_TFT_CS, PIN_TFT_DC, PIN_TFT_SDA, PIN_TFT_SCL}) assert(modes[p] == INPUT);
|
||||
}
|
||||
d.turnOn();
|
||||
if (automatic() && is("wake-fail")) {
|
||||
assert(!d.isOn() && spi_begins == 2 && spi_ends == 2 && transactions == count);
|
||||
assert(!rail_on && levels[30] == LOW && levels[PIN_TFT_RST] == LOW);
|
||||
const auto events=gpio_events; d.turnOn(); d.turnOff(); frame(d); assert(!d.begin());
|
||||
assert(gpio_events == events && spi_transfers == tx);
|
||||
return 0;
|
||||
}
|
||||
assert(d.isOn() && spi_begins == 2 && transactions == count);
|
||||
assert(levels[PIN_TFT_BL] == PIN_TFT_BL_ACTIVE && levels[30] == HIGH);
|
||||
frame(d); assert(spi_transfers > tx);
|
||||
d.turnOff(); d.turnOff(); assert(levels[30] == LOW);
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class RC32DisplayDetectionTest(unittest.TestCase):
|
||||
def build(self, work, flags=(), *, old_bias=False, old_turnaround=False):
|
||||
work.mkdir()
|
||||
for filename in ("NV3001BDisplay.cpp", "NV3001BDisplay.h", "RC32DisplayProbe.h"):
|
||||
source = (UI / filename).read_text()
|
||||
if old_bias and filename == "RC32DisplayProbe.h":
|
||||
mutated = source.replace("ids[0] == ids[1]", "true")
|
||||
self.assertNotEqual(mutated, source)
|
||||
source = mutated
|
||||
if old_turnaround and filename == "RC32DisplayProbe.h":
|
||||
mutated = source.replace("if (mask == 1) pinMode(PIN_TFT_SDA, bias);", "")
|
||||
self.assertNotEqual(mutated, source)
|
||||
source = mutated.replace(" digitalWrite(PIN_TFT_DC, HIGH);",
|
||||
" pinMode(PIN_TFT_SDA, bias);\n digitalWrite(PIN_TFT_DC, HIGH);", 1)
|
||||
(work / filename).write_text(source)
|
||||
for filename, source in {"Arduino.h": ARDUINO, "SPI.h": SPI, "main.cpp": MAIN}.items():
|
||||
(work / filename).write_text(source)
|
||||
sanitizer = (["-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie"]
|
||||
if sys.platform.startswith("linux") else [])
|
||||
binary = work / "test"
|
||||
compiled = subprocess.run([
|
||||
os.environ.get("CXX", "g++"), "-std=c++17", "-Wall", "-Wextra", "-Werror",
|
||||
"-Wno-unused-parameter", "-Wno-type-limits", *sanitizer, *flags,
|
||||
"-I"+str(work), "-I"+str(UI), "-I"+str(ROOT / "src"),
|
||||
str(work / "main.cpp"), str(work / "NV3001BDisplay.cpp"), "-o", str(binary)
|
||||
], capture_output=True, text=True, timeout=60)
|
||||
self.assertEqual(compiled.returncode, 0, compiled.stdout + compiled.stderr)
|
||||
return binary
|
||||
|
||||
def run_case(self, binary, scenario, *, negative=None):
|
||||
ran = subprocess.run([str(binary), scenario], capture_output=True, text=True, timeout=10)
|
||||
if negative:
|
||||
self.assertNotEqual(ran.returncode, 0)
|
||||
self.assertIn(negative, ran.stderr)
|
||||
else:
|
||||
self.assertEqual(ran.returncode, 0, ran.stdout + ran.stderr)
|
||||
|
||||
def test_absent_unstable_wrong_controller_stays_headless(self):
|
||||
with tempfile.TemporaryDirectory(prefix="rc32-display-") as directory:
|
||||
binary = self.build(Path(directory)/"auto", ["-DHELTEC_RC32", "-DRC32_DISPLAY_AUTODETECT=1"])
|
||||
self.assertIn(b"meshcore.capability.display.autodetect.rc32.v1", binary.read_bytes())
|
||||
for scenario in ("floating", "stuck-high", "stuck-low", "noise", "biased", "wrong-id", "bad-fallback", "init-fail"):
|
||||
with self.subTest(scenario=scenario): self.run_case(binary, scenario)
|
||||
|
||||
def test_both_id_protocols_recovery_and_sleep_wake_preserve_screen(self):
|
||||
with tempfile.TemporaryDirectory(prefix="rc32-display-") as directory:
|
||||
for index, active in enumerate(([], ["-DPIN_TFT_EN_ACTIVE=HIGH", "-DPIN_TFT_BL_ACTIVE=LOW"])):
|
||||
flags = ["-DHELTEC_RC32", "-DRC32_DISPLAY_AUTODETECT=1", *active]
|
||||
binary = self.build(Path(directory)/str(index), flags)
|
||||
for scenario in ("valid", "prime", "fallback", "recover", "wake-fail"):
|
||||
with self.subTest(active=active, scenario=scenario): self.run_case(binary, scenario)
|
||||
|
||||
def test_legacy_and_other_nv3001b_boards_do_not_probe(self):
|
||||
with tempfile.TemporaryDirectory(prefix="rc32-display-") as directory:
|
||||
for index, flags in enumerate((["-DHELTEC_RC32"], ["-DHELTEC_RC32", "-DRC32_DISPLAY_AUTODETECT=0"],
|
||||
["-DRC32_DISPLAY_AUTODETECT=1"])):
|
||||
binary = self.build(Path(directory)/str(index), flags)
|
||||
self.run_case(binary, "valid")
|
||||
self.assertNotIn(b"meshcore.capability.display.autodetect.rc32.v1", binary.read_bytes())
|
||||
|
||||
def test_negative_controls_detect_bias_and_sda_turnaround_regressions(self):
|
||||
with tempfile.TemporaryDirectory(prefix="rc32-display-") as directory:
|
||||
flags = ["-DHELTEC_RC32", "-DRC32_DISPLAY_AUTODETECT=1"]
|
||||
binary = self.build(Path(directory)/"bias", flags, old_bias=True)
|
||||
self.run_case(binary, "biased", negative="invalid panel must remain headless")
|
||||
binary = self.build(Path(directory)/"turnaround", flags, old_turnaround=True)
|
||||
self.run_case(binary, "valid", negative="release SDA before command falling edge")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,294 @@
|
||||
"""Exercise optional Tracker V1 display reads and shared-rail driver lifecycle."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from cpp_source import body
|
||||
from test_serial_wifi_sessions import SANITIZER_FLAGS
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
UI = ROOT / "src/helpers/ui"
|
||||
|
||||
ARDUINO = r'''
|
||||
#pragma once
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#define LOW 0
|
||||
#define HIGH 1
|
||||
#define INPUT 0
|
||||
#define OUTPUT 1
|
||||
#define INPUT_PULLUP 2
|
||||
#define INPUT_PULLDOWN 3
|
||||
#define PIN_TFT_SCL 41
|
||||
#define PIN_TFT_SDA 42
|
||||
#define PIN_TFT_RST 39
|
||||
#define PIN_TFT_LEDA_CTL 21
|
||||
#define PIN_TFT_CS 38
|
||||
#define PIN_TFT_DC 40
|
||||
#ifndef PIN_TFT_LEDA_CTL_ACTIVE
|
||||
#define PIN_TFT_LEDA_CTL_ACTIVE HIGH
|
||||
#endif
|
||||
inline int modes[48] = {}, levels[48] = {};
|
||||
inline uint64_t now_us = 0, reset_high_at = 0, reset_low_at = 0, reset_low_us = 0;
|
||||
inline unsigned gpio_events = 0, reads = 0, transactions = 0, spi_begins = 0;
|
||||
inline unsigned initializations = 0, allocations = 0, objects = 0, draws = 0;
|
||||
inline unsigned clock_edges = 0, command_bits = 0, data_bits = 0;
|
||||
inline uint8_t command_value = 0;
|
||||
inline bool transaction = false, fail_pixels = false, initialized = false;
|
||||
inline std::string scenario;
|
||||
inline uint32_t read_value = 0;
|
||||
inline bool automatic() {
|
||||
#ifdef MESHCORE_TRACKER_DISPLAY_AUTODETECT
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
inline void check_panel_pin(int pin) {
|
||||
assert((pin == PIN_TFT_CS || pin == PIN_TFT_DC || pin == PIN_TFT_SDA
|
||||
|| pin == PIN_TFT_SCL || pin == PIN_TFT_RST || pin == PIN_TFT_LEDA_CTL)
|
||||
&& "probe must never drive Vext, GNSS, or radio pins");
|
||||
}
|
||||
inline void pinMode(int pin, int mode) {
|
||||
check_panel_pin(pin); ++gpio_events; modes[pin] = mode;
|
||||
if (pin == PIN_TFT_SDA && (mode == INPUT_PULLUP || mode == INPUT_PULLDOWN)) {
|
||||
assert(transaction && command_bits == 8 && command_value == 0x04);
|
||||
const unsigned within_attempt = (transactions - 1) % 6;
|
||||
read_value = 0x7c89f0;
|
||||
if (scenario == "floating") read_value = mode == INPUT_PULLUP ? 0xffffff : 0;
|
||||
if (scenario == "stuck-high") read_value = 0xffffff;
|
||||
if (scenario == "stuck-low") read_value = 0;
|
||||
if (scenario == "biased") read_value = mode == INPUT_PULLUP ? 0x7c89f0 : 0x123456;
|
||||
if (scenario == "noise") read_value = within_attempt == 2 ? 0x7c89f1 : 0x7c89f0;
|
||||
if (scenario == "recover" && transactions <= 6) read_value = 0;
|
||||
if (scenario == "prime" && within_attempt % 3 == 0) read_value = 0;
|
||||
if (scenario == "programmable") read_value = 0x123456;
|
||||
}
|
||||
}
|
||||
inline void digitalWrite(int pin, int value) {
|
||||
check_panel_pin(pin); ++gpio_events;
|
||||
if (automatic() && pin == PIN_TFT_LEDA_CTL && value == PIN_TFT_LEDA_CTL_ACTIVE)
|
||||
assert(initialized && "backlight must remain off until initialization");
|
||||
if (pin == PIN_TFT_RST && value == HIGH && levels[pin] == LOW) {
|
||||
reset_low_us = now_us - reset_low_at; reset_high_at = now_us;
|
||||
}
|
||||
if (pin == PIN_TFT_RST && value == LOW && levels[pin] == HIGH) reset_low_at = now_us;
|
||||
if (pin == PIN_TFT_CS && value == LOW && levels[pin] == HIGH && spi_begins == 0) {
|
||||
assert(automatic() && now_us - reset_high_at >= 120000 && reset_low_us >= 10000);
|
||||
assert(levels[PIN_TFT_LEDA_CTL] != PIN_TFT_LEDA_CTL_ACTIVE);
|
||||
transaction = true; ++transactions;
|
||||
clock_edges = command_bits = data_bits = 0; command_value = 0;
|
||||
}
|
||||
if (pin == PIN_TFT_SCL && value == HIGH && transaction) {
|
||||
++clock_edges;
|
||||
if (modes[PIN_TFT_SDA] == OUTPUT) {
|
||||
assert(levels[PIN_TFT_DC] == LOW && command_bits < 8);
|
||||
command_value = uint8_t((command_value << 1) | levels[PIN_TFT_SDA]);
|
||||
++command_bits;
|
||||
}
|
||||
}
|
||||
if (pin == PIN_TFT_SCL && value == LOW && transaction && clock_edges == 8)
|
||||
assert(modes[PIN_TFT_SDA] != OUTPUT && "release SDA before command falling edge");
|
||||
if (pin == PIN_TFT_CS && value == HIGH && transaction) {
|
||||
assert(command_bits == 8 && data_bits == 24 && clock_edges == 33);
|
||||
transaction = false;
|
||||
}
|
||||
levels[pin] = value;
|
||||
}
|
||||
inline int digitalRead(int pin) {
|
||||
assert(pin == PIN_TFT_SDA && transaction && levels[PIN_TFT_DC] == HIGH);
|
||||
assert(modes[pin] == INPUT_PULLUP || modes[pin] == INPUT_PULLDOWN);
|
||||
assert(clock_edges == 9 + data_bits && data_bits < 24 && levels[PIN_TFT_SCL] == LOW);
|
||||
++reads;
|
||||
return (read_value >> (23 - data_bits++)) & 1;
|
||||
}
|
||||
inline void delay(unsigned ms) { now_us += uint64_t(ms) * 1000; }
|
||||
inline void delayMicroseconds(unsigned us) { now_us += us; }
|
||||
'''
|
||||
|
||||
HARNESS = r'''
|
||||
@POLICY@
|
||||
#include <Arduino.h>
|
||||
#include "WirelessTrackerDisplayProbe.h"
|
||||
constexpr int SPI_CLOCK_DIV2=2, ST77XX_NORON=0x13, ST77XX_DISPON=0x29;
|
||||
constexpr int ST77XX_DISPOFF=0x28, ST77XX_SLPIN=0x10;
|
||||
constexpr uint16_t ST77XX_BLACK=0, ST77XX_WHITE=0xffff;
|
||||
constexpr int ST7735_BUFFER_COLOR_DEPTH=16;
|
||||
struct Spi {
|
||||
void begin(int scl, int miso, int sda, int cs) {
|
||||
assert(scl==41 && miso==-1 && sda==42 && cs==-1);
|
||||
if (automatic()) assert(transactions>=6);
|
||||
++spi_begins;
|
||||
}
|
||||
void setClockDivider(int) {}
|
||||
} spi;
|
||||
static Spi* _spi=&spi;
|
||||
static int _height, _width, _colstart, _rowstart, lcd;
|
||||
struct TFT_eSprite {
|
||||
bool valid=false;
|
||||
explicit TFT_eSprite(int*) { ++objects; }
|
||||
void setColorDepth(int) {}
|
||||
void* createSprite(int w,int h) {
|
||||
assert(w==160 && h==80); ++allocations;
|
||||
valid=!fail_pixels; fail_pixels=false;
|
||||
return getPointer();
|
||||
}
|
||||
void* getPointer() { return valid ? this : nullptr; }
|
||||
void fillScreen(uint16_t) { assert(valid); ++draws; }
|
||||
void setTextColor(uint16_t) { assert(valid); }
|
||||
};
|
||||
static TFT_eSprite* sprite=nullptr;
|
||||
static uint32_t curr_color;
|
||||
@READY@
|
||||
namespace mesh {
|
||||
struct Logger { void println(const char*) {} } logger;
|
||||
Logger& usbLoggingPort() { return logger; }
|
||||
}
|
||||
struct Rail {
|
||||
unsigned claims;
|
||||
unsigned taken=0,released=0;
|
||||
void claim() { ++claims; ++taken; }
|
||||
void release() { assert(claims); --claims; ++released; }
|
||||
};
|
||||
void sendCommand(int) {}
|
||||
@MARKER@
|
||||
class ST7735Display {
|
||||
public:
|
||||
bool _isOn=false;
|
||||
Rail* _peripher_power;
|
||||
#ifdef MESHCORE_TRACKER_DISPLAY_AUTODETECT
|
||||
bool _probeComplete=false,_present=false;
|
||||
#endif
|
||||
explicit ST7735Display(Rail* power) : _peripher_power(power) {}
|
||||
bool begin();
|
||||
void turnOn();
|
||||
void turnOff();
|
||||
void _resetAndInit() { ++initializations; initialized=true; }
|
||||
void endFrame() { if (spriteReady()) { assert(initialized); ++draws; } }
|
||||
};
|
||||
@METHODS@
|
||||
int main(int argc,char** argv) {
|
||||
assert(argc==3); scenario=argv[1];
|
||||
const unsigned gps_claims=unsigned(std::stoul(argv[2]));
|
||||
Rail rail{gps_claims}; ST7735Display display(&rail);
|
||||
const bool invalid=scenario=="floating" || scenario=="stuck-high" || scenario=="stuck-low"
|
||||
|| scenario=="biased" || scenario=="noise";
|
||||
if (scenario=="alloc-fail") fail_pixels=true;
|
||||
const bool began=display.begin();
|
||||
if (automatic() && invalid) {
|
||||
assert(!began && !display._isOn && "absent panel must remain headless");
|
||||
assert(allocations==0 && objects==0 && spi_begins==0 && initializations==0);
|
||||
assert(rail.claims==gps_claims && rail.taken==1 && rail.released==1);
|
||||
assert(levels[PIN_TFT_RST]==LOW && levels[PIN_TFT_LEDA_CTL]!=PIN_TFT_LEDA_CTL_ACTIVE);
|
||||
for (int pin : {PIN_TFT_CS,PIN_TFT_DC,PIN_TFT_SDA,PIN_TFT_SCL}) assert(modes[pin]==INPUT);
|
||||
const auto events= gpio_events;
|
||||
assert(!display.begin()); display.turnOn(); display.turnOff(); display.endFrame();
|
||||
assert(events==gpio_events && transactions==12 && now_us<400000);
|
||||
assert(rail.claims==gps_claims && allocations==0 && draws==0);
|
||||
return 0;
|
||||
}
|
||||
if (scenario=="alloc-fail") {
|
||||
assert(!began && sprite==nullptr && allocations==1 && spi_begins==0);
|
||||
assert(rail.claims==gps_claims);
|
||||
assert(display.begin());
|
||||
assert(allocations==2 && rail.claims==gps_claims+1);
|
||||
} else {
|
||||
assert(began && allocations==1 && rail.claims==gps_claims+1);
|
||||
}
|
||||
assert(display._isOn && initializations==1);
|
||||
const auto probe_count=transactions;
|
||||
if (automatic()) assert(probe_count==(scenario=="recover" ? 12u : 6u));
|
||||
else assert(probe_count==0 && reads==0);
|
||||
assert(levels[PIN_TFT_LEDA_CTL]==PIN_TFT_LEDA_CTL_ACTIVE);
|
||||
const auto pixels=sprite; const auto allocated=allocations; const auto events=gpio_events;
|
||||
assert(display.begin() && gpio_events==events && rail.claims==gps_claims+1);
|
||||
display.turnOff(); assert(rail.claims==gps_claims && !display._isOn);
|
||||
display.turnOn();
|
||||
assert(display._isOn && rail.claims==gps_claims+1 && initializations==2);
|
||||
assert(transactions==probe_count && allocations==allocated && sprite==pixels);
|
||||
display.turnOff(); assert(rail.claims==gps_claims);
|
||||
delete sprite;
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class WirelessTrackerDisplayDetectionTests(unittest.TestCase):
|
||||
def build(self, work, flags, *, old_bias=False, old_turnaround=False):
|
||||
work.mkdir()
|
||||
driver = (UI / "ST7735Display.cpp").read_text()
|
||||
header = (UI / "ST7735Display.h").read_text()
|
||||
policy = header[header.index("#if defined(WIRELESS_TRACKER_DISPLAY_AUTODETECT)"):header.index("class ST7735Display")]
|
||||
marker = driver[driver.index("#ifdef MESHCORE_TRACKER_DISPLAY_AUTODETECT"):driver.index("// Most ST7735")]
|
||||
methods = "\n".join(body(driver, signature) for signature in (
|
||||
"bool ST7735Display::begin()", "void ST7735Display::turnOn()", "void ST7735Display::turnOff()"))
|
||||
source = (HARNESS.replace("@POLICY@", policy).replace("@MARKER@", marker)
|
||||
.replace("@READY@", body(driver, "static bool spriteReady()"))
|
||||
.replace("@METHODS@", methods))
|
||||
probe = (UI / "WirelessTrackerDisplayProbe.h").read_text()
|
||||
if old_bias:
|
||||
probe = probe.replace("ids[0] == ids[1]", "true")
|
||||
if old_turnaround:
|
||||
probe = probe.replace("if (mask == 1) pinMode(PIN_TFT_SDA, bias);", "")
|
||||
probe = probe.replace(" digitalWrite(PIN_TFT_DC, HIGH);", " pinMode(PIN_TFT_SDA, bias);\n digitalWrite(PIN_TFT_DC, HIGH);", 1)
|
||||
(work / "Arduino.h").write_text(ARDUINO)
|
||||
(work / "WirelessTrackerDisplayProbe.h").write_text(probe)
|
||||
(work / "test.cpp").write_text(source)
|
||||
binary = work / "test"
|
||||
compiled = subprocess.run([os.environ.get("CXX", "g++"), "-std=c++17", "-Wall", "-Wextra", "-Werror",
|
||||
"-Wno-unused-variable", *SANITIZER_FLAGS, "-DESP_PLATFORM=1", *flags, "-I"+str(work),
|
||||
str(work / "test.cpp"), "-o", str(binary)], capture_output=True, text=True, timeout=60)
|
||||
self.assertEqual(compiled.returncode, 0, compiled.stdout + compiled.stderr)
|
||||
return binary
|
||||
|
||||
def run_case(self, binary, scenario, gps=1, negative=None):
|
||||
result = subprocess.run([str(binary), scenario, str(gps)], capture_output=True, text=True, timeout=10)
|
||||
if negative:
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn(negative, result.stderr)
|
||||
else:
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
|
||||
def test_optional_tracker_rejects_absence_noise_and_preserves_gps_power(self):
|
||||
with tempfile.TemporaryDirectory(prefix="tracker-detect-") as directory:
|
||||
for board in ("HELTEC_LORA_V3", "HELTEC_TRACKER_V1_1"):
|
||||
binary = self.build(Path(directory)/board, ["-D"+board, "-DWIRELESS_TRACKER_DISPLAY_AUTODETECT=1"])
|
||||
self.assertIn(b"meshcore.capability.display.autodetect.tracker.v1", binary.read_bytes())
|
||||
for scenario in ("floating", "stuck-high", "stuck-low", "biased", "noise"):
|
||||
for gps in (0, 1):
|
||||
with self.subTest(board=board, scenario=scenario, gps=gps): self.run_case(binary, scenario, gps)
|
||||
|
||||
def test_present_programmable_id_reset_recovery_and_allocation_retry(self):
|
||||
with tempfile.TemporaryDirectory(prefix="tracker-detect-") as directory:
|
||||
for active in (0, 1):
|
||||
binary = self.build(Path(directory)/str(active), ["-DHELTEC_LORA_V3", "-DWIRELESS_TRACKER_DISPLAY_AUTODETECT=1",
|
||||
f"-DPIN_TFT_LEDA_CTL_ACTIVE={active}"])
|
||||
for scenario in ("valid", "programmable", "prime", "recover", "alloc-fail"):
|
||||
for gps in (0, 1):
|
||||
with self.subTest(active=active, scenario=scenario, gps=gps): self.run_case(binary, scenario, gps)
|
||||
|
||||
def test_other_boards_and_disabled_option_keep_existing_initialization(self):
|
||||
with tempfile.TemporaryDirectory(prefix="tracker-detect-") as directory:
|
||||
variants = (["-DHELTEC_LORA_V3"], ["-DHELTEC_LORA_V3", "-DWIRELESS_TRACKER_DISPLAY_AUTODETECT=0"],
|
||||
["-DWIRELESS_TRACKER_DISPLAY_AUTODETECT=1"],
|
||||
*[["-DWIRELESS_TRACKER_DISPLAY_AUTODETECT=1", "-DHELTEC_LORA_V3", "-D"+board]
|
||||
for board in ("HELTEC_TRACKER_V2", "HELTEC_T096", "HELTEC_T1")])
|
||||
for index, flags in enumerate(variants):
|
||||
binary = self.build(Path(directory)/str(index), flags)
|
||||
self.assertNotIn(b"meshcore.capability.display.autodetect.tracker.v1", binary.read_bytes())
|
||||
self.run_case(binary, "floating")
|
||||
|
||||
def test_opposite_pull_and_turnaround_negative_controls_fail(self):
|
||||
with tempfile.TemporaryDirectory(prefix="tracker-detect-") as directory:
|
||||
flags = ["-DHELTEC_LORA_V3", "-DWIRELESS_TRACKER_DISPLAY_AUTODETECT=1"]
|
||||
binary = self.build(Path(directory)/"bias", flags, old_bias=True)
|
||||
self.run_case(binary, "biased", negative="absent panel must remain headless")
|
||||
binary = self.build(Path(directory)/"turnaround", flags, old_turnaround=True)
|
||||
self.run_case(binary, "valid", negative="release SDA before command falling edge")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -71,6 +71,7 @@ extends = Heltec_RC32
|
||||
build_flags =
|
||||
${Heltec_RC32.build_flags}
|
||||
-D HELTEC_RC32_WITH_DISPLAY
|
||||
-D RC32_DISPLAY_AUTODETECT=1
|
||||
-D DISPLAY_CLASS=NV3001BDisplay
|
||||
build_src_filter = ${Heltec_RC32.build_src_filter}
|
||||
+<helpers/ui/NV3001BDisplay.cpp>
|
||||
|
||||
@@ -29,6 +29,7 @@ build_flags =
|
||||
-D PIN_TFT_DC=40 ; RS (register select)
|
||||
-D PIN_TFT_RST=39 ; RES
|
||||
-D PIN_TFT_CS=38
|
||||
-D WIRELESS_TRACKER_DISPLAY_AUTODETECT=1
|
||||
-D USE_PIN_TFT=1
|
||||
-D PIN_VEXT_EN=3 ; Vext is connected to VDD which is also connected to OLED & GPS
|
||||
-D PIN_TFT_LEDA_CTL=21 ; LEDK (switches on/off via mosfet to create the ground)
|
||||
|
||||
@@ -66,6 +66,7 @@ build_flags =
|
||||
${sensor_base.build_flags}
|
||||
-I variants/heltec_tracker_v2
|
||||
-D HELTEC_TRACKER_V1_1
|
||||
-D WIRELESS_TRACKER_DISPLAY_AUTODETECT=1
|
||||
-D ESP32_CPU_FREQ=240
|
||||
-D USE_SX1262
|
||||
-D RADIO_CLASS=CustomSX1262
|
||||
|
||||
@@ -62,6 +62,7 @@ extends = Heltec_v4_r8
|
||||
build_flags =
|
||||
${Heltec_v4_r8.build_flags}
|
||||
-D HELTEC_V4_R8_TFT
|
||||
-D HELTEC_TFT_DISPLAY_AUTODETECT=1
|
||||
-D PIN_BOARD_SDA=17
|
||||
-D PIN_BOARD_SCL=18
|
||||
-D PIN_TFT_RST=21
|
||||
@@ -73,6 +74,7 @@ build_flags =
|
||||
-D PIN_TFT_SCL=16
|
||||
-D PIN_TFT_SDA=15
|
||||
-D PIN_TFT_MISO=45
|
||||
-D SDCARD_CS=3 ; Expansion V2 microSD shares the LCD bus; keep deselected.
|
||||
-D PIN_BUZZER=4
|
||||
build_src_filter = ${Heltec_v4_r8.build_src_filter}
|
||||
+<helpers/ui/buzzer.cpp>
|
||||
|
||||
Reference in new issue
Block a user