├── .gitignore ├── Assemblies ├── SDRSharp.Common.dll ├── SDRSharp.PanView.dll └── SDRSharp.Radio.dll ├── SdrsDecoder.Cli ├── Program.cs └── SdrsDecoder.Cli.csproj ├── SdrsDecoder.Plugin ├── PocsagControl.Designer.cs ├── PocsagControl.cs ├── PocsagControl.resx ├── PocsagPlugin.cs ├── PocsagProcessor.cs ├── PocsagSettings.cs ├── SdrsDecoder.Plugin.csproj └── SdrsDecoder.Plugin.csproj.user ├── SdrsDecoder.sln ├── SdrsDecoder ├── Acars │ ├── AcarsChain.cs │ ├── AcarsDecoder.cs │ └── AcarsMessage.cs ├── Ax25 │ ├── Ax25Chain.cs │ ├── Ax25Decoder.cs │ ├── Ax25Frame.cs │ ├── Ax25FramePost.cs │ └── Ax25Message.cs ├── Flex │ ├── FlexChain.cs │ ├── FlexDecoder.cs │ ├── FlexFrame.cs │ └── FlexMessage.cs ├── Manager.cs ├── Pocsag │ ├── PocsagChain.cs │ ├── PocsagDecoder.cs │ └── PocsagMessage.cs ├── SdrsDecoder.csproj └── Support │ ├── Bch.cs │ ├── BitBuffer.cs │ ├── ChainBase.cs │ ├── ChebyFilter.cs │ ├── DcRemover.cs │ ├── Decimator.cs │ ├── FixedSizeQueue.cs │ ├── Fsk2Demodulator.cs │ ├── Interpolator.cs │ ├── IqDemod.cs │ ├── Log.cs │ ├── MessageBase.cs │ ├── NrzDecoder.cs │ ├── Pll.cs │ ├── PllBase.cs │ └── Unstuffer.cs └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | .vs 2 | bin 3 | obj 4 | -------------------------------------------------------------------------------- /Assemblies/SDRSharp.Common.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dustify/SdrSharpPocsagPlugin/5a5f1697ddd0e2c8629f484381db2994235150b2/Assemblies/SDRSharp.Common.dll -------------------------------------------------------------------------------- /Assemblies/SDRSharp.PanView.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dustify/SdrSharpPocsagPlugin/5a5f1697ddd0e2c8629f484381db2994235150b2/Assemblies/SDRSharp.PanView.dll -------------------------------------------------------------------------------- /Assemblies/SDRSharp.Radio.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dustify/SdrSharpPocsagPlugin/5a5f1697ddd0e2c8629f484381db2994235150b2/Assemblies/SDRSharp.Radio.dll -------------------------------------------------------------------------------- /SdrsDecoder.Cli/Program.cs: -------------------------------------------------------------------------------- 1 | namespace SdrsDecoder.Cli 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Diagnostics; 6 | using System.Linq; 7 | using NAudio.Wave; 8 | using SdrsDecoder.Acars; 9 | using SdrsDecoder.Ax25; 10 | using SdrsDecoder.Pocsag; 11 | using SdrsDecoder.Support; 12 | 13 | class Program 14 | { 15 | 16 | 17 | static void Main(string[] args) 18 | { 19 | try 20 | { 21 | //Ax25Debug(); 22 | //Ax25NoDebug(); 23 | Acars(); 24 | } 25 | catch (Exception exception) 26 | { 27 | Log.LogException(exception); 28 | Console.WriteLine($"Exception: {exception.Message}"); 29 | } 30 | 31 | Console.WriteLine("Done."); 32 | Console.ReadKey(true); 33 | } 34 | 35 | static void Acars() 36 | { 37 | var source = "acars2.wav"; 38 | 39 | Console.WriteLine($"Source: {source}"); 40 | 41 | var file = new WaveFileReader(source); 42 | 43 | var samples = new List(); 44 | 45 | while (true) 46 | { 47 | var frame = file.ReadNextSampleFrame(); 48 | 49 | if (frame == null) 50 | { 51 | break; 52 | } 53 | 54 | var s = frame[0]; 55 | 56 | samples.Add(s); 57 | } 58 | 59 | var sr = file.WaveFormat.SampleRate; 60 | 61 | var mc = 0; 62 | var ms = 0; 63 | 64 | var chain = new AcarsChain(sr, (message) => 65 | { 66 | mc++; 67 | 68 | if (message.HasErrors) 69 | { 70 | return; 71 | } 72 | 73 | ms++; 74 | 75 | Console.WriteLine(message.ErrorText + " " + message.Payload); 76 | }); 77 | 78 | var position = 0; 79 | 80 | var sw = new Stopwatch(); 81 | sw.Start(); 82 | 83 | var debug = new List(); 84 | 85 | while (position < samples.Count) 86 | { 87 | var block = samples.Skip(position).Take(1000).ToArray(); 88 | chain.Process( 89 | block, 90 | (s) => { debug.Add(s); } 91 | ); 92 | 93 | position += 1000; 94 | } 95 | 96 | Console.WriteLine($"{ms} / {mc} = {(float)ms / mc}%"); 97 | 98 | sw.Stop(); 99 | 100 | using (var writer = new WaveFileWriter("debug.wav", new WaveFormat(chain.Rv.dsr, 4))) 101 | { 102 | foreach (var ss in debug.ToArray()) 103 | { 104 | writer.WriteSample(ss); 105 | } 106 | } 107 | } 108 | 109 | static void Ax25NoDebug() 110 | { 111 | var source = "TNC_Test_Ver-1 track 1 AUDIO-2352.wav"; 112 | //var source = "TNC_Test_Ver-1 track 2 AUDIO-2352.wav"; 113 | 114 | Console.WriteLine($"Source: {source}"); 115 | 116 | var file = new WaveFileReader(source); 117 | 118 | var samples = new List(); 119 | 120 | while (true) 121 | { 122 | var frame = file.ReadNextSampleFrame(); 123 | 124 | if (frame == null) 125 | { 126 | break; 127 | } 128 | 129 | var s = frame[0]; 130 | 131 | samples.Add(s); 132 | } 133 | 134 | var sr = file.WaveFormat.SampleRate; 135 | 136 | var messageCount = 0f; 137 | var successCount = 0f; 138 | 139 | var chain = new Ax25Chain( 140 | file.WaveFormat.SampleRate, 141 | (message) => 142 | { 143 | messageCount += 1; 144 | 145 | if (!message.HasErrors) 146 | { 147 | successCount += 1; 148 | Console.WriteLine(message.Address + "/" + message.TypeText + ":" + message.Payload); 149 | } 150 | }); 151 | 152 | var position = 0; 153 | 154 | var sw = new Stopwatch(); 155 | sw.Start(); 156 | 157 | while (position < samples.Count) 158 | { 159 | var block = samples.Skip(position).Take(1000).ToArray(); 160 | chain.Process( 161 | block 162 | ); 163 | 164 | position += 1000; 165 | } 166 | 167 | sw.Stop(); 168 | 169 | var realLength = (float)samples.Count / (float)sr; 170 | var procLength = sw.ElapsedMilliseconds / 1000f; 171 | 172 | Console.WriteLine($"Time factor: {procLength / realLength * 100f}% {sw.Elapsed}"); 173 | Console.WriteLine($"{successCount} / {messageCount} = {successCount / messageCount * 100}%"); 174 | } 175 | 176 | static void Ax25Debug() 177 | { 178 | //var source = "TNC_Test_Ver-1 track 1 AUDIO-2352.wav"; 179 | //var source = "TNC_Test_Ver-1 track 2 AUDIO-2352.wav"; 180 | var source = "aprs4-n3.wav"; 181 | //var source = "ax25.wav"; 182 | 183 | Console.WriteLine($"Source: {source}"); 184 | 185 | var file = new WaveFileReader(source); 186 | 187 | var samples = new List(); 188 | 189 | while (true) 190 | { 191 | var frame = file.ReadNextSampleFrame(); 192 | 193 | if (frame == null) 194 | { 195 | break; 196 | } 197 | 198 | var s = frame[0]; 199 | 200 | samples.Add(s); 201 | } 202 | 203 | var sr = file.WaveFormat.SampleRate; 204 | 205 | var messageCount = 0f; 206 | var successCount = 0f; 207 | 208 | var chain = new Ax25Chain( 209 | file.WaveFormat.SampleRate, 210 | (message) => 211 | { 212 | messageCount += 1; 213 | 214 | if (!message.HasErrors) 215 | { 216 | successCount += 1; 217 | Console.WriteLine(message.Address + "/" + message.TypeText + ":" + message.Payload); 218 | } 219 | 220 | }); 221 | 222 | var debug = new List(); 223 | void ws(float sample) { debug.Add(sample); } 224 | 225 | var position = 0; 226 | 227 | var sw = new Stopwatch(); 228 | sw.Start(); 229 | 230 | while (position < samples.Count) 231 | { 232 | var block = samples.Skip(position).Take(1000).ToArray(); 233 | chain.Process( 234 | block 235 | , writeSample: ws 236 | ); 237 | 238 | position += 1000; 239 | } 240 | 241 | sw.Stop(); 242 | 243 | var realLength = (float)samples.Count / (float)sr; 244 | 245 | var procLength = sw.ElapsedMilliseconds / 1000f; 246 | 247 | Console.WriteLine($"Time factor: {procLength / realLength * 100f}% {sw.Elapsed}"); 248 | Console.WriteLine($"{successCount} / {messageCount} = {successCount / messageCount * 100}%"); 249 | 250 | using (var writer = new WaveFileWriter("debug.wav", new WaveFormat(chain.Rv.dsr, 4))) 251 | { 252 | foreach (var ss in debug.ToArray()) 253 | { 254 | writer.WriteSample(ss); 255 | } 256 | } 257 | } 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /SdrsDecoder.Cli/SdrsDecoder.Cli.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0-windows7.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /SdrsDecoder.Plugin/PocsagControl.Designer.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace SdrsDecoder.Plugin 3 | { 4 | partial class PocsagControl 5 | { 6 | /// 7 | /// Required designer variable. 8 | /// 9 | private System.ComponentModel.IContainer components = null; 10 | 11 | /// 12 | /// Clean up any resources being used. 13 | /// 14 | /// true if managed resources should be disposed; otherwise, false. 15 | protected override void Dispose(bool disposing) 16 | { 17 | if (disposing && (components != null)) 18 | { 19 | components.Dispose(); 20 | } 21 | base.Dispose(disposing); 22 | } 23 | 24 | #region Component Designer generated code 25 | 26 | /// 27 | /// Required method for Designer support - do not modify 28 | /// the contents of this method with the code editor. 29 | /// 30 | private void InitializeComponent() 31 | { 32 | components = new System.ComponentModel.Container(); 33 | System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle2 = new System.Windows.Forms.DataGridViewCellStyle(); 34 | dataGridView1 = new System.Windows.Forms.DataGridView(); 35 | Timestamp = new System.Windows.Forms.DataGridViewTextBoxColumn(); 36 | Protocol = new System.Windows.Forms.DataGridViewTextBoxColumn(); 37 | Address = new System.Windows.Forms.DataGridViewTextBoxColumn(); 38 | Errors = new System.Windows.Forms.DataGridViewTextBoxColumn(); 39 | Type = new System.Windows.Forms.DataGridViewTextBoxColumn(); 40 | Payload = new System.Windows.Forms.DataGridViewTextBoxColumn(); 41 | checkBoxDeDuplicate = new System.Windows.Forms.CheckBox(); 42 | checkBoxHideBad = new System.Windows.Forms.CheckBox(); 43 | buttonClear = new System.Windows.Forms.Button(); 44 | checkBoxMultiline = new System.Windows.Forms.CheckBox(); 45 | toolTip1 = new System.Windows.Forms.ToolTip(components); 46 | label1 = new System.Windows.Forms.Label(); 47 | modeSelector = new System.Windows.Forms.ComboBox(); 48 | checkBoxLogging = new System.Windows.Forms.CheckBox(); 49 | label2 = new System.Windows.Forms.Label(); 50 | textBoxFilter = new System.Windows.Forms.TextBox(); 51 | ((System.ComponentModel.ISupportInitialize)dataGridView1).BeginInit(); 52 | SuspendLayout(); 53 | // 54 | // dataGridView1 55 | // 56 | dataGridView1.AllowUserToAddRows = false; 57 | dataGridView1.AllowUserToDeleteRows = false; 58 | dataGridView1.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; 59 | dataGridView1.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.AllCells; 60 | dataGridView1.AutoSizeRowsMode = System.Windows.Forms.DataGridViewAutoSizeRowsMode.AllCells; 61 | dataGridView1.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; 62 | dataGridView1.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { Timestamp, Protocol, Address, Errors, Type, Payload }); 63 | dataGridView1.Location = new System.Drawing.Point(0, 30); 64 | dataGridView1.Name = "dataGridView1"; 65 | dataGridView1.ReadOnly = true; 66 | dataGridView1.RowTemplate.Height = 25; 67 | dataGridView1.Size = new System.Drawing.Size(1418, 381); 68 | dataGridView1.TabIndex = 0; 69 | // 70 | // Timestamp 71 | // 72 | Timestamp.DataPropertyName = "TimestampText"; 73 | Timestamp.HeaderText = "Timestamp"; 74 | Timestamp.Name = "Timestamp"; 75 | Timestamp.ReadOnly = true; 76 | Timestamp.Width = 91; 77 | // 78 | // Protocol 79 | // 80 | Protocol.DataPropertyName = "Protocol"; 81 | Protocol.HeaderText = "Protocol"; 82 | Protocol.Name = "Protocol"; 83 | Protocol.ReadOnly = true; 84 | Protocol.Width = 77; 85 | // 86 | // Address 87 | // 88 | Address.DataPropertyName = "Address"; 89 | dataGridViewCellStyle2.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleRight; 90 | Address.DefaultCellStyle = dataGridViewCellStyle2; 91 | Address.HeaderText = "Address"; 92 | Address.Name = "Address"; 93 | Address.ReadOnly = true; 94 | Address.Width = 74; 95 | // 96 | // Errors 97 | // 98 | Errors.DataPropertyName = "ErrorText"; 99 | Errors.HeaderText = "Error(s)"; 100 | Errors.Name = "Errors"; 101 | Errors.ReadOnly = true; 102 | Errors.Width = 70; 103 | // 104 | // Type 105 | // 106 | Type.DataPropertyName = "TypeText"; 107 | Type.HeaderText = "Type"; 108 | Type.Name = "Type"; 109 | Type.ReadOnly = true; 110 | Type.Width = 56; 111 | // 112 | // Payload 113 | // 114 | Payload.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill; 115 | Payload.DataPropertyName = "Payload"; 116 | Payload.HeaderText = "Payload"; 117 | Payload.Name = "Payload"; 118 | Payload.ReadOnly = true; 119 | // 120 | // checkBoxDeDuplicate 121 | // 122 | checkBoxDeDuplicate.AutoSize = true; 123 | checkBoxDeDuplicate.Location = new System.Drawing.Point(5, 5); 124 | checkBoxDeDuplicate.Name = "checkBoxDeDuplicate"; 125 | checkBoxDeDuplicate.Size = new System.Drawing.Size(94, 19); 126 | checkBoxDeDuplicate.TabIndex = 1; 127 | checkBoxDeDuplicate.Text = "De-duplicate"; 128 | checkBoxDeDuplicate.UseVisualStyleBackColor = true; 129 | // 130 | // checkBoxHideBad 131 | // 132 | checkBoxHideBad.AutoSize = true; 133 | checkBoxHideBad.Location = new System.Drawing.Point(106, 5); 134 | checkBoxHideBad.Name = "checkBoxHideBad"; 135 | checkBoxHideBad.Size = new System.Drawing.Size(121, 19); 136 | checkBoxHideBad.TabIndex = 2; 137 | checkBoxHideBad.Text = "Hide bad decodes"; 138 | checkBoxHideBad.UseVisualStyleBackColor = true; 139 | // 140 | // buttonClear 141 | // 142 | buttonClear.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right; 143 | buttonClear.Location = new System.Drawing.Point(1337, 3); 144 | buttonClear.Name = "buttonClear"; 145 | buttonClear.Size = new System.Drawing.Size(75, 23); 146 | buttonClear.TabIndex = 3; 147 | buttonClear.Text = "Clear"; 148 | toolTip1.SetToolTip(buttonClear, "Clear all entries from the table"); 149 | buttonClear.UseVisualStyleBackColor = true; 150 | // 151 | // checkBoxMultiline 152 | // 153 | checkBoxMultiline.AutoSize = true; 154 | checkBoxMultiline.Location = new System.Drawing.Point(234, 5); 155 | checkBoxMultiline.Name = "checkBoxMultiline"; 156 | checkBoxMultiline.Size = new System.Drawing.Size(99, 19); 157 | checkBoxMultiline.TabIndex = 4; 158 | checkBoxMultiline.Text = "Wrap payload"; 159 | checkBoxMultiline.UseVisualStyleBackColor = true; 160 | // 161 | // toolTip1 162 | // 163 | toolTip1.AutomaticDelay = 0; 164 | // 165 | // label1 166 | // 167 | label1.AutoSize = true; 168 | label1.Location = new System.Drawing.Point(415, 6); 169 | label1.Name = "label1"; 170 | label1.Size = new System.Drawing.Size(38, 15); 171 | label1.TabIndex = 5; 172 | label1.Text = "Mode"; 173 | // 174 | // modeSelector 175 | // 176 | modeSelector.FormattingEnabled = true; 177 | modeSelector.Location = new System.Drawing.Point(459, 3); 178 | modeSelector.Name = "modeSelector"; 179 | modeSelector.Size = new System.Drawing.Size(196, 23); 180 | modeSelector.TabIndex = 6; 181 | // 182 | // checkBoxLogging 183 | // 184 | checkBoxLogging.AutoSize = true; 185 | checkBoxLogging.Location = new System.Drawing.Point(339, 5); 186 | checkBoxLogging.Name = "checkBoxLogging"; 187 | checkBoxLogging.Size = new System.Drawing.Size(70, 19); 188 | checkBoxLogging.TabIndex = 7; 189 | checkBoxLogging.Text = "Logging"; 190 | checkBoxLogging.UseVisualStyleBackColor = true; 191 | // 192 | // label2 193 | // 194 | label2.AutoSize = true; 195 | label2.Location = new System.Drawing.Point(661, 6); 196 | label2.Name = "label2"; 197 | label2.Size = new System.Drawing.Size(33, 15); 198 | label2.TabIndex = 8; 199 | label2.Text = "Filter"; 200 | // 201 | // textBoxFilter 202 | // 203 | textBoxFilter.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; 204 | textBoxFilter.Location = new System.Drawing.Point(700, 3); 205 | textBoxFilter.Name = "textBoxFilter"; 206 | textBoxFilter.Size = new System.Drawing.Size(631, 23); 207 | textBoxFilter.TabIndex = 9; 208 | // 209 | // PocsagControl 210 | // 211 | AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); 212 | AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 213 | Controls.Add(textBoxFilter); 214 | Controls.Add(label2); 215 | Controls.Add(checkBoxLogging); 216 | Controls.Add(modeSelector); 217 | Controls.Add(label1); 218 | Controls.Add(checkBoxMultiline); 219 | Controls.Add(buttonClear); 220 | Controls.Add(checkBoxHideBad); 221 | Controls.Add(checkBoxDeDuplicate); 222 | Controls.Add(dataGridView1); 223 | Name = "PocsagControl"; 224 | Size = new System.Drawing.Size(1418, 411); 225 | ((System.ComponentModel.ISupportInitialize)dataGridView1).EndInit(); 226 | ResumeLayout(false); 227 | PerformLayout(); 228 | } 229 | 230 | #endregion 231 | private System.Windows.Forms.DataGridView dataGridView1; 232 | private System.Windows.Forms.CheckBox checkBoxDeDuplicate; 233 | private System.Windows.Forms.CheckBox checkBoxHideBad; 234 | private System.Windows.Forms.Button buttonClear; 235 | private System.Windows.Forms.CheckBox checkBoxMultiline; 236 | private System.Windows.Forms.ToolTip toolTip1; 237 | private System.Windows.Forms.DataGridViewTextBoxColumn Timestamp; 238 | private System.Windows.Forms.DataGridViewTextBoxColumn Protocol; 239 | private System.Windows.Forms.DataGridViewTextBoxColumn Address; 240 | private System.Windows.Forms.DataGridViewTextBoxColumn Errors; 241 | private System.Windows.Forms.DataGridViewTextBoxColumn Type; 242 | private System.Windows.Forms.DataGridViewTextBoxColumn Payload; 243 | private System.Windows.Forms.Label label1; 244 | private System.Windows.Forms.ComboBox modeSelector; 245 | private System.Windows.Forms.CheckBox checkBoxLogging; 246 | private System.Windows.Forms.Label label2; 247 | private System.Windows.Forms.TextBox textBoxFilter; 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /SdrsDecoder.Plugin/PocsagControl.cs: -------------------------------------------------------------------------------- 1 | namespace SdrsDecoder.Plugin 2 | { 3 | using SDRSharp.Common; 4 | using System; 5 | using System.ComponentModel; 6 | using System.Windows.Forms; 7 | using System.Linq; 8 | using SdrsDecoder.Support; 9 | using System.IO; 10 | 11 | public partial class PocsagControl : UserControl 12 | { 13 | private ISharpControl control; 14 | 15 | private PocsagProcessor processor; 16 | 17 | public PocsagSettings Settings { get; } 18 | 19 | private BindingSource bindingSource; 20 | private BindingList bindingList; 21 | 22 | protected DataGridViewColumn PayloadColumn => this.dataGridView1.Columns["Payload"]; 23 | 24 | private void UpdateMultilineMode() 25 | { 26 | this.Settings.MultilinePayload = this.checkBoxMultiline.Checked; 27 | 28 | this.PayloadColumn.DefaultCellStyle.WrapMode = 29 | this.Settings.MultilinePayload ? 30 | DataGridViewTriState.True : 31 | DataGridViewTriState.False; 32 | 33 | this.PayloadColumn.AutoSizeMode = 34 | this.Settings.MultilinePayload ? 35 | DataGridViewAutoSizeColumnMode.Fill : 36 | DataGridViewAutoSizeColumnMode.NotSet; 37 | } 38 | 39 | public PocsagControl(ISharpControl control) 40 | { 41 | InitializeComponent(); 42 | 43 | this.Settings = new PocsagSettings(); 44 | 45 | this.bindingSource = new BindingSource(); 46 | this.bindingList = new BindingList(); 47 | 48 | this.bindingSource.DataSource = this.bindingList; 49 | 50 | this.control = control; 51 | 52 | this.processor = 53 | new PocsagProcessor( 54 | this.control.AudioSampleRate, 55 | (MessageBase message) => 56 | { 57 | this.MessageReceived(message); 58 | }); 59 | 60 | this.processor.ChangeMode(this.Settings.SelectedMode); 61 | 62 | this.control.RegisterStreamHook( 63 | this.processor, 64 | SDRSharp.Radio.ProcessorType.DemodulatorOutput); 65 | 66 | this.processor.Enabled = true; 67 | 68 | this.dataGridView1.AutoGenerateColumns = false; 69 | 70 | this.dataGridView1.DataSource = this.bindingSource; 71 | 72 | this.checkBoxDeDuplicate.Checked = this.Settings.DeDuplicate; 73 | this.checkBoxHideBad.Checked = this.Settings.HideBadDecodes; 74 | this.checkBoxMultiline.Checked = this.Settings.MultilinePayload; 75 | this.checkBoxLogging.Checked = this.Settings.Logging; 76 | 77 | foreach (var item in Manager.ConfigSets.Select(x => x.Name)) 78 | { 79 | this.modeSelector.Items.Add(item); 80 | } 81 | 82 | this.modeSelector.SelectedIndex = this.modeSelector.FindStringExact(this.Settings.SelectedMode); 83 | 84 | this.textBoxFilter.Text = this.Settings.Filter; 85 | 86 | this.checkBoxDeDuplicate.Click += 87 | (object sender, EventArgs e) => 88 | { 89 | 90 | this.Settings.DeDuplicate = this.checkBoxDeDuplicate.Checked; 91 | }; 92 | 93 | this.checkBoxHideBad.Click += 94 | (object sender, EventArgs e) => 95 | { 96 | this.Settings.HideBadDecodes = this.checkBoxHideBad.Checked; 97 | }; 98 | 99 | this.checkBoxMultiline.Click += 100 | (object sender, EventArgs e) => 101 | { 102 | this.UpdateMultilineMode(); 103 | }; 104 | 105 | this.checkBoxLogging.Click += 106 | (object sender, EventArgs e) => 107 | { 108 | this.Settings.Logging = this.checkBoxLogging.Checked; 109 | }; 110 | 111 | this.buttonClear.Click += 112 | (object sender, EventArgs e) => 113 | { 114 | this.bindingList.Clear(); 115 | }; 116 | 117 | // prevent typing in mode selector 118 | this.modeSelector.KeyPress += (object sender, KeyPressEventArgs e) => 119 | { 120 | e.Handled = true; 121 | }; 122 | 123 | this.modeSelector.SelectedValueChanged += (object sender, EventArgs e) => 124 | { 125 | var value = this.modeSelector.Text; 126 | 127 | this.Settings.SelectedMode = value; 128 | this.processor.ChangeMode(value); 129 | }; 130 | 131 | this.textBoxFilter.TextChanged += (object sender, EventArgs e) => 132 | { 133 | this.Settings.Filter = this.textBoxFilter.Text; 134 | }; 135 | 136 | this.UpdateMultilineMode(); 137 | } 138 | 139 | private static object LogLock = new object(); 140 | 141 | private string CsvifyText(string source) 142 | { 143 | if (string.IsNullOrWhiteSpace(source)) 144 | { 145 | return "\"\""; 146 | } 147 | 148 | var result = source; 149 | 150 | result = result.Replace("\r\n", " "); 151 | result = result.Replace("\n", " "); 152 | result = result.Replace("\r", " "); 153 | 154 | result = result.Replace("\"", "\"\""); 155 | result = $"\"{result}\""; 156 | 157 | return result; 158 | } 159 | 160 | private void LogMessage(MessageBase message, string fileNameSuffix = "") 161 | { 162 | var directory = "sdrs-log"; 163 | 164 | if (!Directory.Exists(directory)) 165 | { 166 | Directory.CreateDirectory(directory); 167 | } 168 | 169 | var filename = DateTime.Now.ToString("yyyy-MM-dd") + fileNameSuffix + ".csv"; 170 | 171 | var path = $"{directory}/{filename}"; 172 | 173 | lock (LogLock) 174 | { 175 | if (!File.Exists(path)) 176 | { 177 | File.WriteAllText(path, "timestamp,protocol,address,errors,type,payload\n"); 178 | } 179 | 180 | var line = ""; 181 | 182 | line += $"{CsvifyText(message.TimestampText)},"; 183 | line += $"{CsvifyText(message.Protocol)},"; 184 | line += $"{CsvifyText(message.Address)},"; 185 | line += $"{CsvifyText(message.ErrorText)},"; 186 | line += $"{CsvifyText(message.TypeText)},"; 187 | line += $"{CsvifyText(message.Payload)}"; 188 | 189 | File.AppendAllLines(path, new string[] { line }); 190 | } 191 | } 192 | 193 | private void MessageReceived(MessageBase message) 194 | { 195 | if (this.InvokeRequired) 196 | { 197 | this.BeginInvoke( 198 | new Action( 199 | (message) => 200 | { 201 | // skip duplicate messages 202 | if (this.Settings.DeDuplicate && 203 | message.Payload != string.Empty && 204 | this.bindingList.Any(x => x.Hash == message.Hash)) 205 | { 206 | return; 207 | } 208 | 209 | if (this.Settings.HideBadDecodes && message.HasErrors) 210 | { 211 | return; 212 | } 213 | 214 | var filter = this.Settings.Filter; 215 | var filterOn = !string.IsNullOrWhiteSpace(filter); 216 | var filterMatched = false; 217 | 218 | if (filterOn) 219 | { 220 | var filterElements = filter.Split(","); 221 | 222 | foreach (var filterElement in filterElements) 223 | { 224 | if (!string.IsNullOrWhiteSpace(message.Address) && message.Address.Contains(filterElement, StringComparison.InvariantCultureIgnoreCase)) 225 | { 226 | filterMatched = true; 227 | break; 228 | } 229 | 230 | if (!string.IsNullOrWhiteSpace(message.Payload) && message.Payload.Contains(filterElement, StringComparison.InvariantCultureIgnoreCase)) 231 | { 232 | filterMatched = true; 233 | break; 234 | } 235 | } 236 | } 237 | 238 | var messageValidForFilter = filterOn && filterMatched; 239 | 240 | int firstDisplayed = this.dataGridView1.FirstDisplayedScrollingRowIndex; 241 | int displayed = this.dataGridView1.DisplayedRowCount(true); 242 | int lastVisible = (firstDisplayed + displayed) - 1; 243 | int lastIndex = this.dataGridView1.RowCount - 1; 244 | 245 | if (messageValidForFilter || !filterOn) 246 | { 247 | this.bindingList.Add(message); 248 | 249 | while (this.bindingList.Count > 1000) 250 | { 251 | this.bindingList.RemoveAt(0); 252 | } 253 | 254 | if (lastVisible == lastIndex) 255 | { 256 | this.dataGridView1.FirstDisplayedScrollingRowIndex = firstDisplayed + 1; 257 | } 258 | } 259 | 260 | if (this.Settings.Logging) 261 | { 262 | // log everything to main log 263 | this.LogMessage(message); 264 | 265 | // log filtered stuff to filtered log 266 | if (messageValidForFilter) 267 | { 268 | this.LogMessage(message, "-filtered"); 269 | } 270 | } 271 | }), 272 | new object[] { message }); 273 | } 274 | } 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /SdrsDecoder.Plugin/PocsagControl.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | True 122 | 123 | 124 | True 125 | 126 | 127 | True 128 | 129 | 130 | True 131 | 132 | 133 | True 134 | 135 | 136 | True 137 | 138 | 139 | True 140 | 141 | 142 | True 143 | 144 | 145 | True 146 | 147 | 148 | True 149 | 150 | 151 | True 152 | 153 | 154 | True 155 | 156 | 157 | 17, 17 158 | 159 | 160 | 17, 17 161 | 162 | -------------------------------------------------------------------------------- /SdrsDecoder.Plugin/PocsagPlugin.cs: -------------------------------------------------------------------------------- 1 | namespace SdrsDecoder.Plugin 2 | { 3 | using SdrsDecoder.Support; 4 | using SDRSharp.Common; 5 | using System; 6 | using System.Windows.Forms; 7 | 8 | public class PocsagPlugin : ISharpPlugin 9 | { 10 | private PocsagControl gui; 11 | private ISharpControl control; 12 | 13 | public UserControl Gui 14 | { 15 | get 16 | { 17 | try 18 | { 19 | if (this.gui == null) 20 | { 21 | this.gui = new PocsagControl(this.control); 22 | } 23 | 24 | return this.gui; 25 | } 26 | catch (Exception exception) 27 | { 28 | Log.LogException(exception); 29 | } 30 | 31 | return null; 32 | } 33 | } 34 | 35 | public string DisplayName => "Decoder"; 36 | 37 | public void Close() 38 | { 39 | } 40 | 41 | public void Initialize(ISharpControl control) 42 | { 43 | try 44 | { 45 | this.control = control; 46 | } 47 | catch (Exception exception) 48 | { 49 | Log.LogException(exception); 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /SdrsDecoder.Plugin/PocsagProcessor.cs: -------------------------------------------------------------------------------- 1 | namespace SdrsDecoder.Plugin 2 | { 3 | using SdrsDecoder.Support; 4 | using SDRSharp.Radio; 5 | using System; 6 | using System.Runtime.InteropServices; 7 | 8 | public unsafe class PocsagProcessor : IRealProcessor 9 | { 10 | public double SampleRate { get; set; } 11 | 12 | public bool Enabled { get; set; } 13 | 14 | public Manager Manager { get; set; } 15 | 16 | public PocsagProcessor(double sampleRate, Action messageReceived) 17 | { 18 | try 19 | { 20 | this.SampleRate = sampleRate; 21 | 22 | this.Manager = 23 | new Manager( 24 | (int)this.SampleRate, 25 | messageReceived); 26 | } 27 | catch (Exception exception) 28 | { 29 | Log.LogException(exception); 30 | } 31 | } 32 | 33 | public void ChangeMode(string mode) 34 | { 35 | this.Manager.ChangeMode(mode); 36 | } 37 | 38 | public void Process(float* buffer, int length) 39 | { 40 | try 41 | { 42 | var source = new float[length]; 43 | 44 | Marshal.Copy((IntPtr)buffer, source, 0, length); 45 | 46 | this.Manager.Process(source); 47 | } 48 | catch (Exception exception) 49 | { 50 | Log.LogException(exception); 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /SdrsDecoder.Plugin/PocsagSettings.cs: -------------------------------------------------------------------------------- 1 | namespace SdrsDecoder.Plugin 2 | { 3 | using SDRSharp.Radio; 4 | 5 | 6 | public class PocsagSettings 7 | { 8 | public bool DeDuplicate 9 | { 10 | get 11 | { 12 | return Utils.GetBooleanSetting("plugin.pocsag.DeDuplicate", true); 13 | } 14 | set 15 | { 16 | Utils.SaveSetting("plugin.pocsag.DeDuplicate", value); 17 | } 18 | } 19 | 20 | public bool HideBadDecodes 21 | { 22 | get 23 | { 24 | return Utils.GetBooleanSetting("plugin.pocsag.HideBadDecodes", true); 25 | } 26 | set 27 | { 28 | Utils.SaveSetting("plugin.pocsag.HideBadDecodes", value); 29 | } 30 | } 31 | 32 | public bool MultilinePayload 33 | { 34 | get 35 | { 36 | return Utils.GetBooleanSetting("plugin.pocsag.MultilinePayload", false); 37 | } 38 | set 39 | { 40 | Utils.SaveSetting("plugin.pocsag.MultilinePayload", value); 41 | } 42 | } 43 | 44 | public string SelectedMode 45 | { 46 | get 47 | { 48 | return Utils.GetStringSetting("plugin.pocsag.SelectedMode", "POCSAG (512, 1200, 2400)"); 49 | } 50 | set 51 | { 52 | Utils.SaveSetting("plugin.pocsag.SelectedMode", value); 53 | } 54 | } 55 | 56 | public bool Logging 57 | { 58 | get 59 | { 60 | return Utils.GetBooleanSetting("plugin.pocsag.Logging", false); 61 | } 62 | set 63 | { 64 | Utils.SaveSetting("plugin.pocsag.Logging", value); 65 | } 66 | } 67 | 68 | public string Filter 69 | { 70 | get 71 | { 72 | return Utils.GetStringSetting("plugin.pocsag.Filter", ""); 73 | } 74 | set 75 | { 76 | Utils.SaveSetting("plugin.pocsag.Filter", value); 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /SdrsDecoder.Plugin/SdrsDecoder.Plugin.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0-windows7.0 5 | Library 6 | true 7 | 8 | 9 | 10 | true 11 | 12 | 13 | 14 | true 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ..\Assemblies\SDRSharp.Common.dll 28 | False 29 | 30 | 31 | ..\Assemblies\SDRSharp.Radio.dll 32 | False 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /SdrsDecoder.Plugin/SdrsDecoder.Plugin.csproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | UserControl 7 | 8 | 9 | 10 | 11 | Designer 12 | 13 | 14 | -------------------------------------------------------------------------------- /SdrsDecoder.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.6.33829.357 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SdrsDecoder", "SdrsDecoder\SdrsDecoder.csproj", "{5D18619C-8809-4199-9227-242977AC0E68}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SdrsDecoder.Cli", "SdrsDecoder.Cli\SdrsDecoder.Cli.csproj", "{721CF5F9-D91B-4DA1-9EE3-98D13FD64E01}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SdrsDecoder.Plugin", "SdrsDecoder.Plugin\SdrsDecoder.Plugin.csproj", "{EF369083-14DE-4F60-8090-FFC7958192C7}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {5D18619C-8809-4199-9227-242977AC0E68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {5D18619C-8809-4199-9227-242977AC0E68}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {5D18619C-8809-4199-9227-242977AC0E68}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {5D18619C-8809-4199-9227-242977AC0E68}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {721CF5F9-D91B-4DA1-9EE3-98D13FD64E01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {721CF5F9-D91B-4DA1-9EE3-98D13FD64E01}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {721CF5F9-D91B-4DA1-9EE3-98D13FD64E01}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {721CF5F9-D91B-4DA1-9EE3-98D13FD64E01}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {EF369083-14DE-4F60-8090-FFC7958192C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {EF369083-14DE-4F60-8090-FFC7958192C7}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {EF369083-14DE-4F60-8090-FFC7958192C7}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {EF369083-14DE-4F60-8090-FFC7958192C7}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {380EE5FF-EBBA-4BB0-83E5-72C49FCF8F45} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /SdrsDecoder/Acars/AcarsChain.cs: -------------------------------------------------------------------------------- 1 | using SdrsDecoder.Support; 2 | using System; 3 | 4 | namespace SdrsDecoder.Acars 5 | { 6 | public class AcarsChain : ChainBase 7 | { 8 | // https://core.ac.uk/reader/8981460 <- heavily referenced for this implementation 9 | 10 | public ResampleValues Rv; 11 | private Interpolator interpolator; 12 | private ChebyFilter filter; 13 | private Decimator decimator; 14 | private DcRemover dcRemover; 15 | private IqDemod iqDemodulator; 16 | private ChebyFilter[] filter2; 17 | private Fsk2Demodulator[] fskDemodulator; 18 | private NrzDecoder[] nrzDecoder; 19 | private AcarsDecoder[] acarsDecoders; 20 | 21 | static float[] SpaceMultipliers = new float[] { 1.0f }; 22 | 23 | private static T[] GetMultipliedObject(Func func) 24 | { 25 | var result = new T[SpaceMultipliers.Length]; 26 | 27 | for (var i = 0; i < SpaceMultipliers.Length; i++) 28 | { 29 | result[i] = func(SpaceMultipliers[i]); 30 | } 31 | 32 | return result; 33 | } 34 | 35 | public AcarsChain(float sampleRate, Action messageReceived) : base(sampleRate, messageReceived) 36 | { 37 | var baud = 2400; 38 | 39 | var filterFactor = 1.2f; 40 | 41 | Rv = GetResampleValues(baud, sampleRate); 42 | interpolator = new Interpolator(Rv.i); 43 | filter = new ChebyFilter(baud * filterFactor, 1f, Rv.isr); 44 | decimator = new Decimator(Rv.d); 45 | dcRemover = new DcRemover(Rv.dsr, baud); 46 | 47 | iqDemodulator = new IqDemod(Rv.dsr, baud, 2400, 1200, SpaceMultipliers); 48 | filter2 = GetMultipliedObject((float sm) => new ChebyFilter(baud * filterFactor, 1f, Rv.dsr)); 49 | fskDemodulator = GetMultipliedObject((float sm) => { var pll = new Pll(Rv.dsr, baud); return new Fsk2Demodulator(baud, Rv.dsr, pll, false); }); 50 | nrzDecoder = GetMultipliedObject((float sm) => new NrzDecoder(0xFFFFFF, 0x686880, NrzMode.Acars)); 51 | acarsDecoders = GetMultipliedObject((float sm) => new AcarsDecoder(messageReceived)); 52 | } 53 | 54 | public override void Process(float[] values, Action writeSample = null) 55 | { 56 | var processed_values = interpolator.Process(values); 57 | processed_values = filter.Process(processed_values); 58 | processed_values = decimator.Process(processed_values); 59 | processed_values = dcRemover.Process(processed_values); 60 | 61 | var iqdemod_multiplied = iqDemodulator.Process(processed_values); 62 | 63 | for (var x = 0; x < SpaceMultipliers.Length; x++) 64 | { 65 | var iqdemod_values = new float[processed_values.Length]; 66 | 67 | for (var y = 0; y < processed_values.Length; y++) 68 | { 69 | iqdemod_values[y] = iqdemod_multiplied[x, y]; 70 | } 71 | 72 | iqdemod_values = filter2[x].Process(iqdemod_values); 73 | 74 | var fsk_demodulated_values = 75 | x == 0 ? 76 | fskDemodulator[x].Process(iqdemod_values, writeSample) : 77 | fskDemodulator[x].Process(iqdemod_values); 78 | 79 | foreach (var value in fsk_demodulated_values) 80 | { 81 | var nrz_decode = nrzDecoder[x].Process(value); 82 | 83 | var acarsDecoder = acarsDecoders[x]; 84 | 85 | if (nrz_decode.IsFlag || acarsDecoder.Bits.Count > (10 * 1024 * 8)) 86 | { 87 | acarsDecoder.Flag(); 88 | } 89 | 90 | if (!nrz_decode.IsFlag) 91 | { 92 | acarsDecoder.Process(nrz_decode.Value); 93 | } 94 | } 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /SdrsDecoder/Acars/AcarsDecoder.cs: -------------------------------------------------------------------------------- 1 | using SdrsDecoder.Support; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace SdrsDecoder.Acars 6 | { 7 | public class AcarsDecoder 8 | { 9 | public AcarsDecoder(Action messageReceived) 10 | { 11 | this.messageReceived = messageReceived; 12 | } 13 | 14 | public static byte ReverseBits(byte b) 15 | { 16 | byte result = 0; 17 | 18 | for (int i = 0; i < 8; i++) 19 | { 20 | result <<= 1; // Shift the result to make room for the next bit 21 | result |= (byte)(b & 1); // Add the next bit from b 22 | b >>= 1; // Shift b to the right to get the next bit 23 | } 24 | return result; 25 | } 26 | 27 | public List Bits = new List(); 28 | 29 | public void Reset() 30 | { 31 | this.Bits = new List(); 32 | } 33 | 34 | public void Flag() 35 | { 36 | if (this.Bits.Count < 40) 37 | { 38 | this.Reset(); 39 | return; 40 | } 41 | 42 | if (this.Bits.Count % 8 != 0) 43 | { 44 | this.Reset(); 45 | return; 46 | } 47 | 48 | // remove DEL at end 49 | this.Bits.RemoveRange(this.Bits.Count - 8, 8); 50 | 51 | var bytes = new List(); 52 | var current_byte = (byte)0; 53 | var bit_counter = 0; 54 | 55 | for (var i = 0; i < this.Bits.Count; i++) 56 | { 57 | var bit = this.Bits[i]; 58 | current_byte |= (byte)(bit ? 1 : 0); 59 | bit_counter++; 60 | 61 | if (bit_counter < 8) 62 | { 63 | current_byte = (byte)(current_byte << 1); 64 | } 65 | else 66 | { 67 | bit_counter = 0; 68 | 69 | var newByte = ReverseBits(current_byte); 70 | bytes.Add(newByte); 71 | 72 | current_byte = 0; 73 | } 74 | } 75 | 76 | var payload = ""; 77 | 78 | // end of message 79 | 80 | // ETX or ETB 81 | // CRC pt1 82 | // CRC pt2 83 | // DEL (already removed) 84 | 85 | // make sure 3rd from last is ETX / ETB 86 | var hasEomError = true; 87 | 88 | var eomByte = bytes[bytes.Count - 3] & 0b01111111; 89 | 90 | if (eomByte == 3 || eomByte == 23) 91 | { 92 | hasEomError = false; 93 | } 94 | 95 | var hasParityError = false; 96 | var hasCrcError = false; 97 | 98 | var rxCrc = (ushort)(bytes[^2] | (bytes[^1] << 8)); 99 | var calcCrc = (ushort)0; 100 | 101 | for (var i = 0; i < bytes.Count - 2; i++) 102 | { 103 | var b = bytes[i]; 104 | var rxParity = (b >> 7) == 1; 105 | var calcParity = false; 106 | 107 | // crc + parity 108 | 109 | calcCrc ^= b; 110 | 111 | for (var y = 0; y < 8; y++) 112 | { 113 | if ((calcCrc & 1) == 1) 114 | { 115 | calcCrc >>= 1; 116 | calcCrc ^= 0x8408; 117 | } 118 | else 119 | { 120 | calcCrc >>= 1; 121 | } 122 | 123 | if (y >= 7) 124 | { 125 | continue; 126 | } 127 | 128 | if ((b >> y & 1) == 0) 129 | { 130 | calcParity = !calcParity; 131 | } 132 | } 133 | 134 | if (rxParity != calcParity) 135 | { 136 | hasParityError = true; 137 | } 138 | 139 | if (i >= bytes.Count - 3) 140 | { 141 | continue; 142 | } 143 | 144 | // get char for payload 145 | var v = b & 0b01111111; 146 | 147 | payload += (char)v; 148 | } 149 | 150 | if (rxCrc != calcCrc) 151 | { 152 | hasCrcError = true; 153 | } 154 | 155 | // process data 156 | var message = new AcarsMessage(); 157 | 158 | message.HasErrors = hasParityError || hasEomError || hasCrcError; 159 | message.ErrorText = message.HasErrors ? "Yes" : "No"; 160 | message.Payload = payload; 161 | 162 | if (message.HasErrors) 163 | { 164 | message.ErrorText += " ("; 165 | 166 | if (hasParityError) 167 | { 168 | message.ErrorText += "P"; 169 | } 170 | 171 | if (hasEomError) 172 | { 173 | message.ErrorText += "E"; 174 | } 175 | 176 | if (hasCrcError) 177 | { 178 | message.ErrorText += "C"; 179 | } 180 | 181 | message.ErrorText += ")"; 182 | } 183 | 184 | this.messageReceived(message); 185 | 186 | this.Reset(); 187 | } 188 | 189 | int currentValue; 190 | private Action messageReceived; 191 | 192 | public void Process(bool value) 193 | { 194 | this.Bits.Add(value); 195 | 196 | currentValue = currentValue << 1; 197 | 198 | if (value) 199 | { 200 | currentValue |= 1; 201 | } 202 | 203 | if (this.Bits.Count % 8 != 0) 204 | { 205 | return; 206 | } 207 | 208 | if ((currentValue & 0xff) == 0b11111110) 209 | { 210 | this.Flag(); 211 | } 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /SdrsDecoder/Acars/AcarsMessage.cs: -------------------------------------------------------------------------------- 1 | using SdrsDecoder.Support; 2 | using System; 3 | 4 | namespace SdrsDecoder.Acars 5 | { 6 | public class AcarsMessage : MessageBase 7 | { 8 | public AcarsMessage() : base(2400) 9 | { 10 | Protocol = $"ACARS / 2400"; 11 | 12 | Hash = DateTime.Now.ToString(); 13 | Type = MessageType.AlphaNumeric; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /SdrsDecoder/Ax25/Ax25Chain.cs: -------------------------------------------------------------------------------- 1 | using SdrsDecoder.Support; 2 | using System; 3 | 4 | namespace SdrsDecoder.Ax25 5 | { 6 | public class Ax25Chain : ChainBase 7 | { 8 | private Interpolator interpolator; 9 | private ChebyFilter filter; 10 | private Decimator decimator; 11 | private DcRemover dcRemover; 12 | private IqDemod iqDemodulator; 13 | private ChebyFilter[] filter2; 14 | private Fsk2Demodulator[] fskDemodulator; 15 | private Unstuffer[] unstuffer; 16 | 17 | private NrzDecoder[] nrzDecoder; 18 | private Ax25Decoder[] ax25Decoder; 19 | 20 | //static float[] SpaceMultipliers = new float[] { 1.0f, 1.1f, 1.2f, 1.3f, 1.4f, 1.5f, 1.6f, 1.7f, 1.8f, 1.9f, 2.0f }; 21 | //static float[] SpaceMultipliers = new float[] { 1.0f }; 22 | 23 | static float[] SpaceMultipliers = new float[] { 1.0f }; 24 | 25 | public ResampleValues Rv { get; private set; } 26 | 27 | private static T[] GetMultipliedObject(Func func) 28 | { 29 | var result = new T[SpaceMultipliers.Length]; 30 | 31 | for (var i = 0; i < SpaceMultipliers.Length; i++) 32 | { 33 | result[i] = func(SpaceMultipliers[i]); 34 | } 35 | 36 | return result; 37 | } 38 | 39 | public Ax25Chain(float sampleRate, Action messageReceived) : base(sampleRate, messageReceived) 40 | { 41 | var baud = 1200; 42 | var mark = 1200; 43 | var space = 2200; 44 | 45 | this.Rv = GetResampleValues(baud, sampleRate); 46 | 47 | var filterFactor = 1.2f; 48 | 49 | interpolator = new Interpolator(Rv.i); 50 | filter = new ChebyFilter(space * filterFactor, 1f, Rv.isr); 51 | decimator = new Decimator(Rv.d); 52 | dcRemover = new DcRemover(Rv.dsr, baud); 53 | iqDemodulator = new IqDemod(Rv.dsr, baud, mark, space, SpaceMultipliers); 54 | 55 | filter2 = GetMultipliedObject((float sm) => new ChebyFilter(baud * filterFactor, 1f, Rv.dsr)); 56 | 57 | fskDemodulator = GetMultipliedObject((float sm) => { var pll = new Pll(Rv.dsr, baud); return new Fsk2Demodulator(baud, Rv.dsr, pll, false); }); 58 | unstuffer = GetMultipliedObject((float sm) => { return new Unstuffer(); }); 59 | nrzDecoder = GetMultipliedObject((float sm) => new NrzDecoder(0xff, 0x7e, NrzMode.Ax25)); 60 | ax25Decoder = GetMultipliedObject((float sm) => new Ax25Decoder(messageReceived, sm)); 61 | } 62 | 63 | public override void Process(float[] values, Action writeSample = null) 64 | { 65 | var processed_values = interpolator.Process(values); 66 | processed_values = filter.Process(processed_values); 67 | processed_values = decimator.Process(processed_values); 68 | processed_values = dcRemover.Process(processed_values); 69 | 70 | var iqdemod_multiplied = iqDemodulator.Process(processed_values); 71 | 72 | for (var x = 0; x < SpaceMultipliers.Length; x++) 73 | { 74 | var iqdemod_values = new float[processed_values.Length]; 75 | 76 | for (var y = 0; y < processed_values.Length; y++) 77 | { 78 | iqdemod_values[y] = iqdemod_multiplied[x, y]; 79 | } 80 | 81 | iqdemod_values = filter2[x].Process(iqdemod_values); 82 | 83 | var fsk_demodulated_values = 84 | x == 0 ? 85 | fskDemodulator[x].Process(iqdemod_values, writeSample) : 86 | fskDemodulator[x].Process(iqdemod_values); 87 | 88 | foreach (var value in fsk_demodulated_values) 89 | { 90 | var nrz_decode = nrzDecoder[x].Process(value); 91 | 92 | var current_ax25Decoder = ax25Decoder[x]; 93 | 94 | if (nrz_decode.IsFlag || current_ax25Decoder.Frame.Bits.Count > (10 * 1024 * 8)) 95 | { 96 | current_ax25Decoder.Flag(); 97 | } 98 | 99 | var unstuffed = unstuffer[x].Process(nrz_decode.Value); 100 | 101 | if (unstuffed.HasValue) 102 | { 103 | current_ax25Decoder.Process(unstuffed.Value); 104 | } 105 | } 106 | } 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /SdrsDecoder/Ax25/Ax25Decoder.cs: -------------------------------------------------------------------------------- 1 | using SdrsDecoder.Support; 2 | using System; 3 | 4 | namespace SdrsDecoder.Ax25 5 | { 6 | public class Ax25Decoder 7 | { 8 | private float spaceMultiplier; 9 | private Action messageReceived; 10 | 11 | public Ax25Decoder(Action messageReceived, float spaceMultiplier) 12 | { 13 | this.spaceMultiplier = spaceMultiplier; 14 | this.messageReceived = messageReceived; 15 | } 16 | 17 | public Ax25FramePost Frame = new Ax25FramePost(0, 0); 18 | 19 | public UInt64 Index = 0; 20 | 21 | public int CurrentValue = 0; 22 | 23 | public void Flag() 24 | { 25 | this.Frame.Process(messageReceived); 26 | this.Frame = new Ax25FramePost(Index, this.spaceMultiplier); 27 | } 28 | 29 | public void Process(bool value) 30 | { 31 | Index++; 32 | 33 | CurrentValue = CurrentValue << 1; 34 | 35 | if (value) 36 | { 37 | CurrentValue |= 1; 38 | } 39 | 40 | var byteValue = (byte)(CurrentValue & 0xFF); 41 | 42 | this.Frame.Add(byteValue); 43 | } 44 | 45 | public void Process(bool[] values) 46 | { 47 | foreach (var value in values) 48 | { 49 | this.Process(value); 50 | } 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /SdrsDecoder/Ax25/Ax25Frame.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Security.Cryptography.X509Certificates; 3 | 4 | namespace SdrsDecoder 5 | { 6 | public struct Ax25Address 7 | { 8 | public string Call; 9 | public byte Ssid; 10 | public byte RR; 11 | public byte CRH; 12 | } 13 | 14 | public enum Ax25ControlType 15 | { 16 | None, 17 | IShort, 18 | SShort, 19 | UShort, 20 | // ILong, 21 | // SLong 22 | } 23 | 24 | public class Ax25Frame 25 | { 26 | private int address_counter = 0; 27 | 28 | public List Addresses { get; } = new List(); 29 | 30 | public Ax25Address CurrentAddress = new Ax25Address(); 31 | 32 | public Ax25ControlType ControlType { get; private set; } = Ax25ControlType.None; 33 | 34 | public byte ControlNsssmm { get; private set; } 35 | 36 | public byte ControlPf { get; private set; } 37 | 38 | public byte ControlNrmmm { get; private set; } 39 | 40 | public bool AddAddressByte(byte value) 41 | { 42 | this.address_counter++; 43 | 44 | if (this.address_counter < 7) 45 | { 46 | // char 47 | this.CurrentAddress.Call += (char)(value & 0x7f); 48 | } 49 | else 50 | { 51 | // ssid etc. 52 | this.CurrentAddress.Ssid = (byte)(value & 0x78 >> 3); 53 | this.CurrentAddress.RR = (byte)(value & 0x6E >> 1); 54 | this.CurrentAddress.CRH = (byte)(value & 1); 55 | 56 | this.CurrentAddress.Call = this.CurrentAddress.Call.Trim(); 57 | 58 | // reset 59 | this.address_counter = 0; 60 | this.Addresses.Add(this.CurrentAddress); 61 | this.CurrentAddress = new Ax25Address(); 62 | } 63 | 64 | // return true if this is last address byte 65 | return (value & 0x80) == 0x80 && this.address_counter == 0; 66 | } 67 | 68 | public void AddControlByte(byte value) 69 | { 70 | this.ControlPf = (byte)((value & 0x10) >> 4); 71 | this.ControlNrmmm = (byte)((value & 0xE0) >> 5); 72 | 73 | if ((value & 1) == 0) 74 | { 75 | this.ControlType = Ax25ControlType.IShort; 76 | this.ControlNsssmm = (byte)((value & 0xE) >> 1); 77 | } 78 | else 79 | { 80 | this.ControlNsssmm = (byte)((value & 0xC) >> 2); 81 | } 82 | 83 | switch (value & 3) 84 | { 85 | case 1: 86 | this.ControlType = Ax25ControlType.SShort; 87 | break; 88 | case 3: 89 | this.ControlType = Ax25ControlType.UShort; 90 | break; 91 | } 92 | } 93 | 94 | public void AddPidByte(byte value) 95 | { 96 | 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /SdrsDecoder/Ax25/Ax25FramePost.cs: -------------------------------------------------------------------------------- 1 | using SdrsDecoder.Ax25; 2 | using SdrsDecoder.Support; 3 | using System; 4 | using System.Collections.Generic; 5 | using static System.Net.Mime.MediaTypeNames; 6 | 7 | namespace SdrsDecoder 8 | { 9 | public class Ax25FramePost 10 | { 11 | public static List Dedupe = new List(); 12 | 13 | // FCS processing with reference to https://github.com/wb2osz/direwolf/blob/de293a1f2526ec6639fe31fa411bd4f2319ecdf4/src/fcs_calc.c#L114 and 14 | // https://www.ietf.org/rfc/rfc1549.txt 15 | static ushort[] fcstab = { 16 | 0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf, 17 | 0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, 0xe97e, 0xf8f7, 18 | 0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e, 19 | 0x9cc9, 0x8d40, 0xbfdb, 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876, 20 | 0x2102, 0x308b, 0x0210, 0x1399, 0x6726, 0x76af, 0x4434, 0x55bd, 21 | 0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5, 22 | 0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c, 23 | 0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, 0xc974, 24 | 0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb, 25 | 0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3, 26 | 0x5285, 0x430c, 0x7197, 0x601e, 0x14a1, 0x0528, 0x37b3, 0x263a, 27 | 0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72, 28 | 0x6306, 0x728f, 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9, 29 | 0xef4e, 0xfec7, 0xcc5c, 0xddd5, 0xa96a, 0xb8e3, 0x8a78, 0x9bf1, 30 | 0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738, 31 | 0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, 0x9af9, 0x8b70, 32 | 0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, 0xf0b7, 33 | 0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff, 34 | 0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036, 35 | 0x18c1, 0x0948, 0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e, 36 | 0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5, 37 | 0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd, 38 | 0xb58b, 0xa402, 0x9699, 0x8710, 0xf3af, 0xe226, 0xd0bd, 0xc134, 39 | 0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c, 40 | 0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, 0xa33a, 0xb2b3, 41 | 0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, 0x3efb, 42 | 0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232, 43 | 0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a, 44 | 0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1, 45 | 0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9, 46 | 0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330, 47 | 0x7bc7, 0x6a4e, 0x58d5, 0x495c, 0x3de3, 0x2c6a, 0x1ef1, 0x0f78 48 | }; 49 | 50 | static string[] aprs_prefixes = { 51 | "AIR", "ALL", "AP", "BEACON", "CQ", "GPS", "DF", 52 | "DGPS", "DRILL", "DX", "ID", "JAVA", "MAIL", "MICE", 53 | "QST", "QTH", "RTCM", "SKY", "SPACE", "SPC", "SYM", 54 | "TEL", "TEST", "TLM", "WX", "ZIP", 55 | 56 | "APC", "APD", "APE", "API", "APIC", "APK", 57 | "APM", "APP", "APR", "APRS", "APRSM", "APRSW", 58 | "APS", "APW", "APX", "APY", "APZ" 59 | }; 60 | 61 | public List Bits { get; } = new List(); 62 | 63 | private float spaceMultiplier; 64 | public UInt64 Index; 65 | 66 | public Ax25FramePost(UInt64 index, float spaceMultiplier) 67 | { 68 | this.spaceMultiplier = spaceMultiplier; 69 | this.Index = index; 70 | } 71 | 72 | public void Add(byte value) 73 | { 74 | var output = (value & 1) == 1; 75 | 76 | this.Bits.Add(output); 77 | } 78 | 79 | public static byte ReverseBits(byte b) 80 | { 81 | byte result = 0; 82 | 83 | for (int i = 0; i < 8; i++) 84 | { 85 | result <<= 1; // Shift the result to make room for the next bit 86 | result |= (byte)(b & 1); // Add the next bit from b 87 | b >>= 1; // Shift b to the right to get the next bit 88 | } 89 | return result; 90 | } 91 | 92 | public void Process(Action messageReceived) 93 | { 94 | // frame has just been detected but 0 will be removed by unstuffer 95 | // ... so add it here 96 | this.Bits.Add(false); 97 | 98 | var count = this.Bits.Count; 99 | 100 | if (count < 120) 101 | { 102 | return; 103 | } 104 | 105 | var byte_check = count % 8; 106 | 107 | if (byte_check != 0) 108 | { 109 | return; 110 | } 111 | 112 | // remove FLAG at end 113 | this.Bits.RemoveRange(this.Bits.Count - 8, 8); 114 | 115 | var bytes = new List(); 116 | var current_byte = (byte)0; 117 | var bit_counter = 0; 118 | 119 | for (var i = 0; i < this.Bits.Count; i++) 120 | { 121 | var bit = this.Bits[i]; 122 | current_byte |= (byte)(bit ? 1 : 0); 123 | bit_counter++; 124 | 125 | if (bit_counter < 8) 126 | { 127 | current_byte = (byte)(current_byte << 1); 128 | } 129 | else 130 | { 131 | bit_counter = 0; 132 | bytes.Add(ReverseBits(current_byte)); 133 | current_byte = 0; 134 | } 135 | } 136 | 137 | // FCS check here 138 | var calcFcs = (ushort)0xffff; 139 | 140 | // https://github.com/wb2osz/direwolf/blob/de293a1f2526ec6639fe31fa411bd4f2319ecdf4/src/fcs_calc.c#L114 141 | for (var i = 0; i < bytes.Count - 2; i++) 142 | { 143 | var b = bytes[i]; 144 | calcFcs = (ushort)((calcFcs >> 8) ^ fcstab[(calcFcs ^ b) & 0xff]); 145 | } 146 | 147 | calcFcs = (ushort)(calcFcs ^ 0xffff); 148 | 149 | var rxFcs = (ushort)(bytes[^2] | (bytes[^1] << 8)); 150 | 151 | var messageObj = new Ax25Message(); 152 | messageObj.Address = ""; 153 | messageObj.Payload = ""; 154 | messageObj.HasErrors = calcFcs != rxFcs; 155 | messageObj.ErrorText = messageObj.HasErrors ? "Yes" : "No"; 156 | 157 | try 158 | { 159 | var lastAddressFound = false; 160 | var addressCount = 0; 161 | 162 | var calls = new List(); 163 | 164 | // use this later for aprs checking 165 | var destCall = ""; 166 | 167 | while (!lastAddressFound && addressCount < 10) 168 | { 169 | var start = addressCount * 7; 170 | 171 | var call = ""; 172 | 173 | for (var i = 0; i < 7; i++) 174 | { 175 | var b = (int)bytes[i + start]; 176 | 177 | if ((b & 1) == 1) 178 | { 179 | lastAddressFound = true; 180 | } 181 | 182 | b = b >> 1; 183 | 184 | if (i < 6) 185 | { 186 | call += (char)b; 187 | } 188 | else 189 | { 190 | call = call.Trim(); 191 | 192 | if (addressCount == 0) 193 | { 194 | destCall = call; 195 | } 196 | 197 | call += "-" + (b & 0b1111).ToString(); 198 | } 199 | } 200 | 201 | calls.Add(call); 202 | 203 | addressCount++; 204 | } 205 | 206 | if (calls.Count >= 2) 207 | { 208 | messageObj.Address = $"{calls[1]}>{calls[0]}"; 209 | } 210 | 211 | for (var i = 2; i < calls.Count; i++) 212 | { 213 | messageObj.Address += $",{calls[i]}"; 214 | } 215 | 216 | var controlIndex = addressCount * 7; 217 | var controlByte = (int)bytes[controlIndex]; 218 | 219 | var frameType = "Unknown"; 220 | var hasInfo = false; 221 | 222 | if ((controlByte & 0b1) == 0b0) 223 | { 224 | // information 225 | frameType = "I"; 226 | hasInfo = true; 227 | } 228 | else 229 | { 230 | var typeIndicator = controlByte & 0b11; 231 | 232 | if (typeIndicator == 0b01) 233 | { 234 | // supervisory 235 | frameType = "S-"; 236 | 237 | var superBits = (controlByte & 0b1100) >> 0b11; 238 | 239 | switch (superBits) 240 | { 241 | case 0: 242 | frameType += "RR"; 243 | break; 244 | case 1: 245 | frameType += "RNR"; 246 | break; 247 | case 2: 248 | frameType += "REJ"; 249 | break; 250 | case 3: 251 | frameType += "SREJ"; 252 | break; 253 | } 254 | } 255 | 256 | if (typeIndicator == 0b11) 257 | { 258 | // unnumbered 259 | frameType = "U-"; 260 | 261 | var unnumBits1 = (controlByte & 0b11100000) >> 3; 262 | var unnumBits2 = (controlByte & 0b1100) >> 2; 263 | var unnumBits = unnumBits1 + unnumBits2; 264 | 265 | switch (unnumBits) 266 | { 267 | case 0b01111: 268 | frameType += "SABME"; 269 | break; 270 | case 0b00111: 271 | frameType += "SABM"; 272 | break; 273 | case 001000: 274 | frameType += "DISC"; 275 | break; 276 | case 0b00011: 277 | frameType += "DM"; 278 | break; 279 | case 0b01100: 280 | frameType += "UA"; 281 | break; 282 | case 0b10001: 283 | frameType += "FRMR"; 284 | break; 285 | case 0b00000: 286 | frameType += "UI"; 287 | hasInfo = true; 288 | break; 289 | case 0b10111: 290 | frameType += "XID"; 291 | hasInfo = true; 292 | break; 293 | case 0b11100: 294 | frameType += "TEST"; 295 | hasInfo = true; 296 | break; 297 | } 298 | } 299 | } 300 | 301 | messageObj.OverrideType = frameType; 302 | 303 | var informationIndex = controlIndex + 1; 304 | 305 | if (hasInfo) 306 | { 307 | // not using this information (yet) 308 | //// process pid 309 | //var b = bytes[informationIndex]; 310 | 311 | //// XID not implemented here (yet) 312 | //if (frameType != "XID") 313 | //{ 314 | // messageObj.Payload += Convert.ToString(b, 2).PadLeft(8, '0') + " "; 315 | //} 316 | 317 | informationIndex += 1; 318 | } 319 | 320 | // aprs check - do this another time 321 | //var isAprs = false; 322 | 323 | //foreach (var aprsPrefix in aprs_prefixes) 324 | //{ 325 | // if (destCall.StartsWith(aprsPrefix)) 326 | // { 327 | // isAprs = true; 328 | // break; 329 | // } 330 | //} 331 | 332 | //if (isAprs) 333 | //{ 334 | // messageObj.OverrideType += " (APRS)"; 335 | //} 336 | 337 | // dump rest of message 338 | for (var i = informationIndex; i < bytes.Count - 2; i++) 339 | { 340 | var b = bytes[i]; 341 | 342 | messageObj.Payload += (char)b; 343 | } 344 | } 345 | catch (Exception ex) 346 | { 347 | messageObj.ErrorText += " (ET)"; 348 | } 349 | 350 | messageReceived(messageObj); 351 | } 352 | } 353 | } -------------------------------------------------------------------------------- /SdrsDecoder/Ax25/Ax25Message.cs: -------------------------------------------------------------------------------- 1 | using SdrsDecoder.Support; 2 | using System; 3 | 4 | namespace SdrsDecoder.Ax25 5 | { 6 | public class Ax25Message : MessageBase 7 | { 8 | public Ax25Message() : base(1200) 9 | { 10 | Protocol = $"AX25 / 1200"; 11 | 12 | Hash = DateTime.Now.ToString(); 13 | Type = MessageType.AlphaNumeric; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /SdrsDecoder/Flex/FlexChain.cs: -------------------------------------------------------------------------------- 1 | using SdrsDecoder.Support; 2 | using System; 3 | 4 | namespace SdrsDecoder.Flex 5 | { 6 | public class FlexChain : ChainBase 7 | { 8 | private ChebyFilter filter; 9 | private Fsk2Demodulator demodulator; 10 | private FlexDecoder decoder; 11 | private Interpolator interpolator; 12 | private Decimator decimator; 13 | 14 | public ResampleValues Rv { get; private set; } 15 | 16 | public FlexChain(float baud, float sampleRate, Action messageReceived) : base(sampleRate, messageReceived) 17 | { 18 | this.Rv = GetResampleValues(baud, sampleRate); 19 | var pll = new Pll(Rv.dsr, baud); 20 | 21 | interpolator = new Interpolator(Rv.i); 22 | filter = new ChebyFilter(baud * 1.2f, 1f, Rv.isr); 23 | decimator = new Decimator(Rv.d); 24 | demodulator = new Fsk2Demodulator(baud, Rv.dsr, pll, false); 25 | decoder = new FlexDecoder(Convert.ToUInt32(baud), messageReceived); 26 | } 27 | 28 | public override void Process(float[] values, Action writeSample = null) 29 | { 30 | var processed_values = interpolator.Process(values); 31 | processed_values = filter.Process(processed_values); 32 | processed_values = decimator.Process(processed_values); 33 | 34 | var demodulated_values = demodulator.Process(processed_values, writeSample); 35 | decoder.Process(demodulated_values); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /SdrsDecoder/Flex/FlexDecoder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Security.Cryptography; 4 | using SdrsDecoder.Support; 5 | 6 | namespace SdrsDecoder.Flex 7 | { 8 | public class FlexDecoder 9 | { 10 | const uint BS1 = 0b10101010101010101010101010101010; 11 | 12 | const uint A1 = 0b01111000111100110101100100111001; 13 | const uint A2 = 0b10000100111001110101100100111001; 14 | const uint A3 = 0b01001111100101110101100100111001; 15 | const uint A4 = 0b00100001010111110101100100111001; 16 | const uint A5 = 0b11011101010010110101100100111001; 17 | const uint A6 = 0b00010110001110110101100100111001; 18 | const uint A7 = 0b10110011100000110101100100111001; 19 | const uint Ar = 0b11001011001000000101100100111001; 20 | 21 | const uint B = 0b0101010101010101; 22 | // frame info 23 | 24 | const uint BS2 = 0b1010; 25 | const uint C = 0b1110110110000100; 26 | const uint BS2I = BS2 ^ 0b1111; 27 | const uint CI = C ^ 0b1111111111111111; 28 | 29 | Dictionary FlexAValues = new Dictionary 30 | { 31 | { A1, nameof(A1) }, 32 | { A2, nameof(A2) }, 33 | { A3, nameof(A3) }, 34 | { A4, nameof(A4) }, 35 | { A5, nameof(A5) }, 36 | { A6, nameof(A6) }, 37 | { A7, nameof(A7) }, 38 | { Ar, nameof(Ar) } 39 | }; 40 | 41 | Dictionary FlexAIValues = new Dictionary 42 | { 43 | { ~A1, nameof(A1) + "I" }, 44 | { ~A2, nameof(A2) + "I" }, 45 | { ~A3, nameof(A3) + "I" }, 46 | { ~A4, nameof(A4) + "I" }, 47 | { ~A5, nameof(A5) + "I" }, 48 | { ~A6, nameof(A6) + "I" }, 49 | { ~A7, nameof(A7) + "I" }, 50 | { ~Ar, nameof(Ar) + "I" } 51 | }; 52 | 53 | public BitBuffer Buffer { get; set; } = new BitBuffer(); 54 | 55 | private uint bps; 56 | private Action messageReceived; 57 | public FlexFrame Frame; 58 | 59 | public FlexDecoder(uint bps, Action messageReceived) 60 | { 61 | this.bps = bps; 62 | this.messageReceived = messageReceived; 63 | 64 | this.Frame = new FlexFrame(messageReceived); 65 | } 66 | 67 | public int Counter = 0; 68 | 69 | public void BufferUpdated() 70 | { 71 | var value_32 = this.Buffer.GetValue(32); 72 | 73 | //if (value_32 == BS1) 74 | //{ 75 | // this.Frame.State = FrameState.SYNC1_A; 76 | 77 | // return; 78 | //} 79 | 80 | if (this.Frame.State == FrameState.SYNC1_A && FlexAValues.ContainsKey(value_32)) 81 | { 82 | switch (FlexAValues[value_32]) 83 | { 84 | case "A1": 85 | this.Frame.Level = FlexLevel.F1600_2; 86 | break; 87 | case "A2": 88 | this.Frame.Level = FlexLevel.F3200_2; 89 | break; 90 | case "A3": 91 | this.Frame.Level = FlexLevel.F3200_4; 92 | break; 93 | case "A4": 94 | this.Frame.Level = FlexLevel.F6400_4; 95 | break; 96 | } 97 | 98 | this.Frame.State = FrameState.SYNC1_B; 99 | 100 | return; 101 | } 102 | 103 | if (this.Frame.State == FrameState.SYNC1_AI && FlexAIValues.ContainsKey(value_32)) 104 | { 105 | switch (FlexAIValues[value_32]) 106 | { 107 | case "A1I": 108 | this.Frame.Level = FlexLevel.F1600_2; 109 | break; 110 | case "A2I": 111 | this.Frame.Level = FlexLevel.F3200_2; 112 | break; 113 | case "A3I": 114 | this.Frame.Level = FlexLevel.F3200_4; 115 | break; 116 | case "A4I": 117 | this.Frame.Level = FlexLevel.F6400_4; 118 | break; 119 | } 120 | 121 | this.Frame.State = FrameState.FIW; 122 | this.Counter = 0; 123 | } 124 | 125 | var value_16 = this.Buffer.GetValue(16); 126 | var value_4 = this.Buffer.GetValue(4); 127 | 128 | switch (this.Frame.State) 129 | { 130 | case FrameState.SYNC1_B: 131 | if (value_16 == B) 132 | { 133 | this.Frame.State = FrameState.SYNC1_AI; 134 | } 135 | break; 136 | 137 | case FrameState.FIW: 138 | if (this.Counter < 32) 139 | { 140 | break; 141 | } 142 | 143 | this.Frame.ProcessFiw(value_32); 144 | this.Frame.State = FrameState.SYNC2_BS2; 145 | 146 | break; 147 | 148 | case FrameState.SYNC2_BS2: 149 | { 150 | if (value_4 == BS2) 151 | { 152 | this.Frame.State = FrameState.SYNC2_C; 153 | //this.messageReceived(new FlexMessage(1600) { Payload = $"SYNC2_BS2" }); 154 | } 155 | 156 | break; 157 | } 158 | 159 | case FrameState.SYNC2_C: 160 | { 161 | if (value_16 == C) 162 | { 163 | this.Frame.State = FrameState.SYNC2_BS2I; 164 | //this.messageReceived(new FlexMessage(1600) { Payload = $"SYNC2_C" }); 165 | } 166 | 167 | break; 168 | } 169 | 170 | case FrameState.SYNC2_BS2I: 171 | { 172 | if (value_4 == BS2I) 173 | { 174 | this.Frame.State = FrameState.SYNC2_CI; 175 | //this.messageReceived(new FlexMessage(1600) { Payload = $"SYNC2_BS2I" }); 176 | } 177 | 178 | break; 179 | } 180 | 181 | case FrameState.SYNC2_CI: 182 | { 183 | if (value_16 == CI) 184 | { 185 | this.Frame.State = FrameState.BLOCK; 186 | this.Counter = 0; 187 | } 188 | 189 | break; 190 | } 191 | 192 | case FrameState.BLOCK: 193 | 194 | if (this.Counter < 32) 195 | { 196 | break; 197 | } 198 | 199 | this.Counter = 0; 200 | this.Frame.ProcessWord(value_32); 201 | 202 | if (this.Frame.IsIdle) 203 | { 204 | this.Frame = new FlexFrame(this.messageReceived); 205 | } 206 | 207 | if (this.Frame.BlocksComplete) 208 | { 209 | this.Frame = new FlexFrame(this.messageReceived); 210 | 211 | //this.messageReceived(new FlexMessage(1600) { Payload = $"BLOCK RESET" }); 212 | } 213 | 214 | break; 215 | } 216 | 217 | this.Counter++; 218 | } 219 | 220 | public void Process(bool[] bits) 221 | { 222 | foreach (var bit in bits) 223 | { 224 | this.Buffer.Process(bit); 225 | 226 | this.BufferUpdated(); 227 | } 228 | } 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /SdrsDecoder/Flex/FlexFrame.cs: -------------------------------------------------------------------------------- 1 | using SdrsDecoder.Support; 2 | using System; 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | 6 | namespace SdrsDecoder.Flex 7 | { 8 | public enum FrameState 9 | { 10 | SYNC1_BS1, 11 | SYNC1_A, 12 | SYNC1_B, 13 | SYNC1_AI, 14 | FIW, 15 | SYNC2_BS2, 16 | SYNC2_C, 17 | SYNC2_BS2I, 18 | SYNC2_CI, 19 | BLOCK 20 | } 21 | 22 | public enum FlexLevel 23 | { 24 | UNKNOWN, 25 | F1600_2, 26 | F3200_2, 27 | F3200_4, 28 | F6400_4 29 | } 30 | 31 | public struct FlexAddress 32 | { 33 | public bool Short; 34 | public BchResult Result; 35 | public string Type; 36 | public uint Address; 37 | } 38 | 39 | struct FlexVector 40 | { 41 | public uint Data; 42 | public uint Type; 43 | public string TypeText; 44 | } 45 | 46 | public class FlexFrame 47 | { 48 | public uint CycleIndex { get; set; } = 0; 49 | 50 | public uint FrameIndex { get; set; } = 0; 51 | 52 | public uint BlockIndex { get; set; } = 0; 53 | 54 | public FlexLevel Level { get; set; } = FlexLevel.UNKNOWN; 55 | 56 | public FrameState State { get; set; } = FrameState.SYNC1_A; 57 | 58 | public List Addresses { get; set; } = new List(); 59 | 60 | //public List Vectors { get; set; } = new List(); 61 | public uint AddressStart { get; private set; } 62 | public uint VectorStart { get; private set; } 63 | public bool BlocksComplete { get; private set; } 64 | public bool IsIdle { get; private set; } 65 | 66 | public int FirstMessageWordIndex = int.MaxValue; 67 | public int LastMessageWordIndex = int.MinValue; 68 | 69 | public int WordIndex = 0; 70 | 71 | uint[] WordsForBlock = new uint[8]; 72 | 73 | public List Words = new List(); 74 | 75 | public FlexFrame(Action messageReceived) 76 | { 77 | this.messageReceived = messageReceived; 78 | } 79 | 80 | public static uint ExtractUint(uint source, int start, int end) 81 | { 82 | var result = default(uint); 83 | 84 | var length = end - start + 1; 85 | 86 | for (var i = 0; i < length; i++) 87 | { 88 | var bit = (source >> end - i) & 1u; 89 | 90 | result |= (bit << i); 91 | } 92 | 93 | return result; 94 | } 95 | 96 | public void ProcessFiw(uint value) 97 | { 98 | // bch / parity 99 | 100 | var bchResult = Bch.Process(value); 101 | var valueResult = bchResult.Value; 102 | 103 | //var parity = ExtractUint(value, 0, 0); 104 | //var bch = ExtractUint(value, 1, 10); 105 | var t = ExtractUint(valueResult, 11, 14); 106 | var r = ExtractUint(valueResult, 15, 15); 107 | var n = ExtractUint(valueResult, 16, 16); 108 | this.FrameIndex = ExtractUint(valueResult, 17, 23); 109 | this.CycleIndex = ExtractUint(valueResult, 24, 27); 110 | //var checksum = ExtractUint(valueResult, 28, 31); 111 | 112 | //this.messageReceived(new FlexMessage(1600) { Payload = $"FIW: n: {n} t: {t} r: {r}" }); 113 | //Console.WriteLine($"{this.CycleIndex} / {this.FrameIndex}"); 114 | } 115 | 116 | private Action messageReceived; 117 | 118 | public void ProcessFrame() 119 | { 120 | this.BlocksComplete = true; 121 | 122 | for (var i = 0; i < this.Words.Count; i++) 123 | { 124 | try 125 | { 126 | var result = this.Words[i]; 127 | var value = result.Value; 128 | 129 | //Console.WriteLine($"{Convert.ToString(value, 2).PadLeft(32, '0')} {result.BchErrors || result.ParityError}"); 130 | 131 | // handle BIW 132 | if (i == 0) 133 | { 134 | // IDLE 135 | if (value == 0b01010000001000000000101100001100) 136 | { 137 | this.IsIdle = true; 138 | //Console.WriteLine("IDLE"); 139 | return; 140 | } 141 | 142 | this.AddressStart = ExtractUint(value, 22, 23) + 1; 143 | this.VectorStart = ExtractUint(value, 16, 21); 144 | 145 | //this.messageReceived(new FlexMessage(1600) { Payload = $"BIW: a: {this.AddressStart} v: {this.VectorStart} {bchResult.ParityError || bchResult.BchErrors}" }); 146 | 147 | continue; 148 | } 149 | 150 | // handle EXT BIW 151 | if (i < this.AddressStart) 152 | { 153 | // page 45 of arib 154 | this.messageReceived(new FlexMessage(1600) { Payload = $"EXTBIW: {Convert.ToString(value, 2).PadLeft(32, '0')}" }); 155 | continue; 156 | } 157 | 158 | // handle address 159 | if (i >= this.AddressStart && i < this.VectorStart) 160 | { 161 | var address = ExtractUint(value, 11, 31); 162 | 163 | var addressStruct = new FlexAddress 164 | { 165 | Result = result, 166 | Short = false, 167 | Address = address 168 | }; 169 | 170 | // LA2 171 | if (address <= 2097150) 172 | { 173 | addressStruct.Short = false; 174 | addressStruct.Type = "LA2"; 175 | } 176 | 177 | 178 | // RSA 179 | if (address <= 2064382) 180 | { 181 | addressStruct.Short = true; 182 | addressStruct.Type = "RSA"; 183 | } 184 | 185 | // OMA 186 | if (address <= 2062367) 187 | { 188 | addressStruct.Short = true; 189 | addressStruct.Type = "OMA"; 190 | } 191 | 192 | // TA 193 | if (address <= 2062351) 194 | { 195 | addressStruct.Short = true; 196 | addressStruct.Type = "TA"; 197 | } 198 | 199 | // NA 200 | if (address <= 2062335) 201 | { 202 | addressStruct.Short = true; 203 | addressStruct.Type = "NA"; 204 | } 205 | 206 | // ISA 207 | if (address <= 2058239) 208 | { 209 | addressStruct.Short = true; 210 | addressStruct.Type = "ISA"; 211 | } 212 | 213 | 214 | // RSA 215 | if (address <= 2041855) 216 | { 217 | addressStruct.Short = true; 218 | addressStruct.Type = "RSA"; 219 | } 220 | 221 | 222 | // LA4 223 | if (address <= 2031616) 224 | { 225 | addressStruct.Short = false; 226 | addressStruct.Type = "LA4"; 227 | } 228 | 229 | // LA3 230 | if (address <= 1998848) 231 | { 232 | addressStruct.Short = false; 233 | addressStruct.Type = "LA3"; 234 | } 235 | 236 | 237 | // SA 238 | if (address <= 1966080) 239 | { 240 | addressStruct.Short = true; 241 | addressStruct.Type = "SA"; 242 | } 243 | 244 | // LA1 245 | if (address <= 32768) 246 | { 247 | addressStruct.Short = false; 248 | addressStruct.Type = "LA1"; 249 | } 250 | 251 | this.Addresses.Add(addressStruct); 252 | 253 | //this.messageReceived(new FlexMessage(1600) { Payload = $"Address: {address} {result.Type} {result.Short} {bchResult.ParityError || bchResult.BchErrors}" }); 254 | 255 | continue; 256 | } 257 | 258 | // handle vector 259 | if (i >= this.VectorStart && i < this.VectorStart + this.Addresses.Count) 260 | { 261 | var errors = false; 262 | 263 | if (result.ParityError || result.BchErrors) 264 | { 265 | errors = true; 266 | } 267 | 268 | // check for LA + handle differently if needed 269 | 270 | var vector = 271 | new FlexVector 272 | { 273 | Data = value, 274 | Type = ExtractUint(value, 25, 27) 275 | }; 276 | 277 | var isAlpha = false; 278 | var isNum = false; 279 | 280 | // switch to static dict? 281 | switch (vector.Type) 282 | { 283 | case 0: 284 | vector.TypeText = "SECURE MSG"; 285 | isAlpha = true; 286 | break; 287 | case 1: 288 | vector.TypeText = "SHORT INS"; 289 | break; 290 | case 2: 291 | vector.TypeText = "SHORT MSG"; 292 | break; 293 | case 3: 294 | vector.TypeText = "STD NUM"; 295 | isNum = true; 296 | break; 297 | case 4: 298 | vector.TypeText = "SFM NUM"; 299 | isNum = true; 300 | break; 301 | case 5: 302 | vector.TypeText = "ALPHA"; 303 | isAlpha = true; 304 | break; 305 | case 6: 306 | vector.TypeText = "HEX/BIN"; 307 | break; 308 | case 7: 309 | vector.TypeText = "NUM NUM"; 310 | //isNum = true; 311 | break; 312 | } 313 | 314 | var relevantAddressResult = this.Addresses[i - (int)this.VectorStart]; 315 | 316 | var relevantAddress = relevantAddressResult.Address; 317 | 318 | if (relevantAddressResult.Result.ParityError || relevantAddressResult.Result.BchErrors) 319 | { 320 | errors = true; 321 | } 322 | 323 | if (isAlpha) 324 | { 325 | var length = ExtractUint(value, 11, 17); 326 | var start = ExtractUint(value, 18, 24); 327 | 328 | var message = ""; 329 | 330 | var isFrag = false; 331 | 332 | for (var x = (int)start; x < start + length; x++) 333 | { 334 | if (x > this.Words.Count - 1) 335 | { 336 | // something has gone wrong! 337 | break; 338 | } 339 | 340 | var messageResult = this.Words[x]; 341 | 342 | if (messageResult.ParityError || messageResult.BchErrors) 343 | { 344 | errors = true; 345 | } 346 | 347 | var messageValue = this.Words[x].Value; 348 | 349 | // header 350 | if (x == start) 351 | { 352 | var mailDrop = ExtractUint(messageValue, 11, 11); 353 | var retrieval = ExtractUint(messageValue, 12, 12); 354 | var messageNo = ExtractUint(messageValue, 13, 18); 355 | var fragInd = ExtractUint(messageValue, 19, 20); 356 | var contd = ExtractUint(messageValue, 21, 21); 357 | var check = ExtractUint(messageValue, 22, 31); 358 | 359 | if (fragInd != 3) 360 | { 361 | isFrag = true; 362 | } 363 | 364 | continue; 365 | } 366 | 367 | // signature 368 | if (x == start + 1) 369 | { 370 | var char2_sig = (char)ExtractUint(messageValue, 11, 17); 371 | var char1_sig = (char)ExtractUint(messageValue, 18, 24); 372 | var signature = ExtractUint(messageValue, 25, 31); 373 | 374 | if (isFrag) 375 | { 376 | message += (char)signature; 377 | } 378 | 379 | message += char1_sig; 380 | message += char2_sig; 381 | 382 | continue; 383 | } 384 | 385 | var char3 = (char)ExtractUint(messageValue, 11, 17); 386 | var char2 = (char)ExtractUint(messageValue, 18, 24); 387 | var char1 = (char)ExtractUint(messageValue, 25, 31); 388 | 389 | message += char1; 390 | 391 | if (char2 > 3) 392 | { 393 | message += char2; 394 | } 395 | 396 | if (char3 > 3) 397 | { 398 | message += char3; 399 | } 400 | } 401 | 402 | this.messageReceived( 403 | new FlexMessage(1600) 404 | { 405 | Address = relevantAddress.ToString(), 406 | Payload = message, 407 | HasErrors = errors, 408 | ErrorText = errors ? "Yes" : "No" 409 | } 410 | ); 411 | 412 | continue; 413 | } 414 | 415 | if (isNum) 416 | { 417 | var length = ExtractUint(value, 15, 17); 418 | var start = ExtractUint(value, 18, 24); 419 | 420 | for (var x = (int)start; x < start + length; x++) 421 | { 422 | if (x > this.Words.Count - 1) 423 | { 424 | // something has gone wrong! 425 | break; 426 | } 427 | 428 | var messageResult = this.Words[x]; 429 | 430 | if (messageResult.ParityError || messageResult.BchErrors) 431 | { 432 | errors = true; 433 | } 434 | 435 | var messageValue = this.Words[x].Value; 436 | 437 | // header 438 | if (x == start) 439 | { 440 | 441 | 442 | var check = ExtractUint(messageValue, 30, 31); 443 | 444 | continue; 445 | } 446 | } 447 | 448 | continue; 449 | } 450 | 451 | this.messageReceived( 452 | new FlexMessage(1600) 453 | { 454 | Address = relevantAddress.ToString(), 455 | Payload = $"Vector: {vector.TypeText}", 456 | HasErrors = errors, 457 | ErrorText = errors ? "Yes" : "No" 458 | } 459 | ); 460 | 461 | 462 | continue; 463 | } 464 | } 465 | catch (Exception exception) 466 | { 467 | Log.LogException(exception); 468 | } 469 | } 470 | } 471 | 472 | public void ProcessBlock() 473 | { 474 | var realWords = new uint[8]; 475 | 476 | var counter = 0; 477 | 478 | foreach (var word in this.WordsForBlock) 479 | { 480 | for (var i = 0; i < 32; i++) 481 | { 482 | var bit = (word >> 31 - i) & 1u; 483 | 484 | var realWordIndex = counter % 8; 485 | var bitToUpdate = (counter - realWordIndex) / 8; 486 | 487 | realWords[realWordIndex] |= (bit << 31 - bitToUpdate); 488 | 489 | counter++; 490 | } 491 | } 492 | 493 | var errors = 0; 494 | 495 | foreach (var realWord in realWords) 496 | { 497 | var result = Bch.Process(realWord); 498 | 499 | // consider stopping here if value == 0? 500 | 501 | if (result.Value == 0) 502 | { 503 | this.ProcessFrame(); 504 | 505 | return; 506 | } 507 | 508 | if (result.BchErrors || result.ParityError) 509 | { 510 | errors++; 511 | } 512 | 513 | this.Words.Add(result); 514 | } 515 | 516 | //if (errors > 4) 517 | //{ 518 | // this.ProcessFrame(); 519 | //} 520 | } 521 | 522 | public void ProcessWord(uint value) 523 | { 524 | // frame complete, stop processing 525 | if (this.BlockIndex > 10) 526 | { 527 | return; 528 | } 529 | 530 | this.WordsForBlock[this.WordIndex] = value; 531 | 532 | this.WordIndex++; 533 | 534 | // check if block data complete 535 | if (this.WordIndex >= 8) 536 | { 537 | this.ProcessBlock(); 538 | 539 | if (this.BlockIndex == 10) 540 | { 541 | this.ProcessFrame(); 542 | return; 543 | } 544 | 545 | this.WordIndex = 0; 546 | this.BlockIndex++; 547 | } 548 | } 549 | } 550 | } 551 | -------------------------------------------------------------------------------- /SdrsDecoder/Flex/FlexMessage.cs: -------------------------------------------------------------------------------- 1 | using SdrsDecoder.Support; 2 | using System; 3 | 4 | namespace SdrsDecoder.Flex 5 | { 6 | public class FlexMessage : MessageBase 7 | { 8 | public FlexMessage(uint bps) : base(bps) 9 | { 10 | Protocol = $"FLEX / {bps} / 2"; 11 | 12 | Hash = DateTime.Now.ToString(); 13 | Type = MessageType.AlphaNumeric; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /SdrsDecoder/Manager.cs: -------------------------------------------------------------------------------- 1 | namespace SdrsDecoder 2 | { 3 | using SdrsDecoder.Acars; 4 | using SdrsDecoder.Ax25; 5 | using SdrsDecoder.Flex; 6 | using SdrsDecoder.Pocsag; 7 | using SdrsDecoder.Support; 8 | using System; 9 | using System.Linq; 10 | 11 | public struct DecoderConfigSet 12 | { 13 | public string Name { get; set; } 14 | 15 | public Func, ChainBase[]> GetChains { get; set; } 16 | } 17 | 18 | public class Manager 19 | { 20 | public int SampleRate { get; } 21 | public Action MessageReceived { get; } 22 | public ChainBase[] Chains { get; private set; } 23 | 24 | public readonly static DecoderConfigSet[] ConfigSets = { 25 | new DecoderConfigSet{ 26 | Name = "POCSAG (512, 1200, 2400)", 27 | GetChains = (int sampleRate, Action messageReceived)=>{ 28 | return new ChainBase[] { 29 | new PocsagChain(512f, sampleRate, messageReceived), 30 | new PocsagChain(1200f, sampleRate, messageReceived), 31 | new PocsagChain(2400f, sampleRate, messageReceived), 32 | }; 33 | } 34 | }, 35 | new DecoderConfigSet{ 36 | Name = "FLEX (1600/2)", 37 | GetChains = (int sampleRate, Action messageReceived)=>{ 38 | return new ChainBase[] { 39 | new FlexChain(1600f, sampleRate, messageReceived), 40 | }; 41 | } 42 | }, 43 | new DecoderConfigSet{ 44 | Name = "AX.25 / APRS (1200)", 45 | GetChains = (int sampleRate, Action messageReceived)=>{ 46 | return new ChainBase[] { 47 | new Ax25Chain(sampleRate, messageReceived), 48 | }; 49 | } 50 | }, 51 | new DecoderConfigSet{ 52 | Name = "ACARS (2400)", 53 | GetChains = (int sampleRate, Action messageReceived)=>{ 54 | return new ChainBase[] { 55 | new AcarsChain(sampleRate, messageReceived), 56 | }; 57 | } 58 | }, 59 | }; 60 | 61 | public void ChangeMode(string mode) 62 | { 63 | try 64 | { 65 | if (ConfigSets.Count(x=>x.Name == mode)==0) 66 | { 67 | return; 68 | } 69 | 70 | var configSet = ConfigSets.Single(x => x.Name == mode); 71 | 72 | this.Chains = configSet.GetChains(this.SampleRate, this.MessageReceived); 73 | } 74 | catch (Exception exception) 75 | { 76 | Log.LogException(exception); 77 | } 78 | } 79 | 80 | public Manager(int sampleRate, Action messageReceived) 81 | { 82 | try 83 | { 84 | this.SampleRate = sampleRate; 85 | this.MessageReceived = messageReceived; 86 | } 87 | catch (Exception exception) 88 | { 89 | Log.LogException(exception); 90 | } 91 | } 92 | 93 | public void Process(float[] values) 94 | { 95 | try 96 | { 97 | if (this.Chains == null) 98 | { 99 | return; 100 | } 101 | 102 | foreach (var chain in this.Chains) 103 | { 104 | chain.Process(values); 105 | } 106 | } 107 | catch (Exception exception) 108 | { 109 | Log.LogException(exception); 110 | } 111 | } 112 | } 113 | } -------------------------------------------------------------------------------- /SdrsDecoder/Pocsag/PocsagChain.cs: -------------------------------------------------------------------------------- 1 | using SdrsDecoder.Support; 2 | using System; 3 | 4 | namespace SdrsDecoder.Pocsag 5 | { 6 | public class PocsagChain : ChainBase 7 | { 8 | private ChebyFilter filter; 9 | private Fsk2Demodulator demodulator; 10 | private PocsagDecoder decoder; 11 | private Interpolator interpolator; 12 | private Decimator decimator; 13 | 14 | public ResampleValues Rv { get; private set; } 15 | 16 | public PocsagChain(float baud, float sampleRate, Action messageReceived) : base(sampleRate, messageReceived) 17 | { 18 | this.Rv = GetResampleValues(baud, sampleRate); 19 | var pll = new Pll(Rv.dsr, baud); 20 | 21 | interpolator = new Interpolator(Rv.i); 22 | filter = new ChebyFilter(baud * 1.2f, 1f, Rv.isr); 23 | decimator = new Decimator(Rv.d); 24 | demodulator = new Fsk2Demodulator(baud, Rv.dsr, pll, true); 25 | decoder = new PocsagDecoder(Convert.ToUInt32(baud), messageReceived); 26 | } 27 | 28 | public override void Process(float[] values, Action writeSample = null) 29 | { 30 | var processed_values = interpolator.Process(values); 31 | processed_values = filter.Process(processed_values); 32 | processed_values = decimator.Process(processed_values); 33 | 34 | var demodulated_values = demodulator.Process(processed_values, writeSample); 35 | decoder.Process(demodulated_values); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /SdrsDecoder/Pocsag/PocsagDecoder.cs: -------------------------------------------------------------------------------- 1 | using SdrsDecoder.Support; 2 | using System; 3 | 4 | namespace SdrsDecoder.Pocsag 5 | { 6 | public class PocsagDecoder 7 | { 8 | public int BatchIndex { get; private set; } 9 | 10 | public int FrameIndex { get; private set; } 11 | 12 | public int CodeWordInFrameIndex { get; private set; } 13 | 14 | public int CodeWordPosition { get; private set; } 15 | 16 | public PocsagMessage CurrentMessage { get; private set; } 17 | 18 | public BitBuffer Buffer { get; set; } = new BitBuffer(); 19 | 20 | private uint bps; 21 | private Action messageReceived; 22 | 23 | private void QueueCurrentMessage() 24 | { 25 | try 26 | { 27 | if (CurrentMessage != null && 28 | CurrentMessage.HasData) 29 | { 30 | CurrentMessage.ProcessPayload(); 31 | messageReceived(CurrentMessage); 32 | } 33 | 34 | CurrentMessage = new PocsagMessage(bps); 35 | } 36 | catch (Exception exception) 37 | { 38 | Log.LogException(exception); 39 | } 40 | } 41 | 42 | public PocsagDecoder(uint bps, Action messageReceived) 43 | { 44 | this.bps = bps; 45 | this.messageReceived = messageReceived; 46 | 47 | BatchIndex = -1; 48 | FrameIndex = -1; 49 | CodeWordInFrameIndex = -1; 50 | CodeWordPosition = -1; 51 | 52 | QueueCurrentMessage(); 53 | } 54 | 55 | private int timeout = 0; 56 | 57 | public void BufferUpdated(uint bufferValue) 58 | { 59 | if (timeout >= 64 && this.CurrentMessage != null && this.CurrentMessage.HasData) 60 | { 61 | this.QueueCurrentMessage(); 62 | } 63 | 64 | // preamble 65 | if (bufferValue == 0b10101010101010101010101010101010 || 66 | bufferValue == 0b01010101010101010101010101010101) 67 | { 68 | // reset these until we see batch sync 69 | BatchIndex = -1; 70 | FrameIndex = -1; 71 | CodeWordInFrameIndex = -1; 72 | CodeWordPosition = -1; 73 | 74 | timeout = 0; 75 | } 76 | 77 | if (BatchIndex > -1 && 78 | FrameIndex > -1 && 79 | CodeWordInFrameIndex > -1 && 80 | CodeWordPosition > -1) 81 | { 82 | timeout = 0; 83 | 84 | CodeWordPosition++; 85 | 86 | if (CodeWordPosition > 31) 87 | { 88 | CodeWordPosition = 0; 89 | CodeWordInFrameIndex++; 90 | 91 | // idle 92 | if (bufferValue == 0b01111010100010011100000110010111) 93 | { 94 | QueueCurrentMessage(); 95 | } 96 | else 97 | { 98 | // address code word? queue current message and start new message 99 | if (this.Buffer.Buffer[0] == false) 100 | { 101 | QueueCurrentMessage(); 102 | } 103 | 104 | CurrentMessage.AppendCodeWord( 105 | this.Buffer.Buffer.ToArray(), 106 | FrameIndex); 107 | } 108 | 109 | if (CodeWordInFrameIndex > 1) 110 | { 111 | CodeWordInFrameIndex = 0; 112 | FrameIndex++; 113 | } 114 | } 115 | 116 | // doing this allows us to wait for batch sync below 117 | if (FrameIndex > 7) 118 | { 119 | FrameIndex = -1; 120 | CodeWordInFrameIndex = -1; 121 | CodeWordPosition = -1; 122 | } 123 | } 124 | 125 | // batch sync 126 | if (bufferValue == 0b01111100110100100001010111011000) 127 | { 128 | timeout = 0; 129 | 130 | BatchIndex++; 131 | FrameIndex = 0; 132 | CodeWordPosition = 0; 133 | CodeWordInFrameIndex = 0; 134 | } 135 | 136 | timeout++; 137 | } 138 | 139 | public void Process(bool[] bits) 140 | { 141 | foreach (var bit in bits) 142 | { 143 | this.Buffer.Process(bit); 144 | 145 | var bufferValue = this.Buffer.GetValue(); 146 | 147 | BufferUpdated(bufferValue); 148 | } 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /SdrsDecoder/Pocsag/PocsagMessage.cs: -------------------------------------------------------------------------------- 1 | namespace SdrsDecoder.Pocsag 2 | { 3 | using SdrsDecoder.Support; 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | 9 | public class PocsagMessage : MessageBase 10 | { 11 | public const uint Generator = 1897; 12 | 13 | public List RawPayload { get; set; } 14 | public int ErrorsCorrected { get; private set; } 15 | 16 | public static readonly Dictionary NumericMapping = 17 | new Dictionary() 18 | { 19 | { 0, '0' }, 20 | { 1, '1' }, 21 | { 2, '2' }, 22 | { 3, '3' }, 23 | { 4, '4' }, 24 | { 5, '5' }, 25 | { 6, '6' }, 26 | { 7, '7' }, 27 | { 8, '8' }, 28 | { 9, '9' }, 29 | { 10, '?' }, 30 | { 11, 'U' }, 31 | { 12, ' ' }, 32 | { 13, '-' }, 33 | { 14, ')' }, 34 | { 15, '(' } 35 | }; 36 | 37 | public PocsagMessage(uint bps) : base(bps) 38 | { 39 | RawPayload = new List(); 40 | Protocol = $"POCSAG / {bps}"; 41 | } 42 | 43 | public bool IsBitPresent(uint source, int index) 44 | { 45 | return (source & 1 << index) > 0; 46 | } 47 | 48 | public bool CheckBchError(bool[] codeWord) 49 | { 50 | // BCH / error correction 51 | 52 | // converts bool array to uint 53 | uint codeWordAsUInt = 0; 54 | 55 | for (var i = 0; i < 31; i++) 56 | { 57 | if (codeWord[i]) 58 | { 59 | codeWordAsUInt += (uint)(1 << 30 - i); 60 | } 61 | } 62 | 63 | // modulo 2 division 64 | 65 | uint remainder = codeWordAsUInt; 66 | 67 | for (var i = 30; i >= 0; i--) 68 | { 69 | // if bit is set then do xor 70 | if (IsBitPresent(remainder, i)) 71 | { 72 | for (var x = 0; x < 11; x++) 73 | { 74 | var position = i - x; 75 | 76 | var currentValue = IsBitPresent(remainder, position); 77 | var generatorValue = IsBitPresent(Generator, 10 - x); 78 | 79 | var xorResult = currentValue ^ generatorValue; 80 | 81 | if (xorResult) 82 | { 83 | // set bit 84 | remainder |= (uint)1 << position; 85 | } 86 | else 87 | { 88 | // clear bit 89 | remainder &= ~((uint)1 << position); 90 | } 91 | } 92 | } 93 | } 94 | 95 | return remainder > 0; 96 | } 97 | 98 | public void AppendCodeWord(bool[] codeWord, int frameIndex) 99 | { 100 | try 101 | { 102 | var errors = false; 103 | 104 | if (CheckBchError(codeWord)) 105 | { 106 | errors = true; 107 | 108 | // 1 bit error correction 109 | 110 | for (var i = 0; i < codeWord.Length - 1 && errors; i++) 111 | { 112 | var codeWordToCheck = (bool[])codeWord.Clone(); 113 | 114 | codeWordToCheck[i] = !codeWordToCheck[i]; 115 | 116 | if (!CheckBchError(codeWordToCheck)) 117 | { 118 | codeWord = codeWordToCheck; 119 | 120 | this.ErrorsCorrected++; 121 | 122 | errors = false; 123 | } 124 | } 125 | 126 | // 2 bit error correction 127 | 128 | for (var x = 0; x < codeWord.Length - 1 && errors; x++) 129 | { 130 | for (var y = 0; y < codeWord.Length - 1 && errors; y++) 131 | { 132 | if (x == y) 133 | { 134 | continue; 135 | } 136 | 137 | var codeWordToCheck = (bool[])codeWord.Clone(); 138 | 139 | codeWordToCheck[x] = !codeWordToCheck[x]; 140 | codeWordToCheck[y] = !codeWordToCheck[y]; 141 | 142 | if (!CheckBchError(codeWordToCheck)) 143 | { 144 | codeWord = codeWordToCheck; 145 | 146 | this.ErrorsCorrected += 2; 147 | 148 | errors = false; 149 | } 150 | } 151 | } 152 | } 153 | 154 | HasData = true; 155 | 156 | var data = new bool[20]; 157 | 158 | var bch = new bool[10]; 159 | 160 | Array.Copy(codeWord, 1, data, 0, 20); 161 | Array.Copy(codeWord, 21, bch, 0, 10); 162 | 163 | var parity = codeWord[31]; 164 | 165 | // start parity check 166 | var trueCount = codeWord.Take(31).Count(x => x == true); 167 | 168 | if (trueCount % 2 == 0) 169 | { 170 | // parity should be zero (false) 171 | if (parity != false) 172 | { 173 | errors = true; 174 | } 175 | } 176 | else 177 | { 178 | // parity should be one (true) 179 | if (parity != true) 180 | { 181 | errors = true; 182 | } 183 | } 184 | 185 | if (!HasErrors && errors) 186 | { 187 | HasErrors = true; 188 | } 189 | 190 | ErrorText = HasErrors ? $"Yes ({this.ErrorsCorrected})" : $"No ({this.ErrorsCorrected})"; 191 | 192 | // end parity check 193 | 194 | if (codeWord[0] == false) 195 | { 196 | var address = default(uint); 197 | // address 198 | //this.FrameIndex = frameIndex; 199 | 200 | address += (uint)frameIndex; 201 | 202 | for (var i = 0; i < 18; i++) 203 | { 204 | var position = 20 - i; 205 | 206 | if (data[i]) 207 | { 208 | address += (uint)(1 << position); 209 | } 210 | } 211 | 212 | var function = default(byte); 213 | 214 | for (var i = 18; i < 20; i++) 215 | { 216 | var position = 1 - (i - 18); 217 | 218 | if (data[i]) 219 | { 220 | function += (byte)(1 << position); 221 | } 222 | } 223 | 224 | Address = $"{address} / {function}"; 225 | } 226 | else 227 | { 228 | // message 229 | RawPayload.AddRange(data); 230 | } 231 | } 232 | catch (Exception exception) 233 | { 234 | Log.LogException(exception); 235 | } 236 | } 237 | 238 | public void ProcessPayload() 239 | { 240 | try 241 | { 242 | var result = string.Empty; 243 | 244 | var byteCount = (int)Math.Floor(RawPayload.Count / 7.0); 245 | 246 | for (var i = 0; i < byteCount; i++) 247 | { 248 | var position = i * 7; 249 | 250 | var currentBits = 251 | RawPayload. 252 | Skip(position). 253 | Take(7); 254 | 255 | var bitArray = new BitArray(currentBits.ToArray()); 256 | 257 | var byteArray = new byte[1]; 258 | 259 | bitArray.CopyTo(byteArray, 0); 260 | 261 | if (byteArray[0] != 0) 262 | { 263 | result += (char)byteArray[0]; 264 | } 265 | } 266 | 267 | Payload = result; 268 | 269 | var numericResult = string.Empty; 270 | 271 | var numericByteCount = (int)Math.Floor(RawPayload.Count / 4.0); 272 | 273 | for (var i = 0; i < numericByteCount; i++) 274 | { 275 | var position = i * 4; 276 | 277 | var currentBits = 278 | RawPayload. 279 | Skip(position). 280 | Take(4); 281 | 282 | var bitArray = new BitArray(currentBits.ToArray()); 283 | 284 | var byteArray = new byte[1]; 285 | 286 | bitArray.CopyTo(byteArray, 0); 287 | 288 | numericResult += NumericMapping[byteArray[0]]; 289 | } 290 | 291 | Type = MessageType.AlphaNumeric; 292 | 293 | if (numericResult.Length == 0) 294 | { 295 | Payload = ""; 296 | Type = MessageType.Tone; 297 | } 298 | else if (numericResult.Length < 16) 299 | { 300 | Payload = $"{numericResult} (ALPHA: {Payload})"; 301 | Type = MessageType.Numeric; 302 | } 303 | 304 | UpdateHash(); 305 | } 306 | catch (Exception exception) 307 | { 308 | Log.LogException(exception); 309 | } 310 | } 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /SdrsDecoder/SdrsDecoder.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SdrsDecoder/Support/Bch.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SdrsDecoder.Support 4 | { 5 | public struct BchResult 6 | { 7 | public uint Value; 8 | public bool BchErrors; 9 | public bool ParityError; 10 | public int Corrected; 11 | } 12 | 13 | public class Bch 14 | { 15 | public const uint Generator = 1897; 16 | 17 | public static bool CheckBchError(uint codeWordAsUInt) 18 | { 19 | uint remainder = codeWordAsUInt >> 1; 20 | 21 | for (var i = 30; i >= 0; i--) 22 | { 23 | if ((remainder & (1u << i)) != 0) // Is the bit present at index i? 24 | { 25 | for (var x = 0; x < 11; x++) 26 | { 27 | var position = i - x; 28 | var generatorValue = (Generator & (1 << (10 - x))) > 0; 29 | 30 | // XOR the current remainder bit with the generator bit 31 | if ((remainder & (1u << position)) > 0 ^ generatorValue) 32 | { 33 | remainder |= (uint)1 << position; 34 | } 35 | else 36 | { 37 | remainder &= ~((uint)1 << position); 38 | } 39 | } 40 | } 41 | } 42 | 43 | return remainder > 0; 44 | } 45 | 46 | public static BchResult Process(uint value) 47 | { 48 | var bchErrors = false; 49 | var errorsCorrected = 0; 50 | 51 | var valueResult = value; 52 | 53 | if (CheckBchError(valueResult)) 54 | { 55 | bchErrors = true; 56 | 57 | // 1 bit error correction 58 | for (var i = 1; i < 32 && bchErrors; i++) 59 | { 60 | var valueToCheck = value ^ (1U << i); 61 | 62 | if (!CheckBchError(valueToCheck)) 63 | { 64 | bchErrors = false; 65 | errorsCorrected++; 66 | valueResult = valueToCheck; 67 | } 68 | } 69 | 70 | // 2 bit error correction 71 | if (bchErrors) 72 | { 73 | for (var x = 1; x < 32 && bchErrors; x++) 74 | { 75 | for (var y = 1; y < 32 && bchErrors; y++) 76 | { 77 | if (x == y) 78 | { 79 | continue; 80 | } 81 | 82 | var valueToCheck = value ^ (1U << x); 83 | valueToCheck = valueToCheck ^ (1U << y); 84 | 85 | if (!CheckBchError(valueToCheck)) 86 | { 87 | bchErrors = false; 88 | errorsCorrected += 2; 89 | valueResult = valueToCheck; 90 | } 91 | } 92 | } 93 | } 94 | } 95 | 96 | var parityCount = 0; 97 | 98 | for (var i = 1; i < 32; i++) 99 | { 100 | if ((valueResult & (1U << i)) != 0) 101 | { 102 | parityCount++; 103 | } 104 | } 105 | 106 | var calculatedParityBit = (parityCount % 2) == 0 ? 0U : 1U; 107 | var existingParityBit = (valueResult >> 0) & 1U; 108 | 109 | var parityError = calculatedParityBit != existingParityBit; 110 | 111 | return new BchResult 112 | { 113 | Corrected = errorsCorrected, 114 | BchErrors = bchErrors, 115 | ParityError = parityError, 116 | Value = valueResult 117 | }; 118 | } 119 | } 120 | } 121 | 122 | -------------------------------------------------------------------------------- /SdrsDecoder/Support/BitBuffer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace SdrsDecoder.Support 5 | { 6 | public class BitBuffer 7 | { 8 | public List Buffer = new List(); 9 | 10 | public int Length { get; } 11 | 12 | public BitBuffer() 13 | { 14 | for (int i = 0; i < 32; i++) 15 | { 16 | this.Buffer.Add(false); 17 | } 18 | } 19 | 20 | public void Process(bool value) 21 | { 22 | this.Buffer.Add(value); 23 | 24 | while (this.Buffer.Count > 32) 25 | { 26 | this.Buffer.RemoveAt(0); 27 | } 28 | } 29 | 30 | public uint GetValue(int length = 32) 31 | { 32 | var result = default(uint); 33 | 34 | var skip = this.Buffer.Count - length; 35 | 36 | var values = this.Buffer.Skip(skip).Take(length).ToArray(); 37 | 38 | for (var i = 0; i < length; i++) 39 | { 40 | if (values[i]) 41 | { 42 | result += (uint)(1 << length - i - 1); 43 | } 44 | } 45 | 46 | return result; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /SdrsDecoder/Support/ChainBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Numerics; 3 | 4 | namespace SdrsDecoder.Support 5 | { 6 | public struct ResampleValues 7 | { 8 | public int i { get; set; } 9 | public int d { get; set; } 10 | public int isr { get; set; } 11 | public int dsr { get; set; } 12 | } 13 | 14 | public abstract class ChainBase 15 | { 16 | protected float sampleRate; 17 | protected Action messageReceived; 18 | 19 | public ChainBase(float sampleRate, Action messageReceived) 20 | { 21 | this.sampleRate = sampleRate; 22 | this.messageReceived = messageReceived; 23 | } 24 | 25 | public abstract void Process(float[] values, Action writeSample = null); 26 | 27 | public static ResampleValues GetResampleValues(float baud, float sampleRate) 28 | { 29 | var targetRate = (int)baud * 10; 30 | var gcd = (int)BigInteger.GreatestCommonDivisor((BigInteger)sampleRate, (BigInteger)targetRate); 31 | 32 | var result = new ResampleValues(); 33 | 34 | result.i = targetRate / gcd; 35 | result.d = (int)sampleRate / gcd; 36 | 37 | if (result.i > 100) 38 | { 39 | result.i = 1; 40 | result.d = 1; 41 | } 42 | 43 | result.isr = (int)sampleRate * result.i; 44 | result.dsr = result.isr / result.d; 45 | 46 | return result; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /SdrsDecoder/Support/ChebyFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SdrsDecoder.Support 4 | { 5 | public class ChebyFilter 6 | { 7 | private float a0; 8 | private float a1; 9 | private float a2; 10 | private float b0; 11 | private float b1; 12 | private float b2; 13 | 14 | float x1 = 0, x2 = 0, y1 = 0, y2 = 0; 15 | 16 | public ChebyFilter(float cutoffFrequency, float rippleDb, float sampleRate) 17 | { 18 | float Wc = (float)Math.Tan(Math.PI * cutoffFrequency / sampleRate); 19 | 20 | // Calculate filter coefficients 21 | float epsilon = (float)Math.Sqrt(Math.Pow(10, rippleDb / 10) - 1); 22 | float v = (float)(Math.Asinh(1 / epsilon) / 2); 23 | float k1 = (float)Math.Sinh(v); 24 | //float k2 = (float)Math.Cosh(v); 25 | 26 | a0 = 1 + 2 * k1 * Wc + Wc * Wc; 27 | a1 = 2 * (Wc * Wc - 1); 28 | a2 = 1 - 2 * k1 * Wc + Wc * Wc; 29 | b0 = Wc * Wc; 30 | b1 = 2 * Wc * Wc; 31 | b2 = Wc * Wc; 32 | } 33 | 34 | public float[] Process(float[] inputArray) 35 | { 36 | // Initialize the filter state 37 | float[] filteredData = new float[inputArray.Length]; 38 | 39 | 40 | // Apply the filter 41 | for (int i = 0; i < inputArray.Length; i++) 42 | { 43 | float x0 = inputArray[i]; 44 | float y0 = (b0 * x0 + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2) / a0; 45 | 46 | filteredData[i] = y0; 47 | 48 | x2 = x1; 49 | x1 = x0; 50 | y2 = y1; 51 | y1 = y0; 52 | } 53 | 54 | return filteredData; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /SdrsDecoder/Support/DcRemover.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace SdrsDecoder.Support 5 | { 6 | public class DcRemover 7 | { 8 | private FixedSizeQueue queue; 9 | 10 | public DcRemover(float sampleRate, float baud, int lengthFactor = 5) 11 | { 12 | var samples_per_symbol = (int)(sampleRate / baud); 13 | this.queue = new FixedSizeQueue(samples_per_symbol * lengthFactor); 14 | } 15 | 16 | public float Process(float value) 17 | { 18 | this.queue.Enqueue(value); 19 | 20 | var av = this.queue.Queue.Average(); 21 | 22 | value -= av; 23 | 24 | return value; 25 | } 26 | 27 | public float[] Process(float[] values) 28 | { 29 | var result = new List(); 30 | 31 | foreach (var v in values) 32 | { 33 | result.Add(this.Process(v)); 34 | } 35 | 36 | return result.ToArray(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /SdrsDecoder/Support/Decimator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace SdrsDecoder.Support 8 | { 9 | public class Decimator 10 | { 11 | public Decimator(int value) 12 | { 13 | this.Value = value; 14 | } 15 | 16 | //private float[] overflow; 17 | 18 | public float[] Process(float[] values) 19 | { 20 | if (this.Value <= 1) 21 | { 22 | return values; 23 | } 24 | 25 | //if (overflow!=null) 26 | //{ 27 | // values = overflow.Concat(values).ToArray(); 28 | //} 29 | 30 | var result = new float[values.Length / this.Value]; 31 | 32 | //var overflowSet = false; 33 | 34 | for (var x = 0; x < result.Length; x++) 35 | { 36 | var p = x * this.Value; 37 | 38 | if (p > values.Length - 1) 39 | { 40 | //overflowSet = true; 41 | //overflow = values.Skip((x - 1) * this.Value).ToArray(); 42 | 43 | continue; 44 | } 45 | 46 | result[x] = values[p]; 47 | } 48 | 49 | //if (!overflowSet) 50 | //{ 51 | // overflow = null; 52 | //} 53 | 54 | return result; 55 | } 56 | 57 | public int Value { get; } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /SdrsDecoder/Support/FixedSizeQueue.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace SdrsDecoder.Support 4 | { 5 | 6 | public class FixedSizeQueue 7 | { 8 | public readonly List Queue; 9 | private readonly int _limit; 10 | 11 | public FixedSizeQueue(int limit) 12 | { 13 | _limit = limit; 14 | Queue = new List(limit); 15 | } 16 | 17 | public void Enqueue(T item) 18 | { 19 | if (Queue.Count >= _limit) 20 | { 21 | Queue.RemoveAt(0); 22 | } 23 | 24 | Queue.Add(item); 25 | } 26 | 27 | public int Count 28 | { 29 | get { return Queue.Count; } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /SdrsDecoder/Support/Fsk2Demodulator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace SdrsDecoder.Support 6 | { 7 | public class Fsk2Demodulator 8 | { 9 | private FixedSizeQueue value_fifo; 10 | private bool invert; 11 | private PllBase pll; 12 | private bool last_lo_state; 13 | private bool output_state; 14 | 15 | public Fsk2Demodulator(float baud, float sampleRate, PllBase pll, bool invert) 16 | { 17 | this.pll = pll; 18 | 19 | var samples_per_symbol = (int)Math.Round(sampleRate / baud); 20 | 21 | value_fifo = new FixedSizeQueue(samples_per_symbol); 22 | this.invert = invert; 23 | } 24 | 25 | public bool[] Process(float[] values, Action writeSample = null) 26 | { 27 | var result = new List(); 28 | 29 | for (var i = 0; i < values.Length; i++) 30 | { 31 | var value = values[i]; 32 | value_fifo.Enqueue(value); 33 | 34 | var lo_state = pll.Process(value, writeSample); 35 | 36 | if (lo_state != last_lo_state) 37 | { 38 | output_state = value_fifo.Queue.Average() >= 0; 39 | result.Add(invert ? !output_state : output_state); 40 | } 41 | 42 | if (writeSample != null) 43 | { 44 | writeSample(output_state ? 1f : -1f); 45 | } 46 | 47 | last_lo_state = lo_state; 48 | } 49 | 50 | return result.ToArray(); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /SdrsDecoder/Support/Interpolator.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | 3 | namespace SdrsDecoder.Support 4 | { 5 | public class Interpolator 6 | { 7 | public Interpolator(int value) 8 | { 9 | this.Value = value; 10 | } 11 | 12 | public int Value { get; } 13 | 14 | public float[] Process(float[] values) 15 | { 16 | if (this.Value <= 1) 17 | { 18 | return values; 19 | } 20 | 21 | var result = new float[values.Length * this.Value]; 22 | 23 | for (var x = 0; x < values.Length; x++) 24 | { 25 | result[x * this.Value] = values[x]; 26 | } 27 | 28 | return result; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /SdrsDecoder/Support/IqDemod.cs: -------------------------------------------------------------------------------- 1 | using SdrsDecoder.Support; 2 | using System; 3 | 4 | namespace SdrsDecoder 5 | { 6 | 7 | public class IqDemod 8 | { 9 | public float[] SpaceMultiplers; 10 | 11 | public float[] MarkI { get; } 12 | public float[] MarkQ { get; } 13 | public float[] SpaceI { get; } 14 | public float[] SpaceQ { get; } 15 | public FixedSizeQueue Buffer { get; } 16 | 17 | public IqDemod(float sampleRate, float baud, float mark, float space, float[] spaceMultipliers) 18 | { 19 | this.SpaceMultiplers = spaceMultipliers; 20 | 21 | var samps_per_symbol = (int)Math.Round(sampleRate / baud); 22 | 23 | this.MarkI = new float[samps_per_symbol]; 24 | this.MarkQ = new float[samps_per_symbol]; 25 | this.SpaceI = new float[samps_per_symbol]; 26 | this.SpaceQ = new float[samps_per_symbol]; 27 | 28 | var mark_inc = mark / sampleRate * Math.PI * 2; 29 | var space_inc = space / sampleRate * Math.PI * 2; 30 | 31 | for (var i = 0; i < samps_per_symbol; i++) 32 | { 33 | var mark_p = (float)i * mark_inc; 34 | 35 | this.MarkI[i] = (float)Math.Sin(mark_p); 36 | this.MarkQ[i] = (float)Math.Cos(mark_p); 37 | 38 | var space_p = (float)i * space_inc; 39 | 40 | this.SpaceI[i] = (float)Math.Sin(space_p); 41 | this.SpaceQ[i] = (float)Math.Cos(space_p); 42 | } 43 | 44 | this.Buffer = new FixedSizeQueue(samps_per_symbol); 45 | } 46 | 47 | public float[] Process(float value) 48 | { 49 | this.Buffer.Enqueue(value); 50 | 51 | // mark correlation 52 | var mark_product_s = 0f; 53 | var mark_product_c = 0f; 54 | 55 | for (var x = 0; x < this.Buffer.Count; x++) 56 | { 57 | var v = this.Buffer.Queue[x]; 58 | var s = this.MarkI[x]; 59 | var c = this.MarkQ[x]; 60 | 61 | mark_product_s += v * s; 62 | mark_product_c += v * c; 63 | } 64 | 65 | mark_product_s = (float)Math.Pow(mark_product_s, 2); 66 | mark_product_c = (float)Math.Pow(mark_product_c, 2); 67 | 68 | // space correlation 69 | var space_product_s = 0f; 70 | var space_product_c = 0f; 71 | 72 | for (var x = 0; x < this.Buffer.Count; x++) 73 | { 74 | var v = this.Buffer.Queue[x]; 75 | var s = this.SpaceI[x]; 76 | var c = this.SpaceQ[x]; 77 | 78 | space_product_s += v * s; 79 | space_product_c += v * c; 80 | } 81 | 82 | space_product_s = (float)Math.Pow(space_product_s, 2); 83 | space_product_c = (float)Math.Pow(space_product_c, 2); 84 | 85 | var results = new float[this.SpaceMultiplers.Length]; 86 | 87 | var mark_mag = (float)Math.Sqrt(mark_product_s + mark_product_c); 88 | var space_mag = (float)Math.Sqrt(space_product_s + space_product_c); 89 | 90 | for (var i = 0; i < this.SpaceMultiplers.Length; i++) 91 | { 92 | results[i] = mark_mag - (space_mag * this.SpaceMultiplers[i]); 93 | } 94 | 95 | return results; 96 | } 97 | 98 | public float[,] Process(float[] values) 99 | { 100 | var results = new float[this.SpaceMultiplers.Length, values.Length]; 101 | 102 | for (var i = 0; i < values.Length; i++) 103 | { 104 | var result = this.Process(values[i]); 105 | 106 | for (var y = 0; y < this.SpaceMultiplers.Length; y++) 107 | { 108 | results[y, i] = result[y]; 109 | } 110 | } 111 | 112 | return results; 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /SdrsDecoder/Support/Log.cs: -------------------------------------------------------------------------------- 1 | namespace SdrsDecoder.Support 2 | { 3 | using System; 4 | using System.IO; 5 | 6 | public static class Log 7 | { 8 | public static void LogException(Exception exception) 9 | { 10 | try 11 | { 12 | var date = DateTime.Now; 13 | 14 | var filename = $"SdrSharpPocsagPlugin-{date.ToString("yyyy-MM-dd")}.log"; 15 | 16 | var currentException = exception; 17 | 18 | while (currentException != null) 19 | { 20 | File.AppendAllText(filename, $"[{date.ToString()}] {exception.Message}\r\n{exception.StackTrace}\r\n"); 21 | 22 | currentException = currentException.InnerException; 23 | } 24 | } 25 | catch 26 | { 27 | // abandon all hope, ye who enter here 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /SdrsDecoder/Support/MessageBase.cs: -------------------------------------------------------------------------------- 1 | namespace SdrsDecoder.Support 2 | { 3 | using System; 4 | using System.Security.Cryptography; 5 | 6 | public enum MessageType 7 | { 8 | AlphaNumeric, 9 | Numeric, 10 | Tone 11 | } 12 | 13 | public class MessageBase 14 | { 15 | public DateTime Timestamp { get; set; } 16 | 17 | public string Protocol { get; set; } 18 | 19 | public string TimestampText { get; set; } 20 | 21 | public string Address { get; set; } 22 | 23 | public bool HasErrors { get; set; } 24 | 25 | public string ErrorText { get; set; } 26 | 27 | public bool HasData { get; set; } 28 | 29 | public string Hash { get; set; } 30 | 31 | public string Payload { get; set; } 32 | 33 | public MessageType Type { get; set; } 34 | 35 | public string OverrideType { get; set; } 36 | 37 | public string TypeText 38 | { 39 | get 40 | { 41 | if (OverrideType != null) 42 | { 43 | return OverrideType; 44 | } 45 | 46 | switch (Type) 47 | { 48 | case MessageType.AlphaNumeric: 49 | return "Alpha"; 50 | case MessageType.Numeric: 51 | return "Numeric"; 52 | case MessageType.Tone: 53 | return "Tone"; 54 | } 55 | 56 | return string.Empty; 57 | } 58 | } 59 | 60 | public MessageBase(uint bps) 61 | { 62 | try 63 | { 64 | Timestamp = DateTime.Now; 65 | TimestampText = $"{Timestamp.ToShortDateString()} {Timestamp.ToLongTimeString()}"; 66 | } 67 | catch (Exception exception) 68 | { 69 | Log.LogException(exception); 70 | } 71 | } 72 | 73 | protected void UpdateHash() 74 | { 75 | var textToHash = Payload; 76 | 77 | // skip first 9 characters, typically contains time / date + another number which will mess up duplicate detection 78 | 79 | if (textToHash.Length > 9) 80 | { 81 | textToHash = textToHash.Substring(8); 82 | } 83 | 84 | var bytesToHash = System.Text.Encoding.ASCII.GetBytes(textToHash); 85 | 86 | var sha256 = SHA256.Create(); 87 | 88 | var hashBytes = sha256.ComputeHash(bytesToHash); 89 | 90 | sha256.Dispose(); 91 | sha256 = null; 92 | 93 | Hash = BitConverter.ToString(hashBytes).Replace("-", ""); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /SdrsDecoder/Support/NrzDecoder.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | 3 | namespace SdrsDecoder.Support 4 | { 5 | public struct NrzResult 6 | { 7 | public bool Value { get; set; } 8 | public bool IsFlag { get; set; } 9 | } 10 | 11 | public enum NrzMode 12 | { 13 | Ax25, 14 | Acars 15 | } 16 | 17 | public class NrzDecoder 18 | { 19 | private int CurrentValue = 0; 20 | private bool LastBit = true; 21 | private int flagMask; 22 | private int flag; 23 | private NrzMode mode; 24 | public bool LastValue = true; 25 | private FixedSizeQueue history = new FixedSizeQueue(10); 26 | 27 | public NrzDecoder(int flagMask, int flag, NrzMode mode) 28 | { 29 | this.flagMask = flagMask; 30 | this.flag = flag; 31 | this.mode = mode; 32 | } 33 | 34 | public NrzResult Process(bool value) 35 | { 36 | var result = false; 37 | 38 | if (this.mode == NrzMode.Ax25) 39 | { 40 | // no change means 1 41 | if (value == LastBit) 42 | { 43 | result = true; 44 | } 45 | } 46 | 47 | if (this.mode == NrzMode.Acars) 48 | { 49 | history.Enqueue(value); 50 | 51 | // acars nrzi reset on preamble 52 | if (history.Queue.Count(x => x == false) == 0) 53 | { 54 | LastValue = true; 55 | } 56 | 57 | result = LastValue; 58 | 59 | // 0 means change 60 | if (value == false) 61 | { 62 | result = !result; 63 | } 64 | 65 | LastValue = result; 66 | } 67 | 68 | LastBit = value; 69 | 70 | CurrentValue = CurrentValue << 1; 71 | 72 | if (result) 73 | { 74 | CurrentValue |= 1; 75 | } 76 | 77 | var isFlag = false; 78 | 79 | if ((CurrentValue & flagMask) == flag) 80 | { 81 | isFlag = true; 82 | } 83 | 84 | return new NrzResult 85 | { 86 | Value = result, 87 | IsFlag = isFlag 88 | }; 89 | } 90 | 91 | //public bool[] Process(bool[] values) 92 | //{ 93 | // var results = new List(); 94 | 95 | // foreach (var value in values) 96 | // { 97 | // results.Add(this.Process(value)); 98 | // } 99 | 100 | // return results.ToArray(); 101 | //} 102 | } 103 | } -------------------------------------------------------------------------------- /SdrsDecoder/Support/Pll.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace SdrsDecoder.Support 5 | { 6 | public enum PllMode 7 | { 8 | Both, 9 | Rising 10 | } 11 | 12 | public class Pll : PllBase 13 | { 14 | protected int phase_per_sample; 15 | private int lo_phase; 16 | private bool lo_state; 17 | private float last_value; 18 | private int phase_error; 19 | private int half_phase_per_sample; 20 | private PllMode mode; 21 | 22 | public Pll(float sampleRate, float baud, PllMode mode = PllMode.Both) 23 | { 24 | phase_per_sample = (int)sampleRate / (int)baud; 25 | half_phase_per_sample = phase_per_sample / 2; 26 | this.mode = mode; 27 | } 28 | 29 | public override bool Process(float value, Action writeSample = null) 30 | { 31 | if (lo_phase >= phase_per_sample) 32 | { 33 | lo_phase -= phase_per_sample; 34 | lo_state = !lo_state; 35 | } 36 | 37 | var valueStateChanged = false; 38 | 39 | if (mode == PllMode.Both) 40 | { 41 | valueStateChanged = last_value >= 0 != value >= 0; 42 | } 43 | 44 | if (mode == PllMode.Rising) 45 | { 46 | valueStateChanged = last_value < 0 && value >= 0; 47 | } 48 | 49 | if (valueStateChanged) 50 | { 51 | phase_error = lo_phase; 52 | 53 | if (phase_error != 0) 54 | { 55 | if (phase_error > half_phase_per_sample) 56 | { 57 | phase_error -= phase_per_sample; 58 | } 59 | 60 | if (phase_error > 0) 61 | { 62 | lo_phase--; 63 | } 64 | else 65 | { 66 | lo_phase++; 67 | } 68 | } 69 | } 70 | 71 | if (writeSample != null) 72 | { 73 | writeSample(value); 74 | writeSample(lo_state ? 1 : -1); 75 | writeSample((float)phase_error / (float)phase_per_sample); 76 | } 77 | 78 | lo_phase++; 79 | last_value = value; 80 | 81 | return lo_state; 82 | } 83 | 84 | public bool[] Process(float[] values, Action writeSample = null) 85 | { 86 | var result = new List(); 87 | 88 | foreach (var v in values) 89 | { 90 | result.Add(Process(v, writeSample)); 91 | } 92 | 93 | return result.ToArray(); 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /SdrsDecoder/Support/PllBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SdrsDecoder.Support 4 | { 5 | public abstract class PllBase 6 | { 7 | public abstract bool Process(float value, Action writeSample = null); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /SdrsDecoder/Support/Unstuffer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace SdrsDecoder.Support 4 | { 5 | public class Unstuffer 6 | { 7 | public uint CurrentValue = 0; 8 | 9 | public bool? Process(bool value) 10 | { 11 | CurrentValue = CurrentValue << 1; 12 | 13 | if (value) 14 | { 15 | CurrentValue |= 1; 16 | } 17 | 18 | if ((CurrentValue & 0x3f) == 0x3e) 19 | { 20 | return null; 21 | } 22 | 23 | return value; 24 | } 25 | 26 | public bool[] Process(bool[] values) 27 | { 28 | var result = new List(); 29 | 30 | foreach (var v in values) 31 | { 32 | var r = this.Process(v); 33 | 34 | if (!r.HasValue) 35 | { 36 | continue; 37 | } 38 | 39 | result.Add(r.Value); 40 | } 41 | 42 | return result.ToArray(); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Screenshot 2 | 3 | ![POCSAG decoder](https://i.imgur.com/WJaRTUd.png) 4 | 5 | # Installation 6 | 7 | Extract the 'SdrSharpPocsagPlugin' folder in the release zip into the SDR# 'Plugins' directory. 8 | 9 | ![Plugin directory](https://i.imgur.com/5i2CYyo.png) 10 | 11 | # Build from source 12 | 13 | Use Visual Studio 2019, the debug output folder can be put into the SDR# 'Plugins' directory. 14 | 15 | # Usage 16 | 17 | Provided the plugin has been loaded successfully, there should be an option to display it in the main menu. 18 | 19 | NFM with 12kHz bandwidth seems to work reliably, input is taken directly from the demodulator so audio filtering / mute shouldn't affect decoding. 20 | 21 | ![Plugin directory](https://i.imgur.com/9eGnJ9k.png) 22 | 23 | ## Options 24 | 25 | ### De-duplicate 26 | The payload of each message is hashed (ignoring the first 9 characters, which seem to often include time data). With this option enabled new messages received with the same hash will be ignored. 27 | 28 | ### Hide bad decodes 29 | Any messages that have failed BCH (CRC) or parity checks will be ignored. 30 | 31 | ### Wrap payload 32 | The payload section of the table will wrap rather than expand horizontally, newlines in payload data will also be respected. 33 | 34 | ### POCSAG 512 / 1200 / 2400 filter depths 35 | Three controls are available to adjust the length of the moving average filter for each decoder. The filter aims to help decode noisy signals. 36 | 37 | These values should be set as low as possible otherwise the data may be 'smoothed into incoherence'. 38 | 39 | The filters can essentially be disabled by using a value of '1'. 40 | 41 | ### Clear 42 | Will remove all messages from the buffer. 43 | 44 | # Notes 45 | 46 | * A maximum of 1000 messages will be displayed in the buffer, on a first-in-first-out basis. 47 | * If a message payload is empty it is assumed to be tone message. 48 | * If a message payload has fewer than 16 characters numerically, it is assumed to be a numeric message. In this case the numeric decode will be displayed first with the alphanumeric decode following e.g. "123456 (ALPHA: ABC)" 49 | --------------------------------------------------------------------------------