Compare commits

...

14 Commits

Author SHA1 Message Date
Totoo 2d2d9223f2 better rs41 (#2757) 2025-08-15 16:24:07 +02:00
zxkmm 6e33007cb6 move soundbrd app external (#2756) 2025-08-15 13:31:18 +02:00
zxkmm 3f7b59f27e frequency keypad opt (#2755)
* init

* revert changes that caused by batch replace

* revert changes that caused by batch replace

* revert changes that caused by batch replace

* revert submodu;e
2025-08-13 14:25:21 +02:00
Arne Luehrs 375d1ad54e Implementation of EPIRB receiver (#2754)
* Implementation of EPIRB receiver
* Baseband processing of EPIRB signal
* UI to ddecode and display EPIRB message with display on a map
* External application
* External proc element
* Delete CLAUDE.md
2025-08-13 14:24:18 +02:00
Netro 6b05878532 BLE RX/TX Changes (#2752)
* Work on BLE Rx Tx improvements.
* Working on compile size.
* cleanup
* Formatting
* Fixes
* More Improvements + Custom Parsing for Tags
* Moving ERT to external apps.
* Fix Icon.
2025-08-11 07:42:58 +02:00
RocketGod 3983749f11 Update ui_jammer.cpp (#2747)
Update warning to reflect actual range limit of bandwidth.
2025-07-30 20:08:00 +02:00
Totoo 28e2f770e5 fix miss (#2738) 2025-07-12 13:33:22 +02:00
gullradriel 4c5275247e update version (#2735)
Co-authored-by: gullradriel <gullradriel@no-mail.com>
2025-07-11 15:57:04 +02:00
gullradriel a0185d28eb update submodule (#2734)
Co-authored-by: gullradriel <gullradriel@no-mail.com>
2025-07-09 21:33:38 +02:00
RocketGod 2500df310f Add radio settings, new app icon, and other UI improvements (#2732) 2025-07-03 11:03:51 -07:00
Totoo 54f9ff116b AIS map improv (#2725)
* AIS map improv

* format code mismatch with vc
2025-06-29 22:47:18 +02:00
Totoo 47f231ad63 Add blue bar to subghzd+weather (#2724) 2025-06-29 19:31:05 +08:00
Netro f90d3fabce Adding simple FSK Rx Processor. Can be used with New Apps. (#2716)
* Work to allow for unique beacon parsing functions.

* Fixing pull.

* Changes.

* Formatting.

* Fix Copyright

* Update firmware/application/apps/ble_rx_app.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update firmware/baseband/proc_btlerx.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* PR suggestions.

* Fix String.

* FSK Rx Improvements. Works for my custom protocol.

* Fix buffer size.

* Refactor

* Formatting.

* Formatting.

* Fixing compiling, and BLE Rx UI/Performance.

* More improvements.

* Fixing stuck state.

* More stuck parsing fix.

* Combining PR changes.

* Improvements from previous PR.

* Fix dbM calculation relative to device RSSI.

* Formatting.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: TJ <tj.baginski@cognosos.com>
2025-06-29 01:02:12 +02:00
RocketGod 4e276cdc71 Battleship (#2720)
* Made the Battleship 2P 2PP game - FSK is wip
* Using POCSAG
2025-06-28 20:02:29 +02:00
62 changed files with 4470 additions and 1434 deletions
+1 -1
View File
@@ -1 +1 @@
v2.0.2
v2.1.0
+1 -1
View File
@@ -1 +1 @@
v2.1.0
v2.2.0
+1
View File
@@ -62,6 +62,7 @@ cmake-build-debug/
.vscode
.idea
*.swp
.claude
# VSCodium extensions
.history
-3
View File
@@ -270,13 +270,10 @@ set(CPPSRC
ui/ui_bmpview.cpp
apps/ais_app.cpp
apps/analog_audio_app.cpp
apps/ble_comm_app.cpp
apps/ble_rx_app.cpp
apps/ble_tx_app.cpp
apps/capture_app.cpp
apps/ert_app.cpp
apps/pocsag_app.cpp
apps/soundboard_app.cpp
apps/ui_about_simple.cpp
apps/ui_adsb_rx.cpp
apps/ui_aprs_rx.cpp
+40 -2
View File
@@ -301,7 +301,7 @@ AISRecentEntryDetailView::AISRecentEntryDetailView(NavigationView& nav) {
ais::format::text(entry_.name),
0,
GeoPos::alt_unit::METERS,
GeoPos::spd_unit::NONE,
GeoPos::spd_unit::KNOTS,
ais::format::latlon_float(entry_.last_position.latitude.normalized()),
ais::format::latlon_float(entry_.last_position.longitude.normalized()),
entry_.last_position.true_heading,
@@ -324,7 +324,31 @@ AISRecentEntryDetailView& AISRecentEntryDetailView::operator=(const AISRecentEnt
void AISRecentEntryDetailView::update_position() {
if (send_updates)
geomap_view->update_position(ais::format::latlon_float(entry_.last_position.latitude.normalized()), ais::format::latlon_float(entry_.last_position.longitude.normalized()), (float)entry_.last_position.true_heading, 0, entry_.last_position.speed_over_ground);
geomap_view->update_position(ais::format::latlon_float(entry_.last_position.latitude.normalized()), ais::format::latlon_float(entry_.last_position.longitude.normalized()), (float)entry_.last_position.true_heading, 0, entry_.last_position.speed_over_ground > 1022 ? 0 : entry_.last_position.speed_over_ground);
}
bool AISRecentEntryDetailView::add_map_marker(const AISRecentEntry& entry) {
if (geomap_view && send_updates) {
GeoMarker marker{};
marker.lon = ais::format::latlon_float(entry.last_position.longitude.normalized());
marker.lat = ais::format::latlon_float(entry.last_position.latitude.normalized());
marker.angle = entry.last_position.true_heading;
marker.tag = entry.call_sign.empty() ? to_string_dec_uint(entry.mmsi) : entry.call_sign;
auto markerStored = geomap_view->store_marker(marker);
return markerStored == MARKER_STORED;
}
return false;
}
void AISRecentEntryDetailView::update_map_markers(AISRecentEntries& entries) {
if (geomap_view && send_updates) {
geomap_view->clear_markers();
for (const auto& entry : entries) {
// if (entry.last_position.latitude.is_valid() && entry.last_position.longitude.is_valid()) {
add_map_marker(entry);
// }
}
update_position(); // to update view
}
}
void AISRecentEntryDetailView::focus() {
@@ -415,14 +439,27 @@ AISAppView::AISAppView(NavigationView& nav)
audio::set_rate(audio::Rate::Hz_24000);
audio::output::start();
}
signal_token_tick_second = rtc_time::signal_tick_second += [this]() {
on_tick_second();
};
}
AISAppView::~AISAppView() {
rtc_time::signal_tick_second -= signal_token_tick_second;
audio::output::stop();
receiver_model.disable();
baseband::shutdown();
}
void AISAppView::on_tick_second() {
++timer_seconds;
if (timer_seconds % 10 == 0) {
if (recent_entry_detail_view.hidden()) return;
recent_entry_detail_view.update_map_markers(recent);
}
}
void AISAppView::focus() {
options_channel.focus();
}
@@ -461,6 +498,7 @@ void AISAppView::on_show_detail(const AISRecentEntry& entry) {
recent_entry_detail_view.hidden(false);
recent_entry_detail_view.set_entry(entry);
recent_entry_detail_view.focus();
recent_entry_detail_view.update_map_markers(recent);
}
} /* namespace ui */
+7
View File
@@ -125,10 +125,14 @@ class AISRecentEntryDetailView : public View {
void update_position();
void focus() override;
void paint(Painter&) override;
bool add_map_marker(const AISRecentEntry& entry);
void update_map_markers(AISRecentEntries& entries);
AISRecentEntryDetailView(const AISRecentEntryDetailView& Entry);
AISRecentEntryDetailView& operator=(const AISRecentEntryDetailView& Entry);
GeoMapView* get_geomap_view() { return geomap_view; }
private:
AISRecentEntry entry_{};
@@ -215,6 +219,8 @@ class AISAppView : public View {
Channel channel{
{21 * 8, 5, 6 * 8, 4},
};
SignalToken signal_token_tick_second{};
uint8_t timer_seconds = 0;
MessageHandlerRegistration message_handler_packet{
Message::ID::AISPacket,
@@ -229,6 +235,7 @@ class AISAppView : public View {
void on_packet(const ais::Packet& packet);
void on_show_list();
void on_show_detail(const AISRecentEntry& entry);
void on_tick_second();
};
} /* namespace ui */
+63 -105
View File
@@ -69,7 +69,7 @@ static std::uint64_t get_freq_by_channel_number(uint8_t channel_number) {
}
void BLECommView::focus() {
options_channel.focus();
field_frequency.focus();
}
BLECommView::BLECommView(NavigationView& nav)
@@ -79,10 +79,7 @@ BLECommView::BLECommView(NavigationView& nav)
&field_rf_amp,
&field_lna,
&field_vga,
&options_channel,
&field_frequency,
&label_send_adv,
&button_send_adv,
&check_log,
&label_packets_sent,
&text_packets_sent,
@@ -90,10 +87,6 @@ BLECommView::BLECommView(NavigationView& nav)
field_frequency.set_step(0);
button_send_adv.on_select = [this](ImageButton&) {
this->toggle();
};
check_log.set_value(logging);
check_log.on_select = [this](Checkbox&, bool v) {
@@ -104,27 +97,8 @@ BLECommView::BLECommView(NavigationView& nav)
logger->append(logs_dir.string() + "/BLELOG_" + to_string_timestamp(rtc_time::now()) + ".TXT");
};
options_channel.on_change = [this](size_t, int32_t i) {
// If we selected Auto don't do anything and Auto will handle changing.
if (i == 40) {
auto_channel = true;
return;
} else {
auto_channel = false;
}
field_frequency.set_value(get_freq_by_channel_number(i));
channel_number_rx = i;
channel_number_tx = i;
};
options_channel.set_selected_index(3, true);
logger = std::make_unique<BLECommLogger>();
// Generate new random Mac Address upon each new startup.
generateRandomMacAddress(randomMac);
// Setup Initial Advertise Packet.
advertisePacket = build_adv_packet();
}
@@ -145,86 +119,66 @@ bool BLECommView::in_tx_mode() const {
return (bool)is_running_tx;
}
void BLECommView::toggle() {
if (in_tx_mode()) {
sendAdvertisement(false);
} else {
sendAdvertisement(true);
}
}
BLETxPacket BLECommView::build_adv_packet() {
BLETxPacket bleTxPacket;
memset(&bleTxPacket, 0, sizeof(BLETxPacket));
std::string dataString = "11094861636b524620506f7274617061636b";
strncpy(bleTxPacket.macAddress, randomMac, 12);
std::string dataString = "02010603030F1807084861636B5246";
strncpy(bleTxPacket.advertisementData, dataString.c_str(), dataString.length());
// Duty cycle of 40% per 100ms advertisment periods.
strncpy(bleTxPacket.packetCount, "80", 3);
bleTxPacket.packet_count = 80;
strncpy(bleTxPacket.packetCount, "1", 2);
bleTxPacket.packet_count = 1;
bleTxPacket.packetType = PKT_TYPE_DISCOVERY;
bleTxPacket.pduType = PKT_TYPE_ADV_IND;
return bleTxPacket;
}
void BLECommView::sendAdvertisement(bool enable) {
if (enable) {
startTx(advertisePacket);
is_adv = true;
} else {
is_adv = false;
stopTx();
}
void BLECommView::sendAdvertisement(void) {
startTx(advertisePacket);
ble_state = Ble_State_Advertising;
}
void BLECommView::startTx(BLETxPacket packetToSend) {
int randomChannel = channel_number_tx;
switch_rx_tx(false);
if (auto_channel) {
int min = 37;
int max = 39;
currentPacket = packetToSend;
packet_counter = currentPacket.packet_count;
randomChannel = min + std::rand() % (max - min + 1);
field_frequency.set_value(get_freq_by_channel_number(randomChannel));
switch (advCount) {
case 0:
channel_number_tx = 37;
break;
case 1:
channel_number_tx = 38;
break;
case 2:
channel_number_tx = 39;
break;
}
if (!in_tx_mode()) {
switch_rx_tx(false);
field_frequency.set_value(get_freq_by_channel_number(37));
currentPacket = packetToSend;
packet_counter = currentPacket.packet_count;
advCount++;
button_send_adv.set_bitmap(&bitmap_stop);
baseband::set_btletx(randomChannel, currentPacket.macAddress, currentPacket.advertisementData, currentPacket.packetType);
transmitter_model.set_tx_gain(47);
transmitter_model.enable();
is_running_tx = true;
} else {
baseband::set_btletx(randomChannel, currentPacket.macAddress, currentPacket.advertisementData, currentPacket.packetType);
if (advCount == 3) {
advCount = 0;
}
if ((packet_counter % 10) == 0) {
text_packets_sent.set(to_string_dec_uint(packet_counter));
}
baseband::set_btletx(37, deviceMAC, currentPacket.advertisementData, currentPacket.pduType);
transmitter_model.set_tx_gain(47);
transmitter_model.enable();
is_sending = true;
packet_counter--;
is_running_tx = true;
}
void BLECommView::stopTx() {
button_send_adv.set_bitmap(&bitmap_play);
text_packets_sent.set(to_string_dec_uint(packet_counter));
switch_rx_tx(true);
baseband::set_btlerx(channel_number_rx);
field_frequency.set_value(get_freq_by_channel_number(channel_number_rx));
receiver_model.enable();
is_running_tx = false;
@@ -248,14 +202,12 @@ void BLECommView::on_data(BlePacketData* packet) {
parse_received_packet(packet, (ADV_PDU_TYPE)packet->type);
}
// called each 1/60th of second, so 6 = 100ms
void BLECommView::on_timer() {
// Send advertise burst only once every 100ms
if (++timer_counter == timer_period) {
timer_counter = 0;
if (!is_adv) {
sendAdvertisement(true);
if (ble_state == Ble_State_Receiving || ble_state == Ble_State_Idle) {
sendAdvertisement();
}
}
}
@@ -263,17 +215,18 @@ void BLECommView::on_timer() {
void BLECommView::on_tx_progress(const bool done) {
if (done) {
if (in_tx_mode()) {
is_sending = false;
ble_state = Ble_State_Receiving;
if (packet_counter == 0) {
if (is_adv) {
sendAdvertisement(false);
} else {
stopTx();
}
} else {
startTx(currentPacket);
}
timer_counter = 0;
timer_period = 12;
stopTx();
// else
// {
// startTx(advertisePacket);
// advCount++;
// }
}
}
}
@@ -309,23 +262,28 @@ void BLECommView::parse_received_packet(const BlePacketData* packet, ADV_PDU_TYP
}
if (pdu_type == SCAN_REQ || pdu_type == CONNECT_REQ) {
ADV_PDU_PAYLOAD_TYPE_1_3* directed_mac_data = (ADV_PDU_PAYLOAD_TYPE_1_3*)packet->data;
std::reverse(directed_mac_data->A1, directed_mac_data->A1 + 6);
console.clear(true);
std::string str_console = "";
std::string pduTypeStr = "";
if (pdu_type == SCAN_REQ) {
pduTypeStr += "SCAN_REQ";
} else if (pdu_type == CONNECT_REQ) {
pduTypeStr += "CONNECT_REQ";
}
ADV_PDU_PAYLOAD_TYPE_1_3* directed_mac_data = (ADV_PDU_PAYLOAD_TYPE_1_3*)packet->data;
str_console += "PACKET TYPE:" + pduTypeStr + "\n";
str_console += "MY MAC:" + to_string_formatted_mac_address(randomMac) + "\n";
str_console += "SCAN MAC:" + to_string_mac_address(directed_mac_data->A1, 6, false) + "\n";
console.write(str_console);
std::reverse(directed_mac_data->A1, directed_mac_data->A1 + 6);
std::string directedMAC = to_string_mac_address(directed_mac_data->A1, 6, false);
// Compare directed MAC Hex with the device MAC.
if (1) {
// std::string str_console = "Received SCAN_REQ from directed MAC: " + directedMAC + "\n";
// console.clear(true);
// console.writeln(str_console);
}
} else if (pdu_type == CONNECT_REQ) {
ADV_PDU_PAYLOAD_TYPE_5* connectReq = (ADV_PDU_PAYLOAD_TYPE_5*)packet->data;
std::reverse(connectReq->AdvA, connectReq->AdvA + 6);
std::string directedMAC = to_string_mac_address(connectReq->AdvA, 6, false);
std::string str_console = "Received CONNECT_REQ from directed MAC: " + directedMAC + "\n";
console.clear(true);
console.writeln(str_console);
}
}
}
+13 -12
View File
@@ -76,7 +76,16 @@ class BLECommView : public View {
void on_data(BlePacketData* packetData);
void on_tx_progress(const bool done);
void parse_received_packet(const BlePacketData* packet, ADV_PDU_TYPE pdu_type);
void sendAdvertisement(bool enable);
void sendAdvertisement(void);
typedef enum {
Ble_State_Idle = 0,
Ble_State_Advertising = 1,
Ble_State_Receiving = 2,
Ble_State_Sending = 3
} BleState;
BleState ble_state{Ble_State_Idle};
NavigationView& nav_;
@@ -101,8 +110,9 @@ class BLECommView : public View {
uint8_t channel_number_tx = 37;
uint8_t channel_number_rx = 37;
bool auto_channel = false;
uint8_t advCount = 0;
char randomMac[13] = "010203040506";
char deviceMAC[13] = "C23456789ABC";
bool is_running_tx = false;
bool is_sending = false;
@@ -111,7 +121,7 @@ class BLECommView : public View {
int16_t timer_period{6}; // Delay each packet by 16ms.
int16_t timer_counter = 0;
int16_t timer_rx_counter = 0;
int16_t timer_rx_period{12}; // Poll Rx for at least 200ms. (TBD)
int16_t timer_rx_period{12}; // Poll Rx for at least 150ms. (TBD)
uint32_t packet_counter{0};
BLETxPacket advertisePacket{};
@@ -147,15 +157,6 @@ class BLECommView : public View {
Channel channel{
{24 * 8, 5, 6 * 8, 4}};
Labels label_send_adv{
{{0 * 8, 2 * 8}, "Send Advertisement:", Theme::getInstance()->fg_light->foreground}};
ImageButton button_send_adv{
{21 * 8, 1 * 16, 10 * 8, 2 * 16},
&bitmap_play,
Theme::getInstance()->fg_green->foreground,
Theme::getInstance()->fg_green->background};
Checkbox check_log{
{24 * 8, 2 * 8},
3,
+305 -207
View File
@@ -57,95 +57,6 @@ std::string pad_string_with_spaces(int snakes) {
return paddedStr;
}
struct GainEntry {
uint8_t lna;
uint8_t vga;
uint8_t gain;
};
// Only LNA with VGA 0-4 is tested to be accurate. Max zeroized gain tested to be 16dBm.
// Beyond that it is hard to tell distance to transmitting device.
// Test was conducted within a few inches of the device.
// Device was transmitting at 0dBm.
constexpr GainEntry gain_table[] =
{
{40, 0, 19},
{32, 0, 18},
{24, 0, 15},
{16, 0, 8},
{8, 0, 2},
{0, 0, 0},
{40, 2, 20},
{32, 2, 22},
{24, 2, 14},
{16, 2, 8},
{8, 2, 2},
{0, 2, 0},
{40, 4, 21},
{32, 4, 22},
{24, 4, 15},
{16, 4, 10},
{8, 4, 3},
{0, 4, 0},
{40, 6, 26},
{32, 6, 22},
{24, 6, 15},
{16, 6, 10},
{8, 6, 4},
{0, 6, 0},
{40, 8, 26},
{32, 8, 26},
{24, 8, 18},
{16, 8, 12},
{8, 8, 6},
{0, 8, 1},
{40, 10, 26},
{32, 10, 26},
{24, 10, 20},
{16, 10, 15},
{8, 10, 8},
{0, 10, 3},
{40, 12, 26},
{32, 12, 26},
{24, 12, 23},
{16, 12, 17},
{8, 12, 10},
{0, 12, 4},
{40, 14, 26},
{32, 14, 26},
{24, 14, 25},
{16, 14, 19},
{8, 14, 12},
{0, 14, 6},
{40, 16, 26},
{32, 16, 26},
{24, 16, 26},
{16, 16, 20},
{8, 16, 13},
{0, 16, 7},
{40, 18, 26},
{32, 18, 26},
{24, 18, 26},
{16, 18, 21},
{8, 18, 14},
{0, 18, 8},
{40, 20, 26},
{32, 20, 26},
{24, 20, 26},
{16, 20, 23},
{8, 20, 16},
{0, 20, 10},
};
uint8_t get_total_gain(uint8_t lna, uint8_t vga) {
for (const auto& entry : gain_table) {
if (entry.lna == lna && entry.vga == vga)
return entry.gain;
}
return 0;
}
uint64_t copy_mac_address_to_uint64(const uint8_t* macAddress) {
uint64_t result = 0;
@@ -275,10 +186,10 @@ void RecentEntriesTable<BleRecentEntries>::draw(
if (!entry.nameString.empty() && entry.include_name) {
line = entry.nameString;
if (line.length() < 10) {
line += pad_string_with_spaces(10 - line.length());
if (line.length() < 17) {
line += pad_string_with_spaces(17 - line.length());
} else {
line = truncate(line, 10);
line = truncate(line, 17);
}
} else {
line = to_string_mac_address(entry.packetData.macAddress, 6, false);
@@ -289,12 +200,12 @@ void RecentEntriesTable<BleRecentEntries>::draw(
if (!entry.informationString.empty()) {
hitsStr = entry.informationString;
} else {
hitsStr = "Hits: " + to_string_dec_int(entry.numHits);
hitsStr = to_string_dec_int(entry.numHits);
}
// Pushing single digit values down right justified.
int hitsDigits = hitsStr.length();
uint8_t hits_spacing = 14 - hitsDigits;
uint8_t hits_spacing = 8 - hitsDigits;
// Pushing single digit values down right justified.
std::string dbStr = to_string_dec_int(entry.dbValue);
@@ -336,7 +247,7 @@ BleRecentEntryDetailView::BleRecentEntryDetailView(NavigationView& nav, const Bl
};
button_send.on_select = [this, &nav](const ui::Button&) {
auto packetToSend = build_packet();
auto packetToSend = build_packet(entry_);
nav.set_on_pop([packetToSend, &nav]() {
nav.replace<BLETxView>(packetToSend);
});
@@ -344,7 +255,7 @@ BleRecentEntryDetailView::BleRecentEntryDetailView(NavigationView& nav, const Bl
};
button_save.on_select = [this, &nav](const ui::Button&) {
auto packetToSave = build_packet();
auto packetToSave = build_packet(entry_);
packetFileBuffer = "";
text_prompt(
@@ -525,7 +436,7 @@ void BleRecentEntryDetailView::set_entry(const BleRecentEntry& entry) {
set_dirty();
}
BLETxPacket BleRecentEntryDetailView::build_packet() {
BLETxPacket BleRecentEntryDetailView::build_packet(BleRecentEntry entry_) {
BLETxPacket bleTxPacket;
memset(&bleTxPacket, 0, sizeof(BLETxPacket));
@@ -533,8 +444,8 @@ BLETxPacket BleRecentEntryDetailView::build_packet() {
strncpy(bleTxPacket.macAddress, macAddressStr.c_str(), 12);
strncpy(bleTxPacket.advertisementData, entry_.dataString.c_str(), entry_.packetData.dataLen * 2);
strncpy(bleTxPacket.packetCount, "50", 3);
bleTxPacket.packet_count = 50;
strncpy(bleTxPacket.packetCount, "10", 3);
bleTxPacket.packet_count = 10;
return bleTxPacket;
}
@@ -584,16 +495,18 @@ BLERxView::BLERxView(NavigationView& nav)
&field_vga,
&options_channel,
&field_frequency,
&check_log,
&button_find,
&check_name,
&label_sort,
&options_sort,
&label_found,
&text_found_count,
&check_serial_log,
&button_filter,
&options_filter,
&check_name,
&check_log,
&check_serial_log,
&check_unique,
&check_duplicate_packets,
&button_find,
&label_found,
&text_found_count,
&button_save_list,
&button_clear_list,
&button_switch,
@@ -605,6 +518,29 @@ BLERxView::BLERxView(NavigationView& nav)
nav_.push<BleRecentEntryDetailView>(entry);
};
ensure_directory(find_packet_path);
ensure_directory(log_packets_path);
ensure_directory(packet_save_path);
// ------------------------------------------------------------------------------
// Handle Check Boxes
// ------------------------------------------------------------------------------
logger = std::make_unique<BLELogger>();
check_name.on_select = [this](Checkbox&, bool v) {
name_enable = v;
// update the include_name instance variable value of each entry in recent entries
setAllMembersToValue(recent, &BleRecentEntry::include_name, v);
recent_entries_view.set_dirty();
};
check_log.on_select = [this](Checkbox&, bool v) {
logging = v;
if (logger && logging)
logger->append(bletx_dir.string() + "/Packets/BLETx_" + to_string_timestamp(rtc_time::now()) + ".TXT");
};
check_serial_log.on_select = [this](Checkbox&, bool v) {
serial_logging = v;
if (v) {
@@ -613,12 +549,28 @@ BLERxView::BLERxView(NavigationView& nav)
portapack::async_tx_enabled = false;
}
};
check_unique.on_select = [this](Checkbox&, bool v) {
uniqueParsing = v;
recent.clear();
recent_entries_view.set_dirty();
};
check_duplicate_packets.on_select = [this](Checkbox&, bool v) {
duplicatePackets = v;
recent.clear();
recent_entries_view.set_dirty();
};
check_name.set_value(name_enable);
check_log.set_value(logging);
check_serial_log.set_value(serial_logging);
check_unique.set_value(uniqueParsing);
check_duplicate_packets.set_value(duplicatePackets);
ensure_directory(find_packet_path);
ensure_directory(log_packets_path);
ensure_directory(packet_save_path);
// ------------------------------------------------------------------------------
// Handle Buttons
// ------------------------------------------------------------------------------
filterBuffer = filter;
button_filter.on_select = [this](Button&) {
@@ -632,16 +584,6 @@ BLERxView::BLERxView(NavigationView& nav)
});
};
logger = std::make_unique<BLELogger>();
check_log.on_select = [this](Checkbox&, bool v) {
logging = v;
if (logger && logging)
logger->append(blerx_dir.string() + "/Logs/BLELOG_" + to_string_timestamp(rtc_time::now()) + ".TXT");
};
check_log.set_value(logging);
button_save_list.on_select = [this, &nav](const ui::Button&) {
listFileBuffer = "";
text_prompt(
@@ -663,17 +605,18 @@ BLERxView::BLERxView(NavigationView& nav)
nav.replace<BLETxView>();
};
field_frequency.set_step(0);
check_name.set_value(name_enable);
check_name.on_select = [this](Checkbox&, bool v) {
name_enable = v;
// update the include_name instance variable value of each entry in recent entries
setAllMembersToValue(recent, &BleRecentEntry::include_name, v);
recent_entries_view.set_dirty();
button_find.on_select = [this](Button&) {
auto open_view = nav_.push<FileLoadView>(".TXT");
open_view->on_changed = [this](std::filesystem::path new_file_path) {
on_file_changed(new_file_path);
};
};
// ------------------------------------------------------------------------------
// Handle Options
// ------------------------------------------------------------------------------
field_frequency.set_step(0);
options_channel.on_change = [this](size_t index, int32_t v) {
channel_index = (uint8_t)index;
@@ -700,7 +643,6 @@ BLERxView::BLERxView(NavigationView& nav)
filter_index = (uint8_t)index;
recent.clear();
handle_filter_options(v);
uniqueParsing = filter_index == 2 ? true : false;
recent_entries_view.set_dirty();
};
@@ -708,15 +650,6 @@ BLERxView::BLERxView(NavigationView& nav)
options_sort.set_selected_index(sort_index, true);
options_filter.set_selected_index(filter_index, true);
button_find.on_select = [this](Button&) {
auto open_view = nav_.push<FileLoadView>(".TXT");
open_view->on_changed = [this](std::filesystem::path new_file_path) {
on_file_changed(new_file_path);
// nav_.set_on_pop([this]() { button_play.focus(); });
};
};
// Auto-configure modem for LCR RX (will be removed later)
baseband::set_btlerx(channel_number);
@@ -874,22 +807,39 @@ bool BLERxView::saveFile(const std::filesystem::path& path) {
}
void BLERxView::on_data(BlePacketData* packet) {
uint64_t macAddressEncoded = copy_mac_address_to_uint64(packet->macAddress);
uint64_t uniqueKeyEncoded = copy_mac_address_to_uint64(packet->macAddress);
// Start of Packet stuffing.
// Masking off the top 2 bytes to avoid invalid keys.
BleRecentEntry tempEntry;
uint64_t key = (uniqueKeyEncoded & 0xFFFFFFFFFFFF);
if (updateEntry(packet, tempEntry, (ADV_PDU_TYPE)packet->type)) {
auto& entry = ::on_packet(recent, macAddressEncoded & 0xFFFFFFFFFFFF);
if (duplicatePackets) {
key |= ((uint64_t)packet->type) << 48;
}
// Preserve exisisting data from entry.
tempEntry.macAddress = macAddressEncoded;
tempEntry.numHits = ++entry.numHits;
bool packetExists = false;
entry = tempEntry;
// If found store into tempEntry to modify.
auto it = find(recent, key);
if (it != recent.end()) {
recent.push_front(*it);
recent.erase(it);
updateEntry(packet, recent.front(), (ADV_PDU_TYPE)packet->type);
packetExists = true;
} else {
recent.emplace_front(key);
truncate_entries(recent);
packetExists = updateEntry(packet, recent.front(), (ADV_PDU_TYPE)packet->type);
// If parsing failed, remove entry.
if (!packetExists) {
recent.erase(recent.begin());
}
}
if (packetExists) {
handle_filter_options(options_filter.selected_index());
handle_entries_sort(options_sort.selected_index());
@@ -899,7 +849,7 @@ void BLERxView::on_data(BlePacketData* packet) {
while (it != searchList.end()) {
std::string searchStr = (std::string)*it;
if (entry.dataString.find(searchStr) != std::string::npos) {
if (recent.front().dataString.find(searchStr) != std::string::npos) {
searchList.erase(it);
found_count++;
break;
@@ -943,10 +893,6 @@ void BLERxView::log_ble_packet(BlePacketData* packet) {
void BLERxView::on_filter_change(std::string value) {
// New filter? Reset list from recent entries.
if (filter != value) {
// resetFilteredEntries(recent, [&value](const BleRecentEntry& entry) {
// // return (entry.dataString.find(value) == std::string::npos) && (entry.nameString.find(value) == std::string::npos);
// return (entry.dataString.find(value) == std::string::npos) && (entry.nameString.find(value) == std::string::npos) && (to_string_mac_address(entry.packetData.macAddress, 6, false).find(value) == std::string::npos);
// });
filter = value;
handle_filter_options(options_filter.selected_index());
}
@@ -1000,13 +946,10 @@ void BLERxView::on_timer() {
timer_count = 0;
if (auto_channel) {
int min = 37;
int max = 39;
field_frequency.set_value(get_freq_by_channel_number(channel_number));
baseband::set_btlerx(channel_number);
int randomChannel = min + std::rand() % (max - min + 1);
field_frequency.set_value(get_freq_by_channel_number(randomChannel));
baseband::set_btlerx(randomChannel);
channel_number = (channel_number < 39) ? channel_number + 1 : 37;
}
}
if (ble_rx_error != BLE_RX_NO_ERROR) {
@@ -1027,7 +970,7 @@ void BLERxView::handle_entries_sort(uint8_t index) {
switch (index) {
case 0:
sortEntriesBy(
recent, [](const BleRecentEntry& entry) { return entry.macAddress; }, true);
recent, [](const BleRecentEntry& entry) { return entry.uniqueKey & 0xFFFFFFFFFFFF; }, true);
break;
case 1:
sortEntriesBy(
@@ -1045,6 +988,10 @@ void BLERxView::handle_entries_sort(uint8_t index) {
sortEntriesBy(
recent, [](const BleRecentEntry& entry) { return entry.nameString; }, true);
break;
case 5:
sortEntriesBy(
recent, [](const BleRecentEntry& entry) { return entry.informationString; }, true);
break;
default:
break;
}
@@ -1055,16 +1002,43 @@ void BLERxView::handle_entries_sort(uint8_t index) {
void BLERxView::handle_filter_options(uint8_t index) {
auto value = filter;
switch (index) {
case 0: // filter by Data
// Data
case 0:
resetFilteredEntries(recent, [&value](const BleRecentEntry& entry) {
return (entry.dataString.find(value) == std::string::npos) && (entry.nameString.find(value) == std::string::npos);
});
break;
case 1: // filter by MAC address (All caps: e.g. AA:BB:CC:DD:EE:FF)
// MAC address (All caps: e.g. AA:BB:CC:DD:EE:FF)
case 1:
resetFilteredEntries(recent, [&value](const BleRecentEntry& entry) {
return (to_string_mac_address(entry.packetData.macAddress, 6, false).find(value) == std::string::npos);
});
break;
// Name
case 2:
resetFilteredEntries(recent, [&value](const BleRecentEntry& entry) {
return (entry.nameString.find(value) == std::string::npos);
});
break;
// Info
case 3:
resetFilteredEntries(recent, [&value](const BleRecentEntry& entry) {
return (entry.informationString.find(value) == std::string::npos);
});
break;
// Vendor
case 4:
resetFilteredEntries(recent, [&value](const BleRecentEntry& entry) {
std::string vendor_name = lookup_mac_vendor(entry.packetData.macAddress);
return (vendor_name.find(value) == std::string::npos);
});
break;
// Channel
case 5:
resetFilteredEntries(recent, [&value](const BleRecentEntry& entry) {
return (to_string_dec_int(entry.channelNumber).find(value) == std::string::npos);
});
break;
default:
break;
}
@@ -1087,64 +1061,181 @@ bool BLERxView::updateEntry(const BlePacketData* packet, BleRecentEntry& entry,
bool success = false;
int i;
for (i = 0; i < packet->dataLen; i++) {
data_string += to_string_hex(packet->data[i], 2);
}
entry.dbValue = 2 * (packet->max_dB - get_total_gain(receiver_model.lna(), receiver_model.vga()));
entry.timestamp = to_string_timestamp(rtc_time::now());
entry.dataString = data_string;
entry.packetData.type = packet->type;
entry.packetData.size = packet->size;
entry.packetData.dataLen = packet->dataLen;
// Mac Address of sender.
entry.packetData.macAddress[0] = packet->macAddress[0];
entry.packetData.macAddress[1] = packet->macAddress[1];
entry.packetData.macAddress[2] = packet->macAddress[2];
entry.packetData.macAddress[3] = packet->macAddress[3];
entry.packetData.macAddress[4] = packet->macAddress[4];
entry.packetData.macAddress[5] = packet->macAddress[5];
entry.pduType = pdu_type;
entry.channelNumber = channel_number;
if (entry.vendor_status == MAC_VENDOR_UNKNOWN) {
std::string vendor_name;
entry.vendor_status = lookup_mac_vendor_status(entry.packetData.macAddress, vendor_name);
}
// Parse Data Section into buffer to be interpretted later.
for (int i = 0; i < packet->dataLen; i++) {
entry.packetData.data[i] = packet->data[i];
}
entry.include_name = check_name.value();
// Only parse name for advertisment packets and empty name entries
if (pdu_type == ADV_IND || pdu_type == ADV_NONCONN_IND) // || pdu_type == SCAN_RSP || pdu_type == ADV_SCAN_IND)
{
if (pdu_type == ADV_IND || pdu_type == ADV_NONCONN_IND || pdu_type == SCAN_RSP || pdu_type == ADV_SCAN_IND) {
if (uniqueParsing) {
// Add your unique beacon parsing function here.
success = parse_tracking_beacon_data(packet->data, packet->dataLen, entry.nameString, entry.informationString);
}
if (!success && !uniqueParsing) {
success = parse_beacon_data(packet->data, packet->dataLen, entry.nameString, entry.informationString);
}
} else if (pdu_type == ADV_DIRECT_IND || pdu_type == SCAN_REQ) {
ADV_PDU_PAYLOAD_TYPE_1_3* directed_mac_data = (ADV_PDU_PAYLOAD_TYPE_1_3*)entry.packetData.data;
reverse_byte_array(directed_mac_data->A1, 6);
if (!uniqueParsing) {
ADV_PDU_PAYLOAD_TYPE_1_3* directed_mac_data = (ADV_PDU_PAYLOAD_TYPE_1_3*)packet->data;
reverse_byte_array(directed_mac_data->A1, 6);
success = true;
}
}
if (success) {
int i;
for (i = 0; i < packet->dataLen; i++) {
data_string += to_string_hex(packet->data[i], 2);
}
entry.dbValue = packet->max_dB - (receiver_model.lna() + receiver_model.vga() + (receiver_model.rf_amp() ? 14 : 0));
entry.timestamp = to_string_timestamp(rtc_time::now());
entry.dataString = data_string;
entry.packetData.type = packet->type;
entry.packetData.size = packet->size;
entry.packetData.dataLen = packet->dataLen;
// Mac Address of sender.
entry.packetData.macAddress[0] = packet->macAddress[0];
entry.packetData.macAddress[1] = packet->macAddress[1];
entry.packetData.macAddress[2] = packet->macAddress[2];
entry.packetData.macAddress[3] = packet->macAddress[3];
entry.packetData.macAddress[4] = packet->macAddress[4];
entry.packetData.macAddress[5] = packet->macAddress[5];
entry.pduType = pdu_type;
entry.channelNumber = channel_number;
entry.numHits++;
if (entry.vendor_status == MAC_VENDOR_UNKNOWN) {
std::string vendor_name;
entry.vendor_status = lookup_mac_vendor_status(entry.packetData.macAddress, vendor_name);
}
// Parse Data Section into buffer to be interpretted later.
for (int i = 0; i < packet->dataLen; i++) {
entry.packetData.data[i] = packet->data[i];
}
entry.include_name = check_name.value();
}
return success;
}
bool BLERxView::parse_tracking_beacon_data(const uint8_t* data, uint8_t length, std::string& nameString, std::string& informationString) {
uint8_t currentByte, currentLength, currentType = 0;
for (currentByte = 0; currentByte < length;) {
currentLength = data[currentByte++];
currentType = data[currentByte++];
// Manufacturer Specific Data (0xFF)
if (currentType == 0xFF && currentLength >= 4) {
uint16_t companyID = data[currentByte] | (data[currentByte + 1] << 8);
// Apple AirTag / Find My
if (companyID == 0x004C && currentLength >= 6) {
uint8_t appleType = data[currentByte + 2];
uint8_t appleLen = data[currentByte + 3];
if (appleType == 0x12 && appleLen == 0x19 && currentLength >= 4 + appleLen) {
nameString.assign("Apple AirTag");
informationString.assign("Find My");
return true;
} else if (appleType == 0x02 && appleLen == 0x15) {
uint16_t major = (data[currentByte + 20] << 8) | data[currentByte + 21];
uint16_t minor = (data[currentByte + 22] << 8) | data[currentByte + 23];
nameString.assign("iBeacon");
informationString.assign(to_string_hex(major) + to_string_hex(minor));
return true;
}
}
}
// Services
else if ((currentType == 0x02 || currentType == 0x03) && currentLength >= 3) {
for (int u = 0; u < currentLength - 1; u += 2) {
uint16_t uuid16 = data[currentByte + u] | (data[currentByte + u + 1] << 8);
if (uuid16 == 0x1802) { // Immediate Alert Service = Find Me Profile
nameString.assign("FindMe");
informationString.assign("IAS");
return true;
}
}
}
// Service Data
else if (currentType == 0x16 && currentLength >= 3) {
uint16_t uuid16 = data[currentByte] | (data[currentByte + 1] << 8);
switch (uuid16) {
case 0xFD59: { // Samsung SmartTag - Unregistered
nameString.assign("Samsung SmartTag");
informationString.assign("Unreg");
return true;
}
case 0xFD5A: { // Samsung SmartTag - Registered
nameString.assign("Samsung SmartTag");
informationString.assign("Reg");
return true;
}
case 0xFD84: {
if (currentByte + 2 < length) {
uint8_t model = data[currentByte + 2];
switch (model) {
case 0x01:
informationString.assign("Mate");
break;
case 0x02:
informationString.assign("Pro");
break;
case 0x03:
informationString.assign("Slim");
break;
case 0x04:
informationString.assign("Sticker");
break;
default:
informationString.assign("Unknown");
break;
}
}
nameString.assign("Tile");
return true;
}
case 0xFEAA: {
if (currentByte + 2 < length) {
uint8_t frameType = data[currentByte + 2];
switch (frameType) {
case 0x00:
informationString.assign("UID");
break;
case 0x10:
informationString.assign("URL");
break;
case 0x20:
informationString.assign("TLM");
break;
case 0x30:
informationString.assign("EID");
break;
default:
informationString.assign("Unknown");
break;
}
}
nameString.assign("Eddystone");
return true;
}
}
}
currentByte += (currentLength - 1);
}
return false;
}
bool BLERxView::parse_beacon_data(const uint8_t* data, uint8_t length, std::string& nameString, std::string& informationString) {
uint8_t currentByte, currentLength, currentType = 0;
std::string tempName = "";
for (currentByte = 0; currentByte < length;) {
currentLength = data[currentByte++];
@@ -1154,18 +1245,25 @@ bool BLERxView::parse_beacon_data(const uint8_t* data, uint8_t length, std::stri
for (int i = 0; ((i < currentLength - 1) && (currentByte < length)); i++) {
// parse the name of bluetooth device: 0x08->Shortened Local Name; 0x09->Complete Local Name
if (currentType == 0x08 || currentType == 0x09) {
nameString += (char)data[currentByte];
tempName += (char)data[currentByte];
}
currentByte++;
}
}
if (nameString.empty()) {
nameString = "None";
if (!tempName.empty()) {
nameString = tempName;
break;
}
}
informationString = "";
if (!informationString.empty()) {
// Option to change title of Hits Column.
// Setting to default for now.
columns.set(1, "Hits", 7);
}
return true;
}
+48 -30
View File
@@ -71,7 +71,8 @@ typedef enum {
RESERVED5 = 12,
RESERVED6 = 13,
RESERVED7 = 14,
RESERVED8 = 15
RESERVED8 = 15,
UNKNOWN = 16
} ADV_PDU_TYPE;
typedef enum {
@@ -84,9 +85,9 @@ typedef enum {
struct BleRecentEntry {
using Key = uint64_t;
static constexpr Key invalid_key = 0xFFFFFFFFFFFF;
static constexpr Key invalid_key = 0xFFFFFFFFFFFFF;
uint64_t macAddress;
uint64_t uniqueKey;
int dbValue;
BlePacketData packetData;
std::string timestamp;
@@ -105,8 +106,8 @@ struct BleRecentEntry {
}
BleRecentEntry(
const uint64_t macAddress)
: macAddress{macAddress},
const uint64_t uniqueKey)
: uniqueKey{uniqueKey},
dbValue{},
packetData{},
timestamp{},
@@ -122,7 +123,7 @@ struct BleRecentEntry {
}
Key key() const {
return macAddress;
return uniqueKey;
}
};
@@ -139,11 +140,11 @@ class BleRecentEntryDetailView : public View {
void update_data();
void focus() override;
void paint(Painter&) override;
static BLETxPacket build_packet(BleRecentEntry entry_);
private:
NavigationView& nav_;
BleRecentEntry entry_{};
BLETxPacket build_packet();
void on_save_file(const std::string value, BLETxPacket packetToSave);
bool saveFile(const std::filesystem::path& path, BLETxPacket packetToSave);
std::string packetFileBuffer{};
@@ -227,6 +228,7 @@ class BLERxView : public View {
void handle_filter_options(uint8_t index);
bool updateEntry(const BlePacketData* packet, BleRecentEntry& entry, ADV_PDU_TYPE pdu_type);
bool parse_beacon_data(const uint8_t* data, uint8_t length, std::string& nameString, std::string& informationString);
bool parse_tracking_beacon_data(const uint8_t* data, uint8_t length, std::string& nameString, std::string& informationString);
NavigationView& nav_;
@@ -240,6 +242,7 @@ class BLERxView : public View {
uint8_t sort_index{0};
uint8_t filter_index{0};
bool uniqueParsing = false;
bool duplicatePackets = false;
std::string filter{};
bool logging{false};
bool serial_logging{false};
@@ -258,6 +261,8 @@ class BLERxView : public View {
// disabled to always start without USB serial activated until we can make it non blocking if not connected
// {"serial_log"sv, &serial_logging},
{"name"sv, &name_enable},
{"unique_parsing"sv, &uniqueParsing},
{"duplicate_packets"sv, &duplicatePackets},
}};
std::string str_console = "";
@@ -267,7 +272,7 @@ class BLERxView : public View {
bool auto_channel = false;
int16_t timer_count{0};
int16_t timer_period{6}; // 100ms
int16_t timer_period{1}; // 25ms
std::string filterBuffer{};
std::string listFileBuffer{};
@@ -283,7 +288,7 @@ class BLERxView : public View {
std::filesystem::path log_packets_path{blerx_dir / u"Logs/????.TXT"};
std::filesystem::path packet_save_path{blerx_dir / u"Lists/????.csv"};
static constexpr auto header_height = 9 * 8;
static constexpr auto header_height = 12 * 8;
static constexpr auto switch_button_height = 3 * 16;
OptionsField options_channel{
@@ -323,7 +328,8 @@ class BLERxView : public View {
{"Hits", 1},
{"dB", 2},
{"Time", 3},
{"Name", 4}}};
{"Name", 4},
{"Info", 5}}};
Button button_filter{
{11 * 8, 2 * 8, 7 * 8, 16},
@@ -334,7 +340,10 @@ class BLERxView : public View {
7,
{{"Data", 0},
{"MAC", 1},
{"Unique", 2}}};
{"Name", 2},
{"Info", 3},
{"Vendor", 4},
{"Channel", 5}}};
Checkbox check_log{
{10 * 8, 4 * 8 + 2},
@@ -348,36 +357,45 @@ class BLERxView : public View {
"Name",
true};
Button button_find{
{0 * 8, 7 * 8 - 2, 4 * 8, 16},
"Find"};
Labels label_found{
{{5 * 8, 7 * 8 - 2}, "Found:", Theme::getInstance()->fg_light->foreground}};
Text text_found_count{
{11 * 8, 7 * 8 - 2, 20 * 8, 16},
"0/0"};
Checkbox check_serial_log{
{18 * 8 + 2, 4 * 8 + 2},
7,
"USB Log",
true};
// Console console{
// {0, 10 * 8, screen_height, screen_height-80}};
Checkbox check_unique{
{0 * 8 + 2, 7 * 8 + 2},
7,
"Unique",
true};
Checkbox check_duplicate_packets{
{10 * 8 + 2, 7 * 8 + 2},
7,
"Duplicate",
true};
Button button_find{
{0 * 8, 10 * 8 - 2, 4 * 8, 16},
"Find"};
Labels label_found{
{{5 * 8, 10 * 8 - 2}, "Found:", Theme::getInstance()->fg_light->foreground}};
Text text_found_count{
{11 * 8, 10 * 8 - 2, 20 * 8, 16},
"0/0"};
Button button_clear_list{
{2 * 8, screen_height - (16 + 32), 7 * 8, 32},
{2 * 8, 320 - (16 + 32), 7 * 8, 32},
"Clear"};
Button button_save_list{
{11 * 8, screen_height - (16 + 32), 11 * 8, 32},
{11 * 8, 320 - (16 + 32), 11 * 8, 32},
"Export CSV"};
Button button_switch{
{screen_width - 6 * 8, screen_height - (16 + 32), 4 * 8, 32},
{240 - 6 * 8, 320 - (16 + 32), 4 * 8, 32},
"Tx"};
std::string str_log{""};
@@ -386,9 +404,9 @@ class BLERxView : public View {
BleRecentEntries recent{};
BleRecentEntries tempList{};
const RecentEntriesColumns columns{{
{"Name", 10},
{"Information", 13},
RecentEntriesColumns columns{{
{"Name", 17},
{"Hits", 7},
{"dBm", 4},
}};
+115 -80
View File
@@ -35,6 +35,7 @@
#include "rtc_time.hpp"
#include "string_format.hpp"
#include "file_path.hpp"
#include "usb_serial_asyncmsg.hpp"
using namespace portapack;
using namespace modems;
@@ -123,6 +124,31 @@ static std::uint64_t get_freq_by_channel_number(uint8_t channel_number) {
namespace ui {
PKT_TYPE get_pkt_type_from_string(const std::string& type_str) {
if (type_str == "RAW") {
return PKT_TYPE_RAW;
} else if (type_str == "DISCOVERY") {
return PKT_TYPE_DISCOVERY;
} else if (type_str == "IBEACON") {
return PKT_TYPE_IBEACON;
} else if (type_str == "ADV_IND") {
return PKT_TYPE_ADV_IND;
} else if (type_str == "ADV_DIRECT_IND") {
return PKT_TYPE_ADV_DIRECT_IND;
} else if (type_str == "ADV_NONCONN_IND") {
return PKT_TYPE_ADV_NONCONN_IND;
} else if (type_str == "ADV_SCAN_IND") {
return PKT_TYPE_ADV_SCAN_IND;
} else if (type_str == "SCAN_REQ") {
return PKT_TYPE_SCAN_REQ;
} else if (type_str == "SCAN_RSP") {
return PKT_TYPE_SCAN_RSP;
} else if (type_str == "CONNECT_REQ") {
return PKT_TYPE_CONNECT_REQ;
}
return PKT_TYPE_INVALID_TYPE;
}
void BLETxView::focus() {
button_open.focus();
}
@@ -167,28 +193,11 @@ void BLETxView::toggle() {
}
}
void BLETxView::start() {
baseband::run_image(portapack::spi_flash::image_tag_btle_tx);
transmitter_model.enable();
void BLETxView::send_packet() {
// Generate new random Mac Address.
transmitter_model.enable();
generateRandomMacAddress(randomMac);
// If this is our first run, check file.
if (!is_active()) {
File data_file;
auto error = data_file.open(file_path);
if (error && !file_override) {
file_error();
check_loop.set_value(false);
return;
}
button_play.set_bitmap(&bitmap_stop);
is_running = true;
}
char advertisementData[63] = {0};
strcpy(advertisementData, packets[current_packet].advertisementData);
@@ -235,9 +244,28 @@ void BLETxView::start() {
}
}
// Setup next packet configuration.
progressbar.set_max(packets[current_packet].packet_count);
baseband::set_btletx(channel_number, random_mac ? randomMac : packets[current_packet].macAddress, advertisementData, pduType);
progressbar.set_value(packets[current_packet].packet_count - packet_counter);
text_packets_sent.set(to_string_dec_uint(packet_counter));
baseband::set_btletx(channel_number, random_mac ? randomMac : packets[current_packet].macAddress, advertisementData, packets[current_packet].pduType);
packetDone = false;
}
void BLETxView::start() {
if (file_path.empty()) {
file_error();
check_loop.set_value(false);
return;
}
baseband::run_image(portapack::spi_flash::image_tag_btle_tx);
transmitter_model.enable();
button_play.set_bitmap(&bitmap_stop);
is_running = true;
send_packet();
}
void BLETxView::stop() {
@@ -253,66 +281,67 @@ void BLETxView::stop() {
is_running = false;
}
void BLETxView::reset() {
transmitter_model.disable();
baseband::shutdown();
start();
}
// called each 1/60th of second, so 6 = 100ms
void BLETxView::on_timer() {
if (++timer_count == timer_period) {
timer_count = 0;
if (is_active()) {
// Reached end of current packet repeats.
if (packet_counter == 0) {
// Done sending all packets.
if (current_packet == (num_packets - 1)) {
current_packet = 0;
// If looping, restart from beginning.
if (check_loop.value()) {
update_current_packet(packets[current_packet], current_packet);
reset();
} else {
stop();
}
} else {
current_packet++;
update_current_packet(packets[current_packet], current_packet);
reset();
}
} else {
reset();
}
}
}
if (++auto_channel_counter == auto_channel_period) {
auto_channel_counter = 0;
if (auto_channel) {
int min = 37;
int max = 39;
channel_number = min + std::rand() % (max - min + 1);
field_frequency.set_value(get_freq_by_channel_number(channel_number));
if (is_active() && packetDone) {
send_packet();
}
}
}
void BLETxView::on_tx_progress(const bool done) {
void BLETxView::on_tx_progress(const bool done, uint32_t progress) {
if (done) {
if (is_active()) {
if ((packet_counter % 10) == 0) {
text_packets_sent.set(to_string_dec_uint(packet_counter));
}
transmitter_model.disable();
if (auto_channel) {
switch (advCount) {
case 0:
channel_number = 37;
break;
case 1:
channel_number = 38;
break;
case 2:
channel_number = 39;
break;
}
packet_counter--;
progressbar.set_value(packets[current_packet].packet_count - packet_counter);
field_frequency.set_value(get_freq_by_channel_number(channel_number));
if (advCount == 3) {
channel_number = 37;
packet_counter--;
packetDone = true;
advCount = 0;
} else {
send_packet();
advCount++;
}
} else {
packet_counter--;
packetDone = true;
}
}
// Reached end of current packet repeats.
if (packet_counter == 0) {
// Done sending all packets.
if (current_packet == (num_packets - 1)) {
current_packet = 0;
// If looping, restart from beginning.
if (check_loop.value()) {
update_current_packet(packets[current_packet], current_packet);
} else {
stop();
}
} else {
current_packet++;
update_current_packet(packets[current_packet], current_packet);
}
}
}
}
@@ -331,7 +360,6 @@ BLETxView::BLETxView(NavigationView& nav)
&label_speed,
&options_speed,
&options_channel,
&options_adv_type,
&label_marked_data,
&marked_data_sequence,
&label_packet_index,
@@ -371,13 +399,8 @@ BLETxView::BLETxView(NavigationView& nav)
timer_count = 0;
};
options_adv_type.on_change = [this](size_t, int32_t i) {
pduType = (PKT_TYPE)i;
};
options_speed.set_selected_index(0);
options_channel.set_selected_index(0);
options_adv_type.set_selected_index(0);
options_channel.set_selected_index(3);
check_rand_mac.set_value(false);
check_rand_mac.on_select = [this](Checkbox&, bool v) {
@@ -482,17 +505,25 @@ void BLETxView::on_file_changed(const fs::path& new_file_path) {
do {
readUntil(data_file, packets[num_packets].macAddress, mac_address_size_str, ' ');
readUntil(data_file, packets[num_packets].advertisementData, max_packet_size_str, ' ');
readUntil(data_file, packets[num_packets].packetType, max_packet_type_str, ' ');
readUntil(data_file, packets[num_packets].packetCount, max_packet_repeat_str, '\n');
uint64_t macAddressSize = strlen(packets[num_packets].macAddress);
uint64_t advertisementDataSize = strlen(packets[num_packets].advertisementData);
uint64_t packetCountSize = strlen(packets[num_packets].packetCount);
uint64_t packetTypeSize = strlen(packets[num_packets].packetType);
packets[num_packets].packet_count = stringToUint32(packets[num_packets].packetCount);
packets[num_packets].pduType = (PKT_TYPE)get_pkt_type_from_string(packets[num_packets].packetType);
// Verify Data.
if ((macAddressSize == mac_address_size_str) && (advertisementDataSize < max_packet_size_str) && (packetCountSize < max_packet_repeat_str) &&
hasValidHexPairs(packets[num_packets].macAddress, macAddressSize / 2) && hasValidHexPairs(packets[num_packets].advertisementData, advertisementDataSize / 2) && (packets[num_packets].packet_count >= 1) && (packets[num_packets].packet_count < max_packet_repeat_count)) {
if ((macAddressSize == mac_address_size_str) &&
(advertisementDataSize < max_packet_size_str) &&
(packetCountSize < max_packet_repeat_str) &&
hasValidHexPairs(packets[num_packets].macAddress, macAddressSize / 2) &&
hasValidHexPairs(packets[num_packets].advertisementData, advertisementDataSize / 2) &&
(packets[num_packets].packet_count >= 1) && (packets[num_packets].packet_count < max_packet_repeat_count) &&
(packetTypeSize <= max_packet_type_str)) {
text_filename.set(truncate(file_path.filename().string(), 12));
} else {
@@ -510,6 +541,8 @@ void BLETxView::on_file_changed(const fs::path& new_file_path) {
} while (num_packets < max_num_packets);
update_current_packet(packets[0], 0);
data_file.close();
}
}
@@ -540,6 +573,8 @@ void BLETxView::update_current_packet(BLETxPacket packet, uint32_t currentIndex)
text_mac_address.set(formattedMacAddress);
progressbar.set_max(packet.packet_count);
packet_counter = packet.packet_count;
current_packet = currentIndex;
@@ -550,7 +585,7 @@ void BLETxView::update_current_packet(BLETxPacket packet, uint32_t currentIndex)
dataFile.write("\n", 1);
}
dataFile.~File();
dataFile.close();
auto result = FileWrapper::open(dataTempFilePath);
+18 -13
View File
@@ -38,6 +38,8 @@
#include "utility.hpp"
#include "file_path.hpp"
#include "ble_tx_app.hpp"
#include "recent_entries.hpp"
#include <string>
@@ -91,8 +93,9 @@ struct BLETxPacket {
char macAddress[13];
char advertisementData[63];
char packetCount[11];
char packetType[17];
uint32_t packet_count;
PKT_TYPE packetType;
PKT_TYPE pduType;
};
class BLETxView : public View {
@@ -109,8 +112,8 @@ class BLETxView : public View {
bool is_active() const;
void toggle();
void start();
void send_packet();
void stop();
void reset();
void handle_replay_thread_done(const uint32_t return_code);
void file_error();
bool saveFile(const std::filesystem::path& path);
@@ -122,7 +125,7 @@ class BLETxView : public View {
void on_data(uint32_t value, bool is_data);
void on_file_changed(const std::filesystem::path& new_file_path);
void on_save_file(const std::string value);
void on_tx_progress(const bool done);
void on_tx_progress(const bool done, uint32_t progress);
void on_random_data_change(std::string value);
void update_current_packet(BLETxPacket packet, uint32_t currentIndex);
@@ -156,6 +159,8 @@ class BLETxView : public View {
uint32_t packet_counter{0};
uint32_t num_packets{0};
uint32_t current_packet{0};
uint8_t packetTxCount{0};
bool packetDone = false;
bool random_mac = false;
bool file_override = false;
@@ -170,17 +175,17 @@ class BLETxView : public View {
std::vector<uint16_t> markedBytes{};
CursorPos cursor_pos{};
uint8_t marked_counter = 0;
uint8_t advCount = 0;
static constexpr uint8_t mac_address_size_str{12};
static constexpr uint8_t max_packet_size_str{62};
static constexpr uint8_t max_packet_repeat_str{10};
static constexpr uint8_t max_packet_type_str{16};
static constexpr uint32_t max_packet_repeat_count{UINT32_MAX};
static constexpr uint32_t max_num_packets{32};
BLETxPacket packets[max_num_packets];
PKT_TYPE pduType = {PKT_TYPE_DISCOVERY};
static constexpr auto header_height = 10 * 16;
static constexpr auto switch_button_height = 6 * 16;
@@ -216,7 +221,7 @@ class BLETxView : public View {
true};
ImageButton button_play{
{screen_width - 2 * 8, 2 * 16, 2 * 8, 1 * 16},
{28 * 8, 2 * 16, 2 * 8, 1 * 16},
&bitmap_play,
Theme::getInstance()->fg_green->foreground,
Theme::getInstance()->fg_green->background};
@@ -227,11 +232,11 @@ class BLETxView : public View {
OptionsField options_speed{
{7 * 8, 6 * 8},
3,
{{"1 ", 1}, // 16ms
{"2 ", 2}, // 32ms
{"3 ", 3}, // 48ms
{"4 ", 6}, // 100ms
{"5 ", 12}}}; // 200ms
{{"1 ", 2}, // 25ms
{"2 ", 4}, // 50ms
{"3 ", 6}, // 75ms
{"4 ", 8}, // 100ms
{"5 ", 12}}}; // 150ms
OptionsField options_channel{
{11 * 8, 6 * 8},
@@ -288,7 +293,7 @@ class BLETxView : public View {
{{0 * 8, 9 * 16}, "Packet Data:", Theme::getInstance()->fg_light->foreground}};
TextViewer dataEditView{
{0, 9 * 18, screen_width, screen_height - 80}};
{0, 9 * 18, 240, 240}};
Button button_clear_marked{
{1 * 8, 14 * 16, 13 * 8, 3 * 8},
@@ -320,7 +325,7 @@ class BLETxView : public View {
Message::ID::TXProgress,
[this](const Message* const p) {
const auto message = *reinterpret_cast<const TXProgressMessage*>(p);
this->on_tx_progress(message.done);
this->on_tx_progress(message.done, message.progress);
}};
MessageHandlerRegistration message_handler_frame_sync{
+2 -2
View File
@@ -247,8 +247,8 @@ APRSTableView::APRSTableView(NavigationView& nav, Rect parent_rec)
details_view.hidden(true);
recent_entries_view.set_parent_rect({0, 0, screen_width, screen_width - 40});
details_view.set_parent_rect({0, 0, screen_width, screen_width - 40});
recent_entries_view.set_parent_rect({0, 0, screen_width, screen_height - 40});
details_view.set_parent_rect({0, 0, screen_width, screen_height - 40});
recent_entries_view.on_select = [this](const APRSRecentEntry& entry) {
this->on_show_detail(entry);
+1
View File
@@ -84,6 +84,7 @@ void SubGhzDView::focus() {
SubGhzDView::SubGhzDView(NavigationView& nav)
: nav_{nav} {
add_children({&rssi,
&channel,
&field_rf_amp,
&field_lna,
&field_vga,
+3
View File
@@ -129,6 +129,9 @@ class SubGhzDView : public View {
{18 * 8, 0 * 16}};
RSSI rssi{
{21 * 8, 0, 6 * 8, 4}};
Channel channel{
{21 * 8, 5, 6 * 8, 4},
};
RxFrequencyField field_frequency{
{0 * 8, 0 * 16},
nav_};
@@ -107,6 +107,7 @@ void WeatherView::focus() {
WeatherView::WeatherView(NavigationView& nav)
: nav_{nav} {
add_children({&rssi,
&channel,
&field_rf_amp,
&field_lna,
&field_vga,
@@ -147,6 +147,9 @@ class WeatherView : public View {
{18 * 8, 0 * 16}};
RSSI rssi{
{21 * 8, 0, 6 * 8, 4}};
Channel channel{
{21 * 8, 5, 6 * 8, 4},
};
AudioVolumeField field_volume{
{screen_width - 2 * 8, 0 * 16}};
+7 -6
View File
@@ -186,13 +186,14 @@ void set_nrf(const uint32_t baudrate, const uint32_t word_length, const uint32_t
send_message(&message);
}
void set_fsk(const size_t deviation) {
void set_fsk(const uint8_t samplesPerSymbol, const uint32_t syncWord, const uint8_t syncWordLength, const uint32_t preamble, const uint8_t preambleLength, uint16_t numDataBytes) {
const FSKRxConfigureMessage message{
taps_200k_decim_0,
taps_16k0_decim_1,
taps_11k0_channel,
2,
deviation};
samplesPerSymbol,
syncWord,
syncWordLength,
preamble,
preambleLength,
numDataBytes};
send_message(&message);
}
+1 -1
View File
@@ -80,7 +80,7 @@ void set_pitch_rssi(int32_t avg, bool enabled);
void set_afsk_data(const uint32_t afsk_samples_per_bit, const uint32_t afsk_phase_inc_mark, const uint32_t afsk_phase_inc_space, const uint8_t afsk_repeat, const uint32_t afsk_bw, const uint8_t symbol_count);
void kill_afsk();
void set_afsk(const uint32_t baudrate, const uint32_t word_length, const uint32_t trigger_value, const bool trigger_word);
void set_fsk(const size_t deviation);
void set_fsk(const uint32_t samplesPerSymbol, const uint32_t syncWord, const uint8_t syncWordLength, const uint32_t preamble, const uint8_t preambleLength, uint16_t numDataBytes);
void set_aprs(const uint32_t baudrate);
void set_btlerx(uint8_t channel_number);
-38
View File
@@ -3689,44 +3689,6 @@ static constexpr Bitmap bitmap_icon_sonde{
{16, 16},
bitmap_icon_sonde_data};
static constexpr uint8_t bitmap_icon_soundboard_data[] = {
0xF0,
0x0F,
0x1C,
0x18,
0x17,
0x38,
0x15,
0x78,
0x15,
0xF8,
0x15,
0x82,
0x15,
0x8B,
0xD5,
0x83,
0xD5,
0xBB,
0xD5,
0x83,
0x15,
0x8B,
0x15,
0x92,
0x15,
0xA0,
0x17,
0x80,
0x1C,
0x80,
0xF0,
0xFF,
};
static constexpr Bitmap bitmap_icon_soundboard{
{16, 16},
bitmap_icon_soundboard_data};
static constexpr uint8_t bitmap_icon_speaker_data[] = {
0x00,
0x00,
+55
View File
@@ -0,0 +1,55 @@
/*
* ------------------------------------------------------------
* | Made by RocketGod |
* | Find me at https://betaskynet.com |
* | Argh matey! |
* ------------------------------------------------------------
*/
#include "ui.hpp"
#include "ui_battleship.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::battleship {
void initialize_app(ui::NavigationView& nav) {
nav.push<BattleshipView>();
}
} // namespace ui::external_app::battleship
extern "C" {
__attribute__((section(".external_app.app_battleship.application_information"), used)) application_information_t _application_information_battleship = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::battleship::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "Battleship",
/*.bitmap_data = */ {
// Pirate galleon - 16x16
0x80, 0x00, // ........#.......
0x80, 0x00, // ........#.......
0x80, 0x00, // ........#.......
0xC0, 0x01, // .......###......
0xE0, 0x03, // ......#####.....
0xF0, 0x07, // .....#######....
0xF8, 0x0F, // ....#########...
0xFC, 0x1F, // ...###########..
0xFE, 0x3F, // ..#############.
0x00, 0x01, // .......#........
0x00, 0x01, // .......#........
0x00, 0x01, // .......#........
0xFC, 0x3F, // ..############..
0xFE, 0x7F, // .##############.
0xFF, 0xFF, // ################
0xFC, 0x3F, // ..############..
},
/*.icon_color = */ ui::Color::blue().v,
/*.menu_location = */ app_location_t::GAMES,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = */ {'P', 'P', 'O', '2'}, // Use POCSAG2 baseband (larger than FSKTX)
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,250 @@
/*
* ------------------------------------------------------------
* | Made by RocketGod |
* | Find me at https://betaskynet.com |
* | Argh matey! |
* ------------------------------------------------------------
*/
#ifndef __UI_BATTLESHIP_H__
#define __UI_BATTLESHIP_H__
#include "ui.hpp"
#include "ui_navigation.hpp"
#include "ui_receiver.hpp"
#include "ui_transmitter.hpp"
#include "app_settings.hpp"
#include "radio_state.hpp"
#include "baseband_api.hpp"
#include "string_format.hpp"
#include "audio.hpp"
#include "message.hpp"
#include "pocsag.hpp"
#include <array>
namespace ui::external_app::battleship {
constexpr uint8_t GRID_SIZE = 10;
constexpr uint8_t CELL_SIZE = 24;
constexpr uint8_t GRID_OFFSET_X = 0;
constexpr uint8_t GRID_OFFSET_Y = 32;
enum class ShipType : uint8_t {
CARRIER = 5,
BATTLESHIP = 4,
CRUISER = 3,
SUBMARINE = 3,
DESTROYER = 2
};
enum class GameState : uint8_t {
MENU,
PLACING_SHIPS,
WAITING_FOR_OPPONENT,
MY_TURN,
OPPONENT_TURN,
GAME_OVER
};
enum class CellState : uint8_t {
EMPTY,
SHIP,
HIT,
MISS,
SUNK
};
enum class MessageType : uint8_t {
READY,
SHOT,
HIT,
MISS,
SUNK,
WIN
};
struct Ship {
ShipType type;
uint8_t x;
uint8_t y;
bool horizontal;
uint8_t hits;
bool placed;
uint8_t size() const {
return static_cast<uint8_t>(type);
}
bool is_sunk() const {
return hits >= size();
}
};
struct GameMessage {
MessageType type;
uint8_t x;
uint8_t y;
};
class BattleshipView : public View {
public:
BattleshipView(NavigationView& nav);
~BattleshipView();
void focus() override;
void paint(Painter& painter) override;
bool on_touch(const TouchEvent event) override;
bool on_encoder(const EncoderEvent delta) override;
bool on_key(const KeyEvent key) override;
std::string title() const override { return "Battleship"; }
private:
NavigationView& nav_;
RxRadioState rx_radio_state_{
433920000 /* frequency */,
1750000 /* bandwidth */,
2280000 /* sampling rate */
};
TxRadioState tx_radio_state_{
433920000 /* frequency */,
1750000 /* bandwidth */,
2280000 /* sampling rate */
};
// Settings
bool sound_enabled{true};
bool rf_amp_enabled{false};
uint8_t lna_gain{24};
uint8_t vga_gain{24};
uint8_t tx_gain{35};
app_settings::SettingsManager settings_{
"battleship",
app_settings::Mode::RX_TX,
{{"rx_freq"sv, &rx_frequency},
{"tx_freq"sv, &tx_frequency},
{"rf_amp"sv, &rf_amp_enabled}}};
GameState game_state{GameState::MENU};
bool is_red_team{false};
bool opponent_ready{false};
uint8_t wins{0};
uint8_t losses{0};
std::array<std::array<CellState, GRID_SIZE>, GRID_SIZE> my_grid{};
std::array<std::array<CellState, GRID_SIZE>, GRID_SIZE> enemy_grid{};
std::string current_status{"Choose your team!"};
std::string current_score{"W:0 L:0"};
std::array<Ship, 5> my_ships{};
uint8_t current_ship_index{0};
bool placing_horizontal{true};
uint8_t ships_remaining{5};
uint8_t enemy_ships_remaining{5};
uint8_t cursor_x{0};
uint8_t cursor_y{0};
uint8_t target_x{0};
uint8_t target_y{0};
bool touch_enabled{true};
uint32_t tx_frequency{433920000};
uint32_t rx_frequency{433920000};
bool is_transmitting{false};
// POCSAG decoding state
pocsag::EccContainer ecc{};
pocsag::POCSAGState pocsag_state{&ecc};
uint32_t last_address{0};
// UI Elements - Menu/Settings Screen
Text text_title{{60, 2, 120, 24}, "BATTLESHIP"};
Text text_subtitle{{40, 20, 160, 16}, "Naval Combat Game"};
Rectangle rect_radio_settings{{12, 40, 216, 118}, Color::dark_grey()};
Text label_radio{{17, 45, 100, 16}, "RADIO SETUP"};
ButtonWithEncoder button_frequency{{17, 65, 11 * 8, 20}, ""};
// Radio controls
Text label_rf_amp{{17, 90, 35, 16}, "AMP:"};
Checkbox checkbox_rf_amp{{55, 90}, 3, "", false};
Text label_lna{{17, 118, 30, 16}, "LNA:"};
NumberField field_lna{{50, 118}, 2, {0, 40}, 8, ' '};
Text label_vga{{90, 118, 30, 16}, "VGA:"};
NumberField field_vga{{125, 118}, 2, {0, 62}, 8, ' '};
Text label_tx_gain{{155, 118, 25, 16}, "TX:"};
NumberField field_tx_gain{{185, 118}, 2, {0, 47}, 8, ' '};
Rectangle rect_audio_settings{{12, 164, 216, 45}, Color::dark_grey()};
Text label_audio{{17, 169, 80, 16}, "AUDIO"};
Checkbox checkbox_sound{{17, 187}, 8, "Sound On", true};
Text label_volume{{110, 187, 50, 16}, "Volume:"};
AudioVolumeField field_volume{{165, 187}};
Rectangle rect_team_selection{{12, 217, 216, 75}, Color::dark_grey()};
Text label_team{{17, 222, 110, 16}, "SELECT TEAM"};
Button button_red_team{{25, 242, 85, 45}, "RED\nTEAM"};
Button button_blue_team{{130, 242, 85, 45}, "BLUE\nTEAM"};
// In-game UI elements
RSSI rssi{{21 * 8, 0, 6 * 8, 4}};
Text text_status{{10, 16, 220, 16}, ""};
Text text_score{{170, 16, 60, 16}, ""};
Button button_rotate{{10, 265, 65, 32}, "Rotate"};
Button button_place{{82, 265, 65, 32}, "Place"};
Button button_fire{{82, 265, 65, 32}, "Fire!"};
Button button_menu{{155, 265, 65, 32}, "Menu"};
// Methods
void init_game();
void reset_game();
void start_team(bool red);
void setup_ships();
void draw_menu_screen(Painter& painter);
void draw_grid(Painter& painter, uint8_t grid_x, uint8_t grid_y, const std::array<std::array<CellState, GRID_SIZE>, GRID_SIZE>& grid, bool show_ships, bool is_offense_grid = false);
void draw_cell(Painter& painter, uint8_t cell_x, uint8_t cell_y, CellState state, bool show_ships, bool is_offense_grid, bool is_cursor);
void draw_ship_preview(Painter& painter);
void place_ship();
bool can_place_ship(uint8_t x, uint8_t y, uint8_t size, bool horizontal);
void fire_at_position();
void process_shot(uint8_t x, uint8_t y);
void update_score();
bool is_cursor_at(uint8_t x, uint8_t y, bool is_offense_grid);
void send_message(const GameMessage& msg);
void process_message(const std::string& message);
bool parse_coords(const std::string& coords, uint8_t& x, uint8_t& y);
void mark_ship_sunk(uint8_t x, uint8_t y, std::array<std::array<CellState, GRID_SIZE>, GRID_SIZE>& grid);
void configure_radio_tx();
void configure_radio_rx();
MessageHandlerRegistration message_handler_packet{
Message::ID::POCSAGPacket,
[this](Message* const p) {
const auto message = static_cast<const POCSAGPacketMessage*>(p);
on_pocsag_packet(message);
}};
MessageHandlerRegistration message_handler_tx_progress{
Message::ID::TXProgress,
[this](const Message* const p) {
const auto message = static_cast<const TXProgressMessage*>(p);
on_tx_progress(message->progress, message->done);
}};
void on_pocsag_packet(const POCSAGPacketMessage* message);
void on_tx_progress(const uint32_t progress, const bool done);
};
} // namespace ui::external_app::battleship
#endif
+83
View File
@@ -0,0 +1,83 @@
/*
* Copyright (C) 2024 EPIRB Decoder Implementation
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ui.hpp"
#include "ui_epirb_rx.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::epirb_rx {
void initialize_app(ui::NavigationView& nav) {
nav.push<EPIRBAppView>();
}
} // namespace ui::external_app::epirb_rx
extern "C" {
__attribute__((section(".external_app.app_epirb_rx.application_information"), used)) application_information_t _application_information_epirb_rx = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::epirb_rx::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "EPIRB RX",
/*.bitmap_data = */ {
0x00,
0x00,
0x00,
0x00,
0x7C,
0x3E,
0xFE,
0x7F,
0xFF,
0xFF,
0xF7,
0xEF,
0xE3,
0xC7,
0xE3,
0xC7,
0xE3,
0xC7,
0xE3,
0xC7,
0xF7,
0xEF,
0xFF,
0xFF,
0xFE,
0x7F,
0x7C,
0x3E,
0x00,
0x00,
0x00,
0x00,
},
/*.icon_color = */ ui::Color::red().v,
/*.menu_location = */ app_location_t::RX,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = portapack::spi_flash::image_tag_epirb_rx */ {'P', 'E', 'P', 'I'},
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
}
+515
View File
@@ -0,0 +1,515 @@
/*
* Copyright (C) 2024 EPIRB Decoder Implementation
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "baseband_api.hpp"
#include "portapack_persistent_memory.hpp"
#include "file_path.hpp"
#include "ui_epirb_rx.hpp"
using namespace portapack;
#include "rtc_time.hpp"
#include "string_format.hpp"
#include "message.hpp"
namespace ui::external_app::epirb_rx {
EPIRBBeacon EPIRBDecoder::decode_packet(const baseband::Packet& packet) {
EPIRBBeacon beacon;
if (packet.size() < 112) {
return beacon; // Invalid packet
}
// Convert packet bits to byte array for easier processing
std::array<uint8_t, 16> data{};
for (size_t i = 0; i < std::min(packet.size() / 8, data.size()); i++) {
uint8_t byte_val = 0;
for (int bit = 0; bit < 8 && (i * 8 + bit) < packet.size(); bit++) {
if (packet[i * 8 + bit]) {
byte_val |= (1 << (7 - bit));
}
}
data[i] = byte_val;
}
// Extract beacon ID (bits 26-85, 15 hex digits)
beacon.beacon_id = 0;
for (int i = 3; i < 11; i++) {
beacon.beacon_id = (beacon.beacon_id << 8) | data[i];
}
// Extract beacon type (bits 86-88)
uint8_t type_bits = (data[10] >> 5) & 0x07;
beacon.beacon_type = decode_beacon_type(type_bits);
// Extract emergency type (bits 91-94 for some beacon types)
uint8_t emergency_bits = (data[11] >> 4) & 0x0F;
beacon.emergency_type = decode_emergency_type(emergency_bits);
// Extract location if encoded (depends on beacon type and protocol)
beacon.location = decode_location(data);
// Extract country code (bits 1-10)
beacon.country_code = decode_country_code(data);
// Set timestamp
rtc::RTC datetime;
rtcGetTime(&RTCD1, &datetime);
beacon.timestamp = datetime;
return beacon;
}
EPIRBLocation EPIRBDecoder::decode_location(const std::array<uint8_t, 16>& data) {
// EPIRB location encoding varies by protocol version
// This is a simplified decoder for the most common format
// Check for location data presence (bit patterns vary)
if ((data[12] & 0x80) == 0) {
return EPIRBLocation(); // No location data
}
// Extract latitude (simplified - actual encoding is more complex)
int32_t lat_raw = ((data[12] & 0x7F) << 10) | (data[13] << 2) | ((data[14] >> 6) & 0x03);
if (lat_raw & 0x10000) lat_raw |= 0xFFFE0000; // Sign extend
float latitude = lat_raw * (180.0f / 131072.0f);
// Extract longitude (simplified - actual encoding is more complex)
int32_t lon_raw = ((data[14] & 0x3F) << 12) | (data[15] << 4) | ((data[0] >> 4) & 0x0F);
if (lon_raw & 0x20000) lon_raw |= 0xFFFC0000; // Sign extend
float longitude = lon_raw * (360.0f / 262144.0f);
// Validate coordinates
if (latitude < -90.0f || latitude > 90.0f || longitude < -180.0f || longitude > 180.0f) {
return EPIRBLocation(); // Invalid coordinates
}
return EPIRBLocation(latitude, longitude);
}
BeaconType EPIRBDecoder::decode_beacon_type(uint8_t type_bits) {
switch (type_bits) {
case 0:
return BeaconType::OrbitingLocationBeacon;
case 1:
return BeaconType::PersonalLocatorBeacon;
case 2:
return BeaconType::EmergencyLocatorTransmitter;
case 3:
return BeaconType::SerialELT;
case 4:
return BeaconType::NationalELT;
default:
return BeaconType::Other;
}
}
EmergencyType EPIRBDecoder::decode_emergency_type(uint8_t emergency_bits) {
switch (emergency_bits) {
case 0:
return EmergencyType::Fire;
case 1:
return EmergencyType::Flooding;
case 2:
return EmergencyType::Collision;
case 3:
return EmergencyType::Grounding;
case 4:
return EmergencyType::Sinking;
case 5:
return EmergencyType::Disabled;
case 6:
return EmergencyType::Abandoning;
case 7:
return EmergencyType::Piracy;
case 8:
return EmergencyType::Man_Overboard;
default:
return EmergencyType::Other;
}
}
uint32_t EPIRBDecoder::decode_country_code(const std::array<uint8_t, 16>& data) {
// Country code is in bits 1-10 (ITU country code)
return ((data[0] & 0x03) << 8) | data[1];
}
std::string EPIRBDecoder::decode_vessel_name(const std::array<uint8_t, 16>& /* data */) {
// Vessel name extraction depends on beacon type and protocol
// This is a placeholder - actual implementation would be more complex
return "";
}
void EPIRBLogger::on_packet(const EPIRBBeacon& beacon) {
std::string entry = "EPIRB," +
to_string_dec_uint(beacon.beacon_id, 15, '0') + "," +
to_string_dec_uint(static_cast<uint8_t>(beacon.beacon_type)) + "," +
to_string_dec_uint(static_cast<uint8_t>(beacon.emergency_type)) + ",";
if (beacon.location.valid) {
entry += to_string_decimal(beacon.location.latitude, 6) + "," +
to_string_decimal(beacon.location.longitude, 6);
} else {
entry += ",";
}
entry += "," + to_string_dec_uint(beacon.country_code) + "\n";
log_file.write_entry(beacon.timestamp, entry);
}
std::string format_beacon_type(BeaconType type) {
switch (type) {
case BeaconType::OrbitingLocationBeacon:
return "OLB";
case BeaconType::PersonalLocatorBeacon:
return "PLB";
case BeaconType::EmergencyLocatorTransmitter:
return "ELT";
case BeaconType::SerialELT:
return "S-ELT";
case BeaconType::NationalELT:
return "N-ELT";
default:
return "Other";
}
}
std::string format_emergency_type(EmergencyType type) {
switch (type) {
case EmergencyType::Fire:
return "Fire";
case EmergencyType::Flooding:
return "Flooding";
case EmergencyType::Collision:
return "Collision";
case EmergencyType::Grounding:
return "Grounding";
case EmergencyType::Sinking:
return "Sinking";
case EmergencyType::Disabled:
return "Disabled";
case EmergencyType::Abandoning:
return "Abandoning";
case EmergencyType::Piracy:
return "Piracy";
case EmergencyType::Man_Overboard:
return "MOB";
default:
return "Other";
}
}
EPIRBBeaconDetailView::EPIRBBeaconDetailView(ui::NavigationView& nav) {
add_children({&button_done,
&button_see_map});
button_done.on_select = [this](Button&) {
if (on_close) on_close();
};
button_see_map.on_select = [this, &nav](Button&) {
if (beacon_.location.valid) {
nav.push<GeoMapView>(
to_string_hex(beacon_.beacon_id, 8), // tag as string
0, // altitude
GeoPos::alt_unit::METERS,
GeoPos::spd_unit::NONE,
beacon_.location.latitude,
beacon_.location.longitude,
0, // angle
[this]() {
if (on_close) on_close();
});
}
};
}
void EPIRBBeaconDetailView::set_beacon(const EPIRBBeacon& beacon) {
beacon_ = beacon;
set_dirty();
}
void EPIRBBeaconDetailView::focus() {
button_see_map.focus();
}
void EPIRBBeaconDetailView::paint(ui::Painter& painter) {
View::paint(painter);
const auto rect = screen_rect();
const auto s = style();
auto draw_cursor = rect.location();
draw_cursor += {8, 8};
draw_cursor = draw_field(painter, {draw_cursor, {200, 16}}, s,
"Beacon ID", to_string_hex(beacon_.beacon_id, 15))
.location();
draw_cursor = draw_field(painter, {draw_cursor, {200, 16}}, s,
"Type", format_beacon_type(beacon_.beacon_type))
.location();
draw_cursor = draw_field(painter, {draw_cursor, {200, 16}}, s,
"Emergency", format_emergency_type(beacon_.emergency_type))
.location();
if (beacon_.location.valid) {
draw_cursor = draw_field(painter, {draw_cursor, {200, 16}}, s,
"Latitude", to_string_decimal(beacon_.location.latitude, 6) + "°")
.location();
draw_cursor = draw_field(painter, {draw_cursor, {200, 16}}, s,
"Longitude", to_string_decimal(beacon_.location.longitude, 6) + "°")
.location();
} else {
draw_cursor = draw_field(painter, {draw_cursor, {200, 16}}, s,
"Location", "Unknown")
.location();
}
draw_cursor = draw_field(painter, {draw_cursor, {200, 16}}, s,
"Country", to_string_dec_uint(beacon_.country_code))
.location();
draw_cursor = draw_field(painter, {draw_cursor, {200, 16}}, s,
"Time", to_string_datetime(beacon_.timestamp, HMS))
.location();
}
ui::Rect EPIRBBeaconDetailView::draw_field(
ui::Painter& painter,
const ui::Rect& draw_rect,
const ui::Style& style,
const std::string& label,
const std::string& value) {
const auto label_width = 8 * 8;
painter.draw_string({draw_rect.location()}, style, label + ":");
painter.draw_string({draw_rect.location() + ui::Point{label_width, 0}}, style, value);
return {draw_rect.location() + ui::Point{0, draw_rect.height()}, draw_rect.size()};
}
EPIRBAppView::EPIRBAppView(ui::NavigationView& nav)
: nav_(nav) {
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
add_children({&label_frequency,
&field_rf_amp,
&field_lna,
&field_vga,
&rssi,
&field_volume,
&channel,
&label_status,
&label_beacons_count,
&label_latest,
&text_latest_info,
&console,
&button_map,
&button_clear,
&button_log});
button_map.on_select = [this](Button&) {
this->on_show_map();
};
button_clear.on_select = [this](Button&) {
this->on_clear_beacons();
};
button_log.on_select = [this](Button&) {
this->on_toggle_log();
};
signal_token_tick_second = rtc_time::signal_tick_second += [this]() {
this->on_tick_second();
};
// Configure receiver for 406.028 MHz EPIRB frequency
receiver_model.set_target_frequency(406028000);
receiver_model.set_rf_amp(true);
receiver_model.set_lna(32);
receiver_model.set_vga(32);
receiver_model.set_sampling_rate(2457600);
receiver_model.enable();
logger = std::make_unique<EPIRBLogger>();
if (logger) {
logger->append(logs_dir / "epirb_rx.txt");
}
}
EPIRBAppView::~EPIRBAppView() {
rtc_time::signal_tick_second -= signal_token_tick_second;
receiver_model.disable();
baseband::shutdown();
}
void EPIRBAppView::set_parent_rect(const ui::Rect new_parent_rect) {
View::set_parent_rect(new_parent_rect);
const auto console_rect = ui::Rect{
new_parent_rect.left(),
new_parent_rect.top() + header_height,
new_parent_rect.width(),
new_parent_rect.height() - header_height - 32};
console.set_parent_rect(console_rect);
}
void EPIRBAppView::paint(ui::Painter& /* painter */) {
// Custom painting if needed
}
void EPIRBAppView::focus() {
field_rf_amp.focus();
}
void EPIRBAppView::on_packet(const baseband::Packet& packet) {
// Decode the EPIRB packet
auto beacon = EPIRBDecoder::decode_packet(packet);
if (beacon.beacon_id != 0) { // Valid beacon decoded
on_beacon_decoded(beacon);
}
}
void EPIRBAppView::on_beacon_decoded(const EPIRBBeacon& beacon) {
beacons_received++;
recent_beacons.push_back(beacon);
// Keep only last 50 beacons
if (recent_beacons.size() > 50) {
recent_beacons.erase(recent_beacons.begin());
}
// Update display
update_display();
// Log the beacon
if (logger) {
logger->on_packet(beacon);
}
// Display in console with full details
std::string beacon_info = format_beacon_summary(beacon);
if (beacon.emergency_type != EmergencyType::Other) {
beacon_info += " [" + format_emergency_type(beacon.emergency_type) + "]";
}
console.write(beacon_info + "\n");
}
void EPIRBAppView::on_show_map() {
if (!recent_beacons.empty()) {
// Find latest beacon with valid location
for (auto it = recent_beacons.rbegin(); it != recent_beacons.rend(); ++it) {
if (it->location.valid) {
// Create a GeoMapView with all beacon locations
auto map_view = nav_.push<ui::GeoMapView>(
"EPIRB", // tag
0, // altitude
ui::GeoPos::alt_unit::METERS,
ui::GeoPos::spd_unit::NONE,
it->location.latitude,
it->location.longitude,
0 // angle
);
// Add all beacons with valid locations as markers
for (const auto& beacon : recent_beacons) {
if (beacon.location.valid) {
ui::GeoMarker marker;
marker.lat = beacon.location.latitude;
marker.lon = beacon.location.longitude;
marker.angle = 0;
marker.tag = to_string_hex(beacon.beacon_id, 8) + " " +
format_beacon_type(beacon.beacon_type);
map_view->store_marker(marker);
}
}
return;
}
}
}
// No valid location found
nav_.display_modal("No Location", "No beacons with valid\nlocation data found.");
}
void EPIRBAppView::on_clear_beacons() {
recent_beacons.clear();
beacons_received = 0;
console.clear(true);
update_display();
}
void EPIRBAppView::on_toggle_log() {
// Toggle logging functionality
if (logger) {
logger.reset();
button_log.set_text("Log");
} else {
logger = std::make_unique<EPIRBLogger>();
logger->append("epirb_rx.txt");
button_log.set_text("Stop");
}
}
void EPIRBAppView::on_tick_second() {
// Update status display every second
rtc::RTC datetime;
rtcGetTime(&RTCD1, &datetime);
label_status.set("Listening... " + to_string_datetime(datetime, HM));
}
void EPIRBAppView::update_display() {
label_beacons_count.set("Beacons: " + to_string_dec_uint(beacons_received));
if (!recent_beacons.empty()) {
const auto& latest = recent_beacons.back();
text_latest_info.set(format_beacon_summary(latest));
}
}
std::string EPIRBAppView::format_beacon_summary(const EPIRBBeacon& beacon) {
std::string summary = to_string_hex(beacon.beacon_id, 8) + " " +
format_beacon_type(beacon.beacon_type);
if (beacon.location.valid) {
summary += " " + format_location(beacon.location);
}
return summary;
}
std::string EPIRBAppView::format_location(const EPIRBLocation& location) {
return to_string_decimal(location.latitude, 4) + "°," +
to_string_decimal(location.longitude, 4) + "°";
}
} // namespace ui::external_app::epirb_rx
+263
View File
@@ -0,0 +1,263 @@
/*
* Copyright (C) 2024 EPIRB Decoder Implementation
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#ifndef __UI_EPIRB_RX_H__
#define __UI_EPIRB_RX_H__
#include "app_settings.hpp"
#include "radio_state.hpp"
#include "ui_widget.hpp"
#include "ui_navigation.hpp"
#include "ui_receiver.hpp"
#include "ui_geomap.hpp"
#include "event_m0.hpp"
#include "signal.hpp"
#include "message.hpp"
#include "log_file.hpp"
#include "baseband_packet.hpp"
/* #include <cstdint>
#include <cstddef>
#include <string>
#include <array> */
namespace ui::external_app::epirb_rx {
// EPIRB 406 MHz beacon types
enum class BeaconType : uint8_t {
OrbitingLocationBeacon = 0,
PersonalLocatorBeacon = 1,
EmergencyLocatorTransmitter = 2,
SerialELT = 3,
NationalELT = 4,
Other = 15
};
// EPIRB distress and emergency types
enum class EmergencyType : uint8_t {
Fire = 0,
Flooding = 1,
Collision = 2,
Grounding = 3,
Sinking = 4,
Disabled = 5,
Abandoning = 6,
Piracy = 7,
Man_Overboard = 8,
Other = 15
};
struct EPIRBLocation {
float latitude; // degrees, -90 to +90
float longitude; // degrees, -180 to +180
bool valid;
EPIRBLocation()
: latitude(0.0f), longitude(0.0f), valid(false) {}
EPIRBLocation(float lat, float lon)
: latitude(lat), longitude(lon), valid(true) {}
};
struct EPIRBBeacon {
uint32_t beacon_id;
BeaconType beacon_type;
EmergencyType emergency_type;
EPIRBLocation location;
uint32_t country_code;
std::string vessel_name;
rtc::RTC timestamp;
uint32_t sequence_number;
EPIRBBeacon()
: beacon_id(0), beacon_type(BeaconType::Other), emergency_type(EmergencyType::Other), location(), country_code(0), vessel_name(), timestamp(), sequence_number(0) {}
};
class EPIRBDecoder {
public:
static EPIRBBeacon decode_packet(const baseband::Packet& packet);
private:
static EPIRBLocation decode_location(const std::array<uint8_t, 16>& data);
static BeaconType decode_beacon_type(uint8_t type_bits);
static EmergencyType decode_emergency_type(uint8_t emergency_bits);
static uint32_t decode_country_code(const std::array<uint8_t, 16>& data);
static std::string decode_vessel_name(const std::array<uint8_t, 16>& data);
};
class EPIRBLogger {
public:
Optional<File::Error> append(const std::filesystem::path& filename) {
return log_file.append(filename);
}
void on_packet(const EPIRBBeacon& beacon);
private:
LogFile log_file{};
};
// Forward declarations of formatting functions
std::string format_beacon_type(BeaconType type);
std::string format_emergency_type(EmergencyType type);
class EPIRBBeaconDetailView : public ui::View {
public:
std::function<void(void)> on_close{};
EPIRBBeaconDetailView(ui::NavigationView& nav);
EPIRBBeaconDetailView(const EPIRBBeaconDetailView&) = delete;
EPIRBBeaconDetailView& operator=(const EPIRBBeaconDetailView&) = delete;
void set_beacon(const EPIRBBeacon& beacon);
const EPIRBBeacon& beacon() const { return beacon_; }
void focus() override;
void paint(ui::Painter&) override;
ui::GeoMapView* get_geomap_view() { return geomap_view; }
private:
EPIRBBeacon beacon_{};
ui::Button button_done{
{125, 224, 96, 24},
"Done"};
ui::Button button_see_map{
{19, 224, 96, 24},
"See on map"};
ui::GeoMapView* geomap_view{nullptr};
ui::Rect draw_field(
ui::Painter& painter,
const ui::Rect& draw_rect,
const ui::Style& style,
const std::string& label,
const std::string& value);
};
class EPIRBAppView : public ui::View {
public:
EPIRBAppView(ui::NavigationView& nav);
~EPIRBAppView();
void set_parent_rect(const ui::Rect new_parent_rect) override;
void paint(ui::Painter&) override;
void focus() override;
std::string title() const override { return "EPIRB RX"; }
private:
app_settings::SettingsManager settings_{
"rx_epirb", app_settings::Mode::RX};
ui::NavigationView& nav_;
std::vector<EPIRBBeacon> recent_beacons{};
std::unique_ptr<EPIRBLogger> logger{};
EPIRBBeaconDetailView beacon_detail_view{nav_};
static constexpr auto header_height = 3 * 16;
ui::Text label_frequency{
{0 * 8, 0 * 16, 10 * 8, 1 * 16},
"406.028 MHz"};
ui::RFAmpField field_rf_amp{
{13 * 8, 0 * 16}};
ui::LNAGainField field_lna{
{15 * 8, 0 * 16}};
ui::VGAGainField field_vga{
{18 * 8, 0 * 16}};
ui::RSSI rssi{
{21 * 8, 0, 6 * 8, 4}};
ui::AudioVolumeField field_volume{
{screen_width - 2 * 8, 0 * 16}};
ui::Channel channel{
{21 * 8, 5, 6 * 8, 4}};
// Status display
ui::Text label_status{
{0 * 8, 1 * 16, 15 * 8, 1 * 16},
"Listening..."};
ui::Text label_beacons_count{
{16 * 8, 1 * 16, 14 * 8, 1 * 16},
"Beacons: 0"};
// Latest beacon info display
ui::Text label_latest{
{0 * 8, 2 * 16, 8 * 8, 1 * 16},
"Latest:"};
ui::Text text_latest_info{
{8 * 8, 2 * 16, 22 * 8, 1 * 16},
""};
// Beacon list
ui::Console console{
{0, 3 * 16, 240, 168}};
ui::Button button_map{
{0, 224, 60, 24},
"Map"};
ui::Button button_clear{
{64, 224, 60, 24},
"Clear"};
ui::Button button_log{
{128, 224, 60, 24},
"Log"};
SignalToken signal_token_tick_second{};
uint32_t beacons_received = 0;
MessageHandlerRegistration message_handler_packet{
Message::ID::EPIRBPacket,
[this](Message* const p) {
const auto message = static_cast<const EPIRBPacketMessage*>(p);
this->on_packet(message->packet);
}};
void on_packet(const baseband::Packet& packet);
void on_beacon_decoded(const EPIRBBeacon& beacon);
void on_show_map();
void on_clear_beacons();
void on_toggle_log();
void on_tick_second();
void update_display();
std::string format_beacon_summary(const EPIRBBeacon& beacon);
std::string format_location(const EPIRBLocation& location);
};
} // namespace ui::external_app::epirb_rx
#endif // __UI_EPIRB_RX_H__
@@ -96,22 +96,7 @@ void ERTRecentEntry::update(const ert::Packet& packet) {
packet_type = packet.type();
}
namespace ui {
template <>
void RecentEntriesTable<ERTRecentEntries>::draw(
const Entry& entry,
const Rect& target_rect,
Painter& painter,
const Style& style) {
std::string line = ert::format::id(entry.id) + " " + ert::format::commodity_type(entry.commodity_type) + " " + ert::format::consumption(entry.last_consumption) + " ";
line += (entry.packet_type == ert::Packet::Type::SCM) ? ert::format::tamper_flags_scm(entry.last_tamper_flags) : ert::format::tamper_flags(entry.last_tamper_flags);
line += (entry.received_count > 99) ? " ++" : to_string_dec_uint(entry.received_count, 3);
line.resize(target_rect.width() / 8, ' ');
painter.draw_string(target_rect.location(), style, line);
}
namespace ui::external_app::ert_app {
ERTAppView::ERTAppView(NavigationView& nav)
: nav_{nav} {
@@ -182,4 +167,21 @@ void ERTAppView::on_freqchg(int64_t freq) {
field_frequency.set_value(freq);
}
} /* namespace ui */
} /* namespace ui::external_app::ert_app */
namespace ui {
template <>
void RecentEntriesTable<ui::external_app::ert_app::ERTRecentEntries>::draw(
const Entry& entry,
const Rect& target_rect,
Painter& painter,
const Style& style) {
std::string line = ert::format::id(entry.id) + " " + ert::format::commodity_type(entry.commodity_type) + " " + ert::format::consumption(entry.last_consumption) + " ";
line += (entry.packet_type == ert::Packet::Type::SCM) ? ert::format::tamper_flags_scm(entry.last_tamper_flags) : ert::format::tamper_flags(entry.last_tamper_flags);
line += (entry.received_count > 99) ? " ++" : to_string_dec_uint(entry.received_count, 3);
line.resize(target_rect.width() / 8, ' ');
painter.draw_string(target_rect.location(), style, line);
}
} // namespace ui
@@ -104,10 +104,9 @@ class ERTLogger {
LogFile log_file{};
};
namespace ui::external_app::ert_app {
using ERTRecentEntries = RecentEntries<ERTRecentEntry>;
namespace ui {
using ERTRecentEntriesView = RecentEntriesView<ERTRecentEntries>;
class ERTAppView : public View {
@@ -188,6 +187,6 @@ class ERTAppView : public View {
void on_show_list();
};
} /* namespace ui */
} /* namespace ui::external_app::ert_app */
#endif /*__ERT_APP_H__*/
+83
View File
@@ -0,0 +1,83 @@
/*
* Copyright (C) 2023 Bernd Herzog
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ui.hpp"
#include "ert_app.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::ert_app {
void initialize_app(ui::NavigationView& nav) {
nav.push<ERTAppView>();
}
} // namespace ui::external_app::ert_app
extern "C" {
__attribute__((section(".external_app.app_ert.application_information"), used)) application_information_t _application_information_ert = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::ert_app::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "ERT Meter",
/*.bitmap_data = */ {
0x00,
0x00,
0x00,
0x0F,
0x80,
0x7F,
0xC0,
0x0F,
0xFC,
0x0F,
0xC2,
0x0F,
0x82,
0x7F,
0x01,
0x0F,
0x01,
0x00,
0x21,
0x05,
0x53,
0x09,
0x56,
0x09,
0x50,
0x05,
0x50,
0x05,
0x20,
0xAD,
0x00,
0x00,
},
/*.icon_color = */ ui::Color::green().v,
/*.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_offset = */ 0x00000000, // will be filled at compile time
};
}
+29 -8
View File
@@ -110,12 +110,13 @@ set(EXTCPPSRC
#wefax_rx
external/wefax_rx/main.cpp
external/wefax_rx/ui_wefax_rx.cpp
#noaaapt_rx
external/noaaapt_rx/main.cpp
external/noaaapt_rx/ui_noaaapt_rx.cpp
#shoppingcart_lock
external/shoppingcart_lock/main.cpp
@@ -215,19 +216,35 @@ set(EXTCPPSRC
#gfxEQ
external/gfxeq/main.cpp
external/gfxeq/ui_gfxeq.cpp
external/gfxeq/ui_gfxeq.cpp
#detector_rx
external/detector_rx/main.cpp
external/detector_rx/ui_detector_rx.cpp
external/detector_rx/ui_detector_rx.cpp
#space_invaders
external/spaceinv/main.cpp
external/spaceinv/ui_spaceinv.cpp
external/spaceinv/ui_spaceinv.cpp
#blackjack
external/blackjack/main.cpp
external/blackjack/ui_blackjack.cpp
external/blackjack/ui_blackjack.cpp
#battleship
external/battleship/main.cpp
external/battleship/ui_battleship.cpp
#ert
external/ert/main.cpp
external/ert/ert_app.cpp
#epirb_rx
external/epirb_rx/main.cpp
external/epirb_rx/ui_epirb_rx.cpp
#soundboard
external/soundboard/main.cpp
external/soundboard/soundboard_app.cpp
)
set(EXTAPPLIST
@@ -256,7 +273,7 @@ set(EXTAPPLIST
morse_tx
sstvtx
random_password
#acars_rx
acars_rx
ookbrute
ook_editor
wefax_rx
@@ -286,4 +303,8 @@ set(EXTAPPLIST
detector_rx
spaceinv
blackjack
battleship
ert
epirb_rx
soundboard
)
+35 -6
View File
@@ -78,6 +78,11 @@ MEMORY
ram_external_app_dinogame (rwx) : org = 0xADE50000, len = 32k
ram_external_app_spaceinv (rwx) : org = 0xADE60000, len = 32k
ram_external_app_blackjack (rwx) : org = 0xADE70000, len = 32k
ram_external_app_battleship (rwx) : org = 0xADE80000, len = 32k
ram_external_app_ert (rwx) : org = 0xADE90000, len = 32k
ram_external_app_epirb_rx (rwx) : org = 0xADEA0000, len = 32k
ram_external_app_soundboard (rwx) : org = 0xADEB0000, len = 32k
}
SECTIONS
@@ -339,7 +344,7 @@ SECTIONS
KEEP(*(.external_app.app_stopwatch.application_information));
*(*ui*external_app*stopwatch*);
} > ram_external_app_stopwatch
.external_app_wefax_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_wefax_rx.application_information));
@@ -362,19 +367,19 @@ SECTIONS
{
KEEP(*(.external_app.app_doom.application_information));
*(*ui*external_app*doom*);
} > ram_external_app_doom
} > ram_external_app_doom
.external_app_debug_pmem : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_debug_pmem.application_information));
*(*ui*external_app*debug_pmem*);
} > ram_external_app_debug_pmem
} > ram_external_app_debug_pmem
.external_app_scanner : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_scanner.application_information));
*(*ui*external_app*scanner*);
} > ram_external_app_scanner
} > ram_external_app_scanner
.external_app_level : ALIGN(4) SUBALIGN(4)
{
@@ -406,11 +411,35 @@ SECTIONS
*(*ui*external_app*spaceinv*);
} > ram_external_app_spaceinv
.external_app_blackjack : ALIGN(4) SUBALIGN(4)
.external_app_blackjack : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_blackjack.application_information));
*(*ui*external_app*blackjack*);
} > ram_external_app_blackjack
.external_app_battleship : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_battleship.application_information));
*(*ui*external_app*battleship*);
} > ram_external_app_battleship
.external_app_ert : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_ert.application_information));
*(*ui*external_app*ert*);
} > ram_external_app_ert
.external_app_epirb_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_epirb_rx.application_information));
*(*ui*external_app*epirb_rx*);
} > ram_external_app_epirb_rx
.external_app_soundboard : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_soundboard.application_information));
*(*ui*external_app*soundboard*);
} > ram_external_app_soundboard
}
+1 -1
View File
@@ -274,7 +274,7 @@ bool JammerView::update_config() {
return true;
} else {
if (out_of_ranges)
nav_.display_modal("Error", "Jamming bandwidth too large.\nMust be less than 24MHz.");
nav_.display_modal("Error", "Jamming bandwidth too large.\nMust be 80MHz or less.");
else
nav_.display_modal("Error", "No range enabled.");
return false;
+1 -1
View File
@@ -10,7 +10,7 @@
namespace ui::external_app::shoppingcart_lock {
void initialize_app(NavigationView& nav) {
baseband::run_image(portapack::spi_flash::image_tag_audio_tx);
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
nav.push<ShoppingCartLock>();
}
} // namespace ui::external_app::shoppingcart_lock
+83
View File
@@ -0,0 +1,83 @@
/*
* Copyright (C) 2023 Bernd Herzog
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ui.hpp"
#include "soundboard_app.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::soundboard {
void initialize_app(ui::NavigationView& nav) {
nav.push<SoundBoardView>();
}
} // namespace ui::external_app::soundboard
extern "C" {
__attribute__((section(".external_app.app_soundboard.application_information"), used)) application_information_t _application_information_soundboard = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::soundboard::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "Soundbrd",
/*.bitmap_data = */ {
0xF0,
0x0F,
0x1C,
0x18,
0x17,
0x38,
0x15,
0x78,
0x15,
0xF8,
0x15,
0x82,
0x15,
0x8B,
0xD5,
0x83,
0xD5,
0xBB,
0xD5,
0x83,
0x15,
0x8B,
0x15,
0x92,
0x15,
0xA0,
0x17,
0x80,
0x1C,
0x80,
0xF0,
0xFF,
},
/*.icon_color = */ ui::Color::orange().v,
/*.menu_location = */ app_location_t::TX,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = portapack::spi_flash::image_tag_soundboard */ {'P', 'A', 'T', 'X'},
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
}
@@ -33,7 +33,7 @@
using namespace tonekey;
using namespace portapack;
namespace ui {
namespace ui::external_app::soundboard {
#define FILE_PER_PAGE 50
@@ -323,4 +323,4 @@ SoundBoardView::~SoundBoardView() {
baseband::shutdown();
}
} // namespace ui
} // namespace ui::external_app::soundboard
@@ -33,7 +33,7 @@
#include "app_settings.hpp"
#include "radio_state.hpp"
namespace ui {
namespace ui::external_app::soundboard {
class SoundBoardView : public View {
public:
@@ -178,6 +178,6 @@ class SoundBoardView : public View {
}};
};
} /* namespace ui */
} /* namespace ui::external_app::soundboard */
#endif /*__UI_SOUNDBOARD_H__*/
+1 -1
View File
@@ -262,7 +262,7 @@ void ViewWavView::on_playback_progress(const uint32_t progress) {
ViewWavView::ViewWavView(
NavigationView& nav)
: nav_(nav) {
baseband::run_image(portapack::spi_flash::image_tag_audio_tx);
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
wav_reader = std::make_unique<WAVFileReader>();
add_children({&labels,
+1 -1
View File
@@ -24,7 +24,7 @@
namespace ui {
RecentEntriesColumns::RecentEntriesColumns(
const std::initializer_list<RecentEntriesColumn> columns)
std::initializer_list<RecentEntriesColumn> columns)
: _columns{columns} {
}
+18 -5
View File
@@ -133,14 +133,27 @@ class RecentEntriesColumns {
public:
using ContainerType = std::vector<RecentEntriesColumn>;
RecentEntriesColumns(
const std::initializer_list<RecentEntriesColumn> columns);
RecentEntriesColumns(std::initializer_list<RecentEntriesColumn> columns);
ContainerType::const_iterator begin() const { return std::begin(_columns); }
ContainerType::const_iterator end() const { return std::end(_columns); }
ContainerType::iterator begin() { return _columns.begin(); }
ContainerType::iterator end() { return _columns.end(); }
ContainerType::const_iterator begin() const { return _columns.begin(); }
ContainerType::const_iterator end() const { return _columns.end(); }
void set(size_t index, const std::string& name, size_t width) {
if (index < _columns.size()) {
_columns[index] = {name, width};
}
}
const RecentEntriesColumn& at(size_t index) const {
return _columns.at(index);
}
size_t size() const { return _columns.size(); }
private:
const ContainerType _columns;
ContainerType _columns;
};
class RecentEntriesHeader : public Widget {
+1
View File
@@ -110,6 +110,7 @@ GeoPos::GeoPos(
text_alt_unit.set(altitude_unit_ ? "m" : "ft");
if (speed_unit_ == KMPH) text_speed_unit.set("kmph");
if (speed_unit_ == MPH) text_speed_unit.set("mph");
if (speed_unit_ == KNOTS) text_speed_unit.set("knots");
if (speed_unit_ == HIDDEN) {
text_speed_unit.hidden(true);
label_spd_position.hidden(true);
+2 -1
View File
@@ -81,6 +81,7 @@ class GeoPos : public View {
NONE = 0,
MPH,
KMPH,
KNOTS,
HIDDEN = 255
};
@@ -134,7 +135,7 @@ class GeoPos : public View {
{12 * 8, 0 * 16, 2 * 8, 16},
""};
Text text_speed_unit{
{25 * 8, 0 * 16, 4 * 8, 16},
{25 * 8, 0 * 16, 5 * 8, 16},
""};
NumberField field_lat_degrees{
+72 -10
View File
@@ -275,29 +275,63 @@ FrequencyKeypadView::FrequencyKeypadView(
};
button.on_select = button_fn;
button.set_parent_rect({(n % 3) * button_w,
(n / 3) * button_h + 24,
(n / 3) * button_h + 25,
button_w, button_h});
button.set_text(label);
n++;
}
add_children({&button_save,
add_children({&button_save_ghz,
&button_save_khz,
&button_save_mhz,
&button_load,
&button_close});
&button_clear,
&button_done_ghz,
&button_done_mhz,
&button_done_khz});
button_save.on_select = [this, &nav](Button&) {
nav.push<FrequencySaveView>(this->value());
button_save_mhz.on_select = [this, &nav](Button&) {
nav.push<FrequencySaveView>(this->value(FrequencyUnit::MHZ)); // pass as mhz and handle unit convert logic there, cuz idk if pass others would loss something
};
button_save_ghz.on_select = [this, &nav](Button&) {
nav.push<FrequencySaveView>(this->value(FrequencyUnit::GHZ));
};
button_save_khz.on_select = [this, &nav](Button&) {
nav.push<FrequencySaveView>(this->value(FrequencyUnit::KHZ));
};
button_load.on_select = [this, &nav](Button&) {
auto load_view = nav.push<FrequencyLoadView>();
load_view->on_frequency_loaded = [this](rf::Frequency value) {
set_value(value);
};
};
button_clear.on_select = [this](Button&) {
mhz.clear();
submhz.clear();
clear_field_if_digits_entered = true;
if (state == State::DigitSubMHz) {
state = State::DigitMHz;
}
draw_input_hint();
update_text();
};
button_close.on_select = [this, &nav](Button&) {
button_done_ghz.on_select = [this, &nav](Button&) {
if (on_changed)
on_changed(this->value());
on_changed(this->value(FrequencyUnit::GHZ));
nav.pop();
};
button_done_mhz.on_select = [this, &nav](Button&) {
if (on_changed)
on_changed(this->value(FrequencyUnit::MHZ));
nav.pop();
};
button_done_khz.on_select = [this, &nav](Button&) {
if (on_changed)
on_changed(this->value(FrequencyUnit::KHZ));
nav.pop();
};
@@ -305,11 +339,20 @@ FrequencyKeypadView::FrequencyKeypadView(
}
void FrequencyKeypadView::focus() {
button_close.focus();
button_done_mhz.focus();
}
rf::Frequency FrequencyKeypadView::value() const {
return mhz.as_int() * 1000000ULL + submhz.as_int() * submhz_base;
rf::Frequency FrequencyKeypadView::value(FrequencyUnit frequency_uni) const {
switch (frequency_uni) {
case FrequencyUnit::GHZ:
return mhz.as_int() * 1000000000ULL + submhz.as_int() * submhz_base * 1000;
case FrequencyUnit::MHZ:
return mhz.as_int() * 1000000ULL + submhz.as_int() * submhz_base;
case FrequencyUnit::KHZ:
return mhz.as_int() * 1000ULL + submhz.as_int() * submhz_base / 1000;
default:
return mhz.as_int() * 1000000ULL + submhz.as_int() * submhz_base;
}
}
void FrequencyKeypadView::set_value(const rf::Frequency new_value) {
@@ -381,6 +424,25 @@ void FrequencyKeypadView::field_toggle() {
state = State::DigitMHz;
clear_field_if_digits_entered = true;
}
draw_input_hint();
}
void FrequencyKeypadView::draw_input_hint() {
set_dirty();
}
void FrequencyKeypadView::paint(Painter& painter) {
View::paint(painter);
const bool s = state == State::DigitMHz;
painter.draw_hline(
{0, 36},
8 * 4,
s ? Color::white() : Color::black());
painter.draw_hline(
{5 * 8, 36},
8 * 4,
s ? Color::black() : Color::white());
}
void FrequencyKeypadView::update_text() {
+32 -9
View File
@@ -44,6 +44,12 @@ extern options_db_t freqman_steps;
namespace ui {
enum FrequencyUnit {
GHZ = 0,
MHZ,
KHZ,
};
class FrequencyField : public Widget {
public:
std::function<void(rf::Frequency)> on_change{};
@@ -207,8 +213,9 @@ class FrequencyKeypadView : public View {
const rf::Frequency value);
void focus() override;
void paint(Painter& painter) override;
rf::Frequency value() const;
rf::Frequency value(FrequencyUnit frequency_uni) const;
void set_value(const rf::Frequency new_value);
bool on_encoder(const EncoderEvent delta) override;
bool on_keyboard(const KeyboardEvent key) override;
@@ -220,7 +227,6 @@ class FrequencyKeypadView : public View {
static constexpr int mhz_digits = 4;
static constexpr int submhz_digits = 4;
static constexpr int mhz_mod = pow(10, mhz_digits);
static constexpr int submhz_base = pow(10, 6 - submhz_digits);
static constexpr int text_digits = mhz_digits + 1 + submhz_digits;
@@ -230,15 +236,30 @@ class FrequencyKeypadView : public View {
std::array<Button, 12> buttons{};
Button button_save{
{0, button_h * 5, 60, button_h},
"Save"};
Button button_save_ghz{
{0, 14 * 16, 80 / 2 - 1, 2 * 16},
"GHz"};
Button button_save_khz{
{0 + 40 - 1, 14 * 16, 80 / 2, 2 * 16},
"kHz"};
Button button_save_mhz{
{0, 16 * 16, 80 - 1, 3 * 16},
"Save MHz"};
Button button_load{
{60, button_h * 5, 60, button_h},
{80 + 1, 14 * 16, 80 - 2, 40},
"Load"};
Button button_close{
{128, button_h * 5, 112, button_h},
"Done"};
Button button_clear{
{80 + 1, 264, 80 - 2, 40},
"Clear"};
Button button_done_ghz{
{160 + 1, 14 * 16, 80 / 2 - 1, 2 * 16},
"GHz"};
Button button_done_khz{
{160 + 40, 14 * 16, 80 / 2 - 1, 2 * 16},
"kHz"};
Button button_done_mhz{
{160 + 1, 16 * 16, 80 - 2, 3 * 16},
"Done MHz"};
/* TODO: Template arg required in enum?! */
FieldString<mhz_digits> mhz{FieldString<4>::Justify::Right};
@@ -254,6 +275,8 @@ class FrequencyKeypadView : public View {
void on_button(Button& button);
void draw_input_hint();
void digit_add(const char c);
void digit_delete();
+4 -8
View File
@@ -70,9 +70,7 @@
#include "ble_rx_app.hpp"
#include "ble_tx_app.hpp"
#include "capture_app.hpp"
#include "ert_app.hpp"
#include "pocsag_app.hpp"
#include "soundboard_app.hpp"
#include "core_control.hpp"
#include "file.hpp"
@@ -131,7 +129,6 @@ const NavigationView::AppList NavigationView::appList = {
{"aprsrx", "APRS", RX, Color::green(), &bitmap_icon_aprs, new ViewFactory<APRSRXView>()},
{"audio", "Audio", RX, Color::green(), &bitmap_icon_speaker, new ViewFactory<AnalogAudioView>()},
{"blerx", "BLE Rx", RX, Color::green(), &bitmap_icon_btle, new ViewFactory<BLERxView>()},
{"ert", "ERT Meter", RX, Color::green(), &bitmap_icon_ert, new ViewFactory<ERTAppView>()},
{"pocsag", "POCSAG", RX, Color::green(), &bitmap_icon_pocsag, new ViewFactory<POCSAGAppView>()},
{"radiosonde", "Radiosnde", RX, Color::green(), &bitmap_icon_sonde, new ViewFactory<SondeView>()},
{"search", "Search", RX, Color::yellow(), &bitmap_icon_search, new ViewFactory<SearchView>()},
@@ -144,7 +141,6 @@ const NavigationView::AppList NavigationView::appList = {
{"ooktx", "OOK", TX, ui::Color::yellow(), &bitmap_icon_remote, new ViewFactory<EncodersView>()},
{"pocsagtx", "POCSAG TX", TX, ui::Color::green(), &bitmap_icon_pocsag, new ViewFactory<POCSAGTXView>()},
{"rdstx", "RDS", TX, ui::Color::green(), &bitmap_icon_rds, new ViewFactory<RDSView>()},
{"soundbrd", "Soundbrd", TX, ui::Color::green(), &bitmap_icon_soundboard, new ViewFactory<SoundBoardView>()},
{"touchtune", "TouchTune", TX, ui::Color::green(), &bitmap_icon_touchtunes, new ViewFactory<TouchTunesView>()},
{"signalgen", "SignalGen", TX, Color::green(), &bitmap_icon_cwgen, new ViewFactory<SigGenView>()},
/* TRX ********************************************************************/
@@ -764,7 +760,7 @@ void add_apps(NavigationView& nav, BtnGridView& grid, app_location_t loc) {
for (auto& app : NavigationView::appList) {
if (app.menuLocation == loc) {
grid.add_item({app.displayName, app.iconColor, app.icon,
[&nav, &app]() {
[&nav, &app]() {
i2cdev::I2CDevManager::set_autoscan_interval(0); //if i navigate away from any menu, turn off autoscan
nav.push_view(std::unique_ptr<View>(app.viewFactory->produce(nav))); }},
true);
@@ -791,8 +787,8 @@ void add_external_items(NavigationView& nav, app_location_t location, BtnGridVie
error_tile_pos);
} else {
std::sort(externalItems.begin(), externalItems.end(), [](const auto &a, const auto &b)
{
return a.desired_position < b.desired_position;
{
return a.desired_position < b.desired_position;
});
for (auto const& gridItem : externalItems) {
@@ -801,7 +797,7 @@ void add_external_items(NavigationView& nav, app_location_t location, BtnGridVie
} else {
grid.insert_item(gridItem, gridItem.desired_position, true);
}
}
grid.update_items();
+22 -15
View File
@@ -279,7 +279,7 @@ macro(DeclareTargets chunk_tag name)
include_directories(. ${INCDIR} ${MODE_INCDIR})
link_directories(${LLIBDIR})
target_link_libraries(${PROJECT_NAME}.elf ${LIBS})
target_link_libraries(${PROJECT_NAME}.elf -Wl,-Map=${PROJECT_NAME}.map)
target_link_libraries(${PROJECT_NAME}.elf -Wl,--print-memory-usage)
@@ -369,13 +369,6 @@ set(MODE_CPPSRC
DeclareTargets(PAMA am_audio)
### Audio transmit
set(MODE_CPPSRC
proc_audiotx.cpp
)
DeclareTargets(PATX audio_tx)
### Capture
set(MODE_CPPSRC
@@ -397,13 +390,6 @@ set(MODE_CPPSRC
)
DeclareTargets(PSON sonde)
### FSK TX
set(MODE_CPPSRC
proc_fsk.cpp
)
DeclareTargets(PFSK fsktx)
### FSK RX
set(MODE_CPPSRC
@@ -411,6 +397,12 @@ set(MODE_CPPSRC
)
DeclareTargets(PFSR fskrx)
### FSK TX
set(MODE_CPPSRC
proc_fsk.cpp
)
DeclareTargets(PFSK fsktx)
### Microphone transmit
@@ -579,6 +571,14 @@ set(MODE_CPPSRC
)
DeclareTargets(PAFR afskrx)
### EPIRB
set(MODE_CPPSRC
proc_epirb.cpp
)
DeclareTargets(PEPI epirb_rx)
### NRF RX
set(MODE_CPPSRC
@@ -683,6 +683,13 @@ set(MODE_CPPSRC
)
DeclareTargets(PNOA noaaapt_rx)
### Audio transmit
set(MODE_CPPSRC
proc_audiotx.cpp
)
DeclareTargets(PATX audio_tx)
### HackRF "factory" firmware
+58 -40
View File
@@ -30,10 +30,10 @@
#define new_way
int BTLETxProcessor::gen_sample_from_phy_bit(char* bit, char* sample, int num_bit) {
int BTLETxProcessor::gen_sample_from_phy_bit(char* bit, int8_t* sample, int num_bit) {
int num_sample = (num_bit * SAMPLE_PER_SYMBOL) + (LEN_GAUSS_FILTER * SAMPLE_PER_SYMBOL);
int8_t* tmp_phy_bit_over_sampling_int8 = (int8_t*)tmp_phy_bit_over_sampling;
memset(tmp_phy_bit_over_sampling_int8, 0, sizeof(tmp_phy_bit_over_sampling_int8));
int i, j;
@@ -242,6 +242,7 @@ void BTLETxProcessor::fill_adv_pdu_header(PKT_INFO* pkt, int txadd, int rxadd, i
bit_out[2] = 1;
bit_out[1] = 0;
bit_out[0] = 0;
rxadd = 1; // Scan response assumed to be with random address.
} else if (pkt->pkt_type == CONNECT_REQ) {
bit_out[3] = 0;
bit_out[2] = 1;
@@ -279,14 +280,12 @@ void BTLETxProcessor::fill_adv_pdu_header(PKT_INFO* pkt, int txadd, int rxadd, i
int BTLETxProcessor::calculate_sample_for_ADV(PKT_INFO* pkt) {
pkt->num_info_bit = 0;
// gen preamble and access address
const char* AA = "AA";
const char* AAValue = "D6BE898E";
pkt->num_info_bit = pkt->num_info_bit + convert_hex_to_bit((char*)AA, pkt->info_bit, 0, 1);
pkt->num_info_bit = pkt->num_info_bit + convert_hex_to_bit((char*)AAValue, pkt->info_bit + pkt->num_info_bit, 0, 4);
pkt->num_info_bit = pkt->num_info_bit + convert_hex_to_bit(AA, pkt->info_bit, 0, 1);
pkt->num_info_bit = pkt->num_info_bit + convert_hex_to_bit(AAValue, pkt->info_bit + pkt->num_info_bit, 0, 4);
// get txadd and rxadd
int txadd = 0, rxadd = 0;
// Tx assumed to be random address.
int txadd = 1, rxadd = 0;
pkt->num_info_bit = pkt->num_info_bit + 16; // 16 is header length
@@ -332,39 +331,56 @@ int BTLETxProcessor::calculate_pkt_info(PKT_INFO* pkt) {
void BTLETxProcessor::execute(const buffer_c8_t& buffer) {
int8_t re, im;
// This is called at 4M/2048 = 1953Hz
for (size_t i = 0; i < buffer.count; i++) {
if (configured) {
// This is going to loop through each sample bit and push it to the output buffer.
if (sample_count > length) {
configured = false;
sample_count = 0;
txprogress_message.done = true;
shared_memory.application_queue.push(txprogress_message);
} else {
// Real and imaginary was already calculated in gen_sample_from_phy_bit.
// It was processed from each data bit, run through a Gaussian Filter, and then ran through sin and cos table to get each IQ bit.
re = (int8_t)packets.phy_sample[sample_count++];
im = (int8_t)packets.phy_sample[sample_count++];
buffer.p[i] = {re, im};
if (progress_count >= progress_notice) {
progress_count = 0;
txprogress_message.progress++;
txprogress_message.done = false;
shared_memory.application_queue.push(txprogress_message);
} else {
progress_count++;
}
}
} else {
re = 0;
im = 0;
buffer.p[i] = {re, im};
if (!configured) {
for (size_t i = 0; i < buffer.count; i++) {
buffer.p[i] = {0, 0}; // Fill the buffer with zeros if not configured
}
return;
}
if (configured) {
switch (txprogress_message.progress) {
case 1:
case 2: {
for (size_t i = 0; i < buffer.count; i++) {
buffer.p[i] = {0, 0}; // Pre and Post pad BLE Packet.
}
} break;
case 0: {
size_t i = 0;
for (i = 0; (i < buffer.count) && (sample_count < length); i++) {
re = packets.phy_sample[sample_count++];
im = packets.phy_sample[sample_count++];
buffer.p[i] = {re, im};
}
if (sample_count >= length && i < buffer.count) {
// Fill the rest of the buffer with zeros if we reach the end of the packet
for (; i < buffer.count; i++) {
buffer.p[i] = {0, 0};
}
}
} break;
case 3: {
for (size_t i = 0; i < buffer.count; i++) {
buffer.p[i] = {0, 0}; // Pre and Post pad BLE Packet.
}
if (sample_count >= length) {
sample_count = 0;
configured = false;
doneSending = true;
txprogress_message.done = true;
shared_memory.application_queue.push(txprogress_message);
}
} break;
}
txprogress_message.progress++;
}
}
@@ -397,11 +413,13 @@ void BTLETxProcessor::configure(const BTLETxConfigureMessage& message) {
// Starting at sample_count 0 since packets.num_phy_sample contains every sample needed to be sent out.
sample_count = 0;
progress_count = 0;
repeatCount = 0;
progress_notice = 64;
txprogress_message.progress = 0;
txprogress_message.done = false;
configured = true;
doneSending = false;
}
int main() {
+8 -10
View File
@@ -80,18 +80,11 @@ class BTLETxProcessor : public BasebandProcessor {
int num_info_bit;
char info_bit[MAX_NUM_PHY_BYTE * 8]; // without CRC and whitening
int num_info_byte;
uint8_t info_byte[MAX_NUM_PHY_BYTE];
int num_phy_bit;
char phy_bit[MAX_NUM_PHY_BYTE * 8]; // all bits which will be fed to GFSK modulator
int num_phy_byte;
uint8_t phy_byte[MAX_NUM_PHY_BYTE];
int num_phy_sample;
char phy_sample[2 * MAX_NUM_PHY_SAMPLE]; // GFSK output to D/A (hackrf board)
int8_t phy_sample1[2 * MAX_NUM_PHY_SAMPLE]; // GFSK output to D/A (hackrf board)
int8_t phy_sample[2 * MAX_NUM_PHY_SAMPLE]; // GFSK output to D/A (hackrf board)
int space; // how many millisecond null signal shouwl be padded after this packet
};
@@ -108,22 +101,27 @@ class BTLETxProcessor : public BasebandProcessor {
void crc24(char* bit_in, int num_bit, char* init_hex, char* crc_result);
int convert_hex_to_bit(char* hex, char* bit, int stream_flip, int octet_limit);
void octet_hex_to_bit(char* hex, char* bit);
int gen_sample_from_phy_bit(char* bit, char* sample, int num_bit);
int gen_sample_from_phy_bit(char* bit, int8_t* sample, int num_bit);
bool configured = false;
float tmp_phy_bit_over_sampling[MAX_NUM_PHY_SAMPLE + 2 * LEN_GAUSS_FILTER * SAMPLE_PER_SYMBOL];
int8_t tmp_phy_bit_over_sampling_int8[MAX_NUM_PHY_SAMPLE + 2 * LEN_GAUSS_FILTER * SAMPLE_PER_SYMBOL];
float gauss_coef[LEN_GAUSS_FILTER * SAMPLE_PER_SYMBOL] = {7.561773e-09, 1.197935e-06, 8.050684e-05, 2.326833e-03, 2.959908e-02, 1.727474e-01, 4.999195e-01, 8.249246e-01, 9.408018e-01, 8.249246e-01, 4.999195e-01, 1.727474e-01, 2.959908e-02, 2.326833e-03, 8.050684e-05, 1.197935e-06};
uint32_t samples_per_bit{4};
uint32_t channel_number{37};
char macAddress[13] = "FFFFFFFFFF";
char AA[3] = "AA";
char AAValue[9] = "D6BE898E";
char advertisementData[63] = {0};
uint32_t length{0};
uint32_t shift_zero{}, shift_one{};
uint32_t progress_notice{}, progress_count{0};
uint32_t sample_count{0};
uint32_t paddingCount{0};
uint32_t repeatCount{0};
uint32_t phase{0}, sphase{0};
bool doneSending{false};
uint8_t cur_bit{0};
uint16_t bit_pos{0};
+51 -40
View File
@@ -27,7 +27,7 @@
#include "event_m4.hpp"
float BTLERxProcessor::get_phase_diff(const complex16_t& sample0, const complex16_t& sample1) {
inline float BTLERxProcessor::get_phase_diff(const complex16_t& sample0, const complex16_t& sample1) {
// Calculate the phase difference between two samples.
float dI = sample1.real() * sample0.real() + sample1.imag() * sample0.imag();
float dQ = sample1.imag() * sample0.real() - sample1.real() * sample0.imag();
@@ -36,7 +36,7 @@ float BTLERxProcessor::get_phase_diff(const complex16_t& sample0, const complex1
return phase_diff;
}
uint32_t BTLERxProcessor::crc_init_reorder(uint32_t crc_init) {
inline uint32_t BTLERxProcessor::crc_init_reorder(uint32_t crc_init) {
int i;
uint32_t crc_init_tmp, crc_init_input, crc_init_input_tmp;
@@ -62,7 +62,7 @@ uint32_t BTLERxProcessor::crc_init_reorder(uint32_t crc_init) {
return (crc_init_tmp);
}
uint_fast32_t BTLERxProcessor::crc_update(uint_fast32_t crc, const void* data, size_t data_len) {
inline uint_fast32_t BTLERxProcessor::crc_update(uint_fast32_t crc, const void* data, size_t data_len) {
const unsigned char* d = (const unsigned char*)data;
unsigned int tbl_idx;
@@ -76,7 +76,7 @@ uint_fast32_t BTLERxProcessor::crc_update(uint_fast32_t crc, const void* data, s
return crc & 0xffffff;
}
uint_fast32_t BTLERxProcessor::crc24_byte(uint8_t* byte_in, int num_byte, uint32_t init_hex) {
inline uint_fast32_t BTLERxProcessor::crc24_byte(uint8_t* byte_in, int num_byte, uint32_t init_hex) {
uint_fast32_t crc = init_hex;
crc = crc_update(crc, byte_in, num_byte);
@@ -84,7 +84,7 @@ uint_fast32_t BTLERxProcessor::crc24_byte(uint8_t* byte_in, int num_byte, uint32
return (crc);
}
bool BTLERxProcessor::crc_check(uint8_t* tmp_byte, int body_len, uint32_t crc_init) {
inline bool BTLERxProcessor::crc_check(uint8_t* tmp_byte, int body_len, uint32_t crc_init) {
int crc24_checksum;
crc24_checksum = crc24_byte(tmp_byte, body_len, crc_init); // 0x555555 --> 0xaaaaaa. maybe because byte order
@@ -96,7 +96,7 @@ bool BTLERxProcessor::crc_check(uint8_t* tmp_byte, int body_len, uint32_t crc_in
return (crc24_checksum != checksumReceived);
}
void BTLERxProcessor::scramble_byte(uint8_t* byte_in, int num_byte, const uint8_t* scramble_table_byte, uint8_t* byte_out) {
inline void BTLERxProcessor::scramble_byte(uint8_t* byte_in, int num_byte, const uint8_t* scramble_table_byte, uint8_t* byte_out) {
int i;
for (i = 0; i < num_byte; i++) {
@@ -104,7 +104,7 @@ void BTLERxProcessor::scramble_byte(uint8_t* byte_in, int num_byte, const uint8_
}
}
int BTLERxProcessor::verify_payload_byte(int num_payload_byte, ADV_PDU_TYPE pdu_type) {
inline int BTLERxProcessor::verify_payload_byte(int num_payload_byte, ADV_PDU_TYPE pdu_type) {
// Should at least have 6 bytes for the MAC Address.
// Also ensuring that there is at least 1 byte of data.
if (num_payload_byte <= 6) {
@@ -131,26 +131,27 @@ int BTLERxProcessor::verify_payload_byte(int num_payload_byte, ADV_PDU_TYPE pdu_
return 0;
}
void BTLERxProcessor::resetOffsetTracking() {
inline void BTLERxProcessor::resetOffsetTracking() {
frequency_offset = 0.0f;
frequency_offset_estimate = 0.0f;
phase_buffer_index = 0;
memset(phase_buffer, 0, sizeof(phase_buffer));
}
void BTLERxProcessor::resetBitPacketIndex() {
inline void BTLERxProcessor::resetBitPacketIndex() {
memset(rb_buf, 0, sizeof(rb_buf));
packet_index = 0;
bit_index = 0;
}
void BTLERxProcessor::resetToDefaultState() {
inline void BTLERxProcessor::resetToDefaultState() {
parseState = Parse_State_Begin;
resetOffsetTracking();
resetBitPacketIndex();
crc_init_internal = crc_init_reorder(crc_initalVale);
}
void BTLERxProcessor::demodulateFSKBits(int num_demod_byte) {
inline void BTLERxProcessor::demodulateFSKBits(int num_demod_byte) {
for (; packet_index < num_demod_byte; packet_index++) {
for (; bit_index < 8; bit_index++) {
if (samples_eaten >= (int)dst_buffer.count) {
@@ -168,6 +169,15 @@ void BTLERxProcessor::demodulateFSKBits(int num_demod_byte) {
// phaseSum /= (SAMPLE_PER_SYMBOL);
// phaseSum -= frequency_offset;
/*
alternate method. faster, but less precise. with this, you need to check against this: if (samples_eaten >= (int)dst_buffer.count + SAMPLE_PER_SYMBOL) (not so good...)
int I0 = dst_buffer.p[samples_eaten].real();
int Q0 = dst_buffer.p[samples_eaten].imag();
int I1 = dst_buffer.p[samples_eaten + 1 * SAMPLE_PER_SYMBOL].real();
int Q1 = dst_buffer.p[samples_eaten + 1 * SAMPLE_PER_SYMBOL].imag();
bool bitDecision = (I0 * Q1 - I1 * Q0) > 0 ? 1 : 0;
*/
bool bitDecision = (phaseSum > 0.0f);
rb_buf[packet_index] = rb_buf[packet_index] | (bitDecision << bit_index);
@@ -178,9 +188,9 @@ void BTLERxProcessor::demodulateFSKBits(int num_demod_byte) {
}
}
void BTLERxProcessor::handleBeginState() {
inline void BTLERxProcessor::handleBeginState() {
uint32_t validAccessAddress = DEFAULT_ACCESS_ADDR;
uint32_t accesssAddress = 0;
static uint32_t accesssAddress = 0;
int hit_idx = (-1);
@@ -191,8 +201,10 @@ void BTLERxProcessor::handleBeginState() {
phaseDiff += get_phase_diff(dst_buffer.p[i + j], dst_buffer.p[i + j + 1]);
}
phase_buffer[phase_buffer_index] = phaseDiff / (SAMPLE_PER_SYMBOL);
// disabled, due to not used anywhere
/* phase_buffer[phase_buffer_index] = phaseDiff / (SAMPLE_PER_SYMBOL);
phase_buffer_index = (phase_buffer_index + 1) % ROLLING_WINDOW;
*/
bool bitDecision = (phaseDiff > 0);
@@ -200,14 +212,15 @@ void BTLERxProcessor::handleBeginState() {
int errors = __builtin_popcount(accesssAddress ^ validAccessAddress) & 0xFFFFFFFF;
if (errors <= 4) {
if (!errors) {
hit_idx = i + SAMPLE_PER_SYMBOL;
for (int k = 0; k < ROLLING_WINDOW; k++) {
// disabled, due to not used anywhere
/* for (int k = 0; k < ROLLING_WINDOW; k++) {
frequency_offset_estimate += phase_buffer[k];
}
frequency_offset = frequency_offset_estimate / ROLLING_WINDOW;
*/
break;
}
@@ -215,7 +228,7 @@ void BTLERxProcessor::handleBeginState() {
if (hit_idx == -1) {
// Process more samples.
samples_eaten = dst_buffer.count + 1;
samples_eaten = (int)dst_buffer.count + 1;
return;
}
@@ -224,7 +237,7 @@ void BTLERxProcessor::handleBeginState() {
parseState = Parse_State_PDU_Header;
}
void BTLERxProcessor::handlePDUHeaderState() {
inline void BTLERxProcessor::handlePDUHeaderState() {
if (samples_eaten > (int)dst_buffer.count) {
return;
}
@@ -232,6 +245,7 @@ void BTLERxProcessor::handlePDUHeaderState() {
demodulateFSKBits(NUM_PDU_HEADER_BYTE);
if (packet_index < NUM_PDU_HEADER_BYTE || bit_index != 0) {
resetToDefaultState();
return;
}
@@ -244,14 +258,14 @@ void BTLERxProcessor::handlePDUHeaderState() {
// Not a valid Advertise Payload.
if ((payload_len < 6) || (payload_len > 37)) {
parseState = Parse_State_Begin;
resetToDefaultState();
return;
} else {
parseState = Parse_State_PDU_Payload;
}
}
void BTLERxProcessor::handlePDUPayloadState() {
inline void BTLERxProcessor::handlePDUPayloadState() {
const int num_demod_byte = (payload_len + 3);
if (samples_eaten > (int)dst_buffer.count) {
@@ -261,6 +275,7 @@ void BTLERxProcessor::handlePDUPayloadState() {
demodulateFSKBits(num_demod_byte + NUM_PDU_HEADER_BYTE);
if (packet_index < (num_demod_byte + NUM_PDU_HEADER_BYTE) || bit_index != 0) {
resetToDefaultState();
return;
}
@@ -316,26 +331,24 @@ void BTLERxProcessor::handlePDUPayloadState() {
void BTLERxProcessor::execute(const buffer_c8_t& buffer) {
if (!configured) return;
// a less computationally expensive method
max_dB = -128;
real = -128;
imag = -128;
auto* ptr = buffer.p;
auto* end = &buffer.p[buffer.count];
while (ptr < end) {
float dbm = mag2_to_dbm_8bit_normalized(ptr->real(), ptr->imag(), 1.0f, 50.0f);
if (dbm > max_dB) {
max_dB = dbm;
real = ptr->real();
imag = ptr->imag();
uint32_t max_squared = 0;
int8_t imag = 0;
int8_t real = 0;
void* src_p = buffer.p;
while (src_p < &buffer.p[buffer.count]) {
const uint32_t sample = *__SIMD32(src_p)++;
const uint32_t mag_sq = __SMUAD(sample, sample);
if (mag_sq > max_squared) {
max_squared = mag_sq;
imag = ((complex8_t*)src_p)->imag();
real = ((complex8_t*)src_p)->real();
}
ptr++;
}
max_dB = mag2_to_dbm_8bit_normalized(real, imag, 1.0f, 50.0f);
// 4Mhz 2048 samples
// Decimated by 4 to achieve 2048/4 = 512 samples at 1 sample per symbol.
decim_0.execute(buffer, dst_buffer);
@@ -366,11 +379,9 @@ void BTLERxProcessor::on_message(const Message* const message) {
void BTLERxProcessor::configure(const BTLERxConfigureMessage& message) {
channel_number = message.channel_number;
decim_0.configure(taps_BTLE_2M_PHY_decim_0.taps);
decim_0.configure(taps_BTLE_Dual_PHY.taps);
configured = true;
crc_init_internal = crc_init_reorder(crc_initalVale);
}
int main() {
-2
View File
@@ -133,8 +133,6 @@ class BTLERxProcessor : public BasebandProcessor {
uint8_t payload_len{0};
uint8_t pdu_type{0};
int32_t max_dB{0};
int8_t real{0};
int8_t imag{0};
uint16_t packet_index{0};
uint8_t bit_index{0};
+95
View File
@@ -0,0 +1,95 @@
/*
* Copyright (C) 2024 EPIRB Receiver Implementation
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "proc_epirb.hpp"
#include "portapack_shared_memory.hpp"
#include "dsp_fir_taps.hpp"
#include "event_m4.hpp"
#include <ch.h>
EPIRBProcessor::EPIRBProcessor() {
// Configure the decimation filters for narrowband EPIRB signal
// Target: Reduce 2.457600 MHz to ~38.4 kHz for 400 bps processing
decim_0.configure(taps_11k0_decim_0.taps);
decim_1.configure(taps_11k0_decim_1.taps);
baseband_thread.start();
}
void EPIRBProcessor::execute(const buffer_c8_t& buffer) {
/* 2.4576MHz, 2048 samples */
// First decimation stage: 2.4576 MHz -> 307.2 kHz
const auto decim_0_out = decim_0.execute(buffer, dst_buffer);
// Second decimation stage: 307.2 kHz -> 38.4 kHz
const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer);
const auto decimator_out = decim_1_out;
/* 38.4kHz, 32 samples (approximately) */
feed_channel_stats(decimator_out);
// Process each decimated sample through the matched filter
for (size_t i = 0; i < decimator_out.count; i++) {
// Apply matched filter for BPSK demodulation
if (mf.execute_once(decimator_out.p[i])) {
// Feed symbol to clock recovery when matched filter triggers
clock_recovery(mf.get_output());
}
}
}
void EPIRBProcessor::consume_symbol(const float raw_symbol) {
// BPSK demodulation: positive = 1, negative = 0
const uint_fast8_t sliced_symbol = (raw_symbol >= 0.0f) ? 1 : 0;
// Decode bi-phase L encoding manually
// In bi-phase L: 0 = no transition, 1 = transition
// This is a simple edge detector
const auto decoded_symbol = sliced_symbol ^ last_symbol;
last_symbol = sliced_symbol;
// Build packet from decoded symbols
packet_builder.execute(decoded_symbol);
}
void EPIRBProcessor::payload_handler(const baseband::Packet& packet) {
// EPIRB packet received - validate and process
if (packet.size() >= 112) { // Minimum EPIRB data payload size (112 bits)
packets_received++;
last_packet_timestamp = Timestamp::now();
// Create and send EPIRB packet message to application layer
const EPIRBPacketMessage message{packet};
shared_memory.application_queue.push(message);
}
}
void EPIRBProcessor::on_message(const Message* const message) {
}
int main() {
EventDispatcher event_dispatcher{std::make_unique<EPIRBProcessor>()};
event_dispatcher.run();
return 0;
}
+135
View File
@@ -0,0 +1,135 @@
/*
* Copyright (C) 2024 EPIRB Receiver Implementation
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#ifndef __PROC_EPIRB_H__
#define __PROC_EPIRB_H__
#include <cstdint>
#include <cstddef>
#include <array>
#include <complex>
#include "baseband_processor.hpp"
#include "baseband_thread.hpp"
#include "rssi_thread.hpp"
#include "channel_decimator.hpp"
#include "matched_filter.hpp"
#include "clock_recovery.hpp"
#include "symbol_coding.hpp"
#include "packet_builder.hpp"
#include "baseband_packet.hpp"
#include "message.hpp"
#include "buffer.hpp"
// Forward declarations for types only used as pointers/references
class Message;
namespace baseband {
class Packet;
}
// EPIRB 406 MHz Emergency Position Indicating Radio Beacon
// Signal characteristics:
// - Frequency: 406.025 - 406.028 MHz (typically 406.028 MHz)
// - Modulation: BPSK (Binary Phase Shift Keying)
// - Data rate: 400 bps
// - Encoding: Bi-phase L (Manchester)
// - Transmission: Every 50 seconds ± 2.5 seconds
// - Power: 5W ± 2dB
// - Message length: 144 bits (including sync pattern)
// Matched filter for BPSK demodulation at 400 bps
// Using raised cosine filter taps optimized for 400 bps BPSK
static constexpr std::array<std::complex<float>, 64> bpsk_taps = {{// Raised cosine filter coefficients for BPSK 400 bps
-5, -8, -12, -15, -17, -17, -15, -11,
-5, 2, 11, 20, 29, 37, 43, 47,
48, 46, 42, 35, 26, 16, 4, -8,
-21, -33, -44, -53, -59, -62, -62, -58,
-51, -41, -28, -13, 3, 19, 36, 51,
64, 74, 80, 82, 80, 74, 64, 51,
36, 19, 3, -13, -28, -41, -51, -58,
-62, -62, -59, -53, -44, -33, -21, -8}};
class EPIRBProcessor : public BasebandProcessor {
public:
EPIRBProcessor();
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const message) override;
private:
// EPIRB operates at 406 MHz with narrow bandwidth
static constexpr size_t baseband_fs = 2457600;
static constexpr uint32_t epirb_center_freq = 406028000; // 406.028 MHz
static constexpr uint32_t symbol_rate = 400; // 400 bps
static constexpr size_t decimation_factor = 64; // Decimate to ~38.4kHz
std::array<complex16_t, 512> dst{};
const buffer_c16_t dst_buffer{
dst.data(),
dst.size()};
// Decimation chain for 406 MHz EPIRB signal processing
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{};
dsp::decimate::FIRC16xR16x32Decim8 decim_1{};
dsp::matched_filter::MatchedFilter mf{bpsk_taps, 2};
// Clock recovery for 400 bps symbol rate
// Sampling rate after decimation: ~38.4kHz
// Symbols per sample: 38400 / 400 = 96 samples per symbol
clock_recovery::ClockRecovery<clock_recovery::FixedErrorFilter> clock_recovery{
38400, // sampling_rate
400, // symbol_rate (400 bps)
{0.0555f}, // error_filter coefficient
[this](const float symbol) { this->consume_symbol(symbol); }};
// Simple bi-phase L decoder state
uint_fast8_t last_symbol = 0;
// EPIRB packet structure:
// - Sync pattern: 000101010101... (15 bits)
// - Frame sync: 0111110 (7 bits)
// - Data: 112 bits
// - BCH error correction: 10 bits
// Total: 144 bits
PacketBuilder<BitPattern, BitPattern, BitPattern> packet_builder{
{0b010101010101010, 15, 1}, // Preamble pattern
{0b0111110, 7}, // Frame sync pattern
{0b0111110, 7}, // End pattern (same as sync for simplicity)
[this](const baseband::Packet& packet) {
this->payload_handler(packet);
}};
void consume_symbol(const float symbol);
void payload_handler(const baseband::Packet& packet);
// Statistics
uint32_t packets_received = 0;
Timestamp last_packet_timestamp{};
/* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{
baseband_fs, this, baseband::Direction::Receive, /*auto_start*/ false};
RSSIThread rssi_thread{};
};
#endif /*__PROC_EPIRB_H__*/
+246 -166
View File
@@ -24,6 +24,7 @@
*/
#include "proc_fsk_rx.hpp"
#include "dsp_decimate.hpp"
#include "event_m4.hpp"
@@ -32,135 +33,253 @@
#include <cstdint>
#include <cstddef>
using namespace std;
using namespace dsp::decimate;
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
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;
float FSKRxProcessor::detect_peak_power(const buffer_c8_t& buffer, int N) {
int32_t power = 0;
// Initial window power
for (int i = 0; i < N; i++) {
int16_t i_sample = buffer.p[i].real();
int16_t q_sample = buffer.p[i].imag();
power += i_sample * i_sample + q_sample * q_sample;
}
return count;
}
} // namespace
power = power / N;
/* AudioNormalizer ***************************************/
// Convert to dB over noise floor
float power_db = 10.0f * log10f((float)power / noise_floor);
void AudioNormalizer::execute_in_place(const buffer_f32_t& audio) {
// Decay min/max every second (@24kHz).
if (counter_ >= 24'000) {
// 90% decay factor seems to work well.
// This keeps large transients from wrecking the filter.
max_ *= 0.9f;
min_ *= 0.9f;
counter_ = 0;
calculate_thresholds();
}
// If too weak, treat as no signal
if (power_db <= 0.0f) return 0;
counter_ += audio.count;
for (size_t i = 0; i < audio.count; ++i) {
auto& val = audio.p[i];
if (val > max_) {
max_ = val;
calculate_thresholds();
}
if (val < min_) {
min_ = val;
calculate_thresholds();
}
if (val >= t_hi_)
val = 1.0f;
else if (val <= t_lo_)
val = -1.0f;
else
val = 0.0;
}
return power_db;
}
void AudioNormalizer::calculate_thresholds() {
auto center = (max_ + min_) / 2.0f;
auto range = (max_ - min_) / 2.0f;
void FSKRxProcessor::agc_correct_iq(const buffer_c8_t& buffer, int N, float measured_power) {
float power_db = 10.0f * log10f(measured_power / noise_floor);
float error_db = target_power_db - power_db;
// 10% off center force either +/-1.0f.
// Higher == larger dead zone.
// Lower == more false positives.
auto threshold = range * 0.1;
t_hi_ = center + threshold;
t_lo_ = center - threshold;
}
/* FSKRxProcessor ******************************************/
void FSKRxProcessor::clear_data_bits() {
data = 0;
bit_count = 0;
}
void FSKRxProcessor::handle_sync(bool inverted) {
clear_data_bits();
has_sync_ = true;
inverted = inverted;
word_count = 0;
}
void FSKRxProcessor::process_bits(const buffer_c8_t& buffer) {
// Process all of the bits in the bits queue.
while (buffer.count > 0) {
// Wait until data_ is full.
if (bit_count < data_bit_count)
continue;
// Wait for the sync frame.
if (!has_sync_) {
if (diff_bit_count(data, sync_codeword) <= 2)
handle_sync(/*inverted=*/false);
else if (diff_bit_count(data, ~sync_codeword) <= 2)
handle_sync(/*inverted=*/true);
continue;
}
}
}
/* FSKRxProcessor ***************************************/
FSKRxProcessor::FSKRxProcessor() {
}
void FSKRxProcessor::execute(const buffer_c8_t& buffer) {
if (!configured) {
if (error_db <= 0) {
return;
}
// Decimate by current decim 0 and decim 1.
float gain_scalar = powf(10.0f, error_db / 20.0f);
for (int i = 0; i < N; i++) {
buffer.p[i] = {(int8_t)(buffer.p[i].real() * gain_scalar), (int8_t)(buffer.p[i].imag() * gain_scalar)};
}
}
float FSKRxProcessor::get_phase_diff(const complex16_t& sample0, const complex16_t& sample1) {
// Calculate the phase difference between two samples.
float dI = sample1.real() * sample0.real() + sample1.imag() * sample0.imag();
float dQ = sample1.imag() * sample0.real() - sample1.real() * sample0.imag();
float phase_diff = atan2f(dQ, dI);
return phase_diff;
}
void FSKRxProcessor::demodulateFSKBits(const buffer_c16_t& decimator_out, int num_demod_byte) {
for (; packet_index < num_demod_byte; packet_index++) {
for (; bit_index < 8; bit_index++) {
if (samples_eaten >= (int)decimator_out.count) {
return;
}
float phaseSum = 0.0f;
for (int k = 0; k < SAMPLE_PER_SYMBOL - 1; ++k) {
float phase = get_phase_diff(
decimator_out.p[samples_eaten + k],
decimator_out.p[samples_eaten + k + 1]);
phaseSum += phase;
}
phaseSum /= (SAMPLE_PER_SYMBOL - 1);
phaseSum -= frequency_offset;
bool bitDecision = (phaseSum > 0.0f);
rb_buf[packet_index] |= (bitDecision << (7 - bit_index));
samples_eaten += SAMPLE_PER_SYMBOL;
}
bit_index = 0;
}
}
void FSKRxProcessor::resetPreambleTracking() {
frequency_offset = 0.0f;
frequency_offset_estimate = 0.0f;
phase_buffer_index = 0;
memset(phase_buffer, 0, sizeof(phase_buffer));
}
void FSKRxProcessor::resetBitPacketIndex() {
packet_index = 0;
bit_index = 0;
}
void FSKRxProcessor::resetToDefaultState() {
parseState = Parse_State_Wait_For_Peak;
peak_timeout = 0;
fskPacketData.power = 0.0f;
resetPreambleTracking();
resetBitPacketIndex();
}
void FSKRxProcessor::handlePreambleState(const buffer_c16_t& decimator_out) {
const uint32_t validPreamble = DEFAULT_PREAMBLE;
static uint32_t preambleValue = 0;
int hit_idx = -1;
for (; samples_eaten < (int)decimator_out.count; samples_eaten += SAMPLE_PER_SYMBOL) {
float phaseSum = 0.0f;
for (int j = 0; j < SAMPLE_PER_SYMBOL - 1; j++) {
phaseSum += get_phase_diff(decimator_out.p[samples_eaten + j], decimator_out.p[samples_eaten + j + 1]);
}
phase_buffer[phase_buffer_index] = phaseSum / (SAMPLE_PER_SYMBOL - 1);
phase_buffer_index = (phase_buffer_index + 1) % ROLLING_WINDOW;
bool bitDecision = (phaseSum > 0.0f);
preambleValue = (preambleValue << 1) | bitDecision;
int errors = __builtin_popcountl(preambleValue ^ validPreamble) & 0xFFFFFFFF;
if (errors == 0) {
hit_idx = samples_eaten + SAMPLE_PER_SYMBOL;
fskPacketData.syncWord = preambleValue;
fskPacketData.max_dB = max_dB;
for (int k = 0; k < ROLLING_WINDOW; k++) {
frequency_offset_estimate += phase_buffer[k];
}
frequency_offset = frequency_offset_estimate / ROLLING_WINDOW;
fskPacketData.frequency_offset_hz = (frequency_offset * demod_input_fs) / (2.0f * M_PI);
preambleValue = 0;
break;
}
}
if (hit_idx == -1) {
samples_eaten = samples_eaten;
return;
}
samples_eaten = hit_idx;
parseState = Parse_State_Sync;
}
void FSKRxProcessor::handleSyncWordState(const buffer_c16_t& decimator_out) {
const int syncword_bytes = 4;
const uint32_t validSyncWord = DEFAULT_SYNC_WORD;
if ((int)decimator_out.count - samples_eaten <= 0) {
return;
}
demodulateFSKBits(decimator_out, syncword_bytes);
if (packet_index < syncword_bytes || bit_index != 0) {
return;
}
uint32_t receivedSyncWord = (rb_buf[0] << 24) | (rb_buf[1] << 16) | (rb_buf[2] << 8) | rb_buf[3];
int errors = __builtin_popcountl(receivedSyncWord ^ validSyncWord) & 0xFFFFFFFF;
if (errors <= 3) {
fskPacketData.syncWord = receivedSyncWord;
parseState = Parse_State_PDU_Payload;
memset(fskPacketData.data, 0, sizeof(fskPacketData.data));
} else {
resetToDefaultState();
}
memset(rb_buf, 0, sizeof(rb_buf));
resetBitPacketIndex();
}
void FSKRxProcessor::handlePDUPayloadState(const buffer_c16_t& decimator_out) {
if ((int)decimator_out.count - samples_eaten <= 0) {
return;
}
demodulateFSKBits(decimator_out, NUM_DATA_BYTE);
if (packet_index < NUM_DATA_BYTE || bit_index != 0) {
return;
}
fskPacketData.dataLen = NUM_DATA_BYTE;
// Copy the decoded bits to the packet data
for (int i = 0; i < NUM_DATA_BYTE; i++) {
fskPacketData.data[i] |= rb_buf[i];
}
FSKRxPacketMessage data_message{&fskPacketData};
shared_memory.application_queue.push(data_message);
memset(rb_buf, 0, sizeof(rb_buf));
resetToDefaultState();
}
void FSKRxProcessor::execute(const buffer_c8_t& buffer) {
if (!configured || parseState == Parse_State_Parsing_Data) return;
const auto decim_0_out = decim_0.execute(buffer, dst_buffer);
const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer);
feed_channel_stats(decim_1_out);
spectrum_samples += decim_1_out.count;
samples_eaten = 0;
if (spectrum_samples >= spectrum_interval_samples) {
spectrum_samples -= spectrum_interval_samples;
channel_spectrum.feed(decim_1_out, channel_filter_low_f,
channel_filter_high_f, channel_filter_transition);
}
while ((int)decim_1_out.count - samples_eaten > 0) {
if ((parseState == Parse_State_Wait_For_Peak) || (parseState == Parse_State_Preamble)) {
float power = detect_peak_power(buffer, buffer.count);
// process_bits();
if (power) {
parseState = Parse_State_Preamble;
agc_power = power;
fskPacketData.power = power;
} else {
break;
}
}
// Update the status.
samples_processed += buffer.count;
if (agc_power) {
agc_correct_iq(buffer, buffer.count, agc_power);
}
if (samples_processed >= stat_update_threshold) {
// send_packet(data);
samples_processed -= stat_update_threshold;
if (parseState == Parse_State_Preamble) {
peak_timeout++;
// 960,000 fs / 2048 samples = 468.75 Hz, so 55 calls is about 0.053 seconds before timeout.
if (peak_timeout == 4) {
resetToDefaultState();
} else {
handlePreambleState(decim_1_out);
}
}
if (parseState == Parse_State_Sync) {
handleSyncWordState(decim_1_out);
}
if (parseState == Parse_State_PDU_Payload) {
handlePDUPayloadState(decim_1_out);
}
}
}
@@ -171,7 +290,7 @@ void FSKRxProcessor::on_message(const Message* const message) {
break;
case Message::ID::UpdateSpectrum:
case Message::ID::SpectrumStreamingConfig:
channel_spectrum.on_message(message);
// channel_spectrum.on_message(message);
break;
case Message::ID::SampleRateConfig:
@@ -179,7 +298,7 @@ void FSKRxProcessor::on_message(const Message* const message) {
break;
case Message::ID::CaptureConfig:
capture_config(*reinterpret_cast<const CaptureConfigMessage*>(message));
// capture_config(*reinterpret_cast<const CaptureConfigMessage*>(message));
break;
default:
@@ -188,83 +307,65 @@ void FSKRxProcessor::on_message(const Message* const message) {
}
void FSKRxProcessor::configure(const FSKRxConfigureMessage& message) {
// Extract message variables.
deviation = message.deviation;
channel_decimation = message.channel_decimation;
// channel_filter_taps = message.channel_filter;
channel_spectrum.set_decimation_factor(1);
}
void FSKRxProcessor::capture_config(const CaptureConfigMessage& message) {
if (message.config) {
audio_output.set_stream(std::make_unique<StreamInput>(message.config));
} else {
audio_output.set_stream(nullptr);
}
SAMPLE_PER_SYMBOL = message.samplesPerSymbol;
DEFAULT_SYNC_WORD = message.syncWord;
NUM_SYNC_WORD_BYTE = message.syncWordLength;
DEFAULT_PREAMBLE = message.preamble;
NUM_PREAMBLE_BYTE = message.preambleLength;
NUM_DATA_BYTE = message.numDataBytes;
}
void FSKRxProcessor::sample_rate_config(const SampleRateConfigMessage& message) {
const auto sample_rate = message.sample_rate;
// The actual sample rate is the requested rate * the oversample rate.
// See oversample.hpp for more details on oversampling.
baseband_fs = sample_rate * toUType(message.oversample_rate);
baseband_thread.set_sampling_rate(baseband_fs);
// TODO: Do we need to use the taps that the decimators get configured with?
channel_filter_low_f = taps_200k_decim_1.low_frequency_normalized * sample_rate;
channel_filter_high_f = taps_200k_decim_1.high_frequency_normalized * sample_rate;
channel_filter_transition = taps_200k_decim_1.transition_normalized * sample_rate;
// Compute the scalar that corrects the oversample_rate to be x8 when computing
// the spectrum update interval. The original implementation only supported x8.
// TODO: Why is this needed here but not in proc_replay? There must be some other
// assumption about x8 oversampling in some component that makes this necessary.
const auto oversample_correction = toUType(message.oversample_rate) / 8.0;
// The spectrum update interval controls how often the waterfall is fed new samples.
spectrum_interval_samples = sample_rate / (spectrum_rate_hz * oversample_correction);
spectrum_samples = 0;
// For high sample rates, the M4 is busy collecting samples so the
// waterfall runs slower. Reduce the update interval so it runs faster.
// NB: Trade off: looks nicer, but more frequent updates == more CPU.
if (sample_rate >= 1'500'000)
spectrum_interval_samples /= (sample_rate / 750'000);
switch (message.oversample_rate) {
case OversampleRate::x4:
// M4 can't handle 2 decimation passes for sample rates needing x4.
decim_0.set<FIRC8xR16x24FS4Decim4>().configure(taps_200k_decim_0.taps);
decim_0.set<dsp::decimate::FIRC8xR16x24FS4Decim4>().configure(taps_200k_decim_0.taps);
decim_1.set<NoopDecim>();
break;
case OversampleRate::x8:
// M4 can't handle 2 decimation passes for sample rates <= 600k.
if (message.sample_rate < 600'000) {
decim_0.set<FIRC8xR16x24FS4Decim4>().configure(taps_200k_decim_0.taps);
decim_1.set<FIRC16xR16x16Decim2>().configure(taps_200k_decim_1.taps);
decim_0.set<dsp::decimate::FIRC8xR16x24FS4Decim4>().configure(taps_200k_decim_0.taps);
decim_1.set<dsp::decimate::FIRC16xR16x16Decim2>().configure(taps_200k_decim_1.taps);
} else {
// Using 180k taps to provide better filtering with a single pass.
decim_0.set<FIRC8xR16x24FS4Decim8>().configure(taps_180k_wfm_decim_0.taps);
decim_0.set<dsp::decimate::FIRC8xR16x24FS4Decim8>().configure(taps_180k_wfm_decim_0.taps);
decim_1.set<NoopDecim>();
}
break;
case OversampleRate::x16:
decim_0.set<FIRC8xR16x24FS4Decim8>().configure(taps_200k_decim_0.taps);
decim_1.set<FIRC16xR16x16Decim2>().configure(taps_200k_decim_1.taps);
decim_0.set<dsp::decimate::FIRC8xR16x24FS4Decim8>().configure(taps_200k_decim_0.taps);
decim_1.set<dsp::decimate::FIRC16xR16x16Decim2>().configure(taps_200k_decim_1.taps);
break;
case OversampleRate::x32:
decim_0.set<FIRC8xR16x24FS4Decim4>().configure(taps_200k_decim_0.taps);
decim_1.set<FIRC16xR16x32Decim8>().configure(taps_16k0_decim_1.taps);
decim_0.set<dsp::decimate::FIRC8xR16x24FS4Decim4>().configure(taps_200k_decim_0.taps);
decim_1.set<dsp::decimate::FIRC16xR16x32Decim8>().configure(taps_16k0_decim_1.taps);
break;
case OversampleRate::x64:
decim_0.set<FIRC8xR16x24FS4Decim8>().configure(taps_200k_decim_0.taps);
decim_1.set<FIRC16xR16x32Decim8>().configure(taps_16k0_decim_1.taps);
decim_0.set<dsp::decimate::FIRC8xR16x24FS4Decim8>().configure(taps_200k_decim_0.taps);
decim_1.set<dsp::decimate::FIRC16xR16x32Decim8>().configure(taps_16k0_decim_1.taps);
break;
default:
@@ -282,33 +383,12 @@ void FSKRxProcessor::sample_rate_config(const SampleRateConfigMessage& message)
// size_t channel_filter_input_fs = decim_1_output_fs;
// size_t channel_filter_output_fs = channel_filter_input_fs / channel_decimation;
size_t demod_input_fs = decim_1_output_fs;
send_packet((uint32_t)demod_input_fs);
demod_input_fs = decim_1_output_fs;
// Set ready to process data.
configured = true;
}
void FSKRxProcessor::flush() {
// word_extractor.flush();
}
void FSKRxProcessor::reset() {
clear_data_bits();
has_sync_ = false;
inverted = false;
word_count = 0;
samples_processed = 0;
}
void FSKRxProcessor::send_packet(uint32_t data) {
data_message.is_data = true;
data_message.value = data;
shared_memory.application_queue.push(data_message);
}
/* main **************************************************/
int main() {
+71 -86
View File
@@ -30,7 +30,6 @@
#include "dsp_decimate.hpp"
#include "dsp_demodulate.hpp"
#include "dsp_iir_config.hpp"
#include "dsp_fir_taps.hpp"
#include "spectrum_collector.hpp"
@@ -46,21 +45,6 @@
#include <cstdint>
#include <functional>
/* Normalizes audio stream to +/-1.0f */
class AudioNormalizer {
public:
void execute_in_place(const buffer_f32_t& audio);
private:
void calculate_thresholds();
uint32_t counter_ = 0;
float min_ = 99.0f;
float max_ = -99.0f;
float t_hi_ = 1.0;
float t_lo_ = 1.0;
};
/* A decimator that just returns the source buffer. */
class NoopDecim {
public:
@@ -111,44 +95,92 @@ class MultiDecimator {
class FSKRxProcessor : public BasebandProcessor {
public:
FSKRxProcessor();
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const message) override;
private:
size_t baseband_fs = 1024000; // aka: sample_rate
static constexpr int ROLLING_WINDOW{32};
static constexpr uint16_t MAX_BUFFER_SIZE{512};
enum Parse_State {
Parse_State_Wait_For_Peak = 0,
Parse_State_Preamble,
Parse_State_Sync,
Parse_State_PDU_Payload,
Parse_State_Parsing_Data
};
size_t baseband_fs = 960000;
uint8_t stat_update_interval = 10;
uint32_t stat_update_threshold = baseband_fs / stat_update_interval;
static constexpr auto spectrum_rate_hz = 50.0f;
float detect_peak_power(const buffer_c8_t& buffer, int N);
void agc_correct_iq(const buffer_c8_t& buffer, int N, float measured_power);
float get_phase_diff(const complex16_t& sample0, const complex16_t& sample1);
void demodulateFSKBits(const buffer_c16_t& decimator_out, int num_demod_byte);
void resetPreambleTracking();
void resetBitPacketIndex();
void resetToDefaultState();
void handlePreambleState(const buffer_c16_t& decimator_out);
void handleSyncWordState(const buffer_c16_t& decimator_out);
void handlePDUPayloadState(const buffer_c16_t& decimator_out);
void configure(const FSKRxConfigureMessage& message);
void capture_config(const CaptureConfigMessage& message);
void sample_rate_config(const SampleRateConfigMessage& message);
void flush();
void reset();
void send_packet(uint32_t data);
void process_bits(const buffer_c8_t& buffer);
void clear_data_bits();
void handle_sync(bool inverted);
/* Returns true if the batch has as sync frame. */
bool has_sync() const { return has_sync_; }
/* Set once app is ready to receive messages. */
bool configured = false;
/* Buffer for decimated IQ data. */
std::array<complex16_t, 512> dst{};
std::array<complex16_t, MAX_BUFFER_SIZE> dst{};
const buffer_c16_t dst_buffer{
dst.data(),
dst.size()};
/* Buffer for demodulated audio. */
std::array<float, 16> audio{};
const buffer_f32_t audio_buffer{audio.data(), audio.size()};
uint8_t rb_buf[MAX_BUFFER_SIZE];
dsp::demodulate::FM demod{};
int rb_head{-1};
int32_t g_threshold{0};
uint8_t channel_number{0};
uint16_t process = 0;
bool configured{false};
FskPacketData fskPacketData{};
Parse_State parseState{Parse_State_Wait_For_Peak};
int sample_idx{0};
int samples_eaten{0};
int32_t max_dB{0};
int8_t real{0};
int8_t imag{0};
uint16_t peak_timeout{0};
float noise_floor{12.0}; // Using LNA 40 and VGA 20. 10.0 was 40/0 ratio.
float target_power_db{5.0};
float agc_power{0.0f};
float frequency_offset_estimate{0.0f};
float frequency_offset{0.0f};
float phase_buffer[ROLLING_WINDOW] = {0.0f};
int phase_buffer_index = 0;
uint16_t packet_index{0};
uint8_t bit_index{0};
size_t demod_input_fs{0};
uint8_t SAMPLE_PER_SYMBOL{1};
uint32_t DEFAULT_PREAMBLE{0xAAAAAAAA};
uint32_t DEFAULT_SYNC_WORD{0xFFFFFFFF};
uint8_t NUM_SYNC_WORD_BYTE{4};
uint8_t NUM_PREAMBLE_BYTE{4};
uint16_t NUM_DATA_BYTE = MAX_BUFFER_SIZE - NUM_SYNC_WORD_BYTE - NUM_PREAMBLE_BYTE;
SpectrumCollector channel_spectrum{};
size_t spectrum_interval_samples = 0;
size_t spectrum_samples = 0;
static constexpr auto spectrum_rate_hz = 50.0f;
/* The actual type will be configured depending on the sample rate. */
MultiDecimator<
dsp::decimate::FIRC8xR16x24FS4Decim4,
dsp::decimate::FIRC8xR16x24FS4Decim8>
@@ -159,9 +191,7 @@ class FSKRxProcessor : public BasebandProcessor {
NoopDecim>
decim_1{};
/* Filter to 24kHz and demodulate. */
dsp::decimate::FIRAndDecimateComplex channel_filter{};
size_t deviation = 3750;
// fir_taps_real<32> channel_filter_taps = 0;
size_t channel_decimation = 2;
int32_t channel_filter_low_f = 0;
@@ -172,51 +202,6 @@ class FSKRxProcessor : public BasebandProcessor {
FMSquelch squelch{};
uint64_t squelch_history = 0;
// /* LPF to reduce noise. POCSAG supports 2400 baud, but that falls
// * nicely into the transition band of this 1800Hz filter.
// * scipy.signal.butter(2, 1800, "lowpass", fs=24000, analog=False) */
// IIRBiquadFilter lpf{{{0.04125354f, 0.082507070f, 0.04125354f},
// {1.00000000f, -1.34896775f, 0.51398189f}}};
/* Attempts to de-noise and normalize signal. */
AudioNormalizer normalizer{};
/* Handles writing audio stream to hardware. */
AudioOutput audio_output{};
/* Holds the data sent to the app. */
AFSKDataMessage data_message{false, 0};
/* Used to keep track of how many samples were processed
* between status update messages. */
uint32_t samples_processed = 0;
/* Number of bits in 'data_' member. */
static constexpr uint8_t data_bit_count = sizeof(uint32_t) * 8;
/* Sync frame codeword. */
static constexpr uint32_t sync_codeword = 0x12345678;
/* When true, sync frame has been received. */
bool has_sync_ = false;
/* When true, bit vales are flipped in the codewords. */
bool inverted = false;
uint32_t data = 0;
uint8_t bit_count = 0;
uint8_t word_count = 0;
/* LPF to reduce noise. POCSAG supports 2400 baud, but that falls
* nicely into the transition band of this 1800Hz filter.
* scipy.signal.butter(2, 1800, "lowpass", fs=24000, analog=False) */
IIRBiquadFilter lpf{{{0.04125354f, 0.082507070f, 0.04125354f},
{1.00000000f, -1.34896775f, 0.51398189f}}};
SpectrumCollector channel_spectrum{};
size_t spectrum_interval_samples = 0;
size_t spectrum_samples = 0;
/* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive};
RSSIThread rssi_thread{};
+32
View File
@@ -1649,4 +1649,36 @@ static constexpr fir_taps_real<24> taps_BTLE_2M_PHY_decim_0 = {
}},
};
// Tested to be better at capturing both 4.0 and 5.0 device. Better attenuation at channel end.
static constexpr fir_taps_real<24> taps_BTLE_Dual_PHY = {
.low_frequency_normalized = -750000.0f / 4000000.0f,
.high_frequency_normalized = 750000.0f / 4000000.0f,
.transition_normalized = 250000.0f / 4000000.0f,
.taps = {{3,
-5,
-97,
-144,
317,
1099,
396,
-2887,
-4814,
1912,
18134,
32767,
32767,
18134,
1912,
-4814,
-2887,
396,
1099,
317,
-144,
-97,
-5,
3}},
};
#endif /*__DSP_FIR_TAPS_H__*/
+53 -15
View File
@@ -134,6 +134,8 @@ class Message {
NoaaAptRxConfigure = 77,
NoaaAptRxStatusData = 78,
NoaaAptRxImageData = 79,
FSKPacket = 80,
EPIRBPacket = 81,
MAX
};
@@ -338,6 +340,17 @@ class AISPacketMessage : public Message {
baseband::Packet packet;
};
class EPIRBPacketMessage : public Message {
public:
constexpr EPIRBPacketMessage(
const baseband::Packet& packet)
: Message{ID::EPIRBPacket},
packet{packet} {
}
baseband::Packet packet;
};
class TPMSPacketMessage : public Message {
public:
constexpr TPMSPacketMessage(
@@ -452,6 +465,17 @@ struct BlePacketData {
uint8_t dataLen;
};
struct FskPacketData {
int8_t real;
int8_t imag;
int max_dB;
uint8_t data[512];
uint16_t dataLen;
uint64_t syncWord;
float power;
float frequency_offset_hz;
};
class BLEPacketMessage : public Message {
public:
constexpr BLEPacketMessage(
@@ -463,6 +487,17 @@ class BLEPacketMessage : public Message {
BlePacketData* packet{nullptr};
};
class FSKRxPacketMessage : public Message {
public:
constexpr FSKRxPacketMessage(
FskPacketData* packet)
: Message{ID::FSKPacket},
packet{packet} {
}
FskPacketData* packet{nullptr};
};
class CodedSquelchMessage : public Message {
public:
constexpr CodedSquelchMessage(
@@ -1127,24 +1162,27 @@ class FSKConfigureMessage : public Message {
class FSKRxConfigureMessage : public Message {
public:
constexpr FSKRxConfigureMessage(
const fir_taps_real<24> decim_0_filter,
const fir_taps_real<32> decim_1_filter,
const fir_taps_real<32> channel_filter,
const size_t channel_decimation,
const size_t deviation)
const uint8_t samplesPerSymbol,
const uint32_t syncWord,
const uint8_t syncWordLength,
const uint32_t preamble,
const uint8_t preambleLength,
const uint16_t numDataBytes)
: Message{ID::FSKRxConfigure},
decim_0_filter(decim_0_filter),
decim_1_filter(decim_1_filter),
channel_filter(channel_filter),
channel_decimation{channel_decimation},
deviation{deviation} {
samplesPerSymbol(samplesPerSymbol),
syncWord(syncWord),
syncWordLength(syncWordLength),
preamble(preamble),
preambleLength(preambleLength),
numDataBytes(numDataBytes) {
}
const fir_taps_real<24> decim_0_filter;
const fir_taps_real<32> decim_1_filter;
const fir_taps_real<32> channel_filter;
const size_t channel_decimation;
const size_t deviation;
const uint8_t samplesPerSymbol;
const uint32_t syncWord;
const uint8_t syncWordLength;
const uint32_t preamble;
const uint8_t preambleLength;
const uint16_t numDataBytes;
};
class POCSAGConfigureMessage : public Message {
+11 -16
View File
@@ -110,7 +110,7 @@ GPS_data Packet::get_GPS_data() const {
result.alt = reader_bi_m.read(8 * 8, 24) / 100.0; // <|
result.lat = reader_bi_m.read(28 * 8, 32) / 1000000.0; // <| Inspired by https://raw.githubusercontent.com/projecthorus/radiosonde_auto_rx/master/demod/mod/m20mod.c
result.lon = reader_bi_m.read(32 * 8, 32) / 1000000.0; // <|
} else if (type_ == Type::Vaisala_RS41_SG) {
} else if (type_ == Type::Vaisala_RS41_SG && crc16rs41(block_gpspos)) {
uint8_t XYZ_bytes[4];
int32_t XYZ; // 32bit
double_t X[3];
@@ -245,7 +245,7 @@ temp_humid Packet::get_temp_humid() const {
}
}
if (type_ == Type::Vaisala_RS41_SG && crc_ok_RS41()) // Only process if packet is healthy
if (type_ == Type::Vaisala_RS41_SG && crc16rs41(block_meas)) // Only process if packet is healthy
{
// memset(calfrchk, 0, 51); // is this necessary ? only if the sondeID changes (new sonde)
// original code from https://github.com/rs1729/RS/blob/master/rs41/rs41ptu.c
@@ -371,9 +371,11 @@ std::string Packet::serial_number() const {
uint8_t achar;
for (uint8_t i = 0; i < 8; i++) { // euquiq: Serial ID is 8 bytes long, each byte a char
achar = vaisala_descramble(pos_SondeID + i);
if (achar < 32 || achar > 126)
return "?"; // Maybe there are ids with less than 8 bytes and this is not OK.
serial_id += (char)achar;
if (achar < 32 || achar > 126) {
serial_id += "?"; // Maybe there are ids with less than 8 bytes and this is not OK.
} else {
serial_id += (char)achar;
}
}
return serial_id;
} else {
@@ -414,15 +416,9 @@ bool Packet::crc_ok() const {
// 2 bytes CRC16 over the data.
bool Packet::crc_ok_RS41() const // check CRC for the data blocks we need
{
if (!crc16rs41(block_status))
return false;
if (!crc16rs41(block_gpspos))
return false;
if (!crc16rs41(block_meas))
return false;
if (!crc16rs41(block_status)) return false;
if (!crc16rs41(block_gpspos)) return false;
if (!crc16rs41(block_meas)) return false;
return true;
}
@@ -451,8 +447,7 @@ bool Packet::crc16rs41(uint32_t field_start) const {
}
}
// Check calculated CRC against packet's one
pos++;
int crcok = vaisala_descramble(pos) | (vaisala_descramble(pos + 1) << 8);
int crcok = vaisala_descramble(field_start + 2 + length) | (vaisala_descramble(field_start + 2 + length + 1) << 8);
if (crcok != rem)
return false;
return true;
+1
View File
@@ -87,6 +87,7 @@ constexpr image_tag_t image_tag_am_audio{'P', 'A', 'M', 'A'};
constexpr image_tag_t image_tag_am_tv{'P', 'A', 'M', 'T'};
constexpr image_tag_t image_tag_capture{'P', 'C', 'A', 'P'};
constexpr image_tag_t image_tag_ert{'P', 'E', 'R', 'T'};
constexpr image_tag_t image_tag_epirb_rx{'P', 'E', 'P', 'I'};
constexpr image_tag_t image_tag_nfm_audio{'P', 'N', 'F', 'M'};
constexpr image_tag_t image_tag_pocsag{'P', 'P', 'O', 'C'};
constexpr image_tag_t image_tag_pocsag2{'P', 'P', 'O', '2'};
+1
View File
@@ -98,6 +98,7 @@ for external_image_prefix in sys.argv[4:]:
# COMMAND ${CMAKE_OBJCOPY} -v -O binary ${PROJECT_NAME}.elf ${PROJECT_NAME}_ext_pacman.bin --only-section=.external_app_pacman
himg = "{}/external_app_{}.himg".format(binary_dir, external_image_prefix)
print("Creating external application image for {}".format(external_image_prefix))
subprocess.run([cmake_objcopy, "-v", "-O", "binary", "{}/application.elf".format(binary_dir), himg, "--only-section=.external_app_{}".format(external_image_prefix)])
external_application_image = read_image(himg)
+1 -1
Submodule hackrf updated: 65bf67f9b5...651d0a7f23
+464 -464
View File
@@ -1,464 +1,464 @@
010203040506 03032CFE06162CFE000006020AA8 1
010203040506 03032CFE06162CFE000007020AA9 1
010203040506 03032CFE06162CFE000008020AAA 1
010203040506 03032CFE06162CFE000009020AAB 1
010203040506 03032CFE06162CFE00000A020AAC 1
010203040506 03032CFE06162CFE00000B020AAD 1
010203040506 03032CFE06162CFE00000C020AAE 1
010203040506 03032CFE06162CFE00000D020AAF 1
010203040506 03032CFE06162CFE000035020AB0 1
010203040506 03032CFE06162CFE000047020AB1 1
010203040506 03032CFE06162CFE000048020AB2 1
010203040506 03032CFE06162CFE000049020AB3 1
010203040506 03032CFE06162CFE0000F0020AB4 1
010203040506 03032CFE06162CFE0002F0020AB5 1
010203040506 03032CFE06162CFE0003F0020AB6 1
010203040506 03032CFE06162CFE001000020AB7 1
010203040506 03032CFE06162CFE002000020AB8 1
010203040506 03032CFE06162CFE003000020AB9 1
010203040506 03032CFE06162CFE003001020ABA 1
010203040506 03032CFE06162CFE003B41020ABB 1
010203040506 03032CFE06162CFE003D8A020ABC 1
010203040506 03032CFE06162CFE0052DA020ABD 1
010203040506 03032CFE06162CFE005BC3020ABE 1
010203040506 03032CFE06162CFE008F7D020ABF 1
010203040506 03032CFE06162CFE00A168020AC0 1
010203040506 03032CFE06162CFE00AA48020AC1 1
010203040506 03032CFE06162CFE00AA91020AC2 1
010203040506 03032CFE06162CFE00B727020AC3 1
010203040506 03032CFE06162CFE00C95C020AC4 1
010203040506 03032CFE06162CFE00F7D4020AC5 1
010203040506 03032CFE06162CFE00FA72020AC6 1
010203040506 03032CFE06162CFE0100F0020AC7 1
010203040506 03032CFE06162CFE0102F0020AC8 1
010203040506 03032CFE06162CFE0103F0020AC9 1
010203040506 03032CFE06162CFE011242020ACA 1
010203040506 03032CFE06162CFE013D8A020ACB 1
010203040506 03032CFE06162CFE01AA91020ACC 1
010203040506 03032CFE06162CFE01C95C020ACD 1
010203040506 03032CFE06162CFE01E5CE020ACE 1
010203040506 03032CFE06162CFE01EEB4020ACF 1
010203040506 03032CFE06162CFE0200F0020AD0 1
010203040506 03032CFE06162CFE0202F0020AD1 1
010203040506 03032CFE06162CFE0203F0020AD2 1
010203040506 03032CFE06162CFE02AA91020AD3 1
010203040506 03032CFE06162CFE02C95C020AD4 1
010203040506 03032CFE06162CFE02D815020AD5 1
010203040506 03032CFE06162CFE02D886020AD6 1
010203040506 03032CFE06162CFE02DD4F020AD7 1
010203040506 03032CFE06162CFE02E2A9020AD8 1
010203040506 03032CFE06162CFE02F637020AD9 1
010203040506 03032CFE06162CFE0302F0020ADA 1
010203040506 03032CFE06162CFE0303F0020ADB 1
010203040506 03032CFE06162CFE035754020ADC 1
010203040506 03032CFE06162CFE035764020ADD 1
010203040506 03032CFE06162CFE038B91020ADE 1
010203040506 03032CFE06162CFE038CC7020ADF 1
010203040506 03032CFE06162CFE038F16020AE0 1
010203040506 03032CFE06162CFE039F8F020AE1 1
010203040506 03032CFE06162CFE03AA91020AE2 1
010203040506 03032CFE06162CFE03B716020AE3 1
010203040506 03032CFE06162CFE03C95C020AE4 1
010203040506 03032CFE06162CFE03C99C020AE5 1
010203040506 03032CFE06162CFE03F5D4020AE6 1
010203040506 03032CFE06162CFE0402F0020AE7 1
010203040506 03032CFE06162CFE0403F0020AE8 1
010203040506 03032CFE06162CFE045754020AE9 1
010203040506 03032CFE06162CFE045764020AEA 1
010203040506 03032CFE06162CFE04AA91020AEB 1
010203040506 03032CFE06162CFE04ACFC020AEC 1
010203040506 03032CFE06162CFE04AFB8020AED 1
010203040506 03032CFE06162CFE04C95C020AEE 1
010203040506 03032CFE06162CFE0502F0020AEF 1
010203040506 03032CFE06162CFE0503F0020AF0 1
010203040506 03032CFE06162CFE050F0C020AF1 1
010203040506 03032CFE06162CFE052CC7020AF2 1
010203040506 03032CFE06162CFE054B2D020AF3 1
010203040506 03032CFE06162CFE0577B1020AF4 1
010203040506 03032CFE06162CFE057802020AF5 1
010203040506 03032CFE06162CFE0582FD020AF6 1
010203040506 03032CFE06162CFE058D08020AF7 1
010203040506 03032CFE06162CFE05A963020AF8 1
010203040506 03032CFE06162CFE05A9BC020AF9 1
010203040506 03032CFE06162CFE05AA91020AFA 1
010203040506 03032CFE06162CFE05C452020AFB 1
010203040506 03032CFE06162CFE05C95C020AFC 1
010203040506 03032CFE06162CFE060000020AFD 1
010203040506 03032CFE06162CFE0602F0020AFE 1
010203040506 03032CFE06162CFE0603F0020AFF 1
010203040506 03032CFE06162CFE0660D7020B00 1
010203040506 03032CFE06162CFE06AE20020B01 1
010203040506 03032CFE06162CFE06C197020B02 1
010203040506 03032CFE06162CFE06C95C020B03 1
010203040506 03032CFE06162CFE06D8FC020B04 1
010203040506 03032CFE06162CFE070000020B05 1
010203040506 03032CFE06162CFE0702F0020B06 1
010203040506 03032CFE06162CFE0703F0020B07 1
010203040506 03032CFE06162CFE0X071C020B08 1
010203040506 03032CFE06162CFE0744B6020B09 1
010203040506 03032CFE06162CFE07A41C020B0A 1
010203040506 03032CFE06162CFE07C95C020B0B 1
010203040506 03032CFE06162CFE07F426020B0C 1
010203040506 03032CFE06162CFE080000020B0D 1
010203040506 03032CFE06162CFE0802F0020B0E 1
010203040506 03032CFE06162CFE0803F0020B0F 1
010203040506 03032CFE06162CFE090000020B10 1
010203040506 03032CFE06162CFE0903F0020B11 1
010203040506 03032CFE06162CFE0A0000020B12 1
010203040506 03032CFE06162CFE0B0000020B13 1
010203040506 03032CFE06162CFE0C0000020B14 1
010203040506 03032CFE06162CFE0DC6BF020B15 1
010203040506 03032CFE06162CFE0DC95C020B16 1
010203040506 03032CFE06162CFE0DEC2B020B17 1
010203040506 03032CFE06162CFE0E138D020B18 1
010203040506 03032CFE06162CFE0E30C3020B19 1
010203040506 03032CFE06162CFE0EC95C020B1A 1
010203040506 03032CFE06162CFE0ECE95020B1B 1
010203040506 03032CFE06162CFE0F0993020B1C 1
010203040506 03032CFE06162CFE0F1B8D020B1D 1
010203040506 03032CFE06162CFE0F232A020B1E 1
010203040506 03032CFE06162CFE0F2D16020B1F 1
010203040506 03032CFE06162CFE1E89A7020B20 1
010203040506 03032CFE06162CFE1E8B18020B21 1
010203040506 03032CFE06162CFE1E955B020B22 1
010203040506 03032CFE06162CFE1EC95C020B23 1
010203040506 03032CFE06162CFE1ED9F9020B24 1
010203040506 03032CFE06162CFE1EE890020B25 1
010203040506 03032CFE06162CFE1EEDF5020B26 1
010203040506 03032CFE06162CFE1F1101020B27 1
010203040506 03032CFE06162CFE1F181A020B28 1
010203040506 03032CFE06162CFE1F2E13020B29 1
010203040506 03032CFE06162CFE1F4589020B2A 1
010203040506 03032CFE06162CFE1F4627020B2B 1
010203040506 03032CFE06162CFE1F5865020B2C 1
010203040506 03032CFE06162CFE1FBB50020B2D 1
010203040506 03032CFE06162CFE1FC95C020B2E 1
010203040506 03032CFE06162CFE1FE765020B2F 1
010203040506 03032CFE06162CFE1FF8FA020B30 1
010203040506 03032CFE06162CFE201C7C020B31 1
010203040506 03032CFE06162CFE202B3D020B32 1
010203040506 03032CFE06162CFE20330C020B33 1
010203040506 03032CFE06162CFE20A19B020B34 1
010203040506 03032CFE06162CFE20C95C020B35 1
010203040506 03032CFE06162CFE20CC2C020B36 1
010203040506 03032CFE06162CFE213C8C020B37 1
010203040506 03032CFE06162CFE21521D020B38 1
010203040506 03032CFE06162CFE21A04E020B39 1
010203040506 03032CFE06162CFE2D7A23020B3A 1
010203040506 03032CFE06162CFE350000020B3B 1
010203040506 03032CFE06162CFE470000020B3C 1
010203040506 03032CFE06162CFE480000020B3D 1
010203040506 03032CFE06162CFE490000020B3E 1
010203040506 03032CFE06162CFE5BA9B5020B3F 1
010203040506 03032CFE06162CFE5BACD6020B40 1
010203040506 03032CFE06162CFE5BD6C9020B41 1
010203040506 03032CFE06162CFE5BE3D4020B42 1
010203040506 03032CFE06162CFE5C0206020B43 1
010203040506 03032CFE06162CFE5C0C84020B44 1
010203040506 03032CFE06162CFE5C4833020B45 1
010203040506 03032CFE06162CFE5C4A7E020B46 1
010203040506 03032CFE06162CFE5C55E7020B47 1
010203040506 03032CFE06162CFE5C7CDC020B48 1
010203040506 03032CFE06162CFE5C8AA5020B49 1
010203040506 03032CFE06162CFE5CC900020B4A 1
010203040506 03032CFE06162CFE5CC901020B4B 1
010203040506 03032CFE06162CFE5CC902020B4C 1
010203040506 03032CFE06162CFE5CC903020B4D 1
010203040506 03032CFE06162CFE5CC904020B4E 1
010203040506 03032CFE06162CFE5CC905020B4F 1
010203040506 03032CFE06162CFE5CC906020B50 1
010203040506 03032CFE06162CFE5CC907020B51 1
010203040506 03032CFE06162CFE5CC908020B52 1
010203040506 03032CFE06162CFE5CC909020B53 1
010203040506 03032CFE06162CFE5CC90A020B54 1
010203040506 03032CFE06162CFE5CC90B020B55 1
010203040506 03032CFE06162CFE5CC90C020B56 1
010203040506 03032CFE06162CFE5CC90D020B57 1
010203040506 03032CFE06162CFE5CC90E020B58 1
010203040506 03032CFE06162CFE5CC90F020B59 1
010203040506 03032CFE06162CFE5CC910020B5A 1
010203040506 03032CFE06162CFE5CC911020B5B 1
010203040506 03032CFE06162CFE5CC912020B5C 1
010203040506 03032CFE06162CFE5CC913020B5D 1
010203040506 03032CFE06162CFE5CC914020B5E 1
010203040506 03032CFE06162CFE5CC915020B5F 1
010203040506 03032CFE06162CFE5CC916020B60 1
010203040506 03032CFE06162CFE5CC917020B61 1
010203040506 03032CFE06162CFE5CC918020B62 1
010203040506 03032CFE06162CFE5CC919020B63 1
010203040506 03032CFE06162CFE5CC91A020B64 1
010203040506 03032CFE06162CFE5CC91B020B65 1
010203040506 03032CFE06162CFE5CC91C020B66 1
010203040506 03032CFE06162CFE5CC91D020B67 1
010203040506 03032CFE06162CFE5CC91E020B68 1
010203040506 03032CFE06162CFE5CC91F020B69 1
010203040506 03032CFE06162CFE5CC920020B6A 1
010203040506 03032CFE06162CFE5CC921020B6B 1
010203040506 03032CFE06162CFE5CC922020B6C 1
010203040506 03032CFE06162CFE5CC923020B6D 1
010203040506 03032CFE06162CFE5CC924020B6E 1
010203040506 03032CFE06162CFE5CC925020B6F 1
010203040506 03032CFE06162CFE5CC926020B70 1
010203040506 03032CFE06162CFE5CC927020B71 1
010203040506 03032CFE06162CFE5CC928020B72 1
010203040506 03032CFE06162CFE5CC929020B73 1
010203040506 03032CFE06162CFE5CC92A020B74 1
010203040506 03032CFE06162CFE5CC92B020B75 1
010203040506 03032CFE06162CFE5CC92C020B76 1
010203040506 03032CFE06162CFE5CC92D020B77 1
010203040506 03032CFE06162CFE5CC92E020B78 1
010203040506 03032CFE06162CFE5CC92F020B79 1
010203040506 03032CFE06162CFE5CC930020B7A 1
010203040506 03032CFE06162CFE5CC931020B7B 1
010203040506 03032CFE06162CFE5CC932020B7C 1
010203040506 03032CFE06162CFE5CC933020B7D 1
010203040506 03032CFE06162CFE5CC934020B7E 1
010203040506 03032CFE06162CFE5CC935020B7F 1
010203040506 03032CFE06162CFE5CC936020B80 1
010203040506 03032CFE06162CFE5CC937020B81 1
010203040506 03032CFE06162CFE5CC938020B82 1
010203040506 03032CFE06162CFE5CC939020B83 1
010203040506 03032CFE06162CFE5CC93A020B84 1
010203040506 03032CFE06162CFE5CC93B020B85 1
010203040506 03032CFE06162CFE5CC93C020B86 1
010203040506 03032CFE06162CFE5CC93D020B87 1
010203040506 03032CFE06162CFE5CC93E020B88 1
010203040506 03032CFE06162CFE5CC93F020B89 1
010203040506 03032CFE06162CFE5CC940020B8A 1
010203040506 03032CFE06162CFE5CC941020B8B 1
010203040506 03032CFE06162CFE5CC942020B8C 1
010203040506 03032CFE06162CFE5CC943020B8D 1
010203040506 03032CFE06162CFE5CC944020B8E 1
010203040506 03032CFE06162CFE5CC945020B8F 1
010203040506 03032CFE06162CFE5CEE3C020B90 1
010203040506 03032CFE06162CFE6AD226020B91 1
010203040506 03032CFE06162CFE6B1C64020B92 1
010203040506 03032CFE06162CFE6B8C65020B93 1
010203040506 03032CFE06162CFE6B9304020B94 1
010203040506 03032CFE06162CFE6BA5C3020B95 1
010203040506 03032CFE06162CFE6C42C0020B96 1
010203040506 03032CFE06162CFE6C4DE5020B97 1
010203040506 03032CFE06162CFE718FA4020B98 1
010203040506 03032CFE06162CFE72EF8D020B99 1
010203040506 03032CFE06162CFE72FB00020B9A 1
010203040506 03032CFE06162CFE821F66020B9B 1
010203040506 03032CFE06162CFE89BAD5020B9C 1
010203040506 03032CFE06162CFE8A31B7020B9D 1
010203040506 03032CFE06162CFE8A3D00020B9E 1
010203040506 03032CFE06162CFE8A3D01020B9F 1
010203040506 03032CFE06162CFE8A8F23020BA0 1
010203040506 03032CFE06162CFE8AADAE020BA1 1
010203040506 03032CFE06162CFE8B0A91020BA2 1
010203040506 03032CFE06162CFE8B5A7B020BA3 1
010203040506 03032CFE06162CFE8B66AB020BA4 1
010203040506 03032CFE06162CFE8BB0A0020BA5 1
010203040506 03032CFE06162CFE8BF79A020BA6 1
010203040506 03032CFE06162CFE8C07D2020BA7 1
010203040506 03032CFE06162CFE8C1706020BA8 1
010203040506 03032CFE06162CFE8C4236020BA9 1
010203040506 03032CFE06162CFE8C6B6A020BAA 1
010203040506 03032CFE06162CFE8CAD81020BAB 1
010203040506 03032CFE06162CFE8CB05C020BAC 1
010203040506 03032CFE06162CFE8CD10F020BAD 1
010203040506 03032CFE06162CFE8D13B9020BAE 1
010203040506 03032CFE06162CFE8D16EA020BAF 1
010203040506 03032CFE06162CFE8D5B67020BB0 1
010203040506 03032CFE06162CFE8E14D7020BB1 1
010203040506 03032CFE06162CFE8E1996020BB2 1
010203040506 03032CFE06162CFE8E4666020BB3 1
010203040506 03032CFE06162CFE8E5550020BB4 1
010203040506 03032CFE06162CFE9101F0020BB5 1
010203040506 03032CFE06162CFE9128CB020BB6 1
010203040506 03032CFE06162CFE913B0C020BB7 1
010203040506 03032CFE06162CFE915CFA020BB8 1
010203040506 03032CFE06162CFE9171BE020BB9 1
010203040506 03032CFE06162CFE917E46020BBA 1
010203040506 03032CFE06162CFE91AA00020BBB 1
010203040506 03032CFE06162CFE91AA01020BBC 1
010203040506 03032CFE06162CFE91AA02020BBD 1
010203040506 03032CFE06162CFE91AA03020BBE 1
010203040506 03032CFE06162CFE91AA04020BBF 1
010203040506 03032CFE06162CFE91AA05020BC0 1
010203040506 03032CFE06162CFE91BD38020BC1 1
010203040506 03032CFE06162CFE91C813020BC2 1
010203040506 03032CFE06162CFE91DABC020BC3 1
010203040506 03032CFE06162CFE92255E020BC4 1
010203040506 03032CFE06162CFE92BBBD020BC5 1
010203040506 03032CFE06162CFE989D0A020BC6 1
010203040506 03032CFE06162CFE9939BC020BC7 1
010203040506 03032CFE06162CFE994374020BC8 1
010203040506 03032CFE06162CFE997B4A020BC9 1
010203040506 03032CFE06162CFE99C87B020BCA 1
010203040506 03032CFE06162CFE99D7EA020BCB 1
010203040506 03032CFE06162CFE99F098020BCC 1
010203040506 03032CFE06162CFE9A408A020BCD 1
010203040506 03032CFE06162CFE9A9BDD020BCE 1
010203040506 03032CFE06162CFE9ADB11020BCF 1
010203040506 03032CFE06162CFE9AEEA4020BD0 1
010203040506 03032CFE06162CFE9B7339020BD1 1
010203040506 03032CFE06162CFE9B735A020BD2 1
010203040506 03032CFE06162CFE9B9872020BD3 1
010203040506 03032CFE06162CFE9BC64D020BD4 1
010203040506 03032CFE06162CFE9BE931020BD5 1
010203040506 03032CFE06162CFE9C0AF7020BD6 1
010203040506 03032CFE06162CFE9C3997020BD7 1
010203040506 03032CFE06162CFE9C4058020BD8 1
010203040506 03032CFE06162CFE9C6BC0020BD9 1
010203040506 03032CFE06162CFE9C888B020BDA 1
010203040506 03032CFE06162CFE9C98DB020BDB 1
010203040506 03032CFE06162CFE9CA277020BDC 1
010203040506 03032CFE06162CFE9CB5F3020BDD 1
010203040506 03032CFE06162CFE9CB881020BDE 1
010203040506 03032CFE06162CFE9CD0F3020BDF 1
010203040506 03032CFE06162CFE9CE3C7020BE0 1
010203040506 03032CFE06162CFE9CEFD1020BE1 1
010203040506 03032CFE06162CFE9CF08F020BE2 1
010203040506 03032CFE06162CFE9D00A6020BE3 1
010203040506 03032CFE06162CFE9D7D42020BE4 1
010203040506 03032CFE06162CFE9DB896020BE5 1
010203040506 03032CFE06162CFEA7E52B020BE6 1
010203040506 03032CFE06162CFEA7EF76020BE7 1
010203040506 03032CFE06162CFEA8001A020BE8 1
010203040506 03032CFE06162CFEA83C10020BE9 1
010203040506 03032CFE06162CFEA8658F020BEA 1
010203040506 03032CFE06162CFEA8845A020BEB 1
010203040506 03032CFE06162CFEA88B69020BEC 1
010203040506 03032CFE06162CFEA8A00E020BED 1
010203040506 03032CFE06162CFEA8A72A020BEE 1
010203040506 03032CFE06162CFEA8C636020BEF 1
010203040506 03032CFE06162CFEA8CAAD020BF0 1
010203040506 03032CFE06162CFEA8E353020BF1 1
010203040506 03032CFE06162CFEA8F96D020BF2 1
010203040506 03032CFE06162CFEA90358020BF3 1
010203040506 03032CFE06162CFEA92498020BF4 1
010203040506 03032CFE06162CFEA9394A020BF5 1
010203040506 03032CFE06162CFEC6936A020BF6 1
010203040506 03032CFE06162CFEC69AFD020BF7 1
010203040506 03032CFE06162CFEC6ABEA020BF8 1
010203040506 03032CFE06162CFEC6EC5F020BF9 1
010203040506 03032CFE06162CFEC7736C020BFA 1
010203040506 03032CFE06162CFEC79B91020BFB 1
010203040506 03032CFE06162CFEC7A267020BFC 1
010203040506 03032CFE06162CFEC7D620020BFD 1
010203040506 03032CFE06162CFEC7FBCC020BFE 1
010203040506 03032CFE06162CFEC8162A020BFF 1
010203040506 03032CFE06162CFEC85D7A020C00 1
010203040506 03032CFE06162CFEC8777E020C01 1
010203040506 03032CFE06162CFEC878AA020C02 1
010203040506 03032CFE06162CFEC8C641020C03 1
010203040506 03032CFE06162CFEC8D335020C04 1
010203040506 03032CFE06162CFEC8E228020C05 1
010203040506 03032CFE06162CFEC9186B020C06 1
010203040506 03032CFE06162CFEC9836A020C07 1
010203040506 03032CFE06162CFECA7030020C08 1
010203040506 03032CFE06162CFECAB6B8020C09 1
010203040506 03032CFE06162CFECAF511020C0A 1
010203040506 03032CFE06162CFECB093B020C0B 1
010203040506 03032CFE06162CFECB529D020C0C 1
010203040506 03032CFE06162CFECC438E020C0D 1
010203040506 03032CFE06162CFECC5F29020C0E 1
010203040506 03032CFE06162CFECC754F020C0F 1
010203040506 03032CFE06162CFECC93A5020C10 1
010203040506 03032CFE06162CFECCBB7E020C11 1
010203040506 03032CFE06162CFECD8256020C12 1
010203040506 03032CFE06162CFED446A7020C13 1
010203040506 03032CFE06162CFED5A59E020C14 1
010203040506 03032CFE06162CFED5B5F7020C15 1
010203040506 03032CFE06162CFED5C6CE020C16 1
010203040506 03032CFE06162CFED654CD020C17 1
010203040506 03032CFE06162CFED65F4E020C18 1
010203040506 03032CFE06162CFED69B2B020C19 1
010203040506 03032CFE06162CFED6C195020C1A 1
010203040506 03032CFE06162CFED6E870020C1B 1
010203040506 03032CFE06162CFED6EE84020C1C 1
010203040506 03032CFE06162CFED7102F020C1D 1
010203040506 03032CFE06162CFED7E3EB020C1E 1
010203040506 03032CFE06162CFED8058C020C1F 1
010203040506 03032CFE06162CFED820EA020C20 1
010203040506 03032CFE06162CFED87A3E020C21 1
010203040506 03032CFE06162CFED8F3BA020C22 1
010203040506 03032CFE06162CFED8F4E8020C23 1
010203040506 03032CFE06162CFED90617020C24 1
010203040506 03032CFE06162CFED933A7020C25 1
010203040506 03032CFE06162CFED9414F020C26 1
010203040506 03032CFE06162CFED97EBA020C27 1
010203040506 03032CFE06162CFED9964B020C28 1
010203040506 03032CFE06162CFEDA0F83020C29 1
010203040506 03032CFE06162CFEDA4577020C2A 1
010203040506 03032CFE06162CFEDA5200020C2B 1
010203040506 03032CFE06162CFEDAD3A6020C2C 1
010203040506 03032CFE06162CFEDADE43020C2D 1
010203040506 03032CFE06162CFEDAE096020C2E 1
010203040506 03032CFE06162CFEDB8AC7020C2F 1
010203040506 03032CFE06162CFEDBE5B1020C30 1
010203040506 03032CFE06162CFEDC5249020C31 1
010203040506 03032CFE06162CFEDCF33C020C32 1
010203040506 03032CFE06162CFEDD4EC0020C33 1
010203040506 03032CFE06162CFEDE215D020C34 1
010203040506 03032CFE06162CFEDE577F020C35 1
010203040506 03032CFE06162CFEDEC04C020C36 1
010203040506 03032CFE06162CFEDEDD6F020C37 1
010203040506 03032CFE06162CFEDEE8C0020C38 1
010203040506 03032CFE06162CFEDEEA86020C39 1
010203040506 03032CFE06162CFEDEF234020C3A 1
010203040506 03032CFE06162CFEDF01E3020C3B 1
010203040506 03032CFE06162CFEDF271C020C3C 1
010203040506 03032CFE06162CFEDF42DE020C3D 1
010203040506 03032CFE06162CFEDF4B02020C3E 1
010203040506 03032CFE06162CFEDF9BA4020C3F 1
010203040506 03032CFE06162CFEDFD433020C40 1
010203040506 03032CFE06162CFEE020C1020C41 1
010203040506 03032CFE06162CFEE06116020C42 1
010203040506 03032CFE06162CFEE07634020C43 1
010203040506 03032CFE06162CFEE09172020C44 1
010203040506 03032CFE06162CFEE4E457020C45 1
010203040506 03032CFE06162CFEE5440B020C46 1
010203040506 03032CFE06162CFEE57363020C47 1
010203040506 03032CFE06162CFEE57B57020C48 1
010203040506 03032CFE06162CFEE5B4B0020C49 1
010203040506 03032CFE06162CFEE5B91B020C4A 1
010203040506 03032CFE06162CFEE5E2E9020C4B 1
010203040506 03032CFE06162CFEE64613020C4C 1
010203040506 03032CFE06162CFEE64CC6020C4D 1
010203040506 03032CFE06162CFEE69877020C4E 1
010203040506 03032CFE06162CFEE6E37E020C4F 1
010203040506 03032CFE06162CFEE6E771020C50 1
010203040506 03032CFE06162CFEE6E8B8020C51 1
010203040506 03032CFE06162CFEE750CE020C52 1
010203040506 03032CFE06162CFEF00000020C53 1
010203040506 03032CFE06162CFEF00001020C54 1
010203040506 03032CFE06162CFEF00002020C55 1
010203040506 03032CFE06162CFEF00200020C56 1
010203040506 03032CFE06162CFEF00201020C57 1
010203040506 03032CFE06162CFEF00202020C58 1
010203040506 03032CFE06162CFEF00203020C59 1
010203040506 03032CFE06162CFEF00204020C5A 1
010203040506 03032CFE06162CFEF00205020C5B 1
010203040506 03032CFE06162CFEF00206020C5C 1
010203040506 03032CFE06162CFEF00207020C5D 1
010203040506 03032CFE06162CFEF00208020C5E 1
010203040506 03032CFE06162CFEF00209020C5F 1
010203040506 03032CFE06162CFEF0020A020C60 1
010203040506 03032CFE06162CFEF0020B020C61 1
010203040506 03032CFE06162CFEF0020C020C62 1
010203040506 03032CFE06162CFEF0020D020C63 1
010203040506 03032CFE06162CFEF0020E020C64 1
010203040506 03032CFE06162CFEF0020F020C65 1
010203040506 03032CFE06162CFEF00210020C66 1
010203040506 03032CFE06162CFEF00211020C67 1
010203040506 03032CFE06162CFEF00212020C68 1
010203040506 03032CFE06162CFEF00213020C69 1
010203040506 03032CFE06162CFEF00214020C6A 1
010203040506 03032CFE06162CFEF00215020C6B 1
010203040506 03032CFE06162CFEF00300020C6C 1
010203040506 03032CFE06162CFEF00301020C6D 1
010203040506 03032CFE06162CFEF00302020C6E 1
010203040506 03032CFE06162CFEF00303020C6F 1
010203040506 03032CFE06162CFEF00304020C70 1
010203040506 03032CFE06162CFEF00305020C71 1
010203040506 03032CFE06162CFEF00306020C72 1
010203040506 03032CFE06162CFEF00307020C73 1
010203040506 03032CFE06162CFEF00308020C74 1
010203040506 03032CFE06162CFEF00309020C75 1
010203040506 03032CFE06162CFEF00400020C76 1
010203040506 03032CFE06162CFEF00E97020C77 1
010203040506 03032CFE06162CFE000006020AA8 DISCOVERY 1
010203040506 03032CFE06162CFE000007020AA9 DISCOVERY 1
010203040506 03032CFE06162CFE000008020AAA DISCOVERY 1
010203040506 03032CFE06162CFE000009020AAB DISCOVERY 1
010203040506 03032CFE06162CFE00000A020AAC DISCOVERY 1
010203040506 03032CFE06162CFE00000B020AAD DISCOVERY 1
010203040506 03032CFE06162CFE00000C020AAE DISCOVERY 1
010203040506 03032CFE06162CFE00000D020AAF DISCOVERY 1
010203040506 03032CFE06162CFE000035020AB0 DISCOVERY 1
010203040506 03032CFE06162CFE000047020AB1 DISCOVERY 1
010203040506 03032CFE06162CFE000048020AB2 DISCOVERY 1
010203040506 03032CFE06162CFE000049020AB3 DISCOVERY 1
010203040506 03032CFE06162CFE0000F0020AB4 DISCOVERY 1
010203040506 03032CFE06162CFE0002F0020AB5 DISCOVERY 1
010203040506 03032CFE06162CFE0003F0020AB6 DISCOVERY 1
010203040506 03032CFE06162CFE001000020AB7 DISCOVERY 1
010203040506 03032CFE06162CFE002000020AB8 DISCOVERY 1
010203040506 03032CFE06162CFE003000020AB9 DISCOVERY 1
010203040506 03032CFE06162CFE003001020ABA DISCOVERY 1
010203040506 03032CFE06162CFE003B41020ABB DISCOVERY 1
010203040506 03032CFE06162CFE003D8A020ABC DISCOVERY 1
010203040506 03032CFE06162CFE0052DA020ABD DISCOVERY 1
010203040506 03032CFE06162CFE005BC3020ABE DISCOVERY 1
010203040506 03032CFE06162CFE008F7D020ABF DISCOVERY 1
010203040506 03032CFE06162CFE00A168020AC0 DISCOVERY 1
010203040506 03032CFE06162CFE00AA48020AC1 DISCOVERY 1
010203040506 03032CFE06162CFE00AA91020AC2 DISCOVERY 1
010203040506 03032CFE06162CFE00B727020AC3 DISCOVERY 1
010203040506 03032CFE06162CFE00C95C020AC4 DISCOVERY 1
010203040506 03032CFE06162CFE00F7D4020AC5 DISCOVERY 1
010203040506 03032CFE06162CFE00FA72020AC6 DISCOVERY 1
010203040506 03032CFE06162CFE0100F0020AC7 DISCOVERY 1
010203040506 03032CFE06162CFE0102F0020AC8 DISCOVERY 1
010203040506 03032CFE06162CFE0103F0020AC9 DISCOVERY 1
010203040506 03032CFE06162CFE011242020ACA DISCOVERY 1
010203040506 03032CFE06162CFE013D8A020ACB DISCOVERY 1
010203040506 03032CFE06162CFE01AA91020ACC DISCOVERY 1
010203040506 03032CFE06162CFE01C95C020ACD DISCOVERY 1
010203040506 03032CFE06162CFE01E5CE020ACE DISCOVERY 1
010203040506 03032CFE06162CFE01EEB4020ACF DISCOVERY 1
010203040506 03032CFE06162CFE0200F0020AD0 DISCOVERY 1
010203040506 03032CFE06162CFE0202F0020AD1 DISCOVERY 1
010203040506 03032CFE06162CFE0203F0020AD2 DISCOVERY 1
010203040506 03032CFE06162CFE02AA91020AD3 DISCOVERY 1
010203040506 03032CFE06162CFE02C95C020AD4 DISCOVERY 1
010203040506 03032CFE06162CFE02D815020AD5 DISCOVERY 1
010203040506 03032CFE06162CFE02D886020AD6 DISCOVERY 1
010203040506 03032CFE06162CFE02DD4F020AD7 DISCOVERY 1
010203040506 03032CFE06162CFE02E2A9020AD8 DISCOVERY 1
010203040506 03032CFE06162CFE02F637020AD9 DISCOVERY 1
010203040506 03032CFE06162CFE0302F0020ADA DISCOVERY 1
010203040506 03032CFE06162CFE0303F0020ADB DISCOVERY 1
010203040506 03032CFE06162CFE035754020ADC DISCOVERY 1
010203040506 03032CFE06162CFE035764020ADD DISCOVERY 1
010203040506 03032CFE06162CFE038B91020ADE DISCOVERY 1
010203040506 03032CFE06162CFE038CC7020ADF DISCOVERY 1
010203040506 03032CFE06162CFE038F16020AE0 DISCOVERY 1
010203040506 03032CFE06162CFE039F8F020AE1 DISCOVERY 1
010203040506 03032CFE06162CFE03AA91020AE2 DISCOVERY 1
010203040506 03032CFE06162CFE03B716020AE3 DISCOVERY 1
010203040506 03032CFE06162CFE03C95C020AE4 DISCOVERY 1
010203040506 03032CFE06162CFE03C99C020AE5 DISCOVERY 1
010203040506 03032CFE06162CFE03F5D4020AE6 DISCOVERY 1
010203040506 03032CFE06162CFE0402F0020AE7 DISCOVERY 1
010203040506 03032CFE06162CFE0403F0020AE8 DISCOVERY 1
010203040506 03032CFE06162CFE045754020AE9 DISCOVERY 1
010203040506 03032CFE06162CFE045764020AEA DISCOVERY 1
010203040506 03032CFE06162CFE04AA91020AEB DISCOVERY 1
010203040506 03032CFE06162CFE04ACFC020AEC DISCOVERY 1
010203040506 03032CFE06162CFE04AFB8020AED DISCOVERY 1
010203040506 03032CFE06162CFE04C95C020AEE DISCOVERY 1
010203040506 03032CFE06162CFE0502F0020AEF DISCOVERY 1
010203040506 03032CFE06162CFE0503F0020AF0 DISCOVERY 1
010203040506 03032CFE06162CFE050F0C020AF1 DISCOVERY 1
010203040506 03032CFE06162CFE052CC7020AF2 DISCOVERY 1
010203040506 03032CFE06162CFE054B2D020AF3 DISCOVERY 1
010203040506 03032CFE06162CFE0577B1020AF4 DISCOVERY 1
010203040506 03032CFE06162CFE057802020AF5 DISCOVERY 1
010203040506 03032CFE06162CFE0582FD020AF6 DISCOVERY 1
010203040506 03032CFE06162CFE058D08020AF7 DISCOVERY 1
010203040506 03032CFE06162CFE05A963020AF8 DISCOVERY 1
010203040506 03032CFE06162CFE05A9BC020AF9 DISCOVERY 1
010203040506 03032CFE06162CFE05AA91020AFA DISCOVERY 1
010203040506 03032CFE06162CFE05C452020AFB DISCOVERY 1
010203040506 03032CFE06162CFE05C95C020AFC DISCOVERY 1
010203040506 03032CFE06162CFE060000020AFD DISCOVERY 1
010203040506 03032CFE06162CFE0602F0020AFE DISCOVERY 1
010203040506 03032CFE06162CFE0603F0020AFF DISCOVERY 1
010203040506 03032CFE06162CFE0660D7020B00 DISCOVERY 1
010203040506 03032CFE06162CFE06AE20020B01 DISCOVERY 1
010203040506 03032CFE06162CFE06C197020B02 DISCOVERY 1
010203040506 03032CFE06162CFE06C95C020B03 DISCOVERY 1
010203040506 03032CFE06162CFE06D8FC020B04 DISCOVERY 1
010203040506 03032CFE06162CFE070000020B05 DISCOVERY 1
010203040506 03032CFE06162CFE0702F0020B06 DISCOVERY 1
010203040506 03032CFE06162CFE0703F0020B07 DISCOVERY 1
010203040506 03032CFE06162CFE0X071C020B08 DISCOVERY 1
010203040506 03032CFE06162CFE0744B6020B09 DISCOVERY 1
010203040506 03032CFE06162CFE07A41C020B0A DISCOVERY 1
010203040506 03032CFE06162CFE07C95C020B0B DISCOVERY 1
010203040506 03032CFE06162CFE07F426020B0C DISCOVERY 1
010203040506 03032CFE06162CFE080000020B0D DISCOVERY 1
010203040506 03032CFE06162CFE0802F0020B0E DISCOVERY 1
010203040506 03032CFE06162CFE0803F0020B0F DISCOVERY 1
010203040506 03032CFE06162CFE090000020B10 DISCOVERY 1
010203040506 03032CFE06162CFE0903F0020B11 DISCOVERY 1
010203040506 03032CFE06162CFE0A0000020B12 DISCOVERY 1
010203040506 03032CFE06162CFE0B0000020B13 DISCOVERY 1
010203040506 03032CFE06162CFE0C0000020B14 DISCOVERY 1
010203040506 03032CFE06162CFE0DC6BF020B15 DISCOVERY 1
010203040506 03032CFE06162CFE0DC95C020B16 DISCOVERY 1
010203040506 03032CFE06162CFE0DEC2B020B17 DISCOVERY 1
010203040506 03032CFE06162CFE0E138D020B18 DISCOVERY 1
010203040506 03032CFE06162CFE0E30C3020B19 DISCOVERY 1
010203040506 03032CFE06162CFE0EC95C020B1A DISCOVERY 1
010203040506 03032CFE06162CFE0ECE95020B1B DISCOVERY 1
010203040506 03032CFE06162CFE0F0993020B1C DISCOVERY 1
010203040506 03032CFE06162CFE0F1B8D020B1D DISCOVERY 1
010203040506 03032CFE06162CFE0F232A020B1E DISCOVERY 1
010203040506 03032CFE06162CFE0F2D16020B1F DISCOVERY 1
010203040506 03032CFE06162CFE1E89A7020B20 DISCOVERY 1
010203040506 03032CFE06162CFE1E8B18020B21 DISCOVERY 1
010203040506 03032CFE06162CFE1E955B020B22 DISCOVERY 1
010203040506 03032CFE06162CFE1EC95C020B23 DISCOVERY 1
010203040506 03032CFE06162CFE1ED9F9020B24 DISCOVERY 1
010203040506 03032CFE06162CFE1EE890020B25 DISCOVERY 1
010203040506 03032CFE06162CFE1EEDF5020B26 DISCOVERY 1
010203040506 03032CFE06162CFE1F1101020B27 DISCOVERY 1
010203040506 03032CFE06162CFE1F181A020B28 DISCOVERY 1
010203040506 03032CFE06162CFE1F2E13020B29 DISCOVERY 1
010203040506 03032CFE06162CFE1F4589020B2A DISCOVERY 1
010203040506 03032CFE06162CFE1F4627020B2B DISCOVERY 1
010203040506 03032CFE06162CFE1F5865020B2C DISCOVERY 1
010203040506 03032CFE06162CFE1FBB50020B2D DISCOVERY 1
010203040506 03032CFE06162CFE1FC95C020B2E DISCOVERY 1
010203040506 03032CFE06162CFE1FE765020B2F DISCOVERY 1
010203040506 03032CFE06162CFE1FF8FA020B30 DISCOVERY 1
010203040506 03032CFE06162CFE201C7C020B31 DISCOVERY 1
010203040506 03032CFE06162CFE202B3D020B32 DISCOVERY 1
010203040506 03032CFE06162CFE20330C020B33 DISCOVERY 1
010203040506 03032CFE06162CFE20A19B020B34 DISCOVERY 1
010203040506 03032CFE06162CFE20C95C020B35 DISCOVERY 1
010203040506 03032CFE06162CFE20CC2C020B36 DISCOVERY 1
010203040506 03032CFE06162CFE213C8C020B37 DISCOVERY 1
010203040506 03032CFE06162CFE21521D020B38 DISCOVERY 1
010203040506 03032CFE06162CFE21A04E020B39 DISCOVERY 1
010203040506 03032CFE06162CFE2D7A23020B3A DISCOVERY 1
010203040506 03032CFE06162CFE350000020B3B DISCOVERY 1
010203040506 03032CFE06162CFE470000020B3C DISCOVERY 1
010203040506 03032CFE06162CFE480000020B3D DISCOVERY 1
010203040506 03032CFE06162CFE490000020B3E DISCOVERY 1
010203040506 03032CFE06162CFE5BA9B5020B3F DISCOVERY 1
010203040506 03032CFE06162CFE5BACD6020B40 DISCOVERY 1
010203040506 03032CFE06162CFE5BD6C9020B41 DISCOVERY 1
010203040506 03032CFE06162CFE5BE3D4020B42 DISCOVERY 1
010203040506 03032CFE06162CFE5C0206020B43 DISCOVERY 1
010203040506 03032CFE06162CFE5C0C84020B44 DISCOVERY 1
010203040506 03032CFE06162CFE5C4833020B45 DISCOVERY 1
010203040506 03032CFE06162CFE5C4A7E020B46 DISCOVERY 1
010203040506 03032CFE06162CFE5C55E7020B47 DISCOVERY 1
010203040506 03032CFE06162CFE5C7CDC020B48 DISCOVERY 1
010203040506 03032CFE06162CFE5C8AA5020B49 DISCOVERY 1
010203040506 03032CFE06162CFE5CC900020B4A DISCOVERY 1
010203040506 03032CFE06162CFE5CC901020B4B DISCOVERY 1
010203040506 03032CFE06162CFE5CC902020B4C DISCOVERY 1
010203040506 03032CFE06162CFE5CC903020B4D DISCOVERY 1
010203040506 03032CFE06162CFE5CC904020B4E DISCOVERY 1
010203040506 03032CFE06162CFE5CC905020B4F DISCOVERY 1
010203040506 03032CFE06162CFE5CC906020B50 DISCOVERY 1
010203040506 03032CFE06162CFE5CC907020B51 DISCOVERY 1
010203040506 03032CFE06162CFE5CC908020B52 DISCOVERY 1
010203040506 03032CFE06162CFE5CC909020B53 DISCOVERY 1
010203040506 03032CFE06162CFE5CC90A020B54 DISCOVERY 1
010203040506 03032CFE06162CFE5CC90B020B55 DISCOVERY 1
010203040506 03032CFE06162CFE5CC90C020B56 DISCOVERY 1
010203040506 03032CFE06162CFE5CC90D020B57 DISCOVERY 1
010203040506 03032CFE06162CFE5CC90E020B58 DISCOVERY 1
010203040506 03032CFE06162CFE5CC90F020B59 DISCOVERY 1
010203040506 03032CFE06162CFE5CC910020B5A DISCOVERY 1
010203040506 03032CFE06162CFE5CC911020B5B DISCOVERY 1
010203040506 03032CFE06162CFE5CC912020B5C DISCOVERY 1
010203040506 03032CFE06162CFE5CC913020B5D DISCOVERY 1
010203040506 03032CFE06162CFE5CC914020B5E DISCOVERY 1
010203040506 03032CFE06162CFE5CC915020B5F DISCOVERY 1
010203040506 03032CFE06162CFE5CC916020B60 DISCOVERY 1
010203040506 03032CFE06162CFE5CC917020B61 DISCOVERY 1
010203040506 03032CFE06162CFE5CC918020B62 DISCOVERY 1
010203040506 03032CFE06162CFE5CC919020B63 DISCOVERY 1
010203040506 03032CFE06162CFE5CC91A020B64 DISCOVERY 1
010203040506 03032CFE06162CFE5CC91B020B65 DISCOVERY 1
010203040506 03032CFE06162CFE5CC91C020B66 DISCOVERY 1
010203040506 03032CFE06162CFE5CC91D020B67 DISCOVERY 1
010203040506 03032CFE06162CFE5CC91E020B68 DISCOVERY 1
010203040506 03032CFE06162CFE5CC91F020B69 DISCOVERY 1
010203040506 03032CFE06162CFE5CC920020B6A DISCOVERY 1
010203040506 03032CFE06162CFE5CC921020B6B DISCOVERY 1
010203040506 03032CFE06162CFE5CC922020B6C DISCOVERY 1
010203040506 03032CFE06162CFE5CC923020B6D DISCOVERY 1
010203040506 03032CFE06162CFE5CC924020B6E DISCOVERY 1
010203040506 03032CFE06162CFE5CC925020B6F DISCOVERY 1
010203040506 03032CFE06162CFE5CC926020B70 DISCOVERY 1
010203040506 03032CFE06162CFE5CC927020B71 DISCOVERY 1
010203040506 03032CFE06162CFE5CC928020B72 DISCOVERY 1
010203040506 03032CFE06162CFE5CC929020B73 DISCOVERY 1
010203040506 03032CFE06162CFE5CC92A020B74 DISCOVERY 1
010203040506 03032CFE06162CFE5CC92B020B75 DISCOVERY 1
010203040506 03032CFE06162CFE5CC92C020B76 DISCOVERY 1
010203040506 03032CFE06162CFE5CC92D020B77 DISCOVERY 1
010203040506 03032CFE06162CFE5CC92E020B78 DISCOVERY 1
010203040506 03032CFE06162CFE5CC92F020B79 DISCOVERY 1
010203040506 03032CFE06162CFE5CC930020B7A DISCOVERY 1
010203040506 03032CFE06162CFE5CC931020B7B DISCOVERY 1
010203040506 03032CFE06162CFE5CC932020B7C DISCOVERY 1
010203040506 03032CFE06162CFE5CC933020B7D DISCOVERY 1
010203040506 03032CFE06162CFE5CC934020B7E DISCOVERY 1
010203040506 03032CFE06162CFE5CC935020B7F DISCOVERY 1
010203040506 03032CFE06162CFE5CC936020B80 DISCOVERY 1
010203040506 03032CFE06162CFE5CC937020B81 DISCOVERY 1
010203040506 03032CFE06162CFE5CC938020B82 DISCOVERY 1
010203040506 03032CFE06162CFE5CC939020B83 DISCOVERY 1
010203040506 03032CFE06162CFE5CC93A020B84 DISCOVERY 1
010203040506 03032CFE06162CFE5CC93B020B85 DISCOVERY 1
010203040506 03032CFE06162CFE5CC93C020B86 DISCOVERY 1
010203040506 03032CFE06162CFE5CC93D020B87 DISCOVERY 1
010203040506 03032CFE06162CFE5CC93E020B88 DISCOVERY 1
010203040506 03032CFE06162CFE5CC93F020B89 DISCOVERY 1
010203040506 03032CFE06162CFE5CC940020B8A DISCOVERY 1
010203040506 03032CFE06162CFE5CC941020B8B DISCOVERY 1
010203040506 03032CFE06162CFE5CC942020B8C DISCOVERY 1
010203040506 03032CFE06162CFE5CC943020B8D DISCOVERY 1
010203040506 03032CFE06162CFE5CC944020B8E DISCOVERY 1
010203040506 03032CFE06162CFE5CC945020B8F DISCOVERY 1
010203040506 03032CFE06162CFE5CEE3C020B90 DISCOVERY 1
010203040506 03032CFE06162CFE6AD226020B91 DISCOVERY 1
010203040506 03032CFE06162CFE6B1C64020B92 DISCOVERY 1
010203040506 03032CFE06162CFE6B8C65020B93 DISCOVERY 1
010203040506 03032CFE06162CFE6B9304020B94 DISCOVERY 1
010203040506 03032CFE06162CFE6BA5C3020B95 DISCOVERY 1
010203040506 03032CFE06162CFE6C42C0020B96 DISCOVERY 1
010203040506 03032CFE06162CFE6C4DE5020B97 DISCOVERY 1
010203040506 03032CFE06162CFE718FA4020B98 DISCOVERY 1
010203040506 03032CFE06162CFE72EF8D020B99 DISCOVERY 1
010203040506 03032CFE06162CFE72FB00020B9A DISCOVERY 1
010203040506 03032CFE06162CFE821F66020B9B DISCOVERY 1
010203040506 03032CFE06162CFE89BAD5020B9C DISCOVERY 1
010203040506 03032CFE06162CFE8A31B7020B9D DISCOVERY 1
010203040506 03032CFE06162CFE8A3D00020B9E DISCOVERY 1
010203040506 03032CFE06162CFE8A3D01020B9F DISCOVERY 1
010203040506 03032CFE06162CFE8A8F23020BA0 DISCOVERY 1
010203040506 03032CFE06162CFE8AADAE020BA1 DISCOVERY 1
010203040506 03032CFE06162CFE8B0A91020BA2 DISCOVERY 1
010203040506 03032CFE06162CFE8B5A7B020BA3 DISCOVERY 1
010203040506 03032CFE06162CFE8B66AB020BA4 DISCOVERY 1
010203040506 03032CFE06162CFE8BB0A0020BA5 DISCOVERY 1
010203040506 03032CFE06162CFE8BF79A020BA6 DISCOVERY 1
010203040506 03032CFE06162CFE8C07D2020BA7 DISCOVERY 1
010203040506 03032CFE06162CFE8C1706020BA8 DISCOVERY 1
010203040506 03032CFE06162CFE8C4236020BA9 DISCOVERY 1
010203040506 03032CFE06162CFE8C6B6A020BAA DISCOVERY 1
010203040506 03032CFE06162CFE8CAD81020BAB DISCOVERY 1
010203040506 03032CFE06162CFE8CB05C020BAC DISCOVERY 1
010203040506 03032CFE06162CFE8CD10F020BAD DISCOVERY 1
010203040506 03032CFE06162CFE8D13B9020BAE DISCOVERY 1
010203040506 03032CFE06162CFE8D16EA020BAF DISCOVERY 1
010203040506 03032CFE06162CFE8D5B67020BB0 DISCOVERY 1
010203040506 03032CFE06162CFE8E14D7020BB1 DISCOVERY 1
010203040506 03032CFE06162CFE8E1996020BB2 DISCOVERY 1
010203040506 03032CFE06162CFE8E4666020BB3 DISCOVERY 1
010203040506 03032CFE06162CFE8E5550020BB4 DISCOVERY 1
010203040506 03032CFE06162CFE9101F0020BB5 DISCOVERY 1
010203040506 03032CFE06162CFE9128CB020BB6 DISCOVERY 1
010203040506 03032CFE06162CFE913B0C020BB7 DISCOVERY 1
010203040506 03032CFE06162CFE915CFA020BB8 DISCOVERY 1
010203040506 03032CFE06162CFE9171BE020BB9 DISCOVERY 1
010203040506 03032CFE06162CFE917E46020BBA DISCOVERY 1
010203040506 03032CFE06162CFE91AA00020BBB DISCOVERY 1
010203040506 03032CFE06162CFE91AA01020BBC DISCOVERY 1
010203040506 03032CFE06162CFE91AA02020BBD DISCOVERY 1
010203040506 03032CFE06162CFE91AA03020BBE DISCOVERY 1
010203040506 03032CFE06162CFE91AA04020BBF DISCOVERY 1
010203040506 03032CFE06162CFE91AA05020BC0 DISCOVERY 1
010203040506 03032CFE06162CFE91BD38020BC1 DISCOVERY 1
010203040506 03032CFE06162CFE91C813020BC2 DISCOVERY 1
010203040506 03032CFE06162CFE91DABC020BC3 DISCOVERY 1
010203040506 03032CFE06162CFE92255E020BC4 DISCOVERY 1
010203040506 03032CFE06162CFE92BBBD020BC5 DISCOVERY 1
010203040506 03032CFE06162CFE989D0A020BC6 DISCOVERY 1
010203040506 03032CFE06162CFE9939BC020BC7 DISCOVERY 1
010203040506 03032CFE06162CFE994374020BC8 DISCOVERY 1
010203040506 03032CFE06162CFE997B4A020BC9 DISCOVERY 1
010203040506 03032CFE06162CFE99C87B020BCA DISCOVERY 1
010203040506 03032CFE06162CFE99D7EA020BCB DISCOVERY 1
010203040506 03032CFE06162CFE99F098020BCC DISCOVERY 1
010203040506 03032CFE06162CFE9A408A020BCD DISCOVERY 1
010203040506 03032CFE06162CFE9A9BDD020BCE DISCOVERY 1
010203040506 03032CFE06162CFE9ADB11020BCF DISCOVERY 1
010203040506 03032CFE06162CFE9AEEA4020BD0 DISCOVERY 1
010203040506 03032CFE06162CFE9B7339020BD1 DISCOVERY 1
010203040506 03032CFE06162CFE9B735A020BD2 DISCOVERY 1
010203040506 03032CFE06162CFE9B9872020BD3 DISCOVERY 1
010203040506 03032CFE06162CFE9BC64D020BD4 DISCOVERY 1
010203040506 03032CFE06162CFE9BE931020BD5 DISCOVERY 1
010203040506 03032CFE06162CFE9C0AF7020BD6 DISCOVERY 1
010203040506 03032CFE06162CFE9C3997020BD7 DISCOVERY 1
010203040506 03032CFE06162CFE9C4058020BD8 DISCOVERY 1
010203040506 03032CFE06162CFE9C6BC0020BD9 DISCOVERY 1
010203040506 03032CFE06162CFE9C888B020BDA DISCOVERY 1
010203040506 03032CFE06162CFE9C98DB020BDB DISCOVERY 1
010203040506 03032CFE06162CFE9CA277020BDC DISCOVERY 1
010203040506 03032CFE06162CFE9CB5F3020BDD DISCOVERY 1
010203040506 03032CFE06162CFE9CB881020BDE DISCOVERY 1
010203040506 03032CFE06162CFE9CD0F3020BDF DISCOVERY 1
010203040506 03032CFE06162CFE9CE3C7020BE0 DISCOVERY 1
010203040506 03032CFE06162CFE9CEFD1020BE1 DISCOVERY 1
010203040506 03032CFE06162CFE9CF08F020BE2 DISCOVERY 1
010203040506 03032CFE06162CFE9D00A6020BE3 DISCOVERY 1
010203040506 03032CFE06162CFE9D7D42020BE4 DISCOVERY 1
010203040506 03032CFE06162CFE9DB896020BE5 DISCOVERY 1
010203040506 03032CFE06162CFEA7E52B020BE6 DISCOVERY 1
010203040506 03032CFE06162CFEA7EF76020BE7 DISCOVERY 1
010203040506 03032CFE06162CFEA8001A020BE8 DISCOVERY 1
010203040506 03032CFE06162CFEA83C10020BE9 DISCOVERY 1
010203040506 03032CFE06162CFEA8658F020BEA DISCOVERY 1
010203040506 03032CFE06162CFEA8845A020BEB DISCOVERY 1
010203040506 03032CFE06162CFEA88B69020BEC DISCOVERY 1
010203040506 03032CFE06162CFEA8A00E020BED DISCOVERY 1
010203040506 03032CFE06162CFEA8A72A020BEE DISCOVERY 1
010203040506 03032CFE06162CFEA8C636020BEF DISCOVERY 1
010203040506 03032CFE06162CFEA8CAAD020BF0 DISCOVERY 1
010203040506 03032CFE06162CFEA8E353020BF1 DISCOVERY 1
010203040506 03032CFE06162CFEA8F96D020BF2 DISCOVERY 1
010203040506 03032CFE06162CFEA90358020BF3 DISCOVERY 1
010203040506 03032CFE06162CFEA92498020BF4 DISCOVERY 1
010203040506 03032CFE06162CFEA9394A020BF5 DISCOVERY 1
010203040506 03032CFE06162CFEC6936A020BF6 DISCOVERY 1
010203040506 03032CFE06162CFEC69AFD020BF7 DISCOVERY 1
010203040506 03032CFE06162CFEC6ABEA020BF8 DISCOVERY 1
010203040506 03032CFE06162CFEC6EC5F020BF9 DISCOVERY 1
010203040506 03032CFE06162CFEC7736C020BFA DISCOVERY 1
010203040506 03032CFE06162CFEC79B91020BFB DISCOVERY 1
010203040506 03032CFE06162CFEC7A267020BFC DISCOVERY 1
010203040506 03032CFE06162CFEC7D620020BFD DISCOVERY 1
010203040506 03032CFE06162CFEC7FBCC020BFE DISCOVERY 1
010203040506 03032CFE06162CFEC8162A020BFF DISCOVERY 1
010203040506 03032CFE06162CFEC85D7A020C00 DISCOVERY 1
010203040506 03032CFE06162CFEC8777E020C01 DISCOVERY 1
010203040506 03032CFE06162CFEC878AA020C02 DISCOVERY 1
010203040506 03032CFE06162CFEC8C641020C03 DISCOVERY 1
010203040506 03032CFE06162CFEC8D335020C04 DISCOVERY 1
010203040506 03032CFE06162CFEC8E228020C05 DISCOVERY 1
010203040506 03032CFE06162CFEC9186B020C06 DISCOVERY 1
010203040506 03032CFE06162CFEC9836A020C07 DISCOVERY 1
010203040506 03032CFE06162CFECA7030020C08 DISCOVERY 1
010203040506 03032CFE06162CFECAB6B8020C09 DISCOVERY 1
010203040506 03032CFE06162CFECAF511020C0A DISCOVERY 1
010203040506 03032CFE06162CFECB093B020C0B DISCOVERY 1
010203040506 03032CFE06162CFECB529D020C0C DISCOVERY 1
010203040506 03032CFE06162CFECC438E020C0D DISCOVERY 1
010203040506 03032CFE06162CFECC5F29020C0E DISCOVERY 1
010203040506 03032CFE06162CFECC754F020C0F DISCOVERY 1
010203040506 03032CFE06162CFECC93A5020C10 DISCOVERY 1
010203040506 03032CFE06162CFECCBB7E020C11 DISCOVERY 1
010203040506 03032CFE06162CFECD8256020C12 DISCOVERY 1
010203040506 03032CFE06162CFED446A7020C13 DISCOVERY 1
010203040506 03032CFE06162CFED5A59E020C14 DISCOVERY 1
010203040506 03032CFE06162CFED5B5F7020C15 DISCOVERY 1
010203040506 03032CFE06162CFED5C6CE020C16 DISCOVERY 1
010203040506 03032CFE06162CFED654CD020C17 DISCOVERY 1
010203040506 03032CFE06162CFED65F4E020C18 DISCOVERY 1
010203040506 03032CFE06162CFED69B2B020C19 DISCOVERY 1
010203040506 03032CFE06162CFED6C195020C1A DISCOVERY 1
010203040506 03032CFE06162CFED6E870020C1B DISCOVERY 1
010203040506 03032CFE06162CFED6EE84020C1C DISCOVERY 1
010203040506 03032CFE06162CFED7102F020C1D DISCOVERY 1
010203040506 03032CFE06162CFED7E3EB020C1E DISCOVERY 1
010203040506 03032CFE06162CFED8058C020C1F DISCOVERY 1
010203040506 03032CFE06162CFED820EA020C20 DISCOVERY 1
010203040506 03032CFE06162CFED87A3E020C21 DISCOVERY 1
010203040506 03032CFE06162CFED8F3BA020C22 DISCOVERY 1
010203040506 03032CFE06162CFED8F4E8020C23 DISCOVERY 1
010203040506 03032CFE06162CFED90617020C24 DISCOVERY 1
010203040506 03032CFE06162CFED933A7020C25 DISCOVERY 1
010203040506 03032CFE06162CFED9414F020C26 DISCOVERY 1
010203040506 03032CFE06162CFED97EBA020C27 DISCOVERY 1
010203040506 03032CFE06162CFED9964B020C28 DISCOVERY 1
010203040506 03032CFE06162CFEDA0F83020C29 DISCOVERY 1
010203040506 03032CFE06162CFEDA4577020C2A DISCOVERY 1
010203040506 03032CFE06162CFEDA5200020C2B DISCOVERY 1
010203040506 03032CFE06162CFEDAD3A6020C2C DISCOVERY 1
010203040506 03032CFE06162CFEDADE43020C2D DISCOVERY 1
010203040506 03032CFE06162CFEDAE096020C2E DISCOVERY 1
010203040506 03032CFE06162CFEDB8AC7020C2F DISCOVERY 1
010203040506 03032CFE06162CFEDBE5B1020C30 DISCOVERY 1
010203040506 03032CFE06162CFEDC5249020C31 DISCOVERY 1
010203040506 03032CFE06162CFEDCF33C020C32 DISCOVERY 1
010203040506 03032CFE06162CFEDD4EC0020C33 DISCOVERY 1
010203040506 03032CFE06162CFEDE215D020C34 DISCOVERY 1
010203040506 03032CFE06162CFEDE577F020C35 DISCOVERY 1
010203040506 03032CFE06162CFEDEC04C020C36 DISCOVERY 1
010203040506 03032CFE06162CFEDEDD6F020C37 DISCOVERY 1
010203040506 03032CFE06162CFEDEE8C0020C38 DISCOVERY 1
010203040506 03032CFE06162CFEDEEA86020C39 DISCOVERY 1
010203040506 03032CFE06162CFEDEF234020C3A DISCOVERY 1
010203040506 03032CFE06162CFEDF01E3020C3B DISCOVERY 1
010203040506 03032CFE06162CFEDF271C020C3C DISCOVERY 1
010203040506 03032CFE06162CFEDF42DE020C3D DISCOVERY 1
010203040506 03032CFE06162CFEDF4B02020C3E DISCOVERY 1
010203040506 03032CFE06162CFEDF9BA4020C3F DISCOVERY 1
010203040506 03032CFE06162CFEDFD433020C40 DISCOVERY 1
010203040506 03032CFE06162CFEE020C1020C41 DISCOVERY 1
010203040506 03032CFE06162CFEE06116020C42 DISCOVERY 1
010203040506 03032CFE06162CFEE07634020C43 DISCOVERY 1
010203040506 03032CFE06162CFEE09172020C44 DISCOVERY 1
010203040506 03032CFE06162CFEE4E457020C45 DISCOVERY 1
010203040506 03032CFE06162CFEE5440B020C46 DISCOVERY 1
010203040506 03032CFE06162CFEE57363020C47 DISCOVERY 1
010203040506 03032CFE06162CFEE57B57020C48 DISCOVERY 1
010203040506 03032CFE06162CFEE5B4B0020C49 DISCOVERY 1
010203040506 03032CFE06162CFEE5B91B020C4A DISCOVERY 1
010203040506 03032CFE06162CFEE5E2E9020C4B DISCOVERY 1
010203040506 03032CFE06162CFEE64613020C4C DISCOVERY 1
010203040506 03032CFE06162CFEE64CC6020C4D DISCOVERY 1
010203040506 03032CFE06162CFEE69877020C4E DISCOVERY 1
010203040506 03032CFE06162CFEE6E37E020C4F DISCOVERY 1
010203040506 03032CFE06162CFEE6E771020C50 DISCOVERY 1
010203040506 03032CFE06162CFEE6E8B8020C51 DISCOVERY 1
010203040506 03032CFE06162CFEE750CE020C52 DISCOVERY 1
010203040506 03032CFE06162CFEF00000020C53 DISCOVERY 1
010203040506 03032CFE06162CFEF00001020C54 DISCOVERY 1
010203040506 03032CFE06162CFEF00002020C55 DISCOVERY 1
010203040506 03032CFE06162CFEF00200020C56 DISCOVERY 1
010203040506 03032CFE06162CFEF00201020C57 DISCOVERY 1
010203040506 03032CFE06162CFEF00202020C58 DISCOVERY 1
010203040506 03032CFE06162CFEF00203020C59 DISCOVERY 1
010203040506 03032CFE06162CFEF00204020C5A DISCOVERY 1
010203040506 03032CFE06162CFEF00205020C5B DISCOVERY 1
010203040506 03032CFE06162CFEF00206020C5C DISCOVERY 1
010203040506 03032CFE06162CFEF00207020C5D DISCOVERY 1
010203040506 03032CFE06162CFEF00208020C5E DISCOVERY 1
010203040506 03032CFE06162CFEF00209020C5F DISCOVERY 1
010203040506 03032CFE06162CFEF0020A020C60 DISCOVERY 1
010203040506 03032CFE06162CFEF0020B020C61 DISCOVERY 1
010203040506 03032CFE06162CFEF0020C020C62 DISCOVERY 1
010203040506 03032CFE06162CFEF0020D020C63 DISCOVERY 1
010203040506 03032CFE06162CFEF0020E020C64 DISCOVERY 1
010203040506 03032CFE06162CFEF0020F020C65 DISCOVERY 1
010203040506 03032CFE06162CFEF00210020C66 DISCOVERY 1
010203040506 03032CFE06162CFEF00211020C67 DISCOVERY 1
010203040506 03032CFE06162CFEF00212020C68 DISCOVERY 1
010203040506 03032CFE06162CFEF00213020C69 DISCOVERY 1
010203040506 03032CFE06162CFEF00214020C6A DISCOVERY 1
010203040506 03032CFE06162CFEF00215020C6B DISCOVERY 1
010203040506 03032CFE06162CFEF00300020C6C DISCOVERY 1
010203040506 03032CFE06162CFEF00301020C6D DISCOVERY 1
010203040506 03032CFE06162CFEF00302020C6E DISCOVERY 1
010203040506 03032CFE06162CFEF00303020C6F DISCOVERY 1
010203040506 03032CFE06162CFEF00304020C70 DISCOVERY 1
010203040506 03032CFE06162CFEF00305020C71 DISCOVERY 1
010203040506 03032CFE06162CFEF00306020C72 DISCOVERY 1
010203040506 03032CFE06162CFEF00307020C73 DISCOVERY 1
010203040506 03032CFE06162CFEF00308020C74 DISCOVERY 1
010203040506 03032CFE06162CFEF00309020C75 DISCOVERY 1
010203040506 03032CFE06162CFEF00400020C76 DISCOVERY 1
010203040506 03032CFE06162CFEF00E97020C77 DISCOVERY 1
+1 -1
View File
@@ -1 +1 @@
010203040506 00112233445566778899AABBCCDDEEFF 1000
010203040506 00112233445566778899AABBCCDDEEFF DISCOVERY 1000
+1 -1
View File
@@ -1 +1 @@
010203040506 190953445220426c7565746f6f7468204c6f7720456e65726779 1000
010203040506 190953445220426c7565746f6f7468204c6f7720456e65726779 DISCOVERY 1000