diff --git a/firmware/application/external/epirb_tx/beacon.hpp b/firmware/application/external/epirb_tx/beacon.hpp index c6918b488..2fe4599f9 100644 --- a/firmware/application/external/epirb_tx/beacon.hpp +++ b/firmware/application/external/epirb_tx/beacon.hpp @@ -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, 0–65535) +#define SGB_SERIAL_NUMBER 573 // Serial number within TAC (14 bits, 0–16383) +#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__*/ \ No newline at end of file diff --git a/firmware/application/external/epirb_tx/ui_epirb_tx.cpp b/firmware/application/external/epirb_tx/ui_epirb_tx.cpp index a11215ebb..4d87b81bb 100644 --- a/firmware/application/external/epirb_tx/ui_epirb_tx.cpp +++ b/firmware/application/external/epirb_tx/ui_epirb_tx.cpp @@ -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((beacon.frame.size() / 2), 18); + epirb_tx_message.data_len = std::min((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)); } diff --git a/firmware/application/external/epirb_tx/ui_epirb_tx.hpp b/firmware/application/external/epirb_tx/ui_epirb_tx.hpp index 5c70af54a..cfc721016 100644 --- a/firmware/application/external/epirb_tx/ui_epirb_tx.hpp +++ b/firmware/application/external/epirb_tx/ui_epirb_tx.hpp @@ -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 diff --git a/firmware/baseband/proc_epirb_tx.cpp b/firmware/baseband/proc_epirb_tx.cpp index 6a288f4bc..241bdd81f 100644 --- a/firmware/baseband/proc_epirb_tx.cpp +++ b/firmware/baseband/proc_epirb_tx.cpp @@ -27,6 +27,70 @@ #include #include +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(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)message.data_len) * 8, sgb_message_bits); // Total bits in the frame + frame_data_len = std::min(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(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; diff --git a/firmware/baseband/proc_epirb_tx.hpp b/firmware/baseband/proc_epirb_tx.hpp index 218cf2842..20baa0e0b 100644 --- a/firmware/baseband/proc_epirb_tx.hpp +++ b/firmware/baseband/proc_epirb_tx.hpp @@ -26,6 +26,7 @@ #include "baseband_thread.hpp" #include "portapack_shared_memory.hpp" #include "tonesets.hpp" +#include #include /** @@ -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) diff --git a/firmware/common/message.hpp b/firmware/common/message.hpp index ca7c738a8..818c8d36f 100644 --- a/firmware/common/message.hpp +++ b/firmware/common/message.hpp @@ -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; diff --git a/sdcard/EPIRB/BEACONS.TXT b/sdcard/EPIRB/BEACONS.TXT index 6d8426790..a8648e9b3 100644 --- a/sdcard/EPIRB/BEACONS.TXT +++ b/sdcard/EPIRB/BEACONS.TXT @@ -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