EPIRB RX app rework + EPIRB TX app fixes (#3172)

* First step to EPIRB rx app fix
* Added spike filtering on carrier detection for more robustness.
* Created dedicated packet builder
* First step to fixing UI.
* First step to epirb-rx ui rework.
* Made static methods static members
* Code optim
* Code cleanup + first step to new UI
* Fixed display
* Added BeaconList widget + BeaconDB
* Epirb TX app memory optim
* Fixed OOM when opening Map Display or Locator editor on EPIRB TX app
* First step to tabs + code cleanup to free some space...
* Added spectrum analyzer + fixed display
* Fixed display refresh issue after leaving spectrum.
* More code optim + first step to detail view.
* Fixed float coord display.
* Fixed protocol names
* Fixed location display
* Externalized country base to sd card + more code optim
* Fixed country display
* Fixed path
* Added protocol description and resource manager
* Added main/aux loc device
* Removed specan to save space
* First step to adding QR code
* Used a custom TextArea component instead of Console
* Fixed QR code display for map URL
* Added beacon selection management + fixes
* First step to map/data qr.
* QR tab : added data + detail/map toggle
* Removed sqhelch to save space
* Removed audio to save space
* Removed freq label to save space
* Removed packet timestamp to save space
* Moved frequency values to resource file
* Fixed detection parameters to suite real beacons (slower phase shift time).
* Fixed countdown field size
* Fixed frequency list.
* Fixed detail view on select.
* First step to fix settings.
* Fixed squelch for a 0-99 range.
* Added bch code correction (single bit error).
* More code optim, disable country cache, removed inline from location to reduce code size.
* Removed inline, code formatting.
* Added bch code correction display.
* Fixed focus bug, fixed packet size check. Added BCH1/BCH2 correction test cases in BEACONS.TXT file for EPIRB TX application.
* Fixed merge
* Added countdown setting.
* Added selection display to beacon list
* Restored squelch control. Added countdown reset.
* Code optim on formatSummary
* More code optim
* Made ResourceManager methods static
* Externalized beacon strings to res file
*Removed frame parameter from methods
* Added test beacons for emergency. Removed old res file.
* Fixed resource manager
* Fixed resources. Added emergency display.
* BCH + res fixes
* Fixed beacons
* Fixed app color. Fixed HexId and Serial calculation.
* Fixed frequency list. Comments.
* Code cleanup and comments
* Added slideshow mode to EPIRB TX application. Code comments / cleanup.
* Fixed frame end after wrong code cleanup
* More code optim and comments. Removed touch from beacon list.
* Added beacon selection with encoder on detail view.
* Added beacon paging in header. Fixed timeout display
* Fixed build for PRALINE
* Fixed formatting
* Potential fix for pull request finding
* stop on carriage return
* empty string if unknown
* zeroing data buffer before use
* fix typo
* add include
* fix case if pair == 0
* fix scpectrum_on to spectrum_on
* fix typos
* replaced 3 inlined offset blocks with apply_offset calls
* set_beacon consolidation
This commit is contained in:
Frederic BORRY
2026-05-12 14:00:47 +02:00
committed by GitHub
parent 33330e4c34
commit c34a6940ca
26 changed files with 4710 additions and 2328 deletions
+4
View File
@@ -432,6 +432,10 @@ void set_epirb_tx_config(EPIRBTXDataMessage& message) {
send_message(&message);
}
void set_epirb_rx_config(EPIRBRXConfig& message) {
send_message(&message);
}
void set_p25tx_data(const uint8_t* dibits, uint16_t frame_length) {
const size_t max_len = sizeof(shared_memory.bb_data.data);
if (frame_length > max_len) frame_length = max_len;
+1
View File
@@ -118,6 +118,7 @@ void set_bitstream_config(uint32_t deviation, uint8_t mode);
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 set_epirb_rx_config(EPIRBRXConfig& message);
void set_p25tx_data(const uint8_t* dibits, uint16_t frame_length);
void request_roger_beep();
+59
View File
@@ -0,0 +1,59 @@
/*
* 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 "beacon.hpp"
#include <cstdio>
#include "string_format.hpp"
// Some but not all methods of beacon class have been externalized in .cpp file
// This has been done to try and optimize application size by preventing the compiler to inline methods
namespace ui::external_app::epirb_rx {
size_t Beacon::formatSummary(char* buffer, bool with_time) {
size_t result = 0;
if (with_time) {
result += formatTime(buffer);
buffer[result++] = '-';
}
result += sprintf((buffer + result), "%4s-%5s-", shortId().c_str(), getType());
if (location.isUnknown()) {
result += sprintf((buffer + result), " ");
} else {
result += location.toString((buffer + result), Location::LocationFormat::MAIDENHEAD_LOCATOR);
}
result += sprintf((buffer + result), "[%s%s%s]", isFrameValid() ? (bch1Corrected || bch2Corrected) ? STR_COLOR_YELLOW : STR_COLOR_GREEN : STR_COLOR_RED, getSatus().c_str(), STR_COLOR_WHITE);
return result;
}
bool Beacon::isFrameValid() {
return isBch1Valid() && ((!hasBch2) || isBch2Valid()) && (!isEmpty);
}
std::string Beacon::shortId() {
std::string result = std::string(hexId);
return (result.size() >= 4) ? result.substr(0, 4) : result;
}
size_t Beacon::formatTime(char* buffer) {
return sprintf(buffer, "%02d:%02d:%02d", date.hour(), date.minute(), date.second());
}
} // namespace ui::external_app::epirb_rx
+856
View File
@@ -0,0 +1,856 @@
/*
* 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_RX_H__
#define __BEACON_RX_H__
#include "country.hpp"
#include "location.hpp"
#include "rtc_time.hpp"
#include "resources.hpp"
namespace ui::external_app::epirb_rx {
// Size for beacon frame buffer
#define BEACON_DATA_SIZE 18 // Max 144 bits => 18 bytes
// Constants for BCH control code calculation
#define BCH_21_POLYNOMIAL 0b1001101101100111100011UL
#define BCH_21_POLY_LENGTH 22
#define BCH_12_POLYNOMIAL 0b1010100111001UL
#define BCH_12_POLY_LENGTH 13
// See content of sdcard/EPIRB/RES/BEACON.RES
#define ADDITIONAL_DATA_RESOURCE_START 25
#define LONG_ADDITIONAL_DATA_RESOURCE_START 33
#define EMERGENCY_RESOURCE_START 39
#define EMERGENCY_OTHER_RESOURCE_START 49
#define AUX_DEVICE_RESOURCE_START 53
/**
* This is the main class for beaon handling
*/
class Beacon {
public:
// Real beacon or test beacon ?
enum class FrameMode { NORMAL,
SELF_TEST,
UNKNOWN };
// Type of main location device
enum class MainLocatingDevice { UNDEFINED,
INTERNAL_NAV,
EXTERNAL_NAV };
// Type of aux location device
enum class AuxLocatingDevice { UNDEFINED,
NONE,
NONE_OR_OTHER,
OTHER,
MHZ121_5,
SART };
// Protcol type
enum class ProtocolType {
UNKNOWN,
USER,
STANDARD_LOCATION,
NATIONAL_LOCATION,
RLS_LOCATION,
ELT_DT_LOCATION,
SPARE
};
// Protocol
enum class Protocol {
USER_EPIRB_MARITIME,
USER_EPIRB_RADIO,
USER_ELT,
USER_SERIAL,
USER_TEST,
USER_ORB,
USER_NAT,
USER_2G,
STD_EPIRB,
STD_ELT_24,
STD_ELT_SERIAL,
STD_ELT_AIRCRAFT,
STD_EPIRB_SERIAL,
STD_PLB_SERIAL,
STD_SHIP,
STD_TEST,
NAT_ELT,
NAT_EPIRB,
NAT_PLB,
NAT_TEST,
RLS,
ELT_DT,
SPARE,
UNKNOWN
};
#define UNKNOWN_LABEL "Unk."
// Returns true for User protocol beacons
bool protocolIsUser() { return (protocolType == ProtocolType::USER); };
// Returns true for National Location protocol beacons
bool protocolIsNational() { return (protocolType == ProtocolType::NATIONAL_LOCATION); };
// Returns true for Standard Location protocol beacons
bool protocolIsStandard() { return (protocolType == ProtocolType::STANDARD_LOCATION); };
// Returns true for RLS location protocol beacons
bool protocolIsRls() { return (protocolType == ProtocolType::RLS_LOCATION); };
// Returns true for RLS or ELT Location protocol beacons
bool protocolIsRlsOrElt() { return (protocolType == ProtocolType::RLS_LOCATION || protocolType == ProtocolType::ELT_DT_LOCATION); };
// Returns true for unknown protocol beacons
bool protocolIsUnknown() { return (protocolType == ProtocolType::UNKNOWN); };
// Returns the protocol name
const char* getProtocolName() {
switch (protocolType) {
case ProtocolType::STANDARD_LOCATION:
return "Standard";
case ProtocolType::NATIONAL_LOCATION:
return "National";
case ProtocolType::RLS_LOCATION:
return "RLS";
case ProtocolType::ELT_DT_LOCATION:
return "ELT(DT)";
case ProtocolType::USER:
return longFrame ? "User Location" : "User";
case ProtocolType::SPARE:
return "Spare";
default:
return UNKNOWN_LABEL;
}
}
// Get protocol type for this beacon
ProtocolType getProtocolType() {
switch (protocol) {
case Protocol::USER_EPIRB_MARITIME:
case Protocol::USER_EPIRB_RADIO:
case Protocol::USER_ELT:
case Protocol::USER_SERIAL:
case Protocol::USER_TEST:
case Protocol::USER_ORB:
case Protocol::USER_NAT:
case Protocol::USER_2G:
return ProtocolType::USER;
case Protocol::STD_EPIRB:
case Protocol::STD_ELT_24:
case Protocol::STD_ELT_SERIAL:
case Protocol::STD_ELT_AIRCRAFT:
case Protocol::STD_PLB_SERIAL:
case Protocol::STD_SHIP:
case Protocol::STD_TEST:
return ProtocolType::STANDARD_LOCATION;
case Protocol::NAT_ELT:
case Protocol::NAT_EPIRB:
case Protocol::NAT_PLB:
case Protocol::NAT_TEST:
return ProtocolType::NATIONAL_LOCATION;
case Protocol::RLS:
return ProtocolType::RLS_LOCATION;
case Protocol::ELT_DT:
return ProtocolType::ELT_DT_LOCATION;
case Protocol::SPARE:
return ProtocolType::SPARE;
case Protocol::UNKNOWN:
default:
return ProtocolType::UNKNOWN;
}
}
// True for long frames, fals for short grames
bool longFrame{true};
// True if protocol flag is set for this frame
bool protocolFlag{false};
// Frame mode (Test or Real)
FrameMode frameMode{FrameMode::UNKNOWN};
// Main location device
MainLocatingDevice mainLocatingDevice{MainLocatingDevice::UNDEFINED};
// Axiliary location device
AuxLocatingDevice auxLocatingDevice{AuxLocatingDevice::UNDEFINED};
// Protocol code for this beacon
long protocolCode{0};
// Protocol of this beacon
Protocol protocol{Protocol::UNKNOWN};
// Protocol type of this beacon
ProtocolType protocolType{ProtocolType::UNKNOWN};
// Country of this beacon
Country country{};
// Frame data
uint8_t frame[BEACON_DATA_SIZE];
// Location of this beacon
Location location{};
// HexId of this beacon
uint64_t identifier{0};
// Date of reception of this beacon
rtc::RTC date{};
// Trus if this beacon has additional data
bool hasAdditionalData{false};
// The additional data tring
char additionalData[48]{0};
// True if this beacon has a serial number
bool hasSerialNumber{false};
// The serial number string
char serialNumber[32]{0};
// Beacon's Hex ID string
char hexId[32]{0};
// BCH1 code value
uint32_t bch1{};
// BCH1 calculated value
uint32_t computedBch1{};
// True if data has been corrected to match BCH1 code
bool bch1Corrected{false};
// True if this beacon has a BCH2 correction code
bool hasBch2{false};
// BCH2 code value
uint32_t bch2{};
// BCH2 calculated value
uint32_t computedBch2{};
// True if data has been corrected to match BCH2 code
bool bch2Corrected{false};
// True if this beacon is empty (not initialized)
bool isEmpty{true};
// True if thsi beacon contains emergency data
bool hasEmergency{false};
// True if emenrgency data is automatic for this beacon
bool isAutoamticEmergency{false};
// True if this beacon is a Maritime beacon
bool isMaritime{false};
// Emergency type string
char emergencyType[32]{0};
/**
* Returns the requested bits in this beacon's frame
*/
uint64_t getBits(int startBit, int endBit) {
uint64_t result = 0;
startBit--;
int numBits = endBit - startBit;
const uint8_t* pData = &(frame[startBit / 8]);
uint8_t b = *pData;
int bitOffset = 7 - (startBit % 8);
for (int i = 0; i < numBits; ++i) {
result <<= 1;
result |= ((b >> bitOffset) & 0x01);
if (--bitOffset < 0) {
b = *(++pData);
bitOffset = 7;
}
}
return result;
}
/**
* Compute a BCH control code for the given bits and poly
*/
uint64_t computeBCH(int startBit, int endBit, unsigned long poly, int polyLength) {
int dataLength = endBit - startBit + 1;
int totalLength = dataLength + polyLength - 1;
uint64_t result = getBits(startBit, startBit + polyLength - 1);
for (int i = polyLength; i <= totalLength; i++) {
bool firstBit = result >> (polyLength - 1);
if (firstBit) result = result ^ poly;
if (i < totalLength) {
result = result << 1;
if (i < dataLength) result |= getBits(startBit + i, startBit + i);
}
}
return result;
}
/**
* Compute BCH1 code
*/
uint64_t computeBCH1() { return computeBCH(25, 85, BCH_21_POLYNOMIAL, BCH_21_POLY_LENGTH); }
/**
* Compute BCH2 code
*/
uint64_t computeBCH2() { return computeBCH(107, 132, BCH_12_POLYNOMIAL, BCH_12_POLY_LENGTH); }
/**
* Convert this frame's data to an hex string
*/
static size_t toHexString(char* buffer, uint8_t* frame, bool withSpace, int start, int end) {
size_t result = 0;
for (uint8_t i = start; i < end; i++) {
if (withSpace && i > start) buffer[result++] = ' ';
result += sprintf((buffer + result), "%02X", frame[i]);
}
buffer[result] = 0;
return result;
}
Beacon() {}
Beacon(const Beacon& other) = delete;
Beacon& operator=(const Beacon& other) = delete;
/**
* Set the data for this Beacon and parse it
*/
void setFrame(const uint8_t* frameBuffer) {
frameMode = FrameMode::UNKNOWN;
mainLocatingDevice = MainLocatingDevice::UNDEFINED;
auxLocatingDevice = AuxLocatingDevice::UNDEFINED;
protocolCode = 0;
protocol = Protocol::UNKNOWN;
protocolType = ProtocolType::UNKNOWN;
country.code = 0;
country.alphaCode[0] = 0;
country.shortName[0] = 0;
location.clear();
identifier = 0;
hasAdditionalData = false;
additionalData[0] = 0;
hasSerialNumber = false;
serialNumber[0] = 0;
hexId[0] = 0;
bch1 = 0;
computedBch1 = 0;
hasBch2 = false;
bch1Corrected = false;
bch2 = 0;
computedBch2 = 0;
bch2Corrected = false;
isEmpty = true;
hasEmergency = false;
isAutoamticEmergency = false;
isMaritime = false;
emergencyType[0] = 0;
std::memcpy(frame, (const void*)frameBuffer, BEACON_DATA_SIZE);
parseFrame();
}
/**
* Flip the specified bit in this beacon's data
*/
void flipBit(int bit) {
int byteIdx = (bit - 1) / 8;
int bitIdx = 7 - ((bit - 1) % 8);
frame[byteIdx] ^= (1 << bitIdx);
}
/**
* Single bit error correction to match BCH1 or BCH2 code
*/
void simpleCorrection(bool isBCH1) {
for (int i = (isBCH1 ? 25 : 107); i <= (isBCH1 ? 85 : 132); i++) {
flipBit(i);
if ((isBCH1 ? (computeBCH1() == bch1) : (computeBCH2() == bch2))) {
if (isBCH1) {
computedBch1 = bch1;
bch1Corrected = true;
} else {
computedBch2 = bch2;
bch2Corrected = true;
}
break;
}
flipBit(i); // Restore bit value
}
}
/**
* Return the string representation of this beacon's type
*/
const char* getType() {
if ((protocol == Protocol::USER_EPIRB_MARITIME) || (protocol == Protocol::USER_EPIRB_RADIO) || (protocol == Protocol::STD_EPIRB) || (protocol == Protocol::STD_EPIRB_SERIAL) || (protocol == Protocol::NAT_EPIRB)) return "EPIRB";
if ((protocol == Protocol::USER_ELT) || (protocol == Protocol::STD_ELT_24) || (protocol == Protocol::STD_ELT_SERIAL) || (protocol == Protocol::STD_ELT_AIRCRAFT) || (protocol == Protocol::NAT_ELT) || (protocol == Protocol::ELT_DT)) return "ELT";
if ((protocol == Protocol::STD_PLB_SERIAL) || (protocol == Protocol::NAT_PLB)) return "PLB";
if ((protocol == Protocol::USER_TEST) || (protocol == Protocol::STD_TEST) || (protocol == Protocol::NAT_TEST)) return "TEST";
if (protocol == Protocol::USER_SERIAL) return "SRIAL";
if (protocol == Protocol::USER_ORB) return "ORB";
if (protocol == Protocol::USER_NAT) return "NAT";
if (protocol == Protocol::USER_2G) return "2G";
if (protocol == Protocol::STD_SHIP) return "SHIP";
if (protocol == Protocol::RLS) return "RLS";
if (protocol == Protocol::SPARE) return "SPARE";
return UNKNOWN_LABEL;
}
/**
* Returns true if this beacon has a main location device
*/
bool hasMainLocatingDevice() { return (mainLocatingDevice != MainLocatingDevice::UNDEFINED); }
/**
* Returns this beacon's main location device string representation
*/
const char* getMainLocatingDeviceName() {
if (mainLocatingDevice == MainLocatingDevice::EXTERNAL_NAV) return "Exernal";
if (mainLocatingDevice == MainLocatingDevice::INTERNAL_NAV) return "Internal";
return UNKNOWN_LABEL;
}
/**
* Returns true if this beacon has a main location device
*/
bool hasAuxLocatingDevice() { return (auxLocatingDevice != AuxLocatingDevice::UNDEFINED); }
/**
* Returns true if this beacon has a main location device
*/
const char* getAuxLocatingDeviceName() {
return ResourceManager::get_beacon_resource(AUX_DEVICE_RESOURCE_START + ((int)auxLocatingDevice));
}
/**
* Set the serial number for this beacon
*/
void setSerialNumber(uint32_t serial) {
sprintf(serialNumber, "%ld (0x%08lX)", serial, serial);
}
/**
* Returns true if BCH1 code is valid
*/
bool isBch1Valid() { return (bch1 == computedBch1); }
/**
* Returns true if BCH2 code is valid
*/
bool isBch2Valid() { return (bch2 == computedBch2); }
/**
* Returns true if frame is valid
*/
bool isFrameValid();
/**
* Returns true if frame is orbito
*/
bool isOrbito() { return (protocol == Protocol::USER_ORB); }
/**
* Write the hex preresentation of this frame in the provided buffer
*/
size_t hexString(char* buffer, bool withHeader) {
return toHexString(buffer, frame, false, (withHeader ? 0 : 3), (longFrame ? 18 : 14));
}
/**
* Returns the short ID for this beacon
*/
std::string shortId();
/**
* Returns the string representation of the status of this frame
*/
std::string getSatus() {
if (isFrameValid())
return "OK";
else
return "KO";
}
/**
* Format this beacon's time
*/
size_t formatTime(char* buffer);
/**
* Format this beacon's summary
*/
size_t formatSummary(char* buffer, bool with_time);
private:
/* Baudot code matrix */
static constexpr char BAUDOT_CODE[64] = {' ', '5', ' ', '9', ' ', ' ', ' ', ' ', ' ', ' ', '4', ' ', '8', '0', ' ', ' ',
'3', ' ', ' ', ' ', ' ', '6', ' ', '/', '-', '2', ' ', ' ', '7', '1', ' ', ' ',
' ', 'T', ' ', 'O', ' ', 'H', 'N', 'M', ' ', 'L', 'R', 'G', 'I', 'P', 'C', 'V',
'E', 'Z', 'D', 'B', 'S', 'Y', 'F', 'X', 'A', 'W', 'J', ' ', 'U', 'Q', 'K', '\0'};
/**
* Parse this beacon's protocol
*/
void parseProtocol() {
protocolFlag = getBits(26, 26);
if (protocolFlag)
protocolCode = getBits(37, 39);
else
protocolCode = getBits(37, 40);
if (!longFrame || protocolFlag == 1) {
switch (protocolCode) {
case 0b000:
protocol = Protocol::USER_ORB;
break;
case 0b001:
protocol = Protocol::USER_ELT;
break;
case 0b010:
protocol = Protocol::USER_EPIRB_MARITIME;
isMaritime = true;
break;
case 0b011:
protocol = Protocol::USER_SERIAL;
break;
case 0b100:
protocol = Protocol::USER_NAT;
break;
case 0b101:
protocol = Protocol::USER_2G;
break;
case 0b110:
protocol = Protocol::USER_EPIRB_RADIO;
isMaritime = true;
break;
case 0b111:
protocol = Protocol::USER_TEST;
break;
default:
protocol = Protocol::UNKNOWN;
}
} else {
switch (protocolCode) {
case 0b0010:
protocol = Protocol::STD_EPIRB;
break;
case 0b0011:
protocol = Protocol::STD_ELT_24;
break;
case 0b0100:
protocol = Protocol::STD_ELT_SERIAL;
break;
case 0b0101:
protocol = Protocol::STD_ELT_AIRCRAFT;
break;
case 0b0110:
protocol = Protocol::STD_EPIRB_SERIAL;
break;
case 0b0111:
protocol = Protocol::STD_PLB_SERIAL;
break;
case 0b1000:
protocol = Protocol::NAT_ELT;
break;
case 0b1001:
protocol = Protocol::ELT_DT;
break;
case 0b1010:
protocol = Protocol::NAT_EPIRB;
break;
case 0b1011:
protocol = Protocol::NAT_PLB;
break;
case 0b1100:
protocol = Protocol::STD_SHIP;
break;
case 0b1101:
protocol = Protocol::RLS;
break;
case 0b1110:
protocol = Protocol::STD_TEST;
break;
case 0b1111:
protocol = Protocol::NAT_TEST;
break;
default:
protocol = Protocol::UNKNOWN;
}
}
}
/**
* Parse this beacon's additional data
*/
void parseAdditionalData() {
hasAdditionalData = false;
hasSerialNumber = false;
if (protocolFlag) {
if (protocolCode == 0b011) {
uint8_t serialUserProtocol = getBits(40, 42);
hasAdditionalData = true;
switch (serialUserProtocol) {
case 0b100:
case 0b010:
isMaritime = true;
// fallthrough
case 0b000:
case 0b001:
case 0b011:
case 0b110:
strcpy(additionalData, ResourceManager::get_beacon_resource(ADDITIONAL_DATA_RESOURCE_START + serialUserProtocol));
break;
default:
hasAdditionalData = false;
}
hasSerialNumber = true;
setSerialNumber((serialUserProtocol == 0b011) ? getBits(44, 67) : (serialUserProtocol == 0b001) ? getBits(62, 73)
: getBits(44, 63));
}
if (!longFrame) {
hasEmergency = getBits(107, 107);
if (hasEmergency) {
isAutoamticEmergency = getBits(108, 108);
if (isMaritime) {
uint8_t emmergency = getBits(109, 112);
switch (emmergency) {
case 0b0001:
case 0b0010:
case 0b0011:
case 0b0100:
case 0b0101:
case 0b0110:
case 0b0111:
case 0b1000:
strcpy(emergencyType, ResourceManager::get_beacon_resource(EMERGENCY_RESOURCE_START + emmergency));
break;
default:
strcpy(emergencyType, ResourceManager::get_beacon_resource(EMERGENCY_RESOURCE_START));
}
} else {
bool fire = getBits(109, 109);
bool med = getBits(110, 110);
bool disabled = getBits(111, 111);
char* emmergency_pointer = emergencyType;
if (fire) emmergency_pointer += sprintf(emmergency_pointer, "%s", ResourceManager::get_beacon_resource(EMERGENCY_OTHER_RESOURCE_START));
if (med) {
if (fire) emmergency_pointer += sprintf(emmergency_pointer, "%c", '+');
emmergency_pointer += sprintf(emmergency_pointer, "%s", ResourceManager::get_beacon_resource(EMERGENCY_OTHER_RESOURCE_START + 1));
}
if (disabled) {
if (fire || med) emmergency_pointer += sprintf(emmergency_pointer, "%c", '+');
sprintf(emmergency_pointer, "%s", ResourceManager::get_beacon_resource(EMERGENCY_OTHER_RESOURCE_START + 2));
}
}
}
}
} else if (longFrame) {
switch (protocolCode) {
case 0b0010:
hasSerialNumber = true;
setSerialNumber(getBits(61, 64));
break;
case 0b1100: {
hasAdditionalData = true;
uint32_t mmsi = getBits(41, 60);
sprintf(additionalData, ResourceManager::get_beacon_resource(LONG_ADDITIONAL_DATA_RESOURCE_START), mmsi, mmsi);
} break;
case 0b0011: {
hasAdditionalData = true;
hasSerialNumber = true;
setSerialNumber(getBits(41, 64));
strcpy(additionalData, ResourceManager::get_beacon_resource(LONG_ADDITIONAL_DATA_RESOURCE_START + 1));
} break;
case 0b0100:
case 0b0110:
case 0b0111: {
hasAdditionalData = true;
hasSerialNumber = true;
uint32_t csTaNumber = getBits(41, 50);
sprintf(additionalData, ResourceManager::get_beacon_resource(LONG_ADDITIONAL_DATA_RESOURCE_START + 2), csTaNumber);
setSerialNumber(getBits(51, 64));
} break;
case 0b0101: {
hasAdditionalData = true;
hasSerialNumber = true;
uint32_t data = getBits(41, 45);
char char1 = BAUDOT_CODE[data + 32];
data = getBits(46, 50);
char char2 = BAUDOT_CODE[data + 32];
data = getBits(51, 55);
char char3 = BAUDOT_CODE[data + 32];
sprintf(additionalData, ResourceManager::get_beacon_resource(LONG_ADDITIONAL_DATA_RESOURCE_START + 3), char1, char2, char3);
setSerialNumber(getBits(56, 64));
} break;
case 0b1000:
case 0b1010:
case 0b1011:
case 0b1111: {
hasSerialNumber = true;
hasAdditionalData = true;
setSerialNumber(getBits(41, 58));
uint32_t natNum = getBits(127, 132);
sprintf(additionalData, ResourceManager::get_beacon_resource(LONG_ADDITIONAL_DATA_RESOURCE_START + 4), natNum);
} break;
}
}
}
/**
* Parse this beacon's locating device
*/
void parseLocatingDevices() {
bool mainLoc;
if (protocolIsStandard() || protocolIsNational()) {
mainLoc = getBits(111, 111);
mainLocatingDevice = mainLoc ? MainLocatingDevice::INTERNAL_NAV : MainLocatingDevice::EXTERNAL_NAV;
} else if (protocolIsUser() || protocolIsRls()) {
mainLoc = getBits(107, 107);
mainLocatingDevice = mainLoc ? MainLocatingDevice::INTERNAL_NAV : MainLocatingDevice::EXTERNAL_NAV;
}
if (protocolIsStandard() || protocolIsNational()) {
auxLocatingDevice = getBits(112, 112) ? AuxLocatingDevice::MHZ121_5 : AuxLocatingDevice::NONE_OR_OTHER;
} else if (protocolIsRls()) {
auxLocatingDevice = getBits(108, 108) ? AuxLocatingDevice::MHZ121_5 : AuxLocatingDevice::NONE_OR_OTHER;
} else if (protocolIsUser() && protocolCode != 0b100) {
uint8_t aux = getBits(84, 85);
if (aux == 0b00)
auxLocatingDevice = AuxLocatingDevice::NONE;
else if (aux == 0b01)
auxLocatingDevice = AuxLocatingDevice::MHZ121_5;
else if (aux == 0b10)
auxLocatingDevice = AuxLocatingDevice::SART;
else
auxLocatingDevice = AuxLocatingDevice::OTHER;
}
}
/**
* Parse this beacon's frame data
*/
void parseFrame() {
long latofmin, latofsec, lonofmin, lonofsec;
bool latoffset, lonoffset;
longFrame = getBits(25, 25);
parseProtocol();
protocolType = getProtocolType();
CountryManager::get_country(getBits(27, 36), country);
if (frame[2] == 0xD0)
frameMode = FrameMode::SELF_TEST;
else if (frame[2] == 0x2F)
frameMode = FrameMode::NORMAL;
else
frameMode = FrameMode::UNKNOWN;
if (longFrame) {
if (protocolIsUser() && !isOrbito()) {
location.latitude.orientation = (frame[13] & 0x10) >> 4;
location.latitude.degrees = ((frame[13] & 0x0F) << 3 | (frame[14] & 0xE0) >> 5);
location.latitude.minutes = ((frame[14] & 0x1E) >> 1) * 4;
location.longitude.orientation = (frame[14] & 0x01);
location.longitude.degrees = (frame[15]);
location.longitude.minutes = ((frame[16] & 0xF0) >> 4) * 4;
} else if (protocolIsNational()) {
latoffset = (frame[14] & 0x80) >> 7;
location.latitude.orientation = (frame[7] & 0x20) >> 5;
location.latitude.degrees = ((frame[7] & 0x1F) << 2 | (frame[8] & 0xC0) >> 6);
location.latitude.minutes = ((frame[8] & 0x3E) >> 1) * 2;
latofmin = (frame[14] & 0x60) >> 5;
latofsec = ((frame[14] & 0x1E) >> 1) * 4;
location.latitude.apply_offset(latoffset, latofmin, latofsec);
lonoffset = (frame[14] & 0x01);
location.longitude.orientation = (frame[8] & 0x01);
location.longitude.degrees = (frame[9]);
location.longitude.minutes = ((frame[10] & 0xF8) >> 3) * 2;
lonofmin = (frame[15] & 0xC0) >> 6;
lonofsec = ((frame[15] & 0x3C) >> 2) * 4;
location.longitude.apply_offset(lonoffset, lonofmin, lonofsec);
} else if (protocolIsStandard()) {
latoffset = (frame[14] & 0x80) >> 7;
location.latitude.orientation = (frame[8] & 0x80) >> 7;
location.latitude.degrees = (frame[8] & 0x7F);
location.latitude.minutes = ((frame[9] & 0xC0) >> 6) * 15;
latofmin = (frame[14] & 0x7C) >> 2;
latofsec = ((frame[14] & 0x03) << 2 | (frame[15] & 0xC0) >> 6) * 4;
location.latitude.apply_offset(latoffset, latofmin, latofsec);
lonoffset = (frame[15] & 0x20) >> 5;
location.longitude.orientation = (frame[9] & 0x20) >> 5;
location.longitude.degrees = ((frame[9] & 0x1F) << 3 | (frame[10] & 0xE0) >> 5);
location.longitude.minutes = ((frame[10] & 0x18) >> 3) * 15;
lonofmin = (frame[15] & 0x1F);
lonofsec = ((frame[16] & 0xF0) >> 4) * 4;
location.longitude.apply_offset(lonoffset, lonofmin, lonofsec);
} else if (protocolIsRlsOrElt()) {
latoffset = (frame[14] & 0x20) >> 5;
location.latitude.orientation = (frame[8] & 0x20) >> 5;
location.latitude.degrees = ((frame[8] & 0x1F) << 2) | ((frame[9] & 0xC0) >> 6);
location.latitude.minutes = ((frame[9] & 0x20) >> 5) * 30;
latofmin = (frame[14] & 0x1E) >> 1;
latofsec = ((frame[14] & 0x01) << 3 | (frame[15] & 0xE0) >> 5) * 4;
location.latitude.apply_offset(latoffset, latofmin, latofsec);
lonoffset = (frame[15] & 0x10) >> 4;
location.longitude.orientation = (frame[9] & 0x10) >> 4;
location.longitude.degrees = ((frame[9] & 0x0F) << 4 | (frame[10] & 0xF0) >> 4);
location.longitude.minutes = ((frame[10] & 0x08) >> 3) * 30;
lonofmin = (frame[15] & 0x0F);
lonofsec = ((frame[16] & 0xF0) >> 4) * 4;
location.longitude.apply_offset(lonoffset, lonofmin, lonofsec);
}
}
parseAdditionalData();
parseLocatingDevices();
identifier = getBits(26, 85);
uint32_t hexIdHigh = (uint32_t)(identifier >> 32);
uint32_t hexIdLow = (uint32_t)identifier;
if (protocolIsStandard()) {
// default value of bits 65 to 74 = 0 111111111 / default value of bits 75 to 85 = 0 1111111111
hexIdLow &= 0b11111111'11100000'00000000'00000000;
hexIdLow |= 0b00000000'00001111'11111011'11111111;
} else if (protocolIsNational()) {
// default value of bits 59 to 71 = 0 1111111 00000 / default value of bits 72 to 85 = 0 11111111 00000
hexIdLow &= 0b11111000'00000000'00000000'00000000;
hexIdLow |= 0b00000011'11111000'00011111'11100000;
} else if (protocolIsRlsOrElt()) {
// default value of bits 67 to 75 = 0 11111111 / default value of bits 76 to 85 = 0 111111111
hexIdLow &= 0b11111111'11111000'00000000'00000000;
hexIdLow |= 0b00000000'00000011'11111101'11111111;
}
std::sprintf(hexId, "%07lX%08lX", hexIdHigh, hexIdLow);
if (isOrbito() && !longFrame) {
isEmpty = true;
for (size_t i = 3; i < BEACON_DATA_SIZE; i++) {
if (frame[i] != 0) {
isEmpty = false;
break;
}
}
} else
isEmpty = false;
bch1 = getBits(86, 106);
computedBch1 = computeBCH1();
// Try and correct single bit error on BCH1 (bits 25-85)
if (computedBch1 != bch1) {
simpleCorrection(true);
}
hasBch2 = longFrame && !isOrbito();
if (hasBch2) {
bch2 = getBits(133, 144);
computedBch2 = computeBCH2();
// Try and correct single bit error on BCH2 (bits 107 - 132)
if (computedBch2 != bch2) {
simpleCorrection(false);
}
}
static bool isCorrecting = false;
// If BCH1 or BCH2 has been corrected, we need to parse frame again to update beacon properties
if (!isCorrecting && (bch1Corrected || bch2Corrected)) {
// Prevent recursion
isCorrecting = true;
parseFrame();
isCorrecting = false;
}
}
};
} // namespace ui::external_app::epirb_rx
#endif // __BEACON_RX_H__
+95
View File
@@ -0,0 +1,95 @@
/*
* 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 "beacon_db.hpp"
namespace ui::external_app::epirb_rx {
/**
* Add a beacon to the database.
* @return the added beacon reference.
*/
Beacon& BeaconDB::add_beacon() {
Beacon& result = recent_beacons[recent_beacon_pos];
recent_beacon_pos = (recent_beacon_pos + 1) % BEACON_HISTORY_SIZE;
if (recent_beacon_pos == 0) recent_beacon_full = true;
return result;
}
/**
* Get the size of the beacon database
* @return the size of the beacon database
*/
size_t BeaconDB::size() {
return recent_beacon_full ? BEACON_HISTORY_SIZE : recent_beacon_pos;
}
/**
* Returns true if the database is empty
* @return true if the database is empty
*/
bool BeaconDB::empty() {
return (!recent_beacon_full && (recent_beacon_pos == 0));
}
/**
* Returns the beacon at the provided index
*/
Beacon& BeaconDB::get_beacon(size_t index) {
// Start from last beacon et go backward
int16_t pos = (int16_t)recent_beacon_pos - 1 - index;
while (pos < 0) pos += BEACON_HISTORY_SIZE;
return recent_beacons[pos % BEACON_HISTORY_SIZE];
}
/**
* Set currently selected beacon
* @param index the selected index
*/
void BeaconDB::set_current_beacon(size_t index) {
current_beacon_index = index;
}
/**
* Get currently selected index
* @return the currently selected index
*/
size_t BeaconDB::get_current_beacon_index() {
return current_beacon_index;
}
/**
* Get the currently selected beacon
* @return the currently selected beacon
*/
Beacon& BeaconDB::get_current_beacon() {
return get_beacon(current_beacon_index);
}
/**
* Clear beacon database
*/
void BeaconDB::clear() {
recent_beacon_pos = 0;
recent_beacon_full = false;
}
} // namespace ui::external_app::epirb_rx
+56
View File
@@ -0,0 +1,56 @@
/*
* 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_DB_H__
#define __BEACON_DB_H__
#include "beacon.hpp"
namespace ui::external_app::epirb_rx {
// Size of beacon database calculated to match the beacon list component size
#define BEACON_HISTORY_SIZE 13
class BeaconDB {
public:
Beacon& add_beacon();
Beacon& get_beacon(size_t index);
Beacon& get_current_beacon();
size_t get_current_beacon_index();
void set_current_beacon(size_t index);
size_t size();
bool empty();
void clear();
private:
// Actual beacon database array
Beacon recent_beacons[BEACON_HISTORY_SIZE];
// Position of the insertion pointer
int8_t recent_beacon_pos{0};
// Beacon selection handling
size_t current_beacon_index{0};
// True when database is full
bool recent_beacon_full{false};
};
} // namespace ui::external_app::epirb_rx
#endif /* __BEACON_DB_H__ */
+122
View File
@@ -0,0 +1,122 @@
/*
* 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 __COUNTRY_RX_H__
#define __COUNTRY_RX_H__
#include "string_format.hpp"
#include "file_reader.hpp"
#include "file_path.hpp"
namespace ui::external_app::epirb_rx {
#define DISABLE_COUNTRY_CACHE
// Country struct
struct Country {
int16_t code;
char alphaCode[4]; // 3 chars + null
char shortName[11]; // 10 chars + null
};
class CountryManager {
public:
static void get_country(int code, Country& out) {
#ifndef DISABLE_COUNTRY_CACHE
// Check cache
for (int i = 0; i < cache_count; i++) {
if (cache[i].code == code) {
out = cache[i];
if (i > 0) { // Promotion
Country tmp = cache[i];
for (int j = i; j > 0; j--) cache[j] = cache[j - 1];
cache[0] = tmp;
}
return;
}
}
#endif
// Cache miss => load from file
if (load_from_file(code, out)) {
#ifndef DISABLE_COUNTRY_CACHE
// Insert in cache
int limit = (cache_count < 16) ? cache_count : 15;
for (int j = limit; j > 0; j--) cache[j] = cache[j - 1];
cache[0] = out;
if (cache_count < 16) cache_count++;
#endif
} else {
out.code = code;
out.alphaCode[0] = '\0';
out.shortName[0] = '\0';
}
}
private:
#ifndef DISABLE_COUNTRY_CACHE
static Country cache[16];
static int cache_count;
#endif
static bool load_from_file(int target_code, Country& out) {
FIL file;
if (f_open(&file, (epirb_dir / u"RES/COUNTRY.RES").tchar(), FA_READ) != FR_OK) return false;
TCHAR line[48];
bool found = false;
while (f_gets(line, sizeof(line) / sizeof(TCHAR), &file)) {
// Light parsing method
TCHAR* p = line;
// Parse CODE
int c = 0;
while (*p >= '0' && *p <= '9') c = c * 10 + (*p++ - '0');
if (c != target_code || *p != ':') continue;
p++; // Skip ':'
out.code = (int16_t)c;
// Parse ALPHA CODE (3 chars)
for (int i = 0; i < 3 && *p != ':'; i++) out.alphaCode[i] = (char)*p++;
out.alphaCode[3] = '\0';
if (*p != ':') continue;
p++; // Skip ':'
// Parse SHORT NAME
int i = 0;
while (i < static_cast<int>(sizeof(out.shortName) - 1) && *p != '\r' && *p != '\n' && *p != '\0') {
out.shortName[i++] = (char)*p++;
}
out.shortName[i] = '\0';
found = true;
break;
}
f_close(&file);
return found;
}
};
} // namespace ui::external_app::epirb_rx
#endif // __COUNTRY_RX_H__
+149
View File
@@ -0,0 +1,149 @@
/*
* 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 "location.hpp"
#include <cstdio>
#include "string_format.hpp"
namespace ui::external_app::epirb_rx {
void Location::Angle::clear() {
degrees = 255;
minutes = 0;
seconds = 0;
orientation = false;
floatValue = 255;
}
float Location::Angle::getFloatValue() {
if (floatValue >= 255) {
floatValue = (float)degrees;
floatValue += ((float)minutes / 60.0f);
floatValue += ((float)seconds / 3600.0f);
if (orientation) {
floatValue = -floatValue;
}
}
return floatValue;
}
void Location::Angle::apply_offset(bool positive, long ofmin, long ofsec) {
if (positive) {
minutes += ofmin;
seconds += ofsec;
} else {
minutes -= ofmin;
if (minutes < 0) {
minutes += 60;
degrees -= 1;
}
seconds -= ofsec;
if (seconds < 0) {
seconds += 60;
minutes -= 1;
}
}
}
void Location::clear() {
latitude.clear();
longitude.clear();
}
bool Location::isUnknown() {
return (latitude.degrees >= 255 || longitude.degrees >= 255);
}
char Location::gps_letterize(int x) {
return (char)x + 65;
}
size_t Location::gps_compute_locator(char* buffer, float lat, float lon, int precision) {
size_t result = 0;
lon += 180.0f;
lat += 90.0f;
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.0f / 60.0f);
int F = lat / (2.5f / 60.0f);
lon -= E * (5.0f / 60.0f);
lat -= F * (2.5f / 60.0f);
int G = lon / (5.0f / 600.0f);
int H = lat / (2.5f / 600.0f);
buffer[result++] = char('A' + A);
buffer[result++] = char('A' + B);
if (precision >= 4) {
buffer[result++] = char('0' + C);
buffer[result++] = char('0' + D);
}
if (precision >= 6) {
buffer[result++] = char('a' + E);
buffer[result++] = char('a' + F);
}
if (precision >= 8) {
buffer[result++] = char('0' + G);
buffer[result++] = char('0' + H);
}
buffer[result] = 0;
return result;
}
size_t Location::toString(char* buffer, LocationFormat format, int precision) {
if (isUnknown()) {
return sprintf(buffer, "%s", "GPS not synchronized");
}
switch (format) {
case LocationFormat::SEXAGESIMAL: {
return sprintf(buffer, "%ld\xB0%02ld'%02ld\"%c, %ld\xB0%02ld'%02ld\"%c", // 0xB0 is degree ° symbol in our 8x16 font
latitude.degrees, latitude.minutes, latitude.seconds, latitude.orientation ? 'S' : 'N',
longitude.degrees, longitude.minutes, longitude.seconds, longitude.orientation ? 'W' : 'E');
}
case LocationFormat::MAIDENHEAD_LOCATOR:
return gps_compute_locator(buffer, latitude.getFloatValue(), longitude.getFloatValue(), precision);
default:
case LocationFormat::DECIMAL:
return sprintf(buffer, "%s, %s", to_string_decimal(latitude.getFloatValue(), 5).c_str(), to_string_decimal(longitude.getFloatValue(), 5).c_str());
}
}
void Location::formatFloatLocation(char* buffer, const char* format) {
sprintf(buffer, format, to_string_decimal(latitude.getFloatValue(), 6).c_str(), to_string_decimal(longitude.getFloatValue(), 6).c_str());
}
} // namespace ui::external_app::epirb_rx
+76
View File
@@ -0,0 +1,76 @@
/*
* 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_RX_H__
#define __LOCATION_RX_H__
#include <cstddef>
namespace ui::external_app::epirb_rx {
class Location {
public:
class Angle {
public:
long degrees = 0;
long minutes = 0;
long seconds = 0;
bool orientation = false; // false = N/E, true = S/W
Angle() {}
Angle(long degrees)
: degrees(degrees) {}
void clear();
float getFloatValue();
// Apply an offset (minutes / seconds) with sign-based borrow into minutes/degrees.
// Out-of-line to keep parseFrame from inlining the same arithmetic 6 times.
void apply_offset(bool positive, long ofmin, long ofsec);
private:
float floatValue = 255.0f;
};
enum class LocationFormat { DECIMAL,
SEXAGESIMAL,
MAIDENHEAD_LOCATOR };
Angle latitude = Angle(127);
Angle longitude = Angle(255);
void clear();
bool isUnknown();
static char gps_letterize(int x);
// For code size optimization reasons, string manipulation is performed with char buffers
// with no char buffer size check. Caller methods take care of providing large enough buffers
static size_t gps_compute_locator(char* buffer, float lat, float lon, int precision = 6);
size_t toString(char* buffer, LocationFormat format, int precision = 6);
void formatFloatLocation(char* buffer, const char* format);
};
} // namespace ui::external_app::epirb_rx
#endif // __LOCATION_RX_H__
+1 -1
View File
@@ -73,7 +73,7 @@ __attribute__((section(".external_app.app_epirb_rx.application_information"), us
0x00,
0x00,
},
/*.icon_color = */ ui::Color::orange().v,
/*.icon_color = */ ui::Color::green().v,
/*.menu_location = */ app_location_t::RX,
/*.desired_menu_position = */ -1,
+100
View File
@@ -0,0 +1,100 @@
/*
* 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 __RESOUCES_H__
#define __RESOUCES_H__
#include <cstring>
#include <vector>
#include "file_reader.hpp"
#include "file_path.hpp"
namespace ui::external_app::epirb_rx {
#define UNKNOWN_LABEL "Unk."
extern std::vector<std::string> beacon_res;
extern std::vector<std::string> freq;
/**
* This class is used to load string resources from SD card.
* This is done mostly to reduce application size and keep it under the 32kb limit.
* It's also used to load frequency values, allowing users to customize frequency list by editing FREQ.TXT file on SD card.
*/
class ResourceManager {
public:
// Singleton getter
static ResourceManager* getInstance();
// Singleton instance
static ResourceManager* current;
// Used from standalone app, to prevent memleak
static void destroy();
// Get a string from BEACON.RES file
static const char* get_beacon_resource(uint8_t line) {
ResourceManager* rm = getInstance();
if (rm->beacon_res.empty()) {
load_file((epirb_dir / u"RES/BEACON.RES"), rm->beacon_res);
}
if (line < rm->beacon_res.size()) {
return rm->beacon_res[line].c_str();
}
return UNKNOWN_LABEL;
}
// Get frequency values
static const std::vector<std::string>& get_frequencies() {
ResourceManager* rm = getInstance();
if (rm->freq.empty()) {
load_file((epirb_dir / u"FREQ.TXT"), rm->freq);
}
return rm->freq;
}
private:
// BEACON.RES content
std::vector<std::string> beacon_res{};
// FREQ.TXT content
std::vector<std::string> freq{};
static void load_file(const std::filesystem::path& path, std::vector<std::string>& vect) {
FIL file;
if (f_open(&file, path.tchar(), FA_READ) == FR_OK) {
TCHAR line_buffer[32];
// Read the file line by line
while (f_gets(line_buffer, sizeof(line_buffer) / sizeof(TCHAR), &file)) {
std::string s;
for (int i = 0; line_buffer[i] != '\0' && line_buffer[i] != '\n' && line_buffer[i] != '\r'; i++) {
// Convert each TCHAR in char
s += (char)line_buffer[i];
}
vect.push_back(std::move(s));
}
f_close(&file);
}
if (vect.empty()) vect.push_back(UNKNOWN_LABEL);
}
};
} // namespace ui::external_app::epirb_rx
#endif // __RESOUCES_H__
+143
View File
@@ -0,0 +1,143 @@
/*
* 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_beaconlist.hpp"
#include <algorithm>
namespace ui::external_app::epirb_rx {
BeaconUIList::BeaconUIList(Rect parent_rect)
: View{parent_rect} {
this->set_focusable(true);
}
void BeaconUIList::paint(Painter& painter) {
auto rect = screen_rect();
// Clear view
painter.fill_rectangle(rect, Theme::getInstance()->bg_darkest->background);
if (!db_ || (db_->empty())) {
return;
}
auto base_style = Theme::getInstance()->bg_darkest;
for (auto offset = 0u; offset < BEACON_HISTORY_SIZE; ++offset) {
// The whole frame needs to be cleared so every line 'slot'
// is redrawn even when `text` just left empty.
auto text = std::string{};
auto index = start_index_ + offset;
auto line_position = rect.location() + Point{0, 1 + (int)offset * char_height};
auto is_selected = offset == selected_index_;
auto style = base_style;
if (index < db_->size()) {
// Get beacon entry and format it's summary
auto& entry = db_->get_beacon(index);
char buffer[64];
entry.formatSummary(buffer, true);
text = std::string(buffer);
}
if (index == db_->get_current_beacon_index())
// If this is the currently displayed beacon change color
style = Theme::getInstance()->bg_medium;
// Draw entry line
painter.draw_string(
line_position, (is_selected ? style->invert() : *style), text);
}
// Draw a bounding rectangle when focused.
painter.draw_rectangle(rect, (has_focus() ? Theme::getInstance()->bg_darkest->foreground : Theme::getInstance()->bg_darkest->background));
}
void BeaconUIList::on_show() {
set_dirty();
}
bool BeaconUIList::on_key(const KeyEvent key) {
if (!db_ || db_->empty())
return false;
if (key == KeyEvent::Select && on_select) {
// Select this beacon
on_select(get_index());
set_dirty();
return true;
}
auto delta = 0;
// Change position in the list
if (key == KeyEvent::Up && get_index() > 0)
delta = -1;
else if (key == KeyEvent::Down && get_index() < db_->size() - 1)
delta = 1;
else
return false;
adjust_selected_index(delta);
set_dirty();
return true;
}
bool BeaconUIList::on_encoder(EncoderEvent delta) {
if (!db_ || db_->empty())
return false;
// Change position in the list according to encoder
adjust_selected_index(delta);
set_dirty();
return true;
}
size_t BeaconUIList::get_index() const {
return start_index_ + selected_index_;
}
void BeaconUIList::set_db(BeaconDB& db) {
db_ = &db;
start_index_ = 0;
selected_index_ = 0;
set_dirty();
}
void BeaconUIList::adjust_selected_index(int delta) {
int32_t new_index = selected_index_ + delta;
// The selection went off the top of the screen, move up.
if (new_index < 0) {
start_index_ = std::max<int32_t>(start_index_ + new_index, 0);
selected_index_ = 0;
}
// Selection is off the bottom of the screen, move down.
else if (new_index >= (int32_t)BEACON_HISTORY_SIZE) {
start_index_ = std::min<int32_t>(start_index_ + delta, db_->size() - BEACON_HISTORY_SIZE);
selected_index_ = BEACON_HISTORY_SIZE - 1;
}
// Otherwise, scroll within the screen, but not past the end.
else {
selected_index_ = std::min<int32_t>(new_index, db_->size() - 1);
}
}
} // namespace ui::external_app::epirb_rx
@@ -0,0 +1,61 @@
/*
* 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 __UI_BEACONLIST_H__
#define __UI_BEACONLIST_H__
#include "ui.hpp"
#include "ui_painter.hpp"
#include "ui_widget.hpp"
#include "beacon_db.hpp"
namespace ui::external_app::epirb_rx {
/**
* Beacon list component
*/
class BeaconUIList : public View {
public:
std::function<void(size_t)> on_select{};
BeaconUIList(Rect parent_rect);
BeaconUIList(const BeaconUIList& other) = delete;
BeaconUIList& operator=(const BeaconUIList& other) = delete;
void paint(Painter& painter) override;
void on_show() override;
bool on_key(const KeyEvent key) override;
bool on_encoder(EncoderEvent delta) override;
size_t get_index() const;
void set_db(BeaconDB& db);
private:
void adjust_selected_index(int index);
BeaconDB* db_{nullptr};
size_t start_index_{0};
size_t selected_index_{0};
};
} // namespace ui::external_app::epirb_rx
#endif /*__UI_BEACONLIST_H__*/
File diff suppressed because it is too large Load Diff
+323 -296
View File
@@ -1,297 +1,324 @@
/*
* Copyright (C) 2024 EPIRB Decoder Implementation
*
* 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 __UI_EPIRB_RX_H__
#define __UI_EPIRB_RX_H__
#include "app_settings.hpp"
#include "radio_state.hpp"
#include "ui_widget.hpp"
#include "ui_navigation.hpp"
#include "ui_receiver.hpp"
#include "ui_geomap.hpp"
#include "event_m0.hpp"
#include "signal.hpp"
#include "message.hpp"
#include "log_file.hpp"
#include "baseband_packet.hpp"
/* #include <cstdint>
#include <cstddef>
#include <string>
#include <array> */
namespace ui::external_app::epirb_rx {
// EPIRB 406 MHz beacon types
enum class BeaconType : uint8_t {
OrbitingLocationBeacon = 0,
PersonalLocatorBeacon = 1,
EmergencyLocatorTransmitter = 2,
SerialELT = 3,
NationalELT = 4,
Other = 15
};
// EPIRB distress and emergency types
enum class EmergencyType : uint8_t {
Fire = 0,
Flooding = 1,
Collision = 2,
Grounding = 3,
Sinking = 4,
Disabled = 5,
Abandoning = 6,
Piracy = 7,
Man_Overboard = 8,
Other = 15
};
struct EPIRBLocation {
float latitude; // degrees, -90 to +90
float longitude; // degrees, -180 to +180
bool valid;
EPIRBLocation()
: latitude(0.0f), longitude(0.0f), valid(false) {}
EPIRBLocation(float lat, float lon)
: latitude(lat), longitude(lon), valid(true) {}
};
enum class PacketStatus : uint8_t {
Valid = 0,
Corrected = 1,
Error = 2
};
struct EPIRBBeacon {
uint32_t beacon_id;
BeaconType beacon_type;
EmergencyType emergency_type;
EPIRBLocation location;
uint32_t country_code;
std::string vessel_name;
rtc::RTC timestamp;
uint32_t sequence_number;
PacketStatus packet_status;
uint8_t error_count;
EPIRBBeacon()
: beacon_id(0), beacon_type(BeaconType::Other), emergency_type(EmergencyType::Other), location(), country_code(0), vessel_name(), timestamp(), sequence_number(0), packet_status(PacketStatus::Error), error_count(0) {}
};
class EPIRBDecoder {
public:
static EPIRBBeacon decode_packet(const baseband::Packet& packet);
private:
static EPIRBLocation decode_location(const std::array<uint8_t, 16>& data);
static BeaconType decode_beacon_type(uint8_t type_bits);
static EmergencyType decode_emergency_type(uint8_t emergency_bits);
static uint32_t decode_country_code(const std::array<uint8_t, 16>& data);
static std::string decode_vessel_name(const std::array<uint8_t, 16>& data);
// BCH error correction methods
static PacketStatus perform_bch_check(std::array<uint8_t, 16>& data, uint8_t& error_count);
static uint32_t calculate_bch_syndrome(const std::array<uint8_t, 16>& data);
static bool correct_single_error(std::array<uint8_t, 16>& data, uint32_t syndrome);
static uint8_t count_bit_errors(const std::array<uint8_t, 16>& original, const std::array<uint8_t, 16>& corrected);
};
class EPIRBLogger {
public:
Optional<File::Error> append(const std::filesystem::path& filename) {
return log_file.append(filename);
}
void on_packet(const EPIRBBeacon& beacon);
private:
LogFile log_file{};
};
// Forward declarations of formatting functions
std::string format_beacon_type(BeaconType type);
std::string format_emergency_type(EmergencyType type);
std::string format_packet_status(PacketStatus status);
ui::Color get_packet_status_color(PacketStatus status);
class EPIRBBeaconDetailView : public ui::View {
public:
std::function<void(void)> on_close{};
EPIRBBeaconDetailView(ui::NavigationView& nav);
EPIRBBeaconDetailView(const EPIRBBeaconDetailView&) = delete;
EPIRBBeaconDetailView& operator=(const EPIRBBeaconDetailView&) = delete;
void set_beacon(const EPIRBBeacon& beacon);
const EPIRBBeacon& beacon() const { return beacon_; }
void focus() override;
void paint(ui::Painter&) override;
ui::GeoMapView* get_geomap_view() { return geomap_view; }
private:
EPIRBBeacon beacon_{};
ui::Button button_done{
{125, 224, 96, 24},
"Done"};
ui::Button button_see_map{
{19, 224, 96, 24},
"See on map"};
ui::GeoMapView* geomap_view{nullptr};
ui::Rect draw_field(
ui::Painter& painter,
const ui::Rect& draw_rect,
const ui::Style& style,
const std::string& label,
const std::string& value);
};
class EPIRBAppView : public ui::View {
public:
EPIRBAppView(ui::NavigationView& nav);
~EPIRBAppView();
void set_parent_rect(const ui::Rect new_parent_rect) override;
void paint(ui::Painter&) override;
void focus() override;
std::string title() const override { return "EPIRB RX"; }
private:
app_settings::SettingsManager settings_{
"rx_epirb", app_settings::Mode::RX};
ui::NavigationView& nav_;
std::vector<EPIRBBeacon> recent_beacons{};
std::unique_ptr<EPIRBLogger> logger{};
EPIRBBeaconDetailView beacon_detail_view{nav_};
static constexpr auto header_height = 4 * 16;
ui::Text label_frequency{
{UI_POS_X(0), UI_POS_Y(0), 4 * 8, 1 * 16},
"Freq"};
ui::OptionsField options_frequency{
{5 * 8, UI_POS_Y(0)},
7,
{
{"406.028", 406028000},
{"406.025", 406025000},
{"406.037", 406037000},
{"433.025", 433025000},
{"144.875", 144875000},
}};
ui::RFAmpField field_rf_amp{
{13 * 8, UI_POS_Y(0)}};
ui::LNAGainField field_lna{
{15 * 8, UI_POS_Y(0)}};
ui::VGAGainField field_vga{
{18 * 8, UI_POS_Y(0)}};
ui::RSSI rssi{
{UI_POS_X(21), 0, UI_POS_WIDTH_REMAINING(24), 4}};
ui::Channel channel{
{UI_POS_X(21), 5, UI_POS_WIDTH_REMAINING(24), 4}};
ui::AudioVolumeField field_volume{
{screen_width - 2 * 8, UI_POS_Y(0)}};
// Status display
ui::Text label_status{
{UI_POS_X(0), 1 * 16, 15 * 8, 1 * 16},
"Listening..."};
ui::Text label_beacons_count{
{16 * 8, 1 * 16, 14 * 8, 1 * 16},
"Beacons: 0"};
ui::Text label_packet_stats{
{UI_POS_X(0), 3 * 16, 29 * 8, 1 * 16},
""};
// Latest beacon info display
ui::Text label_latest{
{UI_POS_X(0), 2 * 16, 8 * 8, 1 * 16},
"Latest:"};
ui::Text text_latest_info{
{8 * 8, 2 * 16, 22 * 8, 1 * 16},
""};
// Beacon list
ui::Console console{
{0, 4 * 16, 240, 152}};
ui::Button button_map{
{0, 224, 60, 24},
"Map"};
ui::Button button_clear{
{64, 224, 60, 24},
"Clear"};
ui::Button button_log{
{128, 224, 60, 24},
"Log"};
SignalToken signal_token_tick_second{};
uint32_t beacons_received = 0;
uint32_t packets_valid = 0;
uint32_t packets_corrected = 0;
uint32_t packets_error = 0;
MessageHandlerRegistration message_handler_packet{
Message::ID::EPIRBPacket,
[this](Message* const p) {
const auto message = static_cast<const EPIRBPacketMessage*>(p);
this->on_packet(message->packet);
}};
void on_packet(const baseband::Packet& packet);
void on_beacon_decoded(const EPIRBBeacon& beacon);
void on_show_map();
void on_clear_beacons();
void on_toggle_log();
void on_tick_second();
void update_display();
std::string format_beacon_summary(const EPIRBBeacon& beacon);
std::string format_location(const EPIRBLocation& location);
};
} // namespace ui::external_app::epirb_rx
/*
* Copyright (C) 2024 EPIRB Decoder Implementation
* 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 __UI_EPIRB_RX_H__
#define __UI_EPIRB_RX_H__
#include "app_settings.hpp"
#include "radio_state.hpp"
#include "ui_widget.hpp"
#include "ui_navigation.hpp"
#include "ui_receiver.hpp"
#include "ui_geomap.hpp"
// Specan is disable to keep application size below the 32k limit
// #define SPECAN
// Comment to disable timout reset on select and save approx 200 bytes of flash
#ifndef PRALINE
// Application does not fit on Praline with RESET_TIMER enabled
#define RESET_TIMER
#endif
// Comment to disable squelch control
#define SQUELCH
// Comment to disable beacon selection by encoder on detail tab
#define DETAIL_TAB_BEACON_SEL
// #define LOGGER
#ifdef SPECAN
#include "ui_spectrum.hpp"
#endif
#include "ui_tabview.hpp"
#include "ui_qrcode.hpp"
#include "event_m0.hpp"
#include "message.hpp"
#include "log_file.hpp"
#include "baseband_packet.hpp"
#include "audio.hpp"
#include "beacon.hpp"
#include "beacon_db.hpp"
#include "ui_beaconlist.hpp"
#include "resources.hpp"
namespace ui::external_app::epirb_rx {
/**
* Status of a packet
*/
enum class PacketStatus : uint8_t {
Valid = 0,
Corrected = 1,
Error = 2
};
// Position of tabs in tab view
#define EPIRB_TAB_POS_Y (UI_POS_Y(4) + 3 * 8)
// Height of tabs in tab view
#define EPIRB_TAB_HEIGHT (screen_height - EPIRB_TAB_POS_Y - UI_POS_HEIGHT(1))
#ifdef LOGGER
class EPIRBLogger {
public:
Optional<File::Error> append(const std::filesystem::path& filename) {
return log_file.append(filename);
}
void on_packet(Beacon& beacon);
private:
LogFile log_file{};
};
#endif
/**
* Dedicated TextArea component used to optimize application code size
*/
class TextArea : public Widget {
public:
TextArea(Rect parent_rect);
#ifdef RESET_TIMER
std::function<void(TextArea&)> on_select{};
bool on_key(const KeyEvent key) override;
#endif
void set_content(std::string_view value);
void paint(Painter& painter) override;
private:
std::string content{};
};
// Forward declaration
class EPIRBAppView;
/**
* View for beacon detail tab
*/
class EPIRBDetailView : public View {
public:
EPIRBDetailView(Rect parent_rect, EPIRBAppView& parent);
void set_beacon(Beacon& beacon);
#ifdef DETAIL_TAB_BEACON_SEL
bool on_encoder(EncoderEvent delta) override;
#endif
private:
TextArea text_beacon{{UI_POS_X(0), UI_POS_Y(0), UI_POS_MAXWIDTH, EPIRB_TAB_HEIGHT}};
EPIRBAppView& parent_app;
};
#define EPIRB_RX_DEFAULT_LATITUDE 43.604f
#define EPIRB_RX_DEFAULT_LONGITUDE 1.458f
/**
* View for beacon map tab
*/
class EPIRBMapView : public View {
public:
EPIRBMapView(Rect parent_rect);
void paint(Painter& painter) override;
void on_show() override;
void set_main_marker(const std::string& label, float lat, float lon);
void clear_markers();
void add_marker(GeoMarker& marker);
void hide_map(bool hide);
void repaint();
private:
GeoMap geomap{{0, 0, UI_POS_MAXWIDTH, EPIRB_TAB_HEIGHT}};
float lat_{EPIRB_RX_DEFAULT_LATITUDE};
float lon_{EPIRB_RX_DEFAULT_LONGITUDE};
bool map_hidden{true};
};
#define QR_WIDTH 126
#define QR_HEIGHT 127
/**
* Vieaw for Beacon QR tab
*/
class EPRIBQRView : public View {
public:
EPRIBQRView(Rect parent_rect);
EPRIBQRView(const EPRIBQRView&) = delete;
EPRIBQRView& operator=(const EPRIBQRView&) = delete;
void set_beacon(Beacon* beacon);
void update_qr();
void update_display();
private:
bool show_map{true};
Beacon* current_beacon{nullptr};
char qr_url[128];
OptionsField options_qr{
{UI_POS_X(5), UI_POS_Y(1)},
6,
{{"Map", 0},
{"Detail", 1}}};
QRCodeImage qr_code{
{UI_POS_MAXWIDTH - QR_WIDTH - UI_POS_X(1), UI_POS_Y(1), QR_WIDTH, QR_HEIGHT}};
TextArea text_data{{UI_POS_X(0), UI_POS_Y(1), UI_POS_MAXWIDTH, EPIRB_TAB_HEIGHT - UI_POS_Y(1)}};
};
#ifdef SPECAN
class EPIRBRxView : public spectrum::WaterfallView {
public:
EPIRBRxView(EPIRBAppView& parent, Rect parent_rect);
void on_show() override;
void on_hide() override;
private:
EPIRBAppView& app_view;
};
#endif
class EPIRBAppView final : public ui::View {
public:
EPIRBAppView(ui::NavigationView& nav);
~EPIRBAppView();
void focus() override;
void refresh();
// Message to configure rx baseband
EPIRBRXConfig epirb_rx_config_message{};
void send_config();
// Beacons database
BeaconDB beacon_db{};
// Update display when beacon selection changed0
void on_beacon_change();
std::string title() const override { return "EPIRB RX"; }
private:
uint8_t squelch{50};
// The delay between each frame
uint32_t countdown{50};
app_settings::SettingsManager settings_{
"rx_epirb",
app_settings::Mode::RX,
{
{"epirb_squelch"sv, &squelch},
{"countdown"sv, &countdown},
}};
ui::NavigationView& nav_;
#ifdef LOGGER
std::unique_ptr<EPIRBLogger> logger{};
#endif
OptionsField options_frequency{
{UI_POS_X(0), UI_POS_Y(0)},
7,
{}};
ui::RFAmpField field_rf_amp{
{UI_POS_X(8), UI_POS_Y(0)}};
ui::LNAGainField field_lna{
{UI_POS_X(10), UI_POS_Y(0)}};
ui::VGAGainField field_vga{
{UI_POS_X(13), UI_POS_Y(0)}};
ui::RSSI rssi{
{UI_POS_X(16), UI_POS_Y(0), UI_POS_WIDTH_REMAINING(22), 4}};
ui::Channel channel{
{UI_POS_X(16), UI_POS_Y(0) + 5, UI_POS_WIDTH_REMAINING(22), 4}};
// ui::Audio audio{
// {UI_POS_X(16), UI_POS_Y(0) + 10, UI_POS_WIDTH_REMAINING(22), 4}};
ui::AudioVolumeField field_volume{
{UI_POS_WIDTH_REMAINING(2), UI_POS_Y(0)}};
#ifdef SQUELCH
NumberField field_squelch{
{UI_POS_WIDTH_REMAINING(5), UI_POS_Y(0)},
2,
{0, 99},
1,
' '};
#endif
// Status display
TextArea text_status{{UI_POS_X(0), UI_POS_Y(1), UI_POS_MAXWIDTH, UI_POS_HEIGHT(3)}};
TextArea text_timeout{
{UI_POS_X(13), UI_POS_Y(1), UI_POS_WIDTH(3), UI_POS_HEIGHT(1)}};
SignalToken signal_token_tick_second{};
// Timeout string
int16_t timeout{0};
// Tab View
Rect view_rect = {0, EPIRB_TAB_POS_Y, UI_POS_MAXWIDTH, EPIRB_TAB_HEIGHT};
BeaconUIList view_list{view_rect};
EPIRBDetailView view_detail{view_rect, (*this)};
EPIRBMapView view_map{view_rect};
#ifdef SPECAN
EPIRBRxView view_rx{*this, view_rect};
#endif
EPRIBQRView view_qr{view_rect};
TabView tab_view{
{"List", Theme::getInstance()->fg_cyan->foreground, &view_list},
{"Detail", Theme::getInstance()->fg_green->foreground, &view_detail},
{"Map", Theme::getInstance()->fg_yellow->foreground, &view_map},
#ifdef SPECAN
{"RX", Theme::getInstance()->fg_orange->foreground, &view_rx},
#endif
{"QR", Theme::getInstance()->fg_orange->foreground, &view_qr}};
uint16_t beacons_received = 0;
uint16_t packets_valid = 0;
uint16_t packets_corrected = 0;
uint16_t packets_error = 0;
MessageHandlerRegistration message_handler_packet{
Message::ID::EPIRBPacket,
[this](Message* const p) { on_packet(p); }};
static void decode_packet(const baseband::Packet& packet, Beacon& beacon);
void on_packet(Message* const p);
void update_map();
void on_tick_second();
void update_display();
};
} // namespace ui::external_app::epirb_rx
#endif // __UI_EPIRB_RX_H__
+61 -57
View File
@@ -24,7 +24,6 @@
#include "portapack.hpp"
#include "ui_epirb_tx.hpp"
#include <cmath>
#include <cctype>
#include <cstdio>
namespace ui::external_app::epirb_tx {
@@ -37,17 +36,17 @@ namespace ui::external_app::epirb_tx {
* @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) {
static void decimal_to_dms(float value, bool& negative, int16_t& deg, int8_t& min, int8_t& sec) {
negative = value < 0;
value = std::fabs(value);
deg = (uint16_t)value;
deg = (int16_t)value;
double m = (value - deg) * 60.0;
min = (uint8_t)m;
float m = (value - deg) * 60.0f;
min = (int8_t)m;
double s = (m - min) * 60.0;
sec = (uint8_t)(s + 0.5);
float s = (m - min) * 60.0f;
sec = (int8_t)(s + 0.5f);
if (sec == 60) {
sec = 0;
@@ -68,8 +67,8 @@ static void decimal_to_dms(double value, bool& negative, uint16_t& deg, uint8_t&
* @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;
static float dms_to_decimal(bool negative, int16_t deg, int8_t min, int8_t sec) {
float v = deg + min / 60.0f + sec / 3600.0f;
return negative ? -v : v;
}
@@ -80,55 +79,60 @@ static double dms_to_decimal(bool negative, uint16_t deg, uint8_t min, uint8_t s
* @param lon output longitude
*/
static void maidenhead_to_decimal(const std::string& loc, float& lat, float& lon) {
static const double lon_step[] =
static const float lon_step[] =
{
20.0,
2.0,
1.0 / 12.0,
1.0 / 120.0,
1.0 / 2880.0};
20.0f,
2.0f,
1.0f / 12.0f,
1.0f / 120.0f,
1.0f / 2880.0f};
static const double lat_step[] =
static const float lat_step[] =
{
10.0,
1.0,
1.0 / 24.0,
1.0 / 240.0,
1.0 / 5760.0};
10.0f,
1.0f,
1.0f / 24.0f,
1.0f / 240.0f,
1.0f / 5760.0f};
lon = -180.0;
lat = -90.0;
lon = -180.0f;
lat = -90.0f;
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]);
if (pairs > 0) {
for (int i = 0; i < pairs; i++) {
char c1 = loc[i * 2];
char c2 = loc[i * 2 + 1];
if (c1 >= 'a' && c1 <= 'z') c1 -= ('a' - 'A');
if (c2 >= 'a' && c2 <= 'z') c2 -= ('a' - 'A');
double lon_size = lon_step[i];
double lat_size = lat_step[i];
float lon_size = lon_step[i];
float lat_size = lat_step[i];
int v1, v2;
int v1, v2;
if (i % 2 == 0) // letters
{
v1 = c1 - 'A';
v2 = c2 - 'A';
} else // digits
{
v1 = c1 - '0';
v2 = c2 - '0';
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;
}
lon += v1 * lon_size;
lat += v2 * lat_size;
// Cell center
lon += lon_step[pairs - 1] / 2.0f;
lat += lat_step[pairs - 1] / 2.0f;
}
// Cell center
lon += lon_step[pairs - 1] / 2.0;
lat += lat_step[pairs - 1] / 2.0;
}
/**
@@ -138,9 +142,9 @@ static void maidenhead_to_decimal(const std::string& loc, float& lat, float& lon
* @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;
static std::string decimal_to_maidenhead(float lat, float lon, int precision = 8) {
lon += 180.0f;
lat += 90.0f;
int A = lon / 20;
int B = lat / 10;
@@ -154,14 +158,14 @@ static std::string decimal_to_maidenhead(double lat, double lon, int precision =
lon -= C * 2;
lat -= D * 1;
int E = lon / (5.0 / 60.0);
int F = lat / (2.5 / 60.0);
int E = lon / (5.0f / 60.0f);
int F = lat / (2.5f / 60.0f);
lon -= E * (5.0 / 60.0);
lat -= F * (2.5 / 60.0);
lon -= E * (5.0f / 60.0f);
lat -= F * (2.5f / 60.0f);
int G = lon / (5.0 / 600.0);
int H = lat / (2.5 / 600.0);
int G = lon / (5.0f / 600.0f);
int H = lat / (2.5f / 600.0f);
std::string locator;
@@ -247,9 +251,9 @@ void init_from_dms(Location& loc) {
* 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];
char buffer[20];
// 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');
sprintf(buffer, "%3d\260%02d'%02d\"%c", location.lat_deg, location.lat_min, location.lat_sec, location.south ? 'S' : 'N');
return std::string(buffer);
}
@@ -257,12 +261,12 @@ std::string to_latitude_string(const Location& location) {
* 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];
char buffer[20];
// 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');
sprintf(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__*/
#endif /*__LOCATION_H__*/
File diff suppressed because it is too large Load Diff
+417 -404
View File
@@ -1,404 +1,417 @@
/*
* 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
#define AM_TEST_FREQUENCY 121375000
#define AM_REAL_FREQUENCY 121500000
#define BPSK_FREQUENCY_HAM 433025000
#define BPSK_FREQUENCY_B 406025000
#define BPSK_FREQUENCY_C 406028000
#define BPSK_FREQUENCY_F 406037000
#define BPSK_FREQUENCY_G 406040000
#define BPSK_FREQUENCY_J 406049000
#define BPSK_FREQUENCY_K 406052000
#define BPSK_FREQUENCY_N 406061000
#define BPSK_FREQUENCY_O 406064000
namespace ui::external_app::epirb_tx {
enum class BeaconMode {
FILE = 0,
MODE_MANUAL = 1
};
enum class BeaconType {
EPIRB = 0,
ELT = 1,
PLB = 2
};
enum class BeaconProtocol {
USER = 0,
STANDARD = 1,
NATIONAL = 2
};
enum class AmChannel {
TEST = 0,
REAL = 1,
MANUAL = 2
};
enum class BpskChannel {
HAM = 0,
B = 1,
C = 2,
F = 3,
G = 4,
J = 5,
K = 6,
N = 7,
O = 8,
MANUAL = 10
};
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_bpsk_frequency();
void update_am_transmission();
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{AM_TEST_FREQUENCY};
// Frequency of the 406 MHz BPSK signal
rf::Frequency bpsk_frequency{BPSK_FREQUENCY_HAM};
// Selected am channel
uint8_t am_channel{(uint8_t)AmChannel::TEST};
// Selected bpsk channel
uint8_t bpsk_channel{(uint8_t)BpskChannel::HAM};
// Manual AM frequency value
rf::Frequency manual_am_frequency{AM_TEST_FREQUENCY};
// Manual BPSK frequency value
rf::Frequency manual_bpsk_frequency{BPSK_FREQUENCY_HAM};
// True when using a beacon from the BEACONS.TXT file
bool mode_file{false};
// 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{true};
// 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{(uint8_t)BeaconType::EPIRB};
// Currently selected beacon protocol
uint8_t beacon_protocol{(uint8_t)BeaconProtocol::USER};
// 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},
{"amchan"sv, &am_channel},
{"bpskchan"sv, &bpsk_channel},
{"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 transmitting a BPSK frame
bool transmitting_bpsk{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(0), UI_POS_Y(14)}, "AM chan.:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(15)}, "BPSK chan.:", 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", (uint8_t)BeaconType::EPIRB},
{"ELT", (uint8_t)BeaconType::ELT},
{"PLB", (uint8_t)BeaconType::PLB}}};
OptionsField options_beacon_protocol{
{UI_POS_X(9 + 7), UI_POS_Y(1)},
30,
{{"User", (uint8_t)BeaconProtocol::USER},
{"Standard", (uint8_t)BeaconProtocol::STANDARD},
{"National", (uint8_t)BeaconProtocol::NATIONAL}}};
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)", (uint8_t)BeaconMode::FILE},
{"Manual (Editor)", (uint8_t)BeaconMode::MODE_MANUAL}}};
// 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;
OptionsField options_am_channel{
{UI_POS_X(11), UI_POS_Y(14)},
20,
{{"121.375 MHz (Test)", 0},
{"121.500 MHz /!\\Real", 1},
{"Manual", 2}}};
OptionsField options_bpsk_channel{
{UI_POS_X(11), UI_POS_Y(15)},
20,
{{"433.025 MHz (Ham)", (uint8_t)BpskChannel::HAM},
{"406.025 MHz (B)", (uint8_t)BpskChannel::B},
{"406.028 MHz (C)", (uint8_t)BpskChannel::C},
{"406.037 MHz (F)", (uint8_t)BpskChannel::F},
{"406.040 MHz (G)", (uint8_t)BpskChannel::G},
{"406.049 MHz (J)", (uint8_t)BpskChannel::J},
{"406.052 MHz (K)", (uint8_t)BpskChannel::K},
{"406.061 MHz (N)", (uint8_t)BpskChannel::N},
{"406.064 MHz (O)", (uint8_t)BpskChannel::O},
{"Manual", (uint8_t)BpskChannel::MANUAL}}};
// 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__*/
/*
* 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
#define AM_TEST_FREQUENCY 121375000
#define AM_REAL_FREQUENCY 121500000
#define BPSK_FREQUENCY_HAM 433025000
#define BPSK_FREQUENCY_B 406025000
#define BPSK_FREQUENCY_C 406028000
#define BPSK_FREQUENCY_F 406037000
#define BPSK_FREQUENCY_G 406040000
#define BPSK_FREQUENCY_J 406049000
#define BPSK_FREQUENCY_K 406052000
#define BPSK_FREQUENCY_N 406061000
#define BPSK_FREQUENCY_O 406064000
namespace ui::external_app::epirb_tx {
enum class BeaconMode {
FILE = 0,
MODE_MANUAL = 1
};
enum class BeaconType {
EPIRB = 0,
ELT = 1,
PLB = 2
};
enum class BeaconProtocol {
USER = 0,
STANDARD = 1,
NATIONAL = 2
};
enum class AmChannel {
TEST = 0,
REAL = 1,
MANUAL = 2
};
enum class BpskChannel {
HAM = 0,
B = 1,
C = 2,
F = 3,
G = 4,
J = 5,
K = 6,
N = 7,
O = 8,
MANUAL = 10
};
struct Location {
std::string locator;
bool south;
int16_t lat_deg;
int8_t lat_min;
int8_t lat_sec;
float latitude;
bool west;
int16_t long_deg;
int8_t long_min;
int8_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_bpsk_frequency();
void update_am_transmission();
void update_mode();
void update_location(bool updateLocatorField = true);
struct Beacon {
std::string title{};
std::string description{};
std::string frame{};
};
NavigationView& nav_;
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{AM_TEST_FREQUENCY};
// Frequency of the 406 MHz BPSK signal
rf::Frequency bpsk_frequency{BPSK_FREQUENCY_HAM};
// Selected am channel
uint8_t am_channel{(uint8_t)AmChannel::TEST};
// Selected bpsk channel
uint8_t bpsk_channel{(uint8_t)BpskChannel::HAM};
// Manual AM frequency value
rf::Frequency manual_am_frequency{AM_TEST_FREQUENCY};
// Manual BPSK frequency value
rf::Frequency manual_bpsk_frequency{BPSK_FREQUENCY_HAM};
// True when using a beacon from the BEACONS.TXT file
bool mode_file{false};
// True when slideshow is enabled for file mode (change beacon after every transmission)
bool slideshow_enabled{false};
// 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{true};
// 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{(uint8_t)BeaconType::EPIRB};
// Currently selected beacon protocol
uint8_t beacon_protocol{(uint8_t)BeaconProtocol::USER};
// 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},
{"amchan"sv, &am_channel},
{"bpskchan"sv, &bpsk_channel},
{"loop"sv, &loop_enabled},
{"delay"sv, &delay},
{"file"sv, &mode_file},
{"slideshow"sv, &slideshow_enabled},
{"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 transmitting a BPSK frame
bool transmitting_bpsk{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(0), UI_POS_Y(14)}, "AM chan.:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(15)}, "BPSK chan.:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(17), UI_POS_Y(9)}, "s.", Theme::getInstance()->fg_light->foreground}};
// For file mode
struct FileModeWidgets {
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},
""};
Checkbox checkbox_slideshow{
{UI_POS_X(0), UI_POS_Y(5)},
9,
"Slideshow",
true};
};
std::unique_ptr<FileModeWidgets> file_mode_ui{};
// 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", (uint8_t)BeaconType::EPIRB},
{"ELT", (uint8_t)BeaconType::ELT},
{"PLB", (uint8_t)BeaconType::PLB}}};
OptionsField options_beacon_protocol{
{UI_POS_X(9 + 7), UI_POS_Y(1)},
30,
{{"User", (uint8_t)BeaconProtocol::USER},
{"Standard", (uint8_t)BeaconProtocol::STANDARD},
{"National", (uint8_t)BeaconProtocol::NATIONAL}}};
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)", (uint8_t)BeaconMode::FILE},
{"Manual (Editor)", (uint8_t)BeaconMode::MODE_MANUAL}}};
// 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;
OptionsField options_am_channel{
{UI_POS_X(11), UI_POS_Y(14)},
20,
{{"121.375 MHz (Test)", 0},
{"121.500 MHz /!\\Real", 1},
{"Manual", 2}}};
OptionsField options_bpsk_channel{
{UI_POS_X(11), UI_POS_Y(15)},
20,
{{"433.025 MHz (Ham)", (uint8_t)BpskChannel::HAM},
{"406.025 MHz (B)", (uint8_t)BpskChannel::B},
{"406.028 MHz (C)", (uint8_t)BpskChannel::C},
{"406.037 MHz (F)", (uint8_t)BpskChannel::F},
{"406.040 MHz (G)", (uint8_t)BpskChannel::G},
{"406.049 MHz (J)", (uint8_t)BpskChannel::J},
{"406.052 MHz (K)", (uint8_t)BpskChannel::K},
{"406.061 MHz (N)", (uint8_t)BpskChannel::N},
{"406.064 MHz (O)", (uint8_t)BpskChannel::O},
{"Manual", (uint8_t)BpskChannel::MANUAL}}};
// 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__*/
+4
View File
@@ -263,6 +263,10 @@ set(EXTCPPSRC
#epirb_rx 168 byte flash
external/epirb_rx/main.cpp
external/epirb_rx/ui_epirb_rx.cpp
external/epirb_rx/ui_beaconlist.cpp
external/epirb_rx/beacon_db.cpp
external/epirb_rx/beacon.cpp
external/epirb_rx/location.cpp
#epirb_tx
external/epirb_tx/main.cpp