add session sync protocol, extended START fields, and session management handlers
This commit is contained in:
parent
dcde79cf08
commit
d061a17e54
|
|
@ -100,6 +100,9 @@ final class AppState {
|
|||
|
||||
// Session
|
||||
var currentSessionId: Int64? = nil
|
||||
var firmwareSessionMap: [UInt8: Int64] = [:]
|
||||
var sessionListReceived: Bool = false
|
||||
private var pendingEspTimestamp: Int64? = nil
|
||||
|
||||
// Calibration
|
||||
var calVolumeGal: Double = 25
|
||||
|
|
@ -125,6 +128,10 @@ final class AppState {
|
|||
transport.setMessageHandler { [weak self] msg in
|
||||
self?.handleMessage(msg)
|
||||
}
|
||||
transport.setDisconnectHandler { [weak self] in
|
||||
self?.sessionListReceived = false
|
||||
self?.firmwareSessionMap.removeAll()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Send helper
|
||||
|
|
@ -138,7 +145,8 @@ final class AppState {
|
|||
private func handleMessage(_ msg: EisMessage) {
|
||||
switch msg {
|
||||
|
||||
case .sweepStart(let numPoints, let freqStart, let freqStop):
|
||||
case .sweepStart(let numPoints, let freqStart, let freqStop, let espTs, _):
|
||||
pendingEspTimestamp = espTs.map { Int64($0) }
|
||||
if collectingRefs {
|
||||
eisPoints.removeAll()
|
||||
sweepTotal = numPoints
|
||||
|
|
@ -172,9 +180,14 @@ final class AppState {
|
|||
rtia = cfg.rtia
|
||||
rcal = cfg.rcal
|
||||
electrode = cfg.electrode
|
||||
if !sessionListReceived {
|
||||
sessionListReceived = true
|
||||
send(buildSysexSessionList())
|
||||
}
|
||||
status = "Config received"
|
||||
|
||||
case .lsvStart(let numPoints, let vStart, let vStop):
|
||||
case .lsvStart(let numPoints, let vStart, let vStop, let espTs, _):
|
||||
pendingEspTimestamp = espTs.map { Int64($0) }
|
||||
lsvPoints.removeAll()
|
||||
lsvTotal = numPoints
|
||||
status = String(format: "LSV: %d pts, %.0f--%.0f mV", numPoints, vStart, vStop)
|
||||
|
|
@ -214,7 +227,8 @@ final class AppState {
|
|||
status = String(format: "Auto Cl: measuring (free=%.0f, total=%.0f)", pots.vFree, pots.vTotal)
|
||||
}
|
||||
|
||||
case .ampStart(let vHold):
|
||||
case .ampStart(let vHold, let espTs, _):
|
||||
pendingEspTimestamp = espTs.map { Int64($0) }
|
||||
ampPoints.removeAll()
|
||||
ampRunning = true
|
||||
status = String(format: "Amp: %.0f mV", vHold)
|
||||
|
|
@ -229,7 +243,8 @@ final class AppState {
|
|||
saveAmp()
|
||||
status = "Amp complete: \(ampPoints.count) points"
|
||||
|
||||
case .clStart(let numPoints):
|
||||
case .clStart(let numPoints, let espTs, _):
|
||||
pendingEspTimestamp = espTs.map { Int64($0) }
|
||||
clPoints.removeAll()
|
||||
clResult = nil
|
||||
clTotal = numPoints
|
||||
|
|
@ -324,11 +339,70 @@ final class AppState {
|
|||
phOffset = Double(offset)
|
||||
status = String(format: "pH cal: slope=%.4f offset=%.4f", slope, offset)
|
||||
|
||||
case .sessionCreated(let fwId, let name):
|
||||
handleSessionCreated(fwId: fwId, name: name)
|
||||
|
||||
case .sessionSwitched(let fwId):
|
||||
handleSessionSwitched(fwId: fwId)
|
||||
|
||||
case .sessionList(_, let currentId, let sessions):
|
||||
handleSessionList(currentId: currentId, sessions: sessions)
|
||||
|
||||
case .sessionRenamed(let fwId, let name):
|
||||
handleSessionRenamed(fwId: fwId, name: name)
|
||||
|
||||
case .keepalive:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Session sync
|
||||
|
||||
private func handleSessionCreated(fwId: UInt8, name: String) {
|
||||
let fwId64 = Int64(fwId)
|
||||
if let existing = Storage.shared.sessionByFirmwareId(fwId64) {
|
||||
firmwareSessionMap[fwId] = existing.id
|
||||
} else if let session = try? Storage.shared.createSession(
|
||||
label: name.isEmpty ? nil : name,
|
||||
firmwareSessionId: fwId64
|
||||
) {
|
||||
firmwareSessionMap[fwId] = session.id
|
||||
currentSessionId = session.id
|
||||
}
|
||||
}
|
||||
|
||||
private func handleSessionSwitched(fwId: UInt8) {
|
||||
if let localId = firmwareSessionMap[fwId] {
|
||||
currentSessionId = localId
|
||||
} else {
|
||||
let fwId64 = Int64(fwId)
|
||||
if let existing = Storage.shared.sessionByFirmwareId(fwId64) {
|
||||
firmwareSessionMap[fwId] = existing.id
|
||||
currentSessionId = existing.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handleSessionList(currentId: UInt8, sessions: [(id: UInt8, name: String)]) {
|
||||
for entry in sessions {
|
||||
let fwId64 = Int64(entry.id)
|
||||
if let existing = Storage.shared.sessionByFirmwareId(fwId64) {
|
||||
firmwareSessionMap[entry.id] = existing.id
|
||||
} else if let session = try? Storage.shared.createSession(
|
||||
label: entry.name.isEmpty ? nil : entry.name,
|
||||
firmwareSessionId: fwId64
|
||||
) {
|
||||
firmwareSessionMap[entry.id] = session.id
|
||||
}
|
||||
}
|
||||
handleSessionSwitched(fwId: currentId)
|
||||
}
|
||||
|
||||
private func handleSessionRenamed(fwId: UInt8, name: String) {
|
||||
guard let localId = firmwareSessionMap[fwId] else { return }
|
||||
try? Storage.shared.updateSessionLabel(localId, label: name)
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
func applyEISSettings() {
|
||||
|
|
@ -533,6 +607,8 @@ final class AppState {
|
|||
|
||||
private func saveEis() {
|
||||
guard let sid = currentSessionId else { return }
|
||||
let ts = pendingEspTimestamp
|
||||
pendingEspTimestamp = nil
|
||||
let params: [String: String] = [
|
||||
"freq_start": freqStart,
|
||||
"freq_stop": freqStop,
|
||||
|
|
@ -542,7 +618,7 @@ final class AppState {
|
|||
"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 }
|
||||
guard var meas = try? Storage.shared.addMeasurement(sessionId: sid, type: .eis, espTimestamp: ts) else { return }
|
||||
meas.config = configData
|
||||
guard let mid = meas.id else { return }
|
||||
let indexed = eisPoints.enumerated().map { (index: $0.offset, value: $0.element) }
|
||||
|
|
@ -551,6 +627,8 @@ final class AppState {
|
|||
|
||||
private func saveLsv() {
|
||||
guard let sid = currentSessionId else { return }
|
||||
let ts = pendingEspTimestamp
|
||||
pendingEspTimestamp = nil
|
||||
let params: [String: String] = [
|
||||
"v_start": lsvStartV,
|
||||
"v_stop": lsvStopV,
|
||||
|
|
@ -558,7 +636,7 @@ final class AppState {
|
|||
"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 }
|
||||
guard var meas = try? Storage.shared.addMeasurement(sessionId: sid, type: .lsv, espTimestamp: ts) else { return }
|
||||
meas.config = configData
|
||||
guard let mid = meas.id else { return }
|
||||
let indexed = lsvPoints.enumerated().map { (index: $0.offset, value: $0.element) }
|
||||
|
|
@ -567,6 +645,8 @@ final class AppState {
|
|||
|
||||
private func saveAmp() {
|
||||
guard let sid = currentSessionId else { return }
|
||||
let ts = pendingEspTimestamp
|
||||
pendingEspTimestamp = nil
|
||||
let params: [String: String] = [
|
||||
"v_hold": ampVHold,
|
||||
"interval_ms": ampInterval,
|
||||
|
|
@ -574,7 +654,7 @@ final class AppState {
|
|||
"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 }
|
||||
guard var meas = try? Storage.shared.addMeasurement(sessionId: sid, type: .amp, espTimestamp: ts) else { return }
|
||||
meas.config = configData
|
||||
guard let mid = meas.id else { return }
|
||||
let indexed = ampPoints.enumerated().map { (index: $0.offset, value: $0.element) }
|
||||
|
|
@ -583,6 +663,8 @@ final class AppState {
|
|||
|
||||
private func saveCl() {
|
||||
guard let sid = currentSessionId else { return }
|
||||
let ts = pendingEspTimestamp
|
||||
pendingEspTimestamp = nil
|
||||
let params: [String: String] = [
|
||||
"cond_v": clCondV,
|
||||
"cond_t": clCondT,
|
||||
|
|
@ -593,7 +675,7 @@ final class AppState {
|
|||
"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 }
|
||||
guard var meas = try? Storage.shared.addMeasurement(sessionId: sid, type: .chlorine, espTimestamp: ts) else { return }
|
||||
meas.config = configData
|
||||
guard let mid = meas.id else { return }
|
||||
let indexed = clPoints.enumerated().map { (index: $0.offset, value: $0.element) }
|
||||
|
|
@ -605,11 +687,13 @@ final class AppState {
|
|||
|
||||
private func savePh(_ result: PhResult) {
|
||||
guard let sid = currentSessionId else { return }
|
||||
let ts = pendingEspTimestamp
|
||||
pendingEspTimestamp = nil
|
||||
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 }
|
||||
guard var meas = try? Storage.shared.addMeasurement(sessionId: sid, type: .ph, espTimestamp: ts) else { return }
|
||||
meas.config = configData
|
||||
guard let mid = meas.id else { return }
|
||||
try? Storage.shared.addDataPoint(measurementId: mid, index: 0, point: result)
|
||||
|
|
|
|||
|
|
@ -31,6 +31,10 @@ let RSP_REFS_DONE: UInt8 = 0x22
|
|||
let RSP_REF_STATUS: UInt8 = 0x23
|
||||
let RSP_CL_FACTOR: UInt8 = 0x24
|
||||
let RSP_PH_CAL: UInt8 = 0x25
|
||||
let RSP_SESSION_CREATED: UInt8 = 0x40
|
||||
let RSP_SESSION_SWITCHED: UInt8 = 0x41
|
||||
let RSP_SESSION_LIST: UInt8 = 0x42
|
||||
let RSP_SESSION_RENAMED: UInt8 = 0x43
|
||||
let RSP_KEEPALIVE: UInt8 = 0x50
|
||||
|
||||
// Cue -> ESP32
|
||||
|
|
@ -56,6 +60,10 @@ let CMD_GET_PH_CAL: UInt8 = 0x36
|
|||
let CMD_START_REFS: UInt8 = 0x30
|
||||
let CMD_GET_REFS: UInt8 = 0x31
|
||||
let CMD_CLEAR_REFS: UInt8 = 0x32
|
||||
let CMD_SESSION_CREATE: UInt8 = 0x40
|
||||
let CMD_SESSION_SWITCH: UInt8 = 0x41
|
||||
let CMD_SESSION_LIST: UInt8 = 0x42
|
||||
let CMD_SESSION_RENAME: UInt8 = 0x43
|
||||
|
||||
// MARK: - 7-bit MIDI encoding
|
||||
|
||||
|
|
@ -94,6 +102,31 @@ func encodeU16(_ val: UInt16) -> [UInt8] {
|
|||
return [mask, p[0] & 0x7F, p[1] & 0x7F]
|
||||
}
|
||||
|
||||
/// Encode a UInt32 into 5 MIDI-safe bytes.
|
||||
func encodeU32(_ val: UInt32) -> [UInt8] {
|
||||
var v = val
|
||||
let p = withUnsafeBytes(of: &v) { Array($0) }
|
||||
let mask: UInt8 = ((p[0] >> 7) & 1)
|
||||
| ((p[1] >> 6) & 2)
|
||||
| ((p[2] >> 5) & 4)
|
||||
| ((p[3] >> 4) & 8)
|
||||
return [mask, p[0] & 0x7F, p[1] & 0x7F, p[2] & 0x7F, p[3] & 0x7F]
|
||||
}
|
||||
|
||||
/// Decode 5 MIDI-safe bytes back into a UInt32.
|
||||
func decodeU32(_ d: [UInt8], at offset: Int = 0) -> UInt32 {
|
||||
let m = d[offset]
|
||||
let b0 = d[offset + 1] | ((m & 1) << 7)
|
||||
let b1 = d[offset + 2] | ((m & 2) << 6)
|
||||
let b2 = d[offset + 3] | ((m & 4) << 5)
|
||||
let b3 = d[offset + 4] | ((m & 8) << 4)
|
||||
var val: UInt32 = 0
|
||||
withUnsafeMutableBytes(of: &val) { buf in
|
||||
buf[0] = b0; buf[1] = b1; buf[2] = b2; buf[3] = b3
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
/// Decode 3 MIDI-safe bytes back into a UInt16.
|
||||
func decodeU16(_ d: [UInt8], at offset: Int = 0) -> UInt16 {
|
||||
let m = d[offset]
|
||||
|
|
@ -109,17 +142,17 @@ func decodeU16(_ d: [UInt8], at offset: Int = 0) -> UInt16 {
|
|||
// MARK: - Message enum
|
||||
|
||||
enum EisMessage {
|
||||
case sweepStart(numPoints: UInt16, freqStart: Float, freqStop: Float)
|
||||
case sweepStart(numPoints: UInt16, freqStart: Float, freqStop: Float, espTimestamp: UInt32?, espMeasId: UInt16?)
|
||||
case dataPoint(index: UInt16, point: EisPoint)
|
||||
case sweepEnd
|
||||
case config(EisConfig)
|
||||
case lsvStart(numPoints: UInt16, vStart: Float, vStop: Float)
|
||||
case lsvStart(numPoints: UInt16, vStart: Float, vStop: Float, espTimestamp: UInt32?, espMeasId: UInt16?)
|
||||
case lsvPoint(index: UInt16, point: LsvPoint)
|
||||
case lsvEnd
|
||||
case ampStart(vHold: Float)
|
||||
case ampStart(vHold: Float, espTimestamp: UInt32?, espMeasId: UInt16?)
|
||||
case ampPoint(index: UInt16, point: AmpPoint)
|
||||
case ampEnd
|
||||
case clStart(numPoints: UInt16)
|
||||
case clStart(numPoints: UInt16, espTimestamp: UInt32?, espMeasId: UInt16?)
|
||||
case clPoint(index: UInt16, point: ClPoint)
|
||||
case clResult(ClResult)
|
||||
case clEnd
|
||||
|
|
@ -132,6 +165,10 @@ enum EisMessage {
|
|||
case cellK(Float)
|
||||
case clFactor(Float)
|
||||
case phCal(slope: Float, offset: Float)
|
||||
case sessionCreated(id: UInt8, name: String)
|
||||
case sessionSwitched(id: UInt8)
|
||||
case sessionList(count: UInt8, currentId: UInt8, sessions: [(id: UInt8, name: String)])
|
||||
case sessionRenamed(id: UInt8, name: String)
|
||||
case keepalive
|
||||
}
|
||||
|
||||
|
|
@ -146,10 +183,13 @@ func parseSysex(_ data: [UInt8]) -> EisMessage? {
|
|||
switch data[1] {
|
||||
|
||||
case RSP_SWEEP_START where p.count >= 13:
|
||||
let hasExt = p.count >= 21
|
||||
return .sweepStart(
|
||||
numPoints: decodeU16(p, at: 0),
|
||||
freqStart: decodeFloat(p, at: 3),
|
||||
freqStop: decodeFloat(p, at: 8)
|
||||
freqStop: decodeFloat(p, at: 8),
|
||||
espTimestamp: hasExt ? decodeU32(p, at: 13) : nil,
|
||||
espMeasId: hasExt ? decodeU16(p, at: 18) : nil
|
||||
)
|
||||
|
||||
case RSP_DATA_POINT where p.count >= 28:
|
||||
|
|
@ -184,10 +224,13 @@ func parseSysex(_ data: [UInt8]) -> EisMessage? {
|
|||
))
|
||||
|
||||
case RSP_LSV_START where p.count >= 13:
|
||||
let hasExt = p.count >= 21
|
||||
return .lsvStart(
|
||||
numPoints: decodeU16(p, at: 0),
|
||||
vStart: decodeFloat(p, at: 3),
|
||||
vStop: decodeFloat(p, at: 8)
|
||||
vStop: decodeFloat(p, at: 8),
|
||||
espTimestamp: hasExt ? decodeU32(p, at: 13) : nil,
|
||||
espMeasId: hasExt ? decodeU16(p, at: 18) : nil
|
||||
)
|
||||
|
||||
case RSP_LSV_POINT where p.count >= 13:
|
||||
|
|
@ -203,7 +246,12 @@ func parseSysex(_ data: [UInt8]) -> EisMessage? {
|
|||
return .lsvEnd
|
||||
|
||||
case RSP_AMP_START where p.count >= 5:
|
||||
return .ampStart(vHold: decodeFloat(p, at: 0))
|
||||
let hasExt = p.count >= 13
|
||||
return .ampStart(
|
||||
vHold: decodeFloat(p, at: 0),
|
||||
espTimestamp: hasExt ? decodeU32(p, at: 5) : nil,
|
||||
espMeasId: hasExt ? decodeU16(p, at: 10) : nil
|
||||
)
|
||||
|
||||
case RSP_AMP_POINT where p.count >= 13:
|
||||
return .ampPoint(
|
||||
|
|
@ -218,7 +266,12 @@ func parseSysex(_ data: [UInt8]) -> EisMessage? {
|
|||
return .ampEnd
|
||||
|
||||
case RSP_CL_START where p.count >= 3:
|
||||
return .clStart(numPoints: decodeU16(p, at: 0))
|
||||
let hasExt = p.count >= 11
|
||||
return .clStart(
|
||||
numPoints: decodeU16(p, at: 0),
|
||||
espTimestamp: hasExt ? decodeU32(p, at: 3) : nil,
|
||||
espMeasId: hasExt ? decodeU16(p, at: 8) : nil
|
||||
)
|
||||
|
||||
case RSP_CL_POINT where p.count >= 14:
|
||||
return .clPoint(
|
||||
|
|
@ -273,6 +326,46 @@ func parseSysex(_ data: [UInt8]) -> EisMessage? {
|
|||
offset: decodeFloat(p, at: 5)
|
||||
)
|
||||
|
||||
case RSP_SESSION_CREATED where p.count >= 2:
|
||||
let sid = p[0]
|
||||
let nameLen = Int(p[1])
|
||||
let name = nameLen > 0 && p.count >= 2 + nameLen
|
||||
? String(bytes: p[2..<(2 + nameLen)], encoding: .utf8) ?? ""
|
||||
: ""
|
||||
return .sessionCreated(id: sid, name: name)
|
||||
|
||||
case RSP_SESSION_SWITCHED where p.count >= 1:
|
||||
return .sessionSwitched(id: p[0])
|
||||
|
||||
case RSP_SESSION_LIST where p.count >= 2:
|
||||
let count = p[0]
|
||||
let currentId = p[1]
|
||||
var sessions: [(id: UInt8, name: String)] = []
|
||||
var off = 2
|
||||
for _ in 0..<count {
|
||||
guard off < p.count else { break }
|
||||
let sid = p[off]; off += 1
|
||||
guard off < p.count else { break }
|
||||
let nameLen = Int(p[off]); off += 1
|
||||
let name: String
|
||||
if nameLen > 0 && off + nameLen <= p.count {
|
||||
name = String(bytes: p[off..<(off + nameLen)], encoding: .utf8) ?? ""
|
||||
off += nameLen
|
||||
} else {
|
||||
name = ""
|
||||
}
|
||||
sessions.append((id: sid, name: name))
|
||||
}
|
||||
return .sessionList(count: count, currentId: currentId, sessions: sessions)
|
||||
|
||||
case RSP_SESSION_RENAMED where p.count >= 2:
|
||||
let sid = p[0]
|
||||
let nameLen = Int(p[1])
|
||||
let name = nameLen > 0 && p.count >= 2 + nameLen
|
||||
? String(bytes: p[2..<(2 + nameLen)], encoding: .utf8) ?? ""
|
||||
: ""
|
||||
return .sessionRenamed(id: sid, name: name)
|
||||
|
||||
case RSP_KEEPALIVE:
|
||||
return .keepalive
|
||||
|
||||
|
|
@ -417,3 +510,29 @@ func buildSysexSetPhCal(_ slope: Float, _ offset: Float) -> [UInt8] {
|
|||
func buildSysexGetPhCal() -> [UInt8] {
|
||||
[0xF0, sysexMfr, CMD_GET_PH_CAL, 0xF7]
|
||||
}
|
||||
|
||||
// MARK: - Session commands
|
||||
|
||||
func buildSysexSessionCreate(name: String) -> [UInt8] {
|
||||
let nameBytes = Array(name.utf8.prefix(32))
|
||||
var sx: [UInt8] = [0xF0, sysexMfr, CMD_SESSION_CREATE, UInt8(nameBytes.count)]
|
||||
sx.append(contentsOf: nameBytes)
|
||||
sx.append(0xF7)
|
||||
return sx
|
||||
}
|
||||
|
||||
func buildSysexSessionSwitch(id: UInt8) -> [UInt8] {
|
||||
[0xF0, sysexMfr, CMD_SESSION_SWITCH, id, 0xF7]
|
||||
}
|
||||
|
||||
func buildSysexSessionList() -> [UInt8] {
|
||||
[0xF0, sysexMfr, CMD_SESSION_LIST, 0xF7]
|
||||
}
|
||||
|
||||
func buildSysexSessionRename(id: UInt8, name: String) -> [UInt8] {
|
||||
let nameBytes = Array(name.utf8.prefix(32))
|
||||
var sx: [UInt8] = [0xF0, sysexMfr, CMD_SESSION_RENAME, id, UInt8(nameBytes.count)]
|
||||
sx.append(contentsOf: nameBytes)
|
||||
sx.append(0xF7)
|
||||
return sx
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ final class UDPManager: @unchecked Sendable {
|
|||
private var connection: NWConnection?
|
||||
private let queue = DispatchQueue(label: "udp.eis4", qos: .userInitiated)
|
||||
private var onMessage: ((EisMessage) -> Void)?
|
||||
private var onDisconnect: (() -> Void)?
|
||||
private var keepaliveTimer: Timer?
|
||||
private var timeoutTimer: Timer?
|
||||
private var lastReceived: Date = .distantPast
|
||||
|
|
@ -49,6 +50,10 @@ final class UDPManager: @unchecked Sendable {
|
|||
onMessage = handler
|
||||
}
|
||||
|
||||
func setDisconnectHandler(_ handler: @escaping () -> Void) {
|
||||
onDisconnect = handler
|
||||
}
|
||||
|
||||
// MARK: - Connection
|
||||
|
||||
func connect() {
|
||||
|
|
@ -81,6 +86,7 @@ final class UDPManager: @unchecked Sendable {
|
|||
connection = nil
|
||||
measuring = false
|
||||
state = .disconnected
|
||||
onDisconnect?()
|
||||
}
|
||||
|
||||
func send(_ sysex: [UInt8]) {
|
||||
|
|
@ -188,6 +194,7 @@ final class UDPManager: @unchecked Sendable {
|
|||
self.stopTimers()
|
||||
self.connection?.cancel()
|
||||
self.connection = nil
|
||||
self.onDisconnect?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue