cue-ios: strip CoreBluetooth, replace with UDP/WiFi transport
This commit is contained in:
parent
d13909c400
commit
3f91159596
|
|
@ -1,6 +1,5 @@
|
|||
import Foundation
|
||||
import Observation
|
||||
import Combine
|
||||
|
||||
enum Tab: String, CaseIterable, Identifiable {
|
||||
case eis = "EIS"
|
||||
|
|
@ -19,12 +18,13 @@ enum Tab: String, CaseIterable, Identifiable {
|
|||
|
||||
@Observable
|
||||
final class AppState {
|
||||
let ble: BLEManager
|
||||
let transport: UDPManager
|
||||
var tab: Tab = .eis
|
||||
var status: String = "Disconnected"
|
||||
var bleConnected: Bool = false
|
||||
var tempC: Float = 25.0
|
||||
|
||||
var connected: Bool { transport.state == .connected }
|
||||
|
||||
// EIS
|
||||
var eisPoints: [EisPoint] = []
|
||||
var sweepTotal: UInt16 = 0
|
||||
|
|
@ -98,40 +98,17 @@ final class AppState {
|
|||
var cleanV: String = "1200"
|
||||
var cleanDur: String = "30"
|
||||
|
||||
// Temperature polling
|
||||
private var tempTimer: Timer?
|
||||
|
||||
init() {
|
||||
ble = BLEManager()
|
||||
ble.setMessageHandler { [weak self] msg in
|
||||
transport = UDPManager()
|
||||
transport.setMessageHandler { [weak self] msg in
|
||||
self?.handleMessage(msg)
|
||||
}
|
||||
startTempPolling()
|
||||
}
|
||||
|
||||
// MARK: - BLE connection tracking
|
||||
|
||||
func updateConnectionState() {
|
||||
let connected = ble.state == .connected
|
||||
if connected != bleConnected {
|
||||
bleConnected = connected
|
||||
status = ble.state.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Temperature polling
|
||||
|
||||
private func startTempPolling() {
|
||||
tempTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] _ in
|
||||
guard let self, self.bleConnected else { return }
|
||||
self.send(buildSysexGetTemp())
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Send helper
|
||||
|
||||
func send(_ sysex: [UInt8]) {
|
||||
ble.sendCommand(sysex)
|
||||
transport.send(sysex)
|
||||
}
|
||||
|
||||
// MARK: - Message handler
|
||||
|
|
|
|||
|
|
@ -1,227 +0,0 @@
|
|||
/// CoreBluetooth BLE MIDI connection manager.
|
||||
/// Scans for "EIS4", connects, subscribes to MIDI notifications,
|
||||
/// parses incoming BLE MIDI packets into EisMessage values.
|
||||
|
||||
import CoreBluetooth
|
||||
import Foundation
|
||||
|
||||
@Observable
|
||||
final class BLEManager: NSObject {
|
||||
|
||||
nonisolated(unsafe) static let midiServiceUUID = CBUUID(string: "03B80E5A-EDE8-4B33-A751-6CE34EC4C700")
|
||||
nonisolated(unsafe) static let midiCharUUID = CBUUID(string: "7772E5DB-3868-4112-A1A9-F2669D106BF3")
|
||||
|
||||
enum ConnectionState: String {
|
||||
case disconnected = "Disconnected"
|
||||
case scanning = "Scanning..."
|
||||
case connecting = "Connecting..."
|
||||
case connected = "Connected"
|
||||
}
|
||||
|
||||
struct DiscoveredDevice: Identifiable {
|
||||
let id: UUID
|
||||
let peripheral: CBPeripheral
|
||||
let name: String
|
||||
let rssi: Int
|
||||
var serviceUUIDs: [CBUUID]
|
||||
}
|
||||
|
||||
var state: ConnectionState = .disconnected
|
||||
var lastMessage: EisMessage?
|
||||
var discoveredDevices: [DiscoveredDevice] = []
|
||||
|
||||
private var centralManager: CBCentralManager!
|
||||
private(set) var peripheral: CBPeripheral?
|
||||
private var midiCharacteristic: CBCharacteristic?
|
||||
private var onMessage: ((EisMessage) -> Void)?
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
centralManager = CBCentralManager(delegate: self, queue: nil)
|
||||
}
|
||||
|
||||
func setMessageHandler(_ handler: @escaping (EisMessage) -> Void) {
|
||||
onMessage = handler
|
||||
}
|
||||
|
||||
func startScanning() {
|
||||
guard centralManager.state == .poweredOn else {
|
||||
print("[BLE] can't scan, state: \(centralManager.state.rawValue)")
|
||||
return
|
||||
}
|
||||
print("[BLE] starting scan (no filter)")
|
||||
state = .scanning
|
||||
discoveredDevices.removeAll()
|
||||
centralManager.scanForPeripherals(
|
||||
withServices: nil,
|
||||
options: [CBCentralManagerScanOptionAllowDuplicatesKey: false]
|
||||
)
|
||||
}
|
||||
|
||||
func stopScanning() {
|
||||
centralManager.stopScan()
|
||||
if state == .scanning { state = .disconnected }
|
||||
}
|
||||
|
||||
func connectTo(_ device: DiscoveredDevice) {
|
||||
centralManager.stopScan()
|
||||
peripheral = device.peripheral
|
||||
state = .connecting
|
||||
print("[BLE] connecting to \(device.name)")
|
||||
centralManager.connect(device.peripheral, options: nil)
|
||||
}
|
||||
|
||||
func disconnect() {
|
||||
if let p = peripheral {
|
||||
centralManager.cancelPeripheralConnection(p)
|
||||
}
|
||||
peripheral = nil
|
||||
midiCharacteristic = nil
|
||||
state = .disconnected
|
||||
}
|
||||
|
||||
func sendCommand(_ sysex: [UInt8]) {
|
||||
guard let char = midiCharacteristic, let p = peripheral else { return }
|
||||
let packet = Self.wrapBLEMIDI(sysex)
|
||||
p.writeValue(packet, for: char, type: .withoutResponse)
|
||||
}
|
||||
|
||||
// MARK: - BLE MIDI packet handling
|
||||
|
||||
/// Wrap raw SysEx into a BLE MIDI packet.
|
||||
/// Input: [F0, 7D, CMD, ...payload, F7]
|
||||
/// Output: [0x80, 0x80, F0, 7D, CMD, ...payload, 0x80, F7]
|
||||
static func wrapBLEMIDI(_ sysex: [UInt8]) -> Data {
|
||||
var pkt: [UInt8] = [0x80, 0x80]
|
||||
pkt.append(contentsOf: sysex.dropLast())
|
||||
pkt.append(0x80)
|
||||
pkt.append(0xF7)
|
||||
return Data(pkt)
|
||||
}
|
||||
|
||||
/// Extract SysEx payload from BLE MIDI notification.
|
||||
/// Returns bytes between F0 and F7 exclusive, stripping timestamps.
|
||||
/// Result: [7D, RSP_ID, ...data]
|
||||
static func extractSysEx(from data: Data) -> [UInt8]? {
|
||||
let bytes = Array(data)
|
||||
guard bytes.count >= 5 else { return nil }
|
||||
|
||||
var i = 1 // skip header
|
||||
while i < bytes.count {
|
||||
let b = bytes[i]
|
||||
if b == 0xF0 { break }
|
||||
if b & 0x80 != 0 && b != 0xF7 { i += 1; continue }
|
||||
i += 1
|
||||
}
|
||||
guard i < bytes.count, bytes[i] == 0xF0 else { return nil }
|
||||
i += 1
|
||||
|
||||
var payload: [UInt8] = []
|
||||
while i < bytes.count && bytes[i] != 0xF7 {
|
||||
if bytes[i] & 0x80 != 0 { i += 1; continue }
|
||||
payload.append(bytes[i])
|
||||
i += 1
|
||||
}
|
||||
guard !payload.isEmpty else { return nil }
|
||||
return payload
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CBCentralManagerDelegate
|
||||
|
||||
extension BLEManager: CBCentralManagerDelegate {
|
||||
|
||||
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
||||
print("[BLE] centralManager state: \(central.state.rawValue)")
|
||||
if central.state == .poweredOn {
|
||||
startScanning()
|
||||
}
|
||||
}
|
||||
|
||||
func centralManager(
|
||||
_ central: CBCentralManager,
|
||||
didDiscover peripheral: CBPeripheral,
|
||||
advertisementData: [String: Any],
|
||||
rssi RSSI: NSNumber
|
||||
) {
|
||||
let name = peripheral.name ?? advertisementData[CBAdvertisementDataLocalNameKey] as? String ?? "Unknown"
|
||||
let svcUUIDs = advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] ?? []
|
||||
|
||||
print("[BLE] found: \(name) rssi:\(RSSI) services:\(svcUUIDs)")
|
||||
|
||||
if discoveredDevices.contains(where: { $0.id == peripheral.identifier }) { return }
|
||||
|
||||
let device = DiscoveredDevice(
|
||||
id: peripheral.identifier,
|
||||
peripheral: peripheral,
|
||||
name: name,
|
||||
rssi: RSSI.intValue,
|
||||
serviceUUIDs: svcUUIDs
|
||||
)
|
||||
discoveredDevices.append(device)
|
||||
}
|
||||
|
||||
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
||||
peripheral.delegate = self
|
||||
peripheral.discoverServices([Self.midiServiceUUID])
|
||||
}
|
||||
|
||||
func centralManager(
|
||||
_ central: CBCentralManager,
|
||||
didDisconnectPeripheral peripheral: CBPeripheral,
|
||||
error: Error?
|
||||
) {
|
||||
self.peripheral = nil
|
||||
self.midiCharacteristic = nil
|
||||
state = .disconnected
|
||||
startScanning()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CBPeripheralDelegate
|
||||
|
||||
extension BLEManager: CBPeripheralDelegate {
|
||||
|
||||
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
|
||||
guard let service = peripheral.services?.first(where: {
|
||||
$0.uuid == Self.midiServiceUUID
|
||||
}) else { return }
|
||||
peripheral.discoverCharacteristics([Self.midiCharUUID], for: service)
|
||||
}
|
||||
|
||||
func peripheral(
|
||||
_ peripheral: CBPeripheral,
|
||||
didDiscoverCharacteristicsFor service: CBService,
|
||||
error: Error?
|
||||
) {
|
||||
guard let char = service.characteristics?.first(where: {
|
||||
$0.uuid == Self.midiCharUUID
|
||||
}) else { return }
|
||||
midiCharacteristic = char
|
||||
peripheral.setNotifyValue(true, for: char)
|
||||
}
|
||||
|
||||
func peripheral(
|
||||
_ peripheral: CBPeripheral,
|
||||
didUpdateNotificationStateFor characteristic: CBCharacteristic,
|
||||
error: Error?
|
||||
) {
|
||||
if characteristic.isNotifying {
|
||||
state = .connected
|
||||
sendCommand(buildSysexGetConfig())
|
||||
sendCommand(buildSysexGetCellK())
|
||||
}
|
||||
}
|
||||
|
||||
func peripheral(
|
||||
_ peripheral: CBPeripheral,
|
||||
didUpdateValueFor characteristic: CBCharacteristic,
|
||||
error: Error?
|
||||
) {
|
||||
guard let data = characteristic.value,
|
||||
let sysex = Self.extractSysEx(from: data),
|
||||
let msg = parseSysex(sysex) else { return }
|
||||
lastMessage = msg
|
||||
onMessage?(msg)
|
||||
}
|
||||
}
|
||||
|
|
@ -7,9 +7,6 @@ struct CueIOSApp: App {
|
|||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView(state: state)
|
||||
.onReceive(Timer.publish(every: 0.25, on: .main, in: .common).autoconnect()) { _ in
|
||||
state.updateConnectionState()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,12 +14,8 @@
|
|||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>NSBluetoothAlwaysUsageDescription</key>
|
||||
<string>EIS4 uses Bluetooth to communicate with the impedance analyzer</string>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>bluetooth-central</string>
|
||||
</array>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>Cue connects to the EIS4 impedance analyzer over WiFi</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,195 @@
|
|||
/// UDP transport for EIS4 firmware communication.
|
||||
/// Matches the desktop Cue protocol: raw SysEx frames over UDP port 5941.
|
||||
|
||||
import Foundation
|
||||
import Network
|
||||
|
||||
@Observable
|
||||
final class UDPManager: @unchecked Sendable {
|
||||
|
||||
enum ConnectionState: String {
|
||||
case disconnected = "Disconnected"
|
||||
case connecting = "Connecting..."
|
||||
case connected = "Connected"
|
||||
}
|
||||
|
||||
private static let defaultAddr = "192.168.4.1"
|
||||
private static let defaultPort: UInt16 = 5941
|
||||
private static let keepaliveInterval: TimeInterval = 5
|
||||
private static let timeout: TimeInterval = 10
|
||||
private static let addrKey = "eis4_udp_addr"
|
||||
|
||||
var state: ConnectionState = .disconnected
|
||||
var address: String
|
||||
var port: UInt16
|
||||
|
||||
private var connection: NWConnection?
|
||||
private let queue = DispatchQueue(label: "udp.eis4", qos: .userInitiated)
|
||||
private var onMessage: ((EisMessage) -> Void)?
|
||||
private var keepaliveTimer: Timer?
|
||||
private var timeoutTimer: Timer?
|
||||
private var lastReceived: Date = .distantPast
|
||||
|
||||
init() {
|
||||
let saved = UserDefaults.standard.string(forKey: Self.addrKey) ?? ""
|
||||
if saved.isEmpty {
|
||||
address = Self.defaultAddr
|
||||
port = Self.defaultPort
|
||||
} else {
|
||||
let parts = saved.split(separator: ":")
|
||||
address = String(parts[0])
|
||||
port = parts.count > 1 ? UInt16(parts[1]) ?? Self.defaultPort : Self.defaultPort
|
||||
}
|
||||
}
|
||||
|
||||
func setMessageHandler(_ handler: @escaping (EisMessage) -> Void) {
|
||||
onMessage = handler
|
||||
}
|
||||
|
||||
// MARK: - Connection
|
||||
|
||||
func connect() {
|
||||
disconnect()
|
||||
state = .connecting
|
||||
|
||||
let addrStr = "\(address):\(port)"
|
||||
UserDefaults.standard.set(addrStr, forKey: Self.addrKey)
|
||||
|
||||
let host = NWEndpoint.Host(address)
|
||||
let nwPort = NWEndpoint.Port(rawValue: port) ?? NWEndpoint.Port(rawValue: Self.defaultPort)!
|
||||
let params = NWParameters.udp
|
||||
params.allowLocalEndpointReuse = true
|
||||
|
||||
let conn = NWConnection(host: host, port: nwPort, using: params)
|
||||
connection = conn
|
||||
|
||||
conn.stateUpdateHandler = { [weak self] newState in
|
||||
DispatchQueue.main.async {
|
||||
self?.handleStateChange(newState)
|
||||
}
|
||||
}
|
||||
|
||||
conn.start(queue: queue)
|
||||
}
|
||||
|
||||
func disconnect() {
|
||||
stopTimers()
|
||||
connection?.cancel()
|
||||
connection = nil
|
||||
state = .disconnected
|
||||
}
|
||||
|
||||
func send(_ sysex: [UInt8]) {
|
||||
guard let conn = connection else { return }
|
||||
let data = Data(sysex)
|
||||
conn.send(content: data, completion: .contentProcessed({ _ in }))
|
||||
}
|
||||
|
||||
// MARK: - State handling
|
||||
|
||||
private func handleStateChange(_ newState: NWConnection.State) {
|
||||
switch newState {
|
||||
case .ready:
|
||||
state = .connected
|
||||
lastReceived = Date()
|
||||
send(buildSysexGetTemp())
|
||||
send(buildSysexGetConfig())
|
||||
send(buildSysexGetCellK())
|
||||
startTimers()
|
||||
receiveLoop()
|
||||
|
||||
case .failed, .cancelled:
|
||||
state = .disconnected
|
||||
stopTimers()
|
||||
|
||||
case .waiting:
|
||||
state = .connecting
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Receive
|
||||
|
||||
private func receiveLoop() {
|
||||
guard let conn = connection else { return }
|
||||
conn.receiveMessage { [weak self] content, _, _, error in
|
||||
guard let self else { return }
|
||||
if error != nil {
|
||||
DispatchQueue.main.async {
|
||||
if self.state == .connected {
|
||||
self.state = .disconnected
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
guard let data = content, !data.isEmpty else {
|
||||
self.receiveLoop()
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.lastReceived = Date()
|
||||
}
|
||||
|
||||
let frames = Self.extractSysExFrames(data)
|
||||
for frame in frames {
|
||||
if let msg = parseSysex(frame) {
|
||||
DispatchQueue.main.async {
|
||||
self.onMessage?(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.receiveLoop()
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract SysEx payloads from a UDP datagram.
|
||||
/// Scans for F0..F7 boundaries. Returns inner bytes (between F0 and F7, exclusive).
|
||||
static func extractSysExFrames(_ data: Data) -> [[UInt8]] {
|
||||
let bytes = Array(data)
|
||||
var frames: [[UInt8]] = []
|
||||
var i = 0
|
||||
while i < bytes.count {
|
||||
if bytes[i] == 0xF0 {
|
||||
if let end = bytes[i...].firstIndex(of: 0xF7) {
|
||||
let payload = Array(bytes[(i + 1)..<end])
|
||||
if !payload.isEmpty {
|
||||
frames.append(payload)
|
||||
}
|
||||
i = end + 1
|
||||
continue
|
||||
}
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
return frames
|
||||
}
|
||||
|
||||
// MARK: - Timers
|
||||
|
||||
private func startTimers() {
|
||||
keepaliveTimer = Timer.scheduledTimer(withTimeInterval: Self.keepaliveInterval, repeats: true) { [weak self] _ in
|
||||
self?.send(buildSysexGetTemp())
|
||||
}
|
||||
|
||||
timeoutTimer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { [weak self] _ in
|
||||
guard let self, self.state == .connected else { return }
|
||||
if Date().timeIntervalSince(self.lastReceived) > Self.timeout {
|
||||
self.state = .disconnected
|
||||
self.stopTimers()
|
||||
self.connection?.cancel()
|
||||
self.connection = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func stopTimers() {
|
||||
keepaliveTimer?.invalidate()
|
||||
keepaliveTimer = nil
|
||||
timeoutTimer?.invalidate()
|
||||
timeoutTimer = nil
|
||||
}
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@ struct StatusBar: View {
|
|||
|
||||
private var connectionIndicator: some View {
|
||||
Circle()
|
||||
.fill(state.bleConnected ? Color.green : Color.red)
|
||||
.fill(state.connected ? Color.green : Color.red)
|
||||
.frame(width: 8, height: 8)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,33 +2,38 @@ import SwiftUI
|
|||
|
||||
struct ConnectionView: View {
|
||||
var state: AppState
|
||||
@State private var addressField: String = ""
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
header
|
||||
Divider()
|
||||
deviceList
|
||||
connectionForm
|
||||
Spacer()
|
||||
}
|
||||
.navigationTitle("Connection")
|
||||
.onAppear { addressField = state.transport.address }
|
||||
}
|
||||
|
||||
// MARK: - Header
|
||||
|
||||
private var header: some View {
|
||||
HStack {
|
||||
Circle()
|
||||
.fill(statusColor)
|
||||
.frame(width: 10, height: 10)
|
||||
Text(state.ble.state.rawValue)
|
||||
Text(state.transport.state.rawValue)
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
if state.ble.state == .connected {
|
||||
Button("Disconnect") { state.ble.disconnect() }
|
||||
if state.transport.state == .connected {
|
||||
Button("Disconnect") { state.transport.disconnect() }
|
||||
.buttonStyle(.bordered)
|
||||
.tint(.red)
|
||||
} else if state.ble.state == .scanning {
|
||||
Button("Stop") { state.ble.stopScanning() }
|
||||
.buttonStyle(.bordered)
|
||||
} else if state.transport.state == .connecting {
|
||||
ProgressView()
|
||||
.controlSize(.small)
|
||||
} else {
|
||||
Button("Scan") { state.ble.startScanning() }
|
||||
Button("Connect") { connectToDevice() }
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
}
|
||||
|
|
@ -36,52 +41,49 @@ struct ConnectionView: View {
|
|||
}
|
||||
|
||||
private var statusColor: Color {
|
||||
switch state.ble.state {
|
||||
switch state.transport.state {
|
||||
case .connected: .green
|
||||
case .scanning: .orange
|
||||
case .connecting: .yellow
|
||||
case .disconnected: .red
|
||||
}
|
||||
}
|
||||
|
||||
private var deviceList: some View {
|
||||
List {
|
||||
if state.ble.discoveredDevices.isEmpty && state.ble.state == .scanning {
|
||||
// MARK: - Connection form
|
||||
|
||||
private var connectionForm: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Firmware Address")
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundStyle(.secondary)
|
||||
HStack {
|
||||
ProgressView()
|
||||
Text("Scanning for devices...")
|
||||
TextField("192.168.4.1", text: $addressField)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.autocorrectionDisabled()
|
||||
.textInputAutocapitalization(.never)
|
||||
.keyboardType(.numbersAndPunctuation)
|
||||
Text(":\(state.transport.port)")
|
||||
.font(.subheadline.monospacedDigit())
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
ForEach(state.ble.discoveredDevices) { device in
|
||||
Button {
|
||||
state.ble.connectTo(device)
|
||||
} label: {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(device.name)
|
||||
.font(.body.weight(.medium))
|
||||
if !device.serviceUUIDs.isEmpty {
|
||||
Text(device.serviceUUIDs.map(\.uuidString).joined(separator: ", "))
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
Text("\(device.rssi) dBm")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
if device.name == "EIS4" {
|
||||
Image(systemName: "star.fill")
|
||||
.foregroundStyle(.yellow)
|
||||
Button("Connect") { connectToDevice() }
|
||||
.buttonStyle(.borderedProminent)
|
||||
.disabled(state.transport.state == .connecting)
|
||||
|
||||
Text("Connect to the EIS4 WiFi network, then tap Connect.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(Color(white: 0.4))
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
private func connectToDevice() {
|
||||
let addr = addressField.trimmingCharacters(in: .whitespaces)
|
||||
if !addr.isEmpty {
|
||||
state.transport.address = addr
|
||||
}
|
||||
.disabled(state.ble.state == .connecting)
|
||||
}
|
||||
}
|
||||
state.transport.connect()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue