- Device info reply now ends with byte 80 (repeat on/off) and byte 81 (path hash mode), as upstream v9/v10, so the app can show Meck's path hash size.
- Companion repeat mode, switched on only from the app: forwards packets while on. The app's radio command refuses repeat except on 433.000, 869.495 or 918.000 MHz; command 60 returns those frequencies. Stored at prefs byte 62 (spare, so existing devices start with it off).
- While repeat is on, Settings, the serial CLI and import accept only those three frequencies. Settings shows a pop-up (English and French).
- main.cpp: meckShowAlert now defined in every display build (was WiFi builds only), for the Settings pop-up.
EOF
Updated firmware versions for the Max and firmware build date.
New README_FR.md:
- French translation of the full README, AI translated
- Note at the top in French and English: the developer does not speak French, the French version is entirely AI translated, and suggested translations can be posted in the Meck channel on the MeshCore Discord or on the repo's Issues page
- Link back to the English README
- On-screen labels shown as the device's French text with the English in brackets; labels the device only shows in English stay in English
- French headings, with all in-page links pointing to the French anchors
README.md:
- Link to the French version at the top
- Accents: Noto Sans shows accents at every size; Montserrat shows them in Tiny-size text and titles, but not in 9pt text (Larger size, and some text that is always 9pt such as the WiFi home page status lines)
- Settings table: the font rows are now named Font Size and Font, as on the device
v1.15 additions:
- New sections: Timezones (World Clock), Experimental Features (Language, Change Backlight to Alt+B on the Max, RX Boosted Gain, AGC Reset Int, Delete all contacts)
- Combined BLE & WiFi build: Max only in v1.15, T-Deck Pro combined build in a future release; added to the Max build table and contents
- WiFi: reason line on Could not connect; WiFi on/off from the WiFi home page; Connecting to Saved Wifi popup
- Export and import popups; Delete all contacts pointer in Contacts
- Settings channel rows switch between start and end when too long
- DMs and channel messages use the companion app's chosen region first
- Channel data for companion apps; +15 dBm Bluetooth on meck_max_ble and meck_max_ble_wifi
- Accents: Noto Sans at both sizes and Montserrat at Tiny now show them
- Road-map: v1.15 items; T-Deck Pro combined build as a to-do
Accuracy fixes:
- Max included where text said Pro only: OTA update, path hash S key, MeshCore Flasher, RTC, repeater admin keyboard, MeshCore Clients
- P and Mic keys: Pro Audio & Max
- Voice notes: Max records, receives and plays
- WiFi builds turn WiFi on at boot and join the saved network; only the combined build starts with WiFi off. WiFi build list now includes meck_audio_wifi_hyn and meck_max_wifi
- MeshCore Clients note: Bluetooth off at boot on all builds; WiFi and combined build behaviour
- TX Power range 1-22 dBm
- Two LilyGo devices, not three
- OTA Tools do nothing on the 40 MHz builds
- Build tables: added the three Pro HYN builds and the three 40 MHz builds; new Touch chips and HYN builds and 40 MHz battery-saver builds sections
- Max touch text expanded (CST328 on the Max; CST3530 on later Pro v1.1 units)
- New Canned Messages (T-Deck Max) section; speech bubble row updated
- Frontlight: Alt+B toggle documented
- Settings table: LoRa Antenna, Canned Messages, WiFi, WiFi Radio and 4G Modem rows
- Guide links now match the real file names
Removed: Meck Watch paragraph
Release note
Combined Bluetooth + WiFi build (meck_max_ble_wifi): switching between Bluetooth and WiFi now restarts the device each time, because Bluetooth only frees its memory on a restart and WiFi cannot start without it. Whichever you turn on first after a restart works straight away; turning on the other one shows a restart hint, asks you to press Enter again, restarts, and turns the chosen connection on by itself. From the companion app, set wifi.enabled now takes effect at the app's reboot.
Why
Bluetooth off keeps the Bluetooth stack's memory until a restart; WiFi off frees WiFi's driver. On device, WiFi on/off, Bluetooth on/off, then WiFi on failed ("failed to create task", largest free block 7668 bytes) and the app could not connect over Bluetooth until a restart.
Changes (combined build only, except README)
- main.cpp: tracks which connection has been on since this restart; a switch then needs a restart (also Bluetooth after the Web Reader freed it).
- main.cpp: "Switch to WiFi?" / "Switch to Bluetooth?" popup with "Enter again to restart"; a second press within 5 s shows "Restarting..." and restarts, saving pending contacts first (new MyMesh::saveContactsIfDirty(), as CMD_REBOOT does).
- main.cpp: the choice survives the restart in RTC memory and the connection turns on once the home screen shows. Normal restart or power-on: both off.
- main.cpp: app "set wifi.enabled" keeps the choice for the app's reboot instead of switching after 1 s. Reply text unchanged.
- UITask.cpp: restart hints ("restart into Bluetooth:" / "Press Enter to Restart into Wifi") only when a restart is needed; Enter or long press uses the confirmation.
- SettingsScreen.h: WiFi Radio and WiFi setup rows use the same confirmation.
- French added for all new text.
- README.md: new "Combined Bluetooth + WiFi build (T-Deck Max)" section.
Testing: PC harness on the real new logic: both directions, late and timely confirm, start-up pickup after the splash, app wifi.enabled 1/0, Web Reader case, normal restart and power-on leftovers. Full firmware build not run.
Import (main.cpp)
- Enter on "Import from SD" now shows "Importing... / Please Wait..." at once (French added). The import runs from loop() 1.5 s later (meckImportPoll), so the popup is on the e-ink while the import blocks; the existing result popups follow. Same pattern as the export popup (05949f21).
- An import that contains an identity still restarts the device itself 2 s after finishing (MeckImport.h), so the popup stays until the restart.
RX Boosted Gain (Settings > Experimental Features)
- New saved setting rx_boosted_gain (NodePrefs.h), default ON for new devices (MyMesh constructor) and for older prefs files.
- New row "RX Boosted Gain: ON/OFF" toggles and saves it (French added).
- The main loop's 500 ms re-assert (main.cpp) now applies the chosen mode: radio_reset_agc() (boosted) when ON, radio.setRxBoostedGainMode(false) when OFF. Previously boosted gain was always forced on, so OFF could not stick.
AGC Reset Int (Settings > Experimental Features)
- New saved setting agc_reset_interval, stored as seconds / 4 in one byte as upstream's companion does. Default 0 (off).
- New row "AGC Reset Int: Off" / "AGC Reset Int: 60 s" (French added). Enter opens text entry, as for channel regions; Shift+Backspace cancels. The value rounds down to a multiple of 4, empty or 0 means off, and it is capped at 1020 s (upstream wraps round above that).
- MyMesh::getAGCResetInterval() now returns it in milliseconds, as upstream, so the Dispatcher's periodic resetAGC() runs at that interval. Previously it was always off (base default 0).
- Label "AGC Reset Int" matches the Repeater admin screen; "AGC Reset Interval" did not fit at Larger in Montserrat.
Saved settings (DataStore.cpp)
- The two settings are appended at bytes 1524 and 1525 (checked against the real savePrefs write sequence); the prefs file grows from 1524 to 1526 bytes. Older files short-read the new fields and keep the defaults.
Charger readout (UITask.cpp)
- The temporary 10 s chargerDebugPrint() serial call is commented out while debugging BLE/WiFi. The readout code stays for the charger investigation.
Testing
- PC harness on the real new code: import popup timing and all three results in English and French; both rows' display, toggling and typed values (60, 30 -> 28, 1020, 5000 -> 1020, empty, junk) with the resulting Dispatcher interval; prefs byte offsets computed from savePrefs.
- Full firmware build not run in the test environment.
Ports upstream MeshCore's channel datagram support (dz0ny, 9b842786, 5 Mar 2026, with follow-ups a21b83b1, ed326255 and 1d61df72). Upstream added it at protocol version 10, so apps may already send it to Meck, which reports 11.
Before: Meck decrypted incoming channel datagrams and silently dropped them, and the app's send command (62) was answered "unsupported".
Changes:
- BaseChatMesh::onGroupDataRecv() handles PAYLOAD_TYPE_GRP_DATA (2-byte little-endian data type, 1-byte length, payload), drops short or malformed packets, and passes the data to a new onChannelDataRecv() hook. Group text now checks the packet length before reading the text type byte.
- New BaseChatMesh::sendGroupData(): flood (using the channel's region scope) when no path is given, otherwise direct. Maximum payload MAX_GROUP_DATA_LENGTH (165 bytes).
- Companion: received datagrams go to the app as RESP_CODE_CHANNEL_DATA_RECV (27) and count as channel messages in the offline queue. CMD_SEND_CHANNEL_DATA (62) is handled with upstream's error codes.
- DATA_TYPE_RESERVED (0x0000) and DATA_TYPE_DEV (0xFFFF) defined.
Channel data is passed to the app only, not shown on the device. Upstream's MyMesh listener hook is not ported (Meck has no listener interface).
Tested on a PC against Meck's real Mesh and BaseChatMesh code with real encryption (AddressSanitizer on): flood and direct datagrams arrive intact; 165 bytes accepted, 166 refused; a malformed length is dropped; another channel's data is ignored; channel text is unaffected. The app frame and command 62 (flood, direct, 2-byte path hashes, every error case) were tested separately.
Issue #20 (rolling-window duty cycle) was already done in 444631e0 (30 Jun 2026) and shipped in v1.12.4: Dispatcher uses upstream's token bucket (upstream PR #1297) over a one-hour window instead of the old per-transmission silent period. Re-checked against upstream's current Dispatcher: same logic, and companion builds take the airtime factor from saved settings (default 1.0), as upstream does.
Settings > Experimental Features
- SettingsScreen.h: "Experimental Features >>" moved from the end of the
top-level list to directly under "Rx Log >>".
- New Language row (all T-Deck builds), first in the Experimental
sub-screen. It shows the action: "Language: Change to French" in
English, "Langue : Passer a l'anglais" (with the accent) in French.
Enter toggles and saves.
- Choosing French swaps the Classic font style to Noto Sans (Classic has
no accents); the text size is left alone. While French is on, the Font
picker skips Classic. Switching back to English leaves the font as is.
- NodePrefs.h, DataStore.cpp: new pref ui_lang at byte 1523
(0 = English, 1 = French). The prefs file grows to 1524 bytes; older
files load as English.
Translation framework
- MeckLang.h (new): MECK_TR("English", "French") picks the string for
the current language; meckUiLang() in main.cpp reads ui_lang. French
strings are UTF-8, written as \x escapes so source files stay ASCII.
French translations (UITask.cpp)
- Home tiles, Phone tile and the status strip.
- First view of each home page: Recent heard, Radio, Bluetooth, WiFi,
Advert, GPS, Battery gauge, Hibernate/power off options, Timezones.
- Start-up key hint box.
- Not yet translated: Settings and every other screen, and the home
pages' sub-states (GPS UTC editor, power-off confirmation, Timezones
editor hints).
Fonts
- New Adafruit GFX fonts covering U+0020-U+017F (ASCII, Latin-1
Supplement, Latin Extended-A: French, Czech and other European
accents), generated with stock fontconvert from the same sources as the
fonts they replace. Their ASCII glyphs are byte-identical, so English
text is unchanged:
- fonts/NotoSans7pt8b.h (Noto Sans Tiny), from Google Fonts'
NotoSans[wdth,wght].ttf
- fonts/Montserrat7pt8b.h (Montserrat Tiny), from Google Fonts' static
Montserrat Regular (npm @expo-google-fonts/montserrat 0.4.2)
- fonts/NotoSansBold12pt8b.h (Noto titles and clock face), from Google
Fonts' static Noto Sans Bold (npm @expo-google-fonts/noto-sans 0.4.2)
- MeckFonts.h: Noto size 0 -> NotoSans_Regular7pt8b, Noto sizes 3 and 5
-> NotoSans_Bold12pt8b, Montserrat size 0 -> Montserrat_Regular7pt8b.
- Removed the replaced fonts: NotoSans7pt7b.h, Montserrat7pt7b.h,
NotoSansBold12pt7b.h.
- Effect for all users: accented letters in names and messages now show
at Tiny size with Noto Sans and Montserrat, instead of being reduced to
plain letters. Classic is unchanged.
- Still without accents: Montserrat at Larger (Montserrat9pt7b).
Display driver
- GxEPDDisplay.cpp: measureTextRawStyled() and drawTextRawStyled() (home
tiles and status strip) now decode UTF-8 when the font has accented
glyphs: ASCII through Adafruit GFX as before, other characters from the
font's glyph table, as print() does. Plain ASCII labels take the old
path unchanged.
Tested on device (7 Oct 2026): Classic + Tiny -> Change to French swaps
to Noto Sans automatically; the home tiles and translated home pages
display correctly in Noto Sans Tiny. Not yet tested: French at Larger,
French with Montserrat, accented 12pt bold titles (accented capitals are
6 px taller than English ones), English screens for regressions.
Version
- MyMesh.h: FIRMWARE_VERSION default "Meck v1.14" -> "Meck v1.15";
FIRMWARE_BUILD_DATE updated.
- variants/lilygo_tdeck_pro/platformio.ini,
variants/lilygo_tdeck_max/platformio.ini: FIRMWARE_VERSION strings
v1.14 -> v1.15.
Settings > Experimental Features
- SettingsScreen.h: new "Experimental Features >>" folder row as the last
top-level Settings row, opening "Settings > Experimental".
- Delete all contacts (all T-Deck builds), ported from Meck-P4 v0.7.4.
Flow: "Delete all contacts?" box (Enter:Yes Q:No) -> second "Are you
sure? This cannot be undone." popup (its Enter is ignored for the
first second) -> "Purging" -> "Contacts deleted" or "Purge failed" ->
restart 3 s later. Removes every contact, with favourites and custom
paths, and the DM history; channel messages are kept. Q cancels at
either confirmation; keys are ignored once the purge starts.
- MyMesh.h: purgeAllContacts() closes any chunked contact save still in
progress, empties the contact table and saves the empty set.
- ChannelScreen.h: purgeDMs() marks the DM entries in
/meshcore/messages.bin empty in place and re-saves, keeping channel
messages, and clears the DM unread count.
- main.cpp: meckPurgeAllContacts() runs both and checks the contacts file
is empty afterwards. All touch is ignored while the purge box is open.
- Change Backlight to Alt+B (Max only): ON stops the heart touch key
toggling the frontlight, and Alt+B then lights it at the Backlight
Brightness setting. OFF leaves both as before.
- NodePrefs.h, DataStore.cpp: new pref backlight_alt_b_only at byte 1522
(prefs file now 1523 bytes); older files load it as 0 (heart key on).
Timezones comment "these final fields" -> "these fields".
TEMPORARY (Max only; remove after the charging investigation)
- TDeckProMaxBoard.h, TDeckProMaxBoard.cpp: chargerDebugPrint(), a
read-only SY6970 status, fault and register dump plus a BQ27220 gauge
line on serial.
- UITask.cpp: calls it every 10 s from UITask::loop().
What changed
Quiet period 2 s → 20 s. A background save now only happens after the cart RAM has been untouched for 20 seconds, so ordinary in-game writes during active play no longer trip it. A real menu save is followed by a pause, so it still gets written.
Checksum guard. Before any save, background or on-quit, the live cart RAM is checksummed against what was last written; if the bytes are identical, nothing is written to the card. The game replaying identical state, which is what caused the save-just-after-opening, no longer touches the SD card at all. Seeded from the loaded .sav at launch, so opening a game and idling never writes.
The emulator needs 240 MHz to run at speed, so it has no place on the
40 MHz builds (meck_max_standalone_40mhz and the Pro's 40 MHz env).
- GamesMenuScreen.h: the Game Boy entry is omitted from the games menu
on MECK_40MHZ_TEST builds (NUM_GAMES drops to 2, in lockstep with the
entry list). Snake and Minesweeper are unaffected -- they run fine at
40 MHz. The menu simply shows two games instead of three.
- UITask::gotoGBCScreen(): refuses on MECK_40MHZ_TEST with a brief
"Game Boy needs 240MHz" alert and stays put. This is the single point
every launch route (tile, J-key, games menu) funnels through, so it
backstops the menu omission -- nothing can reach the emulator on
those builds.
Files: examples/companion_radio/ui-new/{GamesMenuScreen.h, UITask.cpp}
SOUND (Max, ES8311; Pro stays silent)
- minigb_apu vendored from Meck-P4 (MIT), run at 22050 Hz: at 44100 it
cost 3.6-4.0 ms of the 16.7 ms frame and the game fell to 0.98x, which
drained the DMA queue and glitched. Per-frame timings (core / apu /
i2s wait) now print with the speed line.
- Bring-up reuses the notification-tone path: shared Audio object owns
I2S port 0 (with MCLK), XL9555 routing and amp, then codec init after
the clocks are running. Raw PCM written per frame; a repeated sample
every sixth frame keeps production a hair above the DAC so its clock
paces the game instead of the queue running dry.
- Software gain 1/8 (codec DAC sits at 0 dB); GBC_AUDIO_SHIFT to tune.
- 950 ms delay line (PSRAM) so sound lands with the picture, which
reaches the glass ~1 s after its frame; GBC_AUDIO_DELAY_MS to tune.
- Games start muted; mic key toggles the codec mute in both keyboard
drivers' raw mode; footer shows the current hint. Amp off on quit.
- Notification tones skipped while a game runs (game owns the codec).
MESH LIVE DURING PLAY
- The OTA-style radio pause is gone (GBC_PAUSE_RADIO=1 restores it):
the mesh runs on the other core and already tolerates the panel
blocking the loop. Game header now matches the home screen's -- the
cumulative unread channel count where the node name goes, clock,
battery percent. "New:" toasts suppressed only while a ROM is running
(Snake and Minesweeper keep them).
DISPLAY
- Fast register waveform set to 12 frames per push (FAST_LUT_FRAMES):
judged the best balance of UI speed and contrast. Interface only; the
game keeps the factory waveform.
- Windowed game refresh dropped: measured to save nothing, and it left
the footer row unrefreshed. Normal partial refresh, change-detected.
- Adafruit GFX auto-wrap turned off in the e-ink driver (all e-ink
builds): it dropped a glyph onto the next line's margin when a line
ran a few pixels past the edge (seen with Larger Noto in compose).
Compose wrap width also allows for the driver's 2-unit x offset.
- Game start does one full refresh (tried as a ghosting fix; NPC
ghosting in Pokemon Gold is the game + dither + factory partial
waveform and shows on the stock build too).
Files: examples/companion_radio/{main.cpp, ui-new/GBCEmulatorScreen.{h,cpp},
ui-new/UITask.cpp, ui-new/minigb_apu.{c,h}}, src/helpers/ui/GxEPDDisplay.cpp,
src/helpers/ui/GxEPD2_310_GDEQ031T10_FAST.cpp,
variants/lilygo_tdeck_{max,pro}/TCA8418Keyboard.h
Driver: setTextWrap(false) once at init. GFX no longer moves text to the next line by itself, anywhere. This is what put the stray "t" on the margin; without it, a line the screen miscounts is clipped at the edge instead, which is the lesser evil for a UI that does all its own wrapping. Applies to every e-ink build, Pro and Max.
Compose: the usable line width is now the panel width less the driver's 2-unit x offset, so the compose wrap and the glyphs' real positions agree and lines wrap one word earlier when they need to.
GxEPD2_310_GDEQ031T10_FAST.cpp → fingerprint 473 (was 464). The 9-frame bytes (0x89 / 0x49 / 0x09) become 0x8C / 0x4C / 0x0C: same level bits, count 12. Improves colour density in Max fastlut test build.