forked from fritob/Camper-Monitor
124 lines
4.4 KiB
Swift
124 lines
4.4 KiB
Swift
import Foundation
|
||
|
||
/// Reine Protokoll-Logik für Daly-BMS – ohne CoreBluetooth, damit sie sich
|
||
/// isoliert prüfen lässt.
|
||
///
|
||
/// Daly hat zwei Generationen im Umlauf:
|
||
///
|
||
/// * **Klassisch (`A5`)** – 13-Byte-Rahmen `A5 <adr> <cmd> 08 <8 Datenbytes> <Prüfsumme>`.
|
||
/// Verbreitet bei den Smart-BMS mit dem blauen BLE-Stick, wie sie in vielen
|
||
/// Bulltron-Akkus stecken.
|
||
/// * **Neu (`D2`)** – Modbus-RTU über BLE, `D2 03 <Startregister> <Anzahl> <CRC16>`.
|
||
///
|
||
/// Welche Generation verbaut ist, erkennt `DalySession` anhand der Antwort.
|
||
enum DalyProtocol {
|
||
|
||
// MARK: - Klassisches A5-Protokoll
|
||
|
||
enum Command: UInt8, CaseIterable {
|
||
case soc = 0x90 // Spannung, Strom, Ladezustand
|
||
case cellVoltageMinMax = 0x91
|
||
case temperatureMinMax = 0x92
|
||
case mosfetStatus = 0x93
|
||
case statusInfo = 0x94
|
||
case cellVoltages = 0x95
|
||
case cellTemperatures = 0x96
|
||
}
|
||
|
||
/// Adresse des Anfragenden. 0x80 = Bluetooth-Modul.
|
||
static let hostAddress: UInt8 = 0x80
|
||
|
||
static func requestFrame(_ command: Command) -> Data {
|
||
var frame: [UInt8] = [0xA5, hostAddress, command.rawValue, 0x08]
|
||
frame.append(contentsOf: [UInt8](repeating: 0, count: 8))
|
||
frame.append(frame.reduce(0) { UInt8(($0 &+ $1) & 0xFF) })
|
||
return Data(frame)
|
||
}
|
||
|
||
struct Frame {
|
||
let address: UInt8
|
||
let command: UInt8
|
||
let payload: [UInt8] // immer 8 Bytes
|
||
}
|
||
|
||
/// Sucht vollständige, prüfsummenkorrekte A5-Rahmen im Puffer und gibt sie
|
||
/// zusammen mit dem unverbrauchten Rest zurück.
|
||
static func extractA5Frames(from buffer: [UInt8]) -> (frames: [Frame], remainder: [UInt8]) {
|
||
var frames: [Frame] = []
|
||
var index = 0
|
||
var lastConsumed = 0
|
||
|
||
while index + 13 <= buffer.count {
|
||
guard buffer[index] == 0xA5, buffer[index + 3] == 0x08 else {
|
||
index += 1
|
||
continue
|
||
}
|
||
let slice = Array(buffer[index..<(index + 13)])
|
||
let checksum = slice[0..<12].reduce(UInt8(0)) { UInt8(($0 &+ $1) & 0xFF) }
|
||
guard checksum == slice[12] else {
|
||
index += 1
|
||
continue
|
||
}
|
||
frames.append(Frame(address: slice[1],
|
||
command: slice[2],
|
||
payload: Array(slice[4..<12])))
|
||
index += 13
|
||
lastConsumed = index
|
||
}
|
||
// Angefangene Rahmen aufheben – BLE liefert Antworten oft gestückelt.
|
||
let keepFrom = max(lastConsumed, max(0, buffer.count - 64))
|
||
return (frames, Array(buffer[keepFrom...]))
|
||
}
|
||
|
||
// MARK: - Modbus (D2)
|
||
|
||
/// Ein Lesekommando über alle interessanten Register.
|
||
static func modbusReadFrame(start: UInt16 = 0, count: UInt16 = 62) -> Data {
|
||
var frame: [UInt8] = [0xD2, 0x03,
|
||
UInt8(start >> 8), UInt8(start & 0xFF),
|
||
UInt8(count >> 8), UInt8(count & 0xFF)]
|
||
let crc = crc16Modbus(frame)
|
||
frame.append(UInt8(crc & 0xFF))
|
||
frame.append(UInt8(crc >> 8))
|
||
return Data(frame)
|
||
}
|
||
|
||
static func crc16Modbus(_ bytes: [UInt8]) -> UInt16 {
|
||
var crc: UInt16 = 0xFFFF
|
||
for byte in bytes {
|
||
crc ^= UInt16(byte)
|
||
for _ in 0..<8 {
|
||
if crc & 1 != 0 {
|
||
crc = (crc >> 1) ^ 0xA001
|
||
} else {
|
||
crc >>= 1
|
||
}
|
||
}
|
||
}
|
||
return crc
|
||
}
|
||
|
||
/// Prüft einen vollständigen Modbus-Antwortrahmen und liefert die
|
||
/// Registerwerte. Gibt nil zurück, solange der Rahmen unvollständig ist.
|
||
static func parseModbusResponse(_ buffer: [UInt8]) -> [UInt16]? {
|
||
guard buffer.count >= 5, buffer[0] == 0xD2, buffer[1] == 0x03 else { return nil }
|
||
let byteCount = Int(buffer[2])
|
||
let total = 3 + byteCount + 2
|
||
guard buffer.count >= total else { return nil }
|
||
|
||
let body = Array(buffer[0..<(3 + byteCount)])
|
||
let expected = crc16Modbus(body)
|
||
let actual = UInt16(buffer[3 + byteCount]) | (UInt16(buffer[4 + byteCount]) << 8)
|
||
guard expected == actual else { return nil }
|
||
|
||
var registers: [UInt16] = []
|
||
registers.reserveCapacity(byteCount / 2)
|
||
var i = 3
|
||
while i + 1 < 3 + byteCount {
|
||
registers.append(UInt16(body[i]) << 8 | UInt16(body[i + 1]))
|
||
i += 2
|
||
}
|
||
return registers
|
||
}
|
||
}
|