Added tetra rx ext app (#3257)

This commit is contained in:
Totoo
2026-07-09 09:10:08 +02:00
committed by GitHub
parent 42122a739c
commit d671455f79
22 changed files with 1402 additions and 0 deletions
+12
View File
@@ -380,6 +380,17 @@ set(EXTCPPSRC
#signal_hunter
external/signal_hunter/main.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
@@ -473,6 +484,7 @@ set(EXTAPPLIST
hard_reset
secplustx
signal_hunter
tetra_rx
)
# sdusb has type conflicts with PRALINE (HackRF Pro) - add only for non-PRALINE builds
+10
View File
@@ -114,6 +114,7 @@ MEMORY
ram_external_app_vor_rx (rwx) : org = 0xAE090000, 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_tetra_rx (rwx) : org = 0xAE0C0000, len = 32k
}
SECTIONS
@@ -663,4 +664,13 @@ SECTIONS
KEEP(*(.external_app.app_signal_hunter.application_information));
*(*ui*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
View File
@@ -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
};
}
+45
View File
@@ -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
+33
View File
@@ -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
+17
View File
@@ -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
+24
View File
@@ -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
+18
View File
@@ -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
+354
View File
@@ -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
+192
View File
@@ -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