USB-Meter / USB Meter / Model / UMProtocol.swift
1 contributor
119 lines | 4.183kb
//
//  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))
        )
    }
}