mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-09-16 03:22:59 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1dafdd5a26 | |||
| fc5beb0c09 | |||
| 3f10663bc9 | |||
| 396f8aef41 | |||
| c095a88cb3 | |||
| 900c80549f | |||
| 4dce613172 | |||
| cf44076a34 | |||
| dd006b4830 | |||
| fc18992442 |
@@ -1,15 +1,67 @@
|
||||
<!--
|
||||
👋 Thank you for your contribution!
|
||||
|
||||
⚠️ To help avoid extensive change requests or potential rejection, please review our simple Contributing Guidelines before you start coding: https://github.com/portapack-mayhem/mayhem-firmware/wiki/Contributing-Guidelines.
|
||||
|
||||
Thank you again for contributing!
|
||||
⚠️ Before you start, please review our Contributing Guidelines:
|
||||
https://github.com/portapack-mayhem/mayhem-firmware/wiki/Contributing-Guidelines
|
||||
-->
|
||||
|
||||
## Brief description what you did
|
||||
<!-- Describe your changes in detail here. What problem does this PR solve? What features does it add or fix? -->
|
||||
## Brief description of what you did
|
||||
|
||||
<!-- Replace this line with a description of your changes. What problem does this PR solve? What does it add or fix?
|
||||
Don't forget to describe how maintainers can test your modifications! -->
|
||||
|
||||
---
|
||||
|
||||
## Proof that your changes work
|
||||
|
||||
> ⚠️ **PRs without proof will not be merged.** Please provide evidence for every section below that applies to your change.
|
||||
>
|
||||
> 🛈 **Trusted contributors:** If you have a track record of merged PRs that were properly tested and documented, maintainers may waive one or more of the proof sections below at their discretion. You are still encouraged to include whatever evidence you have, but sections may be marked `N/A — trusted contributor` with a short note. First-time contributors must provide full proof. See the [Contributing Guidelines](https://github.com/portapack-mayhem/mayhem-firmware/wiki/Contributing-Guidelines) and [Trusted Contributors](https://github.com/portapack-mayhem/mayhem-firmware/wiki/Trusted-Contributors) articles for details.
|
||||
|
||||
### 🖥️ Proof it compiles
|
||||
|
||||
Attach a log snippet showing a successful build from the final firmware size/space summary in the build output. Example: `Space remaining in flash ROM: 19552 bytes ( 1.9 % )`.
|
||||
|
||||
<!-- Paste log below -->
|
||||
|
||||
### 📱 Proof of testing on a real device
|
||||
|
||||
Attach photos, screenshots, or a short video of your changes running on actual PortaPack hardware. **Emulator or simulator output is not a substitute for real-device testing.**
|
||||
|
||||
<!-- Paste photo / video / screenshot below -->
|
||||
|
||||
### 📡 Proof against a real emitter/receiver (if applicable)
|
||||
|
||||
If your PR involves RX/TX, protocol decoding, signal generation, or any RF-related functionality, you **must** demonstrate it working against a real emitter or receiver. Include photos, videos, logs, or waterfall/spectrum screenshots.
|
||||
|
||||
If this section does not apply to your PR, write `N/A` below and briefly explain why.
|
||||
|
||||
<!-- Paste RF evidence, or write N/A with a short justification -->
|
||||
|
||||
---
|
||||
|
||||
## 📚 Wiki documentation commitment
|
||||
|
||||
**By submitting this PR, you acknowledge that once it is merged you are responsible for creating or updating the corresponding page on the [project wiki](https://github.com/portapack-mayhem/mayhem-firmware/wiki).**
|
||||
|
||||
Your wiki article should include:
|
||||
|
||||
- A **screenshot of the main app screen** (strongly recommended)
|
||||
- A clear **description** of what the app does
|
||||
- An explanation of the **controls** and UI elements
|
||||
- Any known **limitations**, quirks, or caveats
|
||||
- Anything else a user should know to use the app effectively (dependencies, required hardware, file formats, etc.)
|
||||
|
||||
If you are modifying an existing app, make sure the wiki page reflects your changes.
|
||||
|
||||
---
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Kept changes minimal and limited to necessary files
|
||||
- [ ] Verified functionality remains intact and code compiles
|
||||
- [ ] Attached proof that the code compiles successfully *(or marked N/A as a trusted contributor)*
|
||||
- [ ] Attached proof of testing on real PortaPack hardware *(or marked N/A as a trusted contributor)*
|
||||
- [ ] Attached proof of testing against a real emitter/receiver (if RF-related), or marked N/A with justification
|
||||
- [ ] I understand that by getting this PR merged, I am implicitly agreeing to create or update the corresponding wiki page (including a main-screen screenshot, description, controls, and limitations)
|
||||
- [ ] Reviewed the [Contributing Guidelines](https://github.com/portapack-mayhem/mayhem-firmware/wiki/Contributing-Guidelines)
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "string_format.hpp"
|
||||
#include "utility.hpp"
|
||||
#include "file_path.hpp"
|
||||
#include "usb_serial_asyncmsg.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
using namespace pocsag;
|
||||
@@ -62,6 +63,7 @@ POCSAGSettingsView::POCSAGSettingsView(
|
||||
&check_small_font,
|
||||
&check_hide_bad,
|
||||
&check_hide_addr_only,
|
||||
&check_numeric_detect,
|
||||
&opt_filter_mode,
|
||||
&field_filter_address,
|
||||
&button_save});
|
||||
@@ -72,6 +74,7 @@ POCSAGSettingsView::POCSAGSettingsView(
|
||||
check_small_font.set_value(settings_.enable_small_font);
|
||||
check_hide_bad.set_value(settings_.hide_bad_data);
|
||||
check_hide_addr_only.set_value(settings_.hide_addr_only);
|
||||
check_numeric_detect.set_value(settings_.enable_numeric_detect);
|
||||
opt_filter_mode.set_by_value(settings_.filter_mode);
|
||||
field_filter_address.set_value(settings_.filter_address);
|
||||
|
||||
@@ -81,6 +84,7 @@ POCSAGSettingsView::POCSAGSettingsView(
|
||||
settings_.enable_small_font = check_small_font.value();
|
||||
settings_.hide_bad_data = check_hide_bad.value();
|
||||
settings_.hide_addr_only = check_hide_addr_only.value();
|
||||
settings_.enable_numeric_detect = check_numeric_detect.value();
|
||||
settings_.filter_mode = opt_filter_mode.selected_index_value();
|
||||
settings_.filter_address = field_filter_address.to_integer();
|
||||
settings_.baud_rate = opt_baud_rate.selected_index_value();
|
||||
@@ -219,12 +223,24 @@ void POCSAGAppView::handle_decoded(Timestamp timestamp, const std::string& prefi
|
||||
return;
|
||||
}
|
||||
|
||||
// Color indicates the message has a lot of decoding errors.
|
||||
std::string color = bad_data ? STR_COLOR_MAGENTA : STR_COLOR_WHITE;
|
||||
// Type indicator with its own color.
|
||||
std::string type_str;
|
||||
bool numeric_detect = settings_.enable_numeric_detect;
|
||||
if (numeric_detect && pocsag_state.detected == pocsag::DET_NUMERIC)
|
||||
type_str = STR_COLOR_GREEN "n";
|
||||
else if (numeric_detect && pocsag_state.detected == pocsag::DET_ALPHA)
|
||||
type_str = STR_COLOR_LIGHT_GREY "a";
|
||||
else if (pocsag_state.out_type == ADDRESS)
|
||||
type_str = STR_COLOR_DARK_YELLOW "t";
|
||||
else
|
||||
type_str = "";
|
||||
|
||||
std::string console_info = "\n" + color + prefix;
|
||||
console_info += " #" + to_string_dec_uint(pocsag_state.address);
|
||||
// Header: timestamp+baud in light grey, capcode+function in white.
|
||||
std::string console_info = "\n" STR_COLOR_LIGHT_GREY + prefix;
|
||||
console_info += STR_COLOR_WHITE " #" + to_string_dec_uint(pocsag_state.address);
|
||||
console_info += " F" + to_string_dec_uint(pocsag_state.function);
|
||||
if (!type_str.empty())
|
||||
console_info += " " + type_str;
|
||||
|
||||
if (pocsag_state.out_type == ADDRESS) {
|
||||
last_address = pocsag_state.address;
|
||||
@@ -240,23 +256,111 @@ void POCSAGAppView::handle_decoded(Timestamp timestamp, const std::string& prefi
|
||||
}
|
||||
}
|
||||
|
||||
/* Serial: tone-only page */
|
||||
if (portapack::usb_serial.serial_connected()) {
|
||||
std::string s = "\r\nPOCSAG " + to_string_dec_uint(current_bitrate) + " " + to_string_dec_uint(pocsag_state.address) + " " + std::string(1, 'A' + pocsag_state.function) + (current_inverted ? " I" : " S") + " tone";
|
||||
UsbSerialAsyncmsg::asyncmsg(s);
|
||||
}
|
||||
|
||||
} else if (pocsag_state.out_type == MESSAGE) {
|
||||
if (pocsag_state.address != last_address) {
|
||||
// New message
|
||||
if (pocsag_state.new_message) {
|
||||
last_address = pocsag_state.address;
|
||||
serial_numeric_sent = 0;
|
||||
console.writeln(console_info);
|
||||
console.write(color + pocsag_state.output);
|
||||
|
||||
// If heuristic chose numeric, show numeric line first (green).
|
||||
if (numeric_detect &&
|
||||
pocsag_state.detected == pocsag::DET_NUMERIC &&
|
||||
pocsag_state.numeric_len > 0) {
|
||||
std::string num_str(pocsag_state.numeric_buf, pocsag_state.numeric_len);
|
||||
console.write(STR_COLOR_GREEN + num_str);
|
||||
console.writeln("");
|
||||
}
|
||||
|
||||
// Alpha decode (already has per-char color escapes from decoder).
|
||||
console.write(pocsag_state.output);
|
||||
|
||||
/* Serial: header + first chunk.
|
||||
* hex field contains rendered alpha as hex bytes (color escapes
|
||||
* stripped). Non-printable chars show as '.' (0x2E),
|
||||
* uncorrectable chars show as '?' (0x3F) — original 7-bit
|
||||
* values are not preserved.
|
||||
* Numeric decode goes in the quoted message field only. */
|
||||
if (portapack::usb_serial.serial_connected()) {
|
||||
/* Build hex representation of decoded alpha characters. */
|
||||
std::string raw;
|
||||
for (size_t i = 0; i < pocsag_state.output.size(); ++i)
|
||||
raw += to_string_hex((uint8_t)pocsag_state.output[i], 2);
|
||||
|
||||
std::string s = "\r\nPOCSAG " + to_string_dec_uint(current_bitrate) + " " + to_string_dec_uint(pocsag_state.address) + " " + std::string(1, 'A' + pocsag_state.function) + (current_inverted ? " I" : " S");
|
||||
|
||||
if (numeric_detect && pocsag_state.detected == pocsag::DET_NUMERIC) {
|
||||
s += " numeric \"";
|
||||
if (pocsag_state.numeric_len > 0)
|
||||
s += std::string(pocsag_state.numeric_buf, pocsag_state.numeric_len);
|
||||
s += "\"";
|
||||
serial_numeric_sent = pocsag_state.numeric_len;
|
||||
} else {
|
||||
/* For alpha, the quoted text is the same chars as raw but as ASCII.
|
||||
* Escape " and \ to avoid breaking the quoted field. */
|
||||
s += " alpha \"";
|
||||
for (size_t i = 0; i < pocsag_state.output.size(); ++i) {
|
||||
if (pocsag_state.output[i] == '"' || pocsag_state.output[i] == '\\') {
|
||||
s += '\\';
|
||||
}
|
||||
s += pocsag_state.output[i];
|
||||
}
|
||||
s += "\"";
|
||||
}
|
||||
s += " hex:" + raw;
|
||||
UsbSerialAsyncmsg::asyncmsg(s);
|
||||
}
|
||||
} else {
|
||||
// Message continues...
|
||||
console.write(color + pocsag_state.output);
|
||||
// Message continues from previous batch.
|
||||
bool is_numeric = numeric_detect && pocsag_state.detected == pocsag::DET_NUMERIC;
|
||||
|
||||
// GUI: show numeric continuation if applicable.
|
||||
if (is_numeric && pocsag_state.numeric_len > serial_numeric_sent) {
|
||||
std::string num_str(pocsag_state.numeric_buf + serial_numeric_sent,
|
||||
pocsag_state.numeric_len - serial_numeric_sent);
|
||||
console.write(STR_COLOR_GREEN + num_str);
|
||||
}
|
||||
console.write(pocsag_state.output);
|
||||
|
||||
/* Serial: continuation chunk with full header.
|
||||
* Type label carries over from first batch (numeric+ or alpha+). */
|
||||
if (portapack::usb_serial.serial_connected()) {
|
||||
std::string raw;
|
||||
std::string decoded;
|
||||
for (size_t i = 0; i < pocsag_state.output.size(); ++i) {
|
||||
if (pocsag_state.output[i] == '"' || pocsag_state.output[i] == '\\')
|
||||
decoded += '\\';
|
||||
decoded += pocsag_state.output[i];
|
||||
raw += to_string_hex((uint8_t)pocsag_state.output[i], 2);
|
||||
}
|
||||
if (!decoded.empty() || pocsag_state.numeric_len > serial_numeric_sent) {
|
||||
std::string s = "\r\nPOCSAG " + to_string_dec_uint(current_bitrate) + " " + to_string_dec_uint(pocsag_state.address) + " " + std::string(1, 'A' + pocsag_state.function) + (current_inverted ? " I" : " S") + (is_numeric ? " numeric+" : " alpha+") + " \"";
|
||||
if (is_numeric && pocsag_state.numeric_len > serial_numeric_sent)
|
||||
s += std::string(pocsag_state.numeric_buf + serial_numeric_sent,
|
||||
pocsag_state.numeric_len - serial_numeric_sent);
|
||||
else
|
||||
s += decoded;
|
||||
s += "\" hex:" + raw;
|
||||
UsbSerialAsyncmsg::asyncmsg(s);
|
||||
}
|
||||
}
|
||||
serial_numeric_sent = pocsag_state.numeric_len;
|
||||
}
|
||||
|
||||
if (logging()) {
|
||||
logger.log_decoded(
|
||||
timestamp,
|
||||
to_string_dec_uint(pocsag_state.address) +
|
||||
" F" + to_string_dec_uint(pocsag_state.function) +
|
||||
" " + pocsag_state.output);
|
||||
std::string log_entry = to_string_dec_uint(pocsag_state.address) +
|
||||
" F" + to_string_dec_uint(pocsag_state.function);
|
||||
if (numeric_detect &&
|
||||
pocsag_state.detected == pocsag::DET_NUMERIC &&
|
||||
pocsag_state.numeric_len > 0)
|
||||
log_entry += " N:" + std::string(pocsag_state.numeric_buf, pocsag_state.numeric_len);
|
||||
log_entry += " " + pocsag_state.output;
|
||||
logger.log_decoded(timestamp, log_entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -300,13 +404,19 @@ void POCSAGAppView::on_packet(const POCSAGPacketMessage* message) {
|
||||
image_status.set_foreground(Theme::getInstance()->fg_magenta->foreground);
|
||||
pocsag_state.codeword_index = 0;
|
||||
pocsag_state.errors = 0;
|
||||
current_bitrate = message->packet.bitrate();
|
||||
current_inverted = message->packet.inverted();
|
||||
|
||||
// Handle multiple messages (if any).
|
||||
while (pocsag_decode_batch(message->packet, pocsag_state))
|
||||
handle_decoded(message->packet.timestamp(), prefix);
|
||||
|
||||
// Handle the remainder.
|
||||
handle_decoded(message->packet.timestamp(), prefix);
|
||||
// Handle the remainder. Skip if decoder is still in
|
||||
// STATE_HAVE_ADDRESS — the address was at the end of this
|
||||
// batch and we can't yet tell if it's tone-only or has
|
||||
// message data in the next batch.
|
||||
if (pocsag_state.mode != STATE_HAVE_ADDRESS)
|
||||
handle_decoded(message->packet.timestamp(), prefix);
|
||||
}
|
||||
|
||||
// Set status icon color to indicate state machine state.
|
||||
|
||||
@@ -125,6 +125,7 @@ struct POCSAGSettings {
|
||||
bool enable_raw_log = false;
|
||||
bool hide_bad_data = false;
|
||||
bool hide_addr_only = false;
|
||||
bool enable_numeric_detect = true; /* heuristic alpha/numeric type detection */
|
||||
uint8_t filter_mode = false;
|
||||
int32_t baud_rate = -1;
|
||||
uint32_t filter_address = 0;
|
||||
@@ -150,8 +151,8 @@ class POCSAGSettingsView : public View {
|
||||
|
||||
Labels labels{
|
||||
{{2 * 8, 0 * 16}, "Baud:", Theme::getInstance()->fg_light->foreground},
|
||||
{{2 * 8, 12 * 16}, "Filter Mode:", Theme::getInstance()->fg_light->foreground},
|
||||
{{2 * 8, 13 * 16}, "Filter Addr:", Theme::getInstance()->fg_light->foreground},
|
||||
{{2 * 8, 14 * 16}, "Filter Mode:", Theme::getInstance()->fg_light->foreground},
|
||||
{{2 * 8, 15 * 16}, "Filter Addr:", Theme::getInstance()->fg_light->foreground},
|
||||
};
|
||||
|
||||
Checkbox check_log{
|
||||
@@ -179,21 +180,26 @@ class POCSAGSettingsView : public View {
|
||||
22,
|
||||
"Hide Addr Only"};
|
||||
|
||||
Checkbox check_numeric_detect{
|
||||
{2 * 8, 12 * 16},
|
||||
22,
|
||||
"Detect Numeric"};
|
||||
|
||||
OptionsField opt_filter_mode{
|
||||
{15 * 8, 12 * 16},
|
||||
{15 * 8, 14 * 16},
|
||||
4,
|
||||
{{"None", FILTER_NONE},
|
||||
{"Drop", FILTER_DROP},
|
||||
{"Keep", FILTER_KEEP}}};
|
||||
|
||||
SymField field_filter_address{
|
||||
{15 * 8, 13 * 16},
|
||||
{15 * 8, 15 * 16},
|
||||
7,
|
||||
SymField::Type::Dec,
|
||||
true /*explicit_edit*/};
|
||||
|
||||
Button button_save{
|
||||
{UI_POS_X_CENTER(10), UI_POS_Y(16), 10 * 8, 2 * 16},
|
||||
{UI_POS_X_CENTER(10), UI_POS_Y(17), 10 * 8, 2 * 16},
|
||||
"Save"};
|
||||
};
|
||||
|
||||
@@ -231,6 +237,7 @@ class POCSAGAppView : public View {
|
||||
{"filter_address"sv, &settings_.filter_address},
|
||||
{"hide_bad_data"sv, &settings_.hide_bad_data},
|
||||
{"hide_addr_only"sv, &settings_.hide_addr_only},
|
||||
{"numeric_detect"sv, &settings_.enable_numeric_detect},
|
||||
{"baud_rate"sv, &settings_.baud_rate},
|
||||
}};
|
||||
|
||||
@@ -241,6 +248,10 @@ class POCSAGAppView : public View {
|
||||
void on_stats(const POCSAGStatsMessage* stats);
|
||||
|
||||
uint32_t last_address = 0;
|
||||
uint16_t current_bitrate = 0; /* bitrate of current packet being decoded */
|
||||
bool current_inverted = false; /* polarity of current packet being decoded */
|
||||
uint8_t serial_numeric_sent = 0; /* numeric chars already sent to serial/GUI */
|
||||
|
||||
pocsag::EccContainer ecc{};
|
||||
pocsag::POCSAGState pocsag_state{&ecc};
|
||||
POCSAGLogger logger{};
|
||||
|
||||
@@ -69,29 +69,29 @@ class BattinfoView : public View {
|
||||
};
|
||||
|
||||
Text text_percent{
|
||||
{UI_POS_X(13), UI_POS_Y(1), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(13), UI_POS_Y(1), UI_POS_WIDTH(11), UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
Text text_voltage{
|
||||
{UI_POS_X(13), UI_POS_Y(2), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(13), UI_POS_Y(2), UI_POS_WIDTH(11), UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
Text text_method{
|
||||
{UI_POS_X(13), UI_POS_Y(3), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(13), UI_POS_Y(3), UI_POS_WIDTH(11), UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
Text text_capacity{
|
||||
{UI_POS_X(13), UI_POS_Y(4), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(13), UI_POS_Y(4), UI_POS_WIDTH(11), UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
Text text_current{
|
||||
{UI_POS_X(13), UI_POS_Y(5), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(13), UI_POS_Y(5), UI_POS_WIDTH(11), UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
Text text_charge{
|
||||
{UI_POS_X(13), UI_POS_Y(6), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(13), UI_POS_Y(6), UI_POS_WIDTH(11), UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
Text text_ttef{
|
||||
{UI_POS_X(13), UI_POS_Y(7), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(13), UI_POS_Y(7), UI_POS_WIDTH(11), UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
|
||||
/* Text text_cycles{
|
||||
{UI_POS_X(13), UI_POS_Y(8), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(13), UI_POS_Y(8), UI_POS_WIDTH(11), UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
|
||||
Text text_warn{
|
||||
@@ -111,4 +111,4 @@ class BattinfoView : public View {
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
#endif /*__UI_BATTINFO__*/
|
||||
#endif /*__UI_BATTINFO__*/
|
||||
|
||||
@@ -225,8 +225,15 @@ volume_range_t volume_range() {
|
||||
return audio_codec->headphone_gain_range();
|
||||
}
|
||||
|
||||
void set_volume(const volume_t volume) {
|
||||
audio_codec->set_headphone_volume(volume);
|
||||
bool set_volume(const volume_t volume) {
|
||||
// add retry method
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
if (audio_codec->set_headphone_volume(volume)) {
|
||||
return true;
|
||||
}
|
||||
chThdSleepMilliseconds(2);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} /* namespace headphone */
|
||||
|
||||
@@ -49,7 +49,7 @@ class Codec {
|
||||
virtual void headphone_enable() = 0;
|
||||
virtual void headphone_disable() = 0;
|
||||
virtual volume_range_t headphone_gain_range() const = 0;
|
||||
virtual void set_headphone_volume(const volume_t volume) = 0;
|
||||
virtual bool set_headphone_volume(const volume_t volume) = 0;
|
||||
|
||||
virtual void microphone_enable(int8_t alc_mode, bool mic_to_HP_enabled) = 0; // added user-GUI AK4951 ,selected ALC mode.
|
||||
virtual void microphone_disable() = 0;
|
||||
@@ -93,7 +93,7 @@ namespace headphone {
|
||||
|
||||
volume_range_t volume_range();
|
||||
|
||||
void set_volume(const volume_t volume);
|
||||
bool set_volume(const volume_t volume);
|
||||
|
||||
} /* namespace headphone */
|
||||
|
||||
|
||||
+1
-1
@@ -100,7 +100,7 @@ namespace ui::external_app::ert_app {
|
||||
|
||||
ERTAppView::ERTAppView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_ert);
|
||||
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
|
||||
|
||||
add_children({
|
||||
&field_frequency,
|
||||
|
||||
+1
-1
@@ -77,7 +77,7 @@ __attribute__((section(".external_app.app_ert.application_information"), used))
|
||||
/*.menu_location = */ app_location_t::RX,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_none */ {'P', 'E', 'R', 'T'},
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_ert */ {'P', 'E', 'R', 'T'},
|
||||
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
|
||||
};
|
||||
}
|
||||
|
||||
+8
-1
@@ -309,8 +309,10 @@ set(EXTCPPSRC
|
||||
external/morseradiotx/main.cpp
|
||||
external/morseradiotx/ui_morse_radiotx.cpp
|
||||
|
||||
#keeloqtx
|
||||
external/keeloqtx/main.cpp
|
||||
external/keeloqtx/ui_keeloqtx.cpp
|
||||
|
||||
#rtty_rx
|
||||
external/rtty_rx/main.cpp
|
||||
external/rtty_rx/ui_rtty_rx.cpp
|
||||
@@ -325,6 +327,10 @@ set(EXTCPPSRC
|
||||
external/pocsag_tx/main.cpp
|
||||
external/pocsag_tx/ui_pocsag_tx.cpp
|
||||
|
||||
#flex_tx
|
||||
external/flex_tx/main.cpp
|
||||
external/flex_tx/ui_flex_tx.cpp
|
||||
|
||||
#time_sink
|
||||
external/time_sink/main.cpp
|
||||
external/time_sink/ui_time_sink.cpp
|
||||
@@ -347,7 +353,7 @@ set(EXTCPPSRC
|
||||
|
||||
#two_tone_rx
|
||||
external/two_tone_rx/main.cpp
|
||||
external/two_tone_rx/ui_two_tone_rx.cpp
|
||||
external/two_tone_rx/ui_two_tone_rx.cpp
|
||||
)
|
||||
|
||||
set(EXTAPPLIST
|
||||
@@ -430,6 +436,7 @@ set(EXTAPPLIST
|
||||
rtty_rx
|
||||
rtty_tx
|
||||
pocsag_tx
|
||||
flex_tx
|
||||
time_sink
|
||||
kiss_tnc
|
||||
p25_tx
|
||||
|
||||
+7
@@ -108,6 +108,7 @@ MEMORY
|
||||
ram_external_app_p25_tx (rwx) : org = 0xAE030000, len = 32k
|
||||
ram_external_app_two_tone_pager (rwx) : org = 0xAE040000, len = 32k
|
||||
ram_external_app_two_tone_rx (rwx) : org = 0xAE050000, len = 32k
|
||||
ram_external_app_flex_tx (rwx) : org = 0xAE060000, len = 32k
|
||||
}
|
||||
|
||||
SECTIONS
|
||||
@@ -621,4 +622,10 @@ SECTIONS
|
||||
KEEP(*(.external_app.app_two_tone_rx.application_information));
|
||||
*(*ui*external_app*two_tone_rx*);
|
||||
} > ram_external_app_two_tone_rx
|
||||
|
||||
.external_app_flex_tx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_flex_tx.application_information));
|
||||
*(*ui*external_app*flex_tx*);
|
||||
} > ram_external_app_flex_tx
|
||||
}
|
||||
|
||||
+244
-54
@@ -4,11 +4,15 @@
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "memory_map.hpp"
|
||||
#include "usb_serial_asyncmsg.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
|
||||
namespace ui::external_app::flex_rx {
|
||||
|
||||
static const int flex_tz_table[] = {0, 60, 120, 180, 240, 300, 360, 420, 480, 540, 600, 660, 720,
|
||||
210, 270, 330, 0, 345, 390, 570, -210, -660, -600, -540, -480, -420, -360, -300, -240, -180, -120, -60};
|
||||
|
||||
FlexAppView::FlexAppView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
// Load baseband image for FLEX decoding
|
||||
@@ -19,6 +23,8 @@ FlexAppView::FlexAppView(NavigationView& nav)
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&rssi,
|
||||
&text_status1,
|
||||
&text_status2,
|
||||
&console});
|
||||
|
||||
// Restore saved frequency
|
||||
@@ -39,6 +45,7 @@ FlexAppView::FlexAppView(NavigationView& nav)
|
||||
// Initialize FLEX baseband
|
||||
baseband::set_flex_config();
|
||||
|
||||
text_status1.set("No signal");
|
||||
console.writeln("Ready");
|
||||
}
|
||||
|
||||
@@ -51,54 +58,9 @@ void FlexAppView::focus() {
|
||||
field_frequency.focus();
|
||||
}
|
||||
|
||||
// Redraw all messages to console
|
||||
void FlexAppView::redraw_console() {
|
||||
console.clear(true);
|
||||
bool first = true;
|
||||
for (const auto& msg : messages) {
|
||||
if (!first) {
|
||||
console.writeln(""); // Blank line between messages
|
||||
}
|
||||
first = false;
|
||||
console.writeln(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// Add message to log with automatic line wrapping
|
||||
// Add message to console
|
||||
void FlexAppView::log_message(const std::string& message) {
|
||||
const size_t chars_per_line = screen_width / 8;
|
||||
// Console height accounts for status bar and controls row
|
||||
const size_t console_lines = (screen_height - 2 * 16) / 16;
|
||||
|
||||
messages.push_back(message);
|
||||
|
||||
// Calculate total lines used (messages + blank lines between them)
|
||||
size_t total_lines = 0;
|
||||
for (size_t i = 0; i < messages.size(); i++) {
|
||||
if (i > 0) total_lines++; // Count blank line separator
|
||||
size_t msg_lines = (messages[i].length() + chars_per_line - 1) / chars_per_line;
|
||||
if (msg_lines == 0) msg_lines = 1;
|
||||
total_lines += msg_lines;
|
||||
}
|
||||
|
||||
// If console would overflow, remove oldest messages and redraw
|
||||
if (total_lines > console_lines) {
|
||||
while (total_lines > console_lines && !messages.empty()) {
|
||||
const auto& oldest = messages.front();
|
||||
size_t oldest_lines = (oldest.length() + chars_per_line - 1) / chars_per_line;
|
||||
if (oldest_lines == 0) oldest_lines = 1;
|
||||
total_lines -= oldest_lines;
|
||||
if (messages.size() > 1) total_lines--; // Remove separator line too
|
||||
messages.erase(messages.begin());
|
||||
}
|
||||
redraw_console();
|
||||
} else {
|
||||
// Just append new message
|
||||
if (messages.size() > 1) {
|
||||
console.writeln(""); // Blank line before new message
|
||||
}
|
||||
console.writeln(message);
|
||||
}
|
||||
console.writeln(message);
|
||||
}
|
||||
|
||||
// Update frequency and save for persistence
|
||||
@@ -107,9 +69,234 @@ void FlexAppView::update_freq(rf::Frequency f) {
|
||||
receiver_model.set_target_frequency(f);
|
||||
}
|
||||
|
||||
// Type tag from numeric type code
|
||||
static const char* flex_type_tag(uint32_t type) {
|
||||
switch (type) {
|
||||
case 0:
|
||||
return "SEC";
|
||||
case 1:
|
||||
return "INS";
|
||||
case 2:
|
||||
return "TON";
|
||||
case 3:
|
||||
return "NUM";
|
||||
case 4:
|
||||
return "SNUM";
|
||||
case 5:
|
||||
return "ALN";
|
||||
case 6:
|
||||
return "HEX";
|
||||
case 7:
|
||||
return "NNUM";
|
||||
case 8:
|
||||
return "SHORT";
|
||||
case 9:
|
||||
return "BIW";
|
||||
default:
|
||||
return "UNK";
|
||||
}
|
||||
}
|
||||
|
||||
// Handle decoded FLEX packet from baseband
|
||||
void FlexAppView::on_packet(const FlexPacketMessage* message) {
|
||||
log_message(message->packet.message);
|
||||
const auto& pkt = message->packet;
|
||||
const char* type = flex_type_tag(pkt.type);
|
||||
const char* pol = pkt.is_inverted ? "I" : "N";
|
||||
|
||||
// Update status row 1: C/F speed polarity time timezone
|
||||
{
|
||||
std::string s1 = "C" + to_string_dec_uint(pkt.cycle) +
|
||||
"/F" + to_string_dec_uint(pkt.frame) +
|
||||
" " + to_string_dec_uint(pkt.bitrate) +
|
||||
" " + pol;
|
||||
if (status_time_[0]) {
|
||||
s1 += " ";
|
||||
s1 += status_time_;
|
||||
}
|
||||
if (status_tz_[0]) {
|
||||
s1 += " ";
|
||||
s1 += status_tz_;
|
||||
}
|
||||
text_status1.set(s1);
|
||||
}
|
||||
|
||||
// Update status row 2 from BIW data
|
||||
if (pkt.type == 9) {
|
||||
switch (pkt.biw_field) {
|
||||
case 0: // SSID1
|
||||
status_lid_ = pkt.biw_v1;
|
||||
status_cz_ = pkt.biw_v2;
|
||||
break;
|
||||
case 2: { // Time
|
||||
uint32_t si = (pkt.biw_v3 * 75) / 10;
|
||||
auto h = to_string_dec_uint(pkt.biw_v1, 2, '0');
|
||||
auto m = to_string_dec_uint(pkt.biw_v2, 2, '0');
|
||||
auto sec = to_string_dec_uint(si, 2, '0');
|
||||
// Store for row 1
|
||||
auto ts = h + ":" + m + ":" + sec;
|
||||
memcpy(status_time_, ts.c_str(), ts.size() + 1);
|
||||
break;
|
||||
}
|
||||
case 5: { // SysInfo (timezone)
|
||||
if (pkt.biw_v1 == 4 || pkt.biw_v1 == 5) {
|
||||
uint16_t zone = pkt.biw_v2 & 0x1F;
|
||||
int ofs = (zone < 32) ? flex_tz_table[zone] : 0;
|
||||
int hrs = ofs / 60;
|
||||
int mins = (ofs < 0 ? -ofs : ofs) % 60;
|
||||
auto tzs = std::string("UTC") + (ofs >= 0 ? "+" : "") +
|
||||
to_string_dec_int(hrs);
|
||||
if (mins != 0)
|
||||
tzs += ":" + to_string_dec_int(mins, 2, '0');
|
||||
memcpy(status_tz_, tzs.c_str(), tzs.size() + 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 7: // SSID2
|
||||
status_cc_ = pkt.biw_v1;
|
||||
break;
|
||||
}
|
||||
// Rebuild row 2
|
||||
std::string s2;
|
||||
if (status_lid_) s2 += "LID:" + to_string_dec_uint(status_lid_);
|
||||
if (status_cz_) {
|
||||
s2 += " CZ:";
|
||||
s2 += to_string_dec_uint(status_cz_);
|
||||
}
|
||||
if (status_cc_) {
|
||||
s2 += " CC:";
|
||||
s2 += to_string_dec_uint(status_cc_);
|
||||
}
|
||||
if (pkt.fiw_roaming) s2 += " R";
|
||||
text_status2.set(s2);
|
||||
}
|
||||
|
||||
// Console: skip BIW, tone-only (V=010), SHORT reserved (t=3)
|
||||
bool skip_gui = (pkt.type == 9 || pkt.type == 2);
|
||||
if (pkt.type == 8 && pkt.function == 3) skip_gui = true;
|
||||
if (!skip_gui) {
|
||||
auto cf = to_string_dec_uint(pkt.cycle) + "/" + to_string_dec_uint(pkt.frame);
|
||||
std::string line = cf + " " + to_string_dec_uint(pkt.bitrate) +
|
||||
" " + pol + " " + std::string(1, pkt.phase) + " ";
|
||||
|
||||
if (pkt.type == 1 && pkt.message[0] == 'i' && pkt.message[2] == 't') {
|
||||
// INS temp group: "1234567 +GRP5@F42"
|
||||
line += to_string_dec_uint(pkt.capcode);
|
||||
line += " +TG";
|
||||
line += to_string_dec_uint(pkt.biw_v1);
|
||||
line += "@F";
|
||||
line += to_string_dec_uint(pkt.biw_v2);
|
||||
} else if (pkt.type == 1) {
|
||||
// Other INS types
|
||||
line += to_string_dec_uint(pkt.capcode);
|
||||
line += " INS ";
|
||||
line += pkt.message;
|
||||
} else if (pkt.addr_type == 2) {
|
||||
// Temp group delivery: "GRP5 ALN message"
|
||||
uint32_t slot = (uint32_t)(pkt.capcode + 0x8000 - 0x1F7800) & 0x0F;
|
||||
line += "TG";
|
||||
line += to_string_dec_uint(slot);
|
||||
line += " ";
|
||||
line += type;
|
||||
if (pkt.message[0]) {
|
||||
line += " ";
|
||||
line += pkt.message;
|
||||
}
|
||||
} else {
|
||||
line += to_string_dec_uint(pkt.capcode);
|
||||
if (pkt.is_priority) line += " P";
|
||||
line += " ";
|
||||
line += type;
|
||||
if (pkt.message[0]) {
|
||||
line += " ";
|
||||
line += pkt.message;
|
||||
}
|
||||
}
|
||||
log_message(line);
|
||||
}
|
||||
|
||||
// Serial: pipe-delimited
|
||||
if (portapack::usb_serial.serial_connected()) {
|
||||
std::string s;
|
||||
s = "FLEX|";
|
||||
s += to_string_dec_uint(pkt.cycle);
|
||||
s += '/';
|
||||
s += to_string_dec_uint(pkt.frame);
|
||||
s += '|';
|
||||
s += to_string_dec_uint(pkt.bitrate);
|
||||
s += '|';
|
||||
s += pol;
|
||||
s += '|';
|
||||
s += pkt.phase;
|
||||
|
||||
if (pkt.type == 9) {
|
||||
s += "|BIW|w=";
|
||||
s += to_string_dec_uint(pkt.function);
|
||||
s += "|t=";
|
||||
s += to_string_dec_uint(pkt.biw_field);
|
||||
s += '|';
|
||||
s += to_string_dec_uint(pkt.biw_v1);
|
||||
s += '|';
|
||||
s += to_string_dec_uint(pkt.biw_v2);
|
||||
s += '|';
|
||||
s += to_string_dec_uint(pkt.biw_v3);
|
||||
} else {
|
||||
s += '|';
|
||||
s += type;
|
||||
s += "|cap=";
|
||||
s += to_string_dec_uint(pkt.capcode);
|
||||
if (pkt.is_priority) s += "|pri=1";
|
||||
if (pkt.addr_type == 2) {
|
||||
// Temporary address: show slot number
|
||||
// capcode = aw - 0x8000, aw = capcode + 0x8000
|
||||
// slot = (aw - 0x1F7800) & 0x0F = (capcode + 0x8000 - 0x1F7800) & 0x0F
|
||||
uint32_t slot = (uint32_t)(pkt.capcode + 0x8000 - 0x1F7800) & 0x0F;
|
||||
s += "|slot=";
|
||||
s += to_string_dec_uint(slot);
|
||||
}
|
||||
if (pkt.has_flags) {
|
||||
if (pkt.type == 7) {
|
||||
s += "|seq=";
|
||||
s += to_string_dec_uint(pkt.seq);
|
||||
s += "|new=";
|
||||
s += to_string_dec_uint(pkt.is_new);
|
||||
s += "|fmt=";
|
||||
s += pkt.nnum_s ? "idrom" : "std";
|
||||
} else {
|
||||
s += "|frag=";
|
||||
s += (pkt.frag == 3) ? "first" : to_string_dec_uint(pkt.frag).c_str();
|
||||
s += "|mf=";
|
||||
s += to_string_dec_uint(pkt.more_frag);
|
||||
s += "|seq=";
|
||||
s += to_string_dec_uint(pkt.seq);
|
||||
if (pkt.frag == 3) {
|
||||
s += "|new=";
|
||||
s += to_string_dec_uint(pkt.is_new);
|
||||
s += "|md=";
|
||||
s += to_string_dec_uint(pkt.maildrop);
|
||||
s += "|sig=";
|
||||
s += to_string_hex(pkt.sig, 2);
|
||||
}
|
||||
if (pkt.type == 0) {
|
||||
static const char* enc[] = {"alpha", "sep", "bin", "rsvd"};
|
||||
s += "|enc=";
|
||||
s += enc[pkt.sec_enc & 3];
|
||||
}
|
||||
if (pkt.type == 6 && pkt.frag == 3) {
|
||||
uint8_t b = pkt.function & 0x0F;
|
||||
s += "|bb=";
|
||||
s += to_string_dec_uint(b == 0 ? 16 : b);
|
||||
if (pkt.function & 0x10) s += "|rtl=1";
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pkt.message[0]) {
|
||||
s += "|\"";
|
||||
s += pkt.message;
|
||||
s += '"';
|
||||
}
|
||||
}
|
||||
UsbSerialAsyncmsg::asyncmsg(s);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle stats message (currently unused)
|
||||
@@ -118,12 +305,15 @@ void FlexAppView::on_stats(const FlexStatsMessage*) {
|
||||
|
||||
// Debug handler - uncomment to see baseband debug messages
|
||||
void FlexAppView::on_debug(const FlexDebugMessage* message) {
|
||||
(void)message; // Suppress unused parameter warning
|
||||
// std::string text = "DBG: ";
|
||||
// text += message->text;
|
||||
// text += " " + to_string_hex(message->val1, 8);
|
||||
// text += " " + to_string_hex(message->val2, 8);
|
||||
// log_message(text);
|
||||
if (portapack::usb_serial.serial_connected()) {
|
||||
std::string s = "DBG|";
|
||||
s += message->text;
|
||||
s += "|";
|
||||
s += to_string_dec_int(message->val1);
|
||||
s += "|";
|
||||
s += to_string_dec_int(message->val2);
|
||||
UsbSerialAsyncmsg::asyncmsg(s);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::flex_rx
|
||||
+17
-7
@@ -26,17 +26,19 @@ class FlexAppView : public View {
|
||||
NavigationView& nav_;
|
||||
|
||||
// Saved settings
|
||||
rf::Frequency frequency_value{931740000}; // Default FLEX frequency
|
||||
rf::Frequency frequency_value{931740000};
|
||||
|
||||
RxRadioState radio_state_{};
|
||||
|
||||
// Message storage for console redraw
|
||||
static constexpr size_t MAX_MESSAGES = 20;
|
||||
std::vector<std::string> messages{};
|
||||
// Status bar state (updated from BIW packets)
|
||||
char status_time_[12]{}; // "HH:MM:SS"
|
||||
char status_tz_[12]{}; // "UTC+N"
|
||||
uint16_t status_lid_{0};
|
||||
uint16_t status_cz_{0};
|
||||
uint16_t status_cc_{0};
|
||||
|
||||
// Helper methods
|
||||
void log_message(const std::string& message);
|
||||
void redraw_console();
|
||||
void update_freq(rf::Frequency f);
|
||||
|
||||
// UI Elements - Row 0, dynamically positioned
|
||||
@@ -54,9 +56,17 @@ class FlexAppView : public View {
|
||||
RSSI rssi{
|
||||
{UI_POS_X(21), 0, UI_POS_WIDTH(9), 4}};
|
||||
|
||||
// Message display area (below controls, account for status bar)
|
||||
// Status rows (rows 1-2)
|
||||
Text text_status1{
|
||||
{0, 1 * 16, screen_width, 16},
|
||||
""};
|
||||
Text text_status2{
|
||||
{0, 2 * 16, screen_width, 16},
|
||||
""};
|
||||
|
||||
// Message display area (below status rows)
|
||||
Console console{
|
||||
{0, 1 * 16, screen_width, screen_height - 2 * 16}};
|
||||
{0, 3 * 16, screen_width, screen_height - 4 * 16}};
|
||||
|
||||
// Persistent settings manager
|
||||
app_settings::SettingsManager settings_{
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
#include "ui.hpp"
|
||||
#include "ui_flex_tx.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::flex_tx {
|
||||
void initialize_app(ui::NavigationView& nav) {
|
||||
nav.push<FlexTXView>();
|
||||
}
|
||||
} // namespace ui::external_app::flex_tx
|
||||
|
||||
extern "C" {
|
||||
|
||||
__attribute__((section(".external_app.app_flex_tx.application_information"), used)) application_information_t _application_information_flex_tx = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||
/*.externalAppEntry = */ ui::external_app::flex_tx::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "FLEX TX",
|
||||
/*.bitmap_data = */ {
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0xFC,
|
||||
0x3F,
|
||||
0xFE,
|
||||
0x7F,
|
||||
0x02,
|
||||
0x40,
|
||||
0xBA,
|
||||
0x45,
|
||||
0x02,
|
||||
0x40,
|
||||
0xFE,
|
||||
0x7F,
|
||||
0xFE,
|
||||
0x7F,
|
||||
0x92,
|
||||
0x7C,
|
||||
0x92,
|
||||
0x7C,
|
||||
0xFC,
|
||||
0x3F,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::cyan().v,
|
||||
/*.menu_location = */ app_location_t::TX,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_fsktx */ {'P', 'F', 'S', 'K'},
|
||||
/*.m4_app_offset = */ 0x00000000,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,903 @@
|
||||
#include "ui_flex_tx.hpp"
|
||||
|
||||
#include "baseband_api.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "ui_textentry.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include "portapack_shared_memory.hpp"
|
||||
#include "rtc_time.hpp"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
using namespace portapack;
|
||||
|
||||
namespace ui::external_app::flex_tx {
|
||||
|
||||
#define MAX_FLEX_MSG 240
|
||||
#define BCH_POLY 0x769
|
||||
#define DATA_MASK 0x1FFFFF
|
||||
|
||||
// ===== Bit reversal =====
|
||||
|
||||
static uint32_t reverse_bits32(uint32_t v) {
|
||||
v = ((v >> 1) & 0x55555555) | ((v & 0x55555555) << 1);
|
||||
v = ((v >> 2) & 0x33333333) | ((v & 0x33333333) << 2);
|
||||
v = ((v >> 4) & 0x0F0F0F0F) | ((v & 0x0F0F0F0F) << 4);
|
||||
v = ((v >> 8) & 0x00FF00FF) | ((v & 0x00FF00FF) << 8);
|
||||
v = (v >> 16) | (v << 16);
|
||||
return v;
|
||||
}
|
||||
|
||||
// ===== BCH(31,21) encoder =====
|
||||
|
||||
static uint32_t flex_encode_word(uint32_t dw) {
|
||||
uint32_t data = dw >> 11;
|
||||
uint32_t dividend = data << 10;
|
||||
for (int i = 30; i >= 10; i--) {
|
||||
if ((dividend >> i) & 1)
|
||||
dividend ^= BCH_POLY << (i - 10);
|
||||
}
|
||||
uint32_t ecc = dividend & 0x3FF;
|
||||
uint32_t code31 = (data << 10) | ecc;
|
||||
uint32_t p = 0, tmp = code31;
|
||||
while (tmp) {
|
||||
p ^= (tmp & 1);
|
||||
tmp >>= 1;
|
||||
}
|
||||
return (code31 << 1) | p;
|
||||
}
|
||||
|
||||
static uint32_t flex_enc(uint32_t data21) {
|
||||
return flex_encode_word(reverse_bits32(data21));
|
||||
}
|
||||
|
||||
// ===== Checksum =====
|
||||
|
||||
static uint32_t flex_checksum(uint32_t d) {
|
||||
uint32_t s = (d & 0xF) + ((d >> 4) & 0xF) + ((d >> 8) & 0xF) +
|
||||
((d >> 12) & 0xF) + ((d >> 16) & 0xF) + ((d >> 20) & 0x1);
|
||||
return (d & ~0xFU) | ((0xF - (s & 0xF)) & 0xF);
|
||||
}
|
||||
|
||||
// ===== Year equivalence =====
|
||||
|
||||
static int flex_is_leap(int y) {
|
||||
return (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0);
|
||||
}
|
||||
|
||||
static int flex_jan1_dow(int y) {
|
||||
static const int t[] = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4};
|
||||
return (y + y / 4 - y / 100 + y / 400 + t[0] + 1) % 7;
|
||||
}
|
||||
|
||||
static int flex_equiv_year(int year) {
|
||||
if (year >= 1994 && year <= 2025)
|
||||
return year;
|
||||
int target_leap = flex_is_leap(year);
|
||||
int target_dow = flex_jan1_dow(year);
|
||||
for (int y = 2025; y >= 1994; y--) {
|
||||
if (flex_is_leap(y) == target_leap &&
|
||||
flex_jan1_dow(y) == target_dow)
|
||||
return y;
|
||||
}
|
||||
return 1994 + ((year - 1994) & 0x1F);
|
||||
}
|
||||
|
||||
// ===== FIW =====
|
||||
|
||||
static uint32_t flex_fiw(uint32_t cycle, uint32_t frame, uint32_t roaming) {
|
||||
uint32_t d = 0;
|
||||
d |= (cycle & 0xF) << 4;
|
||||
d |= (frame & 0x7F) << 8;
|
||||
d |= (roaming & 1) << 15;
|
||||
// r=0, t=0 (single transmission, no low traffic flags)
|
||||
return flex_enc(flex_checksum(d));
|
||||
}
|
||||
|
||||
// ===== BIW1 =====
|
||||
|
||||
static uint32_t flex_biw1(uint32_t astart, uint32_t vstart, uint32_t collapse) {
|
||||
uint32_t d = 0;
|
||||
d |= ((astart - 1) & 0x03) << 8;
|
||||
d |= (vstart & 0x3F) << 10;
|
||||
d |= (collapse & 0x07) << 18;
|
||||
return flex_enc(flex_checksum(d));
|
||||
}
|
||||
|
||||
// ===== BIW Date (type 001) =====
|
||||
|
||||
static uint32_t flex_biw_date(uint32_t year_field, uint32_t month, uint32_t day) {
|
||||
uint32_t d = 0;
|
||||
d |= (1U) << 4; // type = 001
|
||||
d |= (year_field & 0x1F) << 7;
|
||||
d |= (day & 0x1F) << 12;
|
||||
d |= (month & 0x0F) << 17;
|
||||
return flex_enc(flex_checksum(d));
|
||||
}
|
||||
|
||||
// ===== BIW Time (type 010) =====
|
||||
|
||||
static uint32_t flex_biw_time(uint32_t hour, uint32_t minute, uint32_t second_step) {
|
||||
uint32_t d = 0;
|
||||
d |= (2U) << 4; // type = 010
|
||||
d |= (hour & 0x1F) << 7;
|
||||
d |= (minute & 0x3F) << 12;
|
||||
d |= (second_step & 0x07) << 18;
|
||||
return flex_enc(flex_checksum(d));
|
||||
}
|
||||
|
||||
// ===== BIW SysInfo (type 101) =====
|
||||
|
||||
static uint32_t flex_biw_sysinfo(uint32_t a_type, uint32_t info) {
|
||||
uint32_t d = 0;
|
||||
d |= (5U) << 4; // type = 101
|
||||
d |= (a_type & 0x0F) << 7;
|
||||
d |= (info & 0x03FF) << 11;
|
||||
return flex_enc(flex_checksum(d));
|
||||
}
|
||||
|
||||
// ===== BIW SSID1 (type 000) =====
|
||||
|
||||
static uint32_t flex_biw_ssid1(uint32_t local_id, uint32_t coverage_zone) {
|
||||
uint32_t d = 0;
|
||||
// type = 000 (no bits to set)
|
||||
d |= (coverage_zone & 0x1F) << 7;
|
||||
d |= (local_id & 0x01FF) << 12;
|
||||
return flex_enc(flex_checksum(d));
|
||||
}
|
||||
|
||||
// ===== BIW SSID2 (type 111) =====
|
||||
|
||||
static uint32_t flex_biw_ssid2(uint32_t country_code, uint32_t tmf) {
|
||||
uint32_t d = 0;
|
||||
d |= (7U) << 4; // type = 111
|
||||
d |= (tmf & 0x0F) << 7;
|
||||
d |= (country_code & 0x03FF) << 11;
|
||||
return flex_enc(flex_checksum(d));
|
||||
}
|
||||
|
||||
// ===== Short address =====
|
||||
|
||||
static uint32_t flex_short_addr(uint64_t capcode) {
|
||||
return flex_enc((uint32_t)(capcode + 0x8000) & DATA_MASK);
|
||||
}
|
||||
|
||||
// ===== Long address (2 words) =====
|
||||
|
||||
static int flex_long_addr(uint64_t capcode, uint32_t out[2]) {
|
||||
uint64_t result;
|
||||
uint32_t w1, w2;
|
||||
if (capcode >= 2101249ULL && capcode <= 1075843072ULL) {
|
||||
result = capcode - 2068481ULL;
|
||||
w1 = (result % 32768) + 1;
|
||||
w2 = 2097151U - (result / 32768);
|
||||
} else if (capcode >= 1075843073ULL && capcode <= 3223326720ULL) {
|
||||
result = capcode - 2068481ULL;
|
||||
w1 = (result % 32768) + 1;
|
||||
w2 = (result / 32768) + 1933312U;
|
||||
} else if (capcode >= 3223326721ULL && capcode <= 4297068542ULL) {
|
||||
result = capcode - 2068479ULL;
|
||||
w1 = (result % 32768) + 2064383U;
|
||||
w2 = (result / 32768) + 1867776U;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
out[0] = flex_enc(w1 & DATA_MASK);
|
||||
out[1] = flex_enc(w2 & DATA_MASK);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ===== Vector words =====
|
||||
|
||||
static uint32_t flex_alpha_vector(uint32_t mw_start, uint32_t mw_count) {
|
||||
uint32_t d = 0;
|
||||
d |= (5U & 0x07) << 4;
|
||||
d |= (mw_start & 0x7F) << 7;
|
||||
d |= (mw_count & 0x7F) << 14;
|
||||
return flex_enc(flex_checksum(d));
|
||||
}
|
||||
|
||||
static uint32_t flex_numeric_vector(uint32_t type, uint32_t mw_start, uint32_t mw_count, uint32_t kbit) {
|
||||
uint32_t n_field = (mw_count > 0) ? mw_count - 1 : 0;
|
||||
uint32_t d = 0;
|
||||
d |= (type & 0x07) << 4;
|
||||
d |= (mw_start & 0x7F) << 7;
|
||||
d |= (n_field & 0x07) << 14;
|
||||
d |= (kbit & 0x0F) << 17;
|
||||
return flex_enc(flex_checksum(d));
|
||||
}
|
||||
|
||||
// Short vector (type 2, t=00): BCD digits in vector word
|
||||
// Short addr: 3 digits in d0-d11. Long addr: 3 + 5 in 2nd word.
|
||||
// Tone-only: call with empty string (all digits become space 0xC).
|
||||
static uint32_t flex_short_vector(int is_long, const std::string& msg, uint32_t* vy_out) {
|
||||
static const char bcd_chars[20] = "0123456789.U -][";
|
||||
auto to_bcd = [&](char c) -> uint8_t {
|
||||
for (int k = 0; k < 16; k++)
|
||||
if (bcd_chars[k] == c) return k;
|
||||
return 0xC; // space
|
||||
};
|
||||
|
||||
int max_digits = is_long ? 8 : 3;
|
||||
uint8_t digits[8];
|
||||
for (int i = 0; i < 8; i++) {
|
||||
digits[i] = (i < (int)msg.size() && i < max_digits) ? to_bcd(msg[i]) : 0xC;
|
||||
}
|
||||
|
||||
uint32_t vw = 0;
|
||||
vw |= (2U & 0x07) << 4; // type = 010
|
||||
// t1t0 = 00 (numeric) — bits 7-8 stay 0
|
||||
vw |= ((uint32_t)digits[0] & 0xF) << 9;
|
||||
vw |= ((uint32_t)digits[1] & 0xF) << 13;
|
||||
vw |= ((uint32_t)digits[2] & 0xF) << 17;
|
||||
|
||||
if (is_long && vy_out) {
|
||||
uint32_t d2 = 0;
|
||||
for (int i = 0; i < 5 && (i + 3) < max_digits; i++)
|
||||
d2 |= ((uint32_t)digits[i + 3] & 0xF) << (i * 4);
|
||||
*vy_out = flex_enc(d2);
|
||||
}
|
||||
return flex_enc(flex_checksum(vw));
|
||||
}
|
||||
|
||||
// ===== Block interleave =====
|
||||
|
||||
static void flex_interleave_block(uint32_t blk, uint32_t* frame_words) {
|
||||
uint32_t src[8];
|
||||
uint8_t dst[32];
|
||||
memcpy(src, frame_words + blk * 8, sizeof(src));
|
||||
for (uint32_t i = 0; i < 32; i++) {
|
||||
dst[i] = (uint8_t)((((src[0] >> (31 - i)) & 1) << 7) |
|
||||
(((src[1] >> (31 - i)) & 1) << 6) |
|
||||
(((src[2] >> (31 - i)) & 1) << 5) |
|
||||
(((src[3] >> (31 - i)) & 1) << 4) |
|
||||
(((src[4] >> (31 - i)) & 1) << 3) |
|
||||
(((src[5] >> (31 - i)) & 1) << 2) |
|
||||
(((src[6] >> (31 - i)) & 1) << 1) |
|
||||
(((src[7] >> (31 - i)) & 1) << 0));
|
||||
}
|
||||
memcpy(frame_words + blk * 8, dst, sizeof(dst));
|
||||
}
|
||||
|
||||
// ===== Alpha encoding =====
|
||||
|
||||
static int flex_encode_alpha(const std::string& msg, uint32_t* words, int max_words, uint32_t seq, uint32_t msg_r) {
|
||||
int wc = 0;
|
||||
uint32_t hdr = 0;
|
||||
hdr |= (3U << 11);
|
||||
hdr |= ((seq & 0x3F) << 13);
|
||||
hdr |= ((msg_r & 1U) << 19);
|
||||
words[wc++] = hdr;
|
||||
|
||||
size_t ci = 0;
|
||||
uint32_t dw = 0;
|
||||
uint8_t ch;
|
||||
ch = (ci < msg.size()) ? msg[ci++] : 0x03;
|
||||
dw |= ((uint32_t)(ch & 0x7F)) << 7;
|
||||
ch = (ci < msg.size()) ? msg[ci++] : 0x03;
|
||||
dw |= ((uint32_t)(ch & 0x7F)) << 14;
|
||||
words[wc++] = dw;
|
||||
|
||||
while (ci < msg.size() && wc < max_words) {
|
||||
dw = 0;
|
||||
for (int s = 0; s < 3; s++) {
|
||||
ch = (ci < msg.size()) ? msg[ci++] : 0x03;
|
||||
dw |= ((uint32_t)(ch & 0x7F)) << (s * 7);
|
||||
}
|
||||
words[wc++] = dw;
|
||||
}
|
||||
|
||||
// Signature
|
||||
{
|
||||
uint32_t sig_sum = 0;
|
||||
for (int i = 1; i < wc; i++) {
|
||||
uint32_t c0 = (words[i] >> 0) & 0x7F;
|
||||
uint32_t c1 = (words[i] >> 7) & 0x7F;
|
||||
uint32_t c2 = (words[i] >> 14) & 0x7F;
|
||||
if (c0 != 0x03) sig_sum += c0;
|
||||
if (c1 != 0x03) sig_sum += c1;
|
||||
if (c2 != 0x03) sig_sum += c2;
|
||||
}
|
||||
words[1] = (words[1] & ~0x7FU) | ((~sig_sum) & 0x7F);
|
||||
}
|
||||
|
||||
// K checksum
|
||||
{
|
||||
uint32_t k_sum = 0;
|
||||
for (int i = 0; i < wc; i++) {
|
||||
k_sum += words[i] & 0xFF;
|
||||
k_sum += (words[i] >> 8) & 0xFF;
|
||||
k_sum += (words[i] >> 16) & 0x1F;
|
||||
}
|
||||
words[0] |= ((~k_sum) & 0x3FF);
|
||||
}
|
||||
|
||||
for (int i = 0; i < wc; i++)
|
||||
words[i] = flex_enc(words[i]);
|
||||
|
||||
return wc;
|
||||
}
|
||||
|
||||
// ===== Numeric BCD encoding =====
|
||||
|
||||
static int flex_encode_numeric(const std::string& msg, uint32_t* words, int max_words, uint32_t* k_out) {
|
||||
static const char bcd[20] = "0123456789.U -][";
|
||||
uint32_t mw[8] = {0};
|
||||
int bit = 2;
|
||||
int word_idx = 0;
|
||||
for (size_t i = 0; i < msg.size(); i++) {
|
||||
uint8_t nib = 0;
|
||||
for (int k = 0; k < 16; k++)
|
||||
if (bcd[k] == msg[i]) {
|
||||
nib = k;
|
||||
break;
|
||||
}
|
||||
for (int b = 0; b < 4; b++) {
|
||||
word_idx = bit / 21;
|
||||
if (word_idx >= 8) break;
|
||||
if (nib & (1 << b))
|
||||
mw[word_idx] |= (1U << (bit % 21));
|
||||
bit++;
|
||||
}
|
||||
}
|
||||
word_idx = (bit > 0) ? (bit - 1) / 21 : 0;
|
||||
{
|
||||
int end_bit = (word_idx + 1) * 21;
|
||||
while (bit + 4 <= end_bit) {
|
||||
for (int b = 0; b < 4; b++) {
|
||||
if (0x0C & (1 << b))
|
||||
mw[bit / 21] |= (1U << (bit % 21));
|
||||
bit++;
|
||||
}
|
||||
}
|
||||
}
|
||||
uint32_t kb = 0;
|
||||
for (int i = 0; i <= word_idx; i++) {
|
||||
kb += mw[i] & 0xFF;
|
||||
kb += (mw[i] >> 8) & 0xFF;
|
||||
kb += (mw[i] >> 16) & 0x1F;
|
||||
}
|
||||
kb &= 0xFF;
|
||||
kb = (kb & 0x3F) + (kb >> 6);
|
||||
kb = ~kb;
|
||||
mw[0] |= ((kb >> 4) & 0x03);
|
||||
*k_out = kb & 0x0F;
|
||||
int wc = word_idx + 1;
|
||||
for (int i = 0; i < wc && i < max_words; i++)
|
||||
words[i] = flex_enc(mw[i]);
|
||||
return wc;
|
||||
}
|
||||
|
||||
// ===== Sync patterns =====
|
||||
|
||||
static const uint8_t bs1[] = {0xAA, 0xAA, 0xAA, 0xAA};
|
||||
static const uint8_t a1[] = {0x78, 0xF3, 0x59, 0x39};
|
||||
static const uint8_t b_code[4] = {0x55, 0x55, 0x00, 0x00};
|
||||
static const uint8_t ar[] = {0xCB, 0x20, 0x59, 0x39};
|
||||
|
||||
static void write_word(uint8_t*& p, uint32_t w) {
|
||||
*p++ = (w >> 24) & 0xFF;
|
||||
*p++ = (w >> 16) & 0xFF;
|
||||
*p++ = (w >> 8) & 0xFF;
|
||||
*p++ = w & 0xFF;
|
||||
}
|
||||
|
||||
// ===== Build frame =====
|
||||
|
||||
static size_t build_frame(uint8_t* buf, uint64_t capcode, int msg_type, const std::string& msg, uint32_t cycle, uint32_t frame, uint32_t msg_r, const FlexBIWParams& bp) {
|
||||
uint8_t* p = buf;
|
||||
|
||||
// S1 sync
|
||||
memcpy(p, bs1, 4);
|
||||
p += 4;
|
||||
memcpy(p, a1, 4);
|
||||
p += 4;
|
||||
memcpy(p, b_code, 2);
|
||||
p += 2;
|
||||
for (int i = 0; i < 4; i++) *p++ = ~a1[i];
|
||||
|
||||
// FIW
|
||||
write_word(p, flex_fiw(cycle, frame, bp.roaming));
|
||||
|
||||
// S2
|
||||
{
|
||||
uint8_t s2[5];
|
||||
memset(s2, 0, 5);
|
||||
uint64_t bits = 0;
|
||||
bits |= (uint64_t)0xA << 36;
|
||||
bits |= (uint64_t)0xED84 << 20;
|
||||
bits |= (uint64_t)0x5 << 16;
|
||||
bits |= (uint64_t)0x127B;
|
||||
s2[0] = (bits >> 32) & 0xFF;
|
||||
s2[1] = (bits >> 24) & 0xFF;
|
||||
s2[2] = (bits >> 16) & 0xFF;
|
||||
s2[3] = (bits >> 8) & 0xFF;
|
||||
s2[4] = bits & 0xFF;
|
||||
memcpy(p, s2, 5);
|
||||
p += 5;
|
||||
}
|
||||
|
||||
// Build extra BIW words (max 3 slots)
|
||||
uint32_t extra_biw[4];
|
||||
int extra_count = 0;
|
||||
|
||||
// Slot priority: SSID1 first (always when enabled), then Time, Date, TZ, SSID2
|
||||
if (bp.send_ssid1 && extra_count < 3)
|
||||
extra_biw[extra_count++] = flex_biw_ssid1(bp.local_id, bp.coverage_zone);
|
||||
if (bp.send_time && extra_count < 3) {
|
||||
uint32_t sec_step = (uint32_t)(bp.second * 2 / 15); // 7.5s steps
|
||||
if (sec_step > 7) sec_step = 7;
|
||||
extra_biw[extra_count++] = flex_biw_time(bp.hour, bp.minute, sec_step);
|
||||
}
|
||||
if (bp.send_date && extra_count < 3) {
|
||||
uint32_t year_field = (uint32_t)(bp.year - 1994);
|
||||
extra_biw[extra_count++] = flex_biw_date(year_field, bp.month, bp.day);
|
||||
}
|
||||
if (bp.send_tz && extra_count < 3) {
|
||||
uint32_t tz_info = (uint32_t)bp.tz_code & 0x1F;
|
||||
if (bp.send_dst)
|
||||
tz_info |= (0U << 5); // L0=0 means DST active
|
||||
else
|
||||
tz_info |= (1U << 5); // L0=1 means standard time
|
||||
extra_biw[extra_count++] = flex_biw_sysinfo(0x04, tz_info);
|
||||
}
|
||||
if (bp.send_ssid2 && extra_count < 3)
|
||||
extra_biw[extra_count++] = flex_biw_ssid2(bp.country_code, 0);
|
||||
|
||||
uint32_t fw[88];
|
||||
uint32_t mw[84];
|
||||
int mwc = 0;
|
||||
uint32_t num_k = 0;
|
||||
|
||||
if (msg_type == 0)
|
||||
mwc = flex_encode_alpha(msg, mw, 84, bp.msg_number, msg_r);
|
||||
else if (msg_type == 1)
|
||||
mwc = flex_encode_numeric(msg, mw, 84, &num_k);
|
||||
|
||||
// Address encoding
|
||||
int is_long = (capcode >= 2101249ULL);
|
||||
int addr_words = is_long ? 2 : 1;
|
||||
int vec_words = is_long ? 2 : 1;
|
||||
int astart = 1 + extra_count;
|
||||
int vstart = astart + addr_words;
|
||||
int mstart = vstart + vec_words;
|
||||
|
||||
fw[0] = flex_biw1(astart, vstart, 0);
|
||||
|
||||
// Extra BIW words (words 1..extra_count)
|
||||
for (int i = 0; i < extra_count; i++)
|
||||
fw[1 + i] = extra_biw[i];
|
||||
|
||||
// Address words
|
||||
if (is_long) {
|
||||
uint32_t la[2];
|
||||
if (flex_long_addr(capcode, la) < 0) return 0;
|
||||
fw[astart] = la[0];
|
||||
fw[astart + 1] = la[1];
|
||||
} else {
|
||||
fw[astart] = flex_short_addr(capcode);
|
||||
}
|
||||
|
||||
// Vector + message
|
||||
if (msg_type == 0) {
|
||||
fw[vstart] = flex_alpha_vector(mstart, mwc);
|
||||
if (is_long && mwc > 0) {
|
||||
fw[vstart + 1] = mw[0];
|
||||
for (int i = 0; i < mwc - 1 && (mstart + i) < 88; i++)
|
||||
fw[mstart + i] = mw[i + 1];
|
||||
mwc = (mwc > 0) ? mwc - 1 : 0;
|
||||
} else {
|
||||
for (int i = 0; i < mwc && (mstart + i) < 88; i++)
|
||||
fw[mstart + i] = mw[i];
|
||||
}
|
||||
} else if (msg_type == 1) {
|
||||
fw[vstart] = flex_numeric_vector(3, mstart, mwc, num_k);
|
||||
if (is_long) {
|
||||
fw[vstart + 1] = (mwc > 0) ? mw[0] : flex_enc(0);
|
||||
for (int i = 1; i < mwc && (mstart + i - 1) < 88; i++)
|
||||
fw[mstart + i - 1] = mw[i];
|
||||
} else {
|
||||
for (int i = 0; i < mwc && (mstart + i) < 88; i++)
|
||||
fw[mstart + i] = mw[i];
|
||||
}
|
||||
} else {
|
||||
// Type 2 = short/tone (empty msg), Type 3 = short numeric (msg digits)
|
||||
std::string short_msg = (msg_type == 3) ? msg : "";
|
||||
uint32_t short_vy = 0;
|
||||
fw[vstart] = flex_short_vector(is_long, short_msg, &short_vy);
|
||||
if (is_long)
|
||||
fw[vstart + 1] = short_vy;
|
||||
}
|
||||
|
||||
// Idle fill
|
||||
int mf_words = mwc;
|
||||
if (is_long && mwc > 0 && msg_type == 1)
|
||||
mf_words = mwc - 1;
|
||||
for (int i = mstart + mf_words; i < 88; i++)
|
||||
fw[i] = (i & 1) ? 0x00000000 : 0xFFFFFFFF;
|
||||
|
||||
for (int blk = 0; blk < 11; blk++)
|
||||
flex_interleave_block(blk, fw);
|
||||
|
||||
memcpy(p, fw, 88 * 4);
|
||||
p += 88 * 4;
|
||||
|
||||
// Trailing idle
|
||||
*p++ = 0xAA;
|
||||
*p++ = 0xAA;
|
||||
*p++ = 0xAA;
|
||||
*p++ = 0xAA;
|
||||
|
||||
return (size_t)(p - buf);
|
||||
}
|
||||
|
||||
// ===== ERS =====
|
||||
|
||||
static size_t build_ers(uint8_t* buf, int cycles) {
|
||||
uint8_t* p = buf;
|
||||
uint8_t ar_inv[4];
|
||||
for (int i = 0; i < 4; i++) ar_inv[i] = ~ar[i];
|
||||
for (int c = 0; c < cycles; c++) {
|
||||
*p++ = 0xAA;
|
||||
*p++ = 0xAA;
|
||||
memcpy(p, ar, 4);
|
||||
p += 4;
|
||||
*p++ = 0x55;
|
||||
*p++ = 0x55;
|
||||
memcpy(p, ar_inv, 4);
|
||||
p += 4;
|
||||
}
|
||||
return (size_t)(p - buf);
|
||||
}
|
||||
|
||||
// ===== FlexParamsView =====
|
||||
|
||||
FlexParamsView::FlexParamsView(NavigationView& nav, FlexBIWParams& params)
|
||||
: params_(params), nav_(nav) {
|
||||
add_children({&labels,
|
||||
&check_date, &field_year, &field_month, &field_day,
|
||||
&check_time, &field_hour, &field_minute, &field_second,
|
||||
&check_tz, &options_tz, &check_dst,
|
||||
&check_ssid1, &field_local_id, &labels_cz, &field_coverage,
|
||||
&check_ssid2, &field_country, &check_roaming,
|
||||
&labels_msg, &field_msg_number,
|
||||
&check_ers, &field_ers_count,
|
||||
&button_save, &button_cancel});
|
||||
|
||||
// Populate from params
|
||||
check_date.set_value(params_.send_date);
|
||||
field_year.set_value(params_.year);
|
||||
field_month.set_value(params_.month);
|
||||
field_day.set_value(params_.day);
|
||||
|
||||
check_time.set_value(params_.send_time);
|
||||
field_hour.set_value(params_.hour);
|
||||
field_minute.set_value(params_.minute);
|
||||
field_second.set_value(params_.second);
|
||||
|
||||
check_tz.set_value(params_.send_tz);
|
||||
options_tz.set_by_value(params_.tz_code);
|
||||
check_dst.set_value(params_.send_dst);
|
||||
|
||||
check_ssid1.set_value(params_.send_ssid1);
|
||||
field_local_id.set_value(params_.local_id);
|
||||
field_coverage.set_value(params_.coverage_zone);
|
||||
|
||||
check_ssid2.set_value(params_.send_ssid2);
|
||||
field_country.set_value(params_.country_code);
|
||||
check_roaming.set_value(params_.roaming);
|
||||
|
||||
field_msg_number.set_value(params_.msg_number);
|
||||
|
||||
check_ers.set_value(params_.send_ers);
|
||||
field_ers_count.set_value(params_.ers_count);
|
||||
|
||||
// Roaming implies SSID1 + SSID2
|
||||
check_roaming.on_select = [this](Checkbox&, bool v) {
|
||||
on_roaming_changed(v);
|
||||
};
|
||||
|
||||
button_save.on_select = [this, &nav](Button&) {
|
||||
int32_t yr = field_year.value();
|
||||
if (yr > 2025) {
|
||||
int equiv = flex_equiv_year(yr);
|
||||
nav.display_modal(
|
||||
"Year > 2025",
|
||||
"Valid: 1994-2025\nEquiv for " + to_string_dec_uint(yr) + ": " + to_string_dec_uint(equiv));
|
||||
field_year.set_value(equiv);
|
||||
return;
|
||||
}
|
||||
params_.send_date = check_date.value();
|
||||
params_.year = yr;
|
||||
params_.month = field_month.value();
|
||||
params_.day = field_day.value();
|
||||
params_.send_time = check_time.value();
|
||||
params_.hour = field_hour.value();
|
||||
params_.minute = field_minute.value();
|
||||
params_.second = field_second.value();
|
||||
params_.send_tz = check_tz.value();
|
||||
params_.tz_code = options_tz.selected_index_value();
|
||||
params_.send_dst = check_dst.value();
|
||||
params_.send_ssid1 = check_ssid1.value();
|
||||
params_.local_id = field_local_id.value();
|
||||
params_.coverage_zone = field_coverage.value();
|
||||
params_.send_ssid2 = check_ssid2.value();
|
||||
params_.country_code = field_country.value();
|
||||
params_.roaming = check_roaming.value();
|
||||
params_.msg_number = field_msg_number.value();
|
||||
params_.send_ers = check_ers.value();
|
||||
params_.ers_count = field_ers_count.value();
|
||||
nav.pop();
|
||||
};
|
||||
|
||||
button_cancel.on_select = [&nav](Button&) {
|
||||
nav.pop();
|
||||
};
|
||||
}
|
||||
|
||||
void FlexParamsView::on_roaming_changed(bool v) {
|
||||
if (v) {
|
||||
check_ssid1.set_value(true);
|
||||
check_ssid2.set_value(true);
|
||||
}
|
||||
}
|
||||
|
||||
void FlexParamsView::focus() {
|
||||
button_save.focus();
|
||||
}
|
||||
|
||||
// ===== Serial message handler =====
|
||||
|
||||
void FlexTXView::on_serial_msg(const FlexTosendMessage data) {
|
||||
field_capcode.set_value(data.capcode);
|
||||
capcode_value = data.capcode;
|
||||
options_type.set_selected_index(data.type);
|
||||
message = std::string((char*)data.msg, data.msglen);
|
||||
buffer = message;
|
||||
text_message.set(message);
|
||||
if (message.length() > 30 && message.length() <= 60)
|
||||
text_message_l2.set(message.substr(29));
|
||||
else if (message.length() > 60)
|
||||
text_message_l2.set(message.substr(29, 27) + "...");
|
||||
else
|
||||
text_message_l2.set("");
|
||||
field_capcode.dirty();
|
||||
options_type.dirty();
|
||||
text_message.dirty();
|
||||
text_message_l2.dirty();
|
||||
tx_view.focus();
|
||||
if (start_tx()) tx_view.set_transmitting(true);
|
||||
}
|
||||
|
||||
// ===== TX =====
|
||||
|
||||
void FlexTXView::on_tx_progress(const uint32_t progress, const bool done) {
|
||||
if (done) {
|
||||
if (tx_phase_ == 0) return; // stopped by user
|
||||
int ers_n = tx_steps_total_ - 2;
|
||||
if (tx_phase_ == 1 && ers_remaining_ > 0) {
|
||||
tx_step_++;
|
||||
text_capinfo.set("[" + to_string_dec_uint(tx_step_) + "/" + to_string_dec_uint(tx_steps_total_) +
|
||||
"] ERS " + to_string_dec_uint(tx_step_) + "/" + to_string_dec_uint(ers_n));
|
||||
uint8_t* data = shared_memory.bb_data.data;
|
||||
size_t total = build_ers(data, 42);
|
||||
ers_remaining_--;
|
||||
uint32_t total_bits = total * 8;
|
||||
progressbar.set_max(total_bits / 64);
|
||||
baseband::set_fsk_data(total_bits, 2280000 / 1600, 4500, 64);
|
||||
} else if (tx_phase_ == 1) {
|
||||
tx_phase_ = 2;
|
||||
tx_step_++;
|
||||
text_capinfo.set("[" + to_string_dec_uint(tx_step_) + "/" + to_string_dec_uint(tx_steps_total_) +
|
||||
"] Frame 1/2 (new)");
|
||||
send_frame(1);
|
||||
} else if (tx_phase_ == 2) {
|
||||
tx_phase_ = 3;
|
||||
tx_step_++;
|
||||
text_capinfo.set("[" + to_string_dec_uint(tx_step_) + "/" + to_string_dec_uint(tx_steps_total_) +
|
||||
"] Frame 2/2 (dup)");
|
||||
send_frame(0);
|
||||
} else {
|
||||
biw_params_.msg_number = (biw_params_.msg_number + 1) & 63;
|
||||
transmitter_model.disable();
|
||||
progressbar.set_value(0);
|
||||
tx_view.set_transmitting(false);
|
||||
tx_phase_ = 0;
|
||||
set_dirty(); // repaint capcode info
|
||||
}
|
||||
} else {
|
||||
if (tx_phase_ != 0)
|
||||
progressbar.set_value(progress);
|
||||
}
|
||||
}
|
||||
|
||||
bool FlexTXView::start_tx() {
|
||||
uint64_t capcode = field_capcode.to_integer();
|
||||
capcode_value = capcode;
|
||||
if (capcode < 1 || capcode > 4297068542ULL) {
|
||||
nav_.display_modal("Bad capcode", "Capcode: 1-4297068542");
|
||||
return false;
|
||||
}
|
||||
|
||||
int msg_type = options_type.selected_index();
|
||||
if (msg_type == 1) {
|
||||
if (message.find_first_not_of("0123456789.U -][") != std::string::npos) {
|
||||
nav_.display_modal("Bad message", "Numeric: 0-9 . U - ] [ space");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
transmitter_model.set_sampling_rate(2280000);
|
||||
transmitter_model.set_baseband_bandwidth(1'750'000);
|
||||
transmitter_model.enable();
|
||||
|
||||
int ers_n = (biw_params_.send_ers && biw_params_.ers_count > 0) ? biw_params_.ers_count : 0;
|
||||
tx_steps_total_ = ers_n + 2;
|
||||
tx_step_ = 0;
|
||||
|
||||
if (ers_n > 0) {
|
||||
tx_phase_ = 1;
|
||||
ers_remaining_ = ers_n;
|
||||
tx_step_++;
|
||||
text_capinfo.set("[" + to_string_dec_uint(tx_step_) + "/" + to_string_dec_uint(tx_steps_total_) +
|
||||
"] ERS " + to_string_dec_uint(tx_step_) + "/" + to_string_dec_uint(ers_n));
|
||||
uint8_t* data = shared_memory.bb_data.data;
|
||||
size_t total = build_ers(data, 42);
|
||||
ers_remaining_--;
|
||||
uint32_t total_bits = total * 8;
|
||||
progressbar.set_max(total_bits / 64);
|
||||
baseband::set_fsk_data(total_bits, 2280000 / 1600, 4500, 64);
|
||||
} else {
|
||||
tx_phase_ = 2;
|
||||
tx_step_++;
|
||||
text_capinfo.set("[" + to_string_dec_uint(tx_step_) + "/" + to_string_dec_uint(tx_steps_total_) +
|
||||
"] Frame 1/2 (new)");
|
||||
send_frame(1);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void FlexTXView::send_frame(uint32_t msg_r) {
|
||||
uint64_t capcode = field_capcode.to_integer();
|
||||
int msg_type = options_type.selected_index();
|
||||
|
||||
uint8_t* data = shared_memory.bb_data.data;
|
||||
size_t total = 0;
|
||||
|
||||
total += build_ers(data + total, 8);
|
||||
total += build_frame(data + total, capcode, msg_type, message, 0, 0, msg_r, biw_params_);
|
||||
|
||||
uint32_t total_bits = total * 8;
|
||||
progressbar.set_max(total_bits / 64);
|
||||
baseband::set_fsk_data(total_bits, 2280000 / 1600, 4500, 64);
|
||||
}
|
||||
|
||||
// ===== UI =====
|
||||
|
||||
void FlexTXView::focus() {
|
||||
field_capcode.focus();
|
||||
}
|
||||
|
||||
FlexTXView::~FlexTXView() {
|
||||
transmitter_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
void FlexTXView::paint(Painter&) {
|
||||
message = buffer;
|
||||
text_message.set(message);
|
||||
if (message.length() > 30 && message.length() <= 60)
|
||||
text_message_l2.set(message.substr(29));
|
||||
else if (message.length() > 60)
|
||||
text_message_l2.set(message.substr(29, 27) + "...");
|
||||
else
|
||||
text_message_l2.set("");
|
||||
|
||||
// Capcode info line
|
||||
uint64_t cap = field_capcode.to_integer();
|
||||
std::string info;
|
||||
if (cap == 0) {
|
||||
info = "Invalid";
|
||||
} else if (cap <= 1933312ULL) {
|
||||
info = "Short addr";
|
||||
} else if (cap >= 2062336ULL && cap <= 2062351ULL) {
|
||||
info = "Temp grp #" + to_string_dec_uint(cap - 2062336ULL);
|
||||
} else if (cap >= 2062352ULL && cap <= 2062367ULL) {
|
||||
info = "Operator msg";
|
||||
} else if (cap >= 2058240ULL && cap <= 2062335ULL) {
|
||||
info = "Network addr";
|
||||
} else if (cap >= 2041856ULL && cap <= 2058239ULL) {
|
||||
info = "Info service";
|
||||
} else if (cap > 1933312ULL && cap < 2101249ULL) {
|
||||
info = "Reserved";
|
||||
} else if (cap >= 2101249ULL && cap <= 4297068542ULL) {
|
||||
info = "Long addr";
|
||||
} else {
|
||||
info = "Invalid";
|
||||
}
|
||||
// Computed frame/phase for all valid addresses
|
||||
if (cap >= 1 && cap <= 4297068542ULL) {
|
||||
static const char ph[] = "ABCD";
|
||||
int frame = (int)((cap / 16) % 128);
|
||||
int phase = (int)((cap / 4) % 4);
|
||||
info += ", F" + to_string_dec_uint(frame) +
|
||||
" " + std::string(1, ph[phase]);
|
||||
}
|
||||
text_capinfo.set(info);
|
||||
}
|
||||
|
||||
void FlexTXView::on_set_text(NavigationView& nav) {
|
||||
text_prompt(nav, buffer, MAX_FLEX_MSG, ENTER_KEYBOARD_MODE_ALPHA);
|
||||
}
|
||||
|
||||
FlexTXView::FlexTXView(NavigationView& nav)
|
||||
: nav_(nav) {
|
||||
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
|
||||
|
||||
// Init random msg number from RTC
|
||||
{
|
||||
rtc::RTC datetime;
|
||||
rtc_time::now(datetime);
|
||||
int seed = datetime.second() + datetime.minute() * 7 + datetime.hour();
|
||||
biw_params_.msg_number = seed & 63;
|
||||
if (biw_params_.msg_number == 0)
|
||||
biw_params_.msg_number = 1;
|
||||
}
|
||||
|
||||
// Always populate date/time/dst from RTC
|
||||
{
|
||||
rtc::RTC datetime;
|
||||
rtc_time::now(datetime);
|
||||
int real_year = datetime.year();
|
||||
biw_params_.year = flex_equiv_year(real_year);
|
||||
biw_params_.month = datetime.month();
|
||||
biw_params_.day = datetime.day();
|
||||
biw_params_.hour = datetime.hour();
|
||||
biw_params_.minute = datetime.minute();
|
||||
biw_params_.second = datetime.second();
|
||||
biw_params_.send_dst = portapack::persistent_memory::dst_enabled() ? 1 : 0;
|
||||
}
|
||||
|
||||
add_children({&labels, &text_capinfo, &field_capcode, &options_speed, &options_type,
|
||||
&text_message, &text_message_l2,
|
||||
&button_message, &button_params, &progressbar, &tx_view});
|
||||
|
||||
options_speed.set_selected_index(0);
|
||||
options_type.set_selected_index(0);
|
||||
field_capcode.set_value(capcode_value);
|
||||
|
||||
field_capcode.on_change = [this](SymField&) {
|
||||
set_dirty(); // trigger repaint to update capcode info text
|
||||
};
|
||||
|
||||
button_message.on_select = [this, &nav](Button&) {
|
||||
this->on_set_text(nav);
|
||||
};
|
||||
|
||||
button_params.on_select = [this, &nav](Button&) {
|
||||
nav.push<FlexParamsView>(biw_params_);
|
||||
};
|
||||
|
||||
tx_view.on_edit_frequency = [this, &nav]() {
|
||||
auto new_view =
|
||||
nav.push<FrequencyKeypadView>(transmitter_model.target_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
transmitter_model.set_target_frequency(f);
|
||||
};
|
||||
};
|
||||
|
||||
tx_view.on_start = [this]() {
|
||||
if (start_tx()) tx_view.set_transmitting(true);
|
||||
};
|
||||
|
||||
tx_view.on_stop = [this]() {
|
||||
tx_phase_ = 0;
|
||||
tx_view.set_transmitting(false);
|
||||
transmitter_model.disable();
|
||||
progressbar.set_value(0);
|
||||
set_dirty(); // repaint capcode info
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::flex_tx
|
||||
@@ -0,0 +1,251 @@
|
||||
#ifndef __UI_FLEX_TX_H__
|
||||
#define __UI_FLEX_TX_H__
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_transmitter.hpp"
|
||||
#include "message.hpp"
|
||||
#include "transmitter_model.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
|
||||
namespace ui::external_app::flex_tx {
|
||||
|
||||
struct FlexBIWParams {
|
||||
int32_t send_date{1};
|
||||
int32_t send_time{1};
|
||||
int32_t send_tz{1};
|
||||
int32_t send_dst{0};
|
||||
int32_t send_ssid1{0};
|
||||
int32_t send_ssid2{0};
|
||||
int32_t roaming{0};
|
||||
int32_t year{2015};
|
||||
int32_t month{1};
|
||||
int32_t day{1};
|
||||
int32_t hour{0};
|
||||
int32_t minute{0};
|
||||
int32_t second{0};
|
||||
int32_t tz_code{0};
|
||||
int32_t local_id{0};
|
||||
int32_t coverage_zone{0};
|
||||
int32_t country_code{0};
|
||||
int32_t msg_number{0};
|
||||
int32_t send_ers{1};
|
||||
int32_t ers_count{1};
|
||||
};
|
||||
|
||||
class FlexParamsView : public View {
|
||||
public:
|
||||
FlexParamsView(NavigationView& nav, FlexBIWParams& params);
|
||||
|
||||
std::string title() const override { return "FLEX Params"; };
|
||||
void focus() override;
|
||||
|
||||
private:
|
||||
FlexBIWParams& params_;
|
||||
NavigationView& nav_;
|
||||
|
||||
void on_roaming_changed(bool v);
|
||||
|
||||
Labels labels{
|
||||
{{12 * 8, 1 * 16}, "-", Theme::getInstance()->fg_light->foreground},
|
||||
{{15 * 8, 1 * 16}, "-", Theme::getInstance()->fg_light->foreground},
|
||||
{{10 * 8, 2 * 16}, ":", Theme::getInstance()->fg_light->foreground},
|
||||
{{13 * 8, 2 * 16}, ":", Theme::getInstance()->fg_light->foreground},
|
||||
};
|
||||
|
||||
Checkbox check_date{{0 * 8, 1 * 16}, 4, "Date", true};
|
||||
NumberField field_year{{8 * 8, 1 * 16}, 4, {1994, 2099}, 1, '0', true};
|
||||
NumberField field_month{{13 * 8, 1 * 16}, 2, {1, 12}, 1, '0', true};
|
||||
NumberField field_day{{16 * 8, 1 * 16}, 2, {1, 31}, 1, '0', true};
|
||||
|
||||
Checkbox check_time{{0 * 8, 2 * 16}, 4, "Time", true};
|
||||
NumberField field_hour{{8 * 8, 2 * 16}, 2, {0, 23}, 1, '0', true};
|
||||
NumberField field_minute{{11 * 8, 2 * 16}, 2, {0, 59}, 1, '0', true};
|
||||
NumberField field_second{{14 * 8, 2 * 16}, 2, {0, 59}, 1, '0', true};
|
||||
|
||||
Checkbox check_tz{{0 * 8, 3 * 16}, 2, "TZ", true};
|
||||
OptionsField options_tz{
|
||||
{6 * 8, 3 * 16},
|
||||
9,
|
||||
{{"UTC+0 ", 0},
|
||||
{"UTC+1 ", 1},
|
||||
{"UTC+2 ", 2},
|
||||
{"UTC+3 ", 3},
|
||||
{"UTC+4 ", 4},
|
||||
{"UTC+5 ", 5},
|
||||
{"UTC+6 ", 6},
|
||||
{"UTC+7 ", 7},
|
||||
{"UTC+8 ", 8},
|
||||
{"UTC+9 ", 9},
|
||||
{"UTC+10 ", 10},
|
||||
{"UTC+11 ", 11},
|
||||
{"UTC+12 ", 12},
|
||||
{"UTC+3:30", 13},
|
||||
{"UTC+4:30", 14},
|
||||
{"UTC+5:30", 15},
|
||||
{"UTC+5:45", 17},
|
||||
{"UTC+6:30", 18},
|
||||
{"UTC+9:30", 19},
|
||||
{"UTC-3:30", 20},
|
||||
{"UTC-11 ", 21},
|
||||
{"UTC-10 ", 22},
|
||||
{"UTC-9 ", 23},
|
||||
{"UTC-8 ", 24},
|
||||
{"UTC-7 ", 25},
|
||||
{"UTC-6 ", 26},
|
||||
{"UTC-5 ", 27},
|
||||
{"UTC-4 ", 28},
|
||||
{"UTC-3 ", 29},
|
||||
{"UTC-2 ", 30},
|
||||
{"UTC-1 ", 31}}};
|
||||
Checkbox check_dst{{18 * 8, 3 * 16}, 3, "DST", true};
|
||||
|
||||
Checkbox check_ssid1{{0 * 8, 5 * 16}, 5, "LocID", true};
|
||||
NumberField field_local_id{{9 * 8, 5 * 16}, 3, {0, 511}, 1, '0'};
|
||||
Labels labels_cz{
|
||||
{{12 * 8, 5 * 16}, "CovZone", Theme::getInstance()->fg_light->foreground},
|
||||
};
|
||||
NumberField field_coverage{{20 * 8, 5 * 16}, 2, {0, 31}, 1, '0'};
|
||||
|
||||
Checkbox check_ssid2{{0 * 8, 6 * 16}, 11, "CountryCode", true};
|
||||
NumberField field_country{{15 * 8, 6 * 16}, 4, {0, 1023}, 1, '0'};
|
||||
Checkbox check_roaming{{0 * 8, 7 * 16}, 4, "Roam", true};
|
||||
|
||||
Labels labels_msg{
|
||||
{{0 * 8, 9 * 16}, "Msg#:", Theme::getInstance()->fg_light->foreground},
|
||||
};
|
||||
NumberField field_msg_number{{5 * 8, 9 * 16}, 2, {0, 63}, 1, '0'};
|
||||
|
||||
Checkbox check_ers{{0 * 8, 10 * 16}, 3, "ERS", true};
|
||||
NumberField field_ers_count{{8 * 8, 10 * 16}, 2, {1, 10}, 1, ' '};
|
||||
|
||||
Button button_save{{1 * 8, 12 * 16, 12 * 8, 32}, "Save"};
|
||||
Button button_cancel{{16 * 8, 12 * 16, 12 * 8, 32}, "Cancel"};
|
||||
};
|
||||
|
||||
class FlexTXView : public View {
|
||||
public:
|
||||
FlexTXView(NavigationView& nav);
|
||||
~FlexTXView();
|
||||
|
||||
FlexTXView(const FlexTXView&) = delete;
|
||||
FlexTXView& operator=(const FlexTXView&) = delete;
|
||||
|
||||
void focus() override;
|
||||
void paint(Painter&) override;
|
||||
std::string title() const override { return "FLEX TX"; };
|
||||
|
||||
FlexBIWParams biw_params_{};
|
||||
|
||||
private:
|
||||
std::string buffer{"FLEX TEST"};
|
||||
std::string message{};
|
||||
NavigationView& nav_;
|
||||
int tx_phase_{0};
|
||||
int ers_remaining_{0};
|
||||
int tx_step_{0};
|
||||
int tx_steps_total_{0};
|
||||
|
||||
int64_t capcode_value{1000};
|
||||
|
||||
TxRadioState radio_state_{
|
||||
931740000 /* frequency */,
|
||||
1750000 /* bandwidth */,
|
||||
2280000 /* sampling rate */
|
||||
};
|
||||
app_settings::SettingsManager settings_{
|
||||
"tx_flex",
|
||||
app_settings::Mode::TX,
|
||||
{{"capcode", &capcode_value},
|
||||
{"biw_date", &biw_params_.send_date},
|
||||
{"biw_time", &biw_params_.send_time},
|
||||
{"biw_tz", &biw_params_.send_tz},
|
||||
{"biw_ssid1", &biw_params_.send_ssid1},
|
||||
{"biw_ssid2", &biw_params_.send_ssid2},
|
||||
{"biw_roam", &biw_params_.roaming},
|
||||
{"biw_tzc", &biw_params_.tz_code},
|
||||
{"biw_lid", &biw_params_.local_id},
|
||||
{"biw_cz", &biw_params_.coverage_zone},
|
||||
{"biw_cc", &biw_params_.country_code},
|
||||
{"biw_ers", &biw_params_.send_ers},
|
||||
{"biw_ersc", &biw_params_.ers_count}}};
|
||||
|
||||
void on_set_text(NavigationView& nav);
|
||||
void on_tx_progress(const uint32_t progress, const bool done);
|
||||
void on_serial_msg(const FlexTosendMessage data);
|
||||
bool start_tx();
|
||||
void send_frame(uint32_t msg_r);
|
||||
|
||||
Labels labels{
|
||||
{{1 * 8, 4 * 8}, "Capcode:", Theme::getInstance()->fg_light->foreground},
|
||||
{{3 * 8, 6 * 8}, "Speed:", Theme::getInstance()->fg_light->foreground},
|
||||
{{4 * 8, 8 * 8}, "Type:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), 12 * 8}, "Message:", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
Text text_capinfo{
|
||||
{UI_POS_X(0), 2 * 8, screen_width, 16},
|
||||
""};
|
||||
|
||||
SymField field_capcode{
|
||||
{10 * 8, 4 * 8},
|
||||
10};
|
||||
|
||||
OptionsField options_speed{
|
||||
{10 * 8, 6 * 8},
|
||||
10,
|
||||
{{"1600/2FSK ", 0}}};
|
||||
|
||||
OptionsField options_type{
|
||||
{10 * 8, 8 * 8},
|
||||
14,
|
||||
{{"Alphanumeric ", 0},
|
||||
{"Numeric ", 1},
|
||||
{"Short/tone ", 2},
|
||||
{"Short numeric", 3}}};
|
||||
|
||||
Text text_message{
|
||||
{UI_POS_X(0), 14 * 8, screen_width, 16},
|
||||
""};
|
||||
Text text_message_l2{
|
||||
{UI_POS_X(0), 16 * 8, screen_width, 16},
|
||||
""};
|
||||
|
||||
Button button_message{
|
||||
{UI_POS_X(0), 18 * 8, 13 * 8, 32},
|
||||
"Set message"};
|
||||
|
||||
Button button_params{
|
||||
{14 * 8, 18 * 8, 13 * 8, 32},
|
||||
"Set params"};
|
||||
|
||||
ProgressBar progressbar{
|
||||
{16, 210, UI_POS_WIDTH_REMAINING(4), 16}};
|
||||
|
||||
TransmitterView tx_view{
|
||||
(int16_t)UI_POS_Y_BOTTOM(4),
|
||||
10000,
|
||||
9};
|
||||
|
||||
MessageHandlerRegistration message_handler_tx_progress{
|
||||
Message::ID::TXProgress,
|
||||
[this](const Message* const p) {
|
||||
const auto message =
|
||||
*reinterpret_cast<const TXProgressMessage*>(p);
|
||||
this->on_tx_progress(message.progress, message.done);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_flex_tosend{
|
||||
Message::ID::FlexTosend,
|
||||
[this](const Message* const p) {
|
||||
const auto message =
|
||||
*reinterpret_cast<const FlexTosendMessage*>(p);
|
||||
this->on_serial_msg(message);
|
||||
}};
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::flex_tx
|
||||
|
||||
#endif
|
||||
+33
-11
@@ -56,7 +56,8 @@ void POCSAGTXView::on_remete(const PocsagTosendMessage data) {
|
||||
if (data.function == 'C') tmp = 2;
|
||||
if (data.function == 'D') tmp = 3;
|
||||
options_function.set_selected_index(tmp);
|
||||
options_phase.set_selected_index(data.phase == 'P' ? 0 : 1);
|
||||
/* 'S' = Standard (CCIR Rec. 584 polarity), 'I' = Inverted */
|
||||
options_polarity.set_selected_index(data.polarity == 'S' ? 0 : 1);
|
||||
field_address.set_value(data.addr);
|
||||
message = (char*)data.msg;
|
||||
buffer = message;
|
||||
@@ -64,7 +65,7 @@ void POCSAGTXView::on_remete(const PocsagTosendMessage data) {
|
||||
options_bitrate.dirty();
|
||||
options_type.dirty();
|
||||
options_function.dirty();
|
||||
options_phase.dirty();
|
||||
options_polarity.dirty();
|
||||
field_address.dirty();
|
||||
text_message.dirty();
|
||||
tx_view.focus();
|
||||
@@ -95,12 +96,34 @@ bool POCSAGTXView::start_tx() {
|
||||
|
||||
if (type == MessageType::NUMERIC_ONLY) {
|
||||
// Check for invalid characters
|
||||
if (message.find_first_not_of("0123456789SU -][") != std::string::npos) {
|
||||
const char* p = message.c_str();
|
||||
bool is_valid = true;
|
||||
while (*p != '\0') {
|
||||
const char c = *p;
|
||||
// Check if char is NOT in the allowed set
|
||||
if (!((c >= '0' && c <= '9') || c == 'S' || c == 'U' ||
|
||||
c == ' ' || c == '-' || c == '[' || c == ']')) {
|
||||
is_valid = false;
|
||||
break;
|
||||
}
|
||||
p++;
|
||||
}
|
||||
if (!is_valid) {
|
||||
nav_.display_modal("Bad message", "A numeric only message must\nonly contain:\n0123456789SU][- or space.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
MessageType phase = (MessageType)options_phase.selected_index_value();
|
||||
/*
|
||||
* TX polarity inversion (CCIR Rec. 584):
|
||||
*
|
||||
* The FSK modulator (proc_fsk.cpp) maps bit 1 to positive deviation.
|
||||
* Standard POCSAG requires bit 1 to negative deviation.
|
||||
*
|
||||
* "Standard" (value 0): invert codewords so that original bit 1 becomes
|
||||
* bit 0 in the modulator, producing negative deviation. This is the standard.
|
||||
* "Inverted" (value 1): send codewords as-is. Bit 1 produces positive deviation.
|
||||
*/
|
||||
bool invert_polarity = (options_polarity.selected_index_value() == 0);
|
||||
|
||||
pocsag_encode(type, BCH_code, options_function.selected_index_value(), message, address, codewords);
|
||||
|
||||
@@ -118,10 +141,7 @@ bool POCSAGTXView::start_tx() {
|
||||
|
||||
bi = 0;
|
||||
for (i = 0; i < codewords.size(); i++) {
|
||||
if (phase == 0)
|
||||
codeword = ~(codewords[i]);
|
||||
else
|
||||
codeword = codewords[i];
|
||||
codeword = invert_polarity ? ~(codewords[i]) : codewords[i];
|
||||
|
||||
data_ptr[bi++] = (codeword >> 24) & 0xFF;
|
||||
data_ptr[bi++] = (codeword >> 16) & 0xFF;
|
||||
@@ -166,15 +186,17 @@ POCSAGTXView::POCSAGTXView(
|
||||
&field_address,
|
||||
&options_type,
|
||||
&options_function,
|
||||
&options_phase,
|
||||
&options_polarity,
|
||||
&text_message,
|
||||
&text_message_l2,
|
||||
&button_message,
|
||||
&progressbar,
|
||||
&tx_view});
|
||||
|
||||
options_bitrate.set_selected_index(1); // 1200bps
|
||||
options_type.set_selected_index(0); // Address only
|
||||
options_bitrate.set_selected_index(1); // 1200 bps
|
||||
options_type.set_selected_index(2); // Alphanumeric
|
||||
options_function.set_selected_index(0); // Function A
|
||||
options_polarity.set_selected_index(0); // Standard (CCIR Rec. 584)
|
||||
|
||||
field_address.set_value(persistent_memory::pocsag_last_address());
|
||||
|
||||
|
||||
+13
-5
@@ -84,7 +84,7 @@ class POCSAGTXView : public View {
|
||||
{{3 * 8, 6 * 8}, "Address:", Theme::getInstance()->fg_light->foreground},
|
||||
{{6 * 8, 8 * 8}, "Type:", Theme::getInstance()->fg_light->foreground},
|
||||
{{2 * 8, 10 * 8}, "Function:", Theme::getInstance()->fg_light->foreground},
|
||||
{{5 * 8, 12 * 8}, "Phase:", Theme::getInstance()->fg_light->foreground},
|
||||
{{2 * 8, 12 * 8}, "Polarity:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), 14 * 8}, "Message:", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
OptionsField options_bitrate{
|
||||
@@ -113,12 +113,20 @@ class POCSAGTXView : public View {
|
||||
{"C", 2},
|
||||
{"D", 3}}};
|
||||
|
||||
OptionsField options_phase{
|
||||
/*
|
||||
* TX polarity (CCIR Rec. 584):
|
||||
* "Standard" (value 0): bit 1 = negative deviation (CCIR Rec. 584 standard).
|
||||
* Codewords are bitwise-inverted before the FSK modulator
|
||||
* because the modulator maps bit 1 to positive deviation.
|
||||
* "Inverted" (value 1): bit 1 = positive deviation (non-standard).
|
||||
* Codewords are sent as-is to the modulator.
|
||||
*/
|
||||
OptionsField options_polarity{
|
||||
{11 * 8, 12 * 8},
|
||||
1,
|
||||
8,
|
||||
{
|
||||
{"P", 0},
|
||||
{"N", 1},
|
||||
{"Standard", 0},
|
||||
{"Inverted", 1},
|
||||
}};
|
||||
|
||||
Text text_message{
|
||||
|
||||
@@ -1262,7 +1262,7 @@ static void cmd_settingsreset(BaseSequentialStream* chp, int argc, char* argv[])
|
||||
}
|
||||
|
||||
static void cmd_sendpocsag(BaseSequentialStream* chp, int argc, char* argv[]) {
|
||||
const char* usage = "usage: sendpocsag <addr> <msglen> [baud] [type] [function] [phase] \r\n";
|
||||
const char* usage = "usage: sendpocsag <addr> <msglen> [baud] [type] [function] [polarity:S|I] \r\n";
|
||||
(void)argv;
|
||||
if (argc < 2) {
|
||||
chprintf(chp, usage);
|
||||
@@ -1293,34 +1293,48 @@ static void cmd_sendpocsag(BaseSequentialStream* chp, int argc, char* argv[]) {
|
||||
}
|
||||
}
|
||||
|
||||
char function = 'D';
|
||||
char function = 'A';
|
||||
if (argc >= 5) {
|
||||
function = *argv[4];
|
||||
if (function < 'A' && function > 'D') {
|
||||
if (function < 'A' || function > 'D') {
|
||||
chprintf(chp, "error, function can be A, B, C or D\r\n");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
char phase = 'P';
|
||||
char polarity = 'S'; /* Standard = CCIR Rec. 584 (bit 1 = negative deviation) */
|
||||
if (argc >= 6) {
|
||||
phase = *argv[5];
|
||||
if (phase != 'P' && phase != 'N') {
|
||||
chprintf(chp, "error, phase can be P or N\r\n");
|
||||
polarity = *argv[5];
|
||||
/*
|
||||
* Legacy compatibility: old firmware used 'P'/'N' with opposite semantics.
|
||||
* Old 'P' (phase=positive) = inverted codewords = standard POCSAG = new 'S' (Standard)
|
||||
* Old 'N' (phase=negative) = no inversion = inverted POCSAG = new 'I' (Inverted)
|
||||
* New firmware uses 'S'/'I':
|
||||
* 'S' = Standard (CCIR Rec. 584, bit 1 = negative deviation)
|
||||
* 'I' = Inverted (bit 1 = positive deviation)
|
||||
*/
|
||||
if (polarity == 'P') polarity = 'S'; /* legacy 'P' maps to Standard */
|
||||
if (polarity == 'N') polarity = 'I'; /* legacy 'N' maps to Inverted */
|
||||
if (polarity != 'S' && polarity != 'I') {
|
||||
chprintf(chp, "error, polarity can be S (Standard) or I (Inverted)\r\n");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t msg[81] = {0};
|
||||
if (msglen > 0) {
|
||||
chprintf(chp, "send %d bytes\r\n", msglen);
|
||||
do {
|
||||
size_t bytes_to_read = msglen > USB_BULK_BUFFER_SIZE ? USB_BULK_BUFFER_SIZE : msglen;
|
||||
size_t bytes_read = chSequentialStreamRead(chp, &msg[0], bytes_to_read);
|
||||
uint8_t msg[31] = {0};
|
||||
uint8_t original_msglen = (msglen > 31) ? 31 : msglen;
|
||||
if (original_msglen > 0) {
|
||||
chprintf(chp, "send %d bytes\r\n", original_msglen);
|
||||
size_t offset = 0;
|
||||
size_t remaining = original_msglen;
|
||||
while (remaining > 0) {
|
||||
size_t bytes_to_read = remaining > USB_BULK_BUFFER_SIZE ? USB_BULK_BUFFER_SIZE : remaining;
|
||||
size_t bytes_read = chSequentialStreamRead(chp, &msg[offset], bytes_to_read);
|
||||
if (bytes_read != bytes_to_read)
|
||||
return;
|
||||
msglen -= bytes_read;
|
||||
} while (msglen > 0);
|
||||
offset += bytes_read;
|
||||
remaining -= bytes_read;
|
||||
}
|
||||
}
|
||||
|
||||
auto evtd = getEventDispatcherInstance();
|
||||
@@ -1334,7 +1348,54 @@ static void cmd_sendpocsag(BaseSequentialStream* chp, int argc, char* argv[]) {
|
||||
return;
|
||||
}
|
||||
chThdSleepMilliseconds(1000); // wait for app to start
|
||||
PocsagTosendMessage message{(uint16_t)baud, (uint8_t)type, function, phase, (uint8_t)msglen, msg, addr};
|
||||
PocsagTosendMessage message{(uint16_t)baud, (uint8_t)type, function, polarity, original_msglen, msg, addr};
|
||||
EventDispatcher::send_message(message);
|
||||
chprintf(chp, "ok\r\n");
|
||||
}
|
||||
|
||||
static void cmd_sendflex(BaseSequentialStream* chp, int argc, char* argv[]) {
|
||||
const char* usage = "usage: sendflex <capcode> <type> <msglen>\r\n type: 0=alpha 1=numeric 2=short/tone 3=short numeric\r\n";
|
||||
if (argc < 3) {
|
||||
chprintf(chp, usage);
|
||||
return;
|
||||
}
|
||||
uint64_t capcode = strtoull(argv[0], nullptr, 10);
|
||||
if (capcode < 1 || capcode > 4297068542ULL) {
|
||||
chprintf(chp, "error, capcode 1-4297068542\r\n");
|
||||
return;
|
||||
}
|
||||
int type = atoi(argv[1]);
|
||||
if (type < 0 || type > 3) {
|
||||
chprintf(chp, "error, type 0=alpha 1=numeric 2=short/tone 3=short numeric\r\n");
|
||||
return;
|
||||
}
|
||||
int msglen = atoi(argv[2]);
|
||||
if (msglen < 0 || msglen > 240) {
|
||||
chprintf(chp, "error, msglen max 240\r\n");
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t msg[240] = {0};
|
||||
if (msglen > 0) {
|
||||
chprintf(chp, "send %d bytes\r\n", msglen);
|
||||
int offset = 0;
|
||||
do {
|
||||
size_t bytes_to_read = (msglen - offset) > USB_BULK_BUFFER_SIZE ? USB_BULK_BUFFER_SIZE : (msglen - offset);
|
||||
size_t bytes_read = chSequentialStreamRead(chp, &msg[offset], bytes_to_read);
|
||||
if (bytes_read != bytes_to_read)
|
||||
return;
|
||||
offset += bytes_read;
|
||||
} while (offset < msglen);
|
||||
}
|
||||
|
||||
auto evtd = getEventDispatcherInstance();
|
||||
if (!evtd) return;
|
||||
auto top_widget = evtd->getTopWidget();
|
||||
if (!top_widget) return;
|
||||
auto nav = static_cast<ui::SystemView*>(top_widget)->get_navigation_view();
|
||||
if (!nav) return;
|
||||
|
||||
FlexTosendMessage message{capcode, (uint8_t)type, (uint8_t)msglen, msg};
|
||||
EventDispatcher::send_message(message);
|
||||
chprintf(chp, "ok\r\n");
|
||||
}
|
||||
@@ -1486,6 +1547,7 @@ static const ShellCommand commands[] = {
|
||||
{"pmemreset", cmd_pmemreset},
|
||||
{"settingsreset", cmd_settingsreset},
|
||||
{"sendpocsag", cmd_sendpocsag},
|
||||
{"sendflex", cmd_sendflex},
|
||||
{"asyncmsg", cmd_asyncmsg},
|
||||
{"setfreq", cmd_setfreq},
|
||||
{"getres", cmd_getres},
|
||||
|
||||
@@ -380,13 +380,6 @@ set(MODE_CPPSRC
|
||||
)
|
||||
DeclareTargets(PCAP capture)
|
||||
|
||||
### ERT
|
||||
|
||||
set(MODE_CPPSRC
|
||||
proc_ert.cpp
|
||||
)
|
||||
DeclareTargets(PERT ert)
|
||||
|
||||
### Radiosonde
|
||||
|
||||
set(MODE_CPPSRC
|
||||
@@ -452,13 +445,6 @@ set(MODE_CPPSRC
|
||||
DeclareTargets(PREP replay)
|
||||
|
||||
|
||||
### Tones
|
||||
|
||||
set(MODE_CPPSRC
|
||||
proc_tones.cpp
|
||||
)
|
||||
DeclareTargets(PTON tones)
|
||||
|
||||
|
||||
### Wideband Spectrum
|
||||
|
||||
@@ -707,6 +693,15 @@ set(MODE_CPPSRC
|
||||
DeclareTargets(PRTR rtty_rx)
|
||||
|
||||
|
||||
|
||||
### ERT
|
||||
|
||||
set(MODE_CPPSRC
|
||||
proc_ert.cpp
|
||||
)
|
||||
DeclareTargets(PERT ert)
|
||||
|
||||
|
||||
### RTTY TX
|
||||
|
||||
set(MODE_CPPSRC
|
||||
@@ -729,6 +724,15 @@ set(MODE_CPPSRC
|
||||
)
|
||||
DeclareTargets(PMRT morsetx)
|
||||
|
||||
### Tones
|
||||
|
||||
set(MODE_CPPSRC
|
||||
proc_tones.cpp
|
||||
)
|
||||
DeclareTargets(PTON tones)
|
||||
|
||||
|
||||
|
||||
### Time Sink
|
||||
|
||||
set(MODE_CPPSRC
|
||||
|
||||
+742
-58
@@ -7,7 +7,45 @@
|
||||
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <cstdio> // for snprintf
|
||||
|
||||
// BCD character table for FLEX numeric messages (index 0-15)
|
||||
static const char flex_bcd[] = "0123456789.U -][";
|
||||
|
||||
// Lightweight string helpers (no snprintf/heap on bare-metal M4)
|
||||
namespace {
|
||||
|
||||
char* str_append(char* dst, const char* end, const char* src) {
|
||||
while (*src && dst < end - 1) *dst++ = *src++;
|
||||
*dst = '\0';
|
||||
return dst;
|
||||
}
|
||||
|
||||
char* str_uint(char* dst, const char* end, uint32_t val, int min_digits = 1) {
|
||||
char tmp[11];
|
||||
int i = 0;
|
||||
if (val == 0) {
|
||||
tmp[i++] = '0';
|
||||
} else {
|
||||
while (val > 0) {
|
||||
tmp[i++] = '0' + (val % 10);
|
||||
val /= 10;
|
||||
}
|
||||
}
|
||||
while (i < min_digits) tmp[i++] = '0';
|
||||
for (int j = i - 1; j >= 0 && dst < end - 1; j--) *dst++ = tmp[j];
|
||||
*dst = '\0';
|
||||
return dst;
|
||||
}
|
||||
|
||||
char* str_hex(char* dst, const char* end, uint32_t val, int digits) {
|
||||
static const char hex[] = "0123456789ABCDEF";
|
||||
for (int i = digits - 1; i >= 0 && dst < end - 1; i--)
|
||||
*dst++ = hex[(val >> (i * 4)) & 0xF];
|
||||
*dst = '\0';
|
||||
return dst;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Constants from demod_flex.c
|
||||
#define FREQ_SAMP 24000 // Our sample rate
|
||||
@@ -155,7 +193,6 @@ uint32_t bit_reverse_32(uint32_t x) {
|
||||
|
||||
void FlexProcessor::send_debug(const char* text, uint32_t v1, uint32_t v2) {
|
||||
if (shared_memory.application_queue.is_empty()) return;
|
||||
|
||||
FlexDebugMessage message(v1, v2, text);
|
||||
shared_memory.application_queue.push(message);
|
||||
}
|
||||
@@ -403,7 +440,9 @@ int FlexProcessor::decode_fiw() {
|
||||
fiw.checksum = fiw_val & 0xF;
|
||||
fiw.cycleno = (fiw_val >> 4) & 0xF;
|
||||
fiw.frameno = (fiw_val >> 8) & 0x7F;
|
||||
fiw.fix3 = (fiw_val >> 15) & 0x3F;
|
||||
fiw.roaming = (fiw_val >> 15) & 0x01;
|
||||
fiw.repeat = (fiw_val >> 16) & 0x01;
|
||||
fiw.traffic = (fiw_val >> 17) & 0x0F;
|
||||
|
||||
unsigned int checksum = (fiw_val & 0xF);
|
||||
checksum += ((fiw_val >> 4) & 0xF);
|
||||
@@ -509,6 +548,9 @@ void FlexProcessor::flex_sym(unsigned char sym) {
|
||||
if (state.fiwcount == 48) {
|
||||
if (decode_fiw() == 0) {
|
||||
state.sync2_count = 0;
|
||||
state.sync2_shiftreg = 0;
|
||||
state.sync2_c_pos = -1;
|
||||
state.sync2_cinv_pos = -1;
|
||||
demodulator.baud = sync.baud;
|
||||
state.Current = flex::State::SYNC2;
|
||||
send_debug("FIW OK", fiw.frameno, fiw.cycleno);
|
||||
@@ -520,8 +562,48 @@ void FlexProcessor::flex_sym(unsigned char sym) {
|
||||
break;
|
||||
}
|
||||
case flex::State::SYNC2: {
|
||||
if (++state.sync2_count == sync.baud * 25 / 1000) {
|
||||
state.data_count = 0;
|
||||
/* S2 structure: BS2 + C(16 bits) + inv.BS2 + inv.C(16 bits)
|
||||
* Total duration: 25ms at the data symbol rate.
|
||||
*
|
||||
* We scan for the 16-bit C pattern (0xED84) using a shift
|
||||
* register. If found, we validate timing. If not found,
|
||||
* we fall back to the nominal 25ms skip (current behavior).
|
||||
*
|
||||
* Only the MSB (bit_a) matters for C detection - it's a
|
||||
* 2-level pattern even in 4FSK modes. */
|
||||
unsigned char s2_sym = sync.polarity ? (3 - sym) : sym;
|
||||
int bit_a = (s2_sym > 1) ? 1 : 0;
|
||||
state.sync2_shiftreg = (state.sync2_shiftreg << 1) | bit_a;
|
||||
state.sync2_count++;
|
||||
|
||||
/* Check for C pattern match (Hamming distance <= 2) */
|
||||
if (state.sync2_count >= 16) {
|
||||
uint16_t diff_c = state.sync2_shiftreg ^ 0xED84;
|
||||
uint16_t diff_cinv = state.sync2_shiftreg ^ 0x127B;
|
||||
int errs_c = __builtin_popcount(diff_c);
|
||||
int errs_cinv = __builtin_popcount(diff_cinv);
|
||||
|
||||
if (errs_c <= 2 && state.sync2_c_pos < 0)
|
||||
state.sync2_c_pos = (int)state.sync2_count;
|
||||
if (errs_cinv <= 2 && state.sync2_cinv_pos < 0)
|
||||
state.sync2_cinv_pos = (int)state.sync2_count;
|
||||
}
|
||||
|
||||
/* Nominal S2 duration in symbols */
|
||||
unsigned int s2_nominal = sync.baud * 25 / 1000;
|
||||
|
||||
/* Data starts after inv.C ends. If we detected inv.C,
|
||||
* use its position as the true data boundary. Otherwise
|
||||
* fall back to the nominal count. */
|
||||
unsigned int s2_end = s2_nominal;
|
||||
if (state.sync2_cinv_pos > 0) {
|
||||
unsigned int cinv_end = (unsigned int)state.sync2_cinv_pos;
|
||||
int diff = (int)cinv_end - (int)s2_nominal;
|
||||
if (diff >= -1 && diff <= 1)
|
||||
s2_end = cinv_end;
|
||||
}
|
||||
|
||||
if (state.sync2_count == s2_end) {
|
||||
// Clear phase data
|
||||
for (int i = 0; i < 88; i++) {
|
||||
data.PhaseA.buf[i] = 0;
|
||||
@@ -535,9 +617,21 @@ void FlexProcessor::flex_sym(unsigned char sym) {
|
||||
data.PhaseD.idle_count = 0;
|
||||
data.phase_toggle = 0;
|
||||
data.data_bit_counter = 0;
|
||||
state.data_count = 0;
|
||||
|
||||
state.sync2_shiftreg = 0;
|
||||
state.sync2_c_pos = -1;
|
||||
state.sync2_cinv_pos = -1;
|
||||
|
||||
state.Current = flex::State::DATA;
|
||||
}
|
||||
/* Safety: don't get stuck past nominal */
|
||||
if (state.sync2_count > s2_nominal + 1) {
|
||||
state.sync2_shiftreg = 0;
|
||||
state.sync2_c_pos = -1;
|
||||
state.sync2_cinv_pos = -1;
|
||||
state.Current = flex::State::SYNC1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case flex::State::DATA: {
|
||||
@@ -593,26 +687,204 @@ void FlexProcessor::decode_phase(char PhaseNo) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Check if phase is all idle BEFORE BCH correction.
|
||||
* Idle fill uses alternating 0xFFFFFFFF and 0x00000000 words.
|
||||
* If every word is one of these two patterns, the phase has no
|
||||
* real data - skip it to avoid BCH "correcting" idle into garbage. */
|
||||
{
|
||||
int all_idle = 1;
|
||||
for (int i = 0; i < 88; i++) {
|
||||
if (phaseptr[i] != 0xFFFFFFFF && phaseptr[i] != 0x00000000) {
|
||||
all_idle = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (all_idle) return;
|
||||
}
|
||||
|
||||
/* BCH decode each word. Mark uncorrectable words but continue. */
|
||||
uint8_t word_bad[88] = {0};
|
||||
for (int i = 0; i < 88; i++) {
|
||||
int decode_error = bch_fix_errors(&phaseptr[i]);
|
||||
if (decode_error > 2) return;
|
||||
phaseptr[i] &= 0x001FFFFF; // Extract message bits
|
||||
if (decode_error > 2) {
|
||||
word_bad[i] = 1;
|
||||
phaseptr[i] = 0;
|
||||
}
|
||||
phaseptr[i] &= 0x001FFFFF;
|
||||
}
|
||||
|
||||
/* BIW must be good to proceed */
|
||||
if (word_bad[0]) return;
|
||||
|
||||
uint32_t biw = phaseptr[0];
|
||||
if (biw == 0 || biw == 0x001FFFFF) return;
|
||||
|
||||
int voffset = (biw >> 10) & 0x3f;
|
||||
int aoffset = ((biw >> 8) & 0x03) + 1;
|
||||
int prio_count = (biw >> 4) & 0x0F; // number of priority address words
|
||||
|
||||
if (voffset < aoffset || voffset >= 88) return;
|
||||
|
||||
/* Always send BIW1 packet so the app knows we decoded a frame.
|
||||
* This updates the status bar even for idle frames. */
|
||||
{
|
||||
flex::FlexPacket bpkt{};
|
||||
bpkt.type = 9;
|
||||
bpkt.bitrate = sync.baud * (sync.levels == 4 ? 2 : 1);
|
||||
bpkt.cycle = fiw.cycleno;
|
||||
bpkt.frame = fiw.frameno;
|
||||
bpkt.phase = PhaseNo;
|
||||
bpkt.is_inverted = sync.polarity;
|
||||
bpkt.fiw_roaming = fiw.roaming;
|
||||
bpkt.function = 0;
|
||||
bpkt.biw_field = 0xFF;
|
||||
bpkt.message[0] = '\0';
|
||||
send_packet(bpkt);
|
||||
}
|
||||
|
||||
/* Parse BIW words (indices 1 through aoffset-1).
|
||||
* Each BIW word has a 3-bit type field (bits 4-6) that determines content.
|
||||
* Send each as a BIW event packet. */
|
||||
for (int bw = 1; bw < aoffset && bw < 88; bw++) {
|
||||
if (word_bad[bw]) continue;
|
||||
uint32_t bword = phaseptr[bw];
|
||||
uint32_t btype = (bword >> 4) & 0x07;
|
||||
|
||||
/* Skip reserved types (3, 4, 6) */
|
||||
if (btype == 3 || btype == 4 || btype == 6) continue;
|
||||
|
||||
flex::FlexPacket bpkt{};
|
||||
bpkt.type = 9; // BIW event
|
||||
bpkt.bitrate = sync.baud * (sync.levels == 4 ? 2 : 1);
|
||||
bpkt.cycle = fiw.cycleno;
|
||||
bpkt.frame = fiw.frameno;
|
||||
bpkt.phase = PhaseNo;
|
||||
bpkt.is_inverted = sync.polarity;
|
||||
bpkt.fiw_roaming = fiw.roaming;
|
||||
bpkt.function = bw; // BIW word index
|
||||
bpkt.biw_field = btype; // BIW type (0,1,2,5,7)
|
||||
bpkt.message[0] = '\0';
|
||||
|
||||
switch (btype) {
|
||||
case 0: // SSID1: v1=lid, v2=cz
|
||||
bpkt.biw_v1 = (bword >> 12) & 0x01FF;
|
||||
bpkt.biw_v2 = (bword >> 7) & 0x1F;
|
||||
break;
|
||||
case 1: // Date: v1=year(+1994), v2=month, v3=day
|
||||
bpkt.biw_v1 = ((bword >> 7) & 0x1F) + 1994;
|
||||
bpkt.biw_v2 = (bword >> 17) & 0x0F;
|
||||
bpkt.biw_v3 = (bword >> 12) & 0x1F;
|
||||
break;
|
||||
case 2: // Time: v1=hour, v2=minute, v3=sec_raw(0-7)
|
||||
bpkt.biw_v1 = (bword >> 7) & 0x1F;
|
||||
bpkt.biw_v2 = (bword >> 12) & 0x3F;
|
||||
bpkt.biw_v3 = (bword >> 18) & 0x07;
|
||||
break;
|
||||
case 5: // SysInfo: v1=a_type, v2=info(10 bits)
|
||||
bpkt.biw_v1 = (bword >> 7) & 0x0F;
|
||||
bpkt.biw_v2 = (bword >> 11) & 0x03FF;
|
||||
break;
|
||||
case 7: // SSID2: v1=country, v2=tmf
|
||||
bpkt.biw_v1 = (bword >> 11) & 0x03FF;
|
||||
bpkt.biw_v2 = (bword >> 7) & 0x0F;
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
send_packet(bpkt);
|
||||
}
|
||||
|
||||
/* Pre-scan: count valid vector words using 4-bit nibble checksum.
|
||||
* Tone-only addresses sit at the end of the address field with no
|
||||
* corresponding vector. We find the last vector that passes checksum.
|
||||
* Note: for long addresses, the 2nd vector word (Vy) is a message word
|
||||
* that won't pass checksum - so we count all passing words, not just
|
||||
* consecutive ones from the start. */
|
||||
int n_valid_vecs = 0;
|
||||
for (int vi = 0; vi < (voffset - aoffset); vi++) {
|
||||
int wi = voffset + vi;
|
||||
if (wi >= 88) break;
|
||||
uint32_t vw = phaseptr[wi];
|
||||
uint32_t csum = (vw & 0xF) + ((vw >> 4) & 0xF) + ((vw >> 8) & 0xF) +
|
||||
((vw >> 12) & 0xF) + ((vw >> 16) & 0xF) + ((vw >> 20) & 0x1);
|
||||
if ((csum & 0xF) == 0xF)
|
||||
n_valid_vecs = vi + 1; // track highest passing index + 1
|
||||
}
|
||||
|
||||
/* No addresses if voffset == aoffset */
|
||||
if (voffset <= aoffset) return;
|
||||
|
||||
int vec_count = 0;
|
||||
int addr_count = 0; // tracks address word position for priority detection
|
||||
for (int i = aoffset; i < voffset; i++) {
|
||||
int j = voffset + i - aoffset;
|
||||
int j = voffset + vec_count;
|
||||
if (j >= 88) break;
|
||||
if (phaseptr[i] == 0x00000000 || phaseptr[i] == 0x001FFFFF) continue;
|
||||
|
||||
parse_capcode(phaseptr[i]);
|
||||
if (decode.long_address) continue; // Skip long addresses for now
|
||||
/* Address word - all 21 information bits are address data
|
||||
* per 3.8.2. Address type is determined by value range
|
||||
* (Table 3.8.1-1). Temporary addresses are range
|
||||
* 0x1F7800-0x1F780F (3.8.2.3), identified via addr_type. */
|
||||
int is_priority = (addr_count < prio_count) ? 1 : 0;
|
||||
|
||||
if (decode.capcode > 4297068542ll || decode.capcode < 0) continue;
|
||||
parse_capcode(phaseptr[i]);
|
||||
decode.is_priority = is_priority;
|
||||
addr_count++;
|
||||
|
||||
if (decode.long_address) {
|
||||
/* Long address: 2 address words, 2 vector words.
|
||||
* Read second address word and compute capcode from set. */
|
||||
if (i + 1 >= voffset) break; // truncated
|
||||
uint32_t aw1 = phaseptr[i];
|
||||
uint32_t aw2 = phaseptr[i + 1];
|
||||
if (aw2 == 0x00000000 || aw2 == 0x001FFFFF) {
|
||||
i++;
|
||||
addr_count++; // second address word counts
|
||||
vec_count += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
int64_t cap = 0;
|
||||
if (aw1 >= 0x000001 && aw1 <= 0x008000 &&
|
||||
aw2 >= 0x1F7FFF && aw2 <= 0x1FFFFE) {
|
||||
/* Set 1-2 */
|
||||
cap = (int64_t)aw1 + (int64_t)(0x1FFFFF - aw2) * 32768LL + 2068480LL;
|
||||
} else if (aw1 >= 0x000001 && aw1 <= 0x008000 &&
|
||||
aw2 >= 0x1E0001 && aw2 <= 0x1F0000) {
|
||||
/* Set 1-3 / 1-4 */
|
||||
cap = (int64_t)aw1 + (int64_t)(aw2 - 1933312) * 32768LL + 2068480LL;
|
||||
} else if (aw1 >= 0x1F7FFF && aw1 <= 0x1FFFFE &&
|
||||
aw2 >= 0x1E0001 && aw2 <= 0x1F0000) {
|
||||
/* Set 2-3 */
|
||||
cap = (int64_t)(aw1 - 2064383) + (int64_t)(aw2 - 1867776) * 32768LL + 2068479LL;
|
||||
} else {
|
||||
/* Unknown set - skip */
|
||||
i++;
|
||||
addr_count++; // second address word counts
|
||||
vec_count += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
decode.capcode = cap;
|
||||
i++; // consumed 2 address words
|
||||
addr_count++; // second address word also counts
|
||||
|
||||
/* Long addresses always have vectors - they cannot be tone-only.
|
||||
* (Tone-only is only for short addresses at the end of AF.)
|
||||
* The second vector word (Vy) contains the first message word,
|
||||
* not a checksummed vector, so skip the pre-scan check here. */
|
||||
vec_count += 2; // consumed 2 vector words
|
||||
j = voffset + vec_count - 2; // point to first vector word of pair
|
||||
} else {
|
||||
if (decode.capcode > 4297068542ll || decode.capcode <= 0) continue;
|
||||
|
||||
/* Tone-only: address beyond valid vector range */
|
||||
if (vec_count >= n_valid_vecs) {
|
||||
parse_tone_only(phaseptr, PhaseNo, 0);
|
||||
continue;
|
||||
}
|
||||
vec_count++;
|
||||
}
|
||||
|
||||
uint32_t viw = phaseptr[j];
|
||||
int type_val = (viw >> 4) & 0x07;
|
||||
@@ -645,7 +917,14 @@ void FlexProcessor::decode_phase(char PhaseNo) {
|
||||
}
|
||||
|
||||
int mw1 = (viw >> 7) & 0x7F;
|
||||
int len = (viw >> 14) & 0x7F;
|
||||
int len;
|
||||
/* Numeric types (3, 4, 7) have a 3-bit n field (bits 14-16)
|
||||
* encoding word_count - 1. Bits 17-20 are the K checksum.
|
||||
* Alpha/hex/secure types use the full 7-bit field (bits 14-20). */
|
||||
if (type_val == 3 || type_val == 4 || type_val == 7)
|
||||
len = ((viw >> 14) & 0x07) + 1;
|
||||
else
|
||||
len = (viw >> 14) & 0x7F;
|
||||
int mw2 = mw1 + (len - 1);
|
||||
|
||||
if (mw1 == 0 && mw2 == 0) continue;
|
||||
@@ -653,121 +932,526 @@ void FlexProcessor::decode_phase(char PhaseNo) {
|
||||
|
||||
if (decode.type == flex::PageType::ALPHANUMERIC || decode.type == flex::PageType::SECURE) {
|
||||
if (mw1 > 87 || mw2 > 87) continue;
|
||||
parse_alphanumeric(phaseptr, PhaseNo, mw1, mw2, 0);
|
||||
if (decode.long_address) {
|
||||
/* For long addresses, body[0] (header with K,C,F,N,R,M) is at
|
||||
* Vy (j+1), not at mw1. The vector's mw1 points to body[1]
|
||||
* in the message field, and len includes body[0].
|
||||
* parse_alphanumeric expects mw1 = header word index (it does
|
||||
* mw1++ internally to skip header). So pass mw1-1 so the
|
||||
* skip lands on mw1 (first real data word). */
|
||||
parse_alphanumeric(phaseptr, word_bad, PhaseNo, mw1 - 1, mw2 - 1, 0);
|
||||
} else {
|
||||
parse_alphanumeric(phaseptr, word_bad, PhaseNo, mw1, mw2, 0);
|
||||
}
|
||||
} else if (decode.type == flex::PageType::STANDARD_NUMERIC || decode.type == flex::PageType::SPECIAL_NUMERIC || decode.type == flex::PageType::NUMBERED_NUMERIC) {
|
||||
parse_numeric(phaseptr, PhaseNo, j);
|
||||
parse_numeric(phaseptr, word_bad, PhaseNo, j);
|
||||
} else if (decode.type == flex::PageType::TONE) {
|
||||
parse_tone_only(phaseptr, PhaseNo, j);
|
||||
} else {
|
||||
// Unknown or unsupported
|
||||
/* Vector type 2: Short Message (3.9.2).
|
||||
* Sub-type t1t0 in bits 7-8, data d0-d11 in bits 9-20. */
|
||||
uint32_t t = (viw >> 7) & 0x03;
|
||||
uint32_t d = (viw >> 9) & 0x0FFF;
|
||||
|
||||
flex::FlexPacket packet{};
|
||||
packet.bitrate = sync.baud * (sync.levels == 4 ? 2 : 1);
|
||||
packet.capcode = decode.capcode;
|
||||
packet.function = t;
|
||||
packet.cycle = fiw.cycleno;
|
||||
packet.frame = fiw.frameno;
|
||||
packet.phase = PhaseNo;
|
||||
packet.is_inverted = sync.polarity;
|
||||
packet.fiw_roaming = fiw.roaming;
|
||||
packet.addr_type = static_cast<uint8_t>(decode.addr_type);
|
||||
packet.is_priority = decode.is_priority;
|
||||
packet.type = 8; // SHORT
|
||||
|
||||
if (t == 0 && d == 0xCCC) {
|
||||
/* Tone-only: all digits are space (0xC) per STD-43A
|
||||
* Table 3.9.2-1 note. For long addresses, also check Vy. */
|
||||
bool tone = true;
|
||||
if (decode.long_address && j + 1 < 88) {
|
||||
uint32_t vy = phaseptr[j + 1] & 0xFFFFF;
|
||||
if (vy != 0xCCCCC) tone = false;
|
||||
}
|
||||
if (tone)
|
||||
strcpy(packet.message, "TONE");
|
||||
else
|
||||
goto short_numeric;
|
||||
} else if (t == 0) {
|
||||
short_numeric:
|
||||
/* Numeric: 3 BCD digits from Vx (d0-d11).
|
||||
* Long addresses: 5 more digits from Vy (d12-d31),
|
||||
* 8 digits total. d32 is spare (set to 0). */
|
||||
char *p = packet.message, *e = p + sizeof(packet.message);
|
||||
p = str_append(p, e, "NUM ");
|
||||
*p++ = flex_bcd[(d >> 0) & 0xF];
|
||||
*p++ = flex_bcd[(d >> 4) & 0xF];
|
||||
*p++ = flex_bcd[(d >> 8) & 0xF];
|
||||
if (decode.long_address && j + 1 < 88) {
|
||||
uint32_t vy = phaseptr[j + 1];
|
||||
*p++ = flex_bcd[(vy >> 0) & 0xF];
|
||||
*p++ = flex_bcd[(vy >> 4) & 0xF];
|
||||
*p++ = flex_bcd[(vy >> 8) & 0xF];
|
||||
*p++ = flex_bcd[(vy >> 12) & 0xF];
|
||||
*p++ = flex_bcd[(vy >> 16) & 0xF];
|
||||
}
|
||||
*p = '\0';
|
||||
} else if (t == 1) {
|
||||
/* Source: S2S1S0 in d0-d2 */
|
||||
char *p = packet.message, *e = p + sizeof(packet.message);
|
||||
p = str_append(p, e, "SRC ");
|
||||
str_uint(p, e, d & 0x07);
|
||||
} else if (t == 2) {
|
||||
/* Numbered: S(3) + N(6) + R(1) */
|
||||
uint32_t src = d & 0x07;
|
||||
uint32_t n = (d >> 3) & 0x3F;
|
||||
uint32_t r = (d >> 9) & 0x01;
|
||||
char *p = packet.message, *e = p + sizeof(packet.message);
|
||||
p = str_append(p, e, "SRC ");
|
||||
p = str_uint(p, e, src);
|
||||
p = str_append(p, e, " N=");
|
||||
p = str_uint(p, e, n);
|
||||
p = str_append(p, e, " R=");
|
||||
str_uint(p, e, r);
|
||||
} else {
|
||||
/* Reserved */
|
||||
char *p = packet.message, *e = p + sizeof(packet.message);
|
||||
p = str_append(p, e, "RESERVED ");
|
||||
str_hex(p, e, d, 3);
|
||||
}
|
||||
send_packet(packet);
|
||||
} else if (decode.type == flex::PageType::BINARY) {
|
||||
/* HEX/Binary message.
|
||||
* Word 1 (mw1): K(12) C(1) F(2) N(6) = header
|
||||
* Word 2 (mw1+1, first frag only): R(1) M(1) D(1) H(1) B(4) I(1) rsvd(4) S(8)
|
||||
* Words 3+: data */
|
||||
if (mw1 > 87 || mw2 > 87) continue;
|
||||
|
||||
/* Extract header from word 1 */
|
||||
uint8_t hex_c = 0, hex_f = 0, hex_n = 0;
|
||||
int hex_hdr_valid = 0;
|
||||
if (!word_bad[mw1]) {
|
||||
uint32_t hw1 = phaseptr[mw1];
|
||||
hex_c = (hw1 >> 12) & 0x01;
|
||||
hex_f = (hw1 >> 13) & 0x03;
|
||||
hex_n = (hw1 >> 15) & 0x3F;
|
||||
hex_hdr_valid = 1;
|
||||
}
|
||||
|
||||
/* Extract word 2 flags (first fragment: F=3) */
|
||||
uint8_t hex_r = 0, hex_m = 0, hex_d = 0, hex_b = 0;
|
||||
int data_start = mw1 + 1; // default: data starts after header
|
||||
if (hex_f == 3 && (mw1 + 1) <= mw2 && !word_bad[mw1 + 1]) {
|
||||
uint32_t hw2 = phaseptr[mw1 + 1];
|
||||
hex_r = (hw2 >> 0) & 0x01;
|
||||
hex_m = (hw2 >> 1) & 0x01;
|
||||
hex_d = (hw2 >> 2) & 0x01;
|
||||
hex_b = (hw2 >> 4) & 0x0F;
|
||||
data_start = mw1 + 2; // skip both header words
|
||||
}
|
||||
|
||||
/* Dump data words as hex */
|
||||
char message[256] = {0};
|
||||
char *mp = message, *me = message + 250;
|
||||
for (int w = data_start; w <= mw2 && mp < me; w++) {
|
||||
if (word_bad[w]) {
|
||||
mp = str_append(mp, me, "?????? ");
|
||||
} else {
|
||||
mp = str_hex(mp, me, phaseptr[w] & 0x1FFFFF, 5);
|
||||
if (mp < me) *mp++ = ' ';
|
||||
*mp = '\0';
|
||||
}
|
||||
}
|
||||
if (mp > message && *(mp - 1) == ' ') {
|
||||
mp--;
|
||||
*mp = '\0';
|
||||
}
|
||||
int pos = (int)(mp - message);
|
||||
|
||||
flex::FlexPacket packet{};
|
||||
packet.bitrate = sync.baud * (sync.levels == 4 ? 2 : 1);
|
||||
packet.capcode = decode.capcode;
|
||||
packet.function = 0;
|
||||
packet.type = 6; // HEX
|
||||
packet.status = 0;
|
||||
packet.cycle = fiw.cycleno;
|
||||
packet.frame = fiw.frameno;
|
||||
packet.phase = PhaseNo;
|
||||
packet.is_inverted = sync.polarity;
|
||||
packet.fiw_roaming = fiw.roaming;
|
||||
packet.addr_type = static_cast<uint8_t>(decode.addr_type);
|
||||
packet.is_priority = decode.is_priority;
|
||||
if (hex_hdr_valid) {
|
||||
packet.frag = hex_f;
|
||||
packet.more_frag = hex_c;
|
||||
packet.seq = hex_n;
|
||||
packet.has_flags = 1;
|
||||
if (hex_f == 3) {
|
||||
packet.is_new = hex_r;
|
||||
packet.maildrop = hex_m;
|
||||
/* Store b and d in function field: low nibble=b, bit4=d */
|
||||
packet.function = (hex_d << 4) | hex_b;
|
||||
}
|
||||
}
|
||||
memcpy(packet.message, message, pos + 1);
|
||||
send_packet(packet);
|
||||
} else if (decode.type == flex::PageType::SHORT_INSTRUCTION) {
|
||||
/* Short instruction: 14-bit data in vector bits 7-20.
|
||||
* i2i1i0 (bits 0-2 of data) = instruction type.
|
||||
* Remaining bits = instruction-specific data. */
|
||||
uint32_t instr_data = (viw >> 7) & 0x3FFF;
|
||||
uint32_t itype = instr_data & 0x07;
|
||||
|
||||
flex::FlexPacket packet{};
|
||||
packet.bitrate = sync.baud * (sync.levels == 4 ? 2 : 1);
|
||||
packet.capcode = decode.capcode;
|
||||
packet.function = 0;
|
||||
packet.type = 1; // INS
|
||||
packet.cycle = fiw.cycleno;
|
||||
packet.frame = fiw.frameno;
|
||||
packet.phase = PhaseNo;
|
||||
packet.is_inverted = sync.polarity;
|
||||
packet.fiw_roaming = fiw.roaming;
|
||||
packet.addr_type = static_cast<uint8_t>(decode.addr_type);
|
||||
packet.is_priority = decode.is_priority;
|
||||
|
||||
if (itype == 0) {
|
||||
uint32_t tgt_frame = (instr_data >> 3) & 0x7F;
|
||||
uint32_t slot = (instr_data >> 10) & 0x0F;
|
||||
packet.biw_v1 = slot;
|
||||
packet.biw_v2 = tgt_frame;
|
||||
{
|
||||
char *p = packet.message, *e = p + sizeof(packet.message);
|
||||
p = str_append(p, e, "i=temp|slot=");
|
||||
p = str_uint(p, e, slot);
|
||||
p = str_append(p, e, "|target=");
|
||||
str_uint(p, e, tgt_frame);
|
||||
}
|
||||
} else if (itype == 1) {
|
||||
uint32_t flags = (instr_data >> 3) & 0x7FF;
|
||||
{
|
||||
char *p = packet.message, *e = p + sizeof(packet.message);
|
||||
p = str_append(p, e, "i=event|flags=");
|
||||
str_hex(p, e, flags, 3);
|
||||
}
|
||||
} else {
|
||||
{
|
||||
char *p = packet.message, *e = p + sizeof(packet.message);
|
||||
p = str_append(p, e, "i=rsvd|type=");
|
||||
p = str_uint(p, e, itype);
|
||||
p = str_append(p, e, "|raw=");
|
||||
str_hex(p, e, instr_data, 4);
|
||||
}
|
||||
}
|
||||
send_packet(packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FlexProcessor::parse_capcode(uint32_t aw1) {
|
||||
decode.long_address = (aw1 < 0x008001L) || (aw1 > 0x1E0000L) || (aw1 > 0x1E7FFEL);
|
||||
/* Classify address word by range. */
|
||||
decode.long_address = 0;
|
||||
decode.addr_type = flex::AddrType::SHORT;
|
||||
|
||||
if ((aw1 >= 0x000001 && aw1 <= 0x008000) || /* LA1 */
|
||||
(aw1 >= 0x1E0001 && aw1 <= 0x1E8000) || /* LA3 */
|
||||
(aw1 >= 0x1E8001 && aw1 <= 0x1F0000) || /* LA4 */
|
||||
(aw1 >= 0x1F7FFF && aw1 <= 0x1FFFFE)) { /* LA2 */
|
||||
decode.long_address = 1;
|
||||
decode.addr_type = flex::AddrType::LONG;
|
||||
} else if (aw1 >= 0x1F7800 && aw1 <= 0x1F780F) {
|
||||
decode.addr_type = flex::AddrType::TEMPORARY;
|
||||
} else if (aw1 >= 0x1F7810 && aw1 <= 0x1F781F) {
|
||||
decode.addr_type = flex::AddrType::OPERATOR;
|
||||
} else if (aw1 >= 0x1F6800 && aw1 <= 0x1F77FF) {
|
||||
decode.addr_type = flex::AddrType::NETWORK;
|
||||
} else if (aw1 >= 0x1F2800 && aw1 <= 0x1F67FF) {
|
||||
decode.addr_type = flex::AddrType::INFO_SVC;
|
||||
} else if ((aw1 >= 0x1F0001 && aw1 <= 0x1F27FF) ||
|
||||
(aw1 >= 0x1F7820 && aw1 <= 0x1F7FFE)) {
|
||||
decode.addr_type = flex::AddrType::RESERVED;
|
||||
} else if (aw1 >= 0x008001 && aw1 <= 0x1E0000) {
|
||||
decode.addr_type = flex::AddrType::SHORT;
|
||||
} else {
|
||||
decode.addr_type = flex::AddrType::UNKNOWN;
|
||||
}
|
||||
|
||||
decode.capcode = aw1 - 0x8000;
|
||||
}
|
||||
|
||||
void FlexProcessor::parse_alphanumeric(uint32_t* phaseptr, char, int mw1, int mw2, int) {
|
||||
char message[128] = {0}; // Fixed buffer for message
|
||||
void FlexProcessor::parse_alphanumeric(uint32_t* phaseptr, const uint8_t* word_bad, char PhaseNo, int mw1, int mw2, int) {
|
||||
char message[256] = {0};
|
||||
int currentChar = 0;
|
||||
|
||||
// int frag = (phaseptr[mw1] >> 11) & 0x03;
|
||||
// int cont = (phaseptr[mw1] >> 0x0A) & 0x01;
|
||||
// Helper logic for fragmentation (ignored for basic display)
|
||||
|
||||
/* First message word is the header (K, C, F, N, R, M fields).
|
||||
* Extract flags before skipping to content. */
|
||||
uint8_t hdr_c = 0, hdr_f = 0, hdr_n = 0, hdr_r = 0, hdr_m = 0;
|
||||
uint8_t hdr_sig = 0;
|
||||
int hdr_valid = 0;
|
||||
if (mw1 >= 0 && mw1 < 88 && !word_bad[mw1]) {
|
||||
uint32_t hdr = phaseptr[mw1];
|
||||
hdr_c = (hdr >> 10) & 0x01; // bit 10
|
||||
hdr_f = (hdr >> 11) & 0x03; // bits 11-12
|
||||
hdr_n = (hdr >> 13) & 0x3F; // bits 13-18
|
||||
hdr_r = (hdr >> 19) & 0x01; // bit 19
|
||||
hdr_m = (hdr >> 20) & 0x01; // bit 20
|
||||
hdr_valid = 1;
|
||||
}
|
||||
mw1++;
|
||||
|
||||
/* Extract signature from first data word (bits 0-6) */
|
||||
if (mw1 >= 0 && mw1 < 88 && !word_bad[mw1]) {
|
||||
hdr_sig = phaseptr[mw1] & 0x7F;
|
||||
}
|
||||
|
||||
for (int i = mw1; i <= mw2; i++) {
|
||||
unsigned int dw = phaseptr[i];
|
||||
unsigned char ch;
|
||||
int bad = (i >= 0 && i < 88) ? word_bad[i] : 1;
|
||||
|
||||
// Extract chars (7-bit ASCII)
|
||||
// If i > mw1 (not first word) or fragment check (simplified here)
|
||||
if (i > mw1) {
|
||||
ch = dw & 0x7F;
|
||||
if (ch != 0x03 && currentChar < 127) message[currentChar++] = ch;
|
||||
if (bad) {
|
||||
if (currentChar < 255) message[currentChar++] = '?';
|
||||
} else if (ch >= 0x20 || ch == 0x0A || ch == 0x0D) {
|
||||
if (currentChar < 255) message[currentChar++] = ch;
|
||||
} else if (ch == 0x03 || ch == 0x00) {
|
||||
if (currentChar < 255) message[currentChar++] = '\x03';
|
||||
}
|
||||
}
|
||||
|
||||
ch = (dw >> 7) & 0x7F;
|
||||
if (ch != 0x03 && currentChar < 127) message[currentChar++] = ch;
|
||||
if (bad) {
|
||||
if (currentChar < 255) message[currentChar++] = '?';
|
||||
} else if (ch >= 0x20 || ch == 0x0A || ch == 0x0D) {
|
||||
if (currentChar < 255) message[currentChar++] = ch;
|
||||
} else if (ch == 0x03 || ch == 0x00) {
|
||||
if (currentChar < 255) message[currentChar++] = '\x03';
|
||||
}
|
||||
|
||||
ch = (dw >> 14) & 0x7F;
|
||||
if (ch != 0x03 && currentChar < 127) message[currentChar++] = ch;
|
||||
if (bad) {
|
||||
if (currentChar < 255) message[currentChar++] = '?';
|
||||
} else if (ch >= 0x20 || ch == 0x0A || ch == 0x0D) {
|
||||
if (currentChar < 255) message[currentChar++] = ch;
|
||||
} else if (ch == 0x03 || ch == 0x00) {
|
||||
if (currentChar < 255) message[currentChar++] = '\x03';
|
||||
}
|
||||
}
|
||||
|
||||
/* Post-process: trim trailing ETX/NUL padding, but if printable chars
|
||||
* appear after an ETX/NUL, show each ETX/NUL as '?' (invalid char). */
|
||||
{
|
||||
/* First find the last printable character */
|
||||
int last_printable = -1;
|
||||
for (int k = 0; k < currentChar; k++) {
|
||||
if (message[k] != '\x03') last_printable = k;
|
||||
}
|
||||
/* Now output up to last_printable, replacing ETX with '?' */
|
||||
int out = 0;
|
||||
for (int k = 0; k <= last_printable && out < 255; k++) {
|
||||
if (message[k] == '\x03')
|
||||
message[out++] = '?';
|
||||
else
|
||||
message[out++] = message[k];
|
||||
}
|
||||
currentChar = out;
|
||||
}
|
||||
message[currentChar] = '\0';
|
||||
|
||||
flex::FlexPacket packet;
|
||||
packet.bitrate = sync.baud;
|
||||
flex::FlexPacket packet{};
|
||||
packet.bitrate = sync.baud * (sync.levels == 4 ? 2 : 1);
|
||||
packet.capcode = decode.capcode;
|
||||
packet.function = 0; // TODO extract function if available
|
||||
packet.type = 5; // ALPHANUMERIC
|
||||
packet.status = 0; // OK
|
||||
packet.function = 0;
|
||||
packet.type = (decode.type == flex::PageType::SECURE) ? 0 : 5;
|
||||
packet.status = 0;
|
||||
packet.cycle = fiw.cycleno;
|
||||
packet.frame = fiw.frameno;
|
||||
packet.phase = PhaseNo;
|
||||
packet.is_inverted = sync.polarity;
|
||||
packet.fiw_roaming = fiw.roaming;
|
||||
packet.addr_type = static_cast<uint8_t>(decode.addr_type);
|
||||
packet.is_priority = decode.is_priority;
|
||||
if (hdr_valid) {
|
||||
packet.frag = hdr_f;
|
||||
packet.more_frag = hdr_c;
|
||||
packet.seq = hdr_n;
|
||||
packet.is_new = hdr_r;
|
||||
packet.maildrop = hdr_m;
|
||||
packet.sig = hdr_sig;
|
||||
packet.has_flags = 1;
|
||||
if (decode.type == flex::PageType::SECURE) {
|
||||
/* Secure: bits 19-20 are t1t0 (encoding type), not R/M */
|
||||
packet.sec_enc = (hdr_r) | (hdr_m << 1); // t0=bit19, t1=bit20
|
||||
packet.is_new = 0;
|
||||
packet.maildrop = 0;
|
||||
}
|
||||
}
|
||||
memcpy(packet.message, message, currentChar + 1);
|
||||
|
||||
send_packet(packet);
|
||||
}
|
||||
|
||||
void FlexProcessor::parse_numeric(uint32_t* phaseptr, char, int j) {
|
||||
// Simplified numeric parsing
|
||||
char message[128] = {0};
|
||||
const char flex_bcd[] = "0123456789 U -][";
|
||||
void FlexProcessor::parse_numeric(uint32_t* phaseptr, const uint8_t* word_bad, char PhaseNo, int j) {
|
||||
char message[256] = {0};
|
||||
|
||||
/* Extract NNUM header fields from first message word if applicable.
|
||||
* Layout: K5K4(2) + N0-N5(6) + R0(1) + S0(1) + BCD digits... */
|
||||
uint8_t nnum_n = 0, nnum_r = 0, nnum_s = 0;
|
||||
int is_nnum = (decode.type == flex::PageType::NUMBERED_NUMERIC);
|
||||
|
||||
int w1 = phaseptr[j] >> 7;
|
||||
int w2 = w1 >> 7;
|
||||
w1 = w1 & 0x7f;
|
||||
w2 = (w2 & 0x07) + w1;
|
||||
int n_field = w2 & 0x07; // word_count - 1
|
||||
w2 = n_field + w1;
|
||||
|
||||
int dw;
|
||||
// Handle short vs long logic if needed (simplified)
|
||||
dw = phaseptr[w1];
|
||||
w1++;
|
||||
w2++;
|
||||
// Bounds check: phase buffer is 88 words (indices 0-87)
|
||||
if (w1 > 87) return;
|
||||
if (w2 > 87) w2 = 87;
|
||||
|
||||
/* For long addresses (3.9.1):
|
||||
* 1-word: b field points to Vy. body[0] at w1.
|
||||
* Multi-word: body[0] at Vy (j+1). b field points to MF body[1]. */
|
||||
int body0_idx;
|
||||
if (decode.long_address && n_field > 0)
|
||||
body0_idx = j + 1; // Vy = 2nd vector word
|
||||
else
|
||||
body0_idx = w1;
|
||||
if (body0_idx < 0 || body0_idx >= 88) return;
|
||||
|
||||
int dw = phaseptr[body0_idx];
|
||||
|
||||
if (is_nnum) {
|
||||
/* Extract N, R, S from the first message word's BCD stream.
|
||||
* After K5K4 (2 bits), next 6 bits = N, then R, then S.
|
||||
* These are consumed by the skip count (count starts at 4+10=14). */
|
||||
nnum_n = (dw >> 2) & 0x3F; // bits 2-7
|
||||
nnum_r = (dw >> 8) & 0x01; // bit 8
|
||||
nnum_s = (dw >> 9) & 0x01; // bit 9
|
||||
}
|
||||
|
||||
unsigned char digit = 0;
|
||||
int count = 4; // Standard numeric skip
|
||||
if (decode.type == flex::PageType::NUMBERED_NUMERIC)
|
||||
count += 10;
|
||||
int count = 4;
|
||||
if (is_nnum)
|
||||
count += 10; // skip K5K4(2) + N(6) + R(1) + S(1)
|
||||
else
|
||||
count += 2;
|
||||
count += 2; // skip K5K4(2)
|
||||
|
||||
int idx = 0;
|
||||
for (int i = w1; i <= w2; i++) {
|
||||
|
||||
/* Phase 1: decode body[0] bits.
|
||||
* For short addresses, body[0] is at w1 and we advance to w1+1.
|
||||
* For long addresses, body[0] is at Vy (j+1), then we continue from w1. */
|
||||
if (word_bad[body0_idx]) {
|
||||
/* Uncorrectable word — emit '?' for each digit slot */
|
||||
int data_bits = 21 - (count - 4); /* bits available after skip */
|
||||
int lost_digits = data_bits / 4;
|
||||
while (lost_digits-- > 0 && idx < 255)
|
||||
message[idx++] = '?';
|
||||
count = 4; /* reset for next word */
|
||||
digit = 0;
|
||||
} else {
|
||||
for (int k = 0; k < 21; k++) {
|
||||
digit = (digit >> 1) & 0x0F;
|
||||
if (dw & 0x01) digit ^= 0x08;
|
||||
dw >>= 1;
|
||||
if (--count == 0) {
|
||||
if (digit != 0x0C && idx < 127) {
|
||||
if (idx < 255) {
|
||||
message[idx++] = flex_bcd[digit];
|
||||
}
|
||||
count = 4;
|
||||
}
|
||||
}
|
||||
dw = phaseptr[i];
|
||||
}
|
||||
|
||||
/* Phase 2: decode remaining body words from MF.
|
||||
* Short: body[1..n] at w1+1 .. w2.
|
||||
* Long: MF has n_field words at w1 .. w1+n_field-1.
|
||||
* (n_field = total_words - 1; body[0] is at Vy, not in MF) */
|
||||
int start, end;
|
||||
if (decode.long_address) {
|
||||
start = w1;
|
||||
end = w1 + n_field - 1; // empty when n_field=0
|
||||
} else {
|
||||
start = w1 + 1;
|
||||
end = w2;
|
||||
}
|
||||
for (int i = start; i <= end && i < 88; i++) {
|
||||
if (word_bad[i]) {
|
||||
/* Uncorrectable word — emit '?' for each digit slot (5 per word) */
|
||||
int lost_digits = 21 / 4; /* 5 digits per 21-bit word */
|
||||
while (lost_digits-- > 0 && idx < 255)
|
||||
message[idx++] = '?';
|
||||
count = 4;
|
||||
digit = 0;
|
||||
continue;
|
||||
}
|
||||
dw = phaseptr[i];
|
||||
for (int k = 0; k < 21; k++) {
|
||||
digit = (digit >> 1) & 0x0F;
|
||||
if (dw & 0x01) digit ^= 0x08;
|
||||
dw >>= 1;
|
||||
if (--count == 0) {
|
||||
if (idx < 255) {
|
||||
message[idx++] = flex_bcd[digit];
|
||||
}
|
||||
count = 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Trim trailing BCD space padding (0x0C = ' ').
|
||||
* The encoder pads unused nibble slots with 0x0C */
|
||||
while (idx > 0 && message[idx - 1] == ' ')
|
||||
idx--;
|
||||
message[idx] = '\0';
|
||||
|
||||
flex::FlexPacket packet;
|
||||
packet.bitrate = sync.baud;
|
||||
flex::FlexPacket packet{};
|
||||
packet.bitrate = sync.baud * (sync.levels == 4 ? 2 : 1);
|
||||
packet.capcode = decode.capcode;
|
||||
packet.function = 0;
|
||||
packet.type = 3; // NUMERIC
|
||||
/* Set correct type: 3=NUM, 4=SNUM, 7=NNUM */
|
||||
if (decode.type == flex::PageType::SPECIAL_NUMERIC)
|
||||
packet.type = 4;
|
||||
else if (is_nnum)
|
||||
packet.type = 7;
|
||||
else
|
||||
packet.type = 3;
|
||||
packet.status = 0;
|
||||
packet.cycle = fiw.cycleno;
|
||||
packet.frame = fiw.frameno;
|
||||
packet.phase = PhaseNo;
|
||||
packet.is_inverted = sync.polarity;
|
||||
packet.fiw_roaming = fiw.roaming;
|
||||
packet.addr_type = static_cast<uint8_t>(decode.addr_type);
|
||||
packet.is_priority = decode.is_priority;
|
||||
if (is_nnum) {
|
||||
packet.seq = nnum_n;
|
||||
packet.is_new = nnum_r;
|
||||
packet.nnum_s = nnum_s;
|
||||
packet.has_flags = 1;
|
||||
}
|
||||
memcpy(packet.message, message, idx + 1);
|
||||
|
||||
send_packet(packet);
|
||||
}
|
||||
|
||||
void FlexProcessor::parse_tone_only(uint32_t*, char, int) {
|
||||
flex::FlexPacket packet;
|
||||
packet.bitrate = sync.baud;
|
||||
void FlexProcessor::parse_tone_only(uint32_t*, char PhaseNo, int) {
|
||||
if (decode.capcode == 1) return; // idle artifact
|
||||
flex::FlexPacket packet{};
|
||||
packet.bitrate = sync.baud * (sync.levels == 4 ? 2 : 1);
|
||||
packet.capcode = decode.capcode;
|
||||
packet.function = 0;
|
||||
packet.type = 2; // TONE
|
||||
packet.status = 0;
|
||||
snprintf(packet.message, sizeof(packet.message), "Tone Only");
|
||||
packet.cycle = fiw.cycleno;
|
||||
packet.frame = fiw.frameno;
|
||||
packet.phase = PhaseNo;
|
||||
packet.is_inverted = sync.polarity;
|
||||
packet.fiw_roaming = fiw.roaming;
|
||||
packet.addr_type = static_cast<uint8_t>(decode.addr_type);
|
||||
packet.is_priority = decode.is_priority;
|
||||
strcpy(packet.message, "");
|
||||
|
||||
send_packet(packet);
|
||||
}
|
||||
|
||||
@@ -66,6 +66,11 @@ struct FlexStateInfo {
|
||||
unsigned int fiwcount = 0;
|
||||
State Current = State::SYNC1;
|
||||
State Previous = State::SYNC1;
|
||||
|
||||
// S2 C-pattern detection
|
||||
uint16_t sync2_shiftreg = 0; // 16-bit shift register for C match
|
||||
int sync2_c_pos = -1; // symbol position where C was found (-1=not found)
|
||||
int sync2_cinv_pos = -1; // symbol position where inv.C was found
|
||||
};
|
||||
|
||||
struct FlexSync {
|
||||
@@ -81,7 +86,9 @@ struct FlexFIW {
|
||||
unsigned int checksum = 0;
|
||||
unsigned int cycleno = 0;
|
||||
unsigned int frameno = 0;
|
||||
unsigned int fix3 = 0;
|
||||
unsigned int roaming = 0; // bit 15: n (1=roaming provided)
|
||||
unsigned int repeat = 0; // bit 16: r (1=multiple transmission)
|
||||
unsigned int traffic = 0; // bits 17-20: t3-t0
|
||||
};
|
||||
|
||||
struct FlexPhase {
|
||||
@@ -98,10 +105,23 @@ struct FlexData {
|
||||
FlexPhase PhaseD;
|
||||
};
|
||||
|
||||
enum class AddrType : uint8_t {
|
||||
SHORT, // normal individual
|
||||
LONG, // 9-10 digit
|
||||
TEMPORARY, // 0x1F7800-0F (16 group slots)
|
||||
OPERATOR, // 0x1F7810-1F (system messages)
|
||||
NETWORK, // 0x1F6800-77FF (NID)
|
||||
INFO_SVC, // 0x1F2800-67FF (under study)
|
||||
RESERVED, // reserved ranges
|
||||
UNKNOWN
|
||||
};
|
||||
|
||||
struct FlexDecode {
|
||||
PageType type = PageType::ALPHANUMERIC;
|
||||
int long_address = 0;
|
||||
int64_t capcode = 0;
|
||||
AddrType addr_type = AddrType::SHORT;
|
||||
int is_priority = 0;
|
||||
};
|
||||
|
||||
} // namespace flex
|
||||
@@ -161,8 +181,8 @@ class FlexProcessor : public BasebandProcessor {
|
||||
|
||||
// Parsing
|
||||
void parse_capcode(uint32_t aw1);
|
||||
void parse_alphanumeric(uint32_t* phaseptr, char PhaseNo, int mw1, int mw2, int flex_groupmessage);
|
||||
void parse_numeric(uint32_t* phaseptr, char PhaseNo, int j);
|
||||
void parse_alphanumeric(uint32_t* phaseptr, const uint8_t* word_bad, char PhaseNo, int mw1, int mw2, int flex_groupmessage);
|
||||
void parse_numeric(uint32_t* phaseptr, const uint8_t* word_bad, char PhaseNo, int j);
|
||||
void parse_tone_only(uint32_t* phaseptr, char PhaseNo, int j);
|
||||
void parse_unknown(uint32_t* phaseptr, char PhaseNo, int mw1, int mw2);
|
||||
|
||||
|
||||
@@ -38,14 +38,7 @@ using namespace std;
|
||||
namespace {
|
||||
/* Count of bits that differ between the two values. */
|
||||
uint8_t diff_bit_count(uint32_t left, uint32_t right) {
|
||||
uint32_t diff = left ^ right;
|
||||
uint8_t count = 0;
|
||||
for (size_t i = 0; i < sizeof(diff) * 8; ++i) {
|
||||
if (((diff >> i) & 0x1) == 1)
|
||||
++count;
|
||||
}
|
||||
|
||||
return count;
|
||||
return __builtin_popcount(left ^ right);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -426,6 +419,7 @@ void POCSAGProcessor::send_packet() {
|
||||
packet.set_flag(pocsag::PacketFlag::NORMAL);
|
||||
packet.set_timestamp(Timestamp::now());
|
||||
packet.set_bitrate(bit_extractor.baud_rate());
|
||||
packet.set_inverted(word_extractor.inverted());
|
||||
packet.set(word_extractor.batch());
|
||||
|
||||
POCSAGPacketMessage message(packet);
|
||||
|
||||
@@ -153,6 +153,9 @@ class CodewordExtractor {
|
||||
/* Returns true if the batch has as sync frame. */
|
||||
bool has_sync() const { return has_sync_; }
|
||||
|
||||
/* Returns true if the signal was received with inverted polarity. */
|
||||
bool inverted() const { return inverted_; }
|
||||
|
||||
private:
|
||||
/* Sync frame codeword. */
|
||||
static constexpr uint32_t sync_codeword = 0x7cd215d8;
|
||||
|
||||
@@ -130,15 +130,15 @@ bool AK4951::reset() {
|
||||
return true;
|
||||
}
|
||||
|
||||
void AK4951::set_digtal_volume_control(const reg_t value) {
|
||||
bool AK4951::set_digtal_volume_control(const reg_t value) {
|
||||
map.r.l_ch_digital_volume_control.DV = value;
|
||||
update(Register::LchDigitalVolumeControl);
|
||||
return update(Register::LchDigitalVolumeControl);
|
||||
}
|
||||
|
||||
void AK4951::set_headphone_volume(const volume_t volume) {
|
||||
bool AK4951::set_headphone_volume(const volume_t volume) {
|
||||
const auto normalized = headphone_gain_range().normalize(volume);
|
||||
auto n = normalized.centibel() / 5;
|
||||
set_digtal_volume_control(0xcb - n);
|
||||
return set_digtal_volume_control(0xcb - n);
|
||||
}
|
||||
|
||||
void AK4951::headphone_mute() {
|
||||
@@ -602,13 +602,13 @@ reg_t AK4951::read(const address_t reg_address) {
|
||||
return rx[0];
|
||||
}
|
||||
|
||||
void AK4951::update(const Register reg) {
|
||||
write(toUType(reg), map.w[toUType(reg)]);
|
||||
bool AK4951::update(const Register reg) {
|
||||
return write(toUType(reg), map.w[toUType(reg)]);
|
||||
}
|
||||
|
||||
void AK4951::write(const address_t reg_address, const reg_t value) {
|
||||
bool AK4951::write(const address_t reg_address, const reg_t value) {
|
||||
const std::array<uint8_t, 2> tx{reg_address, value};
|
||||
bus.transmit(bus_address, tx.data(), tx.size());
|
||||
return bus.transmit(bus_address, tx.data(), tx.size());
|
||||
}
|
||||
|
||||
} /* namespace ak4951 */
|
||||
|
||||
@@ -841,7 +841,7 @@ class AK4951 : public audio::Codec {
|
||||
return true;
|
||||
}
|
||||
|
||||
void set_headphone_volume(const volume_t volume) override;
|
||||
bool set_headphone_volume(const volume_t volume) override;
|
||||
void headphone_mute();
|
||||
|
||||
void microphone_enable(int8_t alc_mode, bool mic_to_HP_enabled); // added user GUI parameter , to set up AK4951 ALC mode, and mic_to_HP_enabled to control "Hear to Mic"
|
||||
@@ -879,15 +879,15 @@ class AK4951 : public audio::Codec {
|
||||
void configure_digital_interface_i2s();
|
||||
void configure_digital_interface_external_slave();
|
||||
void configure_digital_interface_external_master();
|
||||
void set_digtal_volume_control(const reg_t value);
|
||||
bool set_digtal_volume_control(const reg_t value);
|
||||
void set_dac_power(const bool enable);
|
||||
void set_headphone_power(const bool enable);
|
||||
void set_speaker_power(const bool enable);
|
||||
void select_line_out(const LineOutSelect value);
|
||||
|
||||
reg_t read(const address_t reg_address);
|
||||
void update(const Register reg);
|
||||
void write(const address_t reg_address, const reg_t value);
|
||||
bool update(const Register reg);
|
||||
bool write(const address_t reg_address, const reg_t value);
|
||||
};
|
||||
|
||||
} /* namespace ak4951 */
|
||||
|
||||
@@ -21,12 +21,38 @@ struct FlexStats {
|
||||
};
|
||||
|
||||
struct FlexPacket {
|
||||
uint32_t bitrate; // 1600, 3200, 6400
|
||||
uint32_t capcode;
|
||||
uint32_t function; // 0-3
|
||||
uint32_t type; // Message type (e.g. ALN, NUM, etc - could use enum)
|
||||
char message[128]; // Decoded message text
|
||||
uint32_t status; // 0=OK, other=Errors
|
||||
uint32_t bitrate; // 1600, 3200, 6400
|
||||
uint64_t capcode; // supports long addresses (up to 4,297,068,542)
|
||||
uint32_t function; // 0-3 (or BIW word index for type=9)
|
||||
uint32_t type; // 0=SEC 1=INS 2=TON 3=NUM 4=SNUM 5=ALN 6=HEX 7=NNUM 8=SHORT 9=BIW
|
||||
char message[256]; // Decoded message text (not used for BIW)
|
||||
uint32_t status; // 0=OK, other=Errors
|
||||
uint8_t cycle; // FIW cycle (0-14)
|
||||
uint8_t frame; // FIW frame (0-127)
|
||||
char phase; // 'A','B','C','D'
|
||||
uint8_t is_inverted; // 1=inverted polarity
|
||||
uint8_t addr_type; // 0=short 1=long 2=temp 3=oper 4=net 5=info 6=rsvd 7=unk
|
||||
|
||||
// Fragment flags (ALN/SEC/HEX)
|
||||
uint8_t frag; // F field: 3=first, 0/1/2=continuation
|
||||
uint8_t more_frag; // C bit
|
||||
uint8_t seq; // N field (0-63)
|
||||
uint8_t is_new; // R bit
|
||||
uint8_t maildrop; // M bit
|
||||
uint8_t sig; // 7-bit signature
|
||||
uint8_t has_flags; // 1=fragment flags valid
|
||||
uint8_t sec_enc; // secure encoding (0-3)
|
||||
uint8_t nnum_s; // NNUM S flag
|
||||
uint8_t fiw_roaming; // FIW n bit: 1=roaming supported
|
||||
uint8_t is_priority; // 1=priority address (in BIW1 P section)
|
||||
|
||||
// BIW raw values (type=9 only). biw_field identifies the content.
|
||||
// 0=SSID1 1=DATE 2=TIME 5=SYSINFO 7=SSID2
|
||||
uint8_t biw_field; // BIW type field (0-7)
|
||||
uint16_t biw_v1; // field-dependent value 1
|
||||
uint16_t biw_v2; // field-dependent value 2
|
||||
uint16_t biw_v3; // field-dependent value 3
|
||||
uint16_t biw_v4; // field-dependent value 4
|
||||
};
|
||||
|
||||
} /* namespace flex */
|
||||
|
||||
@@ -160,6 +160,7 @@ class Message {
|
||||
P25TxConfigure = 102,
|
||||
ToneDetectData = 103,
|
||||
ToneDetectConfig = 104,
|
||||
FlexTosend = 105,
|
||||
MAX
|
||||
};
|
||||
|
||||
@@ -1569,8 +1570,8 @@ class PocsagTosendMessage : public Message {
|
||||
constexpr PocsagTosendMessage(
|
||||
uint16_t baud = 1200,
|
||||
uint8_t type = 2,
|
||||
char function = 'D',
|
||||
char phase = 'N',
|
||||
char function = 'A',
|
||||
char polarity = 'S', /* 'S' = Standard (CCIR Rec. 584), 'I' = Inverted */
|
||||
uint8_t msglen = 0,
|
||||
uint8_t msg[31] = {0},
|
||||
uint64_t addr = 0)
|
||||
@@ -1578,15 +1579,15 @@ class PocsagTosendMessage : public Message {
|
||||
baud{baud},
|
||||
type{type},
|
||||
function{function},
|
||||
phase{phase},
|
||||
polarity{polarity},
|
||||
msglen{msglen},
|
||||
addr{addr} {
|
||||
memcpy(this->msg, msg, 31);
|
||||
}
|
||||
uint16_t baud = 1200;
|
||||
uint8_t type = 2;
|
||||
char function = 'D';
|
||||
char phase = 'N';
|
||||
char function = 'A';
|
||||
char polarity = 'S'; /* 'S' = Standard, 'I' = Inverted */
|
||||
uint8_t msglen = 0;
|
||||
uint8_t msg[31] = {0};
|
||||
uint64_t addr = 0;
|
||||
@@ -1884,4 +1885,24 @@ class ToneDetectConfigureMessage : public Message {
|
||||
uint32_t ctcss_freq_x10{0}; // CTCSS frequency × 10 (e.g. 1000 = 100.0 Hz); 0 = None
|
||||
};
|
||||
|
||||
class FlexTosendMessage : public Message {
|
||||
public:
|
||||
constexpr FlexTosendMessage(
|
||||
uint64_t capcode = 0,
|
||||
uint8_t type = 0,
|
||||
uint8_t msglen = 0,
|
||||
const uint8_t* msg = nullptr)
|
||||
: Message{ID::FlexTosend},
|
||||
capcode{capcode},
|
||||
type{type},
|
||||
msglen{msglen} {
|
||||
if (msg)
|
||||
memcpy(this->msg, msg, 240);
|
||||
}
|
||||
uint64_t capcode = 0;
|
||||
uint8_t type = 0;
|
||||
uint8_t msglen = 0;
|
||||
uint8_t msg[240] = {0};
|
||||
};
|
||||
|
||||
#endif /*__MESSAGE_H__*/
|
||||
|
||||
+306
-22
@@ -39,8 +39,6 @@ std::string bitrate_str(BitRate bitrate) {
|
||||
return "1200bps";
|
||||
case BitRate::FSK2400:
|
||||
return "2400bps";
|
||||
case BitRate::FSK3200:
|
||||
return "3200bps";
|
||||
default:
|
||||
return "????";
|
||||
}
|
||||
@@ -352,30 +350,228 @@ int EccContainer::error_correct(uint32_t& val) {
|
||||
return errl;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Numeric character table: 4-bit BCD -> ASCII
|
||||
// ----------------------------------------------------------------------------
|
||||
static const char numeric_chars[16] = {
|
||||
'0', '1', '2', '3', '4', '5', '6', '7',
|
||||
'8', '9', 'R', 'U', ' ', '-', ']', '['};
|
||||
|
||||
// Extract and bit-reverse a 4-bit nibble from a message codeword.
|
||||
// POCSAG numeric digits are transmitted LSB first, so bit 30 (first transmitted)
|
||||
// is the LSB of the digit value, not the MSB.
|
||||
static uint8_t decode_nibble(uint32_t codeword, int nibble_idx) {
|
||||
int bit_pos = 30 - nibble_idx * 4;
|
||||
// bit_pos is the first transmitted bit (LSB of digit)
|
||||
// bit_pos-3 is the last transmitted bit (MSB of digit)
|
||||
uint8_t n = 0;
|
||||
n |= ((codeword >> (bit_pos - 3)) & 1) << 3; // MSB of digit
|
||||
n |= ((codeword >> (bit_pos - 2)) & 1) << 2;
|
||||
n |= ((codeword >> (bit_pos - 1)) & 1) << 1;
|
||||
n |= ((codeword >> (bit_pos - 0)) & 1) << 0; // LSB of digit
|
||||
return n;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Heuristic message type detection (first batch only)
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// Count trailing fill characters in alpha buffer (NULL or space).
|
||||
static int count_alpha_fill(const std::string& data) {
|
||||
if (data.empty()) return 0;
|
||||
int fill = 0;
|
||||
for (int i = data.size() - 1; i >= 0; --i) {
|
||||
char c = data[i];
|
||||
if (c == '\0' || c == ' ')
|
||||
fill++;
|
||||
else
|
||||
break;
|
||||
}
|
||||
return fill;
|
||||
}
|
||||
|
||||
// Count trailing fill nibbles (0xC = space) in numeric buffer.
|
||||
static int count_numeric_fill(const uint8_t* nibbles, int count) {
|
||||
int fill = 0;
|
||||
for (int i = count - 1; i >= 0; --i) {
|
||||
if (nibbles[i] == 0x0C)
|
||||
fill++;
|
||||
else
|
||||
break;
|
||||
}
|
||||
return fill;
|
||||
}
|
||||
|
||||
// Score alpha interpretation: +3 alphanumeric/space, -2 other printable, -5 control.
|
||||
static int score_alpha(const std::string& data, int fill) {
|
||||
int score = 0;
|
||||
int content = 0;
|
||||
int len = data.size();
|
||||
|
||||
for (int i = 0; i < len; ++i) {
|
||||
unsigned char c = data[i];
|
||||
|
||||
// Skip trailing fill
|
||||
if (i >= len - fill && (c == 0 || c == ' '))
|
||||
continue;
|
||||
if (c == 0)
|
||||
continue;
|
||||
|
||||
content++;
|
||||
if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
|
||||
(c >= '0' && c <= '9') || c == ' ')
|
||||
score += 3;
|
||||
else if (c >= 0x20 && c <= 0x7E)
|
||||
score -= 2;
|
||||
else if (c == '\n' || c == '\r' || c == '\t' || c == 0x04)
|
||||
score += 0;
|
||||
else
|
||||
score -= 5;
|
||||
}
|
||||
|
||||
if (content > 0)
|
||||
score += fill * fill * 3 + fill * 5;
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
// Score numeric interpretation on raw nibbles.
|
||||
static int score_numeric(const uint8_t* nibbles, int count, int fill) {
|
||||
int raw_score = 0;
|
||||
int scored = 0;
|
||||
int digits = 0;
|
||||
int u_count = 0;
|
||||
|
||||
// Pre-scan for U nibbles
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (nibbles[i] == 0x0C && i >= count - fill)
|
||||
continue;
|
||||
if (nibbles[i] == 0x0B)
|
||||
u_count++;
|
||||
}
|
||||
|
||||
bool urgent_prefix = (u_count == 1);
|
||||
|
||||
for (int i = 0; i < count; ++i) {
|
||||
uint8_t n = nibbles[i];
|
||||
if (n == 0x0C && i >= count - fill)
|
||||
continue;
|
||||
|
||||
scored++;
|
||||
if (n <= 0x09) {
|
||||
raw_score += 3;
|
||||
digits++;
|
||||
} else if (n == 0x0B) {
|
||||
raw_score += urgent_prefix ? -1 : -15;
|
||||
} else if (n == 0x0A) {
|
||||
raw_score -= 5;
|
||||
} else {
|
||||
raw_score -= 2;
|
||||
}
|
||||
}
|
||||
|
||||
int score = scored > 0 ? raw_score * 4 / 7 : 0;
|
||||
|
||||
// Phone numbers have at most ~15 digits
|
||||
if (digits > 15)
|
||||
score -= (digits - 15) * 5;
|
||||
|
||||
// Fill bonus (weaker than alpha: 1/16 vs 1/128 coincidence rate)
|
||||
score += fill * fill;
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
DetectedType detect_message_type(const std::string& alpha,
|
||||
const uint8_t* nibbles,
|
||||
uint8_t nibble_count,
|
||||
uint8_t msg_codewords) {
|
||||
if (alpha.empty() && nibble_count == 0)
|
||||
return DET_TONE;
|
||||
|
||||
// Long messages can't be numeric (phone numbers are short)
|
||||
if (msg_codewords >= 8)
|
||||
return DET_ALPHA;
|
||||
|
||||
int alpha_fill = count_alpha_fill(alpha);
|
||||
int numeric_fill = count_numeric_fill(nibbles, nibble_count);
|
||||
|
||||
int sa = score_alpha(alpha, alpha_fill) + 2; // Alpha prior bias
|
||||
int sn = score_numeric(nibbles, nibble_count, numeric_fill);
|
||||
|
||||
// Short message boost for alpha (1-2 data codewords)
|
||||
if (msg_codewords <= 3)
|
||||
sa += 3;
|
||||
|
||||
return (sn > sa) ? DET_NUMERIC : DET_ALPHA;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Batch decoder
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
bool pocsag_decode_batch(const POCSAGPacket& batch, POCSAGState& state) {
|
||||
constexpr uint8_t codeword_max = 16;
|
||||
state.output.clear();
|
||||
|
||||
/* Only reset numeric accumulator when starting a new message,
|
||||
* not on continuation batches — numeric_buf must be cumulative
|
||||
* across multi-batch messages. */
|
||||
const bool continuing_numeric =
|
||||
(state.mode != STATE_HAVE_ADDRESS) &&
|
||||
(state.out_type == MESSAGE) &&
|
||||
state.type_decided &&
|
||||
(state.detected == DET_NUMERIC);
|
||||
if (!continuing_numeric)
|
||||
state.numeric_len = 0;
|
||||
|
||||
/* Preserve new_message across batch boundary when STATE_HAVE_ADDRESS
|
||||
* persists — the address was at the end of the previous batch and
|
||||
* we haven't displayed it yet. Otherwise reset for this batch. */
|
||||
if (state.mode != STATE_HAVE_ADDRESS)
|
||||
state.new_message = false;
|
||||
|
||||
// Temporary nibble buffer for first-batch numeric decode.
|
||||
uint8_t nibbles[max_batch_nibbles];
|
||||
uint8_t nibble_count = 0;
|
||||
uint8_t msg_codewords = 0;
|
||||
// Also build raw alpha for heuristic (before non-printable replacement).
|
||||
std::string raw_alpha{};
|
||||
// Track whether any characters came from uncorrectable codewords.
|
||||
// If so, heuristic scoring is unreliable — skip it and default to alpha.
|
||||
bool has_bad_chars = false;
|
||||
|
||||
while (state.codeword_index < codeword_max) {
|
||||
auto codeword = batch[state.codeword_index];
|
||||
bool is_address = (codeword & 0x80000000U) == 0;
|
||||
|
||||
// Error correct twice. First time to fix any errors it can,
|
||||
// second time to count number of errors that couldn't be fixed.
|
||||
state.ecc->error_correct(codeword);
|
||||
// Single ECC call: fix errors and get error count.
|
||||
auto error_count = state.ecc->error_correct(codeword);
|
||||
|
||||
switch (state.mode) {
|
||||
case STATE_CLEAR:
|
||||
if (is_address && codeword != POCSAG_IDLEWORD) {
|
||||
state.function = (codeword >> 11) & 3;
|
||||
state.address = (codeword >> 10) & 0x1FFFF8U; // 18 MSBs are transmitted
|
||||
state.address = (codeword >> 10) & 0x1FFFF8U;
|
||||
/* Frame number = lower 3 bits of RIC, derived from the
|
||||
* address codeword's position in the batch (not the
|
||||
* message codeword's position). codeword_index 0-15
|
||||
* maps to frames 0-7 via index >> 1. */
|
||||
state.address |= (state.codeword_index >> 1);
|
||||
state.mode = STATE_HAVE_ADDRESS;
|
||||
state.out_type = ADDRESS;
|
||||
state.errors = error_count;
|
||||
state.new_message = true;
|
||||
state.type_decided = false;
|
||||
state.detected = DET_UNKNOWN;
|
||||
|
||||
state.ascii_idx = 0;
|
||||
state.ascii_data = 0;
|
||||
state.prev_cw_err = 0;
|
||||
state.cur_cw_err = 0;
|
||||
nibble_count = 0;
|
||||
msg_codewords = 0;
|
||||
raw_alpha.clear();
|
||||
} else if (codeword == POCSAG_IDLEWORD) {
|
||||
state.out_type = IDLE;
|
||||
}
|
||||
@@ -383,54 +579,142 @@ bool pocsag_decode_batch(const POCSAGPacket& batch, POCSAGState& state) {
|
||||
|
||||
case STATE_HAVE_ADDRESS:
|
||||
if (is_address) {
|
||||
// Got another address, return the current state.
|
||||
// Got another address. Run heuristic before returning if we have pending data.
|
||||
if (!state.type_decided && msg_codewords > 0) {
|
||||
state.detected = has_bad_chars
|
||||
? DET_ALPHA
|
||||
: detect_message_type(raw_alpha, nibbles, nibble_count, msg_codewords);
|
||||
state.type_decided = true;
|
||||
state.msg_codewords = msg_codewords;
|
||||
if (state.detected == DET_NUMERIC) {
|
||||
state.numeric_len = 0;
|
||||
for (uint8_t ni = 0; ni < nibble_count && state.numeric_len < sizeof(state.numeric_buf); ++ni)
|
||||
state.numeric_buf[state.numeric_len++] = numeric_chars[nibbles[ni] & 0x0F];
|
||||
}
|
||||
}
|
||||
state.mode = STATE_CLEAR;
|
||||
return true;
|
||||
}
|
||||
|
||||
// First message codeword, complete the address.
|
||||
state.address |= (state.codeword_index >> 1); // Add in the 3 LSBs (frame #).
|
||||
/* Frame number already applied in STATE_CLEAR.
|
||||
* Transition to message decoding. */
|
||||
state.mode = STATE_GETTING_MSG;
|
||||
[[fallthrough]];
|
||||
|
||||
case STATE_GETTING_MSG:
|
||||
if (is_address) {
|
||||
// Codeword isn't a message, return the current state.
|
||||
// Message ended. Run heuristic before returning.
|
||||
if (!state.type_decided && msg_codewords > 0) {
|
||||
state.detected = has_bad_chars
|
||||
? DET_ALPHA
|
||||
: detect_message_type(raw_alpha, nibbles, nibble_count, msg_codewords);
|
||||
state.type_decided = true;
|
||||
state.msg_codewords = msg_codewords;
|
||||
if (state.detected == DET_NUMERIC) {
|
||||
state.numeric_len = 0;
|
||||
for (uint8_t ni = 0; ni < nibble_count && state.numeric_len < sizeof(state.numeric_buf); ++ni)
|
||||
state.numeric_buf[state.numeric_len++] = numeric_chars[nibbles[ni] & 0x0F];
|
||||
}
|
||||
}
|
||||
state.mode = STATE_CLEAR;
|
||||
return true;
|
||||
}
|
||||
|
||||
state.out_type = MESSAGE;
|
||||
state.errors += error_count;
|
||||
state.ascii_data |= (codeword >> 11) & 0xFFFFF; // Get 20 message bits.
|
||||
msg_codewords++;
|
||||
|
||||
// Track per-codeword error level for character coloring.
|
||||
// 0=clean, 1-2=corrected, 3=uncorrectable.
|
||||
state.prev_cw_err = state.cur_cw_err;
|
||||
state.cur_cw_err = (error_count >= 3) ? 3 : error_count;
|
||||
|
||||
// --- Alpha decode (always) ---
|
||||
// Bits remaining from previous codeword inherit prev_cw_err.
|
||||
// New 20 bits from this codeword use cur_cw_err.
|
||||
// Characters spanning boundary get the worst of both.
|
||||
uint32_t bits_from_prev = state.ascii_idx; // leftover bits before adding new ones
|
||||
|
||||
state.ascii_data |= ((uint64_t)((codeword >> 11) & 0xFFFFF)) << (44 - state.ascii_idx);
|
||||
state.ascii_idx += 20;
|
||||
|
||||
// Raw 20 bits to 7 bit reversed ASCII.
|
||||
// NB: This is processed MSB first, any remaining bits are shifted
|
||||
// up so a whole 7 bits are processed with the next codeword.
|
||||
while (state.ascii_idx >= 7) {
|
||||
// Per-character error level from codeword error tracking.
|
||||
// Characters spanning a codeword boundary get the worst level.
|
||||
uint8_t char_err;
|
||||
if (bits_from_prev >= 7) {
|
||||
// Entire character from previous codeword's leftover bits.
|
||||
char_err = state.prev_cw_err;
|
||||
bits_from_prev -= 7;
|
||||
} else if (bits_from_prev > 0) {
|
||||
// Character spans boundary.
|
||||
char_err = std::max(state.prev_cw_err, state.cur_cw_err);
|
||||
bits_from_prev = 0;
|
||||
} else {
|
||||
// Entirely from current codeword.
|
||||
char_err = state.cur_cw_err;
|
||||
}
|
||||
|
||||
// Extract top 7 bits from accumulator
|
||||
char ascii_char = (state.ascii_data >> 57) & 0x7F;
|
||||
state.ascii_data <<= 7;
|
||||
state.ascii_idx -= 7;
|
||||
char ascii_char = (state.ascii_data >> state.ascii_idx) & 0x7F;
|
||||
|
||||
// Reverse the bits. (TODO: __RBIT?)
|
||||
ascii_char = (ascii_char & 0xF0) >> 4 | (ascii_char & 0x0F) << 4; // 01234567 -> 45670123
|
||||
ascii_char = (ascii_char & 0xCC) >> 2 | (ascii_char & 0x33) << 2; // 45670123 -> 67452301
|
||||
ascii_char = (ascii_char & 0xAA) >> 2 | (ascii_char & 0x55); // 67452301 -> 76543210
|
||||
// Reverse bits (LSB-first encoding)
|
||||
ascii_char = (ascii_char & 0xF0) >> 4 | (ascii_char & 0x0F) << 4;
|
||||
ascii_char = (ascii_char & 0xCC) >> 2 | (ascii_char & 0x33) << 2;
|
||||
ascii_char = (ascii_char & 0xAA) >> 2 | (ascii_char & 0x55);
|
||||
|
||||
// Translate non-printable chars. TODO: Leave CRLF?
|
||||
if (ascii_char < 32 || ascii_char > 126)
|
||||
// Store raw char for heuristic
|
||||
if (!state.type_decided)
|
||||
raw_alpha += ascii_char;
|
||||
|
||||
// Substitute '?' for characters from uncorrectable codewords
|
||||
if (char_err >= 3) {
|
||||
state.output += "?";
|
||||
has_bad_chars = true;
|
||||
} else if (ascii_char < 32 || ascii_char > 126)
|
||||
state.output += ".";
|
||||
else
|
||||
state.output += ascii_char;
|
||||
}
|
||||
|
||||
state.ascii_data <<= 20; // Remaining bits are for next iteration...
|
||||
// --- Numeric decode ---
|
||||
// First batch: accumulate nibbles locally for heuristic scoring.
|
||||
// Continuation batches: if already decided numeric, decode directly
|
||||
// into numeric_buf for the app layer.
|
||||
if (!state.type_decided && nibble_count + 5 <= max_batch_nibbles) {
|
||||
for (int n = 0; n < 5; ++n) {
|
||||
nibbles[nibble_count++] = decode_nibble(codeword, n);
|
||||
}
|
||||
} else if (state.type_decided && state.detected == DET_NUMERIC &&
|
||||
state.numeric_len + 5 <= (uint8_t)sizeof(state.numeric_buf)) {
|
||||
for (int n = 0; n < 5; ++n) {
|
||||
uint8_t nib = decode_nibble(codeword, n);
|
||||
state.numeric_buf[state.numeric_len++] = numeric_chars[nib & 0x0F];
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
state.codeword_index++;
|
||||
}
|
||||
|
||||
// End of batch. If we have message data and type not yet decided, run heuristic.
|
||||
if (state.out_type == MESSAGE && !state.type_decided && msg_codewords > 0) {
|
||||
state.detected = has_bad_chars
|
||||
? DET_ALPHA
|
||||
: detect_message_type(raw_alpha, nibbles, nibble_count, msg_codewords);
|
||||
state.type_decided = true;
|
||||
state.msg_codewords = msg_codewords;
|
||||
if (state.detected == DET_NUMERIC) {
|
||||
state.numeric_len = 0;
|
||||
for (uint8_t ni = 0; ni < nibble_count && state.numeric_len < sizeof(state.numeric_buf); ++ni)
|
||||
state.numeric_buf[state.numeric_len++] = numeric_chars[nibbles[ni] & 0x0F];
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -77,6 +77,17 @@ class EccContainer {
|
||||
uint32_t bch[1025];
|
||||
};
|
||||
|
||||
/* Detected message type from heuristic scoring. */
|
||||
enum DetectedType : uint8_t {
|
||||
DET_UNKNOWN,
|
||||
DET_TONE,
|
||||
DET_ALPHA,
|
||||
DET_NUMERIC
|
||||
};
|
||||
|
||||
/* Max numeric nibbles per batch: 16 codewords * 5 nibbles. */
|
||||
constexpr uint8_t max_batch_nibbles = 80;
|
||||
|
||||
struct POCSAGState {
|
||||
EccContainer* ecc = nullptr;
|
||||
uint8_t codeword_index = 0;
|
||||
@@ -84,17 +95,28 @@ struct POCSAGState {
|
||||
uint32_t address = 0;
|
||||
Mode mode = STATE_CLEAR;
|
||||
OutputType out_type = EMPTY;
|
||||
uint32_t ascii_data = 0;
|
||||
uint64_t ascii_data = 0; // Was uint32_t — fixed overflow for long messages.
|
||||
uint32_t ascii_idx = 0;
|
||||
uint32_t errors = 0;
|
||||
std::string output{};
|
||||
uint8_t prev_cw_err = 0; // Error level of previous codeword (for chars spanning boundary).
|
||||
uint8_t cur_cw_err = 0; // Error level of current codeword being decoded.
|
||||
bool new_message = false; // True when decoder starts a new address.
|
||||
bool type_decided = false; // True after first-batch heuristic runs.
|
||||
DetectedType detected = DET_UNKNOWN;
|
||||
uint8_t msg_codewords = 0; // Message codewords in current batch (for heuristic).
|
||||
std::string output{}; // Alpha decode (always populated, with color escapes).
|
||||
// Numeric output is built into a separate local in pocsag_decode_batch
|
||||
// and stored here only when heuristic detects numeric.
|
||||
// Using a fixed buffer to avoid std::string overhead in external apps
|
||||
// that embed POCSAGState (e.g. battleship).
|
||||
char numeric_buf[80]{};
|
||||
uint8_t numeric_len = 0;
|
||||
};
|
||||
|
||||
const pocsag::BitRate pocsag_bitrates[4] = {
|
||||
const pocsag::BitRate pocsag_bitrates[3] = {
|
||||
pocsag::BitRate::FSK512,
|
||||
pocsag::BitRate::FSK1200,
|
||||
pocsag::BitRate::FSK2400,
|
||||
pocsag::BitRate::FSK3200,
|
||||
};
|
||||
|
||||
std::string bitrate_str(BitRate bitrate);
|
||||
@@ -107,6 +129,10 @@ void pocsag_encode(const MessageType type, BCHCode& BCH_code, const uint32_t fun
|
||||
// Returns true if the batch has more to process.
|
||||
bool pocsag_decode_batch(const POCSAGPacket& batch, POCSAGState& state);
|
||||
|
||||
// Heuristic: detect whether message content is numeric or alpha.
|
||||
// Called once after first batch of a new message.
|
||||
DetectedType detect_message_type(const std::string& alpha, const uint8_t* nibbles, uint8_t nibble_count, uint8_t msg_codewords);
|
||||
|
||||
} /* namespace pocsag */
|
||||
|
||||
#endif /*__POCSAG_H__*/
|
||||
|
||||
@@ -35,8 +35,7 @@ enum BitRate : uint32_t {
|
||||
UNKNOWN,
|
||||
FSK512 = 512,
|
||||
FSK1200 = 1200,
|
||||
FSK2400 = 2400,
|
||||
FSK3200 = 3200
|
||||
FSK2400 = 2400
|
||||
};
|
||||
|
||||
enum PacketFlag : uint32_t {
|
||||
@@ -88,15 +87,25 @@ class POCSAGPacket {
|
||||
return flag_;
|
||||
}
|
||||
|
||||
void set_inverted(bool inverted) {
|
||||
inverted_ = inverted;
|
||||
}
|
||||
|
||||
bool inverted() const {
|
||||
return inverted_;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
codewords.fill(0);
|
||||
bitrate_ = 0u;
|
||||
flag_ = NORMAL;
|
||||
inverted_ = false;
|
||||
}
|
||||
|
||||
private:
|
||||
uint16_t bitrate_{0};
|
||||
PacketFlag flag_{NORMAL};
|
||||
bool inverted_{false};
|
||||
batch_t codewords{};
|
||||
Timestamp timestamp_{};
|
||||
};
|
||||
|
||||
@@ -145,14 +145,14 @@ void WM8731::write(const RightLineIn value) {
|
||||
write(Register::RightLineIn);
|
||||
}
|
||||
|
||||
void WM8731::write(const LeftHeadphoneOut value) {
|
||||
bool WM8731::write(const LeftHeadphoneOut value) {
|
||||
map.r.left_headphone_out = value;
|
||||
write(Register::LeftHeadphoneOut);
|
||||
return write(Register::LeftHeadphoneOut);
|
||||
}
|
||||
|
||||
void WM8731::write(const RightHeadphoneOut value) {
|
||||
bool WM8731::write(const RightHeadphoneOut value) {
|
||||
map.r.right_headphone_out = value;
|
||||
write(Register::RightHeadphoneOut);
|
||||
return write(Register::RightHeadphoneOut);
|
||||
}
|
||||
|
||||
void WM8731::write(const AnalogAudioPathControl value) {
|
||||
|
||||
@@ -306,21 +306,22 @@ class WM8731 : public audio::Codec {
|
||||
});
|
||||
}
|
||||
|
||||
void set_wm_headphone_volume(const volume_t volume) {
|
||||
bool set_wm_headphone_volume(const volume_t volume) {
|
||||
const auto normalized = headphone_gain_range().normalize(volume);
|
||||
auto n = normalized.centibel() / 10;
|
||||
|
||||
write(LeftHeadphoneOut{
|
||||
bool tag = write(LeftHeadphoneOut{
|
||||
.lhpvol = static_cast<reg_t>(n),
|
||||
.lzcen = 0,
|
||||
.lrhpboth = 1,
|
||||
.reserved0 = 0,
|
||||
});
|
||||
return tag;
|
||||
}
|
||||
|
||||
void set_headphone_volume(const volume_t volume) override {
|
||||
bool set_headphone_volume(const volume_t volume) override {
|
||||
headphone_volume = volume;
|
||||
set_wm_headphone_volume(volume);
|
||||
return set_wm_headphone_volume(volume);
|
||||
}
|
||||
|
||||
volume_range_t headphone_gain_range() const override {
|
||||
@@ -422,8 +423,8 @@ class WM8731 : public audio::Codec {
|
||||
|
||||
void write(const LeftLineIn value);
|
||||
void write(const RightLineIn value);
|
||||
void write(const LeftHeadphoneOut value);
|
||||
void write(const RightHeadphoneOut value);
|
||||
bool write(const LeftHeadphoneOut value);
|
||||
bool write(const RightHeadphoneOut value);
|
||||
void write(const AnalogAudioPathControl value);
|
||||
void write(const DigitalAudioPathControl value);
|
||||
void write(const PowerDownControl value);
|
||||
|
||||
Reference in New Issue
Block a user