1 contributor
//
// 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))
)
}
}