diff --git a/firmware/application/baseband_api.cpp b/firmware/application/baseband_api.cpp index d54c1ee70..fd894245b 100644 --- a/firmware/application/baseband_api.cpp +++ b/firmware/application/baseband_api.cpp @@ -432,6 +432,10 @@ void set_epirb_tx_config(EPIRBTXDataMessage& message) { send_message(&message); } +void set_epirb_rx_config(EPIRBRXConfig& message) { + send_message(&message); +} + void set_p25tx_data(const uint8_t* dibits, uint16_t frame_length) { const size_t max_len = sizeof(shared_memory.bb_data.data); if (frame_length > max_len) frame_length = max_len; diff --git a/firmware/application/baseband_api.hpp b/firmware/application/baseband_api.hpp index f406eef59..09bc7d3f5 100644 --- a/firmware/application/baseband_api.hpp +++ b/firmware/application/baseband_api.hpp @@ -118,6 +118,7 @@ void set_bitstream_config(uint32_t deviation, uint8_t mode); void set_rtty_config(uint16_t baud, uint16_t shift, uint8_t* payload = nullptr, uint16_t payload_length = 0); // baud*100 void set_rtty_config(RTTYDataMessage& message); void set_epirb_tx_config(EPIRBTXDataMessage& message); +void set_epirb_rx_config(EPIRBRXConfig& message); void set_p25tx_data(const uint8_t* dibits, uint16_t frame_length); void request_roger_beep(); diff --git a/firmware/application/external/epirb_rx/beacon.cpp b/firmware/application/external/epirb_rx/beacon.cpp new file mode 100644 index 000000000..45d2efe10 --- /dev/null +++ b/firmware/application/external/epirb_rx/beacon.cpp @@ -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 +#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 diff --git a/firmware/application/external/epirb_rx/beacon.hpp b/firmware/application/external/epirb_rx/beacon.hpp new file mode 100644 index 000000000..21d8382ee --- /dev/null +++ b/firmware/application/external/epirb_rx/beacon.hpp @@ -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__ \ No newline at end of file diff --git a/firmware/application/external/epirb_rx/beacon_db.cpp b/firmware/application/external/epirb_rx/beacon_db.cpp new file mode 100644 index 000000000..1bdcd7c7e --- /dev/null +++ b/firmware/application/external/epirb_rx/beacon_db.cpp @@ -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 diff --git a/firmware/application/external/epirb_rx/beacon_db.hpp b/firmware/application/external/epirb_rx/beacon_db.hpp new file mode 100644 index 000000000..a64e03fc8 --- /dev/null +++ b/firmware/application/external/epirb_rx/beacon_db.hpp @@ -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__ */ diff --git a/firmware/application/external/epirb_rx/country.hpp b/firmware/application/external/epirb_rx/country.hpp new file mode 100644 index 000000000..5e432fc83 --- /dev/null +++ b/firmware/application/external/epirb_rx/country.hpp @@ -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(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__ diff --git a/firmware/application/external/epirb_rx/location.cpp b/firmware/application/external/epirb_rx/location.cpp new file mode 100644 index 000000000..a378c208e --- /dev/null +++ b/firmware/application/external/epirb_rx/location.cpp @@ -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 +#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 diff --git a/firmware/application/external/epirb_rx/location.hpp b/firmware/application/external/epirb_rx/location.hpp new file mode 100644 index 000000000..07a0efdb7 --- /dev/null +++ b/firmware/application/external/epirb_rx/location.hpp @@ -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 + +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__ \ No newline at end of file diff --git a/firmware/application/external/epirb_rx/main.cpp b/firmware/application/external/epirb_rx/main.cpp index 40fc5ec28..fea17fe24 100644 --- a/firmware/application/external/epirb_rx/main.cpp +++ b/firmware/application/external/epirb_rx/main.cpp @@ -73,7 +73,7 @@ __attribute__((section(".external_app.app_epirb_rx.application_information"), us 0x00, 0x00, }, - /*.icon_color = */ ui::Color::orange().v, + /*.icon_color = */ ui::Color::green().v, /*.menu_location = */ app_location_t::RX, /*.desired_menu_position = */ -1, diff --git a/firmware/application/external/epirb_rx/resources.hpp b/firmware/application/external/epirb_rx/resources.hpp new file mode 100644 index 000000000..7782e75ff --- /dev/null +++ b/firmware/application/external/epirb_rx/resources.hpp @@ -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 +#include +#include "file_reader.hpp" +#include "file_path.hpp" + +namespace ui::external_app::epirb_rx { + +#define UNKNOWN_LABEL "Unk." + +extern std::vector beacon_res; +extern std::vector 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& 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 beacon_res{}; + // FREQ.TXT content + std::vector freq{}; + + static void load_file(const std::filesystem::path& path, std::vector& 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__ diff --git a/firmware/application/external/epirb_rx/ui_beaconlist.cpp b/firmware/application/external/epirb_rx/ui_beaconlist.cpp new file mode 100644 index 000000000..ea4e7cdd0 --- /dev/null +++ b/firmware/application/external/epirb_rx/ui_beaconlist.cpp @@ -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 + +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(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(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(new_index, db_->size() - 1); + } +} + +} // namespace ui::external_app::epirb_rx diff --git a/firmware/application/external/epirb_rx/ui_beaconlist.hpp b/firmware/application/external/epirb_rx/ui_beaconlist.hpp new file mode 100644 index 000000000..44eb31d21 --- /dev/null +++ b/firmware/application/external/epirb_rx/ui_beaconlist.hpp @@ -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 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__*/ diff --git a/firmware/application/external/epirb_rx/ui_epirb_rx.cpp b/firmware/application/external/epirb_rx/ui_epirb_rx.cpp index 7a99ddbfe..84aa5540f 100644 --- a/firmware/application/external/epirb_rx/ui_epirb_rx.cpp +++ b/firmware/application/external/epirb_rx/ui_epirb_rx.cpp @@ -1,707 +1,600 @@ -/* - * Copyright (C) 2024 EPIRB Decoder Implementation - * - * This file is part of PortaPack. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2, or (at your option) - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; see the file COPYING. If not, write to - * the Free Software Foundation, Inc., 51 Franklin Street, - * Boston, MA 02110-1301, USA. - */ - -#include "baseband_api.hpp" -#include "portapack_persistent_memory.hpp" -#include "file_path.hpp" - -#include "ui_epirb_rx.hpp" - -using namespace portapack; - -#include "rtc_time.hpp" -#include "string_format.hpp" -#include "ui.hpp" - -#include "message.hpp" - -namespace ui::external_app::epirb_rx { - -EPIRBBeacon EPIRBDecoder::decode_packet(const baseband::Packet& packet) { - EPIRBBeacon beacon; - - if (packet.size() < 112) { - return beacon; // Invalid packet - } - - // Convert packet bits to byte array for easier processing - std::array data{}; - for (size_t i = 0; i < std::min(packet.size() / 8, data.size()); i++) { - uint8_t byte_val = 0; - for (int bit = 0; bit < 8 && (i * 8 + bit) < packet.size(); bit++) { - if (packet[i * 8 + bit]) { - byte_val |= (1 << (7 - bit)); - } - } - data[i] = byte_val; - } - - // Perform BCH error detection and correction - uint8_t error_count = 0; - beacon.packet_status = perform_bch_check(data, error_count); - beacon.error_count = error_count; - - // Extract beacon ID (bits 26-85, 15 hex digits) - beacon.beacon_id = 0; - for (int i = 3; i < 11; i++) { - beacon.beacon_id = (beacon.beacon_id << 8) | data[i]; - } - - // Extract beacon type (bits 86-88) - uint8_t type_bits = (data[10] >> 5) & 0x07; - beacon.beacon_type = decode_beacon_type(type_bits); - - // Extract emergency type (bits 91-94 for some beacon types) - uint8_t emergency_bits = (data[11] >> 4) & 0x0F; - beacon.emergency_type = decode_emergency_type(emergency_bits); - - // Extract location if encoded (depends on beacon type and protocol) - beacon.location = decode_location(data); - - // Extract country code (bits 1-10) - beacon.country_code = decode_country_code(data); - - // Set timestamp - rtc::RTC datetime; - rtcGetTime(&RTCD1, &datetime); - beacon.timestamp = datetime; - - return beacon; -} - -EPIRBLocation EPIRBDecoder::decode_location(const std::array& data) { - // EPIRB location encoding varies by protocol version - // This is a simplified decoder for the most common format - - // Check for location data presence (bit patterns vary) - if ((data[12] & 0x80) == 0) { - return EPIRBLocation(); // No location data - } - - // Extract latitude (simplified - actual encoding is more complex) - int32_t lat_raw = ((data[12] & 0x7F) << 10) | (data[13] << 2) | ((data[14] >> 6) & 0x03); - if (lat_raw & 0x10000) lat_raw |= 0xFFFE0000; // Sign extend - float latitude = lat_raw * (180.0f / 131072.0f); - - // Extract longitude (simplified - actual encoding is more complex) - int32_t lon_raw = ((data[14] & 0x3F) << 12) | (data[15] << 4) | ((data[0] >> 4) & 0x0F); - if (lon_raw & 0x20000) lon_raw |= 0xFFFC0000; // Sign extend - float longitude = lon_raw * (360.0f / 262144.0f); - - // Validate coordinates - if (latitude < -90.0f || latitude > 90.0f || longitude < -180.0f || longitude > 180.0f) { - return EPIRBLocation(); // Invalid coordinates - } - - return EPIRBLocation(latitude, longitude); -} - -BeaconType EPIRBDecoder::decode_beacon_type(uint8_t type_bits) { - switch (type_bits) { - case 0: - return BeaconType::OrbitingLocationBeacon; - case 1: - return BeaconType::PersonalLocatorBeacon; - case 2: - return BeaconType::EmergencyLocatorTransmitter; - case 3: - return BeaconType::SerialELT; - case 4: - return BeaconType::NationalELT; - default: - return BeaconType::Other; - } -} - -EmergencyType EPIRBDecoder::decode_emergency_type(uint8_t emergency_bits) { - switch (emergency_bits) { - case 0: - return EmergencyType::Fire; - case 1: - return EmergencyType::Flooding; - case 2: - return EmergencyType::Collision; - case 3: - return EmergencyType::Grounding; - case 4: - return EmergencyType::Sinking; - case 5: - return EmergencyType::Disabled; - case 6: - return EmergencyType::Abandoning; - case 7: - return EmergencyType::Piracy; - case 8: - return EmergencyType::Man_Overboard; - default: - return EmergencyType::Other; - } -} - -uint32_t EPIRBDecoder::decode_country_code(const std::array& data) { - // Country code is in bits 1-10 (ITU country code) - return ((data[0] & 0x03) << 8) | data[1]; -} - -std::string EPIRBDecoder::decode_vessel_name(const std::array& /* data */) { - // Vessel name extraction depends on beacon type and protocol - // This is a placeholder - actual implementation would be more complex - return ""; -} - -PacketStatus EPIRBDecoder::perform_bch_check(std::array& data, uint8_t& error_count) { - // Make a copy to detect changes - std::array original_data = data; - - // Calculate BCH syndrome - uint32_t syndrome = calculate_bch_syndrome(data); - - if (syndrome == 0) { - // No errors detected - error_count = 0; - return PacketStatus::Valid; - } - - // Try to correct single-bit error - if (correct_single_error(data, syndrome)) { - // Successfully corrected - error_count = count_bit_errors(original_data, data); - return PacketStatus::Corrected; - } - - // Multiple errors or uncorrectable - error_count = 255; // Indicate unknown error count - return PacketStatus::Error; -} - -uint32_t EPIRBDecoder::calculate_bch_syndrome(const std::array& data) { - // BCH(127,92,5) polynomial for EPIRB: x^35 + x^2 + x + 1 - // This is a simplified implementation - actual EPIRB uses BCH(63,21,6) - uint32_t syndrome = 0; - uint32_t polynomial = 0x80000007; // x^31 + x^2 + x + 1 (simplified) - - // Process each byte of the data - for (int i = 0; i < 14; i++) { // Only data bits, not parity - uint32_t byte_val = data[i]; - for (int bit = 7; bit >= 0; bit--) { - syndrome <<= 1; - if (byte_val & (1 << bit)) { - syndrome |= 1; - } - - // XOR with polynomial if MSB is set - if (syndrome & 0x80000000) { - syndrome ^= polynomial; - } - } - } - - // XOR with parity bits - syndrome ^= (data[14] << 8) | data[15]; - - return syndrome & 0xFFFF; // 16-bit syndrome -} - -bool EPIRBDecoder::correct_single_error(std::array& data, uint32_t syndrome) { - // Simplified single-error correction - // This is a basic implementation - real BCH correction is more complex - - if (syndrome == 0) return true; // No error - - // Look up table for single-bit error patterns (simplified) - // In a real implementation, this would be a proper BCH syndrome table - for (int byte_idx = 0; byte_idx < 14; byte_idx++) { - for (int bit_idx = 0; bit_idx < 8; bit_idx++) { - // Create test error pattern - std::array test_data = data; - test_data[byte_idx] ^= (1 << bit_idx); - - // Check if this correction produces zero syndrome - if (calculate_bch_syndrome(test_data) == 0) { - // Found the error location, apply correction - data[byte_idx] ^= (1 << bit_idx); - return true; - } - } - } - - return false; // Could not correct -} - -uint8_t EPIRBDecoder::count_bit_errors(const std::array& original, const std::array& corrected) { - uint8_t count = 0; - for (size_t i = 0; i < 16; i++) { - uint8_t diff = original[i] ^ corrected[i]; - // Count set bits in diff - while (diff) { - count += diff & 1; - diff >>= 1; - } - } - return count; -} - -void EPIRBLogger::on_packet(const EPIRBBeacon& beacon) { - std::string entry = "EPIRB," + - to_string_dec_uint(beacon.beacon_id, 15, '0') + "," + - to_string_dec_uint(static_cast(beacon.beacon_type)) + "," + - to_string_dec_uint(static_cast(beacon.emergency_type)) + ","; - - if (beacon.location.valid) { - entry += to_string_decimal(beacon.location.latitude, 6) + "," + - to_string_decimal(beacon.location.longitude, 6); - } else { - entry += ","; - } - - entry += "," + to_string_dec_uint(beacon.country_code) + "," + - format_packet_status(beacon.packet_status) + "," + - to_string_dec_uint(beacon.error_count) + "\n"; - - log_file.write_entry(beacon.timestamp, entry); -} - -std::string format_beacon_type(BeaconType type) { - switch (type) { - case BeaconType::OrbitingLocationBeacon: - return "OLB"; - case BeaconType::PersonalLocatorBeacon: - return "PLB"; - case BeaconType::EmergencyLocatorTransmitter: - return "ELT"; - case BeaconType::SerialELT: - return "S-ELT"; - case BeaconType::NationalELT: - return "N-ELT"; - default: - return "Other"; - } -} - -std::string format_emergency_type(EmergencyType type) { - switch (type) { - case EmergencyType::Fire: - return "Fire"; - case EmergencyType::Flooding: - return "Flooding"; - case EmergencyType::Collision: - return "Collision"; - case EmergencyType::Grounding: - return "Grounding"; - case EmergencyType::Sinking: - return "Sinking"; - case EmergencyType::Disabled: - return "Disabled"; - case EmergencyType::Abandoning: - return "Abandoning"; - case EmergencyType::Piracy: - return "Piracy"; - case EmergencyType::Man_Overboard: - return "MOB"; - default: - return "Other"; - } -} - -std::string format_packet_status(PacketStatus status) { - switch (status) { - case PacketStatus::Valid: - return "OK"; - case PacketStatus::Corrected: - return "CORR"; - case PacketStatus::Error: - return "ERR"; - default: - return "UNK"; - } -} - -ui::Color get_packet_status_color(PacketStatus status) { - switch (status) { - case PacketStatus::Valid: - return ui::Color::green(); - case PacketStatus::Corrected: - return ui::Color::yellow(); - case PacketStatus::Error: - return ui::Color::red(); - default: - return ui::Color::white(); - } -} - -EPIRBBeaconDetailView::EPIRBBeaconDetailView(ui::NavigationView& nav) { - add_children({&button_done, - &button_see_map}); - - button_done.on_select = [this](Button&) { - if (on_close) on_close(); - }; - - button_see_map.on_select = [this, &nav](Button&) { - if (beacon_.location.valid) { - nav.push( - to_string_hex(beacon_.beacon_id, 8), // tag as string - 0, // altitude - GeoPos::alt_unit::METERS, - GeoPos::spd_unit::NONE, - beacon_.location.latitude, - beacon_.location.longitude, - 0, // angle - [this]() { - if (on_close) on_close(); - }); - } - }; -} - -void EPIRBBeaconDetailView::set_beacon(const EPIRBBeacon& beacon) { - beacon_ = beacon; - set_dirty(); -} - -void EPIRBBeaconDetailView::focus() { - button_see_map.focus(); -} - -void EPIRBBeaconDetailView::paint(ui::Painter& painter) { - View::paint(painter); - - const auto rect = screen_rect(); - const auto s = style(); - - auto draw_cursor = rect.location(); - draw_cursor += {8, 8}; - - draw_cursor = draw_field(painter, {draw_cursor, {200, 16}}, s, - "Beacon ID", to_string_hex(beacon_.beacon_id, 15)) - .location(); - - draw_cursor = draw_field(painter, {draw_cursor, {200, 16}}, s, - "Type", format_beacon_type(beacon_.beacon_type)) - .location(); - - draw_cursor = draw_field(painter, {draw_cursor, {200, 16}}, s, - "Emergency", format_emergency_type(beacon_.emergency_type)) - .location(); - - if (beacon_.location.valid) { - draw_cursor = draw_field(painter, {draw_cursor, {200, 16}}, s, - "Latitude", to_string_decimal(beacon_.location.latitude, 6) + "°") - .location(); - - draw_cursor = draw_field(painter, {draw_cursor, {200, 16}}, s, - "Longitude", to_string_decimal(beacon_.location.longitude, 6) + "°") - .location(); - } else { - draw_cursor = draw_field(painter, {draw_cursor, {200, 16}}, s, - "Location", "Unknown") - .location(); - } - - draw_cursor = draw_field(painter, {draw_cursor, {200, 16}}, s, - "Country", to_string_dec_uint(beacon_.country_code)) - .location(); - - draw_cursor = draw_field(painter, {draw_cursor, {200, 16}}, s, - "Time", to_string_datetime(beacon_.timestamp, HMS)) - .location(); - - // Show packet status with appropriate color - std::string status_text = format_packet_status(beacon_.packet_status); - if (beacon_.error_count > 0 && beacon_.packet_status == PacketStatus::Corrected) { - status_text += " (" + to_string_dec_uint(beacon_.error_count) + " err)"; - } - draw_cursor = draw_field(painter, {draw_cursor, {200, 16}}, s, - "Status", status_text) - .location(); -} - -ui::Rect EPIRBBeaconDetailView::draw_field( - ui::Painter& painter, - const ui::Rect& draw_rect, - const ui::Style& style, - const std::string& label, - const std::string& value) { - const auto label_width = 8 * 8; - - painter.draw_string({draw_rect.location()}, style, label + ":"); - painter.draw_string({draw_rect.location() + ui::Point{label_width, 0}}, style, value); - - return {draw_rect.location() + ui::Point{0, draw_rect.height()}, draw_rect.size()}; -} - -EPIRBAppView::EPIRBAppView(ui::NavigationView& nav) - : nav_(nav) { - baseband::run_prepared_image(portapack::memory::map::m4_code.base()); - - add_children({&label_frequency, - &options_frequency, - &field_rf_amp, - &field_lna, - &field_vga, - &rssi, - &field_volume, - &channel, - &label_status, - &label_beacons_count, - &label_latest, - &text_latest_info, - &label_packet_stats, - &console, - &button_map, - &button_clear, - &button_log}); - - button_map.on_select = [this](Button&) { - this->on_show_map(); - }; - - button_clear.on_select = [this](Button&) { - this->on_clear_beacons(); - }; - - button_log.on_select = [this](Button&) { - this->on_toggle_log(); - }; - - options_frequency.on_change = [this](size_t, ui::OptionsField::value_t v) { - receiver_model.set_target_frequency(v); - }; - options_frequency.set_by_value(receiver_model.target_frequency()); - - signal_token_tick_second = rtc_time::signal_tick_second += [this]() { - this->on_tick_second(); - }; - - // Configure receiver for default EPIRB frequency (406.028 MHz) - receiver_model.set_target_frequency(406028000); - receiver_model.set_rf_amp(true); - receiver_model.set_lna(32); - receiver_model.set_vga(32); - receiver_model.set_sampling_rate(2457600); - receiver_model.enable(); - - logger = std::make_unique(); - if (logger) { - logger->append(logs_dir / "epirb_rx.txt"); - } -} - -EPIRBAppView::~EPIRBAppView() { - rtc_time::signal_tick_second -= signal_token_tick_second; - - receiver_model.disable(); - baseband::shutdown(); -} - -void EPIRBAppView::set_parent_rect(const ui::Rect new_parent_rect) { - View::set_parent_rect(new_parent_rect); - - const auto console_rect = ui::Rect{ - new_parent_rect.left(), - new_parent_rect.top() + header_height, - new_parent_rect.width(), - new_parent_rect.height() - header_height - 32}; - console.set_parent_rect(console_rect); -} - -void EPIRBAppView::paint(ui::Painter& /* painter */) { - // Custom painting if needed -} - -void EPIRBAppView::focus() { - options_frequency.focus(); -} - -void EPIRBAppView::on_packet(const baseband::Packet& packet) { - // Decode the EPIRB packet - auto beacon = EPIRBDecoder::decode_packet(packet); - - if (beacon.beacon_id != 0) { // Valid beacon decoded - on_beacon_decoded(beacon); - } -} - -void EPIRBAppView::on_beacon_decoded(const EPIRBBeacon& beacon) { - beacons_received++; - - // Track packet statistics - switch (beacon.packet_status) { - case PacketStatus::Valid: - packets_valid++; - break; - case PacketStatus::Corrected: - packets_corrected++; - break; - case PacketStatus::Error: - packets_error++; - break; - } - - recent_beacons.push_back(beacon); - - // Keep only last 50 beacons - if (recent_beacons.size() > 50) { - recent_beacons.erase(recent_beacons.begin()); - } - - // Update display - update_display(); - - // Log the beacon - if (logger) { - logger->on_packet(beacon); - } - - // Display in console with full details and colored status - std::string beacon_info = format_beacon_summary(beacon); - if (beacon.emergency_type != EmergencyType::Other) { - beacon_info += " [" + format_emergency_type(beacon.emergency_type) + "]"; - } - - // Add colored status indicator - std::string status_color; - switch (beacon.packet_status) { - case PacketStatus::Valid: - status_color = STR_COLOR_GREEN; - break; - case PacketStatus::Corrected: - status_color = STR_COLOR_YELLOW; - break; - case PacketStatus::Error: - status_color = STR_COLOR_RED; - break; - default: - status_color = STR_COLOR_WHITE; - break; - } - - beacon_info += " [" + status_color + format_packet_status(beacon.packet_status) + STR_COLOR_WHITE + "]"; - if (beacon.error_count > 0 && beacon.packet_status == PacketStatus::Corrected) { - beacon_info += " (" + to_string_dec_uint(beacon.error_count) + "e)"; - } - - console.write(beacon_info + "\n"); -} - -void EPIRBAppView::on_show_map() { - if (!recent_beacons.empty()) { - // Find latest beacon with valid location - for (auto it = recent_beacons.rbegin(); it != recent_beacons.rend(); ++it) { - if (it->location.valid) { - // Create a GeoMapView with all beacon locations - auto map_view = nav_.push( - "EPIRB", // tag - 0, // altitude - ui::GeoPos::alt_unit::METERS, - ui::GeoPos::spd_unit::NONE, - it->location.latitude, - it->location.longitude, - 0 // angle - ); - - // Add all beacons with valid locations as markers - for (const auto& beacon : recent_beacons) { - if (beacon.location.valid) { - ui::GeoMarker marker; - marker.lat = beacon.location.latitude; - marker.lon = beacon.location.longitude; - marker.angle = 0; - marker.tag = to_string_hex(beacon.beacon_id, 8) + " " + - format_beacon_type(beacon.beacon_type); - map_view->store_marker(marker); - } - } - return; - } - } - } - - // No valid location found - nav_.display_modal("No Location", "No beacons with valid\nlocation data found."); -} - -void EPIRBAppView::on_clear_beacons() { - recent_beacons.clear(); - beacons_received = 0; - packets_valid = 0; - packets_corrected = 0; - packets_error = 0; - console.clear(true); - update_display(); -} - -void EPIRBAppView::on_toggle_log() { - // Toggle logging functionality - if (logger) { - logger.reset(); - button_log.set_text("Log"); - } else { - logger = std::make_unique(); - logger->append("epirb_rx.txt"); - button_log.set_text("Stop"); - } -} - -void EPIRBAppView::on_tick_second() { - // Update status display every second - rtc::RTC datetime; - rtcGetTime(&RTCD1, &datetime); - - label_status.set("Listening... " + to_string_datetime(datetime, HM)); -} - -void EPIRBAppView::update_display() { - label_beacons_count.set("Beacons: " + to_string_dec_uint(beacons_received)); - - // Update packet statistics display - std::string stats = std::string("Stats: ") + - STR_COLOR_GREEN + to_string_dec_uint(packets_valid) + "OK " + - STR_COLOR_YELLOW + to_string_dec_uint(packets_corrected) + "CORR " + - STR_COLOR_RED + to_string_dec_uint(packets_error) + "ERR" + STR_COLOR_WHITE; - label_packet_stats.set(stats); - - if (!recent_beacons.empty()) { - const auto& latest = recent_beacons.back(); - text_latest_info.set(format_beacon_summary(latest)); - } -} - -std::string EPIRBAppView::format_beacon_summary(const EPIRBBeacon& beacon) { - std::string summary = to_string_hex(beacon.beacon_id, 8) + " " + - format_beacon_type(beacon.beacon_type); - - if (beacon.location.valid) { - summary += " " + format_location(beacon.location); - } - - // Add status indicator for summary display - summary += " " + format_packet_status(beacon.packet_status); - - return summary; -} - -std::string EPIRBAppView::format_location(const EPIRBLocation& location) { - return to_string_decimal(location.latitude, 4) + "°," + - to_string_decimal(location.longitude, 4) + "°"; -} - -} // namespace ui::external_app::epirb_rx \ No newline at end of file +/* + * Copyright (C) 2024 EPIRB Decoder Implementation + * Copyright (C) 2026 Frederic BORRY - ADRASEC 31 + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "baseband_api.hpp" +#include "portapack_persistent_memory.hpp" +#include "file_path.hpp" + +#include "ui_epirb_rx.hpp" + +using namespace portapack; + +#include "rtc_time.hpp" +#include "string_format.hpp" +#include "ui.hpp" + +#include "message.hpp" +#include "resources.hpp" + +namespace ui::external_app::epirb_rx { + +// URL templates +#define MAPS_URL_TEMPLATE "https://www.google.com/maps/search/?api=1&query=%s%%2C%s" +#define BEACON_URL_TEMPALTE "https://decoder2.herokuapp.com/decoded/" + +#ifndef DISABLE_COUNTRY_CACHE +int CountryManager::cache_count = 0; +Country CountryManager::cache[16]; +#endif + +// ResourceManager class +ResourceManager* ResourceManager::current = nullptr; + +void ResourceManager::destroy() { + if (current != nullptr) + delete current; + current = nullptr; +} + +ResourceManager* ResourceManager::getInstance() { + if (current == nullptr) current = new ResourceManager(); + return ResourceManager::current; +} + +// TextArea class +TextArea::TextArea( + Rect parent_rect) + : Widget{parent_rect} { +} + +void TextArea::paint(Painter& painter) { + const auto rect = screen_rect(); + const Style& s = has_focus() ? style().invert() : style(); + + painter.fill_rectangle(rect, s.background); + // We use \t as line separator since \n is used in STR_COLOR_GREEN + auto rows = split_string(content, '\t'); + + const int line_height = s.font.line_height(); + size_t line_idx = 0; + for (auto row : rows) { + painter.draw_string(rect.location() + Point(0, line_idx * line_height), s, row); + line_idx++; + } +} + +void TextArea::set_content(std::string_view value) { + content = std::string{value}; + set_dirty(); +} + +#ifdef RESET_TIMER +bool TextArea::on_key(const KeyEvent key) { + if (key == KeyEvent::Select && on_select) { + on_select(*this); + set_dirty(); + return true; + } + return false; +} +#endif + +// EPIRBAppView class +void EPIRBAppView::decode_packet(const baseband::Packet& packet, Beacon& beacon) { + // Convert packet bits to byte array for easier processing + uint8_t data[BEACON_DATA_SIZE]{}; + for (size_t i = 0; i < std::min(packet.size() / 8, (size_t)BEACON_DATA_SIZE); i++) { + uint8_t byte_val = 0; + for (int bit = 0; bit < 8 && (i * 8 + bit) < packet.size(); bit++) { + if (packet[i * 8 + bit]) { + byte_val |= (1 << (7 - bit)); + } + } + data[i] = byte_val; + } + beacon.setFrame(data); + // Set timestamp + rtc::RTC datetime; + rtcGetTime(&RTCD1, &datetime); + beacon.date = datetime; +} + +#ifdef LOGGER +void EPIRBLogger::on_packet(Beacon& beacon) { + std::string entry = std::string(beacon.getType()) + "," + + beacon.hexId + "," + + beacon.getProtocolName(); // + ","; + // to_string_dec_uint(static_cast(beacon.emergency_type)) + ","; + /*if (!beacon.location.isUnknown()) { + entry += beacon.location.toString(Location::LocationFormat::DECIMAL); + } else { + entry += ","; + } + entry += "," + beacon.country.toString() + "," + + beacon.getSatus() + "\n";*/ + log_file.write_entry(beacon.date, entry); +} +#endif + +// EPIRBDetailView class +EPIRBDetailView::EPIRBDetailView( + Rect parent_rect, + EPIRBAppView& parent) + : View(parent_rect), parent_app(parent) { + add_children({&text_beacon}); + set_focusable(true); +} + +#ifdef DETAIL_TAB_BEACON_SEL +bool EPIRBDetailView::on_encoder(EncoderEvent delta) { + // Change position in the list according to encoder + int32_t size = (int32_t)parent_app.beacon_db.size(); + if (size == 0) return true; + // Get current index + int32_t current_index = (int32_t)parent_app.beacon_db.get_current_beacon_index(); + // Compute new index with wrap around + int32_t new_index = (current_index + delta + size) % size; + // Set new index + parent_app.beacon_db.set_current_beacon(new_index % size); + parent_app.on_beacon_change(); + return true; +} +#endif + +// Append "CYAN