Skip to content

Add tvOS app: TCP mesh map with Bonjour discovery - #2186

Open
garthvh wants to merge 19 commits into
mainfrom
feat/tvos-mesh-map
Open

Add tvOS app: TCP mesh map with Bonjour discovery#2186
garthvh wants to merge 19 commits into
mainfrom
feat/tvos-mesh-map

Conversation

@garthvh

@garthvh garthvh commented Jul 24, 2026

Copy link
Copy Markdown
Member

What

A new Meshtastic TV target — a thin, independent Apple TV client that connects to a node over TCP and shows a live mesh map operable with the Siri Remote. No Bluetooth (sidesteps BLE pairing on a remote); a plugged-in TV on Wi-Fi is a natural TCP client.

Approach

Mirrors the Watch App precedent (self-contained target), reusing only what's genuinely portable:

  • Reuses the transport core (TCPConnection / Connection, Network.framework) by file membership, plus the MeshtasticProtobufs package. It does not link the iOS AccessoryManager / MeshPackets / SwiftData / BLE / CoreLocation stack. AccessoryError, the want-config nonces and TransportType are small tvOS-local shims.
  • MeshClient runs a minimal want-config / want-database handshake and keeps an in-memory node store built from the FromRadio stream (NodeInfo + Position). No persistence in v1.
  • MeshTVMapView is a fresh MKMapView wrapper (Apple basemap) with annotation diffing, centroid framing, and focus / side-list selection. No user-location features (no GPS on tvOS).
  • NodeDiscovery browses _meshtastic._tcp (Bonjour) so nodes appear in the connect list without typing an IP; manual host/port entry remains as a fallback.
  • tvOS Brand Assets app icon.

iOS impact

None. No existing iOS source is modified — the shared transport files are added by target membership only. The project regeneration is additive (new target + scheme).

Testing

  • Builds for the tvOS 26.5 Simulator; installs and launches.
  • Verified end-to-end against a real node: Bonjour discovery surfaces the node, connecting pulls the node DB, and nodes plot on the map with Siri-Remote-driven selection and node detail.
  • App icon Brand Assets compile into the asset catalog with no warnings.

Deferred (post-v1)

Offline PMTiles basemap, SwiftData persistence, messaging/channels, waypoint placement, traceroute flyover, user-location features, and MQTT/TAK/WatchConnectivity side effects.

Summary by CodeRabbit

  • New Features
    • Launched a tvOS app for connecting to Meshtastic nodes over TCP.
    • Auto-discovers nearby nodes via Bonjour with tap-to-connect, plus manual host/port connection saved across launches.
    • Shows connecting/progress and clear failure messaging, and supports node disconnect.
    • Added a Live mesh map with remote focus navigation, re-center, and clustered node pins, plus node detail screens (identity, battery/SNR/last-heard, optional position).
  • Design & Assets
    • Added tvOS app icon, top-shelf branding, logos, and the LightIndigo accent color.

New 'Meshtastic TV' target — a thin, independent Apple TV client that
connects to a node over TCP and shows a live mesh map operable with the
Siri Remote. No Bluetooth (sidesteps BLE pairing on a remote); a plugged-in
TV on Wi-Fi is a natural TCP client.

- Reuses the portable transport core (TCPConnection / Connection) by file
  membership plus the MeshtasticProtobufs package; does not link the iOS
  AccessoryManager / SwiftData / BLE / CoreLocation stack. AccessoryError,
  the want-config nonces and TransportType are small tvOS-local shims.
- MeshClient runs a minimal want-config / want-database handshake and keeps
  an in-memory node store from the FromRadio stream (NodeInfo + Position);
  no persistence in v1.
- MeshTVMapView is a fresh MKMapView wrapper (Apple basemap) with annotation
  diffing, centroid framing and focus / side-list selection. No user-location
  features (no GPS on tvOS).
- Bonjour discovery (NodeDiscovery) browses _meshtastic._tcp so nodes appear
  in the connect list without typing an IP; manual host/port entry remains.
- tvOS Brand Assets app icon.

iOS target is unchanged (additive project regeneration only).
Copilot AI review requested due to automatic review settings July 24, 2026 08:35
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This pull request adds a tvOS Meshtastic application with a configured Xcode target, TCP client, Bonjour discovery, connection flow, live node map, node details, and tvOS asset catalogs.

Changes

tvOS Mesh Application

