diff --git a/.vscode/launch.json b/.vscode/launch.json index a8c1260..4f33f50 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,15 +2,12 @@ "version": "0.2.0", "configurations": [ { - // Use IntelliSense to find out which attributes exist for C# debugging - // Use hover for the description of the existing attributes - // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md - "name": ".NET Core Launch (console)", + "name": "Grap from serial 1", "type": "coreclr", "request": "launch", "preLaunchTask": "build", // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceFolder}/bin/Debug/net6.0/RS485 Monitor.dll", + "program": "${workspaceFolder}/RS485 Monitor/bin/Debug/net8.0/RS485 Monitor.dll", "args": [""], "cwd": "${workspaceFolder}", // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console @@ -18,41 +15,20 @@ "stopAtEntry": false }, { - // Use IntelliSense to find out which attributes exist for C# debugging - // Use hover for the description of the existing attributes - // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md - "name": ".NET Core Launch with args", + "name": "Read from file", "type": "coreclr", "request": "launch", "preLaunchTask": "build", // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceFolder}/bin/Debug/net6.0/RS485 Monitor.dll", - "args": ["${workspaceFolder}\\..\\Traces\\raw_20230501_103841.bin", "-g"], + "program": "${workspaceFolder}/RS485 Monitor/bin/Debug/net8.0/RS485 Monitor.dll", + "args": [ + "${workspaceFolder}\\..\\Traces\\raw_20230501_103841.bin", + "-g" + ], "cwd": "${workspaceFolder}", // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console "console": "internalConsole", "stopAtEntry": false }, - { - // Use IntelliSense to find out which attributes exist for C# debugging - // Use hover for the description of the existing attributes - // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md - "name": ".NET Core Launch Helptext", - "type": "coreclr", - "request": "launch", - "preLaunchTask": "build", - // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceFolder}/bin/Debug/net6.0/RS485 Monitor.dll", - "args": ["--version"], - "cwd": "${workspaceFolder}", - // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console - "console": "internalConsole", - "stopAtEntry": false - }, - { - "name": ".NET Core Attach", - "type": "coreclr", - "request": "attach" - } ] -} \ No newline at end of file +} diff --git a/README.md b/README.md index 988702d..63516cd 100644 --- a/README.md +++ b/README.md @@ -307,10 +307,10 @@ Current fed into or taken from the battery. Values < 0 describe discharge of the | 6 | 3 | 96 | Ctrl | Ctrl (6,0) | | | 6 | 4 | 95 | Ctrl | Ctrl (6,6) | | | 6 | 5 | 94 | Battery | Disconnect | | -| 6 | 6 | 93 | Battery | Battery (8,1) | | -| 6 | 7 | 92 | Battery | Battery (8,0) | | +| 6 | 6 | 93 | Battery | Battery (8,1) | charge current to high | +| 6 | 7 | 92 | Battery | Battery (8,0) | charging stopped | | 7 | 0 | 91 | Battery | Battery[Temp] >= 3B(60°C) | | -| 7 | 1 | 90 | Battery | Battery (8,2) | | +| 7 | 1 | 90 | Battery | Battery (8,2) | discharge current too high | | 7 | 2 | 89 | Battery | Battery (8,5) | | | 7 | 3 | 88 | Battery | Battery (8,7) | | | 7 | 4 | 87 | X | | | diff --git a/RS485 Monitor/src/TelegramParser.cs b/RS485 Monitor/src/TelegramParser.cs index ac4ea5d..88de773 100644 --- a/RS485 Monitor/src/TelegramParser.cs +++ b/RS485 Monitor/src/TelegramParser.cs @@ -132,7 +132,7 @@ public void ParseChunk(byte[] rawData) /// /// Parse the given file /// - /// + /// public void ParseFile(string filePath) { FileInfo info = new(filePath); @@ -199,28 +199,23 @@ private void FinishBlock() return null; } - // Check if we can convert - if ( tg.Type == BaseTelegram.TelegramType.READ_RESPONSE) + // Define known telegrams + Dictionary knownTelegrams = new() { - if (tg.Source == 0xAA && tg.Destination == 0x5A && tg.PDU.Length == 10) - { - tg = new BatteryStatus(tg); - } - else if (tg.Source == 0xAA && tg.Destination == 0xDA && tg.PDU.Length == 10) - { - tg = new ECUStatus(tg); - } - } - - else if (tg.Type == BaseTelegram.TelegramType.READ_REQUEST) + {ControllerRequest.TELEGRAM_ID, typeof(ControllerRequest) }, + {ControllerResponse.TELEGRAM_ID, typeof(ControllerResponse) }, + {BatteryRequest.TELEGRAM_ID, typeof(BatteryRequest) }, + {BatteryResponse.TELEGRAM_ID, typeof(BatteryResponse) }, + {SpeedometerRequest.TELEGRAM_ID, typeof(SpeedometerRequest) }, + {SpeedometerResponse.TELEGRAM_ID, typeof(SpeedometerResponse) }, + }; + + // try to fetch the special telegram type + if (knownTelegrams.TryGetValue(tg.Id, out Type? specialType)) { - if (tg.Source == 0xBA && tg.Destination == 0xAA && tg.PDU.Length == GSMStatus.RAW_DATA_LEN) - { - tg = new GSMStatus(tg); - } + tg = (BaseTelegram?)Activator.CreateInstance(specialType, [tg]); } return tg; } - -} \ No newline at end of file +} diff --git a/RS485 Monitor/src/Telegrams/BaseTelegram.cs b/RS485 Monitor/src/Telegrams/BaseTelegram.cs index 6b5beb5..ddc3f4d 100644 --- a/RS485 Monitor/src/Telegrams/BaseTelegram.cs +++ b/RS485 Monitor/src/Telegrams/BaseTelegram.cs @@ -1,7 +1,7 @@ using NLog; /// -/// This class defines a basic telegram with data. No special parsing of the +/// This class defines a basic telegram with data. No special parsing of the /// content is done here. Check the specialized classes for more detail /// public class BaseTelegram : IEquatable @@ -11,86 +11,141 @@ public class BaseTelegram : IEquatable /// static readonly Logger log = LogManager.GetCurrentClassLogger(); - #region Properties + + #region Constants /// - /// Start Sequence of the telegram + /// Maximum supported data length /// - public UInt16 Start { get => (UInt16)((Raw[0] << 8) + Raw[1]); } + private const byte MAX_DATA_LEN = 32; /// - /// Source of the telegram + /// Minimum length of a telegram. This contains the following data: + /// - Type (2 bytes) + /// - Destination (1 byte) + /// - Source (1 byte) + /// - Data length (1 byte) + /// - Checksum (1 byte) + /// - End byte (1 byte) /// - public byte Source { get => Raw[POS_SRC]; } + private const byte MIN_DATA_LEN = 7; /// - /// Destination of the telegram + /// End byte of the telegram /// - public byte Destination { get => Raw[POS_DES]; } + private const byte END_TELEGRAM = 0x0D; /// - /// User Data + /// Offset of the high Byte of the the telegram type /// - public byte[] PDU { get; } + private const byte POS_TYPE_H = 0; + /// + /// Offset of the low Byte of the the telegram type + /// + private const byte POS_TYPE_L = 1; + /// + /// Position of the destination id in the raw data + /// + private const byte POS_DES = 2; + /// + /// Position of the source id in the raw data + /// + private const byte POS_SRC = 3; + /// + /// Position of the data length in the raw data + /// + private const byte POS_LEN = 4; + #endregion + #region Properties /// - /// Received checksum + /// Type of the telegram as raw value /// - public byte Checksum { get => Raw[POS_LEN + PDU.Length + 1]; } + public UInt16 RawType + { + get => (UInt16)((Raw[POS_TYPE_H] << 8) + Raw[POS_TYPE_L]); + } /// - /// Copy of the received raw data of the telegram + /// Destination / recipient of the telegram /// - public byte[] Raw { get; } + public byte Destination { get => Raw[POS_DES]; } /// - /// Are the raw data valid against the Checksum + /// Source / sender of the telegram /// - public bool Valid { get; } + public byte Source { get => Raw[POS_SRC]; } - public enum TelegramType + /// + /// Identifier for the telegram type. Is is a mixture of the telegram type, + /// the destination and the source. + /// + public UInt16 Id { - READ_REQUEST = 0xC55C, - READ_RESPONSE = 0xB66B + get => (UInt16)(Destination << 8 | Source); } + /// - /// Type of the telegram + /// User Data / specific to the telegram /// - public TelegramType Type { get => (TelegramType)Start; } - - #endregion + public byte[] PDU { get; } - #region Constants /// - /// Maximum supported data length + /// Received checksum /// - private const byte MAX_DATA_LEN = 32; + public byte Checksum { get => Raw[POS_LEN + PDU.Length + 1]; } /// - /// End byte of the telegram + /// Complete telegram as raw data /// - private const byte END_TELEGRAM = 0x0D; + public byte[] Raw { get; } /// - /// Position of the source id in the raw data + /// The raw PDU is valid against the received checksum /// - private const byte POS_SRC = 2; + public bool Valid + { + get + { + byte calcCheck = Raw[POS_LEN]; + foreach (byte b in PDU) + { + calcCheck ^= b; + } + + log.Trace($"Checksum: read={Checksum.ToString("X2")}, calc={calcCheck.ToString("X2")}"); + return calcCheck == Checksum; + } + } + /// - /// Position of the destination id in the raw data + /// Possible types of the telegram /// - private const byte POS_DES = 3; + public enum TelegramType + { + /// + /// Request telegram triggered by bus master to the unit + /// + REQUEST = 0xC55C, + /// + /// Telegram sent as a response to a request from the unit to the bus master + /// + RESPONSE = 0xB66B + } + /// - /// Position of the data length in the raw data + /// Type of the telegram /// - private const byte POS_LEN = 4; + public TelegramType Type { get => (TelegramType)RawType; } + #endregion /// - /// Internal constructor for specialized classes + /// Internal constructor for specialized classes. Create empty arrays /// protected BaseTelegram() { - PDU = new byte[1]; - Raw = new byte[1]; + PDU = Array.Empty(); + Raw = Array.Empty(); } /// @@ -104,56 +159,52 @@ protected BaseTelegram(BaseTelegram c) this.PDU = new byte[c.PDU.Length]; Array.Copy(c.PDU, this.PDU, this.PDU.Length); - - this.Valid = c.Valid; } /// /// Create a new base telegram based on the given raw data /// /// raw data of one telegram - /// Given data length in the raw data is invalid. + /// Raw data is null. + /// Raw data is too short. + /// Invalid data length in the raw data. + /// Raw data does not contain End tag. public BaseTelegram(byte[] rawData) { + // Basic validation + ArgumentNullException.ThrowIfNull(rawData); + if (rawData.Length < MIN_DATA_LEN) + { + throw new ArgumentException("Raw data is too short"); + } + // Copy raw data Raw = new byte[rawData.Length]; Array.Copy(rawData, Raw, Raw.Length); // fetch user data - byte dataLen = rawData[POS_LEN]; - if (dataLen > MAX_DATA_LEN) + byte pduLen = rawData[POS_LEN]; + if (pduLen > MAX_DATA_LEN) { - throw new ArgumentException($"Invalid data len {dataLen}. Max supported: {MAX_DATA_LEN}"); + throw new ArgumentException($"Invalid data len {pduLen}. Max supported: {MAX_DATA_LEN}"); } - PDU = new byte[dataLen]; + + // copy user data to PDU array + PDU = new byte[pduLen]; Array.Copy(rawData, POS_LEN + 1, PDU, 0, PDU.Length); - /* Verify checksum - * Checksum is calculated as XOR of the user data, including the length byte - */ - byte calcCheck = dataLen; - foreach (byte b in PDU) - { - calcCheck ^= b; - } - log.Trace($"Checksum: read={Checksum.ToString("X2")}, calc={calcCheck.ToString("X2")}"); - Valid = (calcCheck == Checksum); + // Verify checksum if (Valid == false) { - log.Warn($"Telegram has an invalid checksum read:{Checksum.ToString("X2")}, calc:{calcCheck.ToString("X2")}"); + log.Warn("Telegram has an invalid checksum"); } - // Check end telegram - try + // Check end telegram value + int pos_end_tag = MIN_DATA_LEN - 1 + pduLen; + if (rawData.Length < (pos_end_tag + 1) || rawData[pos_end_tag] != END_TELEGRAM) { - if (rawData.Length < (5 + 1 + dataLen) || rawData[5 + 1 + dataLen] != END_TELEGRAM) - { - log.Warn("RawData does not hold Endtag"); - } - } - catch (IndexOutOfRangeException) - { - log.Warn("Rawdata does not contain End tag"); + log.Error("RawData does not hold Endtag"); + throw new ArgumentException("Raw data does not contain End tag"); } } @@ -192,8 +243,18 @@ public virtual string ToStringDetailed() return ToString(); } + /// + /// Compare two telegrams based on the raw data + /// + /// Second object to compare + /// public bool Equals(BaseTelegram? other) { - return Array.Equals(this.Raw, other?.Raw); + if (other == null) + { + return false; + } + return this.Raw.SequenceEqual(other.Raw); } -} \ No newline at end of file + +} diff --git a/RS485 Monitor/src/Telegrams/BatteryRequest.cs b/RS485 Monitor/src/Telegrams/BatteryRequest.cs new file mode 100644 index 0000000..5da716d --- /dev/null +++ b/RS485 Monitor/src/Telegrams/BatteryRequest.cs @@ -0,0 +1,53 @@ +using NLog; + +/// +/// Specialization of the BaseTelegram class that extracts the information +/// of BatteryStatus +/// +public class BatteryRequest : BaseTelegram +{ + /// + /// Internal logging object + /// + private static readonly Logger log = LogManager.GetCurrentClassLogger(); + + #region Constants + /// + /// Required size of PDU data + /// + private const byte PDU_LENGTH = 1; + + + private const byte SOURCE = (byte)Units.ECU; + private const byte DESTINATION = (byte)Units.BATTERY; + public const UInt16 TELEGRAM_ID = DESTINATION << 8 | SOURCE; + #endregion + + /// + /// Create a new Battery Status object based on a received telegram + /// + /// Raw telegram + /// Raw data has an unexpected length + public BatteryRequest(BaseTelegram t) + : base(t) + { + if (t.PDU.Length != PDU_LENGTH) + { + throw new ArgumentException($"Unexpected size of {t.PDU.Length}"); + } + if (t.Source != SOURCE || t.Destination != DESTINATION) + { + throw new ArgumentException("Not a BatteryResponse telegram"); + } + } + + /// + /// Get a string representation of the telegram + /// + /// String representation + public override string ToString() + { + log.Trace(base.ToString()); + return "Battery Request"; + } +} diff --git a/RS485 Monitor/src/Telegrams/BatteryStatus.cs b/RS485 Monitor/src/Telegrams/BatteryResponse.cs similarity index 85% rename from RS485 Monitor/src/Telegrams/BatteryStatus.cs rename to RS485 Monitor/src/Telegrams/BatteryResponse.cs index c3867f9..2153fae 100644 --- a/RS485 Monitor/src/Telegrams/BatteryStatus.cs +++ b/RS485 Monitor/src/Telegrams/BatteryResponse.cs @@ -2,10 +2,10 @@ using NLog; /// -/// Specialization of the BaseTelegram class that extracts the information +/// Specialization of the BaseTelegram class that extracts the information /// of BatteryStatus /// -public class BatteryStatus : BaseTelegram +public class BatteryResponse : BaseTelegram { /// /// Internal logging object @@ -73,7 +73,7 @@ public enum VBreakerStatus /// /// Position of charge / discharge current in PDU /// - private const byte POS_CHARGE = 3; + private const byte POS_CURRENT = 3; /// /// Positiion of high byte of number of charging cycles in PDU /// @@ -91,13 +91,17 @@ public enum VBreakerStatus /// private const byte POS_DISCYCLE_L = 7; /// - /// Position of VBreaker information + /// Position of error information /// - private const byte POS_VBREAKER= 8; + private const byte POS_ERROR_CODE= 8; /// /// Position of charging information in PDU /// private const byte POS_CHARGING = 9; + + private const byte SOURCE = (byte)Units.BATTERY; + private const byte DESTINATION = (byte)Units.ECU; + public const UInt16 TELEGRAM_ID = DESTINATION << 8 | SOURCE; #endregion #region Properties @@ -114,13 +118,13 @@ public enum VBreakerStatus /// public sbyte Temperature { get => (sbyte)PDU[POS_TEMP]; } /// - /// Current VBreaker status + /// error code /// - public VBreakerStatus VBreaker{ get => (VBreakerStatus)PDU[POS_VBREAKER]; } + public VBreakerStatus VBreaker{ get => (VBreakerStatus)PDU[POS_ERROR_CODE]; } /// - /// Current Charge or Discharge in Amps + /// Charge or discharge current in Amps /// - public sbyte Charge { get => (sbyte)PDU[POS_CHARGE]; } + public sbyte Current { get => (sbyte)PDU[POS_CURRENT]; } /// /// Total number of charging cycles /// @@ -151,13 +155,17 @@ public bool Charging /// /// Raw telegram /// Raw data has an unexpected length - public BatteryStatus(BaseTelegram t) + public BatteryResponse(BaseTelegram t) : base(t) { if (t.PDU.Length != TELEGRAM_SIZE) { throw new ArgumentException($"Unexpected size of {t.PDU.Length}"); } + if (t.Source != SOURCE || t.Destination != DESTINATION) + { + throw new ArgumentException("Not a BatteryResponse telegram"); + } } /// @@ -167,8 +175,8 @@ public BatteryStatus(BaseTelegram t) public override string ToString() { log.Trace(base.ToString()); - return $"Battery Status: {Voltage}V, {SoC}%, {Temperature}°C, {Charge} Amp, " + + return $"Battery Response: {Voltage}V, {SoC}%, {Temperature}°C, {Current} Amp, " + $"Charged: {Cycles}x, Discharged: {DischargeCycles}x, VBreaker: {VBreaker}, " + $"Activity: {Activity}, Charging: {Charging}"; } -} \ No newline at end of file +} diff --git a/RS485 Monitor/src/Telegrams/ControllerRequest.cs b/RS485 Monitor/src/Telegrams/ControllerRequest.cs new file mode 100644 index 0000000..c9bd15f --- /dev/null +++ b/RS485 Monitor/src/Telegrams/ControllerRequest.cs @@ -0,0 +1,84 @@ +using NLog; + +/// +/// Specialization of the BaseTelegram class that extracts the information +/// of ControllerResponse +/// +public class ControllerRequest : BaseTelegram +{ + private static readonly Logger log = LogManager.GetCurrentClassLogger(); + + + #region Constants + private const byte POS_CHARGE_STATE = 1; + private const byte RAW_DATA_LEN = 2; + + public const byte SOURCE = (byte)Units.ECU; + public const byte DESTINATION = (byte)Units.ENGINE_CONTROLLER; + public const UInt16 TELEGRAM_ID = DESTINATION << 8 | SOURCE; + #endregion + + /// + /// Possible parking states + /// + public enum ChargingState + { + CHARGING_ON = 0x01, + CHARGING_OFF = 0x00, + /// + /// Invalid / unknown parking state + /// + CHARGING_UNKNOWN = 0xFF + } + + #region Properties + public ChargingState Charge + { + get + { + if (Enum.IsDefined(typeof(ChargingState), (int)PDU[POS_CHARGE_STATE])) + { + return (ChargingState)PDU[POS_CHARGE_STATE]; + } + else + { + return ChargingState.CHARGING_UNKNOWN; + } + } + } + /// + /// Simple property if vehicle is parking + /// + public bool IsCharging { get => Charge == ChargingState.CHARGING_ON; } + #endregion + + /// + /// Create a new ControllerResponse Telegram with the information of BaseTelegram + /// + /// BaseTelegram that holds the raw data + /// Thrown when the size of the PDU is + /// unexpected or the telegram is not a ControllerResponse telegram + public ControllerRequest(BaseTelegram t) + : base(t) + { + if (t.PDU.Length != RAW_DATA_LEN) + { + throw new ArgumentException("Unexpected size"); + } + + if (t.Source != SOURCE || t.Destination != DESTINATION) + { + throw new ArgumentException("Not a ControllerRequest telegram"); + } + } + + /// + /// Get a string representation of the telegram + /// + /// String representation + public override string ToString() + { + log.Trace(base.ToString()); + return $"Controller Request: {Charge}"; + } +} diff --git a/RS485 Monitor/src/Telegrams/ControllerResponse.cs b/RS485 Monitor/src/Telegrams/ControllerResponse.cs new file mode 100644 index 0000000..423c681 --- /dev/null +++ b/RS485 Monitor/src/Telegrams/ControllerResponse.cs @@ -0,0 +1,112 @@ +using System.Reflection.Metadata; +using NLog; +using NLog.Config; + +/// +/// Specialization of the BaseTelegram class that extracts the information +/// of ControllerResponse +/// +public class ControllerResponse : BaseTelegram +{ + private static readonly Logger log = LogManager.GetCurrentClassLogger(); + + + #region Constants + private const byte POS_GEAR = 0; + private const byte POS_CURRENT_H = 1; + private const byte POS_CURRENT_L = 2; + private const byte POS_SPEED_H = 3; + private const byte POS_SPEED_L = 4; + private const byte POS_TEMP = 5; + private const byte POS_ERROR_CODE = 6; + private const byte POS_PARKING = 8; + + public const byte RAW_DATA_LEN = 10; + + public const byte SOURCE = (byte) Units.ENGINE_CONTROLLER; + public const byte DESTINATION = (byte) Units.ECU; + public const UInt16 TELEGRAM_ID = DESTINATION << 8 | SOURCE; + #endregion + + /// + /// Possible parking states + /// + public enum ParkStatus + { + PARKING_ON = 0x02, + PARKING_OFF = 0x01, + /// + /// Invalid / unknown parking state + /// + PARKING_UNKNOWN = 0xFF + } + + #region Properties + public byte Gear { get => PDU[POS_GEAR]; } + /// + /// Current in Ampere + /// + public float Current { get => (float)((ushort)((PDU[POS_CURRENT_H] << 8) + PDU[POS_CURRENT_L]) * 0.1); } + /// + /// Current speed in km/h + /// + public float Speed { get => (float) ((ushort)((PDU[POS_SPEED_H] << 8) + PDU[POS_SPEED_L]) * 0.028); } + /// + /// Simple error code extraction + /// + /// Define error codes + public byte ErrorCode { get => PDU[POS_ERROR_CODE]; } + /// + /// Temperature in °C + /// + public sbyte Temperature { get => (sbyte)PDU[POS_TEMP]; } + /// + /// Status of parking + /// + public ParkStatus Parking { + get { + ParkStatus status = ParkStatus.PARKING_UNKNOWN; + if (Enum.IsDefined(typeof(ParkStatus), (int)PDU[POS_PARKING])) + { + status = (ParkStatus)PDU[POS_PARKING]; + } + return status; + } + } + /// + /// Simple property if vehicle is parking + /// + public bool IsParking { get => Parking == ParkStatus.PARKING_ON; } + #endregion + + /// + /// Create a new ControllerResponse Telegram with the information of BaseTelegram + /// + /// BaseTelegram that holds the raw data + /// Thrown when the size of the PDU is + /// unexpected or the telegram is not a ControllerResponse telegram + public ControllerResponse(BaseTelegram t) + : base(t) + { + if (t.PDU.Length != RAW_DATA_LEN) + { + throw new ArgumentException("Unexpected size"); + } + + if (t.Source != SOURCE || t.Destination != DESTINATION) + { + throw new ArgumentException("Not a ControllerResponse telegram"); + } + } + + /// + /// Get a string representation of the telegram + /// + /// String representation + public override string ToString() + { + log.Trace(base.ToString()); + string current_invariant = string.Create(System.Globalization.CultureInfo.InvariantCulture, $"{Current}"); + return $"Controller Response: Mode {Gear}, {current_invariant}A, {Speed}km/h, {Temperature}°C, Parking: {Parking}"; + } +} diff --git a/RS485 Monitor/src/Telegrams/ECUStatus.cs b/RS485 Monitor/src/Telegrams/ECUStatus.cs deleted file mode 100644 index 23e53b6..0000000 --- a/RS485 Monitor/src/Telegrams/ECUStatus.cs +++ /dev/null @@ -1,74 +0,0 @@ -using NLog; - -/// -/// Specialization of the BaseTelegram class that extracts the information -/// of ECUStatus -/// -public class ECUStatus : BaseTelegram -{ - private static readonly Logger log = LogManager.GetCurrentClassLogger(); - - #region Constants - private const byte POS_PDU = 0; - private const byte POS_CURRENT_H = 1; - private const byte POS_CURRENT_L = 2; - private const byte POS_SPEED_H = 3; - private const byte POS_SPEED_L = 4; - private const byte POS_TEMP = 5; - private const byte POS_PARKING = 8; - - public const byte RAW_DATA_LEN = 10; - #endregion - - /// - /// Possible parking states - /// - public enum ParkStatus - { - PARKING_ON = 0x02, - PARKING_OFF = 0x01, - } - - #region Properties - public byte Mode { get => PDU[POS_PDU]; } - /// - /// Current in Ampere - /// - public float Current { get => (float)((ushort)((PDU[POS_CURRENT_H] << 8) + PDU[POS_CURRENT_L]) * 0.1); } - /// - /// Current speed in km/h - /// - public float Speed { get => (float) ((ushort)((PDU[POS_SPEED_H] << 8) + PDU[POS_SPEED_L]) * 0.028); } - /// - /// Temperature in °C - /// - public sbyte Temperature { get => (sbyte)PDU[POS_TEMP]; } - public ParkStatus Parking { get => (ParkStatus)PDU[POS_PARKING]; } - public bool IsParking { get => Parking == ParkStatus.PARKING_ON; } - - #endregion - - /// - /// Create a new ECUStatus Telegram with the information of BaseTelegram - /// - /// BaseTelegram that holds the raw data - /// Unexpected size - public ECUStatus(BaseTelegram t) - : base(t) - { - if (t.PDU.Length != RAW_DATA_LEN) - { - throw new ArgumentException("Unexpected size"); - } - } - - /// - /// Get a string representation of the telegram - /// - /// String representation - public override string ToString() - { - log.Trace(base.ToString()); - return $"ECU Status: Mode {Mode}, {Current}A, {Speed}km/h, {Temperature}°C, Parking: {IsParking}"; - } -} \ No newline at end of file diff --git a/RS485 Monitor/src/Telegrams/ErrorCode.cs b/RS485 Monitor/src/Telegrams/ErrorCode.cs new file mode 100644 index 0000000..4ffc52f --- /dev/null +++ b/RS485 Monitor/src/Telegrams/ErrorCode.cs @@ -0,0 +1,41 @@ +public class ErrorCode{ + private UInt16 raw; + + [Flags] + public enum ErrorBits { + CTRL_DISCONNECT_99 = 0x0001, + CTRL_ERROR_98 = 0x0002, + CTRL_ERROR_97 = 0x0004, + CTRL_ERROR_96 = 0x0008, + CTRL_ERROR_95 = 0x0010, + /// + /// Battery is disconnected + /// + BATTERY_DISCONNECT_94 = 0x0020, + /// + /// Charging current too high + /// + BATTERY_CHARGE_CURRENT_93 = 0x0040, + /// + /// Charging has stopped + /// + BATTERY_CHARGE_STOPPED_92 = 0x0080, + /// + /// Battery over temperature + /// + BATTERY_OVERTEMP_91 = 0x0100, + /// + /// Discharge current too high + /// + BATTERY_DISCHARGE_CURRENT_90 = 0x0200, + BATTERY_ERROR_89 = 0x0400, + BATTERY_ERROR_88 = 0x0800, + } + + public ErrorCode(int raw) { + this.raw = (UInt16)raw; + } + + public ErrorBits State { get => (ErrorBits)this.raw; } + +} diff --git a/RS485 Monitor/src/Telegrams/SpeedometerRequest.cs b/RS485 Monitor/src/Telegrams/SpeedometerRequest.cs new file mode 100644 index 0000000..2e8f6a5 --- /dev/null +++ b/RS485 Monitor/src/Telegrams/SpeedometerRequest.cs @@ -0,0 +1,201 @@ +using CommandLine; +using NLog; + +/// +/// Specialization of the BaseTelegram class that extracts the information +/// of the SpeedometerRequest +/// +public class SpeedometerRequest : BaseTelegram +{ + /// + /// Internal class logger + /// + private static readonly Logger log = LogManager.GetCurrentClassLogger(); + + #region Constants + /// + /// Position of SOC in the PDU + /// + private const byte POS_SOC = 0; + /// + /// Position of current in the PDU + /// + private const byte POS_CTRL_CURRENT = 1; + /// + /// Position of speed in the PDU + /// + private const byte POS_SPEED = 2; + /// + /// Position of temperature level in the PDU + /// + private const byte POS_TEMP_LEVEL = 3; + /// + /// Position of hour (Localtime) in the PDU + /// + private const byte POS_HOUR = 4; + /// + /// Position of minute in the PDU + /// + private const byte POS_MINUTE = 5; + /// + /// Position of error code high Byte in the PDU + /// + private const byte POS_ERR_1 = 6; + /// + /// Position of error code low Byte in the PDU + /// + private const byte POS_ERR_2 = 7; + /// + /// Position of vehicle state in the PDU + /// + private const byte POS_VEHICLE_STATE = 8; + /// + /// Position of gear in the PDU + /// + private const byte POS_GEAR = 9; + /// + /// Position of speed high Byte in the PDU + /// + private const byte POS_SPEED_H = 10; + /// + /// Position of speed low Byte in the PDU + /// + private const byte POS_SPEED_L = 11; + /// + /// Position of range in the PDU + /// + private const byte POS_RANGE = 13; + + /// + /// Destination of the telegram + /// + private const byte DESTINATION = (byte)Units.SPEEDOMETER; + /// + /// Source of the telegram + /// + private const byte SOURCE = (byte)Units.ECU; + + /// + /// Calculated telegram ID + /// + public const UInt16 TELEGRAM_ID = DESTINATION << 8 | SOURCE; + + /// + /// Length of the required raw data + /// + public const byte RAW_DATA_LEN = 14; + #endregion + + #region Properties + + /// + /// State of Charge in % + /// + public byte Soc { get => PDU[POS_SOC]; } + /// + /// Current received from the engine controller in ampere + /// + public double CtrlCurrent { get => PDU[POS_CTRL_CURRENT] * 2.5; } + /// + /// Speed in km/h + /// + public byte Speed { get => PDU[POS_SPEED]; } + /// + /// Temperature level + /// + public byte TempLevel { get => PDU[POS_TEMP_LEVEL]; } + /// + /// Current hour (localtime) + /// + public byte Hour { get => PDU[POS_HOUR]; } + /// + /// Current minute (localtime) + /// + public byte Minutes { get => PDU[POS_MINUTE]; } + public ErrorCode ErrorValue { + get => new ErrorCode(PDU[POS_ERR_1] << 8 | PDU[POS_ERR_2]); + } + + /// + /// Current state of the vehicle + /// + public VehicleState State + { + get + { + if (Enum.IsDefined(typeof(VehicleState), (Int32)PDU[POS_VEHICLE_STATE])) + { + return (VehicleState)PDU[POS_VEHICLE_STATE]; + } + return VehicleState.UNKNOWN; + } + } + /// + /// Currently selected gear + /// + public byte Gear { get => PDU[POS_GEAR]; } + /// + /// Current speed + /// + public UInt16 SpeedCtrlValue { get => (UInt16)(PDU[POS_SPEED_H] << 8 | PDU[POS_SPEED_L]); } + /// + /// Remaining range in km + /// + public byte Range { get => PDU[POS_RANGE]; } + + #endregion + + /// + /// Different states of the vehicle + /// + public enum VehicleState + { + /// + /// Vehicle is charging + /// + CHARGING = 4, + /// + /// Vehicle is parking + /// + PARKING = 1, + /// + /// Vehicle is active + /// + ACTIVE = 0, + /// + /// Vehicle has an unknown state + /// + UNKNOWN = 0xFF + } + + /// + /// Create a new GSMStatus telegram with the information of BaseTelegram + /// + /// BaseTelegram that holds the raw data + /// Unexpected size + public SpeedometerRequest(BaseTelegram t) + : base(t) + { + if (t.PDU.Length != RAW_DATA_LEN) + { + throw new ArgumentException("Unexpected size"); + } + if (t.Source != SOURCE || t.Destination != DESTINATION) + { + throw new ArgumentException("Not a SpeedometerRequest telegram"); + } + } + + /// + /// Get a string representation of the telegram + /// + /// String representation + public override string ToString() + { + log.Trace(base.ToString()); + return $"Speedometer Request: Soc {Soc}%, Current {CtrlCurrent}A, " + + $"Speed {Speed}km/h, TempLvl {TempLevel}, Time {Hour:d2}:{Minutes:d2}, " + + $"Error {ErrorValue.State}, Vehicle State {State}, Gear {Gear}, " + + $"Speed Ctrl {SpeedCtrlValue}, Range {Range}km"; + } +} diff --git a/RS485 Monitor/src/Telegrams/GSMStatus.cs b/RS485 Monitor/src/Telegrams/SpeedometerResponse.cs similarity index 56% rename from RS485 Monitor/src/Telegrams/GSMStatus.cs rename to RS485 Monitor/src/Telegrams/SpeedometerResponse.cs index 05f0cd4..11b33aa 100644 --- a/RS485 Monitor/src/Telegrams/GSMStatus.cs +++ b/RS485 Monitor/src/Telegrams/SpeedometerResponse.cs @@ -1,56 +1,56 @@ +using CommandLine; using NLog; /// -/// Specialization of the BaseTelegram class that extracts the information -/// of GSM / GPS Status +/// Specialization of the BaseTelegram class that extracts the information +/// of the SpeedometerRequest /// -public class GSMStatus : BaseTelegram +public class SpeedometerResponse : BaseTelegram { -/// -/// Internal class logger{ -/// + /// + /// Internal class logger + /// private static readonly Logger log = LogManager.GetCurrentClassLogger(); #region Constants + /// - /// Position of hour (Localtime) + /// Destination of the telegram /// - private const byte POS_HOUR = 4; + private const byte DESTINATION = (byte)Units.ECU; /// - /// Position of minute + /// Source of the telegram /// - private const byte POS_MINUTE = 5; - -/// -/// LEngth of the required raw data -/// - public const byte RAW_DATA_LEN = 14; - #endregion + private const byte SOURCE = (byte)Units.SPEEDOMETER; - #region Properties /// - /// Current hour (localtime) + /// Calculated telegram ID /// - public byte Hour { get => PDU[POS_HOUR]; } + public const UInt16 TELEGRAM_ID = DESTINATION << 8 | SOURCE; + /// - /// Current minute (localtime) + /// Length of the required raw data /// - public byte Minutes { get => PDU[POS_MINUTE]; } - + public const byte RAW_DATA_LEN = 1; #endregion + /// /// Create a new GSMStatus telegram with the information of BaseTelegram /// /// BaseTelegram that holds the raw data /// Unexpected size - public GSMStatus(BaseTelegram t) + public SpeedometerResponse(BaseTelegram t) : base(t) { if (t.PDU.Length != RAW_DATA_LEN) { throw new ArgumentException("Unexpected size"); } + if (t.Source != SOURCE || t.Destination != DESTINATION) + { + throw new ArgumentException("Not a SpeedometerResponse telegram"); + } } /// @@ -60,6 +60,6 @@ public GSMStatus(BaseTelegram t) public override string ToString() { log.Trace(base.ToString()); - return $"GSM Status: Time {Hour:d2}:{Minutes:d2}"; + return $"Speedometer Response"; } -} \ No newline at end of file +} diff --git a/RS485 Monitor/src/Telegrams/Units.cs b/RS485 Monitor/src/Telegrams/Units.cs new file mode 100644 index 0000000..017256e --- /dev/null +++ b/RS485 Monitor/src/Telegrams/Units.cs @@ -0,0 +1,22 @@ +/// +/// List of available units in the system +/// +public enum Units +{ + /// + /// Main ECU / bus master + /// + ECU = 0xAA, + /// + /// Engine controller + /// + ENGINE_CONTROLLER = 0xDA, + /// + /// Battery management system + /// + BATTERY = 0x5A, + /// + /// Speedometer + /// + SPEEDOMETER = 0xBA +} diff --git a/tests/BaseTelegramTest.cs b/tests/BaseTelegramTest.cs index 86f8791..2a5a2cd 100644 --- a/tests/BaseTelegramTest.cs +++ b/tests/BaseTelegramTest.cs @@ -6,40 +6,112 @@ public class BaseTelegramTest [Test] public void ParseTelegramSuccessful() { - byte[] raw = new byte[] { 0xB6, 0x6B, 0xAA, 0xDA, 0x0A, 0x02, 0x00, 0x04, 0x00, 0x00, 0x13, 0x00, 0x00, 0x02, 0x01, 0x1C, 0x0D }; + byte[] raw = [0xB6, 0x6B, 0xAA, 0xDA, 0x0A, 0x02, 0x00, 0x04, 0x00, 0x00, 0x13, 0x00, 0x00, 0x02, 0x01, 0x1C, 0x0D]; BaseTelegram telegram = new(raw); - Assert.That(telegram.Type, Is.EqualTo(BaseTelegram.TelegramType.READ_RESPONSE)); - Assert.That(telegram.Destination, Is.EqualTo(0xDA)); - Assert.That(telegram.Source, Is.EqualTo(0xAA)); + Assert.That(telegram.Type, Is.EqualTo(BaseTelegram.TelegramType.RESPONSE)); + Assert.That(telegram.Destination, Is.EqualTo(0xAA)); + Assert.That(telegram.Source, Is.EqualTo(0xDA)); Assert.That(telegram.Valid, Is.EqualTo(true)); } [Test] public void ParseTelegramRequestSuccessful() { - byte[] raw = new byte[] { 0xC5, 0x5C, 0xAA, 0xDA, 0x0A, 0x02, 0x00, 0x04, 0x00, 0x00, 0x13, 0x00, 0x00, 0x02, 0x01, 0x1C, 0x0D }; + byte[] raw = [0xC5, 0x5C, 0xAA, 0xDA, 0x0A, 0x02, 0x00, 0x04, 0x00, 0x00, 0x13, 0x00, 0x00, 0x02, 0x01, 0x1C, 0x0D]; BaseTelegram telegram = new(raw); - Assert.That(telegram.Type, Is.EqualTo(BaseTelegram.TelegramType.READ_REQUEST)); - Assert.That(telegram.Destination, Is.EqualTo(0xDA)); - Assert.That(telegram.Source, Is.EqualTo(0xAA)); + Assert.That(telegram.Type, Is.EqualTo(BaseTelegram.TelegramType.REQUEST)); + Assert.That(telegram.Destination, Is.EqualTo(0xAA)); + Assert.That(telegram.Source, Is.EqualTo(0xDA)); Assert.That(telegram.Valid, Is.EqualTo(true)); } [Test] public void ParseTelegramWrongChecksum() { - byte[] raw = new byte[] { 0xB6, 0x6B, 0xAA, 0xDA, 0x0A, 0x02, 0x10, 0x04, 0x00, 0x00, 0x13, 0x00, 0x00, 0x02, 0x01, 0x1C, 0x0D }; + byte[] raw = [0xB6, 0x6B, 0xAA, 0xDA, 0x0A, 0x02, 0x10, 0x04, 0x00, 0x00, 0x13, 0x00, 0x00, 0x02, 0x01, 0x1C, 0x0D]; BaseTelegram telegram = new(raw); - Assert.That(telegram.Type, Is.EqualTo(BaseTelegram.TelegramType.READ_RESPONSE)); - Assert.That(telegram.Destination, Is.EqualTo(0xDA)); - Assert.That(telegram.Source, Is.EqualTo(0xAA)); + Assert.That(telegram.Type, Is.EqualTo(BaseTelegram.TelegramType.RESPONSE)); + Assert.That(telegram.Destination, Is.EqualTo(0xAA)); + Assert.That(telegram.Source, Is.EqualTo(0xDA)); Assert.That(telegram.Valid, Is.EqualTo(false)); } + [Test] + public void ParseTelegramTooShort() + { + byte[] raw = [0xB6, 0x6B, 0xAA]; + + var ex = Assert.Throws(() => new BaseTelegram(raw)); + Assert.That(ex.Message, Is.EqualTo("Raw data is too short")); + } + + [Test] + public void ParseTelegramInvalidDataLength() + { + byte[] raw = [0xB6, 0x6B, 0xAA, 0xDA, 0x21, 0x02, 0x00, 0x04, 0x00, 0x00, 0x13, 0x00, 0x00, 0x02, 0x01, 0x1C, 0x0D]; + + var ex = Assert.Throws(() => new BaseTelegram(raw)); + Assert.That(ex.Message, Is.EqualTo("Invalid data len 33. Max supported: 32")); + } + + [Test] + public void ParseTelegramMissingEndTag() + { + byte[] raw = [0xB6, 0x6B, 0xAA, 0xDA, 0x0A, 0x02, 0x00, 0x04, 0x00, 0x00, 0x13, 0x00, 0x00, 0x02, 0x01, 0x1C]; + + var ex = Assert.Throws(() => new BaseTelegram(raw)); + Assert.That(ex.Message, Is.EqualTo("Raw data does not contain End tag")); + } + + [Test] + public void ToStringTest() + { + byte[] raw = [0xB6, 0x6B, 0xAA, 0xDA, 0x0A, 0x02, 0x00, 0x04, 0x00, 0x00, 0x13, 0x00, 0x00, 0x02, 0x01, 0x1C, 0x0D]; + + BaseTelegram telegram = new(raw); + + string expected = "B6 6B AA DA 0A 02 00 04 00 00 13 00 00 02 01 1C 0D "; + Assert.That(telegram.ToString(), Is.EqualTo(expected)); + } + + [Test] + public void ToStringDetailedTest() + { + byte[] raw = [0xB6, 0x6B, 0xAA, 0xDA, 0x0A, 0x02, 0x00, 0x04, 0x00, 0x00, 0x13, 0x00, 0x00, 0x02, 0x01, 0x1C, 0x0D]; + + BaseTelegram telegram = new(raw); + + string expected = "B6 6B AA DA 0A 02 00 04 00 00 13 00 00 02 01 1C 0D "; + Assert.That(telegram.ToStringDetailed(), Is.EqualTo(expected)); + } + + [Test] + public void EqualsTest() + { + byte[] raw1 = [0xB6, 0x6B, 0xAA, 0xDA, 0x0A, 0x02, 0x00, 0x04, 0x00, 0x00, 0x13, 0x00, 0x00, 0x02, 0x01, 0x1C, 0x0D]; + byte[] raw2 = [0xB6, 0x6B, 0xAA, 0xDA, 0x0A, 0x02, 0x00, 0x04, 0x00, 0x00, 0x13, 0x00, 0x00, 0x02, 0x01, 0x1C, 0x0D]; + + BaseTelegram telegram1 = new(raw1); + BaseTelegram telegram2 = new(raw2); + + Assert.That(telegram1.Equals(telegram2), Is.EqualTo(true)); + } + + [Test] + public void NotEqualsTest() + { + byte[] raw1 = [0xB6, 0x6B, 0xAA, 0xDA, 0x0A, 0x02, 0x00, 0x04, 0x00, 0x00, 0x13, 0x00, 0x00, 0x02, 0x01, 0x1C, 0x0D]; + byte[] raw2 = [0xC5, 0x5C, 0xAA, 0xDA, 0x0A, 0x02, 0x00, 0x04, 0x00, 0x00, 0x13, 0x00, 0x00, 0x02, 0x01, 0x1C, 0x0D]; + + BaseTelegram telegram1 = new(raw1); + BaseTelegram telegram2 = new(raw2); + + Assert.That(telegram1.Equals(telegram2), Is.EqualTo(false)); + } } diff --git a/tests/BatteryRequestTest.cs b/tests/BatteryRequestTest.cs new file mode 100644 index 0000000..b2594a3 --- /dev/null +++ b/tests/BatteryRequestTest.cs @@ -0,0 +1,54 @@ +namespace RS485_Monitor.tests; +using NUnit.Framework; + +public class BatteryRequestTest +{ + [Test] + public void BatteryRequestSuccessful() + { + byte[] raw = [0xC5, 0x5C, 0x5A, 0xAA, 0x01, 0x00, 0x01, 0x0D]; + + BaseTelegram baseTelegram = new(raw); + BatteryRequest batteryRequest = new(baseTelegram); + + using (Assert.EnterMultipleScope()) + { + Assert.That(batteryRequest.Type, Is.EqualTo(BaseTelegram.TelegramType.REQUEST)); + Assert.That(batteryRequest.Destination, Is.EqualTo((byte)Units.BATTERY)); + Assert.That(batteryRequest.Source, Is.EqualTo((byte)Units.ECU)); + Assert.That(batteryRequest.Valid, Is.EqualTo(true)); + } + } + + [Test] + public void BatteryRequestInvalidPDUSize() + { + byte[] raw = [0xC5, 0x5C, 0x5A, 0xAA, 0x02, 0x00, 0x01, 0x02, 0x0D]; + + BaseTelegram baseTelegram = new(raw); + var ex = Assert.Throws(() => new BatteryRequest(baseTelegram)); + Assert.That(ex.Message, Is.EqualTo("Unexpected size of 2")); + } + + [Test] + public void BatteryRequestInvalidSourceOrDestination() + { + byte[] raw = [0xC5, 0x5C, 0xBB, 0xCC, 0x01, 0x00, 0x01, 0x0D]; + + BaseTelegram baseTelegram = new(raw); + var ex = Assert.Throws(() => new BatteryRequest(baseTelegram)); + Assert.That(ex.Message, Is.EqualTo("Not a BatteryResponse telegram")); + } + + [Test] + public void BatteryRequestToString() + { + byte[] raw = [0xC5, 0x5C, 0x5A, 0xAA, 0x01, 0x00, 0x01, 0x0D]; + + BaseTelegram baseTelegram = new(raw); + BatteryRequest batteryRequest = new(baseTelegram); + + string expected = "Battery Request"; + Assert.That(batteryRequest.ToString(), Is.EqualTo(expected)); + } +} diff --git a/tests/BatteryResponseTest.cs b/tests/BatteryResponseTest.cs new file mode 100644 index 0000000..321c5c2 --- /dev/null +++ b/tests/BatteryResponseTest.cs @@ -0,0 +1,76 @@ +namespace RS485_Monitor.tests; +using NUnit.Framework; + +public class BatteryResponseTest +{ + private BaseTelegram? batteryBase = null; + + [SetUp] + public void Setup() + { + byte[] raw = [0xB6, 0x6B, 0xAA, 0x5A, 0x0A, 0x4D, 0x48, 0x17, 0x00, 0x00, 0x23, 0x00, 0x0B, 0x00, 0x00, 0x30, 0x0D]; + batteryBase = new BaseTelegram(raw); + } + + [Test] + public void ConvertToBatteryReponseSuccess() + { + BatteryResponse status = new(batteryBase!); + Assert.That(status, Is.Not.Null); + } + + [Test] + public void ConvertToBatteryResponseFail() + { + byte[] raw = [0xB6, 0x6B, 0xAA, 0x5A, 0x09, 0x4D, 0x48, 0x17, 0x00, 0x00, 0x23, 0x00, 0x0B, 0x00, 0x30, 0x0D]; + BaseTelegram invalidTelegram = new(raw); + + Assert.Throws(() => new BatteryResponse(invalidTelegram)); + } + + [Test] + public void CheckContent() + { + BatteryResponse status = new(batteryBase!); + using (Assert.EnterMultipleScope()) + { + Assert.That(status.Voltage, Is.EqualTo(77)); + Assert.That(status.SoC, Is.EqualTo(72)); + Assert.That(status.Temperature, Is.EqualTo(23)); + Assert.That(status.Current, Is.EqualTo(0x0)); + Assert.That(status.Cycles, Is.EqualTo(35)); + Assert.That(status.DischargeCycles, Is.EqualTo(11)); + Assert.That(status.VBreaker, Is.EqualTo(BatteryResponse.VBreakerStatus.OK)); + Assert.That(status.Activity, Is.EqualTo(BatteryResponse.BatteryActivity.NO_ACTIVITY)); + Assert.That(status.Charging, Is.False); + } + } + + [Test] + public void ToStringTest() + { + BatteryResponse status = new(batteryBase!); + string expected = "Battery Response: 77V, 72%, 23°C, 0 Amp, Charged: 35x, Discharged: 11x, VBreaker: OK, Activity: NO_ACTIVITY, Charging: False"; + Assert.That(status.ToString(), Is.EqualTo(expected)); + } + + [Test] + public void BatteryResponseInvalidSourceOrDestination() + { + byte[] raw = [0xB6, 0x6B, 0xBB, 0xCC, 0x0A, 0x4D, 0x48, 0x17, 0x00, 0x00, 0x23, 0x00, 0x0B, 0x00, 0x00, 0x30, 0x0D]; + BaseTelegram invalidTelegram = new(raw); + + var ex = Assert.Throws(() => new BatteryResponse(invalidTelegram)); + Assert.That(ex.Message, Is.EqualTo("Not a BatteryResponse telegram")); + } + + [Test] + public void BatteryResponseInvalidPDUSize() + { + byte[] raw = [0xB6, 0x6B, 0xAA, 0x5A, 0x0B, 0x4D, 0x48, 0x17, 0x00, 0x00, 0x23, 0x00, 0x0B, 0x00, 0x00, 0x00, 0x30, 0x0D]; + BaseTelegram invalidTelegram = new(raw); + + var ex = Assert.Throws(() => new BatteryResponse(invalidTelegram)); + Assert.That(ex.Message, Is.EqualTo("Unexpected size of 11")); + } +} diff --git a/tests/BatteryStatusTest.cs b/tests/BatteryStatusTest.cs deleted file mode 100644 index de8da92..0000000 --- a/tests/BatteryStatusTest.cs +++ /dev/null @@ -1,48 +0,0 @@ -namespace RS485_Monitor.tests; -using NUnit.Framework; - -public class BatteryStatusTest -{ - private BaseTelegram? batteryBase = null; - - [SetUp] - public void Setup() - { - byte[] raw = new byte[] { 0xB6, 0x6B, 0xAA, 0x5A, 0x0A, 0x4D, 0x48, 0x17, 0x00, 0x00, 0x23, 0x00, 0x0B, 0x00, 0x00, 0x30, 0x0D }; - batteryBase = new BaseTelegram(raw); - } - - [Test] - public void ConvertToBatteryStatusSuccess() - { - BatteryStatus status = new(batteryBase!); - Assert.That(status, Is.Not.Null); - } - - [Test] - public void ConvertToBatteryStatusFail() - { - byte[] raw = new byte[] { 0xB6, 0x6B, 0xAA, 0x5A, 0x09, 0x4D, 0x48, 0x17, 0x00, 0x00, 0x23, 0x00, 0x0B, 0x00, 0x30, 0x0D }; - BaseTelegram invalidTelegram = new(raw); - - Assert.Throws(() => new BatteryStatus(invalidTelegram)); - } - - [Test] - public void CheckContent() - { - BatteryStatus status = new(batteryBase!); - using (Assert.EnterMultipleScope()) - { - Assert.That(status.Voltage, Is.EqualTo(77)); - Assert.That(status.SoC, Is.EqualTo(72)); - Assert.That(status.Temperature, Is.EqualTo(23)); - Assert.That(status.Charge, Is.EqualTo(0x0)); - Assert.That(status.Cycles, Is.EqualTo(35)); - Assert.That(status.DischargeCycles, Is.EqualTo(11)); - Assert.That(status.VBreaker, Is.EqualTo(BatteryStatus.VBreakerStatus.OK)); - Assert.That(status.Activity, Is.EqualTo(BatteryStatus.BatteryActivity.NO_ACTIVITY)); - Assert.That(status.Charging, Is.False); - } - } -} diff --git a/tests/ControllerRequestTest.cs b/tests/ControllerRequestTest.cs new file mode 100644 index 0000000..3ade6d3 --- /dev/null +++ b/tests/ControllerRequestTest.cs @@ -0,0 +1,85 @@ +namespace RS485_Monitor.tests; +using NUnit.Framework; + +public class ControllerRequestTest +{ + private BaseTelegram? controllerBase = null; + + [SetUp] + public void Setup() + { + byte[] raw = [0xC5, 0x5C, 0xDA, 0xAA, 0x02, 0x00, 0x00, 0x01, 0x0D]; + controllerBase = new BaseTelegram(raw); + } + + [Test] + public void ConvertToControllerRequestSuccess() + { + ControllerRequest request = new(controllerBase!); + Assert.That(request, Is.Not.Null); + } + + [Test] + public void ConvertToControllerRequestFail_InvalidPDUSize() + { + byte[] raw = [0xC5, 0x5C, 0xDA, 0xAA, 0x03, 0x00, 0x01, 0x02, 0x03, 0x0D]; + BaseTelegram invalidTelegram = new(raw); + + var ex = Assert.Throws(() => new ControllerRequest(invalidTelegram)); + Assert.That(ex.Message, Is.EqualTo("Unexpected size")); + } + + [Test] + public void ConvertToControllerRequestFail_InvalidSourceOrDestination() + { + byte[] raw = [0xC5, 0x5C, 0xBB, 0xCC, 0x02, 0x00, 0x00, 0x01, 0x0D]; + BaseTelegram invalidTelegram = new(raw); + + var ex = Assert.Throws(() => new ControllerRequest(invalidTelegram)); + Assert.That(ex.Message, Is.EqualTo("Not a ControllerRequest telegram")); + } + + [Test] + public void CheckChargingState_On() + { + byte[] raw = [0xC5, 0x5C, 0xDA, 0xAA, 0x02, 0x00, 0x01, 0x01, 0x0D]; + BaseTelegram baseTelegram = new(raw); + ControllerRequest request = new(baseTelegram); + + Assert.That(request.Charge, Is.EqualTo(ControllerRequest.ChargingState.CHARGING_ON)); + Assert.That(request.IsCharging, Is.True); + } + + [Test] + public void CheckChargingState_Off() + { + byte[] raw = [0xC5, 0x5C, 0xDA, 0xAA, 0x02, 0x00, 0x00, 0x00, 0x0D]; + BaseTelegram baseTelegram = new(raw); + ControllerRequest request = new(baseTelegram); + + Assert.That(request.Charge, Is.EqualTo(ControllerRequest.ChargingState.CHARGING_OFF)); + Assert.That(request.IsCharging, Is.False); + } + + [Test] + public void CheckChargingState_Unknown() + { + byte[] raw = [0xC5, 0x5C, 0xDA, 0xAA, 0x02, 0x00, 0xFF, 0xFF, 0x0D]; + BaseTelegram baseTelegram = new(raw); + ControllerRequest request = new(baseTelegram); + + Assert.That(request.Charge, Is.EqualTo(ControllerRequest.ChargingState.CHARGING_UNKNOWN)); + Assert.That(request.IsCharging, Is.False); + } + + [Test] + public void ToStringTest() + { + byte[] raw = [0xC5, 0x5C, 0xDA, 0xAA, 0x02, 0x00, 0x01, 0x0F, 0x0D]; + BaseTelegram baseTelegram = new(raw); + ControllerRequest request = new(baseTelegram); + + string expected = "Controller Request: CHARGING_ON"; + Assert.That(request.ToString(), Is.EqualTo(expected)); + } +} diff --git a/tests/ControllerResponseTest.cs b/tests/ControllerResponseTest.cs new file mode 100644 index 0000000..15a118d --- /dev/null +++ b/tests/ControllerResponseTest.cs @@ -0,0 +1,96 @@ +namespace RS485_Monitor.tests; +using NUnit.Framework; + +public class ControllerResponseTest +{ + private BaseTelegram? ecuBase; + + [SetUp] + public void Setup() + { + byte[] raw = [0xB6, 0x6B, 0xAA, 0xDA, 0x0A, 0x02, 0x00, 0x04, 0x00, 0x00, 0x13, 0x00, 0x00, 0x02, 0x01, 0x1C, 0x0D]; + ecuBase = new BaseTelegram(raw); + } + + [Test] + public void ConvertToEcuStatusSuccess() + { + ControllerResponse status = new(ecuBase!); + Assert.That(status, Is.Not.Null); + } + + [Test] + public void CheckContent() + { + ControllerResponse status = new(ecuBase!); + using (Assert.EnterMultipleScope()) + { + Assert.That(status.Gear, Is.EqualTo(2)); + Assert.That(status.Current, Is.EqualTo(0.4).Within(0.1)); + Assert.That(status.Speed, Is.EqualTo(0)); + Assert.That(status.Temperature, Is.EqualTo(0x13)); + Assert.That(status.IsParking, Is.EqualTo(true)); + } + } + + [Test] + public void CheckParkingStatus_On() + { + byte[] raw = [0xB6, 0x6B, 0xAA, 0xDA, 0x0A, 0x02, 0x00, 0x04, 0x00, 0x00, 0x13, 0x00, 0x00, 0x02, 0x02, 0x1C, 0x0D]; + BaseTelegram baseTelegram = new(raw); + ControllerResponse response = new(baseTelegram); + + Assert.That(response.Parking, Is.EqualTo(ControllerResponse.ParkStatus.PARKING_ON)); + Assert.That(response.IsParking, Is.True); + } + + [Test] + public void CheckParkingStatus_Off() + { + byte[] raw = [0xB6, 0x6B, 0xAA, 0xDA, 0x0A, 0x02, 0x00, 0x04, 0x00, 0x00, 0x13, 0x00, 0x00, 0x01, 0x01, 0x1C, 0x0D]; + BaseTelegram baseTelegram = new(raw); + ControllerResponse response = new(baseTelegram); + + Assert.That(response.Parking, Is.EqualTo(ControllerResponse.ParkStatus.PARKING_OFF)); + Assert.That(response.IsParking, Is.False); + } + + [Test] + public void CheckParkingStatus_Unknown() + { + byte[] raw = [0xB6, 0x6B, 0xAA, 0xDA, 0x0A, 0x02, 0x00, 0x04, 0x00, 0x00, 0x13, 0x00, 0x00, 0x03, 0xFF, 0x1C, 0x0D]; + BaseTelegram baseTelegram = new(raw); + ControllerResponse response = new(baseTelegram); + + Assert.That(response.Parking, Is.EqualTo(ControllerResponse.ParkStatus.PARKING_UNKNOWN)); + Assert.That(response.IsParking, Is.False); + } + + [Test] + public void ToStringTest() + { + ControllerResponse status = new(ecuBase!); + string expected = "Controller Response: Mode 2, 0.4A, 0km/h, 19°C, Parking: PARKING_ON"; + Assert.That(status.ToString(), Is.EqualTo(expected)); + } + + [Test] + public void InvalidPDUSize() + { + byte[] raw = [0xB6, 0x6B, 0xAA, 0xDA, 0x0B, 0x02, 0x00, 0x04, 0x00, 0x00, 0x13, 0x00, 0x00, 0x02, 0x01, 0xFF, 0x1C, 0x0D]; + BaseTelegram invalidTelegram = new(raw); + + var ex = Assert.Throws(() => new ControllerResponse(invalidTelegram)); + Assert.That(ex.Message, Is.EqualTo("Unexpected size")); + } + + [Test] + public void InvalidSourceOrDestination() + { + byte[] raw = [0xB6, 0x6B, 0xBB, 0xCC, 0x0A, 0x02, 0x00, 0x04, 0x00, 0x00, 0x13, 0x00, 0x00, 0x02, 0x01, 0x1C, 0x0D]; + BaseTelegram invalidTelegram = new(raw); + + var ex = Assert.Throws(() => new ControllerResponse(invalidTelegram)); + Assert.That(ex.Message, Is.EqualTo("Not a ControllerResponse telegram")); + } +} diff --git a/tests/ECUStatusTest.cs b/tests/ECUStatusTest.cs deleted file mode 100644 index 978fbc1..0000000 --- a/tests/ECUStatusTest.cs +++ /dev/null @@ -1,45 +0,0 @@ -namespace RS485_Monitor.tests; -using NUnit.Framework; - -public class ECUStatusTest -{ - private BaseTelegram? ecuBase; - - [SetUp] - public void Setup() - { - byte[] raw = new byte[] { 0xB6, 0x6B, 0xAA, 0xDA, 0x0A, 0x02, 0x00, 0x04, 0x00, 0x00, 0x13, 0x00, 0x00, 0x02, 0x01, 0x1C, 0x0D }; - ecuBase = new BaseTelegram(raw); - } - - [Test] - public void ConvertToEcuStatusSuccess() - { - ECUStatus status = new(ecuBase!); - Assert.That(status, Is.Not.Null); - } - - [Test] - public void ConvertToEcuStatusFail() - { - byte[] raw = new byte[] { 0xC5, 0x5C, 0xBA, 0xAA, 0x0E, 0x48, 0x00, 0x00, 0x00, 0x16, 0x19, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00, 0x40, 0x0A, 0x0D }; - BaseTelegram gsmTelegram = new(raw); - - Assert.Throws(() => new ECUStatus(gsmTelegram)); - } - - [Test] - public void CheckContent() - { - ECUStatus status = new(ecuBase!); - using (Assert.EnterMultipleScope()) - { - Assert.That(status.Mode, Is.EqualTo(2)); - Assert.That(status.Current, Is.EqualTo(0.4).Within(0.1)); - Assert.That(status.Speed, Is.EqualTo(0)); - Assert.That(status.Temperature, Is.EqualTo(0x13)); - Assert.That(status.IsParking, Is.EqualTo(true)); - } - - } -} diff --git a/tests/SpeedometerRequestTest.cs b/tests/SpeedometerRequestTest.cs new file mode 100644 index 0000000..b22b030 --- /dev/null +++ b/tests/SpeedometerRequestTest.cs @@ -0,0 +1,70 @@ +namespace RS485_Monitor.tests; +using NUnit.Framework; + +public class SpeedometerRequestTest +{ + private BaseTelegram? speedometerBase; + + [SetUp] + public void Setup() + { + byte[] raw = [0xC5, 0x5C, 0xBA, 0xAA, 0x0E, 0x34, 0x00, 0x00, 0x01, 0x0A, 0x26, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x48, 0x5E, 0x0D]; + speedometerBase = new BaseTelegram(raw); + } + + [Test] + public void ConvertToSpeedometerRequestSuccess() + { + SpeedometerRequest request = new(speedometerBase!); + Assert.That(request, Is.Not.Null); + } + + [Test] + public void ConvertToSpeedometerRequestFail_InvalidPDUSize() + { + byte[] raw = [0xC5, 0x5C, 0xBA, 0xAA, 0x0F, 0x34, 0x00, 0x00, 0x01, 0x0A, 0x26, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x48, 0x00, 0x5E, 0x0D]; + BaseTelegram invalidTelegram = new(raw); + + var ex = Assert.Throws(() => new SpeedometerRequest(invalidTelegram)); + Assert.That(ex.Message, Is.EqualTo("Unexpected size")); + } + + [Test] + public void ConvertToSpeedometerRequestFail_InvalidSourceOrDestination() + { + byte[] raw = [0xC5, 0x5C, 0xBC, 0xAA, 0x0E, 0x34, 0x00, 0x04, 0x01, 0x0A, 0x26, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x48, 0x5E, 0x0D]; + BaseTelegram invalidTelegram = new(raw); + + var ex = Assert.Throws(() => new SpeedometerRequest(invalidTelegram)); + Assert.That(ex.Message, Is.EqualTo("Not a SpeedometerRequest telegram")); + } + + [Test] + public void CheckContent() + { + // 0x34, 0x00, 0x00, 0x01, 0x0A, 0x26, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x48, 0x5E, 0x0D + SpeedometerRequest request = new(speedometerBase!); + using (Assert.EnterMultipleScope()) + { + Assert.That(request.Soc, Is.EqualTo(0x34)); + Assert.That(request.CtrlCurrent, Is.EqualTo(0.0).Within(0.1)); + Assert.That(request.Speed, Is.EqualTo(0)); + Assert.That(request.TempLevel, Is.EqualTo(0x01)); + Assert.That(request.Hour, Is.EqualTo(0x0A)); + Assert.That(request.Minutes, Is.EqualTo(0x26)); + Assert.That(request.ErrorValue.State, Is.EqualTo((ErrorCode.ErrorBits)0)); + Assert.That(request.State, Is.EqualTo(SpeedometerRequest.VehicleState.PARKING)); + Assert.That(request.Gear, Is.EqualTo(0x01)); + Assert.That(request.SpeedCtrlValue, Is.EqualTo(0x0001)); + Assert.That(request.Range, Is.EqualTo(0x48)); + } + } + + [Test] + public void ToStringTest() + { + SpeedometerRequest request = new(speedometerBase!); + string expected = "Speedometer Request: Soc 52%, Current 0A, Speed 0km/h, TempLvl 1, Time 10:38, Error 0, Vehicle State PARKING, Gear 1, Speed Ctrl 1, Range 72km"; + Assert.That(request.ToString(), Is.EqualTo(expected)); + } +} diff --git a/tests/SpeedometerResponseTest.cs b/tests/SpeedometerResponseTest.cs new file mode 100644 index 0000000..bb36122 --- /dev/null +++ b/tests/SpeedometerResponseTest.cs @@ -0,0 +1,49 @@ +namespace RS485_Monitor.tests; +using NUnit.Framework; + +public class SpeedometerResponseTest +{ + private BaseTelegram? speedometerBase; + + [SetUp] + public void Setup() + { + byte[] raw = [0xC5, 0x5C, 0xAA, 0xBA, 0x01, 0x00, 0x00, 0x0D]; + speedometerBase = new BaseTelegram(raw); + } + + [Test] + public void ConvertToSpeedometerResponseSuccess() + { + SpeedometerResponse response = new(speedometerBase!); + Assert.That(response, Is.Not.Null); + } + + [Test] + public void ConvertToSpeedometerResponseFail_InvalidPDUSize() + { + byte[] raw = [0xC5, 0x5C, 0xAA, 0xBA, 0x02, 0x00, 0x00, 0x00, 0x0D]; + BaseTelegram invalidTelegram = new(raw); + + var ex = Assert.Throws(() => new SpeedometerResponse(invalidTelegram)); + Assert.That(ex.Message, Is.EqualTo("Unexpected size")); + } + + [Test] + public void ConvertToSpeedometerResponseFail_InvalidSourceOrDestination() + { + byte[] raw = [0xC5, 0x5C, 0xBB, 0xCC, 0x01, 0x00, 0x00, 0x0D]; + BaseTelegram invalidTelegram = new(raw); + + var ex = Assert.Throws(() => new SpeedometerResponse(invalidTelegram)); + Assert.That(ex.Message, Is.EqualTo("Not a SpeedometerResponse telegram")); + } + + [Test] + public void ToStringTest() + { + SpeedometerResponse response = new(speedometerBase!); + string expected = "Speedometer Response"; + Assert.That(response.ToString(), Is.EqualTo(expected)); + } +}