FreeDATA/freedata_server/modulator.py

255 lines
10 KiB
Python

import ctypes
import codec2
import structlog
from codec2 import FREEDV_MODE
from codec2 import FREEDV_ADVANCED_FSK
class Modulator:
"""Modulates data using Codec2 and handles transmission parameters.
This class manages the modulation of data using the Codec2 library. It
handles various FreeDV modes, adds preambles, postambles, and silence
to the transmitted data, and creates bursts of modulated frames. It
also initializes and manages Codec2 instances for different modes.
"""
log = structlog.get_logger("RF")
def __init__(self, ctx):
"""Initializes the Modulator with configuration parameters.
Args:
config (dict): Configuration dictionary containing modem settings.
"""
self.ctx = ctx
self.tx_delay = self.ctx.config_manager.config['MODEM']['tx_delay']
self.modem_sample_rate = codec2.api.FREEDV_FS_8000
# Initialize codec2, rig control, and data threads
self.init_codec2()
def init_codec2(self):
"""Initializes Codec2 instances for different FreeDV modes.
This method initializes multiple instances of the Codec2 library,
each corresponding to a different FreeDV mode used for
transmission. This allows for efficient switching between modes
during operation.
"""
# Open codec2 instances
# INIT TX MODES - here we need all modes.
self.freedv_datac0_tx = codec2.open_instance(codec2.FREEDV_MODE.datac0.value)
self.freedv_datac1_tx = codec2.open_instance(codec2.FREEDV_MODE.datac1.value)
self.freedv_datac3_tx = codec2.open_instance(codec2.FREEDV_MODE.datac3.value)
self.freedv_datac4_tx = codec2.open_instance(codec2.FREEDV_MODE.datac4.value)
self.freedv_datac13_tx = codec2.open_instance(codec2.FREEDV_MODE.datac13.value)
self.freedv_datac14_tx = codec2.open_instance(codec2.FREEDV_MODE.datac14.value)
self.data_ofdm_200_tx = codec2.open_instance(codec2.FREEDV_MODE.data_ofdm_200.value)
self.data_ofdm_250_tx = codec2.open_instance(codec2.FREEDV_MODE.data_ofdm_250.value)
self.data_ofdm_500_tx = codec2.open_instance(codec2.FREEDV_MODE.data_ofdm_500.value)
self.data_ofdm_1700_tx = codec2.open_instance(codec2.FREEDV_MODE.data_ofdm_1700.value)
self.data_ofdm_2438_tx = codec2.open_instance(codec2.FREEDV_MODE.data_ofdm_2438.value)
self.data_vhf_1 = codec2.open_instance(codec2.FREEDV_MODE.data_vhf_1.value)
#self.freedv_qam16c2_tx = codec2.open_instance(codec2.FREEDV_MODE.qam16c2.value)
#self.data_qam_2438_tx = codec2.open_instance(codec2.FREEDV_MODE.data_qam_2438.value)
def transmit_add_preamble(self, buffer, freedv):
"""Adds a preamble to the transmit buffer.
This method adds a preamble to the given buffer based on the
provided FreeDV instance. The preamble is generated using the
`freedv_rawdatapreambletx` function from the Codec2 API.
Args:
buffer (bytes): The buffer to add the preamble to.
freedv: The FreeDV instance.
Returns:
bytes: The buffer with the preamble appended.
"""
# Init buffer for preample
n_tx_preamble_modem_samples = codec2.api.freedv_get_n_tx_preamble_modem_samples(
freedv
)
mod_out_preamble = ctypes.create_string_buffer(n_tx_preamble_modem_samples * 2)
# Write preamble to txbuffer
codec2.api.freedv_rawdatapreambletx(freedv, mod_out_preamble)
buffer += bytes(mod_out_preamble)
return buffer
def transmit_add_postamble(self, buffer, freedv):
"""Adds a postamble to the transmit buffer.
This method adds a postamble to the given buffer based on the
provided FreeDV instance. The postamble is generated using the
`freedv_rawdatapostambletx` function from the Codec2 API.
Args:
buffer (bytes): The buffer to add the postamble to.
freedv: The FreeDV instance.
Returns:
bytes: The buffer with the postamble appended.
"""
# Init buffer for postamble
n_tx_postamble_modem_samples = (
codec2.api.freedv_get_n_tx_postamble_modem_samples(freedv)
)
mod_out_postamble = ctypes.create_string_buffer(
n_tx_postamble_modem_samples * 2
)
# Write postamble to txbuffer
codec2.api.freedv_rawdatapostambletx(freedv, mod_out_postamble)
# Append postamble to txbuffer
buffer += bytes(mod_out_postamble)
return buffer
def transmit_add_silence(self, buffer, duration):
"""Adds silence to the transmit buffer.
This method adds a specified duration of silence to the given buffer.
The silence is represented as an empty buffer of the appropriate
size based on the modem sample rate.
Args:
buffer (bytes): The buffer to add silence to.
duration (int): The duration of silence in milliseconds.
Returns:
bytes: The buffer with the silence appended.
"""
data_delay = int(self.modem_sample_rate * (duration / 1000)) # type: ignore
mod_out_silence = ctypes.create_string_buffer(data_delay * 2)
buffer += bytes(mod_out_silence)
return buffer
def transmit_create_frame(self, txbuffer, freedv, frame):
"""Creates and modulates a data frame.
This method creates a data frame with a CRC checksum, modulates it
using the provided FreeDV instance, and appends the modulated data
to the given transmit buffer. It uses the Codec2 API for CRC
generation and modulation.
Args:
txbuffer (bytes): The transmit buffer to append to.
freedv: The FreeDV instance.
frame (bytes): The data frame to modulate.
Returns:
bytes: The updated transmit buffer.
"""
# Get number of bytes per frame for mode
bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv) / 8)
payload_bytes_per_frame = bytes_per_frame - 2
#print(payload_bytes_per_frame)
# Init buffer for data
n_tx_modem_samples = codec2.api.freedv_get_n_tx_modem_samples(freedv)
mod_out = ctypes.create_string_buffer(n_tx_modem_samples * 2)
# Create buffer for data
# Use this if CRC16 checksum is required (DATAc1-3)
buffer = bytearray(payload_bytes_per_frame)
# Set buffersize to length of data which will be sent
buffer[: len(frame)] = frame # type: ignore
# Create crc for data frame -
# Use the crc function shipped with codec2
# to avoid CRC algorithm incompatibilities
# Generate CRC16
crc = ctypes.c_ushort(
codec2.api.freedv_gen_crc16(bytes(buffer), payload_bytes_per_frame)
)
# Convert crc to 2-byte (16-bit) hex string
crc = crc.value.to_bytes(2, byteorder="big")
# Append CRC to data buffer
buffer += crc
assert (bytes_per_frame == len(buffer))
data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer)
# modulate DATA and save it into mod_out pointer
codec2.api.freedv_rawdatatx(freedv, mod_out, data)
txbuffer += bytes(mod_out)
return txbuffer
def create_burst(
self, mode, repeats: int, repeat_delay: int, frames: bytearray
) -> bytes:
"""Creates a burst of modulated frames.
This method creates a burst transmission by repeating the given
frames multiple times with a specified delay between repetitions.
It adds preambles, postambles, and silence to the transmission as
needed. It selects the appropriate FreeDV instance based on the
provided mode and handles potential mode transitions.
Args:
mode: The FreeDV mode to use for modulation.
repeats (int): The number of times to repeat the frames.
repeat_delay (int): The delay between repetitions in milliseconds.
frames (bytearray or list): The frame(s) to modulate and transmit as a burst. Can be a single frame as a bytearray or a list of frames.
Returns:
bytes: The modulated burst data.
"""
# get freedv instance by mode
mode_transition = {
codec2.FREEDV_MODE.signalling_ack: self.freedv_datac14_tx,
codec2.FREEDV_MODE.signalling: self.freedv_datac13_tx,
codec2.FREEDV_MODE.datac0: self.freedv_datac0_tx,
codec2.FREEDV_MODE.datac1: self.freedv_datac1_tx,
codec2.FREEDV_MODE.datac3: self.freedv_datac3_tx,
codec2.FREEDV_MODE.datac4: self.freedv_datac4_tx,
codec2.FREEDV_MODE.datac13: self.freedv_datac13_tx,
codec2.FREEDV_MODE.datac14: self.freedv_datac14_tx,
codec2.FREEDV_MODE.data_ofdm_200: self.data_ofdm_200_tx,
codec2.FREEDV_MODE.data_ofdm_250: self.data_ofdm_250_tx,
codec2.FREEDV_MODE.data_ofdm_500: self.data_ofdm_500_tx,
codec2.FREEDV_MODE.data_ofdm_1700: self.data_ofdm_1700_tx,
codec2.FREEDV_MODE.data_ofdm_2438: self.data_ofdm_2438_tx,
#codec2.FREEDV_MODE.qam16c2: self.freedv_qam16c2_tx,
#codec2.FREEDV_MODE.data_qam_2438: self.freedv_data_qam_2438_tx,
codec2.FREEDV_MODE.data_vhf_1: self.data_vhf_1
}
if mode in mode_transition:
freedv = mode_transition[mode]
else:
print("wrong mode.................")
print(mode)
#return False
# Open codec2 instance
self.MODE = mode
self.log.debug(
"[MDM] TRANSMIT", mode=self.MODE.name, delay=self.tx_delay
)
txbuffer = bytes()
# Add empty data to handle ptt toggle time
if self.tx_delay > 0:
txbuffer = self.transmit_add_silence(txbuffer, self.tx_delay)
if not isinstance(frames, list): frames = [frames]
for _ in range(repeats):
# Create modulation for all frames in the list
for frame in frames:
if not self.ctx.config_manager.config['EXP'].get('enable_vhf'):
txbuffer = self.transmit_add_preamble(txbuffer, freedv)
txbuffer = self.transmit_create_frame(txbuffer, freedv, frame)
if not self.ctx.config_manager.config['EXP'].get('enable_vhf'):
txbuffer = self.transmit_add_postamble(txbuffer, freedv)
# Add delay to end of frames
txbuffer = self.transmit_add_silence(txbuffer, repeat_delay)
return txbuffer