Layer / File(s) Summary
tvOS target and build wiring
project.yml, Meshtastic.xcodeproj/..., Meshtastic TV/Info.plist
Adds the Meshtastic TV target, scheme, tvOS build settings, shared TCP sources, protobuf dependency, and local-network metadata.
Mesh client and discovery flow
Meshtastic TV/Client/*, Meshtastic TV/Models/MeshNode.swift
Adds TCP lifecycle management, protobuf event ingestion, node state storage, Bonjour discovery, transport identifiers, and accessory errors.
Connection and navigation screens
Meshtastic TV/App/*, Meshtastic TV/Views/NodeDetailView.swift
Adds state-based routing, connection progress and failure states, persisted connection fields, discovered-node controls, and node details.
Live mesh map interface
Meshtastic TV/App/MapScreen.swift, Meshtastic TV/Map/MeshTVMapView.swift
Adds the split node-list/map layout, selection synchronization, disconnect action, annotations, clustering, and region framing.
tvOS asset catalog
Meshtastic TV/Assets.xcassets/*
Adds colors, logos, app icon image stacks, and top-shelf image definitions.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested labels: skip-docs-check

Suggested reviewers: copilot

Poem

A bunny hops through screens so bright,
Finds mesh nodes dancing left and right.
TCP streams and map pins glow,
Bonjour helps the pathways flow.
“Connect!” it cheers, with ears held high. 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly names the new tvOS app and its main features: TCP mesh map and Bonjour discovery.
Description check ✅ Passed The description covers what changed, approach, iOS impact, testing, and deferred work, though it lacks explicit screenshots and checklist items.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

- Bring the Meshtastic brand into the TV app: m-logo imagesets and the
  LightIndigo green from the widgets, plus the iOS app's indigo AccentColor.
  Logo hero on the connect screen, logo + wordmark header on the map sidebar,
  brand-green tint throughout.
- Regenerate the app icon and top shelf images as the brand-green M on dark,
  matching the iOS icon rather than a guessed palette.
- Replace default marker pins with iOS-parity node pins: a circle colored by
  UIColor(hex: nodeNum) with the short name inside (CircleText + Color.swift
  reused by target membership), focus-scaled for the Siri Remote; clusters
  render as accent circles with a member count. Cluster select zooms in.
- Node list rows and node detail reuse the same circle badges; battery reads
  in brand green. Fix the discovered-node port rendering with a thousands
  separator (4,403 -> 4403).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Introduces a new standalone tvOS application target (Meshtastic TV) that connects to a Meshtastic node over TCP, discovers nodes via Bonjour (_meshtastic._tcp), and renders a live mesh map with Siri Remote-friendly selection and a node detail view—while intentionally avoiding the iOS BLE/AccessoryManager/SwiftData stack.

Changes:

  • Added a new tvOS app target + scheme and wired it into the Xcode project.
  • Implemented a minimal TCP mesh client (handshake + in-memory node store) and Bonjour discovery for connect flows.
  • Added a tvOS MapKit-backed map UI with focus/selection syncing, plus node list + node detail views.

Reviewed changes

Copilot reviewed 33 out of 49 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
project.yml Adds the Meshtastic TV tvOS target, reusing portable TCP transport primitives and Logger.
Meshtastic.xcodeproj/xcshareddata/xcschemes/Meshtastic TV.xcscheme Adds a shared Xcode scheme for running/building the new tvOS app.
Meshtastic.xcodeproj/project.pbxproj Project regeneration to include the new tvOS target, scheme integration, and file memberships.
Meshtastic TV/Views/NodeDetailView.swift Minimal tvOS node detail view (identity/status/position).
Meshtastic TV/Models/MeshNode.swift Lightweight in-memory node model used by the tvOS client/UI.
Meshtastic TV/Map/MeshTVMapView.swift MKMapView wrapper with annotation diffing, initial framing, and selection sync.
Meshtastic TV/Info.plist tvOS Info.plist including Local Network + Bonjour service declarations.
Meshtastic TV/Client/TransportType.swift tvOS-local transport discriminator shim for reused transport code.
Meshtastic TV/Client/NodeDiscovery.swift Bonjour (NetServiceBrowser) discovery for _meshtastic._tcp nodes.
Meshtastic TV/Client/MeshClient.swift Minimal TCP client with want-config handshake and node/position ingestion.
Meshtastic TV/Client/AccessoryError.swift tvOS-local shim to satisfy reused transport error references.
Meshtastic TV/Assets.xcassets/Contents.json Adds the tvOS asset catalog container.
Meshtastic TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json Top Shelf image asset metadata for tvOS.
Meshtastic TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json Wide Top Shelf image asset metadata for tvOS.
Meshtastic TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json Brand assets catalog metadata (app icon + top shelf images).
Meshtastic TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json App icon imagestack layer metadata (front).
Meshtastic TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json App icon front layer image set metadata.
Meshtastic TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json App icon imagestack metadata (front/back layers).
Meshtastic TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json App icon imagestack layer metadata (back).
Meshtastic TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json App icon back layer image set metadata.
Meshtastic TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json App Store icon imagestack layer metadata (front).
Meshtastic TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json App Store icon front layer image set metadata.
Meshtastic TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json App Store icon imagestack metadata (front/back layers).
Meshtastic TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json App Store icon imagestack layer metadata (back).
Meshtastic TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json App Store icon back layer image set metadata.
Meshtastic TV/Assets.xcassets/AccentColor.colorset/Contents.json Adds tvOS accent color definition.
Meshtastic TV/App/RootView.swift Root state switcher between connect/connecting/map screens.
Meshtastic TV/App/MeshtasticTVApp.swift tvOS app entry point (@main) wiring the client into the root view.
Meshtastic TV/App/MapScreen.swift Main tvOS map screen: side list + map + node detail navigation + disconnect action.
Meshtastic TV/App/ConnectView.swift Connect UI: Bonjour-discovered nodes + manual host/port entry + error display.

Comment on lines +87 to +89
private var port: Int {
Int(portText.trimmingCharacters(in: .whitespaces)) ?? 4403
}
Comment on lines +46 to +50
func stop() {
browser.stop()
resolving.removeAll()
isBrowsing = false
}
Comment on lines +60 to +62
func netServiceBrowser(_ browser: NetServiceBrowser, didRemove service: NetService, moreComing: Bool) {
discovered.removeAll { $0.id == Self.identity(for: service) || $0.name == service.name }
}
Copilot AI review requested due to automatic review settings July 24, 2026 08:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 33 out of 49 changed files in this pull request and generated 3 comments.

Comment on lines +188 to +192
// Remove annotations for nodes that are gone.
for (num, annotation) in annotationsByNum where !incoming.contains(num) {
mapView.removeAnnotation(annotation)
annotationsByNum[num] = nil
}
Comment on lines +84 to +87
} catch {
Logger.transport.error("📺 [MeshClient] connect failed: \(error.localizedDescription, privacy: .public)")
self.state = .failed(error.localizedDescription)
}
Comment thread project.yml
Comment on lines +668 to +670
# Node-pin styling parity with the iOS map (CircleText + Color(hex:)/isLight).
- path: "Meshtastic/Views/Helpers/CircleText.swift"
- path: "Meshtastic/Extensions/Color.swift"

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
Meshtastic TV/Client/NodeDiscovery.swift (1)

26-95: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider NWBrowser instead of the deprecated NetServiceBrowser/NetService.

This is new code built on Foundation's legacy Bonjour APIs, while the rest of the client (TCPConnection) already uses Network.framework. Apple DTS has confirmed NetServiceBrowser has been deprecated in favour of NWBrowser. Consolidating discovery onto NWBrowser would keep the whole client on one modern, async-friendly networking stack.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Meshtastic` TV/Client/NodeDiscovery.swift around lines 26 - 95, Replace the
legacy NetServiceBrowser/NetService implementation in NodeDiscovery with
Network.framework’s NWBrowser, matching the existing TCPConnection networking
stack. Preserve the current published discovery state, start/stop lifecycle,
Meshtastic Bonjour service type, TXT-derived node identity/name, resolved host
and port, deduplication, and removal behavior while adapting callbacks to
NWBrowser’s async result updates.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Meshtastic` TV/Client/MeshClient.swift:
- Around line 40-41: Track the outer asynchronous connection attempt in a
dedicated property alongside connection and consumeTask, and cancel it from
disconnect() and before starting a new connect(). In connect(), guard completion
and subsequent assignment of connection/consumeTask so a stale attempt cannot
overwrite state created by a newer attempt; ensure any stale TCPConnection is
closed. Clear the tracked connect task when the current attempt finishes.

---

Nitpick comments:
In `@Meshtastic` TV/Client/NodeDiscovery.swift:
- Around line 26-95: Replace the legacy NetServiceBrowser/NetService
implementation in NodeDiscovery with Network.framework’s NWBrowser, matching the
existing TCPConnection networking stack. Preserve the current published
discovery state, start/stop lifecycle, Meshtastic Bonjour service type,
TXT-derived node identity/name, resolved host and port, deduplication, and
removal behavior while adapting callbacks to NWBrowser’s async result updates.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3637ed8f-a375-4e19-8bf1-12703b51120b

📥 Commits

Reviewing files that changed from the base of the PR and between f584572 and 47cd229.

⛔ Files ignored due to path filters (10)
  • Meshtastic TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/back_1x.png is excluded by !**/*.png
  • Meshtastic TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/front_1x.png is excluded by !**/*.png
  • Meshtastic TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/back_1x.png is excluded by !**/*.png
  • Meshtastic TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/back_2x.png is excluded by !**/*.png
  • Meshtastic TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/front_1x.png is excluded by !**/*.png
  • Meshtastic TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/front_2x.png is excluded by !**/*.png
  • Meshtastic TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/tsw_1x.png is excluded by !**/*.png
  • Meshtastic TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/tsw_2x.png is excluded by !**/*.png
  • Meshtastic TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/ts_1x.png is excluded by !**/*.png
  • Meshtastic TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/ts_2x.png is excluded by !**/*.png
📒 Files selected for processing (30)
  • Meshtastic TV/App/ConnectView.swift
  • Meshtastic TV/App/MapScreen.swift
  • Meshtastic TV/App/MeshtasticTVApp.swift
  • Meshtastic TV/App/RootView.swift
  • Meshtastic TV/Assets.xcassets/AccentColor.colorset/Contents.json
  • Meshtastic TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json
  • Meshtastic TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json
  • Meshtastic TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json
  • Meshtastic TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json
  • Meshtastic TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json
  • Meshtastic TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json
  • Meshtastic TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json
  • Meshtastic TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json
  • Meshtastic TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json
  • Meshtastic TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json
  • Meshtastic TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json
  • Meshtastic TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json
  • Meshtastic TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json
  • Meshtastic TV/Assets.xcassets/Contents.json
  • Meshtastic TV/Client/AccessoryError.swift
  • Meshtastic TV/Client/MeshClient.swift
  • Meshtastic TV/Client/NodeDiscovery.swift
  • Meshtastic TV/Client/TransportType.swift
  • Meshtastic TV/Info.plist
  • Meshtastic TV/Map/MeshTVMapView.swift
  • Meshtastic TV/Models/MeshNode.swift
  • Meshtastic TV/Views/NodeDetailView.swift
  • Meshtastic.xcodeproj/project.pbxproj
  • Meshtastic.xcodeproj/xcshareddata/xcschemes/Meshtastic TV.xcscheme
  • project.yml

Comment thread Meshtastic TV/Client/MeshClient.swift
The 'crazy static' while moving between nodes was tvOS focus/selection
sounds machine-gunning, from three compounding churn sources:

- Every data tick reconfigured every annotation view, and reassigning
  clusteringIdentifier makes MapKit tear down and rebuild views under the
  focus engine. Now the identifier is set only when an annotation is
  (re)assigned, content updates are cheap and idempotent, and the sync diff
  mutates nothing when nothing changed.
- tvOS List selection follows focus, so gliding across the side list fired
  an animated selectAnnotation (callout + selection sound) per row. Now
  browsing centers the map only, only when the selection actually changed;
  callouts appear only when a pin itself is clicked.
- MeshClient republished the node store for every packet even when nothing
  visible changed. NodeInfo upserts now skip no-op writes, and position
  packets only publish when the fix moved or lastHeard is >60s stale.

Also adds a Re-center button next to Disconnect that re-frames the camera
on the whole mesh (clearing any list selection), with a neutral tint so the
label stays readable against the brand-green tint.
Copilot AI review requested due to automatic review settings July 24, 2026 08:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 33 out of 49 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (2)

Meshtastic TV/Map/MeshTVMapView.swift:205

  • annotationsByNum is mutated (annotationsByNum[num] = nil) while iterating over it, which can trap at runtime ("Dictionary mutated while being enumerated") when nodes disappear. Collect keys to remove first, then mutate the dictionary in a second step.
    project.yml:670
  • This target pulls in Meshtastic/Views/Helpers/CircleText.swift, which currently imports SwiftData. SwiftData is not available on tvOS, so this will fail to compile the new tvOS target unless CircleText.swift drops that import (it appears unused) or gates it with #if canImport(SwiftData) / a tvOS-specific implementation.

Comment on lines +92 to +98
let color = UIColor(hex: node.num)
let text = node.shortName.isEmpty ? String(format: "%04x", node.num & 0xffff) : node.shortName
if circle.backgroundColor != color { circle.backgroundColor = color }
if label.text != text {
label.text = text
label.textColor = color.isLight() ? .black : .white
}
Comment on lines +60 to +70
func connect(host: String, port: Int) {
disconnect()
self.host = host
state = .connecting
nodes = [:]
myNodeNum = nil

Task {
do {
let conn = try await TCPConnection(host: host, port: port)
self.connection = conn
Centering alone at mesh-wide zoom left a list-selected node swallowed by
its cluster badge. Selection now zooms to city scale (4 km) centered on the
node so it breaks out of the cluster — and only ever zooms IN: when the
user is already closer than that, keep their zoom and just center.
Copilot AI review requested due to automatic review settings July 24, 2026 09:01
…on fly

The remaining static on list transitions came from the zoom animation
itself: every animated region change re-evaluates clustering, and each
cluster/pin view added or removed during the flight was a focusable
element, so the focus engine re-evaluated (with its click sound) all the
way through the animation.

- Annotation views now opt out of focus entirely (canBecomeFocused false).
  The side list is the selection path, so pins never needed focus; the map
  itself still pans with the remote. The dead pin/cluster didSelect path is
  removed with it.
- Selection flights are debounced (0.4s): gliding across list rows no
  longer launches an overlapping animated fly per row — the map flies once,
  to the row the user settles on.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 33 out of 49 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (8)

Meshtastic TV/Client/MeshClient.swift:42

  • MeshClient launches an untracked connection Task but there’s no stored handle to cancel it during disconnect(), which can lead to state/connection mutations happening after the user cancels. Add a connectTask property so the connection attempt can be cancelled cleanly.
    Meshtastic TV/Client/MeshClient.swift:71
  • connect(host:port:) uses a detached Task { ... } that is not cancelled on disconnect(). If the user cancels while the awaits are in-flight, the task can still set connection/state later (including flipping back to .failed). Store the task and short-circuit on cancellation.
    Meshtastic TV/Client/MeshClient.swift:95
  • disconnect() cancels the consume task but not the in-flight connection attempt. If connect() is still awaiting TCPConnection(...) / connect(), it can complete after disconnect and re-mutate state. Cancel and nil out connectTask here as well.
    Meshtastic TV/Client/NodeDiscovery.swift:23
  • Using TXT-advertised node IDs as Identifiable.id makes add/update/remove inconsistent: didRemove only receives the NetService (service name), so entries whose id came from the TXT record won’t be removed, leaving stale discovered nodes. Use service.name as the stable identity (like TCPTransport does), and keep the advertised node ID as a separate field if needed.
    Meshtastic TV/Client/NodeDiscovery.swift:50
  • stop() clears resolving but doesn’t actually cancel outstanding NetService.resolve(...) operations. Per Apple’s pattern (and this repo’s TCPTransport.cancelPendingResolutionsAndStopBrowsing()), a late netServiceDidResolveAddress callback can still arrive after stop and repopulate discovered. Explicitly stop each pending service and clear its delegate before stopping the browser.
    Meshtastic TV/Client/NodeDiscovery.swift:62
  • didRemove currently tries to match by TXT-advertised id/shortname, but those values may not equal NetService.name. With DiscoveredNode.id stabilized to service.name, removal can be deterministic; also remove the service from the resolving set when it disappears.
    Meshtastic TV/Client/NodeDiscovery.swift:78
  • After making DiscoveredNode.id a stable service.name, netServiceDidResolveAddress should populate both id and advertisedID consistently and keep the update-keyed-by-id behavior intact (so a later resolve doesn’t create duplicates).
    Meshtastic TV/Map/MeshTVMapView.swift:41
  • The NodeCircleAnnotationView doc comment says the pin “is focus-engine selectable”, but canBecomeFocused is overridden to false. Update the comment so it matches the (non-focusable) behavior and rationale.

Comment on lines +7 to +11
// Live mesh map with a focusable node side-list for Siri Remote operation.
// The list is the primary, reliable selection path on tvOS; clicking a pin on
// the map (focus engine) selects too and keeps the list in sync. Rows carry the
// same node-color circle badges as the map pins (CircleText, shared with iOS).
//
Comment on lines +7 to +12
// Fresh, minimal MKMapView wrapper for tvOS. Deliberately does NOT port the iOS
// `ClusterMapView` (welded to touch gestures, MKUserTrackingButton and SwiftData).
// No device GPS on tvOS, so `showsUserLocation` is off and we only plot the
// reported positions of other nodes. Selection is driven by the focus engine
// (clicking a pin) and by the side list via `selectedNodeNum`.
//
The 'static' was visual: long animated region changes re-evaluate
clustering every frame (pins and cluster badges popping in and out) while
map tiles stream in behind — plus every pin paying an offscreen shadow
pass per frame. Compounding it, annotation views were focusable, so
swiping near the map walked focus into pins and fired flights the list
never asked for.

- setRegionSmart: CUT instead of animating when the jump is big (>80 km or
  >4x span change); short hops still animate. Applies to selection,
  re-center and initial framing.
- Pre-computed shadowPath on node pins: no per-frame offscreen shadow
  passes during map animation.
- Track list-row focus explicitly with @focusstate and mirror it into the
  map selection: List(selection:) does not follow focus for NavigationLink
  rows, and with pins now non-focusable this is the browse signal that
  drives the (debounced) map preview.
Copilot AI review requested due to automatic review settings July 24, 2026 14:08

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Meshtastic` TV/Map/MeshTVMapView.swift:
- Around line 308-338: Update the selection handling around lastAppliedSelection
and the delayed pendingSelectionFly so a selection is not marked applied before
annotationsByNum contains the node. Track the selected number as pending,
reattempt it when the annotation becomes available, and assign
lastAppliedSelection only after setRegionSmart is invoked to center the
annotation; preserve the existing debounce and zoom behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: eef489bc-bf41-4a1a-b383-e3f3e94fbe54

📥 Commits

Reviewing files that changed from the base of the PR and between 20de904 and 634c11d.

📒 Files selected for processing (2)
  • Meshtastic TV/App/MapScreen.swift
  • Meshtastic TV/Map/MeshTVMapView.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • Meshtastic TV/App/MapScreen.swift

Comment thread Meshtastic TV/Map/MeshTVMapView.swift Outdated
MKMapView on tvOS captures directional input for panning and never
releases focus on its own, so once focus entered the map there was no way
back to the node list — and after opening a node detail, focus fell into
the map, leaving Menu unable to pop the detail either.

Claim the Menu press with a gesture recognizer on the map view: it pops
any open node detail (NavigationStack path is now state-driven) and hands
focus back to the node list via the tracked row FocusState. Menu/back now
always returns to the list from anywhere on the map screen.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 33 out of 49 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (8)

Meshtastic TV/Client/MeshClient.swift:1

  • connect() creates an untracked Task that can complete after disconnect() is called (e.g., when the user hits Cancel), reassign self.connection, start consumeTask, and/or overwrite state after the app believes it disconnected. Track the connect task (e.g., private var connectTask: Task<Void, Never>?), cancel it in disconnect(), and add cancellation checks after each await (or use a monotonically increasing connection token / withTaskCancellationHandler) before mutating connection, consumeTask, or state.
    Meshtastic TV/Client/NodeDiscovery.swift:1
  • DiscoveredNode.id is sometimes set from the TXT record "id" (line 75), but removal only matches against service.name (via identity(for:)) or name == service.name. This can leave stale discovered entries when a service disappears. Prefer making DiscoveredNode.id consistently equal to service.name (and store the advertised node id separately), or maintain a mapping from NetService (or service.name) to the chosen DiscoveredNode.id so didRemove can remove the correct entry deterministically.
    Meshtastic TV/Client/NodeDiscovery.swift:1
  • DiscoveredNode.id is sometimes set from the TXT record "id" (line 75), but removal only matches against service.name (via identity(for:)) or name == service.name. This can leave stale discovered entries when a service disappears. Prefer making DiscoveredNode.id consistently equal to service.name (and store the advertised node id separately), or maintain a mapping from NetService (or service.name) to the chosen DiscoveredNode.id so didRemove can remove the correct entry deterministically.
    Meshtastic TV/Map/MeshTVMapView.swift:1
  • The map view currently can’t drive selection back into SwiftUI: selectedNodeNum is only used to apply selection/centering, but there’s no delegate callback (e.g., mapView(_:didSelect:)) updating the @Binding, and the annotation views are explicitly non-focusable. This conflicts with the stated behavior (“clicking a pin … selects too”) and makes pin-based selection effectively impossible on tvOS. Either (mandatory) implement a map-to-binding selection path (delegate didSelect, or a custom selection gesture that identifies the annotation and sets parent.selectedNodeNum), or (optional) if list-only is intended, remove the pin-click selection claim from the surrounding comments/PR narrative and UI expectations.
    Meshtastic.xcodeproj/project.pbxproj:1
  • This introduces duplicate top-level groups with identical names (Accessory, Extensions, Views) under the Meshtastic group, which will make project navigation and future file moves/error triage confusing. Consider placing the tvOS-reused shared files under the Meshtastic TV root group (or a clearly named single “Shared” group) instead of adding parallel duplicate groups with the same display name.
    Meshtastic.xcodeproj/project.pbxproj:1
  • This introduces duplicate top-level groups with identical names (Accessory, Extensions, Views) under the Meshtastic group, which will make project navigation and future file moves/error triage confusing. Consider placing the tvOS-reused shared files under the Meshtastic TV root group (or a clearly named single “Shared” group) instead of adding parallel duplicate groups with the same display name.
    Meshtastic.xcodeproj/project.pbxproj:1
  • This introduces duplicate top-level groups with identical names (Accessory, Extensions, Views) under the Meshtastic group, which will make project navigation and future file moves/error triage confusing. Consider placing the tvOS-reused shared files under the Meshtastic TV root group (or a clearly named single “Shared” group) instead of adding parallel duplicate groups with the same display name.
    Meshtastic TV/Client/MeshClient.swift:1
  • The new in-memory ingestion and “no-op skip” logic is core behavior for the tvOS app (e.g., coalescing frequent position packets, node upserts, and connection cancellation behavior) but currently has no visible tests. Add unit tests around MeshClient ingestion (e.g., moved vs. stale update, correct coordinate scaling, node sorting) and (optionally) NodeDiscovery TXT parsing/removal behavior to prevent regressions as protobuf/admin fields evolve.

Copilot AI review requested due to automatic review settings July 24, 2026 14:26
The buttons floated in an overlay on the map's top-right corner — but with
the map capturing focus for panning (and pins non-focusable), there was no
focus path to reach them with the remote. They now live as the first
section of the left list column, reachable like any row; the map surface
is clean.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 33 out of 49 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (10)

Meshtastic TV/Client/MeshClient.swift:1

  • @Observable requires the Observation module; this file doesn’t import it, which will fail to compile. Add import Observation (or another appropriate import that provides @Observable) alongside the existing imports.
    Meshtastic TV/Client/MeshClient.swift:1
  • @Observable requires the Observation module; this file doesn’t import it, which will fail to compile. Add import Observation (or another appropriate import that provides @Observable) alongside the existing imports.
    Meshtastic TV/Views/NodeDetailView.swift:1
  • UIColor is referenced but UIKit isn’t imported in this file, which will fail to compile. Add import UIKit (or switch to a non-UIKit color constructor if you have one available).
    Meshtastic TV/Views/NodeDetailView.swift:1
  • UIColor is referenced but UIKit isn’t imported in this file, which will fail to compile. Add import UIKit (or switch to a non-UIKit color constructor if you have one available).
    Meshtastic TV/Client/MeshClient.swift:1
  • You’re marking the client .connected on any configCompleteID, but connect() sends two nonces (config + DB). This can transition to .connected early (after config completes) before the DB dump is finished. Track which nonce completed (bind the associated value from .configCompleteID(let nonce) if available) and only switch to .connected after the DB nonce (or after both nonces have completed).
    Meshtastic TV/Client/MeshClient.swift:1
  • If an error is thrown after self.connection = conn (e.g., during connect() or either send), the failure path sets .failed but doesn’t clean up the partially-established connection/stream. Consider clearing self.connection and disconnecting the conn in the catch to avoid leaking an in-flight network connection and to ensure subsequent connects start from a clean state.
    Meshtastic TV/Client/NodeDiscovery.swift:1
  • When the TXT record provides an id (so DiscoveredNode.id != service.name) and name is a shortname (so node.name != service.name), didRemove won’t match and the stale discovered entry can remain. Fix by keying removal (and the list identity) on a value that is guaranteed to match didRemove—e.g., always use service.name (or a composite of name+type+domain) as the stable identifier and keep the Meshtastic node id as a separate field.
    Meshtastic TV/Client/NodeDiscovery.swift:1
  • When the TXT record provides an id (so DiscoveredNode.id != service.name) and name is a shortname (so node.name != service.name), didRemove won’t match and the stale discovered entry can remain. Fix by keying removal (and the list identity) on a value that is guaranteed to match didRemove—e.g., always use service.name (or a composite of name+type+domain) as the stable identifier and keep the Meshtastic node id as a separate field.
    Meshtastic TV/Client/NodeDiscovery.swift:1
  • When the TXT record provides an id (so DiscoveredNode.id != service.name) and name is a shortname (so node.name != service.name), didRemove won’t match and the stale discovered entry can remain. Fix by keying removal (and the list identity) on a value that is guaranteed to match didRemove—e.g., always use service.name (or a composite of name+type+domain) as the stable identifier and keep the Meshtastic node id as a separate field.
    Meshtastic TV/Client/MeshClient.swift:1
  • These top-level globals leak into the module namespace and make it harder to reason about ownership. Prefer moving them into MeshClient (e.g., as private static let constants or a private nested enum) so they’re scoped to the only consumer.

// same node-color circle badges as the map pins (CircleText, shared with iOS).
//

import SwiftUI
Comment on lines +126 to +130
let node: MeshNode

var body: some View {
HStack(spacing: 16) {
// Same circle badge as the map pin (node color + short name).
Comment on lines +110 to +117
}
.padding(.horizontal, 32)
.padding(.vertical, 16)
}

private var disconnectButton: some View {
Button(role: .destructive) {
client.disconnect()
Copilot AI review requested due to automatic review settings July 24, 2026 14:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 33 out of 49 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (4)

Meshtastic TV/Client/NodeDiscovery.swift:1

  • The removal predicate is inconsistent with how DiscoveredNode items are keyed/constructed. When TXT records provide an "id" and/or "shortname", DiscoveredNode.id may not equal service.name, and DiscoveredNode.name may not equal service.name either—so removed services can remain “stuck” in discovered. Consider storing a stable service key (e.g., service.name + service.type + service.domain) in DiscoveredNode and using that for insert/update/remove, while keeping the advertised "id" as a separate display field.
    Meshtastic TV/Client/NodeDiscovery.swift:1
  • isBrowsing is set to true on start(), but there’s no delegate handling for browse failures (e.g., netServiceBrowser(_:didNotSearch:)), so the UI can remain in a perpetual “searching” state if the browse fails due to local network permission denial or other errors. Add didNotSearch (and optionally netServiceBrowserDidStopSearch) to clear isBrowsing and optionally publish an error state for the UI.
    Meshtastic TV/Map/MeshTVMapView.swift:1
  • Region fitting via min/max longitude breaks for meshes spanning the antimeridian (e.g., nodes at +179° and -179°), producing an overly large span and incorrect center. Using MKMapRect unioning via MKMapPoint (and calling setVisibleMapRect(_:edgePadding:animated:)) avoids dateline issues and tends to fit points more robustly.
    Meshtastic.xcodeproj/project.pbxproj:1
  • There are now two sibling PBXGroups both named “Accessory” under Meshtastic, which makes navigation and future maintenance error-prone (it’s easy to add files to the wrong group). Consider renaming the tvOS-specific subset group (e.g., “Accessory (Transport Only)” / “AccessoryTransport”), or nesting it explicitly under a tvOS-related group to keep intent clear.

Comment on lines +110 to +117
private var disconnectButton: some View {
Button(role: .destructive) {
client.disconnect()
} label: {
Label("Disconnect", systemImage: "xmark.circle.fill")
}
.buttonStyle(.bordered)
}
Comment on lines +8 to +10
// The list is the primary, reliable selection path on tvOS; clicking a pin on
// the map (focus engine) selects too and keeps the list in sync. Rows carry the
// same node-color circle badges as the map pins (CircleText, shared with iOS).
… exists

Resolve two CodeRabbit findings on the tvOS app.

MeshClient: overlapping connect() calls raced — the outer Task was never stored,
so disconnect() could not cancel it and a stale attempt awaiting the TCP handshake
could clobber the connection/consumeTask a newer attempt set up, orphaning a
TCPConnection. Track the attempt in connectTask, cancel it from disconnect() (and
implicitly from connect()'s leading disconnect()), and guard the post-handshake
assignment on !Task.isCancelled, tearing down the orphaned connection. Also guard
the catch so a superseded attempt cancelled during connect()/send() can't clobber
the newer attempt's state to .failed.

MeshTVMapView: the list can select a node with no location, but the map only
annotates located nodes. Marking lastAppliedSelection immediately meant that once
the node gained a location its re-applied selection was skipped and never centered.
Defer lastAppliedSelection until centering succeeds, track a separate
pendingSelectionNum, and only schedule the fly once the annotation exists — while
still cancelling a stale pending fly when focus moves to an unlocated node.
Copilot AI review requested due to automatic review settings July 26, 2026 02:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 33 out of 49 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (3)

Meshtastic TV/App/MapScreen.swift:13

  • This file uses UIColor(hex:) in NodeRow but doesn’t import UIKit, which will fail to compile because UIColor is defined in UIKit (not SwiftUI).
import SwiftUI

Meshtastic TV/App/MapScreen.swift:10

  • The header comment says clicking a map pin “keeps the list in sync”, but the current implementation only drives selectedNodeNum from list focus (focusedNodeNum); MeshTVMapView never writes back to selectedNodeNum, and MapScreen doesn’t update focus from map selection. Update the comment or implement two-way selection syncing.
//  Live mesh map with a focusable node side-list for Siri Remote operation.
//  The list is the primary, reliable selection path on tvOS; clicking a pin on
//  the map (focus engine) selects too and keeps the list in sync. Rows carry the
//  same node-color circle badges as the map pins (CircleText, shared with iOS).

project.yml:675

  • The tvOS target pulls in Meshtastic/Views/Helpers/CircleText.swift for pin styling, but that file currently imports SwiftData (import SwiftData). This contradicts the PR description (“does not link … SwiftData”) and may break tvOS builds if SwiftData isn’t available/desired. Consider either (a) removing/conditionalizing the SwiftData import in CircleText.swift, or (b) adding a tvOS-local CircleText implementation instead of sharing the iOS file.

// Minimal node detail. No distance-to-me / bearing (no device GPS on tvOS).
//

import SwiftUI
Comment thread Meshtastic TV/App/RootView.swift Outdated
Comment on lines +24 to +25
// Meshtastic brand green (the Live Activity / widget tint).
.tint(Color("LightIndigo"))
garthvh added 4 commits July 26, 2026 08:58
Replace the SF Rounded 'Meshtastic' text (Connect hero + map header) with the
official outlined wordmark from meshtastic/design (typelogo), recoloured white via
a root fill so every letter inherits it. The rounded system font read as too
playful next to the brand mark.
Make cluster badges focusable with a focus highlight; selecting one zooms to fit
its members so it breaks apart into individual pins. Selecting a node from the
side list now zooms down tight (~800m) and animates, so a node inside a dense
cluster (e.g. the 900-node sim) is actually visible rather than swallowed.
This app is a live wall display, so set isIdleTimerDisabled on appear and
re-assert it on every active scene transition — the tvOS screensaver must not
interrupt the mesh map.
Promote MeshNode to a @model and add a tvOS-local ModelContainer. MeshClient
fetch-or-creates nodes into that context (change-guarded to avoid @query churn)
instead of an in-memory dict; the map/list read via @query. The store is kept
across relaunch, so the map is populated from the last session before the radio's
node-DB dump completes. Deliberately NOT the iOS entities — NodeInfoEntity's
relationship graph is one connected component of ~39 tables, so it can't be
subsetted for a map.
Copilot AI review requested due to automatic review settings July 26, 2026 15:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 34 out of 51 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (4)

Meshtastic TV/Views/NodeDetailView.swift:10

  • NodeDetailView references UIColor (via Color(UIColor(hex: ...))) but the file only imports SwiftUI. UIColor isn’t available without importing UIKit, which will cause a compile error for this target.
    Meshtastic TV/App/MapScreen.swift:14
  • This file uses UIColor (e.g. Color(UIColor(hex: node.num))) but doesn’t import UIKit, which will fail to compile.
import SwiftUI
import SwiftData

Meshtastic TV/Client/NodeDiscovery.swift:79

  • didRemove tries to remove discovered entries using service.name, but DiscoveredNode.id is currently set to the TXT-record id when present. When id is present and shortname differs from service.name, the removed service may never be removed from discovered (and may also duplicate on re-resolve). Using service.name as the stable identifier fixes removal/deduping.
    Meshtastic TV/Client/NodeDiscovery.swift:50
  • stop() clears the resolving set but does not cancel in-flight NetService.resolve(withTimeout:) operations. Late netServiceDidResolveAddress callbacks can still arrive after browsing stops / the view disappears and mutate discovered unexpectedly. The iOS TCPTransport explicitly cancels pending resolutions by calling service.stop() and clearing delegates.

Comment on lines +84 to +86
Button {
client.connect(host: node.host, port: node.port)
} label: {
Comment on lines +19 to +25
init() {
// Slim tvOS-local store — just `MeshNode` (see MeshNode.swift). Persists the
// node database so the map is populated on relaunch before the radio re-dumps.
let container = try! ModelContainer(for: MeshNode.self)
self.container = container
_client = State(initialValue: MeshClient(context: container.mainContext))
}
Comment thread Meshtastic TV/App/MeshtasticTVApp.swift Outdated
Comment on lines +20 to +22
// Slim tvOS-local store — just `MeshNode` (see MeshNode.swift). Persists the
// node database so the map is populated on relaunch before the radio re-dumps.
let container = try! ModelContainer(for: MeshNode.self)
garthvh added 2 commits July 26, 2026 10:09
Add a Settings detail reachable from both the connect screen and the map's
side list. Two options for now:

- Clear Node Database: wipes the persisted MeshNode store; footer shows the
  live saved-node count. Nodes repopulate from the radio on next connect.
- Map Type: Standard / Hybrid / Satellite, stored in the shared tv.mapType
  AppStorage key the map reads.

Default-focuses the destructive Clear button so a Menu press can't drift
onto the map behind it.
…t-zoom

Several map-interaction fixes so the Siri Remote can always get back to the
node list and stacked pins actually separate:

- Make the map non-interactive (isUserInteractionEnabled = false) and the
  cluster views non-focusable, so the focus engine never traps the remote on
  the MKMapView — the list stays the reliable selection path.
- Spread (near-)coincident nodes onto a small ring (ported from the iOS map's
  pre-disambiguation-sheet behaviour, sized up for the TV's larger pins) so a
  stubborn cluster breaks into individual pins once you zoom in.
- Only ever zoom in on selection (selectionZoomMeters), never back out.
- Apply the tv.mapType preference (Standard / Hybrid / Satellite) live.
Copilot AI review requested due to automatic review settings July 26, 2026 17:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 35 out of 52 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (8)

Meshtastic TV/Views/NodeDetailView.swift:11

  • UIColor(hex:) is used here, but this file doesn't import UIKit, so it won't compile on tvOS. Add import UIKit (or switch to a pure-SwiftUI color initializer if you prefer).
    Meshtastic TV/App/MapScreen.swift:15
  • UIColor(hex:) is referenced below, but UIKit isn't imported in this file, which will cause a compile error. Add import UIKit alongside the other imports.
import SwiftUI
import SwiftData

Meshtastic TV/Map/MeshTVMapView.swift:11

  • The header comment says map selection is driven by the focus engine (clicking a pin), but the implementation disables map interaction (isUserInteractionEnabled = false) and pins/clusters are not focusable. This comment is misleading; either update the comment to match the current behavior or re-enable map interaction/focus if pin selection is intended.
    Meshtastic TV/Map/MeshTVMapView.swift:41
  • This comment says the annotation view is “focus-engine selectable”, but canBecomeFocused is overridden to return false below, so pins are explicitly non-focusable. Consider updating the comment to avoid implying pin selection via focus is supported.
    Meshtastic TV/App/MeshtasticTVApp.swift:25
  • Using try! for the SwiftData container will crash the app at launch if the container cannot be created (e.g. file-system or migration issues). Prefer handling the thrown error explicitly and failing with a clearer message (or showing an in-app error state).
	init() {
		// Slim tvOS-local store — just `MeshNode` (see MeshNode.swift). Persists the
		// node database so the map is populated on relaunch before the radio re-dumps.
		let container = try! ModelContainer(for: MeshNode.self)
		self.container = container
		_client = State(initialValue: MeshClient(context: container.mainContext))
	}

Meshtastic TV/App/MapScreen.swift:10

  • This file’s header comment says “clicking a pin” selects nodes, but MeshTVMapView disables map interaction and pins/clusters are non-focusable. Update this comment to match the current interaction model (side list drives selection), or re-enable pin selection if that’s still a goal.
//  Live mesh map with a focusable node side-list for Siri Remote operation.
//  The list is the primary, reliable selection path on tvOS; clicking a pin on
//  the map (focus engine) selects too and keeps the list in sync. Rows carry the
//  same node-color circle badges as the map pins (CircleText, shared with iOS).

Meshtastic.xcodeproj/project.pbxproj:645

  • The Meshtastic group also contains duplicate Extensions groups (both named Extensions). This duplication can cause ambiguous file locations and makes future edits harder; consider consolidating to a single group and keeping only one set of file references.
    Meshtastic.xcodeproj/project.pbxproj:662
  • There are duplicate Views groups under Meshtastic (the newly added A51EAC... and the existing D98377...). Consolidating these avoids a confusing project navigator structure and reduces the chance of accidentally adding files to the wrong group later.

Comment on lines +167 to +172
/// Called on a Menu press while the map has focus. MKMapView captures the
/// directional input for panning and never releases focus on its own, so
/// without this the map is a focus trap (and an unhandled Menu press can
/// suspend the app instead of going back). MapScreen uses it to hand focus
/// back to the node list.
var onMenuExit: (() -> Void)?
Comment on lines +7 to +17
// Persistent, tvOS-local model for a mesh node. Deliberately NOT the iOS
// `NodeInfoEntity` — that entity's relationship graph is a single connected
// component of ~39 tables (messages, channels, traceroutes, every config), so it
// can't be subsetted for a map. This is a slim, map-scoped SwiftData store: the
// map is populated on relaunch from the last session, before the radio's node-DB
// dump completes.
//

import CoreLocation
import Foundation
import SwiftData
Comment on lines 631 to 635
children = (
FB5CB2FD9C793A43DE2DB818 /* Accessory */,
46D9EC9A9870D40B64CBD339 /* Accessory */,
1B104A61866EBBAB39A96CD6 /* API */,
91DE2B8079DF4453A2B7A512 /* AppIntents */,
Bring a few things over from the iOS app:

