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
|
// Session
|
||||||
var currentSessionId: Int64? = nil
|
var currentSessionId: Int64? = nil
|
||||||
|
var firmwareSessionMap: [UInt8: Int64] = [:]
|
||||||
|
var sessionListReceived: Bool = false
|
||||||
|
private var pendingEspTimestamp: Int64? = nil
|
||||||
|
|
||||||
// Calibration
|
// Calibration
|
||||||
var calVolumeGal: Double = 25
|
var calVolumeGal: Double = 25
|
||||||
|
|
@ -125,6 +128,10 @@ final class AppState {
|
||||||
transport.setMessageHandler { [weak self] msg in
|
transport.setMessageHandler { [weak self] msg in
|
||||||
self?.handleMessage(msg)
|
self?.handleMessage(msg)
|
||||||
}
|
}
|
||||||
|
transport.setDisconnectHandler { [weak self] in
|
||||||
|
self?.sessionListReceived = false
|
||||||
|
self?.firmwareSessionMap.removeAll()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Send helper
|
// MARK: - Send helper
|
||||||
|
|
@ -138,7 +145,8 @@ final class AppState {
|
||||||
private func handleMessage(_ msg: EisMessage) {
|
private func handleMessage(_ msg: EisMessage) {
|
||||||
switch msg {
|
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 {
|
if collectingRefs {
|
||||||
eisPoints.removeAll()
|
eisPoints.removeAll()
|
||||||
sweepTotal = numPoints
|
sweepTotal = numPoints
|
||||||
|
|
@ -172,9 +180,14 @@ final class AppState {
|
||||||
rtia = cfg.rtia
|
rtia = cfg.rtia
|
||||||
rcal = cfg.rcal
|
rcal = cfg.rcal
|
||||||
electrode = cfg.electrode
|
electrode = cfg.electrode
|
||||||
|
if !sessionListReceived {
|
||||||
|
sessionListReceived = true
|
||||||
|
send(buildSysexSessionList())
|
||||||
|
}
|
||||||
status = "Config received"
|
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()
|
lsvPoints.removeAll()
|
||||||
lsvTotal = numPoints
|
lsvTotal = numPoints
|
||||||
status = String(format: "LSV: %d pts, %.0f--%.0f mV", numPoints, vStart, vStop)
|
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)
|
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()
|
ampPoints.removeAll()
|
||||||
ampRunning = true
|
ampRunning = true
|
||||||
status = String(format: "Amp: %.0f mV", vHold)
|
status = String(format: "Amp: %.0f mV", vHold)
|
||||||
|
|
@ -229,7 +243,8 @@ final class AppState {
|
||||||
saveAmp()
|
saveAmp()
|
||||||
status = "Amp complete: \(ampPoints.count) points"
|
status = "Amp complete: \(ampPoints.count) points"
|
||||||
|
|
||||||
case .clStart(let numPoints):
|
case .clStart(let numPoints, let espTs, _):
|
||||||
|
pendingEspTimestamp = espTs.map { Int64($0) }
|
||||||
clPoints.removeAll()
|
clPoints.removeAll()
|
||||||
clResult = nil
|
clResult = nil
|
||||||
clTotal = numPoints
|
clTotal = numPoints
|
||||||
|
|
@ -324,11 +339,70 @@ final class AppState {
|
||||||
phOffset = Double(offset)
|
phOffset = Double(offset)
|
||||||
status = String(format: "pH cal: slope=%.4f offset=%.4f", slope, 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:
|
case .keepalive:
|
||||||
break
|
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
|
// MARK: - Actions
|
||||||
|
|
||||||
func applyEISSettings() {
|
func applyEISSettings() {
|
||||||
|
|
@ -533,6 +607,8 @@ final class AppState {
|
||||||
|
|
||||||
private func saveEis() {
|
private func saveEis() {
|
||||||
guard let sid = currentSessionId else { return }
|
guard let sid = currentSessionId else { return }
|
||||||
|
let ts = pendingEspTimestamp
|
||||||
|
pendingEspTimestamp = nil
|
||||||
let params: [String: String] = [
|
let params: [String: String] = [
|
||||||
"freq_start": freqStart,
|
"freq_start": freqStart,
|
||||||
"freq_stop": freqStop,
|
"freq_stop": freqStop,
|
||||||
|
|
@ -542,7 +618,7 @@ final class AppState {
|
||||||
"electrode": electrode.label,
|
"electrode": electrode.label,
|
||||||
]
|
]
|
||||||
guard let configData = try? JSONEncoder().encode(params) else { return }
|
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
|
meas.config = configData
|
||||||
guard let mid = meas.id else { return }
|
guard let mid = meas.id else { return }
|
||||||
let indexed = eisPoints.enumerated().map { (index: $0.offset, value: $0.element) }
|
let indexed = eisPoints.enumerated().map { (index: $0.offset, value: $0.element) }
|
||||||
|
|
@ -551,6 +627,8 @@ final class AppState {
|
||||||
|
|
||||||
private func saveLsv() {
|
private func saveLsv() {
|
||||||
guard let sid = currentSessionId else { return }
|
guard let sid = currentSessionId else { return }
|
||||||
|
let ts = pendingEspTimestamp
|
||||||
|
pendingEspTimestamp = nil
|
||||||
let params: [String: String] = [
|
let params: [String: String] = [
|
||||||
"v_start": lsvStartV,
|
"v_start": lsvStartV,
|
||||||
"v_stop": lsvStopV,
|
"v_stop": lsvStopV,
|
||||||
|
|
@ -558,7 +636,7 @@ final class AppState {
|
||||||
"rtia": lsvRtia.label,
|
"rtia": lsvRtia.label,
|
||||||
]
|
]
|
||||||
guard let configData = try? JSONEncoder().encode(params) else { return }
|
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
|
meas.config = configData
|
||||||
guard let mid = meas.id else { return }
|
guard let mid = meas.id else { return }
|
||||||
let indexed = lsvPoints.enumerated().map { (index: $0.offset, value: $0.element) }
|
let indexed = lsvPoints.enumerated().map { (index: $0.offset, value: $0.element) }
|
||||||
|
|
@ -567,6 +645,8 @@ final class AppState {
|
||||||
|
|
||||||
private func saveAmp() {
|
private func saveAmp() {
|
||||||
guard let sid = currentSessionId else { return }
|
guard let sid = currentSessionId else { return }
|
||||||
|
let ts = pendingEspTimestamp
|
||||||
|
pendingEspTimestamp = nil
|
||||||
let params: [String: String] = [
|
let params: [String: String] = [
|
||||||
"v_hold": ampVHold,
|
"v_hold": ampVHold,
|
||||||
"interval_ms": ampInterval,
|
"interval_ms": ampInterval,
|
||||||
|
|
@ -574,7 +654,7 @@ final class AppState {
|
||||||
"rtia": ampRtia.label,
|
"rtia": ampRtia.label,
|
||||||
]
|
]
|
||||||
guard let configData = try? JSONEncoder().encode(params) else { return }
|
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
|
meas.config = configData
|
||||||
guard let mid = meas.id else { return }
|
guard let mid = meas.id else { return }
|
||||||
let indexed = ampPoints.enumerated().map { (index: $0.offset, value: $0.element) }
|
let indexed = ampPoints.enumerated().map { (index: $0.offset, value: $0.element) }
|
||||||
|
|
@ -583,6 +663,8 @@ final class AppState {
|
||||||
|
|
||||||
private func saveCl() {
|
private func saveCl() {
|
||||||
guard let sid = currentSessionId else { return }
|
guard let sid = currentSessionId else { return }
|
||||||
|
let ts = pendingEspTimestamp
|
||||||
|
pendingEspTimestamp = nil
|
||||||
let params: [String: String] = [
|
let params: [String: String] = [
|
||||||
"cond_v": clCondV,
|
"cond_v": clCondV,
|
||||||
"cond_t": clCondT,
|
"cond_t": clCondT,
|
||||||
|
|
@ -593,7 +675,7 @@ final class AppState {
|
||||||
"rtia": clRtia.label,
|
"rtia": clRtia.label,
|
||||||
]
|
]
|
||||||
guard let configData = try? JSONEncoder().encode(params) else { return }
|
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
|
meas.config = configData
|
||||||
guard let mid = meas.id else { return }
|
guard let mid = meas.id else { return }
|
||||||
let indexed = clPoints.enumerated().map { (index: $0.offset, value: $0.element) }
|
let indexed = clPoints.enumerated().map { (index: $0.offset, value: $0.element) }
|
||||||
|
|
@ -605,11 +687,13 @@ final class AppState {
|
||||||
|
|
||||||
private func savePh(_ result: PhResult) {
|
private func savePh(_ result: PhResult) {
|
||||||
guard let sid = currentSessionId else { return }
|
guard let sid = currentSessionId else { return }
|
||||||
|
let ts = pendingEspTimestamp
|
||||||
|
pendingEspTimestamp = nil
|
||||||
let params: [String: String] = [
|
let params: [String: String] = [
|
||||||
"stabilize_s": phStabilize,
|
"stabilize_s": phStabilize,
|
||||||
]
|
]
|
||||||
guard let configData = try? JSONEncoder().encode(params) else { return }
|
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
|
meas.config = configData
|
||||||
guard let mid = meas.id else { return }
|
guard let mid = meas.id else { return }
|
||||||
try? Storage.shared.addDataPoint(measurementId: mid, index: 0, point: result)
|
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_REF_STATUS: UInt8 = 0x23
|
||||||
let RSP_CL_FACTOR: UInt8 = 0x24
|
let RSP_CL_FACTOR: UInt8 = 0x24
|
||||||
let RSP_PH_CAL: UInt8 = 0x25
|
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
|
let RSP_KEEPALIVE: UInt8 = 0x50
|
||||||
|
|
||||||
// Cue -> ESP32
|
// Cue -> ESP32
|
||||||
|
|
@ -56,6 +60,10 @@ let CMD_GET_PH_CAL: UInt8 = 0x36
|
||||||
let CMD_START_REFS: UInt8 = 0x30
|
let CMD_START_REFS: UInt8 = 0x30
|
||||||
let CMD_GET_REFS: UInt8 = 0x31
|
let CMD_GET_REFS: UInt8 = 0x31
|
||||||
let CMD_CLEAR_REFS: UInt8 = 0x32
|
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
|
// MARK: - 7-bit MIDI encoding
|
||||||
|
|
||||||
|
|
@ -94,6 +102,31 @@ func encodeU16(_ val: UInt16) -> [UInt8] {
|
||||||
return [mask, p[0] & 0x7F, p[1] & 0x7F]
|
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.
|
/// Decode 3 MIDI-safe bytes back into a UInt16.
|
||||||
func decodeU16(_ d: [UInt8], at offset: Int = 0) -> UInt16 {
|
func decodeU16(_ d: [UInt8], at offset: Int = 0) -> UInt16 {
|
||||||
let m = d[offset]
|
let m = d[offset]
|
||||||
|
|
@ -109,17 +142,17 @@ func decodeU16(_ d: [UInt8], at offset: Int = 0) -> UInt16 {
|
||||||
// MARK: - Message enum
|
// MARK: - Message enum
|
||||||
|
|
||||||
enum EisMessage {
|
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 dataPoint(index: UInt16, point: EisPoint)
|
||||||
case sweepEnd
|
case sweepEnd
|
||||||
case config(EisConfig)
|
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 lsvPoint(index: UInt16, point: LsvPoint)
|
||||||
case lsvEnd
|
case lsvEnd
|
||||||
case ampStart(vHold: Float)
|
case ampStart(vHold: Float, espTimestamp: UInt32?, espMeasId: UInt16?)
|
||||||
case ampPoint(index: UInt16, point: AmpPoint)
|
case ampPoint(index: UInt16, point: AmpPoint)
|
||||||
case ampEnd
|
case ampEnd
|
||||||
case clStart(numPoints: UInt16)
|
case clStart(numPoints: UInt16, espTimestamp: UInt32?, espMeasId: UInt16?)
|
||||||
case clPoint(index: UInt16, point: ClPoint)
|
case clPoint(index: UInt16, point: ClPoint)
|
||||||
case clResult(ClResult)
|
case clResult(ClResult)
|
||||||
case clEnd
|
case clEnd
|
||||||
|
|
@ -132,6 +165,10 @@ enum EisMessage {
|
||||||
case cellK(Float)
|
case cellK(Float)
|
||||||
case clFactor(Float)
|
case clFactor(Float)
|
||||||
case phCal(slope: Float, offset: 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
|
case keepalive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -146,10 +183,13 @@ func parseSysex(_ data: [UInt8]) -> EisMessage? {
|
||||||
switch data[1] {
|
switch data[1] {
|
||||||
|
|
||||||
case RSP_SWEEP_START where p.count >= 13:
|
case RSP_SWEEP_START where p.count >= 13:
|
||||||
|
let hasExt = p.count >= 21
|
||||||
return .sweepStart(
|
return .sweepStart(
|
||||||
numPoints: decodeU16(p, at: 0),
|
numPoints: decodeU16(p, at: 0),
|
||||||
freqStart: decodeFloat(p, at: 3),
|
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:
|
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:
|
case RSP_LSV_START where p.count >= 13:
|
||||||
|
let hasExt = p.count >= 21
|
||||||
return .lsvStart(
|
return .lsvStart(
|
||||||
numPoints: decodeU16(p, at: 0),
|
numPoints: decodeU16(p, at: 0),
|
||||||
vStart: decodeFloat(p, at: 3),
|
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:
|
case RSP_LSV_POINT where p.count >= 13:
|
||||||
|
|
@ -203,7 +246,12 @@ func parseSysex(_ data: [UInt8]) -> EisMessage? {
|
||||||
return .lsvEnd
|
return .lsvEnd
|
||||||
|
|
||||||
case RSP_AMP_START where p.count >= 5:
|
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:
|
case RSP_AMP_POINT where p.count >= 13:
|
||||||
return .ampPoint(
|
return .ampPoint(
|
||||||
|
|
@ -218,7 +266,12 @@ func parseSysex(_ data: [UInt8]) -> EisMessage? {
|
||||||
return .ampEnd
|
return .ampEnd
|
||||||
|
|
||||||
case RSP_CL_START where p.count >= 3:
|
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:
|
case RSP_CL_POINT where p.count >= 14:
|
||||||
return .clPoint(
|
return .clPoint(
|
||||||
|
|
@ -273,6 +326,46 @@ func parseSysex(_ data: [UInt8]) -> EisMessage? {
|
||||||
offset: decodeFloat(p, at: 5)
|
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:
|
case RSP_KEEPALIVE:
|
||||||
return .keepalive
|
return .keepalive
|
||||||
|
|
||||||
|
|
@ -417,3 +510,29 @@ func buildSysexSetPhCal(_ slope: Float, _ offset: Float) -> [UInt8] {
|
||||||
func buildSysexGetPhCal() -> [UInt8] {
|
func buildSysexGetPhCal() -> [UInt8] {
|
||||||
[0xF0, sysexMfr, CMD_GET_PH_CAL, 0xF7]
|
[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 var connection: NWConnection?
|
||||||
private let queue = DispatchQueue(label: "udp.eis4", qos: .userInitiated)
|
private let queue = DispatchQueue(label: "udp.eis4", qos: .userInitiated)
|
||||||
private var onMessage: ((EisMessage) -> Void)?
|
private var onMessage: ((EisMessage) -> Void)?
|
||||||
|
private var onDisconnect: (() -> Void)?
|
||||||
private var keepaliveTimer: Timer?
|
private var keepaliveTimer: Timer?
|
||||||
private var timeoutTimer: Timer?
|
private var timeoutTimer: Timer?
|
||||||
private var lastReceived: Date = .distantPast
|
private var lastReceived: Date = .distantPast
|
||||||
|
|
@ -49,6 +50,10 @@ final class UDPManager: @unchecked Sendable {
|
||||||
onMessage = handler
|
onMessage = handler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setDisconnectHandler(_ handler: @escaping () -> Void) {
|
||||||
|
onDisconnect = handler
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Connection
|
// MARK: - Connection
|
||||||
|
|
||||||
func connect() {
|
func connect() {
|
||||||
|
|
@ -81,6 +86,7 @@ final class UDPManager: @unchecked Sendable {
|
||||||
connection = nil
|
connection = nil
|
||||||
measuring = false
|
measuring = false
|
||||||
state = .disconnected
|
state = .disconnected
|
||||||
|
onDisconnect?()
|
||||||
}
|
}
|
||||||
|
|
||||||
func send(_ sysex: [UInt8]) {
|
func send(_ sysex: [UInt8]) {
|
||||||
|
|
@ -188,6 +194,7 @@ final class UDPManager: @unchecked Sendable {
|
||||||
self.stopTimers()
|
self.stopTimers()
|
||||||
self.connection?.cancel()
|
self.connection?.cancel()
|
||||||
self.connection = nil
|
self.connection = nil
|
||||||
|
self.onDisconnect?()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue