Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
47cd229
Add tvOS app: TCP mesh map with Bonjour discovery
garthvh Jul 24, 2026
0991176
Brand the tvOS app and use circle short-name node pins
garthvh Jul 24, 2026
20de904
Fix focus-sound static on the tvOS map and add a Re-center button
garthvh Jul 24, 2026
437dddc
Zoom in when selecting a node hidden inside a cluster
garthvh Jul 24, 2026
f74d5e7
Silence residual focus static: non-focusable pins + debounced selecti…
garthvh Jul 24, 2026
634c11d
Fix visual static on node transitions: cut long flights, track row focus
garthvh Jul 24, 2026
49571c6
Escape the map focus trap with the Menu button
garthvh Jul 24, 2026
95de976
Move Re-center and Disconnect into the node list column
garthvh Jul 24, 2026
1308283
Merge branch 'main' into feat/tvos-mesh-map
garthvh Jul 24, 2026
98ed705
fix(tvos): cancellable connect + defer map selection until annotation…
garthvh Jul 26, 2026
c44fcd5
feat(tvos): use the brand wordmark for the Meshtastic logo lockup
garthvh Jul 26, 2026
f88b6f4
feat(tvos): selectable clusters + zoom onto the selected node
garthvh Jul 26, 2026
c47bf33
feat(tvos): keep the screen awake while foregrounded
garthvh Jul 26, 2026
a4ee513
feat(tvos): slim SwiftData persistence for the node store
garthvh Jul 26, 2026
37f707e
feat(tvos): settings screen — clear node database + map type
garthvh Jul 26, 2026
fed8917
fix(tvos): reliable list focus, coincident-node spread, tighter selec…
garthvh Jul 26, 2026
9f0e612
feat(tvos): default node info, compact list rows, scrollable detail
garthvh Jul 26, 2026
5a546eb
style(tvos): header scrim + design-standards color palette
garthvh Jul 26, 2026
60b95a2
refactor(tvos): Dynamic-Type fonts + shared layout tokens (design audit)
garthvh Jul 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions Meshtastic TV/App/ConnectView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
//
// ConnectView.swift
// Meshtastic TV
//
// Copyright(c) Garth Vander Houwen 7/24/26.
//
// Bonjour-discovered nodes (tap to connect) plus manual host / port entry,
// under a Meshtastic logo hero.
//

import SwiftUI

struct ConnectView: View {
@Bindable var client: MeshClient
@StateObject private var discovery = NodeDiscovery()

@AppStorage("tv.lastHost") private var host: String = ""
@AppStorage("tv.lastPort") private var portText: String = "4403"

var body: some View {
NavigationStack {
HStack(alignment: .top, spacing: 60) {
hero
.frame(maxWidth: 640)

Form {
discoveredSection

Section("Manual Connection") {
TextField("IP address or hostname", text: $host)
.keyboardType(.URL)
.textContentType(.URL)
.autocorrectionDisabled()
TextField("Port", text: $portText)
.keyboardType(.numberPad)
Button {
client.connect(host: trimmedHost, port: port)
} label: {
Label("Connect", systemImage: "network")
}
.disabled(trimmedHost.isEmpty)
}

if case .failed(let message) = client.state {
Section {
Label(message, systemImage: "exclamationmark.triangle.fill")
.foregroundStyle(.red)
}
}
}
}
.padding(.top, 40)
}
}

private var hero: some View {
VStack(alignment: .leading, spacing: 28) {
Image("m-logo-white")
.resizable()
.scaledToFit()
.frame(width: 340)
Text("Meshtastic")
.font(.system(size: 64, weight: .heavy, design: .rounded))
Text("Connect to a Meshtastic node on your network and watch the mesh live on the big screen.")
.font(.title3)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
Label("Radios advertising TCP appear automatically", systemImage: "bonjour")
.font(.callout)
.foregroundStyle(Color("LightIndigo"))
}
.frame(maxHeight: .infinity, alignment: .top)
}

@ViewBuilder
private var discoveredSection: some View {
Section {
if discovery.discovered.isEmpty {
HStack(spacing: 16) {
ProgressView()
Text("Searching the local network…")
.foregroundStyle(.secondary)
}
} else {
ForEach(discovery.discovered) { node in
Button {
client.connect(host: node.host, port: node.port)
} label: {
Comment on lines +92 to +94
HStack(spacing: 16) {
Image(systemName: "antenna.radiowaves.left.and.right")
.foregroundStyle(Color("LightIndigo"))
VStack(alignment: .leading, spacing: 4) {
Text(node.name)
Text(verbatim: "\(node.host):\(String(node.port))")
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
}
}
} header: {
Text("Discovered Nodes")
}
.onAppear { discovery.start() }
.onDisappear { discovery.stop() }
}

private var trimmedHost: String {
host.trimmingCharacters(in: .whitespacesAndNewlines)
}

private var port: Int {
Int(portText.trimmingCharacters(in: .whitespaces)) ?? 4403
}
Comment on lines +119 to +121
}
117 changes: 117 additions & 0 deletions Meshtastic TV/App/MapScreen.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
//
// MapScreen.swift
// Meshtastic TV
//
// Copyright(c) Garth Vander Houwen 7/24/26.
//
// 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 +8 to +10
//
Comment on lines +7 to +11

import SwiftUI

struct MapScreen: View {
@Bindable var client: MeshClient
@State private var selectedNodeNum: UInt32?

var body: some View {
HStack(spacing: 0) {
nodeList
.frame(width: 520)

MeshTVMapView(nodes: client.locatedNodes, selectedNodeNum: $selectedNodeNum)
.ignoresSafeArea()
}
.overlay(alignment: .topTrailing) {
disconnectButton
.padding(40)
}
}

private var nodeList: some View {
NavigationStack {
List(selection: $selectedNodeNum) {
Section {
ForEach(client.sortedNodes) { node in
NavigationLink(value: node.num) {
NodeRow(node: node)
}
}
} header: {
Text("\(client.nodes.count) nodes · \(client.locatedNodes.count) on map")
}
}
.navigationDestination(for: UInt32.self) { num in
if let node = client.nodes[num] {
NodeDetailView(node: node)
}
}
.safeAreaInset(edge: .top) {
header
}
}
}

private var header: some View {
HStack(spacing: 20) {
Image("m-logo-white")
.resizable()
.scaledToFit()
.frame(height: 44)
VStack(alignment: .leading, spacing: 2) {
Text("Meshtastic")
.font(.system(size: 34, weight: .heavy, design: .rounded))
Text(client.host)
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
}
.padding(.horizontal, 32)
.padding(.vertical, 16)
}

private var disconnectButton: some View {
Button(role: .destructive) {
client.disconnect()
Comment on lines +126 to +151
} label: {
Label("Disconnect", systemImage: "xmark.circle.fill")
}
.buttonStyle(.bordered)
}
Comment on lines +149 to +156
Comment on lines +149 to +156
}

private struct NodeRow: View {
let node: MeshNode

var body: some View {
HStack(spacing: 16) {
// Same circle badge as the map pin (node color + short name).
Comment on lines +162 to +166
CircleText(
text: node.shortName.isEmpty ? "?" : node.shortName,
color: Color(UIColor(hex: node.num)),
circleSize: 52
)
.opacity(node.hasLocation ? 1 : 0.45)

VStack(alignment: .leading, spacing: 4) {
Text(node.displayName)
.lineLimit(1)
HStack(spacing: 12) {
if let battery = node.batteryLevel {
Label("\(battery)%", systemImage: battery > 20 ? "battery.75percent" : "battery.25percent")
.foregroundStyle(Color("LightIndigo"))
}
if !node.hasLocation {
Label("No position", systemImage: "location.slash")
.foregroundStyle(.secondary)
}
}
.font(.caption)
}
Spacer()
}
}
}
22 changes: 22 additions & 0 deletions Meshtastic TV/App/MeshtasticTVApp.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// MeshtasticTVApp.swift
// Meshtastic TV
//
// Copyright(c) Garth Vander Houwen 7/24/26.
//
// Apple TV client: connect to a Meshtastic node over TCP and show a live mesh map
// operable with the Siri Remote.
//

import SwiftUI

@main
struct MeshtasticTVApp: App {
@State private var client = MeshClient()

var body: some Scene {
WindowGroup {
RootView(client: client)
}
}
}
48 changes: 48 additions & 0 deletions Meshtastic TV/App/RootView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//
// RootView.swift
// Meshtastic TV
//
// Copyright(c) Garth Vander Houwen 7/24/26.
//

import SwiftUI

struct RootView: View {
@Bindable var client: MeshClient

var body: some View {
Group {
switch client.state {
case .connected:
MapScreen(client: client)
case .connecting:
ConnectingView(host: client.host) { client.disconnect() }
case .disconnected, .failed:
ConnectView(client: client)
}
}
// Meshtastic brand green (the Live Activity / widget tint).
.tint(Color("LightIndigo"))
}
}

private struct ConnectingView: View {
let host: String
let onCancel: () -> Void

var body: some View {
VStack(spacing: 48) {
Image("m-logo-white")
.resizable()
.scaledToFit()
.frame(width: 280)
ProgressView()
.scaleEffect(1.6)
.tint(Color("LightIndigo"))
Text("Connecting to \(host)…")
.font(.title2)
.foregroundStyle(.secondary)
Button("Cancel", action: onCancel)
}
}
}
20 changes: 20 additions & 0 deletions Meshtastic TV/Assets.xcassets/AccentColor.colorset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.659",
"green" : "0.333",
"red" : "0.157"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"images": [
{
"idiom": "tv",
"filename": "back_1x.png",
"scale": "1x"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info": {
"author": "xcode",
"version": 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"layers": [
{
"filename": "Front.imagestacklayer"
},
{
"filename": "Back.imagestacklayer"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"images": [
{
"idiom": "tv",
"filename": "front_1x.png",
"scale": "1x"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info": {
"author": "xcode",
"version": 1
}
}
Loading
Loading