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 08 <8 Datenbytes> `. /// 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 `. /// /// 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 } }