initial commit

main
Carl 2 years ago
parent 38fbf69d7c
commit efaaf39f79
  1. 96
      README.md
  2. 215
      _UI/_web_interface/assets/style.css
  3. 1916
      _UI/_web_interface/kraken_web_interface.py
  4. 210
      _UI/_web_interface/tooltips.py
  5. 102
      _UI/save_settings.py
  6. 152
      _receiver/iq_header.py
  7. 375
      _receiver/krakenSDR_receiver.py
  8. 195
      _receiver/shmemIface.py
  9. 713
      _signal_processing/krakenSDR_signal_processor.py
  10. BIN
      doc/kraken_doadsp_main.png
  11. 19
      gui_run.sh
  12. 5
      kill.sh
  13. 1
      settings.json
  14. 675
      util/LICENCE
  15. 94
      util/README.md
  16. 215
      util/_UI/_web_interface/assets/style.css
  17. 1916
      util/_UI/_web_interface/kraken_web_interface.py
  18. 210
      util/_UI/_web_interface/tooltips.py
  19. 102
      util/_UI/save_settings.py
  20. 152
      util/_receiver/iq_header.py
  21. 375
      util/_receiver/krakenSDR_receiver.py
  22. 195
      util/_receiver/shmemIface.py
  23. 713
      util/_signal_processing/krakenSDR_signal_processor.py
  24. BIN
      util/doc/kraken_doadsp_main.png
  25. 19
      util/gui_run.sh
  26. 5
      util/kill.sh
  27. 11
      util/kraken_doa_start.sh
  28. 5
      util/kraken_doa_stop.sh
  29. 1
      util/settings.json
  30. 1
      util/settings.json_old
  31. 18
      util/setup_init.sh

