EPIRB TX SGB support (#3263)

* Update BEACONS.TXT
Added test beacon with 3 bch1 errors and 2 bch2 errors to test multiple bit error correction.
* Fist step to SGB format
* Added SGB to manual mode.
* SGB support
- Added SGB frame to beacons file
- Fixed hex display for SGB
- Fixed frame type option display
- Fixed self-test mode activation logic
- Fixed PRN for self-test mode
This commit is contained in:
Frederic BORRY
2026-07-09 10:36:26 +02:00
committed by GitHub
parent 7513cd46fc
commit d11669f711
7 changed files with 523 additions and 127 deletions
+198 -24
View File
@@ -143,30 +143,6 @@ static void push_bits(uint8_t* buf, int& pos, uint64_t v, int n) {
set_bit(buf, pos++, (v >> i) & 1);
}
/**
* Convert a beacon to hex string representation
* @param frame the frame to convert
* @param start true for the first half of the frame, false for the second half
* @return the hex string representation of the specieid half of the frame
*/
std::string beacon_to_hex_string(const uint8_t* frame, bool start) {
static const char hex[] = "0123456789ABCDEF";
std::string out;
out.resize(18);
int offset = start ? 0 : 9;
for (int i = 0; i < 9; i++) {
uint8_t b = frame[offset + i];
out[i * 2] = hex[b >> 4];
out[i * 2 + 1] = hex[b & 0x0F];
}
return out;
}
/**
* Generate a beacon in the provided buffer
* @param frame the buffer to generate the frame in
@@ -351,6 +327,204 @@ size_t generate_beacon(uint8_t* frame, const BeaconParams& params) {
return 18;
}
/**
* Convert a beacon hex string representation (generic range)
* @param frame the frame data
* @param offset_bytes starting byte offset
* @param count_bytes number of bytes to convert
* @return the hex string representation
*/
std::string beacon_to_hex_string_range(const uint8_t* frame, int offset_bytes, int count_bytes) {
static const char hex[] = "0123456789ABCDEF";
std::string out;
out.resize(count_bytes * 2);
for (int i = 0; i < count_bytes; i++) {
uint8_t b = frame[offset_bytes + i];
out[i * 2] = hex[b >> 4];
out[i * 2 + 1] = hex[b & 0x0F];
}
return out;
}
// ---------------------------------------------------------------------------
// SGB (Second Generation Beacon) — T.018 Rev 7, March 2021
// ---------------------------------------------------------------------------
// Reference values from T.018 canonical test vector (Appendix B.1)
#define SGB_TAC_NUMBER 230 // TAC number (16 bits, 065535)
#define SGB_SERIAL_NUMBER 573 // Serial number within TAC (14 bits, 016383)
#define SGB_HOMING_DEVICE 1 // 1 = beacon has 121.5 / 243 MHz homing device
#define SGB_RLS_FUNCTION 0 // 0 = no Return Link Service function
// BCH(250,202) generator polynomial — lower 48 bits (without leading X^48 term)
// Full g(x): X^48+X^47+X^46+X^42+X^41+X^40+X^39+X^38+X^37+X^35+X^33+X^32+X^31
// +X^26+X^24+X^23+X^22+X^20+X^19+X^18+X^17+X^16+X^13+X^12+X^11+X^10
// +X^7+X^4+X^2+X+1
// Binary MSB-first: 1110001111110101110000101110111110011110010010111 (T.018 App. B.1)
#define SGB_BCH_G_LOWER UINT64_C(0xC7EB85DF3C97)
/**
* Encode latitude for SGB GNSS location protocol (T.018 Appendix C)
* Pushes 23 bits: 1-bit N/S flag + 7-bit degrees + 15-bit decimal fraction
* @param frame the frame buffer
* @param pos current bit position (modified in-place)
* @param south true if southern hemisphere
* @param lat_mag absolute latitude in decimal degrees (0..90)
*/
static void push_sgb_latitude(uint8_t* frame, int& pos, bool south, float lat_mag) {
push_bits(frame, pos, south ? 1 : 0, 1);
uint32_t deg = (uint32_t)lat_mag;
if (deg > 90) deg = 90;
float frac = lat_mag - (float)deg;
uint32_t frac_n = (uint32_t)(frac * 32768.0f + 0.5f);
if (frac_n >= 32768) {
frac_n = 0;
if (deg < 90) deg++;
}
push_bits(frame, pos, deg, 7);
push_bits(frame, pos, frac_n, 15);
}
/**
* Encode longitude for SGB GNSS location protocol (T.018 Appendix C)
* Pushes 24 bits: 1-bit E/W flag + 8-bit degrees + 15-bit decimal fraction
* @param frame the frame buffer
* @param pos current bit position (modified in-place)
* @param west true if western hemisphere
* @param lon_mag absolute longitude in decimal degrees (0..180)
*/
static void push_sgb_longitude(uint8_t* frame, int& pos, bool west, float lon_mag) {
push_bits(frame, pos, west ? 1 : 0, 1);
uint32_t deg = (uint32_t)lon_mag;
if (deg > 180) deg = 180;
float frac = lon_mag - (float)deg;
uint32_t frac_n = (uint32_t)(frac * 32768.0f + 0.5f);
if (frac_n >= 32768) {
frac_n = 0;
if (deg < 180) deg++;
}
push_bits(frame, pos, deg, 8);
push_bits(frame, pos, frac_n, 15);
}
/**
* Compute BCH(250,202) parity using LFSR method (T.018 Appendix B.1)
* Reads 202 message bits from buffer positions 6..207 and writes
* 48 BCH parity bits to buffer positions 208..255.
* @param frame the 32-byte SGB buffer (message already written at bits 6..207)
*/
static void compute_sgb_bch(uint8_t* frame) {
uint64_t reg = 0;
for (int i = 0; i < 202; i++) {
int bp = i + 6; // buffer bit position (6 = start of message after padding)
bool msg_bit = (frame[bp >> 3] >> (7 - (bp & 7))) & 1;
bool feedback = ((reg >> 47) & 1) ^ (msg_bit ? 1u : 0u);
reg = (reg << 1) & UINT64_C(0xFFFFFFFFFFFF);
if (feedback) reg ^= SGB_BCH_G_LOWER;
}
// Append 48-bit remainder at buffer positions 208..255
int pos = 208;
push_bits(frame, pos, reg, 48);
}
/**
* Map BeaconType to SGB 3-bit beacon type code (T.018 Table 3.1, bits 138-140)
* 000=ELT, 001=EPIRB, 010=PLB
*/
static uint8_t sgb_beacon_type_code(BeaconType t) {
switch (t) {
case BeaconType::EPIRB:
return 0b001;
case BeaconType::PLB:
return 0b010;
default:
case BeaconType::ELT:
return 0b000;
}
}
/**
* Generate a Second Generation Beacon (SGB / T.018) frame.
* The 250-bit frame is stored in 32 bytes with 6 leading padding bits
* (bits 0..4 = 0, bit 5 = 1).
* The rotating field is Type 0 — G.008 Objective Requirements (Table 3.3),
* with elapsed_hours and time_last_loc_min updated from elapsed_s.
* @param frame 32-byte output buffer
* @param params beacon parameters
* @param elapsed_s seconds elapsed since beacon activation (drives rotating field)
* @return 32 (size of the generated frame in bytes)
*/
size_t generate_sgb_beacon(uint8_t* frame, const BeaconParams& params, uint32_t elapsed_s) {
memset(frame, 0, 32);
// 6 padding bits at start of 32-byte buffer: [0,0,0,0,1,0] (5th bit (1 based index as per specification) = 1 to indicate self-test mode)
set_bit(frame, 4, 1);
// Message bits start at buffer bit position 6 (= SGB message bit 1)
int pos = 6;
// Bits 1-16: TAC number (16 bits) — T.018 Table 3.1
push_bits(frame, pos, SGB_TAC_NUMBER, 16);
// Bits 17-30: Serial number within TAC (14 bits)
push_bits(frame, pos, SGB_SERIAL_NUMBER, 14);
// Bits 31-40: Country code (10 bits, ITU MID)
push_bits(frame, pos, params.country & 0x3FFU, 10);
// Bit 41: Status of homing device (1 = has 121.5/243 MHz homing device)
push_bits(frame, pos, params.has_121_5 ? 1 : 0, 1);
// Bit 42: RLS function flag (0 = no RLS)
push_bits(frame, pos, SGB_RLS_FUNCTION, 1);
// Bit 43: Test protocol flag
push_bits(frame, pos, params.is_test ? 1 : 0, 1);
// Bits 44-66: Encoded GNSS latitude (23 bits) — T.018 Appendix C
float lat_mag = params.location.latitude < 0.0f ? -params.location.latitude : params.location.latitude;
push_sgb_latitude(frame, pos, params.location.south, lat_mag);
// Bits 67-90: Encoded GNSS longitude (24 bits)
float lon_mag = params.location.longitude < 0.0f ? -params.location.longitude : params.location.longitude;
push_sgb_longitude(frame, pos, params.location.west, lon_mag);
// Bits 91-137: Vessel ID (47 bits = 3-bit type selector + 44-bit body)
// Type 000 = No aircraft/maritime identity; body all zeros
push_bits(frame, pos, 0, 47);
// Bits 138-140: Beacon type (3 bits)
push_bits(frame, pos, sgb_beacon_type_code(params.type), 3);
// Bits 141-154: Spare (14 bits, all 1s in normal operation)
push_bits(frame, pos, 0x3FFFU, 14);
// Bits 155-202: Rotating Field Type 0 — G.008 Objective Requirements (Table 3.3)
// Bits 155-158: Identifier (4 bits = 0000)
push_bits(frame, pos, 0b0000U, 4);
// Bits 159-164: Elapsed time since activation (6 bits, 0-63 h, truncated at 63)
uint32_t elapsed_h = elapsed_s / 3600;
if (elapsed_h > 63) elapsed_h = 63;
push_bits(frame, pos, elapsed_h, 6);
// Bits 165-175: Time from last encoded location (11 bits, 0-2046 min; 2047 = no fix)
uint32_t loc_age_min = elapsed_s / 60;
if (loc_age_min > 2046) loc_age_min = 2046;
push_bits(frame, pos, loc_age_min, 11);
// Bits 176-185: Altitude of encoded location (10 bits; 0x3FF = no altitude)
push_bits(frame, pos, 0x3FFU, 10);
// Bits 186-193: Dilution of Precision (4-bit HDOP + 4-bit VDOP; 0 = best HDOP, 0 = best VDOP)
push_bits(frame, pos, 0b00000000U, 8);
// Bits 194-195: Activation notification (00 = manual by user)
push_bits(frame, pos, 0b00U, 2);
// Bits 196-198: Remaining battery capacity (101 = >75%)
push_bits(frame, pos, 0b101U, 3);
// Bits 199-200: GNSS status (10 = 3D fix)
push_bits(frame, pos, 0b10U, 2);
// Bits 201-202: Spare (00)
push_bits(frame, pos, 0b00U, 2);
// pos == 208: 6 padding + 202 message bits consumed
// Bits 203-250: BCH(250,202) parity (48 bits) computed and written at positions 208-255
compute_sgb_bch(frame);
return 32;
}
} // namespace ui::external_app::epirb_tx
#endif /*__BEACON_H__*/
+63 -14
View File
@@ -67,12 +67,17 @@ static uint8_t hexToByte(char high, char low) {
return (hexval(high) << 4) | hexval(low);
}
std::string EPIRBTXAppView::frame_to_hex_string(bool start) {
return beacon_to_hex_string(epirb_tx_message.data, start);
std::string EPIRBTXAppView::frame_to_hex_string_range(int offset_bytes, int count_bytes) {
return beacon_to_hex_string_range(epirb_tx_message.data, offset_bytes, count_bytes);
}
void EPIRBTXAppView::generate_frame(BeaconParams params) {
epirb_tx_message.data_len = generate_beacon(epirb_tx_message.data, params);
if (format_sgb) {
uint32_t elapsed_s = sgb_start_time > 0 ? (chTimeNow() - sgb_start_time) / 1000 : 0;
epirb_tx_message.data_len = generate_sgb_beacon(epirb_tx_message.data, params, elapsed_s);
} else {
epirb_tx_message.data_len = generate_beacon(epirb_tx_message.data, params);
}
}
void EPIRBTXAppView::on_timer() {
@@ -125,7 +130,7 @@ void EPIRBTXAppView::update_am_transmission() {
if (am_enabled && transmitting && !transmitting_bpsk) {
// Start am transmission
// Restore am frequency
epirb_tx_message.mode_bpsk = false;
epirb_tx_message.mode_406 = false;
transmitter_model.set_target_frequency(am_frequency);
// Send config to baseband
baseband::set_epirb_tx_config(epirb_tx_message);
@@ -146,10 +151,18 @@ void EPIRBTXAppView::update_frame(bool updateConfig) {
file_mode_ui->text_description.set(beacon.description.substr(0, max_text_width_ext));
file_mode_ui->text_description_end.set(beacon.description.size() > max_text_width_ext ? "-" + beacon.description.substr(max_text_width_ext, max_text_width_ext + max_text_width_ext - 1) : "");
// Udapte frame content on display
text_frame.set(beacon.frame.substr(0, 18));
text_frame_end.set(beacon.frame.size() > 18 ? beacon.frame.substr(18, 36) : "");
bool is_fgb = beacon.frame.size() <= BEACON_HEXA_SIZE_FGB;
if (is_fgb) {
text_frame.set(beacon.frame.substr(0, BEACON_HEXA_SPLIT_FGB));
text_frame_end.set(beacon.frame.size() > BEACON_HEXA_SPLIT_FGB ? beacon.frame.substr(BEACON_HEXA_SPLIT_FGB, BEACON_HEXA_SPLIT_FGB) : "");
text_frame_sgb_end.set("");
} else {
text_frame.set(" " + beacon.frame.substr(1, BEACON_HEXA_SPLIT_SGB - 1));
text_frame_end.set(beacon.frame.size() > BEACON_HEXA_SPLIT_SGB ? beacon.frame.substr(BEACON_HEXA_SPLIT_SGB, BEACON_HEXA_SPLIT_SGB) : "");
text_frame_sgb_end.set(beacon.frame.size() > 2 * BEACON_HEXA_SPLIT_SGB ? beacon.frame.substr(2 * BEACON_HEXA_SPLIT_SGB, BEACON_HEXA_SPLIT_SGB) : "");
}
// Prepare tx configuration
epirb_tx_message.data_len = std::min<size_t>((beacon.frame.size() / 2), 18);
epirb_tx_message.data_len = std::min<size_t>((beacon.frame.size() / 2), BEACON_SIZE_SGB);
for (uint8_t i = 0; i < epirb_tx_message.data_len; i++) {
epirb_tx_message.data[i] = hexToByte(
beacon.frame[2 * i],
@@ -159,8 +172,17 @@ void EPIRBTXAppView::update_frame(bool updateConfig) {
// In manual mode, generate frame content for current beacon params
generate_frame(beacon_params);
// Update frame content on display
text_frame.set(frame_to_hex_string(true));
text_frame_end.set(frame_to_hex_string(false));
if (format_sgb) {
// SGB: 32 bytes across 3 lines (BEACON_HEXA_SPLIT_SGB = 22 hex chars = 11 bytes)
// Remove first nibble and replace it with a space to match COSPAS hexadecimal representation specification
text_frame.set(" " + frame_to_hex_string_range(0, BEACON_HEXA_SPLIT_SGB / 2).substr(1));
text_frame_end.set(frame_to_hex_string_range(BEACON_HEXA_SPLIT_SGB / 2, BEACON_HEXA_SPLIT_SGB / 2));
text_frame_sgb_end.set(frame_to_hex_string_range(BEACON_HEXA_SPLIT_SGB, (BEACON_HEXA_SPLIT_SGB / 2) - 1));
} else {
text_frame.set(frame_to_hex_string_range(0, BEACON_HEXA_SPLIT_FGB / 2));
text_frame_end.set(frame_to_hex_string_range(BEACON_HEXA_SPLIT_FGB / 2, BEACON_HEXA_SPLIT_FGB / 2));
text_frame_sgb_end.set("");
}
}
if (updateConfig && send_on_change && loop) {
// Need to update config / send new beacon
@@ -176,14 +198,14 @@ void EPIRBTXAppView::update_frame(bool updateConfig) {
}
void EPIRBTXAppView::update_config() {
if (!epirb_tx_message.mode_bpsk) {
if (!epirb_tx_message.mode_406) {
// Previously in AM mode => restore bpsk frequency
transmitter_model.set_target_frequency(bpsk_frequency);
// Update displayed frequency
tx_view.on_show();
}
// Set mode to bpsk
epirb_tx_message.mode_bpsk = true;
epirb_tx_message.mode_406 = true;
transmitting_bpsk = true;
// Set pre/post count
epirb_tx_message.pre_count = (160 * TONES_SAMPLERATE) / 1000; // 160 ms carrier (COSPAS spec.)
@@ -198,6 +220,13 @@ void EPIRBTXAppView::set_tx_button_state(bool active) {
}
void EPIRBTXAppView::start_tx() {
if (format_sgb && !mode_file) {
// Track start time for the SGB rotating field elapsed-time counter
if (!transmitting) sgb_start_time = chTimeNow();
// update_frame regenerates the SGB frame with current elapsed time and refreshes display
update_frame(false);
set_dirty();
}
last_frame_time = chTimeNow();
update_config();
loop = loop_enabled;
@@ -221,7 +250,7 @@ void EPIRBTXAppView::on_tx_progress(const uint32_t progress, const bool done) {
transmitting_bpsk = false;
if (am_enabled) {
// BPSK frame sent, switch back to 121.5 AM signal
epirb_tx_message.mode_bpsk = false;
epirb_tx_message.mode_406 = false;
// Start am transmission
update_am_transmission();
} else {
@@ -310,6 +339,7 @@ void EPIRBTXAppView::update_mode() {
options_beacon_type.hidden(mode_file);
options_beacon_protocol.hidden(mode_file);
options_beacon_country.hidden(mode_file);
options_format.hidden(mode_file);
text_field_beacon_locator.hidden(mode_file);
}
@@ -323,6 +353,7 @@ EPIRBTXAppView::EPIRBTXAppView(
&text_beacon_type,
&options_beacon_type,
&options_beacon_protocol,
&options_format,
&text_beacon_country,
&options_beacon_country,
&checkbox_beacon_internal,
@@ -335,6 +366,7 @@ EPIRBTXAppView::EPIRBTXAppView(
&text_field_beacon_locator,
&text_frame,
&text_frame_end,
&text_frame_sgb_end,
&text_timeout,
&checkbox_loop,
&field_delay,
@@ -376,6 +408,7 @@ EPIRBTXAppView::EPIRBTXAppView(
init_from_locator(beacon_params.location);
update_mode();
update_location();
options_format.set_by_value(format_sgb ? 1 : 0);
options_mode.on_change = [this](size_t, OptionsField::value_t value) {
mode_file = (((BeaconMode)value) == BeaconMode::FILE);
@@ -405,6 +438,13 @@ EPIRBTXAppView::EPIRBTXAppView(
set_dirty();
};
options_format.on_change = [this](size_t, OptionsField::value_t v) {
format_sgb = (v == 1);
sgb_start_time = 0; // reset elapsed counter when format changes
update_frame();
set_dirty();
};
options_am_channel.on_change = [this](size_t, OptionsField::value_t v) {
bool is_real = false;
switch ((AmChannel)v) {
@@ -458,6 +498,9 @@ EPIRBTXAppView::EPIRBTXAppView(
case BpskChannel::O:
bpsk_frequency = BPSK_FREQUENCY_O;
break;
case BpskChannel::SGB:
bpsk_frequency = BPSK_FREQUENCY_SGB;
break;
default:
v = (uint8_t)BpskChannel::HAM;
// fallthrough
@@ -590,6 +633,9 @@ EPIRBTXAppView::EPIRBTXAppView(
case BPSK_FREQUENCY_O:
bpsk_channel = (uint8_t)BpskChannel::O;
break;
case BPSK_FREQUENCY_SGB:
bpsk_channel = (uint8_t)BpskChannel::SGB;
break;
default:
bpsk_channel = (uint8_t)BpskChannel::MANUAL;
manual_bpsk_frequency = bpsk_frequency;
@@ -642,11 +688,14 @@ void EPIRBTXAppView::load_beacons() {
Beacon beacon{};
beacon.title = trim(cols[0]);
beacon.description = trim(cols[1]);
// Make sure frame is not longer tha 18 bytes / 36 hex character
beacon.frame = trim(cols[2]).substr(0, 36);
// 1G uses up to 36 hex chars, 2G uses 64 hex chars (250 bits rounded to 32 bytes).
beacon.frame = trim(cols[2]).substr(0, BEACON_HEXA_SIZE_SGB);
size_t size = beacon.frame.size();
if (size <= 0)
continue; // Invalid line.
if ((size % 2) != 0) {
beacon.frame = "0" + beacon.frame; // Pad with leading 0 to make it even length
}
// Beacon is valid, add it to the list
beacons.emplace_back(std::move(beacon));
}
+27 -6
View File
@@ -33,9 +33,12 @@
#include "message.hpp"
#include "tonesets.hpp"
#define BEACON_HEXA_SIZE 36
#define BEACON_HEXA_HALF_SIZE 18
#define BEACON_SIZE 18
#define BEACON_SIZE_FGB 18
#define BEACON_SIZE_SGB 32
#define BEACON_HEXA_SIZE_FGB BEACON_SIZE_FGB * 2
#define BEACON_HEXA_SIZE_SGB BEACON_SIZE_SGB * 2
#define BEACON_HEXA_SPLIT_FGB 18
#define BEACON_HEXA_SPLIT_SGB 22
#define AM_TEST_FREQUENCY 121375000
#define AM_REAL_FREQUENCY 121500000
@@ -49,6 +52,7 @@
#define BPSK_FREQUENCY_K 406052000
#define BPSK_FREQUENCY_N 406061000
#define BPSK_FREQUENCY_O 406064000
#define BPSK_FREQUENCY_SGB 406050000
namespace ui::external_app::epirb_tx {
@@ -85,7 +89,8 @@ enum class BpskChannel {
K = 6,
N = 7,
O = 8,
MANUAL = 10
MANUAL = 10,
SGB = 100
};
struct Location {
@@ -129,7 +134,7 @@ class EPIRBTXAppView : public View {
void on_timer();
void load_beacons();
void set_tx_button_state(bool active);
std::string frame_to_hex_string(bool start);
std::string frame_to_hex_string_range(int offset_bytes, int count_bytes);
void generate_frame(BeaconParams params);
void update_frame(bool updateConfig = true);
void update_bpsk_frequency();
@@ -214,6 +219,7 @@ class EPIRBTXAppView : public View {
{"country"sv, &beacon_country},
{"internal"sv, &beacon_internal},
{"locator"sv, &locator},
{"sgb"sv, &format_sgb},
}};
// Time of the last sent frame
@@ -224,6 +230,10 @@ class EPIRBTXAppView : public View {
bool transmitting_bpsk{false};
// True when currently looping on sending beacons
bool loop{false};
// True when SGB (Second Generation Beacon) format is selected
bool format_sgb{false};
// System time (ms) when the current SGB transmission session started
uint32_t sgb_start_time{0};
// Current EPIRBTXDataMessage for baseband
EPIRBTXDataMessage epirb_tx_message{};
@@ -302,10 +312,17 @@ class EPIRBTXAppView : public View {
{"PLB", (uint8_t)BeaconType::PLB}}};
OptionsField options_beacon_protocol{
{UI_POS_X(9 + 7), UI_POS_Y(1)},
30,
8,
{{"User", (uint8_t)BeaconProtocol::USER},
{"Standard", (uint8_t)BeaconProtocol::STANDARD},
{"National", (uint8_t)BeaconProtocol::NATIONAL}}};
// Format selector (FGB / SGB) — manual mode only, same row as type/protocol
OptionsField options_format{
{UI_POS_X_RIGHT(4), UI_POS_Y(1)},
4,
{{"FGB", 0},
{"SGB", 1}}};
OptionsField options_beacon_country{
{UI_POS_X(9), UI_POS_Y(2)},
7,
@@ -337,6 +354,9 @@ class EPIRBTXAppView : public View {
Text text_frame_end{
{UI_POS_X(6), UI_POS_Y(7), UI_POS_WIDTH_REMAINING(6), UI_POS_DEFAULT_HEIGHT},
""};
Text text_frame_sgb_end{
{UI_POS_X(6), UI_POS_Y(8), UI_POS_WIDTH_REMAINING(6), UI_POS_DEFAULT_HEIGHT},
""};
Text text_timeout{
{UI_POS_X(14), UI_POS_Y(10), UI_POS_WIDTH(2), UI_POS_DEFAULT_HEIGHT},
@@ -389,6 +409,7 @@ class EPIRBTXAppView : public View {
{"406.052 MHz (K)", (uint8_t)BpskChannel::K},
{"406.061 MHz (N)", (uint8_t)BpskChannel::N},
{"406.064 MHz (O)", (uint8_t)BpskChannel::O},
{"406.050 MHz (SGB)", (uint8_t)BpskChannel::SGB},
{"Manual", (uint8_t)BpskChannel::MANUAL}}};
// Transmitter view
+179 -73
View File
@@ -27,6 +27,70 @@
#include <cstdint>
#include <cstring>
uint8_t EPIRBTXProcessor::get_frame_bit(uint16_t bit_pos) const {
// Skip first 6 bits (padding for 250 bits -> 32 bytes)
bit_pos += 6;
if (bit_pos >= frame_sgb_bits_len) {
return 0;
}
const uint8_t byte_value = frame_data[bit_pos >> 3];
const uint8_t bit_offset = 7 - (bit_pos & 0x07);
return (byte_value >> bit_offset) & 0x01;
}
int8_t EPIRBTXProcessor::compute_sgb_chip_level(SGBChannelState& channel, bool q_channel) {
// Table 2.4 behavior: chip = PRN for bit 0, inverted PRN for bit 1.
// PRN generator (23-bit LFSR): out = reg[0], feedback = reg[0] XOR reg[18].
const uint8_t prn_chip = channel.prn_state & 0x01;
uint8_t info_bit = 0;
const uint32_t bit_in_channel = channel.chip_index / sgb_chips_per_bit;
if (bit_in_channel >= sgb_preamble_bits_per_channel) {
const uint32_t message_index = bit_in_channel - sgb_preamble_bits_per_channel;
if (message_index < (sgb_message_bits / 2)) {
const uint16_t frame_bit_pos = q_channel ? (message_index * 2) + 1 : (message_index * 2);
info_bit = get_frame_bit(frame_bit_pos);
}
}
const uint8_t xor_chip = info_bit ^ prn_chip;
const int8_t chip_level = xor_chip ? -sgb_chip_amplitude : sgb_chip_amplitude;
const uint8_t feedback = prn_chip ^ ((channel.prn_state >> 18) & 0x01);
channel.prn_state = (channel.prn_state >> 1) | (uint32_t(feedback) << 22);
channel.chip_index++;
return chip_level;
}
int8_t EPIRBTXProcessor::sample_sgb_channel(SGBChannelState& channel, bool q_channel) {
int8_t level = channel.chip_level;
if (channel.sample_in_chip == 0) {
if (channel.chip_index < sgb_segment_chips) {
channel.chip_level = compute_sgb_chip_level(channel, q_channel);
level = channel.chip_level;
} else {
level = 0;
channel.chip_level = 0;
}
}
channel.sample_in_chip++;
if (channel.sample_in_chip >= sgb_samples_per_chip) {
channel.sample_in_chip = 0;
}
return level;
}
void EPIRBTXProcessor::init_sgb_channel(SGBChannelState& channel, uint32_t initial_state) {
channel.prn_state = initial_state;
channel.chip_index = 0;
channel.sample_in_chip = 0;
channel.chip_level = 0;
}
/**
* Processing method for this processor
*/
@@ -43,75 +107,92 @@ void EPIRBTXProcessor::execute(const buffer_c8_t& buffer) {
shared_memory.application_queue.push(txprogress_message);
}
if (mode_bpsk) {
// BPSK Manchester beacon signal
if (bpsk_pre_count < config_pre_count) {
// Pre-count state: send carrier only during pre-count
bpsk_pre_count++;
re = i_carrier;
im = q_carrier;
} else if (bpsk_post_count > 0) {
// Post-count: send carrier only during post-count
bpsk_post_count++;
re = i_carrier;
im = q_carrier;
if (bpsk_post_count >= config_post_count) {
// End transmission here
byte_index = 0;
bpsk_post_count = 0;
bpsk_pre_count = 0;
if (mode_406) {
if (mode_sgb) {
// 2G SGB DSSS-OQPSK signal
const int8_t i_sample = sample_sgb_channel(sgb_i, false);
const int8_t q_sample = (sgb_sample_counter < sgb_half_chip_samples)
? 0
: sample_sgb_channel(sgb_q, true);
re = i_sample;
im = q_sample;
sgb_sample_counter++;
if (sgb_sample_counter >= sgb_total_samples) {
sgb_sample_counter = 0;
end_of_transmission = true;
}
} else {
if (sample_counter == 0 && manchester_half == false) {
if (bit_index == 0) {
// Read current byte
current_byte = frame_data[byte_index];
// Move to next byte
byte_index++;
}
// Get current bit
current_bit = (current_byte >> (7 - bit_index)) & 0x01;
}
// Manchester encoding
if (current_bit == 1) {
// 1 = falling signal
if (manchester_half == false) {
re = i_pos;
im = q_pos;
} else {
re = i_neg;
im = q_neg;
// 1G BPSK Manchester beacon signal
if (bpsk_pre_count < config_pre_count) {
// Pre-count state: send carrier only during pre-count
bpsk_pre_count++;
re = i_carrier;
im = q_carrier;
} else if (bpsk_post_count > 0) {
// Post-count: send carrier only during post-count
bpsk_post_count++;
re = i_carrier;
im = q_carrier;
if (bpsk_post_count >= config_post_count) {
// End transmission here
byte_index = 0;
bpsk_post_count = 0;
bpsk_pre_count = 0;
end_of_transmission = true;
}
} else {
// 0 = rising signal
if (manchester_half == false) {
re = i_neg;
im = q_neg;
} else {
re = i_pos;
im = q_pos;
if (sample_counter == 0 && manchester_half == false) {
if (bit_index == 0) {
// Read current byte
current_byte = frame_data[byte_index];
// Move to next byte
byte_index++;
}
// Get current bit
current_bit = (current_byte >> (7 - bit_index)) & 0x01;
}
}
// Move to next sample
sample_counter++;
if (sample_counter >= samples_per_halfbit) {
// Move to next half-bit
sample_counter = 0;
manchester_half = !manchester_half;
// Manchester encoding
if (current_bit == 1) {
// 1 = falling signal
if (manchester_half == false) {
re = i_pos;
im = q_pos;
} else {
re = i_neg;
im = q_neg;
}
} else {
// 0 = rising signal
if (manchester_half == false) {
re = i_neg;
im = q_neg;
} else {
re = i_pos;
im = q_pos;
}
}
// Move to next sample
sample_counter++;
// Next bit after two half bits
if (manchester_half == false) {
// Move to next bit
bit_index++;
if (bit_index >= 8) {
// End of byte
bit_index = 0;
if (byte_index >= frame_data_len) {
// End of frame => move to post-count
bpsk_post_count = 1;
if (sample_counter >= samples_per_halfbit) {
// Move to next half-bit
sample_counter = 0;
manchester_half = !manchester_half;
// Next bit after two half bits
if (manchester_half == false) {
// Move to next bit
bit_index++;
if (bit_index >= 8) {
// End of byte
bit_index = 0;
if (byte_index >= frame_data_len) {
// End of frame => move to post-count
bpsk_post_count = 1;
}
}
}
}
@@ -154,26 +235,51 @@ void EPIRBTXProcessor::on_message(const Message* const msg) {
case Message::ID::EPIRBTXData: {
const auto message = *reinterpret_cast<const EPIRBTXDataMessage*>(msg);
// Check transmission mode
mode_bpsk = message.mode_bpsk;
if (mode_bpsk) {
// BPSK mode for 406 frame
mode_406 = message.mode_406;
if (mode_406) {
// 406 MHz frame mode:
// - FGB if data_len <= 144 bits (i.e. 18 bytes)
// - SGB if data_len > 144 bits (actually 250 bits (i.e. 32 bytes))
config_pre_count = message.pre_count;
config_post_count = message.post_count;
mode_sgb = message.data_len > frame_data_fgb_max_len; // SGB if data_len > 18 bytes (144 bits)
frame_data_len = message.data_len;
if (mode_sgb) {
frame_sgb_bits_len = std::min<uint16_t>(((uint16_t)message.data_len) * 8, sgb_message_bits); // Total bits in the frame
frame_data_len = std::min<uint8_t>(frame_data_len, frame_data_sgb_len);
// Detect self-test mode: bit 5 (1 based index as per specification) of message.data[0]
mode_sgb_selftest = (message.data[0] >> 3) & 0x01;
} else {
frame_data_len = std::min<uint8_t>(frame_data_len, frame_data_fgb_max_len);
}
// Get the frame data from the message
memcpy(frame_data, message.data, std::min(frame_data_len, EPIRBTXDataMessage::max_len));
// Init BPSK sequencer
sample_counter = 0;
bpsk_pre_count = 0;
bpsk_post_count = 0;
bit_index = 0;
byte_index = 0;
current_byte = 0;
current_bit = 0;
memcpy(frame_data, message.data, frame_data_len);
if (mode_sgb) {
// Init SGB DSSS-OQPSK sequencer
sgb_sample_counter = 0;
const uint32_t init_i = mode_sgb_selftest ? sgb_init_selftest_i : sgb_init_normal_i;
const uint32_t init_q = mode_sgb_selftest ? sgb_init_selftest_q : sgb_init_normal_q;
init_sgb_channel(sgb_i, init_i);
init_sgb_channel(sgb_q, init_q);
} else {
// Init FGB BPSK sequencer
sample_counter = 0;
bpsk_pre_count = 0;
bpsk_post_count = 0;
bit_index = 0;
byte_index = 0;
current_byte = 0;
current_bit = 0;
}
} else {
// AM mode for 121.5 signal => init AM sequencer
sweep_phase = 0;
audio_phase = 0;
mode_sgb = false;
}
// Tell the processor to start
configured = true;
+49 -5
View File
@@ -26,6 +26,7 @@
#include "baseband_thread.hpp"
#include "portapack_shared_memory.hpp"
#include "tonesets.hpp"
#include <algorithm>
#include <cmath>
/**
@@ -42,8 +43,12 @@ class EPIRBTXProcessor : public BasebandProcessor {
private:
// True when the processor has received a configuration message from the app
bool configured{false};
// True when in BPSK transmission mode, false for AM transmission mode
bool mode_bpsk{false};
// True when in 406 MHz transmission mode, false for AM transmission mode
bool mode_406{false};
// True when current 406 frame is a 2nd generation DSSS-OQPSK frame
bool mode_sgb{false};
// True when in self-test mode (detected from bit 5 of message.data)
bool mode_sgb_selftest{true};
// True when the transmission has to be stopped (e.g. at the end of a frame)
bool end_of_transmission{};
@@ -55,10 +60,15 @@ class EPIRBTXProcessor : public BasebandProcessor {
// Configured post-count value: used to continue the carrier for config_post_count samples after the end of a frame
uint32_t config_post_count = 0;
// Data of the frame to send in BPSK mode
uint8_t frame_data[18]{0};
// Size of the frame to send in BPSK mode
// Data of the frame to send in 406 mode (FGB BPSK or SGB DSSS-OQPSK)
static constexpr uint8_t frame_data_max_len = 32;
static constexpr uint8_t frame_data_fgb_max_len = 18;
static constexpr uint8_t frame_data_sgb_len = 32; // ceil(250 / 8)
uint8_t frame_data[frame_data_max_len]{0};
// Size of the frame to send in bytes
uint8_t frame_data_len = 0;
// Size of the frame payload in bits
uint16_t frame_sgb_bits_len = 0;
// BPSK parameters: Target phase +/-1.1 RAD as per COSPAS/SARSAT specifications
static constexpr float phase_rad = 1.1f;
@@ -89,6 +99,40 @@ class EPIRBTXProcessor : public BasebandProcessor {
// Position in the current manchester bit
bool manchester_half = false; // false = first half
// 2G SGB DSSS-OQPSK parameters
static constexpr uint16_t sgb_message_bits = 256; // 250 bits of data + 6 bits padding
static constexpr uint32_t sgb_chip_rate = 38400;
static constexpr uint32_t sgb_chips_per_bit = 256;
static constexpr uint32_t sgb_preamble_bits_per_channel = 25;
static constexpr uint32_t sgb_bits_per_channel = 150;
static constexpr uint32_t sgb_segment_chips = 38400;
static constexpr uint32_t sgb_samples_per_chip = TONES_SAMPLERATE / sgb_chip_rate;
static constexpr uint32_t sgb_half_chip_samples = sgb_samples_per_chip / 2;
// SGB burst duration is exactly 1 second.
static constexpr uint32_t sgb_total_samples = sgb_segment_chips * sgb_samples_per_chip;
static constexpr int8_t sgb_chip_amplitude = 90;
static constexpr uint32_t sgb_init_normal_i = 0x1;
static constexpr uint32_t sgb_init_normal_q = 0x1AC1FC;
static constexpr uint32_t sgb_init_selftest_i = 0x52C9F0;
static constexpr uint32_t sgb_init_selftest_q = 0x3CE928;
struct SGBChannelState {
uint32_t prn_state = 1;
uint32_t chip_index = 0;
uint32_t sample_in_chip = 0;
int8_t chip_level = 0;
};
SGBChannelState sgb_i{};
SGBChannelState sgb_q{};
uint32_t sgb_sample_counter = 0;
uint8_t get_frame_bit(uint16_t bit_pos) const;
int8_t compute_sgb_chip_level(SGBChannelState& channel, bool q_channel);
int8_t sample_sgb_channel(SGBChannelState& channel, bool q_channel);
void init_sgb_channel(SGBChannelState& channel, uint32_t initial_state);
// 127.5 AM signal parameters
static const uint32_t sweep_rate = 3; // 3 Hz
static const uint32_t f_min = 300; // Sweep min frequency (Hz)
+2 -2
View File
@@ -1136,11 +1136,11 @@ class SigGenToneMessage : public Message {
class EPIRBTXDataMessage : public Message {
public:
static constexpr uint8_t max_len = 18;
static constexpr uint8_t max_len = 32;
constexpr EPIRBTXDataMessage()
: Message{ID::EPIRBTXData} {
}
bool mode_bpsk = true;
bool mode_406 = true;
uint8_t data[max_len]{0};
uint8_t data_len = 0;
uint32_t pre_count = 0;
+5 -3
View File
@@ -1,7 +1,7 @@
#Title; Description; Frame (Hex format : 18 Bytes / 36 Hex char)
Selftest; Serial user Location Protocol; FFFED0D6E6202820000C29FF51041775302D
Standard Test; ADRASEC02 30/11/2014 N49d16m32s/E3d16m32s; FFFED08e3e0425a8318074fe44b735cd7b46
User protocol KO; Radio call sign (wrong BCH1); FFFED03EF613523F81FE0
User protocol KO; Radio call sign (wrong BCH1); FFFED03EF613523F81FE00
User protocol OK; Radio call sign (valid BCH1); FFFED056E6804002202009655250
RLS Location; RLS Location; FFFED08E0D0990014710021963C85C7009F5
PLB Location; PLB Location: National Location; FFFED08E3B15F1DFC0FF07FD1F769F3C0672
@@ -24,11 +24,13 @@ RLS Location; RLS Location Protocol; FFFED096ED09900149D4D467EE0851A3B2E8
Orbitography; Orbitography Protocol (from Toulouse FMCC's LUT); FFFE2FCE3000000000000DBD0E4022417500
ELT 24 2; ELT 24 bits; FFFED08DB345B146202DDF3C71F59BAB7072
Selftest BCH1 ERR; Serial user Location Protocol; FFFED0D7E6202820000C29FF51041775302D
Selftest BCH2 ERR; Serial user Location Protocol; FFFED0D6E6202820000C29FF51041765302D
Selftest BCH12 ERR; Serial user Location Protocol; FFFED0D7E6202820000C29FF51041765302D
#Selftest BCH2 ERR; Serial user Location Protocol; FFFED0D6E6202820000C29FF51041765302D
#Selftest BCH12 ERR; Serial user Location Protocol; FFFED0D7E6202820000C29FF51041765302D
Selftest BCH12 3+2 ERR; Serial user Location Protocol; FFFED0D7D6202820000C29FF51041745302D
Emergency Cap;EPIRB Emergency Capsizing;FFFED056E68040022021A4CDC675
Emergency No Mar.;Emergency non maritime;FFFED056EE8040022021A5623CFE
Emergency PLB;Emergency PLB no fire+med+dis;FFFED056E7804002202123F39E36
Emergency PLB fire;Emergency PLB fire;FFFED056E7804002202123F39E39
EPIRB Sinking;Emergency EPIRB Sinking;FFFED056E58040022021342FAD76
EPIRB Call Sign Col.;EPIRB Radio Call Sign Emergency collision;FFFED056ED80400220217185E933
SGB;2nd Generation Beacon;BFFFE1A738C95C7BE00BD8BE00000000001FFFF0FFFF7FFFEF0470404A448FD