EPIRB TX Application (#3081)

* First step to epirb-tx app

* Next step to epirb_tx app

* Fixed merge

* EPIRB TX app progress

- Fixed UI
- Added automatic frame resend after delay
- Added load of beacons from EPIRB/BEACONS.TXT file

* First step to adding 121.5 MHz signal

* Refactored 121.5 signal generation code

* Added frequency change when switching from BPSK to AM

* Added checkbox for AM signal.

Added START/STOP button
Added AM frequency
First step to editor mode.

* Next step to frame editor

* Fixed location bits.

* Refactored code + fixed location

* Added location fields

* UI fixes + settings backup

Fixed lat/long display
Added send on change checkbox
Added settings backup
Fixed locator edition
Fixed BEACONS.TXT to move to Self test beacons.

* Update ui_epirb_tx.cpp

Fixed timeout reset

* Added national location protocol.

Added national location protocol.
Fixed send on change.

* UI fixes

Added country selection
Separated beacon protocol and beacon type selectors

* Standard protocol

Added standard protocol
Added internal checkbox

* Fixed fomratting

* Code cleanup

* Comments / doc

* More comments / doc

* More comments / doc

Fixed pre count duration.
Added frequency restore after leaving

* Prepare merge

* Prepare merge

* Finished merge

* More comments and doc

* Update ui_epirb_tx.cpp

Fixed out of memory error when using AlphanumView

* Update proc_epirb_tx.hpp

Fixed comment

* Fixed copilot PR comments.

* Update proc_epirb_tx.hpp

Fixed formatting

* Update external.ld

Merge with kiss_tnc app
This commit is contained in:
Frederic BORRY
2026-03-12 10:05:42 +01:00
committed by GitHub
parent 2c2959df30
commit 5aecd4097e
17 changed files with 1885 additions and 0 deletions
+4
View File
@@ -422,6 +422,10 @@ void set_rtty_config(RTTYDataMessage& message) {
send_message(&message);
}
void set_epirb_tx_config(EPIRBTXDataMessage& message) {
send_message(&message);
}
static bool baseband_image_running = false;
void run_image(const spi_flash::image_tag_t image_tag) {
+1
View File
@@ -116,6 +116,7 @@ void set_flex_config();
void set_bitstream_config(uint32_t deviation, uint8_t mode); // mode 0 for am, 1 for 2fsk
void set_rtty_config(uint16_t baud, uint16_t shift, uint8_t* payload = nullptr, uint16_t payload_length = 0); // baud*100
void set_rtty_config(RTTYDataMessage& message);
void set_epirb_tx_config(EPIRBTXDataMessage& message);
void request_roger_beep();
void request_rssi_beep();
+356
View File
@@ -0,0 +1,356 @@
/*
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
*
* 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.
*/
#ifndef __BEACON_H__
#define __BEACON_H__
#include "portapack.hpp"
#include "ui_epirb_tx.hpp"
#include "location.hpp"
namespace ui::external_app::epirb_tx {
/**
* Get bits frop the provided frame
* @param data the frame data
* @param startBit the start bit number (one based to match documentation)
* @param endBit the end bit number (one based to match documentation)
* @return the selected bits (max 64)
*/
static uint64_t get_bits(uint8_t* data, int startBit, int endBit) {
uint64_t result = 0;
// 0 bases bit count
startBit--;
int numBits = endBit - startBit;
// get a pointer to the starting byte...
const uint8_t* pData = &(data[startBit / 8]);
uint8_t b = *pData;
// calculate the starting bit within that byte...
int bitOffset = 7 - (startBit % 8);
// iterate for the desired number of bits...
for (int i = 0; i < numBits; ++i) {
// make room for the next bit...
result <<= 1;
// copy the bit...
result |= ((b >> bitOffset) & 0x01);
// reached the end of the current byte?
if (--bitOffset < 0) {
b = *(++pData); // go to the next byte...
bitOffset = 7; // restart at the first bit in that byte...
}
}
// all done...
return result;
}
/**
* Compute a BCH code
* @param frame the frame data
* @param startBit the bit to start BCH calculation
* @param endBit the bit to stop calculation
* @param poly the BCH polynome
* @param polyLength the BCH polynome length
* @return the BCH code
*/
static uint64_t compute_bch(uint8_t* frame, int startBit, int endBit, unsigned long poly, int polyLength) { // Length of data to be checked (not including the BCH code)
int dataLength = endBit - startBit + 1;
// Total lengh (including the BCH code that will be padded to zeros (BCH code length is polyLengh-1))
int totalLength = dataLength + polyLength - 1;
// Start with the first polyLength bits
uint64_t result = get_bits(frame, startBit, startBit + polyLength - 1);
for (int i = polyLength; i <= totalLength; i++) { // Iterate on each bit after the first polyLength batch
bool firstBit = result >> (polyLength - 1);
if (firstBit) { // We have a leading 1 => xor the result with the poly
result = result ^ poly;
}
if (i < totalLength) { // Move to next bit
result = result << 1;
if (i < dataLength) { // Append next bit
result |= get_bits(frame, startBit + i, startBit + i);
} // else : 0 padding after data length
}
}
return result;
}
// 21 bits BCH polynomial
#define BCH_21_POLYNOMIAL 0b1001101101100111100011UL
#define BCH_21_POLY_LENGTH 22
// 12 bits BCH polynomial
#define BCH_12_POLYNOMIAL 0b1010100111001UL
#define BCH_12_POLY_LENGTH 13
/**
* Combyte BCH1 code for the provided frame
*/
static uint64_t compute_bch1(uint8_t* frame) {
return compute_bch(frame, 25, 85, BCH_21_POLYNOMIAL, BCH_21_POLY_LENGTH);
}
/**
* Combyte BCH2 code for the provided frame
*/
static uint64_t compute_bch2(uint8_t* frame) {
return compute_bch(frame, 107, 132, BCH_12_POLYNOMIAL, BCH_12_POLY_LENGTH);
}
/**
* Set a bit in the provided frame
* @param buf the frame
* @param bit the position of the bit to set (0 based)
* @param v the bit value
*/
static void set_bit(uint8_t* buf, int bit, bool v) {
int byte = bit >> 3;
int off = 7 - (bit & 7);
if (v)
buf[byte] |= (1 << off);
else
buf[byte] &= ~(1 << off);
}
/**
* Push bits to the provided frame
* @param buf the frame
* @param pos the current frame possition (in/out, will be incremented during the opreration)
* @param v the bits to push (max 64)
* @param n the number of bits to push
*/
static void push_bits(uint8_t* buf, int& pos, uint64_t v, int n) {
for (int i = n - 1; i >= 0; i--)
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
* @param params the beacon parameters
* @return the size of the generated frame
*/
size_t generate_beacon(uint8_t* frame, const BeaconParams& params) {
// Clear the content of the frame
memset(frame, 0, 18);
int pos = 0;
uint32_t deg, min, sec;
// bit sync
for (int i = 0; i < 15; i++)
push_bits(frame, pos, 1, 1);
// frame sync
push_bits(frame, pos, params.is_test ? 0b011010000 : 0b000101111, 9);
// PDF-1 (Protexted Data field 1)
int pdf1_start = pos;
push_bits(frame, pos, 1, 1); // format flag (long)
bool is_user = (params.protocol == BeaconProtocol::USER);
bool is_standard = (params.protocol == BeaconProtocol::STANDARD);
push_bits(frame, pos, is_user, 1); // protocol flag
push_bits(frame, pos, params.country, 10); // country code
switch (params.type) {
case BeaconType::EPIRB:
if (is_user)
push_bits(frame, pos, 0b010, 3);
else if (is_standard)
push_bits(frame, pos, 0b0010, 4);
else
push_bits(frame, pos, 0b1010, 4);
break;
case BeaconType::PLB:
if (is_user)
push_bits(frame, pos, 0b011, 3);
else if (is_standard)
push_bits(frame, pos, 0b0111, 4);
else
push_bits(frame, pos, 0b1011, 4);
break;
default:
case BeaconType::ELT:
if (is_user)
push_bits(frame, pos, 0b001, 3);
else if (is_standard)
push_bits(frame, pos, 0b0011, 4);
else
push_bits(frame, pos, 0b1000, 4);
break;
}
// Fill the rest of PDF 1 with zeros
while (pos < pdf1_start + 61)
push_bits(frame, pos, 0, 1);
if (is_user) {
// User Location Protocol: no position in PDF1
if (params.type == BeaconType::PLB) {
// Set bits for PLB
set_bit(frame, 40 - 1, 1);
set_bit(frame, 41 - 1, 1);
set_bit(frame, 42 - 1, 0);
}
set_bit(frame, 85 - 1, params.has_121_5);
} else if (is_standard) {
// Standard Location Protocol
// North / south
set_bit(frame, 65 - 1, params.location.south);
// E / W
set_bit(frame, 75 - 1, params.location.west);
pos = 66 - 1;
// Latitude 1/4 degrees (9 bits)
deg = (params.location.lat_deg << 2) + (params.location.lat_min / 15);
push_bits(frame, pos, deg, 9);
pos = 76 - 1;
// Longitude 1/4 degrees (10 bits)
deg = (params.location.long_deg << 2) + (params.location.long_min / 15);
push_bits(frame, pos, deg, 10);
pos = pdf1_start + 61;
} else {
// National Location Protocol
// North / south
set_bit(frame, 59 - 1, params.location.south);
// E / W
set_bit(frame, 72 - 1, params.location.west);
pos = 60 - 1;
// Latitude degrees (7 bits)
push_bits(frame, pos, params.location.lat_deg, 7);
// Latitude min (5 bits, 2 min increment)
min = params.location.lat_min / 2;
push_bits(frame, pos, min, 5);
pos = 73 - 1;
// Longitude degrees (8 bits)
push_bits(frame, pos, params.location.long_deg, 8);
// Longiitude min (5 bits, 2 min increment)
min = params.location.long_min / 2;
push_bits(frame, pos, min, 5);
pos = pdf1_start + 61;
}
// Set BCH1
uint64_t bch1 = compute_bch1(frame);
push_bits(frame, pos, bch1, 21);
// PDF-2 (Protexted Data Field 2)
int pdf2_start = pos;
if (is_user) {
// User Location Protocol
push_bits(frame, pos, params.is_internal, 1);
// Latitude N/S
push_bits(frame, pos, params.location.south, 1);
// Latitude degrees (7 bits)
push_bits(frame, pos, params.location.lat_deg, 7);
// Latitude minutes (4 bits, 4 minutes precision)
min = params.location.lat_min / 4;
push_bits(frame, pos, min, 4);
// Longitude E/W
push_bits(frame, pos, params.location.west, 1);
// Longitude degrees (8 bits)
push_bits(frame, pos, params.location.long_deg, 8);
// Longitude minutes (4 bits, 4 minutes precision)
min = params.location.long_min / 4;
push_bits(frame, pos, min, 4);
} else if (is_standard) {
// Standard Location Protocol
push_bits(frame, pos, 0b1101, 4);
push_bits(frame, pos, params.is_internal, 1);
push_bits(frame, pos, params.has_121_5, 1);
// Latiitude
// +
push_bits(frame, pos, 1, 1);
// Min
min = params.location.lat_min % 15;
push_bits(frame, pos, min, 5);
// Sec (4 bits, 4 sec precision)
sec = params.location.lat_sec / 4;
push_bits(frame, pos, sec, 4);
// Longitude
// +
push_bits(frame, pos, 1, 1);
// Min
min = params.location.long_min % 15;
push_bits(frame, pos, min, 5);
// Sec (4 bits, 4 sec precision)
sec = params.location.long_sec / 4;
push_bits(frame, pos, sec, 4);
} else {
// National Location Protocol
push_bits(frame, pos, 0b1101, 4);
push_bits(frame, pos, params.is_internal, 1);
push_bits(frame, pos, params.has_121_5, 1);
// Lat
// +
push_bits(frame, pos, 1, 1);
// Min
push_bits(frame, pos, (params.location.lat_min % 2), 2);
// Sec (4 bits, 4 sec precision)
sec = params.location.lat_sec / 4;
push_bits(frame, pos, sec, 4);
// LLon
// +
push_bits(frame, pos, 1, 1);
// Min
push_bits(frame, pos, (params.location.long_min % 2), 2);
// Sec (4 bits, 4 sec precision)
sec = params.location.long_sec / 4;
push_bits(frame, pos, sec, 4);
}
while (pos < pdf2_start + 26)
push_bits(frame, pos, 0, 1);
// Compute BCH 2
uint64_t bch2 = compute_bch2(frame);
push_bits(frame, pos, bch2, 12);
return 18;
}
} // namespace ui::external_app::epirb_tx
#endif /*__BEACON_H__*/
+268
View File
@@ -0,0 +1,268 @@
/*
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
*
* 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.
*/
#ifndef __LOCATION_H__
#define __LOCATION_H__
#include "portapack.hpp"
#include "ui_epirb_tx.hpp"
#include <cmath>
#include <cctype>
#include <cstdio>
namespace ui::external_app::epirb_tx {
/**
* Convert decimal coordinates to sexagesimal coordinates
* @param value the decimal value
* @param negative output N(false)/S(true) or E(false)/W(true) value
* @param deg output degrees value
* @param min output minutes value
* @param sec output seconds value
*/
static void decimal_to_dms(double value, bool& negative, uint16_t& deg, uint8_t& min, uint8_t& sec) {
negative = value < 0;
value = std::fabs(value);
deg = (uint16_t)value;
double m = (value - deg) * 60.0;
min = (uint8_t)m;
double s = (m - min) * 60.0;
sec = (uint8_t)(s + 0.5);
if (sec == 60) {
sec = 0;
min++;
}
if (min == 60) {
min = 0;
deg++;
}
}
/**
* Convert sexagesimal coordinates to decimal
* @param negative N(false)/S(true) or E(false)/W(true) value
* @param deg degrees value
* @param min minutes value
* @param sec seconds value
* @return value the decimal value
*/
static double dms_to_decimal(bool negative, uint16_t deg, uint8_t min, uint8_t sec) {
double v = deg + min / 60.0 + sec / 3600.0;
return negative ? -v : v;
}
/**
* Convert maidenhead locator to decima coordinates
* @param loc the locator
* @param lat output latitude
* @param lon output longitude
*/
static void maidenhead_to_decimal(const std::string& loc, float& lat, float& lon) {
static const double lon_step[] =
{
20.0,
2.0,
1.0 / 12.0,
1.0 / 120.0,
1.0 / 2880.0};
static const double lat_step[] =
{
10.0,
1.0,
1.0 / 24.0,
1.0 / 240.0,
1.0 / 5760.0};
lon = -180.0;
lat = -90.0;
int pairs = loc.size() / 2;
if (pairs > 5)
pairs = 5;
for (int i = 0; i < pairs; i++) {
char c1 = std::toupper(loc[i * 2]);
char c2 = std::toupper(loc[i * 2 + 1]);
double lon_size = lon_step[i];
double lat_size = lat_step[i];
int v1, v2;
if (i % 2 == 0) // letters
{
v1 = c1 - 'A';
v2 = c2 - 'A';
} else // digits
{
v1 = c1 - '0';
v2 = c2 - '0';
}
lon += v1 * lon_size;
lat += v2 * lat_size;
}
// Cell center
lon += lon_step[pairs - 1] / 2.0;
lat += lat_step[pairs - 1] / 2.0;
}
/**
* Convert decimal coordinates to maidenhead locator
* @param lat the latitude
* @param lon the longitude
* @param precision (optional, defaults to 8) the number of charater for the maidenhead locator
* @return the locator
*/
static std::string decimal_to_maidenhead(double lat, double lon, int precision = 8) {
lon += 180.0;
lat += 90.0;
int A = lon / 20;
int B = lat / 10;
lon -= A * 20;
lat -= B * 10;
int C = lon / 2;
int D = lat / 1;
lon -= C * 2;
lat -= D * 1;
int E = lon / (5.0 / 60.0);
int F = lat / (2.5 / 60.0);
lon -= E * (5.0 / 60.0);
lat -= F * (2.5 / 60.0);
int G = lon / (5.0 / 600.0);
int H = lat / (2.5 / 600.0);
std::string locator;
locator += char('A' + A);
locator += char('A' + B);
if (precision >= 4) {
locator += char('0' + C);
locator += char('0' + D);
}
if (precision >= 6) {
locator += char('a' + E);
locator += char('a' + F);
}
if (precision >= 8) {
locator += char('0' + G);
locator += char('0' + H);
}
return locator;
}
/**
* Init the provided Location values from its locator
*/
void init_from_locator(Location& loc) {
maidenhead_to_decimal(loc.locator, loc.latitude, loc.longitude);
decimal_to_dms(loc.latitude,
loc.south,
loc.lat_deg,
loc.lat_min,
loc.lat_sec);
decimal_to_dms(loc.longitude,
loc.west,
loc.long_deg,
loc.long_min,
loc.long_sec);
}
/**
* Init the provided Location values from its decimal coordinates
*/
void init_from_decimal(Location& loc) {
decimal_to_dms(loc.latitude,
loc.south,
loc.lat_deg,
loc.lat_min,
loc.lat_sec);
decimal_to_dms(loc.longitude,
loc.west,
loc.long_deg,
loc.long_min,
loc.long_sec);
loc.locator = decimal_to_maidenhead(loc.latitude, loc.longitude);
}
/**
* Init the provided Location values from its sexagesimal coordinates
*/
void init_from_dms(Location& loc) {
loc.latitude = dms_to_decimal(
loc.south,
loc.lat_deg,
loc.lat_min,
loc.lat_sec);
loc.longitude = dms_to_decimal(
loc.west,
loc.long_deg,
loc.long_min,
loc.long_sec);
loc.locator = decimal_to_maidenhead(loc.latitude, loc.longitude);
}
/**
* Convert the provided Loaction to it's latitude string (format <xxx°yy'zz"N>)
*/
std::string to_latitude_string(const Location& location) {
char buffer[16];
// Format : <xxx°yy'zz"N>
snprintf(buffer, sizeof(buffer), "%3d\260%02d'%02d\"%c", location.lat_deg, location.lat_min, location.lat_sec, location.south ? 'S' : 'N');
return std::string(buffer);
}
/**
* Convert the provided Loaction to it's longitude string (format <xxx°yy'zz"W>)
*/
std::string to_longitude_string(const Location& location) {
char buffer[16];
// Format : <xxx°yy'zz"W>
snprintf(buffer, sizeof(buffer), "%3d\260%02d'%02d\"%c", location.long_deg, location.long_min, location.long_sec, location.west ? 'W' : 'E');
return std::string(buffer);
}
} // namespace ui::external_app::epirb_tx
#endif /*__LOCATION_H__*/
+82
View File
@@ -0,0 +1,82 @@
/*
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
*
* 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.hpp"
#include "ui_epirb_tx.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::epirb_tx {
void initialize_app(ui::NavigationView& nav) {
nav.push<EPIRBTXAppView>();
}
} // namespace ui::external_app::epirb_tx
extern "C" {
__attribute__((section(".external_app.app_epirb_tx.application_information"), used)) application_information_t _application_information_epirb_tx = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::epirb_tx::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "EPIRB TX",
/*.bitmap_data = */ {
0x00,
0x00,
0x00,
0x00,
0x7C,
0x3E,
0xFE,
0x7F,
0xFF,
0xFF,
0xF7,
0xEF,
0xE3,
0xC7,
0xE3,
0xC7,
0xE3,
0xC7,
0xE3,
0xC7,
0xF7,
0xEF,
0xFF,
0xFF,
0xFE,
0x7F,
0x7C,
0x3E,
0x00,
0x00,
0x00,
0x00,
},
/*.icon_color = */ ui::Color::green().v,
/*.menu_location = */ app_location_t::TX,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = portapack::spi_flash::image_tag_epirb_tx */ {'P', 'E', 'P', 'T'},
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
}
+479
View File
@@ -0,0 +1,479 @@
/*
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
*
* 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_epirb_tx.hpp"
#include "tonesets.hpp"
#include "portapack.hpp"
#include "baseband_api.hpp"
#include "string_format.hpp"
#include "file_reader.hpp"
#include "file_path.hpp"
#include "ui_geomap.hpp"
#include "ui_alphanum.hpp"
#include <cstring>
#include <stdio.h>
#include "beacon.hpp"
using namespace portapack;
namespace ui::external_app::epirb_tx {
void EPIRBTXAppView::focus() {
button_tx.focus();
}
EPIRBTXAppView::~EPIRBTXAppView() {
// Restore original frequency before leaving
transmitter_model.set_target_frequency(original_frequency);
transmitter_model.disable();
baseband::shutdown();
}
/**
* Conversion from hex char to half byte
*/
static uint8_t hexval(char c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
return 0;
}
/**
* Convert from hex chars to uint8_t
*/
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);
}
void EPIRBTXAppView::generate_frame(BeaconParams params) {
epirb_tx_message.data_len = generate_beacon(epirb_tx_message.data, params);
}
void EPIRBTXAppView::on_timer() {
// Timer is called on display refresh
if (loop) {
if (loop_enabled) {
// chTimeNow() returns milliseconds on our version of ChibiOS / Hardware
auto now = chTimeNow();
auto elapsed = ((now - last_frame_time) / 1000);
std::string timeout = std::to_string((uint32_t)(delay > elapsed ? delay - elapsed : 0));
if (timeout != text_timeout.get()) {
// Update timeout text every seconds
text_timeout.set(timeout);
}
if (now > (last_frame_time + (delay * 1000))) {
// Send a new frame after the delay
start_tx();
}
} else {
// Stop send loop
loop = false;
}
}
}
void EPIRBTXAppView::update_frame(bool updateConfig) {
if (mode_file) {
// In file mode, currently selected beacon has changed => load the new one
Beacon& beacon = beacons[selected_beacon];
// Set desciption
text_description.set(beacon.description.substr(0, max_text_width_ext));
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) : "");
// Prepare tx configuration
epirb_tx_message.data_len = std::min<size_t>((beacon.frame.size() / 2), 18);
for (uint8_t i = 0; i < epirb_tx_message.data_len; i++) {
epirb_tx_message.data[i] = hexToByte(
beacon.frame[2 * i],
beacon.frame[2 * i + 1]);
}
} else {
// 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 (updateConfig && send_on_change && loop) {
// Need to update config / send new beacon
if (am_enabled) {
// Already transmitting => update config
last_frame_time = chTimeNow();
update_config();
} else {
// Not yet transmitting => start tx
start_tx();
}
}
}
void EPIRBTXAppView::update_config() {
if (epirb_tx_message.mode_bpsk) {
// Already in BPSK mode => backup bpsk frequency
bpsk_frequency = transmitter_model.target_frequency();
} else {
// Previously in AM mode => restore bpsk frequency
transmitter_model.set_target_frequency(bpsk_frequency);
}
// Set mode to bpsk
epirb_tx_message.mode_bpsk = true;
// Set pre/post count
epirb_tx_message.pre_count = (150 * TONES_SAMPLERATE) / 1000; // 150 ms
epirb_tx_message.post_count = (100 * TONES_SAMPLERATE) / 1000; // 100 ms
// Send config to baseband
baseband::set_epirb_tx_config(epirb_tx_message);
}
void EPIRBTXAppView::set_tx_button_state(bool active) {
button_tx.set_text(active ? "START" : "STOP");
button_tx.set_style(active ? &style_tx_start : &style_tx_stop);
}
void EPIRBTXAppView::start_tx() {
last_frame_time = chTimeNow();
update_config();
loop = loop_enabled;
transmitter_model.enable();
tx_view.set_transmitting(true);
set_tx_button_state(false);
transmitting = true;
}
void EPIRBTXAppView::stop_tx() {
loop = false;
transmitter_model.disable();
tx_view.set_transmitting(false);
set_tx_button_state(true);
transmitting = false;
}
void EPIRBTXAppView::on_tx_progress(const uint32_t progress, const bool done) {
(void)progress;
if (done) {
if (am_enabled) {
// BPSK frame sent, switch back to 121.5 AM signal
epirb_tx_message.mode_bpsk = false;
// Backup bpsk frequency for next run
bpsk_frequency = transmitter_model.target_frequency();
// Restore am frequency
transmitter_model.set_target_frequency(am_frequency);
// Send config to baseband
baseband::set_epirb_tx_config(epirb_tx_message);
} else {
// End of BPSK frame
transmitter_model.disable();
tx_view.set_transmitting(false);
if (!loop) {
// Not looping => update transmission state
set_tx_button_state(true);
transmitting = false;
}
}
}
}
void EPIRBTXAppView::update_location(bool updateLocatorField) {
locator = beacon_params.location.locator;
if (updateLocatorField) text_field_beacon_locator.set_text(beacon_params.location.locator);
text_beacon_latitude_value.set(to_latitude_string(beacon_params.location));
text_beacon_longitude_value.set(to_longitude_string(beacon_params.location));
}
void EPIRBTXAppView::update_mode() {
// Hide / show widgets for file mode or manual mode
text_beacon.hidden(!mode_file);
text_description_label.hidden(!mode_file);
options_frame.hidden(!mode_file);
text_description.hidden(!mode_file);
text_description_end.hidden(!mode_file);
text_beacon_type.hidden(mode_file);
text_beacon_country.hidden(mode_file);
checkbox_beacon_internal.hidden(mode_file);
text_beacon_locator.hidden(mode_file);
text_beacon_latitude.hidden(mode_file);
text_beacon_latitude_value.hidden(mode_file);
text_beacon_longitude.hidden(mode_file);
text_beacon_longitude_value.hidden(mode_file);
button_mangps.hidden(mode_file);
options_beacon_type.hidden(mode_file);
options_beacon_protocol.hidden(mode_file);
options_beacon_country.hidden(mode_file);
text_field_beacon_locator.hidden(mode_file);
}
EPIRBTXAppView::EPIRBTXAppView(
NavigationView& nav) {
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
add_children({&labels,
&options_mode,
&text_beacon,
&text_description_label,
&text_beacon_type,
&options_beacon_type,
&options_beacon_protocol,
&text_beacon_country,
&options_beacon_country,
&checkbox_beacon_internal,
&text_beacon_locator,
&text_beacon_latitude,
&text_beacon_latitude_value,
&text_beacon_longitude,
&text_beacon_longitude_value,
&button_mangps,
&text_field_beacon_locator,
&options_frame,
&text_description,
&text_description_end,
&text_frame,
&text_frame_end,
&text_timeout,
&checkbox_loop,
&field_delay,
&button_tx,
&checkbox_am,
&field_am_frequency,
&checkbox_send_on_change,
&tx_view});
text_beacon.set_style(Theme::getInstance()->fg_light);
text_description_label.set_style(Theme::getInstance()->fg_light);
text_beacon_type.set_style(Theme::getInstance()->fg_light);
text_beacon_country.set_style(Theme::getInstance()->fg_light);
text_beacon_locator.set_style(Theme::getInstance()->fg_light);
text_beacon_latitude.set_style(Theme::getInstance()->fg_light);
text_beacon_longitude.set_style(Theme::getInstance()->fg_light);
// Restore settings
checkbox_am.set_value(am_enabled);
checkbox_loop.set_value(loop_enabled);
checkbox_send_on_change.set_value(send_on_change);
options_mode.set_by_value(!mode_file);
transmitter_model.set_target_frequency(bpsk_frequency);
field_am_frequency.set_value(am_frequency);
field_delay.set_value(delay);
options_beacon_type.set_by_value(beacon_type);
options_beacon_protocol.set_by_value(beacon_protocol);
options_beacon_country.set_by_value(beacon_country);
checkbox_beacon_internal.set_value(beacon_internal);
beacon_params.type = (BeaconType)beacon_type;
beacon_params.has_121_5 = am_enabled;
beacon_params.location.locator = locator;
beacon_params.is_internal = beacon_internal;
init_from_locator(beacon_params.location);
update_mode();
update_location();
options_mode.on_change = [this](size_t index, OptionsField::value_t) {
mode_file = (index == 0);
update_mode();
update_frame();
set_dirty();
};
options_beacon_type.on_change = [this](size_t index, OptionsField::value_t) {
beacon_params.type = (BeaconType)index;
beacon_type = index;
update_frame();
set_dirty();
};
options_beacon_protocol.on_change = [this](size_t index, OptionsField::value_t) {
beacon_params.protocol = (BeaconProtocol)index;
beacon_protocol = index;
update_frame();
set_dirty();
};
options_beacon_country.on_change = [this](size_t, OptionsField::value_t v) {
beacon_params.country = v;
beacon_country = v;
update_frame();
set_dirty();
};
checkbox_beacon_internal.on_select = [this](Checkbox&, bool v) {
beacon_internal = v;
beacon_params.is_internal = v;
update_frame();
set_dirty();
};
text_field_beacon_locator.on_select = [this, &nav](TextField&) mutable {
auto te_view = nav.push<AlphanumView>(locator, 10, ENTER_KEYBOARD_MODE_ALPHA);
te_view->on_changed = [this](std::string& value) {
beacon_params.location.locator = value;
init_from_locator(beacon_params.location);
update_location();
update_frame();
set_dirty();
};
};
button_mangps.on_select = [this, &nav](Button&) {
nav.push<GeoMapView>(
0,
GeoPos::alt_unit::METERS,
GeoPos::spd_unit::HIDDEN,
beacon_params.location.latitude,
beacon_params.location.longitude,
[this](int32_t, float lat, float lon, int32_t) {
beacon_params.location.latitude = lat;
beacon_params.location.longitude = lon;
init_from_decimal(beacon_params.location);
// Update locator field
update_location();
update_frame();
set_dirty();
});
};
field_am_frequency.on_change = [this](rf::Frequency freq) {
am_frequency = freq;
if (transmitting && !epirb_tx_message.mode_bpsk && am_enabled)
// Update transmitter frequency
transmitter_model.set_target_frequency(am_frequency);
};
// Load available beacons from BEACONS.TXT files (or default).
load_beacons();
// Setup options_frame content with loaded beacons
using option_t = std::pair<std::string, int32_t>;
using options_t = std::vector<option_t>;
options_t entries;
for (const auto& beacon : beacons)
entries.emplace_back(beacon.title, entries.size());
options_frame.set_options(std::move(entries));
if (selected_beacon >= beacons.size())
// BEACONS.TXT file has changed since last launch, default index to 0
selected_beacon = 0;
options_frame.set_selected_index(selected_beacon);
options_frame.on_change = [this](size_t index, OptionsField::value_t) {
selected_beacon = index;
update_frame();
set_dirty();
};
// Init frame content / baseband param with currently setup beacon
update_frame(false);
checkbox_loop.on_select = [this](Checkbox&, bool v) {
loop_enabled = v;
};
field_delay.on_change = [this](int32_t v) {
delay = v;
};
checkbox_am.on_select = [this](Checkbox&, bool v) {
beacon_params.has_121_5 = v;
am_enabled = v;
if (!mode_file) update_frame(false);
};
// AM frequency field edit
field_am_frequency.on_edit = [this, &nav]() {
auto new_view = nav.push<FrequencyKeypadView>(field_am_frequency.value());
new_view->on_changed = [this](rf::Frequency f) {
field_am_frequency.set_value(f);
};
};
checkbox_send_on_change.on_select = [this](Checkbox&, bool v) {
send_on_change = v;
};
tx_view.on_edit_frequency = [this, &nav]() {
auto new_view = nav.push<FrequencyKeypadView>(transmitter_model.target_frequency());
new_view->on_changed = [this](rf::Frequency f) {
transmitter_model.set_target_frequency(f);
bpsk_frequency = f;
};
};
tx_view.on_start = [this]() {
start_tx();
};
tx_view.on_stop = [this]() {
stop_tx();
};
button_tx.on_select = [this](Button&) {
if (!transmitting)
start_tx();
else
stop_tx();
};
}
/**
* Load beacons from /EPIRB/BEACONS.TXT file on sd card
*/
void EPIRBTXAppView::load_beacons() {
File beacons_file;
auto error = beacons_file.open(epirb_dir / u"BEACONS.TXT");
beacons.clear();
if (!error) {
// BEACONS.TXT file exists
auto reader = FileLineReader(beacons_file);
for (const auto& line : reader) {
// Skip comment lines
if (line.length() == 0 || line[0] == '#')
continue;
// Split line with ';' separator
auto cols = split_string(line, ';');
if (cols.size() != 3)
continue;
// Read current beacon
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);
size_t size = beacon.frame.size();
if (size <= 0)
continue; // Invalid line.
// Beacon is valid, add it to the list
beacons.emplace_back(std::move(beacon));
}
}
if (beacons.empty()) {
// No beacons file or empty flile: just add default beacon
beacons.push_back(default_beacon);
}
}
} // namespace ui::external_app::epirb_tx
+332
View File
@@ -0,0 +1,332 @@
/*
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
*
* 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.
*/
#ifndef __EPIRB_TX_H__
#define __EPIRB_TX_H__
#include "app_settings.hpp"
#include "radio_state.hpp"
#include "ui.hpp"
#include "ui_widget.hpp"
#include "ui_navigation.hpp"
#include "ui_transmitter.hpp"
#include "portapack.hpp"
#include "message.hpp"
#include "tonesets.hpp"
#define BEACON_HEXA_SIZE 36
#define BEACON_HEXA_HALF_SIZE 18
#define BEACON_SIZE 18
namespace ui::external_app::epirb_tx {
enum class BeaconType {
EPIRB = 0,
ELT = 1,
PLB = 2
};
enum class BeaconProtocol {
USER = 0,
STANDARD = 1,
NATIONAL = 2
};
struct Location {
std::string locator;
bool south;
uint16_t lat_deg;
uint8_t lat_min;
uint8_t lat_sec;
float latitude;
bool west;
uint16_t long_deg;
uint8_t long_min;
uint8_t long_sec;
float longitude;
};
struct BeaconParams {
BeaconType type;
BeaconProtocol protocol;
uint32_t country;
bool is_test;
bool is_internal;
bool has_121_5;
Location location;
};
class EPIRBTXAppView : public View {
public:
EPIRBTXAppView(NavigationView& nav);
~EPIRBTXAppView();
void focus() override;
std::string title() const override { return "EPIRB TX"; };
private:
void start_tx();
void stop_tx();
void update_config();
void on_tx_progress(const uint32_t progress, const bool done);
void on_timer();
void load_beacons();
void set_tx_button_state(bool active);
std::string frame_to_hex_string(bool start);
void generate_frame(BeaconParams params);
void update_frame(bool updateConfig = true);
void update_mode();
void update_location(bool updateLocatorField = true);
struct Beacon {
std::string title{};
std::string description{};
std::string frame{};
};
std::vector<Beacon> beacons{};
Beacon default_beacon{"Self test", "Serial User Location Protocol", "FFFED0D6E6202820000C29FF51041775302D"};
BeaconParams beacon_params{BeaconType::ELT, BeaconProtocol::STANDARD, 227, true, true, true, {"JN03RO", false, 0, 0, 0, 0, false, 0, 0, 0, 0}};
// Currently selected beacon index (from BEACONS.TXT file / options_frame combo)
uint32_t selected_beacon{0};
// Frequency of the transmitter before starting the app (used to restore frequency when leaving)
rf::Frequency original_frequency{0};
// Frequency of the AM emergency signal
rf::Frequency am_frequency{121500000};
// Frequency of the 406 MHz BPSK signal
rf::Frequency bpsk_frequency{406025000};
// True when using a beacon from the BEACONS.TXT file
bool mode_file{true};
// True when looping on sending beacons is enabled
bool loop_enabled{true};
// True if AM emergency signal transmission is enabled
bool am_enabled{true};
// True if we want to send a new frame each time the user changes the current beacon
bool send_on_change{false};
// The current locator string
std::string locator{"JN03RO"};
// The delay between each frame when on loop mode
uint32_t delay{50};
uint8_t beacon_type{0};
// Currently selected beacon protocol
uint8_t beacon_protocol{0};
// Currently selected beacon country
uint32_t beacon_country{227};
// Current beacon's internal state (true for internal location system)
bool beacon_internal{true};
TxRadioState radio_state_{
0 /* frequency */,
1750000 /* bandwidth */,
TONES_SAMPLERATE /* sampling rate */
};
app_settings::SettingsManager settings_{
"tx_epirb",
app_settings::Mode::TX,
{
{"sbeacon"sv, &selected_beacon},
{"amfreq"sv, &am_frequency},
{"bpskfreq"sv, &bpsk_frequency},
{"loop"sv, &loop_enabled},
{"delay"sv, &delay},
{"file"sv, &mode_file},
{"am"sv, &am_enabled},
{"soc"sv, &send_on_change},
{"type"sv, &beacon_type},
{"proto"sv, &beacon_protocol},
{"country"sv, &beacon_country},
{"internal"sv, &beacon_internal},
{"locator"sv, &locator},
}};
// Time of the last sent frame
uint32_t last_frame_time{0};
// True when transmission is enabled
bool transmitting{false};
// True when currently looping on sending beacons
bool loop{false};
// Current EPIRBTXDataMessage for baseband
EPIRBTXDataMessage epirb_tx_message{};
const size_t max_text_width = UI_POS_WIDTH_REMAINING(6) / UI_POS_DEFAULT_WIDTH;
const size_t max_text_width_ext = UI_POS_WIDTH_REMAINING(0) / UI_POS_DEFAULT_WIDTH;
Labels labels{
{{UI_POS_X(0), UI_POS_Y(0)}, "Source:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(6)}, "Frame:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(10)}, "Next frame in s.", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(12)}, "AM frequency MHz", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(17), UI_POS_Y(9)}, "s.", Theme::getInstance()->fg_light->foreground}};
// For file mode
Text text_beacon{
{UI_POS_X(0), UI_POS_Y(1), UI_POS_WIDTH(7), UI_POS_DEFAULT_HEIGHT},
"Beacon:"};
// Beacon selection from BEACONS.TXT
OptionsField options_frame{
{UI_POS_X(7), UI_POS_Y(1)},
30,
{}};
Text text_description_label{
{UI_POS_X(0), UI_POS_Y(2), UI_POS_WIDTH(12), UI_POS_DEFAULT_HEIGHT},
"Description:"};
Text text_description{
{UI_POS_X(0), UI_POS_Y(3), UI_POS_WIDTH_REMAINING(0), UI_POS_DEFAULT_HEIGHT},
""};
Text text_description_end{
{UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH_REMAINING(0), UI_POS_DEFAULT_HEIGHT},
""};
// For manual mode
Text text_beacon_type{
{UI_POS_X(0), UI_POS_Y(1), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
"Type:"};
Text text_beacon_country{
{UI_POS_X(0), UI_POS_Y(2), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
"Country:"};
Checkbox checkbox_beacon_internal{
{UI_POS_X_RIGHT(12), UI_POS_Y(2)},
12,
"Internal",
true};
Text text_beacon_locator{
{UI_POS_X(0), UI_POS_Y(3), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
"Locator:"};
Text text_beacon_latitude{
{UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
"Lat.:"};
Text text_beacon_longitude{
{UI_POS_X(0), UI_POS_Y(5), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
"Long.:"};
Text text_beacon_latitude_value{
{UI_POS_X(7), UI_POS_Y(4), UI_POS_WIDTH(12), UI_POS_DEFAULT_HEIGHT},
""};
Text text_beacon_longitude_value{
{UI_POS_X(7), UI_POS_Y(5), UI_POS_WIDTH(12), UI_POS_DEFAULT_HEIGHT},
""};
OptionsField options_beacon_type{
{UI_POS_X(9), UI_POS_Y(1)},
7,
{{"EPIRB", 0},
{"ELT", 1},
{"PLB", 2}}};
OptionsField options_beacon_protocol{
{UI_POS_X(9 + 7), UI_POS_Y(1)},
30,
{{"User", 0},
{"Standard", 1},
{"National", 2}}};
OptionsField options_beacon_country{
{UI_POS_X(9), UI_POS_Y(2)},
7,
{{"France", 227},
{"USA", 366},
{"Germany", 211},
{"Russia", 273},
{"Spain", 224},
{"Japan", 431},
{"UK", 232}}};
TextField text_field_beacon_locator{
{UI_POS_X(9), UI_POS_Y(3), UI_POS_WIDTH(10), UI_POS_DEFAULT_HEIGHT},
"JN03RO"};
Button button_mangps{
{UI_POS_X_RIGHT(9), UI_POS_Y(3), UI_POS_WIDTH(9), UI_POS_HEIGHT(2)},
"Set pos."};
// Mode selection (file/manual)
OptionsField options_mode{
{UI_POS_X(7), UI_POS_Y(0)},
30,
{{"File (BEACONS.TXT)", 0},
{"Manual (Editor)", 1}}};
// Frame content
Text text_frame{
{UI_POS_X(6), UI_POS_Y(6), UI_POS_WIDTH_REMAINING(6), UI_POS_DEFAULT_HEIGHT},
""};
Text text_frame_end{
{UI_POS_X(6), UI_POS_Y(7), 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},
""};
// Transmission settings
Checkbox checkbox_loop{
{UI_POS_X(0), UI_POS_Y(9)},
10,
"Resend every",
true};
NumberField field_delay{
{UI_POS_X(15), UI_POS_Y(9)},
2,
{1, 99},
1,
' '};
Checkbox checkbox_am{
{UI_POS_X(0), UI_POS_Y(11)},
10,
"AM signal",
true};
FrequencyField field_am_frequency{
{UI_POS_X(13), UI_POS_Y(12)}};
Checkbox checkbox_send_on_change{
{UI_POS_X(0), UI_POS_Y(13)},
14,
"Send on change",
true};
Button button_tx{
{UI_POS_X_RIGHT(9), UI_POS_Y(9), UI_POS_WIDTH(9), UI_POS_HEIGHT(2)},
"START"};
const Style& style_tx_start = *Theme::getInstance()->fg_green;
const Style& style_tx_stop = *Theme::getInstance()->fg_red;
// Transmitter view
TransmitterView tx_view{
(int16_t)UI_POS_Y_BOTTOM(4),
10000,
12};
MessageHandlerRegistration message_handler_tx_progress{
Message::ID::TXProgress,
[this](const Message* const p) {
const auto message = *reinterpret_cast<const TXProgressMessage*>(p);
this->on_tx_progress(message.progress, message.done);
}};
MessageHandlerRegistration message_handler_frame_sync{
// Use frame sync for our applcation timer callback
Message::ID::DisplayFrameSync,
[this](const Message* const) {
this->on_timer();
}};
};
} // namespace ui::external_app::epirb_tx
#endif /*__EPIRB_TX_H__*/
+5
View File
@@ -260,6 +260,10 @@ set(EXTCPPSRC
external/epirb_rx/main.cpp
external/epirb_rx/ui_epirb_rx.cpp
#epirb_tx
external/epirb_tx/main.cpp
external/epirb_tx/ui_epirb_tx.cpp
#soundboard 272byte - 1236 bytes
external/soundboard/main.cpp
external/soundboard/soundboard_app.cpp
@@ -389,6 +393,7 @@ set(EXTAPPLIST
battleship
ert
epirb_rx
epirb_tx
soundboard
game2048
bht_tx
+6
View File
@@ -102,6 +102,7 @@ MEMORY
ram_external_app_time_sink (rwx) : org = 0xADFD0000, len = 32k
ram_external_app_same_tx (rwx) : org = 0xADFE0000, len = 32k
ram_external_app_kiss_tnc (rwx) : org = 0xADFF0000, len = 32k
ram_external_app_epirb_tx (rwx) : org = 0xAE000000, len = 32k
}
@@ -582,5 +583,10 @@ SECTIONS
*(*ui*external_app*kiss_tnc*);
} > ram_external_app_kiss_tnc
.external_app_epirb_tx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_epirb_tx.application_information));
*(*ui*external_app*epirb_tx*);
} > ram_external_app_epirb_tx
}
+1
View File
@@ -57,3 +57,4 @@ const std::filesystem::path macaddress_dir = u"MACADDRESS";
const std::filesystem::path splash_dot_bmp = u"/splash.bmp";
const std::filesystem::path keeloq_keys_dir = u"KEELOQKEYS";
const std::filesystem::path keeloq_remotes_dir = u"KEELOQREMOTES";
const std::filesystem::path epirb_dir = u"EPIRB";
+1
View File
@@ -58,6 +58,7 @@ extern const std::filesystem::path waterfalls_dir;
extern const std::filesystem::path macaddress_dir;
extern const std::filesystem::path keeloq_keys_dir;
extern const std::filesystem::path keeloq_remotes_dir;
extern const std::filesystem::path epirb_dir;
extern const std::filesystem::path splash_dot_bmp;
+8
View File
@@ -535,6 +535,14 @@ set(MODE_CPPSRC
)
DeclareTargets(PEPI epirb_rx)
### EPIRB TX
set(MODE_CPPSRC
proc_epirb_tx.cpp
)
DeclareTargets(PEPT epirb_tx)
### NRF RX
+191
View File
@@ -0,0 +1,191 @@
/*
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
*
* 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 "proc_epirb_tx.hpp"
#include "portapack_shared_memory.hpp"
#include "sine_table_int8.hpp"
#include "event_m4.hpp"
#include <cstdint>
#include <cstring>
/**
* Processing method for this processor
*/
void EPIRBTXProcessor::execute(const buffer_c8_t& buffer) {
if (!configured) return;
// Iterate on each sample of the buffer
for (size_t i = 0; i < buffer.count; i++) {
if (end_of_transmission) {
// Stop transmission
configured = false;
end_of_transmission = false;
txprogress_message.done = true;
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 a negative phase carrier during pre-count
bpsk_pre_count++;
re = i_neg;
im = q_neg;
} else if (bpsk_post_count > 0) {
// Post-count: send a negative phase carrier during post-count
bpsk_post_count++;
re = i_neg;
im = q_neg;
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 {
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;
}
} 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++;
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;
}
}
}
}
}
} else {
// AM 127.5 MHz sine sweep
// ---- 3 Hz Sweep ----
sweep_phase += sweep_inc;
uint8_t sweep_index = (sweep_phase & 0xFF000000) >> 24;
int8_t sweep = sine_table_i8[sweep_index]; // -128..127
// Audio frequency based on sweep
int32_t audio_freq = center_freq + sweep * freq_dev;
// ---- Audio signal (sine wave) ----
uint32_t audio_inc = audio_freq * freq_scale;
audio_phase += audio_inc;
uint8_t audio_index = (audio_phase & 0xFF000000) >> 24;
int8_t audio = sine_table_i8[audio_index];
// ---- AM ----
// Double Side Band modulation with modulation index of ~80% (100/128) + offset (74)
int16_t amplitude = 74 + ((100 * audio) >> 7); // 1/128 via shift
if (amplitude > 127) amplitude = 127;
if (amplitude < -128) amplitude = -128;
re = (int8_t)amplitude;
im = 0;
}
buffer.p[i] = {re, im};
}
};
void EPIRBTXProcessor::on_message(const Message* const msg) {
// Configure the processor
switch (msg->id) {
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
config_pre_count = message.pre_count;
config_post_count = message.post_count;
frame_data_len = message.data_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;
} else {
// AM mode for 121.5 signal => init AM sequencer
sweep_phase = 0;
audio_phase = 0;
}
// Tell the processor to start
configured = true;
} break;
default:
break;
}
}
int main() {
EventDispatcher event_dispatcher{std::make_unique<EPIRBTXProcessor>()};
event_dispatcher.run();
return 0;
}
+111
View File
@@ -0,0 +1,111 @@
/*
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
*
* 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.
*/
#ifndef __PROC_EPIRB_TX_H__
#define __PROC_EPIRB_TX_H__
#include "baseband_processor.hpp"
#include "baseband_thread.hpp"
#include "portapack_shared_memory.hpp"
#include "tonesets.hpp"
#include <cmath>
/**
* Processor used by epirb_tx app to simulate a COSPAS/SARSAT emergency beacon
* The processor will alternatively:
* - Send a 406 MHz Manchester encoded BPSK signal containing the beacon information
* - Send a 127.5 MHz AM distress audio signal
*/
class EPIRBTXProcessor : public BasebandProcessor {
public:
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const msg) override;
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 the transmission has to be stopped (e.g. at the end of a frame)
bool end_of_transmission{};
// I/Q values for current sample (ranging from -128 to 127)
int8_t re{0}, im{0};
// Configured pre-count value: used to generate a carrier for config_pre_count samples before starting a frame
uint32_t config_pre_count = 0;
// 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
uint8_t frame_data_len = 0;
// BPSK parameters: Target phase +/-63° as per COSPAS/SARSAT specifications
static constexpr float phase_rad = 63.0f * M_PI / 180.0f;
// I/Q values for BPSK (positive phase and negative phase)
int8_t i_pos = (int8_t)(cos(phase_rad) * 127);
int8_t q_pos = (int8_t)(sin(phase_rad) * 127);
int8_t i_neg = i_pos;
int8_t q_neg = -q_pos;
// COSPAS/SARSAT signal is manchester (2 states per bit) encoded 400 bit/sec
static const uint32_t samples_per_halfbit = TONES_SAMPLERATE / 400 / 2;
// Sequencer state for BPSK
uint32_t sample_counter = 0;
uint32_t bpsk_pre_count = 0;
uint32_t bpsk_post_count = 0;
// Bit position in current byte
uint32_t bit_index = 0;
// Byte position in current frame
uint32_t byte_index = 0;
// Value of the current byte
uint8_t current_byte = 0;
// Value of the current bit
uint8_t current_bit = 0;
// Position in the current manchester bit
bool manchester_half = false; // false = first half
// 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)
static const uint32_t f_max = 1600; // Sweep max frequency (Hz)
static const uint32_t freq_span = f_max - f_min;
// Frequency
static const uint32_t freq_scale = (1ULL << 32) / TONES_SAMPLERATE;
static const int32_t center_freq = f_min + (freq_span / 2);
static const int32_t freq_dev = freq_span / 256;
// Increments
static const uint32_t sweep_inc = sweep_rate * freq_scale;
// Phase accumulators for sweep and audio
uint32_t sweep_phase = 0;
uint32_t audio_phase = 0;
TXProgressMessage txprogress_message{};
/* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{TONES_SAMPLERATE, this, baseband::Direction::Transmit};
};
#endif
+14
View File
@@ -156,6 +156,7 @@ class Message {
RTTYData = 98,
NotificationData = 99,
TimeSinkConfig = 100,
EPIRBTXData = 101,
MAX
};
@@ -1110,6 +1111,19 @@ class SigGenToneMessage : public Message {
const uint32_t tone_delta;
};
class EPIRBTXDataMessage : public Message {
public:
static constexpr uint8_t max_len = 18;
constexpr EPIRBTXDataMessage()
: Message{ID::EPIRBTXData} {
}
bool mode_bpsk = true;
uint8_t data[max_len]{0};
uint8_t data_len = 0;
uint32_t pre_count = 0;
uint32_t post_count = 0;
};
class AFSKTxConfigureMessage : public Message {
public:
constexpr AFSKTxConfigureMessage(
+1
View File
@@ -88,6 +88,7 @@ constexpr image_tag_t image_tag_am_tv{'P', 'A', 'M', 'T'};
constexpr image_tag_t image_tag_capture{'P', 'C', 'A', 'P'};
constexpr image_tag_t image_tag_ert{'P', 'E', 'R', 'T'};
constexpr image_tag_t image_tag_epirb_rx{'P', 'E', 'P', 'I'};
constexpr image_tag_t image_tag_epirb_tx{'P', 'E', 'P', 'T'};
constexpr image_tag_t image_tag_nfm_audio{'P', 'N', 'F', 'M'};
constexpr image_tag_t image_tag_pocsag{'P', 'P', 'O', 'C'};
constexpr image_tag_t image_tag_pocsag2{'P', 'P', 'O', '2'};
+25
View File
@@ -0,0 +1,25 @@
#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 OK; Radio call sign (valid BCH1); FFFED056E6804002202009655250
RLS Location; RLS Location; FFFED08E0D0990014710021963C85C7009F5
PLB Location; PLB Location: National Location; FFFED08E3B15F1DFC0FF07FD1F769F3C0672
RLS Location 2; RLS Location Protocol; FFFED08F4DCBC01ACD004BB10D4E79C4DD86
Std Loc. ELT 24; ELT 24-bit Address Protocol; FFFED096E3AAAAAA7FDFF8211F3583E0FAA8
Standard Location; Standard Location Protocol EPIRB (Serial); FFFED096E61B0CAE7FDFFF0E58B583E0FAA8
PLB Location; PLB Location: National Location; FFFED09F7B00F9E8EC737E5312378A1802B0
EPIRB; Standard Location Protocol EPIRB-MMSI; FFFED0A0D205F260850F3DC9E0B70B6E4FD7
PLB; Standard Location Protocol - PLB (Serial); FFFED0A3E7B10016150D364D8B3689C09437
SAAS; Std. Location ship security protocol (SSAS); FFFED0A5DC19E1A07FDFFE5C803483E0FCCA
ELT 24; Serial user Location (ELT with Aircraft 24-bit Address); FFFED0CE46E76EF8C00C2BAA31CFE0FF0F61
MMSO; Maritime User Protocol MMSI - EPIRB; FFFED0D9D4EB28140AA6893F16A2C67282EC
Float Free EPIRB; Float Free EPIRB with Serial Identification Number; FFFED0DF76A9A9C800174DE27BE1F0277E45
Non Float Free EPIRB; Non Float Free EPIRB with Serial Identification; FFFED0DF77200000001168C610AFE0FF0146
Radio Call Sign; Radio Call Sign - EPIRB; FFFED0E0DDAE599508268E4C05E054651307
ELT Aviation; ELT Aviation User; FFFED0E29325ACB12E938B671E0FE0FF0F61
ELT Aircraft; ELT with Aircraft Operator Designator & Serial Number; FFFED0EDF67B7182038C2F0E10CFE0FF0F61
PLB Serial; Standard Location Protocol - PLB (Serial); FFFED0A157B081437FDFF8B4833783E0F66C
RLS Location; RLS Location Protocol; FFFED096ED09900149D4D467EE0851A3B2E8
Orbitography; Orbitography Protocol (from Toulouse FMCC's LUT); FFFE2FCE3000000000000DBD0E4022417500
ELT 24 2; ELT 24 bits; FFFED08DB345B146202DDF3C71F59BAB7072