@ -1,2 +1,94 @@
# krakensdr_pr
Passive Radar Code for the KrakenSDR
# Kraken SDR DoA DSP
This software is intended to demonstrate the direction of arrival (DoA) estimation capabilities of the KrakenSDR and other RTL-SDR based coherent receiver systems which use the compatible data acquisition system - HeIMDALL DAQ Firmware.
<br>
<br>
The complete application is broken down into two main modules in terms of implementation, into the DAQ Subsystem and to the DSP Subsystem. These two modules can operate together either remotely through Ethernet connection or locally on the same host using shared-memory.
Running these two subsystems on separate processing units can grant higher throughput and stability, while running on the same processing unit makes the entire system more compact.
## Installation
1. Install the prerequisites
``` bash
sudo apt update
sudo apt install php-cli
```
2. Install Heimdall DAQ
If not done already, first, follow the instructions at https://github.com/krakenrf/heimdall_daq_fw/tree/development to install the Heimdall DAQ Firmware.
3. Set up Miniconda environment
You will have created a Miniconda environment during the Heimdall DAQ install phase.
Please run the installs in this order as we need to ensure a specific version of dash is installed.
``` bash
conda activate kraken
conda install quart
conda install pandas
conda install orjson
conda install matplotlib
pip3 install dash_bootstrap_components
pip3 install quart_compress
pip3 install dash_devices
pip3 install pyargus
conda install dash==1.20.0
```
4. Install the krakensdr_doa software
```bash
cd ~/krakensdr
git clone https://github.com/krakenrf/krakensdr_doa
cd krakensdr_doa
git checkout clientside_graphs
```
Copy the the *krakensdr_doa/util/kraken_doa_start.sh* and the *krakensdr_doa/util/kraken_doa_stop.sh* scripts into the krakensdr root folder of the project.
```bash
cd ~/krakensdr
cp krakensdr_doa/util/kraken_doa_start.sh .
cp krakensdr_doa/util/kraken_doa_stop.sh .
```
## Running
### Local operation (Recommended)
```bash
./kraken_doa_start.sh
```
Please be patient on the first run, at it can take 1-2 minutes for the JIT numba compiler to compile the numba optimized functions, and during this compilation time it may appear that the software has gotten stuck. On subsqeuent runs this loading time will be much faster as it will read from cache.
### Remote operation
1. Start the DAQ Subsystem either remotely. (Make sure that the *daq_chain_config.ini* contains the proper configuration)
(See:https://github.com/krakenrf/heimdall_daq_fw/Documentation)
2. Set the IP address of the DAQ Subsystem in the settings.json, *default_ip* field.
3. Start the DoA DSP software by typing:
`./gui_run.sh`
4. To stop the server and the DSP processing chain run the following script:
`./kill.sh`
<p1> After starting the script a web based server opens at port number 8051, which then can be accessed by typing "KRAKEN_IP:8050/" in the address bar of any web browser. You can find the IP address of the KrakenSDR Pi4 wither via your routers WiFi management page, or by typing "ip addr" into the terminal. You can also use the hostname of the Pi4 in place of the IP address, but this only works on local networks, and not the internet, or mobile hotspot networks. </p1>
![image info](./doc/kraken_doadsp_main.png)
## Upcoming Features and Known Bugs
1. [FEATURE] Currently squelch works by selecting the strongest signal that is active and above the set threshold within the active bandwidth. The next steps will be to allow users to create multiple channels within the active bandwidth, each with their own squelch. This will allow users to track multiple signals at once, and ignore unwated signals within the bandwidth at the same time.
2. [FEATURE] It would be better if the KrakenSDR controls, spectrum and/or DOA graphs could be accessible from the same page. Future work will look to integrate the controls in a sidebar.
3. [FEATURE] Some users would like to monitor the spectrum, and manually click on an active signal to DF that particular signal. We will be looking at a way to implement this.
4. [BUG] Sometimes the DOA graphs will not load properly and refreshing the page is required. A fix is being investigated.
This software was 95% developed by Tamas Peto, and makes use of his pyAPRIL and pyARGUS libraries. See his website at www.tamaspeto.com

@ -0,0 +1,215 @@
/*Globlal Properties*/
/*Tags*/
body {
background-color: #000000;
color:#ffffff;
margin: 0px;
padding: 0px;
}
*{
font-size:18px;
font-family: 'Montserrat', sans-serif;
}
h1{
font-size:22px;
}
a{
font-size: 22px;
}
input[type="checkbox"]{
width:20px;
height:20px;
background:white;
border-radius:5px;
border:2px solid #555;
vertical-align: text-bottom;
}
.Select-control {
background-color:#000000;
}
.Select-value {
background-color: rgb(255, 102, 0);
color:#000000;
}
.Select-menu-outer {
background-color: rgb(0, 0, 0);
}
input, select{
width: 100%;
}
iframe{
background-color: transparent;
border: 0px none transparent;
padding: 0px;
overflow: hidden;
}
/*Classes*/
.header{
background-color: #000000;
width: 100%;
text-align: center;
padding: 10px;
}
.header a{
color: white;
text-transform: uppercase;
text-decoration: none;
padding: 5px;
line-height: 40px;
border: 0.1em solid #ffffff;
border-radius:0.05em;
margin: 5px;
}
.header_active, .header a:hover{
background-color: #ff6600c0;
}
.ctr_toolbar{
background-color: #000000;
height: 50px;
width: 100%;
text-align: center;
}
.ctr_toolbar_item {
text-align: center;
display : inline-block;
position : relative;
margin : 0 auto;
padding : 0px;
float : center;
}
.doa_check{
text-align: left;
}
.btn{
height: 40px;
cursor: pointer;
background-color: #141414;
border: 0.1em solid #000000;
border-radius:0.12em;
color: white;
font-size:20px;
text-decoration: none;
text-align: center;
width: 100%;
transition: all 0.2s;
}
.btn:hover{
color: rgb(0, 0, 0);
background-color: #ff6600;
}
.btn a{
color: white;
text-decoration: none;
position: relative;
top: 15%;
}
.btn_start{
height: 40px;
cursor: pointer;
background-color: #02c93d;
border: 0.1em solid #000000;
border-radius:0.12em;
color: rgb(0, 0, 0);
font-size:20px;
text-decoration: none;
text-align: center;
width: 100%;
transition: all 0.2s;
}
.btn_start:hover{
color: rgb(0, 0, 0);
background-color: #ff6600;
}
.btn_stop{
height: 40px;
cursor: pointer;
background-color: #c40404;
border: 0.1em solid #000000;
border-radius:0.12em;
color: rgb(0, 0, 0);
font-size:20px;
text-decoration: none;
text-align: center;
width: 100%;
}
.btn_stop:hover{
color: rgb(0, 0, 0);
background-color: #ff6600;
}
.btn_save_cfg{
height: 40px;
cursor: pointer;
background-color: #b5aef5;
border: 0.1em solid #000000;
border-radius:0.12em;
color: rgb(0, 0, 0);
font-size:20px;
text-decoration: none;
text-align: center;
width: 100%;
}
.btn_save_cfg:hover{
color: rgb(154, 233, 102);
background-color: #ff6600;
}
.tooltip{
color: rgb(0, 0, 0);
background-color: #ffffff;
opacity: 0.95;
border-radius:0.2em;
width: 300px;
}
.card{
background-color: #000000;
border: 0.1em solid #ffffff;
border-radius:0.2em;
width: 400px;
max-width: 500px;
padding: 20px;
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.46);
margin: 10px;
float: left;
}
.monitor_card{
background-color: #000000;
border: 0.1em solid #ffffff;
border-radius:0.2em;
width: 95%;
height: 800px;
overflow:scroll;
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.46);
margin: auto;
}
.field{
width: 400px;
display: block;
margin: 10px auto;
padding: 0px;
}
.field-label{
width: 250px;
display: inline-block;
vertical-align: top;
}
.field-body{
width: 144px;
display: inline-block;
}
/*Mobile Properties*/
@media screen and (max-width: 1072px) {
.card{
width: calc( 100% - 30px );
padding: 5px;
margin: 20px auto;
float: none;
}
}
@media screen and (max-width: 500px) {
.field, .field-label, .field-body, input{
width: calc( 100% - 15px );
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,210 @@
import dash_html_components as html
import dash_bootstrap_components as dbc
dsp_config_tooltips = html.Div([
# Antenna arrangement selection
dbc.Tooltip([
html.P("ULA - Uniform Linear Array"),
html.P("Antenna elements placed on a line with having equal distances between each other"),
html.P("UCA - Uniform Circular Array"),
html.P("Antenna elements are placed on circle equaly distributed on 360°")],
target="label_ant_arrangement",
placement="bottom",
className="tooltip"
),
# Antenna Spacing
# dbc.Tooltip([
# html.P("When ULA is selected: Spacing between antenna elements"),
# html.P("When UCA is selected: Radius of the circle on which the elements are placed")],
# target="label_ant_spacing",
# placement="bottom",
# className="tooltip"
# ),
# Enable F-B averaging
dbc.Tooltip([
html.P("Forward-backward averegaing improves the performance of DoA estimation in multipath environment"),
html.P("(Available only for ULA antenna systems)")],
target="label_en_fb_avg",
placement="bottom",
className="tooltip"
),
])
daq_ini_config_tooltips = html.Div([
# DAQ buffer size
dbc.Tooltip([
html.P("Buffer size of the realtek driver")],
target="label_daq_buffer_size",
placement="bottom",
className="tooltip"
),
# Sampling frequency
dbc.Tooltip([
html.P("Raw - ADC sampling frequency of the realtek chip")],
target="label_sample_rate",
placement="bottom",
className="tooltip"
),
# Enable noise source control
dbc.Tooltip([
html.P("Enables the utilization of the built-in noise source for calibration")],
target="label_en_noise_source_ctr",
placement="bottom",
className="tooltip"
),
# Enable squelch mode
dbc.Tooltip([
html.P("Enable DAQ-side squelch to capture burst like signals - NOTE DISABLED IN THIS VERSION, THIS VERSION USES DSP SIDE SQUELCH ONLY")],
target="label_en_squelch",
placement="bottom",
className="tooltip"
),
# Squelch threshold
dbc.Tooltip([
html.P("Amplitude threshold used for the squelch feature."),
html.P("Should take values on range: 0...1"),
html.P("When set to zero the squelch is bypassed")],
target="label_squelch_init_threshold",
placement="bottom",
className="tooltip"
),
# CPI size
dbc.Tooltip([
html.P("Length of the Coherent Processing Interval (CPI) after decimation")],
target="label_cpi_size",
placement="bottom",
className="tooltip"
),
# Decimation raito
dbc.Tooltip([
html.P("Decimation factor")],
target="label_decimation_ratio",
placement="bottom",
className="tooltip"
),
# FIR relative bandwidth
dbc.Tooltip([
html.P("Anti-aliasing filter bandwith after decimation"),
html.P("Should take values on range: (0, 1]"),
html.P("E.g.: ADC sampling frequency: 1 MHz (IQ!) , Decimation ratio: 2, FIR relative bandwith:0.25"),
html.P("Resulting passband bandwidth: 125 kHz ")],
target="label_fir_relative_bw",
placement="bottom",
className="tooltip"
),
# FIR tap size
dbc.Tooltip([
html.P("Anti-aliasing FIR filter tap size - Do not set too large, or CPU utilization will be 100%"),
html.P("Should be greater than the decimation ratio")],
target="label_fir_tap_size",
placement="bottom",
className="tooltip"
),
# FIR tap size
dbc.Tooltip([
html.P("Window function type for designing the anti-aliasing FIR filter"),
html.P("https://en.wikipedia.org/wiki/Window_function")],
target="label_fir_window",
placement="bottom",
className="tooltip"
),
# Enable filter reset
dbc.Tooltip([
html.P("If enabled, the memory of the anti-aliasing FIR filter is reseted at the begining of every new CPI")],
target="label_en_filter_reset",
placement="bottom",
className="tooltip"
),
# Correlation size
dbc.Tooltip([
html.P("Number of samples used for the calibration procedure (sample delay and IQ compensation)")],
target="label_correlation_size",
placement="bottom",
className="tooltip"
),
# Standard channel index
dbc.Tooltip([
html.P("The selected channel is used as a reference for the IQ compensation")],
target="label_std_ch_index",
placement="bottom",
className="tooltip"
),
# Enable IQ calibration
dbc.Tooltip([
html.P("Enables to compensate the amplitude and phase differences of the receiver channels")],
target="label_en_iq_calibration",
placement="bottom",
className="tooltip"
),
# Gain lock interval
dbc.Tooltip([
html.P("Minimum number of stable frames before terminating the gain tuning procedure")],
target="label_gain_lock_interval",
placement="bottom",
className="tooltip"
),
# Require track lock intervention
dbc.Tooltip([
html.P("When enabled the DAQ firmware waits for manual intervention during the calibraiton procedure"),
html.P("Should be used only for hardave version 1.0")],
target="label_require_track_lock",
placement="bottom",
className="tooltip"
),
# Amplitude calibraiton mode
dbc.Tooltip([
html.P("Amplitude difference compensation method applied as part of the IQ compensation"),
html.P("default: Amplitude differences are estimated by calculating the cross-correlations of the channels"),
html.P("disabled: Amplitude differences are not compensated"),
html.P("channel_power: Ampltiude compensation is set in a way to achieve equal channel powers")],
target="label_amplitude_calibration_mode",
placement="bottom",
className="tooltip"
),
dbc.Tooltip([
html.P("When periodic calibration track mode is selected the firmware regularly turn on the noise source for a short burst to\
check whether the IQ calibration is still valid or not. In case the calibrated state is lost, the firmware automatically\
initiates a reclaibration procedure")],
target="label_calibration_track_mode",
placement="bottom",
className="tooltip"
),
# Calibration frame interval
dbc.Tooltip([
html.P("Number of data frames between two consecutive calibration burst. Used when periodic calibration mode is selected")],
target="label_calibration_frame_interval",
placement="bottom",
className="tooltip"
),
# Calibration frame burst size
dbc.Tooltip([
html.P("Number of calibration frames generated in the periodic calibration mode")],
target="label_calibration_frame_burst_size",
placement="bottom",
className="tooltip"
),
# Amplitude tolerance
dbc.Tooltip([
html.P("Maximum allowed amplitude difference between the receiver channels")],
target="label_amplitude_tolerance",
placement="bottom",
className="tooltip"
),
# Phase tolerance
dbc.Tooltip([
html.P("Maximum allowed phase difference between the receiver channels")],
target="label_phase_tolerance",
placement="bottom",
className="tooltip"
),
# Maximum sync fails
dbc.Tooltip([
html.P("Maximum allowed consecutive IQ difference check failures before initiating a recalibration")],
target="label_max_sync_fails",
placement="bottom",
className="tooltip"
),
])

@ -0,0 +1,102 @@
import json
import os
"""
Handles the DoA DSP settings
Project: Kraken DoA DSP
Author : Tamas Peto
"""
root_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
settings_file_path = os.path.join(root_path,"settings.json")
settings_found =False
if os.path.exists(settings_file_path):
settings_found = True
with open(settings_file_path, 'r') as myfile:
settings=json.loads(myfile.read())
# DAQ Configuration
center_freq = settings.get("center_freq", 100.0)
uniform_gain = settings.get("uniform_gain", 1.4)
data_interface = settings.get("data_interface", "eth")
default_ip = settings.get("default_ip", "0.0.0.0")
# PR Paramaters
en_pr = settings.get("en_pr", 0)
clutter_cancel_algo = settings.get("clutter_cancel_algo", "Wiener MRE")
max_bistatic_range = settings.get("max_bistatic_range", 128)
max_doppler = settings.get("max_doppler", 256)
en_pr_persist = settings.get("en_pr_persist", 1)
pr_persist_decay = settings.get("pr_persist_decay", 0.99)
pr_dynrange_min = settings.get("pr_dynrange_min", -20)
pr_dynrange_max = settings.get("pr_dynrange_max", 100)
#ant_arrangement = settings.get("ant_arrangement", "ULA")
#ant_spacing = settings.get("ant_spacing", 0.5)
#doa_method = settings.get("doa_method", "MUSIC")
#en_fbavg = settings.get("en_fbavg", 0)
#compass_offset = settings.get("compass_offset", 0)
#doa_fig_type = settings.get("doa_fig_type", "Linear plot")
# DSP misc
#en_squelch = settings.get("en_squelch", 0)
#squelch_threshold_dB = settings.get("squelch_threshold_dB", 0.0)
# Web Interface
en_hw_check = settings.get("en_hw_check", 0)
en_advanced_daq_cfg = settings.get("en_advanced_daq_cfg", 0)
logging_level = settings.get("logging_level", 0)
disable_tooltips = settings.get("disable_tooltips", 0)
# Check and correct if needed
#if not ant_arrangement in ["ULA", "UCA"]:
# ant_arrangement="ULA"
#doa_method_dict = {"Bartlett":0, "Capon":1, "MEM":2, "MUSIC":3}
#if not doa_method in doa_method_dict:
# doa_method = "MUSIC"
#doa_fig_type_dict = {"Linear plot":0, "Polar plot":1, "Compass":2}
#if not doa_fig_type in doa_fig_type_dict:
# doa_gfig_type="Linear plot"
def write(data = None):
if data is None:
data = {}
# DAQ Configuration
data["center_freq"] = center_freq
data["uniform_gain"] = uniform_gain
data["data_interface"] = data_interface
data["default_ip"] = default_ip
# DOA Estimation
data["en_pr"] = en_pr
data["clutter_cancel_algo"] = clutter_cancel_algo
data["max_bistatic_range"] = max_bistatic_range
data["max_doppler"] = max_doppler
data["en_pr_persist"] = en_pr_persist
data["pr_persist_decay"] = pr_persist_decay
data["pr_dynrange_min"] = pr_dynrange_min
data["pr_dynrange_max"] = pr_dynrange_max
#data["ant_arrangement"] = ant_arrangement
#data["ant_spacing"] = ant_spacing
#data["doa_method"] = doa_method
#data["en_fbavg"] = en_fbavg
#data["compass_offset"] = compass_offset
#data["doa_fig_tpye"] = doa_fig_type
# DSP misc
#data["en_squelch"] = en_squelch
#data["squelch_threshold_dB"] = squelch_threshold_dB
# Web Interface
data["en_hw_check"] = en_hw_check
data["en_advanced_daq_cfg"] = en_advanced_daq_cfg
data["logging_level"] = logging_level
data["disable_tooltips"] = disable_tooltips
with open(settings_file_path, 'w') as outfile:
json.dump(data, outfile)

@ -0,0 +1,152 @@
from struct import pack,unpack
import logging
import sys
"""
Desctiption: IQ Frame header definition
For header field description check the corresponding documentation
Total length: 1024 byte
Project: HeIMDALL RTL
Author: Tamás Pető
Status: Finished
Version history:
1 : Initial version (2019 04 23)
2 : Fixed 1024 byte length (2019 07 25)
3 : Noise source state (2019 10 01)
4 : IQ sync flag (2019 10 21)
5 : Sync state (2019 11 10)
6 : Unix Epoch timestamp (2019 12 17)
6a: Frame type defines (2020 03 19)
7 : Sync word (2020 05 03)
"""
class IQHeader():
FRAME_TYPE_DATA = 0
FRAME_TYPE_DUMMY = 1
FRAME_TYPE_RAMP = 2
FRAME_TYPE_CAL = 3
FRAME_TYPE_TRIGW = 4
SYNC_WORD = 0x2bf7b95a
def __init__(self):
self.logger = logging.getLogger(__name__)
self.header_size = 1024 # size in bytes
self.reserved_bytes = 192
self.sync_word=self.SYNC_WORD # uint32_t
self.frame_type=0 # uint32_t
self.hardware_id="" # char [16]
self.unit_id=0 # uint32_t
self.active_ant_chs=0 # uint32_t
self.ioo_type=0 # uint32_t
self.rf_center_freq=0 # uint64_t
self.adc_sampling_freq=0 # uint64_t
self.sampling_freq=0 # uint64_t
self.cpi_length=0 # uint32_t
self.time_stamp=0 # uint64_t
self.daq_block_index=0 # uint32_t
self.cpi_index=0 # uint32_t
self.ext_integration_cntr=0 # uint64_t
self.data_type=0 # uint32_t
self.sample_bit_depth=0 # uint32_t
self.adc_overdrive_flags=0 # uint32_t
self.if_gains=[0]*32 # uint32_t x 32
self.delay_sync_flag=0 # uint32_t
self.iq_sync_flag=0 # uint32_t
self.sync_state=0 # uint32_t
self.noise_source_state=0 # uint32_t
self.reserved=[0]*self.reserved_bytes# uint32_t x reserverd_bytes
self.header_version=0 # uint32_t
def decode_header(self, iq_header_byte_array):
"""
Unpack,decode and store the content of the iq header
"""
iq_header_list = unpack("II16sIIIQQQIQIIQIII"+"I"*32+"IIII"+"I"*self.reserved_bytes+"I", iq_header_byte_array)
self.sync_word = iq_header_list[0]
self.frame_type = iq_header_list[1]
self.hardware_id = iq_header_list[2].decode()
self.unit_id = iq_header_list[3]
self.active_ant_chs = iq_header_list[4]
self.ioo_type = iq_header_list[5]
self.rf_center_freq = iq_header_list[6]
self.adc_sampling_freq = iq_header_list[7]
self.sampling_freq = iq_header_list[8]
self.cpi_length = iq_header_list[9]
self.time_stamp = iq_header_list[10]
self.daq_block_index = iq_header_list[11]
self.cpi_index = iq_header_list[12]
self.ext_integration_cntr = iq_header_list[13]
self.data_type = iq_header_list[14]
self.sample_bit_depth = iq_header_list[15]
self.adc_overdrive_flags = iq_header_list[16]
self.if_gains = iq_header_list[17:49]
self.delay_sync_flag = iq_header_list[49]
self.iq_sync_flag = iq_header_list[50]
self.sync_state = iq_header_list[51]
self.noise_source_state = iq_header_list[52]
self.header_version = iq_header_list[52+self.reserved_bytes+1]
def encode_header(self):
"""
Pack the iq header information into a byte array
"""
iq_header_byte_array=pack("II", self.sync_word, self.frame_type)
iq_header_byte_array+=self.hardware_id.encode()+bytearray(16-len(self.hardware_id.encode()))
iq_header_byte_array+=pack("IIIQQQIQIIQIII",
self.unit_id, self.active_ant_chs, self.ioo_type, self.rf_center_freq, self.adc_sampling_freq,
self.sampling_freq, self.cpi_length, self.time_stamp, self.daq_block_index, self.cpi_index,
self.ext_integration_cntr, self.data_type, self.sample_bit_depth, self.adc_overdrive_flags)
for m in range(32):
iq_header_byte_array+=pack("I", self.if_gains[m])
iq_header_byte_array+=pack("I", self.delay_sync_flag)
iq_header_byte_array+=pack("I", self.iq_sync_flag)
iq_header_byte_array+=pack("I", self.sync_state)
iq_header_byte_array+=pack("I", self.noise_source_state)
for m in range(self.reserved_bytes):
iq_header_byte_array+=pack("I",0)
iq_header_byte_array+=pack("I", self.header_version)
return iq_header_byte_array
def dump_header(self):
"""
Prints out the content of the header in human readable format
"""
self.logger.info("Sync word: {:d}".format(self.sync_word))
self.logger.info("Header version: {:d}".format(self.header_version))
self.logger.info("Frame type: {:d}".format(self.frame_type))
self.logger.info("Hardware ID: {:16}".format(self.hardware_id))
self.logger.info("Unit ID: {:d}".format(self.unit_id))
self.logger.info("Active antenna channels: {:d}".format(self.active_ant_chs))
self.logger.info("Illuminator type: {:d}".format(self.ioo_type))
self.logger.info("RF center frequency: {:.2f} MHz".format(self.rf_center_freq/10**6))
self.logger.info("ADC sampling frequency: {:.2f} MHz".format(self.adc_sampling_freq/10**6))
self.logger.info("IQ sampling frequency {:.2f} MHz".format(self.sampling_freq/10**6))
self.logger.info("CPI length: {:d}".format(self.cpi_length))
self.logger.info("Unix Epoch timestamp: {:d}".format(self.time_stamp))
self.logger.info("DAQ block index: {:d}".format(self.daq_block_index))
self.logger.info("CPI index: {:d}".format(self.cpi_index))
self.logger.info("Extended integration counter {:d}".format(self.ext_integration_cntr))
self.logger.info("Data type: {:d}".format(self.data_type))
self.logger.info("Sample bit depth: {:d}".format(self.sample_bit_depth))
self.logger.info("ADC overdrive flags: {:d}".format(self.adc_overdrive_flags))
for m in range(32):
self.logger.info("Ch: {:d} IF gain: {:.1f} dB".format(m, self.if_gains[m]/10))
self.logger.info("Delay sync flag: {:d}".format(self.delay_sync_flag))
self.logger.info("IQ sync flag: {:d}".format(self.iq_sync_flag))
self.logger.info("Sync state: {:d}".format(self.sync_state))
self.logger.info("Noise source state: {:d}".format(self.noise_source_state))
def check_sync_word(self):
"""
Check the sync word of the header
"""
if self.sync_word != self.SYNC_WORD:
return -1
else:
return 0

@ -0,0 +1,375 @@
# KrakenSDR Receiver
# Copyright (C) 2018-2021 Carl Laufer, Tamás Pető
#
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# -*- coding: utf-8 -*-
# Import built-in modules
import sys
import os
import time
from struct import pack, unpack
import socket
import _thread
from threading import Lock
import queue
import logging
#import copy
# Import third party modules
import numpy as np
from scipy import signal
from iq_header import IQHeader
from shmemIface import inShmemIface
class ReceiverRTLSDR():
def __init__(self, data_que, data_interface = "eth", logging_level=10):
"""
Parameter:
----------
:param: data_que: Que to communicate with the UI (web iface/Qt GUI)
:param: data_interface: This field is configured by the GUI during instantiation.
Valid values are the followings:
"eth" : The module will receiver IQ frames through an Ethernet connection
"shmem": The module will receiver IQ frames through a shared memory interface
:type : data_interface: string
"""
self.logger = logging.getLogger(__name__)
self.logger.setLevel(logging_level)
# DAQ parameters
# These values are used by default to configure the DAQ through the configuration interface
# Values are configured externally upon configuration request
self.daq_center_freq = 100 # MHz
self.daq_rx_gain = 0 # [dB]
self.daq_squelch_th_dB = 0
# UI interface
self.data_que = data_que
# IQ data interface
self.data_interface = data_interface
# -> Ethernet
self.receiver_connection_status = False
self.port = 5000
self.rec_ip_addr = "127.0.0.1" # Configured by the GUI prior to connection request
self.socket_inst = socket.socket()
self.receiverBufferSize = 2 ** 18 # Size of the Ethernet receiver buffer measured in bytes
# -> Shared memory
root_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
daq_path = os.path.join(os.path.dirname(root_path),"heimdall_daq_fw")
self.daq_shmem_control_path = os.path.join(os.path.join(daq_path,"Firmware"),"_data_control/")
self.init_data_iface()
# Control interface
self.ctr_iface_socket = socket.socket()
self.ctr_iface_port = 5001
self.ctr_iface_thread_lock = Lock() # Used to synchronize the operation of the ctr_iface thread
self.iq_frame_bytes = None
self.iq_samples = None
self.iq_header = IQHeader()
self.M = 0 # Number of receiver channels, updated after establishing connection
def init_data_iface(self):
if self.data_interface == "shmem":
# Open shared memory interface to capture the DAQ firmware output
self.in_shmem_iface = inShmemIface("delay_sync_iq", self.daq_shmem_control_path)
if not self.in_shmem_iface.init_ok:
self.logger.critical("Shared memory initialization failed")
self.in_shmem_iface.destory_sm_buffer()
return -1
return 0
def eth_connect(self):
"""
Compatible only with DAQ firmwares that has the IQ streaming mode.
HeIMDALL DAQ Firmware version: 1.0 or later
"""
try:
if not self.receiver_connection_status:
if self.data_interface == "eth":
# Establlish IQ data interface connection
self.socket_inst.connect((self.rec_ip_addr, self.port))
self.socket_inst.sendall(str.encode('streaming'))
test_iq = self.receive_iq_frame()
self.M = self.iq_header.active_ant_chs
# Establish control interface connection
self.ctr_iface_socket.connect((self.rec_ip_addr, self.ctr_iface_port))
self.receiver_connection_status = True
self.ctr_iface_init()
self.logger.info("CTR INIT Center freq: {0}".format(self.daq_center_freq))
self.set_center_freq(self.daq_center_freq)
self.set_if_gain(self.daq_rx_gain)
self.set_squelch_threshold(self.daq_squelch_th_dB)
except:
errorMsg = sys.exc_info()[0]
self.logger.error("Error message: "+str(errorMsg))
self.receiver_connection_status = False
self.logger.error("Unexpected error: {0}".format(sys.exc_info()[0]))
# Re-instantiating sockets
self.socket_inst = socket.socket()
self.ctr_iface_socket = socket.socket()
return -1
self.logger.info("Connection established")
que_data_packet = []
que_data_packet.append(['conn-ok',])
self.data_que.put(que_data_packet)
def eth_close(self):
"""
Close Ethernet conenctions including the IQ data and the control interfaces
"""
try:
if self.receiver_connection_status:
if self.data_interface == "eth":
self.socket_inst.sendall(str.encode('q')) # Send exit message
self.socket_inst.close()
self.socket_inst = socket.socket() # Re-instantiating socket
# Close control interface connection
exit_message_bytes=("EXIT".encode()+bytearray(124))
self.ctr_iface_socket.send(exit_message_bytes)
self.ctr_iface_socket.close()
self.ctr_iface_socket = socket.socket()
self.receiver_connection_status = False
que_data_packet = []
que_data_packet.append(['disconn-ok',])
self.data_que.put(que_data_packet)
except:
errorMsg = sys.exc_info()[0]
self.logger.error("Error message: {0}".format(errorMsg))
return -1
if self.data_interface == "shmem":
self.in_shmem_iface.destory_sm_buffer()
return 0
def get_iq_online(self):
"""
This function obtains a new IQ data frame through the Ethernet IQ data or the shared memory interface
"""
# Check connection
if not self.receiver_connection_status:
fail = self.eth_connect()
if fail:
return -1
if self.data_interface == "eth":
self.socket_inst.sendall(str.encode("IQDownload")) # Send iq request command
self.iq_samples = self.receive_iq_frame()
elif self.data_interface == "shmem":
active_buff_index = self.in_shmem_iface.wait_buff_free()
if active_buff_index < 0 or active_buff_index > 1:
self.logger.info("Terminating.., signal: {:d}".format(active_buff_index))
return -1
buffer = self.in_shmem_iface.buffers[active_buff_index]
iq_header_bytes = buffer[0:1024].tobytes()
self.iq_header.decode_header(iq_header_bytes)
# Inititalization from header - Set channel numbers
if self.M == 0:
self.M = self.iq_header.active_ant_chs
incoming_payload_size = self.iq_header.cpi_length*self.iq_header.active_ant_chs*2*int(self.iq_header.sample_bit_depth/8)
if incoming_payload_size > 0:
iq_samples_in = (buffer[1024:1024 + incoming_payload_size].view(dtype=np.complex64))\
.reshape(self.iq_header.active_ant_chs, self.iq_header.cpi_length)
self.iq_samples = iq_samples_in.copy() # Must be .copy
self.in_shmem_iface.send_ctr_buff_ready(active_buff_index)
def receive_iq_frame(self):
"""
Called by the get_iq_online function. Receives IQ samples over the establed Ethernet connection
"""
total_received_bytes = 0
recv_bytes_count = 0
iq_header_bytes = bytearray(self.iq_header.header_size) # allocate array
view = memoryview(iq_header_bytes) # Get buffer
self.logger.debug("Starting IQ header reception")
while total_received_bytes < self.iq_header.header_size:
# Receive into buffer
recv_bytes_count = self.socket_inst.recv_into(view, self.iq_header.header_size-total_received_bytes)
view = view[recv_bytes_count:] # reset memory region
total_received_bytes += recv_bytes_count
self.iq_header.decode_header(iq_header_bytes)
# Uncomment to check the content of the IQ header
#self.iq_header.dump_header()
incoming_payload_size = self.iq_header.cpi_length*self.iq_header.active_ant_chs*2*int(self.iq_header.sample_bit_depth/8)
if incoming_payload_size > 0:
# Calculate total bytes to receive from the iq header data
total_bytes_to_receive = incoming_payload_size
receiver_buffer_size = 2**18
self.logger.debug("Total bytes to receive: {:d}".format(total_bytes_to_receive))
total_received_bytes = 0
recv_bytes_count = 0
iq_data_bytes = bytearray(total_bytes_to_receive + receiver_buffer_size) # allocate array
view = memoryview(iq_data_bytes) # Get buffer
while total_received_bytes < total_bytes_to_receive:
# Receive into buffer
recv_bytes_count = self.socket_inst.recv_into(view, receiver_buffer_size)
view = view[recv_bytes_count:] # reset memory region
total_received_bytes += recv_bytes_count
self.logger.debug(" IQ data succesfully received")
# Convert raw bytes to Complex float64 IQ samples
self.iq_samples = np.frombuffer(iq_data_bytes[0:total_bytes_to_receive], dtype=np.complex64).reshape(self.iq_header.active_ant_chs, self.iq_header.cpi_length)
self.iq_frame_bytes = bytearray()+iq_header_bytes+iq_data_bytes
return self.iq_samples
else:
return 0
def set_squelch_threshold(self, threshold_dB):
"""
Configures the threshold level of the squelch module in the DAQ FW through the control interface
"""
if self.receiver_connection_status: # Check connection
self.daq_squelch_th_dB = threshold_dB
if threshold_dB == -80: threshold = 0
else: threshold = 10**(threshold_dB/20)
# Assembling message
cmd="STHU"
th_bytes=pack("f",threshold)
msg_bytes=(cmd.encode()+th_bytes+bytearray(120))
try:
_thread.start_new_thread(self.ctr_iface_communication, (msg_bytes,))
except:
errorMsg = sys.exc_info()[0]
self.logger.error("Unable to start communication thread")
self.logger.error("Error message: {:s}".format(errorMsg))
def ctr_iface_init(self):
"""
Initialize connection with the DAQ FW through the control interface
"""
if self.receiver_connection_status: # Check connection
# Assembling message
cmd="INIT"
msg_bytes=(cmd.encode()+bytearray(124))
try:
_thread.start_new_thread(self.ctr_iface_communication, (msg_bytes,))
except:
errorMsg = sys.exc_info()[0]
self.logger.error("Unable to start communication thread")
self.logger.error("Error message: {:s}".format(errorMsg))
def ctr_iface_communication(self, msg_bytes):
"""
Handles communication on the control interface with the DAQ FW
Parameters:
-----------
:param: msg: Message bytes, that will be sent ont the control interface
:type: msg: Byte array
"""
self.ctr_iface_thread_lock.acquire()
self.logger.debug("Sending control message")
self.ctr_iface_socket.send(msg_bytes)
# Waiting for the command to take effect
reply_msg_bytes = self.ctr_iface_socket.recv(128)
self.logger.debug("Control interface communication finished")
self.ctr_iface_thread_lock.release()
status = reply_msg_bytes[0:4].decode()
if status == "FNSD":
self.logger.info("Reconfiguration succesfully finished")
que_data_packet = []
que_data_packet.append(['config-ok',])
self.data_que.put(que_data_packet)
else:
self.logger.error("Failed to set the requested parameter, reply: {0}".format(status))
def set_center_freq(self, center_freq):
"""
Configures the RF center frequency of the receiver through the control interface
Paramters:
----------
:param: center_freq: Required center frequency to set [Hz]
:type: center_freq: float
"""
if self.receiver_connection_status: # Check connection
self.daq_center_freq = int(center_freq)
# Set center frequency
cmd="FREQ"
freq_bytes=pack("Q",int(center_freq))
msg_bytes=(cmd.encode()+freq_bytes+bytearray(116))
try:
_thread.start_new_thread(self.ctr_iface_communication, (msg_bytes,))
except:
errorMsg = sys.exc_info()[0]
self.logger.error("Unable to start communication thread")
self.logger.error("Error message: {:s}".format(errorMsg))
def set_if_gain(self, gain):
"""
Configures the IF gain of the receiver through the control interface
Paramters:
----------
:param: gain: IF gain value [dB]
:type: gain: int
"""
if self.receiver_connection_status: # Check connection
self.daq_rx_gain = gain
# Set center frequency
cmd="GAIN"
gain_list=[297, 37] #[int(gain*10)]*self.M
gain_bytes=pack("I"*self.M, *gain_list)
msg_bytes=(cmd.encode()+gain_bytes+bytearray(128-(self.M+1)*4))
try:
_thread.start_new_thread(self.ctr_iface_communication, (msg_bytes,))
except:
errorMsg = sys.exc_info()[0]
self.logger.error("Unable to start communication thread")
self.logger.error("Error message: {:s}".format(errorMsg))
def close(self):
"""
Disconnet the receiver module and the DAQ FW
"""
self.eth_close()

@ -0,0 +1,195 @@
"""
HeIMDALL DAQ Firmware
Python based shared memory interface implementations
Author: Tamás Pető
License: GNU GPL V3
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
from struct import pack, unpack
from multiprocessing import shared_memory
import numpy as np
import os
A_BUFF_READY = 1
B_BUFF_READY = 2
INIT_READY = 10
TERMINATE = 255
class outShmemIface():
def __init__(self, shmem_name, shmem_size, drop_mode = False):
self.init_ok = True
logging.basicConfig(level=logging.INFO)
self.logger = logging.getLogger(__name__)
self.drop_mode = drop_mode
self.dropped_frame_cntr = 0
self.shmem_name = shmem_name
self.buffer_free = [True, True]
self.memories = []
self.buffers = []
# Try to remove shared memories if already exist
try:
shmem_A = shared_memory.SharedMemory(name=shmem_name+'_A',create=False, size=shmem_size)
shmem_A.close()
#shmem_A.unkink()
except FileNotFoundError as err:
self.logger.warning("Shared memory not exist")
try:
shmem_B = shared_memory.SharedMemory(name=shmem_name+'_B',create=False, size=shmem_size)
shmem_B.close()
#shmem_B.unkink()
except FileNotFoundError as err:
self.logger.warning("Shared memory not exist")
# Create the shared memories
self.memories.append(shared_memory.SharedMemory(name=shmem_name+'_A',create=True, size=shmem_size))
self.memories.append(shared_memory.SharedMemory(name=shmem_name+'_B',create=True, size=shmem_size))
self.buffers.append(np.ndarray((shmem_size,), dtype=np.uint8, buffer=self.memories[0].buf))
self.buffers.append(np.ndarray((shmem_size,), dtype=np.uint8, buffer=self.memories[1].buf))
# Opening control FIFOs
if self.drop_mode:
bw_fifo_flags = os.O_RDONLY | os.O_NONBLOCK
else:
bw_fifo_flags = os.O_RDONLY
try:
self.fw_ctr_fifo = os.open('_data_control/'+'fw_'+shmem_name, os.O_WRONLY)
self.bw_ctr_fifo = os.open('_data_control/'+'bw_'+shmem_name, bw_fifo_flags)
except OSError as err:
self.logger.critical("OS error: {0}".format(err))
self.logger.critical("Failed to open control fifos")
self.bw_ctr_fifo = None
self.fw_ctr_fifo = None
self.init_ok = False
# Send init ready signal
if self.init_ok:
os.write(self.fw_ctr_fifo, pack('B',INIT_READY))
def send_ctr_buff_ready(self, active_buffer_index):
# Send buffer ready signal on the forward FIFO
if active_buffer_index == 0:
os.write(self.fw_ctr_fifo, pack('B',A_BUFF_READY))
elif active_buffer_index == 1:
os.write(self.fw_ctr_fifo, pack('B',B_BUFF_READY))
# Deassert buffer free flag
self.buffer_free[active_buffer_index] = False
def send_ctr_terminate(self):
os.write(self.fw_ctr_fifo, pack('B',TERMINATE))
self.logger.info("Terminate signal sent")
def destory_sm_buffer(self):
for memory in self.memories:
memory.close()
memory.unlink()
if self.fw_ctr_fifo is not None:
os.close(self.fw_ctr_fifo)
if self.bw_ctr_fifo is not None:
os.close(self.bw_ctr_fifo)
def wait_buff_free(self):
if self.buffer_free[0]:
return 0
elif self.buffer_free[1]:
return 1
else:
try:
buffer = os.read(self.bw_ctr_fifo, 1)
signal = unpack('B', buffer )[0]
if signal == A_BUFF_READY:
self.buffer_free[0] = True
return 0
if signal == B_BUFF_READY:
self.buffer_free[1] = True
return 1
except BlockingIOError as err:
self.dropped_frame_cntr +=1
self.logger.warning("Dropping frame.. Total: [{:d}] ".format(self.dropped_frame_cntr))
return 3
return -1
class inShmemIface():
def __init__(self, shmem_name, ctr_fifo_path="_data_control/"):
self.init_ok = True
logging.basicConfig(level=logging.INFO)
self.logger = logging.getLogger(__name__)
self.drop_mode = False
self.shmem_name = shmem_name
self.memories = []
self.buffers = []
try:
self.fw_ctr_fifo = os.open(ctr_fifo_path+'fw_'+shmem_name, os.O_RDONLY)
self.bw_ctr_fifo = os.open(ctr_fifo_path+'bw_'+shmem_name, os.O_WRONLY)
except OSError as err:
self.logger.critical("OS error: {0}".format(err))
self.logger.critical("Failed to open control fifos")
self.bw_ctr_fifo = None
self.fw_ctr_fifo = None
self.init_ok = False
if self.fw_ctr_fifo is not None:
if unpack('B', os.read(self.fw_ctr_fifo, 1))[0] == INIT_READY:
self.memories.append(shared_memory.SharedMemory(name=shmem_name+'_A'))
self.memories.append(shared_memory.SharedMemory(name=shmem_name+'_B'))
self.buffers.append(np.ndarray((self.memories[0].size,),
dtype=np.uint8,
buffer=self.memories[0].buf))
self.buffers.append(np.ndarray((self.memories[1].size,),
dtype=np.uint8,
buffer=self.memories[1].buf))
else:
self.init_ok = False
def send_ctr_buff_ready(self, active_buffer_index):
if active_buffer_index == 0:
os.write(self.bw_ctr_fifo, pack('B',A_BUFF_READY))
elif active_buffer_index == 1:
os.write(self.bw_ctr_fifo, pack('B',B_BUFF_READY))
def destory_sm_buffer(self):
for memory in self.memories:
memory.close()
if self.fw_ctr_fifo is not None:
os.close(self.fw_ctr_fifo)
if self.bw_ctr_fifo is not None:
os.close(self.bw_ctr_fifo)
def wait_buff_free(self):
signal = unpack('B', os.read(self.fw_ctr_fifo, 1))[0]
if signal == A_BUFF_READY:
return 0
elif signal == B_BUFF_READY:
return 1
elif signal == TERMINATE:
return TERMINATE
return -1

@ -0,0 +1,713 @@
# KrakenSDR Signal Processor
#
# Copyright (C) 2018-2021 Carl Laufer, Tamás Pető
</