- Generate a default user for packet-discovered nodes: a node first seen via a
  position packet (no NodeInfo yet) now gets "Meshtastic <last4>" / "<last4>"
  (last 4 hex of the node number), matching the firmware/app default, instead of
  rendering as "?". A real NodeInfo frame overwrites it; an empty one won't.
- Compact list rows modelled on NodeListItemCompact: node-color circle, name,
  then last-heard (green when online, heard <2 h) and role (mapped icon + name).
- Scrollable node detail. The old read-only Form had no focusable rows, so tvOS
  couldn't scroll to fields below the fold; rows are now individually focusable
  (with the usual highlight), and the whole panel is resized down for the 520pt
  side column.

Role is now stored as its raw protobuf value and mapped for display by a slim,
tvOS-local NodeRole (name + SF Symbol), so this target doesn't need the app's
full DeviceRoles enum. The store open is now wipe-and-retry resilient so the
role field's type change can't brick launch on an un-migratable store.
Copilot AI review requested due to automatic review settings July 26, 2026 18:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 35 out of 52 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (4)

Meshtastic TV/Map/MeshTVMapView.swift:239

  • annotationsByNum is mutated while being iterated (annotationsByNum[num] = nil inside a for ... in annotationsByNum loop). In Swift this can trap at runtime with “Dictionary mutated while being enumerated”. Collect removals first, then apply them.
    Meshtastic TV/Views/NodeDetailView.swift:13
  • This file uses UIColor(hex:) but only imports SwiftUI. Add import UIKit (or refactor to avoid UIColor) to ensure it compiles on tvOS.
    Meshtastic TV/App/MapScreen.swift:14
  • This file uses UIColor(hex:) in NodeRow but doesn’t import UIKit. Add import UIKit so UIColor is available to this target.
