USB-Meter / USB Meter / Model / TC66Protocol.swift
Newer Older
108 lines | 4.126kb
Bogdan Timofte authored 2 weeks ago
1
//
2
//  TC66Protocol.swift
3
//  USB Meter
4
//
5
//  Created by Codex on 23/03/2026.
6
//
7

            
8
import Foundation
9
import CryptoSwift
10

            
11
struct TC66DataGroupTotals {
12
    let ah: Double
13
    let wh: Double
14
}
15

            
16
struct TC66Snapshot {
17
    let modelName: String
18
    let firmwareVersion: String
19
    let serialNumber: UInt32
20
    let bootCount: UInt32
21
    let voltage: Double
22
    let current: Double
23
    let power: Double
24
    let loadResistance: Double
25
    let dataGroupRecords: [Int: TC66DataGroupTotals]
26
    let temperatureCelsius: Double
27
    let usbPlusVoltage: Double
28
    let usbMinusVoltage: Double
29
}
30

            
31
enum TC66ProtocolError: Error {
32
    case invalidPayloadLength(Int)
33
    case invalidPacket(Int)
34
}
35

            
36
enum TC66Protocol {
37
    static let snapshotLength = 192
38
    static let snapshotRequest = Data("bgetva\r\n".utf8)
39
    static let nextPage = Data("bnextp\r\n".utf8)
40
    static let previousPage = Data("blastp\r\n".utf8)
41
    static let rotateScreen = Data("brotat\r\n".utf8)
42

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

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

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

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

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

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

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

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

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

            
93
        return TC66Snapshot(
94
            modelName: pac1.subdata(from: 4, length: 4).asciiString,
95
            firmwareVersion: pac1.subdata(from: 8, length: 4).asciiString,
96
            serialNumber: UInt32(littleEndian: pac1.value(from: 12)),
97
            bootCount: UInt32(littleEndian: pac1.value(from: 44)),
98
            voltage: Double(UInt32(littleEndian: pac1.value(from: 48))) / 10000,
99
            current: Double(UInt32(littleEndian: pac1.value(from: 52))) / 100000,
100
            power: Double(UInt32(littleEndian: pac1.value(from: 56))) / 10000,
101
            loadResistance: Double(UInt32(littleEndian: pac2.value(from: 4))) / 10,
102
            dataGroupRecords: dataGroupRecords,
103
            temperatureCelsius: temperatureMagnitude * temperatureSign,
104
            usbPlusVoltage: Double(UInt32(littleEndian: pac2.value(from: 32))) / 100,
105
            usbMinusVoltage: Double(UInt32(littleEndian: pac2.value(from: 36))) / 100
106
        )
107
    }
108
}