Compare commits
No commits in common. "7570510491a47a8a8cae5a509b2a06db4e5fa674" and "0ff998c82c3b86dcafc6a46c68401c1386f6d534" have entirely different histories.
7570510491
...
0ff998c82c
|
|
@ -1,6 +1,5 @@
|
|||
import Foundation
|
||||
import Observation
|
||||
import Combine
|
||||
|
||||
enum Tab: String, CaseIterable, Identifiable {
|
||||
case eis = "EIS"
|
||||
|
|
@ -9,7 +8,6 @@ enum Tab: String, CaseIterable, Identifiable {
|
|||
case chlorine = "Chlorine"
|
||||
case ph = "pH"
|
||||
case sessions = "Sessions"
|
||||
case connection = "Connection"
|
||||
|
||||
var id: String { rawValue }
|
||||
}
|
||||
|
|
@ -18,7 +16,6 @@ enum Tab: String, CaseIterable, Identifiable {
|
|||
|
||||
@Observable
|
||||
final class AppState {
|
||||
let ble: BLEManager
|
||||
var tab: Tab = .eis
|
||||
var status: String = "Disconnected"
|
||||
var bleConnected: Bool = false
|
||||
|
|
@ -77,255 +74,52 @@ final class AppState {
|
|||
// Device reference collection
|
||||
var collectingRefs: Bool = false
|
||||
var hasDeviceRefs: Bool = false
|
||||
private var eisRefs: [Int: [EisPoint]] = [:]
|
||||
private var refMode: UInt8?
|
||||
private var refRtia: UInt8?
|
||||
|
||||
// Session
|
||||
var currentSessionId: Int64? = nil
|
||||
|
||||
// Clean
|
||||
var cleanV: String = "1200"
|
||||
var cleanDur: String = "30"
|
||||
|
||||
// Temperature polling
|
||||
private var tempTimer: Timer?
|
||||
|
||||
init() {
|
||||
ble = BLEManager()
|
||||
ble.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)
|
||||
}
|
||||
|
||||
// MARK: - Message handler
|
||||
|
||||
private func handleMessage(_ msg: EisMessage) {
|
||||
switch msg {
|
||||
|
||||
case .sweepStart(let numPoints, let freqStart, let freqStop):
|
||||
if collectingRefs {
|
||||
eisPoints.removeAll()
|
||||
sweepTotal = numPoints
|
||||
} else {
|
||||
eisPoints.removeAll()
|
||||
sweepTotal = numPoints
|
||||
status = String(format: "Sweep: %d pts, %.0f--%.0f Hz", numPoints, freqStart, freqStop)
|
||||
}
|
||||
|
||||
case .dataPoint(_, let point):
|
||||
eisPoints.append(point)
|
||||
if !collectingRefs {
|
||||
status = "Receiving: \(eisPoints.count)/\(sweepTotal)"
|
||||
}
|
||||
|
||||
case .sweepEnd:
|
||||
if collectingRefs {
|
||||
if let r = refRtia, r < 8 {
|
||||
eisRefs[Int(r)] = eisPoints
|
||||
}
|
||||
eisPoints.removeAll()
|
||||
} else {
|
||||
saveEis()
|
||||
status = "Sweep complete: \(eisPoints.count) points"
|
||||
}
|
||||
|
||||
case .config(let cfg):
|
||||
freqStart = String(format: "%.0f", cfg.freqStart)
|
||||
freqStop = String(format: "%.0f", cfg.freqStop)
|
||||
ppd = "\(cfg.ppd)"
|
||||
rtia = cfg.rtia
|
||||
rcal = cfg.rcal
|
||||
electrode = cfg.electrode
|
||||
status = "Config received"
|
||||
|
||||
case .lsvStart(let numPoints, let vStart, let vStop):
|
||||
lsvPoints.removeAll()
|
||||
lsvTotal = numPoints
|
||||
status = String(format: "LSV: %d pts, %.0f--%.0f mV", numPoints, vStart, vStop)
|
||||
|
||||
case .lsvPoint(_, let point):
|
||||
lsvPoints.append(point)
|
||||
status = "LSV: \(lsvPoints.count)/\(lsvTotal)"
|
||||
|
||||
case .lsvEnd:
|
||||
saveLsv()
|
||||
status = "LSV complete: \(lsvPoints.count) points"
|
||||
|
||||
case .ampStart(let vHold):
|
||||
ampPoints.removeAll()
|
||||
ampRunning = true
|
||||
status = String(format: "Amp: %.0f mV", vHold)
|
||||
|
||||
case .ampPoint(let index, let point):
|
||||
ampPoints.append(point)
|
||||
ampTotal = index + 1
|
||||
status = "Amp: \(ampPoints.count) pts"
|
||||
|
||||
case .ampEnd:
|
||||
ampRunning = false
|
||||
saveAmp()
|
||||
status = "Amp complete: \(ampPoints.count) points"
|
||||
|
||||
case .clStart(let numPoints):
|
||||
clPoints.removeAll()
|
||||
clResult = nil
|
||||
clTotal = numPoints
|
||||
status = "Chlorine: \(numPoints) pts"
|
||||
|
||||
case .clPoint(_, let point):
|
||||
clPoints.append(point)
|
||||
status = "Chlorine: \(clPoints.count)/\(clTotal)"
|
||||
|
||||
case .clResult(let r):
|
||||
clResult = r
|
||||
status = String(format: "Chlorine: free=%.3f uA, total=%.3f uA", r.iFreeUa, r.iTotalUa)
|
||||
|
||||
case .clEnd:
|
||||
saveCl()
|
||||
status = "Chlorine complete: \(clPoints.count) points"
|
||||
|
||||
case .phResult(let r):
|
||||
if collectingRefs {
|
||||
phRef = r
|
||||
} else {
|
||||
savePh(r)
|
||||
status = String(format: "pH: %.2f (OCP=%.1f mV, T=%.1fC)", r.ph, r.vOcpMv, r.tempC)
|
||||
phResult = r
|
||||
}
|
||||
|
||||
case .temperature(let t):
|
||||
tempC = t
|
||||
|
||||
case .refFrame(let mode, let rtiaIdx):
|
||||
refMode = mode
|
||||
refRtia = rtiaIdx
|
||||
let modeName: String
|
||||
switch mode {
|
||||
case 0: modeName = "EIS"
|
||||
case 1: modeName = "LSV"
|
||||
case 2: modeName = "Amp"
|
||||
case 3: modeName = "Cl"
|
||||
case 4: modeName = "pH"
|
||||
default: modeName = "?"
|
||||
}
|
||||
if mode == 0 {
|
||||
status = "Ref: \(modeName) RTIA \(rtiaIdx + 1)/8"
|
||||
} else {
|
||||
status = "Ref: \(modeName) range search"
|
||||
}
|
||||
|
||||
case .refLpRange:
|
||||
break
|
||||
|
||||
case .refsDone:
|
||||
collectingRefs = false
|
||||
hasDeviceRefs = true
|
||||
refMode = nil
|
||||
refRtia = nil
|
||||
let idx = Int(rtia.rawValue)
|
||||
if idx < 8, let pts = eisRefs[idx] {
|
||||
eisRef = pts
|
||||
}
|
||||
status = "Reference collection complete"
|
||||
|
||||
case .refStatus(let hasRefs):
|
||||
hasDeviceRefs = hasRefs
|
||||
if !hasRefs {
|
||||
status = "No device refs"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
func applyEISSettings() {
|
||||
let fs = Float(freqStart) ?? 1000
|
||||
let fe = Float(freqStop) ?? 200000
|
||||
let p = UInt16(ppd) ?? 10
|
||||
send(buildSysexSetSweep(freqStart: fs, freqStop: fe, ppd: p))
|
||||
send(buildSysexSetRtia(rtia))
|
||||
send(buildSysexSetRcal(rcal))
|
||||
send(buildSysexSetElectrode(electrode))
|
||||
send(buildSysexGetConfig())
|
||||
status = "Applying: \(fs)-\(fe) Hz, \(p) PPD"
|
||||
}
|
||||
|
||||
func startSweep() {
|
||||
eisPoints.removeAll()
|
||||
send(buildSysexGetTemp())
|
||||
send(buildSysexStartSweep())
|
||||
status = "Starting sweep..."
|
||||
}
|
||||
|
||||
func startLSV() {
|
||||
lsvPoints.removeAll()
|
||||
let vs = Float(lsvStartV) ?? 0
|
||||
let ve = Float(lsvStopV) ?? 500
|
||||
let sr = Float(lsvScanRate) ?? 50
|
||||
send(buildSysexGetTemp())
|
||||
send(buildSysexStartLsv(vStart: vs, vStop: ve, scanRate: sr, lpRtia: lsvRtia))
|
||||
status = "Starting LSV: \(vs)-\(ve) mV"
|
||||
}
|
||||
|
||||
func startAmp() {
|
||||
ampPoints.removeAll()
|
||||
ampRunning = true
|
||||
let vh = Float(ampVHold) ?? 200
|
||||
let iv = Float(ampInterval) ?? 100
|
||||
let dur = Float(ampDuration) ?? 60
|
||||
send(buildSysexGetTemp())
|
||||
send(buildSysexStartAmp(vHold: vh, intervalMs: iv, durationS: dur, lpRtia: ampRtia))
|
||||
status = "Starting amperometry..."
|
||||
}
|
||||
|
||||
func stopAmp() {
|
||||
send(buildSysexStopAmp())
|
||||
ampRunning = false
|
||||
status = "Stopping amperometry..."
|
||||
}
|
||||
|
||||
func startChlorine() {
|
||||
clPoints.removeAll()
|
||||
clResult = nil
|
||||
let vCond = Float(clCondV) ?? 800
|
||||
let tCond = Float(clCondT) ?? 2000
|
||||
let vFree = Float(clFreeV) ?? 100
|
||||
let vTotal = Float(clTotalV) ?? -200
|
||||
let tDep = Float(clDepT) ?? 5000
|
||||
let tMeas = Float(clMeasT) ?? 5000
|
||||
send(buildSysexGetTemp())
|
||||
send(buildSysexStartCl(
|
||||
vCond: vCond, tCondMs: tCond, vFree: vFree, vTotal: vTotal,
|
||||
tDepMs: tDep, tMeasMs: tMeas, lpRtia: clRtia
|
||||
))
|
||||
status = "Starting chlorine measurement..."
|
||||
}
|
||||
|
||||
func startPh() {
|
||||
phResult = nil
|
||||
let stab = Float(phStabilize) ?? 30
|
||||
send(buildSysexGetTemp())
|
||||
send(buildSysexStartPh(stabilizeS: stab))
|
||||
status = "Starting pH measurement..."
|
||||
}
|
||||
|
||||
func setReference() {
|
||||
|
|
@ -361,37 +155,29 @@ final class AppState {
|
|||
case .amp: ampRef = nil; status = "Amp reference cleared"
|
||||
case .chlorine: clRef = nil; status = "Chlorine reference cleared"
|
||||
case .ph: phRef = nil; status = "pH reference cleared"
|
||||
case .sessions, .connection: break
|
||||
case .sessions: break
|
||||
}
|
||||
}
|
||||
|
||||
func collectRefs() {
|
||||
collectingRefs = true
|
||||
eisRefs.removeAll()
|
||||
status = "Starting reference collection..."
|
||||
send(buildSysexStartRefs())
|
||||
}
|
||||
|
||||
func getRefs() {
|
||||
collectingRefs = true
|
||||
eisRefs.removeAll()
|
||||
send(buildSysexGetRefs())
|
||||
}
|
||||
|
||||
func clearRefs() {
|
||||
collectingRefs = false
|
||||
hasDeviceRefs = false
|
||||
eisRefs.removeAll()
|
||||
eisRef = nil
|
||||
lsvRef = nil
|
||||
ampRef = nil
|
||||
clRef = nil
|
||||
phRef = nil
|
||||
send(buildSysexClearRefs())
|
||||
status = "Refs cleared"
|
||||
}
|
||||
|
||||
func startClean() {
|
||||
let v = Float(cleanV) ?? 1200
|
||||
let d = Float(cleanDur) ?? 30
|
||||
send(buildSysexStartClean(vMv: v, durationS: d))
|
||||
status = String(format: "Cleaning: %.0f mV for %.0fs", v, d)
|
||||
}
|
||||
|
||||
|
|
@ -402,7 +188,7 @@ final class AppState {
|
|||
case .amp: ampRef != nil
|
||||
case .chlorine: clRef != nil
|
||||
case .ph: phRef != nil
|
||||
case .sessions, .connection: false
|
||||
case .sessions: false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -413,97 +199,10 @@ final class AppState {
|
|||
case .amp: !ampPoints.isEmpty
|
||||
case .chlorine: clResult != nil
|
||||
case .ph: phResult != nil
|
||||
case .sessions, .connection: false
|
||||
case .sessions: false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Auto-save
|
||||
|
||||
private func saveEis() {
|
||||
guard let sid = currentSessionId else { return }
|
||||
let params: [String: String] = [
|
||||
"freq_start": freqStart,
|
||||
"freq_stop": freqStop,
|
||||
"ppd": ppd,
|
||||
"rtia": rtia.label,
|
||||
"rcal": rcal.label,
|
||||
"electrode": electrode.label,
|
||||
]
|
||||
guard let configData = try? JSONEncoder().encode(params) else { return }
|
||||
guard var meas = try? Storage.shared.addMeasurement(sessionId: sid, type: .eis) else { return }
|
||||
meas.config = configData
|
||||
guard let mid = meas.id else { return }
|
||||
let indexed = eisPoints.enumerated().map { (index: $0.offset, value: $0.element) }
|
||||
try? Storage.shared.addDataPoints(measurementId: mid, points: indexed)
|
||||
}
|
||||
|
||||
private func saveLsv() {
|
||||
guard let sid = currentSessionId else { return }
|
||||
let params: [String: String] = [
|
||||
"v_start": lsvStartV,
|
||||
"v_stop": lsvStopV,
|
||||
"scan_rate": lsvScanRate,
|
||||
"rtia": lsvRtia.label,
|
||||
]
|
||||
guard let configData = try? JSONEncoder().encode(params) else { return }
|
||||
guard var meas = try? Storage.shared.addMeasurement(sessionId: sid, type: .lsv) else { return }
|
||||
meas.config = configData
|
||||
guard let mid = meas.id else { return }
|
||||
let indexed = lsvPoints.enumerated().map { (index: $0.offset, value: $0.element) }
|
||||
try? Storage.shared.addDataPoints(measurementId: mid, points: indexed)
|
||||
}
|
||||
|
||||
private func saveAmp() {
|
||||
guard let sid = currentSessionId else { return }
|
||||
let params: [String: String] = [
|
||||
"v_hold": ampVHold,
|
||||
"interval_ms": ampInterval,
|
||||
"duration_s": ampDuration,
|
||||
"rtia": ampRtia.label,
|
||||
]
|
||||
guard let configData = try? JSONEncoder().encode(params) else { return }
|
||||
guard var meas = try? Storage.shared.addMeasurement(sessionId: sid, type: .amp) else { return }
|
||||
meas.config = configData
|
||||
guard let mid = meas.id else { return }
|
||||
let indexed = ampPoints.enumerated().map { (index: $0.offset, value: $0.element) }
|
||||
try? Storage.shared.addDataPoints(measurementId: mid, points: indexed)
|
||||
}
|
||||
|
||||
private func saveCl() {
|
||||
guard let sid = currentSessionId else { return }
|
||||
let params: [String: String] = [
|
||||
"cond_v": clCondV,
|
||||
"cond_t": clCondT,
|
||||
"free_v": clFreeV,
|
||||
"total_v": clTotalV,
|
||||
"dep_t": clDepT,
|
||||
"meas_t": clMeasT,
|
||||
"rtia": clRtia.label,
|
||||
]
|
||||
guard let configData = try? JSONEncoder().encode(params) else { return }
|
||||
guard var meas = try? Storage.shared.addMeasurement(sessionId: sid, type: .chlorine) else { return }
|
||||
meas.config = configData
|
||||
guard let mid = meas.id else { return }
|
||||
let indexed = clPoints.enumerated().map { (index: $0.offset, value: $0.element) }
|
||||
try? Storage.shared.addDataPoints(measurementId: mid, points: indexed)
|
||||
if let r = clResult {
|
||||
try? Storage.shared.setMeasurementResult(mid, result: r)
|
||||
}
|
||||
}
|
||||
|
||||
private func savePh(_ result: PhResult) {
|
||||
guard let sid = currentSessionId else { return }
|
||||
let params: [String: String] = [
|
||||
"stabilize_s": phStabilize,
|
||||
]
|
||||
guard let configData = try? JSONEncoder().encode(params) else { return }
|
||||
guard var meas = try? Storage.shared.addMeasurement(sessionId: sid, type: .ph) else { return }
|
||||
meas.config = configData
|
||||
guard let mid = meas.id else { return }
|
||||
try? Storage.shared.addDataPoint(measurementId: mid, index: 0, point: result)
|
||||
try? Storage.shared.setMeasurementResult(mid, result: result)
|
||||
}
|
||||
|
||||
// MARK: - Measurement loading
|
||||
|
||||
func loadMeasurement(_ measurement: Measurement) {
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 334 KiB |
|
|
@ -8,8 +8,8 @@ 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")
|
||||
static let midiServiceUUID = CBUUID(string: "03B80E5A-EDE8-4B33-A751-6CE34EC4C700")
|
||||
static let midiCharUUID = CBUUID(string: "7772E5DB-3868-4112-A1A9-F2669D106BF3")
|
||||
|
||||
enum ConnectionState: String {
|
||||
case disconnected = "Disconnected"
|
||||
|
|
@ -18,20 +18,11 @@ final class BLEManager: NSObject {
|
|||
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 peripheral: CBPeripheral?
|
||||
private var midiCharacteristic: CBCharacteristic?
|
||||
private var onMessage: ((EisMessage) -> Void)?
|
||||
|
||||
|
|
@ -45,32 +36,14 @@ final class BLEManager: NSObject {
|
|||
}
|
||||
|
||||
func startScanning() {
|
||||
guard centralManager.state == .poweredOn else {
|
||||
print("[BLE] can't scan, state: \(centralManager.state.rawValue)")
|
||||
return
|
||||
}
|
||||
print("[BLE] starting scan (no filter)")
|
||||
guard centralManager.state == .poweredOn else { return }
|
||||
state = .scanning
|
||||
discoveredDevices.removeAll()
|
||||
centralManager.scanForPeripherals(
|
||||
withServices: nil,
|
||||
options: [CBCentralManagerScanOptionAllowDuplicatesKey: false]
|
||||
withServices: [Self.midiServiceUUID],
|
||||
options: nil
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
|
|
@ -132,7 +105,6 @@ final class BLEManager: NSObject {
|
|||
extension BLEManager: CBCentralManagerDelegate {
|
||||
|
||||
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
||||
print("[BLE] centralManager state: \(central.state.rawValue)")
|
||||
if central.state == .poweredOn {
|
||||
startScanning()
|
||||
}
|
||||
|
|
@ -144,21 +116,11 @@ extension BLEManager: CBCentralManagerDelegate {
|
|||
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)
|
||||
guard peripheral.name == "EIS4" else { return }
|
||||
central.stopScan()
|
||||
self.peripheral = peripheral
|
||||
state = .connecting
|
||||
central.connect(peripheral, options: nil)
|
||||
}
|
||||
|
||||
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
||||
|
|
|
|||
|
|
@ -2,14 +2,11 @@ import SwiftUI
|
|||
|
||||
@main
|
||||
struct CueIOSApp: App {
|
||||
@State private var state = AppState()
|
||||
@State private var ble = BLEManager()
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView(state: state)
|
||||
.onReceive(Timer.publish(every: 0.25, on: .main, in: .common).autoconnect()) { _ in
|
||||
state.updateConnectionState()
|
||||
}
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,18 +2,6 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ struct DataPoint: Codable, FetchableRecord, MutablePersistableRecord {
|
|||
|
||||
// MARK: - Database manager
|
||||
|
||||
final class Storage: @unchecked Sendable {
|
||||
final class Storage {
|
||||
static let shared = Storage()
|
||||
|
||||
private let dbQueue: DatabaseQueue
|
||||
|
|
|
|||
|
|
@ -1,87 +0,0 @@
|
|||
import SwiftUI
|
||||
|
||||
struct ConnectionView: View {
|
||||
var state: AppState
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
header
|
||||
Divider()
|
||||
deviceList
|
||||
}
|
||||
.navigationTitle("Connection")
|
||||
}
|
||||
|
||||
private var header: some View {
|
||||
HStack {
|
||||
Circle()
|
||||
.fill(statusColor)
|
||||
.frame(width: 10, height: 10)
|
||||
Text(state.ble.state.rawValue)
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
if state.ble.state == .connected {
|
||||
Button("Disconnect") { state.ble.disconnect() }
|
||||
.buttonStyle(.bordered)
|
||||
.tint(.red)
|
||||
} else if state.ble.state == .scanning {
|
||||
Button("Stop") { state.ble.stopScanning() }
|
||||
.buttonStyle(.bordered)
|
||||
} else {
|
||||
Button("Scan") { state.ble.startScanning() }
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
private var statusColor: Color {
|
||||
switch state.ble.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 {
|
||||
HStack {
|
||||
ProgressView()
|
||||
Text("Scanning for devices...")
|
||||
.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)
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabled(state.ble.state == .connecting)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
@Bindable var state: AppState
|
||||
@State private var state = AppState()
|
||||
@Environment(\.horizontalSizeClass) private var sizeClass
|
||||
|
||||
var body: some View {
|
||||
|
|
@ -40,7 +40,6 @@ struct ContentView: View {
|
|||
}
|
||||
Section("Data") {
|
||||
sidebarButton(.sessions, "Sessions", "folder")
|
||||
sidebarButton(.connection, "Connection", "antenna.radiowaves.left.and.right")
|
||||
}
|
||||
Section {
|
||||
cleanControls
|
||||
|
|
@ -127,10 +126,6 @@ struct ContentView: View {
|
|||
SessionView(state: state)
|
||||
.tabItem { Label("Sessions", systemImage: "folder") }
|
||||
.tag(Tab.sessions)
|
||||
|
||||
ConnectionView(state: state)
|
||||
.tabItem { Label("Connection", systemImage: "antenna.radiowaves.left.and.right") }
|
||||
.tag(Tab.connection)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -145,7 +140,6 @@ struct ContentView: View {
|
|||
case .chlorine: ChlorineView(state: state)
|
||||
case .ph: PhView(state: state)
|
||||
case .sessions: SessionView(state: state)
|
||||
case .connection: ConnectionView(state: state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
36
main/ble.c
36
main/ble.c
|
|
@ -30,7 +30,7 @@ static const ble_uuid128_t midi_chr_uuid = BLE_UUID128_INIT(
|
|||
0xf3, 0x6b, 0x10, 0x9d, 0x66, 0xf2, 0xa9, 0xa1,
|
||||
0x12, 0x41, 0x68, 0x38, 0xdb, 0xe5, 0x72, 0x77);
|
||||
|
||||
#define MAX_CONNECTIONS 4
|
||||
#define MAX_CONNECTIONS 2
|
||||
|
||||
static EventGroupHandle_t ble_events;
|
||||
static QueueHandle_t cmd_queue;
|
||||
|
|
@ -523,13 +523,8 @@ static int gap_event_cb(struct ble_gap_event *event, void *arg)
|
|||
return 0;
|
||||
}
|
||||
|
||||
static void adv_task(void *param)
|
||||
static void start_adv(void)
|
||||
{
|
||||
(void)param;
|
||||
vTaskDelay(pdMS_TO_TICKS(200));
|
||||
|
||||
ble_gap_adv_stop();
|
||||
|
||||
struct ble_hs_adv_fields fields = {0};
|
||||
fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP;
|
||||
fields.name = (uint8_t *)DEVICE_NAME;
|
||||
|
|
@ -544,38 +539,19 @@ static void adv_task(void *param)
|
|||
fields.uuids16 = adv_uuids;
|
||||
fields.num_uuids16 = 2;
|
||||
fields.uuids16_is_complete = 0;
|
||||
|
||||
int rc = ble_gap_adv_set_fields(&fields);
|
||||
if (rc) { printf("BLE: set_fields failed: %d\n", rc); goto done; }
|
||||
ble_gap_adv_set_fields(&fields);
|
||||
|
||||
struct ble_hs_adv_fields rsp = {0};
|
||||
rsp.uuids128 = (ble_uuid128_t *)&midi_svc_uuid;
|
||||
rsp.num_uuids128 = 1;
|
||||
rsp.uuids128_is_complete = 1;
|
||||
|
||||
rc = ble_gap_adv_rsp_set_fields(&rsp);
|
||||
if (rc) { printf("BLE: set_rsp failed: %d\n", rc); goto done; }
|
||||
ble_gap_adv_rsp_set_fields(&rsp);
|
||||
|
||||
struct ble_gap_adv_params params = {0};
|
||||
params.conn_mode = BLE_GAP_CONN_MODE_UND;
|
||||
params.disc_mode = BLE_GAP_DISC_MODE_GEN;
|
||||
params.itvl_min = BLE_GAP_ADV_FAST_INTERVAL1_MIN;
|
||||
params.itvl_max = BLE_GAP_ADV_FAST_INTERVAL1_MAX;
|
||||
|
||||
rc = ble_gap_adv_start(BLE_OWN_ADDR_PUBLIC, NULL, BLE_HS_FOREVER,
|
||||
¶ms, gap_event_cb, NULL);
|
||||
if (rc)
|
||||
printf("BLE: adv_start failed: %d\n", rc);
|
||||
else
|
||||
printf("BLE: advertising (%d connected)\n", conn_count);
|
||||
|
||||
done:
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
static void start_adv(void)
|
||||
{
|
||||
xTaskCreate(adv_task, "adv", 2048, NULL, 5, NULL);
|
||||
ble_gap_adv_start(BLE_OWN_ADDR_PUBLIC, NULL, BLE_HS_FOREVER,
|
||||
¶ms, gap_event_cb, NULL);
|
||||
}
|
||||
|
||||
static void on_sync(void)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
CONFIG_IDF_TARGET="esp32s3"
|
||||
CONFIG_BT_ENABLED=y
|
||||
CONFIG_BT_NIMBLE_ENABLED=y
|
||||
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=4
|
||||
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=2
|
||||
CONFIG_BT_NIMBLE_ROLE_PERIPHERAL=y
|
||||
CONFIG_BT_NIMBLE_ROLE_CENTRAL=y
|
||||
CONFIG_BT_NIMBLE_ROLE_OBSERVER=n
|
||||
|
|
|
|||
Loading…
Reference in New Issue