| Philipp Le | f65de0e | 2022-06-02 20:08:43 +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 | from tkinter import ttk |
| 10 | from pydantic import confloat |
| 11 | from dcs.config import ConfigObject, ui_create, ConfigControlFrame |
| 12 | from abc import ABC, abstractmethod |
| 13 | from dataclasses import dataclass |
| 14 | from enum import Enum |
| 15 | import numpy as np |
| 16 | from typing import Optional, Type, Callable, Tuple |
| 17 | from .signal import Signal |
| 18 | from .chain import RfBandGenerator, TxBasebandGenerator, RxCorrelationGenerator |
| 19 | from .filter import Filter, NoFilter |
| 20 | from .iq_mixer import IqTxBasebandGenerator |
| 21 | from .qam_modulation import ModulationMethod, QamBasebandModulator, QamBasebandGenerator |
| 22 | from .symbols import Symbols |
| 23 | |
| 24 | |
| 25 | ##################################################################### |
| 26 | # Configuration Items |
| 27 | |
| 28 | |
| 29 | class SpreadingMethod(str, Enum): |
| 30 | DSSS_ASYNC = 'Asynchronous CDMA / DSSS' |
| 31 | |
| 32 | def get_spreader(self) -> Type[Spreader]: |
| 33 | return { |
| 34 | self.DSSS_ASYNC: AsyncDsssSpreader |
| 35 | }[self] |
| 36 | |
| 37 | def get_despreader(self) -> Type[Despreader]: |
| 38 | return { |
| 39 | self.DSSS_ASYNC: AsyncDsssDespreader |
| 40 | }[self] |
| 41 | |
| 42 | |
| 43 | class SpreadingCode(ABC): |
| 44 | @abstractmethod |
| 45 | def get_code(self) -> CodeSequence: |
| 46 | ... |
| 47 | |
| 48 | def get_spreading_factor(self) -> int: |
| 49 | return self.get_code().sequence.bits_per_symbol |
| 50 | |
| 51 | def get_hopping_length(self) -> int: |
| 52 | return len(self.get_code().sequence) |
| 53 | |
| 54 | |
| 55 | class AsyncSpreadingCode(SpreadingCode, ABC): |
| 56 | pass |
| 57 | |
| 58 | |
| 59 | class AsyncDsssCode(str, Enum): |
| 60 | IEEE80211B = 'IEEE 802.11b' |
| 61 | |
| 62 | def make_code(self) -> SpreadingCode: |
| 63 | code = self |
| 64 | |
| 65 | class _Code(AsyncSpreadingCode): |
| 66 | def get_code(self) -> CodeSequence: |
| 67 | return { |
| 68 | AsyncDsssCode.IEEE80211B: CodeSequence( |
| 69 | sequence=Symbols( |
| 70 | # symbols=[1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0b01001000111], |
| 71 | symbols=[0b01001000111], |
| 72 | bits_per_symbol=11 |
| 73 | ) |
| 74 | ), |
| 75 | }[code] |
| 76 | |
| 77 | return _Code() |
| 78 | |
| 79 | |
| 80 | @ui_create |
| 81 | class SpreadingConfigBase(ConfigObject, ABC): |
| 82 | method: SpreadingMethod = SpreadingMethod.DSSS_ASYNC |
| 83 | async_dsss_code: AsyncDsssCode = AsyncDsssCode.IEEE80211B |
| 84 | chip_rate: confloat(ge=0) = 10.0 |
| 85 | |
| 86 | @abstractmethod |
| 87 | def make_config_widget(self, parent: ttk.Widget) -> ConfigControlFrame: |
| 88 | ... |
| 89 | |
| 90 | @abstractmethod |
| 91 | def _get_chip_rate(self) -> float: |
| 92 | ... |
| 93 | |
| 94 | def make_title(self): |
| 95 | if self.method == SpreadingMethod.DSSS_ASYNC: |
| 96 | return f'DSSS Code: {self.async_dsss_code.value}' |
| 97 | else: |
| 98 | raise ValueError(f'Invalid spreading method {self.method}') |
| 99 | |
| 100 | def _get_code(self) -> SpreadingCode: |
| 101 | if self.method == SpreadingMethod.DSSS_ASYNC: |
| 102 | return self.async_dsss_code.make_code() |
| 103 | else: |
| 104 | raise ValueError(f'Invalid spreading method {self.method}') |
| 105 | |
| 106 | def make_spreader(self, modulation: ModulationMethod, baseband_filter: Filter): |
| 107 | cls = self.method.get_spreader() |
| 108 | return cls( |
| 109 | code=self._get_code(), |
| 110 | chip_rate=self._get_chip_rate(), |
| 111 | modulation=modulation, |
| 112 | baseband_filter=baseband_filter |
| 113 | ) |
| 114 | |
| 115 | def make_despreader(self, modulation: ModulationMethod, baseband_filter: Filter): |
| 116 | cls = self.method.get_despreader() |
| 117 | return cls( |
| 118 | code=self._get_code(), |
| 119 | chip_rate=self._get_chip_rate(), |
| 120 | modulation=modulation, |
| 121 | baseband_filter=baseband_filter |
| 122 | ) |
| 123 | |
| 124 | |
| 125 | def create_spreading_config_class(sample_rate: int, global_chip_rate: Optional[Callable[[], float]] = None): |
| 126 | @ui_create |
| 127 | class _SpreadingConfig(SpreadingConfigBase): |
| 128 | chip_rate: confloat(ge=0, lt=sample_rate/4, multiple_of=(4.0/sample_rate)) = 10.0 |
| 129 | |
| 130 | def make_config_widget(self, parent: ttk.Widget) -> ConfigControlFrame: |
| 131 | frm = ConfigControlFrame(parent) |
| 132 | |
| 133 | ttk.Label(frm, text='Spreading Method:').grid(row=0, column=0) |
| 134 | w = self.ui_create_method_dropdown(frm) |
| 135 | frm.add_widget(w) |
| 136 | w.grid(row=0, column=1) |
| 137 | |
| 138 | ttk.Label(frm, text='DSSS Code:').grid(row=1, column=0) |
| 139 | w = self.ui_create_async_dsss_code_dropdown(frm) |
| 140 | frm.add_widget(w) |
| 141 | w.grid(row=1, column=1) |
| 142 | |
| 143 | # ttk.Label(frm, text='DSSS Code:').grid(row=2, column=0) |
| 144 | # w = self.ui_create_sync_dsss_code_dropdown(frm) |
| 145 | # frm.add_widget(w) |
| 146 | # w.grid(row=2, column=1) |
| 147 | |
| 148 | # ttk.Label(frm, text='FHSS Code:').grid(row=3, column=0) |
| 149 | # w = self.ui_create_fhss_code_dropdown(frm) |
| 150 | # frm.add_widget(w) |
| 151 | # w.grid(row=3, column=1) |
| 152 | |
| 153 | # ttk.Label(frm, text='THSS Code:').grid(row=4, column=0) |
| 154 | # w = self.ui_create_thss_code_dropdown(frm) |
| 155 | # frm.add_widget(w) |
| 156 | # w.grid(row=4, column=1) |
| 157 | |
| 158 | if global_chip_rate is None: |
| 159 | ttk.Label(frm, text='Chip Rate:').grid(row=5, column=0) |
| 160 | w = self.ui_create_chip_rate(frm) |
| 161 | frm.add_widget(w) |
| 162 | w.grid(row=5, column=1) |
| 163 | ttk.Label(frm, text='Hz').grid(row=4, column=2) |
| 164 | |
| 165 | return frm |
| 166 | |
| 167 | def _get_chip_rate(self) -> float: |
| 168 | if global_chip_rate is None: |
| 169 | return self.chip_rate |
| 170 | else: |
| 171 | return global_chip_rate() |
| 172 | |
| 173 | _SpreadingConfig.update_forward_refs(sample_rate=sample_rate) |
| 174 | |
| 175 | return _SpreadingConfig |
| 176 | |
| 177 | |
| 178 | ##################################################################### |
| 179 | # Spectrum Plotting Helper |
| 180 | |
| 181 | |
| 182 | class SpreadedBasebandSpectrum3D: |
| 183 | def __init__(self, gen: RfBandGenerator, symbol_rate: float, samples_per_sym: int): |
| 184 | self.gen = gen |
| 185 | self.symbol_rate = symbol_rate |
| 186 | self.samples_per_sym = samples_per_sym |
| 187 | |
| 188 | def make_plot_data(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: |
| 189 | sample_rate = self.symbol_rate * self.samples_per_sym |
| 190 | base_sig = self.gen.generate_rf_signal(sample_rate) |
| 191 | |
| 192 | symbols_sigs = base_sig.split(self.samples_per_sym) |
| 193 | |
| 194 | x = np.zeros(0) |
| 195 | y = np.zeros(0) |
| 196 | z = np.zeros(0) |
| 197 | for idx, sig in enumerate(symbols_sigs): |
| 198 | f, fd = sig.fft() |
| 199 | x = np.concatenate((x, f)) |
| 200 | y = np.concatenate((y, np.ones(len(f)) * idx)) |
| 201 | z = np.concatenate((z, fd)) |
| 202 | |
| 203 | return np.real(x), np.real(y), np.real(z) |
| 204 | |
| 205 | |
| 206 | ##################################################################### |
| 207 | # Code |
| 208 | |
| 209 | |
| 210 | @dataclass |
| 211 | class CodeSequence: |
| 212 | sequence: Symbols |
| 213 | |
| 214 | def make_qam_bandband_generator(self, chip_rate: float, baseband_filter: Filter) -> QamBasebandGenerator: |
| 215 | return QamBasebandGenerator( |
| 216 | self.sequence, |
| 217 | QamBasebandModulator( |
| 218 | symbol_rate=chip_rate, |
| 219 | method=ModulationMethod.BPSK, |
| 220 | baseband_filter=baseband_filter |
| 221 | ) |
| 222 | ) |
| 223 | |
| 224 | def calc_signal(self, chip_rate: float, sample_rate: float, baseband_filter: Filter = NoFilter()) -> Signal: |
| 225 | return self.make_qam_bandband_generator( |
| 226 | chip_rate, |
| 227 | baseband_filter=baseband_filter |
| 228 | ).generate_tx_baseband_signal(sample_rate) |
| 229 | |
| 230 | |
| 231 | ##################################################################### |
| 232 | # Spreader |
| 233 | |
| 234 | |
| 235 | class Spreader(ABC): |
| 236 | def __init__(self, code: SpreadingCode, chip_rate: float, modulation: ModulationMethod, baseband_filter: Filter): |
| 237 | self.code = code |
| 238 | self.chip_rate = chip_rate |
| 239 | self.modulation = modulation |
| 240 | self.baseband_filter = baseband_filter |
| 241 | |
| 242 | @abstractmethod |
| 243 | def make_tx_baseband_generator(self, message: Symbols) -> IqTxBasebandGenerator: |
| 244 | ... |
| 245 | |
| 246 | def calc_tx_baseband(self, message: Symbols, sample_rate: float) -> Signal: |
| 247 | gen = self.make_tx_baseband_generator(message) |
| 248 | return gen.generate_tx_baseband_signal(sample_rate) |
| 249 | |
| 250 | @abstractmethod |
| 251 | def get_symbol_rate(self) -> float: |
| 252 | ... |
| 253 | |
| 254 | |
| 255 | class AsyncDsssSpreader(Spreader): |
| 256 | def make_tx_baseband_generator(self, message: Symbols) -> IqTxBasebandGenerator: |
| 257 | assert isinstance(self.code, AsyncSpreadingCode), f'Code must be AsyncSpreadingCode' |
| 258 | spread_sequence = [] |
| 259 | for msg_sym in message.reencode(1).symbols: |
| 260 | for code_sym in self.code.get_code().sequence.reencode(1).symbols: |
| 261 | spread_sequence.append(msg_sym ^ code_sym) |
| 262 | spread_sym = Symbols(symbols=spread_sequence, bits_per_symbol=1) |
| 263 | return QamBasebandGenerator( |
| 264 | symbols=spread_sym, |
| 265 | qam_mod=QamBasebandModulator( |
| 266 | method=self.modulation, |
| 267 | symbol_rate=self.chip_rate, |
| 268 | baseband_filter=self.baseband_filter |
| 269 | ) |
| 270 | ) |
| 271 | |
| 272 | def get_symbol_rate(self) -> float: |
| 273 | return self.chip_rate / self.code.get_spreading_factor() |
| 274 | |
| 275 | |
| 276 | ##################################################################### |
| 277 | # Depreader |
| 278 | |
| 279 | |
| 280 | class CrossCorrelationGenerator(RxCorrelationGenerator): |
| 281 | def __init__(self, rf_gen: RfBandGenerator, code_signal_gen: TxBasebandGenerator): |
| 282 | self.rf_gen = rf_gen |
| 283 | self.code_signal_gen = code_signal_gen |
| 284 | |
| 285 | def generate_rx_correlation(self, sample_rate: float) -> Signal: |
| 286 | rf = self.rf_gen.generate_rf_signal(sample_rate) |
| 287 | code = self.code_signal_gen.generate_tx_baseband_signal(sample_rate) |
| 288 | corr = np.correlate(rf.signal, code.signal, mode='same') |
| 289 | return Signal( |
| 290 | t=rf.t, |
| 291 | signal=corr, |
| 292 | sample_rate=sample_rate |
| 293 | ) |
| 294 | |
| 295 | |
| 296 | class Despreader(ABC): |
| 297 | def __init__(self, code: SpreadingCode, chip_rate: float, modulation: ModulationMethod, baseband_filter: Filter): |
| 298 | self.code = code |
| 299 | self.chip_rate = chip_rate |
| 300 | self.modulation = modulation |
| 301 | self.baseband_filter = baseband_filter |
| 302 | |
| 303 | @abstractmethod |
| 304 | def make_rx_correlation_generator(self, rf_gen: RfBandGenerator) -> RxCorrelationGenerator: |
| 305 | ... |
| 306 | |
| 307 | def calc_rx_correlation(self, rf_gen: RfBandGenerator, sample_rate: float) -> Signal: |
| 308 | gen = self.make_rx_correlation_generator(rf_gen) |
| 309 | return gen.generate_rx_correlation(sample_rate) |
| 310 | |
| 311 | |
| 312 | class AsyncDsssDespreader(Despreader): |
| 313 | def make_code_signal_generator(self) -> TxBasebandGenerator: |
| 314 | assert isinstance(self.code, AsyncSpreadingCode), f'Code must be AsyncSpreadingCode' |
| 315 | return self.code.get_code().make_qam_bandband_generator(self.chip_rate, self.baseband_filter) |
| 316 | |
| 317 | def calc_code_signal(self, sample_rate: float) -> Signal: |
| 318 | gen = self.make_code_signal_generator() |
| 319 | return gen.generate_tx_baseband_signal(sample_rate) |
| 320 | |
| 321 | def make_rx_correlation_generator(self, rf_gen: RfBandGenerator) -> RxCorrelationGenerator: |
| 322 | return CrossCorrelationGenerator(rf_gen, self.make_code_signal_generator()) |