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