Skip to content

v1.2.0#367

Merged
Avi0n merged 95 commits into
mainfrom
dev
Jul 12, 2026
Merged

v1.2.0#367
Avi0n merged 95 commits into
mainfrom
dev

Conversation

@Avi0n

@Avi0n Avi0n commented Jul 12, 2026

Copy link
Copy Markdown
Owner

No description provided.

Avi0n and others added 30 commits June 26, 2026 13:56
Add a Chile (CL) community radio preset so devices in Chile get a
recommended configuration: 927.875 MHz, 62.5 kHz bandwidth, SF 8, CR 5.

- Add a `southAmerica` case to `RadioRegion` (short code "SA") so Chile
  is grouped under its own "South America" section in the preset picker
  instead of being shoehorned into another continent.
- Register the `cl` preset in `RadioPresets.all`, available to country CL.
- Map CL → .southAmerica in `RegionalAreas.continents` and add Chile to
  the selectable countries catalog so it resolves via the country tier of
  `recommended(for:)`.
- Prefer South America first in `regionsForLocale` for a CL locale.
feat(radio): add Chile region radio preset
…rom radio

When a contact is in the app's database but missing from the radio's firmware
contact table (backup restore, radio reset), CMD_SEND_LOGIN comes back
ERR_CODE_NOT_FOUND (0x02), which was showing as "Item not found on device."
A blank password is not the cause; firmware accepts empty passwords without
complaint.

The fix catches notFound during login, pushes the local contact to the radio
with the flood-route sentinel via addContact (CMD 0x09), syncs the local row
so keep-alive routing stays consistent, then retries. A full contact table now
returns a specific error with an actionable message rather than the generic
device error.

- Widen RemoteNodeService.session to include ContactSessionOps so login can
  call session.addContact (zero call-site changes; MeshCoreSession already
  conforms via MeshCoreSessionProtocol)
- Add sendLoginHealingIfNeeded and addLocalContactToRadio to
  RemoteNodeService+Login.swift; preserve RemoteNodeError through the catch
- Add ContactDTO.floodedContactFrame(asOf:); refactor ContactService.resetPath
  to use it
- Add RemoteNodeError.radioContactsFull, errorDescription, userFacingMessage,
  and the L10n key across all 9 locales
- Extend MockMeshCoreSession with RemoteAccessSessionOps and a scriptable
  sendLogin stub
- Add RemoteNodeLoginHealTests: heal+retry, table-full, no-local-contact
- "View on Map" opens a full-screen map with the repeater centered and each located neighbor pinned by an SNR-colored link line
- Map controls toolbar (north-lock, location, layers, labels) plus center-all that fits the repeater and its neighbors
- Auto-zoom on appear, framed inside the chrome, re-issued once the map style finishes loading
- Neighbors without a usable location surface in a top pill that pushes their list
- NeighborSNRMapBuilder with tests computes the points, links, and fit region
- Add RemoteNodeSessionDTO location helpers and shared SNR line/badge styling
- L10n strings across all 9 locales
- pulled the pill into a shared PathDistanceBanner so trace map and
  message path map use the same component instead of duplicating
  TracePathResultsBanner
- added totalDistance() to sum great-circle distance over a coordinate
  chain
- message path map pill shows total hop count plus the drawn path's
  distance
- decimalPad has no minus key, so switch lat/lon to .numbersAndPunctuation
- pin lat/lon FormatStyle to .locale(.posix) so a comma from that keyboard doesn't get read as the decimal separator
- reject out-of-range coordinates and over-length names instead of
  letting firmware silently normalize them
- validate all three fields up front so a bad one can't leave a
  partial apply
- add inline per-field errors and validation strings for all locales
allSatisfy(\.isFavorite) doesn't compile under Swift Testing's macro
expansion. Switched to a closure so the test target builds.
Avi0n and others added 29 commits July 7, 2026 13:26
The firmware already sends a hop-aware airtime estimate in
suggested_timeout_ms, but the app rejected any value outside [5s, 60s]
and fell back to a flat 30s. On the default SF10/BW250 preset the radio
estimates ~3.2s for a zero-hop trace, so every ping sat for 30s before
reporting no response.

