mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-07-26 10:38:52 +00:00
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:
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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__
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
@@ -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,
|
||||
|
||||
|
||||
@@ -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__
|
||||
@@ -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__*/
|
||||
+600
-707
File diff suppressed because it is too large
Load Diff
+323
-296
@@ -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
@@ -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__*/
|
||||
|
||||
+659
-631
File diff suppressed because it is too large
Load Diff
+417
-404
@@ -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__*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,96 +1,272 @@
|
||||
/*
|
||||
* Copyright (C) 2024 EPIRB Receiver 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.
|
||||
*/
|
||||
|
||||
#include "proc_epirb.hpp"
|
||||
|
||||
#include "portapack_shared_memory.hpp"
|
||||
|
||||
#include "dsp_fir_taps.hpp"
|
||||
|
||||
#include "event_m4.hpp"
|
||||
#include <ch.h>
|
||||
|
||||
EPIRBProcessor::EPIRBProcessor() {
|
||||
// Configure the decimation filters for narrowband EPIRB signal
|
||||
// Target: Reduce 2.457600 MHz to ~38.4 kHz for 400 bps processing
|
||||
decim_0.configure(taps_11k0_decim_0.taps);
|
||||
decim_1.configure(taps_11k0_decim_1.taps);
|
||||
baseband_thread.start();
|
||||
}
|
||||
|
||||
void EPIRBProcessor::execute(const buffer_c8_t& buffer) {
|
||||
/* 2.4576MHz, 2048 samples */
|
||||
|
||||
// First decimation stage: 2.4576 MHz -> 307.2 kHz
|
||||
const auto decim_0_out = decim_0.execute(buffer, dst_buffer);
|
||||
|
||||
// Second decimation stage: 307.2 kHz -> 38.4 kHz
|
||||
const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer);
|
||||
const auto decimator_out = decim_1_out;
|
||||
|
||||
/* 38.4kHz, 32 samples (approximately) */
|
||||
feed_channel_stats(decimator_out);
|
||||
|
||||
// Process each decimated sample through the matched filter
|
||||
for (size_t i = 0; i < decimator_out.count; i++) {
|
||||
// Apply matched filter for BPSK demodulation
|
||||
if (mf.execute_once(decimator_out.p[i])) {
|
||||
// Feed symbol to clock recovery when matched filter triggers
|
||||
clock_recovery(mf.get_output());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EPIRBProcessor::consume_symbol(const float raw_symbol) {
|
||||
// BPSK demodulation: positive = 1, negative = 0
|
||||
const uint_fast8_t sliced_symbol = (raw_symbol >= 0.0f) ? 1 : 0;
|
||||
|
||||
// Decode bi-phase L encoding manually
|
||||
// In bi-phase L: 0 = no transition, 1 = transition
|
||||
// This is a simple edge detector
|
||||
const auto decoded_symbol = sliced_symbol ^ last_symbol;
|
||||
last_symbol = sliced_symbol;
|
||||
|
||||
// Build packet from decoded symbols
|
||||
packet_builder.execute(decoded_symbol);
|
||||
}
|
||||
|
||||
void EPIRBProcessor::payload_handler(const baseband::Packet& packet) {
|
||||
// EPIRB packet received - validate and process
|
||||
if (packet.size() >= 112) { // Minimum EPIRB data payload size (112 bits)
|
||||
packets_received++;
|
||||
last_packet_timestamp = Timestamp::now();
|
||||
|
||||
// Create and send EPIRB packet message to application layer
|
||||
const EPIRBPacketMessage message{packet};
|
||||
shared_memory.application_queue.push(message);
|
||||
}
|
||||
}
|
||||
|
||||
void EPIRBProcessor::on_message(const Message* const message) {
|
||||
(void)message; // Unused in this processor
|
||||
}
|
||||
|
||||
int main() {
|
||||
EventDispatcher event_dispatcher{std::make_unique<EPIRBProcessor>()};
|
||||
event_dispatcher.run();
|
||||
return 0;
|
||||
}
|
||||
/*
|
||||
* Copyright (C) 2024 EPIRB Receiver 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.
|
||||
*/
|
||||
|
||||
#include "proc_epirb.hpp"
|
||||
|
||||
#include "portapack_shared_memory.hpp"
|
||||
|
||||
#include "dsp_fir_taps.hpp"
|
||||
|
||||
#include "audio_dma.hpp"
|
||||
|
||||
#include "event_m4.hpp"
|
||||
#include <ch.h>
|
||||
|
||||
EPIRBProcessor::EPIRBProcessor() {
|
||||
// Configure the decimation filters for narrowband EPIRB signal
|
||||
decim_0.configure(taps_11k0_decim_0.taps);
|
||||
decim_1.configure(taps_11k0_decim_1.taps);
|
||||
// Configure channel filter for audio filtering
|
||||
channel_filter.configure(taps_11k0_channel.taps, 2);
|
||||
// Configure demodulation for audio output
|
||||
demod.configure(SAMPLE_RATE, 5000);
|
||||
// Configure audio output (+squelch level)
|
||||
configure_audio();
|
||||
#ifdef SPECAN
|
||||
channel_spectrum.set_decimation_factor(1);
|
||||
#endif
|
||||
baseband_thread.start();
|
||||
}
|
||||
|
||||
void EPIRBProcessor::configure_audio() {
|
||||
// UI sends an squelch value ranging from 0 to 99, 0 disables squelch, dividing UI value by 40 gives a valid UI threashold around 50
|
||||
audio_output.configure(audio_24k_hpf_300hz_config, audio_24k_deemph_300_6_config, ((float)squelch_level) / 40.0f);
|
||||
}
|
||||
|
||||
float EPIRBProcessor::get_phase_diff(const complex16_t& sample0, const complex16_t& sample1) {
|
||||
// Calculate the phase difference between two samples
|
||||
float dI = sample1.real() * sample0.real() + sample1.imag() * sample0.imag();
|
||||
float dQ = sample1.imag() * sample0.real() - sample1.real() * sample0.imag();
|
||||
float phase_diff = atan2f(dQ, dI);
|
||||
// Prevent phase diff from wrapping around
|
||||
if (phase_diff > M_PI) phase_diff -= 2.0f * M_PI;
|
||||
if (phase_diff < -M_PI) phase_diff += 2.0f * M_PI;
|
||||
return phase_diff;
|
||||
}
|
||||
|
||||
bool EPIRBProcessor::filtered_rise_detect(bool condition) {
|
||||
bool result = false;
|
||||
if (condition) {
|
||||
// If rise condition is matched, filter peaks that last less than 3 samples
|
||||
rise_detection_count++;
|
||||
if (rise_detection_count >= RISE_FILTER_SAMPLES) {
|
||||
result = true;
|
||||
rise_detection_count = 0;
|
||||
}
|
||||
} else {
|
||||
rise_detection_count = 0;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void EPIRBProcessor::execute(const buffer_c8_t& buffer) {
|
||||
// First decimation stage: 3.072000 MHz / 8 -> 384 kHz
|
||||
const auto decim_0_out = decim_0.execute(buffer, dst_buffer);
|
||||
|
||||
// Second decimation stage: 384 kHz / 8 -> 48 kHz
|
||||
const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer);
|
||||
// We use decim1 output as decimator output
|
||||
const auto decimator_out = decim_1_out;
|
||||
|
||||
#ifdef SPECAN
|
||||
// Feed IQ data into spectrum collector for the RF waterfall.
|
||||
if (spectrum_on) channel_spectrum.feed(decim_1_out, -5500, 5500, 3400);
|
||||
#endif
|
||||
|
||||
feed_channel_stats(decimator_out);
|
||||
|
||||
// if (audio_on) {
|
||||
// Channel filter for audio out
|
||||
const auto channel_out = channel_filter.execute(decim_1_out, dst_buffer);
|
||||
auto audio = demod.execute(channel_out, audio_buffer);
|
||||
audio_output.write(audio);
|
||||
//}
|
||||
|
||||
// Process each decimated sample through state machine
|
||||
for (size_t i = 0; i < decimator_out.count; i++) {
|
||||
// Track sample count since last symbol and since begining of the frame
|
||||
sample_count++;
|
||||
frame_sample_count++;
|
||||
// Compute phase delta since last sample
|
||||
float phase_delta = get_phase_diff(last_sample, decimator_out.p[i]);
|
||||
last_sample = decimator_out.p[i];
|
||||
|
||||
// Let's sum phase delta over a 12 sample window to get the full phase jump
|
||||
phase_delta_acc -= phase_delta_buffer[pahse_delta_index];
|
||||
phase_delta_buffer[pahse_delta_index] = phase_delta;
|
||||
phase_delta_acc += phase_delta_buffer[pahse_delta_index];
|
||||
pahse_delta_index = (pahse_delta_index + 1) % PHASE_DELTA_ACC_SIZE;
|
||||
|
||||
// Use accumulated delta
|
||||
phase_delta = phase_delta_acc;
|
||||
|
||||
// State machine for COSPAS frame detection
|
||||
switch (current_state) {
|
||||
case IDLE:
|
||||
// We are waiting for a 160ms empty carrier => phase shouls be stable during this period
|
||||
// We accept a 0.6 phase shift since phase may drift durring carrier if carrier frequency is not alligned with tuner frequency
|
||||
if (filtered_rise_detect(phase_delta >= 0.6f)) {
|
||||
stability_counter = 0;
|
||||
} else {
|
||||
stability_counter++;
|
||||
if (stability_counter > CARRIER_SAMPLES_THRESHOLD) {
|
||||
// Carrier has been stable long enought, go to locked state
|
||||
current_state = CARRIER_LOCKED;
|
||||
frame_sample_count = 0;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case CARRIER_LOCKED:
|
||||
// Carrier is locked, we now wait for a phase 1.1 rad phase jump corresponding to the befining of the frame
|
||||
// Let's use a 0.7 phase jump threshold
|
||||
if (filtered_rise_detect(phase_delta >= 0.7f)) {
|
||||
// Jump detected, frame starts now
|
||||
frame_sample_count = 0;
|
||||
// Go to data sync state
|
||||
current_state = DATA_SYNC;
|
||||
// Frame should always start with a positive phase shift
|
||||
last_phase_positive = true;
|
||||
// And a 1 value
|
||||
last_bit = true;
|
||||
} else if (frame_sample_count > CARRIER_MAX_SAMPLES) {
|
||||
// We missed sync pattern
|
||||
frame_end();
|
||||
}
|
||||
break;
|
||||
|
||||
case DATA_SYNC: {
|
||||
float abs_phase_delta = fabsf(phase_delta);
|
||||
|
||||
if (abs_phase_delta >= 1.6f) {
|
||||
// Phase should jump from 1.1 rad to -1.1 rad or the other way around
|
||||
// Absolute phase jump is expected to be 2.2 rad
|
||||
// Phase jump is either positive or negative
|
||||
bool phase_positive = (phase_delta >= 0.0f);
|
||||
|
||||
if (phase_positive != last_phase_positive) {
|
||||
// Phase jumped to the opposit direction of last jump
|
||||
last_phase_positive = phase_positive;
|
||||
bool cur_bit;
|
||||
// Phase change => how long since last change ?
|
||||
if ((frame_sample_count >= (SAMPLES_PER_SYMBOL - SAMPLES_MARGIN)) && (frame_sample_count <= (SAMPLES_PER_SYMBOL + SAMPLES_MARGIN))) {
|
||||
// Frame start
|
||||
if (!phase_positive) {
|
||||
// Symbol detection is made on falling edge
|
||||
cur_bit = true;
|
||||
} else {
|
||||
// Ignore rising edge
|
||||
continue;
|
||||
}
|
||||
} else if (sample_count > (SAMPLES_PER_SYMBOL * 2 + SAMPLES_MARGIN)) {
|
||||
// We missed something...
|
||||
// Let's keep same value for current bit
|
||||
cur_bit = last_bit;
|
||||
} else if (sample_count >= (SAMPLES_PER_SYMBOL * 2 - SAMPLES_MARGIN)) {
|
||||
// 2 symbols since last change => bit value changes
|
||||
cur_bit = !last_bit;
|
||||
} else if ((sample_count >= (SAMPLES_PER_SYMBOL - SAMPLES_MARGIN)) && (sample_count <= (SAMPLES_PER_SYMBOL + SAMPLES_MARGIN))) {
|
||||
// Phase change occured in first half bit => we keep the same value
|
||||
if ((phase_positive && last_bit) || (!phase_positive && !last_bit)) {
|
||||
sample_count = 0;
|
||||
// Ignore rising edge if current value is 1 and falling edge if current value is 0 and move to next symbol
|
||||
continue;
|
||||
}
|
||||
// Same value on falling/rising edge
|
||||
cur_bit = last_bit;
|
||||
} else {
|
||||
// Filter the rest
|
||||
continue;
|
||||
}
|
||||
// Store new bit and move to next symbol
|
||||
sample_count = 0;
|
||||
packet_builder.execute(cur_bit);
|
||||
last_bit = cur_bit;
|
||||
}
|
||||
}
|
||||
if (frame_sample_count > FRAME_MAX_SAMPLES) {
|
||||
// End of frame
|
||||
current_state = POST_FRAME;
|
||||
packet_builder.flush();
|
||||
}
|
||||
} break;
|
||||
case POST_FRAME:
|
||||
if (frame_sample_count > CARRIER_MAX_SAMPLES) {
|
||||
// End of carrier
|
||||
frame_end();
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EPIRBProcessor::frame_end() {
|
||||
sample_count = 0;
|
||||
frame_sample_count = 0;
|
||||
stability_counter = 0;
|
||||
last_phase_positive = false;
|
||||
last_bit = false;
|
||||
current_state = IDLE;
|
||||
packet_builder.reset_state();
|
||||
}
|
||||
|
||||
void EPIRBProcessor::payload_handler(const baseband::Packet& packet) {
|
||||
// EPIRB packet received: create and send EPIRB packet message to application layer
|
||||
const EPIRBPacketMessage message{packet};
|
||||
shared_memory.application_queue.push(message);
|
||||
}
|
||||
|
||||
void EPIRBProcessor::on_message(const Message* const msg) {
|
||||
// Configure the processor
|
||||
switch (msg->id) {
|
||||
#ifdef SPECAN
|
||||
case Message::ID::UpdateSpectrum:
|
||||
case Message::ID::SpectrumStreamingConfig:
|
||||
channel_spectrum.on_message(msg);
|
||||
break;
|
||||
#endif
|
||||
case Message::ID::EPIRBRXConfig: {
|
||||
const EPIRBRXConfig message = *reinterpret_cast<const EPIRBRXConfig*>(msg);
|
||||
// audio_on = message.audio_on;
|
||||
#ifdef SPECAN
|
||||
spectrum_on = message.spectrum_on;
|
||||
#endif
|
||||
if (message.squelch != squelch_level) {
|
||||
// Update squelch config
|
||||
squelch_level = message.squelch;
|
||||
configure_audio();
|
||||
}
|
||||
} break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
audio::dma::init_audio_out();
|
||||
|
||||
EventDispatcher event_dispatcher{std::make_unique<EPIRBProcessor>()};
|
||||
event_dispatcher.run();
|
||||
return 0;
|
||||
}
|
||||
|
||||
+267
-134
@@ -1,135 +1,268 @@
|
||||
/*
|
||||
* Copyright (C) 2024 EPIRB Receiver 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 __PROC_EPIRB_H__
|
||||
#define __PROC_EPIRB_H__
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <array>
|
||||
#include <complex>
|
||||
|
||||
#include "baseband_processor.hpp"
|
||||
#include "baseband_thread.hpp"
|
||||
#include "rssi_thread.hpp"
|
||||
#include "channel_decimator.hpp"
|
||||
#include "matched_filter.hpp"
|
||||
#include "clock_recovery.hpp"
|
||||
#include "symbol_coding.hpp"
|
||||
#include "packet_builder.hpp"
|
||||
#include "baseband_packet.hpp"
|
||||
#include "message.hpp"
|
||||
#include "buffer.hpp"
|
||||
|
||||
// Forward declarations for types only used as pointers/references
|
||||
class Message;
|
||||
namespace baseband {
|
||||
class Packet;
|
||||
}
|
||||
|
||||
// EPIRB 406 MHz Emergency Position Indicating Radio Beacon
|
||||
// Signal characteristics:
|
||||
// - Frequency: 406.025 - 406.028 MHz (typically 406.028 MHz)
|
||||
// - Modulation: BPSK (Binary Phase Shift Keying)
|
||||
// - Data rate: 400 bps
|
||||
// - Encoding: Bi-phase L (Manchester)
|
||||
// - Transmission: Every 50 seconds ± 2.5 seconds
|
||||
// - Power: 5W ± 2dB
|
||||
// - Message length: 144 bits (including sync pattern)
|
||||
|
||||
// Matched filter for BPSK demodulation at 400 bps
|
||||
// Using raised cosine filter taps optimized for 400 bps BPSK
|
||||
static constexpr std::array<std::complex<float>, 64> bpsk_taps = {{// Raised cosine filter coefficients for BPSK 400 bps
|
||||
-5, -8, -12, -15, -17, -17, -15, -11,
|
||||
-5, 2, 11, 20, 29, 37, 43, 47,
|
||||
48, 46, 42, 35, 26, 16, 4, -8,
|
||||
-21, -33, -44, -53, -59, -62, -62, -58,
|
||||
-51, -41, -28, -13, 3, 19, 36, 51,
|
||||
64, 74, 80, 82, 80, 74, 64, 51,
|
||||
36, 19, 3, -13, -28, -41, -51, -58,
|
||||
-62, -62, -59, -53, -44, -33, -21, -8}};
|
||||
|
||||
class EPIRBProcessor : public BasebandProcessor {
|
||||
public:
|
||||
EPIRBProcessor();
|
||||
|
||||
void execute(const buffer_c8_t& buffer) override;
|
||||
|
||||
void on_message(const Message* const message) override;
|
||||
|
||||
private:
|
||||
// EPIRB operates at 406 MHz with narrow bandwidth
|
||||
static constexpr size_t baseband_fs = 2457600;
|
||||
static constexpr uint32_t epirb_center_freq = 406028000; // 406.028 MHz
|
||||
static constexpr uint32_t symbol_rate = 400; // 400 bps
|
||||
static constexpr size_t decimation_factor = 64; // Decimate to ~38.4kHz
|
||||
|
||||
std::array<complex16_t, 512> dst{};
|
||||
const buffer_c16_t dst_buffer{
|
||||
dst.data(),
|
||||
dst.size()};
|
||||
|
||||
// Decimation chain for 406 MHz EPIRB signal processing
|
||||
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{};
|
||||
dsp::decimate::FIRC16xR16x32Decim8 decim_1{};
|
||||
|
||||
dsp::matched_filter::MatchedFilter mf{bpsk_taps, 2};
|
||||
|
||||
// Clock recovery for 400 bps symbol rate
|
||||
// Sampling rate after decimation: ~38.4kHz
|
||||
// Symbols per sample: 38400 / 400 = 96 samples per symbol
|
||||
clock_recovery::ClockRecovery<clock_recovery::FixedErrorFilter> clock_recovery{
|
||||
38400, // sampling_rate
|
||||
400, // symbol_rate (400 bps)
|
||||
{0.0555f}, // error_filter coefficient
|
||||
[this](const float symbol) { this->consume_symbol(symbol); }};
|
||||
|
||||
// Simple bi-phase L decoder state
|
||||
uint_fast8_t last_symbol = 0;
|
||||
|
||||
// EPIRB packet structure:
|
||||
// - Sync pattern: 000101010101... (15 bits)
|
||||
// - Frame sync: 0111110 (7 bits)
|
||||
// - Data: 112 bits
|
||||
// - BCH error correction: 10 bits
|
||||
// Total: 144 bits
|
||||
PacketBuilder<BitPattern, BitPattern, BitPattern> packet_builder{
|
||||
{0b010101010101010, 15, 1}, // Preamble pattern
|
||||
{0b0111110, 7}, // Frame sync pattern
|
||||
{0b0111110, 7}, // End pattern (same as sync for simplicity)
|
||||
[this](const baseband::Packet& packet) {
|
||||
this->payload_handler(packet);
|
||||
}};
|
||||
|
||||
void consume_symbol(const float symbol);
|
||||
void payload_handler(const baseband::Packet& packet);
|
||||
|
||||
// Statistics
|
||||
uint32_t packets_received = 0;
|
||||
Timestamp last_packet_timestamp{};
|
||||
|
||||
/* NB: Threads should be the last members in the class definition. */
|
||||
BasebandThread baseband_thread{
|
||||
baseband_fs, this, baseband::Direction::Receive, /*auto_start*/ false};
|
||||
RSSIThread rssi_thread{};
|
||||
};
|
||||
|
||||
/*
|
||||
* Copyright (C) 2024 EPIRB Receiver 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 __PROC_EPIRB_H__
|
||||
#define __PROC_EPIRB_H__
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <array>
|
||||
#include <complex>
|
||||
|
||||
#include "baseband_processor.hpp"
|
||||
#include "baseband_thread.hpp"
|
||||
#include "rssi_thread.hpp"
|
||||
#include "channel_decimator.hpp"
|
||||
#include "matched_filter.hpp"
|
||||
#include "packet_builder.hpp"
|
||||
#include "baseband_packet.hpp"
|
||||
#include "message.hpp"
|
||||
#include "buffer.hpp"
|
||||
|
||||
// Specan is disable to keep application size below the 32k limit
|
||||
// #define SPECAN
|
||||
|
||||
#ifdef SPECAN
|
||||
#include "spectrum_collector.hpp"
|
||||
#endif
|
||||
|
||||
#include "audio_output.hpp"
|
||||
#include "dsp_demodulate.hpp"
|
||||
|
||||
// Forward declarations for types only used as pointers/references
|
||||
class Message;
|
||||
namespace baseband {
|
||||
class Packet;
|
||||
}
|
||||
|
||||
// COSPAS / SARSAT 406 frame constants
|
||||
// Size of preamble (bits)
|
||||
#define COSPAS_PREAMBLE_SIZE 24
|
||||
// Size of long frame (bits)
|
||||
#define COSPAS_LONG_FRAME_SIZE 144
|
||||
// Siz of short frame (bits)
|
||||
#define COSPAS_SHORT_FRAME_SIZE 112
|
||||
// Preamble for real frames
|
||||
#define COSPAS_REAL_PREAMBLE 0b1111'1111'1111'1110'0010'1111
|
||||
// Preable for test frames
|
||||
#define COSPAS_TEST_PREAMBLE 0b1111'1111'1111'1110'1101'0000
|
||||
|
||||
// Dedicated EPIRB PacketBuilder
|
||||
// Usees diedicated preamble detection logic to find both real and test frames
|
||||
// Also as a dedicated packet size detection based on frame's size bit
|
||||
class EPIRBPacketBuilder {
|
||||
public:
|
||||
using EPIRBHandler = void (*)(void* context, const baseband::Packet& packet);
|
||||
|
||||
EPIRBPacketBuilder(
|
||||
void* context,
|
||||
EPIRBHandler handler)
|
||||
: context(context),
|
||||
handler(handler) {
|
||||
}
|
||||
|
||||
void execute(
|
||||
const uint_fast8_t symbol) {
|
||||
bit_history.add(symbol);
|
||||
|
||||
switch (state) {
|
||||
case State::Preamble: {
|
||||
// Detect both real and test fram preambles
|
||||
bool is_real = real_sync_matcher(bit_history, packet.size());
|
||||
bool is_test = test_sync_matcher(bit_history, packet.size());
|
||||
if (is_real || is_test) {
|
||||
// Append preamble to the begining of the packet
|
||||
uint64_t preamble = is_real ? COSPAS_REAL_PREAMBLE : COSPAS_TEST_PREAMBLE;
|
||||
for (int8_t i = (COSPAS_PREAMBLE_SIZE - 1); i >= 0; i--) {
|
||||
packet.add((preamble >> i) & 0x1);
|
||||
}
|
||||
state = State::Format;
|
||||
}
|
||||
} break;
|
||||
case State::Format:
|
||||
packet.add(symbol);
|
||||
// 144 bits for long frames and 112 for short frames
|
||||
size = symbol ? COSPAS_LONG_FRAME_SIZE : COSPAS_SHORT_FRAME_SIZE;
|
||||
state = State::Payload;
|
||||
|
||||
break;
|
||||
case State::Payload:
|
||||
packet.add(symbol);
|
||||
|
||||
if (packet.size() >= size) {
|
||||
flush();
|
||||
} else {
|
||||
if (packet.size() >= packet.capacity()) {
|
||||
reset_state();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
reset_state();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void flush() {
|
||||
// Timestamp is not set here to save some app space (not used on ui side)
|
||||
// packet.set_timestamp(Timestamp::now());
|
||||
if (handler) handler(context, packet);
|
||||
reset_state();
|
||||
}
|
||||
|
||||
void reset_state() {
|
||||
packet.clear();
|
||||
bit_history = BitHistory();
|
||||
state = State::Preamble;
|
||||
}
|
||||
|
||||
private:
|
||||
enum State {
|
||||
Preamble,
|
||||
Format,
|
||||
Payload,
|
||||
};
|
||||
|
||||
BitHistory bit_history{};
|
||||
BitPattern real_sync_matcher{COSPAS_REAL_PREAMBLE, COSPAS_PREAMBLE_SIZE};
|
||||
BitPattern test_sync_matcher{COSPAS_TEST_PREAMBLE, COSPAS_PREAMBLE_SIZE};
|
||||
void* context;
|
||||
EPIRBHandler handler;
|
||||
uint8_t size{0};
|
||||
|
||||
State state{State::Preamble};
|
||||
baseband::Packet packet{};
|
||||
};
|
||||
|
||||
class EPIRBProcessor : public BasebandProcessor {
|
||||
public:
|
||||
EPIRBProcessor();
|
||||
|
||||
void execute(const buffer_c8_t& buffer) override;
|
||||
|
||||
void on_message(const Message* const message) override;
|
||||
|
||||
private:
|
||||
// Baseband frequency is set to 3,072,000 samples / sec
|
||||
static constexpr uint32_t BASEBAND_SAMPLE_RATE = 3072000;
|
||||
static constexpr uint32_t SAMPLE_RATE = BASEBAND_SAMPLE_RATE / 8 / 8; // We use to decimators with factor 8 each
|
||||
static constexpr uint32_t SYMBOL_RATE = 800; // 400 bps + Manchester (2 1/2 bits per symbol) => 800
|
||||
static constexpr size_t SAMPLES_PER_SYMBOL = SAMPLE_RATE / SYMBOL_RATE; // = 60 samples per symbol
|
||||
static constexpr size_t SAMPLES_PER_BIT = SAMPLES_PER_SYMBOL * 2; // = 120 samples per bit
|
||||
static constexpr size_t SAMPLES_MARGIN = SAMPLES_PER_SYMBOL / 3; // = Allow 20 sample drift
|
||||
static constexpr size_t SAMPLES_ACCUMUMLATOR = SAMPLES_PER_SYMBOL / 5; // Accumulate phase change across 12 samples
|
||||
static constexpr size_t RISE_FILTER_SAMPLES = SAMPLES_PER_SYMBOL / 20; // Filter peaks of less than 3 samples
|
||||
|
||||
static constexpr size_t CARRIER_SAMPLES_THRESHOLD = 0.080f * SAMPLE_RATE; // Carrier before frame lasts 160ms, require at least 80ms
|
||||
static constexpr size_t CARRIER_MAX_SAMPLES = 0.900f * SAMPLE_RATE; // Carrier + frame lasts 160ms + 520ms + 100ms post carrier = 880ms
|
||||
static constexpr size_t FRAME_MAX_SAMPLES = SAMPLES_PER_BIT * (144 * 1.1f); // Frame max length (add 1% error margin)
|
||||
|
||||
AudioOutput audio_output{};
|
||||
|
||||
// Config
|
||||
uint8_t squelch_level{50};
|
||||
// Audio on/off logic is disabled to save app space
|
||||
// bool audio_on{true};
|
||||
#ifdef SPECAN
|
||||
bool spectrum_on{false};
|
||||
#endif
|
||||
|
||||
std::array<float, 32> audio{};
|
||||
const buffer_f32_t audio_buffer{
|
||||
audio.data(),
|
||||
audio.size()};
|
||||
|
||||
// Last received bit (for manchester deconding)
|
||||
bool last_bit = false;
|
||||
// Sample count since last symbol
|
||||
uint16_t sample_count{0};
|
||||
// Sample count since frame start
|
||||
uint16_t frame_sample_count{0};
|
||||
// True if last phase shift was positive, false otherwise
|
||||
bool last_phase_positive = false;
|
||||
// Counter used for peak filtering
|
||||
uint16_t rise_detection_count{0};
|
||||
|
||||
// Frame detection state machine states
|
||||
enum State { IDLE,
|
||||
CARRIER_LOCKED,
|
||||
DATA_SYNC,
|
||||
POST_FRAME };
|
||||
// Current state for frame detection state machine
|
||||
State current_state = IDLE;
|
||||
// Carrier detection counter
|
||||
uint32_t stability_counter = 0;
|
||||
|
||||
// Phase delta accumulator (6 samples)
|
||||
static constexpr size_t PHASE_DELTA_ACC_SIZE = SAMPLES_ACCUMUMLATOR;
|
||||
float phase_delta_buffer[PHASE_DELTA_ACC_SIZE] = {0.0f};
|
||||
size_t pahse_delta_index = 0;
|
||||
float phase_delta_acc = 0.0f;
|
||||
|
||||
std::array<complex16_t, 512> dst{};
|
||||
const buffer_c16_t dst_buffer{
|
||||
dst.data(),
|
||||
dst.size()};
|
||||
|
||||
// Decimation chain for 406 MHz EPIRB signal processing
|
||||
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{};
|
||||
dsp::decimate::FIRC16xR16x32Decim8 decim_1{};
|
||||
// Audio filtering
|
||||
dsp::decimate::FIRAndDecimateComplex channel_filter{};
|
||||
// Audio demodulation
|
||||
dsp::demodulate::FM demod{};
|
||||
#ifdef SPECAN
|
||||
SpectrumCollector channel_spectrum{};
|
||||
#endif
|
||||
// Store last stample for phase delta calculation
|
||||
complex16_t last_sample{};
|
||||
|
||||
// EPIRB packet structure:
|
||||
// - Sync pattern: 111111111111111 (15 bits)
|
||||
// - Frame sync: 000101111(real) / 011010000(test) (9 bits)
|
||||
// - Data: 120 bits (long frame) / // bits (short frame)
|
||||
// - BCH error correction: 10 bits
|
||||
// Total: 144 bits (long frame) / 112 bits (short frame)
|
||||
EPIRBPacketBuilder packet_builder{
|
||||
this,
|
||||
[](void* ctx, const baseband::Packet& p) {
|
||||
static_cast<EPIRBProcessor*>(ctx)->payload_handler(p);
|
||||
}};
|
||||
|
||||
void payload_handler(const baseband::Packet& packet);
|
||||
// Compute phase diff between two samples
|
||||
float get_phase_diff(const complex16_t& sample0, const complex16_t& sample1);
|
||||
// End current frame
|
||||
void frame_end();
|
||||
// Rise detection with peak filtering
|
||||
bool filtered_rise_detect(bool condition);
|
||||
// Configure audio processing
|
||||
void configure_audio();
|
||||
|
||||
/* NB: Threads should be the last members in the class definition. */
|
||||
BasebandThread baseband_thread{
|
||||
BASEBAND_SAMPLE_RATE, this, baseband::Direction::Receive, /*auto_start*/ false};
|
||||
RSSIThread rssi_thread{};
|
||||
};
|
||||
|
||||
#endif /*__PROC_EPIRB_H__*/
|
||||
@@ -161,6 +161,7 @@ class Message {
|
||||
ToneDetectData = 103,
|
||||
ToneDetectConfig = 104,
|
||||
FlexTosend = 105,
|
||||
EPIRBRXConfig = 106,
|
||||
MAX
|
||||
};
|
||||
|
||||
@@ -390,6 +391,16 @@ class EPIRBPacketMessage : public Message {
|
||||
baseband::Packet packet;
|
||||
};
|
||||
|
||||
class EPIRBRXConfig : public Message {
|
||||
public:
|
||||
constexpr EPIRBRXConfig()
|
||||
: Message{ID::EPIRBRXConfig} {
|
||||
}
|
||||
bool spectrum_on = false;
|
||||
bool audio_on = true;
|
||||
uint8_t squelch{50};
|
||||
};
|
||||
|
||||
class TPMSPacketMessage : public Message {
|
||||
public:
|
||||
constexpr TPMSPacketMessage(
|
||||
|
||||
@@ -13,7 +13,7 @@ EPIRB; Standard Location Protocol EPIRB-MMSI; FFFED0A0D205F260850F3DC9E0B70B6E4F
|
||||
PLB; Standard Location Protocol - PLB (Serial); FFFED0A3E7B10016150D364D8B3689C09437
|
||||
SAAS; Std. Location ship security protocol (SSAS); FFFED0A5DC19E1A07FDFFE5C803483E0FCCA
|
||||
ELT 24; Serial user Location (ELT with Aircraft 24-bit Address); FFFED0CE46E76EF8C00C2BAA31CFE0FF0F61
|
||||
MMSO; Maritime User Protocol MMSI - EPIRB; FFFED0D9D4EB28140AA6893F16A2C67282EC
|
||||
MMSI; Maritime User Protocol MMSI - EPIRB; FFFED0D9D4EB28140AA6893F16A2C67282EC
|
||||
Float Free EPIRB; Float Free EPIRB with Serial Identification Number; FFFED0DF76A9A9C800174DE27BE1F0277E45
|
||||
Non Float Free EPIRB; Non Float Free EPIRB with Serial Identification; FFFED0DF77200000001168C610AFE0FF0146
|
||||
Radio Call Sign; Radio Call Sign - EPIRB; FFFED0E0DDAE599508268E4C05E054651307
|
||||
@@ -22,4 +22,13 @@ ELT Aircraft; ELT with Aircraft Operator Designator & Serial Number; FFFED0EDF67
|
||||
PLB Serial; Standard Location Protocol - PLB (Serial); FFFED0A157B081437FDFF8B4833783E0F66C
|
||||
RLS Location; RLS Location Protocol; FFFED096ED09900149D4D467EE0851A3B2E8
|
||||
Orbitography; Orbitography Protocol (from Toulouse FMCC's LUT); FFFE2FCE3000000000000DBD0E4022417500
|
||||
ELT 24 2; ELT 24 bits; FFFED08DB345B146202DDF3C71F59BAB7072
|
||||
ELT 24 2; ELT 24 bits; FFFED08DB345B146202DDF3C71F59BAB7072
|
||||
Selftest BCH1 ERR; Serial user Location Protocol; FFFED0D7E6202820000C29FF51041775302D
|
||||
Selftest BCH2 ERR; Serial user Location Protocol; FFFED0D6E6202820000C29FF51041765302D
|
||||
Selftest BCH12 ERR; Serial user Location Protocol; FFFED0D7E6202820000C29FF51041765302D
|
||||
Emergency Cap;EPIRB Emergency Capsizing;FFFED056E68040022021A4CDC675
|
||||
Emergency No Mar.;Emergency non maritime;FFFED056EE8040022021A5623CFE
|
||||
Emergency PLB;Emergency PLB no fire+med+dis;FFFED056E7804002202123F39E36
|
||||
Emergency PLB fire;Emergency PLB fire;FFFED056E7804002202123F39E39
|
||||
EPIRB Sinking;Emergency EPIRB Sinking;FFFED056E58040022021342FAD76
|
||||
EPIRB Call Sign Col.;EPIRB Radio Call Sign Emergency collision;FFFED056ED80400220217185E933
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
406025000
|
||||
406028000
|
||||
406037000
|
||||
406040000
|
||||
406049000
|
||||
406052000
|
||||
406061000
|
||||
406064000
|
||||
433025000
|
||||
144875000
|
||||
406022000
|
||||
@@ -0,0 +1,59 @@
|
||||
EPIRB - Maritime
|
||||
EPIRB - Radio Call Sign
|
||||
ELT - Aviation
|
||||
Serial User Protocol
|
||||
Test User Protocol
|
||||
Orbitography Protocol
|
||||
National User Protocol
|
||||
2nd Generation Beacons
|
||||
EPIRB - MMSI / Location
|
||||
ELT-24-bit Address / Location
|
||||
ELT Serial Location
|
||||
ELT Serial Aircraft Location
|
||||
EPIRB Serial Location
|
||||
PLB Serial Location
|
||||
Ship Security Location
|
||||
Test Standard Location
|
||||
ELT National Location
|
||||
EPIRB National Location
|
||||
PLB National Location
|
||||
Test National Location
|
||||
RLS Location Protocol
|
||||
ELT(DT) Location Protocol
|
||||
Spare
|
||||
Unknown
|
||||
# L. 25: additional data
|
||||
ELTs/SN
|
||||
ELTs/AC op & SN
|
||||
FF EPIRBs/SN
|
||||
ELTs+AC 24-bit ad.
|
||||
NFF EPIRBs/SN
|
||||
Not Used
|
||||
PLBs/SN
|
||||
# L. 33: long additinal data
|
||||
MMSI=0x%08lX MID=%ld
|
||||
24 bits AC ad.
|
||||
C/S TA #=%ld
|
||||
Op Design.=%c%c%c
|
||||
Nati. data=%ld
|
||||
# L. 39: emmergency
|
||||
Other
|
||||
Fire
|
||||
Flooding
|
||||
Collision
|
||||
Grounding
|
||||
Capsizing
|
||||
Sinking
|
||||
Disabled
|
||||
Abandoning
|
||||
# L. 49: other emmergency
|
||||
Fire
|
||||
Med.
|
||||
Dis.
|
||||
# L. 53:
|
||||
Undefined
|
||||
No device
|
||||
Other/no device
|
||||
Other device
|
||||
121.5 MHz
|
||||
SART
|
||||
@@ -0,0 +1,292 @@
|
||||
501:ADE:AdelieLand
|
||||
201:ALB:Albania
|
||||
202:AND:Andorra
|
||||
203:AUT:Austria
|
||||
204:AZC:Azores
|
||||
205:BEL:Belgium
|
||||
206:BLR:Belarus
|
||||
207:BUL:Bulgaria
|
||||
208:VAT:Vatican
|
||||
209:CYP:Cyprus
|
||||
210:CYP:Cyprus
|
||||
211:GER:Germany
|
||||
212:CYP:Cyprus
|
||||
213:GOG:Georgia
|
||||
214:MOL:Moldova
|
||||
215:MAL:MALTA
|
||||
216:ARM:Armenia
|
||||
218:GER:Germany
|
||||
219:DEN:Denmark
|
||||
220:DEN:Denmark
|
||||
224:SPA:Spain
|
||||
225:SPA:Spain
|
||||
226:FRA:France
|
||||
227:FRA:France
|
||||
228:FRA:France
|
||||
229:MAL:Malta
|
||||
230:FIN:Finland
|
||||
231:FAR:Faro Isle
|
||||
232:UKM:G Britain
|
||||
233:UKM:G Britain
|
||||
234:UKM:G Britain
|
||||
235:UKM:G Britain
|
||||
236:GIB:Gibraltar
|
||||
237:GRE:Greece
|
||||
238:CRT:Croatia
|
||||
239:GRE:Greece
|
||||
240:GRE:Greece
|
||||
241:GRE:Greece
|
||||
242:MOR:Morocco
|
||||
243:HUN:Hungary
|
||||
244:NET:Netherland
|
||||
245:NET:Netherland
|
||||
246:NET:Netherland
|
||||
247:ITA:Italy
|
||||
248:MAL:Malta
|
||||
249:MAL:Malta
|
||||
250:IRE:Ireland
|
||||
251:ICE:Iceland
|
||||
252:LIE:Liechten
|
||||
253:LUX:Luxembourg
|
||||
254:MON:Monaco
|
||||
255:MAE:Madeira
|
||||
256:MAL:Malta
|
||||
257:NOR:Norway
|
||||
258:NOR:Norway
|
||||
259:NOR:Norway
|
||||
261:POL:Poland
|
||||
262:MNT:Montenegro
|
||||
263:POR:Portugal
|
||||
264:ROM:Romania
|
||||
265:SWE:Sweden
|
||||
266:SWE:Sweden
|
||||
267:SLV:Slovakia
|
||||
268:SAN:San Marino
|
||||
269:SWT:Swiss
|
||||
270:CZH:Czech Rep
|
||||
271:TUR:Turkiye
|
||||
272:UKR:Ukraine
|
||||
273:RUS:Russia
|
||||
274:MKD:North Mac
|
||||
275:LAT:Latvia
|
||||
276:EST:Estonia
|
||||
277:LIT:Lithuania
|
||||
278:SVN:Slovenia
|
||||
279:SER:Serbia
|
||||
301:ANA:Anguilla
|
||||
303:ALA:Alaska
|
||||
304:ANT:Antigua
|
||||
305:ANT:Antigua
|
||||
306:NEA:N Antilles
|
||||
307:ARU:Aruba
|
||||
308:BAA:Bahamas
|
||||
309:BAA:Bahamas
|
||||
310:BER:Bermuda
|
||||
311:BAA:Bahamas
|
||||
312:BEZ:Belize
|
||||
314:BAR:Barbados
|
||||
316:CAN:Canada
|
||||
319:CAY:Cayman Is
|
||||
321:COS:Costa Rica
|
||||
323:CUB:Cuba
|
||||
325:DOM:Dominica
|
||||
327:DOR:Dominican
|
||||
329:GUA:Guadeloupe
|
||||
330:GRA:Grenada
|
||||
331:GRN:Greenland
|
||||
332:GUT:Guatemala
|
||||
334:HON:Honduras
|
||||
336:HAI:Haiti
|
||||
338:USA:USA
|
||||
339:JAM:Jamaica
|
||||
341:SKN:St Kitts
|
||||
343:SLU:St Lucia
|
||||
345:MEX:Mexico
|
||||
347:MTQ:Martinique
|
||||
348:MOT:Montserrat
|
||||
350:NIC:Nicaragua
|
||||
351:PAN:Panama
|
||||
352:PAN:Panama
|
||||
353:PAN:Panama
|
||||
354:PAN:Panama
|
||||
355:PAN:Panama
|
||||
356:PAN:Panama
|
||||
357:PAN:Panama
|
||||
358:PUE:PuertoRico
|
||||
359:ELS:ElSalvador
|
||||
361:SPI:St Pierre
|
||||
362:TAT:Trinidad
|
||||
364:TUK:Caicos Is
|
||||
366:USA:USA
|
||||
367:USA:USA
|
||||
368:USA:USA
|
||||
369:USA:USA
|
||||
370:PAN:Panama
|
||||
371:PAN:Panama
|
||||
372:PAN:Panama
|
||||
373:PAN:Panama
|
||||
374:PAN:Panama
|
||||
375:SVG:St Vincent
|
||||
376:SVG:St Vincent
|
||||
377:SVG:St Vincent
|
||||
378:BVI:Virgin GB
|
||||
379:USV:Virgin US
|
||||
401:AFG:Afghan
|
||||
403:SAU:Saudi
|
||||
405:BAN:Bangladesh
|
||||
408:BAH:Bahrain
|
||||
410:BHU:Bhutan
|
||||
412:CHN:China
|
||||
413:CHN:China
|
||||
414:CHN:CHINA
|
||||
416:TAI:Taipei
|
||||
417:SRI:Sri Lanka
|
||||
419:IND:India
|
||||
422:IRN:Iran
|
||||
423:AZR:Azerbaijan
|
||||
425:IRQ:Iraq
|
||||
428:ISR:Israel
|
||||
431:JPN:Japan
|
||||
432:JPN:Japan
|
||||
434:TKM:Turkmenist
|
||||
436:KAZ:Kazakhstan
|
||||
437:UZB:Uzbekistan
|
||||
438:JOR:Jordan
|
||||
440:KOR:Korea Sou
|
||||
441:KOR:Korea Sou
|
||||
443:PAA:Palestine
|
||||
445:KDR:Korea Nor
|
||||
447:KUW:Kuwait
|
||||
450:LEB:Lebanon
|
||||
451:KYR:Kyrgyzia
|
||||
453:MAC:Macao
|
||||
455:MAV:Maldives
|
||||
457:MNG:Mongolia
|
||||
459:NEP:Nepal
|
||||
461:OMN:Oman
|
||||
463:PAK:Pakistan
|
||||
466:QAT:Qatar
|
||||
468:SYR:Syria
|
||||
470:UAE:UAE
|
||||
471:UAE:UAE
|
||||
472:TJK:TAJIKISTAN
|
||||
473:YEM:Yemen
|
||||
475:YEM:Yemen
|
||||
477:HKG:Hong Kong
|
||||
478:BOS:Bosniaherz
|
||||
503:AUS:Australia
|
||||
506:BUR:Burma
|
||||
508:BRU:Brunei
|
||||
510:MIC:Micronesia
|
||||
511:PAL:Palau
|
||||
512:NZL:NewZealand
|
||||
514:CMB:Cambodia
|
||||
515:CMB:Cambodia
|
||||
516:CHR:Christmas
|
||||
518:COO:Cook Isles
|
||||
520:FIJ:Fiji
|
||||
523:COC:Cocos Isle
|
||||
525:INO:Indonesia
|
||||
529:KIR:Kiribati
|
||||
531:LAO:Lao
|
||||
533:MLY:Malaysia
|
||||
536:MAI:Mariana Is
|
||||
538:MAR:Marshall I
|
||||
540:NCA:Caledonia
|
||||
542:NIU:Niue Isle
|
||||
544:NAU:Nauru
|
||||
546:PLY:Polynesia
|
||||
548:PHI:Philippine
|
||||
550:TIM:TimorLeste
|
||||
553:PAP:Papua NG
|
||||
555:PIT:Pitcairn I
|
||||
557:SOL:Solomon Is
|
||||
559:ASA:Samoa USA
|
||||
561:WSA:West Samoa
|
||||
563:SIN:Singapore
|
||||
564:SIN:Singapore
|
||||
565:SIN:Singapore
|
||||
566:SIN:SINGAPORE
|
||||
567:THA:Thailand
|
||||
570:TON:Tonga
|
||||
572:TUV:Tuvalu Is
|
||||
574:VIE:Vietnam
|
||||
576:VAN:Vanuatu
|
||||
577:VAN:Vanuatu
|
||||
578:WAL:Wallis Is
|
||||
601:SAF:So Africa
|
||||
603:ANG:Angola
|
||||
605:ALG:Algeria
|
||||
607:SPL:St Paul
|
||||
608:ASC:Ascension
|
||||
609:BUI:Burundi
|
||||
610:BEN:Benin
|
||||
611:BOT:Botswana
|
||||
612:CAR:CenAfr Rep
|
||||
613:CAM:Cameroon
|
||||
615:CON:Congo
|
||||
616:COM:Comoros
|
||||
617:CAP:Cape Verde
|
||||
618:CRP:Crozet
|
||||
619:IVO:IvoryCoast
|
||||
620:COM:Comoros
|
||||
621:DJI:Djibouti
|
||||
622:EGY:Egypt
|
||||
624:ETH:Ethiopia
|
||||
625:ERT:Eritrea
|
||||
626:GAB:Gabon Rep
|
||||
627:GHA:Ghana
|
||||
629:GAM:Gambia
|
||||
630:GUB:Guinea Bis
|
||||
631:EQG:Eq Guinea
|
||||
632:GUN:Guinea Rep
|
||||
633:BUF:Burkina FS
|
||||
634:KEN:Kenya
|
||||
635:KER:Kerguelen
|
||||
636:LIB:Liberia
|
||||
637:LIB:Liberia
|
||||
638:SSD:Southsudan
|
||||
642:LBY:Libya
|
||||
644:LES:Lesotho
|
||||
645:MAU:Mauritius
|
||||
647:MAD:Madagascar
|
||||
649:MLI:Mali
|
||||
650:MOZ:Mozambique
|
||||
654:MAA:Mauritania
|
||||
655:MAW:Malawi
|
||||
656:NIG:Niger
|
||||
657:NIA:Nigeria
|
||||
659:NAM:Namibia
|
||||
660:REU:Reunion
|
||||
661:RWA:Rwanda
|
||||
662:SUD:Sudan
|
||||
663:SEN:Senegal
|
||||
664:SEY:Seychelle
|
||||
665:SHE:St Helena
|
||||
666:SOM:Somali
|
||||
667:SIL:Sierra Leo
|
||||
668:SAO:Sao Tome
|
||||
669:SWZ:Eswatini
|
||||
670:CHA:Chad
|
||||
671:TOG:Togo
|
||||
672:TUN:Tunisia
|
||||
674:TAN:Tanzania
|
||||
675:UGA:Uganda
|
||||
676:ZAI:Zaire
|
||||
677:TAN:Tanzania
|
||||
678:ZAM:Zambia
|
||||
679:ZIM:Zimbabwe
|
||||
701:ARG:Argentina
|
||||
710:BRA:Brazil
|
||||
720:BOL:Bolivia
|
||||
725:CHI:Chile
|
||||
730:COL:Colombia
|
||||
735:ECU:Ecuador
|
||||
740:FAL:Falkland I
|
||||
745:GUI:Guiana
|
||||
750:GUY:Guyana
|
||||
755:PAR:Paraguay
|
||||
760:PER:Peru
|
||||
765:SUR:Suriname
|
||||
770:URU:Uruguay
|
||||
775:VEN:Venezuela
|
||||
Reference in New Issue
Block a user