mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-07-26 10:38:52 +00:00
Added tetra rx ext app (#3257)
This commit is contained in:
+12
@@ -380,6 +380,17 @@ set(EXTCPPSRC
|
|||||||
#signal_hunter
|
#signal_hunter
|
||||||
external/signal_hunter/main.cpp
|
external/signal_hunter/main.cpp
|
||||||
external/signal_hunter/ui_signal_hunter.cpp
|
external/signal_hunter/ui_signal_hunter.cpp
|
||||||
|
|
||||||
|
|
||||||
|
#tetra rx
|
||||||
|
external/tetra_rx/main.cpp
|
||||||
|
external/tetra_rx/ui_tetra_rx.cpp
|
||||||
|
external/tetra_rx/tetra_crc.cpp
|
||||||
|
external/tetra_rx/tetra_descrambler.cpp
|
||||||
|
external/tetra_rx/tetra_interleave.cpp
|
||||||
|
external/tetra_rx/tetra_rcpc.cpp
|
||||||
|
external/tetra_rx/tetra_viterbi.cpp
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
set(EXTAPPLIST
|
set(EXTAPPLIST
|
||||||
@@ -473,6 +484,7 @@ set(EXTAPPLIST
|
|||||||
hard_reset
|
hard_reset
|
||||||
secplustx
|
secplustx
|
||||||
signal_hunter
|
signal_hunter
|
||||||
|
tetra_rx
|
||||||
)
|
)
|
||||||
|
|
||||||
# sdusb has type conflicts with PRALINE (HackRF Pro) - add only for non-PRALINE builds
|
# sdusb has type conflicts with PRALINE (HackRF Pro) - add only for non-PRALINE builds
|
||||||
|
|||||||
+10
@@ -114,6 +114,7 @@ MEMORY
|
|||||||
ram_external_app_vor_rx (rwx) : org = 0xAE090000, len = 32k
|
ram_external_app_vor_rx (rwx) : org = 0xAE090000, len = 32k
|
||||||
ram_external_app_vor_tx (rwx) : org = 0xAE0A0000, len = 32k
|
ram_external_app_vor_tx (rwx) : org = 0xAE0A0000, len = 32k
|
||||||
ram_external_app_signal_hunter (rwx) : org = 0xAE0B0000, len = 32k
|
ram_external_app_signal_hunter (rwx) : org = 0xAE0B0000, len = 32k
|
||||||
|
ram_external_app_tetra_rx (rwx) : org = 0xAE0C0000, len = 32k
|
||||||
}
|
}
|
||||||
|
|
||||||
SECTIONS
|
SECTIONS
|
||||||
@@ -663,4 +664,13 @@ SECTIONS
|
|||||||
KEEP(*(.external_app.app_signal_hunter.application_information));
|
KEEP(*(.external_app.app_signal_hunter.application_information));
|
||||||
*(*ui*external_app*signal_hunter*);
|
*(*ui*external_app*signal_hunter*);
|
||||||
} > ram_external_app_signal_hunter
|
} > ram_external_app_signal_hunter
|
||||||
|
|
||||||
|
.external_app_tetra_rx : ALIGN(4) SUBALIGN(4)
|
||||||
|
{
|
||||||
|
KEEP(*(.external_app.app_tetra_rx.application_information));
|
||||||
|
*(*ui*external_app*tetra_rx*);
|
||||||
|
} > ram_external_app_tetra_rx
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+81
@@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2026 HTotoo
|
||||||
|
*
|
||||||
|
* This file is part of PortaPack.
|
||||||
|
*
|
||||||
|
* 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 2, or (at your option)
|
||||||
|
* 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; see the file COPYING. If not, write to
|
||||||
|
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||||
|
* Boston, MA 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "ui_tetra_rx.hpp"
|
||||||
|
#include "ui_navigation.hpp"
|
||||||
|
#include "external_app.hpp"
|
||||||
|
|
||||||
|
namespace ui::external_app::tetra_rx {
|
||||||
|
void initialize_app(ui::NavigationView& nav) {
|
||||||
|
nav.push<TetraRxView>();
|
||||||
|
}
|
||||||
|
} // namespace ui::external_app::tetra_rx
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
__attribute__((section(".external_app.app_tetra_rx.application_information"), used)) application_information_t _application_information_tetra_rx = {
|
||||||
|
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||||
|
/*.externalAppEntry = */ ui::external_app::tetra_rx::initialize_app,
|
||||||
|
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||||
|
/*.app_version = */ VERSION_MD5,
|
||||||
|
|
||||||
|
/*.app_name = */ "Tetra",
|
||||||
|
/*.bitmap_data = */ {
|
||||||
|
0xE0,
|
||||||
|
0x0F,
|
||||||
|
0x18,
|
||||||
|
0x38,
|
||||||
|
0xE4,
|
||||||
|
0x67,
|
||||||
|
0x7E,
|
||||||
|
0xCE,
|
||||||
|
0xC7,
|
||||||
|
0xCC,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0xFF,
|
||||||
|
0x4F,
|
||||||
|
0xBA,
|
||||||
|
0xB2,
|
||||||
|
0x9A,
|
||||||
|
0xEE,
|
||||||
|
0xBA,
|
||||||
|
0xB2,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x3B,
|
||||||
|
0xE3,
|
||||||
|
0x73,
|
||||||
|
0x7E,
|
||||||
|
0xC6,
|
||||||
|
0x27,
|
||||||
|
0x1C,
|
||||||
|
0x18,
|
||||||
|
0xF0,
|
||||||
|
0x07,
|
||||||
|
},
|
||||||
|
/*.icon_color = */ ui::Color::orange().v,
|
||||||
|
/*.menu_location = */ app_location_t::RX,
|
||||||
|
/*.desired_menu_position = */ -1,
|
||||||
|
|
||||||
|
/*.m4_app_tag = portapack::spi_flash::image_tag_tetrarx*/ {'P', 'T', 'E', 'T'},
|
||||||
|
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
namespace ui::external_app::tetra_rx {
|
||||||
|
|
||||||
|
class BitVector {
|
||||||
|
public:
|
||||||
|
BitVector(uint8_t* p, size_t bits)
|
||||||
|
: data_(p),
|
||||||
|
bits_(bits) {
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool get(size_t bit) const {
|
||||||
|
return (data_[bit >> 3] >>
|
||||||
|
(7 - (bit & 7))) &
|
||||||
|
1;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void set(size_t bit, bool value) {
|
||||||
|
if (value)
|
||||||
|
data_[bit >> 3] |=
|
||||||
|
1 << (7 - (bit & 7));
|
||||||
|
else
|
||||||
|
data_[bit >> 3] &=
|
||||||
|
~(1 << (7 - (bit & 7)));
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void xor_bit(size_t bit, bool value) {
|
||||||
|
if (value)
|
||||||
|
data_[bit >> 3] ^=
|
||||||
|
1 << (7 - (bit & 7));
|
||||||
|
}
|
||||||
|
|
||||||
|
inline size_t size() const {
|
||||||
|
return bits_;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
uint8_t* data_;
|
||||||
|
size_t bits_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ui::external_app::tetra_rx
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#include "tetra_crc.hpp"
|
||||||
|
|
||||||
|
namespace ui::external_app::tetra_rx {
|
||||||
|
|
||||||
|
uint16_t CRC::crc16_itut_bits(
|
||||||
|
const uint8_t* bits,
|
||||||
|
uint32_t n_bits,
|
||||||
|
uint16_t init) {
|
||||||
|
uint16_t crc = init;
|
||||||
|
|
||||||
|
for (uint32_t i = 0; i < n_bits; i++) {
|
||||||
|
uint8_t bit =
|
||||||
|
(bits[i >> 3] >>
|
||||||
|
(7 - (i & 7))) &
|
||||||
|
1;
|
||||||
|
|
||||||
|
crc ^= uint16_t(bit) << 15;
|
||||||
|
|
||||||
|
if (crc & 0x8000) {
|
||||||
|
crc =
|
||||||
|
(crc << 1) ^
|
||||||
|
0x1021;
|
||||||
|
} else {
|
||||||
|
crc <<= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
crc &= 0xffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
return crc;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ui::external_app::tetra_rx
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
namespace ui::external_app::tetra_rx {
|
||||||
|
|
||||||
|
class CRC {
|
||||||
|
public:
|
||||||
|
static constexpr uint16_t CRC_OK = 0x1d0f;
|
||||||
|
|
||||||
|
static uint16_t crc16_itut_bits(
|
||||||
|
const uint8_t* bits,
|
||||||
|
uint32_t n_bits,
|
||||||
|
uint16_t init = 0xffff);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ui::external_app::tetra_rx
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#include "tetra_descrambler.hpp"
|
||||||
|
|
||||||
|
namespace ui::external_app::tetra_rx {
|
||||||
|
|
||||||
|
static inline uint8_t lfsr_step(uint32_t& s) {
|
||||||
|
uint8_t b = 0;
|
||||||
|
|
||||||
|
b ^= (s >> (32 - 32)) & 1;
|
||||||
|
b ^= (s >> (32 - 26)) & 1;
|
||||||
|
b ^= (s >> (32 - 23)) & 1;
|
||||||
|
b ^= (s >> (32 - 22)) & 1;
|
||||||
|
b ^= (s >> (32 - 16)) & 1;
|
||||||
|
b ^= (s >> (32 - 12)) & 1;
|
||||||
|
b ^= (s >> (32 - 11)) & 1;
|
||||||
|
b ^= (s >> (32 - 10)) & 1;
|
||||||
|
b ^= (s >> (32 - 8)) & 1;
|
||||||
|
b ^= (s >> (32 - 7)) & 1;
|
||||||
|
b ^= (s >> (32 - 5)) & 1;
|
||||||
|
b ^= (s >> (32 - 4)) & 1;
|
||||||
|
b ^= (s >> (32 - 2)) & 1;
|
||||||
|
b ^= (s >> (32 - 1)) & 1;
|
||||||
|
|
||||||
|
s = (s >> 1) | (uint32_t(b) << 31);
|
||||||
|
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Descrambler::generate(
|
||||||
|
uint32_t init,
|
||||||
|
uint8_t* seq,
|
||||||
|
size_t bits) {
|
||||||
|
uint32_t s = init;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < bits; i++)
|
||||||
|
seq[i] = lfsr_step(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Descrambler::descramble(
|
||||||
|
BitVector& bits,
|
||||||
|
uint32_t init) {
|
||||||
|
uint32_t s = init;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < bits.size(); i++)
|
||||||
|
bits.xor_bit(i, lfsr_step(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t Descrambler::dmo_scramb_init(
|
||||||
|
uint32_t mni,
|
||||||
|
uint32_t src) {
|
||||||
|
uint32_t init =
|
||||||
|
(src & 0xFFFFFF) |
|
||||||
|
((mni & 0x3F) << 24);
|
||||||
|
|
||||||
|
init <<= 2;
|
||||||
|
init |= 3;
|
||||||
|
|
||||||
|
return init;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ui::external_app::tetra_rx
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "tetra_bits.hpp"
|
||||||
|
|
||||||
|
namespace ui::external_app::tetra_rx {
|
||||||
|
|
||||||
|
class Descrambler {
|
||||||
|
public:
|
||||||
|
static constexpr uint32_t SCRAMB_INIT_BSCH = 0x00000003;
|
||||||
|
|
||||||
|
static void descramble(
|
||||||
|
BitVector& bits,
|
||||||
|
uint32_t init = SCRAMB_INIT_BSCH);
|
||||||
|
|
||||||
|
static void generate(
|
||||||
|
uint32_t init,
|
||||||
|
uint8_t* sequence,
|
||||||
|
size_t bits);
|
||||||
|
|
||||||
|
static uint32_t dmo_scramb_init(
|
||||||
|
uint32_t mni,
|
||||||
|
uint32_t src);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ui::external_app::tetra_rx
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#include "tetra_interleave.hpp"
|
||||||
|
|
||||||
|
namespace ui::external_app::tetra_rx {
|
||||||
|
|
||||||
|
void Interleave::block_deinterleave(
|
||||||
|
BitVector& in,
|
||||||
|
BitVector& out,
|
||||||
|
uint32_t K,
|
||||||
|
uint32_t a) {
|
||||||
|
for (uint32_t i = 1; i <= K; i++) {
|
||||||
|
uint32_t k =
|
||||||
|
1 +
|
||||||
|
((a * i) % K);
|
||||||
|
|
||||||
|
out.set(
|
||||||
|
i - 1,
|
||||||
|
in.get(k - 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ui::external_app::tetra_rx
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "tetra_bits.hpp"
|
||||||
|
|
||||||
|
namespace ui::external_app::tetra_rx {
|
||||||
|
|
||||||
|
class Interleave {
|
||||||
|
public:
|
||||||
|
static void block_deinterleave(
|
||||||
|
BitVector& in,
|
||||||
|
BitVector& out,
|
||||||
|
uint32_t K,
|
||||||
|
uint32_t a);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ui::external_app::tetra_rx
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
#include "tetra_rcpc.hpp"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
namespace ui::external_app::tetra_rx {
|
||||||
|
|
||||||
|
static constexpr uint8_t P_RATE_2_3[3] = {1, 2, 5};
|
||||||
|
|
||||||
|
static constexpr uint32_t T_RATE_2_3 = 3;
|
||||||
|
static constexpr uint32_t PERIOD = 8;
|
||||||
|
|
||||||
|
void RCPC::depuncture_2_3(BitVector& in, uint8_t* out, uint32_t in_bits) {
|
||||||
|
const uint32_t mother_bits = ((in_bits + T_RATE_2_3 - 1) / T_RATE_2_3) * PERIOD + PERIOD;
|
||||||
|
memset(out, ERASED, mother_bits);
|
||||||
|
for (uint32_t j = 1; j <= in_bits; j++) {
|
||||||
|
uint32_t i = j;
|
||||||
|
uint32_t k = PERIOD * ((i - 1) / T_RATE_2_3);
|
||||||
|
k += P_RATE_2_3[(i - 1) % T_RATE_2_3];
|
||||||
|
if (k >= 1 && k <= mother_bits) {
|
||||||
|
out[k - 1] = in.get(j - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ui::external_app::tetra_rx
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "tetra_bits.hpp"
|
||||||
|
|
||||||
|
namespace ui::external_app::tetra_rx {
|
||||||
|
|
||||||
|
class RCPC {
|
||||||
|
public:
|
||||||
|
static constexpr uint8_t ERASED = 0xff;
|
||||||
|
|
||||||
|
static void depuncture_2_3(
|
||||||
|
BitVector& in,
|
||||||
|
uint8_t* out,
|
||||||
|
uint32_t in_bits);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ui::external_app::tetra_rx
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
#include "tetra_viterbi.hpp"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
namespace ui::external_app::tetra_rx {
|
||||||
|
|
||||||
|
int Viterbi::decode_cch(
|
||||||
|
const uint8_t* mother,
|
||||||
|
uint32_t n_info,
|
||||||
|
uint8_t* bits,
|
||||||
|
uint8_t* trace_buffer) { // Puffer by caller
|
||||||
|
|
||||||
|
// Store these on stack, to avoid flash space usage
|
||||||
|
const uint8_t next_state[16][2] = {
|
||||||
|
{0, 1}, {2, 3}, {4, 5}, {6, 7}, {8, 9}, {10, 11}, {12, 13}, {14, 15}, {0, 1}, {2, 3}, {4, 5}, {6, 7}, {8, 9}, {10, 11}, {12, 13}, {14, 15}};
|
||||||
|
const uint8_t next_output[16][2] = {
|
||||||
|
{0, 15}, {11, 4}, {6, 9}, {13, 2}, {5, 10}, {14, 1}, {3, 12}, {8, 7}, {15, 0}, {4, 11}, {9, 6}, {2, 13}, {10, 5}, {1, 14}, {12, 3}, {7, 8}};
|
||||||
|
|
||||||
|
const uint16_t INF = 0x3fff;
|
||||||
|
uint16_t cost[16];
|
||||||
|
uint16_t new_cost[16];
|
||||||
|
|
||||||
|
// Initialization
|
||||||
|
for (int i = 0; i < 16; i++) cost[i] = INF;
|
||||||
|
cost[0] = 0;
|
||||||
|
|
||||||
|
for (uint32_t step = 0; step < n_info; step++) {
|
||||||
|
for (int i = 0; i < 16; i++) new_cost[i] = INF;
|
||||||
|
const uint8_t* rx = &mother[step * 4];
|
||||||
|
|
||||||
|
for (int prev = 0; prev < 16; prev++) {
|
||||||
|
if (cost[prev] >= INF) continue;
|
||||||
|
|
||||||
|
for (int bit = 0; bit < 2; bit++) {
|
||||||
|
const uint8_t pattern = next_output[prev][bit];
|
||||||
|
const uint8_t next = next_state[prev][bit];
|
||||||
|
|
||||||
|
uint16_t d = 0;
|
||||||
|
for (int b = 0; b < 4; b++) {
|
||||||
|
if (rx[b] != 0xff && rx[b] != ((pattern >> (3 - b)) & 1)) d++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint16_t total = cost[prev] + d;
|
||||||
|
if (total < new_cost[next]) {
|
||||||
|
new_cost[next] = total;
|
||||||
|
trace_buffer[step * 16 + next] = (prev << 1) | bit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
memcpy(cost, new_cost, sizeof(cost));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Traceback
|
||||||
|
uint8_t best_state = 0;
|
||||||
|
for (int i = 1; i < 16; i++) {
|
||||||
|
if (cost[i] < cost[best_state]) best_state = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t state = best_state;
|
||||||
|
memset(bits, 0, (n_info + 7) >> 3);
|
||||||
|
|
||||||
|
for (int step = (int)n_info - 1; step >= 0; step--) {
|
||||||
|
uint8_t packed = trace_buffer[step * 16 + state];
|
||||||
|
uint8_t prev = (packed >> 1) & 0x0F;
|
||||||
|
uint8_t bit = packed & 0x01;
|
||||||
|
|
||||||
|
if (bit) bits[step >> 3] |= (1 << (7 - (step & 7)));
|
||||||
|
state = prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cost[best_state];
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ui::external_app::tetra_rx
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
namespace ui::external_app::tetra_rx {
|
||||||
|
|
||||||
|
class Viterbi {
|
||||||
|
public:
|
||||||
|
int decode_cch(
|
||||||
|
const uint8_t* mother,
|
||||||
|
uint32_t n_info,
|
||||||
|
uint8_t* bits,
|
||||||
|
uint8_t* trace_buffer);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ui::external_app::tetra_rx
|
||||||
@@ -0,0 +1,354 @@
|
|||||||
|
#include "ui_tetra_rx.hpp"
|
||||||
|
#include "rtc_time.hpp"
|
||||||
|
#include "baseband_api.hpp"
|
||||||
|
#include "string_format.hpp"
|
||||||
|
#include "portapack_persistent_memory.hpp"
|
||||||
|
#include "file_path.hpp"
|
||||||
|
#include "tetra_crc.hpp"
|
||||||
|
#include "tetra_descrambler.hpp"
|
||||||
|
#include "tetra_interleave.hpp"
|
||||||
|
#include "tetra_rcpc.hpp"
|
||||||
|
#include "tetra_viterbi.hpp"
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
using namespace portapack;
|
||||||
|
using namespace ui;
|
||||||
|
|
||||||
|
namespace ui::external_app::tetra_rx {
|
||||||
|
|
||||||
|
template <size_t OutBytes>
|
||||||
|
bool TetraChannelDecoder::decode_block(
|
||||||
|
const std::array<uint8_t, 63>& burst,
|
||||||
|
size_t offset,
|
||||||
|
size_t len_t5,
|
||||||
|
size_t type1_bits,
|
||||||
|
size_t type2_bits,
|
||||||
|
size_t interleave_a,
|
||||||
|
std::array<uint8_t, OutBytes>& out,
|
||||||
|
uint16_t& crc,
|
||||||
|
int& cost,
|
||||||
|
uint32_t scramb_init) {
|
||||||
|
std::array<uint8_t, 64> t5{};
|
||||||
|
std::array<uint8_t, 64> t3{};
|
||||||
|
std::array<uint8_t, 1200> mother{};
|
||||||
|
std::array<uint8_t, 64> type2{};
|
||||||
|
|
||||||
|
out.fill(0);
|
||||||
|
|
||||||
|
for (size_t i = 0; i < len_t5; i++) {
|
||||||
|
set_bit(t5, i, get_bit(burst, offset + i));
|
||||||
|
}
|
||||||
|
|
||||||
|
BitVector t5_bits{t5.data(), len_t5};
|
||||||
|
Descrambler::descramble(t5_bits, scramb_init);
|
||||||
|
|
||||||
|
BitVector t3_bits{t3.data(), len_t5};
|
||||||
|
Interleave::block_deinterleave(t5_bits, t3_bits, len_t5, interleave_a);
|
||||||
|
|
||||||
|
RCPC::depuncture_2_3(t3_bits, mother.data(), len_t5);
|
||||||
|
Viterbi viterbi;
|
||||||
|
cost = viterbi.decode_cch(mother.data(), type2_bits, type2.data(), trace_buffer);
|
||||||
|
crc = CRC::crc16_itut_bits(type2.data(), type1_bits + 16);
|
||||||
|
for (size_t i = 0; i < type1_bits; i++) {
|
||||||
|
set_bit(out, i, (type2[i >> 3] >> (7 - (i & 7))) & 1);
|
||||||
|
}
|
||||||
|
return crc == CRC::CRC_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
TetraChannelDecoder::Result TetraChannelDecoder::decode_dnb(const TetraDnbMessage& message) {
|
||||||
|
Result result{};
|
||||||
|
result.inverted = message.inverted;
|
||||||
|
|
||||||
|
if (!network_synced)
|
||||||
|
return result;
|
||||||
|
|
||||||
|
std::array<uint8_t, 63> buffer{};
|
||||||
|
std::copy(message.payload.begin(), message.payload.end(), buffer.begin());
|
||||||
|
|
||||||
|
uint32_t tmo_dnb_seed = (((uint32_t)last_mcc << 20) | ((uint32_t)last_mnc << 6) | (uint32_t)last_bcc) << 2 | 3;
|
||||||
|
// 1. DNB Full slot (SCH/F)
|
||||||
|
bool ok = decode_block(buffer, 0, SCH_F_LEN_BITS, SCH_F_TYPE1_BITS, SCH_F_TYPE2_BITS,
|
||||||
|
SCH_F_INTERLEAVE_A, result.payload, result.crc, result.cost, tmo_dnb_seed);
|
||||||
|
|
||||||
|
// 2. SCH/HD Block 1
|
||||||
|
if (!ok) {
|
||||||
|
ok = decode_block(buffer, 0, BLK2_LEN_BITS, SB2_TYPE1_BITS, SB2_TYPE2_BITS,
|
||||||
|
SB2_INTERLEAVE_A, result.payload, result.crc, result.cost, tmo_dnb_seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. SCH/HD Block 2
|
||||||
|
if (!ok) {
|
||||||
|
ok = decode_block(buffer, 216, BLK2_LEN_BITS, SB2_TYPE1_BITS, SB2_TYPE2_BITS,
|
||||||
|
SB2_INTERLEAVE_A, result.payload, result.crc, result.cost, tmo_dnb_seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ok) {
|
||||||
|
result.type = Result::Type::Dnb;
|
||||||
|
result.is_ok = true;
|
||||||
|
parse_mac_pdu(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
TetraChannelDecoder::Result TetraChannelDecoder::decode_burst(const TetraBurstMessage& message) {
|
||||||
|
Result result{};
|
||||||
|
result.sync_errors = message.sync_errors;
|
||||||
|
result.inverted = message.inverted;
|
||||||
|
|
||||||
|
// 1. SCH/S
|
||||||
|
bool ok = decode_block(
|
||||||
|
message.payload, BLK1_OFFSET_BITS, BLK1_LEN_BITS,
|
||||||
|
SB1_TYPE1_BITS, SB1_TYPE2_BITS, SB1_INTERLEAVE_A,
|
||||||
|
result.payload, result.crc, result.cost,
|
||||||
|
Descrambler::SCRAMB_INIT_BSCH);
|
||||||
|
|
||||||
|
if (!ok) return result;
|
||||||
|
|
||||||
|
result.type = Result::Type::Sync;
|
||||||
|
result.is_ok = true;
|
||||||
|
parse_sync_pdu(result);
|
||||||
|
|
||||||
|
uint16_t current_mcc = result.mcc;
|
||||||
|
uint16_t current_mnc = result.mnc;
|
||||||
|
uint8_t current_bcc = result.bcc;
|
||||||
|
uint8_t current_enc = result.encryption;
|
||||||
|
|
||||||
|
// 2. SCH/H
|
||||||
|
uint16_t h_crc = 0;
|
||||||
|
int h_cost = 0;
|
||||||
|
|
||||||
|
ok = decode_block(
|
||||||
|
message.payload, TMO_BLK2_OFFSET_BITS, BLK2_LEN_BITS,
|
||||||
|
SB2_TYPE1_BITS, SB2_TYPE2_BITS, SB2_INTERLEAVE_A,
|
||||||
|
result.payload, h_crc, h_cost,
|
||||||
|
Descrambler::SCRAMB_INIT_BSCH);
|
||||||
|
|
||||||
|
if (ok) {
|
||||||
|
result.type = Result::Type::SyncFull;
|
||||||
|
result.crc = h_crc;
|
||||||
|
result.cost = h_cost;
|
||||||
|
|
||||||
|
result.mcc = current_mcc;
|
||||||
|
result.mnc = current_mnc;
|
||||||
|
result.bcc = current_bcc;
|
||||||
|
result.encryption = current_enc;
|
||||||
|
|
||||||
|
parse_mac_pdu(result);
|
||||||
|
} else {
|
||||||
|
result.encryption = current_enc;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t TetraChannelDecoder::read_bits(const uint8_t* bytes, size_t bit, size_t count) const {
|
||||||
|
uint32_t value = 0;
|
||||||
|
for (size_t i = 0; i < count; i++) {
|
||||||
|
value <<= 1;
|
||||||
|
value |= (bytes[(bit + i) >> 3] >> (7 - ((bit + i) & 7))) & 1;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TetraChannelDecoder::parse_sync_pdu(Result& result) {
|
||||||
|
// Sys 4 bit, then BCC
|
||||||
|
result.bcc = read_bits(result.payload.data(), 4, 6);
|
||||||
|
result.timeslot = read_bits(result.payload.data(), 10, 2);
|
||||||
|
result.frame_number = read_bits(result.payload.data(), 12, 5);
|
||||||
|
result.encryption = read_bits(result.payload.data(), 30, 1);
|
||||||
|
result.mcc = read_bits(result.payload.data(), 31, 10);
|
||||||
|
result.mnc = read_bits(result.payload.data(), 41, 14);
|
||||||
|
|
||||||
|
last_mcc = result.mcc;
|
||||||
|
last_mnc = result.mnc;
|
||||||
|
last_bcc = result.bcc;
|
||||||
|
network_synced = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TetraChannelDecoder::parse_mac_pdu(Result& result) {
|
||||||
|
result.pdu_type = read_bits(result.payload.data(), 0, 2);
|
||||||
|
|
||||||
|
if (result.pdu_type == 0) { // MAC-RESOURCE
|
||||||
|
result.encryption = read_bits(result.payload.data(), 4, 2);
|
||||||
|
uint8_t length_ind = read_bits(result.payload.data(), 7, 6);
|
||||||
|
|
||||||
|
if (result.encryption == 0 && length_ind > 0 && length_ind < 62) {
|
||||||
|
size_t offset = 13;
|
||||||
|
uint8_t addr_type = read_bits(result.payload.data(), offset, 3);
|
||||||
|
offset += 3;
|
||||||
|
|
||||||
|
if (addr_type == 1) {
|
||||||
|
result.calling_ssi = read_bits(result.payload.data(), offset, 24);
|
||||||
|
offset += 24;
|
||||||
|
} else if (addr_type == 2) {
|
||||||
|
offset += 10;
|
||||||
|
} else if (addr_type == 3 || addr_type == 4) {
|
||||||
|
result.calling_ssi = read_bits(result.payload.data(), offset, 24);
|
||||||
|
offset += 24;
|
||||||
|
} else if (addr_type == 5) {
|
||||||
|
offset += 34;
|
||||||
|
} else if (addr_type == 6) {
|
||||||
|
offset += 30;
|
||||||
|
} else if (addr_type == 7) {
|
||||||
|
offset += 34;
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip optional fields
|
||||||
|
if (read_bits(result.payload.data(), offset++, 1)) offset += 4; // Power control
|
||||||
|
if (read_bits(result.payload.data(), offset++, 1)) offset += 8; // Slot grant
|
||||||
|
|
||||||
|
uint8_t chan_alloc = read_bits(result.payload.data(), offset++, 1);
|
||||||
|
|
||||||
|
if (chan_alloc == 0) {
|
||||||
|
uint8_t llc_type = read_bits(result.payload.data(), offset, 4);
|
||||||
|
offset += 4;
|
||||||
|
|
||||||
|
// LLC header skipped
|
||||||
|
if (llc_type == 0 || llc_type == 1)
|
||||||
|
offset += 2;
|
||||||
|
else if (llc_type == 2 || llc_type == 3 || llc_type == 6 || llc_type == 7)
|
||||||
|
offset += 1;
|
||||||
|
|
||||||
|
uint8_t mle_type = read_bits(result.payload.data(), offset, 3);
|
||||||
|
offset += 3;
|
||||||
|
|
||||||
|
// 1 = CMCE
|
||||||
|
if (mle_type == 1) {
|
||||||
|
result.cmce_type = read_bits(result.payload.data(), offset, 5);
|
||||||
|
offset += 5;
|
||||||
|
result.call_id = read_bits(result.payload.data(), offset, 14);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (result.pdu_type == 2) { // SYSINFO/BCAST
|
||||||
|
uint8_t bcast_type = read_bits(result.payload.data(), 2, 2);
|
||||||
|
if (bcast_type == 0) {
|
||||||
|
result.la = read_bits(result.payload.data(), 82, 14);
|
||||||
|
result.encryption = read_bits(result.payload.data(), 122, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TetraRxView::TetraRxView(NavigationView& nav)
|
||||||
|
: nav_{nav} {
|
||||||
|
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
|
||||||
|
|
||||||
|
add_children({&rssi, &channel, &field_rf_amp, &field_lna, &field_vga,
|
||||||
|
&field_frequency, &text_mcc, &text_la, &text_pdu,
|
||||||
|
&text_mnc, &text_ts, &text_fn, &text_bcc, &text_enc, &text_debug, &console});
|
||||||
|
|
||||||
|
field_frequency.set_step(25000);
|
||||||
|
|
||||||
|
receiver_model.set_modulation(ReceiverModel::Mode::NarrowbandFMAudio);
|
||||||
|
receiver_model.set_sampling_rate(3072000);
|
||||||
|
receiver_model.set_baseband_bandwidth(1750000);
|
||||||
|
receiver_model.set_squelch_level(0);
|
||||||
|
receiver_model.enable();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TetraRxView::focus() {
|
||||||
|
field_frequency.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TetraRxView::on_data_tetra(const TetraBurstMessage& message) {
|
||||||
|
packet_count++;
|
||||||
|
auto result = decoder.decode_burst(message);
|
||||||
|
|
||||||
|
if (result.type == TetraChannelDecoder::Result::Type::Sync ||
|
||||||
|
result.type == TetraChannelDecoder::Result::Type::SyncFull) {
|
||||||
|
valid_count++;
|
||||||
|
|
||||||
|
text_mcc.set("MCC: " + to_string_dec_uint(result.mcc));
|
||||||
|
text_mnc.set("MNC: " + to_string_dec_uint(result.mnc));
|
||||||
|
text_bcc.set("BCC: " + to_string_dec_uint(result.bcc));
|
||||||
|
text_ts.set("TS: " + to_string_dec_uint(result.timeslot));
|
||||||
|
text_fn.set("FN: " + to_string_dec_uint(result.frame_number));
|
||||||
|
|
||||||
|
std::string enc_str = (result.encryption != 0) ? "Encrypted" : "Clear";
|
||||||
|
text_enc.set("ENC: " + enc_str);
|
||||||
|
|
||||||
|
if (result.type == TetraChannelDecoder::Result::Type::SyncFull) {
|
||||||
|
h_valid_count++;
|
||||||
|
if (result.la != 0xffff) text_la.set("LA: " + to_string_dec_uint(result.la));
|
||||||
|
text_pdu.set("PDU: " + decoder.get_pdu_name(result.pdu_type));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
text_debug.set(
|
||||||
|
"Syn: " + to_string_dec_uint(packet_count) +
|
||||||
|
", V: " + to_string_dec_uint(valid_count) +
|
||||||
|
", H: " + to_string_dec_uint(h_valid_count) +
|
||||||
|
", E:" + to_string_dec_uint(result.sync_errors));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TetraRxView::on_data_dnb(const TetraDnbMessage& message) {
|
||||||
|
dnb_count++;
|
||||||
|
auto result = decoder.decode_dnb(message);
|
||||||
|
|
||||||
|
if (result.is_ok && result.type == TetraChannelDecoder::Result::Type::Dnb) {
|
||||||
|
if (result.la != 0xffff) text_la.set("LA: " + to_string_dec_uint(result.la));
|
||||||
|
text_pdu.set("PDU: " + decoder.get_pdu_name(result.pdu_type));
|
||||||
|
|
||||||
|
std::string enc_str = (result.encryption != 0) ? "Encrypted" : "Clear";
|
||||||
|
text_enc.set("ENC: " + enc_str);
|
||||||
|
|
||||||
|
// if (rate_limiter++ < 2) console.writeln("DNB: " + decoder.get_pdu_name(result.pdu_type));
|
||||||
|
if (result.cmce_type != 0xff) {
|
||||||
|
std::string msg = "";
|
||||||
|
if (result.cmce_type == 7)
|
||||||
|
msg = "SETUP";
|
||||||
|
else if (result.cmce_type == 1)
|
||||||
|
msg = "CALL PROCEEDING";
|
||||||
|
else if (result.cmce_type == 0)
|
||||||
|
msg = "ALERTING";
|
||||||
|
else if (result.cmce_type == 2)
|
||||||
|
msg = "CONNECT";
|
||||||
|
else if (result.cmce_type == 3)
|
||||||
|
msg = "CONNECT ACK";
|
||||||
|
else if (result.cmce_type == 11)
|
||||||
|
msg = "TX GRANTED";
|
||||||
|
else if (result.cmce_type == 9)
|
||||||
|
msg = "TX CEASED";
|
||||||
|
else if (result.cmce_type == 13)
|
||||||
|
msg = "TX WAIT";
|
||||||
|
else if (result.cmce_type == 10)
|
||||||
|
msg = "TX CONTINUE";
|
||||||
|
else if (result.cmce_type == 12)
|
||||||
|
msg = "TX INTERRUPT";
|
||||||
|
else if (result.cmce_type == 4)
|
||||||
|
msg = "DISCONNECT";
|
||||||
|
else if (result.cmce_type == 6)
|
||||||
|
msg = "RELEASE";
|
||||||
|
else if (result.cmce_type == 5)
|
||||||
|
msg = "INFO";
|
||||||
|
else if (result.cmce_type == 8)
|
||||||
|
msg = "STATUS";
|
||||||
|
else if (result.cmce_type == 14)
|
||||||
|
msg = "SDS DATA";
|
||||||
|
else if (result.cmce_type == 15)
|
||||||
|
msg = "FACILITY";
|
||||||
|
else if (result.cmce_type == 16)
|
||||||
|
msg = "CALL RESTORE";
|
||||||
|
else if (result.cmce_type == 31)
|
||||||
|
msg = "NOT SUPPORTED";
|
||||||
|
else
|
||||||
|
msg = "CMCE:" + to_string_dec_uint(result.cmce_type);
|
||||||
|
|
||||||
|
console.writeln(msg);
|
||||||
|
if (result.call_id != 0xffff) {
|
||||||
|
console.writeln("ID:" + to_string_dec_uint(result.call_id) + " | SSI:" + to_string_dec_uint(result.calling_ssi));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TetraRxView::on_timer() {
|
||||||
|
rate_limiter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
TetraRxView::~TetraRxView() {
|
||||||
|
receiver_model.disable();
|
||||||
|
baseband::shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ui::external_app::tetra_rx
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
#ifndef __UI_TETRA_RX_H__
|
||||||
|
#define __UI_TETRA_RX_H__
|
||||||
|
|
||||||
|
#include "ui.hpp"
|
||||||
|
#include "ui_language.hpp"
|
||||||
|
#include "ui_navigation.hpp"
|
||||||
|
#include "ui_receiver.hpp"
|
||||||
|
#include "ui_freq_field.hpp"
|
||||||
|
#include "app_settings.hpp"
|
||||||
|
#include "radio_state.hpp"
|
||||||
|
#include "log_file.hpp"
|
||||||
|
#include "utility.hpp"
|
||||||
|
#include "message.hpp"
|
||||||
|
#include <array>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
using namespace ui;
|
||||||
|
|
||||||
|
namespace ui::external_app::tetra_rx {
|
||||||
|
|
||||||
|
class TetraChannelDecoder {
|
||||||
|
public:
|
||||||
|
struct Result {
|
||||||
|
enum class Type { None,
|
||||||
|
Sync,
|
||||||
|
SyncFull,
|
||||||
|
Dnb };
|
||||||
|
Type type{Type::None};
|
||||||
|
|
||||||
|
bool is_ok{false};
|
||||||
|
uint8_t sync_errors{0};
|
||||||
|
bool inverted{false};
|
||||||
|
|
||||||
|
uint16_t crc{0};
|
||||||
|
int cost{0};
|
||||||
|
|
||||||
|
uint8_t timeslot{0xff};
|
||||||
|
uint8_t frame_number{0xff};
|
||||||
|
uint16_t mcc{0xffff};
|
||||||
|
uint16_t mnc{0xffff};
|
||||||
|
uint8_t bcc{0xff};
|
||||||
|
|
||||||
|
uint8_t pdu_type{0xff};
|
||||||
|
uint16_t la{0xffff};
|
||||||
|
uint8_t encryption{0xff};
|
||||||
|
|
||||||
|
uint8_t cmce_type{0xff};
|
||||||
|
uint16_t call_id{0xffff};
|
||||||
|
uint32_t calling_ssi{0};
|
||||||
|
|
||||||
|
std::array<uint8_t, 34> payload{};
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string get_pdu_name(uint8_t pdu_type) {
|
||||||
|
switch (pdu_type) {
|
||||||
|
case 0:
|
||||||
|
return "MAC-RESOURCE";
|
||||||
|
case 1:
|
||||||
|
return "MAC-FRAG";
|
||||||
|
case 2:
|
||||||
|
return "SYSINFO/BCAST";
|
||||||
|
case 3:
|
||||||
|
return "MAC-U-SIGNAL";
|
||||||
|
default:
|
||||||
|
return "UNK_" + std::to_string(pdu_type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Result decode_burst(const TetraBurstMessage& message);
|
||||||
|
Result decode_dnb(const TetraDnbMessage& message);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr size_t BLK1_OFFSET_BITS = 94;
|
||||||
|
static constexpr size_t BLK1_LEN_BITS = 120;
|
||||||
|
static constexpr size_t TMO_BLK2_OFFSET_BITS = 282;
|
||||||
|
static constexpr size_t BLK2_LEN_BITS = 216;
|
||||||
|
static constexpr size_t SB1_TYPE1_BITS = 60;
|
||||||
|
static constexpr size_t SB1_TYPE2_BITS = 80;
|
||||||
|
static constexpr size_t SB1_INTERLEAVE_A = 11;
|
||||||
|
static constexpr size_t SB2_TYPE1_BITS = 124;
|
||||||
|
static constexpr size_t SB2_TYPE2_BITS = 144;
|
||||||
|
static constexpr size_t SB2_INTERLEAVE_A = 101;
|
||||||
|
|
||||||
|
static constexpr size_t SCH_F_LEN_BITS = 432;
|
||||||
|
static constexpr size_t SCH_F_TYPE1_BITS = 268;
|
||||||
|
static constexpr size_t SCH_F_TYPE2_BITS = 288;
|
||||||
|
static constexpr size_t SCH_F_INTERLEAVE_A = 103;
|
||||||
|
|
||||||
|
uint16_t last_mcc{0};
|
||||||
|
uint16_t last_mnc{0};
|
||||||
|
uint8_t last_bcc{0};
|
||||||
|
bool network_synced{false};
|
||||||
|
|
||||||
|
// Viterbi buffer
|
||||||
|
uint8_t trace_buffer[9216];
|
||||||
|
|
||||||
|
template <size_t OutBytes>
|
||||||
|
bool decode_block(const std::array<uint8_t, 63>& burst,
|
||||||
|
size_t offset,
|
||||||
|
size_t len_t5,
|
||||||
|
size_t type1_bits,
|
||||||
|
size_t type2_bits,
|
||||||
|
size_t interleave_a,
|
||||||
|
std::array<uint8_t, OutBytes>& out,
|
||||||
|
uint16_t& crc,
|
||||||
|
int& cost,
|
||||||
|
uint32_t scramb_init);
|
||||||
|
|
||||||
|
void parse_sync_pdu(Result& result);
|
||||||
|
void parse_mac_pdu(Result& result);
|
||||||
|
uint32_t read_bits(const uint8_t* bytes, size_t bit, size_t count) const;
|
||||||
|
|
||||||
|
template <size_t N>
|
||||||
|
uint8_t get_bit(const std::array<uint8_t, N>& arr, size_t bit_idx) const {
|
||||||
|
return (arr[bit_idx / 8] >> (7 - (bit_idx % 8))) & 0x01;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <size_t N>
|
||||||
|
void set_bit(std::array<uint8_t, N>& arr, size_t bit_idx, uint8_t val) const {
|
||||||
|
if (val)
|
||||||
|
arr[bit_idx / 8] |= (1 << (7 - (bit_idx % 8)));
|
||||||
|
else
|
||||||
|
arr[bit_idx / 8] &= ~(1 << (7 - (bit_idx % 8)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class TetraRxView : public View {
|
||||||
|
public:
|
||||||
|
TetraRxView(NavigationView& nav);
|
||||||
|
~TetraRxView();
|
||||||
|
void focus() override;
|
||||||
|
std::string title() const override { return "Tetra RX"; };
|
||||||
|
|
||||||
|
private:
|
||||||
|
NavigationView& nav_;
|
||||||
|
RxRadioState radio_state_{};
|
||||||
|
app_settings::SettingsManager settings_{"rx_tetra", app_settings::Mode::RX};
|
||||||
|
|
||||||
|
uint32_t packet_count{0};
|
||||||
|
uint32_t valid_count{0};
|
||||||
|
uint32_t h_valid_count{0};
|
||||||
|
uint32_t dnb_count{0};
|
||||||
|
|
||||||
|
RxFrequencyField field_frequency{{UI_POS_X(0), UI_POS_Y(0)}, nav_};
|
||||||
|
RFAmpField field_rf_amp{{UI_POS_X(13), UI_POS_Y(0)}};
|
||||||
|
LNAGainField field_lna{{UI_POS_X(15), UI_POS_Y(0)}};
|
||||||
|
VGAGainField field_vga{{UI_POS_X(18), UI_POS_Y(0)}};
|
||||||
|
RSSI rssi{{UI_POS_X_RIGHT(9), UI_POS_Y(0), UI_POS_WIDTH(9), 4}};
|
||||||
|
Channel channel{{UI_POS_X_RIGHT(9), UI_POS_Y(0) + 5, UI_POS_WIDTH(9), 4}};
|
||||||
|
|
||||||
|
Text text_mcc{{UI_POS_X(0), UI_POS_Y(1), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "MCC: ---"};
|
||||||
|
Text text_mnc{{UI_POS_X(15), UI_POS_Y(1), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "MNC: ---"};
|
||||||
|
Text text_ts{{UI_POS_X(0), UI_POS_Y(2), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "TS: -"}; // time slot
|
||||||
|
Text text_fn{{UI_POS_X(15), UI_POS_Y(2), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "FN: -"}; // frame number
|
||||||
|
Text text_bcc{{UI_POS_X(0), UI_POS_Y(3), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "BCC: -"}; // Base Station Color Code
|
||||||
|
Text text_enc{{UI_POS_X(15), UI_POS_Y(3), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "ENC: -"}; // encoded or not
|
||||||
|
Text text_la{{UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "LA: ----"}; // Location Area
|
||||||
|
Text text_pdu{{UI_POS_X(15), UI_POS_Y(4), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "PDU: ----"}; // pdu content type
|
||||||
|
|
||||||
|
Text text_debug{{UI_POS_X(0), UI_POS_Y(5), UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)}, "Syn: 0, V: 0, H: 0, E:0"}; // sync, valid, h_valid, errors corrected in top part
|
||||||
|
Console console{{UI_POS_X(0), UI_POS_Y(6) + 8, UI_POS_MAXWIDTH, screen_height - (UI_POS_Y(7) + 8)}};
|
||||||
|
|
||||||
|
TetraChannelDecoder decoder{};
|
||||||
|
|
||||||
|
void on_data_tetra(const TetraBurstMessage&);
|
||||||
|
void on_data_dnb(const TetraDnbMessage&);
|
||||||
|
|
||||||
|
uint8_t rate_limiter{0}; // limiter, to keep the device responsive
|
||||||
|
void on_timer();
|
||||||
|
|
||||||
|
MessageHandlerRegistration message_handler_frame_sync{
|
||||||
|
Message::ID::DisplayFrameSync,
|
||||||
|
[this](const Message* const) {
|
||||||
|
this->on_timer();
|
||||||
|
}};
|
||||||
|
|
||||||
|
MessageHandlerRegistration message_handler_packet{
|
||||||
|
Message::ID::TetraBsch,
|
||||||
|
[this](Message* const p) {
|
||||||
|
this->on_data_tetra(*static_cast<const TetraBurstMessage*>(p));
|
||||||
|
}};
|
||||||
|
|
||||||
|
MessageHandlerRegistration message_handler_dnb{
|
||||||
|
Message::ID::TetraDnb,
|
||||||
|
[this](Message* const p) {
|
||||||
|
this->on_data_dnb(*static_cast<const TetraDnbMessage*>(p));
|
||||||
|
}};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ui::external_app::tetra_rx
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -785,6 +785,15 @@ set(MODE_CPPSRC
|
|||||||
)
|
)
|
||||||
DeclareTargets(PTSK time_sink)
|
DeclareTargets(PTSK time_sink)
|
||||||
|
|
||||||
|
|
||||||
|
### Tetra Rx
|
||||||
|
|
||||||
|
set(MODE_CPPSRC
|
||||||
|
proc_tetra.cpp
|
||||||
|
)
|
||||||
|
DeclareTargets(PTET tetra_rx)
|
||||||
|
|
||||||
|
|
||||||
set(MODE_CPPSRC
|
set(MODE_CPPSRC
|
||||||
sd_over_usb/proc_sd_over_usb.cpp
|
sd_over_usb/proc_sd_over_usb.cpp
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,239 @@
|
|||||||
|
#include "proc_tetra.hpp"
|
||||||
|
#include "portapack_shared_memory.hpp"
|
||||||
|
#include "sine_table_int8.hpp"
|
||||||
|
#include "event_m4.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
TetraProcessor::TetraProcessor() {
|
||||||
|
decim_0.configure(taps_25k0_tetra_decim_0.taps);
|
||||||
|
decim_1.configure(taps_25k0_tetra_decim_1.taps);
|
||||||
|
|
||||||
|
baseband_thread.start();
|
||||||
|
configured = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TetraProcessor::execute(const buffer_c8_t& buffer) {
|
||||||
|
if (!configured) return;
|
||||||
|
|
||||||
|
const auto decim_0_out = decim_0.execute(buffer, dst_buffer_0);
|
||||||
|
const auto channel_out = decim_1.execute(decim_0_out, dst_buffer_1);
|
||||||
|
|
||||||
|
feed_channel_stats(channel_out);
|
||||||
|
|
||||||
|
for (size_t i = 0; i < channel_out.count; i++) {
|
||||||
|
complex16_t sample = channel_out.p[i];
|
||||||
|
|
||||||
|
// Carrier PLL Rotation
|
||||||
|
uint8_t phase_idx = (pll_phase >> 24) & 0xFF;
|
||||||
|
uint8_t cos_idx = (phase_idx + 64) & 0xFF;
|
||||||
|
int16_t rotated_real = (sample.real() * sine_table_i8[cos_idx] - sample.imag() * sine_table_i8[phase_idx]) >> 7;
|
||||||
|
int16_t rotated_imag = (sample.real() * sine_table_i8[phase_idx] + sample.imag() * sine_table_i8[cos_idx]) >> 7;
|
||||||
|
complex16_t rotated_sample = {rotated_real, rotated_imag};
|
||||||
|
|
||||||
|
uint32_t old_phase = symbol_phase;
|
||||||
|
symbol_phase += symbol_phase_inc;
|
||||||
|
|
||||||
|
// Half-symbol timing
|
||||||
|
if ((old_phase < 0x80000000) && (symbol_phase >= 0x80000000)) {
|
||||||
|
mid_sample = rotated_sample;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full-symbol timing
|
||||||
|
if (symbol_phase < old_phase) {
|
||||||
|
prev_prompt = prompt_sample;
|
||||||
|
prompt_sample = rotated_sample;
|
||||||
|
|
||||||
|
// Gardner Timing Error Detector
|
||||||
|
int32_t err_i = (mid_sample.real() * (prompt_sample.real() - prev_prompt.real()));
|
||||||
|
int32_t err_q = (mid_sample.imag() * (prompt_sample.imag() - prev_prompt.imag()));
|
||||||
|
int32_t timing_error = err_i + err_q;
|
||||||
|
|
||||||
|
const int64_t delta = (timing_error >> 16);
|
||||||
|
int64_t inc = static_cast<int64_t>(symbol_phase_inc) + delta;
|
||||||
|
// CLAMPING: Prevent the NCO from drifting into the noise!
|
||||||
|
if (inc < 1600000000) inc = 1600000000;
|
||||||
|
if (inc > 1620000000) inc = 1620000000;
|
||||||
|
symbol_phase_inc = static_cast<uint32_t>(inc);
|
||||||
|
|
||||||
|
process_symbol(prompt_sample);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TetraProcessor::process_symbol(const complex16_t& current_sample) {
|
||||||
|
// Differential phase demodulation
|
||||||
|
int32_t dot_i = (current_sample.real() * delay_sample.real()) + (current_sample.imag() * delay_sample.imag());
|
||||||
|
int32_t dot_q = (current_sample.imag() * delay_sample.real()) - (current_sample.real() * delay_sample.imag());
|
||||||
|
delay_sample = current_sample;
|
||||||
|
|
||||||
|
// Phase error for Carrier PLL
|
||||||
|
int32_t phase_err = 0;
|
||||||
|
if (dot_i > 0 && dot_q > 0)
|
||||||
|
phase_err = dot_q - dot_i;
|
||||||
|
else if (dot_i < 0 && dot_q > 0)
|
||||||
|
phase_err = dot_q + dot_i;
|
||||||
|
else if (dot_i < 0 && dot_q < 0)
|
||||||
|
phase_err = -dot_q + dot_i;
|
||||||
|
else
|
||||||
|
phase_err = -dot_q - dot_i;
|
||||||
|
|
||||||
|
pll_freq += (phase_err * pll_beta) >> 8;
|
||||||
|
|
||||||
|
// CLAMPING: Prevent frequency tracking from flying away
|
||||||
|
if (pll_freq > 400000000) pll_freq = 400000000;
|
||||||
|
if (pll_freq < -400000000) pll_freq = -400000000;
|
||||||
|
|
||||||
|
pll_phase += ((phase_err * pll_alpha) >> 8) + pll_freq;
|
||||||
|
|
||||||
|
// ETSI EN 300 392-2 pi/4 DQPSK bit mapping
|
||||||
|
uint8_t dibit = 0;
|
||||||
|
if (dot_i > 0 && dot_q > 0)
|
||||||
|
dibit = 0b00;
|
||||||
|
else if (dot_i < 0 && dot_q > 0)
|
||||||
|
dibit = 0b01;
|
||||||
|
else if (dot_i < 0 && dot_q < 0)
|
||||||
|
dibit = 0b11;
|
||||||
|
else
|
||||||
|
dibit = 0b10;
|
||||||
|
|
||||||
|
// Insert bits into history buffer and sync register
|
||||||
|
for (int b = 1; b >= 0; b--) {
|
||||||
|
uint8_t bit_val = (dibit >> b) & 0x01;
|
||||||
|
|
||||||
|
size_t byte_idx = (history_write_idx / 8) % 128;
|
||||||
|
if ((history_write_idx % 8) == 0) bit_history_buffer[byte_idx] = 0;
|
||||||
|
|
||||||
|
bit_history_buffer[byte_idx] |= (bit_val << (7 - (history_write_idx % 8)));
|
||||||
|
history_write_idx = (history_write_idx + 1) % 1024;
|
||||||
|
bit_count++;
|
||||||
|
|
||||||
|
sync_register = ((sync_register << 1) | bit_val);
|
||||||
|
|
||||||
|
uint64_t sync =
|
||||||
|
sync_register & 0x3FFFFFFFFFULL;
|
||||||
|
|
||||||
|
uint32_t err_pos =
|
||||||
|
__builtin_popcountll(sync ^ Y_SYNC);
|
||||||
|
|
||||||
|
uint32_t err_neg =
|
||||||
|
__builtin_popcountll(
|
||||||
|
sync ^
|
||||||
|
(~Y_SYNC & 0x3FFFFFFFFFULL));
|
||||||
|
|
||||||
|
if (!pending_dsb.valid && bit_count >= Y_SYNC_BITS && (err_pos <= 4 || err_neg <= 4)) {
|
||||||
|
const uint64_t sync_start = bit_count - Y_SYNC_BITS;
|
||||||
|
|
||||||
|
if (sync_start >= SYNC_OFFSET) {
|
||||||
|
pending_dsb.valid = true;
|
||||||
|
pending_dsb.burst_start = sync_start - SYNC_OFFSET;
|
||||||
|
pending_dsb.ready_at = pending_dsb.burst_start + BURST_BITS;
|
||||||
|
pending_dsb.inverted = err_neg < err_pos;
|
||||||
|
pending_dsb.errors = std::min(err_pos, err_neg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ENABLE_DNB_MESSAGES) {
|
||||||
|
const uint32_t train_mask = (1UL << DNB_TRAIN_BITS) - 1;
|
||||||
|
const uint32_t train = sync_register & train_mask;
|
||||||
|
const uint32_t n_err_pos = __builtin_popcount(train ^ N_SYNC);
|
||||||
|
const uint32_t n_err_neg = __builtin_popcount(train ^ (~N_SYNC & train_mask));
|
||||||
|
const uint32_t p_err_pos = __builtin_popcount(train ^ P_SYNC);
|
||||||
|
const uint32_t p_err_neg = __builtin_popcount(train ^ (~P_SYNC & train_mask));
|
||||||
|
|
||||||
|
if (!pending_dnb.valid && bit_count >= DNB_TRAIN_BITS && (n_err_pos <= 1 || n_err_neg <= 1 || p_err_pos <= 1 || p_err_neg <= 1)) {
|
||||||
|
const bool p_train = std::min(p_err_pos, p_err_neg) < std::min(n_err_pos, n_err_neg);
|
||||||
|
const uint32_t pos_err = p_train ? p_err_pos : n_err_pos;
|
||||||
|
const uint32_t neg_err = p_train ? p_err_neg : n_err_neg;
|
||||||
|
|
||||||
|
const uint64_t train_start = bit_count - DNB_TRAIN_BITS;
|
||||||
|
const uint64_t block1_start = train_start - 230;
|
||||||
|
const uint64_t block2_start = train_start + 38;
|
||||||
|
|
||||||
|
if (train_start >= 230) {
|
||||||
|
pending_dnb.valid = true;
|
||||||
|
pending_dnb.block1_start = block1_start;
|
||||||
|
pending_dnb.block2_start = block2_start;
|
||||||
|
pending_dnb.ready_at = block2_start + DNB_BLK_BITS;
|
||||||
|
pending_dnb.inverted = neg_err < pos_err;
|
||||||
|
pending_dnb.p_train = p_train;
|
||||||
|
pending_dnb.errors = std::min(pos_err, neg_err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pending_dnb.valid && bit_count >= pending_dnb.ready_at)
|
||||||
|
push_dnb_to_ui();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pending_dsb.valid && bit_count >= pending_dsb.ready_at)
|
||||||
|
push_burst_to_ui();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t TetraProcessor::history_bit(uint64_t absolute_bit) const {
|
||||||
|
const size_t p = absolute_bit % 1024;
|
||||||
|
|
||||||
|
return (bit_history_buffer[p >> 3] >>
|
||||||
|
(7 - (p & 7))) &
|
||||||
|
1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TetraProcessor::push_burst_to_ui() {
|
||||||
|
std::array<uint8_t, 63> burst{};
|
||||||
|
|
||||||
|
for (size_t i = 0; i < BURST_BITS; i++) {
|
||||||
|
uint8_t b = history_bit(pending_dsb.burst_start + i);
|
||||||
|
|
||||||
|
if (pending_dsb.inverted)
|
||||||
|
b ^= 1;
|
||||||
|
|
||||||
|
burst[i >> 3] |=
|
||||||
|
b << (7 - (i & 7));
|
||||||
|
}
|
||||||
|
|
||||||
|
shared_memory.application_queue.push(
|
||||||
|
TetraBurstMessage(
|
||||||
|
burst.data(),
|
||||||
|
pending_dsb.inverted,
|
||||||
|
pending_dsb.errors));
|
||||||
|
|
||||||
|
pending_dsb.valid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TetraProcessor::push_dnb_to_ui() {
|
||||||
|
std::array<uint8_t, 54> burst{};
|
||||||
|
|
||||||
|
for (size_t i = 0; i < DNB_BLK_BITS; i++) {
|
||||||
|
uint8_t b = history_bit(pending_dnb.block1_start + i);
|
||||||
|
if (pending_dnb.inverted)
|
||||||
|
b ^= 1;
|
||||||
|
burst[i >> 3] |= b << (7 - (i & 7));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < DNB_BLK_BITS; i++) {
|
||||||
|
uint8_t b = history_bit(pending_dnb.block2_start + i);
|
||||||
|
if (pending_dnb.inverted)
|
||||||
|
b ^= 1;
|
||||||
|
const size_t out_bit = DNB_BLK_BITS + i;
|
||||||
|
burst[out_bit >> 3] |= b << (7 - (out_bit & 7));
|
||||||
|
}
|
||||||
|
|
||||||
|
shared_memory.application_queue.push(
|
||||||
|
TetraDnbMessage(
|
||||||
|
burst.data(),
|
||||||
|
pending_dnb.inverted,
|
||||||
|
pending_dnb.errors,
|
||||||
|
pending_dnb.p_train));
|
||||||
|
|
||||||
|
pending_dnb.valid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TetraProcessor::on_message(const Message* const msg) {
|
||||||
|
(void)msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
EventDispatcher event_dispatcher{std::make_unique<TetraProcessor>()};
|
||||||
|
event_dispatcher.run();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
#ifndef __PROC_TETRA_H__
|
||||||
|
#define __PROC_TETRA_H__
|
||||||
|
|
||||||
|
#include "baseband_processor.hpp"
|
||||||
|
#include "baseband_thread.hpp"
|
||||||
|
#include "message.hpp"
|
||||||
|
#include "dsp_decimate.hpp"
|
||||||
|
#include "dsp_fir_taps.hpp"
|
||||||
|
#include "rssi_thread.hpp"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <array>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
class TetraProcessor : public BasebandProcessor {
|
||||||
|
public:
|
||||||
|
TetraProcessor();
|
||||||
|
|
||||||
|
void execute(const buffer_c8_t& buffer) override;
|
||||||
|
void on_message(const Message* const msg);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr size_t baseband_fs = 3072000;
|
||||||
|
static constexpr size_t channel_fs = 48000;
|
||||||
|
static constexpr uint32_t symbol_rate = 18000;
|
||||||
|
|
||||||
|
std::array<complex16_t, 512> dst_0{};
|
||||||
|
const buffer_c16_t dst_buffer_0{dst_0.data(), dst_0.size()};
|
||||||
|
|
||||||
|
std::array<complex16_t, 512> dst_1{};
|
||||||
|
const buffer_c16_t dst_buffer_1{dst_1.data(), dst_1.size()};
|
||||||
|
|
||||||
|
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{};
|
||||||
|
dsp::decimate::FIRC16xR16x32Decim8 decim_1{};
|
||||||
|
|
||||||
|
// PLL and Timing
|
||||||
|
uint32_t symbol_phase{0};
|
||||||
|
uint32_t symbol_phase_inc{(uint32_t)((symbol_rate * 4294967296.0) / channel_fs)};
|
||||||
|
|
||||||
|
complex16_t prompt_sample{0, 0};
|
||||||
|
complex16_t prev_prompt{0, 0};
|
||||||
|
complex16_t mid_sample{0, 0};
|
||||||
|
complex16_t delay_sample{0, 0};
|
||||||
|
static constexpr uint32_t BURST_BITS = 500;
|
||||||
|
static constexpr uint32_t Y_SYNC_BITS = 38;
|
||||||
|
static constexpr uint32_t SYNC_OFFSET = 214;
|
||||||
|
static constexpr uint64_t Y_SYNC = 0x30673A7067ULL;
|
||||||
|
static constexpr uint32_t DNB_TRAIN_BITS = 22;
|
||||||
|
static constexpr uint32_t DNB_BLK_BITS = 216;
|
||||||
|
static constexpr uint32_t DNB_TRAIN_TO_BLK2 = 22;
|
||||||
|
static constexpr uint32_t DNB_TOTAL_T5_BITS = DNB_BLK_BITS * 2;
|
||||||
|
static constexpr uint32_t N_SYNC = 0x343A74UL;
|
||||||
|
static constexpr uint32_t P_SYNC = 0x1E90DEUL;
|
||||||
|
static constexpr bool ENABLE_DNB_MESSAGES = true;
|
||||||
|
|
||||||
|
uint32_t pll_phase{0};
|
||||||
|
int32_t pll_freq{0};
|
||||||
|
static constexpr int32_t pll_alpha = 150;
|
||||||
|
static constexpr int32_t pll_beta = 2;
|
||||||
|
|
||||||
|
uint64_t sync_register{0};
|
||||||
|
std::array<uint8_t, 128> bit_history_buffer{};
|
||||||
|
size_t history_write_idx{0};
|
||||||
|
uint64_t bit_count{0};
|
||||||
|
bool configured{false};
|
||||||
|
|
||||||
|
struct PendingDnb {
|
||||||
|
bool valid{false};
|
||||||
|
uint64_t block1_start{0};
|
||||||
|
uint64_t block2_start{0};
|
||||||
|
uint64_t ready_at{0};
|
||||||
|
bool inverted{false};
|
||||||
|
bool p_train{false};
|
||||||
|
uint8_t errors{0};
|
||||||
|
} pending_dnb{};
|
||||||
|
|
||||||
|
struct PendingDsb {
|
||||||
|
bool valid{false};
|
||||||
|
uint64_t burst_start{0};
|
||||||
|
uint64_t ready_at{0};
|
||||||
|
bool inverted{false};
|
||||||
|
uint8_t errors{0};
|
||||||
|
} pending_dsb{};
|
||||||
|
|
||||||
|
void process_symbol(const complex16_t& sample);
|
||||||
|
uint8_t history_bit(uint64_t absolute_bit) const;
|
||||||
|
void push_burst_to_ui();
|
||||||
|
void push_dnb_to_ui();
|
||||||
|
|
||||||
|
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive, false};
|
||||||
|
RSSIThread rssi_thread{};
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // __PROC_TETRA_H__
|
||||||
@@ -2070,4 +2070,20 @@ static constexpr fir_taps_complex<63> taps_1k5_LSB_channel = {
|
|||||||
}},
|
}},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TETRA 25kHz filters
|
||||||
|
// IFIR wideband pre-filter: fs=3072000, pass=90000, decim=8, fout=384000
|
||||||
|
constexpr fir_taps_real<24> taps_25k0_tetra_decim_0 = {
|
||||||
|
.low_frequency_normalized = -90000.0f / 3072000.0f,
|
||||||
|
.high_frequency_normalized = 90000.0f / 3072000.0f,
|
||||||
|
.transition_normalized = 160000.0f / 3072000.0f,
|
||||||
|
.taps = {{55, 122, 244, 424, 666, 965, 1308, 1669, 2019, 2321, 2544, 2663, 2663, 2544, 2321, 2019, 1669, 1308, 965, 666, 424, 244, 122, 55}},
|
||||||
|
};
|
||||||
|
// IFIR channel filter: fs=384000, pass=15000, decim=8, fout=48000
|
||||||
|
constexpr fir_taps_real<32> taps_25k0_tetra_decim_1 = {
|
||||||
|
.low_frequency_normalized = -15000.0f / 384000.0f,
|
||||||
|
.high_frequency_normalized = 15000.0f / 384000.0f,
|
||||||
|
.transition_normalized = 25000.0f / 384000.0f,
|
||||||
|
.taps = {{2, -18, -60, -124, -198, -259, -267, -177, 53, 448, 1003, 1677, 2398, 3068, 3585, 3868, 3868, 3585, 3068, 2398, 1677, 1003, 448, 53, -177, -267, -259, -198, -124, -60, -18, 2}},
|
||||||
|
};
|
||||||
|
|
||||||
#endif /*__DSP_FIR_TAPS_H__*/
|
#endif /*__DSP_FIR_TAPS_H__*/
|
||||||
|
|||||||
@@ -168,6 +168,8 @@ class Message {
|
|||||||
HunterConfig = 110,
|
HunterConfig = 110,
|
||||||
HunterTrigger = 111,
|
HunterTrigger = 111,
|
||||||
HunterStop = 112,
|
HunterStop = 112,
|
||||||
|
TetraBsch = 113,
|
||||||
|
TetraDnb = 114,
|
||||||
MAX
|
MAX
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2009,4 +2011,46 @@ class HunterStopMessage : public Message {
|
|||||||
: Message{ID::HunterStop} {}
|
: Message{ID::HunterStop} {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct TetraBurstMessage : public Message {
|
||||||
|
constexpr TetraBurstMessage(
|
||||||
|
const uint8_t* bits,
|
||||||
|
bool inv,
|
||||||
|
uint8_t err)
|
||||||
|
: Message(Message::ID::TetraBsch),
|
||||||
|
inverted(inv),
|
||||||
|
sync_errors(err),
|
||||||
|
payload{} {
|
||||||
|
for (size_t i = 0; i < 63; i++)
|
||||||
|
payload[i] = bits[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
bool inverted;
|
||||||
|
uint8_t sync_errors;
|
||||||
|
|
||||||
|
// 500 bit
|
||||||
|
std::array<uint8_t, 63> payload;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TetraDnbMessage : public Message {
|
||||||
|
constexpr TetraDnbMessage(
|
||||||
|
const uint8_t* bits,
|
||||||
|
bool inv,
|
||||||
|
uint8_t err,
|
||||||
|
bool p_train)
|
||||||
|
: Message(Message::ID::TetraDnb),
|
||||||
|
inverted(inv),
|
||||||
|
train_errors(err),
|
||||||
|
is_p_train(p_train),
|
||||||
|
payload{} {
|
||||||
|
for (size_t i = 0; i < 54; i++)
|
||||||
|
payload[i] = bits[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
bool inverted;
|
||||||
|
uint8_t train_errors;
|
||||||
|
bool is_p_train;
|
||||||
|
|
||||||
|
// 432 TCH type-5 bits: 216 bits before the training sequence + 216 bits after.
|
||||||
|
std::array<uint8_t, 54> payload;
|
||||||
|
};
|
||||||
#endif /*__MESSAGE_H__*/
|
#endif /*__MESSAGE_H__*/
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ constexpr image_tag_t image_tag_vor_rx{'P', 'V', 'R', 'X'};
|
|||||||
constexpr image_tag_t image_tag_rttyrx{'P', 'R', 'T', 'R'};
|
constexpr image_tag_t image_tag_rttyrx{'P', 'R', 'T', 'R'};
|
||||||
constexpr image_tag_t image_tag_rttytx{'P', 'R', 'T', 'T'};
|
constexpr image_tag_t image_tag_rttytx{'P', 'R', 'T', 'T'};
|
||||||
constexpr image_tag_t image_tag_tonedetect{'P', 'T', 'N', 'E'};
|
constexpr image_tag_t image_tag_tonedetect{'P', 'T', 'N', 'E'};
|
||||||
|
constexpr image_tag_t image_tag_tetrarx{'P', 'T', 'E', 'T'};
|
||||||
|
|
||||||
constexpr image_tag_t image_tag_noop{'P', 'N', 'O', 'P'};
|
constexpr image_tag_t image_tag_noop{'P', 'N', 'O', 'P'};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user