- Clamp the scaled hint into a per-use-case band instead of rejecting
  it; only fall back to a default when the hint is absent (0)
- Add zero-hop and flood profiles with distinct floor/default/ceiling
  so a single-neighbor ping and a mesh-wide flood no longer share bounds
- Point ping at the zero-hop profile; keep path discovery and multi-hop
  traces on the wider flood band
- Pin the SF10 default-preset case in tests so it can't fall back to 30s
- Persist six per-type counters (sent/received direct and flood, direct/flood
  duplicates) on NodeStatusSnapshot and its DTO
- Overlay Direct and Flood lines in the Packets Sent/Received charts, replacing
  the standalone totals, and add a Duplicates chart
- Generalize MetricChartView to multi-series with a per-series scrub readout;
  keep receiveErrors single-series
- Add RemoteNodes chart-title strings across all locales
- Cover snapshot round-trip, legacy decode, and multi-series ordering in tests
- Track every neighbor prefix seen across stored snapshots, not just the previous one, so a neighbor that went silent and came back isn't flagged new
- Fetch the previous snapshot and the seen-prefix set together in one neighborBaseline call, so a render can't pair a fresh baseline with a stale set
- Cover the silent-then-returning case and in-window exclusion in tests
Image-extension URLs that serve an HTML landing page (imgur,
pasteboard) never rendered. The extension classifier routed
them to the inline-image path, which can't decode HTML, so the
card stayed blank. Detect text/html at fetch time and reroute
to the link-preview fragment, which pulls the page's og:image.

- InlineImageCache returns .notImage for text/html without
  touching the negative cache, so the URL stays retryable
- MessageBuildInputs carries a precomputed isInlineImageURL;
  the builder routes from that field instead of reclassifying
- ChatViewModel tracks page-serving URLs per conversation and
  re-fires the preview fetch on reroute, including in-flight
  dedup twins that would otherwise shimmer forever
- Add hasFavorites computed property to ContactsViewModel
- On first load, land on Favorites instead of Contacts when there's at
  least one favorite; leave it alone if the user already picked a segment
- Add tests for hasFavorites
- Register the meshcore scheme alongside meshcoreone so iOS opens MC1 when the Camera scans a QR or another app invokes the scheme. Routed URLs reuse the existing contact, channel, map, and hashtag handlers and confirmation sheets.
- Stage cold-launch URLs in PendingExternalURL and route only after AppState init settles. Without this, the before-first-unlock AppState swap and the failed-reconnect teardown each wipe a staged link before its sheet can present.
- Treat a malformed meshcore URL as unhandled and restore the previous tab, instead of stranding the user on an empty Chats tab.
- Show the public key above the free-text name in AddContactConfirmationSheet. A planted QR makes name-over-key phishing likelier than the in-chat link path.
- Mark the RelativeTimestampText and BubbleStatusRow suites @mainactor for
  their main-actor-isolated static calls
- Drop redundant #require on statically-valid literal URLs in the
  MessageFragmentBuilder tests
- Remove no-op await/async on the synchronous InlineImageCache stores
- Change never-mutated LinkPreviewPreferences locals to let
- Swap deprecated UITraitCollection(traitsFrom:) for modifyingTraits
- Move .serialized up to the DemoModeManager suite so it guards the shared
  singleton, and exercise both view models in the NodeCLI isolation test
- Drop the always-true UIColor nil check in the bridging test
- Back the appState environment default with a single AppState.placeholder
  instead of minting a fresh AppState() on every default resolution, each of
  which leaked a self-retaining StoreService transaction listener
- Add an isPlaceholder init flag so the stand-in skips publishing the
  process-global DebugLogBuffer and skips the onboarding path-suggestion task
- Reuse PersistenceStore.schema for the in-memory container so DebugLogEntry
  resolves; the drifted preview schema had omitted it
- Cover shared identity, inertness, and the debug-buffer skip path with tests
- Match incoming channel sender names against saved contacts; show the
  nickname with the raw sender name in parentheses
- Add SenderNameLabel with an unverified-identity indicator. A channel
  sender isn't cryptographically verified, so a name match could be
  someone else
