diff --git a/csharp/App/DeligreenBatteryCommunication/DeligreenBatteryCommunication.csproj b/csharp/App/DeligreenBatteryCommunication/DeligreenBatteryCommunication.csproj new file mode 100644 index 000000000..6ab0149e5 --- /dev/null +++ b/csharp/App/DeligreenBatteryCommunication/DeligreenBatteryCommunication.csproj @@ -0,0 +1,19 @@ + + + + InnovEnergy.App.DeligreenBatteryCommunication + + + + + + + + + + + + + + + diff --git a/csharp/App/DeligreenBatteryCommunication/Program.cs b/csharp/App/DeligreenBatteryCommunication/Program.cs new file mode 100644 index 000000000..9d73e76e2 --- /dev/null +++ b/csharp/App/DeligreenBatteryCommunication/Program.cs @@ -0,0 +1,46 @@ +using InnovEnergy.Lib.Devices.BatteryDeligreen; + +internal static class Program +{ + private static readonly TimeSpan UpdateInterval = TimeSpan.FromSeconds(2); + + // private static readonly Channel? BatteriesChannel; + + private const String Port = "/dev/ttyUSB0"; + + + static Program() + { + Console.WriteLine("Hello, Deligreen World!"); + + // BatteriesChannel = new SerialPortChannel(Port, BaudRate, Parity, DataBits, StopBits); + + } + + public static async Task Main(string[] args) + { + Console.WriteLine("Starting Battery Communication"); + + var batteryDevices = new BatteryDeligreenDevice(Port); + + while (true) + { + try + { + Console.WriteLine("***************************** New Frame *********************************"); + Console.WriteLine($"First Reading Timestamp: {DateTime.Now:HH:mm:ss.fff}"); + // Read telemetry data asynchronously + await batteryDevices.ReadTelemetryData(); + Console.WriteLine($"Last Timestamp: {DateTime.Now:HH:mm:ss.fff}"); + // Wait for 2 seconds before the next reading + await Task.Delay(2000); // Delay in milliseconds (2000ms = 2 seconds) + } + catch (Exception e) + { + // Handle exception and print the error + Console.WriteLine(e); + await Task.Delay(2000); // Delay in milliseconds (2000ms = 2 seconds) + } + } + } +} \ No newline at end of file diff --git a/csharp/App/DeligreenBatteryCommunication/deploy.sh b/csharp/App/DeligreenBatteryCommunication/deploy.sh new file mode 100755 index 000000000..05983dda8 --- /dev/null +++ b/csharp/App/DeligreenBatteryCommunication/deploy.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +dotnet_version='net6.0' +salimax_ip="$1" +username='ie-entwicklung' +root_password='Salimax4x25' +set -e + +echo -e "\n============================ Build ============================\n" + +dotnet publish \ + ./DeligreenBatteryCommunication.csproj \ + -p:PublishTrimmed=false \ + -c Release \ + -r linux-x64 + +echo -e "\n============================ Deploy ============================\n" + +rsync -v \ + --exclude '*.pdb' \ + ./bin/Release/$dotnet_version/linux-x64/publish/* \ + $username@"$salimax_ip":~/salimax \ No newline at end of file diff --git a/csharp/InnovEnergy.sln b/csharp/InnovEnergy.sln index baaec091f..a334ae2c0 100644 --- a/csharp/InnovEnergy.sln +++ b/csharp/InnovEnergy.sln @@ -93,6 +93,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SchneiderMeterDriver", "App EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Battery250UP", "Lib\Devices\Battery250UP\Battery250UP.csproj", "{F2967439-A590-4D5E-9208-1B973C83AA1C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BatteryDeligreen", "Lib\Devices\BatteryDeligreen\BatteryDeligreen.csproj", "{1045AC74-D4D8-4581-AAE3-575DF26060E6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeligreenBatteryCommunication", "App\DeligreenBatteryCommunication\DeligreenBatteryCommunication.csproj", "{11ED6871-5B7D-462F-8710-B5D85DEC464A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -244,6 +248,14 @@ Global {F2967439-A590-4D5E-9208-1B973C83AA1C}.Debug|Any CPU.Build.0 = Debug|Any CPU {F2967439-A590-4D5E-9208-1B973C83AA1C}.Release|Any CPU.ActiveCfg = Release|Any CPU {F2967439-A590-4D5E-9208-1B973C83AA1C}.Release|Any CPU.Build.0 = Release|Any CPU + {1045AC74-D4D8-4581-AAE3-575DF26060E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1045AC74-D4D8-4581-AAE3-575DF26060E6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1045AC74-D4D8-4581-AAE3-575DF26060E6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1045AC74-D4D8-4581-AAE3-575DF26060E6}.Release|Any CPU.Build.0 = Release|Any CPU + {11ED6871-5B7D-462F-8710-B5D85DEC464A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {11ED6871-5B7D-462F-8710-B5D85DEC464A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11ED6871-5B7D-462F-8710-B5D85DEC464A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {11ED6871-5B7D-462F-8710-B5D85DEC464A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {CF4834CB-91B7-4172-AC13-ECDA8613CD17} = {145597B4-3E30-45E6-9F72-4DD43194539A} @@ -286,5 +298,7 @@ Global {2C7F3D89-402B-43CB-988E-8D2D853BEF44} = {4931A385-24DC-4E78-BFF4-356F8D6D5183} {2E7E7657-3A53-4B62-8927-FE9A082B81DE} = {145597B4-3E30-45E6-9F72-4DD43194539A} {F2967439-A590-4D5E-9208-1B973C83AA1C} = {4931A385-24DC-4E78-BFF4-356F8D6D5183} + {1045AC74-D4D8-4581-AAE3-575DF26060E6} = {4931A385-24DC-4E78-BFF4-356F8D6D5183} + {11ED6871-5B7D-462F-8710-B5D85DEC464A} = {145597B4-3E30-45E6-9F72-4DD43194539A} EndGlobalSection EndGlobal diff --git a/csharp/Lib/Devices/Battery48TL/Battery48TlRecord.Modbus.cs b/csharp/Lib/Devices/Battery48TL/Battery48TlRecord.Modbus.cs index 5312ba815..b4c2c8aa8 100644 --- a/csharp/Lib/Devices/Battery48TL/Battery48TlRecord.Modbus.cs +++ b/csharp/Lib/Devices/Battery48TL/Battery48TlRecord.Modbus.cs @@ -48,10 +48,6 @@ public partial class Battery48TlRecord private LedState ParseLed(LedColor led) => (LedState)((_LedStates >> (Int32)led) & 3); - // public Decimal CellsVoltage { get; init; } - // - // public Decimal MaxChargingPower { get; init; } - // public Decimal MaxDischargingPower { get; init; } } diff --git a/csharp/Lib/Devices/BatteryDeligreen/AlarmMessage.cs b/csharp/Lib/Devices/BatteryDeligreen/AlarmMessage.cs new file mode 100644 index 000000000..74fca0bc2 --- /dev/null +++ b/csharp/Lib/Devices/BatteryDeligreen/AlarmMessage.cs @@ -0,0 +1,30 @@ +namespace InnovEnergy.Lib.Devices.BatteryDeligreen; + +public class AlarmMessage +{ + // Enum for Alarm Event 1 + public enum AlarmEvent1 + { + VoltageSensorFault, + TemperatureSensorFault, + CurrentSensorFault, + KeySwitchFault, + CellVoltageDropoutFault, + ChargeSwitchFault, + DischargeSwitchFault, + CurrentLimitSwitchFault + } + + // Enum for Alarm Event 2 + public enum AlarmEvent2 + { + MonomerHighVoltageAlarm, + MonomerOvervoltageProtection, + MonomerLowVoltageAlarm, + MonomerUnderVoltageProtection, + HighVoltageAlarmForTotalVoltage, + OvervoltageProtectionForTotalVoltage, + LowVoltageAlarmForTotalVoltage, + UnderVoltageProtectionForTotalVoltage + } +} diff --git a/csharp/Lib/Devices/BatteryDeligreen/BatteryDeligreen.csproj b/csharp/Lib/Devices/BatteryDeligreen/BatteryDeligreen.csproj new file mode 100644 index 000000000..03f0a38e1 --- /dev/null +++ b/csharp/Lib/Devices/BatteryDeligreen/BatteryDeligreen.csproj @@ -0,0 +1,21 @@ + + + + + + InnovEnergy.Lib.Devices.BatteryDeligreen + + + + + + + + + + + + + + + diff --git a/csharp/Lib/Devices/BatteryDeligreen/BatteryDeligreenDevice.cs b/csharp/Lib/Devices/BatteryDeligreen/BatteryDeligreenDevice.cs new file mode 100644 index 000000000..2b590d796 --- /dev/null +++ b/csharp/Lib/Devices/BatteryDeligreen/BatteryDeligreenDevice.cs @@ -0,0 +1,178 @@ +namespace InnovEnergy.Lib.Devices.BatteryDeligreen; + +using System; +using System.IO.Ports; + + +public class BatteryDeligreenDevice +{ + private const Parity Parity = System.IO.Ports.Parity.None; + private const StopBits StopBits = System.IO.Ports.StopBits.One; + private const Int32 BaudRate = 19200; + private const Int32 DataBits = 8; + + private readonly SerialPort _serialPort; + + // Constructor for local serial port connection + public BatteryDeligreenDevice(String tty) + { + _serialPort = new SerialPort(tty, BaudRate, Parity, DataBits, StopBits) + { + ReadTimeout = 1000, // 1 second timeout for reads + WriteTimeout = 1000 // 1 second timeout for writes + }; + + try + { + // Open the serial port + _serialPort.Open(); + Console.WriteLine("Serial port opened successfully."); + } + catch (Exception e) + { + Console.WriteLine(e); + } + + } + + // Method to send data to the device + private void Write(String hexCommand) + { + try + { + // Convert hex string to byte array + byte[] commandBytes = HexStringToByteArray(hexCommand); + + // Send the command + _serialPort.Write(commandBytes, 0, commandBytes.Length); + Console.WriteLine("Command sent successfully."); + } + catch (TimeoutException) + { + Console.WriteLine("Write operation timed out."); + } + catch (Exception e) + { + Console.WriteLine($"Error during write operation: {e.Message}"); + throw; + } + } + + // Method to read data from the device + private Byte[] Read(Int32 bufferSize) + { + try + { + // Read data from the serial port + var buffer = new Byte[bufferSize]; + var bytesRead = _serialPort.Read(buffer, 0, bufferSize); + + Console.WriteLine($"Read {bytesRead} bytes from the device."); + + // Return only the received bytes + var responseData = new Byte[bytesRead]; + Array.Copy(buffer, responseData, bytesRead); + return responseData; + } + catch (TimeoutException) + { + Console.WriteLine("Read operation timed out."); + return Array.Empty(); // Return empty array on timeout + } + catch (Exception e) + { + Console.WriteLine($"Error during read operation: {e.Message}"); + throw; + } + } + + private static String BytesToHexString(byte[] byteArray) + { + return BitConverter.ToString(byteArray).Replace("-", "").ToUpper(); + } + + // Helper method to convert a hex string to a byte array + private static Byte[] HexStringToByteArray(string hex) + { + if (string.IsNullOrWhiteSpace(hex) || hex.Length % 2 != 0) + throw new ArgumentException("Invalid hex string."); + + byte[] bytes = new byte[hex.Length / 2]; + for (var i = 0; i < hex.Length; i += 2) + { + bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16); + } + return bytes; + } + + // Dispose method to release the serial port + public void Dispose() + { + if (_serialPort.IsOpen) + { + _serialPort.Close(); + Console.WriteLine("Serial port closed."); + } + } + + // Read telemetry data from the connected device + public async Task ReadTelemetryData() + { + const String frameToSend = "7E3230303034363432453030323030464433370D"; // Example custom frame + + try + { + // Write the frame to the channel (send it to the device) + await Task.Run(() => Write(frameToSend)); + + // Read the response from the channel (assuming max response size) + var responseBytes = await Task.Run(() => Read(1024)); // Assuming Read can be executed asynchronously + + // Convert the byte array to a hexadecimal string + var responseHex = BytesToHexString(responseBytes); + + new TelemetryFrameParser().ParsingTelemetryFrame(responseHex); + + // Parse the ASCII response (you can implement any custom parsing logic) + var responseData = ParseAsciiResponse(responseBytes.ToArray()); + + return responseData; + } + catch (Exception ex) + { + Console.WriteLine($"Error during telemetry data retrieval: {ex.Message}"); + throw; + } + } + + public Byte[] ReadTelecomandData() + { + const String frameToSend = "7E3230303034363434453030323030464433350D"; // Example custom frame + + // Write the frame to the channel (send it to the device) + Write(frameToSend); + + // Read the response from the channel (assuming max response size) + var responseBytes = Read(1024); // Adjust this size if needed + + // Parse the ASCII response (you can implement any custom parsing logic) + var responseData = ParseAsciiResponse(responseBytes.ToArray()); + + return responseData; + } + + // Helper method to parse the ASCII response (you can add any parsing logic here) + private static byte[] ParseAsciiResponse(byte[] responseBytes) + { + Console.WriteLine($"Last Timestamp: {DateTime.Now:HH:mm:ss.fff}"); + // Convert the byte array to a hex string for display + var hexResponse = BitConverter.ToString(responseBytes).Replace("-", " "); + //Console.WriteLine($"Response (Hex): {hexResponse}"); + // Implement custom parsing logic if necessary based on the protocol's frame + // For now, we return the raw response bytes + return responseBytes; + } +} + + + diff --git a/csharp/Lib/Devices/BatteryDeligreen/Doc/BMS communication home energy storage protocol V2.0.pdf b/csharp/Lib/Devices/BatteryDeligreen/Doc/BMS communication home energy storage protocol V2.0.pdf new file mode 100644 index 000000000..4f88b4a16 Binary files /dev/null and b/csharp/Lib/Devices/BatteryDeligreen/Doc/BMS communication home energy storage protocol V2.0.pdf differ diff --git a/csharp/Lib/Devices/BatteryDeligreen/Doc/retrieve Alarm.py b/csharp/Lib/Devices/BatteryDeligreen/Doc/retrieve Alarm.py new file mode 100644 index 000000000..8c60b65ca --- /dev/null +++ b/csharp/Lib/Devices/BatteryDeligreen/Doc/retrieve Alarm.py @@ -0,0 +1,132 @@ +import serial + +def parse_start_code(frame): + soi = frame[0:2] + if soi == "~": + return "ok!" + else: + raise ValueError(f"Invalid start identifier! ({soi})") + +def parse_version_code(frame): + ver = frame[2:6] + return f"Protocol Version V{ver[0]}.{ver[1]}" + +def parse_address_code(frame): + adr = frame[6:10] + if 0 <= int(adr) <= 15: + return adr + else: + raise ValueError(f"Invalid address: {adr} (out of range 0-15)") + +def parse_device_code(frame): + cid1 = frame[10:14] + return bms.CID1_DEVICE_CODES.get(cid1, "Unknown!") + +def parse_function_code(frame): + cid2 = frame[14:18] + if cid2 in bms.CID2_COMMAND_CODES: + return f"Command -> {bms.CID2_COMMAND_CODES.get(cid2)}" + elif cid2 in bms.CID2_RETURN_CODES: + return f"Return -> {bms.CID2_RETURN_CODES.get(cid2)}" + else: + return f"Unknown CID2: {cid2}" + +def parse_lchksum(length_code): + # implements chapter 3.2.2 of the Protocol Specification + lchksum = int(length_code[0], 16) + # Compute lchksum + d11d10d09d08 = int(length_code[1]) + d07d06d05d04 = int(length_code[2]) + d03d0ld01d00 = int(length_code[3]) + sum = d11d10d09d08 + d07d06d05d04 + d03d0ld01d00 + remainder = sum % 16 + inverted = ~remainder & 0xF + computed_lchksum = (inverted + 1) & 0xF + if computed_lchksum == lchksum: + return "ok!" + else: + raise ValueError(f"Invalid LCHKSUM: {lchksum} (computed: {computed_lchksum})") + + +def parse_lenid(length_code): + # implements chapter 3.2.1 of the Protocol Specification + d11d10d09d08 = int(length_code[1]) + d07d06d05d04 = int(length_code[2]) + d03d0ld01d00 = int(length_code[3]) + lenid = d11d10d09d08 << 8 | d07d06d05d04 << 4 | d03d0ld01d00 + return lenid>>1 + +def parse_length_code(frame): + # implements chapter 3.2 of the Protocol Specification + length_code = frame[18:26] + lchksum = parse_lchksum(length_code) + lenid = parse_lenid(length_code) + return { "LCHKSUM": lchksum, "LENID": lenid } + +def parse_info(frame): + cid2 = frame[14:18] + lenid = parse_lenid(frame[18:26]) + info = frame[26:26+lenid*2] + + if cid2 == '00' and lenid == 49: + return parse_telecommand_return(info) + elif cid2 == '00' and lenid == 75: + return parse_telemetry_return(info) + else: + return info + +def parse_telecommand_return(info_raw, info={}, index=0): + + info["DATA FLAG"] = info_raw[index:index+4] + index += 4 + + info["COMMAND GROUP"] = info_raw[index:index+4] + index += 4 + + +def parse_modbus_ascii_frame(frame, parsed_data = {}): + frame = bytes.fromhex(frame).decode('ascii') + parsed_data["SOI"] = parse_start_code(frame) + parsed_data["VER"] = parse_version_code(frame) + parsed_data["ADR"] = parse_address_code(frame) + parsed_data["CID1"] = parse_device_code(frame) + parsed_data["CID2"] = parse_function_code(frame) + parsed_data["LENGTH"] = parse_length_code(frame) + parsed_data["INFO"] = parse_info(frame) + parsed_data["CHKSUM"] = parse_checksum(frame) + parsed_data["EOI"] = parse_end_code(frame) + return parsed_data + +def send_command(): + + # Define the serial port and baud rate + port = 'COM9' # Replace with your actual port + baudrate = 19200 # Replace with the correct baud rate for your BMS + + # Create the serial connection + try: + with serial.Serial(port, baudrate, timeout=1) as ser: + # Convert the hex string to bytes + command = bytes.fromhex("7E3230303034363434453030323030464433350D") + + # Send the command + ser.write(command) + print("Command sent successfully.") + + # Wait for and read the response + response = ser.read(200) # Adjust the number of bytes to read as needed + if response: + hex_response = response.hex() + print("Response received:", hex_response) + # Process the response to check details + check_starting_byte_and_extract_details(hex_response) + else: + print("No response received.") + except serial.SerialException as e: + print(f"Error opening serial port: {e}") + except Exception as e: + print(f"An unexpected error occurred: {e}") + + +if __name__ == "__main__": + send_command() diff --git a/csharp/Lib/Devices/BatteryDeligreen/Doc/retrieve data.py b/csharp/Lib/Devices/BatteryDeligreen/Doc/retrieve data.py new file mode 100644 index 000000000..617e6f724 --- /dev/null +++ b/csharp/Lib/Devices/BatteryDeligreen/Doc/retrieve data.py @@ -0,0 +1,480 @@ +import serial +import csv + +TELECOMMAND_FILE_PATH = "Telecommand_Return_Record.csv" + +# Table 3 +CID1_DEVICE_CODES = { + "46": "Lithium iron phosphate battery BMS", +} + +# Table 4 +CID2_COMMAND_CODES = { + "42": "Acquisition of telemetering information", + "44": "Acquisition of telecommand information", + "45": "Telecontrol command", + "47": "Acquisition of teleregulation information", + "49": "Setting of teleregulation information", + "4F": "Acquisition of the communication protocol version number", + "51": "Acquisition of device vendor information", + "4B": "Acquisition of historical data", + "4D": "Acquisition time", + "4E": "Synchronization time", + "A0": "Production calibration", + "A1": "Production setting", + "A2": "Regular recording" +} + +# Table 5 +CID2_RETURN_CODES = { + "00": "Normal", + "01": "VER error", + "02": "CHKSUM error", + "03": "LCHKSUM error", + "04": "CID2 invalid", + "05": "Command format error", + "06": "Data invalid (parameter setting)", + "07": "No data (history)", + "E1": "CID1 invalid", + "E2": "Command execution failure", + "E3": "Device fault", + "E4": "Invalid permissions" + } + + +# Table 12 +BYTE_ALARM_CODES = { + "00": "Normal, no alarm", + "01": "Alarm that analog quantity reaches the lower limit", + "02": "Alarm that analog quantity reaches the upper limit", + "F0": "Other alarms" +} + +# Table 13 +BIT_ALARM_CODES = { + "Alarm event 1": ( + "Voltage sensor fault", + "Temperature sensor fault", + "Current sensor fault", + "Key switch fault", + "Cell voltage dropout fault", + "Charge switch fault", + "Discharge switch fault", + "Current limit switch fault" + ), + "Alarm event 2": ( + "Monomer high voltage alarm", + "Monomer overvoltage protection", + "Monomer low voltage alarm", + "Monomer under voltage protection", + "High voltage alarm for total voltage", + "Overvoltage protection for total voltage", + "Low voltage alarm for total voltage", + "Under voltage protection for total voltage" + ), + "Alarm event 3": ( + "Charge high temperature alarm", + "Charge over temperature protection", + "Charge low temperature alarm", + "Charge under temperature protection", + "Discharge high temperature alarm", + "Discharge over temperature protection", + "Discharge low temperature alarm", + "Discharge under temperature protection" + ), + "Alarm event 4": ( + "Environment high temperature alarm", + "Environment over temperature protection", + "Environment low temperature alarm", + "Environment under temperature protection", + "Power over temperature protection", + "Power high temperature alarm", + "Cell low temperature heating", + "Reservation bit" + ), + "Alarm event 5": ( + "Charge over current alarm", + "Charge over current protection", + "Discharge over current alarm", + "Discharge over current protection", + "Transient over current protection", + "Output short circuit protection", + "Transient over current lockout", + "Output short circuit lockout" + ), + "Alarm event 6": ( + "Charge high voltage protection", + "Intermittent recharge waiting", + "Residual capacity alarm", + "Residual capacity protection", + "Cell low voltage charging prohibition", + "Output reverse polarity protection", + "Output connection fault", + "Inside bit" + ), + "On-off state": ( + "Discharge switch state", + "Charge switch state", + "Current limit switch state", + "Heating switch state", + "Reservation bit", + "Reservation bit", + "Reservation bit", + "Reservation bit" + ), + "Equilibrium state 1": ( + "Cell 01 equilibrium", + "Cell 02 equilibrium", + "Cell 03 equilibrium", + "Cell 04 equilibrium", + "Cell 05 equilibrium", + "Cell 06 equilibrium", + "Cell 07 equilibrium", + "Cell 08 equilibrium" + ), + "Equilibrium state 2": ( + "Cell 09 equilibrium", + "Cell 10 equilibrium", + "Cell 11 equilibrium", + "Cell 12 equilibrium", + "Cell 13 equilibrium", + "Cell 14 equilibrium", + "Cell 15 equilibrium", + "Cell 16 equilibrium" + ), + "System state": ( + "Discharge", + "Charge", + "Floating charge", + "Reservation bit", + "Standby", + "Shutdown", + "Reservation bit", + "Reservation bit" + ), + "Disconnection state 1": ( + "Cell 01 disconnection", + "Cell 02 disconnection", + "Cell 03 disconnection", + "Cell 04 disconnection", + "Cell 05 disconnection", + "Cell 06 disconnection", + "Cell 07 disconnection", + "Cell 08 disconnection" + ), + "Disconnection state 2": ( + "Cell 09 disconnection", + "Cell 10 disconnection", + "Cell 11 disconnection", + "Cell 12 disconnection", + "Cell 13 disconnection", + "Cell 14 disconnection", + "Cell 15 disconnection", + "Cell 16 disconnection" + ), + "Alarm event 7": ( + "Inside bit", + "Inside bit", + "Inside bit", + "Inside bit", + "Automatic charging waiting", + "Manual charging waiting", + "Inside bit", + "Inside bit" + ), + "Alarm event 3": ( + "EEP storage fault", + "RTC error", + "Voltage calibration not performed", + "Current calibration not performed", + "Zero calibration not performed", + "Inside bit", + "Inside bit", + "Inside bit" + ), +} + + +def parse_start_code(frame): + soi = frame[0:1] + if soi == "~": + return "ok!" + else: + raise ValueError(f"Invalid start identifier! ({soi})") + +def parse_version_code(frame): + ver = frame[1:3] + return f"Protocol Version V{ver[0]}.{ver[1]}" + +def parse_address_code(frame): + adr = frame[3:5] + if 0 <= int(adr) <= 15: + return adr + else: + raise ValueError(f"Invalid address: {adr} (out of range 0-15)") + +def parse_device_code(frame): + cid1 = frame[5:7] + return CID1_DEVICE_CODES.get(cid1, "Unknown!") + +def parse_function_code(frame): + cid2 = frame[7:9] + if cid2 in CID2_COMMAND_CODES: + return f"Command -> {CID2_COMMAND_CODES.get(cid2)}" + elif cid2 in CID2_RETURN_CODES: + return f"Return -> {CID2_RETURN_CODES.get(cid2)}" + else: + return f"Unknown CID2: {cid2}" + +def parse_lchksum(length_code): + # implements chapter 3.2.2 of the Protocol Specification + lchksum = int(length_code[0], 16) + # Compute lchksum + d11d10d09d08 = int(length_code[1]) + d07d06d05d04 = int(length_code[2]) + d03d0ld01d00 = int(length_code[3]) + sum = d11d10d09d08 + d07d06d05d04 + d03d0ld01d00 + remainder = sum % 16 + inverted = ~remainder & 0xF + computed_lchksum = (inverted + 1) & 0xF + if computed_lchksum == lchksum: + return "ok!" + else: + raise ValueError(f"Invalid LCHKSUM: {lchksum} (computed: {computed_lchksum})") + +def parse_lenid(length_code): + # implements chapter 3.2.1 of the Protocol Specification + d11d10d09d08 = int(length_code[1]) + d07d06d05d04 = int(length_code[2]) + d03d0ld01d00 = int(length_code[3]) + lenid = d11d10d09d08 << 8 | d07d06d05d04 << 4 | d03d0ld01d00 + return lenid >> 1 + +def parse_length_code(frame): + # implements chapter 3.2 of the Protocol Specification + length_code = frame[9:13] + lchksum = parse_lchksum(length_code) + lenid = parse_lenid(length_code) + return { "LCHKSUM": lchksum, "LENID": lenid } + +def parse_info(frame): + cid2 = frame[7:9] + lenid = parse_lenid(frame[9:13]) + info = frame[13:13+lenid*2] + + if cid2 == '00' and lenid == 49: + return parse_telecommand_return(info) + elif cid2 == '00' and lenid == 75: + return parse_telemetry_return(info) + else: + return info + +def parse_telecommand_return(info_raw, info={}, index=0): + + info["DATA FLAG"] = info_raw[index:index+2] + index += 2 + + info["COMMAND GROUP"] = info_raw[index:index+2] + index += 2 + + num_of_cells = int(info_raw[index:index+2], 16) + info["Number of cells"] = num_of_cells + index += 2 + + for cell in range(info["Number of cells"]): + alarm = BYTE_ALARM_CODES.get(info_raw[index:index+2]) + info[f"Cell {cell +1} alarm"] = alarm + index += 2 + + num_of_temperatures = int(info_raw[index:index+2], 16) + info["Number of temperatures"] = num_of_temperatures + index += 2 + + for sensor in range(4): + alarm = BYTE_ALARM_CODES.get(info_raw[index:index+2]) + info[f"Cell temperature alarm {sensor}"] = alarm + index += 2 + + alarm = BYTE_ALARM_CODES.get(info_raw[index:index+2]) + info["Environment temperature alarm"] = alarm + index += 2 + + alarm = BYTE_ALARM_CODES.get(info_raw[index:index+2]) + info["Power temperature alarm 1"] = alarm + index += 2 + + alarm = BYTE_ALARM_CODES.get(info_raw[index:index+2]) + info["Charge/discharge current alarm"] = alarm + index += 2 + + alarm = BYTE_ALARM_CODES.get(info_raw[index:index+2]) + info["Total battery voltage alarm"] = alarm + index += 2 + + num_custom = int(info_raw[index:index+2], 16) + info["Number of custom alarms"] = num_custom + index += 2 + + alarm = info_raw[index:index+2] + info["Alarm event 1"] = alarm + index += 2 + + alarm = info_raw[index:index+2] + info["Alarm event 2"] = alarm + index += 2 + + alarm = info_raw[index:index+2] + info["Alarm event 3"] = alarm + index += 2 + + alarm = info_raw[index:index+2] + info["Alarm event 4"] = alarm + index += 2 + + alarm = info_raw[index:index+2] + info["Alarm event 5"] = alarm + index += 2 + + alarm = info_raw[index:index+2] + info["Alarm event 6"] = alarm + index += 2 + + alarm = info_raw[index:index+2] + info["On-off state"] = alarm + index += 2 + + alarm = info_raw[index:index+2] + info["Equilibrium state 1"] = alarm + index += 2 + + alarm = info_raw[index:index+2] + info["Equilibrium state 2"] = alarm + index += 2 + + alarm = info_raw[index:index+2] + info["System state"] = alarm + index += 2 + + alarm = info_raw[index:index+2] + info["Disconnection state 1"] = alarm + index += 2 + + alarm = info_raw[index:index+2] + info["Disconnection state 2"] = alarm + index += 2 + + alarm = info_raw[index:index+2] + info["Alarm event 7"] = alarm + index += 2 + + alarm = info_raw[index:index+2] + info["Alarm event 8"] = alarm + index += 2 + + alarm = info_raw[index:index+2] + info["Reservation extention 1"] = alarm + index += 2 + + alarm = info_raw[index:index+2] + info["Reservation extention 2"] = alarm + index += 2 + + alarm = info_raw[index:index+2] + info["Reservation extention 3"] = alarm + index += 2 + + alarm = info_raw[index:index+2] + info["Reservation extention 4"] = alarm + index += 2 + + alarm = info_raw[index:index+2] + info["Reservation extention 5"] = alarm + index += 2 + + alarm = info_raw[index:index+2] + info["Reservation extention 6"] = alarm + index += 2 + + save_dict_to_csv(TELECOMMAND_FILE_PATH, info) + return f"Telecommand Return Data saved in ./{TELECOMMAND_FILE_PATH}" + + +def save_dict_to_csv(file_path, data): + with open(file_path, mode='a+', newline='') as csvfile: + csvfile.seek(0) + has_header = csvfile.read(1) != "" + csvfile.seek(0, 2) + writer = csv.DictWriter(csvfile, fieldnames=data.keys()) + if not has_header: + writer.writeheader() + writer.writerow(data) + +def parse_checksum(frame): + """implements section 3.3 of the Protocol Specification""" + chksum = int(frame[-6:-1], 16) + data = frame[1:-5] + # Compute chksum + ascii_sum = sum(ord(char) for char in data) + remainder = ascii_sum % 65536 + inverted = ~remainder & 0xFFFF + computed_chksum = (inverted + 1) & 0xFFFF + # Compare with CHKSUM in frame + if computed_chksum == chksum: + return "ok!" + else: + raise ValueError(f"Invalid CHKSUM: {chksum} (computed: {computed_chksum})") + +def parse_end_code(frame): + eoi = frame[-1] + if eoi == "\r": + return "ok!" + else: + raise ValueError(f"Invalid end identifier! ({eoi})") + +def parse_modbus_ascii_frame(frame, parsed_data = {}): + frame = bytes.fromhex(frame).decode('ascii') + parsed_data["SOI"] = parse_start_code(frame) + parsed_data["VER"] = parse_version_code(frame) + parsed_data["ADR"] = parse_address_code(frame) + parsed_data["CID1"] = parse_device_code(frame) + parsed_data["CID2"] = parse_function_code(frame) + parsed_data["LENGTH"] = parse_length_code(frame) + parsed_data["INFO"] = parse_info(frame) + parsed_data["CHKSUM"] = parse_checksum(frame) + parsed_data["EOI"] = parse_end_code(frame) + return parsed_data + +def send_command(): + + # Define the serial port and baud rate + port = 'COM9' # Replace with your actual port + baudrate = 19200 # Replace with the correct baud rate for your BMS + + # Create the serial connection + try: + with serial.Serial(port, baudrate, timeout=1) as ser: + # Convert the hex string to bytes + command = bytes.fromhex("7E3230303034363434453030323030464433350D") + + # Send the command + ser.write(command) + print("Command sent successfully.") + + # Wait for and read the response + response = ser.read(200) # Adjust the number of bytes to read as needed + if response: + hex_response = response.hex() + print("Response received:", hex_response) + # Process the response to check details + parsed_result = parse_modbus_ascii_frame(hex_response) + for key, value in parsed_result.items(): + print(f"{key}: {value}") + else: + print("No response received.") + except serial.SerialException as e: + print(f"Error opening serial port: {e}") + except Exception as e: + print(f"An unexpected error occurred: {e}") + +if __name__ == "__main__": + send_command() diff --git a/csharp/Lib/Devices/BatteryDeligreen/TelecommandFrameParser.cs b/csharp/Lib/Devices/BatteryDeligreen/TelecommandFrameParser.cs new file mode 100644 index 000000000..f50bf70ec --- /dev/null +++ b/csharp/Lib/Devices/BatteryDeligreen/TelecommandFrameParser.cs @@ -0,0 +1,6 @@ +namespace InnovEnergy.Lib.Devices.BatteryDeligreen; + +public class TelecommandFrameParser +{ + +} \ No newline at end of file diff --git a/csharp/Lib/Devices/BatteryDeligreen/TelemetryFrameParser.cs b/csharp/Lib/Devices/BatteryDeligreen/TelemetryFrameParser.cs new file mode 100644 index 000000000..8045c4dc8 --- /dev/null +++ b/csharp/Lib/Devices/BatteryDeligreen/TelemetryFrameParser.cs @@ -0,0 +1,160 @@ +using System.Globalization; + +namespace InnovEnergy.Lib.Devices.BatteryDeligreen; + +public class TelemetryFrameParser +{ + private static Int32 _currentIndex; + private const Int32 FrameLenght = 286; + + public void ParsingTelemetryFrame(String response) + { + + _currentIndex = 0; // Reset currentIndex to the start + + if (string.IsNullOrEmpty(response) || response.Length < FrameLenght) + { + Console.WriteLine("Response is too short to contain valid data."); + return; + } + + // Check starting byte + string startingByte = response.Substring(_currentIndex, 2).ToUpper(); + if (startingByte == "7E") + { + Console.WriteLine($"Starting byte: {startingByte} (Hex)"); + } + else + { + Console.WriteLine($"Incorrect starting byte: {startingByte}"); + return; + } + _currentIndex += 2; + + // Extract firmware version + var versionBytes = response.Substring(_currentIndex, 4); + try + { + String versionAscii = HexToAscii(versionBytes); + Console.WriteLine($"Firmware version: {versionBytes} (Hex), ASCII: {versionAscii}"); + } + catch (Exception) + { + Console.WriteLine($"Failed to decode firmware version from bytes: {versionBytes}"); + return; + } + _currentIndex += 4; + + // Extract and parse other fields + ParseAndPrintHexField(response, "Device Address", 4); + ParseAndPrintHexField(response, "Device Code (CID1)", 4); + ParseAndPrintHexField(response, "Function Code", 4); + ParseAndPrintHexField(response, "Length Code", 8); + ParseAndPrintHexField(response, "Data Flag", 4); + ParseAndPrintHexField(response, "Command Group", 4); + ParseAndPrintHexField(response, "Number of Cells", 4); + + // Process voltages for all 16 cells + for (var i = 0; i < 16; i++) + { + String cellVoltageBytes = response.Substring(_currentIndex, 8); + try + { + var cellVoltageAscii = HexToAscii(cellVoltageBytes); + var cellVoltageDecimal = HexToDecimal(cellVoltageAscii) / 1000.0; // cell voltage are divided 1000 + Console.WriteLine($"Voltage of Cell {i + 1}: {cellVoltageBytes} (Hex), ASCII: {cellVoltageAscii}, Voltage: {cellVoltageDecimal:F3} V"); + } + catch (Exception) + { + Console.WriteLine($"Failed to decode Voltage of Cell {i + 1} from bytes: {cellVoltageBytes}"); + } + _currentIndex += 8; + } + + // Parse other fields + ParseAndPrintHexField(response, "Number of Temperature Sensors", 4); + + // Parse cell temperatures + for (var i = 1; i <= 4; i++) + { + ParseAndPrintTemperatureField(response, $"Cell Temperature {i}"); + } + + // Parse other temperature and battery information + ParseAndPrintTemperatureField(response, "Environment Temperature"); + ParseAndPrintTemperatureField(response, "Power Temperature"); + ParseAndPrintField(response, "Charge/Discharge Current", 8, value => value / 100.0, "A"); + ParseAndPrintField(response, "Total Battery Voltage", 8, value => value / 100.0, "V"); + ParseAndPrintField(response, "Residual Capacity", 8, value => value / 100.0, "Ah"); + ParseAndPrintHexField(response, "Custom Number", 4); + ParseAndPrintField(response, "Battery Capacity", 8, value => value / 100.0, "Ah"); + ParseAndPrintField(response, "SOC", 8, value => value / 10.0, "%"); + ParseAndPrintField(response, "Rated Capacity", 8, value => value / 100.0, "Ah"); + ParseAndPrintHexField(response, "Number of Cycles", 8); + ParseAndPrintField(response, "SOH", 8, value => value / 10.0, "%"); + ParseAndPrintField(response, "Bus Voltage", 8, value => value / 100.0, "V"); + } + + private static void ParseAndPrintHexField(String response, String fieldName, int length) + { + var hexBytes = response.Substring(_currentIndex, length); + try + { + var asciiValue = HexToAscii(hexBytes); + var decimalValue = int.Parse(asciiValue, NumberStyles.HexNumber); + Console.WriteLine($"{fieldName}: {hexBytes} (Hex), ASCII: {asciiValue}, Decimal: {decimalValue}"); + } + catch (Exception) + { + Console.WriteLine($"Failed to decode {fieldName} from bytes: {hexBytes}"); + } + _currentIndex += length; + } + + private static void ParseAndPrintTemperatureField(String response, String fieldName) + { + var tempBytes = response.Substring(_currentIndex, 8); + try + { + var tempAscii = HexToAscii(tempBytes); + var tempDecimal = (HexToDecimal(tempAscii) - 2731) / 10.0; + Console.WriteLine($"{fieldName}: {tempBytes} (Hex), ASCII: {tempAscii}, Temperature: {tempDecimal:F2} °C"); + } + catch (Exception) + { + Console.WriteLine($"Failed to decode {fieldName} from bytes: {tempBytes}"); + } + _currentIndex += 8; + } + + private static void ParseAndPrintField(String response, String fieldName, Int32 length, Func conversion, String unit) + { + var fieldBytes = response.Substring(_currentIndex, length); + try + { + var fieldAscii = HexToAscii(fieldBytes); + var fieldDecimal = conversion(HexToDecimal(fieldAscii)); + Console.WriteLine($"{fieldName}: {fieldBytes} (Hex), ASCII: {fieldAscii}, {fieldName}: {fieldDecimal:F3} {unit}"); + } + catch (Exception) + { + Console.WriteLine($"Failed to decode {fieldName} from bytes: {fieldBytes}"); + } + _currentIndex += length; + } + + private static String HexToAscii(String hex) + { + var bytes = new Byte[hex.Length / 2]; + for (var i = 0; i < hex.Length; i += 2) + { + bytes[i / 2] = byte.Parse(hex.Substring(i, 2), NumberStyles.HexNumber); + } + return System.Text.Encoding.ASCII.GetString(bytes); + } + + private static double HexToDecimal(String hex) + { + return int.Parse(hex, NumberStyles.HexNumber); + } +} \ No newline at end of file