├── shell_crcbrute ├── Makefile ├── brute └── brute.c ├── SPIlog ├── App.config ├── Properties │ └── AssemblyInfo.cs ├── SPIlog.csproj ├── Program.cs └── Options.cs ├── CRSF_Debug ├── App.config ├── Properties │ └── AssemblyInfo.cs ├── CRSF_Debug.csproj └── Program.cs └── CRSF_Debug.sln /shell_crcbrute/Makefile: -------------------------------------------------------------------------------- 1 | 2 | 3 | all: brute.c 4 | gcc -O3 brute.c -o brute -fopenmp 5 | -------------------------------------------------------------------------------- /shell_crcbrute/brute: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g3gg0/CRSF_RevEng/master/shell_crcbrute/brute -------------------------------------------------------------------------------- /SPIlog/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /CRSF_Debug/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /SPIlog/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // Allgemeine Informationen über eine Assembly werden über die folgenden 6 | // Attribute gesteuert. Ändern Sie diese Attributwerte, um die Informationen zu ändern, 7 | // die einer Assembly zugeordnet sind. 8 | [assembly: AssemblyTitle("SPIlog")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("SPIlog")] 13 | [assembly: AssemblyCopyright("Copyright © 2021")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Durch Festlegen von ComVisible auf FALSE werden die Typen in dieser Assembly 18 | // für COM-Komponenten unsichtbar. Wenn Sie auf einen Typ in dieser Assembly von 19 | // COM aus zugreifen müssen, sollten Sie das ComVisible-Attribut für diesen Typ auf "True" festlegen. 20 | [assembly: ComVisible(false)] 21 | 22 | // Die folgende GUID bestimmt die ID der Typbibliothek, wenn dieses Projekt für COM verfügbar gemacht wird 23 | [assembly: Guid("517de142-fc87-4e09-bd61-2426ccb0c732")] 24 | 25 | // Versionsinformationen für eine Assembly bestehen aus den folgenden vier Werten: 26 | // 27 | // Hauptversion 28 | // Nebenversion 29 | // Buildnummer 30 | // Revision 31 | // 32 | // Sie können alle Werte angeben oder Standardwerte für die Build- und Revisionsnummern verwenden, 33 | // indem Sie "*" wie unten gezeigt eingeben: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /CRSF_Debug/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // Allgemeine Informationen über eine Assembly werden über die folgenden 6 | // Attribute gesteuert. Ändern Sie diese Attributwerte, um die Informationen zu ändern, 7 | // die einer Assembly zugeordnet sind. 8 | [assembly: AssemblyTitle("CRSF_Debug")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("CRSF_Debug")] 13 | [assembly: AssemblyCopyright("Copyright © 2021")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Durch Festlegen von ComVisible auf FALSE werden die Typen in dieser Assembly 18 | // für COM-Komponenten unsichtbar. Wenn Sie auf einen Typ in dieser Assembly von 19 | // COM aus zugreifen müssen, sollten Sie das ComVisible-Attribut für diesen Typ auf "True" festlegen. 20 | [assembly: ComVisible(false)] 21 | 22 | // Die folgende GUID bestimmt die ID der Typbibliothek, wenn dieses Projekt für COM verfügbar gemacht wird 23 | [assembly: Guid("7cf0de11-2c7e-421f-acf2-edf807124555")] 24 | 25 | // Versionsinformationen für eine Assembly bestehen aus den folgenden vier Werten: 26 | // 27 | // Hauptversion 28 | // Nebenversion 29 | // Buildnummer 30 | // Revision 31 | // 32 | // Sie können alle Werte angeben oder Standardwerte für die Build- und Revisionsnummern verwenden, 33 | // indem Sie "*" wie unten gezeigt eingeben: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /CRSF_Debug.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.32112.339 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CRSF_Debug", "CRSF_Debug\CRSF_Debug.csproj", "{7CF0DE11-2C7E-421F-ACF2-EDF807124555}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SPIlog", "SPIlog\SPIlog.csproj", "{517DE142-FC87-4E09-BD61-2426CCB0C732}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {7CF0DE11-2C7E-421F-ACF2-EDF807124555}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {7CF0DE11-2C7E-421F-ACF2-EDF807124555}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {7CF0DE11-2C7E-421F-ACF2-EDF807124555}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {7CF0DE11-2C7E-421F-ACF2-EDF807124555}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {517DE142-FC87-4E09-BD61-2426CCB0C732}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {517DE142-FC87-4E09-BD61-2426CCB0C732}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {517DE142-FC87-4E09-BD61-2426CCB0C732}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {517DE142-FC87-4E09-BD61-2426CCB0C732}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {45A0F764-8198-4AF8-9F92-D4643BF65DA4} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /CRSF_Debug/CRSF_Debug.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {7CF0DE11-2C7E-421F-ACF2-EDF807124555} 8 | Exe 9 | CRSF_Debug 10 | CRSF_Debug 11 | v4.6 12 | 512 13 | true 14 | true 15 | 16 | 17 | AnyCPU 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | AnyCPU 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /SPIlog/SPIlog.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {517DE142-FC87-4E09-BD61-2426CCB0C732} 8 | Exe 9 | SPIlog 10 | SPIlog 11 | v4.6 12 | 512 13 | true 14 | true 15 | 16 | 17 | AnyCPU 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | AnyCPU 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /CRSF_Debug/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO.Ports; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace CRSF_Debug 11 | { 12 | class Program 13 | { 14 | enum eCrsfState 15 | { 16 | STATE_WAIT_SYNC = 0, 17 | STATE_RECEIVE_LEN = 1, 18 | STATE_RECEIVE_DATA = 2 19 | }; 20 | 21 | enum eCrsfFrameType 22 | { 23 | CRSF_FRAMETYPE_GPS = 0x02, 24 | CRSF_FRAMETYPE_BATTERY_SENSOR = 0x08, 25 | CRSF_FRAMETYPE_LINK_STATISTICS = 0x14, 26 | CRSF_FRAMETYPE_RC_CHANNELS_PACKED = 0x16, 27 | CRSF_FRAMETYPE_ATTITUDE = 0x1E, 28 | CRSF_FRAMETYPE_FLIGHT_MODE = 0x21, 29 | // Extended Header Frames, range: 0x28 to 0x96 30 | CRSF_FRAMETYPE_DEVICE_PING = 0x28, 31 | CRSF_FRAMETYPE_DEVICE_INFO = 0x29, 32 | CRSF_FRAMETYPE_PARAMETER_SETTINGS_ENTRY = 0x2B, 33 | CRSF_FRAMETYPE_PARAMETER_READ = 0x2C, 34 | CRSF_FRAMETYPE_PARAMETER_WRITE = 0x2D, 35 | CRSF_FRAMETYPE_COMMAND = 0x32, 36 | // MSP commands 37 | CRSF_FRAMETYPE_MSP_REQ = 0x7A, // response request using msp sequence as command 38 | CRSF_FRAMETYPE_MSP_RESP = 0x7B, // reply with 58 byte chunked binary 39 | CRSF_FRAMETYPE_MSP_WRITE = 0x7C // write with 8 byte chunked binary (OpenTX outbound telemetry buffer limit) 40 | }; 41 | 42 | 43 | static byte crc8(byte[] data, int start, int length) 44 | { 45 | uint crc = 0; 46 | for (uint i = 0; i < length; i++) 47 | { 48 | uint inbyte = data[start + i]; 49 | for (uint j = 0; j < 8; j++) 50 | { 51 | uint mix = (crc ^ inbyte) & 0x80; 52 | crc <<= 1; 53 | if (mix != 0) 54 | { 55 | crc ^= 0xD5; 56 | } 57 | inbyte <<= 1; 58 | } 59 | } 60 | return (byte)crc; 61 | } 62 | 63 | public static string ByteArrayToString(byte[] ba, int start, int length) 64 | { 65 | StringBuilder hex = new StringBuilder(length * 2); 66 | for (int pos = 0; pos < length; pos++) 67 | { 68 | hex.AppendFormat("{0:x2}", ba[start + pos]); 69 | } 70 | return hex.ToString(); 71 | } 72 | 73 | private static SerialPort Port; 74 | 75 | static void Main(string[] args) 76 | { 77 | Port = new SerialPort("COM16", 420000, Parity.None, 8, StopBits.One); 78 | eCrsfState state = eCrsfState.STATE_WAIT_SYNC; 79 | 80 | Port.Open(); 81 | 82 | int received = 0; 83 | byte[] buffer = new byte[0]; 84 | 85 | while (true) 86 | { 87 | int data = Port.ReadByte(); 88 | 89 | switch (state) 90 | { 91 | case eCrsfState.STATE_WAIT_SYNC: 92 | if (data == 0xC8) 93 | { 94 | state = eCrsfState.STATE_RECEIVE_LEN; 95 | } 96 | break; 97 | 98 | case eCrsfState.STATE_RECEIVE_LEN: 99 | buffer = new byte[data]; 100 | received = 0; 101 | state = eCrsfState.STATE_RECEIVE_DATA; 102 | break; 103 | 104 | case eCrsfState.STATE_RECEIVE_DATA: 105 | buffer[received++] = (byte)data; 106 | if (received >= buffer.Length) 107 | { 108 | byte crc = buffer[buffer.Length - 1]; 109 | byte calcCrc = crc8(buffer, 0, buffer.Length - 1); 110 | bool crcFail = (calcCrc != crc); 111 | 112 | if (crcFail) 113 | { 114 | Console.WriteLine("CRC Failed"); 115 | } 116 | else 117 | { 118 | Process(buffer, 0, buffer.Length - 1); 119 | } 120 | 121 | 122 | state = eCrsfState.STATE_WAIT_SYNC; 123 | } 124 | break; 125 | } 126 | } 127 | } 128 | 129 | private static byte dummy_crc = 0; 130 | 131 | private static Dictionary ReceivedTypes = new Dictionary(); 132 | private static List ReceivedTypesIndex = new List(); 133 | 134 | private static DateTime lastTime = DateTime.Now; 135 | private static int framesSent = 0; 136 | private static int framesReceived = 0; 137 | private static int framesReceivedLast = 0; 138 | 139 | private static void Process(byte[] buffer, int start, int length) 140 | { 141 | int pos = 8; 142 | DateTime nowTime = DateTime.Now; 143 | 144 | if (Enum.IsDefined(typeof(eCrsfFrameType), (int)buffer[start + 0])) 145 | { 146 | eCrsfFrameType type = (eCrsfFrameType)buffer[start + 0]; 147 | 148 | if (!ReceivedTypes.ContainsKey(type)) 149 | { 150 | ReceivedTypes.Add(type, 0); 151 | ReceivedTypesIndex.Add(type); 152 | } 153 | 154 | ReceivedTypes[type]++; 155 | pos = ReceivedTypesIndex.IndexOf(type); 156 | 157 | if (type == eCrsfFrameType.CRSF_FRAMETYPE_RC_CHANNELS_PACKED) 158 | { 159 | uint[] channelValues = new uint[16]; 160 | byte chanBits = 0; 161 | byte channel = 0; 162 | uint value = 0; 163 | 164 | framesReceived++; 165 | 166 | /* go through all payload bytes */ 167 | for (int bitPos = 0; bitPos < 22; bitPos++) 168 | { 169 | /* fetch 8 bits */ 170 | value |= ((uint)buffer[start + 1 + bitPos]) << chanBits; 171 | chanBits += 8; 172 | 173 | /* when we got enough (11) bits, treat this as a sample */ 174 | if (chanBits >= 11) 175 | { 176 | channelValues[channel++] = (value & 0x7FF); 177 | /* keep remaining bits */ 178 | value >>= 11; 179 | chanBits -= 11; 180 | } 181 | } 182 | Console.SetCursorPosition(4, 2 + 4 * pos); 183 | Console.Write("Received " + ReceivedTypes[type].ToString().PadLeft(6) + "x 0x" + buffer[start + 0].ToString("X2") + " (" + Enum.GetName(typeof(eCrsfFrameType), type) + ")"); 184 | 185 | Console.SetCursorPosition(4, 2 + 4 * pos + 2); 186 | Console.Write(" parsed " + string.Join(" ", channelValues.Select(v => v.ToString("D4")))); 187 | } 188 | else 189 | { 190 | Console.SetCursorPosition(4, 2 + 4 * pos); 191 | Console.Write("Received " + ReceivedTypes[type].ToString().PadLeft(6) + "x 0x" + buffer[start + 0].ToString("X2") + " (" + Enum.GetName(typeof(eCrsfFrameType), type) + ")"); 192 | } 193 | } 194 | else if ((int)buffer[start + 0] == 0xED) 195 | { 196 | int ms = 0; 197 | 198 | ms |= (int)buffer[start + 1]; 199 | ms |= (int)buffer[start + 2] << 8; 200 | ms |= (int)buffer[start + 3] << 16; 201 | ms |= (int)buffer[start + 4] << 24; 202 | 203 | Console.SetCursorPosition(20, 0); 204 | Console.Write("latency: " + (nowTime.Millisecond - ms) + " ms "); 205 | } 206 | else 207 | { 208 | Console.SetCursorPosition(4, 2 + 4 * pos); 209 | Console.Write("Received 0x" + buffer[start + 0].ToString("X2") + " "); 210 | } 211 | 212 | 213 | if ((nowTime - lastTime).TotalMilliseconds > 500) 214 | { 215 | /* calc rate */ 216 | int rate = (framesReceived - framesReceivedLast) * 2; 217 | Console.SetCursorPosition(0, 0); 218 | Console.Write(" rate " + rate + " Hz "); 219 | framesReceivedLast = framesReceived; 220 | 221 | /* send custom telemetry */ 222 | framesSent++; 223 | lastTime = nowTime; 224 | byte[] buf = new byte[] { 0xC8, 0x06, 0xEC, 0xEC, 0xAA, 0x55, 0x11, 0x00 }; 225 | buf[3] = (byte) (nowTime.Millisecond & 0xFF); 226 | buf[4] = (byte)((nowTime.Millisecond >> 8) & 0xFF); 227 | buf[5] = (byte)((nowTime.Millisecond >> 16) & 0xFF); 228 | buf[6] = (byte)((nowTime.Millisecond >> 24) & 0xFF); 229 | buf[7] = crc8(buf, 2, 5); 230 | 231 | Port.Write(buf, 0, buf.Length); 232 | Console.SetCursorPosition(20, 0); 233 | Console.Write(" sent " + framesSent); 234 | } 235 | 236 | var lines = ByteArrayToString(buffer, start, length).Split(60); 237 | int linenum = 0; 238 | foreach (string l in lines) 239 | { 240 | Console.SetCursorPosition(6, 2 + 4 * pos + 1 + linenum); 241 | Console.Write(l); 242 | } 243 | } 244 | } 245 | 246 | public static class Extensions 247 | { 248 | public static IEnumerable Split(this string str, int n) 249 | { 250 | if (String.IsNullOrEmpty(str) || n < 1) 251 | { 252 | throw new ArgumentException(); 253 | } 254 | 255 | return Enumerable.Range(0, (str.Length + n - 1) / n).Select(i => str.Substring(i * n, Math.Min(n, str.Length - i * n))); 256 | } 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /shell_crcbrute/brute.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #define COUNT(x) (sizeof(x)/sizeof((x)[0])) 8 | 9 | 10 | uint8_t packets_crc8[][13] = { 11 | { 0x03, 0x25, 0xED, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23 }, 12 | { 0x03, 0x25, 0xED, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x68 }, 13 | { 0x03, 0x53, 0xD4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x71 }, 14 | { 0x03, 0x59, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC1 }, 15 | { 0x03, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42 }, 16 | { 0x03, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x86 }, 17 | { 0x03, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xCD }, 18 | { 0x03, 0x74, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6E }, 19 | { 0x03, 0x9E, 0xF1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF1 }, 20 | { 0x03, 0x9F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x39 }, 21 | { 0x03, 0xBF, 0xA6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08 }, 22 | { 0x03, 0xDC, 0x5F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8B }, 23 | { 0x03, 0xE8, 0xC8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x67 }, 24 | { 0x01, 0x9E, 0xF1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4B }, 25 | { 0x83, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xBF } 26 | }; 27 | 28 | uint8_t packets_crc16[][23] = { 29 | { 0x03, 0x17, 0xFE, 0xE7, 0x5F, 0x80, 0xA1, 0x84, 0x12, 0x4A, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF1, 0xBE }, 30 | { 0x03, 0xA1, 0xFC, 0xE7, 0x1F, 0x80, 0xA1, 0x84, 0x12, 0x4A, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5A, 0x03 }, 31 | { 0x03, 0x3A, 0xFD, 0xE7, 0x1F, 0x80, 0xA1, 0x84, 0x12, 0x4A, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD9, 0x22 }, 32 | { 0x03, 0x39, 0xFD, 0xE7, 0x5F, 0x80, 0xA1, 0x84, 0x12, 0x4A, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB6, 0x68 }, 33 | { 0x03, 0x3A, 0xFD, 0xE7, 0x5F, 0x80, 0xA1, 0x84, 0x12, 0x4A, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5D, 0x01 }, 34 | { 0x03, 0x3A, 0xFD, 0xE7, 0x5F, 0x80, 0xA1, 0x84, 0x12, 0x4A, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xBB, 0x40 }, 35 | { 0x03, 0x3A, 0xFD, 0xE7, 0x5F, 0x80, 0xA1, 0x84, 0x12, 0x4A, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5D, 0x01 }, 36 | { 0x03, 0x3A, 0xFD, 0xE7, 0x5F, 0x80, 0xA1, 0x84, 0x12, 0x4A, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xBB, 0x40 }, 37 | { 0x03, 0xD3, 0xFD, 0xE7, 0x5F, 0x80, 0xA1, 0x84, 0x12, 0x4A, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD9, 0xED }, 38 | { 0x03, 0xD3, 0xFD, 0xE7, 0x5F, 0x80, 0xA1, 0x84, 0x12, 0x4A, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD9, 0xED }, 39 | { 0x03, 0xD3, 0xFD, 0xE7, 0x5F, 0x80, 0xA1, 0x84, 0x12, 0x4A, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2A, 0xCD }, 40 | { 0x03, 0xD3, 0xFD, 0xE7, 0x9F, 0x80, 0xA1, 0x84, 0x12, 0x4A, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA6, 0xA9 }, 41 | { 0x03, 0xD3, 0xFD, 0xE7, 0x9F, 0x80, 0xA1, 0x84, 0x12, 0x4A, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xE8 }, 42 | { 0x03, 0x6A, 0xFE, 0xE7, 0x5F, 0x80, 0xA1, 0x84, 0x12, 0x4A, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE9, 0x26 }, 43 | { 0x03, 0x6A, 0xFE, 0xE7, 0x5F, 0x80, 0xA1, 0x84, 0x12, 0x4A, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1A, 0x06 }, 44 | { 0x03, 0x6A, 0xFE, 0xE7, 0x9F, 0x80, 0xA1, 0x84, 0x12, 0x4A, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x96, 0x62 }, 45 | { 0x03, 0x6A, 0xFE, 0xE7, 0x9F, 0x80, 0xA1, 0x84, 0x12, 0x4A, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x65, 0x42 }, 46 | { 0x03, 0x6A, 0xFE, 0xE7, 0x5F, 0x80, 0xA1, 0x84, 0x12, 0x4A, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1A, 0x06 }, 47 | { 0x03, 0x6A, 0xFE, 0xE7, 0x5F, 0x80, 0xA1, 0x84, 0x12, 0x4A, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE9, 0x26 }, 48 | { 0x03, 0x17, 0xFE, 0xE7, 0x9F, 0x80, 0xA1, 0x84, 0x12, 0x4A, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9B, 0x9B }, 49 | { 0x03, 0x17, 0xFE, 0xE7, 0x9F, 0x80, 0xA1, 0x84, 0xF2, 0x5F, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x33, 0x1E } 50 | }; 51 | 52 | uint8_t crc8(uint8_t *data, int start, int length, uint8_t init) 53 | { 54 | uint32_t crc = init; 55 | 56 | for (uint32_t i = 0; i < length; i++) 57 | { 58 | uint32_t inData = data[start + i]; 59 | for (uint32_t j = 0; j < 8; j++) 60 | { 61 | uint32_t mix = (crc ^ inData) & 0x80; 62 | crc <<= 1; 63 | if (mix != 0) 64 | { 65 | //crc ^= 0xd5; 66 | //crc ^= 0x9b; 67 | //crc ^= 0x1d; 68 | crc ^= 0x07; 69 | //crc ^= 0x9b; 70 | //crc ^= 0x9b; 71 | } 72 | inData <<= 1; 73 | } 74 | } 75 | return (uint8_t)crc; 76 | } 77 | 78 | uint16_t reflect(uint16_t inData, int width) 79 | { 80 | uint16_t resByte = 0; 81 | 82 | for (uint8_t i = 0; i < width; i++) 83 | { 84 | if ((inData & (1 << i)) != 0) 85 | { 86 | resByte |= ( (1 << (width-1 - i)) & 0xFFFF); 87 | } 88 | } 89 | 90 | return resByte; 91 | } 92 | 93 | uint16_t crc16(uint8_t *data, int start, int length, uint16_t init) 94 | { 95 | uint16_t table[256] = 96 | { 97 | 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7, 0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF, 98 | 0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6, 0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE, 99 | 0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485, 0xA56A, 0xB54B, 0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D, 100 | 0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4, 0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC, 101 | 0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823, 0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969, 0xA90A, 0xB92B, 102 | 0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12, 0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A, 103 | 0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41, 0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49, 104 | 0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70, 0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78, 105 | 0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0xE16F, 0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, 0x6067, 106 | 0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E, 0x02B1, 0x1290, 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256, 107 | 0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D, 0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 108 | 0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C, 0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634, 109 | 0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB, 0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3, 110 | 0xCB7D, 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A, 0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92, 111 | 0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9, 0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1, 112 | 0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8, 0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0 113 | }; 114 | uint16_t crc = init; 115 | 116 | for (int i = 0; i < length; ++i) 117 | { 118 | crc = (crc ^ (reflect(data[start + i], 8)<< (16 - 8))); 119 | int pos = (crc >> (16 - 8)) & 0xFF; 120 | crc = (crc << 8); 121 | crc = (crc ^ table[pos]); 122 | } 123 | return reflect(crc, 16); 124 | } 125 | 126 | void brute_crc8() 127 | { 128 | uint8_t buffer[32]; 129 | int pkt_start = 0; 130 | int pkt_len = 12; 131 | 132 | memcpy(&buffer[pkt_start], packets_crc8[0], pkt_len + 1); 133 | 134 | for(uint64_t test_value = 0; test_value < 0x100; test_value++) 135 | { 136 | if(crc8(buffer, 0, pkt_start + pkt_len, test_value) == buffer[pkt_start + pkt_len]) 137 | { 138 | uint8_t buffer_check[32]; 139 | 140 | for(int pkt = 1; pkt < COUNT(packets_crc8); pkt++) 141 | { 142 | memcpy(&buffer_check[pkt_start], packets_crc8[pkt], pkt_len + 1); 143 | if(crc8(buffer_check, 0, pkt_start + pkt_len, test_value) == buffer_check[pkt_start + pkt_len]) 144 | { 145 | printf("match: 0/%02d 0x%02X\n", pkt, (uint32_t)test_value); 146 | } 147 | } 148 | } 149 | } 150 | } 151 | 152 | /* 153 | ch @ hop 154 | > FSK, DA3A0B, 49 @ 61 Rx 03 A2 F8 E7 DF 7F A1 84 12 4A 28 00 00 00 00 00 00 00 00 00 00 E0 07 155 | > FSK, DD7A0B, 49 @ 61 Tx 03 4C 5F 00 00 00 00 00 00 00 00 00 C4 crci: 82 156 | > FSK, DA0820, 46 @ 62 Rx 03 A2 FC E7 1F 80 A1 84 12 4A 28 00 00 00 00 00 00 00 00 00 00 E7 45 157 | > FSK, DD4820, 46 @ 62 Tx 03 5F 04 00 00 00 00 00 00 00 00 00 D5 crci: A6 158 | > FSK, D7A072, 09 @ 63 Rx 03 A2 F8 E7 DF 7F A1 84 12 4A 28 70 DA 0F 02 1D 4A 61 CF 00 00 46 CC 159 | > FSK, DAE072, 09 @ 63 Tx 03 C7 96 00 00 00 00 00 00 00 00 00 E9 crci: D8 160 | > FSK, D80449, 15 @ 64 Rx 03 A2 F8 E7 DF 7F A1 84 12 4A 28 00 00 00 00 00 00 00 00 00 00 3F CD 161 | > FSK, DB4449, 15 @ 64 Tx 83 00 6A 00 00 00 00 00 00 00 00 00 ED crci: 38 162 | > FSK, D9E6D9, 44 @ 65 Rx 03 A2 F8 E7 1F 80 A1 84 12 4A 28 00 00 00 00 00 00 00 00 00 00 4A 42 163 | > FSK, DD26D9, 44 @ 65 Tx 03 44 5F 00 00 00 00 00 00 00 00 00 F1 crci: A8 164 | > FSK, DA2968, 48 @ 66 Rx 03 A2 FC E7 1F 80 A1 84 12 4A 28 00 00 00 00 00 00 00 00 00 00 C8 6C 165 | > FSK, DD6968, 48 @ 66 Tx 03 5F 04 00 00 00 00 00 00 00 00 00 2C crci: 77 166 | 167 | */ 168 | 169 | uint8_t packets_crc8_payload[][13] = { 170 | { 0x03, 0x4C, 0x5F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC4 }, 171 | { 0x03, 0x5F, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD5 }, 172 | { 0x03, 0xC7, 0x96, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE9 }, 173 | { 0x83, 0x00, 0x6A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xED }, 174 | { 0x03, 0x44, 0x5F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF1 }, 175 | { 0x03, 0x5F, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2C } 176 | }; 177 | 178 | uint8_t packets_crc8_hopnum[] = { 61, 62, 63, 64, 65, 66 }; 179 | 180 | void brute_crc8_payload() 181 | { 182 | uint8_t buffer[32]; 183 | int pkt_start = 4; 184 | int pkt_len = 12; 185 | 186 | #define off(x) ( ((x) + omp_get_thread_num())%4) 187 | 188 | printf("thread #%d\n", omp_get_thread_num()); 189 | memcpy(&buffer[pkt_start], packets_crc8_payload[0], pkt_len + 1); 190 | 191 | for(uint64_t test_value = 0; test_value < 0x100000000; test_value++) 192 | { 193 | buffer[off(0)] = test_value >> 24; 194 | buffer[off(1)] = test_value >> 16; 195 | buffer[off(2)] = test_value >> 8; 196 | buffer[off(3)] = test_value + packets_crc8_hopnum[0]; 197 | 198 | if(crc8(buffer, 0, pkt_start + pkt_len, 0) == buffer[pkt_start + pkt_len]) 199 | { 200 | uint8_t buffer_check[32]; 201 | uint8_t valid = 1; 202 | 203 | for(int pkt = 1; pkt < COUNT(packets_crc8_payload); pkt++) 204 | { 205 | buffer_check[off(0)] = test_value >> 24; 206 | buffer_check[off(1)] = test_value >> 16; 207 | buffer_check[off(2)] = test_value >> 8; 208 | buffer_check[off(3)] = test_value + packets_crc8_hopnum[pkt]; 209 | 210 | memcpy(&buffer_check[pkt_start], packets_crc8_payload[pkt], pkt_len + 1); 211 | if(crc8(buffer_check, 0, pkt_start + pkt_len, 0) == buffer_check[pkt_start + pkt_len]) 212 | { 213 | 214 | } 215 | else 216 | { 217 | valid = 0; 218 | } 219 | } 220 | 221 | if(valid) 222 | { 223 | printf("match: 0x%08X\n", (uint32_t)test_value); 224 | } 225 | } 226 | } 227 | } 228 | 229 | 230 | void brute_crc16() 231 | { 232 | uint8_t buffer[32]; 233 | int pkt_start = 0; 234 | int pkt_len = 21; 235 | 236 | memcpy(&buffer[pkt_start], packets_crc16[2], pkt_len + 2); 237 | 238 | for(uint64_t test_value = 0; test_value < 0x10000; test_value++) 239 | { 240 | uint16_t crc = crc16(buffer, 0, pkt_start + pkt_len, test_value); 241 | uint16_t crct = *((uint16_t*)&buffer[pkt_start + pkt_len]); 242 | 243 | if(crc == crct) 244 | { 245 | uint8_t buffer_check[32]; 246 | 247 | for(int pkt = 1; pkt < 21; pkt++) 248 | { 249 | memcpy(&buffer_check[pkt_start], packets_crc16[pkt], pkt_len + 2); 250 | uint16_t ccrc = crc16(buffer_check, 0, pkt_start + pkt_len, test_value); 251 | uint16_t ccrct = *((uint16_t*)&buffer_check[pkt_start + pkt_len]); 252 | 253 | if(ccrc == ccrct) 254 | { 255 | printf("match: 0/%d 0x%08X\n", pkt, (uint32_t)test_value); 256 | } 257 | } 258 | } 259 | } 260 | } 261 | 262 | 263 | 264 | 265 | void brute_crc16_header() 266 | { 267 | uint8_t buffer[32]; 268 | int pkt_start = 3; 269 | int pkt_len = 21; 270 | uint16_t crc_init = 0xFFFF; 271 | 272 | memcpy(&buffer[pkt_start], packets_crc16[1], pkt_len + 2); 273 | 274 | for(uint64_t test_value = 0; test_value < 0x10000; test_value++) 275 | { 276 | buffer[0] = test_value; 277 | buffer[1] = test_value >> 8; 278 | //buffer[2] = test_value >> 16; 279 | //buffer[3] = test_value >> 24; 280 | 281 | uint16_t crc = crc16(buffer, 0, pkt_start + pkt_len, crc_init); 282 | uint16_t crct = *((uint16_t*)&buffer[pkt_start + pkt_len]); 283 | 284 | if(crc == crct) 285 | { 286 | uint8_t buffer_check[32]; 287 | 288 | for(int pkt = 1; pkt < 21; pkt++) 289 | { 290 | buffer_check[0] = test_value; 291 | buffer_check[1] = test_value >> 8; 292 | //buffer_check[2] = test_value >> 16; 293 | //buffer_check[3] = test_value >> 24; 294 | 295 | memcpy(&buffer_check[pkt_start], packets_crc16[pkt], pkt_len + 2); 296 | uint16_t ccrc = crc16(buffer_check, 0, pkt_start + pkt_len, crc_init); 297 | uint16_t ccrct = *((uint16_t*)&buffer_check[pkt_start + pkt_len]); 298 | 299 | if(ccrc == ccrct) 300 | { 301 | printf("match: 0/%d 0x%08X\n", pkt, (uint32_t)test_value); 302 | } 303 | } 304 | } 305 | } 306 | } 307 | 308 | 309 | 310 | int main(int argc, char *argv[]) 311 | { 312 | #pragma omp parallel num_threads(4) 313 | brute_crc8_payload(); 314 | return 0; 315 | } -------------------------------------------------------------------------------- /SPIlog/Program.cs: -------------------------------------------------------------------------------- 1 | using Mono.Options; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.ComponentModel; 5 | using System.IO; 6 | using System.IO.Ports; 7 | using System.Linq; 8 | using System.Net.Sockets; 9 | using System.Runtime.InteropServices.ComTypes; 10 | using System.Runtime.Serialization.Formatters.Binary; 11 | using System.Text; 12 | using System.Threading; 13 | using System.Threading.Tasks; 14 | 15 | namespace SPIlog 16 | { 17 | class Program 18 | { 19 | public static SerialPort Port { get; private set; } 20 | public static bool IsLora 21 | { 22 | get 23 | { 24 | return (RegistersFsk[1] & 0x80) != 0; 25 | } 26 | } 27 | 28 | public static int[] HopSequenceDetected = null; 29 | public static byte[] RegistersFsk = new byte[255]; 30 | public static byte[] RegistersLora = new byte[255]; 31 | public static uint[] SharedRegisters = new uint[] { 0x00, 0x01, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x4B, 0x58, 0x5A, 0x5C, 0x5E, 0x63, 0x6C, 0x70 }; 32 | public static byte[] hopDownCrc = new byte[150]; 33 | public static ushort[] hopUpCrc = new ushort[150]; 34 | public static ulong ChannelSpacing = 260000; 35 | public static List Frequencies = new List(); 36 | public static List FrequenciesBinary = new List(); 37 | public static List LastWrittenFrequenciesBinary = new List(); 38 | public static List LastUplinkFrequenciesBinary = new List(); 39 | public static List> FrequenciesLog = new List>(); 40 | public static ulong MinFreq = 0xFFFFFFFF; 41 | public static ulong MaxFreq = 0; 42 | 43 | public enum SX1272RegsFsk 44 | { 45 | RegFifo = 0x00, 46 | RegOpMode = 0x01, 47 | RegBitrateMsb = 0x02, 48 | RegBitrateLsb = 0x03, 49 | RegFdevMsb = 0x04, 50 | RegFdevLsb = 0x05, 51 | RegFrfMsb = 0x06, 52 | RegFrfMid = 0x07, 53 | RegFrfLsb = 0x08, 54 | RegPaConfig = 0x09, 55 | RegPaRamp = 0x0A, 56 | RegOcp = 0x0B, 57 | RegLna = 0x0C, 58 | RegRxConfig = 0x0D, 59 | RegRssiConfig = 0x0E, 60 | RegRssiCollision = 0x0F, 61 | RegRssiThresh = 0x10, 62 | RegRssiValue = 0x11, 63 | RegRxBw = 0x12, 64 | RegAfcBw = 0x13, 65 | RegOokPeak = 0x14, 66 | RegOokFix = 0x15, 67 | RegOokAvg = 0x16, 68 | RegAfcFei = 0x1A, 69 | RegAfcMsb = 0x1B, 70 | RegAfcLsb = 0x1C, 71 | RegFeiMsb = 0x1D, 72 | RegFeiLsb = 0x1E, 73 | RegPreambleDetect = 0x1F, 74 | RegRxTimeout1 = 0x20, 75 | RegRxTimeout2 = 0x21, 76 | RegRxTimeout3 = 0x22, 77 | RegRxDelay = 0x23, 78 | RegOsc = 0x24, 79 | RegPreambleMsb = 0x25, 80 | RegPreambleLsb = 0x26, 81 | RegSyncConfig = 0x27, 82 | RegSyncValue1 = 0x28, 83 | RegSyncValue2 = 0x29, 84 | RegSyncValue3 = 0x2A, 85 | RegSyncValue4 = 0x2B, 86 | RegSyncValue5 = 0x2C, 87 | RegSyncValue6 = 0x2D, 88 | RegSyncValue7 = 0x2E, 89 | RegSyncValue8 = 0x2F, 90 | RegPacketConfig1 = 0x30, 91 | RegPacketConfig2 = 0x31, 92 | RegPayloadLength = 0x32, 93 | RegNodeAdrs = 0x33, 94 | RegBroadcastAdrs = 0x34, 95 | RegFifoThresh = 0x35, 96 | RegSeqConfig1 = 0x36, 97 | RegSeqConfig2 = 0x37, 98 | RegTimerResol = 0x38, 99 | RegTimer1Coef = 0x39, 100 | RegTimer2Coef = 0x3A, 101 | RegImageCal = 0x3B, 102 | RegTemp = 0x3C, 103 | RegLowBat = 0x3D, 104 | RegIrqFlags1 = 0x3E, 105 | RegIrqFlags2 = 0x3F, 106 | RegDioMapping1 = 0x40, 107 | RegDioMapping2 = 0x41, 108 | RegVersion = 0x42, 109 | RegAgcRef = 0x43, 110 | RegAgcThresh1 = 0x44, 111 | RegAgcThresh2 = 0x45, 112 | RegAgcThresh3 = 0x46, 113 | RegPllHop = 0x4B, 114 | RegTcxo = 0x58, 115 | RegPaDac = 0x5A, 116 | RegPll = 0x5C, 117 | RegPllLowPn = 0x5E, 118 | RegPaManual = 0x63, 119 | RegFormerTemp = 0x6C, 120 | RegBitRateFrac = 0x70 121 | } 122 | 123 | public enum SX1272RegsLoRa 124 | { 125 | RegFifo = 0x00, 126 | RegOpMode = 0x01, 127 | RegFrfMsb = 0x06, 128 | RegFrfMid = 0x07, 129 | RegFrfLsb = 0x08, 130 | RegPaConfig = 0x09, 131 | RegPaRamp = 0x0A, 132 | RegOcp = 0x0B, 133 | RegLna = 0x0C, 134 | RegFifoAddrPtr = 0x0D, 135 | RegFifoTxBaseAddr = 0x0E, 136 | RegFifoRxBaseAddr = 0x0F, 137 | RegFifoRxCurrentAddr = 0x10, 138 | RegIrqFlagsMask = 0x11, 139 | RegIrqFlags = 0x12, 140 | RegRxNbBytes = 0x13, 141 | RegRxHeaderCntValueMsb = 0x14, 142 | RegRxHeaderCntValueLsb = 0x15, 143 | RegRxPacketCntMsb = 0x16, 144 | RegRxPacketCntLsb = 0x17, 145 | RegModemStat = 0x18, 146 | RegPktSnrValue = 0x19, 147 | RegPktRssiValue = 0x1A, 148 | RegRssiValue = 0x1B, 149 | RegHopChannel = 0x1C, 150 | RegModemConfig1 = 0x1D, 151 | RegModemConfig2 = 0x1E, 152 | RegSymbTimeoutLsb = 0x1F, 153 | RegPreambleMsb = 0x20, 154 | RegPreambleLsb = 0x21, 155 | RegPayloadLength = 0x22, 156 | RegMaxPayloadLength = 0x23, 157 | RegHopPeriod = 0x24, 158 | RegFifoRxByteAddr = 0x25, 159 | RegFeiMsb = 0x28, 160 | RegFeiMib = 0x29, 161 | RegFeiLsb = 0x2A, 162 | RegRssiWideband = 0x2C, 163 | RegDetectOptimize = 0x31, 164 | RegInvertIQ = 0x33, 165 | RegDetectionThreshold = 0x37, 166 | RegSyncWord = 0x39, 167 | RegInvertIQ2 = 0x3B, 168 | RegDioMapping1 = 0x40, 169 | RegDioMapping2 = 0x41, 170 | RegVersion = 0x42, 171 | RegAgcRef = 0x43, 172 | RegAgcThresh1 = 0x44, 173 | RegAgcThresh2 = 0x45, 174 | RegAgcThresh3 = 0x46, 175 | RegPllHop = 0x4B, 176 | RegTcxo = 0x58, 177 | RegPaDac = 0x5A, 178 | RegPll = 0x5C, 179 | RegPllLowPn = 0x5E, 180 | RegPaManual = 0x63, 181 | RegFormerTemp = 0x6C, 182 | RegBitRateFrac = 0x70 183 | } 184 | 185 | public static byte crc8(byte[] data, int start, int length, byte init) 186 | { 187 | uint crc = init; 188 | 189 | if (data == null || data.Length < start + length) 190 | { 191 | return 0; 192 | } 193 | 194 | for (uint i = 0; i < length; i++) 195 | { 196 | uint inData = data[start + i]; 197 | for (uint j = 0; j < 8; j++) 198 | { 199 | uint mix = (crc ^ inData) & 0x80; 200 | crc <<= 1; 201 | if (mix != 0) 202 | { 203 | crc ^= 0x07; 204 | } 205 | inData <<= 1; 206 | } 207 | } 208 | return (byte)crc; 209 | } 210 | 211 | public static ushort reflect(int inData, int width) 212 | { 213 | int resByte = 0; 214 | 215 | for (int i = 0; i < width; i++) 216 | { 217 | if ((inData & (1 << i)) != 0) 218 | { 219 | resByte |= ((1 << (width - 1 - i)) & 0xFFFF); 220 | } 221 | } 222 | 223 | return (ushort)resByte; 224 | } 225 | 226 | public static ushort[] crc16_table = 227 | { 228 | 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7, 0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF, 229 | 0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6, 0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE, 230 | 0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485, 0xA56A, 0xB54B, 0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D, 231 | 0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4, 0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC, 232 | 0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823, 0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969, 0xA90A, 0xB92B, 233 | 0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12, 0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A, 234 | 0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41, 0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49, 235 | 0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70, 0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78, 236 | 0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0xE16F, 0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, 0x6067, 237 | 0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E, 0x02B1, 0x1290, 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256, 238 | 0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D, 0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 239 | 0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C, 0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634, 240 | 0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB, 0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3, 241 | 0xCB7D, 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A, 0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92, 242 | 0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9, 0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1, 243 | 0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8, 0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0 244 | }; 245 | 246 | public static ushort crc16(byte[] data, int start, int length, ushort init) 247 | { 248 | int crc = init; 249 | 250 | for (int i = 0; i < length; ++i) 251 | { 252 | crc ^= (reflect(data[start + i], 8) << (16 - 8)); 253 | int pos = (crc >> (16 - 8)) & 0xFF; 254 | crc <<= 8; 255 | crc ^= crc16_table[pos]; 256 | } 257 | return reflect(crc, 16); 258 | } 259 | 260 | static ulong GetFrequencyRaw() 261 | { 262 | return (GetRegister(SX1272RegsFsk.RegFrfMsb) << 16) | (GetRegister(SX1272RegsFsk.RegFrfMid) << 8) | (GetRegister(SX1272RegsFsk.RegFrfLsb) << 0); 263 | } 264 | 265 | /// 266 | /// Return the frequency in MHz 267 | /// 268 | static decimal GetFrequency() 269 | { 270 | return FreqToMhz(GetFrequencyRaw()); 271 | } 272 | 273 | static int GetBitrateRaw() 274 | { 275 | return (RegistersFsk[(int)SX1272RegsFsk.RegBitrateMsb] << 8) | RegistersFsk[(int)SX1272RegsFsk.RegBitrateLsb]; 276 | } 277 | 278 | static decimal GetBitrate() 279 | { 280 | if (IsFsk()) 281 | { 282 | int br = GetBitrateRaw(); 283 | decimal rate = (br > 0 ? (32000000.0m / br) : 0) / 1000.0m; 284 | 285 | return Math.Round(rate, 2); 286 | } 287 | 288 | return 0; 289 | } 290 | 291 | static int GetChiprate() 292 | { 293 | if (IsFsk()) 294 | { 295 | return 0; 296 | } 297 | else 298 | { 299 | int rateReg = (RegistersLora[0x1E] >> 4) & 0x0F; 300 | /* check if anything was configured at all */ 301 | if (rateReg < 6 || rateReg > 12) 302 | { 303 | return 0; 304 | } 305 | int rate = 1 << rateReg; 306 | 307 | return rate; 308 | } 309 | } 310 | 311 | static int GetFreqShiftRaw() 312 | { 313 | return (RegistersFsk[(int)SX1272RegsFsk.RegFdevMsb] << 8) | RegistersFsk[(int)SX1272RegsFsk.RegFdevLsb]; 314 | } 315 | 316 | static bool IsFsk() 317 | { 318 | return ((GetRegister(SX1272RegsFsk.RegOpMode) & 0x80) == 0); 319 | } 320 | 321 | static decimal GetFreqShift() 322 | { 323 | if (IsFsk()) 324 | { 325 | decimal width = (GetFreqShiftRaw() * 32000000.0m / (1 << 19)) / 1000.0m; 326 | 327 | return Math.Round(width, 2); 328 | } 329 | else 330 | { 331 | /* check if anything was configured at all */ 332 | if(((RegistersLora[0x1D] >> 3) & 7) == 0) 333 | { 334 | return 0; 335 | } 336 | int width = 0; 337 | switch(RegistersLora[0x1D] >> 6) 338 | { 339 | case 0: 340 | width = 125000; 341 | break; 342 | case 1: 343 | width = 250000; 344 | break; 345 | case 2: 346 | width = 500000; 347 | break; 348 | 349 | } 350 | 351 | return width / 1000; 352 | } 353 | } 354 | 355 | static string GetModulationParameters() 356 | { 357 | if(IsFsk()) 358 | { 359 | return "FSK " + " F:" + GetFrequency().ToString("000.000") + "MHz S:" + GetFreqShift().ToString("00.000") + "kHz B:" + GetBitrate().ToString("00.00") + "k"; 360 | } 361 | return "LoRa" + " F:" + GetFrequency().ToString("000.000") + "MHz S:" + GetFreqShift().ToString("00.000") + "kHz B:" + GetChiprate().ToString("00") + "c/s"; 362 | } 363 | 364 | static void Main(string[] args) 365 | { 366 | string port = null; 367 | string outfile = null; 368 | string infile = null; 369 | bool show_help = false; 370 | bool hasKeys = true; 371 | int displayMode = 1; 372 | int sleepDelay = 0; 373 | string lastModulationParameters = ""; 374 | 375 | Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.InvariantCulture; 376 | 377 | var p = new OptionSet() { 378 | "Usage: SPILog [-p port] [-o log.bin] [-i log.bin]", 379 | "", 380 | "Options:", 381 | { "p|port=", "Serial port of the FPGA SPI logger.", v => port = v }, 382 | { "o|output=", "Filename where to write SPI log to.", v => outfile = v }, 383 | { "i|input=", "Filename where to read SPI log data from.", v => infile = v }, 384 | { "d|display=", "Display mode (0 = nothing, 1 = register r/w, 2 = FIFO payload only, 3 = SPI data, 4 = raw SPI data).", v => displayMode = int.Parse(v) }, 385 | { "s|sleep=", "sleep delay in ms after payload r/w in playback mode (0 = disable).", v => sleepDelay = int.Parse(v) }, 386 | { "h|help", "Show this message and exit", v => show_help = v != null }, 387 | }; 388 | List extra; 389 | 390 | try 391 | { 392 | extra = p.Parse(args); 393 | } 394 | catch (OptionException e) 395 | { 396 | Console.Write("SPILog: "); 397 | Console.WriteLine(e.Message); 398 | Console.WriteLine("Try `SPILog --help' for more information."); 399 | return; 400 | } 401 | 402 | if (show_help) 403 | { 404 | p.WriteOptionDescriptions(Console.Out); 405 | return; 406 | } 407 | 408 | if (port != null && infile != null) 409 | { 410 | Console.Write("SPILog: "); 411 | Console.WriteLine("You cannot specify both -p and -i"); 412 | Console.WriteLine("Try `SPILog --help' for more information."); 413 | return; 414 | } 415 | 416 | BinaryWriter logWriter = null; 417 | BinaryReader logReader = null; 418 | 419 | if (outfile != null) 420 | { 421 | Console.WriteLine("SPILog: Writing log to " + outfile); 422 | if (File.Exists(outfile)) 423 | { 424 | File.Delete(outfile); 425 | } 426 | logWriter = new BinaryWriter(File.OpenWrite(outfile)); 427 | } 428 | 429 | if (infile != null) 430 | { 431 | Console.WriteLine("SPILog: Reading log from " + infile); 432 | logReader = new BinaryReader(File.OpenRead(infile)); 433 | } 434 | 435 | if (port != null) 436 | { 437 | Console.WriteLine("SPILog: Reading data from " + port); 438 | Port = new SerialPort(port, 8000000, Parity.None, 8, StopBits.One); 439 | Port.ReadBufferSize = 8192000; 440 | Port.Open(); 441 | 442 | logReader = new BinaryReader(Port.BaseStream); 443 | logReader.BaseStream.ReadTimeout = 50; 444 | } 445 | 446 | if (logReader == null) 447 | { 448 | Console.Write("SPILog: "); 449 | Console.WriteLine("You have to specify either -p and -i"); 450 | Console.WriteLine("Try `SPILog --help' for more information."); 451 | return; 452 | } 453 | 454 | SX1272RegsFsk lastFskRegister = 0; 455 | SX1272RegsLoRa lastLoRaRegister = 0; 456 | List fifoData = new List(); 457 | bool write = false; 458 | uint currentRegister = 0; 459 | int currentHop = 0; 460 | int hopsMissing = 0; 461 | int hopsSuccess = 0; 462 | bool running = true; 463 | byte fwLastCounter = 0; 464 | string fifoWriteLine = null; 465 | 466 | DateTime StartTime = DateTime.Now; 467 | DateTime lastUpdate = DateTime.Now; 468 | 469 | try 470 | { 471 | bool test = Console.KeyAvailable; 472 | } 473 | catch(Exception ex) 474 | { 475 | hasKeys = false; 476 | } 477 | 478 | switch (displayMode) 479 | { 480 | case 5: 481 | Console.Clear(); 482 | break; 483 | } 484 | 485 | try 486 | { 487 | while (running) 488 | { 489 | DateTime thisTime = DateTime.Now; 490 | 491 | if (hasKeys && Console.KeyAvailable) 492 | { 493 | string line = Console.In.ReadLine().Trim(); 494 | 495 | if (line.Length == 0) 496 | { 497 | continue; 498 | } 499 | 500 | switch (line.Split(' ')[0]) 501 | { 502 | case "help": 503 | Console.WriteLine("Available commands:"); 504 | Console.WriteLine(" help, display , stats, regs, quit"); 505 | break; 506 | 507 | case "stats": 508 | PrintStatistics(); 509 | continue; 510 | 511 | case "quit": 512 | running = false; 513 | continue; 514 | 515 | case "regs": 516 | Console.WriteLine(" Reg FSK LoRa"); 517 | Console.WriteLine("-------------------"); 518 | for (uint reg = 0; reg < 0x80; reg++) 519 | { 520 | if (SharedRegisters.Contains(reg)) 521 | { 522 | Console.WriteLine(" 0x" + reg.ToString("X2") + ": 0x" + RegistersFsk[reg].ToString("X2")); 523 | } 524 | else 525 | { 526 | Console.WriteLine(" 0x" + reg.ToString("X2") + ": 0x" + RegistersFsk[reg].ToString("X2") + " 0x" + RegistersLora[reg].ToString("X2")); 527 | } 528 | } 529 | break; 530 | 531 | case "display": 532 | try 533 | { 534 | displayMode = int.Parse(line.Split(' ')[1]); 535 | Console.Clear(); 536 | } 537 | catch (Exception ex) 538 | { 539 | Console.WriteLine("Usage: display where n = 0..4"); 540 | } 541 | break; 542 | } 543 | 544 | } 545 | 546 | try 547 | { 548 | byte[] buf = logReader.ReadBytes(2); 549 | 550 | if (buf == null || buf.Length == 0) 551 | { 552 | if ((logReader.BaseStream is FileStream)) 553 | { 554 | running = false; 555 | } 556 | continue; 557 | } 558 | byte type = buf[0]; 559 | byte data = buf[1]; 560 | 561 | if (logWriter != null) 562 | { 563 | logWriter.Write(buf); 564 | } 565 | 566 | /* chip enable */ 567 | if (type == 0xDE) 568 | { 569 | bool wasFifo = false; 570 | 571 | switch (lastFskRegister) 572 | { 573 | case SX1272RegsFsk.RegFifo: 574 | wasFifo = true; 575 | break; 576 | } 577 | switch (lastLoRaRegister) 578 | { 579 | case SX1272RegsLoRa.RegFifo: 580 | wasFifo = true; 581 | break; 582 | } 583 | 584 | if (wasFifo) 585 | { 586 | ulong frf = GetFrequencyRaw(); 587 | int arfcn = FreqToArfcn(frf); 588 | string payload = string.Join(" ", fifoData.Select(d => d.ToString("X2"))); 589 | string miss = "@" + currentHop.ToString().PadLeft(3); 590 | 591 | if (HopSequenceDetected != null) 592 | { 593 | if (arfcn == HopSequenceDetected[currentHop]) 594 | { 595 | currentHop++; 596 | hopsSuccess++; 597 | currentHop %= HopSequenceDetected.Length; 598 | hopsMissing = 0; 599 | } 600 | else 601 | { 602 | currentHop = 0; 603 | currentHop %= HopSequenceDetected.Length; 604 | hopsMissing++; 605 | miss = hopsMissing.ToString().PadLeft(4); 606 | hopsSuccess = 0; 607 | } 608 | } 609 | 610 | if (frf != 0 || payload.Length > 0) 611 | { 612 | byte[] binData = fifoData.Select(d => (byte)d).ToArray(); 613 | bool crc8InitZero = false; 614 | 615 | if (binData.Length > 1) 616 | { 617 | byte crc = crc8(binData, 0, binData.Length - 1, 0); 618 | crc8InitZero = (crc == binData[binData.Length - 1]); 619 | } 620 | 621 | string modulationParameters = GetModulationParameters(); 622 | 623 | if (lastModulationParameters != modulationParameters) 624 | { 625 | lastModulationParameters = modulationParameters; 626 | //Console.WriteLine("Modulation: " + modulationParameters); 627 | } 628 | 629 | fifoWriteLine = "> " + modulationParameters + " " + arfcn.ToString("00") + " " + miss + " " + (write ? "Tx" : "Rx") + " " + payload + (crc8InitZero ? " (CRC8 zero-init)" : ""); 630 | 631 | if (!write && !IsFsk() && crc8InitZero) 632 | { 633 | /* then check for a fw packet 00000101 / 00001101 */ 634 | if ((binData[0] == 0x05) || (binData[0] == 0x0D)) 635 | { 636 | byte len = binData[1]; 637 | byte[] fwData = binData.Skip(2).Take(len).ToArray(); 638 | 639 | if (fwLastCounter != binData[0]) 640 | { 641 | fwLastCounter = binData[0]; 642 | //Console.WriteLine("FW> " + string.Join(" ", fwData.Select(d => d.ToString("X2")))); 643 | } 644 | } 645 | 646 | } 647 | } 648 | 649 | if (hopsSuccess > 10 && displayMode == 5) 650 | { 651 | if (write) 652 | { 653 | for (int test_value = 0; test_value < 0x100; test_value++) 654 | { 655 | int pkt_start = 0; 656 | int pkt_len = 12; 657 | byte[] buffer = fifoData.Select(d => (byte)d).ToArray(); 658 | byte initValue = (byte)(test_value + hopDownCrc[currentHop / 2]); 659 | 660 | if (crc8(buffer, 0, pkt_start + pkt_len, initValue) == buffer[pkt_start + pkt_len]) 661 | { 662 | hopDownCrc[currentHop / 2] = initValue; 663 | break; 664 | } 665 | } 666 | } 667 | else 668 | { 669 | for (int test_value = 0; test_value < 0x10000; test_value++) 670 | { 671 | int pkt_start = 0; 672 | int pkt_len = 21; 673 | byte[] buffer = fifoData.Select(d => (byte)d).ToArray(); 674 | ushort initValue = (ushort)(test_value + hopUpCrc[currentHop / 2]); 675 | ushort pktCrc = (ushort)((buffer[pkt_start + pkt_len + 1] << 8) | buffer[pkt_start + pkt_len]); 676 | 677 | if (crc16(buffer, 0, pkt_start + pkt_len, initValue) == pktCrc) 678 | { 679 | hopUpCrc[currentHop / 2] = initValue; 680 | break; 681 | } 682 | } 683 | } 684 | } 685 | } 686 | 687 | if(fifoWriteLine != null) 688 | { 689 | switch (displayMode) 690 | { 691 | case 0: 692 | case 1: 693 | case 2: 694 | case 3: 695 | case 4: 696 | Console.WriteLine(fifoWriteLine); 697 | break; 698 | case 5: 699 | break; 700 | } 701 | fifoWriteLine = null; 702 | } 703 | 704 | fifoData.Clear(); 705 | currentRegister = data & ~0x80U; 706 | write = (data & 0x80) != 0; 707 | 708 | lastFskRegister = (SX1272RegsFsk) 0xff; 709 | lastLoRaRegister = (SX1272RegsLoRa) 0xff; 710 | 711 | if (IsFsk()) 712 | { 713 | lastFskRegister = (SX1272RegsFsk)currentRegister; 714 | } 715 | else 716 | { 717 | lastLoRaRegister = (SX1272RegsLoRa)currentRegister; 718 | } 719 | } 720 | else if (type == 0xDD) 721 | { 722 | /* data */ 723 | string regName = "unk"; 724 | 725 | if (IsFsk()) 726 | { 727 | if (Enum.IsDefined(typeof(SX1272RegsFsk), (int)currentRegister)) 728 | { 729 | regName = Enum.GetName(typeof(SX1272RegsFsk), (int)currentRegister); 730 | } 731 | } 732 | else 733 | { 734 | if (Enum.IsDefined(typeof(SX1272RegsLoRa), (int)currentRegister)) 735 | { 736 | regName = Enum.GetName(typeof(SX1272RegsLoRa), (int)currentRegister); 737 | } 738 | } 739 | 740 | if (currentRegister == 0) 741 | { 742 | fifoData.Add(data); 743 | } 744 | else if(write) 745 | { 746 | WriteRegister(currentRegister, data); 747 | } 748 | 749 | switch (displayMode) 750 | { 751 | case 0: 752 | break; 753 | 754 | case 1: 755 | { 756 | if (write) 757 | { 758 | Console.Write("W Reg: 0x" + currentRegister.ToString("X2") + " " + regName.PadRight(16) + " < " + data.ToString("X2")); 759 | Console.WriteLine(); 760 | 761 | if (IsLora) 762 | { 763 | switch ((SX1272RegsLoRa)currentRegister) 764 | { 765 | case SX1272RegsLoRa.RegOpMode: 766 | string[] modes = { "SLEEP", "STDBY", "Frequency Synthesis TX", "Transmit", "Frequency Synthesis RX", "Receive continuous", "Receive single", "Channel Activity Detection" }; 767 | Console.WriteLine(" => Mode " + (((data & 0x80) == 0) ? "FSK " : "LoRa")); 768 | Console.WriteLine(" => Mode " + modes[data & 7]); 769 | break; 770 | 771 | case SX1272RegsLoRa.RegFrfLsb: 772 | Console.WriteLine(" => Frequency " + GetFrequency() + " MHz"); 773 | break; 774 | 775 | case SX1272RegsLoRa.RegModemConfig2: 776 | { 777 | Console.WriteLine(" => SpreadingFactor " + (1U << (data >> 4)).ToString()); 778 | Console.WriteLine(" => TxContinuousMode " + ((data & (1 << 3)) != 0)); 779 | Console.WriteLine(" => AgcAutoOn " + ((data & (1 << 2)) != 0)); 780 | Console.WriteLine(" => SymbTimeout(9:8) " + ((data >> 0) & 3).ToString()); 781 | } 782 | break; 783 | 784 | case SX1272RegsLoRa.RegModemConfig1: 785 | { 786 | Console.WriteLine(" => Bw " + (125 * (1 << (data >> 6))).ToString() + " kHz"); 787 | Console.WriteLine(" => CodingRate 4/" + (4 + ((data >> 6) & 7))); 788 | Console.WriteLine(" => ImplHdrMode " + ((data & (1 << 2)) != 0)); 789 | Console.WriteLine(" => PayloadCRC " + ((data & (1 << 1)) != 0)); 790 | Console.WriteLine(" => LowDataRateOpt " + ((data & (1 << 0)) != 0)); 791 | } 792 | break; 793 | } 794 | } 795 | else 796 | { 797 | switch ((SX1272RegsFsk)currentRegister) 798 | { 799 | case SX1272RegsFsk.RegOpMode: 800 | string[] modes = { "SLEEP", "STDBY", "Frequency Synthesis TX", "Transmit", "Frequency Synthesis RX", "Receive", "", "" }; 801 | 802 | Console.WriteLine(" => Mode " + (((data & 0x80) == 0) ? "FSK " : "LoRa")); 803 | Console.WriteLine(" => Mode " + modes[data & 7]); 804 | break; 805 | 806 | case SX1272RegsFsk.RegFrfLsb: 807 | Console.WriteLine(" => Frequency " + GetFrequency() + " MHz"); 808 | break; 809 | } 810 | } 811 | break; 812 | } 813 | else 814 | { 815 | Console.Write("R Reg: 0x" + currentRegister.ToString("X2") + " " + regName.PadRight(16) + " > " + data.ToString("X2")); 816 | Console.WriteLine(); 817 | } 818 | break; 819 | } 820 | 821 | case 2: 822 | break; 823 | 824 | case 3: 825 | Console.Write(" " + data.ToString("X2")); 826 | Console.WriteLine(""); 827 | Console.Write("" + (((data & 0x80) != 0) ? "W" : "R") + " " + (data & 0x7F).ToString("X2")); 828 | break; 829 | 830 | case 4: 831 | Console.Write(" " + data.ToString("X2")); Console.WriteLine(""); 832 | Console.Write("" + data.ToString("X2")); 833 | break; 834 | 835 | case 5: 836 | 837 | if ((thisTime - lastUpdate).TotalMilliseconds > 50) 838 | { 839 | lastUpdate = thisTime; 840 | var frfs = LastWrittenFrequenciesBinary.Distinct().ToList(); 841 | List deltas = new List(); 842 | 843 | frfs.Sort(); 844 | 845 | for (int pos = 0; pos < frfs.Count - 1; pos++) 846 | { 847 | ulong delta = frfs[pos + 1] - frfs[pos]; 848 | deltas.Add(delta); 849 | } 850 | decimal avgDelta = 0; 851 | 852 | if (deltas.Count > 0) 853 | { 854 | avgDelta = (decimal)deltas.Average(s => (double)s); 855 | } 856 | decimal avgDeltaFreq = (avgDelta * 32000000.0m / (1 << 19)); 857 | 858 | ChannelSpacing = (ulong)avgDeltaFreq; 859 | 860 | Console.SetCursorPosition(0, 0); 861 | Console.WriteLine("Hop: " + currentHop.ToString().PadLeft(3)); 862 | Console.WriteLine(""); 863 | Console.WriteLine("Freq min: 0x" + MinFreq.ToString("X6") + " (" + FreqToMhzString(MinFreq) + " MHz) "); 864 | Console.WriteLine("Freq max: 0x" + MaxFreq.ToString("X6") + " (" + FreqToMhzString(MaxFreq) + " MHz) "); 865 | Console.WriteLine("Spacing: " + (avgDeltaFreq / 1000.0m).ToString("0.00") + " kHz "); 866 | Console.WriteLine("FreqShift: 0x" + GetFreqShiftRaw().ToString("X6") + " (" + GetFreqShift().ToString("0.00") + " kHz) "); 867 | Console.WriteLine("Bitrate: 0x" + GetBitrateRaw().ToString("X6") + " (" + GetBitrate().ToString("0.00") + " kBaud) "); 868 | Console.WriteLine(""); 869 | Console.WriteLine(""); 870 | Console.WriteLine(" hop sequence"); 871 | Console.WriteLine("--------------"); 872 | for (int hopNum = 0; hopNum < 300; hopNum++) 873 | { 874 | if ((hopNum % 30) == 0) 875 | { 876 | Console.WriteLine(); 877 | Console.Write(" " + hopNum.ToString().PadLeft(3) + " "); 878 | } 879 | if (HopSequenceDetected != null) 880 | { 881 | Console.Write(" " + HopSequenceDetected[hopNum].ToString().PadLeft(2)); 882 | } 883 | } 884 | Console.WriteLine(""); 885 | Console.WriteLine(""); 886 | Console.WriteLine(" Downlink CRC8 table per hop"); 887 | Console.WriteLine("-----------------------------"); 888 | for (int hopNum = 0; hopNum < 150; hopNum++) 889 | { 890 | if ((hopNum % 30) == 0) 891 | { 892 | Console.WriteLine(); 893 | Console.Write(" " + hopNum.ToString().PadLeft(3) + " "); 894 | } 895 | Console.Write(" " + hopDownCrc[hopNum].ToString("X2")); 896 | } 897 | Console.WriteLine(""); 898 | Console.WriteLine(""); 899 | Console.WriteLine(" Uplink CRC16 table per hop"); 900 | Console.WriteLine("----------------------------"); 901 | for (int hopNum = 0; hopNum < 150; hopNum++) 902 | { 903 | if ((hopNum % 20) == 0) 904 | { 905 | Console.WriteLine(); 906 | Console.Write(" " + hopNum.ToString().PadLeft(3) + " "); 907 | } 908 | Console.Write(" " + hopUpCrc[hopNum].ToString("X4")); 909 | } 910 | Console.WriteLine(""); 911 | } 912 | break; 913 | } 914 | 915 | if (write) 916 | { 917 | /* shared between lora and fsk */ 918 | switch ((SX1272RegsFsk)currentRegister) 919 | { 920 | case SX1272RegsFsk.RegFrfLsb: 921 | ulong frf = (GetRegister(SX1272RegsFsk.RegFrfMsb) << 16) + (GetRegister(SX1272RegsFsk.RegFrfMid) << 8) + (GetRegister(SX1272RegsFsk.RegFrfLsb) << 0); 922 | ulong freq = (ulong)(GetFrequency() * 1000000.0m); 923 | 924 | /* delay a few ms */ 925 | if ((logReader.BaseStream is FileStream) && sleepDelay > 0) 926 | { 927 | Thread.Sleep(sleepDelay); 928 | } 929 | 930 | if (frf != 0) 931 | { 932 | int arfcn = FreqToArfcn(frf); 933 | 934 | Frequencies.Add(freq); 935 | FrequenciesBinary.Add(frf); 936 | FrequenciesLog.Add(new Tuple((DateTime.Now - StartTime).TotalMilliseconds, (byte)((frf - 0xD70AB0) / 4260))); 937 | 938 | if ((FrequenciesBinary.Count % 2) == 0) 939 | { 940 | int width = FrequenciesBinary.Count / 2; 941 | 942 | var part1 = FrequenciesBinary.Take(width); 943 | var part2 = FrequenciesBinary.Skip(width); 944 | 945 | //if(Enumerable.SequenceEqual(part1, part2)) 946 | { 947 | //PrintStatistics(); 948 | } 949 | 950 | } 951 | 952 | //Console.WriteLine(" => Frequency: " + (freq / 1000) + " kHz"); 953 | 954 | 955 | LastWrittenFrequenciesBinary.Add(frf); 956 | 957 | if (MinFreq != LastWrittenFrequenciesBinary.Min()) 958 | { 959 | MinFreq = LastWrittenFrequenciesBinary.Min(); 960 | HopSequenceDetected = null; 961 | } 962 | if (MaxFreq != LastWrittenFrequenciesBinary.Max()) 963 | { 964 | MaxFreq = LastWrittenFrequenciesBinary.Max(); 965 | HopSequenceDetected = null; 966 | } 967 | 968 | if (LastWrittenFrequenciesBinary.Count > 300) 969 | { 970 | /* if the pattern does not repeat, reset the hop list */ 971 | if (LastWrittenFrequenciesBinary.First() != LastWrittenFrequenciesBinary.Last()) 972 | { 973 | HopSequenceDetected = null; 974 | } 975 | 976 | /* only update the hop sequence if the current (and first) ARFCN is zero */ 977 | if (HopSequenceDetected == null && arfcn == 0) 978 | { 979 | HopSequenceDetected = LastWrittenFrequenciesBinary.Take(300).Select(v => FreqToArfcn(v)).ToArray(); 980 | } 981 | 982 | LastWrittenFrequenciesBinary.RemoveAt(0); 983 | } 984 | } 985 | break; 986 | } 987 | } 988 | 989 | if (currentRegister != 0) 990 | { 991 | lastFskRegister = (SX1272RegsFsk)0xff; 992 | lastLoRaRegister = (SX1272RegsLoRa)0xff; 993 | 994 | if (IsFsk()) 995 | { 996 | lastFskRegister = (SX1272RegsFsk)currentRegister; 997 | } 998 | else 999 | { 1000 | lastLoRaRegister = (SX1272RegsLoRa)currentRegister; 1001 | } 1002 | currentRegister++; 1003 | } 1004 | } 1005 | else 1006 | { 1007 | Console.WriteLine("Lost a few bytes"); 1008 | lastFskRegister = 0; 1009 | fifoData.Clear(); 1010 | logReader.ReadByte(); 1011 | } 1012 | } 1013 | catch (TimeoutException ex) 1014 | { 1015 | } 1016 | } 1017 | } 1018 | catch (EndOfStreamException ex) 1019 | { 1020 | Console.WriteLine("End of file"); 1021 | } 1022 | } 1023 | 1024 | private static int FreqToArfcn(ulong frf) 1025 | { 1026 | double spacingFrf = ChannelSpacing * (1 << 19) / 32000000; //4260.0f 1027 | return (int)Math.Round((frf - MinFreq) / spacingFrf, 0); 1028 | } 1029 | 1030 | private static decimal FreqToMhz(ulong frf) 1031 | { 1032 | return Math.Round(((frf * 32000000) >> 19) / 1000000.0m, 3); 1033 | } 1034 | 1035 | private static string FreqToMhzString(ulong frf) 1036 | { 1037 | return FreqToMhz(frf).ToString("0.000"); 1038 | } 1039 | 1040 | private static void PrintStatistics() 1041 | { 1042 | ulong min = FrequenciesBinary.Min(); 1043 | ulong max = FrequenciesBinary.Max(); 1044 | decimal minFreq = ((min * 32000000) >> 19) / 1000000.0m; 1045 | decimal maxFreq = ((max * 32000000) >> 19) / 1000000.0m; 1046 | 1047 | var freqs = FrequenciesBinary.OrderBy(s => s).Distinct(); 1048 | 1049 | List deltas = new List(); 1050 | 1051 | ulong prevFreq = freqs.First(); 1052 | 1053 | foreach (var freq in freqs.Skip(1)) 1054 | { 1055 | ulong delta = freq - prevFreq; 1056 | 1057 | deltas.Add(delta); 1058 | 1059 | prevFreq = freq; 1060 | } 1061 | 1062 | if (deltas.Count > 0) 1063 | { 1064 | Console.WriteLine(""); 1065 | Console.WriteLine("Min: " + min.ToString("X6") + " (" + minFreq.ToString("0.000") + ")" + " Max: " + max.ToString("X6") + " (" + maxFreq.ToString("0.000") + ")" + " Distance: " + deltas.First() + " Hz"); 1066 | 1067 | if (!IsLora) 1068 | { 1069 | uint frac = GetRegister(0x70); 1070 | uint bitRate = (GetRegister(2) << 8 | GetRegister(3)); 1071 | 1072 | if (bitRate > 0) 1073 | { 1074 | Console.WriteLine("Rate: " + (32000000 / (bitRate + frac / 16))); 1075 | } 1076 | } 1077 | } 1078 | 1079 | /* 1080 | 1081 | Console.WriteLine("Frequencies: "); 1082 | foreach (ulong frf in FrequenciesBinary) 1083 | { 1084 | var chan = (frf - FrequenciesBinary.Min()) / deltas.First(); 1085 | Console.Write("0x" + chan.ToString("X2")+", "); 1086 | } 1087 | Console.WriteLine(""); 1088 | 1089 | */ 1090 | 1091 | Console.WriteLine("Frequencies: "); 1092 | foreach (var pair in FrequenciesLog.OrderBy(p => p.Item1)) 1093 | { 1094 | Console.Write(pair.Item1 + " " + pair.Item2.ToString("X2") + ", "); 1095 | } 1096 | Console.WriteLine(""); 1097 | } 1098 | 1099 | private static void WriteRegister(uint currentRegister, uint data) 1100 | { 1101 | if (IsLora && !SharedRegisters.Contains(currentRegister)) 1102 | { 1103 | RegistersLora[currentRegister] = (byte)data; 1104 | } 1105 | else 1106 | { 1107 | RegistersFsk[currentRegister] = (byte)data; 1108 | } 1109 | } 1110 | 1111 | private static uint GetRegister(SX1272RegsFsk reg) 1112 | { 1113 | return GetRegister((uint)reg); 1114 | } 1115 | 1116 | private static uint GetRegister(SX1272RegsLoRa reg) 1117 | { 1118 | return GetRegister((uint)reg); 1119 | } 1120 | 1121 | private static uint GetRegister(uint currentRegister) 1122 | { 1123 | if (IsLora && !SharedRegisters.Contains(currentRegister)) 1124 | { 1125 | return RegistersLora[currentRegister]; 1126 | } 1127 | return RegistersFsk[currentRegister]; 1128 | } 1129 | } 1130 | } -------------------------------------------------------------------------------- /SPIlog/Options.cs: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // Options.cs 4 | // 5 | // Authors: 6 | // Jonathan Pryor , 7 | // Federico Di Gregorio 8 | // Rolf Bjarne Kvinge 9 | // 10 | // Copyright (C) 2008 Novell (http://www.novell.com) 11 | // Copyright (C) 2009 Federico Di Gregorio. 12 | // Copyright (C) 2012 Xamarin Inc (http://www.xamarin.com) 13 | // Copyright (C) 2017 Microsoft Corporation (http://www.microsoft.com) 14 | // 15 | // Permission is hereby granted, free of charge, to any person obtaining 16 | // a copy of this software and associated documentation files (the 17 | // "Software"), to deal in the Software without restriction, including 18 | // without limitation the rights to use, copy, modify, merge, publish, 19 | // distribute, sublicense, and/or sell copies of the Software, and to 20 | // permit persons to whom the Software is furnished to do so, subject to 21 | // the following conditions: 22 | // 23 | // The above copyright notice and this permission notice shall be 24 | // included in all copies or substantial portions of the Software. 25 | // 26 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 27 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 28 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 29 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 30 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 31 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 32 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 33 | // 34 | 35 | // Compile With: 36 | // mcs -debug+ -r:System.Core Options.cs -o:Mono.Options.dll -t:library 37 | // mcs -debug+ -d:LINQ -r:System.Core Options.cs -o:Mono.Options.dll -t:library 38 | // 39 | // The LINQ version just changes the implementation of 40 | // OptionSet.Parse(IEnumerable), and confers no semantic changes. 41 | 42 | // 43 | // A Getopt::Long-inspired option parsing library for C#. 44 | // 45 | // Mono.Options.OptionSet is built upon a key/value table, where the 46 | // key is a option format string and the value is a delegate that is 47 | // invoked when the format string is matched. 48 | // 49 | // Option format strings: 50 | // Regex-like BNF Grammar: 51 | // name: .+ 52 | // type: [=:] 53 | // sep: ( [^{}]+ | '{' .+ '}' )? 54 | // aliases: ( name type sep ) ( '|' name type sep )* 55 | // 56 | // Each '|'-delimited name is an alias for the associated action. If the 57 | // format string ends in a '=', it has a required value. If the format 58 | // string ends in a ':', it has an optional value. If neither '=' or ':' 59 | // is present, no value is supported. `=' or `:' need only be defined on one 60 | // alias, but if they are provided on more than one they must be consistent. 61 | // 62 | // Each alias portion may also end with a "key/value separator", which is used 63 | // to split option values if the option accepts > 1 value. If not specified, 64 | // it defaults to '=' and ':'. If specified, it can be any character except 65 | // '{' and '}' OR the *string* between '{' and '}'. If no separator should be 66 | // used (i.e. the separate values should be distinct arguments), then "{}" 67 | // should be used as the separator. 68 | // 69 | // Options are extracted either from the current option by looking for 70 | // the option name followed by an '=' or ':', or is taken from the 71 | // following option IFF: 72 | // - The current option does not contain a '=' or a ':' 73 | // - The current option requires a value (i.e. not a Option type of ':') 74 | // 75 | // The `name' used in the option format string does NOT include any leading 76 | // option indicator, such as '-', '--', or '/'. All three of these are 77 | // permitted/required on any named option. 78 | // 79 | // Option bundling is permitted so long as: 80 | // - '-' is used to start the option group 81 | // - all of the bundled options are a single character 82 | // - at most one of the bundled options accepts a value, and the value 83 | // provided starts from the next character to the end of the string. 84 | // 85 | // This allows specifying '-a -b -c' as '-abc', and specifying '-D name=value' 86 | // as '-Dname=value'. 87 | // 88 | // Option processing is disabled by specifying "--". All options after "--" 89 | // are returned by OptionSet.Parse() unchanged and unprocessed. 90 | // 91 | // Unprocessed options are returned from OptionSet.Parse(). 92 | // 93 | // Examples: 94 | // int verbose = 0; 95 | // OptionSet p = new OptionSet () 96 | // .Add ("v", v => ++verbose) 97 | // .Add ("name=|value=", v => Console.WriteLine (v)); 98 | // p.Parse (new string[]{"-v", "--v", "/v", "-name=A", "/name", "B", "extra"}); 99 | // 100 | // The above would parse the argument string array, and would invoke the 101 | // lambda expression three times, setting `verbose' to 3 when complete. 102 | // It would also print out "A" and "B" to standard output. 103 | // The returned array would contain the string "extra". 104 | // 105 | // C# 3.0 collection initializers are supported and encouraged: 106 | // var p = new OptionSet () { 107 | // { "h|?|help", v => ShowHelp () }, 108 | // }; 109 | // 110 | // System.ComponentModel.TypeConverter is also supported, allowing the use of 111 | // custom data types in the callback type; TypeConverter.ConvertFromString() 112 | // is used to convert the value option to an instance of the specified 113 | // type: 114 | // 115 | // var p = new OptionSet () { 116 | // { "foo=", (Foo f) => Console.WriteLine (f.ToString ()) }, 117 | // }; 118 | // 119 | // Random other tidbits: 120 | // - Boolean options (those w/o '=' or ':' in the option format string) 121 | // are explicitly enabled if they are followed with '+', and explicitly 122 | // disabled if they are followed with '-': 123 | // string a = null; 124 | // var p = new OptionSet () { 125 | // { "a", s => a = s }, 126 | // }; 127 | // p.Parse (new string[]{"-a"}); // sets v != null 128 | // p.Parse (new string[]{"-a+"}); // sets v != null 129 | // p.Parse (new string[]{"-a-"}); // sets v == null 130 | // 131 | 132 | // 133 | // Mono.Options.CommandSet allows easily having separate commands and 134 | // associated command options, allowing creation of a *suite* along the 135 | // lines of **git**(1), **svn**(1), etc. 136 | // 137 | // CommandSet allows intermixing plain text strings for `--help` output, 138 | // Option values -- as supported by OptionSet -- and Command instances, 139 | // which have a name, optional help text, and an optional OptionSet. 140 | // 141 | // var suite = new CommandSet ("suite-name") { 142 | // // Use strings and option values, as with OptionSet 143 | // "usage: suite-name COMMAND [OPTIONS]+", 144 | // { "v:", "verbosity", (int? v) => Verbosity = v.HasValue ? v.Value : Verbosity+1 }, 145 | // // Commands may also be specified 146 | // new Command ("command-name", "command help") { 147 | // Options = new OptionSet {/*...*/}, 148 | // Run = args => { /*...*/}, 149 | // }, 150 | // new MyCommandSubclass (), 151 | // }; 152 | // return suite.Run (new string[]{...}); 153 | // 154 | // CommandSet provides a `help` command, and forwards `help COMMAND` 155 | // to the registered Command instance by invoking Command.Invoke() 156 | // with `--help` as an option. 157 | // 158 | 159 | using System; 160 | using System.Collections; 161 | using System.Collections.Generic; 162 | using System.Collections.ObjectModel; 163 | using System.ComponentModel; 164 | using System.Globalization; 165 | using System.IO; 166 | #if PCL 167 | using System.Reflection; 168 | #else 169 | using System.Runtime.Serialization; 170 | using System.Security.Permissions; 171 | #endif 172 | using System.Text; 173 | using System.Text.RegularExpressions; 174 | 175 | #if LINQ 176 | using System.Linq; 177 | #endif 178 | 179 | #if TEST 180 | using NDesk.Options; 181 | #endif 182 | 183 | #if PCL 184 | using MessageLocalizerConverter = System.Func; 185 | #else 186 | using MessageLocalizerConverter = System.Converter; 187 | #endif 188 | 189 | #if NDESK_OPTIONS 190 | namespace NDesk.Options 191 | #else 192 | namespace Mono.Options 193 | #endif 194 | { 195 | static class StringCoda 196 | { 197 | 198 | public static IEnumerable WrappedLines(string self, params int[] widths) 199 | { 200 | IEnumerable w = widths; 201 | return WrappedLines(self, w); 202 | } 203 | 204 | public static IEnumerable WrappedLines(string self, IEnumerable widths) 205 | { 206 | if (widths == null) 207 | throw new ArgumentNullException("widths"); 208 | return CreateWrappedLinesIterator(self, widths); 209 | } 210 | 211 | private static IEnumerable CreateWrappedLinesIterator(string self, IEnumerable widths) 212 | { 213 | if (string.IsNullOrEmpty(self)) 214 | { 215 | yield return string.Empty; 216 | yield break; 217 | } 218 | using (IEnumerator ewidths = widths.GetEnumerator()) 219 | { 220 | bool? hw = null; 221 | int width = GetNextWidth(ewidths, int.MaxValue, ref hw); 222 | int start = 0, end; 223 | do 224 | { 225 | end = GetLineEnd(start, width, self); 226 | // endCorrection is 1 if the line end is '\n', and might be 2 if the line end is '\r\n'. 227 | int endCorrection = 1; 228 | if (end >= 2 && self.Substring(end - 2, 2).Equals("\r\n")) 229 | endCorrection = 2; 230 | char c = self[end - endCorrection]; 231 | if (char.IsWhiteSpace(c)) 232 | end -= endCorrection; 233 | bool needContinuation = end != self.Length && !IsEolChar(c); 234 | string continuation = ""; 235 | if (needContinuation) 236 | { 237 | --end; 238 | continuation = "-"; 239 | } 240 | string line = self.Substring(start, end - start) + continuation; 241 | yield return line; 242 | start = end; 243 | if (char.IsWhiteSpace(c)) 244 | start += endCorrection; 245 | width = GetNextWidth(ewidths, width, ref hw); 246 | } while (start < self.Length); 247 | } 248 | } 249 | 250 | private static int GetNextWidth(IEnumerator ewidths, int curWidth, ref bool? eValid) 251 | { 252 | if (!eValid.HasValue || (eValid.HasValue && eValid.Value)) 253 | { 254 | curWidth = (eValid = ewidths.MoveNext()).Value ? ewidths.Current : curWidth; 255 | // '.' is any character, - is for a continuation 256 | const string minWidth = ".-"; 257 | if (curWidth < minWidth.Length) 258 | throw new ArgumentOutOfRangeException("widths", 259 | string.Format("Element must be >= {0}, was {1}.", minWidth.Length, curWidth)); 260 | return curWidth; 261 | } 262 | // no more elements, use the last element. 263 | return curWidth; 264 | } 265 | 266 | private static bool IsEolChar(char c) 267 | { 268 | return !char.IsLetterOrDigit(c); 269 | } 270 | 271 | private static int GetLineEnd(int start, int length, string description) 272 | { 273 | int end = System.Math.Min(start + length, description.Length); 274 | int sep = -1; 275 | for (int i = start; i < end; ++i) 276 | { 277 | if (i + 2 <= description.Length && description.Substring(i, 2).Equals("\r\n")) 278 | return i + 2; 279 | if (description[i] == '\n') 280 | return i + 1; 281 | if (IsEolChar(description[i])) 282 | sep = i + 1; 283 | } 284 | if (sep == -1 || end == description.Length) 285 | return end; 286 | return sep; 287 | } 288 | } 289 | 290 | public class OptionValueCollection : IList, IList 291 | { 292 | 293 | List values = new List(); 294 | OptionContext c; 295 | 296 | internal OptionValueCollection(OptionContext c) 297 | { 298 | this.c = c; 299 | } 300 | 301 | #region ICollection 302 | void ICollection.CopyTo(Array array, int index) { (values as ICollection).CopyTo(array, index); } 303 | bool ICollection.IsSynchronized { get { return (values as ICollection).IsSynchronized; } } 304 | object ICollection.SyncRoot { get { return (values as ICollection).SyncRoot; } } 305 | #endregion 306 | 307 | #region ICollection 308 | public void Add(string item) { values.Add(item); } 309 | public void Clear() { values.Clear(); } 310 | public bool Contains(string item) { return values.Contains(item); } 311 | public void CopyTo(string[] array, int arrayIndex) { values.CopyTo(array, arrayIndex); } 312 | public bool Remove(string item) { return values.Remove(item); } 313 | public int Count { get { return values.Count; } } 314 | public bool IsReadOnly { get { return false; } } 315 | #endregion 316 | 317 | #region IEnumerable 318 | IEnumerator IEnumerable.GetEnumerator() { return values.GetEnumerator(); } 319 | #endregion 320 | 321 | #region IEnumerable 322 | public IEnumerator GetEnumerator() { return values.GetEnumerator(); } 323 | #endregion 324 | 325 | #region IList 326 | int IList.Add(object value) { return (values as IList).Add(value); } 327 | bool IList.Contains(object value) { return (values as IList).Contains(value); } 328 | int IList.IndexOf(object value) { return (values as IList).IndexOf(value); } 329 | void IList.Insert(int index, object value) { (values as IList).Insert(index, value); } 330 | void IList.Remove(object value) { (values as IList).Remove(value); } 331 | void IList.RemoveAt(int index) { (values as IList).RemoveAt(index); } 332 | bool IList.IsFixedSize { get { return false; } } 333 | object IList.this[int index] { get { return this[index]; } set { (values as IList)[index] = value; } } 334 | #endregion 335 | 336 | #region IList 337 | public int IndexOf(string item) { return values.IndexOf(item); } 338 | public void Insert(int index, string item) { values.Insert(index, item); } 339 | public void RemoveAt(int index) { values.RemoveAt(index); } 340 | 341 | private void AssertValid(int index) 342 | { 343 | if (c.Option == null) 344 | throw new InvalidOperationException("OptionContext.Option is null."); 345 | if (index >= c.Option.MaxValueCount) 346 | throw new ArgumentOutOfRangeException("index"); 347 | if (c.Option.OptionValueType == OptionValueType.Required && 348 | index >= values.Count) 349 | throw new OptionException(string.Format( 350 | c.OptionSet.MessageLocalizer("Missing required value for option '{0}'."), c.OptionName), 351 | c.OptionName); 352 | } 353 | 354 | public string this[int index] 355 | { 356 | get 357 | { 358 | AssertValid(index); 359 | return index >= values.Count ? null : values[index]; 360 | } 361 | set 362 | { 363 | values[index] = value; 364 | } 365 | } 366 | #endregion 367 | 368 | public List ToList() 369 | { 370 | return new List(values); 371 | } 372 | 373 | public string[] ToArray() 374 | { 375 | return values.ToArray(); 376 | } 377 | 378 | public override string ToString() 379 | { 380 | return string.Join(", ", values.ToArray()); 381 | } 382 | } 383 | 384 | public class OptionContext 385 | { 386 | private Option option; 387 | private string name; 388 | private int index; 389 | private OptionSet set; 390 | private OptionValueCollection c; 391 | 392 | public OptionContext(OptionSet set) 393 | { 394 | this.set = set; 395 | this.c = new OptionValueCollection(this); 396 | } 397 | 398 | public Option Option 399 | { 400 | get { return option; } 401 | set { option = value; } 402 | } 403 | 404 | public string OptionName 405 | { 406 | get { return name; } 407 | set { name = value; } 408 | } 409 | 410 | public int OptionIndex 411 | { 412 | get { return index; } 413 | set { index = value; } 414 | } 415 | 416 | public OptionSet OptionSet 417 | { 418 | get { return set; } 419 | } 420 | 421 | public OptionValueCollection OptionValues 422 | { 423 | get { return c; } 424 | } 425 | } 426 | 427 | public enum OptionValueType 428 | { 429 | None, 430 | Optional, 431 | Required, 432 | } 433 | 434 | public abstract class Option 435 | { 436 | string prototype, description; 437 | string[] names; 438 | OptionValueType type; 439 | int count; 440 | string[] separators; 441 | bool hidden; 442 | 443 | protected Option(string prototype, string description) 444 | : this(prototype, description, 1, false) 445 | { 446 | } 447 | 448 | protected Option(string prototype, string description, int maxValueCount) 449 | : this(prototype, description, maxValueCount, false) 450 | { 451 | } 452 | 453 | protected Option(string prototype, string description, int maxValueCount, bool hidden) 454 | { 455 | if (prototype == null) 456 | throw new ArgumentNullException("prototype"); 457 | if (prototype.Length == 0) 458 | throw new ArgumentException("Cannot be the empty string.", "prototype"); 459 | if (maxValueCount < 0) 460 | throw new ArgumentOutOfRangeException("maxValueCount"); 461 | 462 | this.prototype = prototype; 463 | this.description = description; 464 | this.count = maxValueCount; 465 | this.names = (this is OptionSet.Category) 466 | // append GetHashCode() so that "duplicate" categories have distinct 467 | // names, e.g. adding multiple "" categories should be valid. 468 | ? new[] { prototype + this.GetHashCode() } 469 | : prototype.Split('|'); 470 | 471 | if (this is OptionSet.Category || this is CommandOption) 472 | return; 473 | 474 | this.type = ParsePrototype(); 475 | this.hidden = hidden; 476 | 477 | if (this.count == 0 && type != OptionValueType.None) 478 | throw new ArgumentException( 479 | "Cannot provide maxValueCount of 0 for OptionValueType.Required or " + 480 | "OptionValueType.Optional.", 481 | "maxValueCount"); 482 | if (this.type == OptionValueType.None && maxValueCount > 1) 483 | throw new ArgumentException( 484 | string.Format("Cannot provide maxValueCount of {0} for OptionValueType.None.", maxValueCount), 485 | "maxValueCount"); 486 | if (Array.IndexOf(names, "<>") >= 0 && 487 | ((names.Length == 1 && this.type != OptionValueType.None) || 488 | (names.Length > 1 && this.MaxValueCount > 1))) 489 | throw new ArgumentException( 490 | "The default option handler '<>' cannot require values.", 491 | "prototype"); 492 | } 493 | 494 | public string Prototype { get { return prototype; } } 495 | public string Description { get { return description; } } 496 | public OptionValueType OptionValueType { get { return type; } } 497 | public int MaxValueCount { get { return count; } } 498 | public bool Hidden { get { return hidden; } } 499 | 500 | public string[] GetNames() 501 | { 502 | return (string[])names.Clone(); 503 | } 504 | 505 | public string[] GetValueSeparators() 506 | { 507 | if (separators == null) 508 | return new string[0]; 509 | return (string[])separators.Clone(); 510 | } 511 | 512 | protected static T Parse(string value, OptionContext c) 513 | { 514 | Type tt = typeof(T); 515 | #if PCL 516 | TypeInfo ti = tt.GetTypeInfo (); 517 | #else 518 | Type ti = tt; 519 | #endif 520 | bool nullable = 521 | ti.IsValueType && 522 | ti.IsGenericType && 523 | !ti.IsGenericTypeDefinition && 524 | ti.GetGenericTypeDefinition() == typeof(Nullable<>); 525 | #if PCL 526 | Type targetType = nullable ? tt.GenericTypeArguments [0] : tt; 527 | #else 528 | Type targetType = nullable ? tt.GetGenericArguments()[0] : tt; 529 | #endif 530 | T t = default(T); 531 | try 532 | { 533 | if (value != null) 534 | { 535 | #if PCL 536 | if (targetType.GetTypeInfo ().IsEnum) 537 | t = (T) Enum.Parse (targetType, value, true); 538 | else 539 | t = (T) Convert.ChangeType (value, targetType); 540 | #else 541 | TypeConverter conv = TypeDescriptor.GetConverter(targetType); 542 | t = (T)conv.ConvertFromString(value); 543 | #endif 544 | } 545 | } 546 | catch (Exception e) 547 | { 548 | throw new OptionException( 549 | string.Format( 550 | c.OptionSet.MessageLocalizer("Could not convert string `{0}' to type {1} for option `{2}'."), 551 | value, targetType.Name, c.OptionName), 552 | c.OptionName, e); 553 | } 554 | return t; 555 | } 556 | 557 | internal string[] Names { get { return names; } } 558 | internal string[] ValueSeparators { get { return separators; } } 559 | 560 | static readonly char[] NameTerminator = new char[] { '=', ':' }; 561 | 562 | private OptionValueType ParsePrototype() 563 | { 564 | char type = '\0'; 565 | List seps = new List(); 566 | for (int i = 0; i < names.Length; ++i) 567 | { 568 | string name = names[i]; 569 | if (name.Length == 0) 570 | throw new ArgumentException("Empty option names are not supported.", "prototype"); 571 | 572 | int end = name.IndexOfAny(NameTerminator); 573 | if (end == -1) 574 | continue; 575 | names[i] = name.Substring(0, end); 576 | if (type == '\0' || type == name[end]) 577 | type = name[end]; 578 | else 579 | throw new ArgumentException( 580 | string.Format("Conflicting option types: '{0}' vs. '{1}'.", type, name[end]), 581 | "prototype"); 582 | AddSeparators(name, end, seps); 583 | } 584 | 585 | if (type == '\0') 586 | return OptionValueType.None; 587 | 588 | if (count <= 1 && seps.Count != 0) 589 | throw new ArgumentException( 590 | string.Format("Cannot provide key/value separators for Options taking {0} value(s).", count), 591 | "prototype"); 592 | if (count > 1) 593 | { 594 | if (seps.Count == 0) 595 | this.separators = new string[] { ":", "=" }; 596 | else if (seps.Count == 1 && seps[0].Length == 0) 597 | this.separators = null; 598 | else 599 | this.separators = seps.ToArray(); 600 | } 601 | 602 | return type == '=' ? OptionValueType.Required : OptionValueType.Optional; 603 | } 604 | 605 | private static void AddSeparators(string name, int end, ICollection seps) 606 | { 607 | int start = -1; 608 | for (int i = end + 1; i < name.Length; ++i) 609 | { 610 | switch (name[i]) 611 | { 612 | case '{': 613 | if (start != -1) 614 | throw new ArgumentException( 615 | string.Format("Ill-formed name/value separator found in \"{0}\".", name), 616 | "prototype"); 617 | start = i + 1; 618 | break; 619 | case '}': 620 | if (start == -1) 621 | throw new ArgumentException( 622 | string.Format("Ill-formed name/value separator found in \"{0}\".", name), 623 | "prototype"); 624 | seps.Add(name.Substring(start, i - start)); 625 | start = -1; 626 | break; 627 | default: 628 | if (start == -1) 629 | seps.Add(name[i].ToString()); 630 | break; 631 | } 632 | } 633 | if (start != -1) 634 | throw new ArgumentException( 635 | string.Format("Ill-formed name/value separator found in \"{0}\".", name), 636 | "prototype"); 637 | } 638 | 639 | public void Invoke(OptionContext c) 640 | { 641 | OnParseComplete(c); 642 | c.OptionName = null; 643 | c.Option = null; 644 | c.OptionValues.Clear(); 645 | } 646 | 647 | protected abstract void OnParseComplete(OptionContext c); 648 | 649 | internal void InvokeOnParseComplete(OptionContext c) 650 | { 651 | OnParseComplete(c); 652 | } 653 | 654 | public override string ToString() 655 | { 656 | return Prototype; 657 | } 658 | } 659 | 660 | public abstract class ArgumentSource 661 | { 662 | 663 | protected ArgumentSource() 664 | { 665 | } 666 | 667 | public abstract string[] GetNames(); 668 | public abstract string Description { get; } 669 | public abstract bool GetArguments(string value, out IEnumerable replacement); 670 | 671 | #if !PCL || NETSTANDARD1_3 672 | public static IEnumerable GetArgumentsFromFile(string file) 673 | { 674 | return GetArguments(File.OpenText(file), true); 675 | } 676 | #endif 677 | 678 | public static IEnumerable GetArguments(TextReader reader) 679 | { 680 | return GetArguments(reader, false); 681 | } 682 | 683 | // Cribbed from mcs/driver.cs:LoadArgs(string) 684 | static IEnumerable GetArguments(TextReader reader, bool close) 685 | { 686 | try 687 | { 688 | StringBuilder arg = new StringBuilder(); 689 | 690 | string line; 691 | while ((line = reader.ReadLine()) != null) 692 | { 693 | int t = line.Length; 694 | 695 | for (int i = 0; i < t; i++) 696 | { 697 | char c = line[i]; 698 | 699 | if (c == '"' || c == '\'') 700 | { 701 | char end = c; 702 | 703 | for (i++; i < t; i++) 704 | { 705 | c = line[i]; 706 | 707 | if (c == end) 708 | break; 709 | arg.Append(c); 710 | } 711 | } 712 | else if (c == ' ') 713 | { 714 | if (arg.Length > 0) 715 | { 716 | yield return arg.ToString(); 717 | arg.Length = 0; 718 | } 719 | } 720 | else 721 | arg.Append(c); 722 | } 723 | if (arg.Length > 0) 724 | { 725 | yield return arg.ToString(); 726 | arg.Length = 0; 727 | } 728 | } 729 | } 730 | finally 731 | { 732 | if (close) 733 | reader.Dispose(); 734 | } 735 | } 736 | } 737 | 738 | #if !PCL || NETSTANDARD1_3 739 | public class ResponseFileSource : ArgumentSource 740 | { 741 | 742 | public override string[] GetNames() 743 | { 744 | return new string[] { "@file" }; 745 | } 746 | 747 | public override string Description 748 | { 749 | get { return "Read response file for more options."; } 750 | } 751 | 752 | public override bool GetArguments(string value, out IEnumerable replacement) 753 | { 754 | if (string.IsNullOrEmpty(value) || !value.StartsWith("@")) 755 | { 756 | replacement = null; 757 | return false; 758 | } 759 | replacement = ArgumentSource.GetArgumentsFromFile(value.Substring(1)); 760 | return true; 761 | } 762 | } 763 | #endif 764 | 765 | #if !PCL 766 | [Serializable] 767 | #endif 768 | public class OptionException : Exception 769 | { 770 | private string option; 771 | 772 | public OptionException() 773 | { 774 | } 775 | 776 | public OptionException(string message, string optionName) 777 | : base(message) 778 | { 779 | this.option = optionName; 780 | } 781 | 782 | public OptionException(string message, string optionName, Exception innerException) 783 | : base(message, innerException) 784 | { 785 | this.option = optionName; 786 | } 787 | 788 | #if !PCL 789 | protected OptionException(SerializationInfo info, StreamingContext context) 790 | : base(info, context) 791 | { 792 | this.option = info.GetString("OptionName"); 793 | } 794 | #endif 795 | 796 | public string OptionName 797 | { 798 | get { return this.option; } 799 | } 800 | 801 | #if !PCL 802 | #pragma warning disable 618 // SecurityPermissionAttribute is obsolete 803 | [SecurityPermission(SecurityAction.LinkDemand, SerializationFormatter = true)] 804 | #pragma warning restore 618 805 | public override void GetObjectData(SerializationInfo info, StreamingContext context) 806 | { 807 | base.GetObjectData(info, context); 808 | info.AddValue("OptionName", option); 809 | } 810 | #endif 811 | } 812 | 813 | public delegate void OptionAction(TKey key, TValue value); 814 | 815 | public class OptionSet : KeyedCollection 816 | { 817 | public OptionSet() 818 | : this(null, null) 819 | { 820 | } 821 | 822 | public OptionSet(MessageLocalizerConverter localizer) 823 | : this(localizer, null) 824 | { 825 | } 826 | 827 | public OptionSet(StringComparer comparer) 828 | : this(null, comparer) 829 | { 830 | } 831 | 832 | public OptionSet(MessageLocalizerConverter localizer, StringComparer comparer) 833 | : base(comparer) 834 | { 835 | this.roSources = new ReadOnlyCollection(sources); 836 | this.localizer = localizer; 837 | if (this.localizer == null) 838 | { 839 | this.localizer = delegate (string f) { 840 | return f; 841 | }; 842 | } 843 | } 844 | 845 | MessageLocalizerConverter localizer; 846 | 847 | public MessageLocalizerConverter MessageLocalizer 848 | { 849 | get { return localizer; } 850 | internal set { localizer = value; } 851 | } 852 | 853 | List sources = new List(); 854 | ReadOnlyCollection roSources; 855 | 856 | public ReadOnlyCollection ArgumentSources 857 | { 858 | get { return roSources; } 859 | } 860 | 861 | 862 | protected override string GetKeyForItem(Option item) 863 | { 864 | if (item == null) 865 | throw new ArgumentNullException("option"); 866 | if (item.Names != null && item.Names.Length > 0) 867 | return item.Names[0]; 868 | // This should never happen, as it's invalid for Option to be 869 | // constructed w/o any names. 870 | throw new InvalidOperationException("Option has no names!"); 871 | } 872 | 873 | [Obsolete("Use KeyedCollection.this[string]")] 874 | protected Option GetOptionForName(string option) 875 | { 876 | if (option == null) 877 | throw new ArgumentNullException("option"); 878 | try 879 | { 880 | return base[option]; 881 | } 882 | catch (KeyNotFoundException) 883 | { 884 | return null; 885 | } 886 | } 887 | 888 | protected override void InsertItem(int index, Option item) 889 | { 890 | base.InsertItem(index, item); 891 | AddImpl(item); 892 | } 893 | 894 | protected override void RemoveItem(int index) 895 | { 896 | Option p = Items[index]; 897 | base.RemoveItem(index); 898 | // KeyedCollection.RemoveItem() handles the 0th item 899 | for (int i = 1; i < p.Names.Length; ++i) 900 | { 901 | Dictionary.Remove(p.Names[i]); 902 | } 903 | } 904 | 905 | protected override void SetItem(int index, Option item) 906 | { 907 | base.SetItem(index, item); 908 | AddImpl(item); 909 | } 910 | 911 | private void AddImpl(Option option) 912 | { 913 | if (option == null) 914 | throw new ArgumentNullException("option"); 915 | List added = new List(option.Names.Length); 916 | try 917 | { 918 | // KeyedCollection.InsertItem/SetItem handle the 0th name. 919 | for (int i = 1; i < option.Names.Length; ++i) 920 | { 921 | Dictionary.Add(option.Names[i], option); 922 | added.Add(option.Names[i]); 923 | } 924 | } 925 | catch (Exception) 926 | { 927 | foreach (string name in added) 928 | Dictionary.Remove(name); 929 | throw; 930 | } 931 | } 932 | 933 | public OptionSet Add(string header) 934 | { 935 | if (header == null) 936 | throw new ArgumentNullException("header"); 937 | Add(new Category(header)); 938 | return this; 939 | } 940 | 941 | internal sealed class Category : Option 942 | { 943 | 944 | // Prototype starts with '=' because this is an invalid prototype 945 | // (see Option.ParsePrototype(), and thus it'll prevent Category 946 | // instances from being accidentally used as normal options. 947 | public Category(string description) 948 | : base("=:Category:= " + description, description) 949 | { 950 | } 951 | 952 | protected override void OnParseComplete(OptionContext c) 953 | { 954 | throw new NotSupportedException("Category.OnParseComplete should not be invoked."); 955 | } 956 | } 957 | 958 | 959 | public new OptionSet Add(Option option) 960 | { 961 | base.Add(option); 962 | return this; 963 | } 964 | 965 | sealed class ActionOption : Option 966 | { 967 | Action action; 968 | 969 | public ActionOption(string prototype, string description, int count, Action action) 970 | : this(prototype, description, count, action, false) 971 | { 972 | } 973 | 974 | public ActionOption(string prototype, string description, int count, Action action, bool hidden) 975 | : base(prototype, description, count, hidden) 976 | { 977 | if (action == null) 978 | throw new ArgumentNullException("action"); 979 | this.action = action; 980 | } 981 | 982 | protected override void OnParseComplete(OptionContext c) 983 | { 984 | action(c.OptionValues); 985 | } 986 | } 987 | 988 | public OptionSet Add(string prototype, Action action) 989 | { 990 | return Add(prototype, null, action); 991 | } 992 | 993 | public OptionSet Add(string prototype, string description, Action action) 994 | { 995 | return Add(prototype, description, action, false); 996 | } 997 | 998 | public OptionSet Add(string prototype, string description, Action action, bool hidden) 999 | { 1000 | if (action == null) 1001 | throw new ArgumentNullException("action"); 1002 | Option p = new ActionOption(prototype, description, 1, 1003 | delegate (OptionValueCollection v) { action(v[0]); }, hidden); 1004 | base.Add(p); 1005 | return this; 1006 | } 1007 | 1008 | public OptionSet Add(string prototype, OptionAction action) 1009 | { 1010 | return Add(prototype, null, action); 1011 | } 1012 | 1013 | public OptionSet Add(string prototype, string description, OptionAction action) 1014 | { 1015 | return Add(prototype, description, action, false); 1016 | } 1017 | 1018 | public OptionSet Add(string prototype, string description, OptionAction action, bool hidden) 1019 | { 1020 | if (action == null) 1021 | throw new ArgumentNullException("action"); 1022 | Option p = new ActionOption(prototype, description, 2, 1023 | delegate (OptionValueCollection v) { action(v[0], v[1]); }, hidden); 1024 | base.Add(p); 1025 | return this; 1026 | } 1027 | 1028 | sealed class ActionOption : Option 1029 | { 1030 | Action action; 1031 | 1032 | public ActionOption(string prototype, string description, Action action) 1033 | : base(prototype, description, 1) 1034 | { 1035 | if (action == null) 1036 | throw new ArgumentNullException("action"); 1037 | this.action = action; 1038 | } 1039 | 1040 | protected override void OnParseComplete(OptionContext c) 1041 | { 1042 | action(Parse(c.OptionValues[0], c)); 1043 | } 1044 | } 1045 | 1046 | sealed class ActionOption : Option 1047 | { 1048 | OptionAction action; 1049 | 1050 | public ActionOption(string prototype, string description, OptionAction action) 1051 | : base(prototype, description, 2) 1052 | { 1053 | if (action == null) 1054 | throw new ArgumentNullException("action"); 1055 | this.action = action; 1056 | } 1057 | 1058 | protected override void OnParseComplete(OptionContext c) 1059 | { 1060 | action( 1061 | Parse(c.OptionValues[0], c), 1062 | Parse(c.OptionValues[1], c)); 1063 | } 1064 | } 1065 | 1066 | public OptionSet Add(string prototype, Action action) 1067 | { 1068 | return Add(prototype, null, action); 1069 | } 1070 | 1071 | public OptionSet Add(string prototype, string description, Action action) 1072 | { 1073 | return Add(new ActionOption(prototype, description, action)); 1074 | } 1075 | 1076 | public OptionSet Add(string prototype, OptionAction action) 1077 | { 1078 | return Add(prototype, null, action); 1079 | } 1080 | 1081 | public OptionSet Add(string prototype, string description, OptionAction action) 1082 | { 1083 | return Add(new ActionOption(prototype, description, action)); 1084 | } 1085 | 1086 | public OptionSet Add(ArgumentSource source) 1087 | { 1088 | if (source == null) 1089 | throw new ArgumentNullException("source"); 1090 | sources.Add(source); 1091 | return this; 1092 | } 1093 | 1094 | protected virtual OptionContext CreateOptionContext() 1095 | { 1096 | return new OptionContext(this); 1097 | } 1098 | 1099 | public List Parse(IEnumerable arguments) 1100 | { 1101 | if (arguments == null) 1102 | throw new ArgumentNullException("arguments"); 1103 | OptionContext c = CreateOptionContext(); 1104 | c.OptionIndex = -1; 1105 | bool process = true; 1106 | List unprocessed = new List(); 1107 | Option def = Contains("<>") ? this["<>"] : null; 1108 | ArgumentEnumerator ae = new ArgumentEnumerator(arguments); 1109 | foreach (string argument in ae) 1110 | { 1111 | ++c.OptionIndex; 1112 | if (argument == "--") 1113 | { 1114 | process = false; 1115 | continue; 1116 | } 1117 | if (!process) 1118 | { 1119 | Unprocessed(unprocessed, def, c, argument); 1120 | continue; 1121 | } 1122 | if (AddSource(ae, argument)) 1123 | continue; 1124 | if (!Parse(argument, c)) 1125 | Unprocessed(unprocessed, def, c, argument); 1126 | } 1127 | if (c.Option != null) 1128 | c.Option.Invoke(c); 1129 | return unprocessed; 1130 | } 1131 | 1132 | class ArgumentEnumerator : IEnumerable 1133 | { 1134 | List> sources = new List>(); 1135 | 1136 | public ArgumentEnumerator(IEnumerable arguments) 1137 | { 1138 | sources.Add(arguments.GetEnumerator()); 1139 | } 1140 | 1141 | public void Add(IEnumerable arguments) 1142 | { 1143 | sources.Add(arguments.GetEnumerator()); 1144 | } 1145 | 1146 | public IEnumerator GetEnumerator() 1147 | { 1148 | do 1149 | { 1150 | IEnumerator c = sources[sources.Count - 1]; 1151 | if (c.MoveNext()) 1152 | yield return c.Current; 1153 | else 1154 | { 1155 | c.Dispose(); 1156 | sources.RemoveAt(sources.Count - 1); 1157 | } 1158 | } while (sources.Count > 0); 1159 | } 1160 | 1161 | IEnumerator IEnumerable.GetEnumerator() 1162 | { 1163 | return GetEnumerator(); 1164 | } 1165 | } 1166 | 1167 | bool AddSource(ArgumentEnumerator ae, string argument) 1168 | { 1169 | foreach (ArgumentSource source in sources) 1170 | { 1171 | IEnumerable replacement; 1172 | if (!source.GetArguments(argument, out replacement)) 1173 | continue; 1174 | ae.Add(replacement); 1175 | return true; 1176 | } 1177 | return false; 1178 | } 1179 | 1180 | private static bool Unprocessed(ICollection extra, Option def, OptionContext c, string argument) 1181 | { 1182 | if (def == null) 1183 | { 1184 | extra.Add(argument); 1185 | return false; 1186 | } 1187 | c.OptionValues.Add(argument); 1188 | c.Option = def; 1189 | c.Option.Invoke(c); 1190 | return false; 1191 | } 1192 | 1193 | private readonly Regex ValueOption = new Regex( 1194 | @"^(?--|-|/)(?[^:=]+)((?[:=])(?.*))?$"); 1195 | 1196 | protected bool GetOptionParts(string argument, out string flag, out string name, out string sep, out string value) 1197 | { 1198 | if (argument == null) 1199 | throw new ArgumentNullException("argument"); 1200 | 1201 | flag = name = sep = value = null; 1202 | Match m = ValueOption.Match(argument); 1203 | if (!m.Success) 1204 | { 1205 | return false; 1206 | } 1207 | flag = m.Groups["flag"].Value; 1208 | name = m.Groups["name"].Value; 1209 | if (m.Groups["sep"].Success && m.Groups["value"].Success) 1210 | { 1211 | sep = m.Groups["sep"].Value; 1212 | value = m.Groups["value"].Value; 1213 | } 1214 | return true; 1215 | } 1216 | 1217 | protected virtual bool Parse(string argument, OptionContext c) 1218 | { 1219 | if (c.Option != null) 1220 | { 1221 | ParseValue(argument, c); 1222 | return true; 1223 | } 1224 | 1225 | string f, n, s, v; 1226 | if (!GetOptionParts(argument, out f, out n, out s, out v)) 1227 | return false; 1228 | 1229 | Option p; 1230 | if (Contains(n)) 1231 | { 1232 | p = this[n]; 1233 | c.OptionName = f + n; 1234 | c.Option = p; 1235 | switch (p.OptionValueType) 1236 | { 1237 | case OptionValueType.None: 1238 | c.OptionValues.Add(n); 1239 | c.Option.Invoke(c); 1240 | break; 1241 | case OptionValueType.Optional: 1242 | case OptionValueType.Required: 1243 | ParseValue(v, c); 1244 | break; 1245 | } 1246 | return true; 1247 | } 1248 | // no match; is it a bool option? 1249 | if (ParseBool(argument, n, c)) 1250 | return true; 1251 | // is it a bundled option? 1252 | if (ParseBundledValue(f, string.Concat(n + s + v), c)) 1253 | return true; 1254 | 1255 | return false; 1256 | } 1257 | 1258 | private void ParseValue(string option, OptionContext c) 1259 | { 1260 | if (option != null) 1261 | foreach (string o in c.Option.ValueSeparators != null 1262 | ? option.Split(c.Option.ValueSeparators, c.Option.MaxValueCount - c.OptionValues.Count, StringSplitOptions.None) 1263 | : new string[] { option }) 1264 | { 1265 | c.OptionValues.Add(o); 1266 | } 1267 | if (c.OptionValues.Count == c.Option.MaxValueCount || 1268 | c.Option.OptionValueType == OptionValueType.Optional) 1269 | c.Option.Invoke(c); 1270 | else if (c.OptionValues.Count > c.Option.MaxValueCount) 1271 | { 1272 | throw new OptionException(localizer(string.Format( 1273 | "Error: Found {0} option values when expecting {1}.", 1274 | c.OptionValues.Count, c.Option.MaxValueCount)), 1275 | c.OptionName); 1276 | } 1277 | } 1278 | 1279 | private bool ParseBool(string option, string n, OptionContext c) 1280 | { 1281 | Option p; 1282 | string rn; 1283 | if (n.Length >= 1 && (n[n.Length - 1] == '+' || n[n.Length - 1] == '-') && 1284 | Contains((rn = n.Substring(0, n.Length - 1)))) 1285 | { 1286 | p = this[rn]; 1287 | string v = n[n.Length - 1] == '+' ? option : null; 1288 | c.OptionName = option; 1289 | c.Option = p; 1290 | c.OptionValues.Add(v); 1291 | p.Invoke(c); 1292 | return true; 1293 | } 1294 | return false; 1295 | } 1296 | 1297 | private bool ParseBundledValue(string f, string n, OptionContext c) 1298 | { 1299 | if (f != "-") 1300 | return false; 1301 | for (int i = 0; i < n.Length; ++i) 1302 | { 1303 | Option p; 1304 | string opt = f + n[i].ToString(); 1305 | string rn = n[i].ToString(); 1306 | if (!Contains(rn)) 1307 | { 1308 | if (i == 0) 1309 | return false; 1310 | throw new OptionException(string.Format(localizer( 1311 | "Cannot use unregistered option '{0}' in bundle '{1}'."), rn, f + n), null); 1312 | } 1313 | p = this[rn]; 1314 | switch (p.OptionValueType) 1315 | { 1316 | case OptionValueType.None: 1317 | Invoke(c, opt, n, p); 1318 | break; 1319 | case OptionValueType.Optional: 1320 | case OptionValueType.Required: 1321 | { 1322 | string v = n.Substring(i + 1); 1323 | c.Option = p; 1324 | c.OptionName = opt; 1325 | ParseValue(v.Length != 0 ? v : null, c); 1326 | return true; 1327 | } 1328 | default: 1329 | throw new InvalidOperationException("Unknown OptionValueType: " + p.OptionValueType); 1330 | } 1331 | } 1332 | return true; 1333 | } 1334 | 1335 | private static void Invoke(OptionContext c, string name, string value, Option option) 1336 | { 1337 | c.OptionName = name; 1338 | c.Option = option; 1339 | c.OptionValues.Add(value); 1340 | option.Invoke(c); 1341 | } 1342 | 1343 | private const int OptionWidth = 29; 1344 | private const int Description_FirstWidth = 80 - OptionWidth; 1345 | private const int Description_RemWidth = 80 - OptionWidth - 2; 1346 | 1347 | static readonly string CommandHelpIndentStart = new string(' ', OptionWidth); 1348 | static readonly string CommandHelpIndentRemaining = new string(' ', OptionWidth + 2); 1349 | 1350 | public void WriteOptionDescriptions(TextWriter o) 1351 | { 1352 | foreach (Option p in this) 1353 | { 1354 | int written = 0; 1355 | 1356 | if (p.Hidden) 1357 | continue; 1358 | 1359 | Category c = p as Category; 1360 | if (c != null) 1361 | { 1362 | WriteDescription(o, p.Description, "", 80, 80); 1363 | continue; 1364 | } 1365 | CommandOption co = p as CommandOption; 1366 | if (co != null) 1367 | { 1368 | WriteCommandDescription(o, co.Command, co.CommandName); 1369 | continue; 1370 | } 1371 | 1372 | if (!WriteOptionPrototype(o, p, ref written)) 1373 | continue; 1374 | 1375 | if (written < OptionWidth) 1376 | o.Write(new string(' ', OptionWidth - written)); 1377 | else 1378 | { 1379 | o.WriteLine(); 1380 | o.Write(new string(' ', OptionWidth)); 1381 | } 1382 | 1383 | WriteDescription(o, p.Description, new string(' ', OptionWidth + 2), 1384 | Description_FirstWidth, Description_RemWidth); 1385 | } 1386 | 1387 | foreach (ArgumentSource s in sources) 1388 | { 1389 | string[] names = s.GetNames(); 1390 | if (names == null || names.Length == 0) 1391 | continue; 1392 | 1393 | int written = 0; 1394 | 1395 | Write(o, ref written, " "); 1396 | Write(o, ref written, names[0]); 1397 | for (int i = 1; i < names.Length; ++i) 1398 | { 1399 | Write(o, ref written, ", "); 1400 | Write(o, ref written, names[i]); 1401 | } 1402 | 1403 | if (written < OptionWidth) 1404 | o.Write(new string(' ', OptionWidth - written)); 1405 | else 1406 | { 1407 | o.WriteLine(); 1408 | o.Write(new string(' ', OptionWidth)); 1409 | } 1410 | 1411 | WriteDescription(o, s.Description, new string(' ', OptionWidth + 2), 1412 | Description_FirstWidth, Description_RemWidth); 1413 | } 1414 | } 1415 | 1416 | internal void WriteCommandDescription(TextWriter o, Command c, string commandName) 1417 | { 1418 | var name = new string(' ', 8) + (commandName ?? c.Name); 1419 | if (name.Length < OptionWidth - 1) 1420 | { 1421 | WriteDescription(o, name + new string(' ', OptionWidth - name.Length) + c.Help, CommandHelpIndentRemaining, 80, Description_RemWidth); 1422 | } 1423 | else 1424 | { 1425 | WriteDescription(o, name, "", 80, 80); 1426 | WriteDescription(o, CommandHelpIndentStart + c.Help, CommandHelpIndentRemaining, 80, Description_RemWidth); 1427 | } 1428 | } 1429 | 1430 | void WriteDescription(TextWriter o, string value, string prefix, int firstWidth, int remWidth) 1431 | { 1432 | bool indent = false; 1433 | foreach (string line in GetLines(localizer(GetDescription(value)), firstWidth, remWidth)) 1434 | { 1435 | if (indent) 1436 | o.Write(prefix); 1437 | o.WriteLine(line); 1438 | indent = true; 1439 | } 1440 | } 1441 | 1442 | bool WriteOptionPrototype(TextWriter o, Option p, ref int written) 1443 | { 1444 | string[] names = p.Names; 1445 | 1446 | int i = GetNextOptionIndex(names, 0); 1447 | if (i == names.Length) 1448 | return false; 1449 | 1450 | if (names[i].Length == 1) 1451 | { 1452 | Write(o, ref written, " -"); 1453 | Write(o, ref written, names[0]); 1454 | } 1455 | else 1456 | { 1457 | Write(o, ref written, " --"); 1458 | Write(o, ref written, names[0]); 1459 | } 1460 | 1461 | for (i = GetNextOptionIndex(names, i + 1); 1462 | i < names.Length; i = GetNextOptionIndex(names, i + 1)) 1463 | { 1464 | Write(o, ref written, ", "); 1465 | Write(o, ref written, names[i].Length == 1 ? "-" : "--"); 1466 | Write(o, ref written, names[i]); 1467 | } 1468 | 1469 | if (p.OptionValueType == OptionValueType.Optional || 1470 | p.OptionValueType == OptionValueType.Required) 1471 | { 1472 | if (p.OptionValueType == OptionValueType.Optional) 1473 | { 1474 | Write(o, ref written, localizer("[")); 1475 | } 1476 | Write(o, ref written, localizer("=" + GetArgumentName(0, p.MaxValueCount, p.Description))); 1477 | string sep = p.ValueSeparators != null && p.ValueSeparators.Length > 0 1478 | ? p.ValueSeparators[0] 1479 | : " "; 1480 | for (int c = 1; c < p.MaxValueCount; ++c) 1481 | { 1482 | Write(o, ref written, localizer(sep + GetArgumentName(c, p.MaxValueCount, p.Description))); 1483 | } 1484 | if (p.OptionValueType == OptionValueType.Optional) 1485 | { 1486 | Write(o, ref written, localizer("]")); 1487 | } 1488 | } 1489 | return true; 1490 | } 1491 | 1492 | static int GetNextOptionIndex(string[] names, int i) 1493 | { 1494 | while (i < names.Length && names[i] == "<>") 1495 | { 1496 | ++i; 1497 | } 1498 | return i; 1499 | } 1500 | 1501 | static void Write(TextWriter o, ref int n, string s) 1502 | { 1503 | n += s.Length; 1504 | o.Write(s); 1505 | } 1506 | 1507 | static string GetArgumentName(int index, int maxIndex, string description) 1508 | { 1509 | var matches = Regex.Matches(description ?? "", @"(?<=(? 1 1520 | if (maxIndex > 1 && parts.Length == 2 && 1521 | parts[0] == index.ToString(CultureInfo.InvariantCulture)) 1522 | { 1523 | argName = parts[1]; 1524 | } 1525 | } 1526 | 1527 | if (string.IsNullOrEmpty(argName)) 1528 | { 1529 | argName = maxIndex == 1 ? "VALUE" : "VALUE" + (index + 1); 1530 | } 1531 | return argName; 1532 | } 1533 | 1534 | private static string GetDescription(string description) 1535 | { 1536 | if (description == null) 1537 | return string.Empty; 1538 | StringBuilder sb = new StringBuilder(description.Length); 1539 | int start = -1; 1540 | for (int i = 0; i < description.Length; ++i) 1541 | { 1542 | switch (description[i]) 1543 | { 1544 | case '{': 1545 | if (i == start) 1546 | { 1547 | sb.Append('{'); 1548 | start = -1; 1549 | } 1550 | else if (start < 0) 1551 | start = i + 1; 1552 | break; 1553 | case '}': 1554 | if (start < 0) 1555 | { 1556 | if ((i + 1) == description.Length || description[i + 1] != '}') 1557 | throw new InvalidOperationException("Invalid option description: " + description); 1558 | ++i; 1559 | sb.Append("}"); 1560 | } 1561 | else 1562 | { 1563 | sb.Append(description.Substring(start, i - start)); 1564 | start = -1; 1565 | } 1566 | break; 1567 | case ':': 1568 | if (start < 0) 1569 | goto default; 1570 | start = i + 1; 1571 | break; 1572 | default: 1573 | if (start < 0) 1574 | sb.Append(description[i]); 1575 | break; 1576 | } 1577 | } 1578 | return sb.ToString(); 1579 | } 1580 | 1581 | private static IEnumerable GetLines(string description, int firstWidth, int remWidth) 1582 | { 1583 | return StringCoda.WrappedLines(description, firstWidth, remWidth); 1584 | } 1585 | } 1586 | 1587 | public class Command 1588 | { 1589 | public string Name { get; } 1590 | public string Help { get; } 1591 | 1592 | public OptionSet Options { get; set; } 1593 | public Action> Run { get; set; } 1594 | 1595 | public CommandSet CommandSet { get; internal set; } 1596 | 1597 | public Command(string name, string help = null) 1598 | { 1599 | if (string.IsNullOrEmpty(name)) 1600 | throw new ArgumentNullException(nameof(name)); 1601 | 1602 | Name = NormalizeCommandName(name); 1603 | Help = help; 1604 | } 1605 | 1606 | static string NormalizeCommandName(string name) 1607 | { 1608 | var value = new StringBuilder(name.Length); 1609 | var space = false; 1610 | for (int i = 0; i < name.Length; ++i) 1611 | { 1612 | if (!char.IsWhiteSpace(name, i)) 1613 | { 1614 | space = false; 1615 | value.Append(name[i]); 1616 | } 1617 | else if (!space) 1618 | { 1619 | space = true; 1620 | value.Append(' '); 1621 | } 1622 | } 1623 | return value.ToString(); 1624 | } 1625 | 1626 | public virtual int Invoke(IEnumerable arguments) 1627 | { 1628 | var rest = Options?.Parse(arguments) ?? arguments; 1629 | Run?.Invoke(rest); 1630 | return 0; 1631 | } 1632 | } 1633 | 1634 | class CommandOption : Option 1635 | { 1636 | public Command Command { get; } 1637 | public string CommandName { get; } 1638 | 1639 | // Prototype starts with '=' because this is an invalid prototype 1640 | // (see Option.ParsePrototype(), and thus it'll prevent Category 1641 | // instances from being accidentally used as normal options. 1642 | public CommandOption(Command command, string commandName = null, bool hidden = false) 1643 | : base("=:Command:= " + (commandName ?? command?.Name), (commandName ?? command?.Name), maxValueCount: 0, hidden: hidden) 1644 | { 1645 | if (command == null) 1646 | throw new ArgumentNullException(nameof(command)); 1647 | Command = command; 1648 | CommandName = commandName ?? command.Name; 1649 | } 1650 | 1651 | protected override void OnParseComplete(OptionContext c) 1652 | { 1653 | throw new NotSupportedException("CommandOption.OnParseComplete should not be invoked."); 1654 | } 1655 | } 1656 | 1657 | class HelpOption : Option 1658 | { 1659 | Option option; 1660 | CommandSet commands; 1661 | 1662 | public HelpOption(CommandSet commands, Option d) 1663 | : base(d.Prototype, d.Description, d.MaxValueCount, d.Hidden) 1664 | { 1665 | this.commands = commands; 1666 | this.option = d; 1667 | } 1668 | 1669 | protected override void OnParseComplete(OptionContext c) 1670 | { 1671 | commands.showHelp = true; 1672 | 1673 | option?.InvokeOnParseComplete(c); 1674 | } 1675 | } 1676 | 1677 | class CommandOptionSet : OptionSet 1678 | { 1679 | CommandSet commands; 1680 | 1681 | public CommandOptionSet(CommandSet commands, MessageLocalizerConverter localizer) 1682 | : base(localizer) 1683 | { 1684 | this.commands = commands; 1685 | } 1686 | 1687 | protected override void SetItem(int index, Option item) 1688 | { 1689 | if (ShouldWrapOption(item)) 1690 | { 1691 | base.SetItem(index, new HelpOption(commands, item)); 1692 | return; 1693 | } 1694 | base.SetItem(index, item); 1695 | } 1696 | 1697 | bool ShouldWrapOption(Option item) 1698 | { 1699 | if (item == null) 1700 | return false; 1701 | var help = item as HelpOption; 1702 | if (help != null) 1703 | return false; 1704 | foreach (var n in item.Names) 1705 | { 1706 | if (n == "help") 1707 | return true; 1708 | } 1709 | return false; 1710 | } 1711 | 1712 | protected override void InsertItem(int index, Option item) 1713 | { 1714 | if (ShouldWrapOption(item)) 1715 | { 1716 | base.InsertItem(index, new HelpOption(commands, item)); 1717 | return; 1718 | } 1719 | base.InsertItem(index, item); 1720 | } 1721 | } 1722 | 1723 | public class CommandSet : KeyedCollection 1724 | { 1725 | readonly string suite; 1726 | 1727 | OptionSet options; 1728 | TextWriter outWriter; 1729 | TextWriter errorWriter; 1730 | 1731 | internal List NestedCommandSets; 1732 | 1733 | internal HelpCommand help; 1734 | 1735 | internal bool showHelp; 1736 | 1737 | internal OptionSet Options => options; 1738 | 1739 | #if !PCL || NETSTANDARD1_3 1740 | public CommandSet(string suite, MessageLocalizerConverter localizer = null) 1741 | : this(suite, Console.Out, Console.Error, localizer) 1742 | { 1743 | } 1744 | #endif 1745 | 1746 | public CommandSet(string suite, TextWriter output, TextWriter error, MessageLocalizerConverter localizer = null) 1747 | { 1748 | if (suite == null) 1749 | throw new ArgumentNullException(nameof(suite)); 1750 | if (output == null) 1751 | throw new ArgumentNullException(nameof(output)); 1752 | if (error == null) 1753 | throw new ArgumentNullException(nameof(error)); 1754 | 1755 | this.suite = suite; 1756 | options = new CommandOptionSet(this, localizer); 1757 | outWriter = output; 1758 | errorWriter = error; 1759 | } 1760 | 1761 | public string Suite => suite; 1762 | public TextWriter Out => outWriter; 1763 | public TextWriter Error => errorWriter; 1764 | public MessageLocalizerConverter MessageLocalizer => options.MessageLocalizer; 1765 | 1766 | protected override string GetKeyForItem(Command item) 1767 | { 1768 | return item?.Name; 1769 | } 1770 | 1771 | public new CommandSet Add(Command value) 1772 | { 1773 | if (value == null) 1774 | throw new ArgumentNullException(nameof(value)); 1775 | AddCommand(value); 1776 | options.Add(new CommandOption(value)); 1777 | return this; 1778 | } 1779 | 1780 | void AddCommand(Command value) 1781 | { 1782 | if (value.CommandSet != null && value.CommandSet != this) 1783 | { 1784 | throw new ArgumentException("Command instances can only be added to a single CommandSet.", nameof(value)); 1785 | } 1786 | value.CommandSet = this; 1787 | if (value.Options != null) 1788 | { 1789 | value.Options.MessageLocalizer = options.MessageLocalizer; 1790 | } 1791 | 1792 | base.Add(value); 1793 | 1794 | help = help ?? value as HelpCommand; 1795 | } 1796 | 1797 | public CommandSet Add(string header) 1798 | { 1799 | options.Add(header); 1800 | return this; 1801 | } 1802 | 1803 | public CommandSet Add(Option option) 1804 | { 1805 | options.Add(option); 1806 | return this; 1807 | } 1808 | 1809 | public CommandSet Add(string prototype, Action action) 1810 | { 1811 | options.Add(prototype, action); 1812 | return this; 1813 | } 1814 | 1815 | public CommandSet Add(string prototype, string description, Action action) 1816 | { 1817 | options.Add(prototype, description, action); 1818 | return this; 1819 | } 1820 | 1821 | public CommandSet Add(string prototype, string description, Action action, bool hidden) 1822 | { 1823 | options.Add(prototype, description, action, hidden); 1824 | return this; 1825 | } 1826 | 1827 | public CommandSet Add(string prototype, OptionAction action) 1828 | { 1829 | options.Add(prototype, action); 1830 | return this; 1831 | } 1832 | 1833 | public CommandSet Add(string prototype, string description, OptionAction action) 1834 | { 1835 | options.Add(prototype, description, action); 1836 | return this; 1837 | } 1838 | 1839 | public CommandSet Add(string prototype, string description, OptionAction action, bool hidden) 1840 | { 1841 | options.Add(prototype, description, action, hidden); 1842 | return this; 1843 | } 1844 | 1845 | public CommandSet Add(string prototype, Action action) 1846 | { 1847 | options.Add(prototype, null, action); 1848 | return this; 1849 | } 1850 | 1851 | public CommandSet Add(string prototype, string description, Action action) 1852 | { 1853 | options.Add(prototype, description, action); 1854 | return this; 1855 | } 1856 | 1857 | public CommandSet Add(string prototype, OptionAction action) 1858 | { 1859 | options.Add(prototype, action); 1860 | return this; 1861 | } 1862 | 1863 | public CommandSet Add(string prototype, string description, OptionAction action) 1864 | { 1865 | options.Add(prototype, description, action); 1866 | return this; 1867 | } 1868 | 1869 | public CommandSet Add(ArgumentSource source) 1870 | { 1871 | options.Add(source); 1872 | return this; 1873 | } 1874 | 1875 | public CommandSet Add(CommandSet nestedCommands) 1876 | { 1877 | if (nestedCommands == null) 1878 | throw new ArgumentNullException(nameof(nestedCommands)); 1879 | 1880 | if (NestedCommandSets == null) 1881 | { 1882 | NestedCommandSets = new List(); 1883 | } 1884 | 1885 | if (!AlreadyAdded(nestedCommands)) 1886 | { 1887 | NestedCommandSets.Add(nestedCommands); 1888 | foreach (var o in nestedCommands.options) 1889 | { 1890 | if (o is CommandOption c) 1891 | { 1892 | options.Add(new CommandOption(c.Command, $"{nestedCommands.Suite} {c.CommandName}")); 1893 | } 1894 | else 1895 | { 1896 | options.Add(o); 1897 | } 1898 | } 1899 | } 1900 | 1901 | nestedCommands.options = this.options; 1902 | nestedCommands.outWriter = this.outWriter; 1903 | nestedCommands.errorWriter = this.errorWriter; 1904 | 1905 | return this; 1906 | } 1907 | 1908 | bool AlreadyAdded(CommandSet value) 1909 | { 1910 | if (value == this) 1911 | return true; 1912 | if (NestedCommandSets == null) 1913 | return false; 1914 | foreach (var nc in NestedCommandSets) 1915 | { 1916 | if (nc.AlreadyAdded(value)) 1917 | return true; 1918 | } 1919 | return false; 1920 | } 1921 | 1922 | public IEnumerable GetCompletions(string prefix = null) 1923 | { 1924 | string rest; 1925 | ExtractToken(ref prefix, out rest); 1926 | 1927 | foreach (var command in this) 1928 | { 1929 | if (command.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) 1930 | { 1931 | yield return command.Name; 1932 | } 1933 | } 1934 | 1935 | if (NestedCommandSets == null) 1936 | yield break; 1937 | 1938 | foreach (var subset in NestedCommandSets) 1939 | { 1940 | if (subset.Suite.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) 1941 | { 1942 | foreach (var c in subset.GetCompletions(rest)) 1943 | { 1944 | yield return $"{subset.Suite} {c}"; 1945 | } 1946 | } 1947 | } 1948 | } 1949 | 1950 | static void ExtractToken(ref string input, out string rest) 1951 | { 1952 | rest = ""; 1953 | input = input ?? ""; 1954 | 1955 | int top = input.Length; 1956 | for (int i = 0; i < top; i++) 1957 | { 1958 | if (char.IsWhiteSpace(input[i])) 1959 | continue; 1960 | 1961 | for (int j = i; j < top; j++) 1962 | { 1963 | if (char.IsWhiteSpace(input[j])) 1964 | { 1965 | rest = input.Substring(j).Trim(); 1966 | input = input.Substring(i, j).Trim(); 1967 | return; 1968 | } 1969 | } 1970 | rest = ""; 1971 | if (i != 0) 1972 | input = input.Substring(i).Trim(); 1973 | return; 1974 | } 1975 | } 1976 | 1977 | public int Run(IEnumerable arguments) 1978 | { 1979 | if (arguments == null) 1980 | throw new ArgumentNullException(nameof(arguments)); 1981 | 1982 | this.showHelp = false; 1983 | if (help == null) 1984 | { 1985 | help = new HelpCommand(); 1986 | AddCommand(help); 1987 | } 1988 | Action setHelp = v => showHelp = v != null; 1989 | if (!options.Contains("help")) 1990 | { 1991 | options.Add("help", "", setHelp, hidden: true); 1992 | } 1993 | if (!options.Contains("?")) 1994 | { 1995 | options.Add("?", "", setHelp, hidden: true); 1996 | } 1997 | var extra = options.Parse(arguments); 1998 | if (extra.Count == 0) 1999 | { 2000 | if (showHelp) 2001 | { 2002 | return help.Invoke(extra); 2003 | } 2004 | Out.WriteLine(options.MessageLocalizer($"Use `{Suite} help` for usage.")); 2005 | return 1; 2006 | } 2007 | var command = GetCommand(extra); 2008 | if (command == null) 2009 | { 2010 | help.WriteUnknownCommand(extra[0]); 2011 | return 1; 2012 | } 2013 | if (showHelp) 2014 | { 2015 | if (command.Options?.Contains("help") ?? true) 2016 | { 2017 | extra.Add("--help"); 2018 | return command.Invoke(extra); 2019 | } 2020 | command.Options.WriteOptionDescriptions(Out); 2021 | return 0; 2022 | } 2023 | return command.Invoke(extra); 2024 | } 2025 | 2026 | internal Command GetCommand(List extra) 2027 | { 2028 | return TryGetLocalCommand(extra) ?? TryGetNestedCommand(extra); 2029 | } 2030 | 2031 | Command TryGetLocalCommand(List extra) 2032 | { 2033 | var name = extra[0]; 2034 | if (Contains(name)) 2035 | { 2036 | extra.RemoveAt(0); 2037 | return this[name]; 2038 | } 2039 | for (int i = 1; i < extra.Count; ++i) 2040 | { 2041 | name = name + " " + extra[i]; 2042 | if (!Contains(name)) 2043 | continue; 2044 | extra.RemoveRange(0, i + 1); 2045 | return this[name]; 2046 | } 2047 | return null; 2048 | } 2049 | 2050 | Command TryGetNestedCommand(List extra) 2051 | { 2052 | if (NestedCommandSets == null) 2053 | return null; 2054 | 2055 | var nestedCommands = NestedCommandSets.Find(c => c.Suite == extra[0]); 2056 | if (nestedCommands == null) 2057 | return null; 2058 | 2059 | var extraCopy = new List(extra); 2060 | extraCopy.RemoveAt(0); 2061 | if (extraCopy.Count == 0) 2062 | return null; 2063 | 2064 | var command = nestedCommands.GetCommand(extraCopy); 2065 | if (command != null) 2066 | { 2067 | extra.Clear(); 2068 | extra.AddRange(extraCopy); 2069 | return command; 2070 | } 2071 | return null; 2072 | } 2073 | } 2074 | 2075 | public class HelpCommand : Command 2076 | { 2077 | public HelpCommand() 2078 | : base("help", help: "Show this message and exit") 2079 | { 2080 | } 2081 | 2082 | public override int Invoke(IEnumerable arguments) 2083 | { 2084 | var extra = new List(arguments ?? new string[0]); 2085 | var _ = CommandSet.Options.MessageLocalizer; 2086 | if (extra.Count == 0) 2087 | { 2088 | CommandSet.Options.WriteOptionDescriptions(CommandSet.Out); 2089 | return 0; 2090 | } 2091 | var command = CommandSet.GetCommand(extra); 2092 | if (command == this || extra.Contains("--help")) 2093 | { 2094 | CommandSet.Out.WriteLine(_($"Usage: {CommandSet.Suite} COMMAND [OPTIONS]")); 2095 | CommandSet.Out.WriteLine(_($"Use `{CommandSet.Suite} help COMMAND` for help on a specific command.")); 2096 | CommandSet.Out.WriteLine(); 2097 | CommandSet.Out.WriteLine(_($"Available commands:")); 2098 | CommandSet.Out.WriteLine(); 2099 | var commands = GetCommands(); 2100 | commands.Sort((x, y) => string.Compare(x.Key, y.Key, StringComparison.OrdinalIgnoreCase)); 2101 | foreach (var c in commands) 2102 | { 2103 | if (c.Key == "help") 2104 | { 2105 | continue; 2106 | } 2107 | CommandSet.Options.WriteCommandDescription(CommandSet.Out, c.Value, c.Key); 2108 | } 2109 | CommandSet.Options.WriteCommandDescription(CommandSet.Out, CommandSet.help, "help"); 2110 | return 0; 2111 | } 2112 | if (command == null) 2113 | { 2114 | WriteUnknownCommand(extra[0]); 2115 | return 1; 2116 | } 2117 | if (command.Options != null) 2118 | { 2119 | command.Options.WriteOptionDescriptions(CommandSet.Out); 2120 | return 0; 2121 | } 2122 | return command.Invoke(new[] { "--help" }); 2123 | } 2124 | 2125 | List> GetCommands() 2126 | { 2127 | var commands = new List>(); 2128 | 2129 | foreach (var c in CommandSet) 2130 | { 2131 | commands.Add(new KeyValuePair(c.Name, c)); 2132 | } 2133 | 2134 | if (CommandSet.NestedCommandSets == null) 2135 | return commands; 2136 | 2137 | foreach (var nc in CommandSet.NestedCommandSets) 2138 | { 2139 | AddNestedCommands(commands, "", nc); 2140 | } 2141 | 2142 | return commands; 2143 | } 2144 | 2145 | void AddNestedCommands(List> commands, string outer, CommandSet value) 2146 | { 2147 | foreach (var v in value) 2148 | { 2149 | commands.Add(new KeyValuePair($"{outer}{value.Suite} {v.Name}", v)); 2150 | } 2151 | if (value.NestedCommandSets == null) 2152 | return; 2153 | foreach (var nc in value.NestedCommandSets) 2154 | { 2155 | AddNestedCommands(commands, $"{outer}{value.Suite} ", nc); 2156 | } 2157 | } 2158 | 2159 | internal void WriteUnknownCommand(string unknownCommand) 2160 | { 2161 | CommandSet.Error.WriteLine(CommandSet.Options.MessageLocalizer($"{CommandSet.Suite}: Unknown command: {unknownCommand}")); 2162 | CommandSet.Error.WriteLine(CommandSet.Options.MessageLocalizer($"{CommandSet.Suite}: Use `{CommandSet.Suite} help` for usage.")); 2163 | } 2164 | } 2165 | } 2166 | 2167 | --------------------------------------------------------------------------------