- Only match a name owned by exactly one contact, so an ambiguous
  collision never asserts a nickname
- Carry the nickname into the DM picker sheet title and the actions
  preview header
- Include the nickname and unverified cue in the VoiceOver label
- Trim surrounding whitespace when storing a contact nickname
- Set repeat-869 to 869.495 MHz to match the firmware's allowed repeat set; 869.000 was rejected by isValidClientRepeatFreq
- Enabling Repeat Mode now only changes the frequency and leaves bandwidth, spreading factor, and coding rate as they were
- Match repeat presets on frequency alone so the picker and settings row still resolve once BW/SF/CR are kept
- Drive the Repeat Mode toggle off a synchronous flag so a programmatic flip no longer loops the confirmation or desyncs the picker
- In Advanced Settings, snap to the nearest allowed repeat frequency when enabling and restore the previous frequency when disabling, so the toggle never submits a value the firmware rejects and never strands the radio on the repeat frequency
- Stop the Advanced radio form reloading mid-apply, which flickered the frequency field as the frequency and clientRepeat updates arrived as separate events
- Correct the Advanced repeat footer to list 869.495 MHz across all locales
- Share one nearestRepeatPreset helper between the preset and Advanced screens
- Add RepeatPresetTests for the firmware frequency contract, frequency-only matching, and nearest-frequency snapping
- Fall back to a plain-GET og:image/twitter:image scrape in
  LinkPreviewService when LinkPresentation returns no hero image
- Send scrape and image GETs through a session that re-checks every
  redirect hop, so a 302 to a private host is refused
- Cap the HTML and image downloads with streaming byte limits and a
  pixel-bounded ImageIO decode
- Widen the fetch semaphore doc to note it now covers these GETs
- Test the meta-tag parser and the network guards
- PersistentLogger.persist goes through DebugLogBuffer.record: entries
  queue (max 500, oldest dropped) while shared is nil and drain in order
  once a buffer is assigned
- entries lost to save failures or the requeue cap are counted and
  reported as a summary entry on the next successful save
- tests cover the pending queue, drain order, overflow, and drop summary
- the auto-reconnect rebuild now sets .connected up front like a fresh
  connect so the syncing pill shows, and its session.start and
  queryDevice get the same 10s timeout as the initial connect
- stop the old session during reconnect teardown (keeping the transport
  for the pending reconnect) so its receive and auto-fetch loops end now
  instead of parking on the finished dispatcher stream until dealloc; the
  poll loop checks for cancellation each iteration so it actually unwinds
- the resync loop guards on container identity at the loop top like the
  success path already did, so a loop orphaned by a later reconnect
  cycle can't burn its attempts against a dead container and then
  disconnect the healthy successor
- promote to .ready before the post-sync room re-auth, matching
  promoteToReady's ordering, so the syncing pill and the send queue's
  ready gate don't wait on work that isn't sync
- tests for the rebuild window, bond loss mid-reconnect, and the
  superseded and cancellation paths
- map bond failures (CBError.peerRemovedPairingInformation, CBATT auth
  codes, and ATT auth codes on a write) to authenticationFailed at every
  disconnect and write callback, latched per failure episode so watchdog
  retries don't re-alert; the latch clears on ready promotion and
  explicit disconnect
- a peripheral still connected after the discovery extension budget is
  spent, or a disconnect in the discoveryComplete window, is classified
  as a dead bond and routed to guided re-pair instead of retrying
  timeouts forever
- removeFailedPairing demotes the Device row to a ghost instead of
  deleting it, so a radio that lost its bond re-pairs onto its original
  radioID and keeps its contacts, messages, and channels; a stranded
  association is swept only when it maps to no saved device, leaving the
  live connection, active attempt, and saved radios protected
- manual Connect on a dead-bond radio and a fresh pair rejected on the
  PIN each present guided recovery, with copy distinct to each case;
  presentConnectionFailure clears stale pairing state between alerts
- rebuild the ASK session after ASAccessorySession invalidation, so
  pairing isn't wedged into a silent no-op until app relaunch
