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