USB-Meter / USB Meter / Model / TC66Protocol.swift
1 contributor
108 lines | 4.126kb
//
//  TC66Protocol.swift
//  USB Meter
//
//  Created by Codex on 23/03/2026.
//

import Foundation
import CryptoSwift

struct TC66DataGroupTotals {
    let ah: Double
    let wh: Double
}

struct TC66Snapshot {
    let modelName: String
    let firmwareVersion: String
    let serialNumber: UInt32
    let bootCount: UInt32
    let voltage: Double
    let current: Double
    let power: Double
    let loadResistance: Double
    let dataGroupRecords: [Int: TC66DataGroupTotals]
    let temperatureCelsius: Double
    let usbPlusVoltage: Double
    let usbMinusVoltage: Double
}

enum TC66ProtocolError: Error {
    case invalidPayloadLength(Int)
    case invalidPacket(Int)
}

enum TC66Protocol {
    static let snapshotLength = 192
    static let snapshotRequest = Data("bgetva\r\n".utf8)
    static let nextPage = Data("bnextp\r\n".utf8)
    static let previousPage = Data("blastp\r\n".utf8)
    static let rotateScreen = Data("brotat\r\n".utf8)

    private static let aesKey: [UInt8] = [
        0x58, 0x21, 0xfa, 0x56, 0x01, 0xb2, 0xf0, 0x26,
        0x87, 0xff, 0x12, 0x04, 0x62, 0x2a, 0x4f, 0xb0,
        0x86, 0xf4, 0x02, 0x60, 0x81, 0x6f, 0x9a, 0x0b,
        0xa7, 0xf1, 0x06, 0x61, 0x9a, 0xb8, 0x72, 0x88
    ]

    private static func validate(packetId: UInt8, packet: Data) -> Bool {
        let expectedHeader = "pac\(packetId)".data(using: .ascii)
        let packetHeader = packet.subdata(from: 0, length: 4)
        let expectedCRC = UInt16(bigEndian: packet.subdata(from: 0, length: 60).crc16(seed: 0xFFFF).value(from: 0))
        let packetCRC = UInt16(littleEndian: packet.value(from: 60))
        return expectedHeader == packetHeader && expectedCRC == packetCRC
    }

    static func parseSnapshot(from buffer: Data) throws -> TC66Snapshot {
        guard buffer.count == snapshotLength else {
            throw TC66ProtocolError.invalidPayloadLength(buffer.count)
        }

        let cipher = try AES(key: aesKey, blockMode: ECB())
        let decryptedBuffer = Data(try cipher.decrypt(buffer.bytes))

        let pac1 = decryptedBuffer.subdata(from: 0, length: 64)
        guard validate(packetId: 1, packet: pac1) else {
            throw TC66ProtocolError.invalidPacket(1)
        }

        let pac2 = decryptedBuffer.subdata(from: 64, length: 64)
        guard validate(packetId: 2, packet: pac2) else {
            throw TC66ProtocolError.invalidPacket(2)
        }

        let pac3 = decryptedBuffer.subdata(from: 128, length: 64)
        guard validate(packetId: 3, packet: pac3) else {
            throw TC66ProtocolError.invalidPacket(3)
        }

        var dataGroupRecords: [Int: TC66DataGroupTotals] = [:]
        for index in stride(from: 0, through: 1, by: 1) {
            let offset = 8 + index * 8
            dataGroupRecords[index] = TC66DataGroupTotals(
                ah: Double(UInt32(littleEndian: pac2.value(from: offset))) / 1000,
                wh: Double(UInt32(littleEndian: pac2.value(from: offset + 40))) / 1000
            )
        }

        let temperatureMagnitude = Double(UInt32(littleEndian: pac2.value(from: 28)))
        let temperatureSign = UInt32(littleEndian: pac2.value(from: 24)) == 1 ? -1.0 : 1.0

        return TC66Snapshot(
            modelName: pac1.subdata(from: 4, length: 4).asciiString,
            firmwareVersion: pac1.subdata(from: 8, length: 4).asciiString,
            serialNumber: UInt32(littleEndian: pac1.value(from: 12)),
            bootCount: UInt32(littleEndian: pac1.value(from: 44)),
            voltage: Double(UInt32(littleEndian: pac1.value(from: 48))) / 10000,
            current: Double(UInt32(littleEndian: pac1.value(from: 52))) / 100000,
            power: Double(UInt32(littleEndian: pac1.value(from: 56))) / 10000,
            loadResistance: Double(UInt32(littleEndian: pac2.value(from: 4))) / 10,
            dataGroupRecords: dataGroupRecords,
            temperatureCelsius: temperatureMagnitude * temperatureSign,
            usbPlusVoltage: Double(UInt32(littleEndian: pac2.value(from: 32))) / 100,
            usbMinusVoltage: Double(UInt32(littleEndian: pac2.value(from: 36))) / 100
        )
    }
}