peat-btle 0.3.0

Bluetooth Low Energy mesh transport for Peat Protocol
Documentation
//
//  ContentView.swift
//  PeatTest
//
//  Main view for Peat BLE mesh demo
//  Mirrors the Android PeatDemo MainActivity layout
//

import SwiftUI

struct ContentView: View {
    @EnvironmentObject var viewModel: PeatViewModel

    var body: some View {
        NavigationStack {
            VStack(spacing: 0) {
                // Status section
                StatusHeaderView()

                Divider()

                // Peer list
                PeerListView()

                Divider()

                // ACK status panel (only visible during active alert)
                if viewModel.ackStatus.isActive {
                    AckStatusView()
                }

                // Action buttons
                ActionButtonsView()
            }
            .navigationTitle("Peat Demo")
            #if os(iOS)
            .navigationBarTitleDisplayMode(.inline)
            #endif
            .onAppear {
                viewModel.startMesh()
            }
            .onDisappear {
                viewModel.shutdown()
            }
        }
        .overlay(alignment: .top) {
            // Toast overlay
            if let toast = viewModel.toastMessage {
                ToastView(message: toast)
                    .transition(.move(edge: .top).combined(with: .opacity))
                    .animation(.easeInOut(duration: 0.3), value: viewModel.toastMessage)
                    .padding(.top, 60)
            }
        }
    }
}

// MARK: - Status Header

struct StatusHeaderView: View {
    @EnvironmentObject var viewModel: PeatViewModel

    var body: some View {
        VStack(spacing: 8) {
            // Mesh ID badge
            Text("Mesh: \(PeatViewModel.MESH_ID)")
                .font(.caption)
                .fontWeight(.bold)
                .padding(.horizontal, 8)
                .padding(.vertical, 2)
                .background(Color.blue.opacity(0.2))
                .cornerRadius(4)
                .padding(.top, 8)

            // Local node ID
            Text("This device: \(viewModel.localDisplayName)")
                .font(.caption)
                .foregroundColor(.secondary)

            // Status message
            Text(viewModel.statusMessage)
                .font(.headline)
                .foregroundColor(viewModel.ackStatus.isActive ? .red : .primary)

            // Connected count
            if !viewModel.peers.isEmpty {
                Text("\(viewModel.connectedCount)/\(viewModel.totalPeerCount) peers connected")
                    .font(.subheadline)
                    .foregroundColor(.secondary)
            }
        }
        .padding(.horizontal)
        .padding(.bottom, 8)
    }
}

// MARK: - Peer List

struct PeerListView: View {
    @EnvironmentObject var viewModel: PeatViewModel

    var body: some View {
        List {
            if viewModel.peers.isEmpty {
                HStack {
                    ProgressView()
                        .padding(.trailing, 8)
                    Text("Scanning for Peat peers...")
                        .foregroundColor(.secondary)
                }
            } else {
                ForEach(viewModel.peers) { peer in
                    PeerRowView(peer: peer)
                }
            }
        }
        .listStyle(.plain)
    }
}

struct PeerRowView: View {
    let peer: PeatPeer

    var body: some View {
        VStack(alignment: .leading, spacing: 6) {
            HStack {
                Text(peer.displayName)
                    .font(.headline)
                    .foregroundColor(peer.isConnected ? .green : .primary)

                Spacer()

                Text("\(peer.rssi) dBm")
                    .font(.subheadline)
                    .foregroundColor(.secondary)

                // Connection status indicator
                if peer.isConnected {
                    Image(systemName: "checkmark.circle.fill")
                        .foregroundColor(.green)
                } else {
                    ProgressView()
                        .scaleEffect(0.8)
                }
            }

            // Show metadata
            VStack(alignment: .leading, spacing: 2) {
                if let advName = peer.advertisedName {
                    Text("Advertised: \(advName)")
                        .font(.caption2)
                        .foregroundColor(.secondary)
                }

                HStack {
                    Text("Node: 0x\(String(format: "%08X", peer.nodeId))")
                        .font(.caption2)
                        .foregroundColor(.secondary)

                    if let meshId = peer.meshId {
                        Text("Mesh: \(meshId)")
                            .font(.caption2)
                            .padding(.horizontal, 4)
                            .background(meshId == PeatViewModel.MESH_ID ? Color.green.opacity(0.2) : Color.orange.opacity(0.2))
                            .cornerRadius(2)
                    }
                }

                Text("UUID: \(peer.identifier)")
                    .font(.caption2)
                    .foregroundColor(.secondary)
                    .lineLimit(1)
                    .truncationMode(.middle)

                HStack {
                    Text(peer.isConnected ? "● Connected" : "○ Connecting...")
                        .font(.caption)
                        .foregroundColor(peer.isConnected ? .green : .orange)

                    Spacer()

                    Text("Last seen: \(peer.lastSeen.formatted(.relative(presentation: .numeric)))")
                        .font(.caption2)
                        .foregroundColor(.secondary)
                }
            }
        }
        .padding(.vertical, 6)
    }
}

