Skip to content

Meck-P4 v0.3.3: MeshCore Config Import, BQ27220 Calibration, UI Polish

Pre-release
Pre-release

Choose a tag to compare

@pelgraine pelgraine released this 18 May 02:47
· 89 commits to main since this release

One-step config import from the MeshCore mobile app so existing users can move to a P4 without re-entering everything, a firmware-side fix for the BQ27220 fuel gauge calibration so the battery percentage matches reality, and substantial UI work on the home screen, Settings, and tile layouts. Plus a small power-savings pass that trims about 38 mA off idle draw.

⚠️ Still a pre-release. ⚠️ Direct messaging, roomserver access, repeater admin, trace route, web browser, IRC, and the BLE companion path are still not in this build. See What's Not Yet Implemented for the full pending list.

First-Time Flashing: Read This First

Meck-P4 ships as a single merged binary (bootloader + partition table + application combined). One file, flash at offset 0x0.

An SD card is recommended. With a FAT32-formatted card inserted, every saved setting and channel message is mirrored automatically, audio files have somewhere to live, and the device recovers gracefully from a wiped NVS. Without an SD card the device still works but loses message history on reboot.

Ensure you use the right-side USB-C port (the data port), not the high-speed charger port, to flash.

Flashing with the MeshCore Web Flasher (recommended)

  1. Go to https://flasher.meshcore.io/
  2. Scroll to the bottom and select Custom Firmware
  3. Select the meck-p4-0.3.3-version-merged.bin file you downloaded
  4. Click OK on the merged-binary warning
  5. Click Flash, pick your device in the popup, and click Connect

Flashing with esptool.py

pip install esptool
esptool.py --chip esp32p4 -p PORT write_flash 0x0 meck-p4-0.3.3-version-merged.bin

(Replace PORT with /dev/cu.usbmodemXXXX on macOS, /dev/ttyACM0 on Linux, or COM3 on Windows.)

Upgrading from v0.3 does not require erase_flash. The prefs loader has been made tolerant of older shorter-blob layouts, so any new fields added in v0.3.3 (such as Font Size) come up at default rather than wiping existing settings.

⚠️ AMOLED variant remains untested by the maintainer. The -amoled.bin is built from the same source tree with the AMOLED display option selected in menuconfig, but the project maintainer doesn't have the AMOLED hardware on hand. The TFT variant is the tested and known-working build.

What's New Since v0.3

MeshCore App Config Import

Drop your MeshCore mobile or desktop app config export onto the SD card, reboot, and your identity, channels, contacts, node name, and radio settings come across in one step. No more re-entering contacts or generating a fresh keypair when moving to the P4.

How to use:

  1. In the MeshCore mobile or desktop app, export your config as JSON
  2. Rename to import.json and copy to /sdcard/meshcore/import.json on the P4's SD card
  3. Reboot the device

On boot, Meck reads the file and applies it:

Field Behaviour
Identity (private + public key) Replaces _main.id atomically (tmp + rename). Existing identity is overwritten.
Channels Merged by 16-byte secret. Existing channels kept; new ones appended to free slots; duplicates skipped.
Contacts Merged by public key. Existing kept; new ones appended. Lat/lon converted from string-decimal to int32 e7. custom_name preferred over name.
Node name Replaces prefs.node_name.
Radio settings Replaces freq, BW, SF, CR, TX power on P4NodePrefs. Freq converted kHz to MHz, BW Hz to kHz.

On success the file is moved to /sdcard/meshcore/import.history/import-<unix_epoch>.json so it doesn't re-apply on subsequent boots and leaves an audit trail. On parse failure (bad JSON, missing identity keys, wrong-length hex) a warning is logged and the file is left in place for inspection.

Imported names (node, channels, contacts) are stripped to printable ASCII before being stored. Meck-P4's Montserrat fonts cover Latin-1 only, so emoji codepoints would otherwise render as tofu boxes. Leading and trailing whitespace left by stripped emoji is also trimmed.

BQ27220 Battery Calibration

Firmware-side calibration for the BQ27220 fuel gauge, fixing the long-standing issue where the chip's reported Full Charge Capacity stayed pinned at the factory default of 3000 mAh even after set_design_capacity(1000) had been written. The displayed percentage now tracks the cell voltage instead of drifting.

