diff --git a/Crypto/fernet.cs b/Crypto/fernet.cs new file mode 100644 index 0000000..e530547 --- /dev/null +++ b/Crypto/fernet.cs @@ -0,0 +1,7 @@ +namespace RNS.Cryptography +{ + public class Fernet + { + public const int FERNET_OVERHEAD = 48; //Bytes + } +} \ No newline at end of file diff --git a/Identity.cs b/Identity.cs new file mode 100644 index 0000000..9baec62 --- /dev/null +++ b/Identity.cs @@ -0,0 +1,46 @@ +// 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. + +namespace RNS +{ + public class Identity + { + string CURVE = "Curve25519"; + + + const int KEYSIZE = 256 * 2; + + + public const int FERNET_OVERHEAD = RNS.Cryptography.Fernet.FERNET_OVERHEAD; + public const int AES128_BLOCKSIZE = 16; // In bytes + int HASHLENGTH = 256; // In bits + int SIGLENGTH = KEYSIZE; // In bits + + int NAME_HASH_LENGTH = 80; + int TRUNCATED_HASHLENGTH = RNS.Reticulum.TRUNCATED_HASHLENGTH; + + //known_destinations = {} //toDo FixMe + } +} \ No newline at end of file diff --git a/Interfaces/Interface.cs b/Interfaces/Interface.cs new file mode 100644 index 0000000..09712cc --- /dev/null +++ b/Interfaces/Interface.cs @@ -0,0 +1,101 @@ +// 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. + +namespace RNS +{ + public class Interface + { + public bool IN = false; + public bool OUT = false; + public bool FWD = false; + public bool RPT = false; + public string name = ""; + + // Interface mode definitions + const byte MODE_FULL = 0x01; + const byte MODE_POINT_TO_POINT = 0x02; + const byte MODE_ACCESS_POINT = 0x03; + const byte MODE_ROAMING = 0x04; + const byte MODE_BOUNDARY = 0x05; + const byte MODE_GATEWAY = 0x06; + + // Which interface modes a Transport Node + // should actively discover paths for. + byte[] DISCOVER_PATHS_FOR = {MODE_ACCESS_POINT, MODE_GATEWAY}; + + + int rxb; + int txb; + bool online; + + public CallbackClass Callbacks; + + int ANNOUNCE_CAP; + public int ifac_size; + //List<> + + public Interface() + { + rxb = 0; + txb = 0; + online = false; + if(ANNOUNCE_CAP == 0) + { + ANNOUNCE_CAP = RNS.Reticulum.ANNOUNCE_CAP; + } + Callbacks = new CallbackClass(); + } + + public class CallbackArgs : EventArgs + { + public byte[] Message { get; private set; } + public RNS.Interface Interface { get; private set; } + public CallbackArgs(byte[] _message, RNS.Interface _interface) + { + Message = _message; + Interface = _interface; + } + } + + public class CallbackClass + { + //public delegate void CallbackEventHandler(object sender, CallbackArgs args); + public event EventHandler? CallbackEventHandler; + public void Process_Inbound(byte[] _message, RNS.Interface _interface) + { + OnCallback(new CallbackArgs(_message, _interface)); + } + protected virtual void OnCallback(CallbackArgs e) + { + EventHandler? handler = CallbackEventHandler; + if (handler != null) + { + handler(this, e); + } + } + } + + } +} \ No newline at end of file diff --git a/Link.cs b/Link.cs new file mode 100644 index 0000000..f524466 --- /dev/null +++ b/Link.cs @@ -0,0 +1,63 @@ +// 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. + +namespace RNS +{ + public class Link + { + + //object CURVE = RNS.Identity.CURVE; //ToDo FixMe + const int ECPUBSIZE = 32 + 32; + const int KEYSIZE = 32; + + const int MDU = ((RNS.Reticulum.MTU - RNS.Reticulum.IFAC_MIN_SIZE - RNS.Reticulum.HEADER_MINSIZE - RNS.Identity.FERNET_OVERHEAD) / RNS.Identity.AES128_BLOCKSIZE) * RNS.Identity.AES128_BLOCKSIZE - 1; + + const int ESTABLISHMENT_TIMEOUT_PER_HOP = RNS.Reticulum.DEFAULT_PER_HOP_TIMEOUT; + + const int TRAFFIC_TIMEOUT_FACTOR = 6; + const int KEEPALIVE_TIMEOUT_FACTOR = 4; + + const int STALE_GRACE = 2; + + const int KEEPALIVE = 360; + + public const int STALE_TIME = 2 * KEEPALIVE; + + const byte PENDING = 0x00; + const byte HANDSHAKE = 0x01; + const byte ACTIVE = 0x02; + const byte STALE = 0x03; + const byte CLOSED = 0x04; + + const byte TIMEOUT = 0x01; + const byte INITIATOR_CLOSED = 0x02; + const byte DESTINATION_CLOSED = 0x03; + + const byte ACCEPT_NONE = 0x00; + const byte ACCEPT_APP = 0x01; + const byte ACCEPT_ALL = 0x02; + byte[] resource_strategies = {ACCEPT_NONE, ACCEPT_APP, ACCEPT_ALL}; + } +} \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..f900798 --- /dev/null +++ b/Program.cs @@ -0,0 +1,92 @@ +// 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. + + + +byte[] sideband_fb_data = new byte[] { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x07, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xf8, 0x00, 0x00, + 0x00, 0x00, 0x7f, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, + 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xc1, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, + 0x00, 0x1f, 0xff, 0xff, 0xfc, 0x1f, 0xf8, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xf0, 0x07, 0xfc, 0x00, + 0x00, 0x7f, 0xff, 0xff, 0xe0, 0x03, 0xfe, 0x00, 0x00, 0xff, 0xff, 0xff, 0xc1, 0x01, 0xff, 0x00, + 0x01, 0xff, 0xff, 0xff, 0x80, 0x00, 0xff, 0x00, 0x01, 0xff, 0xff, 0xff, 0x80, 0x00, 0xff, 0x80, + 0x03, 0xff, 0xff, 0xff, 0x00, 0x00, 0x7f, 0xc1, 0x07, 0xff, 0xff, 0xff, 0x00, 0x00, 0x7f, 0xe0, + 0x07, 0xff, 0xff, 0xff, 0x00, 0x00, 0x7f, 0xe0, 0x0f, 0xff, 0xff, 0xff, 0x00, 0x00, 0x7f, 0xf0, + 0x0f, 0xff, 0xff, 0xff, 0x00, 0x00, 0x7f, 0xf0, 0x1f, 0xff, 0xff, 0xfe, 0x00, 0x00, 0xff, 0xf8, + 0x1f, 0xff, 0xff, 0xf8, 0x00, 0x00, 0xff, 0xf8, 0x3f, 0xff, 0xff, 0xf0, 0x00, 0x01, 0xff, 0xf8, + 0x3f, 0xff, 0xff, 0xc1, 0x20, 0x03, 0xff, 0xfc, 0x3f, 0xf8, 0x3f, 0x80, 0xf0, 0x07, 0xff, 0xfc, + 0x3f, 0xe0, 0x0f, 0x01, 0xfc, 0x1f, 0xff, 0xfc, 0x7f, 0xc1, 0x04, 0x07, 0xfe, 0x3f, 0xff, 0xfe, + 0x7f, 0x80, 0x00, 0x0f, 0xfe, 0x3f, 0xff, 0xfe, 0x7f, 0x00, 0x00, 0x3f, 0xfe, 0x3f, 0xff, 0xfe, + 0x7f, 0x00, 0x00, 0x7f, 0xfe, 0x3f, 0xff, 0xfe, 0x7e, 0x00, 0x00, 0xff, 0xfe, 0x3f, 0xff, 0xfe, + 0x7e, 0x00, 0x00, 0xff, 0xfe, 0x3f, 0xff, 0xfe, 0x7e, 0x00, 0x00, 0xff, 0xfe, 0x3f, 0xff, 0xfe, + 0x7e, 0x00, 0x00, 0xff, 0xfe, 0x3f, 0xff, 0xfe, 0x7e, 0x00, 0x00, 0xff, 0xfe, 0x3f, 0xff, 0xfe, + 0x7f, 0x00, 0x01, 0xff, 0xfe, 0x3f, 0xff, 0xfe, 0x7f, 0x00, 0x01, 0xff, 0xfe, 0x3f, 0xff, 0xfe, + 0x7f, 0x80, 0x03, 0xff, 0xfe, 0x3f, 0xff, 0xfe, 0x7f, 0xc0, 0x07, 0xff, 0xfe, 0x3f, 0xff, 0xfe, + 0x7f, 0xe0, 0x0f, 0xff, 0xfc, 0x1f, 0xff, 0xfe, 0x3f, 0xf8, 0x3f, 0xff, 0xf0, 0x07, 0xff, 0xfc, + 0x3f, 0xff, 0xff, 0xff, 0xe0, 0x03, 0xff, 0xfc, 0x3f, 0xff, 0xff, 0xff, 0xc0, 0x01, 0xff, 0xfc, + 0x3f, 0xff, 0xff, 0xff, 0x80, 0x00, 0xff, 0xf8, 0x1f, 0xff, 0xff, 0xff, 0x80, 0x00, 0xff, 0xf8, + 0x1f, 0xff, 0xff, 0xff, 0x00, 0x00, 0x7f, 0xf8, 0x0f, 0xff, 0xff, 0xff, 0x00, 0x00, 0x7f, 0xf0, + 0x0f, 0xff, 0xff, 0xff, 0x00, 0x00, 0x7f, 0xf0, 0x07, 0xff, 0xff, 0xff, 0x00, 0x00, 0x7f, 0xe0, + 0x07, 0xff, 0xff, 0xff, 0x00, 0x00, 0x7f, 0xe0, 0x03, 0xff, 0xff, 0xff, 0x80, 0x00, 0xff, 0xc1, + 0x01, 0xff, 0xff, 0xff, 0x80, 0x00, 0xff, 0x80, 0x00, 0xff, 0xff, 0xff, 0xc0, 0x01, 0xff, 0x00, + 0x00, 0xff, 0xff, 0xff, 0xe0, 0x03, 0xff, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xf0, 0x07, 0xfe, 0x00, + 0x00, 0x3f, 0xff, 0xff, 0xfc, 0x1f, 0xfc, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, + 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xc1, 0x00, + 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xfe, 0x00, 0x00, + 0x00, 0x00, 0x1f, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xc0, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x7f, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +//Modify the COM port and broadcast specifications as required +RNode.RNodeInterface R = new RNode.RNodeInterface(new RNS.Transport(), "RNS Test Node", "COM6",915000000,125000,2,8,5,false); + +void TestCallback(object sender, RNS.Interface.CallbackArgs e) +{ + Console.WriteLine("Got a packet: "); + foreach(byte b in e.Message) + { + Console.Write(b.ToString("X")+" "); + } + Console.WriteLine(); +} + +R.Callbacks.CallbackEventHandler += TestCallback; + +Console.WriteLine("Return to send packet. Q then return to quit."); +R.EnableBacklight(); +R.Enable_External_Framebuffer(); +R.Display_Image(sideband_fb_data); + +while (true) +{ + var Dummy = Console.ReadLine(); + if (Dummy == "q") break; + R.Send(new byte[] { 0x47, 0x6f, 0x6f, 0x64, 0x20, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x6d, 0x69, 0x74 }); +} +Console.WriteLine("Terminating..."); +R.Disable_External_Framebuffer(); +R.DisableBacklight(); +R.CloseRadio(); diff --git a/RNodeInterface.cs b/RNodeInterface.cs new file mode 100644 index 0000000..93a3ffa --- /dev/null +++ b/RNodeInterface.cs @@ -0,0 +1,1353 @@ +// 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 RNode +{ + 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; + + + 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(); + } + } + + + + } + + 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() + { + + } + + 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); + } + + } + 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); + } + } + 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); + } + } + 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); + } + } + 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); + } + } + 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); + } + } + + 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; + } + + 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; + } + } + + 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(); + + } + + 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"); + + } + } + + 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); + } + + 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()); + } + } + + 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(); + } + + 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()); + } + //ToDo Add Event + 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); + } + + + 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; + } + } + + 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); + } + + + } + + public void CloseRadio() + { + if (Port == null) { return; } + if (Port.IsOpen) + { + Port.Close(); + } + } + + public void Queue(byte[] Payload) + { + Packet_Queue.Add(Payload); + } + + public void Force_Queue() + { + Process_Queue(); + } + + 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)); + + } + + void Process_Queue() //debug + { + if (Packet_Queue.Count > 0) + { + //Console.WriteLine("I have a packet!"); + byte[] data = Packet_Queue[0]; + Packet_Queue.RemoveAt(0); + Interface_Ready = true; + Process_Outgoing(data); + } + else if (Packet_Queue.Count == 0) + { + Interface_Ready = true; + } + } + + 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); + } + } + } + + + 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."); + + } + + byte[] UTF8_to_Bytes(string Input) + { + return System.Text.Encoding.UTF8.GetBytes(Input); + } + + public void Detect() + { + isDetected = SendDetectCommand(Port); + } + + 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(); + } + + 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; + } + + 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; + + } + + public void Detach() + { + isDetached = true; + Disable_External_Framebuffer(); + setRadioState(KISS.RADIO_STATE_OFF); + Leave(); + } + + 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"); + } + } + } + + 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"); + } + } + } + + 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); + } + } + } + + 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 diff --git a/Reticulum.cs b/Reticulum.cs new file mode 100644 index 0000000..10653e9 --- /dev/null +++ b/Reticulum.cs @@ -0,0 +1,202 @@ +// 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. + +namespace RNS +{ + public class Reticulum + { + public const int MTU = 500; + const int MAX_QUEUED_ANNOUNCES = 16384; + const int QUEUED_ANNOUNCE_LIFE = 60 * 60 * 24; + + public const int ANNOUNCE_CAP = 2; + + + const int MINIMUM_BITRATE = 500; + + + public const int DEFAULT_PER_HOP_TIMEOUT = 6; + + + + public const int TRUNCATED_HASHLENGTH = 128; + + public const int HEADER_MINSIZE = 2 + 1 + (TRUNCATED_HASHLENGTH / 8) * 1; + public const int HEADER_MAXSIZE = 2 + 1 + (TRUNCATED_HASHLENGTH / 8) * 2; + public const int IFAC_MIN_SIZE = 1; + byte[] IFAC_SALT = ParseHexToByteArray("adf54d882c9a9b80771eb4995d702d4a3e733391b2a0f53f416d9f907e55cff8"); + + int MDU = MTU - HEADER_MAXSIZE - IFAC_MIN_SIZE; + + int RESOURCE_CACHE = 24 * 60 * 60; + int JOB_INTERVAL = 5 * 60; + int CLEAN_INTERVAL = 15 * 60; + int PERSIST_INTERVAL = 60 * 60 * 12; + + // router = None + // config = None //ToDo FixMe + + // # The default configuration path will be expanded to a directory + //# named ".reticulum" inside the current users home directory + public string userdir = Environment.SpecialFolder.UserProfile.ToString(); + string configdir = ""; + string configpath = ""; + string storagepath = ""; + string cachepath = ""; + + + public static byte[] ParseHexToByteArray(string S) + { + byte[] buffer = new byte[S.Length / 2]; + int i = 0; + while (i < buffer.Length) + { + buffer[i] = byte.Parse(S.Substring(i * 2, 2), System.Globalization.NumberStyles.HexNumber); + i++; + } + return buffer; + } + + public class Packet + { + byte HeaderA; + byte Hops; + byte Context; + byte[] AddressA = new byte[16]; + byte[] AddressB = new byte[16]; + byte[] DataBuffer = new byte[465]; + byte[] Data; + byte[] IFAC = new byte[64]; + byte PropagationType; + byte DestinationType; + byte PacketType; + int DataLength; + bool TypeTwoHeader; + const byte IFAC_Header = 0b10000000; + const byte Header_Type_Header = 0b01000000; + const byte Propagation_Type_Header = 0b00110000; + const byte Destination_Type_Header = 0b00001100; + const byte Packet_Type_Header = 0b00000011; + bool IFAC_Flag; + + int Position; + public Packet(byte[] Payload, Interface Inter) + { + + + HeaderA = Payload[0]; + Hops = Payload[1]; + + Position = 2; + + IFAC_Flag = ((HeaderA & IFAC_Header) == 0b10000000) ? true : false; + TypeTwoHeader = ((HeaderA & Header_Type_Header) == 0b01000000) ? true : false; + PropagationType = (byte)((HeaderA & Propagation_Type_Header) >> 4); + DestinationType = (byte)((HeaderA & Destination_Type_Header) >> 2); + PacketType = (byte)(HeaderA & Packet_Type_Header); + + if (IFAC_Flag) + { + if (Inter.ifac_size > 0) + { + for (int i = 0; i < Inter.ifac_size; i++) + { + IFAC[i] = Payload[Position]; + Position++; + } + } + } + for (int i = 0; i < 16; i++) + { + AddressA[i] = Payload[Position]; + Position++; + } + if (TypeTwoHeader) + { + for (int i = 0; i < 16; i++) + { + AddressB[i] = Payload[Position]; + Position++; + } + } + Context = Payload[Position]; + Position++; + DataLength = Payload.Length - Position; + Data = new byte[DataLength]; + for(int i = 0; i < DataLength; i++) + { + Data[i] = Payload[Position]; + Position++; + } + + } + public void Verbose() + { + Console.WriteLine("Packet contents:"); + Console.WriteLine("IFAC: "+IFAC_Flag.ToString()); + if (IFAC_Flag) { + Console.WriteLine(" Length: "+IFAC.Length.ToString()); + Console.WriteLine(" "+IFAC.ToString()); + } + string buffer = TypeTwoHeader ? "2" : "1"; + Console.WriteLine("Header Type: "+buffer); + Console.WriteLine("Propagation Type: "+PropagationType.ToString()+" ("+Transport.PropagationType[PropagationType]+")"); + Console.WriteLine("Destination Type: " + DestinationType.ToString() + " ("+Transport.DestinationType[DestinationType]+")"); + Console.WriteLine("Packet Type: " + PacketType.ToString() + " (" + Transport.PacketType[PacketType] + ")"); + Console.WriteLine("Hops: " + Hops); + Console.Write("Address: "); + foreach(byte B in AddressA) + { + Console.Write(B.ToString("X") + " "); + } + Console.WriteLine(); + if (TypeTwoHeader) + { + Console.Write("Address 2: "); + foreach (byte B in AddressB) + { + Console.Write(B.ToString("X") + " "); + } + Console.WriteLine() ; + } + Console.WriteLine("Context: " + Context.ToString("X")); + Console.WriteLine("Data Length: " + DataLength); + Console.Write("Data Content: "); + foreach(byte B in Data) + { + Console.Write(B.ToString("X")+" "); + } + Console.WriteLine(); + Console.WriteLine("End packet"); + + + + + } + } + + + } +} \ No newline at end of file diff --git a/Transport.cs b/Transport.cs new file mode 100644 index 0000000..995c2a8 --- /dev/null +++ b/Transport.cs @@ -0,0 +1,128 @@ +// 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. + +namespace RNS +{ + public class Transport + { + const byte BROADCAST = 0x00; + const byte TRANSPORT = 0x01; + const byte RELAY = 0x02; + const byte TUNNEL = 0x03; + byte[] types = {BROADCAST, TRANSPORT, RELAY, TUNNEL}; + static public Dictionary PropagationType = new Dictionary() { { 0x00, "Broadcast" }, { 0x01, "Transport" }, { 0x02,"Relay"},{ 0x03, "Tunnel" } }; + + static public Dictionary DestinationType = new Dictionary() { {0x00,"Single" },{0x01,"Group" },{0x02,"Plain" },{0x03,"link" } }; + + static public Dictionary PacketType = new Dictionary() { { 0x00, "Data" }, { 0x01, "announce" }, { 0x02, "Link Request" }, { 0x03, "Proof" } }; + + const byte REACHABILITY_UNREACHABLE = 0x00; + const byte REACHABILITY_DIRECT = 0x01; + const byte REACHABILITY_TRANSPORT = 0x02; + + string APP_NAME = "rnstransport"; + + int PATHFINDER_M = 128; //Maximum amount of hops that Reticulum will transport a packet. + + + int PATHFINDER_R = 1; // Retransmit retries + int PATHFINDER_G = 5; // Retry grace period + float PATHFINDER_RW = 0.5f; // Random window for announce rebroadcast + int PATHFINDER_E = 60 * 60 * 24 * 7; // Path expiration of one week + int AP_PATH_TIME = 60 * 60 * 24; // Path expiration of one day for Access Point paths + int ROAMING_PATH_TIME = 60 * 60 * 6; // Path expiration of 6 hours for Roaming paths + + // TODO: Calculate an optimal number for this in + // various situations + int LOCAL_REBROADCASTS_MAX = 2; // How many local rebroadcasts of an announce is allowed + + int PATH_REQUEST_TIMEOUT = 15; // Default timuout for client path requests in seconds + float PATH_REQUEST_GRACE = 0.35f; // Grace time before a path announcement is made, allows directly reachable peers to respond first + int PATH_REQUEST_RW = 2; // Path request random window + int PATH_REQUEST_MI = 5; // Minimum interval in seconds for automated path requests + + float LINK_TIMEOUT = RNS.Link.STALE_TIME * 1.25f; + int REVERSE_TIMEOUT = 30 * 60; // Reverse table entries are removed after 30 minutes + int DESTINATION_TIMEOUT = 60 * 60 * 24 * 7; // Destination table entries are removed if unused for one week + int MAX_RECEIPTS = 1024; // Maximum number of receipts to keep track of + int MAX_RATE_TIMESTAMPS = 16; // Maximum number of announce timestamps to keep per destination + + List interfaces = new List(); // All active interfaces +// destinations = [] # All active destinations +// pending_links = [] # Links that are being established +// active_links = [] # Links that are active +// packet_hashlist = [] # A list of packet hashes for duplicate detection +// receipts = [] # Receipts of all outgoing packets for proof processing + +// # TODO: "destination_table" should really be renamed to "path_table" +// # Notes on memory usage: 1 megabyte of memory can store approximately +// # 55.100 path table entries or approximately 22.300 link table entries. + +// announce_table = { } # A table for storing announces currently waiting to be retransmitted +// destination_table = {} # A lookup table containing the next hop to a given destination +// reverse_table = {} # A lookup table for storing packet hashes used to return proofs and replies +// link_table = {} # A lookup table containing hops for links +// held_announces = {} # A table containing temporarily held announce-table entries +// announce_handlers = [] # A table storing externally registered announce handlers +// tunnels = {} # A table storing tunnels to other transport instances +// announce_rate_table = {} # A table for keeping track of announce rates +// path_requests = {} # A table for storing path request timestamps + +// discovery_path_requests = {} # A table for keeping track of path requests on behalf of other nodes +// discovery_pr_tags = [] # A table for keeping track of tagged path requests +// max_pr_tags = 32000 # Maximum amount of unique path request tags to remember + +// # Transport control destinations are used +// # for control purposes like path requests +// control_destinations = [] +// control_hashes = [] + +//# Interfaces for communicating with +//# local clients connected to a shared +//# Reticulum instance +// local_client_interfaces = [] + +// local_client_rssi_cache = [] +// local_client_snr_cache = [] +// LOCAL_CLIENT_CACHE_MAXSIZE = 512 + +// pending_local_path_requests = {} + +// jobs_locked = False +// jobs_running = False +// job_interval = 0.250 +// links_last_checked = 0.0 +// links_check_interval = 1.0 +// receipts_last_checked = 0.0 +// receipts_check_interval = 1.0 +// announces_last_checked = 0.0 +// announces_check_interval = 1.0 +// hashlist_maxsize = 1000000 +// tables_last_culled = 0.0 +// tables_cull_interval = 5.0 + +// identity = None + } +} \ No newline at end of file