├── .gitignore ├── assets ├── GRC │ └── iq-transfer │ │ ├── 2017-10-07_20_15_54-SmartSDR-Win.png │ │ ├── iq-transfer-test.grc │ │ ├── iq_transfer_fft.png │ │ ├── openwebrx.png │ │ └── top_block.py ├── grc_sample.png ├── mqtt_sample.png ├── test_input │ └── flex.pcap ├── test_output │ ├── dax_raw_float_32_BE_48000.raw │ ├── fft.png │ ├── opus_decoded_float_32_LE_24000.raw │ └── waterfall.png └── wsjtx_use_case.png ├── cmd ├── smartsdr-daxclient │ └── main.go ├── smartsdr-iqtransfer │ └── main.go └── smartsdr-mqttadapter │ └── main.go ├── crossbuild └── build.sh ├── extlib └── libSmartsdrIqTransfer │ └── libiqtransfer.go ├── go.mod ├── go.sum ├── obj ├── client.go ├── panadapter.go ├── radio.go └── slice.go ├── pcap_test.go.archive ├── radio_integration_test.go.archive ├── readme.md ├── sdrobjects ├── sdrobjects.go └── util.go └── vita ├── vitahandler.go └── vitatypes.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | **/bin/* 3 | **/pkg/* 4 | -------------------------------------------------------------------------------- /assets/GRC/iq-transfer/2017-10-07_20_15_54-SmartSDR-Win.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hb9fxq/flexlib-go/8acafd8a4e14c8af9b0c1ba796db68ebd6d26875/assets/GRC/iq-transfer/2017-10-07_20_15_54-SmartSDR-Win.png -------------------------------------------------------------------------------- /assets/GRC/iq-transfer/iq-transfer-test.grc: -------------------------------------------------------------------------------- 1 | options: 2 | parameters: 3 | author: '' 4 | category: Custom 5 | cmake_opt: '' 6 | comment: '' 7 | copyright: '' 8 | description: '' 9 | gen_cmake: 'On' 10 | gen_linking: dynamic 11 | generate_options: qt_gui 12 | hier_block_src_path: '.:' 13 | id: top_block 14 | max_nouts: '0' 15 | output_language: python 16 | placement: (0,0) 17 | qt_qss_theme: '' 18 | realtime_scheduling: '' 19 | run: 'True' 20 | run_command: '{python} -u {filename}' 21 | run_options: prompt 22 | sizing_mode: fixed 23 | thread_safe_setters: '' 24 | title: '' 25 | window_size: '' 26 | states: 27 | bus_sink: false 28 | bus_source: false 29 | bus_structure: null 30 | coordinate: [8, 8] 31 | rotation: 0 32 | state: enabled 33 | 34 | blocks: 35 | - name: samp_rate 36 | id: variable 37 | parameters: 38 | comment: '' 39 | value: 192e3 40 | states: 41 | bus_sink: false 42 | bus_source: false 43 | bus_structure: null 44 | coordinate: [679, 33] 45 | rotation: 0 46 | state: enabled 47 | - name: blocks_deinterleave_0 48 | id: blocks_deinterleave 49 | parameters: 50 | affinity: '' 51 | alias: '' 52 | blocksize: '1' 53 | comment: '' 54 | maxoutbuf: '0' 55 | minoutbuf: '0' 56 | num_streams: '2' 57 | type: float 58 | vlen: '1' 59 | states: 60 | bus_sink: false 61 | bus_source: false 62 | bus_structure: null 63 | coordinate: [259, 185] 64 | rotation: 0 65 | state: enabled 66 | - name: blocks_float_to_complex_0 67 | id: blocks_float_to_complex 68 | parameters: 69 | affinity: '' 70 | alias: '' 71 | comment: '' 72 | maxoutbuf: '0' 73 | minoutbuf: '0' 74 | vlen: '1' 75 | states: 76 | bus_sink: false 77 | bus_source: false 78 | bus_structure: null 79 | coordinate: [452, 173] 80 | rotation: 0 81 | state: enabled 82 | - name: blocks_udp_source_0 83 | id: blocks_udp_source 84 | parameters: 85 | affinity: '' 86 | alias: '' 87 | comment: '' 88 | eof: 'True' 89 | ipaddr: 0.0.0.0 90 | maxoutbuf: '0' 91 | minoutbuf: '0' 92 | port: '2345' 93 | psize: '4096' 94 | type: float 95 | vlen: '1' 96 | states: 97 | bus_sink: false 98 | bus_source: false 99 | bus_structure: null 100 | coordinate: [223, 60] 101 | rotation: 0 102 | state: enabled 103 | - name: qtgui_freq_sink_x_0 104 | id: qtgui_freq_sink_x 105 | parameters: 106 | affinity: '' 107 | alias: '' 108 | alpha1: '1.0' 109 | alpha10: '1.0' 110 | alpha2: '1.0' 111 | alpha3: '1.0' 112 | alpha4: '1.0' 113 | alpha5: '1.0' 114 | alpha6: '1.0' 115 | alpha7: '1.0' 116 | alpha8: '1.0' 117 | alpha9: '1.0' 118 | autoscale: 'True' 119 | average: '0.2' 120 | axislabels: 'True' 121 | bw: samp_rate 122 | color1: '"blue"' 123 | color10: '"dark blue"' 124 | color2: '"red"' 125 | color3: '"green"' 126 | color4: '"black"' 127 | color5: '"cyan"' 128 | color6: '"magenta"' 129 | color7: '"yellow"' 130 | color8: '"dark red"' 131 | color9: '"dark green"' 132 | comment: '' 133 | ctrlpanel: 'False' 134 | fc: '0' 135 | fftsize: '4096' 136 | freqhalf: 'True' 137 | grid: 'False' 138 | gui_hint: '' 139 | label: Relative Gain 140 | label1: '' 141 | label10: '''''' 142 | label2: '''''' 143 | label3: '''''' 144 | label4: '''''' 145 | label5: '''''' 146 | label6: '''''' 147 | label7: '''''' 148 | label8: '''''' 149 | label9: '''''' 150 | legend: 'True' 151 | maxoutbuf: '0' 152 | minoutbuf: '0' 153 | name: '""' 154 | nconnections: '1' 155 | showports: 'False' 156 | tr_chan: '0' 157 | tr_level: '0.0' 158 | tr_mode: qtgui.TRIG_MODE_FREE 159 | tr_tag: '""' 160 | type: complex 161 | units: dB 162 | update_time: '0.10' 163 | width1: '1' 164 | width10: '1' 165 | width2: '1' 166 | width3: '1' 167 | width4: '1' 168 | width5: '1' 169 | width6: '1' 170 | width7: '1' 171 | width8: '1' 172 | width9: '1' 173 | wintype: firdes.WIN_BLACKMAN_hARRIS 174 | ymax: '10' 175 | ymin: '-140' 176 | states: 177 | bus_sink: false 178 | bus_source: false 179 | bus_structure: null 180 | coordinate: [605, 391] 181 | rotation: 0 182 | state: true 183 | - name: qtgui_waterfall_sink_x_0 184 | id: qtgui_waterfall_sink_x 185 | parameters: 186 | affinity: '' 187 | alias: '' 188 | alpha1: '1.0' 189 | alpha10: '1.0' 190 | alpha2: '1.0' 191 | alpha3: '1.0' 192 | alpha4: '1.0' 193 | alpha5: '1.0' 194 | alpha6: '1.0' 195 | alpha7: '1.0' 196 | alpha8: '1.0' 197 | alpha9: '1.0' 198 | axislabels: 'True' 199 | bw: samp_rate 200 | color1: '0' 201 | color10: '0' 202 | color2: '0' 203 | color3: '0' 204 | color4: '0' 205 | color5: '0' 206 | color6: '0' 207 | color7: '0' 208 | color8: '0' 209 | color9: '0' 210 | comment: '' 211 | fc: '0' 212 | fftsize: '4096' 213 | freqhalf: 'True' 214 | grid: 'False' 215 | gui_hint: '' 216 | int_max: '10' 217 | int_min: '-140' 218 | label1: '' 219 | label10: '' 220 | label2: '' 221 | label3: '' 222 | label4: '' 223 | label5: '' 224 | label6: '' 225 | label7: '' 226 | label8: '' 227 | label9: '' 228 | legend: 'True' 229 | maxoutbuf: '0' 230 | minoutbuf: '0' 231 | name: '""' 232 | nconnections: '1' 233 | showports: 'False' 234 | type: complex 235 | update_time: '0.10' 236 | wintype: firdes.WIN_BLACKMAN_hARRIS 237 | states: 238 | bus_sink: false 239 | bus_source: false 240 | bus_structure: null 241 | coordinate: [650, 226] 242 | rotation: 0 243 | state: true 244 | 245 | connections: 246 | - [blocks_deinterleave_0, '0', blocks_float_to_complex_0, '0'] 247 | - [blocks_deinterleave_0, '1', blocks_float_to_complex_0, '1'] 248 | - [blocks_float_to_complex_0, '0', qtgui_freq_sink_x_0, '0'] 249 | - [blocks_float_to_complex_0, '0', qtgui_waterfall_sink_x_0, '0'] 250 | - [blocks_udp_source_0, '0', blocks_deinterleave_0, '0'] 251 | 252 | metadata: 253 | file_format: 1 254 | -------------------------------------------------------------------------------- /assets/GRC/iq-transfer/iq_transfer_fft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hb9fxq/flexlib-go/8acafd8a4e14c8af9b0c1ba796db68ebd6d26875/assets/GRC/iq-transfer/iq_transfer_fft.png -------------------------------------------------------------------------------- /assets/GRC/iq-transfer/openwebrx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hb9fxq/flexlib-go/8acafd8a4e14c8af9b0c1ba796db68ebd6d26875/assets/GRC/iq-transfer/openwebrx.png -------------------------------------------------------------------------------- /assets/GRC/iq-transfer/top_block.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # SPDX-License-Identifier: GPL-3.0 6 | # 7 | # GNU Radio Python Flow Graph 8 | # Title: Top Block 9 | # GNU Radio version: 3.8.1.0 10 | 11 | from distutils.version import StrictVersion 12 | 13 | if __name__ == '__main__': 14 | import ctypes 15 | import sys 16 | if sys.platform.startswith('linux'): 17 | try: 18 | x11 = ctypes.cdll.LoadLibrary('libX11.so') 19 | x11.XInitThreads() 20 | except: 21 | print("Warning: failed to XInitThreads()") 22 | 23 | from PyQt5 import Qt 24 | from gnuradio import qtgui 25 | from gnuradio.filter import firdes 26 | import sip 27 | from gnuradio import blocks 28 | from gnuradio import gr 29 | import sys 30 | import signal 31 | from argparse import ArgumentParser 32 | from gnuradio.eng_arg import eng_float, intx 33 | from gnuradio import eng_notation 34 | from gnuradio import qtgui 35 | 36 | class top_block(gr.top_block, Qt.QWidget): 37 | 38 | def __init__(self): 39 | gr.top_block.__init__(self, "Top Block") 40 | Qt.QWidget.__init__(self) 41 | self.setWindowTitle("Top Block") 42 | qtgui.util.check_set_qss() 43 | try: 44 | self.setWindowIcon(Qt.QIcon.fromTheme('gnuradio-grc')) 45 | except: 46 | pass 47 | self.top_scroll_layout = Qt.QVBoxLayout() 48 | self.setLayout(self.top_scroll_layout) 49 | self.top_scroll = Qt.QScrollArea() 50 | self.top_scroll.setFrameStyle(Qt.QFrame.NoFrame) 51 | self.top_scroll_layout.addWidget(self.top_scroll) 52 | self.top_scroll.setWidgetResizable(True) 53 | self.top_widget = Qt.QWidget() 54 | self.top_scroll.setWidget(self.top_widget) 55 | self.top_layout = Qt.QVBoxLayout(self.top_widget) 56 | self.top_grid_layout = Qt.QGridLayout() 57 | self.top_layout.addLayout(self.top_grid_layout) 58 | 59 | self.settings = Qt.QSettings("GNU Radio", "top_block") 60 | 61 | try: 62 | if StrictVersion(Qt.qVersion()) < StrictVersion("5.0.0"): 63 | self.restoreGeometry(self.settings.value("geometry").toByteArray()) 64 | else: 65 | self.restoreGeometry(self.settings.value("geometry")) 66 | except: 67 | pass 68 | 69 | ################################################## 70 | # Variables 71 | ################################################## 72 | self.samp_rate = samp_rate = 192e3 73 | 74 | ################################################## 75 | # Blocks 76 | ################################################## 77 | self.qtgui_waterfall_sink_x_0 = qtgui.waterfall_sink_c( 78 | 4096, #size 79 | firdes.WIN_BLACKMAN_hARRIS, #wintype 80 | 0, #fc 81 | samp_rate, #bw 82 | "", #name 83 | 1 #number of inputs 84 | ) 85 | self.qtgui_waterfall_sink_x_0.set_update_time(0.10) 86 | self.qtgui_waterfall_sink_x_0.enable_grid(False) 87 | self.qtgui_waterfall_sink_x_0.enable_axis_labels(True) 88 | 89 | 90 | 91 | labels = ['', '', '', '', '', 92 | '', '', '', '', ''] 93 | colors = [0, 0, 0, 0, 0, 94 | 0, 0, 0, 0, 0] 95 | alphas = [1.0, 1.0, 1.0, 1.0, 1.0, 96 | 1.0, 1.0, 1.0, 1.0, 1.0] 97 | 98 | for i in range(1): 99 | if len(labels[i]) == 0: 100 | self.qtgui_waterfall_sink_x_0.set_line_label(i, "Data {0}".format(i)) 101 | else: 102 | self.qtgui_waterfall_sink_x_0.set_line_label(i, labels[i]) 103 | self.qtgui_waterfall_sink_x_0.set_color_map(i, colors[i]) 104 | self.qtgui_waterfall_sink_x_0.set_line_alpha(i, alphas[i]) 105 | 106 | self.qtgui_waterfall_sink_x_0.set_intensity_range(-140, 10) 107 | 108 | self._qtgui_waterfall_sink_x_0_win = sip.wrapinstance(self.qtgui_waterfall_sink_x_0.pyqwidget(), Qt.QWidget) 109 | self.top_grid_layout.addWidget(self._qtgui_waterfall_sink_x_0_win) 110 | self.qtgui_freq_sink_x_0 = qtgui.freq_sink_c( 111 | 4096, #size 112 | firdes.WIN_BLACKMAN_hARRIS, #wintype 113 | 0, #fc 114 | samp_rate, #bw 115 | "", #name 116 | 1 117 | ) 118 | self.qtgui_freq_sink_x_0.set_update_time(0.10) 119 | self.qtgui_freq_sink_x_0.set_y_axis(-140, 10) 120 | self.qtgui_freq_sink_x_0.set_y_label('Relative Gain', 'dB') 121 | self.qtgui_freq_sink_x_0.set_trigger_mode(qtgui.TRIG_MODE_FREE, 0.0, 0, "") 122 | self.qtgui_freq_sink_x_0.enable_autoscale(True) 123 | self.qtgui_freq_sink_x_0.enable_grid(False) 124 | self.qtgui_freq_sink_x_0.set_fft_average(0.2) 125 | self.qtgui_freq_sink_x_0.enable_axis_labels(True) 126 | self.qtgui_freq_sink_x_0.enable_control_panel(False) 127 | 128 | 129 | 130 | labels = ['', '', '', '', '', 131 | '', '', '', '', ''] 132 | widths = [1, 1, 1, 1, 1, 133 | 1, 1, 1, 1, 1] 134 | colors = ["blue", "red", "green", "black", "cyan", 135 | "magenta", "yellow", "dark red", "dark green", "dark blue"] 136 | alphas = [1.0, 1.0, 1.0, 1.0, 1.0, 137 | 1.0, 1.0, 1.0, 1.0, 1.0] 138 | 139 | for i in range(1): 140 | if len(labels[i]) == 0: 141 | self.qtgui_freq_sink_x_0.set_line_label(i, "Data {0}".format(i)) 142 | else: 143 | self.qtgui_freq_sink_x_0.set_line_label(i, labels[i]) 144 | self.qtgui_freq_sink_x_0.set_line_width(i, widths[i]) 145 | self.qtgui_freq_sink_x_0.set_line_color(i, colors[i]) 146 | self.qtgui_freq_sink_x_0.set_line_alpha(i, alphas[i]) 147 | 148 | self._qtgui_freq_sink_x_0_win = sip.wrapinstance(self.qtgui_freq_sink_x_0.pyqwidget(), Qt.QWidget) 149 | self.top_grid_layout.addWidget(self._qtgui_freq_sink_x_0_win) 150 | self.blocks_udp_source_0 = blocks.udp_source(gr.sizeof_float*1, '0.0.0.0', 2345, 4096, True) 151 | self.blocks_float_to_complex_0 = blocks.float_to_complex(1) 152 | self.blocks_deinterleave_0 = blocks.deinterleave(gr.sizeof_float*1, 1) 153 | 154 | 155 | 156 | ################################################## 157 | # Connections 158 | ################################################## 159 | self.connect((self.blocks_deinterleave_0, 1), (self.blocks_float_to_complex_0, 1)) 160 | self.connect((self.blocks_deinterleave_0, 0), (self.blocks_float_to_complex_0, 0)) 161 | self.connect((self.blocks_float_to_complex_0, 0), (self.qtgui_freq_sink_x_0, 0)) 162 | self.connect((self.blocks_float_to_complex_0, 0), (self.qtgui_waterfall_sink_x_0, 0)) 163 | self.connect((self.blocks_udp_source_0, 0), (self.blocks_deinterleave_0, 0)) 164 | 165 | def closeEvent(self, event): 166 | self.settings = Qt.QSettings("GNU Radio", "top_block") 167 | self.settings.setValue("geometry", self.saveGeometry()) 168 | event.accept() 169 | 170 | def get_samp_rate(self): 171 | return self.samp_rate 172 | 173 | def set_samp_rate(self, samp_rate): 174 | self.samp_rate = samp_rate 175 | self.qtgui_freq_sink_x_0.set_frequency_range(0, self.samp_rate) 176 | self.qtgui_waterfall_sink_x_0.set_frequency_range(0, self.samp_rate) 177 | 178 | 179 | 180 | def main(top_block_cls=top_block, options=None): 181 | 182 | if StrictVersion("4.5.0") <= StrictVersion(Qt.qVersion()) < StrictVersion("5.0.0"): 183 | style = gr.prefs().get_string('qtgui', 'style', 'raster') 184 | Qt.QApplication.setGraphicsSystem(style) 185 | qapp = Qt.QApplication(sys.argv) 186 | 187 | tb = top_block_cls() 188 | tb.start() 189 | tb.show() 190 | 191 | def sig_handler(sig=None, frame=None): 192 | Qt.QApplication.quit() 193 | 194 | signal.signal(signal.SIGINT, sig_handler) 195 | signal.signal(signal.SIGTERM, sig_handler) 196 | 197 | timer = Qt.QTimer() 198 | timer.start(500) 199 | timer.timeout.connect(lambda: None) 200 | 201 | def quitting(): 202 | tb.stop() 203 | tb.wait() 204 | qapp.aboutToQuit.connect(quitting) 205 | qapp.exec_() 206 | 207 | 208 | if __name__ == '__main__': 209 | main() 210 | -------------------------------------------------------------------------------- /assets/grc_sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hb9fxq/flexlib-go/8acafd8a4e14c8af9b0c1ba796db68ebd6d26875/assets/grc_sample.png -------------------------------------------------------------------------------- /assets/mqtt_sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hb9fxq/flexlib-go/8acafd8a4e14c8af9b0c1ba796db68ebd6d26875/assets/mqtt_sample.png -------------------------------------------------------------------------------- /assets/test_input/flex.pcap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hb9fxq/flexlib-go/8acafd8a4e14c8af9b0c1ba796db68ebd6d26875/assets/test_input/flex.pcap -------------------------------------------------------------------------------- /assets/test_output/dax_raw_float_32_BE_48000.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hb9fxq/flexlib-go/8acafd8a4e14c8af9b0c1ba796db68ebd6d26875/assets/test_output/dax_raw_float_32_BE_48000.raw -------------------------------------------------------------------------------- /assets/test_output/fft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hb9fxq/flexlib-go/8acafd8a4e14c8af9b0c1ba796db68ebd6d26875/assets/test_output/fft.png -------------------------------------------------------------------------------- /assets/test_output/opus_decoded_float_32_LE_24000.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hb9fxq/flexlib-go/8acafd8a4e14c8af9b0c1ba796db68ebd6d26875/assets/test_output/opus_decoded_float_32_LE_24000.raw -------------------------------------------------------------------------------- /assets/test_output/waterfall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hb9fxq/flexlib-go/8acafd8a4e14c8af9b0c1ba796db68ebd6d26875/assets/test_output/waterfall.png -------------------------------------------------------------------------------- /assets/wsjtx_use_case.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hb9fxq/flexlib-go/8acafd8a4e14c8af9b0c1ba796db68ebd6d26875/assets/wsjtx_use_case.png -------------------------------------------------------------------------------- /cmd/smartsdr-daxclient/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "github.com/hb9fxq/flexlib-go/obj" 6 | "github.com/hb9fxq/flexlib-go/sdrobjects" 7 | "log" 8 | "net" 9 | "os" 10 | "strconv" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | type AppContext struct { 16 | radioAddr string 17 | myPort string 18 | daxIqChan string 19 | sampleRate string 20 | forwardAddess string 21 | RadioReponseStreamSequence int 22 | RadioResponseStream uint64 23 | forwardConnection net.Conn 24 | } 25 | 26 | func main() { 27 | 28 | l := log.New(os.Stderr, "RADIO_MSG ", 0) 29 | 30 | appContext := new(AppContext) 31 | flag.StringVar(&appContext.radioAddr, "RADIO", "", "IP ADDRESS OF THE RADIO e.g 192.168.41.8") 32 | flag.StringVar(&appContext.myPort, "MYUDP", "", "LOCAL UDP PORT 7788") 33 | flag.StringVar(&appContext.daxIqChan, "CH", "", "DAX CHANNEL NUMBER e.g. 2") 34 | flag.StringVar(&appContext.forwardAddess, "FWD", "", "If empty, IQ data will be written to stdout. UDP Forward address for the IQ samples with port, e.g. 192.168.50.5:5000") 35 | flag.Parse() 36 | 37 | if len(appContext.forwardAddess) > 0 { 38 | appContext.forwardConnection, _ = net.Dial("udp", appContext.forwardAddess) 39 | } 40 | 41 | radioContext := new(obj.RadioContext) 42 | radioContext.RadioAddr = appContext.radioAddr 43 | radioContext.MyUdpEndpointPort = appContext.myPort 44 | radioContext.ChannelRadioResponse = make(chan string) 45 | radioContext.ChannelVitaIfData = make(chan *sdrobjects.SdrIfData) 46 | radioContext.Debug = true 47 | 48 | go func(ctx *obj.RadioContext) { 49 | for { 50 | response := <-ctx.ChannelRadioResponse 51 | 52 | if strings.HasPrefix(response, "R"+strconv.Itoa(appContext.RadioReponseStreamSequence)) { 53 | streamHexString := strings.Split(response, "|")[2] 54 | l.Println("Stream filter streamId 0x" + streamHexString) 55 | stream, _ := strconv.ParseUint(streamHexString, 16, 64) 56 | appContext.RadioResponseStream = stream 57 | 58 | } 59 | } 60 | }(radioContext) 61 | 62 | go func(ctx *obj.RadioContext) { 63 | for { /* we'll only receive the samples for the stream requested on that port so we can ignore the stream id*/ 64 | handleData(appContext, *<-ctx.ChannelVitaIfData) 65 | } 66 | }(radioContext) 67 | 68 | go obj.InitRadioContext(radioContext) 69 | time.Sleep(2 * time.Second) 70 | 71 | for { 72 | if len(radioContext.RadioHandle) > 0 { // wait until we got our handle 73 | break 74 | } 75 | time.Sleep(500) 76 | } 77 | 78 | obj.SendRadioCommand(radioContext, "client program DAX") 79 | obj.SendRadioCommand(radioContext, "client set send_reduced_bw_dax=0") 80 | 81 | // wait for first clientId 82 | var firstClient = "" 83 | l.Println("waiting for first client") 84 | for { 85 | 86 | radioContext.Clients.Range(func(k interface{}, value interface{}) bool { 87 | firstClient = value.(obj.Client).ClientId 88 | return true 89 | }) 90 | 91 | if firstClient != "" { 92 | break 93 | } 94 | } 95 | 96 | // wait for first panadapter 97 | var firstPan = "" 98 | l.Println("waiting for first panadapter") 99 | for { 100 | 101 | radioContext.Panadapters.Range(func(k interface{}, value interface{}) bool { 102 | firstPan = value.(obj.Panadapter).Id 103 | return true 104 | }) 105 | 106 | if firstPan != "" { 107 | break 108 | } 109 | } 110 | l.Println("Binding to client_id " + firstClient) 111 | obj.SendRadioCommand(radioContext, "client bind client_id="+firstClient) 112 | 113 | l.Println("Requesting UDP VITA data to be sent to " + appContext.myPort) 114 | obj.SendRadioCommand(radioContext, "client udpport "+appContext.myPort) 115 | 116 | cmd := "stream create type=dax_rx dax_channel=" + appContext.daxIqChan 117 | appContext.RadioReponseStreamSequence = obj.SendRadioCommand(radioContext, cmd) 118 | 119 | if len(appContext.forwardAddess) > 0 { 120 | l.Println("Forwarding data to " + appContext.forwardAddess) 121 | } 122 | 123 | forever := make(chan bool) 124 | forever <- true 125 | } 126 | 127 | func handleData(appctx *AppContext, ifDataPackage sdrobjects.SdrIfData) { 128 | 129 | if uint64(ifDataPackage.Stream_id) != appctx.RadioResponseStream { 130 | return 131 | } 132 | 133 | if len(appctx.forwardAddess) > 0 { 134 | appctx.forwardConnection.Write(ifDataPackage.Data) 135 | } else { 136 | os.Stdout.Write(ifDataPackage.Data) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /cmd/smartsdr-iqtransfer/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "flag" 6 | "github.com/hb9fxq/flexlib-go/obj" 7 | "github.com/hb9fxq/flexlib-go/sdrobjects" 8 | "log" 9 | "net" 10 | "os" 11 | "strconv" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | type AppContext struct { 17 | radioAddr string 18 | myPort string 19 | daxIqChan string 20 | sampleRate string 21 | forwardAddess string 22 | RadioReponseStreamSequence int 23 | RadioResponseStream uint64 24 | forwardConnection net.Conn 25 | fCenterArg string 26 | } 27 | 28 | func main() { 29 | 30 | l := log.New(os.Stderr, "IQTRANSFER ", 0) 31 | 32 | appContext := new(AppContext) 33 | flag.StringVar(&appContext.radioAddr, "RADIO", "", "IP ADDRESS OF THE RADIO e.g 192.168.41.8") 34 | flag.StringVar(&appContext.myPort, "MYUDP", "", "LOCAL UDP PORT 7788") 35 | flag.StringVar(&appContext.daxIqChan, "CH", "", "DAX IQ CHANNEL NUMBER e.g. ") 36 | flag.StringVar(&appContext.sampleRate, "RATE", "", "DAX IQ sample rate in kHz - 24 / 48 / 96 / 192") 37 | flag.StringVar(&appContext.forwardAddess, "FWD", "", "If empty, IQ data will be written to stdout. UDP Forward address for the IQ samples with port, e.g. 192.168.50.5:5000") 38 | flag.StringVar(&appContext.fCenterArg, "FCENTER", "", "(Optional) Tune panadapter to initial fCenter (Mhz) e.g. 7.1") 39 | flag.Parse() 40 | 41 | if appContext.sampleRate != "24000" && appContext.sampleRate != "48000" && appContext.sampleRate != "96000" && appContext.sampleRate != "192000" { 42 | panic("Invalid Sample Rate! Allowed values 24000, 48000, 96000, 192000") 43 | } 44 | 45 | if len(appContext.forwardAddess) > 0 { 46 | appContext.forwardConnection, _ = net.Dial("udp", appContext.forwardAddess) 47 | } 48 | 49 | radioContext := new(obj.RadioContext) 50 | radioContext.RadioAddr = appContext.radioAddr 51 | radioContext.MyUdpEndpointPort = appContext.myPort 52 | radioContext.ChannelRadioResponse = make(chan string) 53 | radioContext.ChannelVitaIfData = make(chan *sdrobjects.SdrIfData) 54 | radioContext.Debug = false 55 | 56 | go func(ctx *obj.RadioContext) { 57 | for { 58 | response := <-ctx.ChannelRadioResponse 59 | 60 | if strings.HasPrefix(response, "R"+strconv.Itoa(appContext.RadioReponseStreamSequence)) { 61 | streamHexString := strings.Split(response, "|")[2] 62 | l.Println("Stream filter streamId 0x" + streamHexString) 63 | stream, _ := strconv.ParseUint(streamHexString, 16, 64) 64 | appContext.RadioResponseStream = stream 65 | obj.SendRadioCommand(radioContext, "stream set 0x"+streamHexString+" daxiq_rate="+appContext.sampleRate) 66 | } 67 | } 68 | }(radioContext) 69 | 70 | go func(ctx *obj.RadioContext) { 71 | for { /* we'll only receive the samples for the stream requested on that port so we can ignore the stream id*/ 72 | handleData(appContext, *<-ctx.ChannelVitaIfData) 73 | } 74 | }(radioContext) 75 | 76 | go obj.InitRadioContext(radioContext) 77 | 78 | for { 79 | if len(radioContext.RadioHandle) > 0 { // wait until we got our handle 80 | break 81 | } 82 | time.Sleep(500) 83 | } 84 | 85 | // wait for first clientId 86 | var firstClient = "" 87 | 88 | if radioContext.Debug { 89 | l.Println("waiting for first client") 90 | } 91 | 92 | for { 93 | 94 | radioContext.Clients.Range(func(k interface{}, value interface{}) bool { 95 | firstClient = value.(obj.Client).ClientId 96 | return true 97 | }) 98 | 99 | if firstClient != "" { 100 | break 101 | } 102 | } 103 | 104 | // wait for first panadapter 105 | var firstPan = "" 106 | 107 | if radioContext.Debug { 108 | l.Println("waiting for first panadapter") 109 | } 110 | 111 | for { 112 | 113 | radioContext.Panadapters.Range(func(k interface{}, value interface{}) bool { 114 | firstPan = value.(obj.Panadapter).Id 115 | return true 116 | }) 117 | 118 | if firstPan != "" { 119 | break 120 | } 121 | } 122 | l.Println("Binding to client_id " + firstClient) 123 | obj.SendRadioCommand(radioContext, "client bind client_id="+firstClient) 124 | l.Println("Requesting UDP VITA data to be sent to " + appContext.myPort) 125 | 126 | obj.SendRadioCommand(radioContext, "client udpport "+appContext.myPort) 127 | 128 | appContext.RadioReponseStreamSequence = obj.SendRadioCommand(radioContext, "stream create type=dax_iq daxiq_channel=1") 129 | 130 | l.Println("binding to panadapter " + firstPan) 131 | 132 | obj.SendRadioCommand(radioContext, "dax iq set 1 pan="+firstPan+" rate="+appContext.sampleRate) 133 | 134 | if len(appContext.forwardAddess) > 0 { 135 | l.Println("Forwarding data to " + appContext.forwardAddess) 136 | } 137 | 138 | go func(ctx *obj.RadioContext) { 139 | 140 | if appContext.fCenterArg != "" { 141 | obj.SendRadioCommand(radioContext, "display pan set "+firstPan+" center="+appContext.fCenterArg) 142 | l.Println("Instructed pan " + firstPan + " to tune to " + appContext.fCenterArg + " MHz") 143 | } 144 | 145 | for { 146 | 147 | reader := bufio.NewReader(os.Stdin) 148 | l.Print("Listening for tuning instruction (MHz) at stdin") 149 | text, _ := reader.ReadString('\n') 150 | text = strings.Trim(text, " ") 151 | text = strings.Trim(text, "\n") 152 | if _, err := strconv.ParseFloat(text, 32); err == nil { 153 | //obj.SendRadioCommand(radioContext, "display pan s") 154 | obj.SendRadioCommand(radioContext, "display pan set "+firstPan+" center="+text) 155 | l.Println("Instructed pan " + firstPan + " to tune to" + text + " MHz") 156 | 157 | } 158 | 159 | } 160 | }(radioContext) 161 | 162 | forever := make(chan bool) 163 | forever <- true 164 | } 165 | 166 | func handleData(appctx *AppContext, ifDataPackage sdrobjects.SdrIfData) { 167 | 168 | if uint64(ifDataPackage.Stream_id) != appctx.RadioResponseStream { 169 | return 170 | } 171 | 172 | if len(appctx.forwardAddess) > 0 { 173 | appctx.forwardConnection.Write(ifDataPackage.Data) 174 | } else { 175 | os.Stdout.Write(ifDataPackage.Data) 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /cmd/smartsdr-mqttadapter/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | mqtt "github.com/eclipse/paho.mqtt.golang" 8 | "github.com/hb9fxq/flexlib-go/obj" 9 | "net" 10 | "time" 11 | ) 12 | 13 | type AppContext struct { 14 | radioAddr string 15 | myPort string 16 | daxIqChan string 17 | sampleRate string 18 | RadioReponseStreamSequence int 19 | forwardConnection net.Conn 20 | mqttClient mqtt.Client 21 | mqttBroker string 22 | mqttClientId string 23 | mqttTopic string 24 | } 25 | 26 | const NDEF_STRING string = "NDEF" 27 | 28 | func publishRaw(appContext *AppContext, message string) { 29 | rtoken := appContext.mqttClient.Publish(appContext.mqttTopic+"/raw", 0, false, message) 30 | rtoken.Wait() 31 | } 32 | 33 | func main() { 34 | 35 | appContext := new(AppContext) 36 | 37 | flag.StringVar(&appContext.radioAddr, "RADIO", "", "IP ADDRESS OF THE RADIO e.g 192.168.41.8") 38 | flag.StringVar(&appContext.mqttBroker, "MQTTBROKER", NDEF_STRING, "MQTT Broker conn str.") 39 | flag.StringVar(&appContext.mqttTopic, "MQTTTOPIC", NDEF_STRING, "MQTT Broker conn str.") 40 | flag.StringVar(&appContext.mqttClientId, "MQTTCLIENTID", NDEF_STRING, "MQTT Broker conn str.") 41 | flag.Parse() 42 | 43 | opts := mqtt.NewClientOptions().AddBroker(appContext.mqttBroker).SetClientID(appContext.mqttClientId) 44 | opts.SetKeepAlive(2 * time.Second) 45 | opts.SetPingTimeout(1 * time.Second) 46 | opts.SetCleanSession(false) 47 | 48 | appContext.mqttClient = mqtt.NewClient(opts) 49 | if token := appContext.mqttClient.Connect(); token.Wait() && token.Error() != nil { 50 | panic(token.Error()) 51 | } 52 | 53 | radioContext := new(obj.RadioContext) 54 | radioContext.RadioAddr = appContext.radioAddr 55 | radioContext.MyUdpEndpointPort = appContext.myPort 56 | radioContext.ChannelRadioResponse = make(chan string) 57 | radioContext.Debug = true 58 | radioContext.ManualSubscribe = true 59 | 60 | go obj.InitRadioContext(radioContext) 61 | 62 | time.Sleep(2 * time.Second) 63 | 64 | go func(ctx *obj.RadioContext) { 65 | for { 66 | response := <-ctx.ChannelRadioResponse 67 | //fmt.Println("F:" + response) 68 | 69 | go publishRaw(appContext, response) 70 | } 71 | }(radioContext) 72 | 73 | obj.SendRadioCommand(radioContext, "sub client all") 74 | obj.SendRadioCommand(radioContext, "sub pan all") 75 | obj.SendRadioCommand(radioContext, "sub slice all") 76 | obj.SendRadioCommand(radioContext, "sub tx all") 77 | 78 | for { 79 | time.Sleep(1 * time.Second) 80 | 81 | jsonSlices := make(map[string]interface{}) 82 | radioContext.Slices.Range(func(k interface{}, value interface{}) bool { 83 | jsonSlices[k.(string)] = value 84 | return true 85 | }) 86 | j, err := json.Marshal(&jsonSlices) 87 | 88 | if err != nil { 89 | fmt.Println(err) 90 | continue 91 | } 92 | 93 | stoken := appContext.mqttClient.Publish(appContext.mqttTopic+"/slices", 0, false, j) 94 | stoken.Wait() 95 | 96 | jsonPanadapters := make(map[string]interface{}) 97 | radioContext.Panadapters.Range(func(k interface{}, value interface{}) bool { 98 | jsonPanadapters[k.(string)] = value 99 | return true 100 | }) 101 | j, err = json.Marshal(&jsonPanadapters) 102 | 103 | if err != nil { 104 | fmt.Println(err) 105 | continue 106 | } 107 | 108 | ptoken := appContext.mqttClient.Publish(appContext.mqttTopic+"/panadapters", 0, false, j) 109 | ptoken.Wait() 110 | 111 | jsonClients := make(map[string]interface{}) 112 | radioContext.Clients.Range(func(k interface{}, value interface{}) bool { 113 | jsonClients[k.(string)] = value 114 | return true 115 | }) 116 | j, err = json.Marshal(&jsonClients) 117 | 118 | if err != nil { 119 | fmt.Println(err) 120 | continue 121 | } 122 | 123 | ctoken := appContext.mqttClient.Publish(appContext.mqttTopic+"/clients", 0, false, j) 124 | ctoken.Wait() 125 | } 126 | 127 | forever := make(chan bool) 128 | forever <- true 129 | } 130 | -------------------------------------------------------------------------------- /crossbuild/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd .. 4 | #Mac 5 | env GOOS=darwin GOARCH=amd64 go build -o ../../../../bin/flexlib-go/osx/smartsdr-daxclient github.com/hb9fxq/flexlib-go/cmd/smartsdr-daxclient 6 | env GOOS=darwin GOARCH=amd64 go build -o ../../../../bin/flexlib-go/osx/smartsdr-iqtransfer github.com/hb9fxq/flexlib-go/cmd/smartsdr-iqtransfer 7 | env GOOS=darwin GOARCH=amd64 go build -o ../../../../bin/flexlib-go/osx/smartsdr-mqttadapter github.com/hb9fxq/flexlib-go/cmd/smartsdr-mqttadapter 8 | 9 | # Linux 10 | env GOOS=linux GOARCH=amd64 go build -o ../../../../bin/flexlib-go/linux64/smartsdr-daxclient github.com/hb9fxq/flexlib-go/cmd/smartsdr-daxclient 11 | env GOOS=linux GOARCH=amd64 go build -o ../../../../bin/flexlib-go/linux64/smartsdr-iqtransfer github.com/hb9fxq/flexlib-go/cmd/smartsdr-iqtransfer 12 | env GOOS=linux GOARCH=amd64 go build -o ../../../../bin/flexlib-go/linux64/smartsdr-mqttadapter github.com/hb9fxq/flexlib-go/cmd/smartsdr-mqttadapter 13 | 14 | # Raspi 15 | env GOOS=linux GOARCH=arm GOARM=5 go build -o ../../../../bin/flexlib-go/raspberryPi/smartsdr-daxclient github.com/hb9fxq/flexlib-go/cmd/smartsdr-daxclient 16 | env GOOS=linux GOARCH=arm GOARM=5 go build -o ../../../../bin/flexlib-go/raspberryPi/smartsdr-iqtransfer github.com/hb9fxq/flexlib-go/cmd/smartsdr-iqtransfer 17 | env GOOS=linux GOARCH=arm GOARM=5 go build -o ../../../../bin/flexlib-go/raspberryPi/smartsdr-mqttadapter github.com/hb9fxq/flexlib-go/cmd/smartsdr-mqttadapter 18 | 19 | # Windows 20 | env GOOS=windows GOARCH=amd64 go build -o ../../../../bin/flexlib-go/Win64/smartsdr-daxclient.exe github.com/hb9fxq/flexlib-go/cmd/smartsdr-daxclient 21 | env GOOS=windows GOARCH=amd64 go build -o ../../../../bin/flexlib-go/Win64/smartsdr-iqtransfer.exe github.com/hb9fxq/flexlib-go/cmd/smartsdr-iqtransfer 22 | env GOOS=windows GOARCH=amd64 go build -o ../../../../bin/flexlib-go/Win64/smartsdr-mqttadapter github.com/hb9fxq/flexlib-go/cmd/smartsdr-mqttadapter 23 | 24 | env GOOS=windows GOARCH=386 go build -o ../../../../bin/flexlib-go/Win32/smartsdr-daxclient github.com/hb9fxq/flexlib-go/cmd/smartsdr-daxclient 25 | env GOOS=windows GOARCH=386 go build -o ../../../../bin/flexlib-go/Win32/smartsdr-iqtransfer.exe github.com/hb9fxq/flexlib-go/cmd/smartsdr-iqtransfer 26 | env GOOS=windows GOARCH=386 go build -o ../../../../bin/flexlib-go/Win32/smartsdr-mqttadapter.exe github.com/hb9fxq/flexlib-go/cmd/smartsdr-mqttadapter -------------------------------------------------------------------------------- /extlib/libSmartsdrIqTransfer/libiqtransfer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "C" 4 | 5 | import ( 6 | "bufio" 7 | "fmt" 8 | "github.com/hb9fxq/flexlib-go/obj" 9 | "github.com/hb9fxq/flexlib-go/sdrobjects" 10 | "github.com/smallnest/ringbuffer" 11 | "log" 12 | "net" 13 | "os" 14 | "strconv" 15 | "strings" 16 | "time" 17 | ) 18 | 19 | type AppContext struct { 20 | radioAddr string 21 | myPort string 22 | daxIqChan string 23 | sampleRate string 24 | forwardAddess string 25 | RadioReponseStreamSequence int 26 | RadioResponseStream uint64 27 | forwardConnection net.Conn 28 | fCenterArg string 29 | readBuffer *ringbuffer.RingBuffer 30 | } 31 | 32 | var gloabalAppCtx *AppContext 33 | var initSuccess bool 34 | var firstPan = "" 35 | var radioContext *obj.RadioContext 36 | var l = log.New(os.Stderr, "IQTRANSFERLIB ", 0) 37 | 38 | //export InitRadio 39 | func InitRadio(radioAddr string, myudp string, channel string, rate string) { 40 | 41 | if initSuccess { 42 | return 43 | } 44 | 45 | appContext := new(AppContext) 46 | appContext.radioAddr = radioAddr 47 | appContext.myPort = myudp 48 | appContext.daxIqChan = channel 49 | appContext.sampleRate = rate 50 | appContext.readBuffer = ringbuffer.New(16 * 100000) 51 | gloabalAppCtx = appContext 52 | 53 | if appContext.sampleRate != "24000" && appContext.sampleRate != "48000" && appContext.sampleRate != "96000" && appContext.sampleRate != "192000" { 54 | panic("Invalid Sample Rate! Allowed values 24000, 48000, 96000, 192000") 55 | } 56 | 57 | if len(appContext.forwardAddess) > 0 { 58 | appContext.forwardConnection, _ = net.Dial("udp", appContext.forwardAddess) 59 | } 60 | 61 | radioContext = new(obj.RadioContext) 62 | radioContext.RadioAddr = appContext.radioAddr 63 | radioContext.MyUdpEndpointPort = appContext.myPort 64 | radioContext.ChannelRadioResponse = make(chan string) 65 | radioContext.ChannelVitaIfData = make(chan *sdrobjects.SdrIfData) 66 | radioContext.Debug = false 67 | 68 | go func(ctx *obj.RadioContext) { 69 | for { 70 | response := <-ctx.ChannelRadioResponse 71 | 72 | if strings.HasPrefix(response, "R"+strconv.Itoa(appContext.RadioReponseStreamSequence)) { 73 | streamHexString := strings.Split(response, "|")[2] 74 | l.Println("Stream filter streamId 0x" + streamHexString) 75 | stream, _ := strconv.ParseUint(streamHexString, 16, 64) 76 | appContext.RadioResponseStream = stream 77 | obj.SendRadioCommand(radioContext, "stream set 0x"+streamHexString+" daxiq_rate="+appContext.sampleRate) 78 | } 79 | } 80 | }(radioContext) 81 | 82 | go func(ctx *obj.RadioContext) { 83 | for { /* we'll only receive the samples for the stream requested on that port so we can ignore the stream id*/ 84 | handleData(appContext, *<-ctx.ChannelVitaIfData) 85 | } 86 | }(radioContext) 87 | 88 | go obj.InitRadioContext(radioContext) 89 | 90 | for { 91 | if len(radioContext.RadioHandle) > 0 { // wait until we got our handle 92 | break 93 | } 94 | time.Sleep(500) 95 | } 96 | 97 | // wait for first clientId 98 | var firstClient = "" 99 | 100 | if radioContext.Debug { 101 | l.Println("waiting for first client") 102 | } 103 | 104 | for { 105 | 106 | radioContext.Clients.Range(func(k interface{}, value interface{}) bool { 107 | firstClient = value.(obj.Client).ClientId 108 | return true 109 | }) 110 | 111 | if firstClient != "" { 112 | break 113 | } 114 | } 115 | 116 | // wait for first panadapter 117 | 118 | if radioContext.Debug { 119 | l.Println("waiting for first panadapter") 120 | } 121 | 122 | for { 123 | 124 | radioContext.Panadapters.Range(func(k interface{}, value interface{}) bool { 125 | firstPan = value.(obj.Panadapter).Id 126 | return true 127 | }) 128 | 129 | if firstPan != "" { 130 | break 131 | } 132 | } 133 | l.Println("Binding to client_id " + firstClient) 134 | obj.SendRadioCommand(radioContext, "client bind client_id="+firstClient) 135 | obj.SendRadioCommand(radioContext, "client set enforce_network_mtu=1 network_mtu=1420") 136 | l.Println("Requesting UDP VITA data to be sent to " + appContext.myPort) 137 | 138 | obj.SendRadioCommand(radioContext, "client udpport "+appContext.myPort) 139 | 140 | appContext.RadioReponseStreamSequence = obj.SendRadioCommand(radioContext, "stream create type=dax_iq daxiq_channel=1") 141 | obj.SendRadioCommand(radioContext, "client set enforce_network_mtu=1 network_mtu=1420") 142 | l.Println("binding to panadapter " + firstPan) 143 | 144 | obj.SendRadioCommand(radioContext, "dax iq set 1 pan="+firstPan+" rate="+appContext.sampleRate) 145 | 146 | if len(appContext.forwardAddess) > 0 { 147 | l.Println("Forwarding data to " + appContext.forwardAddess) 148 | } 149 | 150 | go func(ctx *obj.RadioContext) { 151 | 152 | if appContext.fCenterArg != "" { 153 | obj.SendRadioCommand(radioContext, "display pan set "+firstPan+" center="+appContext.fCenterArg) 154 | l.Println("Instructed pan " + firstPan + " to tune to " + appContext.fCenterArg + " MHz") 155 | } 156 | 157 | for { 158 | 159 | reader := bufio.NewReader(os.Stdin) 160 | l.Print("Listening for tuning instruction (MHz) at stdin") 161 | text, _ := reader.ReadString('\n') 162 | text = strings.Trim(text, " ") 163 | text = strings.Trim(text, "\n") 164 | if _, err := strconv.ParseFloat(text, 32); err == nil { 165 | //obj.SendRadioCommand(radioContext, "display pan s") 166 | obj.SendRadioCommand(radioContext, "display pan set "+firstPan+" center="+text) 167 | l.Println("Instructed pan " + firstPan + " to tune to" + text + " MHz") 168 | 169 | } 170 | 171 | } 172 | }(radioContext) 173 | 174 | initSuccess = true 175 | 176 | } 177 | 178 | var dataAvail bool 179 | 180 | //export SetFrequency 181 | func SetFrequency(freq int64) { 182 | 183 | if firstPan == "" { 184 | return 185 | } 186 | 187 | freqMhz := float64(freq) / 1000000 188 | 189 | s := fmt.Sprintf("%.6f", freqMhz) 190 | 191 | l.Println(s) 192 | l.Println("Instructed pan " + firstPan + " to tune to " + s + " MHz") 193 | obj.SendRadioCommand(radioContext, "display pan set "+firstPan+" center="+s) 194 | 195 | } 196 | 197 | //export ReadStream3 198 | func ReadStream3(elements int) (C.size_t, *C.uchar) { 199 | 200 | size := elements 201 | p := C.malloc(C.size_t(size)) 202 | 203 | bytes := (*[1<<30 - 1]C.uchar)(p)[:size:size] 204 | 205 | for { 206 | if bytesWritten >= size { 207 | break 208 | } 209 | time.Sleep(10 * time.Millisecond) 210 | } 211 | 212 | if bytesWritten < size { 213 | return C.size_t(0), (*C.uchar)(p) 214 | } 215 | 216 | buf := make([]byte, size) 217 | readSizze, _ := gloabalAppCtx.readBuffer.Read(buf) 218 | bytesWritten -= elements 219 | 220 | for i := 0; i < readSizze; i++ { 221 | bytes[i] = C.uchar(buf[i]) 222 | } 223 | 224 | //fmt.Printf("%s", hex.Dump(buf)) 225 | 226 | return C.size_t(size), (*C.uchar)(p) 227 | } 228 | 229 | var LastBlock []byte 230 | 231 | var bytesWritten = 0 232 | 233 | func handleData(appctx *AppContext, ifDataPackage sdrobjects.SdrIfData) { 234 | 235 | if uint64(ifDataPackage.Stream_id) != appctx.RadioResponseStream { 236 | return 237 | } 238 | //fmt.Printf("%s", hex.Dump(ifDataPackage.Data)) 239 | appctx.readBuffer.Write(ifDataPackage.Data) 240 | bytesWritten += len(ifDataPackage.Data) 241 | } 242 | 243 | func main() {} 244 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hb9fxq/flexlib-go 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/eclipse/paho.mqtt.golang v1.2.0 7 | github.com/smallnest/ringbuffer v0.0.0-20201021141743-dc0a6f7571a3 8 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b // indirect 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/eclipse/paho.mqtt.golang v1.2.0 h1:1F8mhG9+aO5/xpdtFkW4SxOJB67ukuDC3t2y2qayIX0= 2 | github.com/eclipse/paho.mqtt.golang v1.2.0/go.mod h1:H9keYFcgq3Qr5OUJm/JZI/i6U7joQ8SYLhZwfeOo6Ts= 3 | github.com/smallnest/ringbuffer v0.0.0-20201021141743-dc0a6f7571a3 h1:Bq1CtUTxX3GtKY18EkVj2IXqDB5sWMnXbgS30/cczQ0= 4 | github.com/smallnest/ringbuffer v0.0.0-20201021141743-dc0a6f7571a3/go.mod h1:wIpUJ8WEUx959cAgIwpDuHUcE7aexxxYNcvXUT08L90= 5 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 6 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 7 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 8 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME= 9 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 10 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 11 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 12 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 13 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 14 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 15 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 16 | -------------------------------------------------------------------------------- /obj/client.go: -------------------------------------------------------------------------------- 1 | package obj 2 | 3 | type Client struct { 4 | Handle string 5 | ClientId string 6 | Program string 7 | } 8 | -------------------------------------------------------------------------------- /obj/panadapter.go: -------------------------------------------------------------------------------- 1 | package obj 2 | 3 | type Panadapter struct { 4 | Id string 5 | Center int32 6 | ClientHandle string 7 | XPixels int32 8 | YPixels int32 9 | Bandwidth float64 10 | Min_dbm float64 11 | Max_dbm float64 12 | } 13 | 14 | type IqStream struct { 15 | Id int 16 | Pan string 17 | Rate int 18 | } 19 | 20 | /* 21 | F:SCDD36271|display pan 22 | 0x40000000 23 | client_handle=0x736ABCCB 24 | wnb=0 25 | wnb_level=0 26 | wnb_updating=1 27 | band_zoom=0 28 | segment_zoom=0 29 | x_pixels=1047 30 | y_pixels=510 31 | center=18.119837 32 | bandwidth=0.162466 33 | min_dbm=-137.66 34 | max_dbm=-42.66 35 | fps=12 36 | average=50 37 | weighted_average=0 38 | rfgain=0 39 | rxant=ANT1 wide=0 40 | loopa=0 41 | loopb=0 42 | band=17 43 | daxiq_channel=0 44 | waterfall=0x42000000 45 | min_bw=0.004920 46 | max_bw=14.745601 47 | xvtr= 48 | pre= 49 | ant_list=ANT1,ANT2,RX_A,XVTR 50 | 51 | 52 | */ 53 | -------------------------------------------------------------------------------- /obj/radio.go: -------------------------------------------------------------------------------- 1 | package obj 2 | 3 | import ( 4 | "errors" 5 | "github.com/hb9fxq/flexlib-go/sdrobjects" 6 | "github.com/hb9fxq/flexlib-go/vita" 7 | "log" 8 | "net" 9 | "os" 10 | "strconv" 11 | "strings" 12 | "sync" 13 | ) 14 | 15 | type RadioData struct { 16 | Preampble *vita.VitaPacketPreamble 17 | Payload []byte 18 | LastErr error 19 | } 20 | 21 | type RadioContext struct { 22 | RadioAddr string `json:"RadioAddr"` 23 | RadioCmdSeqNumber int 24 | RadioConn *net.TCPConn 25 | ChannelRadioData chan *RadioData `json:"-"` 26 | ChannelRadioResponse chan string `json:"-"` 27 | RadioHandle string 28 | MyUdpEndpointIP *net.IP 29 | MyUdpEndpointPort string // we need strings for all cmds.... 30 | ChannelVitaFFT chan *sdrobjects.SdrFFTPacket `json:"-"` 31 | ChannelVitaOpus chan []byte `json:"-"` 32 | ChannelVitaIfData chan *sdrobjects.SdrIfData `json:"-"` 33 | ChannelVitaMeter chan *sdrobjects.SdrMeterPacket `json:"-"` 34 | ChannelVitaWaterfallTile chan *sdrobjects.SdrWaterfallTile `json:"-"` 35 | Panadapters sync.Map `json:"Panadapters"` 36 | IqStreams sync.Map `json:"RadioAddr"` 37 | Slices sync.Map `json:"Slices"` 38 | Clients sync.Map 39 | Debug bool 40 | ManualSubscribe bool 41 | } 42 | 43 | func getNextCommandPrefix(ctx *RadioContext) (string, int) { 44 | ctx.RadioCmdSeqNumber += 1 45 | return "C" + strconv.Itoa(ctx.RadioCmdSeqNumber) + "|", ctx.RadioCmdSeqNumber 46 | } 47 | 48 | func SendRadioCommand(ctx *RadioContext, cmd string) int { 49 | 50 | l := log.New(os.Stderr, "DEBUG >> ", 0) 51 | 52 | prefixString, sequence := getNextCommandPrefix(ctx) 53 | 54 | if ctx.Debug { 55 | l.Println(prefixString + cmd) 56 | } 57 | _, err := ctx.RadioConn.Write([]byte(prefixString + cmd + "\r")) 58 | 59 | if err != nil { 60 | panic(err) 61 | } 62 | 63 | return sequence 64 | } 65 | 66 | func GetOutboundIP() *net.IP { 67 | conn, err := net.Dial("udp", "8.8.8.8:80") 68 | if err != nil { 69 | log.Fatal(err) 70 | } 71 | defer conn.Close() 72 | 73 | localAddr := conn.LocalAddr().(*net.UDPAddr) 74 | return &localAddr.IP 75 | } 76 | 77 | func InitRadioContext(ctx *RadioContext) { 78 | 79 | tcpAddr, err := net.ResolveTCPAddr("tcp", ctx.RadioAddr+":4992") 80 | 81 | if err != nil { 82 | log.Fatal(err) 83 | panic(err) 84 | } 85 | 86 | // dial TCP connection to radio 87 | conn, err := net.DialTCP("tcp", nil, tcpAddr) 88 | ctx.RadioConn = conn 89 | 90 | ctx.MyUdpEndpointIP = GetOutboundIP() 91 | 92 | if err != nil { 93 | log.Fatal(err) 94 | panic(err) 95 | } 96 | 97 | if err != nil { 98 | log.Println(err) 99 | panic(err) 100 | } 101 | 102 | go subscribeRadioUdp(ctx) 103 | go subscribeRadioUpdates(conn, ctx) 104 | 105 | // Subscribe data from radio 106 | 107 | if !ctx.ManualSubscribe { 108 | SendRadioCommand(ctx, "sub tx all") 109 | SendRadioCommand(ctx, "sub client all") 110 | SendRadioCommand(ctx, "sub atu all") 111 | SendRadioCommand(ctx, "sub amplifier all") 112 | SendRadioCommand(ctx, "sub meter all") 113 | SendRadioCommand(ctx, "sub pan all") 114 | SendRadioCommand(ctx, "sub slice all") 115 | SendRadioCommand(ctx, "sub gps all") 116 | SendRadioCommand(ctx, "sub audio_stream all") 117 | SendRadioCommand(ctx, "sub cwx all") 118 | SendRadioCommand(ctx, "sub xvtr all") 119 | SendRadioCommand(ctx, "sub memories all") 120 | SendRadioCommand(ctx, "sub daxiq all") 121 | SendRadioCommand(ctx, "sub dax all") 122 | SendRadioCommand(ctx, "sub usb_cable all") 123 | } 124 | 125 | forever := make(chan bool) 126 | forever <- true 127 | 128 | } 129 | 130 | func subscribeRadioUpdates(conn *net.TCPConn, ctx *RadioContext) { 131 | 132 | l := log.New(os.Stderr, "DEBUG << ", 0) 133 | buf := make([]byte, 4096) 134 | 135 | for { 136 | n, err := conn.Read(buf) 137 | 138 | if err != nil { 139 | continue 140 | } 141 | 142 | response := string(buf[:n]) 143 | 144 | if len(response) == 0 { 145 | continue 146 | } 147 | 148 | lines := strings.Split(response, "\n") 149 | 150 | for _, responseLine := range lines { 151 | 152 | if ctx.Debug { 153 | l.Println(responseLine) 154 | } 155 | 156 | if len(strings.Trim(responseLine, " ")) == 0 { 157 | continue 158 | } 159 | 160 | if len(ctx.RadioHandle) == 0 && strings.HasPrefix(strings.ToUpper(responseLine), "H") { 161 | ctx.RadioHandle = responseLine[1:] 162 | l.Println("\nHANDLE >>" + ctx.RadioHandle) 163 | } else { 164 | 165 | if nil == ctx.ChannelRadioResponse { 166 | l.Println("Response channel not bound: " + responseLine) 167 | } else { 168 | ctx.ChannelRadioResponse <- responseLine 169 | 170 | go ParseResponseLine(ctx, responseLine) 171 | } 172 | } 173 | } 174 | 175 | if err != nil { 176 | l.Println(err) 177 | } 178 | } 179 | } 180 | func ParseResponseLine(context *RadioContext, respLine string) { 181 | 182 | _, message := parseReplyStringPrefix(respLine) 183 | 184 | if strings.Contains(message, "display pan") { 185 | parsePanAdapterParams(context, message) 186 | } else if strings.Contains(message, "daxiq ") { 187 | parseDaxIqStatusParams(context, message) 188 | } else if strings.Contains(message, "slice ") { 189 | parseSliceParams(context, message) 190 | } else if strings.Contains(message, "client") && (strings.Contains(message, "connected ") || strings.Contains(message, "disconnected ")) { 191 | parseClientParams(context, message) 192 | } 193 | } 194 | 195 | func parsePanAdapterParams(context *RadioContext, i string) { 196 | /* 197 | >0x40000000 wnb=0 wnb_level=50 wnb_updating=0 x_pixels=490 y_pixels=535 center=3.792057 bandwidth=0.885342 min_dbm=-126.84 max_dbm=-66.812 fps=5 average=70 weighted_average=0 rfgain=0 rxant=ANT2 wide=1 loopa=0 loopb=0 band=80 daxiq=0 daxiq_rate=0 capacity=16 available=16 waterfall=42000000 min_bw=0.004919999957085 max_b<>w=14.74560058594 xvtr= pre= ant_list=ANT1,ANT2,RX_A,XVTR< 198 | */ 199 | _, res := parseKeyValueString(i) 200 | 201 | idSelector := "STMT2" 202 | 203 | if strings.Contains(i, " set ") { 204 | idSelector = "STMT3" 205 | } 206 | 207 | if 1 > len(res[idSelector]) { 208 | return 209 | } 210 | 211 | if strings.Contains(i, " removed") { 212 | context.Panadapters.Delete(res["STMT2"]) 213 | return 214 | } 215 | 216 | var panadapter Panadapter 217 | 218 | panadapter.Id = res[idSelector] 219 | 220 | dirty := false 221 | 222 | actual, loaded := context.Panadapters.LoadOrStore(res[idSelector], panadapter) 223 | 224 | if loaded { 225 | panadapter = actual.(Panadapter) 226 | } 227 | 228 | if val, ok := res["center"]; ok { 229 | rawFloatCenter, _ := strconv.ParseFloat(val, 64) 230 | panadapter.Center = int32(rawFloatCenter * 1000000) 231 | dirty = true 232 | } 233 | 234 | if val, ok := res["client_handle"]; ok { 235 | panadapter.ClientHandle = val 236 | dirty = true 237 | } 238 | 239 | if val, ok := res["xpixels"]; ok { 240 | raw, _ := strconv.ParseFloat(val, 64) 241 | panadapter.XPixels = int32(raw) 242 | dirty = true 243 | } 244 | 245 | if val, ok := res["ypixels"]; ok { 246 | raw, _ := strconv.ParseFloat(val, 64) 247 | panadapter.YPixels = int32(raw) 248 | dirty = true 249 | } 250 | 251 | if val, ok := res["x_pixels"]; ok { 252 | raw, _ := strconv.ParseFloat(val, 64) 253 | panadapter.XPixels = int32(raw) 254 | dirty = true 255 | } 256 | 257 | if val, ok := res["y_pixels"]; ok { 258 | raw, _ := strconv.ParseFloat(val, 64) 259 | panadapter.YPixels = int32(raw) 260 | dirty = true 261 | } 262 | 263 | if val, ok := res["bandwidth"]; ok { 264 | panadapter.Bandwidth, _ = strconv.ParseFloat(val, 64) 265 | dirty = true 266 | } 267 | 268 | if val, ok := res["min_dbm"]; ok { 269 | panadapter.Min_dbm, _ = strconv.ParseFloat(val, 64) 270 | dirty = true 271 | } 272 | 273 | if val, ok := res["max_dbm"]; ok { 274 | panadapter.Max_dbm, _ = strconv.ParseFloat(val, 64) 275 | dirty = true 276 | } 277 | 278 | if dirty { 279 | context.Panadapters.Store(res[idSelector], panadapter) 280 | } 281 | } 282 | 283 | func parseSliceParams(context *RadioContext, i string) { 284 | 285 | _, res := parseKeyValueString(i) 286 | 287 | if 1 > len(res["STMT1"]) { 288 | return 289 | } 290 | 291 | var slice Slice 292 | slice.Id = res["STMT1"] 293 | dirty := false 294 | 295 | actual, loaded := context.Slices.LoadOrStore(res["STMT1"], slice) 296 | 297 | if loaded { 298 | slice = actual.(Slice) 299 | } 300 | 301 | if val, ok := res["client_handle"]; ok { 302 | slice.ClientHandle = val 303 | dirty = true 304 | } 305 | 306 | if val, ok := res["txant"]; ok { 307 | slice.TxAnt = val 308 | dirty = true 309 | } 310 | 311 | if val, ok := res["mode"]; ok { 312 | slice.Mode = val 313 | dirty = true 314 | } 315 | 316 | if val, ok := res["rxant"]; ok { 317 | slice.RxAnt = val 318 | dirty = true 319 | } 320 | 321 | if val, ok := res["pan"]; ok { 322 | slice.Panadapter = val 323 | dirty = true 324 | } 325 | 326 | if val, ok := res["dax"]; ok { 327 | slice.Dax = val 328 | dirty = true 329 | } 330 | 331 | if val, ok := res["index_letter"]; ok { 332 | slice.IndexLetter = val 333 | dirty = true 334 | } 335 | 336 | if val, ok := res["in_use"]; ok { 337 | if val == "0" { 338 | context.Slices.Delete(res["STMT1"]) 339 | return 340 | } else { 341 | slice.InUse = true 342 | dirty = true 343 | } 344 | } 345 | 346 | if val, ok := res["RF_frequency"]; ok { 347 | rawFloatCenter, _ := strconv.ParseFloat(val, 64) 348 | slice.RfFrequency = rawFloatCenter 349 | dirty = true 350 | } 351 | 352 | if dirty { 353 | context.Slices.Store(res["STMT1"], slice) 354 | } 355 | } 356 | 357 | func parseClientParams(context *RadioContext, i string) { 358 | 359 | _, res := parseKeyValueString(i) 360 | 361 | if 1 > len(res["STMT1"]) { 362 | return 363 | } 364 | 365 | var client Client 366 | client.Handle = res["STMT1"] 367 | dirty := false 368 | 369 | actual, loaded := context.Clients.LoadOrStore(res["STMT1"], client) 370 | 371 | if loaded { 372 | client = actual.(Client) 373 | } 374 | 375 | /* 376 | DEBUG SDEC1D512|client 0x5CD6439B connected local_ptt=1 client_id=76D40FCB-9FB8-49E1-8A62-7728737A7955 program=SmartSDR-Mac station=HB9FXQ 377 | DEBUG S5CD6439B|client 0x5CD6439B disconnected forced=0 wan_validation_failed=0 duplicate_client_id=0 378 | DEBUG S5EB17F29|client 0x5EB17F29 connected local_ptt=1 client_id=76D40FCB-9FB8-49E1-8A62-7728737A7955 program=SmartSDR-Mac 379 | DEBUG S5EB17F29|client 0x5EB17F29 connected local_ptt=1 client_id=76D40FCB-9FB8-49E1-8A62-7728737A7955 program=SmartSDR-Mac 380 | DEBUG S5EB17F29|client 0x5EB17F29 connected local_ptt=1 client_id=76D40FCB-9FB8-49E1-8A62-7728737A7955 program=SmartSDR-Mac station=HB9FXQ 381 | 382 | */ 383 | 384 | if val, ok := res["client_id"]; ok { 385 | client.ClientId = val 386 | dirty = true 387 | } 388 | 389 | if val, ok := res["program"]; ok { 390 | client.Program = val 391 | dirty = true 392 | } 393 | 394 | if val, ok := res["STMT2"]; ok { 395 | 396 | if val == "disconnected" { 397 | context.Clients.Delete(res["STMT1"]) 398 | return 399 | } 400 | } 401 | 402 | if dirty { 403 | context.Clients.Store(res["STMT1"], client) 404 | } 405 | } 406 | 407 | func parseDaxIqStatusParams(context *RadioContext, i string) { 408 | _, res := parseKeyValueString(i) 409 | 410 | streamId, _ := strconv.Atoi(res["STMT1"]) 411 | var iqStream IqStream 412 | iqStream.Id = streamId 413 | dirty := false 414 | 415 | actual, loaded := context.IqStreams.LoadOrStore(res["STMT1"], iqStream) 416 | 417 | if loaded { 418 | iqStream = actual.(IqStream) 419 | } 420 | 421 | if val, ok := res["pan"]; ok { 422 | iqStream.Pan = val 423 | dirty = true 424 | } 425 | 426 | if val, ok := res["rate"]; ok { 427 | iqStream.Rate, _ = strconv.Atoi(val) 428 | dirty = true 429 | } 430 | 431 | if dirty { 432 | context.IqStreams.Store(res["STMT1"], iqStream) 433 | } 434 | } 435 | 436 | func parseReplyStringPrefix(in string) (string, string) { 437 | var prefix string 438 | var message string 439 | 440 | tokens := strings.Split(in, "|") 441 | 442 | if len(tokens) == 2 { 443 | return tokens[0], tokens[1] 444 | } 445 | 446 | return prefix, message 447 | } 448 | 449 | func parseKeyValueString(in string) (error, map[string]string) { 450 | 451 | var res map[string]string 452 | res = map[string]string{} 453 | 454 | tokens := strings.Split(in, " ") 455 | 456 | if len(tokens) == 0 { 457 | return errors.New("no tokens found"), res 458 | } 459 | 460 | statements := 0 461 | 462 | for rngAttr := range tokens[:] { 463 | 464 | contentTokens := strings.Split(tokens[rngAttr], " ") 465 | 466 | for cntToken := range contentTokens { 467 | 468 | keyValueTokens := strings.Split(contentTokens[cntToken], "=") 469 | 470 | if len(keyValueTokens) == 2 { 471 | res[keyValueTokens[0]] = keyValueTokens[1] 472 | } else { 473 | res["STMT"+strconv.Itoa(statements)] = keyValueTokens[0] 474 | statements++ 475 | } 476 | } 477 | } 478 | 479 | return nil, res 480 | } 481 | 482 | func subscribeRadioUdp(ctx *RadioContext) { 483 | 484 | FLexBroadcastAddr, err := net.ResolveUDPAddr("udp", "0.0.0.0:"+ctx.MyUdpEndpointPort) 485 | 486 | if err != nil { 487 | panic(err) 488 | } 489 | 490 | ServerConn, err := net.ListenUDP("udp", FLexBroadcastAddr) 491 | 492 | if err != nil { 493 | panic(err) 494 | } 495 | 496 | defer ServerConn.Close() 497 | buf := make([]byte, 64000) 498 | 499 | if err != nil { 500 | panic(err) 501 | } 502 | 503 | if err != nil { 504 | panic(err) 505 | } 506 | 507 | for { 508 | n, _, _ := ServerConn.ReadFromUDP(buf) 509 | radioData := new(RadioData) 510 | radioData.LastErr, radioData.Preampble, radioData.Payload = vita.ParseVitaPreamble(buf[:n]) 511 | if ctx.ChannelRadioData != nil { 512 | ctx.ChannelRadioData <- radioData 513 | } 514 | 515 | dispatchDataToChannels(ctx, radioData) 516 | } 517 | } 518 | 519 | func dispatchDataToChannels(ctx *RadioContext, data *RadioData) { 520 | switch data.Preampble.Header.Pkt_type { 521 | 522 | case vita.ExtDataWithStream: 523 | 524 | switch data.Preampble.Class_id.PacketClassCode { 525 | 526 | case vita.SL_VITA_FFT_CLASS: 527 | if nil != ctx.ChannelVitaFFT { 528 | ctx.ChannelVitaFFT <- vita.ParseVitaFFT(data.Payload, data.Preampble) 529 | } 530 | break 531 | case vita.SL_VITA_OPUS_CLASS: 532 | if nil != ctx.ChannelVitaOpus { 533 | ctx.ChannelVitaOpus <- data.Payload[:len(data.Payload)-data.Preampble.Header.Payload_cutoff_bytes] 534 | } 535 | break 536 | case vita.SL_VITA_IF_NARROW_CLASS: 537 | if nil != ctx.ChannelVitaIfData { 538 | ctx.ChannelVitaIfData <- vita.ParseFData(data.Payload, data.Preampble) 539 | } 540 | break 541 | case vita.SL_VITA_METER_CLASS: 542 | if nil != ctx.ChannelVitaMeter { 543 | ctx.ChannelVitaMeter <- vita.ParseVitaMeterPacket(data.Payload, data.Preampble) 544 | } 545 | break 546 | case vita.SL_VITA_DISCOVERY_CLASS: 547 | // maybe later - we use static addresses 548 | break 549 | case vita.SL_VITA_WATERFALL_CLASS: 550 | if nil != ctx.ChannelVitaWaterfallTile { 551 | vita.ParseVitaWaterfall(data.Payload, data.Preampble) 552 | } 553 | break 554 | default: 555 | break 556 | } 557 | 558 | break 559 | 560 | case vita.IFDataWithStream: 561 | switch data.Preampble.Class_id.PacketClassCode { 562 | case vita.SL_VITA_IF_WIDE_CLASS_24kHz: 563 | fallthrough 564 | case vita.SL_VITA_IF_WIDE_CLASS_48kHz: 565 | fallthrough 566 | case vita.SL_VITA_IF_WIDE_CLASS_96kHz: 567 | fallthrough 568 | case vita.SL_VITA_IF_WIDE_CLASS_192kHz: 569 | if nil != ctx.ChannelVitaIfData { 570 | ctx.ChannelVitaIfData <- vita.ParseFData(data.Payload, data.Preampble) 571 | } 572 | } 573 | break 574 | } 575 | } 576 | -------------------------------------------------------------------------------- /obj/slice.go: -------------------------------------------------------------------------------- 1 | package obj 2 | 3 | type Slice struct { 4 | Id string 5 | InUse bool 6 | RfFrequency float64 7 | ClientHandle string 8 | IndexLetter string 9 | Mode string 10 | TxAnt string 11 | RxAnt string 12 | Panadapter string 13 | Dax string 14 | } 15 | 16 | /* 17 | impl: 18 | in_use=1 19 | RF_frequency=18.100100 20 | index_letter=A 21 | client_handle=0x736ABCCB 22 | rxant=ANT1 23 | mode=USB 24 | txant=ANT1 25 | */ 26 | 27 | /* 28 | F:SCDD36271| 29 | slice 0 30 | in_use=1 31 | RF_frequency=18.100100 32 | client_handle=0x736ABCCB 33 | index_letter=A 34 | rit_on=0 35 | rit_freq=0 36 | xit_on=0 37 | xit_freq=0 38 | rxant=ANT1 39 | mode=USB 40 | wide=0 41 | filter_lo=100 42 | filter_hi=2800 43 | step=100 44 | step_list=1,10,50,100,500,1000,2000,3000 45 | agc_mode=med 46 | agc_threshold=65 47 | agc_off_level=10 48 | pan=0x40000000 49 | txant=ANT1 50 | loopa=0 51 | loopb=0 52 | qsk=0 53 | dax=0 54 | dax_clients=0 55 | lock=0 56 | tx=1 57 | active=0 58 | audio_level=50 59 | audio_pan=50 60 | audio_mute=0 61 | record=0 62 | play=disabled 63 | record_time=0.0 64 | anf=0 65 | anf_level=0 66 | nr=0 67 | nr_level=0 68 | nb=0 69 | nb_level=50 70 | wnb=0 71 | wnb_level=0 72 | apf=0 73 | apf_level=0 74 | squelch=1 75 | squelch_level=20 76 | diversity=0 77 | diversity_parent=0 78 | diversity_child=0 79 | diversity_index=1342177293 80 | ant_list=ANT1,ANT2,RX_A,XVTR 81 | mode_list=LSB,USB,AM,CW,DIGL,DIGU,SAM,FM,NFM,DFM,RTTY 82 | fm_tone_mode=OFF 83 | fm_tone_value=67.0 84 | fm_repeater_offset_freq=0.000000 85 | tx_offset_freq=0.000000 86 | repeater_offset_dir=SIMPLEX 87 | fm_tone_burst=0 88 | fm_deviation=5000 89 | dfm_pre_de_emphasis=0 90 | post_demod_low=300 91 | post_demod_high=3300 92 | rtty_mark=2125 93 | rtty_shift=170 94 | digl_offset=2210 95 | digu_offset=1500 96 | post_demod_bypass=0 97 | rfgain=0 98 | tx_ant_list=ANT1,ANT2,XVTR 99 | */ 100 | -------------------------------------------------------------------------------- /pcap_test.go.archive: -------------------------------------------------------------------------------- 1 | /* 2017 by Frank Werner-hb9fxq / HB9FXQ, mail@hb9fxq.ch 2 | Permission is hereby granted, free of charge, to any person obtaining a copy 3 | of this software and associated documentation files (the "Software"), to deal 4 | in the Software without restriction, including without limitation the rights 5 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 6 | copies of the Software, and to permit persons to whom the Software is 7 | furnished to do so, subject to the following conditions: 8 | The above copyright notice and this permission notice shall be included in 9 | all copies or substantial portions of the Software. 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 11 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 12 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 13 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 14 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 15 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 16 | THE SOFTWARE. 17 | */ 18 | package main 19 | 20 | import ( 21 | "fmt" 22 | "github.com/google/gopacket" 23 | "github.com/google/gopacket/pcap" 24 | "github.com/hb9fxq/flexlib-go/sdrobjects" 25 | "github.com/hb9fxq/flexlib-go/vita" 26 | "github.com/lucasb-eyer/go-colorful" 27 | "gopkg.in/hraban/opus.v2" 28 | "image" 29 | "image/draw" 30 | "image/png" 31 | "os" 32 | "testing" 33 | ) 34 | 35 | type GradientTable []struct { 36 | Col colorful.Color 37 | Pos float64 38 | } 39 | 40 | /* thx https://github.com/lucasb-eyer/go-colorful/blob/master/doc/gradientgen/gradientgen.go*/ 41 | func (self GradientTable) GetInterpolatedColorFor(t float64) colorful.Color { 42 | for i := 0; i < len(self)-1; i++ { 43 | c1 := self[i] 44 | c2 := self[i+1] 45 | if c1.Pos <= t && t <= c2.Pos { 46 | t := (t - c1.Pos) / (c2.Pos - c1.Pos) 47 | return c1.Col.BlendHcl(c2.Col, t).Clamped() 48 | } 49 | } 50 | 51 | return self[len(self)-1].Col 52 | } 53 | 54 | func TestParsePcap(t *testing.T) { 55 | 56 | // package counters 57 | _countFFT := 0 58 | _countRXOpus := 0 59 | _countDAX := 0 60 | _countMeter := 0 61 | _countWaterfall := 0 62 | _countUnknown := 0 63 | _countIf := 0 64 | _countDiscovery := 0 65 | 66 | TCP_FRAGMENTATION_SIZE := 1514 67 | 68 | // waterfall render imgWaterfall canvas 69 | var imgWaterfall = image.NewRGBA(image.Rect(0, 0, 2460, 560*3)) 70 | var imgFFT = image.NewRGBA(image.Rect(0, 0, 1630, 900)) 71 | 72 | keypoints := GradientTable{ 73 | {MustParseHex("#000000"), 0.0}, 74 | {MustParseHex("#0000ff"), 0.15}, 75 | {MustParseHex("#00FF00"), 0.30}, 76 | {MustParseHex("#ffff00"), 0.45}, 77 | {MustParseHex("#ff0000"), 0.60}, 78 | {MustParseHex("#800080"), 0.75}, 79 | {MustParseHex("#ffffff"), 1.0}, 80 | } 81 | 82 | // opus stream test output 83 | fOpus, err := os.Create("../../../../test_output/opus_decoded_float_32_LE_24000.raw") 84 | if err != nil { 85 | panic(err) 86 | } 87 | defer fOpus.Close() 88 | 89 | // opus stream test output 90 | fDax, err := os.Create("../../../../test_output/dax_raw_float_32_BE_48000.raw") 91 | if err != nil { 92 | panic(err) 93 | } 94 | defer fDax.Close() 95 | 96 | // pcap input 97 | if handle, err := pcap.OpenOffline("../../../../test_input/flex.pcap"); err != nil { 98 | panic(err) 99 | } else { 100 | 101 | dec, err := opus.NewDecoder(24e3, 2) 102 | 103 | if err != nil { 104 | panic(err) 105 | } 106 | 107 | packetSource := gopacket.NewPacketSource(handle, handle.LinkType()) 108 | 109 | var buff []byte 110 | previous_fragment := false 111 | 112 | for packet := range packetSource.Packets() { 113 | 114 | temp := packet.ApplicationLayer().Payload() 115 | packet.Dump() 116 | 117 | // reassemble fragmented packages 118 | if len(packet.Data()) == TCP_FRAGMENTATION_SIZE { 119 | 120 | offset := 0 121 | 122 | if !previous_fragment { 123 | buff = []byte{} 124 | offset = 8 125 | } 126 | 127 | buff = append(buff, temp[offset:]...) 128 | previous_fragment = true 129 | continue 130 | } 131 | 132 | if previous_fragment { 133 | buff = append(buff, temp...) 134 | previous_fragment = false 135 | 136 | } else { 137 | buff = temp 138 | } 139 | 140 | // parse preamble 141 | err, preamble, payload := vita.ParseVitaPreamble(buff) 142 | 143 | if err != nil || preamble.Class_id == nil { 144 | continue 145 | } 146 | 147 | switch preamble.Header.Pkt_type { 148 | 149 | case vita.ExtDataWithStream: 150 | 151 | switch preamble.Class_id.PacketClassCode { 152 | 153 | case vita.SL_VITA_FFT_CLASS: 154 | fftPacket := vita.ParseVitaFFT(payload, preamble) 155 | if _countFFT%5 == 0 { 156 | renderAppendFFTPacket(fftPacket, imgFFT) 157 | } 158 | fmt.Sprintf("%d", fftPacket.NumBins) 159 | _countFFT++ 160 | break 161 | case vita.SL_VITA_OPUS_CLASS: 162 | // decode opus and output raw PCM 163 | frameSizeMs := 10 // ms 164 | frameSize := 2 * frameSizeMs * 24e3 / 1000 165 | pcm := make([]float32, frameSize) 166 | dec.DecodeFloat32(payload, pcm) 167 | for sample := range pcm { 168 | fOpus.Write(sdrobjects.Float32ToBytes(pcm[sample])) 169 | } 170 | _countRXOpus++ 171 | break 172 | case vita.SL_VITA_IF_NARROW_CLASS: 173 | daxPkg := vita.ParseFData(payload, preamble) 174 | fDax.Write(daxPkg.Data) 175 | _countDAX++ 176 | break 177 | case vita.SL_VITA_METER_CLASS: 178 | vita.ParseVitaMeterPacket(payload, preamble) 179 | _countMeter++ 180 | break 181 | case vita.SL_VITA_DISCOVERY_CLASS: 182 | _countDiscovery++ 183 | break 184 | case vita.SL_VITA_WATERFALL_CLASS: 185 | tile := vita.ParseVitaWaterfall(payload, preamble) 186 | renderAppendWaterfallTile(_countWaterfall*3, tile, imgWaterfall, keypoints) 187 | _countWaterfall++ 188 | break 189 | default: 190 | _countUnknown++ 191 | break 192 | } 193 | 194 | break 195 | 196 | case vita.IFDataWithStream: 197 | switch preamble.Class_id.InformationClassCode { 198 | case vita.SL_VITA_IF_WIDE_CLASS_24kHz: 199 | case vita.SL_VITA_IF_WIDE_CLASS_48kHz: 200 | case vita.SL_VITA_IF_WIDE_CLASS_96kHz: 201 | case vita.SL_VITA_IF_WIDE_CLASS_192kHz: 202 | _countIf++ 203 | } 204 | 205 | break 206 | } 207 | } 208 | 209 | discoveryString := vita.ParseDiscoveryPackage(discoveryPackage, nil) 210 | 211 | fmt.Printf("_countFFT %d\n", _countFFT) 212 | fmt.Printf("_countRXOpus %d\n", _countRXOpus) 213 | fmt.Printf("_countDAX %d\n", _countDAX) 214 | fmt.Printf("_countMeter %d\n", _countMeter) 215 | fmt.Printf("_countWaterfall %d\n", _countWaterfall) 216 | fmt.Printf("_countUnknown %d\n", _countUnknown) 217 | fmt.Printf("_countIf %d\n", _countIf) 218 | fmt.Printf("_countDiscovery %d - %s\n", _countDiscovery, discoveryString) 219 | 220 | f, _ := os.OpenFile("../../test_output/waterfall.png", os.O_WRONLY|os.O_CREATE, 0600) 221 | defer f.Close() 222 | png.Encode(f, imgWaterfall) 223 | 224 | fFFT, _ := os.OpenFile("../../test_output/fft.png", os.O_WRONLY|os.O_CREATE, 0600) 225 | defer fFFT.Close() 226 | png.Encode(fFFT, imgFFT) 227 | 228 | } 229 | } 230 | func renderAppendFFTPacket(packet *sdrobjects.SdrFFTPacket, rgba *image.RGBA) { 231 | for i := 0; i < int(packet.NumBins); i++ { 232 | rgba.Set(i, int(packet.Payload[i]), MustParseHex("#0000FF")) 233 | } 234 | } 235 | func renderAppendWaterfallTile(y int, tile *sdrobjects.SdrWaterfallTile, img *image.RGBA, keypoints GradientTable) { 236 | i := 0 237 | cBlackLevel := keypoints.GetInterpolatedColorFor(0.0) 238 | 239 | for value := range tile.Data { 240 | gain := 1.125 241 | pVal := (float64(tile.Data[value])) 242 | cv := (1.0 / (65535.0)) * (pVal * gain) 243 | c := cBlackLevel 244 | 245 | if (tile.Data[value] - uint16(tile.AutoBlackLevel)) >= 1 { 246 | c = keypoints.GetInterpolatedColorFor(cv) 247 | } 248 | 249 | draw.Draw(img, image.Rect(i, y, i+1, y+3), &image.Uniform{c}, image.ZP, draw.Src) 250 | i++ 251 | } 252 | } 253 | 254 | func MustParseHex(s string) colorful.Color { 255 | c, err := colorful.Hex(s) 256 | if err != nil { 257 | panic("MustParseHex: " + err.Error()) 258 | } 259 | return c 260 | } 261 | 262 | var discoveryPackage = []byte{ 263 | 0x64, 0x69, 0x73, 0x63, 264 | 0x6f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x70, 0x72, 265 | 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x5f, 0x76, 266 | 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x3d, 0x32, 267 | 0x2e, 0x30, 0x2e, 0x30, 0x2e, 0x31, 0x20, 0x6d, 268 | 0x6f, 0x64, 0x65, 0x6c, 0x3d, 0x46, 0x4c, 0x45, 269 | 0x58, 0x2d, 0x36, 0x35, 0x30, 0x30, 0x20, 0x73, 270 | 0x65, 0x72, 0x69, 0x61, 0x6c, 0x3d, 0x34, 0x32, 271 | 0x31, 0x33, 0x2d, 0x33, 0x31, 0x30, 0x35, 0x2d, 272 | 0x36, 0x35, 0x30, 0x30, 0x2d, 0x38, 0x32, 0x39, 273 | 0x36, 0x20, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 274 | 0x6e, 0x3d, 0x32, 0x2e, 0x30, 0x2e, 0x31, 0x39, 275 | 0x2e, 0x39, 0x38, 0x20, 0x6e, 0x69, 0x63, 0x6b, 276 | 0x6e, 0x61, 0x6d, 0x65, 0x3d, 0x53, 0x63, 0x68, 277 | 0x77, 0x61, 0x72, 0x7a, 0x65, 0x6e, 0x62, 0x75, 278 | 0x72, 0x67, 0x20, 0x63, 0x61, 0x6c, 0x6c, 0x73, 279 | 0x69, 0x67, 0x6e, 0x3d, 0x53, 0x43, 0x48, 0x57, 280 | 0x41, 0x52, 0x5a, 0x45, 0x4e, 0x42, 0x55, 0x52, 281 | 0x20, 0x69, 0x70, 0x3d, 0x31, 0x39, 0x32, 0x2e, 282 | 0x31, 0x36, 0x38, 0x2e, 0x39, 0x32, 0x2e, 0x38, 283 | 0x20, 0x70, 0x6f, 0x72, 0x74, 0x3d, 0x34, 0x39, 284 | 0x39, 0x32, 0x20, 0x73, 0x74, 0x61, 0x74, 0x75, 285 | 0x73, 0x3d, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 286 | 0x62, 0x6c, 0x65, 0x20, 0x69, 0x6e, 0x75, 0x73, 287 | 0x65, 0x5f, 0x69, 0x70, 0x3d, 0x20, 0x69, 0x6e, 288 | 0x75, 0x73, 0x65, 0x5f, 0x68, 0x6f, 0x73, 0x74, 289 | 0x3d, 0x20, 0x6d, 0x61, 0x78, 0x5f, 0x6c, 0x69, 290 | 0x63, 0x65, 0x6e, 0x73, 0x65, 0x64, 0x5f, 0x76, 291 | 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x3d, 0x76, 292 | 0x32, 0x20, 0x72, 0x61, 0x64, 0x69, 0x6f, 0x5f, 293 | 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x5f, 294 | 0x69, 0x64, 0x3d, 0x30, 0x30, 0x2d, 0x31, 0x43, 295 | 0x2d, 0x32, 0x44, 0x2d, 0x30, 0x32, 0x2d, 0x30, 296 | 0x32, 0x2d, 0x39, 0x30, 0x20, 0x72, 0x65, 0x71, 297 | 0x75, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x61, 0x64, 298 | 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 299 | 0x5f, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 300 | 0x3d, 0x30, 0x00, 0x00} 301 | -------------------------------------------------------------------------------- /radio_integration_test.go.archive: -------------------------------------------------------------------------------- 1 | /* 2017 by Frank Werner-hb9fxq / HB9FXQ, mail@hb9fxq.ch 2 | Permission is hereby granted, free of charge, to any person obtaining a copy 3 | of this software and associated documentation files (the "Software"), to deal 4 | in the Software without restriction, including without limitation the rights 5 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 6 | copies of the Software, and to permit persons to whom the Software is 7 | furnished to do so, subject to the following conditions: 8 | The above copyright notice and this permission notice shall be included in 9 | all copies or substantial portions of the Software. 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 11 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 12 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 13 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 14 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 15 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 16 | THE SOFTWARE. 17 | */ 18 | package main 19 | 20 | import ( 21 | "fmt" 22 | "github.com/hb9fxq/flexlib-go/obj" 23 | "github.com/hb9fxq/flexlib-go/sdrobjects" 24 | "net" 25 | "strconv" 26 | "testing" 27 | "time" 28 | ) 29 | 30 | func TestRadioInitIntegration(t *testing.T) { 31 | 32 | ctx := new(obj.RadioContext) 33 | ctx.RadioAddr = "192.168.92.8" 34 | ctx.MyUdpEndpointPort = "4700" 35 | ctx.ChannelRadioResponse = make(chan string) 36 | 37 | go obj.InitRadioContext(ctx) 38 | 39 | go func(ctx *obj.RadioContext) { 40 | for { 41 | fmt.Println(">" + <-ctx.ChannelRadioResponse + "<") 42 | } 43 | }(ctx) 44 | 45 | for { 46 | if len(ctx.RadioHandle) > 0 { 47 | break 48 | } 49 | time.Sleep(500) 50 | } 51 | 52 | forever := make(chan bool) 53 | forever <- true 54 | } 55 | 56 | func TestRadioSubIqInitIntegration(t *testing.T) { 57 | ctx := new(obj.RadioContext) 58 | ctx.RadioAddr = "192.168.92.8" 59 | ctx.MyUdpEndpointPort = "4700" 60 | ctx.ChannelRadioResponse = make(chan string) 61 | ctx.ChannelVitaIfData = make(chan *sdrobjects.SdrIfData) 62 | 63 | go obj.InitRadioContext(ctx) 64 | 65 | go func(ctx *obj.RadioContext) { 66 | for { 67 | fmt.Println(">" + <-ctx.ChannelRadioResponse + "<") 68 | } 69 | }(ctx) 70 | 71 | ServerAddr, _ := net.ResolveUDPAddr("udp", "192.168.178.75:1234") 72 | LocalAddr, _ := net.ResolveUDPAddr("udp", "192.168.178.75:0") 73 | 74 | Conn, _ := net.DialUDP("udp", LocalAddr, ServerAddr) 75 | 76 | go func(ctx *obj.RadioContext) { 77 | 78 | cnt := 0 79 | 80 | for { 81 | dat := <-ctx.ChannelVitaIfData 82 | Conn.Write(dat.Data) 83 | cnt++ 84 | if cnt%500 == 0 { 85 | fmt.Println("VitaIfDataPacket count: " + strconv.Itoa(cnt)) 86 | } 87 | } 88 | }(ctx) 89 | 90 | for { 91 | if len(ctx.RadioHandle) > 0 { 92 | break 93 | } 94 | time.Sleep(500) 95 | } 96 | 97 | obj.SendRadioCommand(ctx, "stream create daxiq=1ip="+ctx.MyUdpEndpointIP.String()+" port="+ctx.MyUdpEndpointPort) 98 | 99 | forever := make(chan bool) 100 | forever <- true 101 | } 102 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # flexlib-go 2 | Multi platform tools to interact with flexradio 6k series radio. 3 | 4 | Currently, any tools, except the MQTT adapter require an instance of SmartSDR Windows/OSX/IOS to be running. DAX and DAX IQ Data is currently not "headless". 5 | 6 | ## Installation 7 | Option A) Binary Download 8 | * Download the latest binary release from https://github.com/hb9fxq/flexlib-go/releases 9 | 10 | Option B) Install from source 11 | 12 | * Install GO https://golang.org/doc/install 13 | * Install CMDs 14 |
 15 | go get -u github.com/hb9fxq/flexlib-go/cmd/smartsdr-iqtransfer
 16 | go get -u github.com/hb9fxq/flexlib-go/cmd/smartsdr-mqttadapter
 17 | go get -u github.com/hb9fxq/flexlib-go/cmd/smartsdr-daxclient
 18 | 
19 | 20 | ## Tools 21 | 22 | ### Binary "smartsdr-iqtransfer" 23 | Tool to transfer DAX IQ data from a FRS 6K Radio to any platform. 24 | 25 | When you run smartsdr-iqtransfer, make sure, that you select the matching DAX IQ channel in Smartsdr. 26 | 27 | fCenter is the center of the GUI Panadapter, it can be set via argument '--FCENTER' or changed while running by typing to stdin. 28 | 29 | _Options:_ 30 | * **RADIO** IP address of the radio 31 | * **MYUDP** UDP port on local machine that the radio will send VITA49 traffic. Must be a free port on your machine. Check your firewall! 32 | * **CH** DAX-IQ channel to stream. 33 | * **FWD** (Optional) Endpoint to send the Float32 IQ data to. If not supplied, the data is written to stdout and can be used for piping. You can find a sample for GNU Radio under https://github.com/hb9fxq/flexlib-go/tree/master/GRC/iq-transfer 34 | * **RATE** SampleRate in kHz, Possible Values: 24000 48000 96000 192000 35 | * **FCENTER** (Optional) Initially tune panadapter to fCenter in MHz, e.g. 7.1 36 | 37 | 38 | __e.g.__ 39 | 40 | Send raw IQ data 127.0.0.1:2345
./smartsdr-iqtransfer --RADIO=192.168.92.8 --MYUDP=5999 --RATE=192000 --CH=1 --FCENTER=7.1 --FWD=127.0.0.1:2345
41 | 42 | record IQ Data to a file 43 |
./smartsdr-iqtransfer  --RADIO=192.168.92.8 --MYUDP=5999 --RATE=192000 --CH=1 --FCENTER=7.1 --FWD=127.0.0.1:2345 > "$(date +"%FT%T").raw"
44 | 45 | ![alt text](https://github.com/hb9fxq/flexlib-go/raw/master/assets/grc_sample.png "FFT with GRC using iq-transfer util") 46 | 47 | ### Binary "smartsdr-daxclient" 48 | 49 | Receives RAW DAX audio streams (RX Channels 1-6) 50 | 51 | _Options:_ 52 | * **RADIO** IP address of the radio 53 | * **MYUDP** UDP port on local machine that the radio will send VITA49 traffic. Must be a free port on your machine. Check your firewall! 54 | * **CH** DAX audio channel to stream. 55 | * **FWD** Endpoint to send the Float32 IQ data to. If not supplied, the data is written to stdout and can be used for piping. You can find a sample for GNU Radio under https://github.com/hb9fxq/flexlib-go/tree/master/GRC/iq-transfer 56 | 57 | e.g. 58 | Forward raw DAX audio stream from channel 1 to a computer on the network (FWD) 59 |
./smartsdr-daxclient --RADIO=192.168.92.8 --MYUDP=5999 --CH=1 --FWD=127.0.0.1:2345
60 | 61 | LiveStream via ffmpeg with libopus transcoding 62 |
ffmpeg -flags low_delay -fflags nobuffer -f f32be -ar 24000 -ac 2 -i udp://127.0.0.1:2345 -acodec libopus -ar 24000 -f rtp rtp://127.0.0.1:1234 -sdp_file daxslice.sdp
63 | 64 | 65 | Play RAW audio to the speaker. 2 Channels, 32 Bit float, big endian 66 |
socat -u udp-recv:2345 - | play -q -t f32 -r 24k --endian big -c 2 -
67 | 68 | 69 | 70 | ![alt text](https://github.com/hb9fxq/flexlib-go/raw/master/assets/wsjtx_use_case.png "Pulling DAX Audio to WSJX-T on Ubuntu") 71 | 72 | ### Binary "smartsdr-mqttadapter" 73 | 74 | Tool to reflect most important radio status, like Slices, Panadapters and connected clients to a MQTT broker. Useful for status monitoring, dashboards or advanced radio integration. 75 | 76 |
./smartsdr-mqttadapter --RADIO=192.168.92.8 --MQTTBROKER=tcp://192.168.92.7:1883 --MQTTCLIENTID=flexdev --MQTTTOPIC=flexdev
77 | 78 | 79 | ![alt text](https://github.com/hb9fxq/flexlib-go/raw/master/assets/mqtt_sample.png "DAX IQ setting in SmartSDR") 80 | 81 | ## Some experiments 82 | The library is currently able to parse most of the VITA 49 types, that the FRS is using... 83 | 84 | SoapySDR Module draft version based on flexlib-go with CGO bindings: 85 | [![IMAGE ALT TEXT HERE](https://img.youtube.com/vi/YW4s5wfcFa0/0.jpg)](https://www.youtube.com/watch?v=YW4s5wfcFa0) 86 | 87 | 88 | Reconstructed waterfall tile data from pcap: 89 | 90 | ![alt text](https://raw.githubusercontent.com/hb9fxq/flexlib-go/master/assets/test_output/waterfall.png "waterfall from pcap") 91 | 92 | Reconstructed opus audio from pcap: 93 | 94 | https://soundcloud.com/frank-werner-hb9fxq-14069568/opus-decoded 95 | 96 | Reconstructed FFT Plot (all captured fft points aggregated) 97 | 98 | ![alt text](https://github.com/hb9fxq/flexlib-go/raw/master/assets/test_output/fft.png "fft from pcap") 99 | 100 | 101 | -------------------------------------------------------------------------------- /sdrobjects/sdrobjects.go: -------------------------------------------------------------------------------- 1 | /* 2017 by Frank Werner-hb9fxq / HB9FXQ, mail@hb9fxq.ch 2 | Permission is hereby granted, free of charge, to any person obtaining a copy 3 | of this software and associated documentation files (the "Software"), to deal 4 | in the Software without restriction, including without limitation the rights 5 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 6 | copies of the Software, and to permit persons to whom the Software is 7 | furnished to do so, subject to the following conditions: 8 | The above copyright notice and this permission notice shall be included in 9 | all copies or substantial portions of the Software. 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 11 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 12 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 13 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 14 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 15 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 16 | THE SOFTWARE. 17 | */ 18 | package sdrobjects 19 | 20 | type VitaPacketType int 21 | 22 | type SdrWaterfallTile struct { 23 | FrameLowFreq uint64 24 | BinBandwidth uint64 25 | MysteryValue uint16 26 | LineDurationMS uint16 27 | Width uint16 28 | Height uint16 29 | Timecode uint32 30 | AutoBlackLevel uint32 31 | TotalBinsInFrame uint16 32 | FirstBinIndex uint16 33 | Data []uint16 34 | } 35 | 36 | type SdrFFTPacket struct { 37 | StartBin_index uint16 38 | NumBins uint16 39 | BinSize uint16 40 | FrameIndex uint32 41 | TotalBinsInFrame uint16 42 | Payload []uint16 43 | } 44 | 45 | type SdrMeterPacket struct { 46 | Ids []uint16 47 | Vals []int16 48 | } 49 | 50 | type SdrIfData struct { 51 | Stream_id uint32 52 | Data []byte 53 | } 54 | -------------------------------------------------------------------------------- /sdrobjects/util.go: -------------------------------------------------------------------------------- 1 | /* 2017 by Frank Werner-hb9fxq / HB9FXQ, mail@hb9fxq.ch 2 | Permission is hereby granted, free of charge, to any person obtaining a copy 3 | of this software and associated documentation files (the "Software"), to deal 4 | in the Software without restriction, including without limitation the rights 5 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 6 | copies of the Software, and to permit persons to whom the Software is 7 | furnished to do so, subject to the following conditions: 8 | The above copyright notice and this permission notice shall be included in 9 | all copies or substantial portions of the Software. 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 11 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 12 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 13 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 14 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 15 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 16 | THE SOFTWARE. 17 | */ 18 | package sdrobjects 19 | 20 | import ( 21 | "encoding/binary" 22 | "math" 23 | ) 24 | 25 | func Float32ToBytes(float float32) []byte { 26 | bits := math.Float32bits(float) 27 | bytes := make([]byte, 4) 28 | binary.LittleEndian.PutUint32(bytes, bits) 29 | return bytes 30 | } 31 | -------------------------------------------------------------------------------- /vita/vitahandler.go: -------------------------------------------------------------------------------- 1 | /* 2017 by Frank Werner-hb9fxq / HB9FXQ, mail@hb9fxq.ch 2 | Permission is hereby granted, free of charge, to any person obtaining a copy 3 | of this software and associated documentation files (the "Software"), to deal 4 | in the Software without restriction, including without limitation the rights 5 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 6 | copies of the Software, and to permit persons to whom the Software is 7 | furnished to do so, subject to the following conditions: 8 | The above copyright notice and this permission notice shall be included in 9 | all copies or substantial portions of the Software. 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 11 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 12 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 13 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 14 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 15 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 16 | THE SOFTWARE. 17 | */ 18 | package vita 19 | 20 | import ( 21 | "bytes" 22 | "encoding/binary" 23 | "errors" 24 | "github.com/hb9fxq/flexlib-go/sdrobjects" 25 | "math" 26 | ) 27 | 28 | var ONE_OVER_ZERO_DBFS = 1.0 / math.Pow(2, 15) 29 | 30 | func ParseVitaPreamble(data []byte) (error, *VitaPacketPreamble, []byte) { 31 | 32 | if len(data) < 20 { 33 | return errors.New("not a VITA49 Package"), nil, nil 34 | } 35 | 36 | var vitaPacketPreamble VitaPacketPreamble 37 | 38 | var header VitaHeader 39 | var vitaClassId VitaClassID 40 | 41 | vitaPacketPreamble.Header = &header 42 | 43 | index := 0 44 | rHeader := binary.BigEndian.Uint32(data[0:4]) 45 | index += 4 46 | header.Pkt_type = VitaPacketType((rHeader >> 28)) 47 | header.C = ((rHeader & 0x08000000) != 0) 48 | header.T = ((rHeader & 0x04000000) != 0) 49 | header.Tsi = VitaTimeStampIntegerType(((rHeader >> 22) & 0x03)) 50 | header.Tsf = VitaTimeStampFractionalType(((rHeader >> 20) & 0x03)) 51 | header.Packet_count = uint16(((rHeader >> 16) & 0x0F)) 52 | header.Packet_size = uint16(rHeader & 0xFFFF) 53 | 54 | if header.Pkt_type == IFDataWithStream || header.Pkt_type == ExtDataWithStream { 55 | vitaPacketPreamble.Stream_id = binary.BigEndian.Uint32(data[index : index+4]) 56 | index += 4 57 | } 58 | 59 | if header.C { 60 | vitaPacketPreamble.Class_id = &vitaClassId 61 | temp := binary.BigEndian.Uint32(data[index : index+4]) 62 | index += 4 63 | vitaClassId.OUI = temp & 0x00FFFFFF 64 | 65 | temp = binary.BigEndian.Uint32(data[index : index+4]) 66 | index += 4 67 | vitaClassId.InformationClassCode = uint16((temp >> 16)) 68 | vitaClassId.PacketClassCode = uint16(temp) 69 | } 70 | 71 | if header.Tsi != NoneTsi { 72 | vitaPacketPreamble.Timestamp_int = binary.BigEndian.Uint32(data[index : index+4]) 73 | index += 4 74 | } 75 | 76 | if header.Tsf != NoneTsf { 77 | vitaPacketPreamble.Timestamp_frac = binary.BigEndian.Uint64(data[index : index+8]) 78 | index += 8 79 | } 80 | 81 | if header.T { 82 | //index += 4 83 | header.Payload_cutoff_bytes = 4 84 | } 85 | 86 | return nil, &vitaPacketPreamble, data[index:] 87 | } 88 | 89 | func ParseVitaFFT(data []byte, preamble *VitaPacketPreamble) *sdrobjects.SdrFFTPacket { 90 | 91 | index := 0 92 | var fftPacket sdrobjects.SdrFFTPacket 93 | 94 | fftPacket.StartBin_index = binary.BigEndian.Uint16(data[index : index+2]) 95 | index += 2 96 | 97 | fftPacket.NumBins = binary.BigEndian.Uint16(data[index : index+2]) 98 | index += 2 99 | 100 | fftPacket.BinSize = binary.BigEndian.Uint16(data[index : index+2]) 101 | index += 2 102 | 103 | fftPacket.TotalBinsInFrame = binary.BigEndian.Uint16(data[index : index+2]) 104 | index += 2 105 | 106 | fftPacket.FrameIndex = binary.BigEndian.Uint32(data[index : index+4]) 107 | index += 4 108 | 109 | for i := 0; i < int(fftPacket.NumBins)*2; i += 2 { 110 | fftPacket.Payload = append(fftPacket.Payload, binary.BigEndian.Uint16(data[i+index:i+index+2])) 111 | } 112 | 113 | return &fftPacket 114 | } 115 | 116 | func ParseVitaMeterPacket(data []byte, preamble *VitaPacketPreamble) *sdrobjects.SdrMeterPacket { 117 | index := 0 118 | var meterPacket sdrobjects.SdrMeterPacket 119 | 120 | numberOfMeters := (len(data) - preamble.Header.Payload_cutoff_bytes) / 4 121 | 122 | for i := 0; i < numberOfMeters; i++ { 123 | 124 | meterPacket.Ids = append(meterPacket.Ids, binary.BigEndian.Uint16(data[index:index+2])) 125 | index += 2 126 | buf := bytes.NewBuffer(data[index : index+2]) 127 | var valueRes int16 128 | binary.Read(buf, binary.BigEndian, &valueRes) 129 | index += 2 130 | meterPacket.Vals = append(meterPacket.Vals, valueRes) 131 | } 132 | 133 | return &meterPacket 134 | 135 | } 136 | 137 | func ParseVitaWaterfall(data []byte, preamble *VitaPacketPreamble) *sdrobjects.SdrWaterfallTile { 138 | index := 0 139 | var wftile sdrobjects.SdrWaterfallTile 140 | 141 | wftile.FrameLowFreq = binary.BigEndian.Uint64(data[index:8]) >> 20 142 | index += 8 143 | 144 | wftile.BinBandwidth = binary.BigEndian.Uint64(data[index:index+8]) >> 20 145 | index += 8 146 | 147 | wftile.MysteryValue = binary.BigEndian.Uint16(data[index : index+2]) 148 | index += 2 149 | 150 | wftile.LineDurationMS = binary.BigEndian.Uint16(data[index : index+2]) 151 | index += 2 152 | 153 | wftile.Width = binary.BigEndian.Uint16(data[index : index+2]) 154 | index += 2 155 | 156 | wftile.Height = binary.BigEndian.Uint16(data[index : index+2]) 157 | index += 2 158 | 159 | wftile.Timecode = binary.BigEndian.Uint32(data[index : index+4]) 160 | index += 4 161 | 162 | wftile.AutoBlackLevel = binary.BigEndian.Uint32(data[index : index+4]) 163 | index += 4 164 | 165 | wftile.TotalBinsInFrame = binary.BigEndian.Uint16(data[index : index+2]) 166 | index += 2 167 | 168 | wftile.FirstBinIndex = binary.BigEndian.Uint16(data[index : index+2]) 169 | index += 2 170 | 171 | for i := 0; i < (len(data))-preamble.Header.Payload_cutoff_bytes-index-4; /* -4 should not be.... another mytery*/ i += 2 { 172 | wftile.Data = append(wftile.Data, binary.BigEndian.Uint16(data[i+index:i+index+2])) 173 | } 174 | 175 | return &wftile 176 | } 177 | 178 | func ParseVitaOpus(data []byte, preamble *VitaPacketPreamble) []byte { 179 | return data[:len(data)-preamble.Header.Payload_cutoff_bytes] 180 | } 181 | 182 | func ParseFData(data []byte, preamble *VitaPacketPreamble) *sdrobjects.SdrIfData { 183 | 184 | payload := data[:len(data)-preamble.Header.Payload_cutoff_bytes] 185 | 186 | var res sdrobjects.SdrIfData 187 | res.Stream_id = preamble.Stream_id 188 | 189 | switch preamble.Class_id.PacketClassCode { // dax audio 190 | case SL_VITA_IF_NARROW_CLASS: 191 | res.Data = payload 192 | return &res 193 | } 194 | 195 | for i := 0; i <= (len(payload)-4)/4; i++ { 196 | fVal := getFloat32fromLE(data[i*4:(i*4)+4]) * float32(ONE_OVER_ZERO_DBFS) 197 | res.Data = append(res.Data, getBytesFromFloat(fVal)...) 198 | } 199 | return &res 200 | } 201 | 202 | func ParseDiscoveryPackage(data []byte, preamble *VitaPacketPreamble) string { 203 | return string(data[:len(data)-4]) 204 | } 205 | 206 | func getFloat32fromLE(bytes []byte) float32 { 207 | return math.Float32frombits(binary.LittleEndian.Uint32(bytes)) 208 | } 209 | 210 | func getBytesFromFloat(float float32) []byte { 211 | bits := math.Float32bits(float) 212 | bytes := make([]byte, 4) 213 | binary.LittleEndian.PutUint32(bytes, bits) 214 | return bytes 215 | } 216 | -------------------------------------------------------------------------------- /vita/vitatypes.go: -------------------------------------------------------------------------------- 1 | /* 2017 by Frank Werner-hb9fxq / HB9FXQ, mail@hb9fxq.ch 2 | Permission is hereby granted, free of charge, to any person obtaining a copy 3 | of this software and associated documentation files (the "Software"), to deal 4 | in the Software without restriction, including without limitation the rights 5 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 6 | copies of the Software, and to permit persons to whom the Software is 7 | furnished to do so, subject to the following conditions: 8 | The above copyright notice and this permission notice shall be included in 9 | all copies or substantial portions of the Software. 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 11 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 12 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 13 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 14 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 15 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 16 | THE SOFTWARE. 17 | */ 18 | package vita 19 | 20 | type VitaPacketType int 21 | 22 | const ( 23 | IFData VitaPacketType = iota 24 | IFDataWithStream VitaPacketType = iota 25 | ExtData VitaPacketType = iota 26 | ExtDataWithStream VitaPacketType = iota 27 | IFContext VitaPacketType = iota 28 | ExtContext VitaPacketType = iota 29 | ) 30 | 31 | type VitaTimeStampIntegerType uint 32 | 33 | const ( 34 | NoneTsi VitaTimeStampIntegerType = iota 35 | UTC VitaTimeStampIntegerType = iota 36 | GPS VitaTimeStampIntegerType = iota 37 | Other VitaTimeStampIntegerType = iota 38 | ) 39 | 40 | type VitaTimeStampFractionalType uint 41 | 42 | const ( 43 | NoneTsf VitaTimeStampFractionalType = iota 44 | SampleCount VitaTimeStampFractionalType = iota 45 | RealTime VitaTimeStampFractionalType = iota 46 | FreeRunning VitaTimeStampFractionalType = iota 47 | ) 48 | 49 | type VitaClassID struct { 50 | OUI uint32 51 | InformationClassCode uint16 52 | PacketClassCode uint16 53 | } 54 | 55 | type VitaTrailer struct { 56 | CalibratedTimeEnable bool 57 | ValidDataEnable bool 58 | ReferenceLockEnable bool 59 | AGCMGCEnable bool 60 | DetectedSignalEnable bool 61 | SpectralInversionEnable bool 62 | OverrangeEnable bool 63 | SampleLossEnable bool 64 | CalibratedTimeIndicator bool 65 | ValidDataIndicator bool 66 | ReferenceLockIndicator bool 67 | AGCMGCIndicator bool 68 | DetectedSignalIndicator bool 69 | SpectralInversionIndicator bool 70 | OverrangeIndicator bool 71 | SampleLossIndicator bool 72 | } 73 | 74 | type VitaHeader struct { 75 | Pkt_type VitaPacketType 76 | C bool 77 | T bool 78 | Tsi VitaTimeStampIntegerType 79 | Tsf VitaTimeStampFractionalType 80 | Packet_count uint16 81 | Packet_size uint16 82 | Payload_cutoff_bytes int 83 | } 84 | 85 | type VitaPacketPreamble struct { 86 | Header *VitaHeader 87 | Stream_id uint32 88 | Class_id *VitaClassID 89 | Timestamp_int uint32 90 | Timestamp_frac uint64 91 | } 92 | 93 | type VitaIfData struct { 94 | Header *VitaHeader 95 | Stream_id uint32 96 | Class_id_h uint32 97 | Class_id_l uint32 98 | Timestamp_int uint32 99 | Timestamp_frac uint64 100 | Payload []float32 101 | } 102 | 103 | const ( 104 | SL_VITA_DISCOVERY_CLASS = uint16(0xFFFF) 105 | SL_VITA_METER_CLASS = uint16(0x8002) 106 | SL_VITA_FFT_CLASS = uint16(0x8003) 107 | SL_VITA_WATERFALL_CLASS = uint16(0x8004) 108 | SL_VITA_OPUS_CLASS = uint16(0x8005) 109 | SL_VITA_IF_NARROW_CLASS = uint16(0x03E3) 110 | SL_VITA_IF_WIDE_CLASS_24kHz = uint16(0x02E3) 111 | SL_VITA_IF_WIDE_CLASS_48kHz = uint16(0x02E4) 112 | SL_VITA_IF_WIDE_CLASS_96kHz = uint16(0x02E5) 113 | SL_VITA_IF_WIDE_CLASS_192kHz = uint16(0x02E6) 114 | MAX_VITA_PACKET_SIZE = uint16(16384) 115 | FLEX_OUI = uint16(0x1C2D) 116 | ) 117 | --------------------------------------------------------------------------------