USB-Meter / USB Meter / Model / TC66Protocol.swift
Newer Older
114 lines | 4.387kb
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

            
Bogdan Timofte authored 2 weeks ago
50
    private static func sanitizedASCII(from data: Data) -> String {
51
        data.asciiString
52
            .replacingOccurrences(of: "\0", with: "")
53
            .trimmingCharacters(in: .whitespacesAndNewlines.union(.controlCharacters))
54
    }
55

            
Bogdan Timofte authored 2 weeks ago
56
    private static func validate(packetId: UInt8, packet: Data) -> Bool {
57
        let expectedHeader = "pac\(packetId)".data(using: .ascii)
58
        let packetHeader = packet.subdata(from: 0, length: 4)
59
        let expectedCRC = UInt16(bigEndian: packet.subdata(from: 0, length: 60).crc16(seed: 0xFFFF).value(from: 0))
60
        let packetCRC = UInt16(littleEndian: packet.value(from: 60))
61
        return expectedHeader == packetHeader && expectedCRC == packetCRC
62
    }
63

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

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

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

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

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

            
87
        var dataGroupRecords: [Int: TC66DataGroupTotals] = [:]
88
        for index in stride(from: 0, through: 1, by: 1) {
89
            let offset = 8 + index * 8
90
            dataGroupRecords[index] = TC66DataGroupTotals(
91
                ah: Double(UInt32(littleEndian: pac2.value(from: offset))) / 1000,
Bogdan Timofte authored 2 weeks ago
92
                wh: Double(UInt32(littleEndian: pac2.value(from: offset + 4))) / 1000
Bogdan Timofte authored 2 weeks ago
93
            )
94
        }
95

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

            
99
        return TC66Snapshot(
Bogdan Timofte authored 2 weeks ago
100
            modelName: sanitizedASCII(from: pac1.subdata(from: 4, length: 4)),
101
            firmwareVersion: sanitizedASCII(from: pac1.subdata(from: 8, length: 4)),
Bogdan Timofte authored 2 weeks ago
102
            serialNumber: UInt32(littleEndian: pac1.value(from: 12)),
103
            bootCount: UInt32(littleEndian: pac1.value(from: 44)),
104
            voltage: Double(UInt32(littleEndian: pac1.value(from: 48))) / 10000,
105
            current: Double(UInt32(littleEndian: pac1.value(from: 52))) / 100000,
106
            power: Double(UInt32(littleEndian: pac1.value(from: 56))) / 10000,
Bogdan Timofte authored 2 weeks ago
107
            loadResistance: Double(UInt32(littleEndian: pac2.value(from: 4))) / 100,
Bogdan Timofte authored 2 weeks ago
108
            dataGroupRecords: dataGroupRecords,
109
            temperatureCelsius: temperatureMagnitude * temperatureSign,
110
            usbPlusVoltage: Double(UInt32(littleEndian: pac2.value(from: 32))) / 100,
111
            usbMinusVoltage: Double(UInt32(littleEndian: pac2.value(from: 36))) / 100
112
        )
113
    }
114
}