import SwiftUI
import SwiftData

Meshtastic TV/App/MapScreen.swift:135

  • disconnectButton is a private computed view that isn’t used anywhere in this file. Keeping it around risks an “unused” warning and adds dead code to maintain.
	private var disconnectButton: some View {
		Button(role: .destructive) {
			client.disconnect()
		} label: {
			Label("Disconnect", systemImage: "xmark.circle.fill")

Comment on lines +40 to +42
/// The tvOS-local SwiftData store the map and list read via `@Query`. Every
/// upsert lands here; nothing is kept in memory, so the map survives relaunch.
private let context: ModelContext
- Frost the map's wordmark header (a bottom-fading .ultraThinMaterial scrim) so
  scrolling node rows no longer collide with the logo / host text.
- Align to design-standards v1.4: port the semantic palette
  (MeshtasticSuccess / Warning / Error) into the tvOS asset catalog and use it for
  online / offline / connect-error states instead of ad-hoc .green / .orange / .red.
- Drop the off-palette "LightIndigo" green tint. Interactive buttons and icons now
  follow the app AccentColor (Blue 700), matching the iOS app — the brand green
  fails WCAG contrast as a UI tint, so it stays reserved for the logo and semantic
  success only. The Bonjour hint reverts to .secondary (accent-blue text would fail
  contrast on the dark background).
Copilot AI review requested due to automatic review settings July 26, 2026 18:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 37 out of 54 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (4)

Meshtastic TV/App/MapScreen.swift:14

  • This file references UIColor (via Color(UIColor(hex: ...))) but doesn’t import UIKit, which will fail to compile.
import SwiftUI
import SwiftData

Meshtastic TV/Views/NodeDetailView.swift:13

  • This file references UIColor (via Color(UIColor(hex: ...))) but doesn’t import UIKit, which will fail to compile.
    Meshtastic.xcodeproj/project.pbxproj:643
  • This adds a second Accessory and Extensions group under the main Meshtastic group (duplicate names at the same hierarchy level), which makes the Xcode navigator ambiguous/confusing. Consider nesting these tvOS-shared file references under the Meshtastic TV group (or renaming to Accessory (Shared) / Extensions (Shared)) to avoid duplicates.
    Meshtastic TV/App/MeshtasticTVApp.swift:14
  • The PR description says the tvOS target does not link SwiftData and that there is 'No persistence in v1', but this target imports SwiftData and creates a persistent ModelContainer for MeshNode. Either update the PR description to reflect the persistence, or adjust the implementation to be in-memory only.
import OSLog
import SwiftUI
import SwiftData

Comment on lines +18 to +19
import MapKit
import SwiftUI
Comment on lines +176 to +177
@AppStorage("tv.mapType") private var mapTypeRaw: Int = Int(MKMapType.standard.rawValue)
private var mkMapType: MKMapType { MKMapType(rawValue: UInt(mapTypeRaw)) ?? .standard }
Comment on lines +145 to +148
case .configCompleteID:
// Initial dump finished — we have the node database; go live.
if state != .connected { state = .connected }

Address the two actionable findings from the design-standards /audit-ui pass:

- Typography: NodeDetailView's fixed .system(size:) fonts are now @ScaledMetric
  (mirroring the iOS NodeListItemCompact) so they track Dynamic Type rather than
  being hardcoded. Default sizes are unchanged.
- Magic numbers: extract the recurring design-decision sizes — side-list width,
  node-avatar diameters, screen padding, section spacing, row corner radius,
  wordmark height — into a shared TVTheme, replacing scattered literals across the
  map, detail, and connect views. Values are identical, so no visual change.
Copilot AI review requested due to automatic review settings July 26, 2026 18:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 38 out of 55 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (3)

Meshtastic TV/Client/MeshClient.swift:10

  • The PR description (and this header comment) describe MeshClient as “in-memory” and explicitly “no SwiftData persistence”, but the implementation uses SwiftData (ModelContext, MeshNode, persisted node store). Please either update the PR description to reflect the intentional persistence, or remove persistence if that’s not intended. At minimum, the header comment should match the actual behavior.
    Meshtastic TV/App/MapScreen.swift:10
  • This file header says “clicking a pin on the map (focus engine) selects too”, but MeshTVMapView currently sets mapView.isUserInteractionEnabled = false, making the map non-interactive. Update this comment to reflect the actual interaction model (side-list-driven selection), or re-enable map interaction if pin selection is intended.
//  Live mesh map with a focusable node side-list for Siri Remote operation.
//  The list is the primary, reliable selection path on tvOS; clicking a pin on
//  the map (focus engine) selects too and keeps the list in sync. Rows carry the
//  same node-color circle badges as the map pins (CircleText, shared with iOS).

Meshtastic TV/Map/MeshTVMapView.swift:171

  • The doc comment for onMenuExit describes handling Menu presses when the map has focus, but makeUIView disables user interaction (isUserInteractionEnabled = false), so the map shouldn’t receive focus/presses and menuPressed() is effectively unused. Either wire up Menu handling + re-enable interaction, or adjust/remove this comment/callback to match the current non-interactive map behavior.

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.

2 participants