The motherboard has two RF daughterboard slots on separate GPIOs, mirrored by
its "LNA P" / "LNA S" jumpers, but the variant only ever pinned slot 1.
The shared configuration moves into [Station_G3_ESP32_common], with the SPI and
LNA lines split into per-slot sections. [Station_G3_ESP32] (slot 1) and
[Station_G3_ESP32_r2] (slot 2) each compose common + their own pins, so neither
set of -D flags has to shadow the other on the compile line - appending an
override would work only by last-wins, and esp32_base's -w hides the resulting
redefinition warning. Every pre-existing env keeps its name and resolves to
byte-identical options; verified by diffing pio project config --json-output.
PA PL1 is one board-level jumper shared by both slots, so P_PA1_EN stays in the
common section along with the TX power calibration.
Adds r2 twins of the repeater, room server and both observer envs, each an exact
mirror of its slot-1 sibling apart from the base it extends.
Slot 2 pins come from on-hardware testing in agessaman/MeshCore#49 and are not
documented publicly; the vendor wiki serves no content to fetchers and Meshtastic
implements slot 1 only.
Known limitation, unchanged from that PR: GPIO 42 and 43 fall outside the
ESP32-S3 RTC GPIO range (0-21), so the rtc_gpio_hold_en() calls in
ESP32Board::enterDeepSleep() and StationG3Board::powerOff() return
ESP_ERR_INVALID_ARG on slot 2 and the NSS and LNA pins are not latched through
deep sleep. Fixing it means gpio_hold_en() plus gpio_deep_sleep_hold_en() in
shared code.
Rotation 2 was consistently upside down on the Expansion Kit V2 panel, so 0
becomes the compiled default for both portrait observer targets.
display.flip is unchanged and still defaults to off: the compiled constant
should be the correct orientation, with the setting reserved for a board
mounted the other way up. A node already carrying `display.flip 1` from
testing needs `set display.flip 0` after this.
Landscape targets are untouched - they take the driver's own DISPLAY_ROTATION
default of 3.
Power-off now needs a 3 second hold; any shorter press toggles the display.
MomentaryButton reports a CLICK for any release short of its threshold, so
that single value defines both.
The button also felt unreliable - "a brief press doesn't wake it, more often
than not". MomentaryButton's multi-click detection withholds a CLICK for
MULTI_CLICK_WINDOW_MS (280 ms) after release, and folds a second press
arriving inside that window into a DOUBLE_CLICK. Since the handler only acts
on CLICK, an impatient second press produced nothing at all: press, see
nothing, press again, still nothing. Multi-click is now off for these targets,
so CLICK fires on release.
Both settings are build flags defaulted in variants/heltec_v4_r8/target.cpp
and overridden only on the two TFT observer bases, because the companion
builds share this user_btn and do use double/triple click.
DISPLAY_TOUCH_DEBUG additionally logs which input caused a toggle
("Display: button -> on"), so any remaining flake can be attributed to the
button or to a spurious touch read rather than guessed at.
Four defects found by hardware testing of the Expansion Kit V2.
Touch never registered. The panel's controller does not use the point-count
encoding the reference CHSC6X drivers document: byte 0 reads 0x00 idle and
0x1F while a finger is down, so testing for a count of 1 never fired. A
partly-failed read leaves 0xFF, which must not count as a press either, so
the test is now != 0x00 && != 0xFF.
Touch polling could stall the UI loop for ~1 s at a time. The controller
NACKs its address whenever it has nothing to report, and calling requestFrom()
unconditionally logged a bus error on every 50 ms poll and, once the bus
wedged, burned a full ESP_ERR_TIMEOUT inside loop(). Probe the address first,
which reports the same NACK quietly, and bound the read with setTimeOut().
The display could not be woken once it blanked; only RST brought it back.
turnOn() re-ran the whole display.init(), which re-enters SPI setup, spends
~500 ms in Adafruit's reset delays and pulses GPIO 21 - the line shared with
TP_RST, so it reset the touch controller on every wake. Since turnOff() no
longer parks that line low, the panel stays configured while dark and waking
is just the backlight. Toggling also clears the refresh deadline so the
current frame is drawn immediately instead of the stale one.
Power-off rebooted instead of staying off. powerOff() went through
enterDeepSleep(), which always arms an ext1 wake on P_LORA_DIO_1; a deep-sleep
wake is a full reboot, so a node in live traffic restarted within seconds of
showing "Turning OFF". It now disables every wake source, so the node stays
down until RST or a power cycle.
Note that the display off/on cycle had never been exercised on this board
before: observer builds pinned AUTO_OFF_MILLIS=0, so the panel never blanked
until display.timeout made it a runtime setting.
Replace the sparse Heltec V4 R8 observer home screen with a padded dark
analytics dashboard, add manual display control, and make blanking a runtime
setting.
Dashboard (DISPLAY_ACTIVITY_DASHBOARD, the four R8 TFT observer envs):
- RadioActivityWindow: 20 one-minute buckets of valid RX packets, no heap.
The caller's 32-bit millis() is extended to a monotonic 64-bit clock, so
nothing downstream has a rollover case; an always-on node passes 2^32 ms
after ~49.7 days, which would otherwise re-enter warm-up and divide 20
minutes of traffic by seconds. Rates use 19 whole minutes plus the elapsed
part of the current one rather than a fixed 1200 s.
- ObserverDashboard: header, radio strip, headline totals, a 20-bar
packets-per-minute graph and RF/status footers, with separate portrait and
landscape layouts. A text row is a fixed 16 px, which is 3.2 logical units
in portrait but 4.27 in landscape, so one shared grid would overlap.
Text is trimmed by character budget, not measured width: getTextWidth()
reports an over-long string at the portrait driver's fallback scale, so
DisplayDriver::drawTextEllipsized() under-trims and the row renders at half
height.
- Six per-row signatures computed from what is actually drawn, so only the
rows whose pixels changed repaint. No startFrame(), no whole-screen clear.
Link state moved out of the full-frame signature, so a DHCP renewal or WiFi
flap repaints one footer row instead of the panel.
- Dark theme by retuning the UIColor statics at runtime, which needs no
display-driver edit and carries boot, setup, reboot and power-off with it.
Touch and button (DISPLAY_TOUCH_TOGGLE):
- CHSC6X at I2C 0x2E, polled; TP_INT is unusable (optional R13, and GPIO 43
is U0TXD). The point-count byte is tested against a valid count, never
against non-zero: an idle read returns 0xFF, which reads as a finger held
down forever and latches the tap detector after one event.
- turnOff() no longer parks PIN_TFT_RST low on this board. GPIO 21 is a
shared LCD_RST/TP_RST net, so doing that held the touch controller in
reset for as long as the display was off. Verified against Heltec's
expansion-board and mainboard schematics and the V4-R8 datasheet pinout,
which also correct the pin comment in HeltecV4R8Board.cpp.
- The USER button click now toggles the display too; it previously did
nothing whenever the display was already on.
display.timeout:
- `set display.timeout <secs>` / `get display.timeout`, 0 = stay on, 60 s
default, 3600 max. Read live, so a change applies without a reboot and
restarts the countdown rather than firing on the old deadline.
- Stored in MQTTPrefs (/mqtt.json), keeping NodePrefs aligned with upstream.
Runtime-only: LegacyV1MQTTPrefs and the four frozen binary payload sizes
are unchanged. No JSON format-version bump - the loader skips keys no
def() claims, so older firmware reads newer files and this firmware reads
older ones with the default applied. Both directions are covered by tests.
- Joins the observer atomic-setter contract, so a failed save rolls the live
value back instead of only claiming to.
New periodic work uses a wrap-safe deadline check; `millis() >= deadline`
fires every loop for a whole interval before each rollover.
Adds test_radio_activity_window, test_observer_dashboard (driving the real
renderer against a recording DisplayDriver in both orientation profiles) and
test_touch_tap_detector. 440 native cases pass.
thinknode_m8: initial support
adds some functionalities to ui_task and epddisplay:
epd display,
- supports screen off, and clear screen
- supports forcing full refresh every N partial refreshes (off by default, configured to 60 for M8)
ui_task
- UI_SHOW_CLOCK flag to display clock on first screen
- rotary button can act like joystick (with regards to Enter key), defaults to old config for existing nodes
- screen off on long press when using rotary button (very handy for the watch)
Picks up upstream MeshCore 1.17.1.
Notable upstream content:
- 1.17.1 version/build-date bump in the example MyMesh headers.
- nRF52: combine radio entropy with CC310 RNG.
- Companion FEM prefs: load/save of fem_ properties commented out until they
can be set from the client.
- Scoped reply routing: replies no longer dropped when flood.max.unscoped is
low (RoutingPolicy + unit tests).
- nRF52 unused-pin sweep (T1, T-Echo Lite, MeshPocket).
No conflicts.
Brings in the external FEM gain preferences (fem_txgain, PR #3137 plus the
companion-side port), the AGC reset rxgain fix, the LR2021 preamble/IRQ
timeout logic, and assorted variant fixes (T096, T-Echo Card TCXO, promicro
pinmap, minewsemi, R1 Neo).
Conflict resolutions:
- SH1106Display: both sides fixed T-Beam Supreme startup independently. Kept
our _initialized guard and DISPLAY_ADDRESS_ALT override, took upstream's
SA0-pair fallback and its unconditional display.begin() so the frame buffer
is allocated even when no panel answers.
- MyMesh/SensorMesh/CommonCLI: took upstream's fem_txgain default and wiring,
kept our comments and the observer-side prefs layout.
Also fixes CustomLLCC68Wrapper, which upstream missed when sx126xResetAGC
gained its rx_boost_gain parameter. No variant builds that wrapper today, so
neither tree failed to compile.
SPI pins that are set to -1 cause OOB reads on NRF52. The unused Serial2 pin definitions were removed to avoid potential issues with the Uart framework.
Setting SPI pins to values that are OOB of the g_ADigitalPinMap[] array causes OOB reads. This variant.h has pin 0 as 0xFF which passes through as NRFX_SPIM_PIN_NOT_USED.
Picks up upstream MeshCore 1.17.0 and the dev commits that followed it.
Notable upstream content:
- 1.17.0 version/build-date bump in the example MyMesh headers.
- anon_req hardening: reply_path_len is now uint8_t with an isValidPathLen()
bounds check and a 0xFF sentinel; reply_path_hash_size is gone.
- LR2021 support (Meshnology W12, Seeed MeshTracker X1) incl. side detectors,
multi-SF and the new `extra.sf` get/set CLI.
- CustomLFS 0.2.2 -> 0.2.3 (GD25Q64C support).
- kiss_modem envs for several nRF52 variants; ThinkNode M6 GPS/flash fixes.
Conflicts resolved:
- platformio.ini: took upstream's CustomLFS 0.2.3, kept our lib_ignore for
the vendored PsychicMqttClient.
- CommonCLI.h: kept both upstream's USE_LR2021 configSideDetectors() hook and
our fault-alert callbacks.
NodePrefs gains upstream's extra_sf[4], which is not registered with
ConfigSerializer, so /prefs.json layout is unchanged.
Verified: 275/275 native host tests pass; Heltec_v3_repeater_observer_mqtt,
Heltec_v3_repeater and heltec_v4_repeater_observer_mqtt all build.