// MARK: - ACK Status Panel

struct AckStatusView: View {
    @EnvironmentObject var viewModel: PeatViewModel

    var body: some View {
        VStack(alignment: .leading, spacing: 6) {
            if !viewModel.ackStatus.ackedNodes.isEmpty {
                HStack {
                    Text("✓ ACK'd:")
                        .foregroundColor(.green)
                    Text(viewModel.ackStatus.ackedNodes.map { String(format: "Peat-%08X", $0) }.joined(separator: ", "))
                        .font(.caption)
                }
            }

            if !viewModel.ackStatus.waitingNodes.isEmpty {
                HStack {
                    Text("⏳ Waiting:")
                        .foregroundColor(.orange)
                    Text(viewModel.ackStatus.waitingNodes.map { String(format: "Peat-%08X", $0) }.joined(separator: ", "))
                        .font(.caption)
                }
            }
        }
        .padding()
        .frame(maxWidth: .infinity, alignment: .leading)
        .background(Color.yellow.opacity(0.2))
    }
}

// MARK: - Action Buttons

struct ActionButtonsView: View {
    @EnvironmentObject var viewModel: PeatViewModel

    var body: some View {
        VStack(spacing: 12) {
            // Emergency button
            Button(action: { viewModel.sendEmergency() }) {
                HStack {
                    Image(systemName: "exclamationmark.triangle.fill")
                    Text("EMERGENCY")
                        .fontWeight(.bold)
                }
                .frame(maxWidth: .infinity)
                .padding()
                .background(Color.red)
                .foregroundColor(.white)
                .cornerRadius(10)
            }
            .disabled(!viewModel.isMeshActive)

            HStack(spacing: 12) {
                // ACK button - green when alert active, grey when not
                Button(action: { viewModel.sendAck() }) {
                    HStack {
                        Image(systemName: "checkmark.circle.fill")
                        Text("ACK")
                            .fontWeight(.bold)
                    }
                    .frame(maxWidth: .infinity)
                    .padding()
                    .background(viewModel.ackStatus.isActive ? Color.green : Color.gray.opacity(0.5))
                    .foregroundColor(.white)
                    .cornerRadius(10)
                }
                .disabled(!viewModel.isMeshActive || !viewModel.ackStatus.isActive)

                // Reset button
                Button(action: { viewModel.resetAlert() }) {
                    HStack {
                        Image(systemName: "xmark.circle.fill")
                        Text("RESET")
                            .fontWeight(.bold)
                    }
                    .frame(maxWidth: .infinity)
                    .padding()
                    .background(viewModel.ackStatus.isActive ? Color.gray : Color.gray.opacity(0.5))
                    .foregroundColor(.white)
                    .cornerRadius(10)
                }
                .disabled(!viewModel.ackStatus.isActive)
            }
        }
        .padding()
    }
}

// MARK: - Toast View

struct ToastView: View {
    let message: String

    var body: some View {
        Text(message)
            .font(.subheadline)
            .padding(.horizontal, 16)
            .padding(.vertical, 10)
            .background(Color.black.opacity(0.8))
            .foregroundColor(.white)
            .cornerRadius(20)
    }
}

#Preview {
    ContentView()
        .environmentObject(PeatViewModel())
}