Compare commits

..
20 Commits
Author SHA1 Message Date
Kaj Schittecat ba1667b4d1 touch: beta_78 release notes 2026-09-07 16:43:11 +02:00
Kaj SchittecatandClaude Opus 5 c106d47b9b At-a-glance body drops to 20 px on the M9 (#446)
Buko84: the notification text is too big on the M9. It is, and the fix was
already made once for the same reason.

28 px was chosen so the message is legible from across a room, which it is, but
on a 240x320 panel it fills the glance and crowds out the message it is supposed
to be showing. The T-Deck was moved to 20 px for exactly this and the M9 has the
same size panel; nobody widened the gate at the time.

Three things had to change, not one: the extern declaration, the 20 px Latin
fallback font's own build gate, and a LV_FONT_MONTSERRAT_20 flag on the M9 env,
which was not building that size at all. Missing any of them is a link error
rather than a small font, which is why it is worth naming.

All nine S3 envs and both ESP32-P4 targets green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 16:32:51 +02:00
Kaj SchittecatandClaude Opus 5 1aa4496530 Lock-screen glance: say who and how far, and stay dark for muted channels
Two changes to the at-a-glance notification, both asked for by Kaj.

The title now carries the sender and the hop count, not just the thread. A
channel post reads "#public - Alice - 3 hops"; a direct message reads
"Alice - direct" without naming the sender twice, since a DM is already titled
with them. Zero hops says "direct" rather than "0 hops", and 0xFF is
OUT_PATH_UNKNOWN, an advert that arrived by flood, so it is omitted rather than
printed as "255 hops" -- the same trap that produced the Discovered list bug.

The point is that a count off a dark screen tells you nothing you can act on,
whereas a named contact one hop away against channel chatter from five hops out
is the reason to pick the device up at all.

Muted channels no longer light the screen. That was arguably backwards: muting
silenced the chime while the message still woke the display and printed itself
in large type, which is the louder half of the notification rather than the
quieter one. Muting is a statement about a conversation, so it now covers both.

Mentions keep their own mute bit, matching how the chime already treats them, so
a channel muted for chatter still surfaces an @-mention. If mute should instead
mean nothing at all gets through, that is a one-line change.

All nine S3 envs and both ESP32-P4 targets green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 15:54:37 +02:00
Kaj SchittecatandClaude Opus 5 37aac2736c Accent colour picker: the swatch you are on no longer hides its own colour (#431)
Buko84 again, and the same defect I fixed for the lock-screen picker: each swatch
set its colour for the default state only, so the theme was free to repaint the
focused one. On a d-pad board the swatch you are standing on is always focused,
so it turned white and hid the colour you were choosing.

I fixed the lock-screen picker and missed this one, which is the second instance
of the same mistake in the same file. The colour is now pinned across the
focused, pressed and checked states here too, and focus draws as a ring outside
the swatch so it can never cover the colour.

The swatches are also scaled rather than raw pixels now, the same class as #238.

All nine S3 envs and both ESP32-P4 targets green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 15:01:29 +02:00
Kaj SchittecatandClaude Opus 5 6160184531 Translation placeholders for the strings from oumike's seven PRs
Five new user-facing strings arrived with #429/#439/#440/#441/#442/#444/#445.

All nine S3 envs and both ESP32-P4 targets green on the merged result.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 14:35:56 +02:00
Kaj Schittecat 6ee33990a3 Merge PR #445 (oumike) 2026-09-07 14:20:11 +02:00
Kaj Schittecat 8b75132f07 Merge PR #444 (oumike) 2026-09-07 14:20:09 +02:00
Kaj Schittecat 03d05d7e57 Merge PR #442 (oumike): focus the M9 composer when opening chats
Conflict with the M9 pending-focus block from #440 resolved by keeping both and
making the ordering explicit. #442 reworks the focus-hint logic so a hint
belonging to the page behind a newly opened overlay cannot suppress the
new-chat composer fallback (#436); #440's block places focus deliberately and
sets focus_set. Without a guard the reworked chain fell through to the
just-opened-chat branch and moved that focus, which is precisely what #440's
block exists to prevent, so the chain now has an explicit first arm for it.
2026-09-07 14:19:49 +02:00
Kaj Schittecat a4dc077d80 Merge PR #441 (oumike) 2026-09-07 14:18:06 +02:00
Kaj Schittecat 74594d3d54 Merge PR #440 (oumike) 2026-09-07 14:18:05 +02:00
Kaj Schittecat c18916835a Merge PR #439 (oumike) 2026-09-07 14:18:04 +02:00
Kaj Schittecat 7352a58701 Merge PR #429 (oumike) 2026-09-07 14:18:03 +02:00
Kaj SchittecatandClaude Opus 5 96ba457984 Fix a Wi-Fi callback race that crashed at boot, and make the GPS privacy setting reachable
Two things, both found by looking rather than by being told.

A Heltec V4-R8 crash dump from beta_77 decodes to a data race in the Arduino
core's Wi-Fi event list. Correcting for the unwinder mangling Xtensa return
addresses, the event task was inside _eventCallback copying a WiFiEventCbList
entry, iterating the callback vector, while loopTask was still in setup() inside
_M_realloc_insert growing that same vector through WiFi.onEvent(). The
reallocation frees the buffer the event task is walking, so it then calls
through a dangling function pointer, which is the garbage program counter in the
dump.

Neither side locks, and the vector is the core's, not ours. What is ours is the
ordering: both handlers were registered AFTER the stack was started, one of them
after WiFi.begin(), so a STA_DISCONNECTED arriving during boot association lands
in the window. A failed first association is ordinary, which is why this happens
at all. Both now register before WiFi.mode(WIFI_STA), so nothing mutates the
vector once events can flow and the race is impossible rather than unlikely.
This is a crash during setup(), so on an unlucky unit it is a bootloop, which
makes it worth more than its single report.

The moved handler drops its WIFI_DEBUG_PRINTLN tracing, which comes from a
header not in scope that early. The prints compile to nothing in a release build
and the reconnect flag they accompanied is unchanged.

Separately: the location-privacy setting shipped in beta_77 on the wrong page.
It was inside the Sensors section rather than GPS, because the insertion was
anchored to a comment belonging to the Sensors block, and that section is
described in its own code as V4-with-kit only. So on most boards it was not
reachable at all, which is exactly what two people reported: they went looking
and correctly concluded it was not there. It now sits in the GPS section, is
named "Location privacy" rather than "Position in adverts" (nobody searching for
a privacy control scans for the word "adverts"), and offers four buttons instead
of a cycling one, so every option and the current choice are visible without
touching anything. Given the failure here was discoverability, showing the most
without interaction is the point.

All nine S3 envs and both ESP32-P4 targets green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 14:17:50 +02:00
Michael A. Cojocari f1f12fb30f Add Pager special-character picker shortcut (#435) 2026-09-06 21:00:27 -04:00
Michael A. Cojocari 5c3a7a5d46 Avoid M9 Contacts transition delay (#433) 2026-09-06 20:45:18 -04:00
Michael A. Cojocari 530b539957 Focus M9 composer when opening chats (#436) 2026-09-06 17:39:11 -04:00
Michael A. Cojocari 7f20ebe522 Show M9 battery saver in status icon (#437) 2026-09-06 17:29:33 -04:00
Michael A. Cojocari 565eb7d954 Fix M9 contact search navigation (#438) 2026-09-06 17:24:16 -04:00
Michael A. Cojocari 28fb57ef4c Fix Attaky usability issues (#423) 2026-09-06 16:55:41 -04:00
Michael A. Cojocari b692fa403c fix(m9): add selectable special characters 2026-09-06 15:03:47 -04:00
26 changed files with 1031 additions and 150 deletions

No files matched your search

+12
View File
@@ -71,6 +71,18 @@ field, so you're never stuck. Enter sends (toggleable under Settings), Back
leaves the field. In the Terminal, Enter runs the command; in the text editor
it inserts a newline.
Channels, direct messages, and room conversations place **#** and emoji to the
right of the message input. Press Right to move from the input to **#**, and
Right again to move to emoji. Press **OK** on either button to open its picker.
In the symbol grid, use all four arrows to move the highlight and **OK** to
insert the selected character.
The Create/Join channel forms use the same **#** action beside every text
field. Right moves from the field to **#**; Left returns to editing the field.
Password fields use the same control, including Wi-Fi, MQTT, and repeater login.
After inserting a symbol, focus returns to the originating text field.
## Lock screen
**Hold OK** to unlock — the keyboard's hardware long-press stands in for the
+14 -3
View File
@@ -17,9 +17,9 @@ on both.
(press the knob) to select. It's your only pointing device, so almost
every screen is navigable with turn + click alone.
Everything below only fires while you're **not** actively typing into a text
field — if a field is focused and you're typing into it, letters type
normally and these combos step out of the way.
Unless a row explicitly says it applies to a text field, the shortcuts below
only fire while you're **not** actively typing. Letters continue to type
normally while a field is active.
## Bottom menu shortcuts
@@ -45,6 +45,7 @@ while editing a text field, where the keys type normally.
| Hold ~1 s, then release | **Back**: closes a popup → closes an open chat → goes Home → Esc (whichever applies first) | **Backspace held ~1 s** |
| **Fn (Alt) + turn**, on a main tab | Move between the 5 main tabs (Mail / Contacts / Home / Map / Settings) | **M / C / H / A / S** jumps directly |
| **Fn (Alt) + turn**, inside a settings page or chat | Scroll the page up/down | none — encoder only |
| **Fn (Alt) + short click**, with a text field focused | Open the special-character picker | none — encoder only |
| Turn, with a dropdown open | Scroll through the dropdown's options | none — encoder only |
| Turn, with the accent picker open | Cycle through the accent variants (see below) | none — encoder only (Fn+Space to enter is shared) |
| Turn, with the @-mention list open | Cycle through matching contacts (see below) | none — encoder only |
@@ -129,6 +130,15 @@ return to letters.
[Space]
```
### Symbols not on the keyboard
While any text field is focused, including a Wi-Fi or repeater password:
1. Hold **Fn (Alt)** and short-click the encoder to open **Special characters**.
2. Turn the encoder to highlight a symbol such as `%`, `<`, `>`, or `€`.
3. Short-click the encoder or press **Enter** to insert it.
4. Hold the encoder for about one second to close the picker without inserting.
### Shift and Caps Lock
- **Hold Shift + a letter**: that letter (or letters, for as long as you
@@ -247,6 +257,7 @@ that changes keyboard language.
| Hold encoder ~1s (or hold Backspace ~1s) | Back |
| Fn + turn (main tab) | Switch tabs |
| Fn + turn (page/chat) | Scroll |
| Fn + encoder click (text field focused) | Open special-character picker |
| Fn tap alone | Use the symbol layer for the next key |
| Fn double-tap | Lock the symbol layer; tap Fn again to unlock |
| Hold Shift + letter | Momentary uppercase |
+10
View File
@@ -1162,3 +1162,13 @@ Upload files or download screenshots from a browser on the same Wi-Fi. Upload fi
Waiting for browser Waiting for browser
Download mode (wait for USB flash) Download mode (wait for USB flash)
Download mode… reflash over USB,\nor press RESET to cancel Download mode… reflash over USB,\nor press RESET to cancel
1 km 1 km
100 m 100 m
250 m 250 m
Location privacy Location privacy
position others see is shifted; your own map keeps the real fix position others see is shifted; your own map keeps the real fix
Arrows • OK Arrows • OK
Direct message color Direct message color
Incoming message blink Incoming message blink
Notifications Notifications
Room / channel color Room / channel color
+10
View File
@@ -1158,3 +1158,13 @@ Upload files or download screenshots from a browser on the same Wi-Fi. Upload fi
Waiting for browser Waiting for browser
Download mode (wait for USB flash) Download mode (wait for USB flash)
Download mode… reflash over USB,\nor press RESET to cancel Download mode… reflash over USB,\nor press RESET to cancel
1 km 1 km
100 m 100 m
250 m 250 m
Location privacy Location privacy
position others see is shifted; your own map keeps the real fix position others see is shifted; your own map keeps the real fix
Arrows • OK Arrows • OK
Direct message color Direct message color
Incoming message blink Incoming message blink
Notifications Notifications
Room / channel color Room / channel color
+10
View File
@@ -1162,3 +1162,13 @@ Upload files or download screenshots from a browser on the same Wi-Fi. Upload fi
Waiting for browser Waiting for browser
Download mode (wait for USB flash) Download mode (wait for USB flash)
Download mode… reflash over USB,\nor press RESET to cancel Download mode… reflash over USB,\nor press RESET to cancel
1 km 1 km
100 m 100 m
250 m 250 m
Location privacy Location privacy
position others see is shifted; your own map keeps the real fix position others see is shifted; your own map keeps the real fix
Arrows • OK Arrows • OK
Direct message color Direct message color
Incoming message blink Incoming message blink
Notifications Notifications
Room / channel color Room / channel color
+10
View File
@@ -1162,3 +1162,13 @@ Upload files or download screenshots from a browser on the same Wi-Fi. Upload fi
Waiting for browser Waiting for browser
Download mode (wait for USB flash) Download mode (wait for USB flash)
Download mode… reflash over USB,\nor press RESET to cancel Download mode… reflash over USB,\nor press RESET to cancel
1 km 1 km
100 m 100 m
250 m 250 m
Location privacy Location privacy
position others see is shifted; your own map keeps the real fix position others see is shifted; your own map keeps the real fix
Arrows • OK Arrows • OK
Direct message color Direct message color
Incoming message blink Incoming message blink
Notifications Notifications
Room / channel color Room / channel color
+10
View File
@@ -1161,3 +1161,13 @@ Upload files or download screenshots from a browser on the same Wi-Fi. Upload fi
Waiting for browser Waiting for browser
Download mode (wait for USB flash) Download mode (wait for USB flash)
Download mode… reflash over USB,\nor press RESET to cancel Download mode… reflash over USB,\nor press RESET to cancel
1 km 1 km
100 m 100 m
250 m 250 m
Location privacy Location privacy
position others see is shifted; your own map keeps the real fix position others see is shifted; your own map keeps the real fix
Arrows • OK Arrows • OK
Direct message color Direct message color
Incoming message blink Incoming message blink
Notifications Notifications
Room / channel color Room / channel color
+10
View File
@@ -1179,3 +1179,13 @@ Upload files or download screenshots from a browser on the same Wi-Fi. Upload fi
Waiting for browser Waiting for browser
Download mode (wait for USB flash) Download mode (wait for USB flash)
Download mode… reflash over USB,\nor press RESET to cancel Download mode… reflash over USB,\nor press RESET to cancel
1 km 1 km
100 m 100 m
250 m 250 m
Location privacy Location privacy
position others see is shifted; your own map keeps the real fix position others see is shifted; your own map keeps the real fix
Arrows • OK Arrows • OK
Direct message color Direct message color
Incoming message blink Incoming message blink
Notifications Notifications
Room / channel color Room / channel color
+10
View File
@@ -1161,3 +1161,13 @@ Upload files or download screenshots from a browser on the same Wi-Fi. Upload fi
Waiting for browser Waiting for browser
Download mode (wait for USB flash) Download mode (wait for USB flash)
Download mode… reflash over USB,\nor press RESET to cancel Download mode… reflash over USB,\nor press RESET to cancel
1 km 1 km
100 m 100 m
250 m 250 m
Location privacy Location privacy
position others see is shifted; your own map keeps the real fix position others see is shifted; your own map keeps the real fix
Arrows • OK Arrows • OK
Direct message color Direct message color
Incoming message blink Incoming message blink
Notifications Notifications
Room / channel color Room / channel color
+10
View File
@@ -1159,3 +1159,13 @@ Upload files or download screenshots from a browser on the same Wi-Fi. Upload fi
Waiting for browser Waiting for browser
Download mode (wait for USB flash) Download mode (wait for USB flash)
Download mode… reflash over USB,\nor press RESET to cancel Download mode… reflash over USB,\nor press RESET to cancel
1 km 1 km
100 m 100 m
250 m 250 m
Location privacy Location privacy
position others see is shifted; your own map keeps the real fix position others see is shifted; your own map keeps the real fix
Arrows • OK Arrows • OK
Direct message color Direct message color
Incoming message blink Incoming message blink
Notifications Notifications
Room / channel color Room / channel color
+10
View File
@@ -1161,3 +1161,13 @@ Upload files or download screenshots from a browser on the same Wi-Fi. Upload fi
Waiting for browser Waiting for browser
Download mode (wait for USB flash) Download mode (wait for USB flash)
Download mode… reflash over USB,\nor press RESET to cancel Download mode… reflash over USB,\nor press RESET to cancel
1 km 1 km
100 m 100 m
250 m 250 m
Location privacy Location privacy
position others see is shifted; your own map keeps the real fix position others see is shifted; your own map keeps the real fix
Arrows • OK Arrows • OK
Direct message color Direct message color
Incoming message blink Incoming message blink
Notifications Notifications
Room / channel color Room / channel color
+10
View File
@@ -1163,3 +1163,13 @@ Upload files or download screenshots from a browser on the same Wi-Fi. Upload fi
Waiting for browser Waiting for browser
Download mode (wait for USB flash) Download mode (wait for USB flash)
Download mode… reflash over USB,\nor press RESET to cancel Download mode… reflash over USB,\nor press RESET to cancel
1 km 1 km
100 m 100 m
250 m 250 m
Location privacy Location privacy
position others see is shifted; your own map keeps the real fix position others see is shifted; your own map keeps the real fix
Arrows • OK Arrows • OK
Direct message color Direct message color
Incoming message blink Incoming message blink
Notifications Notifications
Room / channel color Room / channel color
+10
View File
@@ -1162,3 +1162,13 @@ Upload files or download screenshots from a browser on the same Wi-Fi. Upload fi
Waiting for browser Waiting for browser
Download mode (wait for USB flash) Download mode (wait for USB flash)
Download mode… reflash over USB,\nor press RESET to cancel Download mode… reflash over USB,\nor press RESET to cancel
1 km 1 km
100 m 100 m
250 m 250 m
Location privacy Location privacy
position others see is shifted; your own map keeps the real fix position others see is shifted; your own map keeps the real fix
Arrows • OK Arrows • OK
Direct message color Direct message color
Incoming message blink Incoming message blink
Notifications Notifications
Room / channel color Room / channel color
+10
View File
@@ -1162,3 +1162,13 @@ Upload files or download screenshots from a browser on the same Wi-Fi. Upload fi
Waiting for browser Waiting for browser
Download mode (wait for USB flash) Download mode (wait for USB flash)
Download mode… reflash over USB,\nor press RESET to cancel Download mode… reflash over USB,\nor press RESET to cancel
1 km 1 km
100 m 100 m
250 m 250 m
Location privacy Location privacy
position others see is shifted; your own map keeps the real fix position others see is shifted; your own map keeps the real fix
Arrows • OK Arrows • OK
Direct message color Direct message color
Incoming message blink Incoming message blink
Notifications Notifications
Room / channel color Room / channel color
+10
View File
@@ -1162,3 +1162,13 @@ Upload files or download screenshots from a browser on the same Wi-Fi. Upload fi
Waiting for browser Waiting for browser
Download mode (wait for USB flash) Download mode (wait for USB flash)
Download mode… reflash over USB,\nor press RESET to cancel Download mode… reflash over USB,\nor press RESET to cancel
1 km 1 km
100 m 100 m
250 m 250 m
Location privacy Location privacy
position others see is shifted; your own map keeps the real fix position others see is shifted; your own map keeps the real fix
Arrows • OK Arrows • OK
Direct message color Direct message color
Incoming message blink Incoming message blink
Notifications Notifications
Room / channel color Room / channel color
+4
View File
@@ -1506,6 +1506,10 @@ build_flags =
-D UI_LVGL=1
-D HAS_TOUCH_UI=1
-D FIRMWARE_OTA_ENV='"ThinkNode_M9_companion_radio_touch"'
; 20px Montserrat for the at-a-glance body (atGlanceEnsureFont). 28px filled
; this 240x320 panel and crowded out the message (#446); the T-Deck already
; dropped to 20 for the same reason. Only 20px is forced, nothing else.
-D LV_FONT_MONTSERRAT_20=1
-D ENABLE_ADVERT_ON_BOOT=0
-D LV_CONF_PATH=lv_conf_m9.h
-D LV_CONF_INCLUDE_SIMPLE=1
+27
View File
@@ -0,0 +1,27 @@
# beta_78 - test build. Stable stays beta_65.
# One user-facing note per non-blank, non-# line; # lines are section comments.
# --- A crash at startup ---------------------------------------------------------------------
A device could crash while starting up, before it finished booting, and on an unlucky unit that means it never comes up at all. Found by decoding a crash dump from a Heltec V4-R8.
The cause was a race inside the Wi-Fi stack: the firmware was adding its Wi-Fi event handlers after the radio had already started, and a disconnect arriving in that moment could be handled while the list of handlers was being rebuilt underneath. The handlers are now all registered before the radio starts, which closes the window rather than narrowing it. A failed first connection is ordinary, which is why this could happen at all.
# --- The location privacy setting was on the wrong page --------------------------------------
beta_77 added an option to advertise a position near you rather than your exact one, and put it on a settings page most devices do not show. Two people went looking for it and correctly concluded it was not there. It is now in Settings, GPS, at the bottom.
It is also easier to recognise: it is called "Location privacy" rather than "Position in adverts", and it shows four buttons, Exact, 100 m, 250 m and 1 km, so every choice and the current one are visible without tapping anything.
# --- Lock screen notifications ----------------------------------------------------------------
A message arriving on the lock screen now says who sent it and how far away they are, not just which conversation it was in. A channel post shows the channel, the sender and the hop count; a direct message shows the contact and "direct" when they are one hop away.
Messages in a muted channel no longer light the screen. Muting silenced the sound but still woke the display and printed the message in large type, which was the louder half of the notification rather than the quieter one. Mentions are separate, so a channel muted for chatter still shows an @-mention.
# --- ThinkNode M9 -------------------------------------------------------------------------------
Special characters can be selected. Contact search navigation, the contacts menu delay, and the composer being focused when a chat opens are all fixed, and the battery saver now shows in the status icons. All by Michael A. Cojocari, from reports by Buko84.
The colour picker in Display settings no longer turns the swatch you are on white, which hid the colour you were choosing. Reported by Buko84.
The notification text on a dark screen is smaller, so the message has room. It was sized to be readable from across a room, which crowded a 240x320 panel. Requested by Buko84.
# --- Also ----------------------------------------------------------------------------------------
The T-Lora Pager has a shortcut to the special-character picker, and several Attaky usability problems are fixed. Both by Michael A. Cojocari.
# --- For testers ---------------------------------------------------------------------------------
If you use the lock screen notification, the two things worth confirming are that a muted channel stays dark and that an @-mention in that same channel still comes through.
Still not verified on hardware, both off by default and unchanged: the DS3231 clock module support, written from the datasheet with nobody having wired one up, and the louder chime, whose pitch has not been measured on an M9.
Still open and not fixed here: a reboot when pressing down past the bottom of a chat on the M9, which needs a crash dump to place; a screen that will not stay dark after a manual lock; and the web flasher hanging at "wrapping up" on the T-Display P4.
+15 -3
View File
@@ -9,7 +9,7 @@
namespace TouchPrefsSchema {
static constexpr uint16_t MAGIC = 0x5743; // 'WC' (WadaCfg)
static constexpr uint8_t CURRENT_VERSION = 57; // v49: fem_lna default flip on the V4-R8; v50: map tile z/x/y line off by default (no new fields); v51: console_mode (boot into the text console; new trailing field, default OFF); v52: console_monitor (show incoming messages in the console, default ON); v54: boot_wifi_time + boot_wifi_open (cold-boot saved-Wi-Fi time sync, #383; both OFF); v55: loud_alerts (resonant-pitch chime, #388; OFF); v56: theme_mode (Day/Night firmware theme, #296; default Night); v57: gps_fuzz_m (displace the advertised position, #399; 0 = off)
static constexpr uint8_t CURRENT_VERSION = 58; // v49: fem_lna default flip on the V4-R8; v50: map tile z/x/y line off by default (no new fields); v51: console_mode (boot into the text console; new trailing field, default OFF); v52: console_monitor (show incoming messages in the console, default ON); v54: boot_wifi_time + boot_wifi_open (cold-boot saved-Wi-Fi time sync, #383; both OFF); v55: loud_alerts (resonant-pitch chime, #388; OFF); v56: theme_mode (Day/Night firmware theme, #296; default Night); v57: gps_fuzz_m (displace the advertised position, #399; 0 = off); v58: Attaky notification blink enable + room/DM color indexes (#423)
static constexpr uint8_t BROKEN_MID_INSERT_VERSION = 44;
// Persisted byte layout. New fields must be appended at the end: older blobs
@@ -109,6 +109,9 @@ struct __attribute__((packed)) Config {
// advert: a fresh random offset each time would let anyone averaging a night
// of adverts recover the true centre, which is the opposite of the intent.
uint16_t gps_fuzz_m;
uint8_t attaky_notify_enabled; // v58: blink both keyboard indicators for incoming messages
uint8_t attaky_notify_room_color; // v58: seven-color palette index for rooms/channels
uint8_t attaky_notify_dm_color; // v58: seven-color palette index for direct messages
};
static constexpr size_t HEADER_SIZE = offsetof(Config, bright);
@@ -121,10 +124,10 @@ static_assert(offsetof(Config, web_mirror) == offsetof(Config, rx_queue) + sizeo
// rejection is silent: cfgFlush just returns false and every preference stops
// persisting, with nothing in the log to say why. The struct grows most releases,
// so guard the ceiling here rather than discover it as "settings do not save" on
// whichever board is using the file backend. 114 bytes today.
// whichever board is using the file backend. 117 bytes today.
static_assert(sizeof(Config) <= 2048,
"Config exceeds the SdNvsPrefs value cap; prefs would silently stop saving");
static_assert(offsetof(Config, gps_fuzz_m) + sizeof(Config::gps_fuzz_m) == sizeof(Config),
static_assert(offsetof(Config, attaky_notify_dm_color) + sizeof(Config::attaky_notify_dm_color) == sizeof(Config),
"new preference fields must remain trailing");
// lang_file must stay immediately before the tail, or a v48-era blob overlays
// onto the wrong bytes. Checking both ends means the next person to append is
@@ -150,6 +153,15 @@ static_assert(offsetof(Config, theme_mode) ==
static_assert(offsetof(Config, gps_fuzz_m) ==
offsetof(Config, theme_mode) + sizeof(Config::theme_mode),
"gps_fuzz_m must follow theme_mode");
static_assert(offsetof(Config, attaky_notify_enabled) ==
offsetof(Config, gps_fuzz_m) + sizeof(Config::gps_fuzz_m),
"attaky_notify_enabled must follow gps_fuzz_m");
static_assert(offsetof(Config, attaky_notify_room_color) ==
offsetof(Config, attaky_notify_enabled) + sizeof(Config::attaky_notify_enabled),
"attaky_notify_room_color must follow attaky_notify_enabled");
static_assert(offsetof(Config, attaky_notify_dm_color) ==
offsetof(Config, attaky_notify_room_color) + sizeof(Config::attaky_notify_room_color),
"attaky_notify_dm_color must follow attaky_notify_room_color");
// Overlay a persisted blob on caller-provided defaults. Beta 57 wrote v44 with
// retry_echo inserted before web_mirror, shifting every later value. That blob
+40
View File
@@ -144,6 +144,9 @@ static void cfgSetDefaults(TouchCfg& c) {
c.loud_alerts = 0; // OFF: the standard chime pitch unless asked for
c.theme_mode = 0; // Night: preserves the existing firmware appearance
c.gps_fuzz_m = 0; // OFF: advertise the real position unless asked otherwise
c.attaky_notify_enabled = 0; // OFF: incoming messages do not blink the keyboard indicators
c.attaky_notify_room_color = 0; // red
c.attaky_notify_dm_color = 1; // green
c.compact_chat = 0; // OFF: bubble chat layout (opt-in IRC-style dense rows)
c.clock_floor = 0; // no persisted send-timestamp floor yet
c.rx_queue = 1; // ON: buffered receive (test-channel default; opt-out toggle in Radio & Mesh)
@@ -259,6 +262,11 @@ static void cfgLoadOrMigrate() {
if (stored_version < 55) { s_cfg.loud_alerts = 0; }
if (stored_version < 56) { s_cfg.theme_mode = 0; } // Night: unchanged appearance
if (stored_version < 57) { s_cfg.gps_fuzz_m = 0; } // OFF: real position
if (stored_version < 58) {
s_cfg.attaky_notify_enabled = 0;
s_cfg.attaky_notify_room_color = 0;
s_cfg.attaky_notify_dm_color = 1;
}
if (stored_version < 31) s_cfg.compact_chat = 0; // new trailing field: compact chat rows off by default
if (stored_version < 32) s_cfg.clock_floor = 0; // new trailing field: no send-timestamp floor persisted yet (#89)
if (stored_version < 33) s_cfg.rx_queue = 1; // buffered LoRa receive ON for the test channel (opt-out toggle in Radio & Mesh)
@@ -1130,6 +1138,38 @@ bool touchPrefsSetMsgFlash(bool on) {
return cfgFlush();
}
bool touchPrefsGetAttakyNotifyEnabled() {
if (!s_begun) touchPrefsBegin();
return s_cfg.attaky_notify_enabled != 0;
}
bool touchPrefsSetAttakyNotifyEnabled(bool on) {
if (!s_begun) touchPrefsBegin();
s_cfg.attaky_notify_enabled = on ? 1 : 0;
return cfgFlush();
}
uint8_t touchPrefsGetAttakyNotifyRoomColor() {
if (!s_begun) touchPrefsBegin();
return s_cfg.attaky_notify_room_color < TOUCH_ATTAKY_NOTIFY_COLOR_COUNT
? s_cfg.attaky_notify_room_color : 0;
}
bool touchPrefsSetAttakyNotifyRoomColor(uint8_t color) {
if (color >= TOUCH_ATTAKY_NOTIFY_COLOR_COUNT) return false;
if (!s_begun) touchPrefsBegin();
s_cfg.attaky_notify_room_color = color;
return cfgFlush();
}
uint8_t touchPrefsGetAttakyNotifyDmColor() {
if (!s_begun) touchPrefsBegin();
return s_cfg.attaky_notify_dm_color < TOUCH_ATTAKY_NOTIFY_COLOR_COUNT
? s_cfg.attaky_notify_dm_color : 1;
}
bool touchPrefsSetAttakyNotifyDmColor(uint8_t color) {
if (color >= TOUCH_ATTAKY_NOTIFY_COLOR_COUNT) return false;
if (!s_begun) touchPrefsBegin();
s_cfg.attaky_notify_dm_color = color;
return cfgFlush();
}
// Console mode is a BOOT mode, so this is read before the UI is built. It fails
// safe by construction: the only value that means console is exactly 1, so a
// corrupt or unreadable pref boots the graphical UI, which is the mode everyone
+8
View File
@@ -196,6 +196,14 @@ bool touchPrefsSetFemLna(bool on);
bool touchPrefsGetMsgFlash();
bool touchPrefsSetMsgFlash(bool on);
static constexpr uint8_t TOUCH_ATTAKY_NOTIFY_COLOR_COUNT = 7;
bool touchPrefsGetAttakyNotifyEnabled();
bool touchPrefsSetAttakyNotifyEnabled(bool on);
uint8_t touchPrefsGetAttakyNotifyRoomColor();
bool touchPrefsSetAttakyNotifyRoomColor(uint8_t color);
uint8_t touchPrefsGetAttakyNotifyDmColor();
bool touchPrefsSetAttakyNotifyDmColor(uint8_t color);
/* Advertise on boot (#76): fire one flood self-advert ~6s after boot so peers with auto-add on
* refresh our pubkey (useful after a reflash wiped storage). Opt-in, default off. All boards. */
bool touchPrefsGetConsoleMode(); // boot into the LVGL-free console (CONSOLE_MODE.md)
+30 -16
View File
@@ -1435,6 +1435,36 @@ void setup() {
* contiguous DMA block, so let it claim memory before BLE. (Association
* happens later in loop(); this just inits the stack.) */
if (want_wifi) {
// Register every Wi-Fi event handler BEFORE the stack is started, never
// after. WiFi.onEvent() push_backs into a std::vector that the Arduino core
// iterates from its own event task, with no lock on either side: a
// registration that reallocates the vector while an event is being
// dispatched frees the buffer the event task is walking, and it then calls
// through a dangling function pointer. That is a real crash we have a dump
// of, on a Heltec V4-R8 on beta_77: the event task was inside
// _eventCallback copying a WiFiEventCbList entry while setup() was inside
// _M_realloc_insert growing that same vector, and it jumped to a garbage PC.
//
// The bug is in the Arduino core rather than here, and we cannot lock its
// vector, but we can make the race impossible: with nothing registered
// after the stack can emit events, the vector is never mutated while it is
// being read. Both handlers used to be added afterwards, one of them after
// WiFi.begin(), which is where the window came from.
//
// Records WHY every STA disconnect happened, surfaced by the UI's Wi-Fi
// status string as "auth failed (rNN)", so failures are diagnosable on a
// device with no serial console.
WiFi.onEvent([](WiFiEvent_t, WiFiEventInfo_t info){
g_wifi_last_disc_reason = info.wifi_sta_disconnected.reason;
}, ARDUINO_EVENT_WIFI_STA_DISCONNECTED);
// Without the WIFI_DEBUG_PRINTLN tracing the old copy had: that macro comes
// from SerialWifiInterface.h, which is not in scope this early in setup().
// The prints were debug-only and compile to nothing in a release build; the
// flag is the handler's actual job and is unchanged.
WiFi.onEvent([](WiFiEvent_t event, WiFiEventInfo_t){
if (event == ARDUINO_EVENT_WIFI_STA_DISCONNECTED) wifi_needs_reconnect = true;
else if (event == ARDUINO_EVENT_WIFI_STA_GOT_IP) wifi_needs_reconnect = false;
});
const bool wifi_mode_ready = WiFi.mode(WIFI_STA);
#if defined(TLORA_PAGER)
if (!wifi_mode_ready) {
@@ -1448,12 +1478,6 @@ void setup() {
// exists so a later WPA re-auth cannot bypass Wi-Fi-first ordering
#endif
WiFi.persistent(false);
// Record WHY every STA disconnect happened — surfaced by the UI's Wi-Fi
// status string as "auth failed (rNN)" so failures are diagnosable on a
// device with no serial console. Additive: multiple onEvent handlers coexist.
WiFi.onEvent([](WiFiEvent_t, WiFiEventInfo_t info){
g_wifi_last_disc_reason = info.wifi_sta_disconnected.reason;
}, ARDUINO_EVENT_WIFI_STA_DISCONNECTED);
// NOTE: do NOT enable modem-sleep here. On a fresh, *unassociated* STA (the
// setup wizard, no creds yet) DTIM modem-sleep naps the radio through the
// scan dwell, so WiFi.scanNetworks() comes back empty ("no networks found").
@@ -1562,16 +1586,6 @@ void setup() {
board.setInhibitSleep(true); // prevent sleep when WiFi is active
WiFi.setAutoReconnect(true);
WiFi.onEvent([](WiFiEvent_t event, WiFiEventInfo_t info){
if (event == ARDUINO_EVENT_WIFI_STA_DISCONNECTED) {
WIFI_DEBUG_PRINTLN("WiFi disconnected. Flagging for reconnect...");
wifi_needs_reconnect = true;
} else if (event == ARDUINO_EVENT_WIFI_STA_GOT_IP) {
WIFI_DEBUG_PRINTLN("WiFi connected successfully!");
wifi_needs_reconnect = false;
}
});
if (wifiConfigHasRuntime()) {
char ssid[WIFI_CONFIG_SSID_MAX];
char pwd[WIFI_CONFIG_PWD_MAX];
+675 -116
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -3,7 +3,7 @@
// now also builds this for the "at a glance" notification's 20 px message body
// (see atGlanceEnsureFont() in UITask.cpp) -- an experiment to see whether a
// smaller-than-28px glance body is still legible on that panel.
#if defined(HAS_TANMATSU) || defined(HAS_TDECK_GT911)
#if defined(HAS_TANMATSU) || defined(HAS_TDECK_GT911) || defined(HAS_THINKNODE_M9)
/*******************************************************************************
* Size: 20 px
+42 -8
View File
@@ -27,6 +27,9 @@ static Config safeDefaults() {
c.boot_wifi_open = 0;
c.theme_mode = 0;
c.gps_fuzz_m = 0;
c.attaky_notify_enabled = 0;
c.attaky_notify_room_color = 0;
c.attaky_notify_dm_color = 1;
return c;
}
@@ -85,6 +88,9 @@ int main() {
assert(migrated.loud_alerts == 0);
assert(migrated.theme_mode == 0);
assert(migrated.gps_fuzz_m == 0);
assert(migrated.attaky_notify_enabled == 0);
assert(migrated.attaky_notify_room_color == 0);
assert(migrated.attaky_notify_dm_color == 1);
// Reproduce beta 57 exactly: retry was inserted before the old suffix, then
// the v43 bytes were copied and the full shifted v44 structure was written.
@@ -116,7 +122,9 @@ int main() {
constexpr size_t v53_size = offsetof(Config, boot_wifi_time);
static_assert(v53_size + sizeof(Config::boot_wifi_time) + sizeof(Config::boot_wifi_open)
+ sizeof(Config::loud_alerts) + sizeof(Config::theme_mode)
+ sizeof(Config::gps_fuzz_m) == sizeof(Config),
+ sizeof(Config::gps_fuzz_m) + sizeof(Config::attaky_notify_enabled)
+ sizeof(Config::attaky_notify_room_color)
+ sizeof(Config::attaky_notify_dm_color) == sizeof(Config),
"v53 is the current layout minus every byte appended since");
Config v53 = safeDefaults();
@@ -155,6 +163,9 @@ int main() {
current.loud_alerts = 1;
current.theme_mode = 1;
current.gps_fuzz_m = 150;
current.attaky_notify_enabled = 1;
current.attaky_notify_room_color = 6;
current.attaky_notify_dm_color = 4;
migrated = safeDefaults();
assert(TouchPrefsSchema::overlayStored(migrated, &current, sizeof(current), &stored_version));
assert(stored_version == TouchPrefsSchema::CURRENT_VERSION);
@@ -177,15 +188,16 @@ int main() {
assert(migrated.app_hide == safeDefaults().app_hide);
assert(migrated.theme_mode == 0);
// v56 appends only theme_mode. It was written as v54 on the contributing
// v56 appended only theme_mode. It was written as v54 on the contributing
// branch, but v54 and v55 were taken by boot_wifi_* and loud_alerts before it
// merged, so the field sits behind those and the blob one version short of
// current is the one that must leave it at the Night default.
// merged, so the field sits behind those and a historical v55 blob must leave
// it at the Night default.
constexpr size_t v55_size = offsetof(Config, theme_mode);
static_assert(offsetof(Config, gps_fuzz_m) + sizeof(Config::gps_fuzz_m) == sizeof(Config),
"gps_fuzz_m must remain last");
static_assert(v55_size + sizeof(Config::theme_mode) + sizeof(Config::gps_fuzz_m) == sizeof(Config),
"v55 is the current layout minus theme_mode and gps_fuzz_m");
static_assert(v55_size + sizeof(Config::theme_mode) + sizeof(Config::gps_fuzz_m)
+ sizeof(Config::attaky_notify_enabled)
+ sizeof(Config::attaky_notify_room_color)
+ sizeof(Config::attaky_notify_dm_color) == sizeof(Config),
"v55 is the current layout minus every byte appended since");
Config v55 = safeDefaults();
v55.ver = 55;
v55.kb_force_legacy = 1;
@@ -198,6 +210,28 @@ int main() {
assert(migrated.loud_alerts == 1);
assert(migrated.theme_mode == 0);
assert(migrated.gps_fuzz_m == 0);
assert(migrated.attaky_notify_enabled == 0);
assert(migrated.attaky_notify_room_color == 0);
assert(migrated.attaky_notify_dm_color == 1);
constexpr size_t v57_size = offsetof(Config, attaky_notify_enabled);
static_assert(v57_size + sizeof(Config::attaky_notify_enabled)
+ sizeof(Config::attaky_notify_room_color)
+ sizeof(Config::attaky_notify_dm_color) == sizeof(Config),
"v57 is the current layout minus the Attaky notification fields");
Config v57 = safeDefaults();
v57.ver = 57;
v57.gps_fuzz_m = 250;
v57.attaky_notify_enabled = 1; // outside the stored v57 extent
v57.attaky_notify_room_color = 6;
v57.attaky_notify_dm_color = 5;
migrated = safeDefaults();
assert(TouchPrefsSchema::overlayStored(migrated, &v57, v57_size, &stored_version));
assert(stored_version == 57);
assert(migrated.gps_fuzz_m == 250);
assert(migrated.attaky_notify_enabled == 0);
assert(migrated.attaky_notify_room_color == 0);
assert(migrated.attaky_notify_dm_color == 1);
Config invalid = safeDefaults();
uint8_t garbage[sizeof(Config)] = {};
@@ -46,6 +46,7 @@ static const uint8_t SHIFT_HALF = 0, SHIFT_ROW = 3, SHIFT_COL = 0;
static bool s_inited = false;
static bool s_present[2] = { false, false };
static uint8_t s_notify_color = ATTAKY_NOTIFY_OFF;
static uint8_t s_prev[2][5] = { { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF },
{ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF } };
@@ -72,6 +73,11 @@ static uint8_t awRead(uint8_t addr, uint8_t reg) {
return (uint8_t)Wire.read();
}
static uint8_t p1Value(uint8_t row_bits) {
const uint8_t led_bits = (uint8_t)((~s_notify_color & 0x07u) << 5);
return (uint8_t)((row_bits & 0x1Fu) | led_bits);
}
static bool initHalf(uint8_t addr) {
bool ok = true;
ok &= awWrite(addr, AW_REG_SWRST, 0x00);
@@ -81,7 +87,7 @@ static bool initHalf(uint8_t addr) {
ok &= awWrite(addr, AW_REG_CFG_P0, 0xFF);
ok &= awWrite(addr, AW_REG_CFG_P1, 0x00);
ok &= awWrite(addr, AW_REG_OUT_P0, 0x00);
ok &= awWrite(addr, AW_REG_OUT_P1, P1_IDLE);
ok &= awWrite(addr, AW_REG_OUT_P1, p1Value(P1_IDLE));
return ok;
}
@@ -97,11 +103,11 @@ static void ensureInit() {
static void scanHalf(uint8_t addr, uint8_t cols[5]) {
for (uint8_t row = 0; row < 5; row++) {
awWrite(addr, AW_REG_OUT_P1, (uint8_t)(ROW_DRIVE[row] | 0xE0));
awWrite(addr, AW_REG_OUT_P1, p1Value(ROW_DRIVE[row]));
delayMicroseconds(50);
cols[row] = (uint8_t)(awRead(addr, AW_REG_IN_P0) & 0x1F);
}
awWrite(addr, AW_REG_OUT_P1, P1_IDLE);
awWrite(addr, AW_REG_OUT_P1, p1Value(P1_IDLE));
}
void attakyKeyboardPoll(bool active) {
@@ -155,4 +161,20 @@ int attakyKeyboardReadKey() {
bool attakyKeyboardPresent() { return s_present[0] || s_present[1]; }
bool attakyKeyboardSetNotificationColor(uint8_t color_mask) {
s_notify_color = color_mask & 0x07u;
ensureInit();
if (!attakyI2cLock(20)) return false;
bool wrote = false;
bool ok = true;
for (int h = 0; h < 2; ++h) {
if (!s_present[h]) continue;
wrote = true;
if (!awWrite(KB_ADDR[h], AW_REG_OUT_P1, p1Value(P1_IDLE))) ok = false;
}
attakyI2cUnlock();
return wrote && ok;
}
#endif
@@ -5,8 +5,16 @@
#include <stdint.h>
enum : uint8_t {
ATTAKY_NOTIFY_OFF = 0,
ATTAKY_NOTIFY_BLUE = 1u << 0,
ATTAKY_NOTIFY_GREEN = 1u << 1,
ATTAKY_NOTIFY_RED = 1u << 2,
};
void attakyKeyboardPoll(bool active);
int attakyKeyboardReadKey();
bool attakyKeyboardPresent();
bool attakyKeyboardSetNotificationColor(uint8_t color_mask);
#endif