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