diff --git a/python-api/lock_in_amplifier_fm_radio/README.md b/python-api/lock_in_amplifier_fm_radio/README.md new file mode 100644 index 00000000..cef0509c --- /dev/null +++ b/python-api/lock_in_amplifier_fm_radio/README.md @@ -0,0 +1,10 @@ +# FM Radio with Moku and GNU Radio + +This example contains control code for streaming IQ samples from Moku Lock-in Amplifier. It also includes necessary GNU Radio files to implement a simple FM Radio Receiver. + +Python script for Moku streaming has standard dependancies similar to other examples. To run GNU Radio side of this example, the user must first install [GNU Radio](https://wiki.gnuradio.org/index.php/InstallingGR) and [DIFI OOT module](https://github.com/DIFI-Consortium/gr-difi) respectively. + +After GNU Radio setup is complete, the user must run the GNU Radio flowgraph to listen to the specified radio channel, while streaming script is responsible for sending IQ samples exracted from Moku hardware as DIFI packets. + + + diff --git a/python-api/lock_in_amplifier_fm_radio/gnu_radio/FM_Receiver.grc b/python-api/lock_in_amplifier_fm_radio/gnu_radio/FM_Receiver.grc new file mode 100644 index 00000000..0c248c35 --- /dev/null +++ b/python-api/lock_in_amplifier_fm_radio/gnu_radio/FM_Receiver.grc @@ -0,0 +1,463 @@ +options: + parameters: + author: Upul Bulathsinhala + catch_exceptions: 'True' + category: '[GRC Hier Blocks]' + cmake_opt: '' + comment: '' + copyright: Liquid Instruments + description: '' + gen_cmake: 'On' + gen_linking: dynamic + generate_options: qt_gui + hier_block_src_path: '.:' + id: fm_receiver + max_nouts: '0' + output_language: python + placement: (0,0) + qt_qss_theme: '' + realtime_scheduling: '' + run: 'True' + run_command: '{python} -u {filename}' + run_options: prompt + sizing_mode: fixed + thread_safe_setters: '' + title: FM Receiver + window_size: (1000,1000) + states: + bus_sink: false + bus_source: false + bus_structure: null + coordinate: [8, 8] + rotation: 0 + state: enabled + +blocks: +- name: center_freq + id: variable + parameters: + comment: '' + value: 105.9e6 + states: + bus_sink: false + bus_source: false + bus_structure: null + coordinate: [376, 16.0] + rotation: 0 + state: enabled +- name: samp_rate + id: variable + parameters: + comment: '' + value: 1.25e5 + states: + bus_sink: false + bus_source: false + bus_structure: null + coordinate: [184, 12] + rotation: 0 + state: enabled +- name: volume + id: variable_qtgui_range + parameters: + comment: '' + gui_hint: '' + label: Volume + min_len: '200' + orient: QtCore.Qt.Horizontal + rangeType: float + start: '0' + step: '0.1' + stop: '4' + value: '1' + widget: counter_slider + states: + bus_sink: false + bus_source: false + bus_structure: null + coordinate: [272, 8.0] + rotation: 0 + state: enabled +- name: analog_wfm_rcv_0 + id: analog_wfm_rcv + parameters: + affinity: '' + alias: '' + audio_decimation: '2' + comment: '' + maxoutbuf: '0' + minoutbuf: '0' + quad_rate: 96e3 + states: + bus_sink: false + bus_source: false + bus_structure: null + coordinate: [696, 576.0] + rotation: 0 + state: enabled +- name: audio_sink_0 + id: audio_sink + parameters: + affinity: '' + alias: '' + comment: '' + device_name: '' + num_inputs: '1' + ok_to_block: 'True' + samp_rate: '48000' + states: + bus_sink: false + bus_source: false + bus_structure: null + coordinate: [1152, 584.0] + rotation: 0 + state: enabled +- name: blocks_message_debug_0 + id: blocks_message_debug + parameters: + affinity: '' + alias: '' + comment: '' + en_uvec: 'True' + log_level: info + states: + bus_sink: false + bus_source: false + bus_structure: null + coordinate: [832, 152.0] + rotation: 0 + state: enabled +- name: blocks_multiply_const_vxx_0 + id: blocks_multiply_const_vxx + parameters: + affinity: '' + alias: '' + comment: '' + const: volume + maxoutbuf: '0' + minoutbuf: '0' + type: float + vlen: '1' + states: + bus_sink: false + bus_source: false + bus_structure: null + coordinate: [936, 584.0] + rotation: 0 + state: enabled +- name: blocks_probe_rate_0 + id: blocks_probe_rate + parameters: + affinity: '' + alias: '' + alpha: '0.15' + comment: '' + maxoutbuf: '0' + minoutbuf: '0' + mintime: '500.0' + name: '' + type: complex + vlen: '1' + states: + bus_sink: false + bus_source: false + bus_structure: null + coordinate: [536, 192.0] + rotation: 0 + state: enabled +- name: difi_source_cpp_0 + id: difi_source_cpp + parameters: + affinity: '' + alias: '' + bit_depth: '16' + comment: '' + context_behavior: '0' + ip_addr: 127.0.0.1 + maxoutbuf: '0' + minoutbuf: '0' + port: '1234' + protocol: '2' + stream_num: '0' + type: complex + states: + bus_sink: false + bus_source: false + bus_structure: null + coordinate: [96, 312.0] + rotation: 0 + state: enabled +- name: qtgui_freq_sink_x_0 + id: qtgui_freq_sink_x + parameters: + affinity: '' + alias: '' + alpha1: '1.0' + alpha10: '1.0' + alpha2: '1.0' + alpha3: '1.0' + alpha4: '1.0' + alpha5: '1.0' + alpha6: '1.0' + alpha7: '1.0' + alpha8: '1.0' + alpha9: '1.0' + autoscale: 'True' + average: '1.0' + axislabels: 'True' + bw: samp_rate + color1: '"blue"' + color10: '"dark blue"' + color2: '"red"' + color3: '"green"' + color4: '"black"' + color5: '"cyan"' + color6: '"magenta"' + color7: '"yellow"' + color8: '"dark red"' + color9: '"dark green"' + comment: '' + ctrlpanel: 'False' + fc: center_freq + fftsize: '1024' + freqhalf: 'True' + grid: 'False' + gui_hint: '' + label: Relative Gain + label1: '' + label10: '''''' + label2: '''''' + label3: '''''' + label4: '''''' + label5: '''''' + label6: '''''' + label7: '''''' + label8: '''''' + label9: '''''' + legend: 'True' + maxoutbuf: '0' + minoutbuf: '0' + name: '""' + nconnections: '1' + norm_window: 'False' + showports: 'False' + tr_chan: '0' + tr_level: '0.0' + tr_mode: qtgui.TRIG_MODE_FREE + tr_tag: '""' + type: complex + units: dB + update_time: '0.10' + width1: '1' + width10: '1' + width2: '1' + width3: '1' + width4: '1' + width5: '1' + width6: '1' + width7: '1' + width8: '1' + width9: '1' + wintype: window.WIN_BLACKMAN_hARRIS + ymax: '10' + ymin: '-140' + states: + bus_sink: false + bus_source: false + bus_structure: null + coordinate: [664, 464.0] + rotation: 0 + state: enabled +- name: qtgui_time_sink_x_0 + id: qtgui_time_sink_x + parameters: + affinity: '' + alias: '' + alpha1: '1.0' + alpha10: '1.0' + alpha2: '1.0' + alpha3: '1.0' + alpha4: '1.0' + alpha5: '1.0' + alpha6: '1.0' + alpha7: '1.0' + alpha8: '1.0' + alpha9: '1.0' + autoscale: 'True' + axislabels: 'True' + color1: blue + color10: dark blue + color2: red + color3: green + color4: black + color5: cyan + color6: magenta + color7: yellow + color8: dark red + color9: dark green + comment: '' + ctrlpanel: 'False' + entags: 'True' + grid: 'False' + gui_hint: '' + label1: Signal 1 + label10: Signal 10 + label2: Signal 2 + label3: Signal 3 + label4: Signal 4 + label5: Signal 5 + label6: Signal 6 + label7: Signal 7 + label8: Signal 8 + label9: Signal 9 + legend: 'True' + marker1: '-1' + marker10: '-1' + marker2: '-1' + marker3: '-1' + marker4: '-1' + marker5: '-1' + marker6: '-1' + marker7: '-1' + marker8: '-1' + marker9: '-1' + name: '""' + nconnections: '1' + size: '1024' + srate: samp_rate + stemplot: 'False' + style1: '1' + style10: '1' + style2: '1' + style3: '1' + style4: '1' + style5: '1' + style6: '1' + style7: '1' + style8: '1' + style9: '1' + tr_chan: '0' + tr_delay: '0' + tr_level: '0.0' + tr_mode: qtgui.TRIG_MODE_FREE + tr_slope: qtgui.TRIG_SLOPE_POS + tr_tag: '""' + type: complex + update_time: '0.10' + width1: '1' + width10: '1' + width2: '1' + width3: '1' + width4: '1' + width5: '1' + width6: '1' + width7: '1' + width8: '1' + width9: '1' + ylabel: Amplitude + ymax: '1' + ymin: '-1' + yunit: '""' + states: + bus_sink: false + bus_source: false + bus_structure: null + coordinate: [664, 360.0] + rotation: 0 + state: enabled +- name: qtgui_waterfall_sink_x_0 + id: qtgui_waterfall_sink_x + parameters: + affinity: '' + alias: '' + alpha1: '1.0' + alpha10: '1.0' + alpha2: '1.0' + alpha3: '1.0' + alpha4: '1.0' + alpha5: '1.0' + alpha6: '1.0' + alpha7: '1.0' + alpha8: '1.0' + alpha9: '1.0' + axislabels: 'True' + bw: 48e3 + color1: '0' + color10: '0' + color2: '0' + color3: '0' + color4: '0' + color5: '0' + color6: '0' + color7: '0' + color8: '0' + color9: '0' + comment: '' + fc: center_freq + fftsize: '1024' + freqhalf: 'False' + grid: 'False' + gui_hint: '' + int_max: '10' + int_min: '-140' + label1: '' + label10: '' + label2: '' + label3: '' + label4: '' + label5: '' + label6: '' + label7: '' + label8: '' + label9: '' + legend: 'True' + maxoutbuf: '0' + minoutbuf: '0' + name: '""' + nconnections: '1' + showports: 'False' + type: float + update_time: '0.10' + wintype: window.WIN_HANN + states: + bus_sink: false + bus_source: false + bus_structure: null + coordinate: [928, 440.0] + rotation: 0 + state: enabled +- name: rational_resampler_xxx_0 + id: rational_resampler_xxx + parameters: + affinity: '' + alias: '' + comment: '' + decim: '125' + fbw: '0' + interp: '96' + maxoutbuf: '0' + minoutbuf: '0' + taps: '' + type: ccc + states: + bus_sink: false + bus_source: false + bus_structure: null + coordinate: [472, 560.0] + rotation: 0 + state: enabled + +connections: +- [analog_wfm_rcv_0, '0', blocks_multiply_const_vxx_0, '0'] +- [analog_wfm_rcv_0, '0', qtgui_waterfall_sink_x_0, '0'] +- [blocks_multiply_const_vxx_0, '0', audio_sink_0, '0'] +- [blocks_probe_rate_0, rate, blocks_message_debug_0, print] +- [difi_source_cpp_0, '0', blocks_probe_rate_0, '0'] +- [difi_source_cpp_0, '0', qtgui_freq_sink_x_0, '0'] +- [difi_source_cpp_0, '0', qtgui_time_sink_x_0, '0'] +- [difi_source_cpp_0, '0', rational_resampler_xxx_0, '0'] +- [rational_resampler_xxx_0, '0', analog_wfm_rcv_0, '0'] + +metadata: + file_format: 1 + grc_version: 3.10.9.2 diff --git a/python-api/lock_in_amplifier_fm_radio/gnu_radio/fm_receiver.py b/python-api/lock_in_amplifier_fm_radio/gnu_radio/fm_receiver.py new file mode 100755 index 00000000..c5340c35 --- /dev/null +++ b/python-api/lock_in_amplifier_fm_radio/gnu_radio/fm_receiver.py @@ -0,0 +1,300 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# +# SPDX-License-Identifier: GPL-3.0 +# +# GNU Radio Python Flow Graph +# Title: FM Receiver +# Author: Upul Bulathsinhala +# Copyright: Liquid Instruments +# GNU Radio version: 3.10.9.2 + +from PyQt5 import Qt +from gnuradio import qtgui +from PyQt5 import QtCore +from gnuradio import analog +from gnuradio import audio +from gnuradio import blocks +from gnuradio import blocks, gr +from gnuradio import filter +from gnuradio.filter import firdes +from gnuradio import gr +from gnuradio.fft import window +import sys +import signal +from PyQt5 import Qt +from argparse import ArgumentParser +from gnuradio.eng_arg import eng_float, intx +from gnuradio import eng_notation +import difi +import sip + + + +class fm_receiver(gr.top_block, Qt.QWidget): + + def __init__(self): + gr.top_block.__init__(self, "FM Receiver", catch_exceptions=True) + Qt.QWidget.__init__(self) + self.setWindowTitle("FM Receiver") + qtgui.util.check_set_qss() + try: + self.setWindowIcon(Qt.QIcon.fromTheme('gnuradio-grc')) + except BaseException as exc: + print(f"Qt GUI: Could not set Icon: {str(exc)}", file=sys.stderr) + self.top_scroll_layout = Qt.QVBoxLayout() + self.setLayout(self.top_scroll_layout) + self.top_scroll = Qt.QScrollArea() + self.top_scroll.setFrameStyle(Qt.QFrame.NoFrame) + self.top_scroll_layout.addWidget(self.top_scroll) + self.top_scroll.setWidgetResizable(True) + self.top_widget = Qt.QWidget() + self.top_scroll.setWidget(self.top_widget) + self.top_layout = Qt.QVBoxLayout(self.top_widget) + self.top_grid_layout = Qt.QGridLayout() + self.top_layout.addLayout(self.top_grid_layout) + + self.settings = Qt.QSettings("GNU Radio", "fm_receiver") + + try: + geometry = self.settings.value("geometry") + if geometry: + self.restoreGeometry(geometry) + except BaseException as exc: + print(f"Qt GUI: Could not restore geometry: {str(exc)}", file=sys.stderr) + + ################################################## + # Variables + ################################################## + self.volume = volume = 1 + self.samp_rate = samp_rate = 1.25e5 + self.center_freq = center_freq = 105.9e6 + + ################################################## + # Blocks + ################################################## + + self._volume_range = qtgui.Range(0, 4, 0.1, 1, 200) + self._volume_win = qtgui.RangeWidget(self._volume_range, self.set_volume, "Volume", "counter_slider", float, QtCore.Qt.Horizontal) + self.top_layout.addWidget(self._volume_win) + self.rational_resampler_xxx_0 = filter.rational_resampler_ccc( + interpolation=96, + decimation=125, + taps=[], + fractional_bw=0) + self.qtgui_waterfall_sink_x_0 = qtgui.waterfall_sink_f( + 1024, #size + window.WIN_HANN, #wintype + center_freq, #fc + 48e3, #bw + "", #name + 1, #number of inputs + None # parent + ) + self.qtgui_waterfall_sink_x_0.set_update_time(0.10) + self.qtgui_waterfall_sink_x_0.enable_grid(False) + self.qtgui_waterfall_sink_x_0.enable_axis_labels(True) + + + self.qtgui_waterfall_sink_x_0.set_plot_pos_half(not False) + + labels = ['', '', '', '', '', + '', '', '', '', ''] + colors = [0, 0, 0, 0, 0, + 0, 0, 0, 0, 0] + alphas = [1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0] + + for i in range(1): + if len(labels[i]) == 0: + self.qtgui_waterfall_sink_x_0.set_line_label(i, "Data {0}".format(i)) + else: + self.qtgui_waterfall_sink_x_0.set_line_label(i, labels[i]) + self.qtgui_waterfall_sink_x_0.set_color_map(i, colors[i]) + self.qtgui_waterfall_sink_x_0.set_line_alpha(i, alphas[i]) + + self.qtgui_waterfall_sink_x_0.set_intensity_range(-140, 10) + + self._qtgui_waterfall_sink_x_0_win = sip.wrapinstance(self.qtgui_waterfall_sink_x_0.qwidget(), Qt.QWidget) + + self.top_layout.addWidget(self._qtgui_waterfall_sink_x_0_win) + self.qtgui_time_sink_x_0 = qtgui.time_sink_c( + 1024, #size + samp_rate, #samp_rate + "", #name + 1, #number of inputs + None # parent + ) + self.qtgui_time_sink_x_0.set_update_time(0.10) + self.qtgui_time_sink_x_0.set_y_axis(-1, 1) + + self.qtgui_time_sink_x_0.set_y_label('Amplitude', "") + + self.qtgui_time_sink_x_0.enable_tags(True) + self.qtgui_time_sink_x_0.set_trigger_mode(qtgui.TRIG_MODE_FREE, qtgui.TRIG_SLOPE_POS, 0.0, 0, 0, "") + self.qtgui_time_sink_x_0.enable_autoscale(True) + self.qtgui_time_sink_x_0.enable_grid(False) + self.qtgui_time_sink_x_0.enable_axis_labels(True) + self.qtgui_time_sink_x_0.enable_control_panel(False) + self.qtgui_time_sink_x_0.enable_stem_plot(False) + + + labels = ['Signal 1', 'Signal 2', 'Signal 3', 'Signal 4', 'Signal 5', + 'Signal 6', 'Signal 7', 'Signal 8', 'Signal 9', 'Signal 10'] + widths = [1, 1, 1, 1, 1, + 1, 1, 1, 1, 1] + colors = ['blue', 'red', 'green', 'black', 'cyan', + 'magenta', 'yellow', 'dark red', 'dark green', 'dark blue'] + alphas = [1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0] + styles = [1, 1, 1, 1, 1, + 1, 1, 1, 1, 1] + markers = [-1, -1, -1, -1, -1, + -1, -1, -1, -1, -1] + + + for i in range(2): + if len(labels[i]) == 0: + if (i % 2 == 0): + self.qtgui_time_sink_x_0.set_line_label(i, "Re{{Data {0}}}".format(i/2)) + else: + self.qtgui_time_sink_x_0.set_line_label(i, "Im{{Data {0}}}".format(i/2)) + else: + self.qtgui_time_sink_x_0.set_line_label(i, labels[i]) + self.qtgui_time_sink_x_0.set_line_width(i, widths[i]) + self.qtgui_time_sink_x_0.set_line_color(i, colors[i]) + self.qtgui_time_sink_x_0.set_line_style(i, styles[i]) + self.qtgui_time_sink_x_0.set_line_marker(i, markers[i]) + self.qtgui_time_sink_x_0.set_line_alpha(i, alphas[i]) + + self._qtgui_time_sink_x_0_win = sip.wrapinstance(self.qtgui_time_sink_x_0.qwidget(), Qt.QWidget) + self.top_layout.addWidget(self._qtgui_time_sink_x_0_win) + self.qtgui_freq_sink_x_0 = qtgui.freq_sink_c( + 1024, #size + window.WIN_BLACKMAN_hARRIS, #wintype + center_freq, #fc + samp_rate, #bw + "", #name + 1, + None # parent + ) + self.qtgui_freq_sink_x_0.set_update_time(0.10) + self.qtgui_freq_sink_x_0.set_y_axis((-140), 10) + self.qtgui_freq_sink_x_0.set_y_label('Relative Gain', 'dB') + self.qtgui_freq_sink_x_0.set_trigger_mode(qtgui.TRIG_MODE_FREE, 0.0, 0, "") + self.qtgui_freq_sink_x_0.enable_autoscale(True) + self.qtgui_freq_sink_x_0.enable_grid(False) + self.qtgui_freq_sink_x_0.set_fft_average(1.0) + self.qtgui_freq_sink_x_0.enable_axis_labels(True) + self.qtgui_freq_sink_x_0.enable_control_panel(False) + self.qtgui_freq_sink_x_0.set_fft_window_normalized(False) + + + + labels = ['', '', '', '', '', + '', '', '', '', ''] + widths = [1, 1, 1, 1, 1, + 1, 1, 1, 1, 1] + colors = ["blue", "red", "green", "black", "cyan", + "magenta", "yellow", "dark red", "dark green", "dark blue"] + alphas = [1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0] + + for i in range(1): + if len(labels[i]) == 0: + self.qtgui_freq_sink_x_0.set_line_label(i, "Data {0}".format(i)) + else: + self.qtgui_freq_sink_x_0.set_line_label(i, labels[i]) + self.qtgui_freq_sink_x_0.set_line_width(i, widths[i]) + self.qtgui_freq_sink_x_0.set_line_color(i, colors[i]) + self.qtgui_freq_sink_x_0.set_line_alpha(i, alphas[i]) + + self._qtgui_freq_sink_x_0_win = sip.wrapinstance(self.qtgui_freq_sink_x_0.qwidget(), Qt.QWidget) + self.top_layout.addWidget(self._qtgui_freq_sink_x_0_win) + self.difi_source_cpp_0 = difi.difi_source_cpp_fc32('127.0.0.1', 1234, 2, 0, int(16), int(0)) + self.blocks_probe_rate_0 = blocks.probe_rate(gr.sizeof_gr_complex*1, 500.0, 0.15, '') + self.blocks_multiply_const_vxx_0 = blocks.multiply_const_ff(volume) + self.blocks_message_debug_0 = blocks.message_debug(True, gr.log_levels.info) + self.audio_sink_0 = audio.sink(48000, '', True) + self.analog_wfm_rcv_0 = analog.wfm_rcv( + quad_rate=96e3, + audio_decimation=2, + ) + + + ################################################## + # Connections + ################################################## + self.msg_connect((self.blocks_probe_rate_0, 'rate'), (self.blocks_message_debug_0, 'print')) + self.connect((self.analog_wfm_rcv_0, 0), (self.blocks_multiply_const_vxx_0, 0)) + self.connect((self.analog_wfm_rcv_0, 0), (self.qtgui_waterfall_sink_x_0, 0)) + self.connect((self.blocks_multiply_const_vxx_0, 0), (self.audio_sink_0, 0)) + self.connect((self.difi_source_cpp_0, 0), (self.blocks_probe_rate_0, 0)) + self.connect((self.difi_source_cpp_0, 0), (self.qtgui_freq_sink_x_0, 0)) + self.connect((self.difi_source_cpp_0, 0), (self.qtgui_time_sink_x_0, 0)) + self.connect((self.difi_source_cpp_0, 0), (self.rational_resampler_xxx_0, 0)) + self.connect((self.rational_resampler_xxx_0, 0), (self.analog_wfm_rcv_0, 0)) + + + def closeEvent(self, event): + self.settings = Qt.QSettings("GNU Radio", "fm_receiver") + self.settings.setValue("geometry", self.saveGeometry()) + self.stop() + self.wait() + + event.accept() + + def get_volume(self): + return self.volume + + def set_volume(self, volume): + self.volume = volume + self.blocks_multiply_const_vxx_0.set_k(self.volume) + + def get_samp_rate(self): + return self.samp_rate + + def set_samp_rate(self, samp_rate): + self.samp_rate = samp_rate + self.qtgui_freq_sink_x_0.set_frequency_range(self.center_freq, self.samp_rate) + self.qtgui_time_sink_x_0.set_samp_rate(self.samp_rate) + + def get_center_freq(self): + return self.center_freq + + def set_center_freq(self, center_freq): + self.center_freq = center_freq + self.qtgui_freq_sink_x_0.set_frequency_range(self.center_freq, self.samp_rate) + self.qtgui_waterfall_sink_x_0.set_frequency_range(self.center_freq, 48e3) + + + + +def main(top_block_cls=fm_receiver, options=None): + + qapp = Qt.QApplication(sys.argv) + + tb = top_block_cls() + + tb.start() + + tb.show() + + def sig_handler(sig=None, frame=None): + tb.stop() + tb.wait() + + Qt.QApplication.quit() + + signal.signal(signal.SIGINT, sig_handler) + signal.signal(signal.SIGTERM, sig_handler) + + timer = Qt.QTimer() + timer.start(500) + timer.timeout.connect(lambda: None) + + qapp.exec_() + +if __name__ == '__main__': + main() diff --git a/python-api/lock_in_amplifier_fm_radio/lock_in_amplifier_fm_radio.py b/python-api/lock_in_amplifier_fm_radio/lock_in_amplifier_fm_radio.py new file mode 100644 index 00000000..f859d4e6 --- /dev/null +++ b/python-api/lock_in_amplifier_fm_radio/lock_in_amplifier_fm_radio.py @@ -0,0 +1,172 @@ +# +# moku example: FM Radio Receiver with Moku Lock-in Amplifier +# +# This example demonstrates how to configure the Lock-in +# Amplifier instrument to downconvert an input signal from Input 1 +# to obtain IQ samples and send them as DIFI packets to a UDP socket. +# This DIFI data can be received and demodulated from GNU Radio or +# similar framework to listen to a FM Radio channel. + +# (c) 2024 Liquid Instruments Pty. Ltd. +# + +from moku.instruments import LockInAmp +from moku.exceptions import StreamException +import matplotlib.pyplot as plt +import numpy as np +import traceback +import socket +import time + +MOKU_IP = '192.168.0.105' +CHANNEL_FREQ = 105.9e6 # Frequnecy of the channel in Hz +CHANNEL_BANDWIDTH = 125e3 # Bandwidth of the channel in Hz +STREAMING_DURATION = None # Duration of streaming for lock-in amplifier, None for infinite +ENABLE_PLOTTING = False # Enable plotting +ENABLE_DIFI = True # Enable sending DIFI +DESTINATION_IP = '127.0.0.1' # dest address to send packets to +DESTINATION_PORT = 1234 # dest port to send packets to +STREAM_ID = 0 # 32 bit int id of stream +MAX_DATA_LEN = 1017 # max samples per packet + +pkt_count = 0 # packet counter for generating seqnum + +# Funtion for sending multiple DIFI packets +def send_data(udp_socket, dataI, dataQ): + for i in range(0, len(dataI), MAX_DATA_LEN): + send_difi(udp_socket, dataI[i:i+MAX_DATA_LEN], dataQ[i:i+MAX_DATA_LEN]) + +# Funtion for creating and sending a DIFI packet +def send_difi(udp_socket, dataI, dataQ): + # 1st 16 bits of header in hex + pkt_type = 1 + clsid = "1" # 1 bit + ti = "0" # 1 bits + vitai = "0" # 1 bit + vitast = "0" # 1 bit + tsi = "01" # 2 bits + tsf = "10" # 2 bits + packetchunk = int(clsid + ti + vitai + vitast + tsi + tsf, 2) + global pkt_count + seqnum = pkt_count % 16 + difi_packet = bytearray.fromhex(f"{pkt_type:01x}{packetchunk:02x}{seqnum:01x}") + + # 2nd 16 bits of header in hex + difi_packet.extend(bytearray.fromhex(f"{(len(dataI)+7):04x}")) + # number of samples + 7 for the header + difi_packet.extend(STREAM_ID.to_bytes(4, 'big')) # add stream id + difi_packet.extend(bytearray.fromhex("006A621E")) # XX:006A621E + # XX:OUI for Vita + difi_packet.extend(bytearray.fromhex("00000000")) # Info Class Code, + # Packet Class Code + # #icc=0x0000,pcc=0x0000 + + # Insert time stamp + current_time = time.time() + integer_part = int(current_time) # in seconds + fractional_part = int((current_time - integer_part) * (1e12)) # in picoseconds + difi_packet.extend(bytearray.fromhex(f"{integer_part:08x}{fractional_part:016x}")) + + # Insert data + for i, q in zip(dataI, dataQ): + difi_packet.extend(bytearray(np.float16([i, q]))) + + # Print packet information and send + # length = len(difi_packet) + # print(f'Length of this bytes object is {length} bytes and {length/4} 32 bit words') + udp_socket.sendto(difi_packet, (DESTINATION_IP, DESTINATION_PORT)) + pkt_count = pkt_count % 32 + 1 + + +# Connect to your Moku by its ip address +i = LockInAmp(MOKU_IP, force_connect=True) + +try: + # Set Channel 1 and 2 to DC coupled, 1 MOhm impedance, and + # 400 mVpp range + i.set_frontend(1, coupling='AC', impedance='50Ohm', + attenuation='0dB') + i.set_frontend(2, coupling='AC', impedance='50Ohm', + attenuation='0dB') + + # Configure the demodulation signal to Local oscillator + i.set_demodulation('Internal', frequency=CHANNEL_FREQ, phase=0) + + # Set low pass filter to channel bandwidth with 24 dB/octave slope + i.set_filter(CHANNEL_BANDWIDTH, slope='Slope24dB') + + # Configure output signals + # X component to Output 1, Y component to Output 2 + i.set_outputs('X', 'Y') + + # Set gain or both outputs to 72dB + i.set_gain(72,72) + + # Set up signal monitoring + # Configure monitor points to Input 1 and main output + i.set_monitor(1, 'MainOutput') + i.set_monitor(2, 'AuxOutput') + + # Configure the trigger conditions + # Trigger on Probe A, rising edge, 0V + i.set_trigger(type='Edge', source='ProbeA', level=0) + + # View +- 1 ms i.e. trigger in the centre + i.set_timebase(-1e-3, 1e-3) + + i.start_streaming(duration=STREAMING_DURATION, rate=CHANNEL_BANDWIDTH) + + if ENABLE_DIFI: + # Create the socket + udp_socket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) + + if ENABLE_PLOTTING: + # Set up the plotting parameters + plt.ion() + plt.show() + plt.grid(visible=True) + plt.ylim([-1, 1]) + line1, = plt.plot([]) + line2, = plt.plot([]) + ax = plt.gca() # Configure labels for axes + + # This loops continuously extracts data and plot/send as configured + while True: + # Get new data + start_time = time.time() + data = i.get_stream_data() + if data: + if ENABLE_DIFI: + # Send data as DIFI packets + send_data(udp_socket, data['ch1'], data['ch2']) + if ENABLE_PLOTTING: + plt.xlim([data['time'][0], data['time'][-1]]) + # Update the plot + line1.set_ydata(data['ch1']) + line2.set_ydata(data['ch2']) + line1.set_xdata(data['time']) + line2.set_xdata(data['time']) + plt.pause(0.001) + end_time = time.time() + time_difference = (end_time - start_time) * 10**3 + num_samples = len(data['ch1']) + print(f'Number of IQ samples: {num_samples}') + print(f'Time taken for transmit: {time_difference:6.2f} ms') + print(f'Transmit sample rate: {(len(data['ch1'])/time_difference):6.2f} ksps') + print("------------------------------------------------------") + +except (StreamException,KeyboardInterrupt) as e1: + # stop streaming and print message to user + i.stop_streaming() + print("Streaming terminated.") + +except Exception as e2: + # stop streaming and print exception details + i.stop_streaming() + print(f'Exception occurred: {e2}') + print(traceback.format_exc()) + +finally: + # Close the connection to the Moku device + # This ensures network resources are released correctly + i.relinquish_ownership() diff --git a/python-api/lock_in_amplifier_fm_radio/lock_in_amplifier_fm_radio_ext.py b/python-api/lock_in_amplifier_fm_radio/lock_in_amplifier_fm_radio_ext.py new file mode 100644 index 00000000..bd53affe --- /dev/null +++ b/python-api/lock_in_amplifier_fm_radio/lock_in_amplifier_fm_radio_ext.py @@ -0,0 +1,175 @@ +from moku.exceptions import StreamException +from moku.instruments import LockInAmp +import numpy as np +from threading import Thread +import socket +import time +import traceback +import tty +import termios +import sys + +MOKU_IP = '192.168.0.105' +CHANNEL_FREQ = 105.9e6 # Frequnecy of the channel in Hz +CHANNEL_BANDWIDTH = 125e3 # Bandwidth of the channel in Hz +CHANNEL_SPACING = 800e3 # Channel spacing in Hz +CHANNEL_CHANGE_OPTION = 1 # 1 for re-instantiating the stream (works), + # 2 for restarting the stream (doesn't work) +STREAMING_DURATION = 20 # Duration of streaming for lock-in amplifier +ENABLE_PLOTTING = False # Enable plotting +DESTINATION_IP = '127.0.0.1' # dest address to send packets to +DESTINATION_PORT = 1234 # dest port to send packets to +STREAM_ID = 0 # 32 bit int id of stream +MAX_DATA_LEN = 1017 # max samples per packet + +class grmoku(object): + + def __init__(self, moku_ip, chnl_freq, chnl_bw, duration, + dest_ip, dest_port, stream_id, max_samples): + self.moku_ip = moku_ip + self.chnl_freq = chnl_freq + self.chnl_bw = chnl_bw + self.duration = duration + self.dest_ip = dest_ip + self.dest_port = dest_port + self.stream_id = stream_id + self.max_samples = max_samples + self.pkt_count = 0 + self.udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.chnl_freq_changed = False + self.run_thread = True + self.thread = Thread(target=self.stream) + self.thread.start() + + def __del__(self): + self.run_thread = False + self.thread.join() + self.udp_socket.close() + + def stream(self): + try: + self.instrument = LockInAmp(self.moku_ip, force_connect=True) + self.instrument.set_frontend(1, coupling='AC', impedance='50Ohm', + attenuation='0dB') + self.instrument.set_frontend(2, coupling='AC', impedance='50Ohm', + attenuation='0dB') + self.instrument.set_demodulation('Internal', frequency=self.chnl_freq, phase=0) + self.instrument.set_filter(200e3, slope='Slope24dB') + self.instrument.set_outputs('X', 'Y') + self.instrument.set_gain(72,72) + self.instrument.set_monitor(1, 'MainOutput') + self.instrument.set_monitor(2, 'AuxOutput') + self.instrument.set_trigger(type='Edge', source='ProbeA', level=0) + self.instrument.set_timebase(-1e-3, 1e-3) + self.instrument.start_streaming(duration=self.duration, rate=self.chnl_bw) + print('Start streaming...') + print('Press arrow keys to change channel frequency, other keys to exit') + while self.run_thread: + if self.chnl_freq_changed: + self.instrument.stop_streaming() + self.instrument.set_demodulation('Internal', + frequency=self.chnl_freq, phase=0) + self.chnl_freq_changed = False + self.instrument.start_streaming(duration=self.duration, + rate=self.chnl_bw) + start_time = time.time() + data = self.instrument.get_stream_data() + if data: + self.send_data(data['ch1'], data['ch2']) + end_time = time.time() + time_difference = (end_time - start_time) * 10**3 + num_samples = len(data['ch1']) + print(f'Number of IQ samples: {num_samples}') + print(f'Time taken for transmit: {time_difference:6.2f} ms') + print(f'Transmit sample rate: {(num_samples/time_difference):6.2f} ksps') + print("------------------------------------------------------") + + except Exception as e: + print(f'Exception occurred: {e}') + print(traceback.format_exc()) + finally: + self.instrument.stop_streaming() + self.instrument.relinquish_ownership() + print("Streaming terminated") + + def send_data(self, dataI, dataQ): + for i in range(0, len(dataI), self.max_samples): + self.send_difi(dataI[i:i+self.max_samples], dataQ[i:i+self.max_samples]) + + def send_difi(self, dataI, dataQ): + # 1st 16 bits of header in hex + pkt_type = 1 + clsid = "1" # 1 bit + ti = "0" # 1 bits + vitai = "0" # 1 bit + vitast = "0" # 1 bit + tsi = "01" # 2 bits + tsf = "10" # 2 bits + packetchunk = int(clsid + ti + vitai + vitast + tsi + tsf, 2) + seqnum = self.pkt_count % 16 + difi_packet = bytearray.fromhex(f"{pkt_type:01x}{packetchunk:02x}{seqnum:01x}") + + # 2nd 16 bits of header in hex + difi_packet.extend(bytearray.fromhex(f"{(len(dataI)+7):04x}")) + # number of samples + 7 for the header + difi_packet.extend(self.stream_id.to_bytes(4, 'big')) # add stream id + difi_packet.extend(bytearray.fromhex("006A621E")) # XX:006A621E + # XX:OUI for Vita + difi_packet.extend(bytearray.fromhex("00000000")) # Info Class Code, + # Packet Class Code + # #icc=0x0000,pcc=0x0000 + + # Insert time stamp + current_time = time.time() + integer_part = int(current_time) + fractional_part = int((current_time - integer_part) * (1e12)) + difi_packet.extend(bytearray.fromhex(f"{integer_part:08x}{fractional_part:016x}")) + + # Insert data + for i, q in zip(dataI, dataQ): + difi_packet.extend(bytearray(np.float16([i, q]))) + + # Print packet information and send + # length = len(difi_packet) + # print(f'Length of this bytes object is {length} bytes and {length/4} 32 bit words') + self.udp_socket.sendto(difi_packet, (self.dest_ip, self.dest_port)) + self.pkt_count = self.pkt_count % 32 + 1 + + def set_chnl_freq(self, increament, option = 1): + if option == 1: + self.chnl_freq += increament + print(f'Opt1 setting channel frequency to {self.chnl_freq} Hz') + self.run_thread = False + self.thread.join() + self.run_thread = True + self.thread = Thread(target=self.stream) + self.thread.start() + elif option == 2: + self.chnl_freq += increament + print(f'Opt2 setting channel frequency to {self.chnl_freq} Hz') + self.chnl_freq_changed = True + +def get_key(): + fd = sys.stdin.fileno() + old_settings = termios.tcgetattr(fd) + try: + tty.setraw(fd) + ch = sys.stdin.read(3) + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + return ch + +grmoku_object = grmoku(MOKU_IP, CHANNEL_FREQ, CHANNEL_BANDWIDTH, + STREAMING_DURATION, DESTINATION_IP, DESTINATION_PORT, + STREAM_ID, MAX_DATA_LEN) +while True: + key = get_key() + if key == '\x1b[A' or key == '\x1b[C': # Up or right arrow key + grmoku_object.set_chnl_freq(CHANNEL_SPACING, CHANNEL_CHANGE_OPTION) + elif key == '\x1b[B' or key == '\x1b[D': # Down or left arrow key + grmoku_object.set_chnl_freq(-CHANNEL_SPACING, CHANNEL_CHANGE_OPTION) + else: + print('Request to exit') + grmoku_object.__del__() + break +sys.exit(0) \ No newline at end of file