// // UMProtocol.swift // USB Meter // // Created by Codex on 23/03/2026. // import Foundation struct UMDataGroupTotals { let ah: Double let wh: Double } struct UMSnapshot { let modelNumber: UInt16 let voltage: Double let current: Double let power: Double let temperatureCelsius: Double let temperatureFahrenheit: Double let selectedDataGroup: UInt8 let dataGroupRecords: [Int: UMDataGroupTotals] let usbPlusVoltage: Double let usbMinusVoltage: Double let chargerTypeIndex: UInt16 let recordedAH: Double let recordedWH: Double let recordingThreshold: Double let recordingDuration: UInt32 let recording: Bool let screenTimeout: Int let screenBrightness: Int let loadResistance: Double let currentScreen: UInt16 } enum UMProtocolError: Error { case invalidPayloadLength(Int) } enum UMProtocol { static let minimumSnapshotLength = 128 static let snapshotRequest = Data([0xF0]) static let nextScreen = Data([0xF1]) static let rotateScreen = Data([0xF2]) static let previousScreen = Data([0xF3]) static let clearCurrentGroup = Data([0xF4]) static func selectDataGroup(_ id: UInt8) -> Data { Data([0xA0 | id]) } static func setScreenBrightness(_ value: UInt8) -> Data { Data([0xD0 | value]) } static func setScreenSaverTimeout(_ value: UInt8) -> Data { Data([0xE0 | value]) } static func setRecordingThreshold(_ value: UInt8) -> Data { Data([0xB0 + value]) } static func parseSnapshot(from buffer: Data, model: Model) throws -> UMSnapshot { guard buffer.count >= minimumSnapshotLength else { throw UMProtocolError.invalidPayloadLength(buffer.count) } let modelNumber = UInt16(bigEndian: buffer.value(from: 0)) let voltageScale: Double let currentScale: Double switch model { case .UM25C: voltageScale = 1000 currentScale = 10000 case .UM34C: voltageScale = 100 currentScale = 1000 case .TC66C: voltageScale = 1 currentScale = 1 } var dataGroupRecords: [Int: UMDataGroupTotals] = [:] for index in stride(from: 0, through: 9, by: 1) { let offset = 16 + index * 8 dataGroupRecords[index] = UMDataGroupTotals( ah: Double(UInt32(bigEndian: buffer.value(from: offset))) / 1000, wh: Double(UInt32(bigEndian: buffer.value(from: offset + 4))) / 1000 ) } return UMSnapshot( modelNumber: modelNumber, voltage: Double(UInt16(bigEndian: buffer.value(from: 2))) / voltageScale, current: Double(UInt16(bigEndian: buffer.value(from: 4))) / currentScale, power: Double(UInt32(bigEndian: buffer.value(from: 6))) / 1000, temperatureCelsius: Double(UInt16(bigEndian: buffer.value(from: 10))), temperatureFahrenheit: Double(UInt16(bigEndian: buffer.value(from: 12))), selectedDataGroup: UInt8(UInt16(bigEndian: buffer.value(from: 14))), dataGroupRecords: dataGroupRecords, usbPlusVoltage: Double(UInt16(bigEndian: buffer.value(from: 96))) / 100, usbMinusVoltage: Double(UInt16(bigEndian: buffer.value(from: 98))) / 100, chargerTypeIndex: UInt16(bigEndian: buffer.value(from: 100)), recordedAH: Double(UInt32(bigEndian: buffer.value(from: 102))) / 1000, recordedWH: Double(UInt32(bigEndian: buffer.value(from: 106))) / 1000, recordingThreshold: Double(UInt16(bigEndian: buffer.value(from: 110))) / 100, recordingDuration: UInt32(bigEndian: buffer.value(from: 112)), recording: UInt16(bigEndian: buffer.value(from: 116)) == 1, screenTimeout: Int(UInt16(bigEndian: buffer.value(from: 118))), screenBrightness: Int(UInt16(bigEndian: buffer.value(from: 120))), loadResistance: Double(UInt32(bigEndian: buffer.value(from: 122))) / 10, currentScreen: UInt16(bigEndian: buffer.value(from: 126)) ) } }