2026-01-08 19:16:32 +00:00
|
|
|
//
|
|
|
|
|
// BleStatusView.swift
|
|
|
|
|
// iris
|
|
|
|
|
//
|
|
|
|
|
// Created by Codex.
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
import SwiftUI
|
|
|
|
|
|
|
|
|
|
struct BleStatusView: View {
|
|
|
|
|
@EnvironmentObject private var ble: BlePeripheralManager
|
|
|
|
|
@EnvironmentObject private var orchestrator: ContextOrchestrator
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
VStack(alignment: .leading, spacing: 16) {
|
|
|
|
|
VStack(alignment: .leading, spacing: 6) {
|
2026-01-10 01:32:42 +00:00
|
|
|
Text("Aris BLE")
|
2026-01-08 19:16:32 +00:00
|
|
|
.font(.title2.bold())
|
|
|
|
|
Text("Bluetooth: \(bluetoothStateText)")
|
|
|
|
|
.font(.subheadline)
|
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Toggle(isOn: $ble.advertisingEnabled) {
|
|
|
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
|
|
|
Text("Advertising")
|
|
|
|
|
.font(.headline)
|
|
|
|
|
Text(ble.isAdvertising ? "On" : "Off")
|
|
|
|
|
.font(.subheadline)
|
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.onChange(of: ble.advertisingEnabled) { _ in
|
|
|
|
|
ble.start()
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-10 14:23:39 +00:00
|
|
|
Toggle(isOn: Binding(get: { ble.wifiRequested }, set: { ble.setWifiRequested($0) })) {
|
|
|
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
|
|
|
Text("Request Wi-Fi (Glass)")
|
|
|
|
|
.font(.headline)
|
|
|
|
|
Text(ble.wifiRequested ? "On" : "Off")
|
|
|
|
|
.font(.subheadline)
|
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-08 19:16:32 +00:00
|
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
|
|
|
Text("Connection")
|
|
|
|
|
.font(.headline)
|
|
|
|
|
Text("Subscribed: \(ble.isSubscribed ? "Yes" : "No")")
|
|
|
|
|
.font(.subheadline)
|
|
|
|
|
Text("Subscribers: \(ble.subscribedCount)")
|
|
|
|
|
.font(.subheadline)
|
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
|
|
|
Text("Telemetry")
|
|
|
|
|
.font(.headline)
|
|
|
|
|
Text("Last msgId: \(ble.lastMsgIdSent)")
|
|
|
|
|
.font(.subheadline)
|
|
|
|
|
Text("Last ping: \(ble.lastPingAt.map { timeOnly(from: $0) } ?? "Never")")
|
|
|
|
|
.font(.subheadline)
|
|
|
|
|
Text("Last data: \(ble.lastDataAt.map { timeOnly(from: $0) } ?? "Never")")
|
|
|
|
|
.font(.subheadline)
|
|
|
|
|
Text("Notify queue: \(ble.notifyQueueDepth)")
|
|
|
|
|
.font(.subheadline)
|
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
if ble.droppedNotifyPackets > 0 {
|
|
|
|
|
Text("Dropped notify packets: \(ble.droppedNotifyPackets)")
|
|
|
|
|
.font(.subheadline)
|
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
}
|
|
|
|
|
Text("Last notify: \(ble.lastNotifyAt.map { timeOnly(from: $0) } ?? "Never")")
|
|
|
|
|
.font(.subheadline)
|
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
if let cmd = ble.lastCommand, !cmd.isEmpty {
|
|
|
|
|
Text("Last control: \(cmd)")
|
|
|
|
|
.font(.subheadline)
|
|
|
|
|
.foregroundStyle(.secondary)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
|
|
|
Button("Send Fixture Feed Now") {
|
|
|
|
|
orchestrator.sendFixtureFeedNow()
|
|
|
|
|
}
|
|
|
|
|
.buttonStyle(.borderedProminent)
|
|
|
|
|
|
|
|
|
|
Button("Copy UUIDs") {
|
|
|
|
|
ble.copyUUIDsToPasteboard()
|
|
|
|
|
}
|
|
|
|
|
.buttonStyle(.bordered)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
|
|
|
Text("UUIDs")
|
|
|
|
|
.font(.headline)
|
2026-01-10 14:23:39 +00:00
|
|
|
Text("Service: \(BlePeripheralManager.serviceUUID.uuidString)\nFEED_TX: \(BlePeripheralManager.feedTxUUID.uuidString)\nCONTROL_RX: \(BlePeripheralManager.controlRxUUID.uuidString)\nWIFI_REQUEST_TX: \(BlePeripheralManager.wifiRequestTxUUID.uuidString)")
|
2026-01-08 19:16:32 +00:00
|
|
|
.font(.caption)
|
|
|
|
|
.textSelection(.enabled)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.padding(.vertical, 16)
|
|
|
|
|
.padding(.horizontal, 24)
|
|
|
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
|
|
|
.onAppear { ble.start() }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private var bluetoothStateText: String {
|
|
|
|
|
switch ble.bluetoothState {
|
|
|
|
|
case .unknown: return "Unknown"
|
|
|
|
|
case .resetting: return "Resetting"
|
|
|
|
|
case .unsupported: return "Unsupported"
|
|
|
|
|
case .unauthorized: return "Unauthorized"
|
|
|
|
|
case .poweredOff: return "Powered Off"
|
|
|
|
|
case .poweredOn: return "Powered On"
|
|
|
|
|
@unknown default: return "Other"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func timeOnly(from date: Date) -> String {
|
|
|
|
|
let formatter = DateFormatter()
|
|
|
|
|
formatter.dateStyle = .none
|
|
|
|
|
formatter.timeStyle = .medium
|
|
|
|
|
return formatter.string(from: date)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct BleStatusView_Previews: PreviewProvider {
|
|
|
|
|
static var previews: some View {
|
|
|
|
|
Text("Preview unavailable (requires EnvironmentObjects).")
|
|
|
|
|
}
|
|
|
|
|
}
|