This commit is contained in:
BiasF
2026-08-30 10:36:52 +02:00
commit fdcc644828
30 changed files with 4152 additions and 0 deletions
+123
View File
@@ -0,0 +1,123 @@
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
}
}