From 36b07fc150682e6fc1f2e6655c0dbdb7bb2a1711 Mon Sep 17 00:00:00 2001 From: faragher <47290019+faragher@users.noreply.github.com> Date: Fri, 12 May 2023 16:31:25 -0500 Subject: [PATCH] Documenting, altering Namespaces --- Interfaces/Interface.cs | 12 + Interfaces/RNodeInterface.cs | 1512 ++++++++++++++++++++++++++++++++++ 2 files changed, 1524 insertions(+) create mode 100644 Interfaces/RNodeInterface.cs diff --git a/Interfaces/Interface.cs b/Interfaces/Interface.cs index 09712cc..e87de5a 100644 --- a/Interfaces/Interface.cs +++ b/Interfaces/Interface.cs @@ -25,6 +25,9 @@ namespace RNS { + /// + /// Initial Interface class + /// public class Interface { public bool IN = false; @@ -56,6 +59,9 @@ namespace RNS public int ifac_size; //List<> + /// + /// Basic initialization + /// public Interface() { rxb = 0; @@ -68,6 +74,9 @@ namespace RNS Callbacks = new CallbackClass(); } + /// + /// Arguments for callback event handler + /// public class CallbackArgs : EventArgs { public byte[] Message { get; private set; } @@ -79,6 +88,9 @@ namespace RNS } } + /// + /// Class for callback event handler + /// public class CallbackClass { //public delegate void CallbackEventHandler(object sender, CallbackArgs args); diff --git a/Interfaces/RNodeInterface.cs b/Interfaces/RNodeInterface.cs new file mode 100644 index 0000000..17f3fb2 --- /dev/null +++ b/Interfaces/RNodeInterface.cs @@ -0,0 +1,1512 @@ +// MIT License +// +// Copyright (c) 2016-2022 Mark Qvist / unsigned.io +// C# Port (c) 2023 Michael Faragher / betweentheborders.com +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +// This is considered pre-alpha of a minimum viable product. No warranties are provided and compatibility is not assured. + +using System; +using System.IO; +using System.IO.Ports; + +namespace RNS.Interfaces +{ + + public class RNodeInterface: RNS.Interface + { + SerialPort Port; + + int Timeout = 1000; + + + //RNode Serial Specs + int SerialSpeed = 115200; + int SerialDataBits = 8; + StopBits SerialStopBits = StopBits.One; + Parity SerialParity = Parity.None; + + + //Stack Specs + int HW_MTU = 508; + + //Network + List Packet_Queue = new List(); + string ID_Callsign = ""; + int ID_Interval = 0; + int Announce_Rate_Target; + + //RNode Fixed Config + int RSSI_Offset = 157; + const int FB_Pixel_Width = 64; + const int FB_Bits_Per_Pixel = 1; + const int FB_Pixels_Per_Byte = 8 / FB_Bits_Per_Pixel; + const int FB_Bytes_Per_Line = FB_Pixel_Width / FB_Pixels_Per_Byte; + const int MAX_CHUNK = 32768; + const int FREQ_MIN = 137000000; + const int FREQ_MAX = 1020000000; + const int CALLSIGN_MAX_LEN = 32; + const int REQUIRED_FW_VER_MAJ = 1; + const int REQUIRED_FW_VER_MIN = 52; + const int RECONNECT_WAIT = 5000; + + //RNode state + bool isOnline = false; + bool isDetected = false; + bool isDetached = false; + bool isReconnecting = false; + bool Firmware_OK = false; + bool Interface_Ready = false; + bool FlowControl = false; + DateTime? First_TX; + Thread ReceiveThread; + public string Name { get; private set; } = "Undefined Interface"; + RNS.Transport Owner; + int Last_ID; + int Reconnect_W; + bool isValidConfig = false; + bool should_ID = false; + + //Desired State + uint Frequency; + uint Bandwidth; + byte SF; + byte CR; + byte TXPower; + byte State; + + //Reported State + int R_Frequency; + int R_Bandwidth; + int R_SF; + byte R_CR; + byte R_TXPower; + byte R_State; + byte R_Lock; + int R_Stat_RX; + int R_Stat_TX; + int R_Stat_RSSI; + float R_Stat_SNR; + byte R_Random; + int Bitrate; + float Bitrate_kbps; + + int RXB; + int TXB; + + bool DebugOutput = true; + + //RNode Hard/firmware + int Maj_Version; + int Min_Version; + byte Platform; + byte MCU; + bool hasDisplay = false; + + /// + /// RNode initialization routine. + /// + /// Reticulum specific. NYI. + /// Interface name. Example: RNode on Server 3 + /// Port name. Example: COM6 or /dev/ttyAMC0 + /// Frequency in Hz. Example: 915000000 for a 915MHz signal. Note: You are responsible for selecting a band legal in your nation and municipality. + /// Bandwidth in Hz: Example: 125000 for 125kHz bandwidth. Acceptable values from 7800 to 500000. + /// Transmission power in dBm. Acceptable values range from 0 - 17. + /// Spreading factor. Acceptable values range from 7 - 12. + /// Coding rate, 4 data bits per N transmitted bits. Acceptable values range from 5 - 8. + /// I'm actually unsure. Will consult with stack designer. + /// Time, in seconds, between callsign broadcasts. 0 disables. + /// Station callsign. When a packet is sent, begins a cooldown of _id_interval seconds, then broadcasts station ID. Meant for amateur radio compliance. string = "" disables. + /// Thrown if platform is unsupported. + /// Thrown when interface contains errors. + /// Thrown if serial port is unavailable. + public RNodeInterface(RNS.Transport _owner, string _name, string _port, uint _frequency = 0, uint _bandwidth = 0, byte _txpower = 0, byte _sf = 0, byte _cr = 0, bool _flow_control = false, int _id_interval = 0, string _id_callsign = "") + { + if(System.Environment.OSVersion.Platform == PlatformID.Other) + { + throw new Exception("Invalid platform. Library currently supports Windows and Linux. If you see this message on either of these platforms, please file a bug report"); + } + + RXB = 0; + TXB = 0; + + Owner = _owner; + Name = _name; + Port = new SerialPort(_port); + + isOnline = false; + isDetached = false; + isReconnecting = false; + + Frequency = _frequency; + Bandwidth = _bandwidth; + TXPower = _txpower; + SF = _sf; + CR = _cr; + State = KISS.RADIO_STATE_OFF; + Bitrate = 0; + Platform = 0x00; + MCU = 0x00; + isDetected = false; + Firmware_OK = false; + Maj_Version = 0; + Min_Version = 0; + + Last_ID = 0; + First_TX = null; + Reconnect_W = RECONNECT_WAIT; + + R_Frequency = 0; + R_Bandwidth = 0; + R_TXPower = 0; + R_SF = 0; + R_CR = 0; + R_State = KISS.RADIO_STATE_OFF; + R_Lock = 0; + R_Stat_RX = 0; + R_Stat_TX = 0; + R_Stat_RSSI = 0; + R_Random = 0; + + Packet_Queue.Clear(); + FlowControl = _flow_control; + Interface_Ready = false; + Announce_Rate_Target = 0; + + isValidConfig = true; + if(Frequency < FREQ_MIN || Frequency > FREQ_MAX) + { + isValidConfig = false; + Console.WriteLine("Invalid frequency configured for " + name); + } + if (TXPower<0 ||TXPower>17) + { + isValidConfig = false; + Console.WriteLine("Invalid TX power configured for " + name); + } + if (Bandwidth < 7800 || Bandwidth > 500000) + { + isValidConfig = false; + Console.WriteLine("Invalid bandwidth configured for " + name); + } + if (SF< 7 || SF>12) + { + isValidConfig = false; + Console.WriteLine("Invalid spreading factor configured for " + name); + } + if (CR<5 || CR>8) + { + isValidConfig = false; + Console.WriteLine("Invalid coding rate configured for " + name); + } + if (_id_interval > 0 && _id_callsign != "") + { + if (ID_Callsign.Length <= CALLSIGN_MAX_LEN) + { + ID_Callsign = _id_callsign; + ID_Interval = _id_interval; + should_ID = true; + } + else + { + Console.WriteLine("The ID callsign for " + Name + " exceeds the max length of " + CALLSIGN_MAX_LEN.ToString()); + isValidConfig = false; + } + } + else + { + ID_Interval = 0; + ID_Callsign = ""; + } + + if (!isValidConfig) + { + throw new ArgumentException("The configuration for " + name + " contains errors; interface offline"); + } + + try + { + Port.Open(); + if (Port.IsOpen) + { + Configure_Device(); + } + else + { + throw new IOException("Could not open serial port " + Port.PortName); + } + } + catch (Exception e) + { + Console.WriteLine("Could not open serial port for interface " + Name); + Console.WriteLine("Contained exception was: "+e.ToString()); + Console.WriteLine("Will attempt to bring this interface up periodically"); + if(!isDetached && !isReconnecting) + { + Thread Reconnect = new Thread(Reconnect_Port); + Reconnect.Start(); + } + } + + + + } + /// + /// Built-In Test. Currently depreciated. + /// + /// True if BIT passed, false if failed. + //public bool BIT() + //{ + // Console.WriteLine("Beginning Built-In Test"); + // Console.WriteLine("Detecting serial ports:"); + // DetectRadio(); + // return true; + //} + + //public bool InitializeSerial(string TargetPort) + //{ + // CloseRadio(); + // ConfigureSerialPort(ref Port, TargetPort); + // Port.Open(); + // return true; + //} + + //public void Open_Port() + //{ + // Console.WriteLine("Opening serial port " + Port.PortName); + // Port.BaudRate = SerialSpeed; + // Port.DataBits = SerialDataBits; + // Port.Parity = SerialParity; + // Port.StopBits = SerialStopBits; + // Port.RtsEnable = false; + // Port.ReadTimeout = 250; + // Port.WriteTimeout = 250; + //} + + //void ConfigureRadio() + //{ + + //} + + /// + /// Sends frequency change request to radio. Uses Frequency variable. + /// + /// Thrown if transmission error. + void setFrequency() + { + byte c1 = (byte)(Frequency >> 24); + byte c2 = (byte)((byte)(Frequency >> 16) & 0xFF); + byte c3 = (byte)((byte)(Frequency >> 8) & 0xFF); + byte c4 = (byte)((byte)(Frequency) & 0xFF); + List data = new List() { c1, c2, c3, c4 }; + data = KISS.Escape(data); + byte[] Command = new byte[data.Count + 3]; + for (int i = 2; i < data.Count; i++) + { + Command[i] = (byte)data[i - 2]; + } + Command[0] = KISS.FEND; + Command[Command.Length - 1] = KISS.FEND; + Command[1] = KISS.CMD_FREQUENCY; + try + { + Port.Write(Command, 0, Command.Length); + } + catch + { + throw new IOException("An IO error occurred while configuring frequency for " + Port.PortName); + } + + } + /// + /// Sends bandwidth change request to radio. Uses Bandwidth variable. + /// + /// Thrown if transmission error. + void setBandwidth() + { + + byte c1 = (byte)(Bandwidth >> 24); + byte c2 = (byte)((byte)(Bandwidth >> 16) & 0xFF); + byte c3 = (byte)((byte)(Bandwidth >> 8) & 0xFF); + byte c4 = (byte)((byte)(Bandwidth) & 0xFF); + List data = new List() { c1, c2, c3, c4 }; + data = KISS.Escape(data); + byte[] Command = new byte[data.Count + 3]; + for (int i = 2; i < data.Count; i++) + { + Command[i] = (byte)data[i - 2]; + } + Command[0] = KISS.FEND; + Command[Command.Length - 1] = KISS.FEND; + Command[1] = KISS.CMD_BANDWIDTH; + try + { + Port.Write(Command, 0, Command.Length); + } + catch + { + throw new IOException("An IO error occurred while configuring bandwidth for " + Port.PortName); + } + } + /// + /// Sends transmission power change request to radio. Uses TXPower variable. + /// + /// Thrown if transmission error. + void setTXPower() + { + byte[] Command = new byte[] { KISS.FEND, KISS.CMD_TXPOWER, TXPower, KISS.FEND }; + try + { + Port.Write(Command, 0, Command.Length); + } + catch + { + throw new IOException("An IO error occurred while configuring TX power for " + Port.PortName); + } + } + + /// + /// Sends spreading factor change request to radio. Uses SF variable. + /// + /// Thrown if transmission error. + void setSpreadingFactor() + { + byte[] Command = new byte[] { KISS.FEND, KISS.CMD_SF, SF, KISS.FEND }; + try + { + Port.Write(Command, 0, Command.Length); + } + catch + { + throw new IOException("An IO error occurred while configuring spreading factor for " + Port.PortName); + } + } + + /// + /// Sends coding rate change request to radio. Uses CR variable. + /// + /// Thrown if transmission error. + void setCodingRate() + { + byte[] Command = new byte[] { KISS.FEND, KISS.CMD_CR, CR, KISS.FEND }; + try + { + Port.Write(Command, 0, Command.Length); + } + catch + { + throw new IOException("An IO error occurred while configuring coding rate for " + Port.PortName); + } + } + + /// + /// Sets State variable and send radio state change command. + /// + /// Target radio state + /// Thrown if a transmission error occurs + void setRadioState(byte TargetState) + { + State = TargetState; + byte[] Command = new byte[] { KISS.FEND, KISS.CMD_RADIO_STATE, TargetState, KISS.FEND }; + try + { + Port.Write(Command, 0, Command.Length); + } + catch + { + throw new IOException("An IO error occurred while configuring radio state for " + Port.PortName); + } + } + + /// + /// Sweeps serial ports looking for a radio. Non-API. Depreciated. + /// + /// Null string + //string DetectRadio() + //{ + // CloseRadio(); + // string[] DetectedPorts = SerialPort.GetPortNames(); + // SerialPort TestPort = new SerialPort(); + // if (DetectedPorts == null || DetectedPorts.Length == 0) { + // Console.WriteLine("No serial ports detected"); + // return null; + // } + // foreach (string DetectedPort in DetectedPorts) + // { + // try + // { + // Console.WriteLine(DetectedPort); + // ConfigureSerialPort(ref TestPort, DetectedPort); + // TestPort.Open(); + // Console.WriteLine("Port " + DetectedPort + " opened."); + // Console.WriteLine("Interrogating:"); + // SendDetectCommand(TestPort); + // ReceiveTestResponse(TestPort); + // TestPort.Close(); + // Console.WriteLine(DetectedPort + " closed."); + // } + // catch + // { + // Console.WriteLine(DetectedPort + " not an RNode or non-functional"); + // } + + // } + // return null; + //} + + /// + /// Sends a detect request to the RNode. Non-API. Depreciated. + /// + /// + /// True if command sent successfully. False otherwise. + /// Thrown if serial communication fails. + bool SendDetectCommand(SerialPort Ser) + { + byte[] Command = { KISS.FEND, KISS.CMD_DETECT, KISS.DETECT_REQ, KISS.FEND, KISS.CMD_FW_VERSION, 0x00, KISS.FEND, KISS.CMD_PLATFORM, 0x00, KISS.FEND, KISS.CMD_MCU, 0x00, KISS.FEND }; + try + { + Ser.Write(Command, 0, Command.Length); + return true; + } + catch + { + throw new IOException("An IO error occurred while detecting hardware for " + Port.PortName.ToString()); + return false; + } + } + + /// + /// Simple hook for manual packet transmission. Non-API. + /// + /// Raw message in an array of bytes. + public void Send(byte[] data) + { + Process_Outgoing(data); + } + + //void SendFrequencyTest(SerialPort Ser) + //{ + // byte[] Command = { KISS.FEND, KISS.CMD_FREQUENCY, 0x36, 0x89, 0xCA, 0xDB, 0xDC, KISS.FEND }; + // Console.Write("Frequency: Sending "); + // foreach (byte b in Command) + // { + // Console.Write(b.ToString("X") + " "); + // } + // Console.WriteLine(); + + //} + + /// + /// Leave radio + /// + /// Thrown on serial error + void Leave() + { + byte[] Command = new byte[] { KISS.FEND, KISS.CMD_LEAVE, 0xFF, KISS.FEND }; + try + { + Port.Write(Command, 0, Command.Length); + } + catch + { + throw new IOException("An IO error occurred while sending host left command to device"); + + } + } + + /// + /// Hard resets the RNode, then waits for 225ms + /// + /// Thrown on serial error + void Hard_Reset() + { + byte[] Command = new byte[] { KISS.FEND, KISS.CMD_RESET, 0xF8, KISS.FEND }; + try + { + Port.Write(Command, 0, Command.Length); + } + catch + { + throw new IOException("An IO error occurred while restarting device"); + } + Thread.Sleep(225); + } + + /// + /// Disables RNode backlight. Non-API, but planned for next Python release. Will be brought into line after release. + /// + /// Thrown on serial error + public void DisableBacklight() + { + + byte[] Command = { KISS.FEND, 0x45, 0x00, KISS.FEND }; + if (!isOnline) return; + try + { + Port.Write(Command, 0, Command.Length); + } + catch + { + throw new IOException("Cannot disable backlight on " + Port.PortName.ToString()); + } + } + + /// + /// Enables RNode backlight. Non-API, but planned for next Python release. Will be brought into line after release. + /// + /// Thrown on serial error + public void EnableBacklight() + { + + byte[] Command = { KISS.FEND, 0x45, 0x01, KISS.FEND }; + if (!isOnline) return; + try + { + Port.Write(Command, 0, Command.Length); + } + catch + { + throw new IOException("Cannot enable backlight on " + Port.PortName.ToString()); + } + } + + //void ReceiveRawResponse(SerialPort Ser) + //{ + // Console.WriteLine("Begin raw receive"); + // bool isInFrame = false; + // List Buffer = new List(); + // DateTime TimeToFail = DateTime.UtcNow; + // TimeToFail = TimeToFail.AddSeconds(2); + // while (Ser.BytesToRead == 0 && TimeToFail > DateTime.UtcNow) + // { + // System.Threading.Thread.Sleep(100); + // } + // while (Ser.BytesToRead > 0) + // { + // byte b = (byte)Ser.ReadByte(); + // Console.Write(b.ToString("X") + " "); + + // } + // Console.WriteLine(); + // Console.WriteLine("End raw receive"); + //} + + //void ReceiveTestResponse(SerialPort Ser) + //{ + // bool isInFrame = false; + // List Buffer = new List(); + // DateTime TimeToFail = DateTime.UtcNow; + // TimeToFail = TimeToFail.AddSeconds(2); + // while (Ser.BytesToRead == 0 && TimeToFail > DateTime.UtcNow) + // { + // System.Threading.Thread.Sleep(100); + // } + // while (Ser.BytesToRead > 0) + // { + // byte b = (byte)Ser.ReadByte(); + // if (!isInFrame && b == 0xc0) + // { + // isInFrame = true; + // } + // else if (isInFrame && b == 0xc0) + // { + // isInFrame = false; + // } + // else if (!isInFrame) + // { + // Console.WriteLine("Error: Byte received out of frame"); + // } + // else if (isInFrame) + // { + // if (b == 0x08) + // { + // Console.Write("Detect sent: "); + // if (Ser.ReadByte() == 0x46) + // { + // Console.WriteLine("Proper response received."); + // } + // else + // { + // Console.WriteLine("Inappropriate response received."); + // } + + // } + + // if (b == 0x50) + // { + // try + // { + // Console.WriteLine("Firmware version: " + Ser.ReadByte().ToString() + "." + Ser.ReadByte().ToString()); + // } + // catch + // { + // Console.WriteLine("Error reading firmware version"); + // } + // } + + // if (b == 0x48) + // { + // Console.Write("Platform: "); + // switch (Ser.ReadByte()) + // { + // case 0x80: + // Console.WriteLine("ESP32"); + // break; + // case 0x90: + // Console.WriteLine("AVR"); + // break; + // default: + // Console.WriteLine("Undefined"); + // break; + // } + // } + + // if (b == 0x49) + // { + // Console.Write("MCU: "); + // switch (Ser.ReadByte()) + // { + // case 0x81: + // Console.WriteLine("ESP32"); + // break; + // case 0x91: + // Console.WriteLine("1284P"); + // break; + // case 0x92: + // Console.WriteLine("2560"); + // break; + // default: + // Console.WriteLine("Undefined"); + // break; + // } + // } + + // //Console.Write(b); + // //Console.Write(" "); + // } + // } + // Console.WriteLine(); + //} + + /// + /// Sends packet data to callback event + /// + /// Raw packet data, in a list of bytes + void ProcessIncoming(List Payload) + { + RXB += Payload.Count; + //if (DebugOutput) + //{ + // Console.WriteLine("Data received:"); + // foreach (byte b in Payload) + // { + // Console.Write((char)b); + // } + // Console.WriteLine(""); + // Console.WriteLine("RSSI: " + R_Stat_RSSI.ToString()); + // Console.WriteLine("SNR: " + R_Stat_SNR.ToString()); + //} + Callbacks.Process_Inbound(Payload.ToArray(), this); + R_Stat_RSSI = 0; + R_Stat_SNR = 0; + } + + //void ConfigureSerialPort(ref SerialPort PortOfInterest, string TargetPortName) + //{ + // PortOfInterest = new SerialPort(TargetPortName, SerialSpeed, Parity.None, SerialDataBits, SerialStopBits); + //} + + /// + /// Calculates bitrate based on radio configuration. Uses radio response data, not configuration data + /// + void UpdateBitrate() + { + try + { + Bitrate = (int)(R_SF * 1000 * (4.0f / R_CR) / (MathF.Pow(2, R_SF) / (R_Bandwidth / 1000))); + if (Bitrate <= 0) + { + Bitrate = 0; + return; + } + + Bitrate_kbps = MathF.Round(Bitrate / 1000.0f, 2); + Console.WriteLine("On air bitrate is now " + Bitrate_kbps.ToString() + " kbps"); + } + catch + { + Bitrate = 0; + } + } + + /// + /// Starts and monitors a thread to attempt automatic reconnection of a disconnected RNode + /// + void Reconnect_Port() + { + isReconnecting = true; + while (!isOnline && !isDetached) + { + try + { + Thread.Sleep(5000); + Console.WriteLine("Attempting to reconnect serial port " + Port.PortName + " for " + Name); + Port.Open(); + if (Port.IsOpen) + { + Configure_Device(); + } + } + + catch (Exception ex) + { + Console.WriteLine("Error while reconnecting port. The contained exception was: "+ex.Message); + } + } + isReconnecting = false; + if (isOnline) + { + Console.WriteLine("Reconnected port "+Port.PortName+" for "+Name); + } + + + } + + + /// + /// Closes the radio port. Non-API. Pending depreciation + /// + public void CloseRadio() + { + if (Port == null) { return; } + if (Port.IsOpen) + { + Port.Close(); + } + } + + /// + /// Add message to transmit queue + /// + /// Message data in an array of bytes + public void Queue(byte[] Payload) + { + Packet_Queue.Add(Payload); + } + + //public void Force_Queue() + //{ + // Process_Queue(); + //} + + /// + /// Attemps to send configuration data to the radio. Aborts configuration if radio does not return the configuration. + /// + public void Configure_Device() + { + R_Frequency = 0; + R_Bandwidth = 0; + R_TXPower = 0; + R_SF = 0; + R_CR = 0; + R_State = 0; + R_Lock = 0; + + Port.BaudRate = SerialSpeed; + Port.DataBits = SerialDataBits; + Port.Parity = SerialParity; + Port.StopBits = SerialStopBits; + Port.RtsEnable = false; + Port.ReadTimeout = 250; + Port.WriteTimeout = 250; + + Thread.Sleep(2000); + + ReceiveThread = new Thread(ReceiveLoop); + ReceiveThread.Start(); + + Detect(); + Thread.Sleep(200); + + if (!isDetected) + { + Console.WriteLine("Could not detect device for " + Port.PortName); + Port.Close(); + return; + } + else + { + if(Platform == KISS.PLATFORM_ESP32) { hasDisplay = true; } + } + Console.WriteLine("Serial port " + Port.PortName + " is now open"); + Console.WriteLine("Configuring RNode interface..."); + InitRadio(); + if (ValidateRadioState()) + { + Interface_Ready = true; + Console.WriteLine(Name+" is configured and powered up"); + Thread.Sleep(300); + isOnline = true; + } + else + { + Console.WriteLine("After configuring " + Name + " the reported radio parameters did not match your configuration."); + Console.WriteLine("Make sure that your hardware actually supports the parameters specified in the configuration"); + Console.WriteLine("Aborting RNode startup"); + Port.Close(); + return; + } + } + + //public void Send_Plaintext(string Payload) + //{ + + // Queue(System.Text.Encoding.ASCII.GetBytes(Payload)); + + //} + + /// + /// Pop a packey from the queue, send it to Process_Outgoing, and mark the interface ready. + /// + void Process_Queue() + { + if (Packet_Queue.Count > 0) + { + byte[] data = Packet_Queue[0]; + Packet_Queue.RemoveAt(0); + Interface_Ready = true; + Process_Outgoing(data); + } + else if (Packet_Queue.Count == 0) + { + Interface_Ready = true; + } + } + + + /// + /// Applies FlowControl, processes ID_Callsign transmissions, escapes the data, and writes the packet to the serial port. If the interface is not ready, it re-queues it. + /// + /// + /// + void Process_Outgoing(byte[] Payload) + { + int DataLen = Payload.Length; + if (isOnline) + { + //Console.WriteLine("Online!"); + if (Interface_Ready) + { + if (FlowControl) + { + Interface_Ready = false; + } + if (Payload.SequenceEqual(UTF8_to_Bytes(ID_Callsign))) + { + First_TX = null; + } + else + { + if (First_TX == null) + { + First_TX = DateTime.UtcNow; + } + } + int datalen = Payload.Length; + Payload = KISS.Escape(Payload.ToList()).ToArray(); + byte[] Output = new byte[Payload.Length + 3]; + Payload.CopyTo(Output, 2); + Output[0] = 0xC0; + Output[Output.Length - 1] = 0xC0; + try + { + Port.Write(Output, 0, Output.Length); + TXB += datalen; + } + catch + { + throw new IOException("Process_Outgoing has failed."); + } + + } + else + { + Queue(Payload); + } + } + } + + /// + /// Main receive loop. Processes incoming control and data packets, writing to registers or passing off to an event handler as appropriate + /// + /// Thrown on serial error + public void ReceiveLoop() + { + Console.WriteLine("ReceiveLoop running"); + if (Port == null || !Port.IsOpen) + { + Console.WriteLine("Port is not properly initialized"); + return; + } + bool isInFrame = false; + bool Escape = false; + byte Command = KISS.CMD_UNKNOWN; + byte Buffer; + List DataBuffer = new List(); + List CommandBuffer = new List(); + DateTime Last_Read = DateTime.UtcNow; + while (Port.IsOpen) + { + if (Port.BytesToRead > 0) + { + Buffer = (byte)Port.ReadByte(); + if (isInFrame && Buffer == KISS.FEND && Command == KISS.CMD_DATA) + { + isInFrame = false; + ProcessIncoming(DataBuffer); + DataBuffer.Clear(); + CommandBuffer.Clear(); + } + else if (Buffer == KISS.FEND) + { + isInFrame = true; + Command = KISS.CMD_UNKNOWN; + DataBuffer.Clear(); + CommandBuffer.Clear(); + } + else if (isInFrame && DataBuffer.Count < HW_MTU) + { + if (DataBuffer.Count == 0 && Command == KISS.CMD_UNKNOWN) { Command = Buffer; } + else if (Command == KISS.CMD_DATA) + { + if (Buffer == KISS.FESC) { Escape = true; } + else + { + if (Escape) + { + if (Buffer == KISS.TFEND) { Buffer = KISS.FEND; } + if (Buffer == KISS.TFESC) { Buffer = KISS.FESC; } + Escape = false; + } + DataBuffer.Add(Buffer); + } + } + else if (Command == KISS.CMD_FREQUENCY) + { + if (Buffer == KISS.FESC) { Escape = true; } + else + { + if (Escape) + { + if (Buffer == KISS.TFEND) { Buffer = KISS.FEND; } + if (Buffer == KISS.TFESC) { Buffer = KISS.FESC; } + Escape = false; + } + CommandBuffer.Add(Buffer); + if (CommandBuffer.Count == 4) + { + R_Frequency = CommandBuffer[0] << 24 | CommandBuffer[1] << 16 | CommandBuffer[2] << 8 | CommandBuffer[3]; + Console.WriteLine("Radio reporting frequency is " + (R_Frequency / 1000000.0).ToString() + "MHz"); + UpdateBitrate(); + } + } + } + else if (Command == KISS.CMD_BANDWIDTH) + { + if (Buffer == KISS.FESC) { Escape = true; } + else + { + if (Escape) + { + if (Buffer == KISS.TFEND) { Buffer = KISS.FEND; } + if (Buffer == KISS.TFESC) { Buffer = KISS.FESC; } + Escape = false; + } + CommandBuffer.Add(Buffer); + if (CommandBuffer.Count == 4) + { + R_Bandwidth = CommandBuffer[0] << 24 | CommandBuffer[1] << 16 | CommandBuffer[2] << 8 | CommandBuffer[3]; + Console.WriteLine("Radio reporting bandwidth is " + (R_Bandwidth / 1000.0).ToString() + "KHz"); + UpdateBitrate(); + } + } + } + else if (Command == KISS.CMD_TXPOWER) + { + R_TXPower = Buffer; + Console.WriteLine("Radio reporting TX power is " + R_TXPower.ToString() + " dBm"); + } + else if (Command == KISS.CMD_SF) + { + R_SF = Buffer; + Console.WriteLine("Radio reporting spreading factor is " + R_SF.ToString()); + } + else if (Command == KISS.CMD_CR) + { + R_CR = Buffer; + Console.WriteLine("Radio reporting coding rate is " + R_CR.ToString()); + } + else if (Command == KISS.CMD_RADIO_STATE) + { + R_State = Buffer; + if (R_State == 0) { + Console.WriteLine(Name + ": Radio reporting state is offline."); + } + } + else if (Command == KISS.CMD_RADIO_LOCK) + { + R_Lock = Buffer; + } + else if (Command == KISS.CMD_FW_VERSION) + { + if (Buffer == KISS.FESC) { Escape = true; } + else + { + if (Escape) + { + if (Buffer == KISS.TFEND) { Buffer = KISS.FEND; } + if (Buffer == KISS.TFESC) { Buffer = KISS.FESC; } + Escape = false; + } + CommandBuffer.Add(Buffer); + if (CommandBuffer.Count == 2) + { + Maj_Version = (int)CommandBuffer[0]; + Min_Version = (int)CommandBuffer[1]; + Validate_Firmware(); + } + + } + } + else if (Command == KISS.CMD_STAT_RX) + { + if (Buffer == KISS.FESC) { Escape = true; } + else + { + if (Escape) + { + if (Buffer == KISS.TFEND) { Buffer = KISS.FEND; } + if (Buffer == KISS.TFESC) { Buffer = KISS.FESC; } + Escape = false; + } + CommandBuffer.Add(Buffer); + if (CommandBuffer.Count == 4) + { + R_Stat_RX = CommandBuffer[0] << 24 | CommandBuffer[1] << 16 | CommandBuffer[2] << 8 | CommandBuffer[3]; + } + } + } + else if (Command == KISS.CMD_STAT_TX) + { + if (Buffer == KISS.FESC) { Escape = true; } + else + { + if (Escape) + { + if (Buffer == KISS.TFEND) { Buffer = KISS.FEND; } + if (Buffer == KISS.TFESC) { Buffer = KISS.FESC; } + Escape = false; + } + CommandBuffer.Add(Buffer); + if (CommandBuffer.Count == 4) + { + R_Stat_TX = CommandBuffer[0] << 24 | CommandBuffer[1] << 16 | CommandBuffer[2] << 8 | CommandBuffer[3]; + } + } + } + else if (Command == KISS.CMD_STAT_RSSI) + { + R_Stat_RSSI = Buffer - RSSI_Offset; + } + else if (Command == KISS.CMD_STAT_SNR) + { + R_Stat_SNR = (float)(sbyte)Buffer / 4.0f; + } + else if (Command == KISS.CMD_RANDOM) + { + R_Random = Buffer; + } + else if (Command == KISS.CMD_PLATFORM) + { + Platform = Buffer; + } + else if (Command == KISS.CMD_MCU) + { + MCU = Buffer; + } + else if (Command == KISS.CMD_ERROR) + { + if (Buffer == KISS.ERROR_INITRADIO) + { + throw new IOException("Radio initialization failure"); + } + else if (Buffer == KISS.ERROR_TXFAILED) + { + throw new IOException("Hardware transmit failure"); + } + else + { + throw new IOException("Unknown hardware failure"); + } + } + else if (Command == KISS.CMD_RESET) + { + if (Buffer == 0xF8 && Platform == KISS.PLATFORM_ESP32 && isOnline) + { + throw new IOException("ESP32 reset while device online"); + } + } + else if (Command == KISS.CMD_READY) + { + Console.WriteLine("Received CMD_READY"); + Process_Queue(); + } + else if (Command == KISS.CMD_DETECT) + { + if (Buffer == KISS.DETECT_RESP) + { + isDetected = true; + } + else + { + isDetected = false; + } + } + } + } + else + { + if (DataBuffer.Count > 0 && DateTime.UtcNow > Last_Read.AddMilliseconds(Timeout)) + { + Console.WriteLine("Serial Read Timeout"); + DataBuffer.Clear(); + isInFrame = false; + Command = KISS.CMD_UNKNOWN; + Escape = false; + } + + if (ID_Interval > 0 && ID_Callsign != null) + { + if(First_TX != null) + { + if (DateTime.UtcNow > First_TX.Value.AddSeconds(ID_Interval)) + { + Console.WriteLine("Interface " + Name + " is transmitting beacon data: " + ID_Callsign.ToString()); + Process_Outgoing(UTF8_to_Bytes(ID_Callsign)); + } + } + } + + System.Threading.Thread.Sleep(80); + } + } + Console.WriteLine("Receive loop terminated."); + + } + + /// + /// Utility function. Non-API. Takes C# string in UTF-8 format and converts it to an array of bytes + /// + /// Data string in UTF-8 + /// Byte array representation of data string + + byte[] UTF8_to_Bytes(string Input) + { + return System.Text.Encoding.UTF8.GetBytes(Input); + } + + /// + /// Detects the RNode and its firmware version + /// + /// Thrown on serial error + public void Detect() + { + //isDetected = SendDetectCommand(Port); + byte[] Command = { KISS.FEND, KISS.CMD_DETECT, KISS.DETECT_REQ, KISS.FEND, KISS.CMD_FW_VERSION, 0x00, KISS.FEND, KISS.CMD_PLATFORM, 0x00, KISS.FEND, KISS.CMD_MCU, 0x00, KISS.FEND }; + try + { + Port.Write(Command, 0, Command.Length); + } + catch + { + throw new IOException("An IO error occurred while detecting hardware for " + Port.PortName.ToString()); + } + } + + /// + /// Detects if RNode firmware version is above listed requirements + /// + void Validate_Firmware() + { + if(Maj_Version>=REQUIRED_FW_VER_MAJ && Min_Version >= REQUIRED_FW_VER_MIN) + { + Firmware_OK = true; + return; + } + + Console.WriteLine("The firmware version of the connected RNode is "+Maj_Version.ToString()+ "."+Min_Version.ToString()); + Console.WriteLine("This library requires at least version " + Maj_Version.ToString() + "." + Min_Version.ToString()); + Console.WriteLine("Please update your RNode firmware with rnodeconf from https://github.com/markqvist/rnodeconfigutil/"); + //ToDo RNS.Panic(); + } + + /// + /// Detects if the radio's reported state matches, within tolerance, the desired configuration. Frequency is the only variable with a tolerance: +/- 500 Hz. Sets isValidConfig as well as returns a boolean + /// + /// True if state is valid, else false + public bool ValidateRadioState() + { + isValidConfig = true; + Console.WriteLine("Waiting for config validation"); + System.Threading.Thread.Sleep(250); + if (R_Frequency > 0 && Math.Abs(R_Frequency - Frequency) > 500) + { + isValidConfig = false; + Console.WriteLine("Frequency mismatch"); + Console.WriteLine("Expected " + Frequency.ToString() + " - got " + R_Frequency.ToString()); + } + if (R_Bandwidth != Bandwidth) + { + isValidConfig = false; + Console.WriteLine("Bandwidth mismatch"); + Console.WriteLine("Expected " + Bandwidth.ToString() + " - got " + R_Bandwidth.ToString()); + } + if (R_TXPower != TXPower) + { + isValidConfig = false; + Console.WriteLine("TX power mismatch"); + Console.WriteLine("Expected " + TXPower.ToString() + " - got " + R_TXPower.ToString()); + } + if (R_SF != SF) + { + isValidConfig = false; + Console.WriteLine("Spreading factor mismatch"); + Console.WriteLine("Expected " + SF.ToString() + " - got " + R_SF.ToString()); + } + if (R_State != State) + { + isValidConfig = false; + Console.WriteLine("Radio state mismatch"); + } + return isValidConfig; + } + + /// + /// Sends radio initialization commands + /// + public void InitRadio() + { + setFrequency(); + setBandwidth(); + setTXPower(); + setSpreadingFactor(); + setCodingRate(); + setRadioState(KISS.RADIO_STATE_ON); + } + + //public void DefineRatio(uint CenterFrequency, uint BW, byte Code_Rade, byte SpreadFactor, byte TX_Power) + //{ + // Frequency = CenterFrequency; + // Bandwidth = BW; + // SF = SpreadFactor; + // CR = Code_Rade; + // TXPower = TX_Power; + + //} + + /// + /// Detaches radio + /// + public void Detach() + { + isDetached = true; + Disable_External_Framebuffer(); + setRadioState(KISS.RADIO_STATE_OFF); + Leave(); + } + + /// + /// Disables the external framebuffer, returnig the RNode display graphic to internal control + /// + /// Thrown on serial error + + public void Disable_External_Framebuffer() + { + if (hasDisplay && isOnline) + { + byte[] Command = new byte[] { KISS.FEND, KISS.CMD_FB_EXT, 0x00, KISS.FEND }; + try + { + Port.Write(Command, 0, Command.Length); + } + catch + { + throw new IOException("An IO error occurred while disabling external framebuffer on device"); + } + } + } + + /// + /// Enables external frame buffer, taking control of RNode display graphic + /// + /// Thrown on serial error + public void Enable_External_Framebuffer() + { + if (hasDisplay && isOnline) + { + byte[] Command = new byte[] { KISS.FEND, KISS.CMD_FB_EXT, 0x01, KISS.FEND }; + try + { + Port.Write(Command, 0, Command.Length); + } + catch + { + throw new IOException("An IO error occurred while enabling external framebuffer on device"); + } + } + } + + /// + /// Sends image data to RNode display + /// + /// Array of bytes containing image information + public void Display_Image(byte[] ImageData) + { + if (hasDisplay) + { + int lines = ImageData.Length / 8; + for (int i = 0; i < lines; i++) + { + + int LineStart = i * FB_Bytes_Per_Line; + int LineEnd = LineStart + FB_Bytes_Per_Line; + byte[] LineData = new byte[FB_Bytes_Per_Line]; + Array.Copy(ImageData, LineStart, LineData, 0, FB_Bytes_Per_Line); + Write_Framebuffer(i, LineData); + } + } + } + + /// + /// Parses image data and send to RNode for display + /// + /// Target line on RNode Display + /// Data for the line, in a byte array + /// + void Write_Framebuffer(int Line, byte[] LineData) + { + if (hasDisplay && isOnline) + { + + LineData = KISS.Escape(LineData.ToList()).ToArray(); + byte[] Command = new byte[LineData.Length + 4]; + Array.Copy(LineData, 0, Command, 3, LineData.Length); + Command[2] = (byte)Line; + Command[0] = KISS.FEND; + Command[1] = KISS.CMD_FB_WRITE; + Command[Command.Length - 1] = KISS.FEND; + try + { + Port.Write(Command, 0, Command.Length); + } + catch + { + throw new IOException("An IO error occurred while writing framebuffer data"); + } + + } + } + + class KISS + { + public const byte FEND = 0xC0; + public const byte FESC = 0xDB; + public const byte TFEND = 0xDC; + public const byte TFESC = 0xDD; + public const byte CMD_UNKNOWN = 0xFE; + public const byte CMD_DATA = 0x00; + public const byte CMD_FREQUENCY = 0x01; + public const byte CMD_BANDWIDTH = 0x02; + public const byte CMD_TXPOWER = 0x03; + public const byte CMD_SF = 0x04; + public const byte CMD_CR = 0x05; + public const byte CMD_RADIO_STATE = 0x06; + public const byte CMD_RADIO_LOCK = 0x07; + public const byte CMD_DETECT = 0x08; + public const byte CMD_LEAVE = 0x0A; + public const byte CMD_READY = 0x0F; + public const byte CMD_STAT_RX = 0x21; + public const byte CMD_STAT_TX = 0x22; + public const byte CMD_STAT_RSSI = 0x23; + public const byte CMD_STAT_SNR = 0x24; + public const byte CMD_BLINK = 0x30; + public const byte CMD_RANDOM = 0x40; + public const byte CMD_FB_EXT = 0x41; + public const byte CMD_FB_READ = 0x42; + public const byte CMD_FB_WRITE = 0x43; + public const byte CMD_PLATFORM = 0x48; + public const byte CMD_MCU = 0x49; + public const byte CMD_FW_VERSION = 0x50; + public const byte CMD_ROM_READ = 0x51; + public const byte CMD_RESET = 0x55; + public const byte DETECT_REQ = 0x73; + public const byte DETECT_RESP = 0x46; + public const byte RADIO_STATE_OFF = 0x00; + public const byte RADIO_STATE_ON = 0x01; + public const byte RADIO_STATE_ASK = 0xFF; + public const byte CMD_ERROR = 0x90; + public const byte ERROR_INITRADIO = 0x01; + public const byte ERROR_TXFAILED = 0x02; + public const byte ERROR_EEPROM_LOCKED = 0x03; + public const byte PLATFORM_AVR = 0x90; + public const byte PLATFORM_ESP32 = 0x80; + + + + + + + public static List Escape(List Input) + { + List Output = new List(); + foreach (byte b in Input) + { + if (b == 0xDB) + { + Output.Add(0xDB); + Output.Add(0xDD); + } + else if (b == 0xC0) + { + Output.Add(0xDB); + Output.Add(0xDC); + } + else + { + Output.Add(b); + } + } + return Output; + } + } + } + + +} \ No newline at end of file