blob: 448f37fbbf7f1dd61b4de5116a078f9463e8e458 [file] [log] [blame]
Philipp Le86184212022-05-10 20:46:23 +02001# SPDX-License-Identifier: MPL-2.0
2# Copyright (c) 2022 Philipp Le <philipp@philipple.de>.
3# This Source Code Form is subject to the terms of the Mozilla Public
4# License, v. 2.0. If a copy of the MPL was not distributed with this
5# file, You can obtain one at https://mozilla.org/MPL/2.0/.
6
7from __future__ import annotations
8
9from tkinter import ttk, LEFT, BOTH, BOTTOM
10from pydantic import confloat, conint
11from dcs.config import default_store, ConfigObject, ui_create, ConfigControlFrame
12import numpy as np
13import scipy.signal
14from dcs.frames.base import BaseFrame, Window
15from dcs.frames.groups import Ch05Group
16from dcs.utils.filter import create_lp_filter_class
17from dcs.utils.iq_mixer import create_iq_oscillator_class, IqUpMixer, IqDownMixer, IqRfBandGenerator, IqRxBasebandGenerator
18from dcs.utils.qam_modulation import ModulationMethod, QamBasebandModulator, QamBasebandGenerator
19from dcs.utils.symbols import MessageBinarySerializer
20from typing import Dict
21
22import matplotlib
23matplotlib.use('TkAgg')
24from matplotlib.pyplot import Circle, Text
25from matplotlib.figure import Figure
26from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
27
28
29SAMPLE_RATE = 10000
30FFT_SAMPLES_PER_SYM = 64
31
32
33IqOscillator = create_iq_oscillator_class(sample_rate=SAMPLE_RATE, imbalance_wdg=True)
34
35TxBasebandFilter = create_lp_filter_class(SAMPLE_RATE, 'TX Baseband')
36RxBasebandFilter = create_lp_filter_class(SAMPLE_RATE, 'RX Baseband')
37
38
39class ConfigCh05QamFrame(ConfigControlFrame):
40 wdg_msg_update_btn: ttk.Button
41
42
43@ui_create
44class ConfigCh05Qam(ConfigObject):
45 _KEY = 'ch05_qam'
46
47 message: str = 'DCS'
48 symbol_rate: confloat(ge=-SAMPLE_RATE/4, lt=SAMPLE_RATE/4, multiple_of=(4.0/SAMPLE_RATE)) = 10.0
49 modulation: ModulationMethod = ModulationMethod.QPSK
50 tx_lp: TxBasebandFilter = TxBasebandFilter()
51 tx_osc: IqOscillator = IqOscillator(freq=100.0)
52 noise_dBc: confloat(ge=-160.0, le=40.0, multiple_of=0.1) = -60.0
53 random_seed: conint(ge=0, lt=10000) = 1000
54 rx_osc: IqOscillator = IqOscillator(freq=100.0)
55 rx_lp: RxBasebandFilter = RxBasebandFilter()
56
57 def _mod_frame(self, parent: ConfigCh05QamFrame) -> ConfigControlFrame:
58 frm = ConfigControlFrame(parent, borderwidth=1, relief='raised')
59
60 ttk.Label(frm, text='Message:').grid(row=0, column=0)
61 # NOTE: Recalculating the message is slow. So only update on click on the Update button.
62 self.ui_create_message(frm).grid(row=0, column=1)
63 parent.wdg_msg_update_btn = ttk.Button(frm, text='Update')
64 parent.wdg_msg_update_btn.grid(row=0, column=2)
65
66 ttk.Label(frm, text='Symbol Rate:').grid(row=1, column=0)
67 w = self.ui_create_symbol_rate(frm)
68 frm.add_widget(w)
69 w.grid(row=1, column=1)
70 ttk.Label(frm, text='Hz').grid(row=1, column=2)
71
72 ttk.Label(frm, text='Modulation:').grid(row=2, column=0)
73 w = self.ui_create_modulation_dropdown(frm)
74 frm.add_widget(w)
75 w.grid(row=2, column=1)
76
77 ttk.Label(frm, text='TX Baseband Low Pass Cut-Off:').grid(row=3, column=0)
78 w = self.tx_lp.ui_create_cutoff(frm)
79 frm.add_widget(w)
80 w.grid(row=3, column=1)
81 ttk.Label(frm, text='Hz').grid(row=3, column=2)
82
83 return frm
84
85 def _noise_frame(self, parent: ttk.Widget) -> ConfigControlFrame:
86 frm = ConfigControlFrame(parent, borderwidth=1, relief='raised')
87
88 ttk.Label(frm, text='Signal-Noise Ratio:').grid(row=0, column=0)
89 w = self.ui_create_noise_dBc(frm)
90 frm.add_widget(w)
91 w.grid(row=0, column=1)
92 ttk.Label(frm, text='dBc').grid(row=0, column=2)
93
94 ttk.Label(frm, text='Random Seed:').grid(row=1, column=0)
95 w = self.ui_create_random_seed(frm)
96 frm.add_widget(w)
97 w.grid(row=1, column=1)
98
99 return frm
100
101 def _demod_frame(self, parent: ttk.Widget) -> ConfigControlFrame:
102 frm = ConfigControlFrame(parent, borderwidth=1, relief='raised')
103
104 frm1 = self.rx_lp.make_config_widget(frm)
105 frm1.pack()
106 for w in frm1.ctrl_widgets:
107 frm.add_widget(w)
108
109 return frm
110
111 def make_config_widget(self, parent: ttk.Widget) -> ConfigCh05QamFrame:
112 frm = ConfigCh05QamFrame(parent, borderwidth=1, relief='raised')
113
114 sym_frm = self._mod_frame(frm)
115 sym_frm.pack()
116 for w in sym_frm.ctrl_widgets:
117 frm.add_widget(w)
118
119 tx_frm = ttk.Frame(frm, borderwidth=1, relief='raised')
120 tx_frm.pack()
121 ttk.Label(tx_frm, text='TX Oscillator:').pack()
122 tx_ctrl_frm = self.tx_osc.make_config_widget(tx_frm)
123 tx_ctrl_frm.pack()
124 for w in tx_ctrl_frm.ctrl_widgets:
125 frm.add_widget(w)
126
127 noise_frm = self._noise_frame(frm)
128 noise_frm.pack()
129 for w in noise_frm.ctrl_widgets:
130 frm.add_widget(w)
131
132 rx_frm = ttk.Frame(frm, borderwidth=1, relief='raised')
133 rx_frm.pack()
134 ttk.Label(rx_frm, text='RX Oscillator:').pack()
135 rx_ctrl_frm = self.tx_osc.make_config_widget(rx_frm)
136 rx_ctrl_frm.pack()
137 for w in rx_ctrl_frm.ctrl_widgets:
138 frm.add_widget(w)
139
140 demod_frm = self._demod_frame(frm)
141 demod_frm.pack()
142 for w in demod_frm.ctrl_widgets:
143 frm.add_widget(w)
144
145 return frm
146
147
148class Ch05QamFrame(BaseFrame):
149 def __init__(self, *args, **kwargs):
150 super().__init__(*args, **kwargs)
151
152 self._config: ConfigCh05Qam = default_store().get_config(ConfigCh05Qam)
153
154 ctrl_frm = self._create_control()
155 ctrl_frm.pack(side=LEFT)
156
157 signal_frm = self._create_signal_tabs()
158 signal_frm.pack(expand=True, fill=BOTH)
159
160 def _create_control(self) -> ConfigControlFrame:
161 frm = self._config.make_config_widget(self)
162 frm.widgets_on_change(self._on_change)
163 frm.wdg_msg_update_btn.configure(command=lambda: self._on_change(0, 0, 0))
164 return frm
165
166 def _on_change(self, _, __, ___):
167 default_store().save()
168 self.refresh()
169
170 def _create_signal_tabs(self) -> ttk.Widget:
171 tabs = ttk.Notebook(self)
172
173 tx_frm = ttk.Frame(tabs)
174 tabs.add(tx_frm, text='TX Baseband')
175 self._tx_fig = Figure(figsize=(12, 6), dpi=100)
176 self._tx_canvas = FigureCanvasTkAgg(self._tx_fig, tx_frm)
177 self._tx_canvas.get_tk_widget().pack(expand=True, fill=BOTH)
178 tx_tb = NavigationToolbar2Tk(self._tx_canvas, tx_frm, pack_toolbar=False)
179 tx_tb.pack(side=BOTTOM)
180
181 self._data_bytes = ttk.Label(tx_frm, text='')
182 self._data_bytes.pack(side=BOTTOM)
183 self._data_syms = ttk.Label(tx_frm, text='')
184 self._data_syms.pack(side=BOTTOM)
185
186 hf_frm = ttk.Frame(tabs)
187 tabs.add(hf_frm, text='HF Band')
188 self._hf_fig = Figure(figsize=(12, 6), dpi=100)
189 self._hf_canvas = FigureCanvasTkAgg(self._hf_fig, hf_frm)
190 self._hf_canvas.get_tk_widget().pack(expand=True, fill=BOTH)
191 hf_tb = NavigationToolbar2Tk(self._hf_canvas, hf_frm, pack_toolbar=False)
192 hf_tb.pack(side=BOTTOM)
193
194 rx_frm = ttk.Frame(tabs)
195 tabs.add(rx_frm, text='RX Baseband')
196 self._rx_fig = Figure(figsize=(12, 6), dpi=100)
197 self._rx_canvas = FigureCanvasTkAgg(self._rx_fig, rx_frm)
198 self._rx_canvas.get_tk_widget().pack(expand=True, fill=BOTH)
199 rx_tb = NavigationToolbar2Tk(self._rx_canvas, rx_frm, pack_toolbar=False)
200 rx_tb.pack(side=BOTTOM)
201
202 self.refresh()
203
204 return tabs
205
206 def refresh(self):
207 # Make constellation
208 constellation = self._config.modulation.make_constellation_map_str()
209
210 # Make symbols vector
211 bytes_symbols = MessageBinarySerializer(self._config.message).to_symbols()
212 symbols = bytes_symbols.reencode(self._config.modulation.bits_per_symbol())
213
214 # Update symbol output widgets
215 data_bytes = ', '.join(bytes_symbols.to_bit_str())
216 self._data_bytes.configure(text=f'Data = [{data_bytes}]')
217 data_syms = ', '.join(symbols.to_bit_str())
218 self._data_syms.configure(text=f'Symbols = [{data_syms}]')
219
220 # Create signal generators
221 baseband_gen = QamBasebandGenerator(
222 symbols,
223 QamBasebandModulator(
224 method=self._config.modulation,
225 baseband_filter=self._config.tx_lp,
226 symbol_rate=self._config.symbol_rate
227 )
228 )
229 iq_mod = baseband_gen.make_rf_band_generator(
230 IqUpMixer(
231 tx_osc=self._config.tx_osc,
232 noise_dBc=self._config.noise_dBc,
233 random_seed=self._config.random_seed
234 )
235 )
236 iq_demod = iq_mod.make_iq_down_mixer(
237 IqDownMixer(
238 rx_osc=self._config.rx_osc,
239 baseband_filter=self._config.rx_lp
240 )
241 )
242
243 # Draw
244 self.draw_tx(constellation, baseband_gen)
245 self.draw_hf(iq_mod)
246 self.draw_rx(constellation, iq_demod)
247
248 def draw_tx(self, constellation: Dict[str, complex], baseband_gen: QamBasebandGenerator):
249 self._tx_fig.clear()
250
251 ax_const = self._tx_fig.add_subplot(2, 1, 1)
252 ax_const.set_xlim(-1.2, 1.2)
253 ax_const.set_ylim(-1.2, 1.2)
254 ax_const.set_xlabel('real')
255 ax_const.set_ylabel('imaginary')
256 ax_const.set_title('Ideal Constellation Diagram')
257 ax_const.set_aspect(1)
258 ax_base = self._tx_fig.add_subplot(2, 1, 2)
259 ax_base.set_xlabel('time')
260 ax_base.set_ylabel('value')
261 ax_base.set_title('TX Baseband Signal')
262
263 for sym_str, sym_val in constellation.items():
264 circ = Circle((sym_val.real, sym_val.imag), 0.03, alpha=0.4, color='blue', label=sym_str)
265 ax_const.add_artist(circ)
266 txt = Text(sym_val.real, sym_val.imag, sym_str, color='orange', fontsize=8)
267 ax_const.add_artist(txt)
268
269 sig_base_tx = baseband_gen.generate_tx_baseband_signal(SAMPLE_RATE)
270 ax_base.set_xlim(np.min(sig_base_tx.t), np.max(sig_base_tx.t))
271 ax_base.plot(sig_base_tx.t, np.real(sig_base_tx.signal), label='Baseband I', linestyle='solid', color='blue', linewidth=1)
272 ax_base.plot(sig_base_tx.t, np.imag(sig_base_tx.signal), label='Baseband Q', linestyle='solid', color='red', linewidth=1)
273 ax_base.legend()
274
275 self._tx_fig.tight_layout()
276 self._tx_canvas.draw()
277
278 def draw_hf(self, rf_gen: IqRfBandGenerator):
279 self._hf_fig.clear()
280
281 ax_td = self._hf_fig.add_subplot(2, 1, 1)
282 ax_td.set_xlabel('time')
283 ax_td.set_ylabel('value')
284 ax_td.set_title('HF Signal Time-Domain')
285 ax_fd = self._hf_fig.add_subplot(2, 1, 2)
286 ax_fd.set_ylim(-140, 40)
287 ax_fd.set_xlabel('frequency')
288 ax_fd.set_ylabel('value (logarithmic)')
289 ax_fd.set_title('HF Signal Frequency-Domain')
290
291 sig_hf_normal = rf_gen.generate_rf_signal(SAMPLE_RATE)
292 ax_td.set_xlim(np.min(sig_hf_normal.t), np.max(sig_hf_normal.t))
293 ax_td.plot(sig_hf_normal.t, np.real(sig_hf_normal.signal), label='HF', linestyle='solid', color='brown', linewidth=1)
294 ax_td.legend()
295
296 fd_smp_rate = FFT_SAMPLES_PER_SYM * self._config.symbol_rate
297 sig_hf_ovs = rf_gen.generate_rf_signal(fd_smp_rate)
298 f_fd, X_hf_fd = sig_hf_ovs.fft(window_fn=scipy.signal.windows.blackman)
299
300 ax_fd.set_xlim(0, np.max(f_fd))
301 ax_fd.plot(f_fd, X_hf_fd, label='HF', linestyle='solid', color='brown', linewidth=1)
302 ax_fd.legend()
303
304 self._hf_fig.tight_layout()
305 self._hf_canvas.draw()
306
307 def draw_rx(self, constellation: Dict[str, complex], rx_gen: IqRxBasebandGenerator):
308 self._rx_fig.clear()
309 ax_base = self._rx_fig.add_subplot(2, 1, 1)
310 ax_base.set_xlabel('time')
311 ax_base.set_ylabel('value')
312 ax_base.set_title('RX Baseband Signal')
313 ax_const = self._rx_fig.add_subplot(2, 1, 2)
314 ax_const.set_xlim(-1.2, 1.2)
315 ax_const.set_ylim(-1.2, 1.2)
316 ax_const.set_xlabel('real')
317 ax_const.set_ylabel('imaginary')
318 ax_const.set_title('RX Constellation Diagram')
319 ax_const.set_aspect(1)
320
321 sig_base_rx = rx_gen.generate_rx_baseband_signal(SAMPLE_RATE)
322 ax_base.set_xlim(np.min(sig_base_rx.t), np.max(sig_base_rx.t))
323 ax_base.plot(sig_base_rx.t, np.real(sig_base_rx.signal), label='Baseband I', linestyle='solid', color='blue', linewidth=1)
324 ax_base.plot(sig_base_rx.t, np.imag(sig_base_rx.signal), label='Baseband Q', linestyle='solid', color='red', linewidth=1)
325 ax_base.legend()
326
327 ax_const.plot(np.real(sig_base_rx.signal), np.imag(sig_base_rx.signal), label='Baseband', linestyle='solid', color='purple', linewidth=1)
328 ax_const.legend()
329
330 for sym_str, sym_val in constellation.items():
331 circ = Circle((sym_val.real, sym_val.imag), 0.05, alpha=0.4, color='blue', label=sym_str)
332 ax_const.add_artist(circ)
333 txt = Text(sym_val.real, sym_val.imag, sym_str, color='orange', fontsize=8)
334 ax_const.add_artist(txt)
335
336 self._rx_fig.tight_layout()
337 self._rx_canvas.draw()
338
339
340class Ch05QamWindow(Window):
341 GROUP = Ch05Group
342 TITLE = 'Quadrature Amplitude Modulation'
343 FRAME = Ch05QamFrame
344
345
346if __name__ == '__main__':
347 Ch05QamWindow.main()
348