using System; using System.Linq; using System.Collections.Generic; public static class CSAFEDictionary { public static readonly byte ExtendedFrameStartFlag = 0xF0; public static readonly byte StandardFrameStartFlag = 0xF1; public static readonly byte StopFrameFlag = 0xF2; public static readonly byte ByteStuffingFlag = 0xF3; public static readonly Dictionary> Cmd = new Dictionary> { // Standard Short Commands { "CSAFE_GETSTATUS_CMD", new List { 0x80, new int[] { } } }, { "CSAFE_RESET_CMD", new List { 0x81, new int[] { } } }, { "CSAFE_GOIDLE_CMD", new List { 0x82, new int[] { } } }, { "CSAFE_GOHAVEID_CMD", new List { 0x83, new int[] { } } }, { "CSAFE_GOINUSE_CMD", new List { 0x85, new int[] { } } }, { "CSAFE_GOFINISHED_CMD", new List { 0x86, new int[] { } } }, { "CSAFE_GOREADY_CMD", new List { 0x87, new int[] { } } }, { "CSAFE_BADID_CMD", new List { 0x88, new int[] { } } }, { "CSAFE_GETVERSION_CMD", new List { 0x91, new int[] { } } }, { "CSAFE_GETID_CMD", new List { 0x92, new int[] { } } }, { "CSAFE_GETUNITS_CMD", new List { 0x93, new int[] { } } }, { "CSAFE_GETSERIAL_CMD", new List { 0x94, new int[] { } } }, { "CSAFE_GETODOMETER_CMD", new List { 0x9B, new int[] { } } }, { "CSAFE_GETERRORCODE_CMD", new List { 0x9C, new int[] { } } }, { "CSAFE_GETTWORK_CMD", new List { 0xA0, new int[] { } } }, { "CSAFE_GETHORIZONTAL_CMD", new List { 0xA1, new int[] { } } }, { "CSAFE_GETCALORIES_CMD", new List { 0xA3, new int[] { } } }, { "CSAFE_GETPROGRAM_CMD", new List { 0xA4, new int[] { } } }, { "CSAFE_GETPACE_CMD", new List { 0xA6, new int[] { } } }, { "CSAFE_GETCADENCE_CMD", new List { 0xA7, new int[] { } } }, { "CSAFE_GETUSERINFO_CMD", new List { 0xAB, new int[] { } } }, { "CSAFE_GETHRCUR_CMD", new List { 0xB0, new int[] { } } }, { "CSAFE_GETPOWER_CMD", new List { 0xB4, new int[] { } } }, // Configuration / Set Commands { "CSAFE_AUTOUPLOAD_CMD", new List { 0x01, new int[] { 1 } } }, { "CSAFE_IDDIGITS_CMD", new List { 0x10, new int[] { 1 } } }, { "CSAFE_SETTIME_CMD", new List { 0x11, new int[] { 1, 1, 1 } } }, { "CSAFE_SETDATE_CMD", new List { 0x12, new int[] { 1, 1, 1 } } }, { "CSAFE_SETTIMEOUT_CMD", new List { 0x13, new int[] { 1 } } }, { "CSAFE_SETUSERCFG1_CMD", new List { 0x1A, new int[] { 0 } } }, { "CSAFE_SETTWORK_CMD", new List { 0x20, new int[] { 1, 1, 1 } } }, { "CSAFE_SETHORIZONTAL_CMD", new List { 0x21, new int[] { 2, 1 } } }, { "CSAFE_SETCALORIES_CMD", new List { 0x23, new int[] { 2 } } }, { "CSAFE_SETPROGRAM_CMD", new List { 0x24, new int[] { 1, 1 } } }, { "CSAFE_SETPOWER_CMD", new List { 0x34, new int[] { 2, 1 } } }, { "CSAFE_GETCAPS_CMD", new List { 0x70, new int[] { 1 } } }, // PM5 Specific Long Commands (Wrapper 0x1A) { "CSAFE_PM_GET_WORKOUTTYPE", new List { 0x89, new int[] { }, 0x1A } }, { "CSAFE_PM_GET_DRAGFACTOR", new List { 0xC1, new int[] { }, 0x1A } }, { "CSAFE_PM_GET_STROKESTATE", new List { 0xBF, new int[] { }, 0x1A } }, { "CSAFE_PM_GET_WORKTIME", new List { 0xA0, new int[] { }, 0x1A } }, { "CSAFE_PM_GET_WORKDISTANCE", new List { 0xA3, new int[] { }, 0x1A } }, { "CSAFE_PM_GET_ERRORVALUE", new List { 0xC9, new int[] { }, 0x1A } }, { "CSAFE_PM_GET_WORKOUTSTATE", new List { 0x8D, new int[] { }, 0x1A } }, { "CSAFE_PM_GET_WORKOUTINTERVALCOUNT", new List { 0x9F, new int[] { }, 0x1A } }, { "CSAFE_PM_GET_INTERVALTYPE", new List { 0x8E, new int[] { }, 0x1A } }, { "CSAFE_PM_GET_RESTTIME", new List { 0xCF, new int[] { }, 0x1A } }, { "CSAFE_PM_SET_SPLITDURATION", new List { 0x05, new int[] { 1, 4 }, 0x1A } }, // Fixed ID { "CSAFE_PM_GET_FORCEPLOTDATA", new List { 0x6B, new int[] { 1 }, 0x1A } }, // Fixed to 0x6B, 1 arg (block size) { "CSAFE_PM_SET_SCREENERRORMODE", new List { 0x27, new int[] { 1 }, 0x1A } }, { "CSAFE_PM_GET_HEARTBEATDATA", new List { 0x6C, new int[] { }, 0x1A } } // FIXED: No arguments required }; public static readonly Dictionary> Resp = new Dictionary> { { 0x80, new List { "CSAFE_GETSTATUS_CMD", new int[] { 0 } } }, { 0x81, new List { "CSAFE_RESET_CMD", new int[] { 0 } } }, { 0x82, new List { "CSAFE_GOIDLE_CMD", new int[] { 0 } } }, { 0x83, new List { "CSAFE_GOHAVEID_CMD", new int[] { 0 } } }, { 0x85, new List { "CSAFE_GOINUSE_CMD", new int[] { 0 } } }, { 0x86, new List { "CSAFE_GOFINISHED_CMD", new int[] { 0 } } }, { 0x87, new List { "CSAFE_GOREADY_CMD", new int[] { 0 } } }, { 0x88, new List { "CSAFE_BADID_CMD", new int[] { 0 } } }, { 0x91, new List { "CSAFE_GETVERSION_CMD", new int[] { 1, 1, 1, 2, 2 } } }, { 0x92, new List { "CSAFE_GETID_CMD", new int[] { -5 } } }, { 0x93, new List { "CSAFE_GETUNITS_CMD", new int[] { 1 } } }, { 0x94, new List { "CSAFE_GETSERIAL_CMD", new int[] { -9 } } }, { 0x9B, new List { "CSAFE_GETODOMETER_CMD", new int[] { 4, 1 } } }, { 0x9C, new List { "CSAFE_GETERRORCODE_CMD", new int[] { 3 } } }, { 0xA0, new List { "CSAFE_GETTWORK_CMD", new int[] { 1, 1, 1 } } }, { 0xA1, new List { "CSAFE_GETHORIZONTAL_CMD", new int[] { 2, 1 } } }, { 0xA3, new List { "CSAFE_GETCALORIES_CMD", new int[] { 2 } } }, { 0xA4, new List { "CSAFE_GETPROGRAM_CMD", new int[] { 1 } } }, { 0xA6, new List { "CSAFE_GETPACE_CMD", new int[] { 2, 1 } } }, { 0xA7, new List { "CSAFE_GETCADENCE_CMD", new int[] { 2, 1 } } }, { 0xAB, new List { "CSAFE_GETUSERINFO_CMD", new int[] { 2, 1, 1, 1 } } }, { 0xB0, new List { "CSAFE_GETHRCUR_CMD", new int[] { 1 } } }, { 0xB4, new List { "CSAFE_GETPOWER_CMD", new int[] { 2, 1 } } }, { 0x01, new List { "CSAFE_AUTOUPLOAD_CMD", new int[] { 0 } } }, { 0x10, new List { "CSAFE_IDDIGITS_CMD", new int[] { 0 } } }, { 0x11, new List { "CSAFE_SETTIME_CMD", new int[] { 0 } } }, { 0x12, new List { "CSAFE_SETDATE_CMD", new int[] { 0 } } }, { 0x13, new List { "CSAFE_SETTIMEOUT_CMD", new int[] { 0 } } }, { 0x1A, new List { "CSAFE_SETUSERCFG1_CMD", new int[] { 0 } } }, { 0x20, new List { "CSAFE_SETTWORK_CMD", new int[] { 0 } } }, { 0x21, new List { "CSAFE_SETHORIZONTAL_CMD", new int[] { 0 } } }, { 0x23, new List { "CSAFE_SETCALORIES_CMD", new int[] { 0 } } }, { 0x24, new List { "CSAFE_SETPROGRAM_CMD", new int[] { 0 } } }, { 0x34, new List { "CSAFE_SETPOWER_CMD", new int[] { 0 } } }, { 0x70, new List { "CSAFE_GETCAPS_CMD", new int[] { 11 } } }, { 0x1A89, new List { "CSAFE_PM_GET_WORKOUTTYPE", new int[] { 1 } } }, { 0x1AC1, new List { "CSAFE_PM_GET_DRAGFACTOR", new int[] { 1 } } }, { 0x1ABF, new List { "CSAFE_PM_GET_STROKESTATE", new int[] { 1 } } }, { 0x1AA0, new List { "CSAFE_PM_GET_WORKTIME", new int[] { 4, 1 } } }, { 0x1AA3, new List { "CSAFE_PM_GET_WORKDISTANCE", new int[] { 4, 1 } } }, { 0x1AC9, new List { "CSAFE_PM_GET_ERRORVALUE", new int[] { 2 } } }, { 0x1A8D, new List { "CSAFE_PM_GET_WORKOUTSTATE", new int[] { 1 } } }, { 0x1A9F, new List { "CSAFE_PM_GET_WORKOUTINTERVALCOUNT", new int[] { 1 } } }, { 0x1A8E, new List { "CSAFE_PM_GET_INTERVALTYPE", new int[] { 1 } } }, { 0x1ACF, new List { "CSAFE_PM_GET_RESTTIME", new int[] { 2 } } }, { 0x1A05, new List { "CSAFE_PM_SET_SPLITDURATION", new int[] { 0 } } }, { 0x1A6B, new List { "CSAFE_PM_GET_FORCEPLOTDATA", new int[] { -1 } } }, { 0x1A27, new List { "CSAFE_PM_SET_SCREENERRORMODE", new int[] { 0 } } }, { 0x1A6C, new List { "CSAFE_PM_GET_HEARTBEATDATA", new int[] { -1 } } } }; } public static class CSAFECommand { public static List Write(string[] arguments) { int i = 0; List message = new List(); List wrapper = new List(); int wrapped = 0; while (i < arguments.Length) { string argument = arguments[i]; int commandPropertiesIdInt = Convert.ToInt32(CSAFEDictionary.Cmd[argument][0]); int[] cmdProps = CSAFEDictionary.Cmd[argument][1] as int[]; int wrapperInt = 0; if (CSAFEDictionary.Cmd[argument].Count > 2) wrapperInt = Convert.ToInt32(CSAFEDictionary.Cmd[argument][2]); List command = new List(); if (cmdProps.Length != 0) { for (int bytes = 0; bytes < cmdProps.Length; bytes++) { i++; int intValue = int.Parse(arguments[i]); byte[] value = IntToBytes(intValue, cmdProps[bytes]); command.AddRange(value); } command.Insert(0, Convert.ToByte(command.Count)); } command.Insert(0, Convert.ToByte(commandPropertiesIdInt)); if (wrapperInt == 0x1A) { if (wrapped == 0) { wrapper.Insert(0, Convert.ToByte(wrapperInt)); wrapped = 1; } wrapper.AddRange(command); } else { message.AddRange(command); } i++; } if (wrapped == 1) { wrapper.Insert(1, Convert.ToByte(wrapper.Count - 1)); message.AddRange(wrapper); } int checksum = 0; for (int j = 0; j < message.Count; j++) { checksum ^= message[j]; } int k = 0; while (k < message.Count) { if (0xF0 <= message[k] && message[k] <= 0xF3) { byte stuffValue = Convert.ToByte(message[k] & 0x03); message[k] = CSAFEDictionary.ByteStuffingFlag; message.Insert(k + 1, stuffValue); k++; } k++; } if (0xF0 <= checksum && checksum <= 0xF3) { message.Add(CSAFEDictionary.ByteStuffingFlag); message.Add(Convert.ToByte(checksum & 0x03)); } else { message.Add(Convert.ToByte(checksum)); } message.Insert(0, CSAFEDictionary.StandardFrameStartFlag); message.Add(CSAFEDictionary.StopFrameFlag); return message; } public static Dictionary> Read(byte[] transmission) { Dictionary> response = new Dictionary>(); List message = transmission.ToList(); int startFlagIndex = message.IndexOf(CSAFEDictionary.StandardFrameStartFlag); int extStartFlagIndex = message.IndexOf(CSAFEDictionary.ExtendedFrameStartFlag); int startIndex = Math.Max(startFlagIndex, extStartFlagIndex); int stopFlagIndex = message.IndexOf(CSAFEDictionary.StopFrameFlag); if (startIndex == -1 || stopFlagIndex == -1 || stopFlagIndex < startIndex) return response; message = message.GetRange(startIndex + 1, stopFlagIndex - startIndex - 1); message = CheckMessage(message); if (message == null || message.Count == 0) return response; int k = 0; int msgState = message[k]; response["CSAFE_GETSTATUS_CMD"] = new List { msgState.ToString() }; k++; // NEW: Track the end index of the PM5 wrapper block int wrapperEndIndex = -1; while (k < message.Count) { try { int k_before = k; // Track where this specific command started int responseId = message[k]; k++; // Wrapper Check if (responseId == 0x1A) { if (k >= message.Count) break; int wrapperLength = message[k]; k++; if (wrapperLength > 0) { wrapperEndIndex = k + wrapperLength; // Mark the boundary of wrapped data if (k < message.Count) { responseId = message[k]; responseId = responseId | (0x1A << 8); k++; } } else { continue; } } // NEW: If we are still reading bytes that fall inside the wrapper length, // automatically apply the 0x1A PM5 prefix! else if (k_before < wrapperEndIndex) { responseId = responseId | (0x1A << 8); } if (!CSAFEDictionary.Resp.ContainsKey(responseId)) { if (k < message.Count) k += message[k] + 1; continue; } List commandResponse = CSAFEDictionary.Resp[responseId]; string commandName = commandResponse[0] as string; int[] commandByteCountArray = commandResponse[1] as int[]; List result = new List(); // SAFEGUARD: Prevent out of bounds if (k >= message.Count) break; int payloadLength = message[k]; k++; int bytesReadForCmd = 0; foreach (int commandByteCount in commandByteCountArray) { if (bytesReadForCmd >= payloadLength) break; int bytesToRead = commandByteCount >= 0 ? commandByteCount : (payloadLength - bytesReadForCmd); if (k + bytesToRead > message.Count) break; byte[] rawBytes = message.GetRange(k, bytesToRead).ToArray(); string value = ""; if (commandByteCount >= 0) { int intValue = 0; for (int b = 0; b < rawBytes.Length; b++) { intValue |= (rawBytes[b] << (8 * b)); } value = intValue.ToString(); } else { value = string.Join(",", rawBytes.Select(b => b.ToString())); } result.Add(value); k += bytesToRead; bytesReadForCmd += bytesToRead; } if (bytesReadForCmd < payloadLength) { k += (payloadLength - bytesReadForCmd); } response[commandName] = result; } catch (Exception) { break; } } return response; } private static List CheckMessage(List message) { int i = 0; int checksum = 0; while (i < message.Count) { if (message[i] == CSAFEDictionary.ByteStuffingFlag) { if (i + 1 >= message.Count) break; byte stuffValue = message[i + 1]; message.RemoveAt(i + 1); message[i] = Convert.ToByte(0xF0 | stuffValue); } checksum ^= message[i]; i++; } if (checksum != 0) return new List(); if (message.Count > 0) message.RemoveAt(message.Count - 1); return message; } private static byte[] IntToBytes(int value, int length) { byte[] result = new byte[length]; for (int i = 0; i < length; i++) { result[i] = (byte)((value >> (i * 8)) & 0xFF); } return result; } }