using System; using System.Collections.Generic; using System.IO.Ports; using System.Text; namespace SerialComms { internal class SerialConnector { private static SerialPort serial = new SerialPort(); // The serial port used for all communications private static Dictionary> commands = new Dictionary>(); // Stores all the names of the incomming commands and their callback functions public bool EnableDebugLogs { get; set; } // Enables or disables debugging console logs /// /// Initializes a new instance of the class with the specified debug logging /// option and baud rate. /// /// Use this constructor to configure the serial connection's baud rate and optionally /// enable debug logging for troubleshooting or development purposes. /// to enable detailed debug logging for serial communication; otherwise, . The default is . /// The baud rate to use for the serial connection. Must be a positive integer. The default is 9600. public SerialConnector(bool enableDebugLogs = false, int baudRate = 9600) { serial.BaudRate = baudRate; this.EnableDebugLogs = enableDebugLogs; } /// /// Sets the serial port to the specified port name and opens the connection for communication. /// /// If a serial port connection is already open, it is closed before setting the new port /// name and opening the connection. /// The name of the serial port to use (for example, "COM1" or "/dev/ttyUSB0"). public void SetSerialPort(string port) { // If a port is already open, close it first to ensure a clean state before reconfiguring. if (serial.IsOpen) serial.Close(); // Optional debug log to indicate which port will be used. if (EnableDebugLogs) Console.WriteLine("Setting serial port to: " + port); // Assign the requested port name to the SerialPort instance. serial.PortName = port; try { // Attempt to open the configured serial port. Opening can fail if the port doesn't exist // or is already in use by another process. if (EnableDebugLogs) Console.WriteLine("Attempting to open serial port"); serial.Open(); // Clear any leftover data in the input buffer so subsequent reads start fresh. serial.DiscardInBuffer(); if (EnableDebugLogs) Console.WriteLine("Opened serial port"); } catch (Exception ex) { throw ex; } } /// /// Processes incoming serial data, verifies its integrity, and executes the corresponding command if valid. /// /// This method reads available data from the serial buffer, validates the data using a /// check bit, acknowledges receipt, and dispatches the command for execution. If the data fails integrity /// checks or cannot be parsed, the method asks the sender to repeat the command or throws an exception. Debug /// logging is performed if enabled. /// Thrown if the check bit verification fails due to parsing errors or if command execution fails. public void Cycle() { // Only proceed if there is data waiting in the serial input buffer. if (serial.BytesToRead > 0) { if (EnableDebugLogs) Console.WriteLine("Starting to read from serial buffer"); // Read until the protocol terminator '@' is encountered. string raw = serial.ReadTo("@"); if (EnableDebugLogs) Console.WriteLine("Read: '" + raw + "' from serial buffer"); // Split the incoming packet into its three components: command, args, checkBit. string[] data = raw.Split('#'); if (EnableDebugLogs) Console.WriteLine("Split serial buffer in to: " + data[0] + ", " + data[1] + ", " + data[2]); try { if (EnableDebugLogs) Console.WriteLine("Attempting to verify check bit"); // Verify integrity using the provided check bit; parse the check bit to an integer. if (!VerifyCheckBit(data[0], data[1], Int32.Parse(data[2]))) { if (EnableDebugLogs) Console.WriteLine("Check failed"); // Ask the sender to repeat the command if the integrity check failed and return early. Repeat(); return; } } catch (Exception ex) { // Wrap parsing/verification errors with context and rethrow. throw new Exception("Failed to verify check bit. Was unable to parse command: " + raw + "\n" + ex.Message); } if (EnableDebugLogs) Console.WriteLine("Command integrity verified successfuly"); // Send an acknowledgement back to the sender using the parsed check bit. Acknowledge(Int32.Parse(data[2])); try { // If a handler for this command is registered, invoke it with the argument string. if (commands.ContainsKey(data[0])) commands[data[0]](data[1]); } catch { // Provide context that execution of the command handler failed. throw new Exception("Failed to get and execute command: " + data[0]); } } } /// /// Receives all three parts of an icomming packet and check for the commands integrity using it's check bit. /// /// The command /// The arguments to the command /// The check bit of the packet /// true if the packet is intact, false if it's malformed private bool VerifyCheckBit(string cmd, string args, int checkBit) { // Recreate the portion of the packet which was used to generate the original checksum. string toCheck = cmd + "#" + args; // Compute the numeric checksum and compare to the provided check bit. if (StringToCheckNum(toCheck) == checkBit) return true; else return false; } /// /// Generates a checksum for a given string /// /// The string for which a checksum is needed /// private int StringToCheckNum(string str) { // Initialize accumulator for checksum. int x = 0; // Convert the input string to bytes using the system default encoding. byte[] ba = Encoding.Default.GetBytes(str); // Convert the byte array to a hex string representation (e.g. "DE-AD-BE-EF"). var hexString = BitConverter.ToString(ba); // Remove the hyphens so the string is a contiguous sequence of hex digits. hexString = hexString.Replace("-", ""); // Sum the numeric value of each element in the hexString. foreach (byte bit in hexString) { x += bit; } // Return the computed checksum. return x; } /// /// Sends a command to the serial device to request repetition of the last operation. /// /// This method transmits a predefined repeat command to the connected serial device. If /// debug logging is enabled, a message is written to the console indicating that a repeat request has been /// sent. /// Thrown if the repeat command cannot be sent to the serial device. private void Repeat() { // Pre-built repeat packet according to the protocol. string cmd = "RPT##410@"; try { // Send the repeat request downstream. serial.Write(cmd); } catch (Exception ex) { // Wrap and rethrow serial write exceptions for callers to handle. throw new Exception("Failed to send RPT.\n" + ex.Message); } if (EnableDebugLogs) Console.WriteLine("Asking for repeat of last command"); } /// /// Sends an acknowledgment command with the specified check bit to the connected device. /// /// The check bit value to include in the acknowledgment command. This value is used to confirm receipt of a /// previous message or command. /// Thrown if the acknowledgment command cannot be sent to the device. private void Acknowledge(int checkBit) { // Build the acknowledge payload and compute its checksum to form a complete packet. string cmd = "ACKG"; string toSend = cmd + "#" + checkBit; int checkNum = StringToCheckNum(toSend); string final = toSend + "#" + checkNum + "@"; try { // Transmit the ACKG packet. serial.Write(final); } catch (Exception ex) { // Wrap and rethrow serial write exceptions for callers to handle. throw new Exception("Failed to send ACKG.\n" + ex.Message); } if (EnableDebugLogs) Console.WriteLine("Acknowledged last command: " + final); } /// /// Sends a command with the specified arguments to the connected device over the serial interface. /// /// The method formats the command and arguments according to the device protocol and /// transmits them over the serial connection. If debug logging is enabled, the formatted command is written to /// the console output. /// The command to send. Cannot be or empty. /// The arguments to include with the command. Cannot be ; may be empty if the command /// does not require arguments. /// An error occurred while sending the command or during post-send validation. public void SendCommand(string cmd, string args) { // Compose the command payload and compute its checksum to create the final packet string. string toSend = cmd + "#" + args; int checkNum = StringToCheckNum(toSend); string final = toSend + "#" + checkNum + "@"; try { // Send the final packet over serial. serial.Write(final); if (EnableDebugLogs) Console.WriteLine("Sending command: " + final); } catch (Exception ex) { // Provide context about the failed send and propagate the error. throw new Exception("Failed to send command: " + final + "\n" + ex.Message); } try { // Perform additional response handling after sending the command (e.g. check for ACKG or RPT). AfterSendCheck(final); } catch (Exception ex) { // Rethrow any errors produced by the after-send logic. throw new Exception(ex.Message); } } /// /// Performs post-send validation and handling based on the response received from the serial device after /// sending a command. /// /// This method reads the response from the serial device and processes it to determine /// whether the command should be repeated or acknowledged. If the response indicates a repeat request, the /// original command is resent. If the response is an acknowledgment, the method verifies the integrity of the /// acknowledgment using check values. /// The command string that was sent to the serial device. /// Thrown if the response from the serial device cannot be parsed, or if the check values in the acknowledgment /// cannot be converted to integers. private void AfterSendCheck(string cmd) { // Read the next response packet. string raw = serial.ReadTo("@"); // If an empty string was returned, attempt to read again recursively. if (raw == "") AfterSendCheck(cmd); if (EnableDebugLogs) Console.WriteLine("Performing after send checks"); // Split the incoming response into components. string[] data = raw.Split('#'); if (data.Length < 1) throw new Exception("Failed to parse Data after send packet"); if (EnableDebugLogs) Console.WriteLine("Data array: " + data[0] + ", " + data[1]); // Parse the original command that was sent (remove terminator, then split). string[] dataCmd = cmd.Replace("@", "").Split('#'); if (dataCmd.Length < 2) throw new Exception("Failed to parse DataCmd after send packet"); if (EnableDebugLogs) Console.WriteLine("DataCmd array: " + data[0] + ", " + data[1] + ", " + data[2]); // If the remote requested a repeat, resend the original command. if (data[0] == "RPT") { if (EnableDebugLogs) Console.WriteLine("Repeat command received"); SendCommand(dataCmd[0], dataCmd[1]); } else if (data[0] == "ACKG") { if (EnableDebugLogs) Console.WriteLine("Command acknowledged"); int check, cmdCheck; try { // Convert the received check value to an integer. check = Int32.Parse(data[1]); } catch { throw new Exception("Failed to convert check bit to int"); } try { // Extract the checksum portion of the original sent command for comparison. cmdCheck = Int32.Parse(dataCmd[2]); } catch { throw new Exception("Failed to convert checkCmd bit in to int"); } // Compare the two check values and optionally log the result. if (check != cmdCheck) { if (EnableDebugLogs) Console.WriteLine("Acknowledge check bad"); } else { if (EnableDebugLogs) Console.WriteLine("Acknowledge check good"); } } } /// /// Registers a callback to be invoked when the specified command is triggered. /// /// Use this method to associate a handler with a command name. Attempting to register /// the same command more than once will result in an exception. /// The name of the command to register. Cannot be null or empty. Must be unique among registered commands. /// The callback action to execute when the command is triggered. Cannot be null. /// Thrown if the command already exists public void OnCommand(string command, Action callback) { // Add the callback to the commands dictionary if it does not already exist. if (!commands.ContainsKey(command)) commands.Add(command, callback); else // Provide a clear exception if the command name is already registered. throw new Exception("Unable to register command: " + command + " as command already exists"); } /// /// Closes the serial port connection if it is currently open. /// /// Calling this method when the serial port is already closed has no effect. public void CloseSerial() { // Optionally log that the serial port is being closed. if (EnableDebugLogs) Console.WriteLine("Closing serial port"); // Only attempt to close if the port is currently open. if (serial.IsOpen) serial.Close(); } } }