| Philipp Le | d818bd5 | 2022-06-02 20:09:17 +0200 | [diff] [blame] | 1 | # 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 | |
| 7 | from __future__ import annotations |
| 8 | |
| 9 | import copy |
| 10 | from tkinter import ttk, LEFT, BOTH, BOTTOM |
| 11 | from pydantic import confloat, conint |
| 12 | from dcs.config import default_store, ConfigObject, ui_create, ConfigControlFrame |
| 13 | import numpy as np |
| 14 | import scipy.signal |
| 15 | from dcs.frames.base import BaseFrame, Window |
| 16 | from dcs.frames.groups import Ch07Group |
| 17 | from dcs.utils.chain import TxBasebandGenerator, RxCorrelationGenerator, RfBandGenerator |
| 18 | from dcs.utils.filter import create_lp_filter_class, NoFilter |
| 19 | from dcs.utils.iq_mixer import create_iq_oscillator_class, IqUpMixer, IqDownMixer, IqRfBandGenerator, \ |
| 20 | IqRxBasebandGenerator, IqTxBasebandGenerator, IqOscillatorBase |
| 21 | from dcs.utils.qam_modulation import ModulationMethod, QamBasebandModulator, QamBasebandGenerator |
| 22 | from dcs.utils.symbols import MessageBinarySerializer, Symbols |
| 23 | from dcs.utils.spreading import create_spreading_config_class, Spreader, Despreader, SpreadedBasebandSpectrum3D |
| 24 | from typing import Dict, List |
| 25 | |
| 26 | import matplotlib |
| 27 | matplotlib.use('TkAgg') |
| 28 | from matplotlib.pyplot import Circle, Text |
| 29 | from matplotlib.figure import Figure |
| 30 | from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk |
| 31 | |
| 32 | |
| 33 | SAMPLE_RATE = 10000 |
| 34 | FFT_SAMPLES_PER_SYM = 128 |
| 35 | |
| 36 | TxBasebandFilter = create_lp_filter_class(SAMPLE_RATE, 'TX Baseband') |
| 37 | |
| 38 | Spreading = create_spreading_config_class(SAMPLE_RATE) |
| 39 | |
| 40 | |
| 41 | @ui_create |
| 42 | class User(ConfigObject): |
| 43 | message: str = 'DCS' |
| 44 | spreading: Spreading = Spreading() |
| 45 | modulation: ModulationMethod = ModulationMethod.BPSK |
| 46 | tx_lp: TxBasebandFilter = TxBasebandFilter() |
| 47 | |
| 48 | def make_config_widget(self, parent: ttk.Widget) -> ConfigControlFrame: |
| 49 | frm = ConfigControlFrame(parent) |
| 50 | |
| 51 | frm1 = ConfigControlFrame(frm, borderwidth=1, relief='raised') |
| 52 | frm1.pack() |
| 53 | spreading_frame = self.spreading.make_config_widget(frm1) |
| 54 | spreading_frame.pack() |
| 55 | |
| 56 | frm2 = ConfigControlFrame(frm, borderwidth=1, relief='raised') |
| 57 | frm2.pack() |
| 58 | ttk.Label(frm2, text='Message:').grid(row=0, column=0) |
| 59 | self.ui_create_message(frm2).grid(row=0, column=1) |
| 60 | |
| 61 | ttk.Label(frm2, text='Modulation:').grid(row=1, column=0) |
| 62 | self.ui_create_modulation_dropdown(frm2).grid(row=1, column=1) |
| 63 | |
| 64 | ttk.Label(frm2, text='TX Baseband Low Pass Cut-Off:').grid(row=2, column=0) |
| 65 | self.tx_lp.ui_create_cutoff(frm2).grid(row=2, column=1) |
| 66 | ttk.Label(frm2, text='Hz').grid(row=2, column=2) |
| 67 | |
| 68 | return frm |
| 69 | |
| 70 | def make_title(self): |
| 71 | return f'{self.message} - {self.spreading.make_title()}' |
| 72 | |
| 73 | def get_symbols(self) -> Symbols: |
| 74 | return MessageBinarySerializer(self.message).to_symbols() |
| 75 | |
| 76 | def make_unspreaded_signal(self) -> TxBasebandGenerator: |
| 77 | return QamBasebandGenerator( |
| 78 | self.get_symbols(), |
| 79 | QamBasebandModulator( |
| 80 | symbol_rate=self.make_spreader().get_symbol_rate(), |
| 81 | method=ModulationMethod.BPSK, |
| 82 | baseband_filter=NoFilter(), |
| 83 | ) |
| 84 | ) |
| 85 | |
| 86 | def make_spreader(self) -> Spreader: |
| 87 | return self.spreading.make_spreader(self.modulation, self.tx_lp) |
| 88 | |
| 89 | def make_despreader(self) -> Despreader: |
| 90 | return self.spreading.make_despreader(self.modulation, self.tx_lp) |
| 91 | |
| 92 | |
| 93 | @ui_create |
| 94 | class ConfigCh07Spreading(ConfigObject): |
| 95 | _KEY = 'ch07_spreading' |
| 96 | |
| 97 | users: List[User] = [ |
| 98 | User() |
| 99 | ] |
| 100 | #tx_osc: IqOscillator = IqOscillator(freq=100.0) |
| 101 | noise_dBc: confloat(ge=-160.0, le=40.0, multiple_of=0.1) = -60.0 |
| 102 | random_seed: conint(ge=0, lt=10000) = 1000 |
| 103 | #rx_osc: IqOscillator = IqOscillator(freq=100.0) |
| 104 | #rx_lp: RxBasebandFilter = RxBasebandFilter() |
| 105 | |
| 106 | def _noise_frame(self, parent: ttk.Widget) -> ConfigControlFrame: |
| 107 | frm = ConfigControlFrame(parent, borderwidth=1, relief='raised') |
| 108 | |
| 109 | ttk.Label(frm, text='Signal-Noise Ratio:').grid(row=0, column=0) |
| 110 | w = self.ui_create_noise_dBc(frm) |
| 111 | frm.add_widget(w) |
| 112 | w.grid(row=0, column=1) |
| 113 | ttk.Label(frm, text='dBc').grid(row=0, column=2) |
| 114 | |
| 115 | ttk.Label(frm, text='Random Seed:').grid(row=1, column=0) |
| 116 | w = self.ui_create_random_seed(frm) |
| 117 | frm.add_widget(w) |
| 118 | w.grid(row=1, column=1) |
| 119 | |
| 120 | return frm |
| 121 | |
| 122 | def make_config_widget(self, parent: ttk.Widget) -> ConfigControlFrame: |
| 123 | frm = ConfigControlFrame(parent, borderwidth=1, relief='raised') |
| 124 | |
| 125 | user_frm = ttk.Frame(frm, borderwidth=1, relief='raised') |
| 126 | user_frm.pack() |
| 127 | ttk.Label(user_frm, text='TX Oscillator:').pack() |
| 128 | w = self.ui_create_users_list(user_frm, lambda e: e.make_title()) |
| 129 | frm.add_widget(w) |
| 130 | w.pack() |
| 131 | |
| 132 | noise_frm = self._noise_frame(frm) |
| 133 | noise_frm.pack() |
| 134 | for w in noise_frm.ctrl_widgets: |
| 135 | frm.add_widget(w) |
| 136 | |
| 137 | return frm |
| 138 | |
| 139 | |
| 140 | class Ch07SpreadingFrame(BaseFrame): |
| 141 | def __init__(self, *args, **kwargs): |
| 142 | super().__init__(*args, **kwargs) |
| 143 | |
| 144 | self._config: ConfigCh07Spreading = default_store().get_config(ConfigCh07Spreading) |
| 145 | |
| 146 | ctrl_frm = self._create_control() |
| 147 | ctrl_frm.pack(side=LEFT) |
| 148 | |
| 149 | signal_frm = self._create_signal_tabs() |
| 150 | signal_frm.pack(expand=True, fill=BOTH) |
| 151 | |
| 152 | def _create_control(self) -> ConfigControlFrame: |
| 153 | frm = self._config.make_config_widget(self) |
| 154 | frm.widgets_on_change(self._on_change) |
| 155 | return frm |
| 156 | |
| 157 | def _on_change(self, _, __, ___): |
| 158 | default_store().save() |
| 159 | self.refresh() |
| 160 | |
| 161 | def _create_signal_tabs(self) -> ttk.Widget: |
| 162 | tabs = ttk.Notebook(self) |
| 163 | |
| 164 | tx_frm = ttk.Frame(tabs) |
| 165 | tabs.add(tx_frm, text='TX Baseband') |
| 166 | self._tx_fig = Figure(figsize=(12, 6), dpi=100) |
| 167 | self._tx_canvas = FigureCanvasTkAgg(self._tx_fig, tx_frm) |
| 168 | self._tx_canvas.get_tk_widget().pack(expand=True, fill=BOTH) |
| 169 | tx_tb = NavigationToolbar2Tk(self._tx_canvas, tx_frm, pack_toolbar=False) |
| 170 | tx_tb.pack(side=BOTTOM) |
| 171 | |
| 172 | self._data_bytes = ttk.Label(tx_frm, text='') |
| 173 | self._data_bytes.pack(side=BOTTOM) |
| 174 | self._data_syms = ttk.Label(tx_frm, text='') |
| 175 | self._data_syms.pack(side=BOTTOM) |
| 176 | |
| 177 | fd_frm = ttk.Frame(tabs) |
| 178 | tabs.add(fd_frm, text='Frequency Domain') |
| 179 | self._fd_fig = Figure(figsize=(12, 6), dpi=100) |
| 180 | self._fd_canvas = FigureCanvasTkAgg(self._fd_fig, fd_frm) |
| 181 | self._fd_canvas.get_tk_widget().pack(expand=True, fill=BOTH) |
| 182 | fd_tb = NavigationToolbar2Tk(self._fd_canvas, fd_frm, pack_toolbar=False) |
| 183 | fd_tb.pack(side=BOTTOM) |
| 184 | |
| 185 | rx_frm = ttk.Frame(tabs) |
| 186 | tabs.add(rx_frm, text='RX Cross-Correlation') |
| 187 | self._rx_fig = Figure(figsize=(12, 6), dpi=100) |
| 188 | self._rx_canvas = FigureCanvasTkAgg(self._rx_fig, rx_frm) |
| 189 | self._rx_canvas.get_tk_widget().pack(expand=True, fill=BOTH) |
| 190 | rx_tb = NavigationToolbar2Tk(self._rx_canvas, rx_frm, pack_toolbar=False) |
| 191 | rx_tb.pack(side=BOTTOM) |
| 192 | |
| 193 | self.refresh() |
| 194 | |
| 195 | return tabs |
| 196 | |
| 197 | def refresh(self): |
| 198 | if len(self._config.users) > 0: |
| 199 | user0 = self._config.users[0] |
| 200 | user0_spreader = user0.make_spreader() |
| 201 | tx_gen0 = user0_spreader.make_tx_baseband_generator(user0.get_symbols()) |
| 202 | |
| 203 | unspreaded_gen0 = user0.make_unspreaded_signal() |
| 204 | |
| 205 | tx_gens = copy.deepcopy(tx_gen0) |
| 206 | for user in self._config.users[1:]: |
| 207 | tx_gens += user.make_spreader().make_tx_baseband_generator(user.get_symbols()) |
| 208 | |
| 209 | class _Osc(IqOscillatorBase): |
| 210 | def make_config_widget(self, parent: ttk.Widget) -> ConfigControlFrame: |
| 211 | pass # Won't be called |
| 212 | |
| 213 | rf_gen = IqRfBandGenerator( |
| 214 | tx_gens, |
| 215 | IqUpMixer( |
| 216 | tx_osc=_Osc(freq=0), |
| 217 | noise_dBc=self._config.noise_dBc, |
| 218 | random_seed=self._config.random_seed, |
| 219 | ) |
| 220 | ) |
| 221 | # rx_gen = rf_gen.make_iq_down_mixer( |
| 222 | # IqDownMixer( |
| 223 | # rx_osc=_Osc(freq=0), |
| 224 | # baseband_filter=NoFilter(), |
| 225 | # ) |
| 226 | # ) |
| 227 | |
| 228 | rx_gen = user0.make_despreader().make_rx_correlation_generator(rf_gen) |
| 229 | |
| 230 | # Draw |
| 231 | self.draw_tx(tx_gen0, unspreaded_gen0, rf_gen) |
| 232 | self.draw_fd(rf_gen, user0_spreader) |
| 233 | self.draw_rx(rx_gen, unspreaded_gen0) |
| 234 | |
| 235 | def draw_tx(self, tx_gen0: TxBasebandGenerator, unspreaded_gen0: TxBasebandGenerator, rf_gen: RfBandGenerator): |
| 236 | self._tx_fig.clear() |
| 237 | |
| 238 | ax_msg = self._tx_fig.add_subplot(3, 1, 1) |
| 239 | ax_msg.set_xlabel('time') |
| 240 | ax_msg.set_ylabel('value') |
| 241 | ax_msg.set_title('User 0 Message (Binary)') |
| 242 | ax_u0 = self._tx_fig.add_subplot(3, 1, 2) |
| 243 | ax_u0.set_xlabel('time') |
| 244 | ax_u0.set_ylabel('value') |
| 245 | ax_u0.set_title('User 0 TX Baseband Signal') |
| 246 | ax_tx = self._tx_fig.add_subplot(3, 1, 3) |
| 247 | ax_tx.set_xlabel('time') |
| 248 | ax_tx.set_ylabel('value') |
| 249 | ax_tx.set_title('TX Baseband Signal') |
| 250 | |
| 251 | sig_msg = unspreaded_gen0.generate_tx_baseband_signal(SAMPLE_RATE) |
| 252 | ax_msg.set_xlim(np.min(sig_msg.t), np.max(sig_msg.t)) |
| 253 | ax_msg.plot(sig_msg.t, np.real(sig_msg.signal), label='Message', color='blue', linewidth=1) |
| 254 | |
| 255 | sig_u0 = tx_gen0.generate_tx_baseband_signal(SAMPLE_RATE) |
| 256 | ax_u0.set_xlim(np.min(sig_u0.t), np.max(sig_u0.t)) |
| 257 | ax_u0.plot(sig_u0.t, np.real(sig_u0.signal), label='User 0', color='red', linewidth=1) |
| 258 | |
| 259 | sig_tx = rf_gen.generate_rf_signal(SAMPLE_RATE) |
| 260 | ax_tx.set_xlim(np.min(sig_tx.t), np.max(sig_tx.t)) |
| 261 | ax_tx.plot(sig_tx.t, np.real(sig_tx.signal), label='Baseband', color='brown', linewidth=1) |
| 262 | |
| 263 | self._tx_fig.tight_layout() |
| 264 | self._tx_canvas.draw() |
| 265 | |
| 266 | def draw_fd(self, rf_gen: RfBandGenerator, spreader: Spreader): |
| 267 | self._fd_fig.clear() |
| 268 | |
| 269 | ax_fd = self._fd_fig.add_subplot(1, 1, 1, projection='3d') |
| 270 | ax_fd.set_xlabel('frequency') |
| 271 | ax_fd.set_ylabel('symbol') |
| 272 | ax_fd.set_zlabel('value (logarithmic)') |
| 273 | ax_fd.set_title('HF Signal Frequency-Domain') |
| 274 | ax_fd.set_zlim(-120, 40) |
| 275 | |
| 276 | x, y, z = SpreadedBasebandSpectrum3D( |
| 277 | rf_gen, |
| 278 | spreader.get_symbol_rate(), |
| 279 | FFT_SAMPLES_PER_SYM |
| 280 | ).make_plot_data() |
| 281 | |
| 282 | ax_fd.plot_trisurf(x, y, z) |
| 283 | |
| 284 | self._fd_fig.tight_layout() |
| 285 | self._fd_canvas.draw() |
| 286 | |
| 287 | def draw_rx(self, rx_correlation: RxCorrelationGenerator, unspreaded_gen0: TxBasebandGenerator): |
| 288 | self._rx_fig.clear() |
| 289 | |
| 290 | ax_msg = self._rx_fig.add_subplot(2, 1, 1) |
| 291 | ax_msg.set_xlabel('time') |
| 292 | ax_msg.set_ylabel('value') |
| 293 | ax_msg.set_title('User 0 Message (Binary)') |
| 294 | ax_correlation = self._rx_fig.add_subplot(2, 1, 2) |
| 295 | ax_correlation.set_xlabel('time') |
| 296 | ax_correlation.set_ylabel('value') |
| 297 | ax_correlation.set_title('RX Correlation') |
| 298 | |
| 299 | sig_msg = unspreaded_gen0.generate_tx_baseband_signal(SAMPLE_RATE) |
| 300 | ax_msg.set_xlim(np.min(sig_msg.t), np.max(sig_msg.t)) |
| 301 | ax_msg.plot(sig_msg.t, np.real(sig_msg.signal), label='Message', color='blue', linewidth=1) |
| 302 | |
| 303 | corr = rx_correlation.generate_rx_correlation(SAMPLE_RATE) |
| 304 | ax_correlation.set_xlim(np.min(corr.t), np.max(corr.t)) |
| 305 | ax_correlation.plot(corr.t, np.real(corr.signal), label='Correlation', color='blue', linewidth=1) |
| 306 | |
| 307 | self._rx_fig.tight_layout() |
| 308 | self._rx_canvas.draw() |
| 309 | |
| 310 | |
| 311 | class Ch07SpreadingWindow(Window): |
| 312 | GROUP = Ch07Group |
| 313 | TITLE = 'Spread Spectrum' |
| 314 | FRAME = Ch07SpreadingFrame |
| 315 | |
| 316 | |
| 317 | if __name__ == '__main__': |
| 318 | Ch07SpreadingWindow.main() |
| 319 | |