What changed:

  • New meck_battery_calibrate() runs on every boot from meck_app_init(). It reads the chip's current Design Capacity and Full Charge Capacity, and if FCC is not already in the correct range it runs the full TI procedure: Unseal + Full Access, Enter CFG_UPDATE, write Design Energy / Qmax / stored FCC via MAC differential checksum, Exit with REINIT, Seal, then RESET to force the gauge to recompute against the new values.
  • Self-gated: returns in milliseconds on boots where the chip already reports the correct FCC. The full procedure only runs on first boot after upgrading, plus on the rare boot where something has re-staled the chip's data memory.
  • The header battery percentage now reads from the chip's coulomb counter (meck_battery_pct_from_chip()), which is the authoritative measurement post-calibration. Previous builds used a voltage-curve estimate as a stopgap because the chip's stale stored FCC made the chip-based reading unusable.
  • The Battery Gauge detail tile now shows just the raw mV next to "Voltage", since the voltage-derived percentage was misleading under load (the cell sags ~50 to 100 mV at typical discharge, dragging the curve estimate down well below the chip's coulomb-counter truth). Voltage-vs-chip-SoC disagreement still surfaces a note if the gap is greater than 30 points (raised from 15 to clear typical under-load divergence).

No EV2400 dongle required. If your gauge still reports 3000 mAh FCC from an earlier build, the calibration runs once on first boot of v0.3.3 and brings it back into line.

UI: Clock and Battery in the Header

Clock and battery percentage now appear in the top-right corner of every home tileview page (7 tiles) and every non-home full screen (Settings, Settings/Contacts, Radio Picker, Channel Picker, Messages, Contacts, Discover). Contact Detail and modal overlays are deliberately excluded; Contact Detail has its Fav button in that corner, modals were too cluttered.

  • Per-screen font matches the local title size (24pt on most screens, 22pt on Messages to match the channel-name title)
  • Battery field uses %3u%% so the bounding box stays constant width regardless of value; the layout doesn't jitter as digits change
  • Battery is colour-coded: green ≥70, orange 40 to 69, yellow 20 to 39, red <20
  • Clock is blank until the RTC has synced (epoch ≥ 1750000000)

UI: Settings Screen Polish

Brightness slider replaces the old 8-step ladder. 12% to 100% in 1% increments, live-applies while dragging, persists on release. Slider accent uses the same cyan as the Identity row.

Font Size cycles three states: Classic / Larger / Extra Large. The font registry walks on toggle and re-applies the right scaled font to every registered text object live, so the change is visible immediately. Extra Large double-jumps where possible (14 to 18, 16 to 22, 18 to 24, 22 to 28). 28pt remains the ceiling.

UI: Home and Modal Layout

  • Home tile 0's node name auto-sizes by character count: ≤12 chars → 28pt, 13 to 17 → 24pt, 18 to 19 → 22pt, ≥20 → 18pt. Title refreshes every tick so renames live-resize.
  • Home tile 0's freq/BW/SF/CR/RX/dBm block was removed (replaced by clock+battery in that corner). RX Packets moved into the Radio Details tile alongside Frequency, Bandwidth, SF, CR, TX Power, Sync Word, and Noise Floor.
  • Tile titles bumped to 28pt across Recent Heard, Radio Details, Adverts, GPS, and Battery Gauge.
  • Tile 3 renamed from "Advertising" to "Adverts" to match MeshCore terminology.
  • Body text on Recent Heard, Radio Details, and Battery Gauge tiles shifted from y=60 to y=80 so the first line clears the camera punch-hole at Larger and Extra Large font scales.
  • Edit Node Name and Add Channel modal titles moved from y=20 to y=50 to clear the punch-hole. Dependent textareas and hints shifted down accordingly.

Power Savings

A small idle-current improvement. Three changes landed:

  • Ethernet PHY held in reset. The IP101GRI ethernet PHY is unused by Meck and was previously powered up by LilyGo's Ethernet_Init(). The reset line is now driven LOW at boot, the Ethernet_Init call is skipped entirely, and the device_ethernet_task no longer spawns. Removes the tiT (LWIP) and emac_rx tasks from the runtime task list and stops needlessly powering the PHY chip.
  • ES8311 audio codec is no longer initialised eagerly at boot. The MeckAudio backend (introduced in v0.3) now owns the codec lifecycle and brings it up lazily on first audio request.
  • Dynamic frequency scaling configured in esp_pm_configure (360 MHz max, 40 MHz min). Light sleep is set in the config but couldn't be made to actually engage in this build, so DFS contributes less than originally hoped. Reasons under investigation; revisit in a future release.

Measured impact: about 38 mA off idle current. Modest but real.

Channels: Delete Actually Deletes History

Bug fix. Previously, deleting a channel and re-adding into the same slot inherited the old channel's message history. Root cause: deleteChannel compacted the in-memory channel records but left the per-channel .bin files on SD untouched, and didn't clear the in-RAM message rings either. The freshly created channel slid into the slot still pointing at stale state.

Fixed:

  • P4DataStore::deleteChannelMessageFile() and renameChannelMessageFile() added
  • MeckMesh::deleteChannel now deletes ch_<idx>.bin for the removed slot, renames ch_<N+1>.bin to ch_<N>.bin for every channel shifted down, and compacts the in-RAM rings (_msgs_ch[], _msg_count_ch[], _msg_newest_ch[], _msg_unread_ch[]) in lockstep under a mutex
  • Vacated tail slot is cleared so the next channel added won't pick up displaced state

Emoji-Safe Rendering for Contact and Heard Names

Contact names in the Contacts list and entries in the Recent Heard tile are now passed through strip_unrenderable() before display. Names like "🌱Camperdown⭐️" render as "Camperdown" instead of a row of tofu boxes. Channel names are not stripped since channels rarely include emojis. The chat compose path is unchanged; this only affects how names are displayed.

Discover Bug Fix

Removed a pre-seed loop from MeckMesh::startDiscovery() that pre-populated the discovered list from _recent[] at scan start. The pre-seed was meant to give the screen content immediately, but it conflicted with the per-scan random tag used to dedupe stale responses. Discover now starts with an empty list and fills in as live DISCOVER_RESP packets arrive; already-known repeaters typically respond within 1 to 3 seconds anyway, so the user-visible delay is minimal.

Other Changes

  • Prefs migration safety: the prefs loader now tolerates shorter blobs from older firmware. New fields added to P4NodePrefs (such as Font Size in this release) come up at default on first boot after upgrade rather than wiping existing settings.
  • Diagnostic stats task (meck_stats_task) added to main.cpp for capturing FreeRTOS task list, CPU runtime percentages, PM lock state, and a battery time-series on serial. Compiled in but not spawned by default; uncomment the xTaskCreate call near the end of app_main to enable.
  • Removed unused meck_set_antenna_default() function and its callers. The SKY13453 RF switch path only applies to LR1121 P4 variants; on the SX1262 board the call was effectively a no-op against a pin the standalone code already configures.
  • Build dependency added: espressif__cjson (used by the MeshCore config importer).

Known Limitations

  • First boot after upgrade adds about 2 seconds. meck_battery_calibrate() runs the full TI procedure including a 2-second post-RESET settling delay on the boot where the gauge actually needs remediation. Subsequent boots short-circuit at the in-band check and return in milliseconds.
  • BQ27220 calibration occasionally times out on the unseal step. The chip can re-seal itself between LilyGo's BSP init and Meck's calibration call, and the unseal handshake is timing-sensitive against any concurrent bus traffic. If you see meck_battery_calibrate: ERROR timeout entering CFG_UPDATE in the serial log, simply reboot; the next boot usually succeeds. A retry loop is on the list for v0.3.4.
  • Cover art still doesn't display in the audio player. Carry-over from v0.3. Music-note placeholder shows in the meantime.
  • MP3 files with large embedded album art can starve the IDLE task on the audio core and fire the watchdog. There's a defensive ID3v2-skip patch in firmware; the durable fix is tools/mp3_clean.py (see the audio guide).
  • AMOLED variant has not been flashed or tested by the maintainer. Reports welcome via Discord or GitHub Issues.
  • Default channel boots on Australia Narrow. If your local mesh has moved to a different preset (e.g. Australia Mid in Sydney as of mid-2026), change it in Settings → Radio Preset after first boot. The change persists.

What's Not Yet Implemented

Carried over from v0.3:

  • Direct messaging (DM compose, DM inbox with unread indicators, DM persistence to SD)
  • Private channel creation (no UI to generate new channel secrets or add shared ones; approach undecided)
  • Roomserver access (login flow, message handling, mark-read on login)
  • Repeater admin (login, clock sync push, send advert, get status, neighbours, version)
  • Trace route (view the relay path of a received packet)
  • Notes app
  • Audio cover-art rendering (pre-flight succeeds but heap allocation for the decoded framebuffer fails for typical cover sizes; needs decode-time downscale or a streaming decoder)
  • Emoji support (real Unicode codepoints with Twemoji-style colour font fallback in LVGL, plus an emoji picker accessible from the keyboard's symbols page; current builds strip non-ASCII from displayed names instead)
  • Web browser & IRC client
  • ESP32-C6 BLE companion firmware (the C6 is on the board but Meck-P4 doesn't drive it yet)
  • PCF8563 hardware RTC integration (init only, not yet read on boot or written on shutdown)
  • Deep sleep with wake-on-touch (auto-off dims the screen but the SoC doesn't yet enter deep sleep)
  • Map tile rendering
  • OTA firmware updates over WiFi via the C6
  • Region scope (MeshCore v1.15+ compatibility)

PRs welcome. See the full roadmap in the README.

Reporting Issues

The Meck-P4 channel on the MeshCore Discord is the fastest path. GitHub Issues on the Meck-P4 repo also work for anything reproducible. Include the serial log if you can (idf.py monitor or any serial terminal at 115200 baud will show what's happening).

License

MIT for Meck-specific code. The combined firmware binary links libraries with mixed licensing including GPL-3.0 and is effectively GPL-3.0 when distributed.