- AppState presents the recovery flow; new copy is localized
- tests for the latch, routing, each mapped callback site, the re-pair
  round trip, and the sweep and cancellation paths
- a connect attempt parked in .waitingForBluetooth is failed before
  state restoration claims the machine; transition(to:) never resumes a
  phase continuation, so clobbering the phase would hang that connect and
  leak its device claim, turning every later Reconnect into a no-op
- an isReconnecting disconnect arriving in .idle or .waitingForBluetooth
  (no operation owns a peripheral) is treated as a full disconnect
  instead of entering .autoReconnecting to wait on a reconnection this
  machine never started
- the RSSI keepalive is cancelled during stop's direct-write path, which
  bypasses cleanupPhaseResources, and the connection-timeout task no-ops
  when cancelled, so neither outlives shutdown
- auto-reconnect entry stands down for a different in-flight manual
  connect and adopts a matching one, rather than disconnecting the
  transport and cancelling the OS pending connect, which would strand a
  reconnect-cycle claim that silently disables the watchdog, the
  foreground health check, and power-on recovery
- tests for restoration, teardown, the auto-reconnect entry race, and
  the circuit-breaker cooldown boundary
- Collapse the standalone Inline Images setting into the Link Content
  master toggle; DM/channel scope and GIF autoplay nest beneath it
- Add InlineImage .disabled tap-to-load state so images sit behind a
  placeholder when the master is on but the scope's auto-resolve is off
- Gate inline images on previewsEnabled in MessageFragmentBuilder; drop
  the now-unused showInlineImages EnvInputs field and default constant
- Render the disabled placeholder as a text-style bubble to skip the
  edge-to-edge image clip pass
- Persist the image-URL-serves-a-page verdict in InlineImageCache so a
  reloaded card is not re-classified as an inline-image shimmer when the
  same chat is reopened
- Rename Link Previews to Link Content; add the reduce-motion note and
  update the privacy footer copy across all locales
- Add onAutoReconnectStarted, fired from BLEReconnectionCoordinator after the reconnect cycle is claimed and before session teardown, so the Live Activity write lands at the front of the disconnect wake window rather than behind teardown
- Gate entry on connectionIntent.wantsConnection so a user disconnect or a stand-down for a different device doesn't flash a false loss
- Self-heal a stale activity cached disconnected while the same radio is actually connected, keyed on a radio-scoped provider so a connection to a different radio can't relabel it
- Cover entry ordering and gating in BLEReconnectionCoordinatorTests and the three entry paths in ConnectionManagerAutoReconnectEntryTests
- Read the dBm value out of a `get tx` reply instead of requiring the
  whole string to be an integer, so ZephCore's "22dBm (apc=off)" no
  longer fails to load and blocks the radio settings apply
- With Adaptive Power Control on, take the `max=` ceiling that `set tx`
  writes back rather than the reduced live power, so applying doesn't
  quietly lower the configured ceiling
- Cover the apc=off and apc=on reply shapes with tests
- Compute one Y-axis domain across every packet series and apply it to the Sent, Received, Duplicates, and Errors charts
- The Direct/Flood split had given each chart its own scale, so counter growth rates could no longer be compared at a glance
- Apply .listSectionSpacing(.compact) to the telemetry, telemetry overview, and node status history lists
- Shrinks the blank space around the time range selector so it matches the other node status screens
- re-issue the pending connect on a transient didFailToConnect instead of
  going idle. The watchdog can't run while the app is suspended, so giving
  up meant no background reconnect at all
- cap retries at maxAutoReconnectConnectFailures. Definitive bond errors
  still tear down immediately; if the budget runs out on mostly encryption
  timeouts, treat it as a bond failure so re-pair guidance still appears
- a lone CBError.encryptionTimedOut now maps to connectionFailed, not
  authenticationFailed. One timeout is a weak link, not a lost bond
- show the pairing-failure alert only while the app is active, clear the
  surfacing latch on foreground, drop stale alert state once connected
- Add a success-rate percentage to the batch complete and success labels
- Default batch size to 3 and drop the size-10 chip
- Compute the percentage from success count over batch size
@Avi0n
Avi0n merged commit 1301a1c into main Jul 12, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants