init
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user