The exported log could not show what the client did when a channel's
phone notify mode was set, or why a channel message notified, so #1121
could not be diagnosed from a log.
- Every notify-mode save logs radio, slot, channel, a one-way PSK
fingerprint, old mode with its source, new mode, and the read-back.
A save refused for a missing radio key or PSK logs as an error.
- Any change to the notify table, by any path, logs before and after.
Settings that fail to parse at startup, or fail to write, log errors.
- The notify dialog and the channel action sheet log what they show.
- Every incoming channel message logs its notify decision: which
channel the slot held, from the live or cached list, the mode and
where it came from, and posted or suppressed with the reason.
- NotificationService queue, batch, suppress and failure paths go to
the app log instead of debugPrint.
- The app version is logged at startup and on every connect.
- appLogger no longer drops messages when the in-app log view is off;
the log file always receives them, as connector lines already did.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rebrand wrote Offband Meshcore, against the owner's capitalization. Launcher label, title, strings, notifications, window titles, web, release titles and docs are corrected. ProductName in Runner.rc deliberately keeps the old spelling: path_provider builds the Windows data folder from it, so changing it would orphan every Windows user's data. Bundle IDs, past release texts and the certificate subject are unchanged.
Epic #1014, Feature #200.
Agent: ChartreuseBay (session 425e424d)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A radio that will not connect is exactly when a user needs the log, and
the only way to it ran through device Settings, which needs a connected
radio. The scanner now has a settings icon that opens an app-only
settings screen: App Settings, Debug (Share logs, the logging switch,
both log viewers) and About.
Navigation only. How logs are written is untouched: Share logs calls the
same LogExport.shareLogs the log screens use, on the same on-disk file.
The device Settings screen keeps every category. Its About pane and the
app-level Debug rows moved into AboutView and DebugLogTiles, which both
screens now share; Device CLI, Serial capture and Mesh topology need the
radio and stay in device Settings only.
Export failures now persist until dismissed and are logged: "No log
available to export" was a 4-second snackbar, and a share or save that
threw had no handler at all. New owner-approved string:
debugLog_exportFailed. Other locales fall back to English per
untranslated.json.
Tests pin both sides with the radio disconnected: Share logs hands the
on-disk file to the share sheet, About shows the version, no device
category or radio row appears, both error messages persist, and device
Settings still has everything. Each check was made to fail against a
deliberately broken build first.
Part of Epic #693.
Agent: BrightDog (session 38fb0e08)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1. existingFiles() walked hardcoded names with per-name exists(), so the periodic flush could rotate mid-walk and return a name it had just confirmed and rotation then renamed. Handing the share sheet a path that no longer resolves fails the WHOLE export, which is the exact failure the rotated-log fix existed to prevent. Now one directory listing, filtered and ordered by name. flushAndGetAllFiles() degrades to the active file if listing throws, because the logger cannot report its own listing failure anywhere, and one log beats none. shareLogs() also re-checks existence at the last moment, since a trace can be pruned between collection and share.
2. forceDartIsolateBlock used DateTime.now() for its deadline. The wall clock can move backwards, which would push the deadline away and leave the app frozen past the window or indefinitely, needing a force-kill on a test build. Now a Stopwatch, which is monotonic, matching what the Kotlin block already did with nanoTime.
analyze clean, 1307 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1. The export shipped only the active log, and FileLogWriter._rotated was private, so offband.1.log and .2.log were unreachable by anything. An exit-record pointer line written at startup rotates out after a couple of MiB of ordinary logging, handing over a trace file with its metadata gone. FileLogWriter now exposes the files that exist, keeping the rotation naming where it belongs, and the mobile export attaches all of them.
2. dispose() called ioExecutor.shutdownNow() while a read could be holding a MethodChannel.Result. Interrupting it left the Result neither succeeded nor errored, so the Dart future never completed. Now shutdown(), with a disposed flag and a RejectedExecutionException path that always replies.
3. The trace read was skipped for REASON_CRASH_NATIVE on the assumption that its stream is always a protobuf. AOSP says a trace from an earlier ANR stays attached even when the process later died for another reason, so gating on the reason loses real text traces. The read is now attempted on every record and a printable-bytes check decides whether the content is usable.
4. The test-only main-thread block used a single Thread.sleep, which an interrupt could cut short: no ANR, no record, and a wasted trip to the hardware. It now sleeps to a deadline and ignores interrupts, which guarantees the duration without a busy-wait's CPU burn.
Review also flagged that FileLogWriter can exceed maxBytes for a single oversized entry. That is pre-existing writer semantics, already pinned by a test in this branch, and deliberately not changed here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
shareLogs() exported exactly one file, the active offband.log, and the rotated .1.log and .2.log were never exported at all. So a trace written anywhere other than inline would never have reached anyone, and a trace written inline would later rotate out of the only file that ships.
SharePlus already takes a list of files, so this attaches the trace files beside the log with no new UI and no new localized strings. Mobile only: the records are Android-only and the desktop path saves a single chosen file.
Without this the Epic would ship and deliver nothing, which is why it is part of the build rather than a follow-up.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mirrors stock's Export Config screen: section list, Select All and Deselect
All, everything checked by default. The asymmetry with import, which will
start with nothing checked, is stock's own and is deliberate: opt out on the
way out, opt in on the way in.
A section the user ticked that could not be gathered is shown before the file
is written, with its specific reason, and the user chooses whether to export
anyway. Firmware-built-without-it reads differently from device-did-not-answer
because only one of them is worth retrying.
The screen also states plainly that this is the stock format and cannot carry
Offband-only data, so importing the file back will not restore it.
File naming follows stock, <name>_meshcore_config_<stamp>.json, and is
sanitized for the filesystem without touching the name written inside the
file, which has to round-trip exactly. Real device names in the reference
corpus contain emoji and a trailing space, both covered by tests.
Writing reuses LogExport for the per-platform mechanism (share sheet on
mobile, Save As on desktop, download on web) rather than adding a second
export path that could drift. The temp file can contain the node private key,
so it is written under the app's own temp directory.
The public key is abbreviated on screen; the private key is never rendered.
6 tests on the file name.
A truncated caplog download used to completeError and discard every received
byte, so the user got nothing from a buffer still intact on the device (a tester
lost 8461/8608 bytes, 98.3%, deterministically per firmware#711 so retry can't
help). Now truncation is a soft outcome:
- downloadCaplog() returns a CaplogDownload (bytes + received/expected/chunks +
truncated) instead of throwing; busy/timeout still throw.
- serial_capture_screen still writes + LogExport.shareFile the partial bytes,
marks the file "# PARTIAL ..." with the counts, and shows a non-fatal warning
instead of a red error.
- Button label is now platform-aware (Download & save on desktop, & share on
mobile) via LogExport.actionVerb, matching the App/BLE log screens.
Replaces CaplogTruncatedException with CaplogDownload. Reassembler unchanged
(already keeps the bytes). Adds CaplogDownload contract tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ben's style rule: no em-dash character in copy, docs, or code comments
(it reads as an AI tell), and the repo is going public. Replaced the
em-dashes in code comments and English UI strings with commas, colons,
or periods, whichever reads best. User-facing strings were hand-tuned
for natural punctuation rather than a blanket comma.
Preserved the lone "no data" glyph placeholders (a standalone dash used
as a not-available indicator in status displays); those are a design
element, not prose.
Regenerated app_localizations*.dart from app_en.arb (the English
fallback for untranslated keys propagates to every locale's generated
file). Non-English ARB translations left untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- LogExport.shareLogs/shareFile now resolve ScaffoldMessenger and l10n strings
off the context BEFORE the flush await, and _exportFile takes them as values
(no BuildContext use across the async gap; avoids a deactivated-ancestor crash
on Back-during-flush).
- Web download: append the anchor to the document before click() (Firefox needs
a connected anchor) and revoke the object URL on a delay (synchronous revoke
can abort the download in Safari/iOS).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The share helper only invoked the OS share sheet on mobile; on all desktop it
opened the containing folder, which for a temp-dir capture dumped the user into
TEMP amid unrelated files. Now:
- Android/iOS: OS share sheet (unchanged).
- Windows/macOS/Linux: native Save As dialog (file_selector) writing the file
to a user-chosen location.
- Web: browser download of the log text (no on-disk file on web).
Adds file_selector; web download via a js_interop helper behind a conditional
import. Folds in #427 (localized share strings). LogExport.shareFile keeps a
compatible signature for the serial-capture screen (#430).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Route LogExport's Share tooltip, share subject, and file-logging-unavailable
snackbar through context.l10n instead of hardcoded English. Adds four keys to
app_en.arb (debugLog_shareLog, debugLog_openLogsFolder, debugLog_shareSubject,
debugLog_fileLoggingUnavailable) and regenerates all locales (English fallback
until translated). Resolves the deferred Gemini finding from #393.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract the BLE screen's on-disk log export into lib/utils/log_export.dart and
reuse it on the App-log screen, so both log screens have one consistent Share
action: OS share sheet on mobile, open logs folder on desktop. The shared file
already holds the app log and BLE frames combined.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
No in-app signal of which binary is running has caused repeated confusion
(a debug-signed APK silently not installing over the release app, old layout
persisting with nothing to indicate the new build never landed).
BuildInfo reads GIT_SHA/GIT_BRANCH/BUILD_TIME from --dart-define with dev
fallbacks, so every build carries its own identity independent of the pubspec
version. Surfaced as its own copyable Build row in Device Info.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Back at the root was calling SystemNavigator.pop(). That is not "background
the app": on Android it calls finish() on the activity, which tears down the
Flutter engine and drops the radio connection. Backing out and reopening
therefore landed on a disconnected radio needing a fresh connect. Wrong call
for the job, and worse than the behaviour it replaced.
Backgrounding without finishing needs moveTaskToBack, which has no Flutter
equivalent, so it goes over a method channel:
- MainActivity exposes meshcore_open/app_lifecycle with a moveTaskToBack
method, alongside the existing USB channel.
- AppBackgrounder wraps it and is a no-op off Android, where programmatic
backgrounding either does not apply (desktop windows close by their own
chrome) or is forbidden (iOS). On those platforms back at the root stays
unhandled rather than doing something destructive.
- AppShell awaits it instead of calling SystemNavigator.pop().
The activity stays alive, so the connection survives and reopening returns to
where the user was.
flutter analyze clean, dart format clean, 469 tests pass.
Sensors were the one advert type with no filter. advTypeSensor (4) already
existed in the protocol and Contact.typeLabelRaw already returned 'Sensor',
but ContactTypeFilter stopped at rooms, so sensor contacts could not be
isolated in either the contacts list or discovery.
- ContactTypeFilter gains `sensors`. Both exhaustive switches over it were
found by the analyzer rather than by hand: the contacts list predicate and
the search-hint text.
- discovery_screen's predicate has a `default: return false`, so it compiled
without a sensors case but would have silently shown an empty list. Added
explicitly.
- New l10n strings contacts_searchSensors and listFilter_sensors, regenerated
across all 18 locales. The 17 non-English locales fall back to the English
text and are recorded in untranslated.json, matching how existing strings
are handled.
- Persisted round-trip verified: UiViewStateService stores the filter by name
with orElse -> ContactTypeFilter.all, so older persisted values cannot throw
and `sensors` persists like the rest.
Groundwork for the Contacts filter rail (#307), but useful on its own: the
existing filter menu now offers Sensors.
flutter analyze clean, dart format clean, 445 tests pass.
The DTR low→high pulse in the desktop USB open path resets ESP32 USB-Serial/JTAG
chips (VID 303A: C6/H2/S3-HWCDC) into ROM download mode mid-handshake, wedging
the device until a physical reset. The pulse exists for nRF52 (VID 239A)
reconnect-after-unclean-disconnect, so it can't just be removed.
connect() now recovers the port's USB VID from the flserial enumeration
(hardware_id) and gates the pulse: 239A keeps the DTR low→high pulse; 303A and
any unknown/unparseable VID open with DTR asserted and no pulse. VID lookup is
enumeration-only (never opens the port). New usb_vid helper parses both flserial
hardware_id formats (Windows VID_XXXX and macOS/Linux VID:PID=xxxx:xxxx), unit
tested. Build of epic #245.
Always-on file logging so users/support can retrieve a log without enabling
anything in the UI. No-op on web (no filesystem).
- FileLogWriter: rotating file writer (size cap + max files), single + batch
writes, unit-tested against a real temp dir.
- FileLogService: singleton initialised in main(); resolves
getApplicationSupportDirectory()/logs, queues lines, periodic 2s batched
flush, bounded queue, never crashes the app on an IO error.
- AppDebugLogService.log + BleDebugLogService.logFrame write every entry to the
file (app logs always, independent of the in-app viewer's enable gate).
Location per the #97 decision: standard support dir on all platforms (Windows
%APPDATA%\app.offband.meshcore\logs). Still TODO before recompile: access UI
("Open logs folder" on desktop / "Share logs" on mobile).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Freeze permanent applicationId/bundle id to app.offband.meshcore
(three-segment so app.offband.* reserves the brand family; a future
app.offband.meshtastic won't collide).
Display name "Offband Meshcore" across: Android android:label, iOS
CFBundleDisplayName, Windows + Linux window titles, MaterialApp title,
Windows VERSIONINFO (ProductName/FileDescription/CompanyName), pubspec
description, GPX creator. iOS/macOS bundle ids + Linux APPLICATION_ID
set to app.offband.meshcore.
Version 9.0.0+12 -> 9.0.1+13 (PATCH milestone + build).
Deferred (unchanged): internal Dart package name (meshcore_open imports),
Android namespace + MainActivity package, exe/bundle filenames, docs prose
(README/privacy/documentation still say MeshCore Open - separate sweep).
MIT attribution to zjs81/MeshCore preserved (LICENSE untouched).
Verified: flutter analyze clean; Android debug APK built green;
aapt2 dump packagename = app.offband.meshcore; Windows compiled clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Refactor contact filtering and improve localization strings; enhance path trace handling
* Add localization for new CLI commands and update existing strings
* Enhance contact handling and UI updates across multiple screens
add unfiltered contact access and improve last seen resolution
* Add polling interval configuration and improve contact handling
* Reorder command constants for better organization and clarity
* Refactor contact handling by removing unnecessary mapping and improving clarity across multiple screens
* Moved RadioStatsIconButton in chat screen for improved UI consistency
* Added indicators to AppBar for channels
* Ignore contacts with self public key in contact handling
* Simplify path removal logic and clean up unused imports in path management dialog
* Enhance path hop resolution by adding distance checks to improve candidate selection accuracy
* Remove unnecessary reset of radio stats poll reference count in polling interval setter
- Move ContactSortOption/ContactTypeFilter enums to dedicated
contact_filter_types.dart (re-exported from contact_search.dart)
- Migrate ContactsFilterMenu and DiscoveryContactsFilterMenu to use
sealed class action types with SortFilterMenu<T> generics, replacing
int action constants and switch statements
- Guard _closeDropdownAndRun with ModalRoute.isCurrent check to prevent
accidental dismissal of parent routes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Refactor contact handling: replace DiscoveryContact with Contact, update related methods and settings
* Enhance contact handling: include latitude, longitude, and last modified timestamp in contact updates; refactor path handling to accommodate discovered contacts across multiple screens
* Enhance SNRIndicator: include discovered contacts in name resolution for repeaters
* Refactor path handling: replace addReturnPath with buildPath to improve path construction logic and handle target contact types
* Update lib/screens/map_screen.dart
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Add localization for "Show Discovery Contacts" in multiple languages and refactor location plausibility check in map screen
* Enhance contact management: update discovered contacts' active status and improve contact handling with flags and raw packet data
* Refactor ChannelsScreen: pass ChannelMessageStore to buildExpandedContent and ensure messages are cleared after channel creation
* Update MapScreen: adjust label zoom threshold and refactor guessed marker building to include labels
* Refactor ChannelsScreen: change channelMessageStore to a private getter and update its usage in buildExpandedContent calls
* Enhance location plausibility check: add latitude and longitude bounds to ensure valid coordinates
* Update lib/connector/meshcore_connector.dart
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Refactor MeshCoreConnector and related stores: update discovered contacts handling, migrate legacy keys, and set public key in community store
* Refactor MeshCoreConnector and ChannelsScreen: update discovered contacts handling and set public key in community store; enhance location plausibility check in MapScreen
* Update CMD_ADD_UPDATE_CONTACT frame format to include optional latitude and longitude fields
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
- Rewrite UsbScreen to mirror ScannerScreen patterns (status bar,
tap-to-connect port list, bottom FABs, SnackBar errors)
- Extract MeshCoreUsbManager from MeshCoreConnector for cleaner
USB transport ownership
- Add debug logging throughout USB connection flow (connector,
manager, web/native services)
- Print debug logs to console in debug mode even when app debug
log setting is disabled
- Localize remaining hardcoded strings (Web Serial Device fallback
label, USB status bar keys, companion firmware timeout hint)
- Fix Swedish misspelling in translations (stöderliga → stödda)
- Guard Linux notification init against missing D-Bus session bus
- Fix SNRIndicator hit-test error by adding minimum size constraints
- Update USB flow tests for new UI patterns
- replace Android USB dependency with app-owned USB host implementation\n- restore BLE-first scanner flow with USB secondary action\n- tighten Web Serial key handling and disconnect logging\n\nTODO (follow-up):\n- review non-English localization copy for tone and consistency\n- trim remaining unused/awkward localization strings introduced during USB UI changes
formats all dart files using `dart format .` from the root project dir
this makes the code style repeatable by new contributors and makes PR review easier
- Introduced a new extension for localization in Flutter with `LocalizationExtension` in `l10n.dart`.
- Added a Python script `translate.py` for translating ARB/JSON localization files using a local Ollama model, preserving keys and placeholders, and handling ICU format rules.
New features:
- In-app debug log viewer with copy/clear functionality
- Advanced path management UI with history and custom path builder
- Battery indicator widget with voltage/percentage toggle
- Contact/channel filtering and sorting improvements
- Repeater command ACK tracking with path history integration
Fixes:
- Switch channel sync from parallel to sequential to prevent timeouts
- Preserve path overrides when contacts refresh from device
- Fix ACK hash computation for SMAZ-encoded messages
- Proper cleanup of pending operations on disconnect