mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-09-11 00:59:28 +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);
|
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) {
|
void set_p25tx_data(const uint8_t* dibits, uint16_t frame_length) {
|
||||||
const size_t max_len = sizeof(shared_memory.bb_data.data);
|
const size_t max_len = sizeof(shared_memory.bb_data.data);
|
||||||
if (frame_length > max_len) frame_length = max_len;
|
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(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_rtty_config(RTTYDataMessage& message);
|
||||||
void set_epirb_tx_config(EPIRBTXDataMessage& 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 set_p25tx_data(const uint8_t* dibits, uint16_t frame_length);
|
||||||
|
|
||||||
void request_roger_beep();
|
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,
|
||||||
0x00,
|
0x00,
|
||||||
},
|
},
|
||||||
/*.icon_color = */ ui::Color::orange().v,
|
/*.icon_color = */ ui::Color::green().v,
|
||||||
/*.menu_location = */ app_location_t::RX,
|
/*.menu_location = */ app_location_t::RX,
|
||||||
/*.desired_menu_position = */ -1,
|
/*.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
|
* Copyright (C) 2024 EPIRB Decoder Implementation
|
||||||
*
|
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
||||||
* This file is part of PortaPack.
|
*
|
||||||
*
|
* 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
|
* This program is free software; you can redistribute it and/or modify
|
||||||
* the Free Software Foundation; either version 2, or (at your option)
|
* it under the terms of the GNU General Public License as published by
|
||||||
* any later version.
|
* 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
|
* This program is distributed in the hope that it will be useful,
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* GNU General Public License for more details.
|
* 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
|
* You should have received a copy of the GNU General Public License
|
||||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
* along with this program; see the file COPYING. If not, write to
|
||||||
* Boston, MA 02110-1301, USA.
|
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||||
*/
|
* Boston, MA 02110-1301, USA.
|
||||||
|
*/
|
||||||
#ifndef __UI_EPIRB_RX_H__
|
|
||||||
#define __UI_EPIRB_RX_H__
|
#ifndef __UI_EPIRB_RX_H__
|
||||||
|
#define __UI_EPIRB_RX_H__
|
||||||
#include "app_settings.hpp"
|
|
||||||
#include "radio_state.hpp"
|
#include "app_settings.hpp"
|
||||||
#include "ui_widget.hpp"
|
#include "radio_state.hpp"
|
||||||
#include "ui_navigation.hpp"
|
#include "ui_widget.hpp"
|
||||||
#include "ui_receiver.hpp"
|
#include "ui_navigation.hpp"
|
||||||
#include "ui_geomap.hpp"
|
#include "ui_receiver.hpp"
|
||||||
|
#include "ui_geomap.hpp"
|
||||||
#include "event_m0.hpp"
|
|
||||||
#include "signal.hpp"
|
// Specan is disable to keep application size below the 32k limit
|
||||||
#include "message.hpp"
|
// #define SPECAN
|
||||||
#include "log_file.hpp"
|
|
||||||
|
// Comment to disable timout reset on select and save approx 200 bytes of flash
|
||||||
#include "baseband_packet.hpp"
|
#ifndef PRALINE
|
||||||
|
// Application does not fit on Praline with RESET_TIMER enabled
|
||||||
/* #include <cstdint>
|
#define RESET_TIMER
|
||||||
#include <cstddef>
|
#endif
|
||||||
#include <string>
|
// Comment to disable squelch control
|
||||||
#include <array> */
|
#define SQUELCH
|
||||||
|
// Comment to disable beacon selection by encoder on detail tab
|
||||||
namespace ui::external_app::epirb_rx {
|
#define DETAIL_TAB_BEACON_SEL
|
||||||
|
// #define LOGGER
|
||||||
// EPIRB 406 MHz beacon types
|
|
||||||
enum class BeaconType : uint8_t {
|
#ifdef SPECAN
|
||||||
OrbitingLocationBeacon = 0,
|
#include "ui_spectrum.hpp"
|
||||||
PersonalLocatorBeacon = 1,
|
#endif
|
||||||
EmergencyLocatorTransmitter = 2,
|
|
||||||
SerialELT = 3,
|
#include "ui_tabview.hpp"
|
||||||
NationalELT = 4,
|
|
||||||
Other = 15
|
#include "ui_qrcode.hpp"
|
||||||
};
|
|
||||||
|
#include "event_m0.hpp"
|
||||||
// EPIRB distress and emergency types
|
#include "message.hpp"
|
||||||
enum class EmergencyType : uint8_t {
|
#include "log_file.hpp"
|
||||||
Fire = 0,
|
|
||||||
Flooding = 1,
|
#include "baseband_packet.hpp"
|
||||||
Collision = 2,
|
|
||||||
Grounding = 3,
|
#include "audio.hpp"
|
||||||
Sinking = 4,
|
|
||||||
Disabled = 5,
|
#include "beacon.hpp"
|
||||||
Abandoning = 6,
|
#include "beacon_db.hpp"
|
||||||
Piracy = 7,
|
#include "ui_beaconlist.hpp"
|
||||||
Man_Overboard = 8,
|
#include "resources.hpp"
|
||||||
Other = 15
|
|
||||||
};
|
namespace ui::external_app::epirb_rx {
|
||||||
|
|
||||||
struct EPIRBLocation {
|
/**
|
||||||
float latitude; // degrees, -90 to +90
|
* Status of a packet
|
||||||
float longitude; // degrees, -180 to +180
|
*/
|
||||||
bool valid;
|
enum class PacketStatus : uint8_t {
|
||||||
|
Valid = 0,
|
||||||
EPIRBLocation()
|
Corrected = 1,
|
||||||
: latitude(0.0f), longitude(0.0f), valid(false) {}
|
Error = 2
|
||||||
EPIRBLocation(float lat, float lon)
|
};
|
||||||
: latitude(lat), longitude(lon), valid(true) {}
|
|
||||||
};
|
// Position of tabs in tab view
|
||||||
|
#define EPIRB_TAB_POS_Y (UI_POS_Y(4) + 3 * 8)
|
||||||
enum class PacketStatus : uint8_t {
|
// Height of tabs in tab view
|
||||||
Valid = 0,
|
#define EPIRB_TAB_HEIGHT (screen_height - EPIRB_TAB_POS_Y - UI_POS_HEIGHT(1))
|
||||||
Corrected = 1,
|
|
||||||
Error = 2
|
#ifdef LOGGER
|
||||||
};
|
class EPIRBLogger {
|
||||||
|
public:
|
||||||
struct EPIRBBeacon {
|
Optional<File::Error> append(const std::filesystem::path& filename) {
|
||||||
uint32_t beacon_id;
|
return log_file.append(filename);
|
||||||
BeaconType beacon_type;
|
}
|
||||||
EmergencyType emergency_type;
|
|
||||||
EPIRBLocation location;
|
void on_packet(Beacon& beacon);
|
||||||
uint32_t country_code;
|
|
||||||
std::string vessel_name;
|
private:
|
||||||
rtc::RTC timestamp;
|
LogFile log_file{};
|
||||||
uint32_t sequence_number;
|
};
|
||||||
PacketStatus packet_status;
|
#endif
|
||||||
uint8_t error_count;
|
|
||||||
|
/**
|
||||||
EPIRBBeacon()
|
* Dedicated TextArea component used to optimize application code size
|
||||||
: 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 TextArea : public Widget {
|
||||||
|
public:
|
||||||
class EPIRBDecoder {
|
TextArea(Rect parent_rect);
|
||||||
public:
|
|
||||||
static EPIRBBeacon decode_packet(const baseband::Packet& packet);
|
#ifdef RESET_TIMER
|
||||||
|
std::function<void(TextArea&)> on_select{};
|
||||||
private:
|
bool on_key(const KeyEvent key) override;
|
||||||
static EPIRBLocation decode_location(const std::array<uint8_t, 16>& data);
|
#endif
|
||||||
static BeaconType decode_beacon_type(uint8_t type_bits);
|
|
||||||
static EmergencyType decode_emergency_type(uint8_t emergency_bits);
|
void set_content(std::string_view value);
|
||||||
static uint32_t decode_country_code(const std::array<uint8_t, 16>& data);
|
void paint(Painter& painter) override;
|
||||||
static std::string decode_vessel_name(const std::array<uint8_t, 16>& data);
|
|
||||||
|
private:
|
||||||
// BCH error correction methods
|
std::string content{};
|
||||||
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);
|
// Forward declaration
|
||||||
static uint8_t count_bit_errors(const std::array<uint8_t, 16>& original, const std::array<uint8_t, 16>& corrected);
|
class EPIRBAppView;
|
||||||
};
|
|
||||||
|
/**
|
||||||
class EPIRBLogger {
|
* View for beacon detail tab
|
||||||
public:
|
*/
|
||||||
Optional<File::Error> append(const std::filesystem::path& filename) {
|
class EPIRBDetailView : public View {
|
||||||
return log_file.append(filename);
|
public:
|
||||||
}
|
EPIRBDetailView(Rect parent_rect, EPIRBAppView& parent);
|
||||||
|
void set_beacon(Beacon& beacon);
|
||||||
void on_packet(const EPIRBBeacon& beacon);
|
#ifdef DETAIL_TAB_BEACON_SEL
|
||||||
|
bool on_encoder(EncoderEvent delta) override;
|
||||||
private:
|
#endif
|
||||||
LogFile log_file{};
|
|
||||||
};
|
private:
|
||||||
|
TextArea text_beacon{{UI_POS_X(0), UI_POS_Y(0), UI_POS_MAXWIDTH, EPIRB_TAB_HEIGHT}};
|
||||||
// Forward declarations of formatting functions
|
EPIRBAppView& parent_app;
|
||||||
std::string format_beacon_type(BeaconType type);
|
};
|
||||||
std::string format_emergency_type(EmergencyType type);
|
|
||||||
std::string format_packet_status(PacketStatus status);
|
#define EPIRB_RX_DEFAULT_LATITUDE 43.604f
|
||||||
ui::Color get_packet_status_color(PacketStatus status);
|
#define EPIRB_RX_DEFAULT_LONGITUDE 1.458f
|
||||||
|
|
||||||
class EPIRBBeaconDetailView : public ui::View {
|
/**
|
||||||
public:
|
* View for beacon map tab
|
||||||
std::function<void(void)> on_close{};
|
*/
|
||||||
|
class EPIRBMapView : public View {
|
||||||
EPIRBBeaconDetailView(ui::NavigationView& nav);
|
public:
|
||||||
EPIRBBeaconDetailView(const EPIRBBeaconDetailView&) = delete;
|
EPIRBMapView(Rect parent_rect);
|
||||||
EPIRBBeaconDetailView& operator=(const EPIRBBeaconDetailView&) = delete;
|
void paint(Painter& painter) override;
|
||||||
|
void on_show() override;
|
||||||
void set_beacon(const EPIRBBeacon& beacon);
|
void set_main_marker(const std::string& label, float lat, float lon);
|
||||||
const EPIRBBeacon& beacon() const { return beacon_; }
|
void clear_markers();
|
||||||
|
void add_marker(GeoMarker& marker);
|
||||||
void focus() override;
|
void hide_map(bool hide);
|
||||||
void paint(ui::Painter&) override;
|
void repaint();
|
||||||
|
|
||||||
ui::GeoMapView* get_geomap_view() { return geomap_view; }
|
private:
|
||||||
|
GeoMap geomap{{0, 0, UI_POS_MAXWIDTH, EPIRB_TAB_HEIGHT}};
|
||||||
private:
|
float lat_{EPIRB_RX_DEFAULT_LATITUDE};
|
||||||
EPIRBBeacon beacon_{};
|
float lon_{EPIRB_RX_DEFAULT_LONGITUDE};
|
||||||
|
bool map_hidden{true};
|
||||||
ui::Button button_done{
|
};
|
||||||
{125, 224, 96, 24},
|
|
||||||
"Done"};
|
#define QR_WIDTH 126
|
||||||
ui::Button button_see_map{
|
#define QR_HEIGHT 127
|
||||||
{19, 224, 96, 24},
|
|
||||||
"See on map"};
|
/**
|
||||||
|
* Vieaw for Beacon QR tab
|
||||||
ui::GeoMapView* geomap_view{nullptr};
|
*/
|
||||||
|
class EPRIBQRView : public View {
|
||||||
ui::Rect draw_field(
|
public:
|
||||||
ui::Painter& painter,
|
EPRIBQRView(Rect parent_rect);
|
||||||
const ui::Rect& draw_rect,
|
EPRIBQRView(const EPRIBQRView&) = delete;
|
||||||
const ui::Style& style,
|
EPRIBQRView& operator=(const EPRIBQRView&) = delete;
|
||||||
const std::string& label,
|
|
||||||
const std::string& value);
|
void set_beacon(Beacon* beacon);
|
||||||
};
|
void update_qr();
|
||||||
|
void update_display();
|
||||||
class EPIRBAppView : public ui::View {
|
|
||||||
public:
|
private:
|
||||||
EPIRBAppView(ui::NavigationView& nav);
|
bool show_map{true};
|
||||||
~EPIRBAppView();
|
Beacon* current_beacon{nullptr};
|
||||||
|
char qr_url[128];
|
||||||
void set_parent_rect(const ui::Rect new_parent_rect) override;
|
|
||||||
void paint(ui::Painter&) override;
|
OptionsField options_qr{
|
||||||
void focus() override;
|
{UI_POS_X(5), UI_POS_Y(1)},
|
||||||
|
6,
|
||||||
std::string title() const override { return "EPIRB RX"; }
|
{{"Map", 0},
|
||||||
|
{"Detail", 1}}};
|
||||||
private:
|
|
||||||
app_settings::SettingsManager settings_{
|
QRCodeImage qr_code{
|
||||||
"rx_epirb", app_settings::Mode::RX};
|
{UI_POS_MAXWIDTH - QR_WIDTH - UI_POS_X(1), UI_POS_Y(1), QR_WIDTH, QR_HEIGHT}};
|
||||||
|
|
||||||
ui::NavigationView& nav_;
|
TextArea text_data{{UI_POS_X(0), UI_POS_Y(1), UI_POS_MAXWIDTH, EPIRB_TAB_HEIGHT - UI_POS_Y(1)}};
|
||||||
|
};
|
||||||
std::vector<EPIRBBeacon> recent_beacons{};
|
|
||||||
std::unique_ptr<EPIRBLogger> logger{};
|
#ifdef SPECAN
|
||||||
|
class EPIRBRxView : public spectrum::WaterfallView {
|
||||||
EPIRBBeaconDetailView beacon_detail_view{nav_};
|
public:
|
||||||
|
EPIRBRxView(EPIRBAppView& parent, Rect parent_rect);
|
||||||
static constexpr auto header_height = 4 * 16;
|
void on_show() override;
|
||||||
|
void on_hide() override;
|
||||||
ui::Text label_frequency{
|
|
||||||
{UI_POS_X(0), UI_POS_Y(0), 4 * 8, 1 * 16},
|
private:
|
||||||
"Freq"};
|
EPIRBAppView& app_view;
|
||||||
|
};
|
||||||
ui::OptionsField options_frequency{
|
#endif
|
||||||
{5 * 8, UI_POS_Y(0)},
|
|
||||||
7,
|
class EPIRBAppView final : public ui::View {
|
||||||
{
|
public:
|
||||||
{"406.028", 406028000},
|
EPIRBAppView(ui::NavigationView& nav);
|
||||||
{"406.025", 406025000},
|
~EPIRBAppView();
|
||||||
{"406.037", 406037000},
|
|
||||||
{"433.025", 433025000},
|
void focus() override;
|
||||||
{"144.875", 144875000},
|
void refresh();
|
||||||
}};
|
|
||||||
|
// Message to configure rx baseband
|
||||||
ui::RFAmpField field_rf_amp{
|
EPIRBRXConfig epirb_rx_config_message{};
|
||||||
{13 * 8, UI_POS_Y(0)}};
|
void send_config();
|
||||||
|
// Beacons database
|
||||||
ui::LNAGainField field_lna{
|
BeaconDB beacon_db{};
|
||||||
{15 * 8, UI_POS_Y(0)}};
|
// Update display when beacon selection changed0
|
||||||
|
void on_beacon_change();
|
||||||
ui::VGAGainField field_vga{
|
|
||||||
{18 * 8, UI_POS_Y(0)}};
|
std::string title() const override { return "EPIRB RX"; }
|
||||||
|
|
||||||
ui::RSSI rssi{
|
private:
|
||||||
{UI_POS_X(21), 0, UI_POS_WIDTH_REMAINING(24), 4}};
|
uint8_t squelch{50};
|
||||||
|
// The delay between each frame
|
||||||
ui::Channel channel{
|
uint32_t countdown{50};
|
||||||
{UI_POS_X(21), 5, UI_POS_WIDTH_REMAINING(24), 4}};
|
app_settings::SettingsManager settings_{
|
||||||
|
"rx_epirb",
|
||||||
ui::AudioVolumeField field_volume{
|
app_settings::Mode::RX,
|
||||||
{screen_width - 2 * 8, UI_POS_Y(0)}};
|
{
|
||||||
|
{"epirb_squelch"sv, &squelch},
|
||||||
// Status display
|
{"countdown"sv, &countdown},
|
||||||
ui::Text label_status{
|
}};
|
||||||
{UI_POS_X(0), 1 * 16, 15 * 8, 1 * 16},
|
|
||||||
"Listening..."};
|
ui::NavigationView& nav_;
|
||||||
|
|
||||||
ui::Text label_beacons_count{
|
#ifdef LOGGER
|
||||||
{16 * 8, 1 * 16, 14 * 8, 1 * 16},
|
std::unique_ptr<EPIRBLogger> logger{};
|
||||||
"Beacons: 0"};
|
#endif
|
||||||
|
|
||||||
ui::Text label_packet_stats{
|
OptionsField options_frequency{
|
||||||
{UI_POS_X(0), 3 * 16, 29 * 8, 1 * 16},
|
{UI_POS_X(0), UI_POS_Y(0)},
|
||||||
""};
|
7,
|
||||||
|
{}};
|
||||||
// Latest beacon info display
|
|
||||||
ui::Text label_latest{
|
ui::RFAmpField field_rf_amp{
|
||||||
{UI_POS_X(0), 2 * 16, 8 * 8, 1 * 16},
|
{UI_POS_X(8), UI_POS_Y(0)}};
|
||||||
"Latest:"};
|
|
||||||
|
ui::LNAGainField field_lna{
|
||||||
ui::Text text_latest_info{
|
{UI_POS_X(10), UI_POS_Y(0)}};
|
||||||
{8 * 8, 2 * 16, 22 * 8, 1 * 16},
|
|
||||||
""};
|
ui::VGAGainField field_vga{
|
||||||
|
{UI_POS_X(13), UI_POS_Y(0)}};
|
||||||
// Beacon list
|
|
||||||
ui::Console console{
|
ui::RSSI rssi{
|
||||||
{0, 4 * 16, 240, 152}};
|
{UI_POS_X(16), UI_POS_Y(0), UI_POS_WIDTH_REMAINING(22), 4}};
|
||||||
|
|
||||||
ui::Button button_map{
|
ui::Channel channel{
|
||||||
{0, 224, 60, 24},
|
{UI_POS_X(16), UI_POS_Y(0) + 5, UI_POS_WIDTH_REMAINING(22), 4}};
|
||||||
"Map"};
|
|
||||||
|
// ui::Audio audio{
|
||||||
ui::Button button_clear{
|
// {UI_POS_X(16), UI_POS_Y(0) + 10, UI_POS_WIDTH_REMAINING(22), 4}};
|
||||||
{64, 224, 60, 24},
|
|
||||||
"Clear"};
|
ui::AudioVolumeField field_volume{
|
||||||
|
{UI_POS_WIDTH_REMAINING(2), UI_POS_Y(0)}};
|
||||||
ui::Button button_log{
|
|
||||||
{128, 224, 60, 24},
|
#ifdef SQUELCH
|
||||||
"Log"};
|
NumberField field_squelch{
|
||||||
|
{UI_POS_WIDTH_REMAINING(5), UI_POS_Y(0)},
|
||||||
SignalToken signal_token_tick_second{};
|
2,
|
||||||
uint32_t beacons_received = 0;
|
{0, 99},
|
||||||
uint32_t packets_valid = 0;
|
1,
|
||||||
uint32_t packets_corrected = 0;
|
' '};
|
||||||
uint32_t packets_error = 0;
|
#endif
|
||||||
|
|
||||||
MessageHandlerRegistration message_handler_packet{
|
// Status display
|
||||||
Message::ID::EPIRBPacket,
|
TextArea text_status{{UI_POS_X(0), UI_POS_Y(1), UI_POS_MAXWIDTH, UI_POS_HEIGHT(3)}};
|
||||||
[this](Message* const p) {
|
TextArea text_timeout{
|
||||||
const auto message = static_cast<const EPIRBPacketMessage*>(p);
|
{UI_POS_X(13), UI_POS_Y(1), UI_POS_WIDTH(3), UI_POS_HEIGHT(1)}};
|
||||||
this->on_packet(message->packet);
|
SignalToken signal_token_tick_second{};
|
||||||
}};
|
// Timeout string
|
||||||
|
int16_t timeout{0};
|
||||||
void on_packet(const baseband::Packet& packet);
|
|
||||||
void on_beacon_decoded(const EPIRBBeacon& beacon);
|
// Tab View
|
||||||
void on_show_map();
|
Rect view_rect = {0, EPIRB_TAB_POS_Y, UI_POS_MAXWIDTH, EPIRB_TAB_HEIGHT};
|
||||||
void on_clear_beacons();
|
|
||||||
void on_toggle_log();
|
BeaconUIList view_list{view_rect};
|
||||||
void on_tick_second();
|
EPIRBDetailView view_detail{view_rect, (*this)};
|
||||||
|
EPIRBMapView view_map{view_rect};
|
||||||
void update_display();
|
#ifdef SPECAN
|
||||||
std::string format_beacon_summary(const EPIRBBeacon& beacon);
|
EPIRBRxView view_rx{*this, view_rect};
|
||||||
std::string format_location(const EPIRBLocation& location);
|
#endif
|
||||||
};
|
|
||||||
|
EPRIBQRView view_qr{view_rect};
|
||||||
} // namespace ui::external_app::epirb_rx
|
|
||||||
|
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__
|
#endif // __UI_EPIRB_RX_H__
|
||||||
+61
-57
@@ -24,7 +24,6 @@
|
|||||||
#include "portapack.hpp"
|
#include "portapack.hpp"
|
||||||
#include "ui_epirb_tx.hpp"
|
#include "ui_epirb_tx.hpp"
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cctype>
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
|
||||||
namespace ui::external_app::epirb_tx {
|
namespace ui::external_app::epirb_tx {
|
||||||
@@ -37,17 +36,17 @@ namespace ui::external_app::epirb_tx {
|
|||||||
* @param min output minutes value
|
* @param min output minutes value
|
||||||
* @param sec output seconds 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;
|
negative = value < 0;
|
||||||
value = std::fabs(value);
|
value = std::fabs(value);
|
||||||
|
|
||||||
deg = (uint16_t)value;
|
deg = (int16_t)value;
|
||||||
|
|
||||||
double m = (value - deg) * 60.0;
|
float m = (value - deg) * 60.0f;
|
||||||
min = (uint8_t)m;
|
min = (int8_t)m;
|
||||||
|
|
||||||
double s = (m - min) * 60.0;
|
float s = (m - min) * 60.0f;
|
||||||
sec = (uint8_t)(s + 0.5);
|
sec = (int8_t)(s + 0.5f);
|
||||||
|
|
||||||
if (sec == 60) {
|
if (sec == 60) {
|
||||||
sec = 0;
|
sec = 0;
|
||||||
@@ -68,8 +67,8 @@ static void decimal_to_dms(double value, bool& negative, uint16_t& deg, uint8_t&
|
|||||||
* @param sec seconds value
|
* @param sec seconds value
|
||||||
* @return value the decimal value
|
* @return value the decimal value
|
||||||
*/
|
*/
|
||||||
static double dms_to_decimal(bool negative, uint16_t deg, uint8_t min, uint8_t sec) {
|
static float dms_to_decimal(bool negative, int16_t deg, int8_t min, int8_t sec) {
|
||||||
double v = deg + min / 60.0 + sec / 3600.0;
|
float v = deg + min / 60.0f + sec / 3600.0f;
|
||||||
return negative ? -v : v;
|
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
|
* @param lon output longitude
|
||||||
*/
|
*/
|
||||||
static void maidenhead_to_decimal(const std::string& loc, float& lat, float& lon) {
|
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,
|
20.0f,
|
||||||
2.0,
|
2.0f,
|
||||||
1.0 / 12.0,
|
1.0f / 12.0f,
|
||||||
1.0 / 120.0,
|
1.0f / 120.0f,
|
||||||
1.0 / 2880.0};
|
1.0f / 2880.0f};
|
||||||
|
|
||||||
static const double lat_step[] =
|
static const float lat_step[] =
|
||||||
{
|
{
|
||||||
10.0,
|
10.0f,
|
||||||
1.0,
|
1.0f,
|
||||||
1.0 / 24.0,
|
1.0f / 24.0f,
|
||||||
1.0 / 240.0,
|
1.0f / 240.0f,
|
||||||
1.0 / 5760.0};
|
1.0f / 5760.0f};
|
||||||
|
|
||||||
lon = -180.0;
|
lon = -180.0f;
|
||||||
lat = -90.0;
|
lat = -90.0f;
|
||||||
|
|
||||||
int pairs = loc.size() / 2;
|
int pairs = loc.size() / 2;
|
||||||
|
|
||||||
if (pairs > 5)
|
if (pairs > 5)
|
||||||
pairs = 5;
|
pairs = 5;
|
||||||
|
|
||||||
for (int i = 0; i < pairs; i++) {
|
if (pairs > 0) {
|
||||||
char c1 = std::toupper(loc[i * 2]);
|
for (int i = 0; i < pairs; i++) {
|
||||||
char c2 = std::toupper(loc[i * 2 + 1]);
|
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];
|
float lon_size = lon_step[i];
|
||||||
double lat_size = lat_step[i];
|
float lat_size = lat_step[i];
|
||||||
|
|
||||||
int v1, v2;
|
int v1, v2;
|
||||||
|
|
||||||
if (i % 2 == 0) // letters
|
if (i % 2 == 0) // letters
|
||||||
{
|
{
|
||||||
v1 = c1 - 'A';
|
v1 = c1 - 'A';
|
||||||
v2 = c2 - 'A';
|
v2 = c2 - 'A';
|
||||||
} else // digits
|
} else // digits
|
||||||
{
|
{
|
||||||
v1 = c1 - '0';
|
v1 = c1 - '0';
|
||||||
v2 = c2 - '0';
|
v2 = c2 - '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
lon += v1 * lon_size;
|
||||||
|
lat += v2 * lat_size;
|
||||||
}
|
}
|
||||||
|
|
||||||
lon += v1 * lon_size;
|
// Cell center
|
||||||
lat += v2 * lat_size;
|
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
|
* @param precision (optional, defaults to 8) the number of charater for the maidenhead locator
|
||||||
* @return the locator
|
* @return the locator
|
||||||
*/
|
*/
|
||||||
static std::string decimal_to_maidenhead(double lat, double lon, int precision = 8) {
|
static std::string decimal_to_maidenhead(float lat, float lon, int precision = 8) {
|
||||||
lon += 180.0;
|
lon += 180.0f;
|
||||||
lat += 90.0;
|
lat += 90.0f;
|
||||||
|
|
||||||
int A = lon / 20;
|
int A = lon / 20;
|
||||||
int B = lat / 10;
|
int B = lat / 10;
|
||||||
@@ -154,14 +158,14 @@ static std::string decimal_to_maidenhead(double lat, double lon, int precision =
|
|||||||
lon -= C * 2;
|
lon -= C * 2;
|
||||||
lat -= D * 1;
|
lat -= D * 1;
|
||||||
|
|
||||||
int E = lon / (5.0 / 60.0);
|
int E = lon / (5.0f / 60.0f);
|
||||||
int F = lat / (2.5 / 60.0);
|
int F = lat / (2.5f / 60.0f);
|
||||||
|
|
||||||
lon -= E * (5.0 / 60.0);
|
lon -= E * (5.0f / 60.0f);
|
||||||
lat -= F * (2.5 / 60.0);
|
lat -= F * (2.5f / 60.0f);
|
||||||
|
|
||||||
int G = lon / (5.0 / 600.0);
|
int G = lon / (5.0f / 600.0f);
|
||||||
int H = lat / (2.5 / 600.0);
|
int H = lat / (2.5f / 600.0f);
|
||||||
|
|
||||||
std::string locator;
|
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>)
|
* Convert the provided Loaction to it's latitude string (format <xxx°yy'zz"N>)
|
||||||
*/
|
*/
|
||||||
std::string to_latitude_string(const Location& location) {
|
std::string to_latitude_string(const Location& location) {
|
||||||
char buffer[16];
|
char buffer[20];
|
||||||
// Format : <xxx°yy'zz"N>
|
// 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);
|
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>)
|
* Convert the provided Loaction to it's longitude string (format <xxx°yy'zz"W>)
|
||||||
*/
|
*/
|
||||||
std::string to_longitude_string(const Location& location) {
|
std::string to_longitude_string(const Location& location) {
|
||||||
char buffer[16];
|
char buffer[20];
|
||||||
// Format : <xxx°yy'zz"W>
|
// 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);
|
return std::string(buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace ui::external_app::epirb_tx
|
} // 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
|
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
||||||
*
|
*
|
||||||
* This file is part of PortaPack.
|
* This file is part of PortaPack.
|
||||||
*
|
*
|
||||||
* This program is free software; you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation; either version 2, or (at your option)
|
* the Free Software Foundation; either version 2, or (at your option)
|
||||||
* any later version.
|
* any later version.
|
||||||
*
|
*
|
||||||
* This program is distributed in the hope that it will be useful,
|
* This program is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* GNU General Public License for more details.
|
* GNU General Public License for more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program; see the file COPYING. If not, write to
|
* along with this program; see the file COPYING. If not, write to
|
||||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||||
* Boston, MA 02110-1301, USA.
|
* Boston, MA 02110-1301, USA.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef __EPIRB_TX_H__
|
#ifndef __EPIRB_TX_H__
|
||||||
#define __EPIRB_TX_H__
|
#define __EPIRB_TX_H__
|
||||||
|
|
||||||
#include "app_settings.hpp"
|
#include "app_settings.hpp"
|
||||||
#include "radio_state.hpp"
|
#include "radio_state.hpp"
|
||||||
#include "ui.hpp"
|
#include "ui.hpp"
|
||||||
#include "ui_widget.hpp"
|
#include "ui_widget.hpp"
|
||||||
#include "ui_navigation.hpp"
|
#include "ui_navigation.hpp"
|
||||||
#include "ui_transmitter.hpp"
|
#include "ui_transmitter.hpp"
|
||||||
|
|
||||||
#include "portapack.hpp"
|
#include "portapack.hpp"
|
||||||
#include "message.hpp"
|
#include "message.hpp"
|
||||||
#include "tonesets.hpp"
|
#include "tonesets.hpp"
|
||||||
|
|
||||||
#define BEACON_HEXA_SIZE 36
|
#define BEACON_HEXA_SIZE 36
|
||||||
#define BEACON_HEXA_HALF_SIZE 18
|
#define BEACON_HEXA_HALF_SIZE 18
|
||||||
#define BEACON_SIZE 18
|
#define BEACON_SIZE 18
|
||||||
|
|
||||||
#define AM_TEST_FREQUENCY 121375000
|
#define AM_TEST_FREQUENCY 121375000
|
||||||
#define AM_REAL_FREQUENCY 121500000
|
#define AM_REAL_FREQUENCY 121500000
|
||||||
|
|
||||||
#define BPSK_FREQUENCY_HAM 433025000
|
#define BPSK_FREQUENCY_HAM 433025000
|
||||||
#define BPSK_FREQUENCY_B 406025000
|
#define BPSK_FREQUENCY_B 406025000
|
||||||
#define BPSK_FREQUENCY_C 406028000
|
#define BPSK_FREQUENCY_C 406028000
|
||||||
#define BPSK_FREQUENCY_F 406037000
|
#define BPSK_FREQUENCY_F 406037000
|
||||||
#define BPSK_FREQUENCY_G 406040000
|
#define BPSK_FREQUENCY_G 406040000
|
||||||
#define BPSK_FREQUENCY_J 406049000
|
#define BPSK_FREQUENCY_J 406049000
|
||||||
#define BPSK_FREQUENCY_K 406052000
|
#define BPSK_FREQUENCY_K 406052000
|
||||||
#define BPSK_FREQUENCY_N 406061000
|
#define BPSK_FREQUENCY_N 406061000
|
||||||
#define BPSK_FREQUENCY_O 406064000
|
#define BPSK_FREQUENCY_O 406064000
|
||||||
|
|
||||||
namespace ui::external_app::epirb_tx {
|
namespace ui::external_app::epirb_tx {
|
||||||
|
|
||||||
enum class BeaconMode {
|
enum class BeaconMode {
|
||||||
FILE = 0,
|
FILE = 0,
|
||||||
MODE_MANUAL = 1
|
MODE_MANUAL = 1
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class BeaconType {
|
enum class BeaconType {
|
||||||
EPIRB = 0,
|
EPIRB = 0,
|
||||||
ELT = 1,
|
ELT = 1,
|
||||||
PLB = 2
|
PLB = 2
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class BeaconProtocol {
|
enum class BeaconProtocol {
|
||||||
USER = 0,
|
USER = 0,
|
||||||
STANDARD = 1,
|
STANDARD = 1,
|
||||||
NATIONAL = 2
|
NATIONAL = 2
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class AmChannel {
|
enum class AmChannel {
|
||||||
TEST = 0,
|
TEST = 0,
|
||||||
REAL = 1,
|
REAL = 1,
|
||||||
MANUAL = 2
|
MANUAL = 2
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class BpskChannel {
|
enum class BpskChannel {
|
||||||
HAM = 0,
|
HAM = 0,
|
||||||
B = 1,
|
B = 1,
|
||||||
C = 2,
|
C = 2,
|
||||||
F = 3,
|
F = 3,
|
||||||
G = 4,
|
G = 4,
|
||||||
J = 5,
|
J = 5,
|
||||||
K = 6,
|
K = 6,
|
||||||
N = 7,
|
N = 7,
|
||||||
O = 8,
|
O = 8,
|
||||||
MANUAL = 10
|
MANUAL = 10
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Location {
|
struct Location {
|
||||||
std::string locator;
|
std::string locator;
|
||||||
bool south;
|
bool south;
|
||||||
uint16_t lat_deg;
|
int16_t lat_deg;
|
||||||
uint8_t lat_min;
|
int8_t lat_min;
|
||||||
uint8_t lat_sec;
|
int8_t lat_sec;
|
||||||
float latitude;
|
float latitude;
|
||||||
bool west;
|
bool west;
|
||||||
uint16_t long_deg;
|
int16_t long_deg;
|
||||||
uint8_t long_min;
|
int8_t long_min;
|
||||||
uint8_t long_sec;
|
int8_t long_sec;
|
||||||
float longitude;
|
float longitude;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct BeaconParams {
|
struct BeaconParams {
|
||||||
BeaconType type;
|
BeaconType type;
|
||||||
BeaconProtocol protocol;
|
BeaconProtocol protocol;
|
||||||
uint32_t country;
|
uint32_t country;
|
||||||
bool is_test;
|
bool is_test;
|
||||||
bool is_internal;
|
bool is_internal;
|
||||||
bool has_121_5;
|
bool has_121_5;
|
||||||
Location location;
|
Location location;
|
||||||
};
|
};
|
||||||
|
|
||||||
class EPIRBTXAppView : public View {
|
class EPIRBTXAppView : public View {
|
||||||
public:
|
public:
|
||||||
EPIRBTXAppView(NavigationView& nav);
|
EPIRBTXAppView(NavigationView& nav);
|
||||||
~EPIRBTXAppView();
|
~EPIRBTXAppView();
|
||||||
|
|
||||||
void focus() override;
|
void focus() override;
|
||||||
|
|
||||||
std::string title() const override { return "EPIRB TX"; };
|
std::string title() const override { return "EPIRB TX"; };
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void start_tx();
|
void start_tx();
|
||||||
void stop_tx();
|
void stop_tx();
|
||||||
void update_config();
|
void update_config();
|
||||||
void on_tx_progress(const uint32_t progress, const bool done);
|
void on_tx_progress(const uint32_t progress, const bool done);
|
||||||
void on_timer();
|
void on_timer();
|
||||||
void load_beacons();
|
void load_beacons();
|
||||||
void set_tx_button_state(bool active);
|
void set_tx_button_state(bool active);
|
||||||
std::string frame_to_hex_string(bool start);
|
std::string frame_to_hex_string(bool start);
|
||||||
void generate_frame(BeaconParams params);
|
void generate_frame(BeaconParams params);
|
||||||
void update_frame(bool updateConfig = true);
|
void update_frame(bool updateConfig = true);
|
||||||
void update_bpsk_frequency();
|
void update_bpsk_frequency();
|
||||||
void update_am_transmission();
|
void update_am_transmission();
|
||||||
void update_mode();
|
void update_mode();
|
||||||
void update_location(bool updateLocatorField = true);
|
void update_location(bool updateLocatorField = true);
|
||||||
|
|
||||||
struct Beacon {
|
struct Beacon {
|
||||||
std::string title{};
|
std::string title{};
|
||||||
std::string description{};
|
std::string description{};
|
||||||
std::string frame{};
|
std::string frame{};
|
||||||
};
|
};
|
||||||
std::vector<Beacon> beacons{};
|
NavigationView& nav_;
|
||||||
Beacon default_beacon{"Self test", "Serial User Location Protocol", "FFFED0D6E6202820000C29FF51041775302D"};
|
|
||||||
|
std::vector<Beacon> beacons{};
|
||||||
BeaconParams beacon_params{BeaconType::ELT, BeaconProtocol::STANDARD, 227, true, true, true, {"JN03RO", false, 0, 0, 0, 0, false, 0, 0, 0, 0}};
|
Beacon default_beacon{"Self test", "Serial User Location Protocol", "FFFED0D6E6202820000C29FF51041775302D"};
|
||||||
|
|
||||||
// Currently selected beacon index (from BEACONS.TXT file / options_frame combo)
|
BeaconParams beacon_params{BeaconType::ELT, BeaconProtocol::STANDARD, 227, true, true, true, {"JN03RO", false, 0, 0, 0, 0, false, 0, 0, 0, 0}};
|
||||||
uint32_t selected_beacon{0};
|
|
||||||
|
// Currently selected beacon index (from BEACONS.TXT file / options_frame combo)
|
||||||
// Frequency of the transmitter before starting the app (used to restore frequency when leaving)
|
uint32_t selected_beacon{0};
|
||||||
rf::Frequency original_frequency{0};
|
|
||||||
// Frequency of the AM emergency signal
|
// Frequency of the transmitter before starting the app (used to restore frequency when leaving)
|
||||||
rf::Frequency am_frequency{AM_TEST_FREQUENCY};
|
rf::Frequency original_frequency{0};
|
||||||
// Frequency of the 406 MHz BPSK signal
|
// Frequency of the AM emergency signal
|
||||||
rf::Frequency bpsk_frequency{BPSK_FREQUENCY_HAM};
|
rf::Frequency am_frequency{AM_TEST_FREQUENCY};
|
||||||
// Selected am channel
|
// Frequency of the 406 MHz BPSK signal
|
||||||
uint8_t am_channel{(uint8_t)AmChannel::TEST};
|
rf::Frequency bpsk_frequency{BPSK_FREQUENCY_HAM};
|
||||||
// Selected bpsk channel
|
// Selected am channel
|
||||||
uint8_t bpsk_channel{(uint8_t)BpskChannel::HAM};
|
uint8_t am_channel{(uint8_t)AmChannel::TEST};
|
||||||
// Manual AM frequency value
|
// Selected bpsk channel
|
||||||
rf::Frequency manual_am_frequency{AM_TEST_FREQUENCY};
|
uint8_t bpsk_channel{(uint8_t)BpskChannel::HAM};
|
||||||
// Manual BPSK frequency value
|
// Manual AM frequency value
|
||||||
rf::Frequency manual_bpsk_frequency{BPSK_FREQUENCY_HAM};
|
rf::Frequency manual_am_frequency{AM_TEST_FREQUENCY};
|
||||||
|
// Manual BPSK frequency value
|
||||||
// True when using a beacon from the BEACONS.TXT file
|
rf::Frequency manual_bpsk_frequency{BPSK_FREQUENCY_HAM};
|
||||||
bool mode_file{false};
|
|
||||||
// True when looping on sending beacons is enabled
|
// True when using a beacon from the BEACONS.TXT file
|
||||||
bool loop_enabled{true};
|
bool mode_file{false};
|
||||||
// True if AM emergency signal transmission is enabled
|
// True when slideshow is enabled for file mode (change beacon after every transmission)
|
||||||
bool am_enabled{true};
|
bool slideshow_enabled{false};
|
||||||
// True if we want to send a new frame each time the user changes the current beacon
|
// True when looping on sending beacons is enabled
|
||||||
bool send_on_change{true};
|
bool loop_enabled{true};
|
||||||
// The current locator string
|
// True if AM emergency signal transmission is enabled
|
||||||
std::string locator{"JN03RO"};
|
bool am_enabled{true};
|
||||||
// The delay between each frame when on loop mode
|
// True if we want to send a new frame each time the user changes the current beacon
|
||||||
uint32_t delay{50};
|
bool send_on_change{true};
|
||||||
uint8_t beacon_type{(uint8_t)BeaconType::EPIRB};
|
// The current locator string
|
||||||
// Currently selected beacon protocol
|
std::string locator{"JN03RO"};
|
||||||
uint8_t beacon_protocol{(uint8_t)BeaconProtocol::USER};
|
// The delay between each frame when on loop mode
|
||||||
// Currently selected beacon country
|
uint32_t delay{50};
|
||||||
uint32_t beacon_country{227};
|
uint8_t beacon_type{(uint8_t)BeaconType::EPIRB};
|
||||||
// Current beacon's internal state (true for internal location system)
|
// Currently selected beacon protocol
|
||||||
bool beacon_internal{true};
|
uint8_t beacon_protocol{(uint8_t)BeaconProtocol::USER};
|
||||||
|
// Currently selected beacon country
|
||||||
TxRadioState radio_state_{
|
uint32_t beacon_country{227};
|
||||||
0 /* frequency */,
|
// Current beacon's internal state (true for internal location system)
|
||||||
1750000 /* bandwidth */,
|
bool beacon_internal{true};
|
||||||
TONES_SAMPLERATE /* sampling rate */
|
|
||||||
};
|
TxRadioState radio_state_{
|
||||||
app_settings::SettingsManager settings_{
|
0 /* frequency */,
|
||||||
"tx_epirb",
|
1750000 /* bandwidth */,
|
||||||
app_settings::Mode::TX,
|
TONES_SAMPLERATE /* sampling rate */
|
||||||
{
|
};
|
||||||
{"sbeacon"sv, &selected_beacon},
|
app_settings::SettingsManager settings_{
|
||||||
{"amfreq"sv, &am_frequency},
|
"tx_epirb",
|
||||||
{"bpskfreq"sv, &bpsk_frequency},
|
app_settings::Mode::TX,
|
||||||
{"amchan"sv, &am_channel},
|
{
|
||||||
{"bpskchan"sv, &bpsk_channel},
|
{"sbeacon"sv, &selected_beacon},
|
||||||
{"loop"sv, &loop_enabled},
|
{"amfreq"sv, &am_frequency},
|
||||||
{"delay"sv, &delay},
|
{"bpskfreq"sv, &bpsk_frequency},
|
||||||
{"file"sv, &mode_file},
|
{"amchan"sv, &am_channel},
|
||||||
{"am"sv, &am_enabled},
|
{"bpskchan"sv, &bpsk_channel},
|
||||||
{"soc"sv, &send_on_change},
|
{"loop"sv, &loop_enabled},
|
||||||
{"type"sv, &beacon_type},
|
{"delay"sv, &delay},
|
||||||
{"proto"sv, &beacon_protocol},
|
{"file"sv, &mode_file},
|
||||||
{"country"sv, &beacon_country},
|
{"slideshow"sv, &slideshow_enabled},
|
||||||
{"internal"sv, &beacon_internal},
|
{"am"sv, &am_enabled},
|
||||||
{"locator"sv, &locator},
|
{"soc"sv, &send_on_change},
|
||||||
}};
|
{"type"sv, &beacon_type},
|
||||||
|
{"proto"sv, &beacon_protocol},
|
||||||
// Time of the last sent frame
|
{"country"sv, &beacon_country},
|
||||||
uint32_t last_frame_time{0};
|
{"internal"sv, &beacon_internal},
|
||||||
// True when transmission is enabled
|
{"locator"sv, &locator},
|
||||||
bool transmitting{false};
|
}};
|
||||||
// True when transmitting a BPSK frame
|
|
||||||
bool transmitting_bpsk{false};
|
// Time of the last sent frame
|
||||||
// True when currently looping on sending beacons
|
uint32_t last_frame_time{0};
|
||||||
bool loop{false};
|
// True when transmission is enabled
|
||||||
|
bool transmitting{false};
|
||||||
// Current EPIRBTXDataMessage for baseband
|
// True when transmitting a BPSK frame
|
||||||
EPIRBTXDataMessage epirb_tx_message{};
|
bool transmitting_bpsk{false};
|
||||||
|
// True when currently looping on sending beacons
|
||||||
const size_t max_text_width = UI_POS_WIDTH_REMAINING(6) / UI_POS_DEFAULT_WIDTH;
|
bool loop{false};
|
||||||
const size_t max_text_width_ext = UI_POS_WIDTH_REMAINING(0) / UI_POS_DEFAULT_WIDTH;
|
|
||||||
|
// Current EPIRBTXDataMessage for baseband
|
||||||
Labels labels{
|
EPIRBTXDataMessage epirb_tx_message{};
|
||||||
{{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},
|
const size_t max_text_width = UI_POS_WIDTH_REMAINING(6) / UI_POS_DEFAULT_WIDTH;
|
||||||
{{UI_POS_X(0), UI_POS_Y(10)}, "Next frame in s.", Theme::getInstance()->fg_light->foreground},
|
const size_t max_text_width_ext = UI_POS_WIDTH_REMAINING(0) / UI_POS_DEFAULT_WIDTH;
|
||||||
{{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},
|
Labels labels{
|
||||||
{{UI_POS_X(0), UI_POS_Y(15)}, "BPSK chan.:", Theme::getInstance()->fg_light->foreground},
|
{{UI_POS_X(0), UI_POS_Y(0)}, "Source:", Theme::getInstance()->fg_light->foreground},
|
||||||
{{UI_POS_X(17), UI_POS_Y(9)}, "s.", 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},
|
||||||
// For file mode
|
{{UI_POS_X(0), UI_POS_Y(12)}, "AM frequency: MHz", Theme::getInstance()->fg_light->foreground},
|
||||||
Text text_beacon{
|
{{UI_POS_X(0), UI_POS_Y(14)}, "AM chan.:", Theme::getInstance()->fg_light->foreground},
|
||||||
{UI_POS_X(0), UI_POS_Y(1), UI_POS_WIDTH(7), UI_POS_DEFAULT_HEIGHT},
|
{{UI_POS_X(0), UI_POS_Y(15)}, "BPSK chan.:", Theme::getInstance()->fg_light->foreground},
|
||||||
"Beacon:"};
|
{{UI_POS_X(17), UI_POS_Y(9)}, "s.", Theme::getInstance()->fg_light->foreground}};
|
||||||
// Beacon selection from BEACONS.TXT
|
|
||||||
OptionsField options_frame{
|
// For file mode
|
||||||
{UI_POS_X(7), UI_POS_Y(1)},
|
struct FileModeWidgets {
|
||||||
30,
|
Text text_beacon{
|
||||||
{}};
|
{UI_POS_X(0), UI_POS_Y(1), UI_POS_WIDTH(7), UI_POS_DEFAULT_HEIGHT},
|
||||||
Text text_description_label{
|
"Beacon:"};
|
||||||
{UI_POS_X(0), UI_POS_Y(2), UI_POS_WIDTH(12), UI_POS_DEFAULT_HEIGHT},
|
// Beacon selection from BEACONS.TXT
|
||||||
"Description:"};
|
OptionsField options_frame{
|
||||||
Text text_description{
|
{UI_POS_X(7), UI_POS_Y(1)},
|
||||||
{UI_POS_X(0), UI_POS_Y(3), UI_POS_WIDTH_REMAINING(0), UI_POS_DEFAULT_HEIGHT},
|
30,
|
||||||
""};
|
{}};
|
||||||
Text text_description_end{
|
Text text_description_label{
|
||||||
{UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH_REMAINING(0), UI_POS_DEFAULT_HEIGHT},
|
{UI_POS_X(0), UI_POS_Y(2), UI_POS_WIDTH(12), UI_POS_DEFAULT_HEIGHT},
|
||||||
""};
|
"Description:"};
|
||||||
|
Text text_description{
|
||||||
// For manual mode
|
{UI_POS_X(0), UI_POS_Y(3), UI_POS_WIDTH_REMAINING(0), UI_POS_DEFAULT_HEIGHT},
|
||||||
Text text_beacon_type{
|
""};
|
||||||
{UI_POS_X(0), UI_POS_Y(1), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
|
Text text_description_end{
|
||||||
"Type:"};
|
{UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH_REMAINING(0), UI_POS_DEFAULT_HEIGHT},
|
||||||
Text text_beacon_country{
|
""};
|
||||||
{UI_POS_X(0), UI_POS_Y(2), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
|
Checkbox checkbox_slideshow{
|
||||||
"Country:"};
|
{UI_POS_X(0), UI_POS_Y(5)},
|
||||||
Checkbox checkbox_beacon_internal{
|
9,
|
||||||
{UI_POS_X_RIGHT(12), UI_POS_Y(2)},
|
"Slideshow",
|
||||||
12,
|
true};
|
||||||
"Internal",
|
};
|
||||||
true};
|
std::unique_ptr<FileModeWidgets> file_mode_ui{};
|
||||||
Text text_beacon_locator{
|
|
||||||
{UI_POS_X(0), UI_POS_Y(3), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
|
// For manual mode
|
||||||
"Locator:"};
|
Text text_beacon_type{
|
||||||
Text text_beacon_latitude{
|
{UI_POS_X(0), UI_POS_Y(1), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
|
||||||
{UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
|
"Type:"};
|
||||||
"Lat.:"};
|
Text text_beacon_country{
|
||||||
Text text_beacon_longitude{
|
{UI_POS_X(0), UI_POS_Y(2), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
|
||||||
{UI_POS_X(0), UI_POS_Y(5), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
|
"Country:"};
|
||||||
"Long.:"};
|
Checkbox checkbox_beacon_internal{
|
||||||
Text text_beacon_latitude_value{
|
{UI_POS_X_RIGHT(12), UI_POS_Y(2)},
|
||||||
{UI_POS_X(7), UI_POS_Y(4), UI_POS_WIDTH(12), UI_POS_DEFAULT_HEIGHT},
|
12,
|
||||||
""};
|
"Internal",
|
||||||
Text text_beacon_longitude_value{
|
true};
|
||||||
{UI_POS_X(7), UI_POS_Y(5), UI_POS_WIDTH(12), UI_POS_DEFAULT_HEIGHT},
|
Text text_beacon_locator{
|
||||||
""};
|
{UI_POS_X(0), UI_POS_Y(3), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
|
||||||
OptionsField options_beacon_type{
|
"Locator:"};
|
||||||
{UI_POS_X(9), UI_POS_Y(1)},
|
Text text_beacon_latitude{
|
||||||
7,
|
{UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
|
||||||
{{"EPIRB", (uint8_t)BeaconType::EPIRB},
|
"Lat.:"};
|
||||||
{"ELT", (uint8_t)BeaconType::ELT},
|
Text text_beacon_longitude{
|
||||||
{"PLB", (uint8_t)BeaconType::PLB}}};
|
{UI_POS_X(0), UI_POS_Y(5), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
|
||||||
OptionsField options_beacon_protocol{
|
"Long.:"};
|
||||||
{UI_POS_X(9 + 7), UI_POS_Y(1)},
|
Text text_beacon_latitude_value{
|
||||||
30,
|
{UI_POS_X(7), UI_POS_Y(4), UI_POS_WIDTH(12), UI_POS_DEFAULT_HEIGHT},
|
||||||
{{"User", (uint8_t)BeaconProtocol::USER},
|
""};
|
||||||
{"Standard", (uint8_t)BeaconProtocol::STANDARD},
|
Text text_beacon_longitude_value{
|
||||||
{"National", (uint8_t)BeaconProtocol::NATIONAL}}};
|
{UI_POS_X(7), UI_POS_Y(5), UI_POS_WIDTH(12), UI_POS_DEFAULT_HEIGHT},
|
||||||
OptionsField options_beacon_country{
|
""};
|
||||||
{UI_POS_X(9), UI_POS_Y(2)},
|
OptionsField options_beacon_type{
|
||||||
7,
|
{UI_POS_X(9), UI_POS_Y(1)},
|
||||||
{{"France", 227},
|
7,
|
||||||
{"USA", 366},
|
{{"EPIRB", (uint8_t)BeaconType::EPIRB},
|
||||||
{"Germany", 211},
|
{"ELT", (uint8_t)BeaconType::ELT},
|
||||||
{"Russia", 273},
|
{"PLB", (uint8_t)BeaconType::PLB}}};
|
||||||
{"Spain", 224},
|
OptionsField options_beacon_protocol{
|
||||||
{"Japan", 431},
|
{UI_POS_X(9 + 7), UI_POS_Y(1)},
|
||||||
{"UK", 232}}};
|
30,
|
||||||
TextField text_field_beacon_locator{
|
{{"User", (uint8_t)BeaconProtocol::USER},
|
||||||
{UI_POS_X(9), UI_POS_Y(3), UI_POS_WIDTH(10), UI_POS_DEFAULT_HEIGHT},
|
{"Standard", (uint8_t)BeaconProtocol::STANDARD},
|
||||||
"JN03RO"};
|
{"National", (uint8_t)BeaconProtocol::NATIONAL}}};
|
||||||
Button button_mangps{
|
OptionsField options_beacon_country{
|
||||||
{UI_POS_X_RIGHT(9), UI_POS_Y(3), UI_POS_WIDTH(9), UI_POS_HEIGHT(2)},
|
{UI_POS_X(9), UI_POS_Y(2)},
|
||||||
"Set pos."};
|
7,
|
||||||
|
{{"France", 227},
|
||||||
// Mode selection (file/manual)
|
{"USA", 366},
|
||||||
OptionsField options_mode{
|
{"Germany", 211},
|
||||||
{UI_POS_X(7), UI_POS_Y(0)},
|
{"Russia", 273},
|
||||||
30,
|
{"Spain", 224},
|
||||||
{{"File (BEACONS.TXT)", (uint8_t)BeaconMode::FILE},
|
{"Japan", 431},
|
||||||
{"Manual (Editor)", (uint8_t)BeaconMode::MODE_MANUAL}}};
|
{"UK", 232}}};
|
||||||
|
TextField text_field_beacon_locator{
|
||||||
// Frame content
|
{UI_POS_X(9), UI_POS_Y(3), UI_POS_WIDTH(10), UI_POS_DEFAULT_HEIGHT},
|
||||||
Text text_frame{
|
"JN03RO"};
|
||||||
{UI_POS_X(6), UI_POS_Y(6), UI_POS_WIDTH_REMAINING(6), UI_POS_DEFAULT_HEIGHT},
|
Button button_mangps{
|
||||||
""};
|
{UI_POS_X_RIGHT(9), UI_POS_Y(3), UI_POS_WIDTH(9), UI_POS_HEIGHT(2)},
|
||||||
Text text_frame_end{
|
"Set pos."};
|
||||||
{UI_POS_X(6), UI_POS_Y(7), UI_POS_WIDTH_REMAINING(6), UI_POS_DEFAULT_HEIGHT},
|
|
||||||
""};
|
// Mode selection (file/manual)
|
||||||
|
OptionsField options_mode{
|
||||||
Text text_timeout{
|
{UI_POS_X(7), UI_POS_Y(0)},
|
||||||
{UI_POS_X(14), UI_POS_Y(10), UI_POS_WIDTH(2), UI_POS_DEFAULT_HEIGHT},
|
30,
|
||||||
""};
|
{{"File (BEACONS.TXT)", (uint8_t)BeaconMode::FILE},
|
||||||
|
{"Manual (Editor)", (uint8_t)BeaconMode::MODE_MANUAL}}};
|
||||||
// Transmission settings
|
|
||||||
Checkbox checkbox_loop{
|
// Frame content
|
||||||
{UI_POS_X(0), UI_POS_Y(9)},
|
Text text_frame{
|
||||||
10,
|
{UI_POS_X(6), UI_POS_Y(6), UI_POS_WIDTH_REMAINING(6), UI_POS_DEFAULT_HEIGHT},
|
||||||
"Resend every",
|
""};
|
||||||
true};
|
Text text_frame_end{
|
||||||
NumberField field_delay{
|
{UI_POS_X(6), UI_POS_Y(7), UI_POS_WIDTH_REMAINING(6), UI_POS_DEFAULT_HEIGHT},
|
||||||
{UI_POS_X(15), UI_POS_Y(9)},
|
""};
|
||||||
2,
|
|
||||||
{1, 99},
|
Text text_timeout{
|
||||||
1,
|
{UI_POS_X(14), UI_POS_Y(10), UI_POS_WIDTH(2), UI_POS_DEFAULT_HEIGHT},
|
||||||
' '};
|
""};
|
||||||
Checkbox checkbox_am{
|
|
||||||
{UI_POS_X(0), UI_POS_Y(11)},
|
// Transmission settings
|
||||||
10,
|
Checkbox checkbox_loop{
|
||||||
"AM signal",
|
{UI_POS_X(0), UI_POS_Y(9)},
|
||||||
true};
|
10,
|
||||||
FrequencyField field_am_frequency{
|
"Resend every",
|
||||||
{UI_POS_X(13), UI_POS_Y(12)}};
|
true};
|
||||||
Checkbox checkbox_send_on_change{
|
NumberField field_delay{
|
||||||
{UI_POS_X(0), UI_POS_Y(13)},
|
{UI_POS_X(15), UI_POS_Y(9)},
|
||||||
14,
|
2,
|
||||||
"Send on change",
|
{1, 99},
|
||||||
true};
|
1,
|
||||||
Button button_tx{
|
' '};
|
||||||
{UI_POS_X_RIGHT(9), UI_POS_Y(9), UI_POS_WIDTH(9), UI_POS_HEIGHT(2)},
|
Checkbox checkbox_am{
|
||||||
"START"};
|
{UI_POS_X(0), UI_POS_Y(11)},
|
||||||
const Style& style_tx_start = *Theme::getInstance()->fg_green;
|
10,
|
||||||
const Style& style_tx_stop = *Theme::getInstance()->fg_red;
|
"AM signal",
|
||||||
OptionsField options_am_channel{
|
true};
|
||||||
{UI_POS_X(11), UI_POS_Y(14)},
|
FrequencyField field_am_frequency{
|
||||||
20,
|
{UI_POS_X(13), UI_POS_Y(12)}};
|
||||||
{{"121.375 MHz (Test)", 0},
|
Checkbox checkbox_send_on_change{
|
||||||
{"121.500 MHz /!\\Real", 1},
|
{UI_POS_X(0), UI_POS_Y(13)},
|
||||||
{"Manual", 2}}};
|
14,
|
||||||
OptionsField options_bpsk_channel{
|
"Send on change",
|
||||||
{UI_POS_X(11), UI_POS_Y(15)},
|
true};
|
||||||
20,
|
Button button_tx{
|
||||||
{{"433.025 MHz (Ham)", (uint8_t)BpskChannel::HAM},
|
{UI_POS_X_RIGHT(9), UI_POS_Y(9), UI_POS_WIDTH(9), UI_POS_HEIGHT(2)},
|
||||||
{"406.025 MHz (B)", (uint8_t)BpskChannel::B},
|
"START"};
|
||||||
{"406.028 MHz (C)", (uint8_t)BpskChannel::C},
|
const Style& style_tx_start = *Theme::getInstance()->fg_green;
|
||||||
{"406.037 MHz (F)", (uint8_t)BpskChannel::F},
|
const Style& style_tx_stop = *Theme::getInstance()->fg_red;
|
||||||
{"406.040 MHz (G)", (uint8_t)BpskChannel::G},
|
OptionsField options_am_channel{
|
||||||
{"406.049 MHz (J)", (uint8_t)BpskChannel::J},
|
{UI_POS_X(11), UI_POS_Y(14)},
|
||||||
{"406.052 MHz (K)", (uint8_t)BpskChannel::K},
|
20,
|
||||||
{"406.061 MHz (N)", (uint8_t)BpskChannel::N},
|
{{"121.375 MHz (Test)", 0},
|
||||||
{"406.064 MHz (O)", (uint8_t)BpskChannel::O},
|
{"121.500 MHz /!\\Real", 1},
|
||||||
{"Manual", (uint8_t)BpskChannel::MANUAL}}};
|
{"Manual", 2}}};
|
||||||
|
OptionsField options_bpsk_channel{
|
||||||
// Transmitter view
|
{UI_POS_X(11), UI_POS_Y(15)},
|
||||||
TransmitterView tx_view{
|
20,
|
||||||
(int16_t)UI_POS_Y_BOTTOM(4),
|
{{"433.025 MHz (Ham)", (uint8_t)BpskChannel::HAM},
|
||||||
10000,
|
{"406.025 MHz (B)", (uint8_t)BpskChannel::B},
|
||||||
12};
|
{"406.028 MHz (C)", (uint8_t)BpskChannel::C},
|
||||||
|
{"406.037 MHz (F)", (uint8_t)BpskChannel::F},
|
||||||
MessageHandlerRegistration message_handler_tx_progress{
|
{"406.040 MHz (G)", (uint8_t)BpskChannel::G},
|
||||||
Message::ID::TXProgress,
|
{"406.049 MHz (J)", (uint8_t)BpskChannel::J},
|
||||||
[this](const Message* const p) {
|
{"406.052 MHz (K)", (uint8_t)BpskChannel::K},
|
||||||
const auto message = *reinterpret_cast<const TXProgressMessage*>(p);
|
{"406.061 MHz (N)", (uint8_t)BpskChannel::N},
|
||||||
this->on_tx_progress(message.progress, message.done);
|
{"406.064 MHz (O)", (uint8_t)BpskChannel::O},
|
||||||
}};
|
{"Manual", (uint8_t)BpskChannel::MANUAL}}};
|
||||||
|
|
||||||
MessageHandlerRegistration message_handler_frame_sync{
|
// Transmitter view
|
||||||
// Use frame sync for our applcation timer callback
|
TransmitterView tx_view{
|
||||||
Message::ID::DisplayFrameSync,
|
(int16_t)UI_POS_Y_BOTTOM(4),
|
||||||
[this](const Message* const) {
|
10000,
|
||||||
this->on_timer();
|
12};
|
||||||
}};
|
|
||||||
};
|
MessageHandlerRegistration message_handler_tx_progress{
|
||||||
|
Message::ID::TXProgress,
|
||||||
} // namespace ui::external_app::epirb_tx
|
[this](const Message* const p) {
|
||||||
|
const auto message = *reinterpret_cast<const TXProgressMessage*>(p);
|
||||||
#endif /*__EPIRB_TX_H__*/
|
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
|
#epirb_rx 168 byte flash
|
||||||
external/epirb_rx/main.cpp
|
external/epirb_rx/main.cpp
|
||||||
external/epirb_rx/ui_epirb_rx.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
|
#epirb_tx
|
||||||
external/epirb_tx/main.cpp
|
external/epirb_tx/main.cpp
|
||||||
|
|||||||
@@ -1,96 +1,272 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 EPIRB Receiver Implementation
|
* Copyright (C) 2024 EPIRB Receiver Implementation
|
||||||
*
|
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
||||||
* This file is part of PortaPack.
|
*
|
||||||
*
|
* 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
|
* This program is free software; you can redistribute it and/or modify
|
||||||
* the Free Software Foundation; either version 2, or (at your option)
|
* it under the terms of the GNU General Public License as published by
|
||||||
* any later version.
|
* 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
|
* This program is distributed in the hope that it will be useful,
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* GNU General Public License for more details.
|
* 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
|
* You should have received a copy of the GNU General Public License
|
||||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
* along with this program; see the file COPYING. If not, write to
|
||||||
* Boston, MA 02110-1301, USA.
|
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||||
*/
|
* Boston, MA 02110-1301, USA.
|
||||||
|
*/
|
||||||
#include "proc_epirb.hpp"
|
|
||||||
|
#include "proc_epirb.hpp"
|
||||||
#include "portapack_shared_memory.hpp"
|
|
||||||
|
#include "portapack_shared_memory.hpp"
|
||||||
#include "dsp_fir_taps.hpp"
|
|
||||||
|
#include "dsp_fir_taps.hpp"
|
||||||
#include "event_m4.hpp"
|
|
||||||
#include <ch.h>
|
#include "audio_dma.hpp"
|
||||||
|
|
||||||
EPIRBProcessor::EPIRBProcessor() {
|
#include "event_m4.hpp"
|
||||||
// Configure the decimation filters for narrowband EPIRB signal
|
#include <ch.h>
|
||||||
// Target: Reduce 2.457600 MHz to ~38.4 kHz for 400 bps processing
|
|
||||||
decim_0.configure(taps_11k0_decim_0.taps);
|
EPIRBProcessor::EPIRBProcessor() {
|
||||||
decim_1.configure(taps_11k0_decim_1.taps);
|
// Configure the decimation filters for narrowband EPIRB signal
|
||||||
baseband_thread.start();
|
decim_0.configure(taps_11k0_decim_0.taps);
|
||||||
}
|
decim_1.configure(taps_11k0_decim_1.taps);
|
||||||
|
// Configure channel filter for audio filtering
|
||||||
void EPIRBProcessor::execute(const buffer_c8_t& buffer) {
|
channel_filter.configure(taps_11k0_channel.taps, 2);
|
||||||
/* 2.4576MHz, 2048 samples */
|
// Configure demodulation for audio output
|
||||||
|
demod.configure(SAMPLE_RATE, 5000);
|
||||||
// First decimation stage: 2.4576 MHz -> 307.2 kHz
|
// Configure audio output (+squelch level)
|
||||||
const auto decim_0_out = decim_0.execute(buffer, dst_buffer);
|
configure_audio();
|
||||||
|
#ifdef SPECAN
|
||||||
// Second decimation stage: 307.2 kHz -> 38.4 kHz
|
channel_spectrum.set_decimation_factor(1);
|
||||||
const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer);
|
#endif
|
||||||
const auto decimator_out = decim_1_out;
|
baseband_thread.start();
|
||||||
|
}
|
||||||
/* 38.4kHz, 32 samples (approximately) */
|
|
||||||
feed_channel_stats(decimator_out);
|
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
|
||||||
// Process each decimated sample through the matched filter
|
audio_output.configure(audio_24k_hpf_300hz_config, audio_24k_deemph_300_6_config, ((float)squelch_level) / 40.0f);
|
||||||
for (size_t i = 0; i < decimator_out.count; i++) {
|
}
|
||||||
// Apply matched filter for BPSK demodulation
|
|
||||||
if (mf.execute_once(decimator_out.p[i])) {
|
float EPIRBProcessor::get_phase_diff(const complex16_t& sample0, const complex16_t& sample1) {
|
||||||
// Feed symbol to clock recovery when matched filter triggers
|
// Calculate the phase difference between two samples
|
||||||
clock_recovery(mf.get_output());
|
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;
|
||||||
void EPIRBProcessor::consume_symbol(const float raw_symbol) {
|
if (phase_diff < -M_PI) phase_diff += 2.0f * M_PI;
|
||||||
// BPSK demodulation: positive = 1, negative = 0
|
return phase_diff;
|
||||||
const uint_fast8_t sliced_symbol = (raw_symbol >= 0.0f) ? 1 : 0;
|
}
|
||||||
|
|
||||||
// Decode bi-phase L encoding manually
|
bool EPIRBProcessor::filtered_rise_detect(bool condition) {
|
||||||
// In bi-phase L: 0 = no transition, 1 = transition
|
bool result = false;
|
||||||
// This is a simple edge detector
|
if (condition) {
|
||||||
const auto decoded_symbol = sliced_symbol ^ last_symbol;
|
// If rise condition is matched, filter peaks that last less than 3 samples
|
||||||
last_symbol = sliced_symbol;
|
rise_detection_count++;
|
||||||
|
if (rise_detection_count >= RISE_FILTER_SAMPLES) {
|
||||||
// Build packet from decoded symbols
|
result = true;
|
||||||
packet_builder.execute(decoded_symbol);
|
rise_detection_count = 0;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
void EPIRBProcessor::payload_handler(const baseband::Packet& packet) {
|
rise_detection_count = 0;
|
||||||
// EPIRB packet received - validate and process
|
}
|
||||||
if (packet.size() >= 112) { // Minimum EPIRB data payload size (112 bits)
|
return result;
|
||||||
packets_received++;
|
}
|
||||||
last_packet_timestamp = Timestamp::now();
|
|
||||||
|
void EPIRBProcessor::execute(const buffer_c8_t& buffer) {
|
||||||
// Create and send EPIRB packet message to application layer
|
// First decimation stage: 3.072000 MHz / 8 -> 384 kHz
|
||||||
const EPIRBPacketMessage message{packet};
|
const auto decim_0_out = decim_0.execute(buffer, dst_buffer);
|
||||||
shared_memory.application_queue.push(message);
|
|
||||||
}
|
// 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
|
||||||
void EPIRBProcessor::on_message(const Message* const message) {
|
const auto decimator_out = decim_1_out;
|
||||||
(void)message; // Unused in this processor
|
|
||||||
}
|
#ifdef SPECAN
|
||||||
|
// Feed IQ data into spectrum collector for the RF waterfall.
|
||||||
int main() {
|
if (spectrum_on) channel_spectrum.feed(decim_1_out, -5500, 5500, 3400);
|
||||||
EventDispatcher event_dispatcher{std::make_unique<EPIRBProcessor>()};
|
#endif
|
||||||
event_dispatcher.run();
|
|
||||||
return 0;
|
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
|
* Copyright (C) 2024 EPIRB Receiver Implementation
|
||||||
*
|
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
||||||
* This file is part of PortaPack.
|
*
|
||||||
*
|
* 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
|
* This program is free software; you can redistribute it and/or modify
|
||||||
* the Free Software Foundation; either version 2, or (at your option)
|
* it under the terms of the GNU General Public License as published by
|
||||||
* any later version.
|
* 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
|
* This program is distributed in the hope that it will be useful,
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* GNU General Public License for more details.
|
* 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
|
* You should have received a copy of the GNU General Public License
|
||||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
* along with this program; see the file COPYING. If not, write to
|
||||||
* Boston, MA 02110-1301, USA.
|
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||||
*/
|
* Boston, MA 02110-1301, USA.
|
||||||
|
*/
|
||||||
#ifndef __PROC_EPIRB_H__
|
|
||||||
#define __PROC_EPIRB_H__
|
#ifndef __PROC_EPIRB_H__
|
||||||
|
#define __PROC_EPIRB_H__
|
||||||
#include <cstdint>
|
|
||||||
#include <cstddef>
|
#include <cstdint>
|
||||||
#include <array>
|
#include <cstddef>
|
||||||
#include <complex>
|
#include <array>
|
||||||
|
#include <complex>
|
||||||
#include "baseband_processor.hpp"
|
|
||||||
#include "baseband_thread.hpp"
|
#include "baseband_processor.hpp"
|
||||||
#include "rssi_thread.hpp"
|
#include "baseband_thread.hpp"
|
||||||
#include "channel_decimator.hpp"
|
#include "rssi_thread.hpp"
|
||||||
#include "matched_filter.hpp"
|
#include "channel_decimator.hpp"
|
||||||
#include "clock_recovery.hpp"
|
#include "matched_filter.hpp"
|
||||||
#include "symbol_coding.hpp"
|
#include "packet_builder.hpp"
|
||||||
#include "packet_builder.hpp"
|
#include "baseband_packet.hpp"
|
||||||
#include "baseband_packet.hpp"
|
#include "message.hpp"
|
||||||
#include "message.hpp"
|
#include "buffer.hpp"
|
||||||
#include "buffer.hpp"
|
|
||||||
|
// Specan is disable to keep application size below the 32k limit
|
||||||
// Forward declarations for types only used as pointers/references
|
// #define SPECAN
|
||||||
class Message;
|
|
||||||
namespace baseband {
|
#ifdef SPECAN
|
||||||
class Packet;
|
#include "spectrum_collector.hpp"
|
||||||
}
|
#endif
|
||||||
|
|
||||||
// EPIRB 406 MHz Emergency Position Indicating Radio Beacon
|
#include "audio_output.hpp"
|
||||||
// Signal characteristics:
|
#include "dsp_demodulate.hpp"
|
||||||
// - Frequency: 406.025 - 406.028 MHz (typically 406.028 MHz)
|
|
||||||
// - Modulation: BPSK (Binary Phase Shift Keying)
|
// Forward declarations for types only used as pointers/references
|
||||||
// - Data rate: 400 bps
|
class Message;
|
||||||
// - Encoding: Bi-phase L (Manchester)
|
namespace baseband {
|
||||||
// - Transmission: Every 50 seconds ± 2.5 seconds
|
class Packet;
|
||||||
// - Power: 5W ± 2dB
|
}
|
||||||
// - Message length: 144 bits (including sync pattern)
|
|
||||||
|
// COSPAS / SARSAT 406 frame constants
|
||||||
// Matched filter for BPSK demodulation at 400 bps
|
// Size of preamble (bits)
|
||||||
// Using raised cosine filter taps optimized for 400 bps BPSK
|
#define COSPAS_PREAMBLE_SIZE 24
|
||||||
static constexpr std::array<std::complex<float>, 64> bpsk_taps = {{// Raised cosine filter coefficients for BPSK 400 bps
|
// Size of long frame (bits)
|
||||||
-5, -8, -12, -15, -17, -17, -15, -11,
|
#define COSPAS_LONG_FRAME_SIZE 144
|
||||||
-5, 2, 11, 20, 29, 37, 43, 47,
|
// Siz of short frame (bits)
|
||||||
48, 46, 42, 35, 26, 16, 4, -8,
|
#define COSPAS_SHORT_FRAME_SIZE 112
|
||||||
-21, -33, -44, -53, -59, -62, -62, -58,
|
// Preamble for real frames
|
||||||
-51, -41, -28, -13, 3, 19, 36, 51,
|
#define COSPAS_REAL_PREAMBLE 0b1111'1111'1111'1110'0010'1111
|
||||||
64, 74, 80, 82, 80, 74, 64, 51,
|
// Preable for test frames
|
||||||
36, 19, 3, -13, -28, -41, -51, -58,
|
#define COSPAS_TEST_PREAMBLE 0b1111'1111'1111'1110'1101'0000
|
||||||
-62, -62, -59, -53, -44, -33, -21, -8}};
|
|
||||||
|
// Dedicated EPIRB PacketBuilder
|
||||||
class EPIRBProcessor : public BasebandProcessor {
|
// Usees diedicated preamble detection logic to find both real and test frames
|
||||||
public:
|
// Also as a dedicated packet size detection based on frame's size bit
|
||||||
EPIRBProcessor();
|
class EPIRBPacketBuilder {
|
||||||
|
public:
|
||||||
void execute(const buffer_c8_t& buffer) override;
|
using EPIRBHandler = void (*)(void* context, const baseband::Packet& packet);
|
||||||
|
|
||||||
void on_message(const Message* const message) override;
|
EPIRBPacketBuilder(
|
||||||
|
void* context,
|
||||||
private:
|
EPIRBHandler handler)
|
||||||
// EPIRB operates at 406 MHz with narrow bandwidth
|
: context(context),
|
||||||
static constexpr size_t baseband_fs = 2457600;
|
handler(handler) {
|
||||||
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
|
void execute(
|
||||||
|
const uint_fast8_t symbol) {
|
||||||
std::array<complex16_t, 512> dst{};
|
bit_history.add(symbol);
|
||||||
const buffer_c16_t dst_buffer{
|
|
||||||
dst.data(),
|
switch (state) {
|
||||||
dst.size()};
|
case State::Preamble: {
|
||||||
|
// Detect both real and test fram preambles
|
||||||
// Decimation chain for 406 MHz EPIRB signal processing
|
bool is_real = real_sync_matcher(bit_history, packet.size());
|
||||||
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{};
|
bool is_test = test_sync_matcher(bit_history, packet.size());
|
||||||
dsp::decimate::FIRC16xR16x32Decim8 decim_1{};
|
if (is_real || is_test) {
|
||||||
|
// Append preamble to the begining of the packet
|
||||||
dsp::matched_filter::MatchedFilter mf{bpsk_taps, 2};
|
uint64_t preamble = is_real ? COSPAS_REAL_PREAMBLE : COSPAS_TEST_PREAMBLE;
|
||||||
|
for (int8_t i = (COSPAS_PREAMBLE_SIZE - 1); i >= 0; i--) {
|
||||||
// Clock recovery for 400 bps symbol rate
|
packet.add((preamble >> i) & 0x1);
|
||||||
// Sampling rate after decimation: ~38.4kHz
|
}
|
||||||
// Symbols per sample: 38400 / 400 = 96 samples per symbol
|
state = State::Format;
|
||||||
clock_recovery::ClockRecovery<clock_recovery::FixedErrorFilter> clock_recovery{
|
}
|
||||||
38400, // sampling_rate
|
} break;
|
||||||
400, // symbol_rate (400 bps)
|
case State::Format:
|
||||||
{0.0555f}, // error_filter coefficient
|
packet.add(symbol);
|
||||||
[this](const float symbol) { this->consume_symbol(symbol); }};
|
// 144 bits for long frames and 112 for short frames
|
||||||
|
size = symbol ? COSPAS_LONG_FRAME_SIZE : COSPAS_SHORT_FRAME_SIZE;
|
||||||
// Simple bi-phase L decoder state
|
state = State::Payload;
|
||||||
uint_fast8_t last_symbol = 0;
|
|
||||||
|
break;
|
||||||
// EPIRB packet structure:
|
case State::Payload:
|
||||||
// - Sync pattern: 000101010101... (15 bits)
|
packet.add(symbol);
|
||||||
// - Frame sync: 0111110 (7 bits)
|
|
||||||
// - Data: 112 bits
|
if (packet.size() >= size) {
|
||||||
// - BCH error correction: 10 bits
|
flush();
|
||||||
// Total: 144 bits
|
} else {
|
||||||
PacketBuilder<BitPattern, BitPattern, BitPattern> packet_builder{
|
if (packet.size() >= packet.capacity()) {
|
||||||
{0b010101010101010, 15, 1}, // Preamble pattern
|
reset_state();
|
||||||
{0b0111110, 7}, // Frame sync pattern
|
}
|
||||||
{0b0111110, 7}, // End pattern (same as sync for simplicity)
|
}
|
||||||
[this](const baseband::Packet& packet) {
|
break;
|
||||||
this->payload_handler(packet);
|
|
||||||
}};
|
default:
|
||||||
|
reset_state();
|
||||||
void consume_symbol(const float symbol);
|
break;
|
||||||
void payload_handler(const baseband::Packet& packet);
|
}
|
||||||
|
}
|
||||||
// Statistics
|
|
||||||
uint32_t packets_received = 0;
|
void flush() {
|
||||||
Timestamp last_packet_timestamp{};
|
// Timestamp is not set here to save some app space (not used on ui side)
|
||||||
|
// packet.set_timestamp(Timestamp::now());
|
||||||
/* NB: Threads should be the last members in the class definition. */
|
if (handler) handler(context, packet);
|
||||||
BasebandThread baseband_thread{
|
reset_state();
|
||||||
baseband_fs, this, baseband::Direction::Receive, /*auto_start*/ false};
|
}
|
||||||
RSSIThread rssi_thread{};
|
|
||||||
};
|
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__*/
|
#endif /*__PROC_EPIRB_H__*/
|
||||||
@@ -161,6 +161,7 @@ class Message {
|
|||||||
ToneDetectData = 103,
|
ToneDetectData = 103,
|
||||||
ToneDetectConfig = 104,
|
ToneDetectConfig = 104,
|
||||||
FlexTosend = 105,
|
FlexTosend = 105,
|
||||||
|
EPIRBRXConfig = 106,
|
||||||
MAX
|
MAX
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -390,6 +391,16 @@ class EPIRBPacketMessage : public Message {
|
|||||||
baseband::Packet packet;
|
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 {
|
class TPMSPacketMessage : public Message {
|
||||||
public:
|
public:
|
||||||
constexpr TPMSPacketMessage(
|
constexpr TPMSPacketMessage(
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ EPIRB; Standard Location Protocol EPIRB-MMSI; FFFED0A0D205F260850F3DC9E0B70B6E4F
|
|||||||
PLB; Standard Location Protocol - PLB (Serial); FFFED0A3E7B10016150D364D8B3689C09437
|
PLB; Standard Location Protocol - PLB (Serial); FFFED0A3E7B10016150D364D8B3689C09437
|
||||||
SAAS; Std. Location ship security protocol (SSAS); FFFED0A5DC19E1A07FDFFE5C803483E0FCCA
|
SAAS; Std. Location ship security protocol (SSAS); FFFED0A5DC19E1A07FDFFE5C803483E0FCCA
|
||||||
ELT 24; Serial user Location (ELT with Aircraft 24-bit Address); FFFED0CE46E76EF8C00C2BAA31CFE0FF0F61
|
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
|
Float Free EPIRB; Float Free EPIRB with Serial Identification Number; FFFED0DF76A9A9C800174DE27BE1F0277E45
|
||||||
Non Float Free EPIRB; Non Float Free EPIRB with Serial Identification; FFFED0DF77200000001168C610AFE0FF0146
|
Non Float Free EPIRB; Non Float Free EPIRB with Serial Identification; FFFED0DF77200000001168C610AFE0FF0146
|
||||||
Radio Call Sign; Radio Call Sign - EPIRB; FFFED0E0DDAE599508268E4C05E054651307
|
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
|
PLB Serial; Standard Location Protocol - PLB (Serial); FFFED0A157B081437FDFF8B4833783E0F66C
|
||||||
RLS Location; RLS Location Protocol; FFFED096ED09900149D4D467EE0851A3B2E8
|
RLS Location; RLS Location Protocol; FFFED096ED09900149D4D467EE0851A3B2E8
|
||||||
Orbitography; Orbitography Protocol (from Toulouse FMCC's LUT); FFFE2FCE3000000000000DBD0E4022417500
|
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