mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-09-04 05:49:01 +00:00
Compare commits
69 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b7b4a10485 | |||
| cca0e18f5a | |||
| 8846926b68 | |||
| 7ceefb1b81 | |||
| 861f684d00 | |||
| 17b146b960 | |||
| 02251eeeb4 | |||
| c486572d3d | |||
| a6ed6e3099 | |||
| 175e5e2e8c | |||
| 7bc27038be | |||
| 609bf3219f | |||
| bdbd616a84 | |||
| 1cbe5cb293 | |||
| df6003c08f | |||
| 50fcbf0f5a | |||
| 5a1676a0b9 | |||
| 309f2fbd2c | |||
| 650aacfaa7 | |||
| 8655027a5b | |||
| de937f02d4 | |||
| 2c0446e83d | |||
| 9e6699d8cc | |||
| ef92c5bc0a | |||
| 10f6600f52 | |||
| aeb607e73b | |||
| 8479d2edf0 | |||
| d2e9a8dc06 | |||
| 4571cb0b61 | |||
| 351f7b13b3 | |||
| 5953cb57b0 | |||
| b60b873428 | |||
| a3249cae26 | |||
| f7f784c0f4 | |||
| f4f538f69b | |||
| bbd1a5a2ef | |||
| bc71238046 | |||
| a9df9dde69 | |||
| 7a87e3f3af | |||
| 68f960e4e7 | |||
| 6784fe72dd | |||
| 312df0fcec | |||
| 2913a41aec | |||
| adabbfbef1 | |||
| 2b7962fa7c | |||
| 645f02e2f3 | |||
| b178462e47 | |||
| 1639348b94 | |||
| f67fe262bb | |||
| 0681117af6 | |||
| b5c244a09b | |||
| dceb7255b0 | |||
| 62307b93d7 | |||
| 61ce8b11a6 | |||
| 211f40ddf3 | |||
| 01dd0c79ed | |||
| be7b402c95 | |||
| a11929d2ca | |||
| 0bacb31d17 | |||
| cc7e4aec8a | |||
| 15c338e5b3 | |||
| 80bfcd5eb1 | |||
| b96f14762f | |||
| 0feacfa102 | |||
| a1496c1b79 | |||
| 1b73a138b6 | |||
| efcedd9005 | |||
| 86d4b17257 | |||
| f6a437f7fb |
+2
-1
@@ -55,10 +55,11 @@ CMakeFiles/
|
||||
# Debugging
|
||||
.gdbinit*
|
||||
|
||||
# Editor files
|
||||
# Editor/IDE files
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
.vscode
|
||||
.idea
|
||||
|
||||
# VSCodium extensions
|
||||
.history
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# PortaPack Mayhem
|
||||
|
||||
[](https://travis-ci.com/eried/portapack-mayhem) [](https://github.com/eried/portapack-mayhem/actions/workflows/create_nightly_release.yml) [](https://codescene.io/projects/8381) [](https://github.com/eried/portapack-mayhem/releases) [](https://github.com/eried/portapack-mayhem/releases/latest) [](https://hub.docker.com/r/eried/portapack) [](https://discord.gg/tuwVMv3) [](https://www.bountysource.com/teams/portapack-mayhem/issues)
|
||||
[](https://travis-ci.com/eried/portapack-mayhem) [](https://github.com/eried/portapack-mayhem/actions/workflows/create_nightly_release.yml) [](https://codescene.io/projects/8381) [](https://github.com/eried/portapack-mayhem/releases) [](https://github.com/eried/portapack-mayhem/releases/latest) [](https://hub.docker.com/r/eried/portapack) [](https://discord.gg/tuwVMv3)
|
||||
|
||||
This is a fork of the [Havoc](https://github.com/furrtek/portapack-havoc/) firmware, which itself was a fork of the [PortaPack](https://github.com/sharebrained/portapack-hackrf) firmware, an add-on for the [HackRF](http://greatscottgadgets.com/hackrf/). A fork is a derivate, in this case one that has extra features and fixes when compared to the older versions.
|
||||
|
||||
@@ -20,7 +20,9 @@ This repository expands upon the previous work by many people and aims to consta
|
||||
|
||||
## What to buy?
|
||||
|
||||
:heavy_check_mark: A recommended one is this [PortaPack H2](https://s.click.aliexpress.com/e/_DmU7GQX) pack, that includes everything you need. Sadly, the people making the H2 never made the updated schematics available (against the terms of the license).
|
||||
:heavy_check_mark: A recommended one is this [PortaPack H2](https://s.click.aliexpress.com/e/_DmU7GQX), that includes everything you need with the plastic case "inspired" on [this](https://github.com/eried/portapack-mayhem/wiki/H2-Enclosure).
|
||||
|
||||
:heavy_check_mark: Our friends at OpenSourceSDRLab give away five units every three months in our discord (check the badge on top) of their [PortaPack H2](https://www.aliexpress.com/item/4000247041639.html?gatewayAdapt=4itemAdapt), you can support them too by ordering.
|
||||
|
||||
:heavy_check_mark: Another popular option is the clone of the [PortaPack H1](https://s.click.aliexpress.com/e/_Dkbqs2X).
|
||||
|
||||
@@ -48,9 +50,11 @@ To support the people behind the hardware, please buy a genuine [HackRF](https:/
|
||||
## What if I really want something specific?
|
||||
If what you need can be relevant in general, you can [request a feature](https://github.com/eried/portapack-mayhem/issues/new?labels=enhancement&template=feature_request.md).
|
||||
|
||||
You can create a bounty and invite people to your own bounty. This will incentivize coders to work on a new feature, solving a bug or even writting documentation. Start a bounty by [creating](https://github.com/eried/portapack-mayhem/issues/new/choose) or [choosing](https://github.com/eried/portapack-mayhem/issues/) an existing issue. Then, go to [Bountysource](https://www.bountysource.com/) and post a bounty using the link to that specific [issue](https://www.bountysource.com/teams/portapack-mayhem/issues).
|
||||
<del>You can create a bounty and invite people to your own bounty. This will incentivize coders to work on a new feature, solving a bug or even writting documentation. Start a bounty by [creating](https://github.com/eried/portapack-mayhem/issues/new/choose) or [choosing](https://github.com/eried/portapack-mayhem/issues/) an existing issue. Then, go to [Bountysource](https://www.bountysource.com/) and post a bounty using the link to that specific [issue](https://www.bountysource.com/teams/portapack-mayhem/issues).</del>
|
||||
|
||||
Promote your bounty over our Discord by clicking the chat badge on [top](#portapack-mayhem).
|
||||
<del>Promote your bounty over our Discord by clicking the chat badge on [top](#portapack-mayhem).</del>
|
||||
|
||||
Bountysource has not been reliable lately, so until this changes, please **DO NOT** post a bounty there. Go to our Discord by clicking the chat badge on [top](#portapack-mayhem) and discuss there.
|
||||
|
||||
## What if I need help?
|
||||
First, check the [documentation](https://github.com/eried/portapack-mayhem/wiki). If you find a bug or you think the problem is related to the current repository, please open an [issue](https://github.com/eried/portapack-mayhem/issues/new/choose).
|
||||
|
||||
@@ -244,6 +244,9 @@ set(CPPSRC
|
||||
apps/ais_app.cpp
|
||||
apps/analog_audio_app.cpp
|
||||
apps/analog_tv_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/gps_sim_app.cpp
|
||||
@@ -268,6 +271,7 @@ set(CPPSRC
|
||||
apps/ui_fileman.cpp
|
||||
apps/ui_flash_utility.cpp
|
||||
apps/ui_freqman.cpp
|
||||
apps/ui_fsk_rx.cpp
|
||||
apps/ui_iq_trim.cpp
|
||||
apps/ui_jammer.cpp
|
||||
# apps/ui_keyfob.cpp
|
||||
@@ -303,6 +307,7 @@ set(CPPSRC
|
||||
apps/ui_touch_calibration.cpp
|
||||
apps/ui_touchtunes.cpp
|
||||
apps/ui_view_wav.cpp
|
||||
apps/ui_weatherstation.cpp
|
||||
apps/ui_whipcalc.cpp
|
||||
protocols/aprs.cpp
|
||||
protocols/ax25.cpp
|
||||
|
||||
@@ -156,6 +156,7 @@ bool save_settings(std::string_view store_name, const SettingBindings& bindings)
|
||||
File f;
|
||||
auto path = get_settings_path(std::string{store_name});
|
||||
|
||||
make_new_directory(SETTINGS_DIR);
|
||||
auto error = f.create(path);
|
||||
if (error)
|
||||
return false;
|
||||
@@ -249,7 +250,7 @@ SettingsManager::SettingsManager(
|
||||
settings_.options = options;
|
||||
|
||||
// Pre-alloc enough for app settings and additional settings.
|
||||
additional_settings.reserve(17 + additional_settings.size());
|
||||
additional_settings.reserve(COMMON_APP_SETTINGS_COUNT + additional_settings.size());
|
||||
bindings_ = std::move(additional_settings);
|
||||
|
||||
// Settings should always be loaded because apps now rely
|
||||
|
||||
@@ -36,9 +36,14 @@
|
||||
#include "max283x.hpp"
|
||||
#include "string_format.hpp"
|
||||
|
||||
// Folder to store app settings, pmem_fileflag, and date_fileflag
|
||||
#define SETTINGS_DIR u"/SETTINGS"
|
||||
|
||||
// Bring in the string_view literal.
|
||||
using std::literals::operator""sv;
|
||||
|
||||
#define COMMON_APP_SETTINGS_COUNT 19
|
||||
|
||||
/* Represents a named setting bound to a variable instance. */
|
||||
/* Using void* instead of std::variant, because variant is a pain to dispatch over. */
|
||||
class BoundSetting {
|
||||
@@ -140,8 +145,8 @@ struct AppSettings {
|
||||
uint8_t nbfm_config_index = 0;
|
||||
uint8_t wfm_config_index = 0;
|
||||
uint8_t squelch = 80;
|
||||
|
||||
uint8_t volume;
|
||||
// NOTE: update COMMON_APP_SETTINGS_COUNT when adding to this
|
||||
};
|
||||
|
||||
/* Copies common values to the receiver/transmitter models. */
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2017 Furrtek
|
||||
* Copyright (C) 2023 TJ Baginski
|
||||
*
|
||||
* 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 "ble_comm_app.hpp"
|
||||
|
||||
#include "ui_modemsetup.hpp"
|
||||
|
||||
#include "modems.hpp"
|
||||
#include "audio.hpp"
|
||||
#include "rtc_time.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include "ui_text.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
using namespace modems;
|
||||
|
||||
void BLECommLogger::log_raw_data(const std::string& data) {
|
||||
log_file.write_entry(data);
|
||||
}
|
||||
|
||||
namespace ui {
|
||||
static std::uint64_t get_freq_by_channel_number(uint8_t channel_number) {
|
||||
uint64_t freq_hz;
|
||||
|
||||
switch (channel_number) {
|
||||
case 37:
|
||||
freq_hz = 2'402'000'000ull;
|
||||
break;
|
||||
case 38:
|
||||
freq_hz = 2'426'000'000ull;
|
||||
break;
|
||||
case 39:
|
||||
freq_hz = 2'480'000'000ull;
|
||||
break;
|
||||
case 0 ... 10:
|
||||
freq_hz = 2'404'000'000ull + channel_number * 2'000'000ull;
|
||||
break;
|
||||
case 11 ... 36:
|
||||
freq_hz = 2'428'000'000ull + (channel_number - 11) * 2'000'000ull;
|
||||
break;
|
||||
default:
|
||||
freq_hz = UINT64_MAX;
|
||||
}
|
||||
|
||||
return freq_hz;
|
||||
}
|
||||
|
||||
void BLECommView::focus() {
|
||||
options_channel.focus();
|
||||
}
|
||||
|
||||
BLECommView::BLECommView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
add_children({&rssi,
|
||||
&channel,
|
||||
&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,
|
||||
&console});
|
||||
|
||||
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) {
|
||||
str_log = "";
|
||||
logging = v;
|
||||
|
||||
if (logger && logging)
|
||||
logger->append(LOG_ROOT_DIR "/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();
|
||||
}
|
||||
|
||||
void BLECommView::set_parent_rect(const Rect new_parent_rect) {
|
||||
View::set_parent_rect(new_parent_rect);
|
||||
const Rect content_rect{0, header_height, new_parent_rect.width(), new_parent_rect.height() - header_height};
|
||||
console.set_parent_rect(content_rect);
|
||||
}
|
||||
|
||||
BLECommView::~BLECommView() {
|
||||
receiver_model.disable();
|
||||
transmitter_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
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);
|
||||
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;
|
||||
|
||||
bleTxPacket.packetType = PKT_TYPE_DISCOVERY;
|
||||
|
||||
return bleTxPacket;
|
||||
}
|
||||
|
||||
void BLECommView::sendAdvertisement(bool enable) {
|
||||
if (enable) {
|
||||
startTx(advertisePacket);
|
||||
is_adv = true;
|
||||
} else {
|
||||
is_adv = false;
|
||||
stopTx();
|
||||
}
|
||||
}
|
||||
|
||||
void BLECommView::startTx(BLETxPacket packetToSend) {
|
||||
int randomChannel = channel_number_tx;
|
||||
|
||||
if (auto_channel) {
|
||||
int min = 37;
|
||||
int max = 39;
|
||||
|
||||
randomChannel = min + std::rand() % (max - min + 1);
|
||||
|
||||
field_frequency.set_value(get_freq_by_channel_number(randomChannel));
|
||||
}
|
||||
|
||||
if (!in_tx_mode()) {
|
||||
switch_rx_tx(false);
|
||||
|
||||
currentPacket = packetToSend;
|
||||
packet_counter = currentPacket.packet_count;
|
||||
|
||||
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 ((packet_counter % 10) == 0) {
|
||||
text_packets_sent.set(to_string_dec_uint(packet_counter));
|
||||
}
|
||||
|
||||
is_sending = true;
|
||||
|
||||
packet_counter--;
|
||||
}
|
||||
|
||||
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);
|
||||
receiver_model.enable();
|
||||
|
||||
is_running_tx = false;
|
||||
}
|
||||
|
||||
void BLECommView::switch_rx_tx(bool inRxMode) {
|
||||
if (inRxMode) {
|
||||
// Start Rx
|
||||
transmitter_model.disable();
|
||||
baseband::shutdown();
|
||||
baseband::run_image(portapack::spi_flash::image_tag_btle_rx);
|
||||
} else {
|
||||
// Start Tx
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
baseband::run_image(portapack::spi_flash::image_tag_btle_tx);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BLECommView::on_tx_progress(const bool done) {
|
||||
if (done) {
|
||||
if (in_tx_mode()) {
|
||||
is_sending = false;
|
||||
|
||||
if (packet_counter == 0) {
|
||||
if (is_adv) {
|
||||
sendAdvertisement(false);
|
||||
} else {
|
||||
stopTx();
|
||||
}
|
||||
} else {
|
||||
startTx(currentPacket);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BLECommView::parse_received_packet(const BlePacketData* packet, ADV_PDU_TYPE pdu_type) {
|
||||
std::string data_string;
|
||||
|
||||
int i;
|
||||
|
||||
for (i = 0; i < packet->dataLen; i++) {
|
||||
data_string += to_string_hex(packet->data[i], 2);
|
||||
}
|
||||
|
||||
receivedPacket.dbValue = packet->max_dB;
|
||||
receivedPacket.timestamp = to_string_timestamp(rtc_time::now());
|
||||
receivedPacket.dataString = data_string;
|
||||
|
||||
receivedPacket.packetData.type = packet->type;
|
||||
receivedPacket.packetData.size = packet->size;
|
||||
receivedPacket.packetData.dataLen = packet->dataLen;
|
||||
|
||||
receivedPacket.packetData.macAddress[0] = packet->macAddress[0];
|
||||
receivedPacket.packetData.macAddress[1] = packet->macAddress[1];
|
||||
receivedPacket.packetData.macAddress[2] = packet->macAddress[2];
|
||||
receivedPacket.packetData.macAddress[3] = packet->macAddress[3];
|
||||
receivedPacket.packetData.macAddress[4] = packet->macAddress[4];
|
||||
receivedPacket.packetData.macAddress[5] = packet->macAddress[5];
|
||||
|
||||
receivedPacket.numHits++;
|
||||
|
||||
for (int i = 0; i < packet->dataLen; i++) {
|
||||
receivedPacket.packetData.data[i] = packet->data[i];
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2017 Furrtek
|
||||
* Copyright (C) 2023 TJ Baginski
|
||||
*
|
||||
* 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 __BLE_COMM_APP_H__
|
||||
#define __BLE_COMM_APP_H__
|
||||
|
||||
#include "ble_rx_app.hpp"
|
||||
#include "ble_tx_app.hpp"
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_transmitter.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "ui_record_view.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "log_file.hpp"
|
||||
#include "utility.hpp"
|
||||
|
||||
#include "recent_entries.hpp"
|
||||
|
||||
class BLECommLogger {
|
||||
public:
|
||||
Optional<File::Error> append(const std::string& filename) {
|
||||
return log_file.append(filename);
|
||||
}
|
||||
|
||||
void log_raw_data(const std::string& data);
|
||||
|
||||
private:
|
||||
LogFile log_file{};
|
||||
};
|
||||
|
||||
namespace ui {
|
||||
class BLECommView : public View {
|
||||
public:
|
||||
BLECommView(NavigationView& nav);
|
||||
~BLECommView();
|
||||
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
void paint(Painter&) override{};
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "BLE Comm"; };
|
||||
|
||||
private:
|
||||
BLETxPacket build_adv_packet();
|
||||
void switch_rx_tx(bool inRxMode);
|
||||
void startTx(BLETxPacket packetToSend);
|
||||
void stopTx();
|
||||
void toggle();
|
||||
bool in_tx_mode() const;
|
||||
void on_timer();
|
||||
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);
|
||||
|
||||
NavigationView& nav_;
|
||||
|
||||
RxRadioState radio_state_rx_{
|
||||
2'402'000'000 /* frequency */,
|
||||
4'000'000 /* bandwidth */,
|
||||
4'000'000 /* sampling rate */,
|
||||
ReceiverModel::Mode::WidebandFMAudio};
|
||||
|
||||
TxRadioState radio_state_tx_{
|
||||
2'402'000'000 /* frequency */,
|
||||
4'000'000 /* bandwidth */,
|
||||
4'000'000 /* sampling rate */
|
||||
};
|
||||
|
||||
app_settings::SettingsManager settings_{
|
||||
"BLE Comm Tx", app_settings::Mode::RX_TX};
|
||||
|
||||
uint8_t console_color{0};
|
||||
uint32_t prev_value{0};
|
||||
|
||||
uint8_t channel_number_tx = 37;
|
||||
uint8_t channel_number_rx = 37;
|
||||
bool auto_channel = false;
|
||||
|
||||
char randomMac[13] = "010203040506";
|
||||
|
||||
bool is_running_tx = false;
|
||||
bool is_sending = false;
|
||||
bool is_adv = false;
|
||||
|
||||
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)
|
||||
|
||||
uint32_t packet_counter{0};
|
||||
BLETxPacket advertisePacket{};
|
||||
BLETxPacket currentPacket{};
|
||||
BleRecentEntry receivedPacket{};
|
||||
|
||||
static constexpr auto header_height = 5 * 16;
|
||||
|
||||
OptionsField options_channel{
|
||||
{0 * 8, 0 * 8},
|
||||
5,
|
||||
{{"Ch.37 ", 37},
|
||||
{"Ch.38", 38},
|
||||
{"Ch.39", 39},
|
||||
{"Auto", 40}}};
|
||||
|
||||
RxFrequencyField field_frequency{
|
||||
{6 * 8, 0 * 16},
|
||||
nav_};
|
||||
|
||||
RFAmpField field_rf_amp{
|
||||
{16 * 8, 0 * 16}};
|
||||
|
||||
LNAGainField field_lna{
|
||||
{18 * 8, 0 * 16}};
|
||||
|
||||
VGAGainField field_vga{
|
||||
{21 * 8, 0 * 16}};
|
||||
|
||||
RSSI rssi{
|
||||
{24 * 8, 0, 6 * 8, 4}};
|
||||
|
||||
Channel channel{
|
||||
{24 * 8, 5, 6 * 8, 4}};
|
||||
|
||||
Labels label_send_adv{
|
||||
{{0 * 8, 2 * 8}, "Send Advertisement:", Color::light_grey()}};
|
||||
|
||||
ImageButton button_send_adv{
|
||||
{21 * 8, 1 * 16, 10 * 8, 2 * 16},
|
||||
&bitmap_play,
|
||||
Color::green(),
|
||||
Color::black()};
|
||||
|
||||
Checkbox check_log{
|
||||
{24 * 8, 2 * 8},
|
||||
3,
|
||||
"Log",
|
||||
true};
|
||||
|
||||
Labels label_packets_sent{
|
||||
{{0 * 8, 4 * 8}, "Packets Left:", Color::light_grey()}};
|
||||
|
||||
Text text_packets_sent{
|
||||
{13 * 8, 2 * 16, 12 * 8, 16},
|
||||
"-"};
|
||||
|
||||
Console console{
|
||||
{0, 4 * 16, 240, 240}};
|
||||
|
||||
std::string str_log{""};
|
||||
bool logging{false};
|
||||
|
||||
std::unique_ptr<BLECommLogger> logger{};
|
||||
|
||||
MessageHandlerRegistration message_handler_packet{
|
||||
Message::ID::BlePacket,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const BLEPacketMessage*>(p);
|
||||
this->on_data(message->packet);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_tx_progress{
|
||||
Message::ID::TXProgress,
|
||||
[this](const Message* const p) {
|
||||
const auto message = *reinterpret_cast<const TXProgressMessage*>(p);
|
||||
this->on_tx_progress(message.done);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_frame_sync{
|
||||
Message::ID::DisplayFrameSync,
|
||||
[this](const Message* const) {
|
||||
this->on_timer();
|
||||
}};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
#endif /*__UI_AFSK_RX_H__*/
|
||||
@@ -0,0 +1,843 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2017 Furrtek
|
||||
* Copyright (C) 2023 TJ Baginski
|
||||
*
|
||||
* 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 "ble_rx_app.hpp"
|
||||
|
||||
#include "ui_modemsetup.hpp"
|
||||
|
||||
#include "modems.hpp"
|
||||
#include "audio.hpp"
|
||||
#include "rtc_time.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include "ui_textentry.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
using namespace modems;
|
||||
|
||||
void BLELogger::log_raw_data(const std::string& data) {
|
||||
log_file.write_entry(data);
|
||||
}
|
||||
|
||||
std::string pad_string_with_spaces(int snakes) {
|
||||
std::string paddedStr(snakes, ' ');
|
||||
return paddedStr;
|
||||
}
|
||||
|
||||
uint64_t copy_mac_address_to_uint64(const uint8_t* macAddress) {
|
||||
uint64_t result = 0;
|
||||
|
||||
// Copy each byte of the MAC address to the corresponding byte in the uint64_t.
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
result |= static_cast<uint64_t>(macAddress[i]) << ((5 - i) * 8);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void reverse_byte_array(uint8_t* arr, int length) {
|
||||
int start = 0;
|
||||
int end = length - 1;
|
||||
|
||||
while (start < end) {
|
||||
// Swap elements at start and end
|
||||
uint8_t temp = arr[start];
|
||||
arr[start] = arr[end];
|
||||
arr[end] = temp;
|
||||
|
||||
// Move the indices towards the center
|
||||
start++;
|
||||
end--;
|
||||
}
|
||||
}
|
||||
|
||||
namespace ui {
|
||||
|
||||
std::string pdu_type_to_string(ADV_PDU_TYPE type) {
|
||||
std::string pduTypeStr = "";
|
||||
|
||||
switch (type) {
|
||||
case ADV_IND:
|
||||
pduTypeStr += "ADV_IND";
|
||||
break;
|
||||
case ADV_DIRECT_IND:
|
||||
pduTypeStr += "ADV_DIRECT_IND";
|
||||
break;
|
||||
case ADV_NONCONN_IND:
|
||||
pduTypeStr += "ADV_NONCONN_IND";
|
||||
break;
|
||||
case SCAN_REQ:
|
||||
pduTypeStr += "SCAN_REQ";
|
||||
break;
|
||||
case SCAN_RSP:
|
||||
pduTypeStr += "SCAN_RSP";
|
||||
break;
|
||||
case CONNECT_REQ:
|
||||
pduTypeStr += "CONNECT_REQ";
|
||||
break;
|
||||
case ADV_SCAN_IND:
|
||||
pduTypeStr += "ADV_SCAN_IND";
|
||||
break;
|
||||
case RESERVED0:
|
||||
case RESERVED1:
|
||||
case RESERVED2:
|
||||
case RESERVED3:
|
||||
case RESERVED4:
|
||||
case RESERVED5:
|
||||
case RESERVED6:
|
||||
case RESERVED7:
|
||||
case RESERVED8:
|
||||
pduTypeStr += "RESERVED";
|
||||
break;
|
||||
default:
|
||||
pduTypeStr += "UNKNOWN";
|
||||
break;
|
||||
}
|
||||
|
||||
return pduTypeStr;
|
||||
}
|
||||
|
||||
template <>
|
||||
void RecentEntriesTable<BleRecentEntries>::draw(
|
||||
const Entry& entry,
|
||||
const Rect& target_rect,
|
||||
Painter& painter,
|
||||
const Style& style) {
|
||||
std::string line{};
|
||||
line.reserve(30);
|
||||
|
||||
if (!entry.nameString.empty() && entry.include_name) {
|
||||
line = entry.nameString;
|
||||
|
||||
if (line.length() < 17) {
|
||||
line += pad_string_with_spaces(17 - line.length());
|
||||
} else {
|
||||
line = truncate(line, 17);
|
||||
}
|
||||
} else {
|
||||
line = to_string_mac_address(entry.packetData.macAddress, 6, false);
|
||||
}
|
||||
|
||||
// Pushing single digit values down right justified.
|
||||
std::string hitsStr = to_string_dec_int(entry.numHits);
|
||||
int hitsDigits = hitsStr.length();
|
||||
uint8_t hits_spacing = 8 - hitsDigits;
|
||||
|
||||
// Pushing single digit values down right justified.
|
||||
std::string dbStr = to_string_dec_int(entry.dbValue);
|
||||
int dbDigits = dbStr.length();
|
||||
uint8_t db_spacing = 5 - dbDigits;
|
||||
|
||||
line += pad_string_with_spaces(hits_spacing) + hitsStr;
|
||||
|
||||
line += pad_string_with_spaces(db_spacing) + dbStr;
|
||||
|
||||
line.resize(target_rect.width() / 8, ' ');
|
||||
painter.draw_string(target_rect.location(), style, line);
|
||||
}
|
||||
|
||||
BleRecentEntryDetailView::BleRecentEntryDetailView(NavigationView& nav, const BleRecentEntry& entry)
|
||||
: nav_{nav},
|
||||
entry_{entry} {
|
||||
add_children({&button_done,
|
||||
&button_send,
|
||||
&button_save,
|
||||
&label_mac_address,
|
||||
&text_mac_address,
|
||||
&label_pdu_type,
|
||||
&text_pdu_type,
|
||||
&labels});
|
||||
|
||||
text_mac_address.set(to_string_mac_address(entry.packetData.macAddress, 6, false));
|
||||
text_pdu_type.set(pdu_type_to_string(entry.pduType));
|
||||
|
||||
button_done.on_select = [&nav](const ui::Button&) {
|
||||
nav.pop();
|
||||
};
|
||||
|
||||
button_send.on_select = [this, &nav](const ui::Button&) {
|
||||
auto packetToSend = build_packet();
|
||||
nav.set_on_pop([packetToSend, &nav]() {
|
||||
nav.replace<BLETxView>(packetToSend);
|
||||
});
|
||||
nav.pop();
|
||||
};
|
||||
|
||||
button_save.on_select = [this, &nav](const ui::Button&) {
|
||||
auto packetToSave = build_packet();
|
||||
|
||||
packetFileBuffer = "";
|
||||
text_prompt(
|
||||
nav,
|
||||
packetFileBuffer,
|
||||
64,
|
||||
[this, packetToSave](std::string& buffer) {
|
||||
on_save_file(buffer, packetToSave);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
void BleRecentEntryDetailView::on_save_file(const std::string value, BLETxPacket packetToSave) {
|
||||
std::filesystem::path packet_save_path{u"BLERX/Packets/????.TXT"};
|
||||
|
||||
ensure_directory(packet_save_path);
|
||||
auto folder = packet_save_path.parent_path();
|
||||
auto ext = packet_save_path.extension();
|
||||
auto new_path = folder / value + ext;
|
||||
|
||||
saveFile(new_path, packetToSave);
|
||||
}
|
||||
|
||||
bool BleRecentEntryDetailView::saveFile(const std::filesystem::path& path, BLETxPacket packetToSave) {
|
||||
File f;
|
||||
auto error = f.create(path);
|
||||
if (error)
|
||||
return false;
|
||||
|
||||
std::string macAddressStr = packetToSave.macAddress;
|
||||
std::string advertisementDataStr = packetToSave.advertisementData;
|
||||
std::string packetCountStr = packetToSave.packetCount;
|
||||
|
||||
std::string packetString = macAddressStr + ' ' + advertisementDataStr + ' ' + packetCountStr;
|
||||
|
||||
f.write(packetString.c_str(), packetString.length());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void BleRecentEntryDetailView::update_data() {
|
||||
}
|
||||
|
||||
void BleRecentEntryDetailView::focus() {
|
||||
button_done.focus();
|
||||
}
|
||||
|
||||
Rect BleRecentEntryDetailView::draw_field(
|
||||
Painter& painter,
|
||||
const Rect& draw_rect,
|
||||
const Style& style,
|
||||
const std::string& label,
|
||||
const std::string& value) {
|
||||
const int label_length_max = 4;
|
||||
|
||||
painter.draw_string(Point{draw_rect.left(), draw_rect.top()}, style, label);
|
||||
painter.draw_string(Point{draw_rect.left() + (label_length_max + 1) * 8, draw_rect.top()}, style, value);
|
||||
|
||||
return {draw_rect.left(), draw_rect.top() + draw_rect.height(), draw_rect.width(), draw_rect.height()};
|
||||
}
|
||||
|
||||
void BleRecentEntryDetailView::paint(Painter& painter) {
|
||||
View::paint(painter);
|
||||
|
||||
const auto s = style();
|
||||
const auto rect = screen_rect();
|
||||
|
||||
auto field_rect = Rect{rect.left(), rect.top() + 64, rect.width(), 16};
|
||||
|
||||
uint8_t type[total_data_lines];
|
||||
uint8_t length[total_data_lines];
|
||||
uint8_t data[total_data_lines][40];
|
||||
|
||||
int currentByte = 0;
|
||||
int currentPacket = 0;
|
||||
int i = 0;
|
||||
int j = 0;
|
||||
int k = 0;
|
||||
|
||||
switch (entry_.pduType) {
|
||||
case ADV_IND:
|
||||
case ADV_NONCONN_IND:
|
||||
case SCAN_RSP:
|
||||
case ADV_SCAN_IND: {
|
||||
ADV_PDU_PAYLOAD_TYPE_0_2_4_6* advertiseData = (ADV_PDU_PAYLOAD_TYPE_0_2_4_6*)entry_.packetData.data;
|
||||
|
||||
for (currentByte = 0; (currentByte < entry_.packetData.dataLen) && (currentPacket < total_data_lines);) {
|
||||
length[currentPacket] = advertiseData->Data[currentByte++];
|
||||
type[currentPacket] = advertiseData->Data[currentByte++];
|
||||
|
||||
// Subtract 1 because type is part of the length.
|
||||
for (i = 0; i < length[currentPacket] - 1; i++) {
|
||||
data[currentPacket][i] = advertiseData->Data[currentByte++];
|
||||
}
|
||||
|
||||
currentPacket++;
|
||||
}
|
||||
|
||||
for (i = 0; i < currentPacket; i++) {
|
||||
uint8_t number_data_lines = ceil((float)(length[i] - 1) / 10.0);
|
||||
uint8_t current_line = 0;
|
||||
std::array<std::string, total_data_lines> data_strings{};
|
||||
|
||||
for (j = 0; (j < (number_data_lines * 10)) && (j < length[i] - 1); j++) {
|
||||
if ((j / 10) != current_line) {
|
||||
current_line++;
|
||||
}
|
||||
|
||||
data_strings[current_line] += to_string_hex(data[i][j], 2);
|
||||
}
|
||||
|
||||
// Read the type back to the total length.
|
||||
field_rect = draw_field(painter, field_rect, s, to_string_hex(length[i]), to_string_hex(type[i]) + pad_string_with_spaces(3) + data_strings[0]);
|
||||
|
||||
if (number_data_lines > 1) {
|
||||
for (k = 1; k < number_data_lines; k++) {
|
||||
if (!data_strings[k].empty()) {
|
||||
field_rect = draw_field(painter, field_rect, s, "", pad_string_with_spaces(5) + data_strings[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} break;
|
||||
|
||||
case ADV_DIRECT_IND:
|
||||
case SCAN_REQ: {
|
||||
ADV_PDU_PAYLOAD_TYPE_1_3* directed_mac_data = (ADV_PDU_PAYLOAD_TYPE_1_3*)entry_.packetData.data;
|
||||
uint8_t type = 0xFF;
|
||||
field_rect = draw_field(painter, field_rect, s, to_string_hex(entry_.packetData.dataLen), to_string_hex(type) + pad_string_with_spaces(3) + to_string_mac_address(directed_mac_data->A1, 6, false));
|
||||
} break;
|
||||
|
||||
case CONNECT_REQ:
|
||||
default: {
|
||||
uint8_t type = 0xFF;
|
||||
|
||||
// TODO: Display Connect Request Information. For right now just printing full hex data.
|
||||
// This struct will eventually be used to break apart containing data of Connect Request.
|
||||
// ADV_PDU_PAYLOAD_TYPE_5 * connect_req = (ADV_PDU_PAYLOAD_TYPE_5 *)entry_.packetData.data;
|
||||
|
||||
for (currentByte = 0; (currentByte < entry_.packetData.dataLen); currentByte++) {
|
||||
data[0][currentByte] = entry_.packetData.data[currentByte];
|
||||
}
|
||||
|
||||
uint8_t number_data_lines = ceil((float)entry_.packetData.dataLen / 10.0);
|
||||
uint8_t current_line = 0;
|
||||
std::array<std::string, total_data_lines> data_strings{};
|
||||
|
||||
for (j = 0; (j < (number_data_lines * 10)) && (j < entry_.packetData.dataLen); j++) {
|
||||
if ((j / 10) != current_line) {
|
||||
current_line++;
|
||||
}
|
||||
|
||||
data_strings[current_line] += to_string_hex(data[0][j], 2);
|
||||
}
|
||||
|
||||
field_rect = draw_field(painter, field_rect, s, to_string_hex(entry_.packetData.dataLen), to_string_hex(type) + pad_string_with_spaces(3) + data_strings[0]);
|
||||
|
||||
if (number_data_lines > 1) {
|
||||
for (k = 1; k < number_data_lines; k++) {
|
||||
if (!data_strings[k].empty()) {
|
||||
field_rect = draw_field(painter, field_rect, s, "", pad_string_with_spaces(5) + data_strings[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
void BleRecentEntryDetailView::set_entry(const BleRecentEntry& entry) {
|
||||
entry_ = entry;
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
BLETxPacket BleRecentEntryDetailView::build_packet() {
|
||||
BLETxPacket bleTxPacket;
|
||||
memset(&bleTxPacket, 0, sizeof(BLETxPacket));
|
||||
|
||||
std::string macAddressStr = to_string_mac_address(entry_.packetData.macAddress, 6, true);
|
||||
|
||||
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;
|
||||
|
||||
return bleTxPacket;
|
||||
}
|
||||
|
||||
static std::uint64_t get_freq_by_channel_number(uint8_t channel_number) {
|
||||
uint64_t freq_hz;
|
||||
|
||||
switch (channel_number) {
|
||||
case 37:
|
||||
freq_hz = 2'402'000'000ull;
|
||||
break;
|
||||
case 38:
|
||||
freq_hz = 2'426'000'000ull;
|
||||
break;
|
||||
case 39:
|
||||
freq_hz = 2'480'000'000ull;
|
||||
break;
|
||||
case 0 ... 10:
|
||||
freq_hz = 2'404'000'000ull + channel_number * 2'000'000ull;
|
||||
break;
|
||||
case 11 ... 36:
|
||||
freq_hz = 2'428'000'000ull + (channel_number - 11) * 2'000'000ull;
|
||||
break;
|
||||
default:
|
||||
freq_hz = UINT64_MAX;
|
||||
}
|
||||
|
||||
return freq_hz;
|
||||
}
|
||||
|
||||
void BLERxView::focus() {
|
||||
options_channel.focus();
|
||||
}
|
||||
|
||||
BLERxView::BLERxView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_btle_rx);
|
||||
|
||||
add_children({&rssi,
|
||||
&channel,
|
||||
&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&options_channel,
|
||||
&field_frequency,
|
||||
&check_log,
|
||||
&check_name,
|
||||
&label_sort,
|
||||
&options_sort,
|
||||
&button_filter,
|
||||
&button_save_list,
|
||||
&button_clear_list,
|
||||
&button_switch,
|
||||
&recent_entries_view});
|
||||
|
||||
recent_entries_view.on_select = [this](const BleRecentEntry& entry) {
|
||||
nav_.push<BleRecentEntryDetailView>(entry);
|
||||
};
|
||||
|
||||
button_filter.on_select = [this](Button&) {
|
||||
text_prompt(
|
||||
nav_,
|
||||
filterBuffer,
|
||||
64,
|
||||
[this](std::string& buffer) {
|
||||
on_filter_change(buffer);
|
||||
});
|
||||
};
|
||||
|
||||
button_save_list.on_select = [this, &nav](const ui::Button&) {
|
||||
listFileBuffer = "";
|
||||
text_prompt(
|
||||
nav,
|
||||
listFileBuffer,
|
||||
64,
|
||||
[this](std::string& buffer) {
|
||||
on_save_file(buffer);
|
||||
});
|
||||
};
|
||||
|
||||
button_clear_list.on_select = [this](Button&) {
|
||||
recent.clear();
|
||||
};
|
||||
|
||||
button_switch.on_select = [&nav](Button&) {
|
||||
nav.replace<BLETxView>();
|
||||
};
|
||||
|
||||
field_frequency.set_step(0);
|
||||
|
||||
check_log.set_value(logging);
|
||||
|
||||
check_log.on_select = [this](Checkbox&, bool v) {
|
||||
str_log = "";
|
||||
logging = v;
|
||||
|
||||
if (logger && logging)
|
||||
logger->append(LOG_ROOT_DIR "/BLELOG_" + to_string_timestamp(rtc_time::now()) + ".TXT");
|
||||
};
|
||||
|
||||
check_name.set_value(true);
|
||||
|
||||
check_name.on_select = [this](Checkbox&, bool v) {
|
||||
setAllMembersToValue(recent, &BleRecentEntry::include_name, v);
|
||||
recent_entries_view.set_dirty();
|
||||
};
|
||||
|
||||
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 = i;
|
||||
|
||||
baseband::set_btlerx(channel_number);
|
||||
};
|
||||
|
||||
options_sort.on_change = [this](size_t, int32_t index) {
|
||||
handle_entries_sort(index);
|
||||
};
|
||||
|
||||
options_channel.set_selected_index(0, true);
|
||||
options_sort.set_selected_index(0, true);
|
||||
|
||||
logger = std::make_unique<BLELogger>();
|
||||
|
||||
// Auto-configure modem for LCR RX (will be removed later)
|
||||
baseband::set_btlerx(channel_number);
|
||||
|
||||
receiver_model.enable();
|
||||
}
|
||||
|
||||
std::string BLERxView::build_line_str(BleRecentEntry entry) {
|
||||
std::string macAddressStr = to_string_mac_address(entry.packetData.macAddress, 6, false) + ",";
|
||||
std::string timestameStr = entry.timestamp + ",";
|
||||
std::string nameStr = entry.nameString + ",";
|
||||
std::string pduStr = pdu_type_to_string(entry.pduType) + ",";
|
||||
std::string dataStr = "0x" + entry.dataString + ",";
|
||||
std::string hitsStr = to_string_dec_int(entry.numHits) + ",";
|
||||
std::string dbStr = to_string_dec_int(entry.dbValue) + ",";
|
||||
std::string channelStr = to_string_dec_int(entry.channelNumber) + ",";
|
||||
|
||||
std::string lineStr = timestameStr + macAddressStr + nameStr + pduStr + dataStr + hitsStr + dbStr + channelStr;
|
||||
lineStr += pad_string_with_spaces(maxLineLength - lineStr.length());
|
||||
|
||||
return lineStr;
|
||||
}
|
||||
|
||||
void BLERxView::on_save_file(const std::string value) {
|
||||
std::filesystem::path packet_save_path{u"BLERX/Lists/????.csv"};
|
||||
|
||||
ensure_directory(packet_save_path);
|
||||
auto folder = packet_save_path.parent_path();
|
||||
auto ext = packet_save_path.extension();
|
||||
auto new_path = folder / value + ext;
|
||||
|
||||
saveFile(new_path);
|
||||
}
|
||||
|
||||
bool BLERxView::saveFile(const std::filesystem::path& path) {
|
||||
// Check to see if file was previously saved.
|
||||
bool file_existed = file_exists(path);
|
||||
|
||||
// Attempt to open, if it can't be opened. Create new.
|
||||
auto src = std::make_unique<File>();
|
||||
auto error = src->open(path, false, true);
|
||||
|
||||
if (error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const auto& entry : recent) {
|
||||
tempList.emplace_back(entry);
|
||||
}
|
||||
|
||||
if (!file_existed) {
|
||||
src->write_line(headerStr.c_str());
|
||||
|
||||
auto it = tempList.begin();
|
||||
|
||||
while (it != tempList.end()) {
|
||||
BleRecentEntry entry = (BleRecentEntry)*it;
|
||||
src->write_line(build_line_str(entry).c_str());
|
||||
it++;
|
||||
}
|
||||
} else {
|
||||
// Check file for macAddressStr before adding.
|
||||
char currentLine[maxLineLength];
|
||||
uint64_t startPos = headerStr.length();
|
||||
uint64_t bytesRead = 0;
|
||||
uint64_t bytePos = 0;
|
||||
|
||||
File::Size currentSize = src->size();
|
||||
|
||||
auto dst = std::make_unique<File>();
|
||||
const std::filesystem::path tempFilePath = path + "~";
|
||||
auto error = dst->open(tempFilePath, false, true);
|
||||
|
||||
if (error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
dst->write_line(headerStr.c_str());
|
||||
|
||||
src->seek(startPos);
|
||||
|
||||
// Look for ones found and rewrite.
|
||||
do {
|
||||
memset(currentLine, 0, maxLineLength);
|
||||
|
||||
bytesRead = readUntil(*src, currentLine, currentSize, '\n');
|
||||
|
||||
if (!bytesRead) {
|
||||
break;
|
||||
}
|
||||
|
||||
bytePos += bytesRead;
|
||||
|
||||
std::string lineStr = "";
|
||||
std::string macAddressStr = "";
|
||||
BleRecentEntry foundEntry;
|
||||
|
||||
char* token;
|
||||
token = strtok(currentLine, ",");
|
||||
|
||||
while (token != NULL) {
|
||||
auto it = tempList.begin();
|
||||
|
||||
while (it != tempList.end()) {
|
||||
BleRecentEntry& entry = reinterpret_cast<BleRecentEntry&>(*it);
|
||||
|
||||
macAddressStr = to_string_mac_address(entry.packetData.macAddress, 6, false);
|
||||
|
||||
if (strstr(token, macAddressStr.c_str()) != NULL) {
|
||||
entry.entryFound = true;
|
||||
foundEntry = entry;
|
||||
break;
|
||||
}
|
||||
|
||||
it++;
|
||||
}
|
||||
|
||||
if (foundEntry.entryFound) {
|
||||
break;
|
||||
}
|
||||
|
||||
token = strtok(NULL, ",");
|
||||
}
|
||||
|
||||
if (foundEntry.entryFound) {
|
||||
dst->write_line(build_line_str(foundEntry).c_str());
|
||||
}
|
||||
|
||||
} while (bytePos <= currentSize);
|
||||
|
||||
// Write the ones not found.
|
||||
auto it = tempList.begin();
|
||||
|
||||
while (it != tempList.end()) {
|
||||
BleRecentEntry entry = (BleRecentEntry)*it;
|
||||
|
||||
if (!entry.entryFound) {
|
||||
dst->write_line(build_line_str(entry).c_str());
|
||||
}
|
||||
|
||||
it++;
|
||||
}
|
||||
|
||||
// Close files before renaming/deleting.
|
||||
src.reset();
|
||||
dst.reset();
|
||||
|
||||
// Delete original and overwrite with temp file.
|
||||
delete_file(path);
|
||||
rename_file(tempFilePath, path);
|
||||
}
|
||||
|
||||
tempList.clear();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void BLERxView::on_data(BlePacketData* packet) {
|
||||
std::string str_console = "";
|
||||
|
||||
if (!logging) {
|
||||
str_log = "";
|
||||
}
|
||||
|
||||
str_console += pdu_type_to_string((ADV_PDU_TYPE)packet->type);
|
||||
|
||||
str_console += " Len:";
|
||||
str_console += to_string_dec_uint(packet->size);
|
||||
|
||||
str_console += "\n";
|
||||
|
||||
str_console += "Mac:";
|
||||
str_console += to_string_mac_address(packet->macAddress, 6, false);
|
||||
|
||||
str_console += "\n";
|
||||
str_console += "Data:";
|
||||
|
||||
int i;
|
||||
|
||||
for (i = 0; i < packet->dataLen; i++) {
|
||||
str_console += to_string_hex(packet->data[i], 2);
|
||||
}
|
||||
|
||||
str_console += "\n";
|
||||
|
||||
uint64_t macAddressEncoded = copy_mac_address_to_uint64(packet->macAddress);
|
||||
|
||||
// Start of Packet stuffing.
|
||||
// Masking off the top 2 bytes to avoid invalid keys.
|
||||
auto& entry = ::on_packet(recent, macAddressEncoded & 0xFFFFFFFFFFFF);
|
||||
updateEntry(packet, entry, (ADV_PDU_TYPE)packet->type);
|
||||
|
||||
// Add entries if they meet the criteria.
|
||||
auto value = filter;
|
||||
resetFilteredEntries(recent, [&value](const BleRecentEntry& entry) {
|
||||
return (entry.dataString.find(value) == std::string::npos) && (entry.nameString.find(value) == std::string::npos);
|
||||
});
|
||||
|
||||
handle_entries_sort(options_sort.selected_index());
|
||||
|
||||
// Log at End of Packet.
|
||||
if (logger && logging) {
|
||||
logger->log_raw_data(str_console);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
filter = value;
|
||||
}
|
||||
|
||||
// called each 1/60th of second, so 6 = 100ms
|
||||
void BLERxView::on_timer() {
|
||||
if (++timer_count == timer_period) {
|
||||
timer_count = 0;
|
||||
|
||||
if (auto_channel) {
|
||||
int min = 37;
|
||||
int max = 39;
|
||||
|
||||
int randomChannel = min + std::rand() % (max - min + 1);
|
||||
|
||||
field_frequency.set_value(get_freq_by_channel_number(randomChannel));
|
||||
baseband::set_btlerx(randomChannel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BLERxView::handle_entries_sort(uint8_t index) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
sortEntriesBy(
|
||||
recent, [](const BleRecentEntry& entry) { return entry.macAddress; }, true);
|
||||
break;
|
||||
case 1:
|
||||
sortEntriesBy(
|
||||
recent, [](const BleRecentEntry& entry) { return entry.numHits; }, false);
|
||||
break;
|
||||
case 2:
|
||||
sortEntriesBy(
|
||||
recent, [](const BleRecentEntry& entry) { return entry.dbValue; }, false);
|
||||
break;
|
||||
case 3:
|
||||
sortEntriesBy(
|
||||
recent, [](const BleRecentEntry& entry) { return entry.timestamp; }, false);
|
||||
break;
|
||||
case 4:
|
||||
sortEntriesBy(
|
||||
recent, [](const BleRecentEntry& entry) { return entry.nameString; }, true);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
recent_entries_view.set_dirty();
|
||||
}
|
||||
|
||||
void BLERxView::set_parent_rect(const Rect new_parent_rect) {
|
||||
View::set_parent_rect(new_parent_rect);
|
||||
const Rect content_rect{0, header_height, new_parent_rect.width(), new_parent_rect.height() - header_height - switch_button_height};
|
||||
recent_entries_view.set_parent_rect(content_rect);
|
||||
}
|
||||
|
||||
BLERxView::~BLERxView() {
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
void BLERxView::updateEntry(const BlePacketData* packet, BleRecentEntry& entry, ADV_PDU_TYPE pdu_type) {
|
||||
std::string data_string;
|
||||
|
||||
int i;
|
||||
|
||||
for (i = 0; i < packet->dataLen; i++) {
|
||||
data_string += to_string_hex(packet->data[i], 2);
|
||||
}
|
||||
|
||||
entry.dbValue = packet->max_dB;
|
||||
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.numHits++;
|
||||
entry.pduType = pdu_type;
|
||||
entry.channelNumber = channel_number;
|
||||
|
||||
// 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) && entry.nameString.empty()) {
|
||||
ADV_PDU_PAYLOAD_TYPE_0_2_4_6* advertiseData = (ADV_PDU_PAYLOAD_TYPE_0_2_4_6*)entry.packetData.data;
|
||||
|
||||
uint8_t currentByte = 0;
|
||||
uint8_t length = 0;
|
||||
uint8_t type = 0;
|
||||
|
||||
std::string decoded_data;
|
||||
for (currentByte = 0; (currentByte < entry.packetData.dataLen);) {
|
||||
length = advertiseData->Data[currentByte++];
|
||||
type = advertiseData->Data[currentByte++];
|
||||
|
||||
// Subtract 1 because type is part of the length.
|
||||
for (int i = 0; i < length - 1; i++) {
|
||||
if (type == 0x08 || type == 0x09) {
|
||||
decoded_data += (char)advertiseData->Data[currentByte];
|
||||
}
|
||||
currentByte++;
|
||||
}
|
||||
if (!decoded_data.empty()) {
|
||||
entry.nameString = std::move(decoded_data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
@@ -0,0 +1,330 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2017 Furrtek
|
||||
* Copyright (C) 2023 TJ Baginski
|
||||
*
|
||||
* 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 __BLE_RX_APP_H__
|
||||
#define __BLE_RX_APP_H__
|
||||
|
||||
#include "ble_tx_app.hpp"
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "ui_record_view.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "log_file.hpp"
|
||||
#include "utility.hpp"
|
||||
|
||||
#include "recent_entries.hpp"
|
||||
|
||||
class BLELogger {
|
||||
public:
|
||||
Optional<File::Error> append(const std::string& filename) {
|
||||
return log_file.append(filename);
|
||||
}
|
||||
|
||||
void log_raw_data(const std::string& data);
|
||||
|
||||
private:
|
||||
LogFile log_file{};
|
||||
};
|
||||
|
||||
namespace ui {
|
||||
typedef enum {
|
||||
ADV_IND = 0,
|
||||
ADV_DIRECT_IND = 1,
|
||||
ADV_NONCONN_IND = 2,
|
||||
SCAN_REQ = 3,
|
||||
SCAN_RSP = 4,
|
||||
CONNECT_REQ = 5,
|
||||
ADV_SCAN_IND = 6,
|
||||
RESERVED0 = 7,
|
||||
RESERVED1 = 8,
|
||||
RESERVED2 = 9,
|
||||
RESERVED3 = 10,
|
||||
RESERVED4 = 11,
|
||||
RESERVED5 = 12,
|
||||
RESERVED6 = 13,
|
||||
RESERVED7 = 14,
|
||||
RESERVED8 = 15
|
||||
} ADV_PDU_TYPE;
|
||||
|
||||
struct BleRecentEntry {
|
||||
using Key = uint64_t;
|
||||
|
||||
static constexpr Key invalid_key = 0xffffffff;
|
||||
|
||||
uint64_t macAddress;
|
||||
int dbValue;
|
||||
BlePacketData packetData;
|
||||
std::string timestamp;
|
||||
std::string dataString;
|
||||
std::string nameString;
|
||||
bool include_name;
|
||||
uint16_t numHits;
|
||||
ADV_PDU_TYPE pduType;
|
||||
uint8_t channelNumber;
|
||||
bool entryFound;
|
||||
|
||||
BleRecentEntry()
|
||||
: BleRecentEntry{0} {
|
||||
}
|
||||
|
||||
BleRecentEntry(
|
||||
const uint64_t macAddress)
|
||||
: macAddress{macAddress},
|
||||
dbValue{},
|
||||
packetData{},
|
||||
timestamp{},
|
||||
dataString{},
|
||||
nameString{},
|
||||
include_name{},
|
||||
numHits{},
|
||||
pduType{},
|
||||
channelNumber{},
|
||||
entryFound{} {
|
||||
}
|
||||
|
||||
Key key() const {
|
||||
return macAddress;
|
||||
}
|
||||
};
|
||||
|
||||
using BleRecentEntries = RecentEntries<BleRecentEntry>;
|
||||
using BleRecentEntriesView = RecentEntriesView<BleRecentEntries>;
|
||||
|
||||
class BleRecentEntryDetailView : public View {
|
||||
public:
|
||||
BleRecentEntryDetailView(NavigationView& nav, const BleRecentEntry& entry);
|
||||
|
||||
void set_entry(const BleRecentEntry& new_entry);
|
||||
const BleRecentEntry& entry() const { return entry_; };
|
||||
|
||||
void update_data();
|
||||
void focus() override;
|
||||
void paint(Painter&) override;
|
||||
|
||||
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{};
|
||||
|
||||
static constexpr uint8_t total_data_lines{5};
|
||||
|
||||
Labels label_mac_address{
|
||||
{{0 * 8, 0 * 16}, "Mac Address:", Color::light_grey()}};
|
||||
|
||||
Text text_mac_address{
|
||||
{12 * 8, 0 * 16, 17 * 8, 16},
|
||||
"-"};
|
||||
|
||||
Labels label_pdu_type{
|
||||
{{0 * 8, 1 * 16}, "PDU Type:", Color::light_grey()}};
|
||||
|
||||
Text text_pdu_type{
|
||||
{9 * 8, 1 * 16, 17 * 8, 16},
|
||||
"-"};
|
||||
|
||||
Labels labels{
|
||||
{{0 * 8, 3 * 16}, "Len", Color::light_grey()},
|
||||
{{5 * 8, 3 * 16}, "Type", Color::light_grey()},
|
||||
{{10 * 8, 3 * 16}, "Value", Color::light_grey()},
|
||||
};
|
||||
|
||||
Button button_send{
|
||||
{19, 224, 96, 24},
|
||||
"Send"};
|
||||
|
||||
Button button_done{
|
||||
{125, 224, 96, 24},
|
||||
"Done"};
|
||||
|
||||
Button button_save{
|
||||
{72, 264, 96, 24},
|
||||
"Save"};
|
||||
|
||||
bool send_updates{false};
|
||||
|
||||
Rect draw_field(
|
||||
Painter& painter,
|
||||
const Rect& draw_rect,
|
||||
const Style& style,
|
||||
const std::string& label,
|
||||
const std::string& value);
|
||||
};
|
||||
|
||||
class BLERxView : public View {
|
||||
public:
|
||||
BLERxView(NavigationView& nav);
|
||||
~BLERxView();
|
||||
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
void paint(Painter&) override{};
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "BLE RX"; };
|
||||
|
||||
private:
|
||||
std::string build_line_str(BleRecentEntry entry);
|
||||
void on_save_file(const std::string value);
|
||||
bool saveFile(const std::filesystem::path& path);
|
||||
void on_data(BlePacketData* packetData);
|
||||
void on_filter_change(std::string value);
|
||||
void on_timer();
|
||||
void handle_entries_sort(uint8_t index);
|
||||
void updateEntry(const BlePacketData* packet, BleRecentEntry& entry, ADV_PDU_TYPE pdu_type);
|
||||
|
||||
NavigationView& nav_;
|
||||
RxRadioState radio_state_{
|
||||
2402000000 /* frequency */,
|
||||
4000000 /* bandwidth */,
|
||||
4000000 /* sampling rate */,
|
||||
ReceiverModel::Mode::WidebandFMAudio};
|
||||
app_settings::SettingsManager settings_{
|
||||
"BLE Rx", app_settings::Mode::RX};
|
||||
|
||||
uint8_t console_color{0};
|
||||
uint32_t prev_value{0};
|
||||
uint8_t channel_number = 37;
|
||||
bool auto_channel = false;
|
||||
|
||||
int16_t timer_count{0};
|
||||
int16_t timer_period{6}; // 100ms
|
||||
|
||||
std::string filterBuffer{};
|
||||
std::string filter{};
|
||||
std::string listFileBuffer{};
|
||||
std::string headerStr = "Timestamp, MAC Address, Name, Packet Type, Data, Hits, dB, Channel";
|
||||
uint16_t maxLineLength = 140;
|
||||
|
||||
static constexpr auto header_height = 3 * 16;
|
||||
static constexpr auto switch_button_height = 3 * 16;
|
||||
|
||||
OptionsField options_channel{
|
||||
{0 * 8, 0 * 8},
|
||||
5,
|
||||
{{"Ch.37 ", 37},
|
||||
{"Ch.38", 38},
|
||||
{"Ch.39", 39},
|
||||
{"Auto", 40}}};
|
||||
|
||||
RxFrequencyField field_frequency{
|
||||
{6 * 8, 0 * 16},
|
||||
nav_};
|
||||
|
||||
RFAmpField field_rf_amp{
|
||||
{16 * 8, 0 * 16}};
|
||||
|
||||
LNAGainField field_lna{
|
||||
{18 * 8, 0 * 16}};
|
||||
|
||||
VGAGainField field_vga{
|
||||
{21 * 8, 0 * 16}};
|
||||
|
||||
RSSI rssi{
|
||||
{24 * 8, 0, 6 * 8, 4}};
|
||||
|
||||
Channel channel{
|
||||
{24 * 8, 5, 6 * 8, 4}};
|
||||
|
||||
Labels label_sort{
|
||||
{{0 * 8, 3 * 8}, "Sort:", Color::light_grey()}};
|
||||
|
||||
OptionsField options_sort{
|
||||
{5 * 8, 3 * 8},
|
||||
4,
|
||||
{{"MAC", 0},
|
||||
{"Hits", 1},
|
||||
{"dB", 2},
|
||||
{"Time", 3},
|
||||
{"Name", 4}}};
|
||||
|
||||
Button button_filter{
|
||||
{11 * 8, 3 * 8, 4 * 8, 16},
|
||||
"Filter"};
|
||||
|
||||
Checkbox check_log{
|
||||
{17 * 8, 3 * 8},
|
||||
3,
|
||||
"Log",
|
||||
true};
|
||||
|
||||
Checkbox check_name{
|
||||
{23 * 8, 3 * 8},
|
||||
3,
|
||||
"Name",
|
||||
true};
|
||||
|
||||
Console console{
|
||||
{0, 4 * 16, 240, 240}};
|
||||
|
||||
Button button_clear_list{
|
||||
{2 * 8, 320 - (16 + 32), 7 * 8, 32},
|
||||
"Clear"};
|
||||
|
||||
Button button_save_list{
|
||||
{11 * 8, 320 - (16 + 32), 11 * 8, 32},
|
||||
"Export CSV"};
|
||||
|
||||
Button button_switch{
|
||||
{240 - 6 * 8, 320 - (16 + 32), 4 * 8, 32},
|
||||
"Tx"};
|
||||
|
||||
std::string str_log{""};
|
||||
bool logging{false};
|
||||
|
||||
std::unique_ptr<BLELogger> logger{};
|
||||
|
||||
BleRecentEntries recent{};
|
||||
BleRecentEntries tempList{};
|
||||
|
||||
const RecentEntriesColumns columns{{
|
||||
{"Mac Address", 17},
|
||||
{"Hits", 7},
|
||||
{"dB", 4},
|
||||
}};
|
||||
|
||||
BleRecentEntriesView recent_entries_view{columns, recent};
|
||||
|
||||
MessageHandlerRegistration message_handler_packet{
|
||||
Message::ID::BlePacket,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const BLEPacketMessage*>(p);
|
||||
this->on_data(message->packet);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_frame_sync{
|
||||
Message::ID::DisplayFrameSync,
|
||||
[this](const Message* const) {
|
||||
this->on_timer();
|
||||
}};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
#endif /*__UI_AFSK_RX_H__*/
|
||||
@@ -0,0 +1,534 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2017 Furrtek
|
||||
* Copyright (C) 2023 TJ Baginski
|
||||
*
|
||||
* 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 "ble_tx_app.hpp"
|
||||
#include "ble_rx_app.hpp"
|
||||
|
||||
#include "ui_fileman.hpp"
|
||||
#include "ui_modemsetup.hpp"
|
||||
|
||||
#include "audio.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "io_file.hpp"
|
||||
#include "modems.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include "rtc_time.hpp"
|
||||
#include "string_format.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
using namespace modems;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
void BLELoggerTx::log_raw_data(const std::string& data) {
|
||||
log_file.write_entry(data);
|
||||
}
|
||||
|
||||
bool hasValidHexPairs(const std::string& str, int totalPairs) {
|
||||
int validPairs = 0;
|
||||
|
||||
for (int i = 0; i < totalPairs * 2; i += 2) {
|
||||
char c1 = str[i];
|
||||
char c2 = str[i + 1];
|
||||
|
||||
if (!(std::isxdigit(c1) && std::isxdigit(c2))) {
|
||||
return false; // Return false if any pair is invalid.
|
||||
}
|
||||
|
||||
validPairs++;
|
||||
}
|
||||
|
||||
return (validPairs == totalPairs);
|
||||
}
|
||||
|
||||
std::vector<std::string> splitIntoStrings(const char* input) {
|
||||
std::vector<std::string> result;
|
||||
int length = std::strlen(input);
|
||||
int start = 0;
|
||||
|
||||
while (start < length) {
|
||||
int remaining = length - start;
|
||||
int chunkSize = (remaining > 30) ? 30 : remaining;
|
||||
result.push_back(std::string(input + start, chunkSize));
|
||||
start += chunkSize;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t stringToUint32(const std::string& str) {
|
||||
size_t pos = 0;
|
||||
uint32_t result = 0;
|
||||
|
||||
while (pos < str.size() && std::isdigit(str[pos])) {
|
||||
int digit = str[pos] - '0';
|
||||
|
||||
// Check for overflow before adding the next digit
|
||||
if (result > (UINT32_MAX - digit) / 10) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
result = result * 10 + digit;
|
||||
pos++;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::uint64_t get_freq_by_channel_number(uint8_t channel_number) {
|
||||
uint64_t freq_hz;
|
||||
|
||||
switch (channel_number) {
|
||||
case 37:
|
||||
freq_hz = 2'402'000'000ull;
|
||||
break;
|
||||
case 38:
|
||||
freq_hz = 2'426'000'000ull;
|
||||
break;
|
||||
case 39:
|
||||
freq_hz = 2'480'000'000ull;
|
||||
break;
|
||||
case 0 ... 10:
|
||||
freq_hz = 2'404'000'000ull + channel_number * 2'000'000ull;
|
||||
break;
|
||||
case 11 ... 36:
|
||||
freq_hz = 2'428'000'000ull + (channel_number - 11) * 2'000'000ull;
|
||||
break;
|
||||
default:
|
||||
freq_hz = UINT64_MAX;
|
||||
}
|
||||
|
||||
return freq_hz;
|
||||
}
|
||||
|
||||
namespace ui {
|
||||
|
||||
void BLETxView::focus() {
|
||||
button_open.focus();
|
||||
}
|
||||
|
||||
bool BLETxView::is_active() const {
|
||||
return (bool)is_running;
|
||||
}
|
||||
|
||||
void BLETxView::file_error() {
|
||||
nav_.display_modal("Error", "File read error.");
|
||||
}
|
||||
|
||||
bool BLETxView::saveFile(const std::filesystem::path& path) {
|
||||
File f;
|
||||
auto error = f.create(path);
|
||||
if (error)
|
||||
return false;
|
||||
|
||||
for (uint32_t i = 0; i < num_packets; i++) {
|
||||
std::string macAddressStr = packets[i].macAddress;
|
||||
std::string advertisementDataStr = packets[i].advertisementData;
|
||||
std::string packetCountStr = packets[i].packetCount;
|
||||
|
||||
std::string packetString = macAddressStr + ' ' + advertisementDataStr + ' ' + packetCountStr;
|
||||
|
||||
// Are we on the last line?
|
||||
if (i != num_packets - 1) {
|
||||
packetString += '\n';
|
||||
}
|
||||
|
||||
f.write(packetString.c_str(), packetString.length());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void BLETxView::toggle() {
|
||||
if (is_active()) {
|
||||
stop();
|
||||
} else {
|
||||
start();
|
||||
}
|
||||
}
|
||||
|
||||
void BLETxView::start() {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_btle_tx);
|
||||
transmitter_model.enable();
|
||||
|
||||
// Generate new random Mac Address.
|
||||
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);
|
||||
|
||||
// TODO: Make this a checkbox.
|
||||
if (!markedBytes.empty()) {
|
||||
for (size_t i = 0; i < strlen(advertisementData); i++) {
|
||||
bool found = false;
|
||||
|
||||
auto it = std::find(markedBytes.begin(), markedBytes.end(), i);
|
||||
|
||||
if (it != markedBytes.end()) {
|
||||
found = true;
|
||||
}
|
||||
|
||||
if (found) {
|
||||
int min = 0;
|
||||
int max = 15;
|
||||
|
||||
int hexDigit = min + std::rand() % (max - min + 1);
|
||||
|
||||
// Map the random number to a hexadecimal digit
|
||||
char randomHexChar = (hexDigit < 10) ? ('0' + hexDigit) : ('A' + hexDigit - 10);
|
||||
advertisementData[i] = randomHexChar;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
void BLETxView::stop() {
|
||||
transmitter_model.disable();
|
||||
baseband::shutdown();
|
||||
|
||||
progressbar.set_value(0);
|
||||
button_play.set_bitmap(&bitmap_play);
|
||||
check_loop.set_value(false);
|
||||
|
||||
update_current_packet(packets[0], 0);
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BLETxView::on_tx_progress(const bool done) {
|
||||
if (done) {
|
||||
if (is_active()) {
|
||||
if ((packet_counter % 10) == 0) {
|
||||
text_packets_sent.set(to_string_dec_uint(packet_counter));
|
||||
}
|
||||
|
||||
packet_counter--;
|
||||
progressbar.set_value(packets[current_packet].packet_count - packet_counter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BLETxView::BLETxView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
add_children({&button_open,
|
||||
&text_filename,
|
||||
&progressbar,
|
||||
&check_rand_mac,
|
||||
&field_frequency,
|
||||
&tx_view, // now it handles previous rfgain, rfamp.
|
||||
&check_loop,
|
||||
&button_play,
|
||||
&label_speed,
|
||||
&options_speed,
|
||||
&options_channel,
|
||||
&options_adv_type,
|
||||
&label_packet_index,
|
||||
&text_packet_index,
|
||||
&label_packets_sent,
|
||||
&text_packets_sent,
|
||||
&label_mac_address,
|
||||
&text_mac_address,
|
||||
&label_data_packet,
|
||||
&label_data_index,
|
||||
&text_data_index,
|
||||
&dataEditView,
|
||||
&button_save_packet,
|
||||
&button_switch});
|
||||
|
||||
field_frequency.set_step(0);
|
||||
|
||||
ensure_directory(packet_save_path);
|
||||
|
||||
button_play.on_select = [this](ImageButton&) {
|
||||
this->toggle();
|
||||
};
|
||||
|
||||
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 = i;
|
||||
};
|
||||
|
||||
options_speed.on_change = [this](size_t, int32_t i) {
|
||||
timer_period = i;
|
||||
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);
|
||||
|
||||
check_rand_mac.set_value(false);
|
||||
check_rand_mac.on_select = [this](Checkbox&, bool v) {
|
||||
random_mac = v;
|
||||
};
|
||||
|
||||
button_open.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(); });
|
||||
};
|
||||
};
|
||||
|
||||
button_save_packet.on_select = [this, &nav](Button&) {
|
||||
packetFileBuffer = "";
|
||||
text_prompt(
|
||||
nav,
|
||||
packetFileBuffer,
|
||||
64,
|
||||
[this](std::string& buffer) {
|
||||
on_save_file(buffer);
|
||||
});
|
||||
};
|
||||
|
||||
button_switch.on_select = [&nav](Button&) {
|
||||
nav.replace<BLERxView>();
|
||||
};
|
||||
|
||||
dataEditView.on_select = [this] {
|
||||
// Save last selected cursor.
|
||||
cursor_pos.line = dataEditView.line();
|
||||
cursor_pos.col = dataEditView.col();
|
||||
|
||||
uint16_t dataBytePos = ((dataEditView.line()) * 30) + dataEditView.col();
|
||||
|
||||
auto it = std::find(markedBytes.begin(), markedBytes.end(), dataBytePos);
|
||||
|
||||
if (it != markedBytes.end()) {
|
||||
markedBytes.erase(it);
|
||||
} else {
|
||||
markedBytes.push_back(dataBytePos);
|
||||
}
|
||||
|
||||
dataEditView.cursor_mark_selected();
|
||||
|
||||
text_data_index.set(to_string_dec_uint(dataBytePos));
|
||||
};
|
||||
}
|
||||
|
||||
BLETxView::BLETxView(
|
||||
NavigationView& nav,
|
||||
BLETxPacket packet)
|
||||
: BLETxView(nav) {
|
||||
packets[0] = packet;
|
||||
|
||||
update_current_packet(packets[0], 0);
|
||||
|
||||
num_packets = 1;
|
||||
file_override = true;
|
||||
}
|
||||
|
||||
void BLETxView::on_file_changed(const fs::path& new_file_path) {
|
||||
file_path = fs::path(u"/") + new_file_path;
|
||||
num_packets = 0;
|
||||
|
||||
{ // Get the size of the data file.
|
||||
File data_file;
|
||||
auto error = data_file.open(file_path);
|
||||
if (error) {
|
||||
file_error();
|
||||
file_path = "";
|
||||
return;
|
||||
}
|
||||
|
||||
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].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);
|
||||
|
||||
packets[num_packets].packet_count = stringToUint32(packets[num_packets].packetCount);
|
||||
|
||||
// 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)) {
|
||||
text_filename.set(truncate(file_path.filename().string(), 12));
|
||||
|
||||
} else {
|
||||
// Did not find any packets.
|
||||
if (num_packets == 0) {
|
||||
file_path = "";
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
num_packets++;
|
||||
|
||||
} while (num_packets < max_num_packets);
|
||||
|
||||
update_current_packet(packets[0], 0);
|
||||
}
|
||||
}
|
||||
|
||||
void BLETxView::on_save_file(const std::string value) {
|
||||
auto folder = packet_save_path.parent_path();
|
||||
auto ext = packet_save_path.extension();
|
||||
auto new_path = folder / value + ext;
|
||||
|
||||
saveFile(new_path);
|
||||
}
|
||||
|
||||
void BLETxView::on_data(uint32_t value, bool is_data) {
|
||||
std::string str_console = "";
|
||||
|
||||
if (is_data) {
|
||||
str_console += (char)(value);
|
||||
}
|
||||
}
|
||||
|
||||
void BLETxView::update_current_packet(BLETxPacket packet, uint32_t currentIndex) {
|
||||
std::string formattedMacAddress = to_string_formatted_mac_address(packet.macAddress);
|
||||
|
||||
std::vector<std::string> strings = splitIntoStrings(packet.advertisementData);
|
||||
|
||||
text_packet_index.set(to_string_dec_uint(current_packet));
|
||||
|
||||
text_packets_sent.set(to_string_dec_uint(packet.packet_count));
|
||||
|
||||
text_mac_address.set(formattedMacAddress);
|
||||
|
||||
packet_counter = packet.packet_count;
|
||||
current_packet = currentIndex;
|
||||
|
||||
dataFile.create(dataTempFilePath);
|
||||
|
||||
for (const std::string& str : strings) {
|
||||
dataFile.write(str.c_str(), str.size());
|
||||
dataFile.write("\n", 1);
|
||||
;
|
||||
}
|
||||
|
||||
dataFile.~File();
|
||||
|
||||
auto result = FileWrapper::open(dataTempFilePath);
|
||||
|
||||
if (!result)
|
||||
return;
|
||||
|
||||
dataFileWrapper = *std::move(result);
|
||||
|
||||
dataEditView.set_font_zoom(true);
|
||||
dataEditView.set_file(*dataFileWrapper);
|
||||
dataEditView.redraw(true, true);
|
||||
}
|
||||
|
||||
void BLETxView::set_parent_rect(const Rect new_parent_rect) {
|
||||
View::set_parent_rect(new_parent_rect);
|
||||
const Rect content_rect{0, header_height, new_parent_rect.width(), new_parent_rect.height() - header_height - switch_button_height};
|
||||
dataEditView.set_parent_rect(content_rect);
|
||||
}
|
||||
|
||||
BLETxView::~BLETxView() {
|
||||
delete_file(dataTempFilePath);
|
||||
transmitter_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
@@ -0,0 +1,329 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2017 Furrtek
|
||||
* Copyright (C) 2023 TJ Baginski
|
||||
*
|
||||
* 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 __BLE_TX_APP_H__
|
||||
#define __BLE_TX_APP_H__
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_transmitter.hpp"
|
||||
#include "ui_text_editor.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "ui_record_view.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "replay_thread.hpp"
|
||||
#include "log_file.hpp"
|
||||
#include "utility.hpp"
|
||||
|
||||
#include "recent_entries.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <memory>
|
||||
|
||||
class BLELoggerTx {
|
||||
public:
|
||||
Optional<File::Error> append(const std::string& filename) {
|
||||
return log_file.append(filename);
|
||||
}
|
||||
|
||||
void log_raw_data(const std::string& data);
|
||||
|
||||
private:
|
||||
LogFile log_file{};
|
||||
};
|
||||
|
||||
namespace ui {
|
||||
|
||||
enum PKT_TYPE {
|
||||
PKT_TYPE_INVALID_TYPE,
|
||||
PKT_TYPE_RAW,
|
||||
PKT_TYPE_DISCOVERY,
|
||||
PKT_TYPE_IBEACON,
|
||||
PKT_TYPE_ADV_IND,
|
||||
PKT_TYPE_ADV_DIRECT_IND,
|
||||
PKT_TYPE_ADV_NONCONN_IND,
|
||||
PKT_TYPE_ADV_SCAN_IND,
|
||||
PKT_TYPE_SCAN_REQ,
|
||||
PKT_TYPE_SCAN_RSP,
|
||||
PKT_TYPE_CONNECT_REQ,
|
||||
PKT_TYPE_LL_DATA,
|
||||
PKT_TYPE_LL_CONNECTION_UPDATE_REQ,
|
||||
PKT_TYPE_LL_CHANNEL_MAP_REQ,
|
||||
PKT_TYPE_LL_TERMINATE_IND,
|
||||
PKT_TYPE_LL_ENC_REQ,
|
||||
PKT_TYPE_LL_ENC_RSP,
|
||||
PKT_TYPE_LL_START_ENC_REQ,
|
||||
PKT_TYPE_LL_START_ENC_RSP,
|
||||
PKT_TYPE_LL_UNKNOWN_RSP,
|
||||
PKT_TYPE_LL_FEATURE_REQ,
|
||||
PKT_TYPE_LL_FEATURE_RSP,
|
||||
PKT_TYPE_LL_PAUSE_ENC_REQ,
|
||||
PKT_TYPE_LL_PAUSE_ENC_RSP,
|
||||
PKT_TYPE_LL_VERSION_IND,
|
||||
PKT_TYPE_LL_REJECT_IND,
|
||||
PKT_TYPE_NUM_PKT_TYPE
|
||||
};
|
||||
|
||||
struct BLETxPacket {
|
||||
char macAddress[13];
|
||||
char advertisementData[63];
|
||||
char packetCount[11];
|
||||
uint32_t packet_count;
|
||||
PKT_TYPE packetType;
|
||||
};
|
||||
|
||||
class BLETxView : public View {
|
||||
public:
|
||||
BLETxView(NavigationView& nav);
|
||||
BLETxView(NavigationView& nav, BLETxPacket packet);
|
||||
~BLETxView();
|
||||
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
void paint(Painter&) override{};
|
||||
|
||||
void focus() override;
|
||||
|
||||
bool is_active() const;
|
||||
void toggle();
|
||||
void start();
|
||||
void stop();
|
||||
void reset();
|
||||
void handle_replay_thread_done(const uint32_t return_code);
|
||||
void file_error();
|
||||
bool saveFile(const std::filesystem::path& path);
|
||||
|
||||
std::string title() const override { return "BLE TX"; };
|
||||
|
||||
private:
|
||||
void on_timer();
|
||||
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_random_data_change(std::string value);
|
||||
void update_current_packet(BLETxPacket packet, uint32_t currentIndex);
|
||||
|
||||
NavigationView& nav_;
|
||||
TxRadioState radio_state_{
|
||||
2'402'000'000 /* frequency */,
|
||||
4'000'000 /* bandwidth */,
|
||||
4'000'000 /* sampling rate */
|
||||
};
|
||||
app_settings::SettingsManager settings_{
|
||||
"ble_tx_app", app_settings::Mode::TX};
|
||||
|
||||
uint8_t console_color{0};
|
||||
uint32_t prev_value{0};
|
||||
|
||||
std::filesystem::path file_path{};
|
||||
std::filesystem::path packet_save_path{u"BLETX/BLETX_????.TXT"};
|
||||
uint8_t channel_number = 37;
|
||||
bool auto_channel = false;
|
||||
|
||||
char randomMac[13] = "010203040506";
|
||||
|
||||
bool is_running = false;
|
||||
|
||||
int16_t timer_count{0};
|
||||
int16_t timer_period{1};
|
||||
int16_t auto_channel_counter = 0;
|
||||
int16_t auto_channel_period{6};
|
||||
|
||||
bool repeatLoop = false;
|
||||
uint32_t packet_counter{0};
|
||||
uint32_t num_packets{0};
|
||||
uint32_t current_packet{0};
|
||||
bool random_mac = false;
|
||||
bool file_override = false;
|
||||
|
||||
typedef struct {
|
||||
uint16_t line;
|
||||
uint16_t col;
|
||||
} CursorPos;
|
||||
|
||||
std::unique_ptr<FileWrapper> dataFileWrapper{};
|
||||
File dataFile{};
|
||||
std::filesystem::path dataTempFilePath{u"BLETX/dataFileTemp.TXT"};
|
||||
std::vector<uint16_t> markedBytes{};
|
||||
CursorPos cursor_pos{};
|
||||
|
||||
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 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 = 9 * 16;
|
||||
static constexpr auto switch_button_height = 5 * 16;
|
||||
|
||||
Button button_open{
|
||||
{0 * 8, 0 * 16, 10 * 8, 2 * 16},
|
||||
"Open file"};
|
||||
|
||||
Text text_filename{
|
||||
{11 * 8, 0 * 16, 12 * 8, 16},
|
||||
"-"};
|
||||
|
||||
ProgressBar progressbar{
|
||||
{11 * 8, 1 * 16, 9 * 8, 16}};
|
||||
|
||||
Checkbox check_rand_mac{
|
||||
{21 * 8, 1 * 16},
|
||||
6,
|
||||
"Random",
|
||||
true};
|
||||
|
||||
TxFrequencyField field_frequency{
|
||||
{0 * 8, 2 * 16},
|
||||
nav_};
|
||||
|
||||
TransmitterView2 tx_view{
|
||||
{11 * 8, 2 * 16},
|
||||
/*short_ui*/ true};
|
||||
|
||||
Checkbox check_loop{
|
||||
{21 * 8, 2 * 16},
|
||||
4,
|
||||
"Loop",
|
||||
true};
|
||||
|
||||
ImageButton button_play{
|
||||
{28 * 8, 2 * 16, 2 * 8, 1 * 16},
|
||||
&bitmap_play,
|
||||
Color::green(),
|
||||
Color::black()};
|
||||
|
||||
Labels label_speed{
|
||||
{{0 * 8, 6 * 8}, "Speed:", Color::light_grey()}};
|
||||
|
||||
OptionsField options_speed{
|
||||
{7 * 8, 6 * 8},
|
||||
3,
|
||||
{{"1 ", 1}, // 16ms
|
||||
{"2 ", 2}, // 32ms
|
||||
{"3 ", 3}, // 48ms
|
||||
{"4 ", 6}, // 100ms
|
||||
{"5 ", 12}}}; // 200ms
|
||||
|
||||
OptionsField options_channel{
|
||||
{11 * 8, 6 * 8},
|
||||
5,
|
||||
{{"Ch.37 ", 37},
|
||||
{"Ch.38", 38},
|
||||
{"Ch.39", 39},
|
||||
{"Auto", 40}}};
|
||||
|
||||
OptionsField options_adv_type{
|
||||
{17 * 8, 6 * 8},
|
||||
14,
|
||||
{{"DISCOVERY ", PKT_TYPE_DISCOVERY},
|
||||
{"ADV_IND", PKT_TYPE_ADV_IND},
|
||||
{"ADV_DIRECT", PKT_TYPE_ADV_DIRECT_IND},
|
||||
{"ADV_NONCONN", PKT_TYPE_ADV_NONCONN_IND},
|
||||
{"ADV_SCAN_IND", PKT_TYPE_ADV_SCAN_IND},
|
||||
{"SCAN_REQ", PKT_TYPE_SCAN_REQ},
|
||||
{"SCAN_RSP", PKT_TYPE_SCAN_RSP},
|
||||
{"CONNECT_REQ", PKT_TYPE_CONNECT_REQ}}};
|
||||
|
||||
Labels label_packet_index{
|
||||
{{0 * 8, 10 * 8}, "Packet Index:", Color::light_grey()}};
|
||||
|
||||
Text text_packet_index{
|
||||
{13 * 8, 5 * 16, 12 * 8, 16},
|
||||
"-"};
|
||||
|
||||
Labels label_packets_sent{
|
||||
{{0 * 8, 12 * 8}, "Repeat Count:", Color::light_grey()}};
|
||||
|
||||
Text text_packets_sent{
|
||||
{13 * 8, 6 * 16, 12 * 8, 16},
|
||||
"-"};
|
||||
|
||||
Labels label_mac_address{
|
||||
{{0 * 8, 14 * 8}, "Mac Address:", Color::light_grey()}};
|
||||
|
||||
Text text_mac_address{
|
||||
{12 * 8, 7 * 16, 20 * 8, 16},
|
||||
"-"};
|
||||
|
||||
Labels label_data_packet{
|
||||
{{0 * 8, 8 * 16}, "Packet Data:", Color::light_grey()}};
|
||||
|
||||
Console console{
|
||||
{0, 9 * 16, 240, 240}};
|
||||
|
||||
TextViewer dataEditView{
|
||||
{0, 9 * 16, 240, 240}};
|
||||
|
||||
Labels label_data_index{
|
||||
{{0 * 8, 28 * 8}, "Data Index:", Color::light_grey()}};
|
||||
|
||||
Text text_data_index{
|
||||
{11 * 8, 14 * 16, 8 * 8, 16},
|
||||
"-"};
|
||||
|
||||
Button button_save_packet{
|
||||
{1 * 8, 16 * 16, 13 * 8, 2 * 16},
|
||||
"Save Packet"};
|
||||
|
||||
Button button_switch{
|
||||
{16 * 8, 16 * 16, 13 * 8, 2 * 16},
|
||||
"Switch to Rx"};
|
||||
|
||||
std::string str_log{""};
|
||||
bool logging{true};
|
||||
bool logging_done{false};
|
||||
std::string packetFileBuffer{};
|
||||
|
||||
std::unique_ptr<BLELoggerTx> logger{};
|
||||
|
||||
MessageHandlerRegistration message_handler_packet{
|
||||
Message::ID::AFSKData,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const AFSKDataMessage*>(p);
|
||||
this->on_data(message->value, message->is_data);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_tx_progress{
|
||||
Message::ID::TXProgress,
|
||||
[this](const Message* const p) {
|
||||
const auto message = *reinterpret_cast<const TXProgressMessage*>(p);
|
||||
this->on_tx_progress(message.done);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_frame_sync{
|
||||
Message::ID::DisplayFrameSync,
|
||||
[this](const Message* const) {
|
||||
this->on_timer();
|
||||
}};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
#endif /*__UI_AFSK_RX_H__*/
|
||||
@@ -65,9 +65,13 @@ std::string commodity_type(CommodityType value) {
|
||||
|
||||
} /* namespace ert */
|
||||
|
||||
void ERTLogger::on_packet(const ert::Packet& packet) {
|
||||
void ERTLogger::on_packet(const ert::Packet& packet, const uint32_t target_frequency) {
|
||||
const auto formatted = packet.symbols_formatted();
|
||||
std::string entry = ert::format::type(packet.type()) + " " + formatted.data + "/" + formatted.errors;
|
||||
|
||||
// TODO: function doesn't take uint64_t, so when >= 1<<32, weirdness will ensue!
|
||||
const auto target_frequency_str = to_string_dec_uint(target_frequency, 10);
|
||||
|
||||
std::string entry = target_frequency_str + " " + ert::format::type(packet.type()) + " " + formatted.data + "/" + formatted.errors;
|
||||
log_file.write_entry(packet.received_at(), entry);
|
||||
}
|
||||
|
||||
@@ -99,10 +103,12 @@ void RecentEntriesTable<ERTRecentEntries>::draw(
|
||||
painter.draw_string(target_rect.location(), style, line);
|
||||
}
|
||||
|
||||
ERTAppView::ERTAppView(NavigationView&) {
|
||||
ERTAppView::ERTAppView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_ert);
|
||||
|
||||
add_children({
|
||||
&field_frequency,
|
||||
&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
@@ -110,6 +116,8 @@ ERTAppView::ERTAppView(NavigationView&) {
|
||||
&recent_entries_view,
|
||||
});
|
||||
|
||||
field_frequency.set_step(1000000);
|
||||
|
||||
receiver_model.enable();
|
||||
|
||||
logger = std::make_unique<ERTLogger>();
|
||||
@@ -134,7 +142,7 @@ void ERTAppView::set_parent_rect(const Rect new_parent_rect) {
|
||||
|
||||
void ERTAppView::on_packet(const ert::Packet& packet) {
|
||||
if (logger) {
|
||||
logger->on_packet(packet);
|
||||
logger->on_packet(packet, receiver_model.target_frequency());
|
||||
}
|
||||
|
||||
if (packet.crc_ok()) {
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "ui_rssi.hpp"
|
||||
#include "ui_channel.hpp"
|
||||
|
||||
@@ -95,7 +96,7 @@ class ERTLogger {
|
||||
return log_file.append(filename);
|
||||
}
|
||||
|
||||
void on_packet(const ert::Packet& packet);
|
||||
void on_packet(const ert::Packet& packet, const uint32_t target_frequency);
|
||||
|
||||
private:
|
||||
LogFile log_file{};
|
||||
@@ -126,6 +127,7 @@ class ERTAppView : public View {
|
||||
ERTRecentEntries recent{};
|
||||
std::unique_ptr<ERTLogger> logger{};
|
||||
|
||||
NavigationView& nav_;
|
||||
RxRadioState radio_state_{
|
||||
911600000 /* frequency */,
|
||||
2500000 /* bandwidth */,
|
||||
@@ -143,6 +145,10 @@ class ERTAppView : public View {
|
||||
|
||||
static constexpr auto header_height = 1 * 16;
|
||||
|
||||
RxFrequencyField field_frequency{
|
||||
{5 * 8, 0 * 16},
|
||||
nav_};
|
||||
|
||||
RFAmpField field_rf_amp{
|
||||
{13 * 8, 0 * 16}};
|
||||
|
||||
|
||||
@@ -34,9 +34,6 @@ namespace tpms {
|
||||
|
||||
namespace format {
|
||||
|
||||
static bool use_kpa = true;
|
||||
static bool use_celsius = true;
|
||||
|
||||
std::string type(Reading::Type type) {
|
||||
return to_string_dec_uint(toUType(type), 2);
|
||||
}
|
||||
@@ -46,11 +43,11 @@ std::string id(TransponderID id) {
|
||||
}
|
||||
|
||||
std::string pressure(Pressure pressure) {
|
||||
return to_string_dec_int(use_kpa ? pressure.kilopascal() : pressure.psi(), 3);
|
||||
return to_string_dec_int(units_psi ? pressure.psi() : pressure.kilopascal(), 3);
|
||||
}
|
||||
|
||||
std::string temperature(Temperature temperature) {
|
||||
return to_string_dec_int(use_celsius ? temperature.celsius() : temperature.fahrenheit(), 3);
|
||||
return to_string_dec_int(units_fahr ? temperature.fahrenheit() : temperature.celsius(), 3);
|
||||
}
|
||||
|
||||
std::string flags(Flags flags) {
|
||||
@@ -165,18 +162,16 @@ TPMSAppView::TPMSAppView(NavigationView&) {
|
||||
options_band.set_by_value(receiver_model.target_frequency());
|
||||
|
||||
options_pressure.on_change = [this](size_t, int32_t i) {
|
||||
tpms::format::use_kpa = !i;
|
||||
tpms::format::units_psi = (bool)i;
|
||||
update_view();
|
||||
};
|
||||
|
||||
options_pressure.set_selected_index(0, true);
|
||||
options_pressure.set_selected_index(tpms::format::units_psi, true);
|
||||
|
||||
options_temperature.on_change = [this](size_t, int32_t i) {
|
||||
tpms::format::use_celsius = !i;
|
||||
tpms::format::units_fahr = (bool)i;
|
||||
update_view();
|
||||
};
|
||||
|
||||
options_temperature.set_selected_index(0, true);
|
||||
options_temperature.set_selected_index(tpms::format::units_fahr, true);
|
||||
|
||||
logger = std::make_unique<TPMSLogger>();
|
||||
if (logger) {
|
||||
|
||||
@@ -37,6 +37,17 @@
|
||||
|
||||
#include "tpms_packet.hpp"
|
||||
|
||||
namespace tpms {
|
||||
|
||||
namespace format {
|
||||
|
||||
static bool units_psi{false};
|
||||
static bool units_fahr{false};
|
||||
|
||||
} /* namespace format */
|
||||
|
||||
} /* namespace tpms */
|
||||
|
||||
namespace std {
|
||||
|
||||
} /* namespace std */
|
||||
@@ -103,11 +114,17 @@ class TPMSAppView : public View {
|
||||
|
||||
private:
|
||||
RxRadioState radio_state_{
|
||||
315000000 /* frequency*/,
|
||||
314950000 /* frequency*/,
|
||||
1750000 /* bandwidth */,
|
||||
2457600 /* sampling rate */};
|
||||
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_tpms", app_settings::Mode::RX};
|
||||
"rx_tpms",
|
||||
app_settings::Mode::RX,
|
||||
{
|
||||
{"units_psi"sv, &tpms::format::units_psi},
|
||||
{"units_fahr"sv, &tpms::format::units_fahr},
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_packet{
|
||||
Message::ID::TPMSPacket,
|
||||
@@ -129,11 +146,12 @@ class TPMSAppView : public View {
|
||||
{21 * 8, 5, 6 * 8, 4},
|
||||
};
|
||||
|
||||
// "315 MHz" TPMS sensors transmit at either 314.9 or 315 MHz but we should pick up either
|
||||
OptionsField options_band{
|
||||
{0 * 8, 0 * 16},
|
||||
3,
|
||||
{
|
||||
{"315", 315000000},
|
||||
{"315", 314950000},
|
||||
{"434", 433920000},
|
||||
}};
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ void AboutView::update() {
|
||||
break;
|
||||
case 2:
|
||||
console.writeln("NotherNgineer,zxkmm,u-foka");
|
||||
console.writeln("Netro,HTotoo");
|
||||
console.writeln("");
|
||||
break;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2017 Furrtek
|
||||
* Copyright (C) 2023 Kyle Reed
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
@@ -20,21 +21,23 @@
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include <strings.h>
|
||||
#include <algorithm>
|
||||
|
||||
#include "ui_adsb_rx.hpp"
|
||||
#include "ui_alphanum.hpp"
|
||||
|
||||
#include "rtc_time.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include "rtc_time.hpp"
|
||||
#include "string_format.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
|
||||
namespace ui {
|
||||
|
||||
bool ac_details_view_active{false};
|
||||
static std::string get_map_tag(const AircraftRecentEntry& entry) {
|
||||
return trimr(entry.callsign.empty() ? entry.icao_str : entry.callsign);
|
||||
}
|
||||
|
||||
template <>
|
||||
void RecentEntriesTable<AircraftRecentEntries>::draw(
|
||||
@@ -43,23 +46,25 @@ void RecentEntriesTable<AircraftRecentEntries>::draw(
|
||||
Painter& painter,
|
||||
const Style& style) {
|
||||
Color target_color;
|
||||
auto entry_age = entry.age;
|
||||
std::string entry_string;
|
||||
|
||||
// Color decay for flights not being updated anymore
|
||||
if (entry_age < ADSB_CURRENT) {
|
||||
entry_string = "";
|
||||
target_color = Color::green();
|
||||
} else if (entry_age < ADSB_RECENT) {
|
||||
entry_string = STR_COLOR_LIGHT_GREY;
|
||||
target_color = Color::light_grey();
|
||||
} else {
|
||||
entry_string = STR_COLOR_DARK_GREY;
|
||||
target_color = Color::grey();
|
||||
}
|
||||
switch (entry.state) {
|
||||
case ADSBAgeState::Invalid:
|
||||
case ADSBAgeState::Current:
|
||||
entry_string = "";
|
||||
target_color = Color::green();
|
||||
break;
|
||||
case ADSBAgeState::Recent:
|
||||
entry_string = STR_COLOR_LIGHT_GREY;
|
||||
target_color = Color::light_grey();
|
||||
break;
|
||||
default:
|
||||
entry_string = STR_COLOR_DARK_GREY;
|
||||
target_color = Color::grey();
|
||||
};
|
||||
|
||||
entry_string +=
|
||||
(entry.callsign[0] != ' ' ? entry.callsign + " " : entry.icaoStr + " ") +
|
||||
(entry.callsign.empty() ? entry.icao_str + " " : entry.callsign + " ") +
|
||||
to_string_dec_uint((unsigned int)(entry.pos.altitude / 100), 4) +
|
||||
to_string_dec_uint((unsigned int)entry.velo.speed, 4) +
|
||||
to_string_dec_uint((unsigned int)(entry.amp >> 9), 4) + " " +
|
||||
@@ -72,47 +77,59 @@ void RecentEntriesTable<AircraftRecentEntries>::draw(
|
||||
entry_string);
|
||||
|
||||
if (entry.pos.valid)
|
||||
painter.draw_bitmap(target_rect.location() + Point(8 * 8, 0), bitmap_target, target_color, style.background);
|
||||
painter.draw_bitmap(target_rect.location() + Point(8 * 8, 0),
|
||||
bitmap_target, target_color, style.background);
|
||||
}
|
||||
|
||||
void ADSBLogger::log_str(std::string& logline) {
|
||||
log_file.write_entry(logline);
|
||||
/* ADSBLogger ********************************************/
|
||||
|
||||
void ADSBLogger::log(const ADSBLogEntry& log_entry) {
|
||||
std::string log_line;
|
||||
log_line.reserve(100);
|
||||
|
||||
log_line = log_entry.raw_data;
|
||||
log_line += "ICAO:" + log_entry.icao;
|
||||
|
||||
if (!log_entry.callsign.empty())
|
||||
log_line += " " + log_entry.callsign;
|
||||
|
||||
if (log_entry.pos.valid)
|
||||
log_line += " Alt:" + to_string_dec_int(log_entry.pos.altitude) +
|
||||
" Lat:" + to_string_decimal(log_entry.pos.latitude, 7) +
|
||||
" Lon:" + to_string_decimal(log_entry.pos.longitude, 7);
|
||||
|
||||
if (log_entry.vel.valid)
|
||||
log_line += " Type:" + to_string_dec_uint(log_entry.vel_type) +
|
||||
" Hdg:" + to_string_dec_uint(log_entry.vel.heading) +
|
||||
" Spd: " + to_string_dec_int(log_entry.vel.speed);
|
||||
|
||||
log_file.write_entry(log_line);
|
||||
}
|
||||
|
||||
// Aircraft Details
|
||||
void ADSBRxAircraftDetailsView::focus() {
|
||||
button_close.focus();
|
||||
}
|
||||
|
||||
ADSBRxAircraftDetailsView::~ADSBRxAircraftDetailsView() {
|
||||
on_close_();
|
||||
}
|
||||
/* ADSBRxAircraftDetailsView *****************************/
|
||||
|
||||
ADSBRxAircraftDetailsView::ADSBRxAircraftDetailsView(
|
||||
NavigationView& nav,
|
||||
const AircraftRecentEntry& entry,
|
||||
const std::function<void(void)> on_close)
|
||||
: entry_copy(entry),
|
||||
on_close_(on_close) {
|
||||
add_children({&labels,
|
||||
&text_icao_address,
|
||||
&text_registration,
|
||||
&text_manufacturer,
|
||||
&text_model,
|
||||
&text_type,
|
||||
&text_number_of_engines,
|
||||
&text_engine_type,
|
||||
&text_owner,
|
||||
&text_operator,
|
||||
&button_close});
|
||||
const AircraftRecentEntry& entry) {
|
||||
add_children(
|
||||
{&labels,
|
||||
&text_icao_address,
|
||||
&text_registration,
|
||||
&text_manufacturer,
|
||||
&text_model,
|
||||
&text_type,
|
||||
&text_number_of_engines,
|
||||
&text_engine_type,
|
||||
&text_owner,
|
||||
&text_operator,
|
||||
&button_close});
|
||||
|
||||
std::unique_ptr<ADSBLogger> logger{};
|
||||
|
||||
icao_code = to_string_hex(entry_copy.ICAO_address, 6);
|
||||
text_icao_address.set(to_string_hex(entry_copy.ICAO_address, 6));
|
||||
text_icao_address.set(entry.icao_str);
|
||||
|
||||
// Try getting the aircraft information from icao24.db
|
||||
return_code = db.retrieve_aircraft_record(&aircraft_record, icao_code);
|
||||
std::database db{};
|
||||
std::database::AircraftDBRecord aircraft_record;
|
||||
auto return_code = db.retrieve_aircraft_record(&aircraft_record, entry.icao_str);
|
||||
switch (return_code) {
|
||||
case DATABASE_RECORD_FOUND:
|
||||
text_registration.set(aircraft_record.aircraft_registration);
|
||||
@@ -120,6 +137,7 @@ ADSBRxAircraftDetailsView::ADSBRxAircraftDetailsView(
|
||||
text_model.set(aircraft_record.aircraft_model);
|
||||
text_owner.set(aircraft_record.aircraft_owner);
|
||||
text_operator.set(aircraft_record.aircraft_operator);
|
||||
|
||||
// Check for ICAO type, e.g. L2J
|
||||
if (strlen(aircraft_record.icao_type) == 3) {
|
||||
switch (aircraft_record.icao_type[0]) {
|
||||
@@ -142,7 +160,8 @@ ADSBRxAircraftDetailsView::ADSBRxAircraftDetailsView(
|
||||
text_type.set("Tilt-wing aircraft");
|
||||
break;
|
||||
}
|
||||
text_number_of_engines.set(std::string(1, aircraft_record.icao_type[1]));
|
||||
|
||||
text_number_of_engines.set(std::string{1, aircraft_record.icao_type[1]});
|
||||
switch (aircraft_record.icao_type[2]) {
|
||||
case 'P':
|
||||
text_engine_type.set("Piston engine");
|
||||
@@ -157,8 +176,8 @@ ADSBRxAircraftDetailsView::ADSBRxAircraftDetailsView(
|
||||
text_engine_type.set("Electric engine");
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Check for ICAO type designator
|
||||
else if (strlen(aircraft_record.icao_type) == 4) {
|
||||
if (strcmp(aircraft_record.icao_type, "SHIP") == 0)
|
||||
@@ -179,77 +198,49 @@ ADSBRxAircraftDetailsView::ADSBRxAircraftDetailsView(
|
||||
text_type.set("Powered parachute/paraplane");
|
||||
}
|
||||
break;
|
||||
|
||||
case DATABASE_NOT_FOUND:
|
||||
text_manufacturer.set("No icao24.db file");
|
||||
break;
|
||||
}
|
||||
|
||||
button_close.on_select = [&nav](Button&) {
|
||||
ac_details_view_active = false;
|
||||
nav.pop();
|
||||
};
|
||||
};
|
||||
|
||||
// End of Aicraft details
|
||||
|
||||
void ADSBRxDetailsView::focus() {
|
||||
button_see_map.focus();
|
||||
}
|
||||
|
||||
void ADSBRxDetailsView::update(const AircraftRecentEntry& entry) {
|
||||
entry_copy = entry;
|
||||
uint32_t age = entry_copy.age;
|
||||
|
||||
if (age < 60)
|
||||
text_last_seen.set(to_string_dec_uint(age) + " seconds ago");
|
||||
else
|
||||
text_last_seen.set(to_string_dec_uint(age / 60) + " minutes ago");
|
||||
|
||||
text_infos.set(entry_copy.info_string);
|
||||
if (entry_copy.velo.heading < 360 && entry_copy.velo.speed >= 0) { // I don't like this but...
|
||||
text_info2.set("Hdg:" + to_string_dec_uint(entry_copy.velo.heading) + " Spd:" + to_string_dec_int(entry_copy.velo.speed));
|
||||
} else {
|
||||
text_info2.set("");
|
||||
}
|
||||
text_frame_pos_even.set(to_string_hex_array(entry_copy.frame_pos_even.get_raw_data(), 14));
|
||||
text_frame_pos_odd.set(to_string_hex_array(entry_copy.frame_pos_odd.get_raw_data(), 14));
|
||||
|
||||
if (send_updates) {
|
||||
geomap_view->update_tag(trimr(entry.callsign[0] != ' ' ? entry.callsign : to_string_hex(entry.ICAO_address, 6)));
|
||||
geomap_view->update_position(entry_copy.pos.latitude, entry_copy.pos.longitude, entry_copy.velo.heading, entry_copy.pos.altitude);
|
||||
}
|
||||
void ADSBRxAircraftDetailsView::focus() {
|
||||
button_close.focus();
|
||||
}
|
||||
|
||||
ADSBRxDetailsView::~ADSBRxDetailsView() {
|
||||
ac_details_view_active = false;
|
||||
on_close_();
|
||||
}
|
||||
/* ADSBRxDetailsView *************************************/
|
||||
|
||||
ADSBRxDetailsView::ADSBRxDetailsView(
|
||||
NavigationView& nav,
|
||||
const AircraftRecentEntry& entry,
|
||||
const std::function<void(void)> on_close)
|
||||
: entry_copy(entry),
|
||||
on_close_(on_close) {
|
||||
add_children({&labels,
|
||||
&text_icao_address,
|
||||
&text_callsign,
|
||||
&text_last_seen,
|
||||
&text_airline,
|
||||
&text_country,
|
||||
&text_infos,
|
||||
&text_info2,
|
||||
&text_frame_pos_even,
|
||||
&text_frame_pos_odd,
|
||||
&button_aircraft_details,
|
||||
&button_see_map});
|
||||
const AircraftRecentEntry& entry)
|
||||
: entry_(entry) {
|
||||
add_children(
|
||||
{&labels,
|
||||
&text_icao_address,
|
||||
&text_callsign,
|
||||
&text_last_seen,
|
||||
&text_airline,
|
||||
&text_country,
|
||||
&text_infos,
|
||||
&text_info2,
|
||||
&text_frame_pos_even,
|
||||
&text_frame_pos_odd,
|
||||
&button_aircraft_details,
|
||||
&button_see_map});
|
||||
|
||||
std::unique_ptr<ADSBLogger> logger{};
|
||||
update(entry_copy);
|
||||
// The following won't change for a given airborne aircraft.
|
||||
// Try getting the airline's name from airlines.db.
|
||||
// NB: Only works once callsign has been read and won't be updated.
|
||||
std::database db;
|
||||
std::database::AirlinesDBRecord airline_record;
|
||||
std::string airline_code = entry_.callsign.substr(0, 3);
|
||||
auto return_code = db.retrieve_airline_record(&airline_record, airline_code);
|
||||
|
||||
// The following won't (shouldn't !) change for a given airborne aircraft
|
||||
// Try getting the airline's name from airlines.db
|
||||
airline_code = entry_copy.callsign.substr(0, 3);
|
||||
return_code = db.retrieve_airline_record(&airline_record, airline_code);
|
||||
switch (return_code) {
|
||||
case DATABASE_RECORD_FOUND:
|
||||
text_airline.set(airline_record.airline);
|
||||
@@ -260,34 +251,125 @@ ADSBRxDetailsView::ADSBRxDetailsView(
|
||||
break;
|
||||
}
|
||||
|
||||
text_callsign.set(entry_copy.callsign);
|
||||
text_icao_address.set(to_string_hex(entry_copy.ICAO_address, 6));
|
||||
text_icao_address.set(entry_.icao_str);
|
||||
|
||||
button_aircraft_details.on_select = [this, &nav](Button&) {
|
||||
ac_details_view_active = true;
|
||||
aircraft_details_view = nav.push<ADSBRxAircraftDetailsView>(entry_copy, [this]() { send_updates = false; });
|
||||
send_updates = false;
|
||||
aircraft_details_view_ = nav.push<ADSBRxAircraftDetailsView>(entry_);
|
||||
nav.set_on_pop([this]() {
|
||||
aircraft_details_view_ = nullptr;
|
||||
refresh_ui();
|
||||
});
|
||||
};
|
||||
|
||||
button_see_map.on_select = [this, &nav](Button&) {
|
||||
if (!send_updates) { // Prevent recursively launching the map
|
||||
geomap_view = nav.push<GeoMapView>(
|
||||
trimr(entry_copy.callsign[0] != ' ' ? entry_copy.callsign : entry_copy.icaoStr),
|
||||
entry_copy.pos.altitude,
|
||||
GeoPos::alt_unit::FEET,
|
||||
entry_copy.pos.latitude,
|
||||
entry_copy.pos.longitude,
|
||||
entry_copy.velo.heading,
|
||||
[this]() {
|
||||
send_updates = false;
|
||||
});
|
||||
send_updates = true;
|
||||
}
|
||||
geomap_view_ = nav.push<GeoMapView>(
|
||||
get_map_tag(entry_),
|
||||
entry_.pos.altitude,
|
||||
GeoPos::alt_unit::FEET,
|
||||
entry_.pos.latitude,
|
||||
entry_.pos.longitude,
|
||||
entry_.velo.heading);
|
||||
nav.set_on_pop([this]() {
|
||||
geomap_view_ = nullptr;
|
||||
refresh_ui();
|
||||
});
|
||||
};
|
||||
|
||||
refresh_ui();
|
||||
};
|
||||
|
||||
void ADSBRxView::focus() {
|
||||
field_vga.focus();
|
||||
void ADSBRxDetailsView::focus() {
|
||||
button_see_map.focus();
|
||||
}
|
||||
|
||||
void ADSBRxDetailsView::update(const AircraftRecentEntry& entry) {
|
||||
entry_ = entry;
|
||||
|
||||
if (aircraft_details_view_) {
|
||||
// AC Details view is showing, nothing to update.
|
||||
} else if (geomap_view_) {
|
||||
// Map is showing, update the current item.
|
||||
geomap_view_->update_tag(get_map_tag(entry_));
|
||||
geomap_view_->update_position(entry.pos.latitude, entry.pos.longitude, entry.velo.heading, entry.pos.altitude);
|
||||
} else {
|
||||
// Details is showing, update details.
|
||||
refresh_ui();
|
||||
}
|
||||
}
|
||||
|
||||
void ADSBRxDetailsView::clear_map_markers() {
|
||||
if (geomap_view_)
|
||||
geomap_view_->clear_markers();
|
||||
}
|
||||
|
||||
bool ADSBRxDetailsView::add_map_marker(const AircraftRecentEntry& entry) {
|
||||
// Map not shown, can't add markers.
|
||||
if (!geomap_view_)
|
||||
return false;
|
||||
|
||||
GeoMarker marker{};
|
||||
marker.lon = entry.pos.longitude;
|
||||
marker.lat = entry.pos.latitude;
|
||||
marker.angle = entry.velo.heading;
|
||||
marker.tag = get_map_tag(entry);
|
||||
|
||||
auto markerStored = geomap_view_->store_marker(marker);
|
||||
return markerStored == MARKER_STORED;
|
||||
}
|
||||
|
||||
void ADSBRxDetailsView::refresh_ui() {
|
||||
auto age = entry_.age;
|
||||
if (age < 60)
|
||||
text_last_seen.set(to_string_dec_uint(age) + " seconds ago");
|
||||
else
|
||||
text_last_seen.set(to_string_dec_uint(age / 60) + " minutes ago");
|
||||
|
||||
text_callsign.set(entry_.callsign);
|
||||
text_infos.set(entry_.info_string);
|
||||
if (entry_.velo.heading < 360 && entry_.velo.speed >= 0)
|
||||
text_info2.set("Hdg:" + to_string_dec_uint(entry_.velo.heading) +
|
||||
" Spd:" + to_string_dec_int(entry_.velo.speed));
|
||||
else
|
||||
text_info2.set("");
|
||||
|
||||
text_frame_pos_even.set(to_string_hex_array(entry_.frame_pos_even.get_raw_data(), 14));
|
||||
text_frame_pos_odd.set(to_string_hex_array(entry_.frame_pos_odd.get_raw_data(), 14));
|
||||
}
|
||||
|
||||
/* ADSBRxView ********************************************/
|
||||
|
||||
ADSBRxView::ADSBRxView(NavigationView& nav) {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_adsb_rx);
|
||||
add_children(
|
||||
{&labels,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&field_rf_amp,
|
||||
&rssi,
|
||||
&recent_entries_view,
|
||||
&status_frame,
|
||||
&status_good_frame});
|
||||
|
||||
recent_entries_view.set_parent_rect({0, 16, 240, 272});
|
||||
recent_entries_view.on_select = [this, &nav](const AircraftRecentEntry& entry) {
|
||||
detail_key = entry.key();
|
||||
details_view = nav.push<ADSBRxDetailsView>(entry);
|
||||
|
||||
nav.set_on_pop([this]() {
|
||||
detail_key = AircraftRecentEntry::invalid_key;
|
||||
details_view = nullptr;
|
||||
});
|
||||
};
|
||||
|
||||
signal_token_tick_second = rtc_time::signal_tick_second += [this]() {
|
||||
on_tick_second();
|
||||
};
|
||||
|
||||
logger = std::make_unique<ADSBLogger>();
|
||||
logger->append(LOG_ROOT_DIR "/ADSB.TXT");
|
||||
|
||||
receiver_model.enable();
|
||||
baseband::set_adsb();
|
||||
}
|
||||
|
||||
ADSBRxView::~ADSBRxView() {
|
||||
@@ -296,232 +378,185 @@ ADSBRxView::~ADSBRxView() {
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
AircraftRecentEntry ADSBRxView::find_or_create_entry(uint32_t ICAO_address) {
|
||||
auto it = find(recent, ICAO_address);
|
||||
|
||||
// If not found
|
||||
if (it == std::end(recent)) {
|
||||
recent.emplace_front(ICAO_address); // Add it
|
||||
it = find(recent, ICAO_address); // Find it again
|
||||
}
|
||||
return *it;
|
||||
}
|
||||
|
||||
void ADSBRxView::replace_entry(AircraftRecentEntry& entry) {
|
||||
uint32_t ICAO_address = entry.ICAO_address;
|
||||
|
||||
std::replace_if(
|
||||
recent.begin(), recent.end(),
|
||||
[ICAO_address](const AircraftRecentEntry& compEntry) { return ICAO_address == compEntry.ICAO_address; },
|
||||
entry);
|
||||
}
|
||||
|
||||
void ADSBRxView::remove_old_entries() {
|
||||
auto it = recent.rbegin();
|
||||
auto end = recent.rend();
|
||||
while (it != end) {
|
||||
if (it->age_state >= 4) {
|
||||
std::advance(it, 1);
|
||||
recent.erase(it.base());
|
||||
} else {
|
||||
break; // stop looking because the list is sorted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ADSBRxView::sort_entries_by_state() {
|
||||
// Sorting List pn age_state using lambda function as comparator
|
||||
recent.sort([](const AircraftRecentEntry& left, const AircraftRecentEntry& right) { return (left.age_state < right.age_state); });
|
||||
void ADSBRxView::focus() {
|
||||
field_vga.focus();
|
||||
}
|
||||
|
||||
void ADSBRxView::on_frame(const ADSBFrameMessage* message) {
|
||||
logger = std::make_unique<ADSBLogger>();
|
||||
rtc::RTC datetime;
|
||||
std::string callsign;
|
||||
std::string str_info;
|
||||
std::string logentry;
|
||||
|
||||
auto frame = message->frame;
|
||||
uint32_t ICAO_address = frame.get_ICAO_address();
|
||||
status_frame.toggle();
|
||||
|
||||
if (frame.check_CRC() && ICAO_address) {
|
||||
rtcGetTime(&RTCD1, &datetime);
|
||||
auto entry = find_or_create_entry(ICAO_address);
|
||||
frame.set_rx_timestamp(datetime.minute() * 60 + datetime.second());
|
||||
entry.reset_age();
|
||||
if (entry.hits == 0) {
|
||||
entry.amp = message->amp; // Store amplitude on first hit
|
||||
} else {
|
||||
entry.amp = ((entry.amp * 15) + message->amp) >> 4; // Update smoothed amplitude on updates
|
||||
// Bad frame, skip it.
|
||||
if (!frame.check_CRC() || ICAO_address == 0)
|
||||
return;
|
||||
|
||||
ADSBLogEntry log_entry;
|
||||
status_good_frame.toggle();
|
||||
|
||||
rtc::RTC datetime;
|
||||
rtcGetTime(&RTCD1, &datetime);
|
||||
frame.set_rx_timestamp(datetime.minute() * 60 + datetime.second());
|
||||
|
||||
// NB: Reference to update entry in-place.
|
||||
auto& entry = find_or_create_entry(ICAO_address);
|
||||
entry.inc_hit();
|
||||
entry.reset_age();
|
||||
|
||||
// Store smoothed amplitude on updates.
|
||||
entry.amp = entry.hits == 0
|
||||
? message->amp
|
||||
: ((entry.amp * 15) + message->amp) >> 4;
|
||||
|
||||
log_entry.raw_data = to_string_hex_array(frame.get_raw_data(), 14);
|
||||
log_entry.icao = entry.icao_str;
|
||||
|
||||
if (frame.get_DF() == DF_ADSB) {
|
||||
uint8_t msg_type = frame.get_msg_type();
|
||||
uint8_t msg_sub = frame.get_msg_sub();
|
||||
uint8_t* raw_data = frame.get_raw_data();
|
||||
|
||||
// 4: // surveillance, altitude reply
|
||||
if ((msg_type >= AIRCRAFT_ID_L) && (msg_type <= AIRCRAFT_ID_H)) {
|
||||
entry.set_callsign(decode_frame_id(frame));
|
||||
log_entry.callsign = entry.callsign;
|
||||
}
|
||||
|
||||
entry.inc_hit();
|
||||
if (logger) {
|
||||
logentry += to_string_hex_array(frame.get_raw_data(), 14) + " ";
|
||||
logentry += "ICAO:" + entry.icaoStr + " ";
|
||||
}
|
||||
// 9:
|
||||
// 18: // Extended squitter/non-transponder
|
||||
// 21: // Comm-B, identity reply
|
||||
// 20: // Comm-B, altitude reply
|
||||
else if (((msg_type >= AIRBORNE_POS_BARO_L) && (msg_type <= AIRBORNE_POS_BARO_H)) ||
|
||||
((msg_type >= AIRBORNE_POS_GPS_L) && (msg_type <= AIRBORNE_POS_GPS_H))) {
|
||||
entry.set_frame_pos(frame, raw_data[6] & 4);
|
||||
log_entry.pos = entry.pos;
|
||||
|
||||
if (frame.get_DF() == DF_ADSB) {
|
||||
uint8_t msg_type = frame.get_msg_type();
|
||||
uint8_t msg_sub = frame.get_msg_sub();
|
||||
uint8_t* raw_data = frame.get_raw_data();
|
||||
if (entry.pos.valid) {
|
||||
std::string str_info =
|
||||
"Alt:" + to_string_dec_int(entry.pos.altitude) +
|
||||
" Lat:" + to_string_decimal(entry.pos.latitude, 2) +
|
||||
" Lon:" + to_string_decimal(entry.pos.longitude, 2);
|
||||
|
||||
// 4: // surveillance, altitude reply
|
||||
if ((msg_type >= AIRCRAFT_ID_L) && (msg_type <= AIRCRAFT_ID_H)) {
|
||||
callsign = decode_frame_id(frame);
|
||||
entry.set_callsign(callsign);
|
||||
if (logger) {
|
||||
logentry += callsign + " ";
|
||||
}
|
||||
entry.set_info_string(std::move(str_info));
|
||||
}
|
||||
// 9:
|
||||
// 18: { // Extended squitter/non-transponder
|
||||
// 21: // Comm-B, identity reply
|
||||
// 20: // Comm-B, altitude reply
|
||||
else if (((msg_type >= AIRBORNE_POS_BARO_L) && (msg_type <= AIRBORNE_POS_BARO_H)) ||
|
||||
((msg_type >= AIRBORNE_POS_GPS_L) && (msg_type <= AIRBORNE_POS_GPS_H))) {
|
||||
entry.set_frame_pos(frame, raw_data[6] & 4);
|
||||
|
||||
if (entry.pos.valid) {
|
||||
str_info = "Alt:" + to_string_dec_int(entry.pos.altitude) +
|
||||
" Lat:" + to_string_decimal(entry.pos.latitude, 2) +
|
||||
" Lon:" + to_string_decimal(entry.pos.longitude, 2);
|
||||
|
||||
entry.set_info_string(str_info);
|
||||
|
||||
if (logger) {
|
||||
// printing the coordinates in the log file with more
|
||||
// resolution, as we are not constrained by screen
|
||||
// real estate there:
|
||||
|
||||
std::string log_info = "Alt:" + to_string_dec_int(entry.pos.altitude) +
|
||||
" Lat:" + to_string_decimal(entry.pos.latitude, 7) +
|
||||
" Lon:" + to_string_decimal(entry.pos.longitude, 7);
|
||||
logentry += log_info + " ";
|
||||
}
|
||||
}
|
||||
} else if (msg_type == AIRBORNE_VEL && msg_sub >= VEL_GND_SUBSONIC && msg_sub <= VEL_AIR_SUPERSONIC) {
|
||||
entry.set_frame_velo(frame);
|
||||
if (logger) {
|
||||
logger->append(LOG_ROOT_DIR "/ADSB.TXT");
|
||||
logentry += "Type:" + to_string_dec_uint(msg_sub) +
|
||||
" Hdg:" + to_string_dec_uint(entry.velo.heading) +
|
||||
" Spd: " + to_string_dec_int(entry.velo.speed);
|
||||
}
|
||||
}
|
||||
replace_entry(entry);
|
||||
} // frame.get_DF() == DF_ADSB
|
||||
|
||||
if (logger) {
|
||||
logger->append(LOG_ROOT_DIR "/ADSB.TXT");
|
||||
// will log each frame in format:
|
||||
// 20171103100227 8DADBEEFDEADBEEFDEADBEEFDEADBEEF ICAO:nnnnnn callsign Alt:nnnnnn Latnnn.nn Lonnnn.nn
|
||||
logger->log_str(logentry);
|
||||
} else if (msg_type == AIRBORNE_VEL && msg_sub >= VEL_GND_SUBSONIC && msg_sub <= VEL_AIR_SUPERSONIC) {
|
||||
entry.set_frame_velo(frame);
|
||||
log_entry.vel = entry.velo;
|
||||
log_entry.vel_type = msg_sub;
|
||||
}
|
||||
}
|
||||
|
||||
logger->log(log_entry);
|
||||
}
|
||||
|
||||
void ADSBRxView::on_tick_second() {
|
||||
if (recent.size() <= 16) { // Not many entries update everything (16 is one screen full)
|
||||
updateDetailsAndMap(1);
|
||||
updateRecentEntries();
|
||||
} else if (updateState == 0) { // Even second
|
||||
updateState = 1;
|
||||
updateDetailsAndMap(2);
|
||||
} else { // Odd second only performed when there are many entries
|
||||
updateState = 0;
|
||||
updateRecentEntries();
|
||||
status_frame.reset();
|
||||
status_good_frame.reset();
|
||||
|
||||
++tick_count;
|
||||
++ticks_since_marker_refresh;
|
||||
|
||||
// Small list, update all at once.
|
||||
if (recent.size() <= max_update_entries) {
|
||||
update_recent_entries(/*age_delta*/ 1);
|
||||
refresh_ui();
|
||||
return;
|
||||
}
|
||||
|
||||
// Too many items, split update work into two phases:
|
||||
// Entry maintenance and UI update.
|
||||
if ((tick_count & 1) == 0)
|
||||
update_recent_entries(/*age_delta*/ 2);
|
||||
else
|
||||
refresh_ui();
|
||||
}
|
||||
|
||||
void ADSBRxView::updateDetailsAndMap(int ageStep) {
|
||||
ui::GeoMarker marker;
|
||||
bool storeNewMarkers = false;
|
||||
void ADSBRxView::refresh_ui() {
|
||||
// There's only one ticks handler, but 3 UIs that need to be updated.
|
||||
// This code will dispatch updates to the currently active view.
|
||||
|
||||
// NB: Temporarily pausing updates in rtc_timer_tick context when viewing AC Details screen (kludge for some Guru faults)
|
||||
// TODO: More targeted blocking of updates in rtc_timer_tick when ADSB processes are running
|
||||
if (ac_details_view_active)
|
||||
return;
|
||||
if (details_view) {
|
||||
// The details view is showing, forward updates to that UI.
|
||||
bool current_updated = false;
|
||||
bool map_needs_update = false;
|
||||
|
||||
// Sort and truncate the entries, grouped, newest group first
|
||||
sort_entries_by_state();
|
||||
truncate_entries(recent);
|
||||
remove_old_entries();
|
||||
if (details_view->map_active()) {
|
||||
// Is it time to clear and refresh the map's markers?
|
||||
if (ticks_since_marker_refresh >= MARKER_UPDATE_SECONDS) {
|
||||
map_needs_update = true;
|
||||
ticks_since_marker_refresh = 0;
|
||||
details_view->clear_map_markers();
|
||||
}
|
||||
} else {
|
||||
// Refresh map immediately once active.
|
||||
ticks_since_marker_refresh = MARKER_UPDATE_SECONDS;
|
||||
}
|
||||
|
||||
// Calculate if it is time to update markers
|
||||
if (send_updates && details_view && details_view->geomap_view) {
|
||||
ticksSinceMarkerRefresh += ageStep;
|
||||
if (ticksSinceMarkerRefresh >= MARKER_UPDATE_SECONDS) { // Update other aircraft every few seconds
|
||||
storeNewMarkers = true;
|
||||
ticksSinceMarkerRefresh = 0;
|
||||
// Process the entries list.
|
||||
for (const auto& entry : recent) {
|
||||
// Found the entry being shown in details view. Update it.
|
||||
if (entry.key() == detail_key) {
|
||||
details_view->update(entry);
|
||||
current_updated = true;
|
||||
}
|
||||
|
||||
// NB: current entry also gets a marker so it shows up if map is panned.
|
||||
if (map_needs_update && entry.pos.valid && entry.state <= ADSBAgeState::Recent) {
|
||||
map_needs_update = details_view->add_map_marker(entry);
|
||||
}
|
||||
|
||||
// Any work left to do?
|
||||
if (current_updated && !map_needs_update)
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
ticksSinceMarkerRefresh = MARKER_UPDATE_SECONDS; // Send the markers as soon as the geoview exists
|
||||
}
|
||||
|
||||
// Increment age, and pass updates to the details and map
|
||||
const bool otherMarkersCanBeSent = send_updates && storeNewMarkers && details_view && details_view->geomap_view; // Save retesting all of this
|
||||
MapMarkerStored markerStored = MARKER_NOT_STORED;
|
||||
if (otherMarkersCanBeSent) {
|
||||
details_view->geomap_view->clear_markers();
|
||||
}
|
||||
// Loop through all entries
|
||||
for (auto& entry : recent) {
|
||||
entry.inc_age(ageStep);
|
||||
|
||||
// Only if there is a details view
|
||||
if (send_updates && details_view) {
|
||||
if (entry.key() == detailed_entry_key) // Check if the ICAO address match
|
||||
{
|
||||
details_view->update(entry);
|
||||
}
|
||||
// Store if the view is present and the list isn't full
|
||||
// Note -- Storing the selected entry too, in case map panning occurs
|
||||
if (otherMarkersCanBeSent && (markerStored != MARKER_LIST_FULL) && entry.pos.valid && (entry.age_state <= 2)) {
|
||||
marker.lon = entry.pos.longitude;
|
||||
marker.lat = entry.pos.latitude;
|
||||
marker.angle = entry.velo.heading;
|
||||
marker.tag = trimr(entry.callsign[0] != ' ' ? entry.callsign : entry.icaoStr);
|
||||
markerStored = details_view->geomap_view->store_marker(marker);
|
||||
}
|
||||
}
|
||||
} // Loop through all entries, if only to update the age
|
||||
}
|
||||
|
||||
void ADSBRxView::updateRecentEntries() {
|
||||
// Redraw the list of aircraft
|
||||
if (!send_updates) {
|
||||
// Main page is the top view. Redraw the entries view.
|
||||
recent_entries_view.set_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
ADSBRxView::ADSBRxView(NavigationView& nav) {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_adsb_rx);
|
||||
add_children({&labels,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&field_rf_amp,
|
||||
&rssi,
|
||||
&recent_entries_view});
|
||||
void ADSBRxView::update_recent_entries(int age_delta) {
|
||||
for (auto& entry : recent)
|
||||
entry.inc_age(age_delta);
|
||||
|
||||
recent_entries_view.set_parent_rect({0, 16, 240, 272});
|
||||
recent_entries_view.on_select = [this, &nav](const AircraftRecentEntry& entry) {
|
||||
detailed_entry_key = entry.key();
|
||||
details_view = nav.push<ADSBRxDetailsView>(
|
||||
entry,
|
||||
[this]() {
|
||||
send_updates = false;
|
||||
});
|
||||
send_updates = true;
|
||||
};
|
||||
// Sort and truncate the entries, grouped by state, newest first.
|
||||
sort_entries_by_state();
|
||||
truncate_entries(recent);
|
||||
remove_expired_entries();
|
||||
}
|
||||
|
||||
signal_token_tick_second = rtc_time::signal_tick_second += [this]() {
|
||||
on_tick_second();
|
||||
};
|
||||
AircraftRecentEntry& ADSBRxView::find_or_create_entry(uint32_t ICAO_address) {
|
||||
// Find ...
|
||||
auto it = find(recent, ICAO_address);
|
||||
if (it != recent.end())
|
||||
return *it;
|
||||
|
||||
baseband::set_adsb();
|
||||
// ... or Create.
|
||||
return recent.emplace_front(ICAO_address);
|
||||
}
|
||||
|
||||
receiver_model.enable();
|
||||
void ADSBRxView::sort_entries_by_state() {
|
||||
recent.sort([](const auto& left, const auto& right) {
|
||||
return (left.state < right.state);
|
||||
});
|
||||
}
|
||||
|
||||
void ADSBRxView::remove_expired_entries() {
|
||||
// NB: Assumes entried are sorted with oldest last.
|
||||
auto it = recent.rbegin();
|
||||
auto end = recent.rend();
|
||||
|
||||
// Find the first !expired entry from the back.
|
||||
while (it != end) {
|
||||
if (it->state != ADSBAgeState::Expired)
|
||||
break;
|
||||
|
||||
std::advance(it, 1);
|
||||
}
|
||||
|
||||
// Remove the range of expired items.
|
||||
recent.erase(it.base(), recent.end());
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2017 Furrtek
|
||||
* Copyright (C) 2023 Kyle Reed
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
@@ -21,29 +22,23 @@
|
||||
*/
|
||||
|
||||
#include "ui.hpp"
|
||||
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_geomap.hpp"
|
||||
#include "string_format.hpp"
|
||||
|
||||
#include "file.hpp"
|
||||
#include "database.hpp"
|
||||
#include "recent_entries.hpp"
|
||||
#include "log_file.hpp"
|
||||
#include "adsb.hpp"
|
||||
#include "message.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "crc.hpp"
|
||||
#include "database.hpp"
|
||||
#include "file.hpp"
|
||||
#include "log_file.hpp"
|
||||
#include "message.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "recent_entries.hpp"
|
||||
#include "string_format.hpp"
|
||||
|
||||
using namespace adsb;
|
||||
|
||||
namespace ui {
|
||||
|
||||
#define ADSB_CURRENT 10 // Seconds
|
||||
#define ADSB_RECENT 30 // Seconds
|
||||
#define ADSB_REMOVE 300 // Used for removing old entries
|
||||
|
||||
#define AIRCRAFT_ID_L 1 // aircraft ID message type (lowest type id)
|
||||
#define AIRCRAFT_ID_H 4 // aircraft ID message type (highest type id)
|
||||
|
||||
@@ -66,8 +61,26 @@ namespace ui {
|
||||
#define VEL_AIR_SUBSONIC 3
|
||||
#define VEL_AIR_SUPERSONIC 4
|
||||
|
||||
#define O_E_FRAME_TIMEOUT 20 // timeout between odd and even frames
|
||||
#define O_E_FRAME_TIMEOUT 20 // timeout between odd and even frames
|
||||
#define MARKER_UPDATE_SECONDS 5 // "other" map marker redraw interval
|
||||
|
||||
/* Thresholds (in seconds) that define the transition between ages. */
|
||||
struct ADSBAgeLimit {
|
||||
static constexpr int Current = 10;
|
||||
static constexpr int Recent = 30;
|
||||
static constexpr int Expired = 300;
|
||||
};
|
||||
|
||||
/* Age states used for sorting and drawing recent entries. */
|
||||
enum class ADSBAgeState : uint8_t {
|
||||
Invalid,
|
||||
Current,
|
||||
Recent,
|
||||
Old,
|
||||
Expired,
|
||||
};
|
||||
|
||||
/* Data extracted from ADSB frames. */
|
||||
struct AircraftRecentEntry {
|
||||
using Key = uint32_t;
|
||||
|
||||
@@ -76,30 +89,30 @@ struct AircraftRecentEntry {
|
||||
uint32_t ICAO_address{};
|
||||
uint16_t hits{0};
|
||||
|
||||
uint16_t age_state{1};
|
||||
uint32_t age{0};
|
||||
ADSBAgeState state{ADSBAgeState::Invalid};
|
||||
uint32_t age{0}; // In seconds
|
||||
uint32_t amp{0};
|
||||
adsb_pos pos{false, 0, 0, 0};
|
||||
adsb_vel velo{false, 0, 999, 0};
|
||||
ADSBFrame frame_pos_even{};
|
||||
ADSBFrame frame_pos_odd{};
|
||||
|
||||
std::string icaoStr{" "};
|
||||
std::string callsign{" "};
|
||||
std::string info_string{""};
|
||||
std::string icao_str{};
|
||||
std::string callsign{};
|
||||
std::string info_string{};
|
||||
|
||||
AircraftRecentEntry(
|
||||
const uint32_t ICAO_address)
|
||||
AircraftRecentEntry(const uint32_t ICAO_address)
|
||||
: ICAO_address{ICAO_address} {
|
||||
this->icaoStr = to_string_hex(ICAO_address, 6);
|
||||
this->icao_str = to_string_hex(ICAO_address, 6);
|
||||
}
|
||||
|
||||
/* RecentEntries helpers expect a "key" on every item. */
|
||||
Key key() const {
|
||||
return ICAO_address;
|
||||
}
|
||||
|
||||
void set_callsign(std::string& new_callsign) {
|
||||
callsign = new_callsign;
|
||||
void set_callsign(std::string new_callsign) {
|
||||
callsign = std::move(new_callsign);
|
||||
}
|
||||
|
||||
void inc_hit() {
|
||||
@@ -122,8 +135,8 @@ struct AircraftRecentEntry {
|
||||
velo = decode_frame_velo(frame);
|
||||
}
|
||||
|
||||
void set_info_string(std::string& new_info_string) {
|
||||
info_string = new_info_string;
|
||||
void set_info_string(std::string new_info_string) {
|
||||
info_string = std::move(new_info_string);
|
||||
}
|
||||
|
||||
void reset_age() {
|
||||
@@ -132,59 +145,59 @@ struct AircraftRecentEntry {
|
||||
|
||||
void inc_age(int delta) {
|
||||
age += delta;
|
||||
if (age < ADSB_CURRENT) {
|
||||
age_state = pos.valid ? 0 : 1;
|
||||
} else if (age < ADSB_RECENT) {
|
||||
age_state = 2;
|
||||
} else if (age < ADSB_REMOVE) {
|
||||
age_state = 3;
|
||||
} else {
|
||||
age_state = 4;
|
||||
}
|
||||
|
||||
if (age < ADSBAgeLimit::Current)
|
||||
state = pos.valid ? ADSBAgeState::Invalid
|
||||
: ADSBAgeState::Current;
|
||||
|
||||
else if (age < ADSBAgeLimit::Recent)
|
||||
state = ADSBAgeState::Recent;
|
||||
|
||||
else if (age < ADSBAgeLimit::Expired)
|
||||
state = ADSBAgeState::Old;
|
||||
|
||||
else
|
||||
state = ADSBAgeState::Expired;
|
||||
}
|
||||
};
|
||||
|
||||
// NB: uses std::list underneath so assuming refs are NOT invalidated.
|
||||
using AircraftRecentEntries = RecentEntries<AircraftRecentEntry>;
|
||||
|
||||
/* Holds data for logging. */
|
||||
struct ADSBLogEntry {
|
||||
std::string raw_data{};
|
||||
std::string icao{};
|
||||
std::string callsign{};
|
||||
adsb_pos pos{};
|
||||
adsb_vel vel{};
|
||||
uint8_t vel_type{};
|
||||
};
|
||||
|
||||
// TODO: Make logging optional.
|
||||
/* Logs entries to a log file. */
|
||||
class ADSBLogger {
|
||||
public:
|
||||
Optional<File::Error> append(const std::filesystem::path& filename) {
|
||||
return log_file.append(filename);
|
||||
}
|
||||
void log_str(std::string& logline);
|
||||
void log(const ADSBLogEntry& log_entry);
|
||||
|
||||
private:
|
||||
LogFile log_file{};
|
||||
};
|
||||
|
||||
/* Shows detailed information about an aircraft. */
|
||||
class ADSBRxAircraftDetailsView : public View {
|
||||
public:
|
||||
ADSBRxAircraftDetailsView(NavigationView&, const AircraftRecentEntry& entry, const std::function<void(void)> on_close);
|
||||
~ADSBRxAircraftDetailsView();
|
||||
|
||||
ADSBRxAircraftDetailsView(const ADSBRxAircraftDetailsView&) = delete;
|
||||
ADSBRxAircraftDetailsView(ADSBRxAircraftDetailsView&&) = delete;
|
||||
ADSBRxAircraftDetailsView& operator=(const ADSBRxAircraftDetailsView&) = delete;
|
||||
ADSBRxAircraftDetailsView& operator=(ADSBRxAircraftDetailsView&&) = delete;
|
||||
ADSBRxAircraftDetailsView(
|
||||
NavigationView&,
|
||||
const AircraftRecentEntry& entry);
|
||||
|
||||
void focus() override;
|
||||
|
||||
void update(const AircraftRecentEntry& entry);
|
||||
|
||||
std::string title() const override { return "AC Details"; };
|
||||
|
||||
AircraftRecentEntry get_current_entry() { return entry_copy; }
|
||||
|
||||
std::database::AircraftDBRecord aircraft_record = {};
|
||||
std::string title() const override { return "AC Details"; }
|
||||
|
||||
private:
|
||||
AircraftRecentEntry entry_copy{0};
|
||||
std::function<void(void)> on_close_{};
|
||||
bool send_updates{false};
|
||||
std::database db = {};
|
||||
std::string icao_code = "";
|
||||
int return_code = 0;
|
||||
|
||||
Labels labels{
|
||||
{{0 * 8, 1 * 16}, "ICAO:", Color::light_grey()},
|
||||
{{0 * 8, 2 * 16}, "Registration:", Color::light_grey()},
|
||||
@@ -237,36 +250,34 @@ class ADSBRxAircraftDetailsView : public View {
|
||||
"Back"};
|
||||
};
|
||||
|
||||
/* Shows detailed information about an aircraft's flight. */
|
||||
class ADSBRxDetailsView : public View {
|
||||
public:
|
||||
ADSBRxDetailsView(NavigationView&, const AircraftRecentEntry& entry, const std::function<void(void)> on_close);
|
||||
~ADSBRxDetailsView();
|
||||
ADSBRxDetailsView(NavigationView&, const AircraftRecentEntry& entry);
|
||||
|
||||
ADSBRxDetailsView(const ADSBRxDetailsView&) = delete;
|
||||
ADSBRxDetailsView(ADSBRxDetailsView&&) = delete;
|
||||
ADSBRxDetailsView& operator=(const ADSBRxDetailsView&) = delete;
|
||||
ADSBRxDetailsView& operator=(ADSBRxDetailsView&&) = delete;
|
||||
|
||||
void focus() override;
|
||||
|
||||
void update(const AircraftRecentEntry& entry);
|
||||
|
||||
std::string title() const override { return "Details"; };
|
||||
/* Calls forwarded to map view if shown. */
|
||||
bool map_active() const { return geomap_view_; }
|
||||
void clear_map_markers();
|
||||
/* Adds a marker for the entry to the map. Returns true on success. */
|
||||
bool add_map_marker(const AircraftRecentEntry& entry);
|
||||
|
||||
AircraftRecentEntry get_current_entry() { return entry_copy; }
|
||||
|
||||
std::database::AirlinesDBRecord airline_record = {};
|
||||
|
||||
GeoMapView* geomap_view{nullptr};
|
||||
std::string title() const override { return "Details"; }
|
||||
|
||||
private:
|
||||
AircraftRecentEntry entry_copy{0};
|
||||
std::function<void(void)> on_close_{};
|
||||
ADSBRxAircraftDetailsView* aircraft_details_view{nullptr};
|
||||
bool send_updates{false};
|
||||
std::database db = {};
|
||||
std::string airline_code = "";
|
||||
int return_code = 0;
|
||||
void refresh_ui();
|
||||
|
||||
GeoMapView* geomap_view_{nullptr};
|
||||
ADSBRxAircraftDetailsView* aircraft_details_view_{nullptr};
|
||||
|
||||
// NB: Keeping a copy so that it doesn't end up dangling
|
||||
// if removed from the recent entries list.
|
||||
AircraftRecentEntry entry_{AircraftRecentEntry::invalid_key};
|
||||
|
||||
Labels labels{
|
||||
{{0 * 8, 1 * 16}, "ICAO:", Color::light_grey()},
|
||||
@@ -321,6 +332,7 @@ class ADSBRxDetailsView : public View {
|
||||
"See on map"};
|
||||
};
|
||||
|
||||
/* Main ADSB application view and message dispatch. */
|
||||
class ADSBRxView : public View {
|
||||
public:
|
||||
ADSBRxView(NavigationView& nav);
|
||||
@@ -332,46 +344,52 @@ class ADSBRxView : public View {
|
||||
ADSBRxView& operator=(ADSBRxView&&) = delete;
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "ADS-B RX"; };
|
||||
|
||||
void replace_entry(AircraftRecentEntry& entry);
|
||||
void remove_old_entries();
|
||||
AircraftRecentEntry find_or_create_entry(uint32_t ICAO_address);
|
||||
void sort_entries_by_state();
|
||||
|
||||
private:
|
||||
RxRadioState radio_state_{
|
||||
1090000000 /* frequency */,
|
||||
2500000 /* bandwidth */,
|
||||
2000000 /* sampling rate */,
|
||||
1'090'000'000 /* frequency */,
|
||||
2'500'000 /* bandwidth */,
|
||||
2'000'000 /* sampling rate */,
|
||||
ReceiverModel::Mode::SpectrumAnalysis};
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_adsb", app_settings::Mode::RX};
|
||||
|
||||
std::unique_ptr<ADSBLogger> logger{};
|
||||
|
||||
/* Event Handlers */
|
||||
void on_frame(const ADSBFrameMessage* message);
|
||||
void on_tick_second();
|
||||
int updateState = {0};
|
||||
void updateRecentEntries();
|
||||
void updateDetailsAndMap(int ageStep);
|
||||
|
||||
#define MARKER_UPDATE_SECONDS (5)
|
||||
int ticksSinceMarkerRefresh{MARKER_UPDATE_SECONDS - 1};
|
||||
|
||||
const RecentEntriesColumns columns{{{"ICAO/Call", 9},
|
||||
{"Lvl", 3},
|
||||
{"Spd", 3},
|
||||
{"Amp", 3},
|
||||
{"Hit", 3},
|
||||
{"Age", 4}}};
|
||||
AircraftRecentEntries recent{};
|
||||
RecentEntriesView<RecentEntries<AircraftRecentEntry>> recent_entries_view{columns, recent};
|
||||
void refresh_ui();
|
||||
|
||||
SignalToken signal_token_tick_second{};
|
||||
uint8_t tick_count = 0;
|
||||
uint16_t ticks_since_marker_refresh{MARKER_UPDATE_SECONDS};
|
||||
|
||||
/* Max number of entries that can be updated in a single pass.
|
||||
* 16 is one screen of recent entries. */
|
||||
static constexpr uint8_t max_update_entries = 16;
|
||||
|
||||
/* Recent Entries */
|
||||
const RecentEntriesColumns columns{
|
||||
{{"ICAO/Call", 9},
|
||||
{"Lvl", 3},
|
||||
{"Spd", 3},
|
||||
{"Amp", 3},
|
||||
{"Hit", 3},
|
||||
{"Age", 4}}};
|
||||
AircraftRecentEntries recent{};
|
||||
RecentEntriesView<AircraftRecentEntries> recent_entries_view{columns, recent};
|
||||
|
||||
/* Entry Management */
|
||||
void update_recent_entries(int age_delta);
|
||||
AircraftRecentEntry& find_or_create_entry(uint32_t ICAO_address);
|
||||
void sort_entries_by_state();
|
||||
void remove_expired_entries();
|
||||
|
||||
/* The key of the entry in the details view if shown. */
|
||||
AircraftRecentEntry::Key detail_key{AircraftRecentEntry::invalid_key};
|
||||
ADSBRxDetailsView* details_view{nullptr};
|
||||
uint32_t detailed_entry_key{0};
|
||||
bool send_updates{false};
|
||||
|
||||
Labels labels{
|
||||
{{0 * 8, 0 * 8}, "LNA: VGA: AMP:", Color::light_grey()}};
|
||||
@@ -386,7 +404,17 @@ class ADSBRxView : public View {
|
||||
{18 * 8, 0 * 16}};
|
||||
|
||||
RSSI rssi{
|
||||
{20 * 8, 4, 10 * 8, 8},
|
||||
{20 * 8, 4, 10 * 7, 8},
|
||||
};
|
||||
|
||||
ActivityDot status_frame{
|
||||
{screen_width - 3, 5, 2, 2},
|
||||
Color::white(),
|
||||
};
|
||||
|
||||
ActivityDot status_good_frame{
|
||||
{screen_width - 3, 9, 2, 2},
|
||||
Color::green(),
|
||||
};
|
||||
|
||||
MessageHandlerRegistration message_handler_frame{
|
||||
|
||||
@@ -70,7 +70,7 @@ BTLERxView::BTLERxView(NavigationView& nav)
|
||||
};
|
||||
|
||||
// Auto-configure modem for LCR RX (will be removed later)
|
||||
baseband::set_btle(persistent_memory::modem_baudrate(), 8, 0, false);
|
||||
baseband::set_btlerx(channel_number);
|
||||
|
||||
audio::set_rate(audio::Rate::Hz_24000);
|
||||
audio::output::start();
|
||||
|
||||
@@ -60,6 +60,8 @@ class BTLERxView : public View {
|
||||
uint8_t console_color{0};
|
||||
uint32_t prev_value{0};
|
||||
|
||||
static constexpr uint8_t channel_number = 37;
|
||||
|
||||
RFAmpField field_rf_amp{
|
||||
{13 * 8, 0 * 16}};
|
||||
LNAGainField field_lna{
|
||||
|
||||
@@ -124,8 +124,8 @@ void TemperatureWidget::paint(Painter& painter) {
|
||||
}
|
||||
|
||||
TemperatureWidget::temperature_t TemperatureWidget::temperature(const sample_t sensor_value) const {
|
||||
/*It seems to be a temperature difference of 25C*/
|
||||
return -40 + (sensor_value * 4.31) + 25; // max2837 datasheet temp 25ºC has sensor value: 15
|
||||
// Scaling is different for MAX2837 vs MAX2839 so it's now done in the respective chip-specific module
|
||||
return sensor_value;
|
||||
}
|
||||
|
||||
std::string TemperatureWidget::temperature_str(const temperature_t temperature) const {
|
||||
@@ -159,11 +159,8 @@ void TemperatureView::focus() {
|
||||
/* RegistersWidget *******************************************************/
|
||||
|
||||
RegistersWidget::RegistersWidget(
|
||||
RegistersWidgetConfig&& config,
|
||||
std::function<uint32_t(const size_t register_number)>&& reader)
|
||||
: Widget{},
|
||||
config(std::move(config)),
|
||||
reader(std::move(reader)) {
|
||||
RegistersWidgetConfig&& config)
|
||||
: Widget{}, config(std::move(config)), page_number(0) {
|
||||
}
|
||||
|
||||
void RegistersWidget::update() {
|
||||
@@ -180,7 +177,7 @@ void RegistersWidget::paint(Painter& painter) {
|
||||
void RegistersWidget::draw_legend(const Coord left, Painter& painter) {
|
||||
const auto pos = screen_pos();
|
||||
|
||||
for (size_t i = 0; i < config.registers_count; i += config.registers_per_row()) {
|
||||
for (uint32_t i = 0; i < config.registers_count; i += config.registers_per_row()) {
|
||||
const Point offset{
|
||||
left, static_cast<int>((i / config.registers_per_row()) * row_height)};
|
||||
|
||||
@@ -197,12 +194,12 @@ void RegistersWidget::draw_values(
|
||||
Painter& painter) {
|
||||
const auto pos = screen_pos();
|
||||
|
||||
for (size_t i = 0; i < config.registers_count; i++) {
|
||||
for (uint32_t i = 0; i < config.registers_count; i++) {
|
||||
const Point offset = {
|
||||
static_cast<int>(left + config.legend_width() + 8 + (i % config.registers_per_row()) * (config.value_width() + 8)),
|
||||
static_cast<int>((i / config.registers_per_row()) * row_height)};
|
||||
|
||||
const auto value = reader(i);
|
||||
const auto value = reg_read(i);
|
||||
|
||||
const auto text = to_string_hex(value, config.value_length());
|
||||
painter.draw_string(
|
||||
@@ -212,19 +209,61 @@ void RegistersWidget::draw_values(
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t RegistersWidget::reg_read(const uint32_t register_number) {
|
||||
if (register_number < config.registers_count) {
|
||||
switch (config.chip_type) {
|
||||
case CT_PMEM:
|
||||
return portapack::persistent_memory::pmem_data_word((page_number * config.registers_count + register_number) / 4) >> (register_number % 4 * 8);
|
||||
case CT_RFFC5072:
|
||||
return radio::debug::first_if::register_read(register_number);
|
||||
case CT_MAX283X:
|
||||
return radio::debug::second_if::register_read(register_number);
|
||||
case CT_SI5351:
|
||||
return portapack::clock_generator.read_register(register_number);
|
||||
case CT_AUDIO:
|
||||
return audio::debug::reg_read(register_number);
|
||||
}
|
||||
}
|
||||
return 0xFFFF;
|
||||
}
|
||||
|
||||
void RegistersWidget::reg_write(const uint32_t register_number, const uint32_t value) {
|
||||
if (register_number < config.registers_count) {
|
||||
switch (config.chip_type) {
|
||||
case CT_PMEM:
|
||||
break;
|
||||
case CT_RFFC5072:
|
||||
radio::debug::first_if::register_write(register_number, value);
|
||||
break;
|
||||
case CT_MAX283X:
|
||||
radio::debug::second_if::register_write(register_number, value);
|
||||
break;
|
||||
case CT_SI5351:
|
||||
portapack::clock_generator.write_register(register_number, value);
|
||||
break;
|
||||
case CT_AUDIO:
|
||||
audio::debug::reg_write(register_number, value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* RegistersView *********************************************************/
|
||||
|
||||
RegistersView::RegistersView(
|
||||
NavigationView& nav,
|
||||
const std::string& title,
|
||||
RegistersWidgetConfig&& config,
|
||||
std::function<uint32_t(const size_t register_number)>&& reader)
|
||||
: registers_widget{std::move(config), std::move(reader)} {
|
||||
RegistersWidgetConfig&& config)
|
||||
: registers_widget{std::move(config)} {
|
||||
add_children({
|
||||
&text_title,
|
||||
®isters_widget,
|
||||
&button_update,
|
||||
&button_done,
|
||||
&labels,
|
||||
&field_write_reg_num,
|
||||
&field_write_data_val,
|
||||
&button_write,
|
||||
});
|
||||
|
||||
button_update.on_select = [this](Button&) {
|
||||
@@ -237,6 +276,20 @@ RegistersView::RegistersView(
|
||||
text_title.set_parent_rect({(240 - static_cast<int>(title.size()) * 8) / 2, 16,
|
||||
static_cast<int>(title.size()) * 8, 16});
|
||||
text_title.set(title);
|
||||
|
||||
field_write_reg_num.on_change = [this](SymField&) {
|
||||
field_write_data_val.set_value(this->registers_widget.reg_read(field_write_reg_num.to_integer()));
|
||||
field_write_data_val.set_dirty();
|
||||
};
|
||||
|
||||
const auto value = registers_widget.reg_read(0);
|
||||
field_write_data_val.set_value(value);
|
||||
|
||||
button_write.set_style(&Styles::red);
|
||||
button_write.on_select = [this](Button&) {
|
||||
this->registers_widget.reg_write(field_write_reg_num.to_integer(), field_write_data_val.to_integer());
|
||||
this->registers_widget.update();
|
||||
};
|
||||
}
|
||||
|
||||
void RegistersView::focus() {
|
||||
@@ -376,18 +429,10 @@ DebugPeripheralsMenuView::DebugPeripheralsMenuView(NavigationView& nav) {
|
||||
const char* max283x = hackrf_r9 ? "MAX2839" : "MAX2837";
|
||||
const char* si5351x = hackrf_r9 ? "Si5351A" : "Si5351C";
|
||||
add_items({
|
||||
{"RFFC5072", ui::Color::dark_cyan(), &bitmap_icon_peripherals_details, [&nav]() { nav.push<RegistersView>(
|
||||
"RFFC5072", RegistersWidgetConfig{31, 16},
|
||||
[](const size_t register_number) { return radio::debug::first_if::register_read(register_number); }); }},
|
||||
{max283x, ui::Color::dark_cyan(), &bitmap_icon_peripherals_details, [&nav, max283x]() { nav.push<RegistersView>(
|
||||
max283x, RegistersWidgetConfig{32, 10},
|
||||
[](const size_t register_number) { return radio::debug::second_if::register_read(register_number); }); }},
|
||||
{si5351x, ui::Color::dark_cyan(), &bitmap_icon_peripherals_details, [&nav, si5351x]() { nav.push<RegistersView>(
|
||||
si5351x, RegistersWidgetConfig{96, 8},
|
||||
[](const size_t register_number) { return portapack::clock_generator.read_register(register_number); }); }},
|
||||
{audio::debug::codec_name(), ui::Color::dark_cyan(), &bitmap_icon_peripherals_details, [&nav]() { nav.push<RegistersView>(
|
||||
audio::debug::codec_name(), RegistersWidgetConfig{audio::debug::reg_count(), audio::debug::reg_bits()},
|
||||
[](const size_t register_number) { return audio::debug::reg_read(register_number); }); }},
|
||||
{"RFFC5072", ui::Color::dark_cyan(), &bitmap_icon_peripherals_details, [&nav]() { nav.push<RegistersView>("RFFC5072", RegistersWidgetConfig{CT_RFFC5072, 31, 16}); }},
|
||||
{max283x, ui::Color::dark_cyan(), &bitmap_icon_peripherals_details, [&nav, max283x]() { nav.push<RegistersView>(max283x, RegistersWidgetConfig{CT_MAX283X, 32, 10}); }},
|
||||
{si5351x, ui::Color::dark_cyan(), &bitmap_icon_peripherals_details, [&nav, si5351x]() { nav.push<RegistersView>(si5351x, RegistersWidgetConfig{CT_SI5351, 96, 8}); }},
|
||||
{audio::debug::codec_name(), ui::Color::dark_cyan(), &bitmap_icon_peripherals_details, [&nav]() { nav.push<RegistersView>(audio::debug::codec_name(), RegistersWidgetConfig{CT_AUDIO, audio::debug::reg_count(), audio::debug::reg_bits()}); }},
|
||||
});
|
||||
set_max_rows(2); // allow wider buttons
|
||||
}
|
||||
@@ -402,9 +447,10 @@ DebugMenuView::DebugMenuView(NavigationView& nav) {
|
||||
{"Buttons Test", ui::Color::dark_cyan(), &bitmap_icon_controls, [&nav]() { nav.push<DebugControlsView>(); }},
|
||||
{"Debug Dump", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { portapack::persistent_memory::debug_dump(); }},
|
||||
{"M0 Stack Dump", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { stack_dump(); }},
|
||||
{"Memory", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { nav.push<DebugMemoryView>(); }},
|
||||
{"P.Memory", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { nav.push<DebugPmemView>(); }},
|
||||
{"Memory Dump", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { nav.push<DebugMemoryDumpView>(); }},
|
||||
{"Memory Usage", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { nav.push<DebugMemoryView>(); }},
|
||||
{"Peripherals", ui::Color::dark_cyan(), &bitmap_icon_peripherals, [&nav]() { nav.push<DebugPeripheralsMenuView>(); }},
|
||||
{"Pers. Memory", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { nav.push<DebugPmemView>(); }},
|
||||
//{ "Radio State", ui::Color::white(), nullptr, [&nav](){ nav.push<NotImplementedView>(); } },
|
||||
{"SD Card", ui::Color::dark_cyan(), &bitmap_icon_sdcard, [&nav]() { nav.push<SDCardDebugView>(); }},
|
||||
{"Temperature", ui::Color::dark_cyan(), &bitmap_icon_temperature, [&nav]() { nav.push<TemperatureView>(); }},
|
||||
@@ -418,30 +464,53 @@ DebugMenuView::DebugMenuView(NavigationView& nav) {
|
||||
set_max_rows(2); // allow wider buttons
|
||||
}
|
||||
|
||||
/* DebugPmemView *********************************************************/
|
||||
/* DebugMemoryDumpView *********************************************************/
|
||||
|
||||
uint32_t pmem_checksum(volatile const uint32_t data[63]) {
|
||||
CRC<32> crc{0x04c11db7, 0xffffffff, 0xffffffff};
|
||||
for (size_t i = 0; i < 63; i++) {
|
||||
const auto word = data[i];
|
||||
crc.process_byte((word >> 0) & 0xff);
|
||||
crc.process_byte((word >> 8) & 0xff);
|
||||
crc.process_byte((word >> 16) & 0xff);
|
||||
crc.process_byte((word >> 24) & 0xff);
|
||||
}
|
||||
return crc.checksum();
|
||||
DebugMemoryDumpView::DebugMemoryDumpView(NavigationView& nav) {
|
||||
add_children({
|
||||
&button_dump,
|
||||
&button_read,
|
||||
&button_write,
|
||||
&button_done,
|
||||
&labels,
|
||||
&field_starting_address,
|
||||
&field_byte_count,
|
||||
&field_rw_address,
|
||||
&field_data_value,
|
||||
});
|
||||
|
||||
button_done.on_select = [&nav](Button&) { nav.pop(); };
|
||||
|
||||
button_dump.on_select = [this](Button&) {
|
||||
if (field_byte_count.to_integer() != 0)
|
||||
memory_dump((uint32_t*)field_starting_address.to_integer(), ((uint32_t)field_byte_count.to_integer() + 3) / 4, false);
|
||||
};
|
||||
|
||||
button_read.on_select = [this](Button&) {
|
||||
field_data_value.set_value(*(uint32_t*)field_rw_address.to_integer());
|
||||
field_data_value.set_dirty();
|
||||
};
|
||||
|
||||
button_write.set_style(&Styles::red);
|
||||
button_write.on_select = [this](Button&) {
|
||||
*(uint32_t*)field_rw_address.to_integer() = (uint32_t)field_data_value.to_integer();
|
||||
};
|
||||
}
|
||||
|
||||
DebugPmemView::DebugPmemView(NavigationView& nav)
|
||||
: data{*reinterpret_cast<pmem_data*>(memory::map::backup_ram.base())}, registers_widget(RegistersWidgetConfig{page_size, 8}, std::bind(&DebugPmemView::registers_widget_feed, this, std::placeholders::_1)) {
|
||||
static_assert(sizeof(pmem_data) == memory::map::backup_ram.size());
|
||||
void DebugMemoryDumpView::focus() {
|
||||
button_done.focus();
|
||||
}
|
||||
|
||||
/* DebugPmemView *********************************************************/
|
||||
|
||||
DebugPmemView::DebugPmemView(NavigationView& nav)
|
||||
: registers_widget(RegistersWidgetConfig{CT_PMEM, page_size, 8}) {
|
||||
add_children({&text_page, ®isters_widget, &text_checksum, &text_checksum2, &button_ok});
|
||||
|
||||
registers_widget.set_parent_rect({0, 32, 240, 192});
|
||||
|
||||
text_checksum.set("Size: " + to_string_dec_uint(portapack::persistent_memory::data_size(), 3) + " CRC: " + to_string_hex(data.check_value, 8));
|
||||
text_checksum2.set("Calculated CRC: " + to_string_hex(pmem_checksum(data.regfile), 8));
|
||||
text_checksum.set("Size: " + to_string_dec_uint(portapack::persistent_memory::data_size(), 3) + " CRC: " + to_string_hex(portapack::persistent_memory::pmem_stored_checksum(), 8));
|
||||
text_checksum2.set("Calculated CRC: " + to_string_hex(portapack::persistent_memory::pmem_calculated_checksum(), 8));
|
||||
|
||||
button_ok.on_select = [&nav](Button&) {
|
||||
nav.pop();
|
||||
@@ -451,7 +520,7 @@ DebugPmemView::DebugPmemView(NavigationView& nav)
|
||||
}
|
||||
|
||||
bool DebugPmemView::on_encoder(const EncoderEvent delta) {
|
||||
page = std::max(0l, std::min((int32_t)page_max, page + delta));
|
||||
registers_widget.set_page(std::max(0ul, std::min((uint32_t)page_count - 1, registers_widget.page() + delta)));
|
||||
|
||||
update();
|
||||
|
||||
@@ -463,17 +532,10 @@ void DebugPmemView::focus() {
|
||||
}
|
||||
|
||||
void DebugPmemView::update() {
|
||||
text_page.set(to_string_hex(page_size * page, 2) + "+");
|
||||
text_page.set(to_string_hex(registers_widget.page() * page_size, 2) + "+");
|
||||
registers_widget.update();
|
||||
}
|
||||
|
||||
uint32_t DebugPmemView::registers_widget_feed(const size_t register_number) {
|
||||
if (page_size * page + register_number >= memory::map::backup_ram.size()) {
|
||||
return 0xff;
|
||||
}
|
||||
return data.regfile[(page_size * page + register_number) / 4] >> (register_number % 4 * 8);
|
||||
}
|
||||
|
||||
/* DebugScreenTest ****************************************************/
|
||||
|
||||
DebugScreenTest::DebugScreenTest(NavigationView& nav)
|
||||
|
||||
@@ -48,8 +48,8 @@ class DebugMemoryView : public View {
|
||||
|
||||
private:
|
||||
Text text_title{
|
||||
{96, 96, 48, 16},
|
||||
"Memory",
|
||||
{72, 96, 96, 16},
|
||||
"Memory Usage",
|
||||
};
|
||||
|
||||
Text text_label_m0_core_free{
|
||||
@@ -131,9 +131,18 @@ class TemperatureView : public View {
|
||||
"Done"};
|
||||
};
|
||||
|
||||
typedef enum {
|
||||
CT_PMEM,
|
||||
CT_RFFC5072,
|
||||
CT_MAX283X,
|
||||
CT_SI5351,
|
||||
CT_AUDIO,
|
||||
} chip_type_t;
|
||||
|
||||
struct RegistersWidgetConfig {
|
||||
size_t registers_count;
|
||||
size_t register_bits;
|
||||
chip_type_t chip_type;
|
||||
uint32_t registers_count;
|
||||
uint32_t register_bits;
|
||||
|
||||
constexpr size_t legend_length() const {
|
||||
return (registers_count >= 0x10) ? 2 : 1;
|
||||
@@ -174,17 +183,21 @@ struct RegistersWidgetConfig {
|
||||
|
||||
class RegistersWidget : public Widget {
|
||||
public:
|
||||
RegistersWidget(
|
||||
RegistersWidgetConfig&& config,
|
||||
std::function<uint32_t(const size_t register_number)>&& reader);
|
||||
RegistersWidget(RegistersWidgetConfig&& config);
|
||||
|
||||
void update();
|
||||
|
||||
void paint(Painter& painter) override;
|
||||
|
||||
uint32_t reg_read(const uint32_t register_number);
|
||||
void reg_write(const uint32_t register_number, const uint32_t value);
|
||||
|
||||
void set_page(int32_t value) { page_number = value; }
|
||||
uint32_t page(void) { return page_number; }
|
||||
|
||||
private:
|
||||
const RegistersWidgetConfig config;
|
||||
const std::function<uint32_t(const size_t register_number)> reader;
|
||||
uint32_t page_number;
|
||||
|
||||
static constexpr size_t row_height = 16;
|
||||
|
||||
@@ -194,11 +207,7 @@ class RegistersWidget : public Widget {
|
||||
|
||||
class RegistersView : public View {
|
||||
public:
|
||||
RegistersView(
|
||||
NavigationView& nav,
|
||||
const std::string& title,
|
||||
RegistersWidgetConfig&& config,
|
||||
std::function<uint32_t(const size_t register_number)>&& reader);
|
||||
RegistersView(NavigationView& nav, const std::string& title, RegistersWidgetConfig&& config);
|
||||
|
||||
void focus();
|
||||
|
||||
@@ -208,12 +217,30 @@ class RegistersView : public View {
|
||||
RegistersWidget registers_widget;
|
||||
|
||||
Button button_update{
|
||||
{16, 256, 96, 24},
|
||||
{16, 280, 96, 24},
|
||||
"Update"};
|
||||
|
||||
Button button_done{
|
||||
{128, 256, 96, 24},
|
||||
{128, 280, 96, 24},
|
||||
"Done"};
|
||||
|
||||
Button button_write{
|
||||
{144, 248, 80, 20},
|
||||
"Write"};
|
||||
|
||||
Labels labels{
|
||||
{{1 * 8, 248}, "Reg:", Color::light_grey()},
|
||||
{{8 * 8, 248}, "Data:", Color::light_grey()}};
|
||||
|
||||
SymField field_write_reg_num{
|
||||
{5 * 8, 248},
|
||||
2,
|
||||
SymField::Type::Hex};
|
||||
|
||||
SymField field_write_data_val{
|
||||
{13 * 8, 248},
|
||||
4,
|
||||
SymField::Type::Hex};
|
||||
};
|
||||
|
||||
class ControlsSwitchesWidget : public Widget {
|
||||
@@ -274,6 +301,58 @@ class DebugControlsView : public View {
|
||||
"Done"};
|
||||
};
|
||||
|
||||
class DebugMemoryDumpView : public View {
|
||||
public:
|
||||
DebugMemoryDumpView(NavigationView& nav);
|
||||
void focus() override;
|
||||
std::string title() const override { return "Memory Dump"; };
|
||||
|
||||
private:
|
||||
Button button_dump{
|
||||
{72, 4 * 16, 96, 24},
|
||||
"Dump"};
|
||||
|
||||
Button button_read{
|
||||
{16, 11 * 16, 96, 24},
|
||||
"Read"};
|
||||
|
||||
Button button_write{
|
||||
{128, 11 * 16, 96, 24},
|
||||
"Write"};
|
||||
|
||||
Button button_done{
|
||||
{128, 240, 96, 24},
|
||||
"Done"};
|
||||
|
||||
Labels labels{
|
||||
{{5 * 8, 1 * 16}, "Dump Range to File", Color::yellow()},
|
||||
{{0 * 8, 2 * 16}, "Starting Address: 0x", Color::light_grey()},
|
||||
{{0 * 8, 3 * 16}, "Byte Count: 0x", Color::light_grey()},
|
||||
{{3 * 8, 8 * 16}, "Read/Write Single Word", Color::yellow()},
|
||||
{{0 * 8, 9 * 16}, "Memory Address: 0x", Color::light_grey()},
|
||||
{{0 * 8, 10 * 16}, "Data Value: 0x", Color::light_grey()}};
|
||||
|
||||
SymField field_starting_address{
|
||||
{20 * 8, 2 * 16},
|
||||
8,
|
||||
SymField::Type::Hex};
|
||||
|
||||
SymField field_byte_count{
|
||||
{20 * 8, 3 * 16},
|
||||
8,
|
||||
SymField::Type::Hex};
|
||||
|
||||
SymField field_rw_address{
|
||||
{20 * 8, 9 * 16},
|
||||
8,
|
||||
SymField::Type::Hex};
|
||||
|
||||
SymField field_data_value{
|
||||
{20 * 8, 10 * 16},
|
||||
8,
|
||||
SymField::Type::Hex};
|
||||
};
|
||||
|
||||
class DebugPmemView : public View {
|
||||
public:
|
||||
DebugPmemView(NavigationView& nav);
|
||||
@@ -282,17 +361,8 @@ class DebugPmemView : public View {
|
||||
std::string title() const override { return "P.Mem Debug"; }
|
||||
|
||||
private:
|
||||
struct pmem_data {
|
||||
uint32_t regfile[63];
|
||||
uint32_t check_value;
|
||||
};
|
||||
|
||||
static constexpr uint8_t page_size{96}; // Must be multiply of 4 otherwise bit shifting for register view wont work properly
|
||||
static constexpr uint8_t page_max{(portapack::memory::map::backup_ram.size() + page_size - 1) / page_size - 1};
|
||||
|
||||
int32_t page{0};
|
||||
|
||||
volatile const pmem_data& data;
|
||||
static constexpr uint8_t page_count{(portapack::memory::map::backup_ram.size() + page_size - 1) / page_size};
|
||||
|
||||
Text text_page{{16, 16, 208, 16}};
|
||||
|
||||
@@ -307,7 +377,6 @@ class DebugPmemView : public View {
|
||||
};
|
||||
|
||||
void update();
|
||||
uint32_t registers_widget_feed(const size_t register_number);
|
||||
};
|
||||
|
||||
class DebugScreenTest : public View {
|
||||
|
||||
@@ -156,7 +156,7 @@ void FileManBaseView::load_directory_contents(const fs::path& dir_path) {
|
||||
|
||||
for (const auto& entry : fs::directory_iterator(dir_path, u"*")) {
|
||||
// Hide files starting with '.' (hidden / tmp).
|
||||
if (is_hidden_file(entry.path()))
|
||||
if (!show_hidden_files && is_hidden_file(entry.path()))
|
||||
continue;
|
||||
|
||||
if (fs::is_regular_file(entry.status())) {
|
||||
@@ -568,6 +568,7 @@ FileManagerView::FileManagerView(
|
||||
&button_open_notepad,
|
||||
&button_rename_timestamp,
|
||||
&button_open_iq_trim,
|
||||
&button_show_hidden_files,
|
||||
});
|
||||
|
||||
menu_view.on_highlight = [this]() {
|
||||
@@ -658,6 +659,12 @@ FileManagerView::FileManagerView(
|
||||
} else
|
||||
nav_.display_modal("IQ Trim", "Not a capture file.");
|
||||
};
|
||||
|
||||
button_show_hidden_files.on_select = [this]() {
|
||||
show_hidden_files = !show_hidden_files;
|
||||
button_show_hidden_files.set_color(show_hidden_files ? Color::green() : Color::dark_grey());
|
||||
reload_current();
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
|
||||
@@ -104,6 +104,8 @@ class FileManBaseView : public View {
|
||||
std::vector<fileman_entry> entry_list{};
|
||||
std::vector<uint32_t> saved_index_stack{};
|
||||
|
||||
bool show_hidden_files{false};
|
||||
|
||||
Labels labels{
|
||||
{{0, 0}, "Path:", Color::light_grey()}};
|
||||
|
||||
@@ -278,6 +280,12 @@ class FileManagerView : public FileManBaseView {
|
||||
{},
|
||||
&bitmap_icon_trim,
|
||||
Color::orange()};
|
||||
|
||||
NewButton button_show_hidden_files{
|
||||
{13 * 8, 34 * 8, 4 * 8, 32},
|
||||
{},
|
||||
&bitmap_icon_hide,
|
||||
Color::dark_grey()};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2016 Furrtek
|
||||
*
|
||||
* 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_fsk_rx.hpp"
|
||||
|
||||
#include "audio.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "utility.hpp"
|
||||
|
||||
#include "ui_freqman.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
namespace pmem = portapack::persistent_memory;
|
||||
|
||||
void FskRxLogger::log_raw_data(const std::string& data, const uint32_t frequency) {
|
||||
std::string entry = "Raw: F:" + to_string_dec_uint(frequency) + "Hz";
|
||||
|
||||
// // Raw hex dump of all the codewords
|
||||
// for (size_t c = 0; c < 16; c++)
|
||||
// entry += to_string_hex(packet[c], 8) + " ";
|
||||
|
||||
log_file.write_entry(data + entry);
|
||||
}
|
||||
|
||||
void FskRxLogger::log_decoded(Timestamp timestamp, const std::string& text) {
|
||||
log_file.write_entry(timestamp, text);
|
||||
}
|
||||
|
||||
namespace ui {
|
||||
//---------------------------------------------------------------------------------------------------------------
|
||||
// Console View
|
||||
//---------------------------------------------------------------------------------------------------------------
|
||||
FskRxAppConsoleView::FskRxAppConsoleView(NavigationView& nav, Rect parent_rect)
|
||||
: View(parent_rect), nav_{nav} {
|
||||
add_child(&console);
|
||||
};
|
||||
|
||||
void FskRxAppConsoleView::on_packet(uint32_t value, bool is_data) {
|
||||
if (is_data) {
|
||||
console.write(to_string_dec_uint(value) + " ");
|
||||
}
|
||||
}
|
||||
|
||||
void FskRxAppConsoleView::on_show() {
|
||||
hidden(false);
|
||||
}
|
||||
|
||||
void FskRxAppConsoleView::on_hide() {
|
||||
hidden(true);
|
||||
}
|
||||
|
||||
FskRxAppConsoleView::~FskRxAppConsoleView() {
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------------------------------------------------
|
||||
// Spectrum View
|
||||
//---------------------------------------------------------------------------------------------------------------
|
||||
FskRxAppView::FskRxAppView(NavigationView& nav, Rect parent_rect)
|
||||
: View(parent_rect), nav_{nav} {
|
||||
add_child(&waterfall);
|
||||
hidden(true);
|
||||
}
|
||||
|
||||
FskRxAppView::~FskRxAppView() {
|
||||
}
|
||||
|
||||
void FskRxAppView::focus() {
|
||||
}
|
||||
|
||||
void FskRxAppView::on_show() {
|
||||
hidden(false);
|
||||
waterfall.start();
|
||||
}
|
||||
|
||||
void FskRxAppView::on_hide() {
|
||||
hidden(true);
|
||||
waterfall.stop();
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------------------------------------------------
|
||||
// Base View
|
||||
//---------------------------------------------------------------------------------------------------------------
|
||||
FskxRxMainView::FskxRxMainView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
add_children({&tab_view,
|
||||
&view_data,
|
||||
&view_stream,
|
||||
&labels,
|
||||
&rssi,
|
||||
&channel,
|
||||
&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&field_frequency,
|
||||
&deviation_frequency,
|
||||
&record_view});
|
||||
|
||||
baseband::run_image(portapack::spi_flash::image_tag_fskrx);
|
||||
|
||||
// DEBUG
|
||||
record_view.on_error = [&nav](std::string message) {
|
||||
nav.display_modal("Error", message);
|
||||
};
|
||||
|
||||
deviation_frequency.on_change = [this](rf::Frequency f) {
|
||||
refresh_ui(f);
|
||||
};
|
||||
|
||||
// Set initial sampling rate
|
||||
/* Bandwidth of 2FSK is 2 * Deviation */
|
||||
record_view.set_sampling_rate(initial_deviation * 2);
|
||||
|
||||
field_frequency.set_value(initial_target_frequency);
|
||||
deviation_frequency.set_value(initial_deviation);
|
||||
|
||||
logger.append(LOG_ROOT_DIR "/FSKRX.TXT");
|
||||
|
||||
baseband::set_fsk(initial_deviation);
|
||||
|
||||
audio::output::start();
|
||||
receiver_model.enable();
|
||||
}
|
||||
|
||||
void FskxRxMainView::handle_decoded(Timestamp timestamp, const std::string& prefix) {
|
||||
if (logging()) {
|
||||
logger.log_decoded(timestamp, prefix);
|
||||
}
|
||||
}
|
||||
|
||||
void FskxRxMainView::refresh_ui(rf::Frequency deviationHz) {
|
||||
/* Nyquist would imply a sample rate of 2x bandwidth, but because the ADC
|
||||
* provides 2 values (I,Q), the sample_rate is equal to bandwidth here. */
|
||||
/* Bandwidth of 2FSK is 2 * Deviation */
|
||||
auto sample_rate = deviationHz * 2;
|
||||
|
||||
/* base_rate (bandwidth) is used for FFT calculation and display LCD, and also in recording writing SD Card rate. */
|
||||
/* ex. sampling_rate values, 4Mhz, when recording 500 kHz (BW) and fs 8 Mhz, when selected 1 Mhz BW ... */
|
||||
/* ex. recording 500kHz BW to .C16 file, base_rate clock 500kHz x2(I,Q) x 2 bytes (int signed) =2MB/sec rate SD Card. */
|
||||
|
||||
if (!view_stream.hidden()) {
|
||||
view_stream.waterfall.stop();
|
||||
}
|
||||
|
||||
// record_view determines the correct oversampling to apply and returns the actual sample rate.
|
||||
// NB: record_view is what actually updates proc_capture baseband settings.
|
||||
auto actual_sample_rate = record_view.set_sampling_rate(sample_rate);
|
||||
|
||||
// Update the radio model with the actual sampling rate.
|
||||
receiver_model.set_sampling_rate(actual_sample_rate);
|
||||
|
||||
// Get suitable anti-aliasing BPF bandwidth for MAX2837 given the actual sample rate.
|
||||
auto anti_alias_filter_bandwidth = filter_bandwidth_for_sampling_rate(actual_sample_rate);
|
||||
receiver_model.set_baseband_bandwidth(anti_alias_filter_bandwidth);
|
||||
|
||||
if (!view_stream.hidden()) {
|
||||
view_stream.waterfall.start();
|
||||
}
|
||||
}
|
||||
|
||||
void FskxRxMainView::focus() {
|
||||
field_frequency.focus();
|
||||
}
|
||||
|
||||
void FskxRxMainView::set_parent_rect(const Rect new_parent_rect) {
|
||||
View::set_parent_rect(new_parent_rect);
|
||||
|
||||
ui::Rect waterfall_rect{0, 0, new_parent_rect.width(), new_parent_rect.height() - header_height};
|
||||
view_stream.waterfall.set_parent_rect(waterfall_rect);
|
||||
}
|
||||
|
||||
FskxRxMainView::~FskxRxMainView() {
|
||||
audio::output::stop();
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
} /* namespace ui */
|
||||
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2018 Furrtek
|
||||
* Copyright (C) 2023 gullradriel, Nilorea Studio Inc.
|
||||
*
|
||||
* 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_FSK_RX_H__
|
||||
#define __UI_FSK_RX_H__
|
||||
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_record_view.hpp"
|
||||
#include "ui_rssi.hpp"
|
||||
#include "ui_spectrum.hpp"
|
||||
#include "ui_styles.hpp"
|
||||
#include "ui_tabview.hpp"
|
||||
|
||||
#include "app_settings.hpp"
|
||||
#include "log_file.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "pocsag_app.hpp"
|
||||
|
||||
#include <functional>
|
||||
|
||||
class FskRxLogger {
|
||||
public:
|
||||
Optional<File::Error> append(const std::string& filename) {
|
||||
return log_file.append(filename);
|
||||
}
|
||||
|
||||
void log_raw_data(const std::string& data, const uint32_t frequency);
|
||||
void log_decoded(Timestamp timestamp, const std::string& text);
|
||||
|
||||
private:
|
||||
LogFile log_file{};
|
||||
};
|
||||
|
||||
namespace ui {
|
||||
class FskRxAppConsoleView : public View {
|
||||
public:
|
||||
FskRxAppConsoleView(NavigationView& nav, Rect parent_rec);
|
||||
~FskRxAppConsoleView();
|
||||
|
||||
std::string title() const override { return "FSK RX Data"; };
|
||||
|
||||
void on_packet(uint32_t value, bool is_data);
|
||||
|
||||
void on_show() override;
|
||||
void on_hide() override;
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
|
||||
Console console{
|
||||
{0, 0, 240, 224}};
|
||||
};
|
||||
|
||||
class FskRxAppView : public View {
|
||||
public:
|
||||
FskRxAppView(NavigationView& nav, Rect parent_rect);
|
||||
~FskRxAppView();
|
||||
|
||||
void focus() override;
|
||||
void on_show() override;
|
||||
void on_hide() override;
|
||||
|
||||
spectrum::WaterfallView waterfall{};
|
||||
|
||||
std::string title() const override { return "FSK RX Stream"; };
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
RxRadioState radio_state_{};
|
||||
};
|
||||
|
||||
class FskxRxMainView : public View {
|
||||
public:
|
||||
FskxRxMainView(NavigationView& nav);
|
||||
~FskxRxMainView();
|
||||
|
||||
void focus() override;
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
|
||||
std::string title() const override { return "FSK RX"; };
|
||||
|
||||
private:
|
||||
static constexpr uint32_t initial_target_frequency = 902'075'000;
|
||||
static constexpr ui::Dim header_height = (5 * 16);
|
||||
|
||||
uint32_t initial_deviation{3750};
|
||||
|
||||
bool logging() const { return false; };
|
||||
bool logging_raw() const { return false; };
|
||||
|
||||
NavigationView& nav_;
|
||||
Rect view_rect = {0, header_height, 240, 224};
|
||||
|
||||
FskRxAppView view_stream{nav_, view_rect};
|
||||
FskRxAppConsoleView view_data{nav_, view_rect};
|
||||
|
||||
TabView tab_view{
|
||||
{"Data", Color::yellow(), &view_data},
|
||||
{"Stream", Color::cyan(), &view_stream}};
|
||||
|
||||
void refresh_ui(rf::Frequency f);
|
||||
void on_packet(uint32_t value, bool is_data);
|
||||
void handle_decoded(Timestamp timestamp, const std::string& prefix);
|
||||
|
||||
uint32_t last_address = 0;
|
||||
FskRxLogger logger{};
|
||||
uint16_t packet_count = 0;
|
||||
|
||||
RxFrequencyField field_frequency{
|
||||
{0 * 8, 4 * 8},
|
||||
nav_};
|
||||
|
||||
RFAmpField field_rf_amp{
|
||||
{11 * 8, 2 * 16}};
|
||||
|
||||
LNAGainField field_lna{
|
||||
{13 * 8, 2 * 16}};
|
||||
|
||||
VGAGainField field_vga{
|
||||
{16 * 8, 2 * 16}};
|
||||
|
||||
RSSI rssi{
|
||||
{19 * 8 - 4, 35, 6 * 8, 4}};
|
||||
|
||||
Channel channel{
|
||||
{19 * 8 - 4, 40, 6 * 8, 4}};
|
||||
|
||||
Labels labels{
|
||||
{{0 * 8, 3 * 16}, "Deviation:", Color::light_grey()},
|
||||
};
|
||||
|
||||
FrequencyField deviation_frequency{
|
||||
{10 * 8, 3 * 16},
|
||||
{3750, 500000},
|
||||
};
|
||||
|
||||
// DEBUG
|
||||
RecordView record_view{
|
||||
{0 * 8, 4 * 16, 30 * 8, 1 * 16},
|
||||
u"FSKRX_????.C16",
|
||||
u"FSKRX",
|
||||
RecordView::FileType::RawS16,
|
||||
16384,
|
||||
3};
|
||||
|
||||
MessageHandlerRegistration message_handler_packet{
|
||||
Message::ID::AFSKData,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const AFSKDataMessage*>(p);
|
||||
this->view_data.on_packet(message->value, message->is_data);
|
||||
}};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
#endif /*__POCSAG_APP_H__*/
|
||||
@@ -40,6 +40,7 @@ IQTrimView::IQTrimView(NavigationView& nav)
|
||||
&text_samples,
|
||||
&text_max,
|
||||
&field_cutoff,
|
||||
&field_amplify,
|
||||
&button_trim,
|
||||
});
|
||||
|
||||
@@ -72,6 +73,11 @@ IQTrimView::IQTrimView(NavigationView& nav)
|
||||
refresh_ui();
|
||||
};
|
||||
|
||||
field_amplify.set_value(1); // 1X is default (no amplification)
|
||||
field_amplify.on_change = [this](int32_t) {
|
||||
refresh_ui();
|
||||
};
|
||||
|
||||
button_trim.on_select = [this](Button&) {
|
||||
if (trim_capture()) {
|
||||
profile_capture();
|
||||
@@ -133,7 +139,18 @@ void IQTrimView::focus() {
|
||||
void IQTrimView::refresh_ui() {
|
||||
field_path.set_text(path_.filename().string());
|
||||
text_samples.set(to_string_dec_uint(info_->sample_count));
|
||||
text_max.set(to_string_dec_uint(info_->max_power));
|
||||
|
||||
// show max power after amplification applied
|
||||
uint64_t power_amp = field_amplify.value() * field_amplify.value() * field_amplify.value() * field_amplify.value();
|
||||
text_max.set(to_string_dec_uint(info_->max_power * power_amp));
|
||||
|
||||
// show max power in red if amplification is too high, causing clipping
|
||||
uint32_t clipping_limit = (fs::capture_file_sample_size(path_) == sizeof(complex8_t)) ? 0x80 : 0x8000;
|
||||
if ((field_amplify.value() * info_->max_iq) > clipping_limit)
|
||||
text_max.set_style(&Styles::red);
|
||||
else
|
||||
text_max.set_style(&Styles::light_grey);
|
||||
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
@@ -191,7 +208,7 @@ bool IQTrimView::trim_capture() {
|
||||
}
|
||||
|
||||
progress_ui.show_trimming();
|
||||
trimmed = iq::trim_capture_with_range(path_, trim_range, progress_ui.get_callback());
|
||||
trimmed = iq::trim_capture_with_range(path_, trim_range, progress_ui.get_callback(), field_amplify.value());
|
||||
progress_ui.clear();
|
||||
|
||||
if (!trimmed)
|
||||
|
||||
@@ -107,6 +107,8 @@ class IQTrimView : public View {
|
||||
{{0 * 8, 9 * 16}, "Max Pwr:", Color::light_grey()},
|
||||
{{0 * 8, 10 * 16}, "Cutoff :", Color::light_grey()},
|
||||
{{12 * 8, 10 * 16}, "%", Color::light_grey()},
|
||||
{{0 * 8, 12 * 16}, "Amplify:", Color::light_grey()},
|
||||
{{10 * 8, 12 * 16}, "x", Color::light_grey()},
|
||||
};
|
||||
|
||||
TextField field_path{
|
||||
@@ -135,7 +137,7 @@ class IQTrimView : public View {
|
||||
"0"};
|
||||
|
||||
Text text_max{
|
||||
{9 * 8, 9 * 16, 10 * 8, 1 * 16},
|
||||
{9 * 8, 9 * 16, 20 * 8, 1 * 16},
|
||||
"0"};
|
||||
|
||||
NumberField field_cutoff{
|
||||
@@ -145,6 +147,13 @@ class IQTrimView : public View {
|
||||
1,
|
||||
' '};
|
||||
|
||||
NumberField field_amplify{
|
||||
{9 * 8, 12 * 16},
|
||||
1,
|
||||
{1, 9},
|
||||
1,
|
||||
' '};
|
||||
|
||||
Button button_trim{
|
||||
{20 * 8, 16 * 16, 8 * 8, 2 * 16},
|
||||
"Trim"};
|
||||
|
||||
@@ -142,31 +142,24 @@ LevelView::LevelView(NavigationView& nav)
|
||||
}
|
||||
|
||||
void LevelView::on_statistics_update(const ChannelStatistics& statistics) {
|
||||
static int last_max_db = -1000;
|
||||
static int last_min_rssi = -1000;
|
||||
static int last_avg_rssi = -1000;
|
||||
static int last_max_rssi = -1000;
|
||||
static int16_t last_max_db = -1000;
|
||||
static int16_t last_min_rssi = -1000;
|
||||
static int16_t last_avg_rssi = -1000;
|
||||
static int16_t last_max_rssi = -1000;
|
||||
|
||||
rssi_graph.add_values(rssi.get_min(), rssi.get_avg(), rssi.get_max(), statistics.max_db);
|
||||
|
||||
bool refresh_db = false;
|
||||
bool refresh_rssi = false;
|
||||
|
||||
// refresh db
|
||||
if (last_max_db != statistics.max_db) {
|
||||
refresh_db = true;
|
||||
}
|
||||
if (last_min_rssi != rssi.get_min() || last_avg_rssi != rssi.get_avg() || last_max_rssi != rssi.get_max()) {
|
||||
refresh_rssi = true;
|
||||
}
|
||||
if (refresh_db) {
|
||||
last_max_db = statistics.max_db;
|
||||
freq_stats_db.set("Power: " + to_string_dec_int(statistics.max_db) + " db");
|
||||
}
|
||||
if (refresh_rssi) {
|
||||
last_min_rssi = rssi.get_min();
|
||||
last_avg_rssi = rssi.get_avg();
|
||||
last_max_rssi = rssi.get_max();
|
||||
freq_stats_rssi.set("RSSI: " + to_string_dec_int(rssi.get_min()) + "/" + to_string_dec_int(rssi.get_avg()) + "/" + to_string_dec_int(rssi.get_max()) + ",dt: " + to_string_dec_int(rssi.get_delta()));
|
||||
// refresh rssi
|
||||
if (last_min_rssi != rssi_graph.get_graph_min() || last_avg_rssi != rssi_graph.get_graph_avg() || last_max_rssi != rssi_graph.get_graph_max()) {
|
||||
last_min_rssi = rssi_graph.get_graph_min();
|
||||
last_avg_rssi = rssi_graph.get_graph_avg();
|
||||
last_max_rssi = rssi_graph.get_graph_max();
|
||||
freq_stats_rssi.set("RSSI: " + to_string_dec_int(last_min_rssi) + "/" + to_string_dec_int(last_avg_rssi) + "/" + to_string_dec_int(last_max_rssi) + ",dt: " + to_string_dec_int(rssi_graph.get_graph_delta()));
|
||||
}
|
||||
} /* on_statistic_updates */
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ void MicTXView::focus() {
|
||||
field_rxfrequency.focus();
|
||||
break;
|
||||
default:
|
||||
field_va.focus();
|
||||
check_va.focus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -60,24 +60,56 @@ void MicTXView::update_vumeter() {
|
||||
vumeter.set_value(audio_level);
|
||||
}
|
||||
|
||||
void MicTXView::update_tx_icon() {
|
||||
tx_icon.set_foreground(transmitting ? Color::red() : Color::black());
|
||||
tx_icon.set_background(transmitting ? Color::yellow() : Color::black());
|
||||
}
|
||||
|
||||
void MicTXView::on_tx_progress(const bool done) {
|
||||
// Roger beep played, stop transmitting
|
||||
if (done)
|
||||
set_tx(false);
|
||||
}
|
||||
|
||||
uint8_t MicTXView::shift_bits(void) {
|
||||
uint8_t shift_bits_s16{4}; // shift bits factor to the captured ADC S16 audio sample.
|
||||
|
||||
if (audio::debug::codec_name() == "WM8731") {
|
||||
switch (ak4951_alc_and_wm8731_boost_GUI) {
|
||||
case 0: // +12 dB’s respect reference level orig fw 1.5.x fw FM : when +20dB's boost ON) and shift bits (>>8),
|
||||
shift_bits_s16 = 6; // now mic-boost on (+20dBs) and shift bits (>>6), +20+12=32 dB’s (orig fw +20 dBs+ 0dBs)=> +12dB's respect ref.
|
||||
break;
|
||||
case 1: // +06 dB’s reference level, (when +20dB's boost ON)
|
||||
shift_bits_s16 = 7; // now mic-boost on (+20dBs) and shift bits (>>7), +20+06=26 dB’s (orig fw +20 dBs+ 0dBs) => +06dB's respect ref.
|
||||
break;
|
||||
case 2:
|
||||
shift_bits_s16 = 4; // +04 dB’s respect ref level, (when +20dB's boost OFF)
|
||||
break; // now mic-boost off (+00dBs) shift bits (4) (+0+24dB's)=24 dBs => +04dB's respect ref.
|
||||
case 3:
|
||||
shift_bits_s16 = 5; // -02 dB’s respect ref level, (when +20dB's boost OFF)
|
||||
break; // now mic-boost off (+00dBs) shift bits (5) (+0+18dB's)=18 dBs => -02dB's respect ref.
|
||||
case 4:
|
||||
shift_bits_s16 = 6; // -08 dB’s respect ref level, (when +20dB's boost OFF)
|
||||
break; // now mic-boost off (+00dBs) shift bits (6) (+0+12dB's)=12 dBs => -08dB's respect ref.
|
||||
}
|
||||
} else {
|
||||
shift_bits_s16 = 8; // Initialized default fixed >>8_FM for FM tx mod, shift audio data for AK4951, using top 8 bits s16 data (>>8)
|
||||
}
|
||||
return shift_bits_s16;
|
||||
}
|
||||
|
||||
void MicTXView::configure_baseband() {
|
||||
// TODO: Can this use the transmitter model instead?
|
||||
baseband::set_audiotx_config(
|
||||
sampling_rate / 20, // Update vu-meter at 20Hz
|
||||
transmitting ? transmitter_model.channel_bandwidth() : 0,
|
||||
mic_gain,
|
||||
shift_bits_s16, // to be used in dsp_modulate
|
||||
mic_gain_x10 / 10.0,
|
||||
shift_bits(), // to be used in dsp_modulate
|
||||
TONES_F2D(tone_key_frequency(tone_key_index), sampling_rate),
|
||||
enable_am,
|
||||
enable_dsb,
|
||||
enable_usb,
|
||||
enable_lsb);
|
||||
(mic_mod_index == MIC_MOD_AM),
|
||||
(mic_mod_index == MIC_MOD_DSB),
|
||||
(mic_mod_index == MIC_MOD_USB),
|
||||
(mic_mod_index == MIC_MOD_LSB));
|
||||
}
|
||||
|
||||
void MicTXView::set_tx(bool enable) {
|
||||
@@ -101,12 +133,24 @@ void MicTXView::set_tx(bool enable) {
|
||||
transmitting = false;
|
||||
configure_baseband();
|
||||
transmitter_model.disable();
|
||||
if (rx_enabled) // If audio RX is enabled and we've been transmitting
|
||||
|
||||
if (rx_enabled) {
|
||||
rxaudio(true); // Turn back on audio RX
|
||||
|
||||
// TODO FIXME: this isn't working: vu meter isn't going to 0:
|
||||
vumeter.set_value(0); // Reset vumeter
|
||||
vumeter.dirty(); // Force to refresh vumeter.
|
||||
}
|
||||
}
|
||||
}
|
||||
update_tx_icon();
|
||||
}
|
||||
|
||||
bool MicTXView::tx_button_held() {
|
||||
const auto switches_state = get_switches_state();
|
||||
return switches_state[(size_t)ui::KeyEvent::Select];
|
||||
};
|
||||
|
||||
void MicTXView::do_timing() {
|
||||
if (va_enabled) {
|
||||
if (!transmitting) {
|
||||
@@ -128,7 +172,8 @@ void MicTXView::do_timing() {
|
||||
if ((decay_timer >> 8) >= decay_ms) {
|
||||
decay_timer = 0;
|
||||
attack_timer = 0;
|
||||
set_tx(false);
|
||||
if (!button_touch && !tx_button_held())
|
||||
set_tx(false);
|
||||
} else {
|
||||
decay_timer += lcd_frame_duration;
|
||||
}
|
||||
@@ -138,42 +183,39 @@ void MicTXView::do_timing() {
|
||||
}
|
||||
} else {
|
||||
// Check for PTT release
|
||||
const auto switches_state = get_switches_state();
|
||||
if (!switches_state[4] && transmitting && !button_touch) // Select button
|
||||
if (transmitting && !button_touch && !tx_button_held())
|
||||
set_tx(false);
|
||||
}
|
||||
}
|
||||
|
||||
void MicTXView::rxaudio(bool is_on) {
|
||||
if (is_on) {
|
||||
void MicTXView::rxaudio(bool enable) {
|
||||
if (enable) {
|
||||
audio::input::stop();
|
||||
baseband::shutdown();
|
||||
|
||||
if (enable_am || enable_usb || enable_lsb || enable_dsb) { // "NFM/FM", 0, " WFM ", 1, " AM ", 2, " USB ", 3, " LSB ", 4, " DSB-SC", 5
|
||||
baseband::run_image(portapack::spi_flash::image_tag_am_audio);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::AMAudio); // that AM demodulation engine is common to all Amplitude mod : AM/USB/LSB/DSB (2,3,4,5)
|
||||
if (options_mode.selected_index() < 5) // We will be called here with 2,3,4,5. We treat here demod. filter 2,3,4; (excluding DSB-C case (5) it is treated more down).
|
||||
receiver_model.set_am_configuration(options_mode.selected_index() - 1); // selecting proper filter(2,3,4). 2-1=1=>6k-AM(1), 3-1=2=>+3k-USB(2), 4-1=3=>-3K-LSB(3),
|
||||
} else { // We are in NFM/FM or WFM (NFM BW:8k5 or 11k / FM BW 16k / WFM BW:200k)
|
||||
|
||||
if (enable_wfm) { // WFM, BW 200Khz aprox, or the two new addional BW filters (180k, 40k)
|
||||
switch (mic_mod_index) {
|
||||
case MIC_MOD_NFM: // NFM BW:8k5 or 11k / FM BW 16k
|
||||
baseband::run_image(portapack::spi_flash::image_tag_nfm_audio);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::NarrowbandFMAudio);
|
||||
// receiver_model.set_nbfm_configuration(n); is called above, depending user's selection (8k5, 11k, 16k).
|
||||
break;
|
||||
case MIC_MOD_WFM: // WFM, BW 200Khz aprox, or the two new addional BW filters (180k, 40k)
|
||||
baseband::run_image(portapack::spi_flash::image_tag_wfm_audio);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::WidebandFMAudio);
|
||||
// receiver_model.set_wfm_configuration(n); // it is called above, depending user's selection (200k, 180k, 0k).
|
||||
} else { // NFM BW:8k5 or 11k / FM BW 16k
|
||||
baseband::run_image(portapack::spi_flash::image_tag_nfm_audio);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::NarrowbandFMAudio); //
|
||||
// receiver_model.set_nbfm_configuration(n); is called above, depending user's selection (8k5, 11k, 16k).
|
||||
}
|
||||
// receiver_model.set_wfm_configuration(n); is called above, depending user's selection (200k, 180k, 0k).
|
||||
break;
|
||||
case MIC_MOD_AM:
|
||||
case MIC_MOD_DSB:
|
||||
case MIC_MOD_USB:
|
||||
case MIC_MOD_LSB:
|
||||
baseband::run_image(portapack::spi_flash::image_tag_am_audio);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::AMAudio); // that AM demodulation engine is common to all Amplitude mod : AM/USB/LSB/DSB (2,3,4,5)
|
||||
if (options_mode.selected_index() < 5) // We will be called here with 2,3,4,5. We treat here demod. filter 2,3,4; (excluding DSB-C case (5) it is treated more down).
|
||||
receiver_model.set_am_configuration(options_mode.selected_index() - 1); // selecting proper filter(2,3,4). 2-1=1=>6k-AM(1), 3-1=2=>+3k-USB(2), 4-1=3=>-3K-LSB(3),
|
||||
break;
|
||||
}
|
||||
|
||||
if (bool_same_F_tx_rx_enabled) // when stop TX, define to which freq RX we return
|
||||
receiver_model.set_target_frequency(tx_frequency); // Update freq also for RX = TX
|
||||
else
|
||||
receiver_model.set_target_frequency(rx_frequency); // Now with separate freq controls!
|
||||
receiver_model.set_lna(rx_lna);
|
||||
receiver_model.set_vga(rx_vga);
|
||||
receiver_model.set_rf_amp(rx_amp);
|
||||
receiver_model.set_target_frequency(bool_same_F_tx_rx_enabled ? tx_frequency : rx_frequency);
|
||||
receiver_model.enable();
|
||||
audio::output::start();
|
||||
} else { // These incredibly convoluted steps are required for the vumeter to reappear when stopping RX.
|
||||
@@ -183,14 +225,82 @@ void MicTXView::rxaudio(bool is_on) {
|
||||
|
||||
baseband::run_image(portapack::spi_flash::image_tag_mic_tx);
|
||||
audio::output::stop();
|
||||
audio::input::start(ak4951_alc_and_wm8731_boost_GUI); // When detected AK4951 => set up ALC mode; when detected WM8731 => set up mic_boost ON/OFF.
|
||||
audio::input::start(ak4951_alc_and_wm8731_boost_GUI, mic_to_HP_enabled); // set up ALC mode (AK4951) or set up mic_boost ON/OFF (WM8731). and the check box "Hear Mic"
|
||||
portapack::pin_i2s0_rx_sda.mode(3);
|
||||
configure_baseband();
|
||||
}
|
||||
}
|
||||
|
||||
void MicTXView::set_ptt_visibility(bool v) {
|
||||
tx_button.hidden(!v);
|
||||
void MicTXView::set_rxbw_options(void) {
|
||||
using option_t = std::pair<std::string, int32_t>;
|
||||
using options_t = std::vector<option_t>;
|
||||
options_t rxbw; // Aux structure to change dynamically field_rxbw contents
|
||||
|
||||
switch (mic_mod_index) {
|
||||
case MIC_MOD_NFM:
|
||||
freqman_set_bandwidth_option(NFM_MODULATION, field_rxbw); // restore dynamic field_rxbw value with NFM options from freqman_db.cpp
|
||||
field_rxbw.set_by_value(receiver_model.nbfm_configuration());
|
||||
break;
|
||||
case MIC_MOD_WFM:
|
||||
freqman_set_bandwidth_option(WFM_MODULATION, field_rxbw); // restore dynamic field_rxbw value with WFM options from freqman_db.cpp
|
||||
field_rxbw.set_by_value(receiver_model.wfm_configuration());
|
||||
break;
|
||||
case MIC_MOD_AM:
|
||||
rxbw.emplace_back("DSB 9k", 0); // we offer in AM DSB two audio BW 9k / 6k.
|
||||
rxbw.emplace_back("DSB 6k", 1);
|
||||
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
|
||||
break;
|
||||
case MIC_MOD_DSB:
|
||||
rxbw.emplace_back("USB+3k", 0); // added dynamically two options (index 0,1) to that DSB-SC case to the field_rxbw value.
|
||||
rxbw.emplace_back("LSB-3k", 1);
|
||||
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
|
||||
break;
|
||||
case MIC_MOD_USB:
|
||||
rxbw.emplace_back("USB+3k", 0); // locked a fixed option, to display it.
|
||||
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
|
||||
break;
|
||||
case MIC_MOD_LSB:
|
||||
rxbw.emplace_back("LSB-3k", 0); // locked a fixed option, to display it.
|
||||
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void MicTXView::set_rxbw_defaults(bool use_app_settings) {
|
||||
if (use_app_settings) {
|
||||
field_bw.set_value(transmitter_model.channel_bandwidth() / 1000);
|
||||
field_rxbw.set_by_value(rxbw_index);
|
||||
} else if (mic_mod_index == MIC_MOD_NFM) {
|
||||
field_bw.set_value(10); // NFM TX bw 10k, RX bw 16k (2) default
|
||||
field_rxbw.set_by_value(2);
|
||||
} else if (mic_mod_index == MIC_MOD_WFM) {
|
||||
field_bw.set_value(75); // WFM TX bw 75K, RX bw 200k (0) default
|
||||
field_rxbw.set_by_value(0);
|
||||
}
|
||||
// field_bw is hidden in other modulation cases
|
||||
}
|
||||
|
||||
void MicTXView::update_receiver_rxbw(void) {
|
||||
// In Previous fw versions, that nbfm_configuration(n) was done in any mode (FM/AM/SSB/DSB)...strictly speaking only need it in (NFM/FM)
|
||||
// In AM TX mode, we will allow both independent RX audio BW : AM 9K (9K00AE3 / AM 6K (6K00AE3). (In AM option v can be 0 (9k), 1 (6k)
|
||||
// In DSB-SC TX mode, we will allow both independent RX SSB demodulation (USB / LSB side band). in that submenu, v is 0 (SSB1 USB) or 1 (SSB2 LSB)
|
||||
switch (mic_mod_index) {
|
||||
case MIC_MOD_NFM:
|
||||
receiver_model.set_nbfm_configuration(rxbw_index); // we are in NFM/FM case, we need to select proper NFM/FM RX channel filter, NFM BW 8K5(0), NFM BW 11K(1), FM BW 16K (2)
|
||||
break;
|
||||
case MIC_MOD_WFM:
|
||||
receiver_model.set_wfm_configuration(rxbw_index); // we are in WFM case, we need to select proper WFB RX BW filter, WFM BW 200K(0), WFM BW 180K(1), WFM BW 40K(2)
|
||||
break;
|
||||
case MIC_MOD_AM:
|
||||
receiver_model.set_am_configuration(rxbw_index); // we are in AM TX mode, we need to select proper AM full path config AM-9K filter. 0+0 =>AM-9K(0), 0+1=1 =>AM-6K(1),
|
||||
break;
|
||||
case MIC_MOD_USB:
|
||||
case MIC_MOD_LSB:
|
||||
break; // change can't happen in these modes because there's only one BW choice
|
||||
case MIC_MOD_DSB:
|
||||
receiver_model.set_am_configuration(rxbw_index + 2); // we are in DSB-SC TX mode, we need to select proper SSB filter. 0+2 =>usb(2), 1+2=3 =>lsb(3),
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
MicTXView::MicTXView(
|
||||
@@ -199,400 +309,260 @@ MicTXView::MicTXView(
|
||||
|
||||
baseband::run_image(portapack::spi_flash::image_tag_mic_tx);
|
||||
|
||||
if (audio::debug::codec_name() == "WM8731") {
|
||||
add_children({&labels_WM8731, // we have audio codec WM8731, same MIC menu as original.
|
||||
&vumeter,
|
||||
&options_gain, // MIC GAIN float factor on the GUI.
|
||||
&options_wm8731_boost_mode,
|
||||
// &check_va,
|
||||
&field_va,
|
||||
&field_va_level,
|
||||
&field_va_attack,
|
||||
&field_va_decay,
|
||||
&field_bw,
|
||||
&tx_view, // it already integrates previous rfgain, rfamp.
|
||||
&options_mode,
|
||||
&field_frequency,
|
||||
&options_tone_key,
|
||||
&check_rogerbeep,
|
||||
&check_common_freq_tx_rx, // added to handle common or separate freq- TX/RX
|
||||
&check_rxactive,
|
||||
&field_volume,
|
||||
&field_rxbw,
|
||||
&field_squelch,
|
||||
&field_rxfrequency,
|
||||
&field_rxlna,
|
||||
&field_rxvga,
|
||||
&field_rxamp,
|
||||
&tx_button});
|
||||
bool wm = (audio::debug::codec_name() == "WM8731");
|
||||
add_children({&labels_both,
|
||||
wm ? &labels_WM8731 : &labels_AK4951, // enable ALC if AK4951
|
||||
&vumeter,
|
||||
&options_gain, // MIC GAIN float factor on the GUI.
|
||||
wm ? &options_wm8731_boost_mode : &options_ak4951_alc_mode,
|
||||
&check_va,
|
||||
&field_va_level,
|
||||
&field_va_attack,
|
||||
&field_va_decay,
|
||||
&field_bw,
|
||||
&tx_view, // it already integrates previous rfgain, rfamp.
|
||||
&options_mode,
|
||||
&field_frequency,
|
||||
&options_tone_key,
|
||||
&check_rogerbeep,
|
||||
&check_mic_to_HP, // check box to activate "hear mic"
|
||||
&check_common_freq_tx_rx, // added to handle common or separate freq- TX/RX
|
||||
&check_rxactive,
|
||||
&field_volume,
|
||||
&field_rxbw,
|
||||
&field_squelch,
|
||||
&field_rxfrequency,
|
||||
&field_rxlna,
|
||||
&field_rxvga,
|
||||
&field_rxamp,
|
||||
&tx_button,
|
||||
&tx_icon});
|
||||
|
||||
} else {
|
||||
add_children({&labels_AK4951, // we have audio codec AK4951, enable Automatic Level Control
|
||||
&vumeter,
|
||||
&options_gain,
|
||||
&options_ak4951_alc_mode,
|
||||
// &check_va,
|
||||
&field_va,
|
||||
&field_va_level,
|
||||
&field_va_attack,
|
||||
&field_va_decay,
|
||||
&field_bw,
|
||||
&tx_view, // it already integrates previous rfgain, rfamp.
|
||||
&options_mode,
|
||||
&field_frequency,
|
||||
&options_tone_key,
|
||||
&check_rogerbeep,
|
||||
&check_common_freq_tx_rx, // added to handle common or separate freq- TX/RX
|
||||
&check_rxactive,
|
||||
&field_volume,
|
||||
&field_rxbw,
|
||||
&field_squelch,
|
||||
&field_rxfrequency,
|
||||
&field_rxlna,
|
||||
&field_rxvga,
|
||||
&field_rxamp,
|
||||
&tx_button});
|
||||
}
|
||||
set_rxbw_options();
|
||||
set_rxbw_defaults(settings_.loaded());
|
||||
|
||||
tone_keys_populate(options_tone_key);
|
||||
options_tone_key.set_selected_index(tone_key_index);
|
||||
options_tone_key.on_change = [this](size_t i, int32_t) {
|
||||
tone_key_index = i;
|
||||
};
|
||||
options_tone_key.set_selected_index(0);
|
||||
|
||||
check_rogerbeep.set_value(rogerbeep_enabled);
|
||||
check_rogerbeep.on_select = [this](Checkbox&, bool v) {
|
||||
rogerbeep_enabled = v;
|
||||
};
|
||||
|
||||
field_rxlna.set_value(receiver_model.lna());
|
||||
field_rxlna.on_change = [this](int32_t v) {
|
||||
receiver_model.set_lna(v);
|
||||
};
|
||||
|
||||
field_rxvga.set_value(receiver_model.vga());
|
||||
field_rxvga.on_change = [this](int32_t v) {
|
||||
receiver_model.set_vga(v);
|
||||
};
|
||||
|
||||
field_rxamp.set_value(receiver_model.rf_amp());
|
||||
field_rxamp.on_change = [this](int32_t v) {
|
||||
receiver_model.set_rf_amp(v);
|
||||
};
|
||||
|
||||
options_gain.on_change = [this](size_t, int32_t v) {
|
||||
mic_gain = v / 10.0;
|
||||
mic_gain_x10 = v;
|
||||
configure_baseband();
|
||||
};
|
||||
options_gain.set_selected_index(1); // x1.0 preselected default.
|
||||
options_gain.set_by_value(mic_gain_x10); // x1.0 preselected default.
|
||||
|
||||
field_rxbw.on_change = [this](size_t, int32_t v) {
|
||||
rxbw_index = v;
|
||||
update_receiver_rxbw();
|
||||
};
|
||||
field_rxbw.set_by_value(rxbw_index);
|
||||
|
||||
field_squelch.set_value(receiver_model.squelch_level());
|
||||
field_squelch.on_change = [this](int32_t v) {
|
||||
receiver_model.set_squelch_level(v);
|
||||
};
|
||||
|
||||
if (audio::debug::codec_name() == "WM8731") {
|
||||
options_wm8731_boost_mode.on_change = [this](size_t, int8_t v) {
|
||||
switch (v) {
|
||||
case 0: // +12 dB’s respect reference level orig fw 1.5.x fw FM : when +20dB's boost ON) and shift bits (>>8),
|
||||
shift_bits_s16 = 6; // now mic-boost on (+20dBs) and shift bits (>>6), +20+12=32 dB’s (orig fw +20 dBs+ 0dBs)=> +12dB's respect ref.
|
||||
break;
|
||||
case 1: // +06 dB’s reference level, (when +20dB's boost ON)
|
||||
shift_bits_s16 = 7; // now mic-boost on (+20dBs) and shift bits (>>7), +20+06=26 dB’s (orig fw +20 dBs+ 0dBs) => +06dB's respect ref.
|
||||
break;
|
||||
case 2:
|
||||
shift_bits_s16 = 4; // +04 dB’s respect ref level, (when +20dB's boost OFF)
|
||||
break; // now mic-boost off (+00dBs) shift bits (4) (+0+24dB's)=24 dBs => +04dB's respect ref.
|
||||
case 3:
|
||||
shift_bits_s16 = 5; // -02 dB’s respect ref level, (when +20dB's boost OFF)
|
||||
break; // now mic-boost off (+00dBs) shift bits (5) (+0+18dB's)=18 dBs => -02dB's respect ref.
|
||||
case 4:
|
||||
shift_bits_s16 = 6; // -08 dB’s respect ref level, (when +20dB's boost OFF)
|
||||
break; // now mic-boost off (+00dBs) shift bits (6) (+0+12dB's)=12 dBs => -08dB's respect ref.
|
||||
}
|
||||
ak4951_alc_and_wm8731_boost_GUI = v; // 0..4, WM8731_boost dB's options, (combination boost on/off, and effective gain in captured data >>x)
|
||||
audio::input::start(ak4951_alc_and_wm8731_boost_GUI); // Detected (WM8731), set up the proper wm_boost on/off, 0..4 (0,1) boost_on, (2,3,4) boost_off
|
||||
configure_baseband(); // to update in real time, sending msg, var-parameters >>shift_bits FM msg, to audio_tx from M0 to M4 Proc -
|
||||
ak4951_alc_and_wm8731_boost_GUI = v; // 0..4, WM8731_boost dB's options, (combination boost on/off, and effective gain in captured data >>x)
|
||||
audio::input::start(ak4951_alc_and_wm8731_boost_GUI, mic_to_HP_enabled); // Detected (WM8731), set up the proper wm_boost on/off, 0..4 (0,1) boost_on, (2,3,4) boost_off,and the check box "Hear Mic"
|
||||
configure_baseband(); // to update in real time, sending msg, var-parameters >>shift_bits FM msg, to audio_tx from M0 to M4 Proc -
|
||||
};
|
||||
options_wm8731_boost_mode.set_selected_index(3); // preset GUI index 3 as default WM -> -02 dB's.
|
||||
if (!settings_.loaded()) {
|
||||
ak4951_alc_and_wm8731_boost_GUI = 3; // preset GUI index 3 as default WM -> -02 dB's
|
||||
}
|
||||
options_wm8731_boost_mode.set_selected_index(ak4951_alc_and_wm8731_boost_GUI);
|
||||
} else {
|
||||
shift_bits_s16 = 8; // Initialized default fixed >>8_FM for FM tx mod, shift audio data for AK4951, using top 8 bits s16 data (>>8)
|
||||
options_ak4951_alc_mode.on_change = [this](size_t, int8_t v) {
|
||||
ak4951_alc_and_wm8731_boost_GUI = v; // 0..11, AK4951 Mic -Automatic volume Level Control options,
|
||||
audio::input::start(ak4951_alc_and_wm8731_boost_GUI); // Detected (AK4951) ==> Set up proper ALC mode from 0..11 options
|
||||
configure_baseband(); // sending fixed >>8_FM, var-parameters msg, to audiotx from this M0 to M4 process.
|
||||
ak4951_alc_and_wm8731_boost_GUI = v; // 0..11, AK4951 Mic -Automatic volume Level Control options,
|
||||
audio::input::start(ak4951_alc_and_wm8731_boost_GUI, mic_to_HP_enabled); // Detected (AK4951) ==> Set up proper ALC mode from 0..11 options, and the check box "Hear Mic"
|
||||
configure_baseband(); // sending fixed >>8_FM, var-parameters msg, to audiotx from this M0 to M4 process.
|
||||
};
|
||||
if (!settings_.loaded())
|
||||
ak4951_alc_and_wm8731_boost_GUI = 0;
|
||||
options_ak4951_alc_mode.set_selected_index(ak4951_alc_and_wm8731_boost_GUI);
|
||||
}
|
||||
|
||||
// options_ak4951_alc_mode.set_selected_index(0);
|
||||
|
||||
// WARNING: transmitter_model.set_target_frequency() and receiver_model.set_target_frequency() both update the same tuning freq, but one has an offset!
|
||||
tx_frequency = transmitter_model.target_frequency();
|
||||
field_frequency.set_value(transmitter_model.target_frequency());
|
||||
field_frequency.set_step(receiver_model.frequency_step());
|
||||
field_frequency.on_change = [this](rf::Frequency f) {
|
||||
tx_frequency = f;
|
||||
if (!rx_enabled) { // not activated receiver. just update freq TX
|
||||
if (!rx_enabled)
|
||||
transmitter_model.set_target_frequency(f);
|
||||
} else { // activated receiver.
|
||||
if (bool_same_F_tx_rx_enabled) // user selected common freq- TX = RX
|
||||
receiver_model.set_target_frequency(f); // Update common freq also for RX
|
||||
}
|
||||
else if (bool_same_F_tx_rx_enabled)
|
||||
receiver_model.set_target_frequency(f);
|
||||
// else tuning freq will be updated when rx is enabled, transmitting starts, or when rx_frequency is changed (if rx_enabled)
|
||||
};
|
||||
field_frequency.on_edit = [this, &nav]() {
|
||||
focused_ui = 0;
|
||||
// TODO: Provide separate modal method/scheme?
|
||||
auto new_view = nav.push<FrequencyKeypadView>(tx_frequency);
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
tx_frequency = f;
|
||||
if (!rx_enabled) {
|
||||
transmitter_model.set_target_frequency(f);
|
||||
} else {
|
||||
if (bool_same_F_tx_rx_enabled)
|
||||
receiver_model.set_target_frequency(f); // Update freq also for RX
|
||||
}
|
||||
this->field_frequency.set_value(f);
|
||||
set_dirty();
|
||||
};
|
||||
};
|
||||
field_frequency.set_value(tx_frequency);
|
||||
|
||||
field_bw.on_change = [this](uint32_t v) {
|
||||
transmitter_model.set_channel_bandwidth(v * 1000);
|
||||
};
|
||||
field_bw.set_value(10); // pre-default first time, TX deviation FM for NFM / FM
|
||||
// TODO: would be nice if frequency step was configurable in this app
|
||||
field_frequency.set_step(receiver_model.frequency_step());
|
||||
|
||||
// now, no need direct update, field_rfgain, field_rfamp (it is done in ui_transmitter.cpp)
|
||||
|
||||
options_mode.on_change = [this](size_t, int32_t v) { // { "NFM/FM", 0 }, { " WFM ", 1 }, { "AM", 2 }, { "USB", 3 }, { "LSB", 4 }, { "DSB", 5 }
|
||||
enable_am = false;
|
||||
enable_usb = false;
|
||||
enable_lsb = false;
|
||||
enable_dsb = false;
|
||||
enable_wfm = false;
|
||||
|
||||
using option_t = std::pair<std::string, int32_t>;
|
||||
using options_t = std::vector<option_t>;
|
||||
options_t rxbw; // Aux structure to change dynamically field_rxbw contents,
|
||||
|
||||
switch (v) {
|
||||
case 0: //{ "FM", 0 }
|
||||
enable_am = false;
|
||||
enable_usb = false;
|
||||
enable_lsb = false;
|
||||
enable_dsb = false;
|
||||
field_bw.set_value(10); // pre-default deviation FM for WFM
|
||||
// field_bw.set_value(transmitter_model.channel_bandwidth() / 1000);
|
||||
// if (rx_enabled)
|
||||
rxaudio(rx_enabled); // Update now if we have RX audio on
|
||||
options_tone_key.hidden(0); // we are in FM mode, we should have active the Key-tones & CTCSS option.
|
||||
|
||||
freqman_set_bandwidth_option(NFM_MODULATION, field_rxbw); // restore dynamic field_rxbw value with NFM options from freqman_db.cpp
|
||||
field_rxbw.set_by_value(2); // // bw 16k (2) default
|
||||
field_rxbw.hidden(0); // we are in FM mode, we need to allow the user set up of the RX NFM BW selection (8K5, 11K, 16K)
|
||||
field_bw.hidden(0); // we are in FM mode, we need to allow FM deviation parameter, in non FM mode.
|
||||
break;
|
||||
case 1: //{ "WFM", 1 }
|
||||
enable_am = false;
|
||||
enable_usb = false;
|
||||
enable_lsb = false;
|
||||
enable_dsb = false;
|
||||
enable_wfm = true;
|
||||
field_bw.set_value(75); // pre-default deviation FM for WFM
|
||||
// field_bw.set_value(transmitter_model.channel_bandwidth() / 1000);
|
||||
// if (rx_enabled)
|
||||
rxaudio(rx_enabled); // Update now if we have RX audio on
|
||||
options_tone_key.hidden(0); // we are in WFM mode, we should have active the Key-tones & CTCSS option.
|
||||
|
||||
freqman_set_bandwidth_option(WFM_MODULATION, field_rxbw); // restore dynamic field_rxbw value with WFM options from freqman_db.cpp
|
||||
field_rxbw.set_by_value(0); // bw 200k (0) default
|
||||
field_rxbw.hidden(0); // we are in WFM mode, we need to show to the user the selected BW WFM filter.
|
||||
field_bw.hidden(0); // we are in WFM mode, we need to allow WFM deviation parameter, in non FM mode.
|
||||
break;
|
||||
case 2: //{ "AM", 2 }
|
||||
enable_am = true;
|
||||
rxaudio(rx_enabled); // Update now if we have RX audio on
|
||||
options_tone_key.set_selected_index(0); // we are NOT in FM mode, we reset the possible previous key-tones &CTCSS selection.
|
||||
set_dirty(); // Refresh display
|
||||
options_tone_key.hidden(1); // we hide that Key-tones & CTCSS input selecction, (no meaning in AM/DSB/SSB).
|
||||
|
||||
rxbw.emplace_back("DSB 9k", 0); // we offer in AM DSB two audio BW 9k / 6k.
|
||||
rxbw.emplace_back("DSB 6k", 1);
|
||||
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
|
||||
|
||||
field_rxbw.hidden(0); // we show fixed RX AM BW 6Khz
|
||||
field_bw.hidden(1); // we hide the FM TX deviation parameter, in non FM mode.
|
||||
check_rogerbeep.hidden(0); // make visible again the "rogerbeep" selection.
|
||||
break;
|
||||
case 3: //{ "USB", 3 }
|
||||
enable_usb = true;
|
||||
rxaudio(rx_enabled); // Update now if we have RX audio on
|
||||
check_rogerbeep.set_value(false); // reset the possible activation of roger beep, because it is not compatible with SSB, by now.
|
||||
check_rogerbeep.hidden(1); // hide that roger beep selection.
|
||||
|
||||
rxbw.emplace_back("USB+3k", 0); // locked a fixed option, to display it.
|
||||
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
|
||||
|
||||
set_dirty(); // Refresh display
|
||||
break;
|
||||
case 4: //{ "LSB", 4 }
|
||||
enable_lsb = true;
|
||||
rxaudio(rx_enabled); // Update now if we have RX audio on
|
||||
check_rogerbeep.set_value(false); // reset the possible activation of roger beep, because it is not compatible with SSB, by now.
|
||||
check_rogerbeep.hidden(1); // hide that roger beep selection.
|
||||
|
||||
rxbw.emplace_back("LSB-3k", 0); // locked a fixed option, to display it.
|
||||
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
|
||||
|
||||
set_dirty(); // Refresh display
|
||||
break;
|
||||
case 5: //{ "DSB-SC", 5 }
|
||||
enable_dsb = true;
|
||||
rxaudio(rx_enabled); // Update now if we have RX audio on
|
||||
check_rogerbeep.hidden(0); // make visible again the "rogerbeep" selection.
|
||||
|
||||
rxbw.emplace_back("USB+3k", 0); // added dynamically two options (index 0,1) to that DSB-SC case to the field_rxbw value.
|
||||
rxbw.emplace_back("LSB-3k", 1);
|
||||
|
||||
field_rxbw.set_options(rxbw); // store that aux GUI option to the field_rxbw.
|
||||
|
||||
break;
|
||||
}
|
||||
// configure_baseband();
|
||||
};
|
||||
|
||||
/*
|
||||
check_va.on_select = [this](Checkbox&, bool v) {
|
||||
va_enabled = v;
|
||||
text_ptt.hidden(v); //hide / show PTT text
|
||||
check_rxactive.hidden(v); //hide / show the RX AUDIO
|
||||
set_dirty(); //Refresh display
|
||||
};
|
||||
*/
|
||||
field_va.set_selected_index(1);
|
||||
field_va.on_change = [this](size_t, int32_t v) {
|
||||
switch (v) {
|
||||
case 0:
|
||||
va_enabled = 0;
|
||||
this->set_ptt_visibility(0);
|
||||
check_rxactive.hidden(0);
|
||||
ptt_enabled = 0;
|
||||
break;
|
||||
case 1:
|
||||
va_enabled = 0;
|
||||
this->set_ptt_visibility(1);
|
||||
check_rxactive.hidden(0);
|
||||
ptt_enabled = 1;
|
||||
break;
|
||||
case 2:
|
||||
if (!rx_enabled) {
|
||||
va_enabled = 1;
|
||||
this->set_ptt_visibility(0);
|
||||
check_rxactive.hidden(1);
|
||||
ptt_enabled = 0;
|
||||
} else {
|
||||
field_va.set_selected_index(1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
set_dirty();
|
||||
};
|
||||
|
||||
check_rogerbeep.on_select = [this](Checkbox&, bool v) {
|
||||
rogerbeep_enabled = v;
|
||||
};
|
||||
|
||||
check_common_freq_tx_rx.on_select = [this](Checkbox&, bool v) {
|
||||
bool_same_F_tx_rx_enabled = v;
|
||||
field_rxfrequency.hidden(v); // Hide or show separated freq RX field. (When no hide user can enter down indep. freq for RX)
|
||||
set_dirty(); // Refresh GUI interface
|
||||
receiver_model.set_target_frequency(v ? tx_frequency : rx_frequency); // To go to the proper tuned freq. when toggling it
|
||||
};
|
||||
|
||||
field_va_level.on_change = [this](int32_t v) {
|
||||
va_level = v;
|
||||
vumeter.set_mark(v);
|
||||
};
|
||||
field_va_level.set_value(40);
|
||||
|
||||
field_va_attack.on_change = [this](int32_t v) {
|
||||
attack_ms = v;
|
||||
};
|
||||
field_va_attack.set_value(500);
|
||||
|
||||
field_va_decay.on_change = [this](int32_t v) {
|
||||
decay_ms = v;
|
||||
};
|
||||
field_va_decay.set_value(1000);
|
||||
|
||||
check_rxactive.on_select = [this](Checkbox&, bool v) {
|
||||
// vumeter.set_value(0); //Start with a clean vumeter
|
||||
rx_enabled = v;
|
||||
// check_va.hidden(v); //Hide or show voice activation
|
||||
rxaudio(v); // Activate-Deactivate audio rx accordingly
|
||||
set_dirty(); // Refresh interface
|
||||
};
|
||||
|
||||
field_rxbw.on_change = [this](size_t, int32_t v) {
|
||||
if (!(enable_am || enable_usb || enable_lsb || enable_dsb || enable_wfm)) {
|
||||
// In Previous fw versions, that nbfm_configuration(n) was done in any mode (FM/AM/SSB/DSB)...strictly speaking only need it in (NFM/FM)
|
||||
receiver_model.set_nbfm_configuration(v); // we are in NFM/FM case, we need to select proper NFM/FM RX channel filter, NFM BW 8K5(0), NFM BW 11K(1), FM BW 16K (2)
|
||||
} else { // we are not in NFM/FM mode. (we could be in any of the rest : AM /USB/LSB/DSB-SC)
|
||||
if (enable_am) { // we are in AM TX mode, we will allow both independent RX audio BW : AM 9K (9K00AE3 / AM 6K (6K00AE3). (In AM option v can be 0 (9k), 1 (6k)
|
||||
receiver_model.set_am_configuration(v); // we are in AM TX mode, we need to select proper AM full path config AM-9K filter. 0+0 =>AM-9K(0), 0+1=1 =>AM-6K(1),
|
||||
}
|
||||
if (enable_dsb) { // we are in DSB-SC in TX mode, we will allow both independent RX SSB demodulation (USB / LSB side band). in that submenu, v is 0 (SSB1 USB) or 1 (SSB2 LSB)
|
||||
receiver_model.set_am_configuration(v + 2); // we are in DSB-SC TX mode, we need to select proper SSB filter. 0+2 =>usb(2), 1+2=3 =>lsb(3),
|
||||
}
|
||||
if (enable_wfm) {
|
||||
receiver_model.set_wfm_configuration(v); // we are in WFM case, we need to select proper WFB RX BW filter, WFM BW 200K(0), WFM BW 180K(1), WFM BW 40K(2)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
field_squelch.on_change = [this](int32_t v) {
|
||||
receiver_model.set_squelch_level(100 - v);
|
||||
};
|
||||
field_squelch.set_value(0);
|
||||
receiver_model.set_squelch_level(0);
|
||||
|
||||
rx_frequency = receiver_model.target_frequency();
|
||||
// WARNING: transmitter_model.set_target_frequency() and receiver_model.set_target_frequency() both update the same tuning freq, but one has an offset!
|
||||
field_rxfrequency.set_value(rx_frequency);
|
||||
field_rxfrequency.set_step(receiver_model.frequency_step());
|
||||
field_rxfrequency.on_change = [this](rf::Frequency f) { // available when field rxfrequency not hidden => user selected separated freq RX/TX-
|
||||
rx_frequency = f;
|
||||
if (rx_enabled)
|
||||
receiver_model.set_target_frequency(f);
|
||||
receiver_model.set_target_frequency(rx_frequency);
|
||||
};
|
||||
field_rxfrequency.on_edit = [this, &nav]() { // available when field rxfrequency not hidden => user selected separated freq RX/TX-
|
||||
focused_ui = 1;
|
||||
// TODO: Provide separate modal method/scheme?
|
||||
auto new_view = nav.push<FrequencyKeypadView>(rx_frequency);
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
rx_frequency = f;
|
||||
if (rx_enabled)
|
||||
receiver_model.set_target_frequency(f);
|
||||
this->field_rxfrequency.set_value(f);
|
||||
set_dirty();
|
||||
};
|
||||
};
|
||||
|
||||
rx_lna = receiver_model.lna();
|
||||
field_rxlna.on_change = [this](int32_t v) {
|
||||
rx_lna = v;
|
||||
if (rx_enabled)
|
||||
receiver_model.set_lna(v);
|
||||
};
|
||||
field_rxlna.set_value(rx_lna);
|
||||
// TODO: would be nice if frequency step was configurable in this app
|
||||
field_rxfrequency.set_step(receiver_model.frequency_step());
|
||||
|
||||
rx_vga = receiver_model.vga();
|
||||
field_rxvga.on_change = [this](int32_t v) {
|
||||
rx_vga = v;
|
||||
if (rx_enabled)
|
||||
receiver_model.set_vga(v);
|
||||
check_common_freq_tx_rx.on_select = [this](Checkbox&, bool v) {
|
||||
bool_same_F_tx_rx_enabled = v;
|
||||
field_rxfrequency.hidden(v);
|
||||
set_dirty();
|
||||
receiver_model.set_target_frequency(bool_same_F_tx_rx_enabled ? tx_frequency : rx_frequency); // To go to the proper tuned freq. when toggling it
|
||||
};
|
||||
field_rxvga.set_value(rx_vga);
|
||||
check_common_freq_tx_rx.set_value(bool_same_F_tx_rx_enabled);
|
||||
|
||||
rx_amp = receiver_model.rf_amp();
|
||||
field_rxamp.on_change = [this](int32_t v) {
|
||||
rx_amp = v;
|
||||
if (rx_enabled)
|
||||
receiver_model.set_rf_amp(rx_amp);
|
||||
field_bw.on_change = [this](uint32_t v) {
|
||||
transmitter_model.set_channel_bandwidth(v * 1000);
|
||||
};
|
||||
field_rxamp.set_value(rx_amp);
|
||||
field_bw.set_value(transmitter_model.channel_bandwidth() / 1000); // pre-default first time, TX deviation FM for NFM / FM
|
||||
|
||||
// now, no need direct update, field_rfgain, field_rfamp (it is done in ui_transmitter.cpp)
|
||||
|
||||
options_mode.on_change = [this](size_t, int32_t v) {
|
||||
mic_mod_index = v;
|
||||
|
||||
// update bw & tone key visibility - only visible in NFM/WFM modes
|
||||
if ((mic_mod_index == MIC_MOD_NFM) || (mic_mod_index == MIC_MOD_WFM)) {
|
||||
field_bw.hidden(false);
|
||||
options_tone_key.hidden(false);
|
||||
} else {
|
||||
field_bw.hidden(true);
|
||||
options_tone_key.set_selected_index(0);
|
||||
options_tone_key.hidden(true);
|
||||
}
|
||||
|
||||
// update rogerbeep visibility - disable in USB/LSB modes
|
||||
if ((mic_mod_index == MIC_MOD_USB) || (mic_mod_index == MIC_MOD_LSB)) {
|
||||
check_rogerbeep.set_value(false);
|
||||
check_rogerbeep.hidden(true);
|
||||
} else {
|
||||
check_rogerbeep.hidden(false);
|
||||
}
|
||||
|
||||
// update squelch visibility - only visible in NFM mode
|
||||
field_squelch.hidden(mic_mod_index != MIC_MOD_NFM);
|
||||
|
||||
// update bandwidth
|
||||
set_rxbw_options();
|
||||
set_rxbw_defaults(false);
|
||||
field_rxbw.hidden(false);
|
||||
set_dirty(); // Refresh display
|
||||
|
||||
rxaudio(rx_enabled); // Update now if we have RX audio on
|
||||
// configure_baseband();
|
||||
};
|
||||
options_mode.set_selected_index(mic_mod_index);
|
||||
|
||||
check_mic_to_HP.on_select = [this](Checkbox&, bool v) {
|
||||
mic_to_HP_enabled = v;
|
||||
if (mic_to_HP_enabled) { // When user click to "hear mic to HP", we will select the higher acoustic sound Option MODE ALC or BOOST-
|
||||
audio::input::loopback_mic_to_hp_enable();
|
||||
if (audio::debug::codec_name() == "WM8731") {
|
||||
options_wm8731_boost_mode.set_selected_index(0); // In WM we always go to Boost +12 dB’s respect reference level
|
||||
} else if (ak4951_alc_and_wm8731_boost_GUI == 0) // In AK we are changing only that ALC index =0, because in that option, there is no sound.
|
||||
options_ak4951_alc_mode.set_selected_index(1); // alc_mode =0 , means no ALC,no DIGITAL filter block (by passed), and that mode has no loopback mode.
|
||||
} else {
|
||||
audio::input::loopback_mic_to_hp_disable();
|
||||
}
|
||||
};
|
||||
check_mic_to_HP.set_value(mic_to_HP_enabled);
|
||||
|
||||
check_rxactive.on_select = [this](Checkbox&, bool v) {
|
||||
// vumeter.set_value(0); //Start with a clean vumeter
|
||||
rx_enabled = v;
|
||||
check_mic_to_HP.hidden(rx_enabled); // Toggle Hide / show "Hear Mic" checkbox depending if we activate or not the receiver. (if RX on => no visible "Mic Hear" option)
|
||||
if ((rx_enabled) && (transmitting))
|
||||
check_mic_to_HP.set_value(transmitting); // Once we activate the "Rx audio" in reception time we disable "Hear Mic", but we allow it again in TX time.
|
||||
|
||||
if (rx_enabled)
|
||||
check_va.set_value(false); // Disallow voice activation during RX audio (for now) - Future TODO: Should allow VOX during RX audio
|
||||
|
||||
rxaudio(v); // Activate-Deactivate audio RX (receiver) accordingly
|
||||
set_dirty(); // Refresh interface
|
||||
};
|
||||
check_rxactive.set_value(rx_enabled);
|
||||
|
||||
tx_button.on_select = [this](Button&) {
|
||||
if (ptt_enabled && !transmitting) {
|
||||
if (!transmitting) {
|
||||
set_tx(true);
|
||||
}
|
||||
};
|
||||
|
||||
tx_button.on_touch_release = [this](Button&) {
|
||||
if (button_touch) {
|
||||
button_touch = false;
|
||||
set_tx(false);
|
||||
}
|
||||
button_touch = false;
|
||||
};
|
||||
|
||||
tx_button.on_touch_press = [this](Button&) {
|
||||
if (!transmitting) {
|
||||
button_touch = true;
|
||||
}
|
||||
button_touch = true;
|
||||
};
|
||||
|
||||
field_va_level.on_change = [this](int32_t v) {
|
||||
va_level = v;
|
||||
vumeter.set_mark(v);
|
||||
};
|
||||
field_va_level.set_value(va_level);
|
||||
|
||||
field_va_attack.set_value(attack_ms);
|
||||
field_va_attack.on_change = [this](int32_t v) {
|
||||
attack_ms = v;
|
||||
};
|
||||
|
||||
field_va_decay.set_value(decay_ms);
|
||||
field_va_decay.on_change = [this](int32_t v) {
|
||||
decay_ms = v;
|
||||
};
|
||||
|
||||
check_va.on_select = [this](Checkbox&, bool v) {
|
||||
va_enabled = v;
|
||||
if (va_enabled)
|
||||
check_rxactive.set_value(false); // Disallow RX-audio in VOX mode (for now) - Future TODO: Should allow VOX during RX audio
|
||||
};
|
||||
check_va.set_value(va_enabled);
|
||||
|
||||
// These shouldn't be necessary, but because
|
||||
// this app uses both transmitter_model and directly
|
||||
// configures the baseband, these end up being required.
|
||||
@@ -602,7 +572,12 @@ MicTXView::MicTXView(
|
||||
set_tx(false);
|
||||
|
||||
audio::set_rate(audio::Rate::Hz_24000);
|
||||
audio::input::start(ak4951_alc_and_wm8731_boost_GUI); // When detected AK4951 => set up ALC mode; when detected WM8731 => set up mic_boost ON/OFF.
|
||||
audio::input::start(ak4951_alc_and_wm8731_boost_GUI, mic_to_HP_enabled); // set up ALC mode (AK4951) or set up mic_boost ON/OFF (WM8731). and the check box "Hear Mic"
|
||||
|
||||
// Workaround for bad RX reception when app first started -- shouldn't be necessary:
|
||||
// Trigger receiver to update modulation.
|
||||
if (rx_enabled)
|
||||
receiver_model.set_squelch_level(receiver_model.squelch_level());
|
||||
}
|
||||
|
||||
MicTXView::MicTXView(
|
||||
@@ -611,16 +586,15 @@ MicTXView::MicTXView(
|
||||
: MicTXView(nav) {
|
||||
// Try to use the modulation/bandwidth from RX settings.
|
||||
// TODO: These concepts should be merged so there's only one.
|
||||
// TODO: enums/constants for these indexes.
|
||||
switch (override.mode) {
|
||||
case ReceiverModel::Mode::AMAudio:
|
||||
options_mode.set_selected_index(2);
|
||||
options_mode.set_selected_index(MIC_MOD_AM);
|
||||
break;
|
||||
case ReceiverModel::Mode::NarrowbandFMAudio:
|
||||
options_mode.set_selected_index(0);
|
||||
options_mode.set_selected_index(MIC_MOD_NFM);
|
||||
break;
|
||||
case ReceiverModel::Mode::WidebandFMAudio:
|
||||
options_mode.set_selected_index(1);
|
||||
options_mode.set_selected_index(MIC_MOD_WFM);
|
||||
break;
|
||||
|
||||
// Unsupported modulations.
|
||||
@@ -630,6 +604,8 @@ MicTXView::MicTXView(
|
||||
break;
|
||||
}
|
||||
|
||||
check_common_freq_tx_rx.set_value(true); // freq passed from other app is in tx_frequency, so set rx_frequency=tx_frequency
|
||||
|
||||
// TODO: bandwidth selection is tied too tightly to the UI
|
||||
// controls. It's not possible to set the bandwidth here without
|
||||
// refactoring. Also options_mode seems to have a category error.
|
||||
@@ -637,10 +613,12 @@ MicTXView::MicTXView(
|
||||
|
||||
MicTXView::~MicTXView() {
|
||||
audio::input::stop();
|
||||
transmitter_model.set_target_frequency(tx_frequency);
|
||||
transmitter_model.disable();
|
||||
if (rx_enabled) // Also turn off audio rx if enabled
|
||||
if (rx_enabled) { // Also turn off both (audio rx if enabled, and disable mic_loop to HP)
|
||||
rxaudio(false);
|
||||
audio::input::loopback_mic_to_hp_disable(); // Leave Mic audio off in the main menu (as original audio path, otherwise we had No audio in next "Audio App")
|
||||
}
|
||||
transmitter_model.set_target_frequency(tx_frequency); // Set tx_frequency here to be saved in app_settings (might not match rx_frequency)
|
||||
transmitter_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
|
||||
@@ -70,67 +70,85 @@ class MicTXView : public View {
|
||||
static constexpr uint32_t lcd_frame_duration = (256 * 1000UL) / 60; // 1 frame @ 60fps in ms .8 fixed point /60
|
||||
|
||||
void update_vumeter();
|
||||
bool tx_button_held();
|
||||
void do_timing();
|
||||
void set_tx(bool enable);
|
||||
// void on_target_frequency_changed(rf::Frequency f);
|
||||
void on_tx_progress(const bool done);
|
||||
void update_tx_icon();
|
||||
uint8_t shift_bits(void);
|
||||
void configure_baseband();
|
||||
|
||||
void set_rxbw_options(void);
|
||||
void set_rxbw_defaults(bool use_app_settings);
|
||||
void update_receiver_rxbw(void);
|
||||
void rxaudio(bool is_on);
|
||||
|
||||
void set_ptt_visibility(bool v);
|
||||
|
||||
RxRadioState rx_radio_state_{};
|
||||
TxRadioState tx_radio_state_{
|
||||
0 /* frequency */,
|
||||
1750000 /* bandwidth */,
|
||||
sampling_rate /* sampling rate */
|
||||
};
|
||||
app_settings::SettingsManager settings_{
|
||||
"tx_mic", app_settings::Mode::RX_TX,
|
||||
app_settings::Options::UseGlobalTargetFrequency};
|
||||
|
||||
bool transmitting{false};
|
||||
enum Mic_Modulation : uint32_t {
|
||||
MIC_MOD_NFM = 0,
|
||||
MIC_MOD_WFM = 1,
|
||||
MIC_MOD_AM = 2,
|
||||
MIC_MOD_USB = 3,
|
||||
MIC_MOD_LSB = 4,
|
||||
MIC_MOD_DSB = 5,
|
||||
};
|
||||
|
||||
// Settings
|
||||
uint32_t mic_mod_index{0};
|
||||
uint32_t rxbw_index{0};
|
||||
bool va_enabled{false};
|
||||
bool ptt_enabled{true};
|
||||
bool rogerbeep_enabled{false};
|
||||
bool mic_to_HP_enabled{false};
|
||||
bool bool_same_F_tx_rx_enabled{false};
|
||||
rf::Frequency rx_frequency{0};
|
||||
bool rx_enabled{false};
|
||||
uint32_t tone_key_index{};
|
||||
float mic_gain{1.0};
|
||||
uint32_t tone_key_index{0};
|
||||
uint32_t mic_gain_x10{1};
|
||||
uint8_t ak4951_alc_and_wm8731_boost_GUI{0};
|
||||
uint32_t va_level{40};
|
||||
uint32_t attack_ms{500};
|
||||
uint32_t decay_ms{1000};
|
||||
app_settings::SettingsManager settings_{
|
||||
"tx_mic",
|
||||
app_settings::Mode::RX_TX,
|
||||
app_settings::Options::UseGlobalTargetFrequency,
|
||||
{
|
||||
{"mic_mod_index"sv, &mic_mod_index},
|
||||
{"rxbw_index"sv, &rxbw_index},
|
||||
{"same_F_tx_rx"sv, &bool_same_F_tx_rx_enabled},
|
||||
{"mic_rx_frequency"sv, &rx_frequency},
|
||||
{"rx_enabled"sv, &rx_enabled},
|
||||
{"mic_gain_x10"sv, &mic_gain_x10},
|
||||
{"mic_to_HP"sv, &mic_to_HP_enabled},
|
||||
{"alc_and_boost"sv, &ak4951_alc_and_wm8731_boost_GUI},
|
||||
{"va_level"sv, &va_level},
|
||||
{"attack_ms"sv, &attack_ms},
|
||||
{"decay_ms"sv, &decay_ms},
|
||||
{"vox"sv, &va_enabled},
|
||||
{"rogerbeep"sv, &rogerbeep_enabled},
|
||||
{"tone_key_index"sv, &tone_key_index},
|
||||
}};
|
||||
|
||||
rf::Frequency tx_frequency{0};
|
||||
bool transmitting{false};
|
||||
uint32_t audio_level{0};
|
||||
uint32_t va_level{};
|
||||
uint32_t attack_ms{};
|
||||
uint32_t decay_ms{};
|
||||
uint32_t attack_timer{0};
|
||||
uint32_t decay_timer{0};
|
||||
int32_t tx_gain{47};
|
||||
bool rf_amp{false};
|
||||
int32_t rx_lna{32};
|
||||
int32_t rx_vga{32};
|
||||
bool rx_amp{false};
|
||||
rf::Frequency tx_frequency{0};
|
||||
rf::Frequency rx_frequency{0};
|
||||
int32_t focused_ui{2};
|
||||
bool button_touch{false};
|
||||
uint8_t shift_bits_s16{4}; // shift bits factor to the captured ADC S16 audio sample.
|
||||
|
||||
// AM TX Stuff
|
||||
// TODO: Some of this stuff is mutually exclusive. Need a better representation.
|
||||
bool enable_am{false};
|
||||
bool enable_dsb{false};
|
||||
bool enable_usb{false};
|
||||
bool enable_lsb{false};
|
||||
bool enable_wfm{false}; // added to distinguish in the FM mode, RX BW : NFM (8K5, 11K), FM (16K), WFM(200K)
|
||||
|
||||
Labels labels_WM8731{
|
||||
Labels labels_both{
|
||||
{{3 * 8, 1 * 8}, "MIC-GAIN:", Color::light_grey()},
|
||||
{{17 * 8, 1 * 8}, "Boost", Color::light_grey()},
|
||||
{{3 * 8, 3 * 8}, "F:", Color::light_grey()},
|
||||
{{15 * 8, 3 * 8}, "FM TXBW: kHz", Color::light_grey()}, // to be more symetric and consistent to the below FM RXBW
|
||||
{{18 * 8, (5 * 8)}, "Mode:", Color::light_grey()}, // now, no need to handle GAIN, Amp here It is handled by ui_transmitter.cpp
|
||||
{{3 * 8, 8 * 8}, "TX Activation:", Color::light_grey()}, // we delete { { 3 * 8, 5 * 8 }, "GAIN:", Color::light_grey() },
|
||||
{{4 * 8, 10 * 8}, "LVL:", Color::light_grey()}, // we delete { {11 * 8, 5 * 8 }, "Amp:", Color::light_grey() },
|
||||
{{12 * 8, 10 * 8}, "ATT:", Color::light_grey()},
|
||||
{{20 * 8, 10 * 8}, "DEC:", Color::light_grey()},
|
||||
@@ -143,25 +161,10 @@ class MicTXView : public View {
|
||||
{{5 * 8, (27 * 8) + 2}, "LNA:", Color::light_grey()},
|
||||
{{12 * 8, (27 * 8) + 2}, "VGA:", Color::light_grey()},
|
||||
{{19 * 8, (27 * 8) + 2}, "AMP:", Color::light_grey()}};
|
||||
Labels labels_WM8731{
|
||||
{{17 * 8, 1 * 8}, "Boost", Color::light_grey()}};
|
||||
Labels labels_AK4951{
|
||||
{{3 * 8, 1 * 8}, "MIC-GAIN:", Color::light_grey()},
|
||||
{{17 * 8, 1 * 8}, "ALC", Color::light_grey()},
|
||||
{{3 * 8, 3 * 8}, "F:", Color::light_grey()},
|
||||
{{15 * 8, 3 * 8}, "FM TXBW: kHz", Color::light_grey()},
|
||||
{{18 * 8, (5 * 8)}, "Mode:", Color::light_grey()}, // now, no need to handle GAIN, Amp here It is handled by ui_transmitter.cpp
|
||||
{{3 * 8, 8 * 8}, "TX Activation:", Color::light_grey()}, // we delete { { 3 * 8, 5 * 8 }, "GAIN:", Color::light_grey() },
|
||||
{{4 * 8, 10 * 8}, "LVL:", Color::light_grey()}, // we delete { {11 * 8, 5 * 8 }, "Amp:", Color::light_grey() },
|
||||
{{12 * 8, 10 * 8}, "ATT:", Color::light_grey()},
|
||||
{{20 * 8, 10 * 8}, "DEC:", Color::light_grey()},
|
||||
{{3 * 8, (13 * 8) - 5}, "TONE KEY:", Color::light_grey()},
|
||||
{{3 * 8, (18 * 8) - 1}, "======== Receiver ========", Color::green()},
|
||||
{{(5 * 8), (23 * 8) + 2}, "VOL:", Color::light_grey()},
|
||||
{{14 * 8, (23 * 8) + 2}, "RXBW:", Color::light_grey()}, // we remove the label "FM" because we will display all MOD types RX_BW.
|
||||
{{20 * 8, (25 * 8) + 2}, "SQ:", Color::light_grey()},
|
||||
{{5 * 8, (25 * 8) + 2}, "F_RX:", Color::light_grey()},
|
||||
{{5 * 8, (27 * 8) + 2}, "LNA:", Color::light_grey()},
|
||||
{{12 * 8, (27 * 8) + 2}, "VGA:", Color::light_grey()},
|
||||
{{19 * 8, (27 * 8) + 2}, "AMP:", Color::light_grey()}};
|
||||
{{17 * 8, 1 * 8}, "ALC", Color::light_grey()}};
|
||||
|
||||
VuMeter vumeter{
|
||||
{0 * 8, 1 * 8, 2 * 8, 33 * 8},
|
||||
@@ -224,28 +227,19 @@ class MicTXView : public View {
|
||||
{24 * 8, 5 * 8},
|
||||
6,
|
||||
{
|
||||
{"NFM/FM", 0},
|
||||
{" WFM ", 1},
|
||||
{" AM ", 2}, // in fact that TX mode = AM -DSB with carrier.
|
||||
{" USB ", 3},
|
||||
{" LSB ", 4},
|
||||
{"DSB-SC", 5} // We are TX Double Side AM Band with suppressed carrier, and allowing in RX both indep SSB lateral band (USB/LSB).
|
||||
{"NFM/FM", MIC_MOD_NFM},
|
||||
{" WFM ", MIC_MOD_WFM},
|
||||
{" AM ", MIC_MOD_AM}, // in fact that TX mode = AM -DSB with carrier.
|
||||
{" USB ", MIC_MOD_USB},
|
||||
{" LSB ", MIC_MOD_LSB},
|
||||
{"DSB-SC", MIC_MOD_DSB} // We are TX Double Side AM Band with suppressed carrier, and allowing in RX both indep SSB lateral band (USB/LSB).
|
||||
}};
|
||||
/*
|
||||
Checkbox check_va {
|
||||
{ 3 * 8, (10 * 8) - 4 },
|
||||
7,
|
||||
"Voice activation",
|
||||
false
|
||||
};
|
||||
*/
|
||||
|
||||
OptionsField field_va{
|
||||
{17 * 8, 8 * 8},
|
||||
4,
|
||||
{{" OFF", 0},
|
||||
{" PTT", 1},
|
||||
{"AUTO", 2}}};
|
||||
Checkbox check_va{
|
||||
{3 * 8, 8 * 7},
|
||||
10,
|
||||
"VOX enable",
|
||||
false};
|
||||
|
||||
NumberField field_va_level{
|
||||
{8 * 8, 10 * 8},
|
||||
@@ -277,6 +271,12 @@ class MicTXView : public View {
|
||||
"Roger beep",
|
||||
false};
|
||||
|
||||
Checkbox check_mic_to_HP{
|
||||
{18 * 8, (14 * 8) + 4},
|
||||
10,
|
||||
"Hear Mic",
|
||||
false};
|
||||
|
||||
Checkbox check_rxactive{
|
||||
{3 * 8, (21 * 8) - 7},
|
||||
8, // it was 18, but if it is string size should be 8
|
||||
@@ -286,7 +286,7 @@ class MicTXView : public View {
|
||||
Checkbox check_common_freq_tx_rx{
|
||||
{18 * 8, (21 * 8) - 7},
|
||||
8,
|
||||
"F TX=RX",
|
||||
"F RX=TX",
|
||||
false};
|
||||
|
||||
AudioVolumeField field_volume{
|
||||
@@ -340,9 +340,15 @@ class MicTXView : public View {
|
||||
|
||||
Button tx_button{
|
||||
{10 * 8, 30 * 8, 10 * 8, 5 * 8},
|
||||
"TX",
|
||||
"PTT TX",
|
||||
true};
|
||||
|
||||
Image tx_icon{
|
||||
{6 * 8, 31 * 8 + 4, 16, 16},
|
||||
&bitmap_icon_microphone,
|
||||
Color::black(),
|
||||
Color::black()};
|
||||
|
||||
MessageHandlerRegistration message_handler_lcd_sync{
|
||||
Message::ID::DisplayFrameSync,
|
||||
[this](const Message* const) {
|
||||
|
||||
@@ -56,13 +56,23 @@ static msg_t ookthread_fn(void* arg) {
|
||||
v = (symbol < 2) ? 1 : 0; // TX on for dot or dash, off for pause
|
||||
delay = morse_symbols[symbol];
|
||||
|
||||
gpio_og_tx.write(v);
|
||||
if (hackrf_r9) { // r9 has a common PIN 54 for MODE CONTROL TX / RX, <=r6 has two independent MODE CONTROL pins (TX and RX)
|
||||
gpio_r9_rx.write(!v); // in hackrf r9 opposite logic "0" means tx , "1" rx = no tx)
|
||||
} else {
|
||||
gpio_og_tx.write(v); // in hackrf <=r6, "1" means tx , "0" no tx.
|
||||
}
|
||||
|
||||
arg_c->on_tx_progress(i, false);
|
||||
|
||||
chThdSleepMilliseconds(delay * arg_c->time_unit_ms);
|
||||
}
|
||||
|
||||
gpio_og_tx.write(0); // Ensure TX is off
|
||||
if (hackrf_r9) { // r9 has a common PIN 54 for MODE CONTROL TX / RX, <=r6 has two independent MODE CONTROL pins (TX and RX)
|
||||
gpio_r9_rx.write(1); // Hackrf_r9 , common pin, rx="1" (we ensure TX is off) / tx="0"
|
||||
} else {
|
||||
gpio_og_tx.write(0); // Hackrf <=r6 independent TX / RX pins ,now touching the tx pin ==> Ensure TX is off
|
||||
}
|
||||
|
||||
arg_c->on_tx_progress(0, true);
|
||||
chThdExit(0);
|
||||
|
||||
@@ -121,9 +131,9 @@ bool MorseView::start_tx() {
|
||||
transmitter_model.set_baseband_bandwidth(1'750'000); // Min TX LPF .already tested in FM morse max tone 9,999k , max dev 150khz
|
||||
transmitter_model.enable();
|
||||
|
||||
if (modulation == CW) {
|
||||
if (mode_cw) {
|
||||
ookthread = chThdCreateStatic(ookthread_wa, sizeof(ookthread_wa), NORMALPRIO + 10, ookthread_fn, this);
|
||||
} else if (modulation == FM) {
|
||||
} else {
|
||||
baseband::set_tones_config(transmitter_model.channel_bandwidth(), 0, symbol_count, false, false);
|
||||
}
|
||||
|
||||
@@ -133,8 +143,8 @@ bool MorseView::start_tx() {
|
||||
void MorseView::update_tx_duration() {
|
||||
uint32_t duration_ms;
|
||||
|
||||
time_unit_ms = 1200 / field_speed.value();
|
||||
symbol_count = morse_encode(message, time_unit_ms, field_tone.value(), &time_units);
|
||||
time_unit_ms = 1200 / speed;
|
||||
symbol_count = morse_encode(message, time_unit_ms, tone, &time_units);
|
||||
|
||||
if (symbol_count) {
|
||||
duration_ms = time_units * time_unit_ms;
|
||||
@@ -175,7 +185,7 @@ void MorseView::on_loop_progress(const uint32_t progress, const bool done) {
|
||||
}
|
||||
|
||||
void MorseView::set_foxhunt(size_t i) {
|
||||
message = foxhunt_codes[i];
|
||||
message = foxhunt_codes[i - 1];
|
||||
buffer = message.c_str();
|
||||
text_message.set(message);
|
||||
update_tx_duration();
|
||||
@@ -188,7 +198,7 @@ MorseView::MorseView(
|
||||
|
||||
add_children({&labels,
|
||||
&checkbox_foxhunt,
|
||||
&options_foxhunt,
|
||||
&field_foxhunt,
|
||||
&field_speed,
|
||||
&field_tone,
|
||||
&options_modulation,
|
||||
@@ -200,36 +210,46 @@ MorseView::MorseView(
|
||||
&tx_view});
|
||||
|
||||
// Default settings
|
||||
field_speed.set_value(15); // 15wps
|
||||
field_tone.set_value(700); // 700Hz FM tone
|
||||
options_modulation.set_selected_index(0); // CW mode
|
||||
options_loop.set_selected_index(0); // Off
|
||||
field_speed.set_value(speed);
|
||||
field_tone.set_value(tone);
|
||||
options_modulation.set_by_value(mode_cw);
|
||||
options_loop.set_by_value(loop);
|
||||
checkbox_foxhunt.set_value(foxhunt_mode);
|
||||
field_foxhunt.set_value(foxhunt_code);
|
||||
|
||||
checkbox_foxhunt.on_select = [this](Checkbox&, bool value) {
|
||||
foxhunt_mode = value;
|
||||
|
||||
if (foxhunt_mode)
|
||||
set_foxhunt(options_foxhunt.selected_index_value());
|
||||
set_foxhunt(foxhunt_code);
|
||||
};
|
||||
|
||||
options_foxhunt.on_change = [this](size_t i, int32_t) {
|
||||
field_foxhunt.on_change = [this](int32_t value) {
|
||||
foxhunt_code = value;
|
||||
|
||||
if (foxhunt_mode)
|
||||
set_foxhunt(i);
|
||||
set_foxhunt(foxhunt_code);
|
||||
};
|
||||
|
||||
options_modulation.on_change = [this](size_t i, int32_t) {
|
||||
modulation = (modulation_t)i;
|
||||
};
|
||||
|
||||
options_loop.on_change = [this](size_t i, uint32_t n) {
|
||||
options_modulation.on_change = [this](size_t i, int32_t value) {
|
||||
(void)i; // avoid unused warning
|
||||
loop = n;
|
||||
mode_cw = (bool)value;
|
||||
};
|
||||
|
||||
field_speed.on_change = [this](int32_t) {
|
||||
options_loop.on_change = [this](size_t i, uint32_t value) {
|
||||
(void)i; // avoid unused warning
|
||||
loop = value;
|
||||
};
|
||||
|
||||
field_speed.on_change = [this](int32_t value) {
|
||||
speed = value;
|
||||
update_tx_duration();
|
||||
};
|
||||
|
||||
field_tone.on_change = [this](int32_t value) {
|
||||
tone = value;
|
||||
};
|
||||
|
||||
button_message.on_select = [this, &nav](Button&) {
|
||||
this->on_set_text(nav);
|
||||
};
|
||||
|
||||
@@ -65,7 +65,6 @@ class MorseView : public View {
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
std::string buffer{"PORTAPACK"};
|
||||
std::string message{};
|
||||
uint32_t time_units{0};
|
||||
|
||||
@@ -74,14 +73,25 @@ class MorseView : public View {
|
||||
1750000 /* bandwidth */,
|
||||
1536000 /* sampling rate */
|
||||
};
|
||||
app_settings::SettingsManager settings_{
|
||||
"tx_morse", app_settings::Mode::TX};
|
||||
|
||||
enum modulation_t {
|
||||
CW = 0,
|
||||
FM = 1
|
||||
};
|
||||
modulation_t modulation{CW};
|
||||
std::string buffer{"PORTAPACK"};
|
||||
bool mode_cw{true};
|
||||
bool foxhunt_mode{false};
|
||||
uint32_t foxhunt_code{1};
|
||||
uint32_t speed{15};
|
||||
uint32_t tone{700};
|
||||
app_settings::SettingsManager settings_{
|
||||
"tx_morse",
|
||||
app_settings::Mode::TX,
|
||||
{
|
||||
{"message"sv, &buffer},
|
||||
{"foxhunt"sv, &foxhunt_mode},
|
||||
{"foxhunt_code"sv, &foxhunt_code},
|
||||
{"speed"sv, &speed},
|
||||
{"tone"sv, &tone},
|
||||
{"mode_cw"sv, &mode_cw},
|
||||
{"loop"sv, &loop},
|
||||
}};
|
||||
|
||||
bool start_tx();
|
||||
void update_tx_duration();
|
||||
@@ -90,7 +100,6 @@ class MorseView : public View {
|
||||
|
||||
Thread* ookthread{nullptr};
|
||||
Thread* loopthread{nullptr};
|
||||
bool foxhunt_mode{false};
|
||||
bool run{false};
|
||||
|
||||
Labels labels{
|
||||
@@ -104,20 +113,12 @@ class MorseView : public View {
|
||||
{4 * 8, 16},
|
||||
8,
|
||||
"Foxhunt:"};
|
||||
OptionsField options_foxhunt{
|
||||
NumberField field_foxhunt{
|
||||
{17 * 8, 16 + 4},
|
||||
7,
|
||||
{{"1 (MOE)", 0},
|
||||
{"2 (MOI)", 1},
|
||||
{"3 (MOS)", 2},
|
||||
{"4 (MOH)", 3},
|
||||
{"5 (MO5)", 4},
|
||||
{"6 (MON)", 5},
|
||||
{"7 (MOD)", 6},
|
||||
{"8 (MOB)", 7},
|
||||
{"9 (MO6)", 8},
|
||||
{"X (MO) ", 9},
|
||||
{"T (S) ", 10}}};
|
||||
2,
|
||||
{1, 11},
|
||||
1,
|
||||
' '};
|
||||
|
||||
NumberField field_speed{
|
||||
{10 * 8, 6 * 8},
|
||||
@@ -136,8 +137,8 @@ class MorseView : public View {
|
||||
OptionsField options_modulation{
|
||||
{15 * 8, 10 * 8},
|
||||
2,
|
||||
{{"CW", 0},
|
||||
{"FM", 1}}};
|
||||
{{"CW", true},
|
||||
{"FM", false}}};
|
||||
|
||||
OptionsField options_loop{
|
||||
{9 * 8, 12 * 8},
|
||||
|
||||
@@ -481,10 +481,9 @@ ReconView::ReconView(NavigationView& nav)
|
||||
|
||||
button_remove.on_select = [this](ButtonWithEncoder&) {
|
||||
handle_remove_current_item();
|
||||
|
||||
// TODO: refresh UI.
|
||||
timer = 0;
|
||||
freq_lock = 0;
|
||||
recon_redraw();
|
||||
};
|
||||
|
||||
button_remove.on_change = [this]() {
|
||||
@@ -567,7 +566,7 @@ ReconView::ReconView(NavigationView& nav)
|
||||
};
|
||||
|
||||
button_restart.on_select = [this](Button&) {
|
||||
frequency_file_load(true);
|
||||
frequency_file_load();
|
||||
if (frequency_list.size() > 0) {
|
||||
if (fwd) {
|
||||
button_dir.set_text("FW>");
|
||||
@@ -614,7 +613,7 @@ ReconView::ReconView(NavigationView& nav)
|
||||
button_scanner_mode.set_text("SCAN");
|
||||
button_remove.set_text("DELETE");
|
||||
}
|
||||
frequency_file_load(true);
|
||||
frequency_file_load();
|
||||
if (autostart) {
|
||||
recon_resume();
|
||||
} else {
|
||||
@@ -642,7 +641,7 @@ ReconView::ReconView(NavigationView& nav)
|
||||
load_persisted_settings();
|
||||
ui_settings.save();
|
||||
|
||||
frequency_file_load(false);
|
||||
frequency_file_load();
|
||||
freqlist_cleared_for_ui_action = false;
|
||||
|
||||
if (autostart) {
|
||||
@@ -709,7 +708,7 @@ ReconView::ReconView(NavigationView& nav)
|
||||
delete_file(freq_file_path);
|
||||
}
|
||||
|
||||
frequency_file_load(false); /* do not stop all at start */
|
||||
frequency_file_load(); /* do not stop all at start */
|
||||
if (autostart) {
|
||||
recon_resume();
|
||||
} else {
|
||||
@@ -718,7 +717,7 @@ ReconView::ReconView(NavigationView& nav)
|
||||
recon_redraw();
|
||||
}
|
||||
|
||||
void ReconView::frequency_file_load(bool) {
|
||||
void ReconView::frequency_file_load() {
|
||||
if (field_mode.selected_index_value() != SPEC_MODULATION)
|
||||
audio::output::stop();
|
||||
|
||||
@@ -784,7 +783,7 @@ void ReconView::on_statistics_update(const ChannelStatistics& statistics) {
|
||||
// hack to reload the list if it was cleared by going into CONFIG
|
||||
if (freqlist_cleared_for_ui_action) {
|
||||
if (!manual_mode) {
|
||||
frequency_file_load(false);
|
||||
frequency_file_load();
|
||||
}
|
||||
if (autostart && !user_pause) {
|
||||
recon_resume();
|
||||
@@ -1214,11 +1213,12 @@ void ReconView::handle_remove_current_item() {
|
||||
if (frequency_list.size() > 0) {
|
||||
current_index = clip<int32_t>(current_index, 0u, frequency_list.size() - 1);
|
||||
text_cycle.set_text(to_string_dec_uint(current_index + 1, 3));
|
||||
entry = current_entry();
|
||||
freq = entry.frequency_a;
|
||||
} else {
|
||||
current_index = 0;
|
||||
text_cycle.set_text(" ");
|
||||
}
|
||||
|
||||
update_description();
|
||||
}
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ class ReconView : public View {
|
||||
void show_max(bool refresh_display = false);
|
||||
void recon_pause();
|
||||
void recon_resume();
|
||||
void frequency_file_load(bool stop_all_before = false);
|
||||
void frequency_file_load();
|
||||
void on_statistics_update(const ChannelStatistics& statistics);
|
||||
void on_index_delta(int32_t v);
|
||||
void on_stepper_delta(int32_t v);
|
||||
@@ -154,8 +154,8 @@ class ReconView : public View {
|
||||
uint16_t last_nb_match{999};
|
||||
uint16_t last_freq_lock{999};
|
||||
size_t last_list_size{0};
|
||||
int8_t last_rssi_min{-127};
|
||||
int8_t last_rssi_med{-127};
|
||||
int8_t last_rssi_min{127};
|
||||
int8_t last_rssi_med{0};
|
||||
int8_t last_rssi_max{-127};
|
||||
int32_t last_index{-1};
|
||||
int64_t last_freq{0};
|
||||
|
||||
@@ -39,10 +39,10 @@ namespace ui {
|
||||
ReconSetupViewMain::ReconSetupViewMain(NavigationView& nav, Rect parent_rect, std::string input_file, std::string output_file)
|
||||
: View(parent_rect), _input_file{input_file}, _output_file{output_file} {
|
||||
hidden(true);
|
||||
add_children({&button_load_freqs,
|
||||
add_children({&button_input_file,
|
||||
&text_input_file,
|
||||
&button_save_freqs,
|
||||
&button_output_file,
|
||||
&button_choose_output_file,
|
||||
&button_choose_output_name,
|
||||
&checkbox_autosave_freqs,
|
||||
&checkbox_autostart_recon,
|
||||
&checkbox_clear_output});
|
||||
@@ -52,9 +52,9 @@ ReconSetupViewMain::ReconSetupViewMain(NavigationView& nav, Rect parent_rect, st
|
||||
checkbox_clear_output.set_value(persistent_memory::recon_clear_output());
|
||||
|
||||
text_input_file.set(_input_file);
|
||||
button_output_file.set_text(_output_file);
|
||||
button_choose_output_name.set_text(_output_file);
|
||||
|
||||
button_load_freqs.on_select = [this, &nav](Button&) {
|
||||
button_input_file.on_select = [this, &nav](Button&) {
|
||||
auto open_view = nav.push<FileLoadView>(".TXT");
|
||||
open_view->push_dir(freqman_dir);
|
||||
open_view->on_changed = [this, &nav](std::filesystem::path new_file_path) {
|
||||
@@ -67,25 +67,24 @@ ReconSetupViewMain::ReconSetupViewMain(NavigationView& nav, Rect parent_rect, st
|
||||
};
|
||||
};
|
||||
|
||||
button_save_freqs.on_select = [this, &nav](Button&) {
|
||||
button_choose_output_file.on_select = [this, &nav](Button&) {
|
||||
auto open_view = nav.push<FileLoadView>(".TXT");
|
||||
open_view->push_dir(freqman_dir);
|
||||
open_view->on_changed = [this, &nav](std::filesystem::path new_file_path) {
|
||||
if (new_file_path.native().find(freqman_dir.native()) == 0) {
|
||||
_output_file = new_file_path.stem().string();
|
||||
button_output_file.set_text(_output_file);
|
||||
button_choose_output_name.set_text(_output_file);
|
||||
} else {
|
||||
nav.display_modal("LOAD ERROR", "A valid file from\nFREQMAN directory is\nrequired.");
|
||||
nav.display_modal("SAVE ERROR", "A valid file from\nFREQMAN directory is\nrequired.");
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
button_output_file.on_select = [this, &nav](Button&) {
|
||||
text_prompt(nav, _output_file, 28,
|
||||
[this](std::string& buffer) {
|
||||
_output_file = std::move(buffer);
|
||||
button_output_file.set_text(_output_file);
|
||||
});
|
||||
button_choose_output_name.on_select = [this, &nav](Button&) {
|
||||
text_prompt(nav, _output_file, 28, [this](std::string& buffer) {
|
||||
_output_file = buffer;
|
||||
button_choose_output_name.set_text(_output_file);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
@@ -105,7 +104,7 @@ void ReconSetupViewMore::save() {
|
||||
};
|
||||
|
||||
void ReconSetupViewMain::focus() {
|
||||
button_load_freqs.focus();
|
||||
button_input_file.focus();
|
||||
}
|
||||
|
||||
ReconSetupViewMore::ReconSetupViewMore(NavigationView& nav, Rect parent_rect)
|
||||
|
||||
@@ -73,17 +73,18 @@ class ReconSetupViewMain : public View {
|
||||
std::string _input_file{"RECON"};
|
||||
std::string _output_file{"RECON_RESULTS"};
|
||||
|
||||
Button button_load_freqs{
|
||||
Button button_input_file{
|
||||
{1 * 8, 12, 18 * 8, 22},
|
||||
"select input file"};
|
||||
Text text_input_file{
|
||||
{1 * 8, 4 + 2 * 16, 18 * 8, 22},
|
||||
"RECON"};
|
||||
|
||||
Button button_save_freqs{
|
||||
Button button_choose_output_file{
|
||||
{1 * 8, 4 * 16 - 8, 18 * 8, 22},
|
||||
"select output file"};
|
||||
Button button_output_file{
|
||||
|
||||
Button button_choose_output_name{
|
||||
{1 * 8, 5 * 16 - 2, 18 * 8, 22},
|
||||
"RECON_RESULTS"};
|
||||
|
||||
|
||||
@@ -343,6 +343,38 @@ void SetUIView::focus() {
|
||||
button_save.focus();
|
||||
}
|
||||
|
||||
/* SetSDCardView *********************************************/
|
||||
|
||||
SetSDCardView::SetSDCardView(NavigationView& nav) {
|
||||
add_children({&labels,
|
||||
&checkbox_sdcard_speed,
|
||||
&button_test_sdcard_high_speed,
|
||||
&text_sdcard_test_status,
|
||||
&button_save,
|
||||
&button_cancel});
|
||||
|
||||
checkbox_sdcard_speed.set_value(pmem::config_sdcard_high_speed_io());
|
||||
|
||||
button_test_sdcard_high_speed.on_select = [&nav, this](Button&) {
|
||||
pmem::set_config_sdcard_high_speed_io(true, false);
|
||||
text_sdcard_test_status.set("!! HIGH SPEED MODE ON !!");
|
||||
};
|
||||
|
||||
button_save.on_select = [&nav, this](Button&) {
|
||||
pmem::set_config_sdcard_high_speed_io(checkbox_sdcard_speed.value(), true);
|
||||
send_system_refresh();
|
||||
nav.pop();
|
||||
};
|
||||
|
||||
button_cancel.on_select = [&nav, this](Button&) {
|
||||
nav.pop();
|
||||
};
|
||||
}
|
||||
|
||||
void SetSDCardView::focus() {
|
||||
button_save.focus();
|
||||
}
|
||||
|
||||
/* SetConverterSettingsView ******************************/
|
||||
|
||||
SetConverterSettingsView::SetConverterSettingsView(NavigationView& nav) {
|
||||
@@ -636,6 +668,7 @@ SettingsMenuView::SettingsMenuView(NavigationView& nav) {
|
||||
{"QR Code", ui::Color::dark_cyan(), &bitmap_icon_qr_code, [&nav]() { nav.push<SetQRCodeView>(); }},
|
||||
{"Radio", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [&nav]() { nav.push<SetRadioView>(); }},
|
||||
{"User Interface", ui::Color::dark_cyan(), &bitmap_icon_options_ui, [&nav]() { nav.push<SetUIView>(); }},
|
||||
{"SD Card", ui::Color::dark_cyan(), &bitmap_icon_sdcard, [&nav]() { nav.push<SetSDCardView>(); }},
|
||||
});
|
||||
set_max_rows(2); // allow wider buttons
|
||||
}
|
||||
|
||||
@@ -306,8 +306,43 @@ class SetUIView : public View {
|
||||
|
||||
Button button_cancel{
|
||||
{16 * 8, 16 * 16, 12 * 8, 32},
|
||||
"Cancel",
|
||||
};
|
||||
"Cancel"};
|
||||
};
|
||||
|
||||
class SetSDCardView : public View {
|
||||
public:
|
||||
SetSDCardView(NavigationView& nav);
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "SD Card"; };
|
||||
|
||||
private:
|
||||
Labels labels{
|
||||
// 01234567890123456789012345678
|
||||
{{1 * 8, 120 - 48}, " HIGH SPEED SDCARD IO ", Color::light_grey()},
|
||||
{{1 * 8, 120 - 32}, " May or may not work !! ", Color::light_grey()}};
|
||||
|
||||
Checkbox checkbox_sdcard_speed{
|
||||
{2 * 8, 120},
|
||||
20,
|
||||
"enable high speed IO"};
|
||||
|
||||
Button button_test_sdcard_high_speed{
|
||||
{2 * 8, 152, 27 * 8, 32},
|
||||
"TEST BUTTON (NO PMEM SAVE)"};
|
||||
|
||||
Text text_sdcard_test_status{
|
||||
{2 * 8, 198, 28 * 8, 16},
|
||||
""};
|
||||
|
||||
Button button_save{
|
||||
{2 * 8, 16 * 16, 12 * 8, 32},
|
||||
"Save"};
|
||||
|
||||
Button button_cancel{
|
||||
{16 * 8, 16 * 16, 12 * 8, 32},
|
||||
"Cancel"};
|
||||
};
|
||||
|
||||
class SetConverterSettingsView : public View {
|
||||
|
||||
@@ -87,10 +87,18 @@ SigGenView::SigGenView(
|
||||
&field_stop,
|
||||
&tx_view});
|
||||
|
||||
symfield_tone.hidden(1); // At first launch , by default we are in CW Shape has NO MOD , we are not using Tone modulation.
|
||||
symfield_tone.set_value(1000); // Default: 1000 Hz
|
||||
options_shape.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
text_shape.set(shape_strings[v]);
|
||||
if (auto_update)
|
||||
update_config();
|
||||
if ((v == 0) || (v == 6)) { // In Shapes Options (CW & Pseudo Random Noise) we are not using Tone modulation freq.
|
||||
symfield_tone.hidden(1);
|
||||
} else {
|
||||
symfield_tone.hidden(0);
|
||||
}
|
||||
set_dirty();
|
||||
};
|
||||
options_shape.set_selected_index(0);
|
||||
text_shape.set(shape_strings[0]);
|
||||
|
||||
@@ -58,15 +58,16 @@ class SigGenView : public View {
|
||||
app_settings::SettingsManager settings_{
|
||||
"tx_siggen", app_settings::Mode::TX};
|
||||
|
||||
const std::string shape_strings[7] = {
|
||||
"CW-just carrier",
|
||||
"Sine signal ",
|
||||
"Triangle signal",
|
||||
"Saw up signal ",
|
||||
"Saw down signal",
|
||||
"Square signal ",
|
||||
"Noise signal " // using 16 bits LFSR register, 16 order polynomial feedback.
|
||||
};
|
||||
const std::string shape_strings[9] = {
|
||||
"CW (No mod.) ",
|
||||
"Sine mod. FM",
|
||||
"Triangle mod.FM", // max 15 character text space.
|
||||
"Saw up mod. FM",
|
||||
"Saw down mod.FM",
|
||||
"Square mod. FM",
|
||||
"Pseudo Noise FM", // using 16 bits LFSR register, 16 order polynomial feedback.
|
||||
"BPSK 0,1,0,1...",
|
||||
"QPSK 00-01-10.."};
|
||||
|
||||
bool auto_update{false};
|
||||
|
||||
@@ -86,10 +87,12 @@ class SigGenView : public View {
|
||||
{&bitmap_sig_saw_up, 3},
|
||||
{&bitmap_sig_saw_down, 4},
|
||||
{&bitmap_sig_square, 5},
|
||||
{&bitmap_sig_noise, 6}}};
|
||||
{&bitmap_sig_noise, 6},
|
||||
{&bitmap_sig_noise, 7}, // Pending to add a correct BPSK icon.
|
||||
{&bitmap_sig_noise, 8}}}; // Pending to add a correct QPSK icon.
|
||||
|
||||
Text text_shape{
|
||||
{15 * 8, 4 + 10, 8 * 8, 16},
|
||||
{15 * 8, 4 + 10, 15 * 8, 16},
|
||||
""};
|
||||
|
||||
SymField symfield_tone{
|
||||
|
||||
@@ -72,6 +72,11 @@ void TextViewer::paint(Painter& painter) {
|
||||
paint_state_.redraw_text = false;
|
||||
}
|
||||
|
||||
if (paint_state_.redraw_marked) {
|
||||
paint_marked(painter);
|
||||
paint_state_.redraw_marked = false;
|
||||
}
|
||||
|
||||
paint_cursor(painter);
|
||||
}
|
||||
|
||||
@@ -118,8 +123,9 @@ bool TextViewer::on_encoder(EncoderEvent delta) {
|
||||
return updated;
|
||||
}
|
||||
|
||||
void TextViewer::redraw(bool redraw_text) {
|
||||
void TextViewer::redraw(bool redraw_text, bool redraw_marked) {
|
||||
paint_state_.redraw_text = redraw_text;
|
||||
paint_state_.redraw_marked = redraw_marked;
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
@@ -140,6 +146,27 @@ void TextViewer::cursor_end() {
|
||||
redraw();
|
||||
}
|
||||
|
||||
void TextViewer::cursor_set(uint16_t line, uint16_t col) {
|
||||
cursor_.line = line;
|
||||
cursor_.col = col;
|
||||
}
|
||||
|
||||
void TextViewer::cursor_mark_selected() {
|
||||
LineColPair newMarker = std::make_pair(cursor_.line, cursor_.col);
|
||||
auto it = std::find(pairedVector.begin(), pairedVector.end(), newMarker);
|
||||
|
||||
if (it != pairedVector.end()) {
|
||||
pairedVector.erase(it);
|
||||
} else {
|
||||
pairedVector.push_back(newMarker);
|
||||
}
|
||||
|
||||
// Mark pending change.
|
||||
cursor_.mark_change = false;
|
||||
|
||||
redraw();
|
||||
}
|
||||
|
||||
uint16_t TextViewer::line_length() {
|
||||
return file_->line_length(cursor_.line);
|
||||
}
|
||||
@@ -247,12 +274,52 @@ void TextViewer::paint_cursor(Painter& painter) {
|
||||
};
|
||||
|
||||
if (paint_state_.line != UINT32_MAX) // only XOR old cursor if it still appears on the screen
|
||||
xor_cursor(paint_state_.line, paint_state_.col);
|
||||
{
|
||||
// Only reset previous cursor if we aren't marking.
|
||||
if (paint_state_.mark_change) {
|
||||
xor_cursor(paint_state_.line, paint_state_.col);
|
||||
}
|
||||
}
|
||||
|
||||
xor_cursor(cursor_.line, cursor_.col);
|
||||
|
||||
paint_state_.line = cursor_.line;
|
||||
paint_state_.col = cursor_.col;
|
||||
paint_state_.mark_change = cursor_.mark_change;
|
||||
|
||||
// Reset marking and wait for new change.
|
||||
cursor_.mark_change = true;
|
||||
}
|
||||
|
||||
void TextViewer::paint_marked(Painter& painter) {
|
||||
auto xor_cursor = [this, &painter](int32_t line, uint16_t col) {
|
||||
int cursor_width = char_width + 1;
|
||||
int x = (col - paint_state_.first_col) * char_width - 1;
|
||||
if (x < 0) { // cursor is one pixel narrower when in left column
|
||||
cursor_width--;
|
||||
x = 0;
|
||||
}
|
||||
int y = screen_rect().top() + (line - paint_state_.first_line) * char_height;
|
||||
|
||||
// Converting one row at a time to reduce buffer size
|
||||
auto pbuf8 = cursor_.pixel_buffer8;
|
||||
auto pbuf = cursor_.pixel_buffer;
|
||||
for (auto col = 0; col < char_height; col++) {
|
||||
// Annoyingly, read_pixels uses a 24-bit pixel format vs draw_pixels which uses 16-bit
|
||||
portapack::display.read_pixels({x, y + col, cursor_width, 1}, pbuf8, cursor_width);
|
||||
for (auto i = 0; i < cursor_width; i++)
|
||||
pbuf[i] = Color(pbuf8[i].r, pbuf8[i].g, pbuf8[i].b).v ^ 0xFFFF;
|
||||
portapack::display.draw_pixels({x, y + col, cursor_width, 1}, pbuf, cursor_width);
|
||||
}
|
||||
};
|
||||
|
||||
auto it = pairedVector.begin();
|
||||
|
||||
while (it != pairedVector.end()) {
|
||||
LineColPair entry = (LineColPair)*it;
|
||||
xor_cursor(entry.first, entry.second);
|
||||
it++;
|
||||
}
|
||||
}
|
||||
|
||||
void TextViewer::reset_file(FileWrapper* file) {
|
||||
|
||||
@@ -59,7 +59,7 @@ class TextViewer : public Widget {
|
||||
bool on_key(KeyEvent key) override;
|
||||
bool on_encoder(EncoderEvent delta) override;
|
||||
|
||||
void redraw(bool redraw_text = false);
|
||||
void redraw(bool redraw_text = false, bool redraw_marked = false);
|
||||
|
||||
void set_file(FileWrapper& file) { reset_file(&file); }
|
||||
void clear_file() { reset_file(); }
|
||||
@@ -71,6 +71,11 @@ class TextViewer : public Widget {
|
||||
|
||||
void cursor_home();
|
||||
void cursor_end();
|
||||
void cursor_set(uint16_t line, uint16_t col);
|
||||
void cursor_mark_selected();
|
||||
|
||||
typedef std::pair<uint16_t, uint16_t> LineColPair;
|
||||
std::vector<LineColPair> pairedVector{};
|
||||
|
||||
// Gets the length of the current line.
|
||||
uint16_t line_length();
|
||||
@@ -97,6 +102,7 @@ class TextViewer : public Widget {
|
||||
|
||||
void paint_text(Painter& painter, uint32_t line, uint16_t col);
|
||||
void paint_cursor(Painter& painter);
|
||||
void paint_marked(Painter& painter);
|
||||
|
||||
void reset_file(FileWrapper* file = nullptr);
|
||||
|
||||
@@ -111,6 +117,8 @@ class TextViewer : public Widget {
|
||||
uint32_t first_line{};
|
||||
uint16_t first_col{};
|
||||
bool redraw_text{true};
|
||||
bool redraw_marked{false};
|
||||
bool mark_change{true};
|
||||
} paint_state_{};
|
||||
|
||||
struct {
|
||||
@@ -121,6 +129,8 @@ class TextViewer : public Widget {
|
||||
// Pixel buffer used for cursor XOR'ing - Max cursor width = Max char width + 1
|
||||
ColorRGB888 pixel_buffer8[ui::char_width + 1]{};
|
||||
Color pixel_buffer[ui::char_width + 1]{};
|
||||
|
||||
bool mark_change{true};
|
||||
} cursor_{};
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2017 Furrtek
|
||||
*
|
||||
* 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_weatherstation.hpp"
|
||||
#include "modems.hpp"
|
||||
#include "audio.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
using namespace ui;
|
||||
|
||||
namespace ui {
|
||||
|
||||
void WeatherRecentEntryDetailView::update_data() {
|
||||
// set text elements
|
||||
text_type.set(WeatherView::getWeatherSensorTypeName((FPROTO_WEATHER_SENSOR)entry_.sensorType));
|
||||
text_id.set("0x" + to_string_hex(entry_.id));
|
||||
text_temp.set(weather_units_fahr ? to_string_decimal((entry_.temp * 9 / 5) + 32, 1) + STR_DEGREES_F : to_string_decimal(entry_.temp, 2) + STR_DEGREES_C);
|
||||
text_hum.set(to_string_dec_uint(entry_.humidity) + "%");
|
||||
text_ch.set(to_string_dec_uint(entry_.channel));
|
||||
text_batt.set(to_string_dec_uint(entry_.battery_low) + " " + ((entry_.battery_low == 0) ? "OK" : "LOW"));
|
||||
text_age.set(to_string_dec_uint(entry_.age) + " sec");
|
||||
}
|
||||
|
||||
WeatherRecentEntryDetailView::WeatherRecentEntryDetailView(NavigationView& nav, const WeatherRecentEntry& entry)
|
||||
: nav_{nav},
|
||||
entry_{entry} {
|
||||
add_children({&button_done,
|
||||
&text_type,
|
||||
&text_id,
|
||||
&text_temp,
|
||||
&text_hum,
|
||||
&text_ch,
|
||||
&text_batt,
|
||||
&text_age,
|
||||
&labels});
|
||||
|
||||
button_done.on_select = [&nav](const ui::Button&) {
|
||||
nav.pop();
|
||||
};
|
||||
update_data();
|
||||
}
|
||||
|
||||
void WeatherRecentEntryDetailView::focus() {
|
||||
button_done.focus();
|
||||
}
|
||||
|
||||
void WeatherView::focus() {
|
||||
field_frequency.focus();
|
||||
}
|
||||
|
||||
WeatherView::WeatherView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
add_children({&rssi,
|
||||
&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&field_frequency,
|
||||
&options_temperature,
|
||||
&button_clear_list,
|
||||
&recent_entries_view});
|
||||
|
||||
baseband::run_image(portapack::spi_flash::image_tag_weather);
|
||||
|
||||
button_clear_list.on_select = [this](Button&) {
|
||||
recent.clear();
|
||||
recent_entries_view.set_dirty();
|
||||
};
|
||||
field_frequency.set_step(100000);
|
||||
|
||||
options_temperature.on_change = [this](size_t, int32_t i) {
|
||||
weather_units_fahr = (bool)i;
|
||||
recent_entries_view.set_dirty();
|
||||
};
|
||||
options_temperature.set_selected_index(weather_units_fahr, false);
|
||||
|
||||
const Rect content_rect{0, header_height, screen_width, screen_height - header_height};
|
||||
recent_entries_view.set_parent_rect(content_rect);
|
||||
recent_entries_view.on_select = [this](const WeatherRecentEntry& entry) {
|
||||
nav_.push<WeatherRecentEntryDetailView>(entry);
|
||||
};
|
||||
baseband::set_weather();
|
||||
receiver_model.enable();
|
||||
signal_token_tick_second = rtc_time::signal_tick_second += [this]() {
|
||||
on_tick_second();
|
||||
};
|
||||
}
|
||||
|
||||
void WeatherView::on_tick_second() {
|
||||
for (auto& entry : recent) {
|
||||
entry.inc_age(1);
|
||||
}
|
||||
recent_entries_view.set_dirty();
|
||||
}
|
||||
|
||||
void WeatherView::on_data(const WeatherDataMessage* data) {
|
||||
WeatherRecentEntry key{data->sensorType, data->id, data->temp, data->humidity, data->channel, data->battery_low};
|
||||
auto matching_recent = find(recent, key.key());
|
||||
if (matching_recent != std::end(recent)) {
|
||||
// Found within. Move to front of list, increment counter.
|
||||
(*matching_recent).reset_age();
|
||||
recent.push_front(*matching_recent);
|
||||
recent.erase(matching_recent);
|
||||
} else {
|
||||
recent.emplace_front(key);
|
||||
truncate_entries(recent, 64);
|
||||
}
|
||||
recent_entries_view.set_dirty();
|
||||
}
|
||||
|
||||
WeatherView::~WeatherView() {
|
||||
rtc_time::signal_tick_second -= signal_token_tick_second;
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
const char* WeatherView::getWeatherSensorTypeName(FPROTO_WEATHER_SENSOR type) {
|
||||
switch (type) {
|
||||
case FPW_NexusTH:
|
||||
return "NexusTH";
|
||||
case FPW_Acurite592TXR:
|
||||
return "Acurite592TXR";
|
||||
case FPW_Acurite606TX:
|
||||
return "Acurite606TX";
|
||||
case FPW_Acurite609TX:
|
||||
return "Acurite609TX";
|
||||
case FPW_Ambient:
|
||||
return "Ambient";
|
||||
case FPW_AuriolAhfl:
|
||||
return "AuriolAhfl";
|
||||
case FPW_AuriolTH:
|
||||
return "AuriolTH";
|
||||
case FPW_GTWT02:
|
||||
return "GT-WT02";
|
||||
case FPW_GTWT03:
|
||||
return "GT-WT03";
|
||||
case FPW_INFACTORY:
|
||||
return "InFactory";
|
||||
case FPW_LACROSSETX:
|
||||
return "LaCrosse TX";
|
||||
case FPW_LACROSSETX141thbv2:
|
||||
return "LaCrosse TX141THBv2";
|
||||
case FPW_OREGON2:
|
||||
return "Oregon2";
|
||||
case FPW_OREGON3:
|
||||
return "Oregon3";
|
||||
case FPW_OREGONv1:
|
||||
return "OregonV1";
|
||||
case FPW_THERMOPROTX4:
|
||||
return "ThermoPro TX4";
|
||||
case FPW_TX_8300:
|
||||
return "TX 8300";
|
||||
case FPW_WENDOX_W6726:
|
||||
return "Wendox W6726";
|
||||
case FPW_Acurite986:
|
||||
return "Acurite986";
|
||||
|
||||
case FPW_Invalid:
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
std::string WeatherView::pad_string_with_spaces(int snakes) {
|
||||
std::string paddedStr(snakes, ' ');
|
||||
return paddedStr;
|
||||
}
|
||||
|
||||
template <>
|
||||
void RecentEntriesTable<ui::WeatherRecentEntries>::draw(
|
||||
const Entry& entry,
|
||||
const Rect& target_rect,
|
||||
Painter& painter,
|
||||
const Style& style) {
|
||||
std::string line{};
|
||||
line.reserve(30);
|
||||
|
||||
line = WeatherView::getWeatherSensorTypeName((FPROTO_WEATHER_SENSOR)entry.sensorType);
|
||||
if (line.length() < 10) {
|
||||
line += WeatherView::pad_string_with_spaces(10 - line.length());
|
||||
} else {
|
||||
line = truncate(line, 10);
|
||||
}
|
||||
|
||||
std::string temp = (weather_units_fahr ? to_string_decimal((entry.temp * 9 / 5) + 32, 1) : to_string_decimal(entry.temp, 1));
|
||||
std::string humStr = to_string_dec_uint(entry.humidity) + "%";
|
||||
std::string chStr = to_string_dec_uint(entry.channel);
|
||||
std::string ageStr = to_string_dec_uint(entry.age);
|
||||
|
||||
line += WeatherView::pad_string_with_spaces(6 - temp.length()) + temp;
|
||||
line += WeatherView::pad_string_with_spaces(5 - humStr.length()) + humStr;
|
||||
line += WeatherView::pad_string_with_spaces(4 - chStr.length()) + chStr;
|
||||
line += WeatherView::pad_string_with_spaces(4 - ageStr.length()) + ageStr;
|
||||
|
||||
line.resize(target_rect.width() / 8, ' ');
|
||||
painter.draw_string(target_rect.location(), style, line);
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2017 Furrtek
|
||||
*
|
||||
* 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_WEATHER_H__
|
||||
#define __UI_WEATHER_H__
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "utility.hpp"
|
||||
#include "recent_entries.hpp"
|
||||
|
||||
#include "../baseband/fprotos/weathertypes.hpp"
|
||||
using namespace ui;
|
||||
|
||||
namespace ui {
|
||||
|
||||
static bool weather_units_fahr{false};
|
||||
|
||||
struct WeatherRecentEntry {
|
||||
using Key = uint64_t;
|
||||
static constexpr Key invalid_key = 0x0fffffff; // todo calc the invalid all
|
||||
uint8_t sensorType = FPW_Invalid;
|
||||
uint32_t id = 0xFFFFFFFF;
|
||||
float temp = -273.0f;
|
||||
uint8_t humidity = 0xFF;
|
||||
uint8_t battery_low = 0xFF;
|
||||
uint8_t channel = 0xFF;
|
||||
uint16_t age = 0; // updated on each seconds, show how long the signal was last seen
|
||||
|
||||
WeatherRecentEntry() {}
|
||||
WeatherRecentEntry(
|
||||
uint8_t sensorType,
|
||||
uint32_t id,
|
||||
float temp,
|
||||
uint8_t humidity,
|
||||
uint8_t channel,
|
||||
uint8_t battery_low = 0xff)
|
||||
: sensorType{sensorType},
|
||||
id{id},
|
||||
temp{temp},
|
||||
humidity{humidity},
|
||||
battery_low{battery_low},
|
||||
channel{channel} {
|
||||
}
|
||||
Key key() const {
|
||||
return (((static_cast<uint64_t>(temp * 10) & 0xFFFF) << 48) ^ static_cast<uint64_t>(id) << 24) |
|
||||
(static_cast<uint64_t>(sensorType) & 0xFF) << 16 |
|
||||
(static_cast<uint64_t>(humidity) & 0xFF) << 8 |
|
||||
(static_cast<uint64_t>(battery_low) & 0xF) << 4 |
|
||||
(static_cast<uint64_t>(channel) & 0xF);
|
||||
}
|
||||
void inc_age(int delta) {
|
||||
if (UINT16_MAX - delta > age) age += delta;
|
||||
}
|
||||
void reset_age() {
|
||||
age = 0;
|
||||
}
|
||||
};
|
||||
using WeatherRecentEntries = RecentEntries<WeatherRecentEntry>;
|
||||
using WeatherRecentEntriesView = RecentEntriesView<WeatherRecentEntries>;
|
||||
|
||||
class WeatherView : public View {
|
||||
public:
|
||||
WeatherView(NavigationView& nav);
|
||||
~WeatherView();
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "Weather"; };
|
||||
static const char* getWeatherSensorTypeName(FPROTO_WEATHER_SENSOR type);
|
||||
static std::string pad_string_with_spaces(int snakes);
|
||||
|
||||
private:
|
||||
void on_tick_second();
|
||||
void on_data(const WeatherDataMessage* data);
|
||||
|
||||
NavigationView& nav_;
|
||||
RxRadioState radio_state_{
|
||||
433'920'000 /* frequency */,
|
||||
1'750'000 /* bandwidth */,
|
||||
2'000'000 /* sampling rate */,
|
||||
ReceiverModel::Mode::AMAudio};
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_weather",
|
||||
app_settings::Mode::RX,
|
||||
{
|
||||
{"units_fahr"sv, &weather_units_fahr},
|
||||
}};
|
||||
|
||||
WeatherRecentEntries recent{};
|
||||
|
||||
OptionsField options_temperature{
|
||||
{10 * 8, 0 * 16},
|
||||
2,
|
||||
{{STR_DEGREES_C, 0},
|
||||
{STR_DEGREES_F, 1}}};
|
||||
|
||||
RFAmpField field_rf_amp{
|
||||
{13 * 8, 0 * 16}};
|
||||
LNAGainField field_lna{
|
||||
{15 * 8, 0 * 16}};
|
||||
VGAGainField field_vga{
|
||||
{18 * 8, 0 * 16}};
|
||||
RSSI rssi{
|
||||
{21 * 8, 0, 6 * 8, 4}};
|
||||
RxFrequencyField field_frequency{
|
||||
{0 * 8, 0 * 16},
|
||||
nav_};
|
||||
|
||||
SignalToken signal_token_tick_second{};
|
||||
|
||||
Button button_clear_list{
|
||||
{0, 16, 7 * 8, 32},
|
||||
"Clear"};
|
||||
|
||||
static constexpr auto header_height = 3 * 16;
|
||||
|
||||
const RecentEntriesColumns columns{{
|
||||
{"Type", 10},
|
||||
{"Temp", 5},
|
||||
{"Hum", 4},
|
||||
{"Ch", 3},
|
||||
{"Age", 3},
|
||||
}};
|
||||
WeatherRecentEntriesView recent_entries_view{columns, recent};
|
||||
|
||||
MessageHandlerRegistration message_handler_packet{
|
||||
Message::ID::WeatherData,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const WeatherDataMessage*>(p);
|
||||
this->on_data(message);
|
||||
}};
|
||||
};
|
||||
|
||||
class WeatherRecentEntryDetailView : public View {
|
||||
public:
|
||||
WeatherRecentEntryDetailView(NavigationView& nav, const WeatherRecentEntry& entry);
|
||||
|
||||
void update_data();
|
||||
void focus() override;
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
WeatherRecentEntry entry_{};
|
||||
Text text_type{{0 * 8, 1 * 16, 15 * 8, 16}, "?"};
|
||||
Text text_id{{6 * 8, 2 * 16, 10 * 8, 16}, "?"};
|
||||
Text text_temp{{6 * 8, 3 * 16, 8 * 8, 16}, "?"};
|
||||
Text text_hum{{11 * 8, 4 * 16, 6 * 8, 16}, "?"};
|
||||
Text text_ch{{11 * 8, 5 * 16, 6 * 8, 16}, "?"};
|
||||
Text text_batt{{11 * 8, 6 * 16, 6 * 8, 16}, "?"};
|
||||
Text text_age{{11 * 8, 7 * 16, 6 * 8, 16}, "?"};
|
||||
|
||||
Labels labels{
|
||||
{{0 * 8, 0 * 16}, "Weather station type:", Color::light_grey()},
|
||||
{{0 * 8, 2 * 16}, "Id: ", Color::light_grey()},
|
||||
{{0 * 8, 3 * 16}, "Temp:", Color::light_grey()},
|
||||
{{0 * 8, 4 * 16}, "Humidity:", Color::light_grey()},
|
||||
{{0 * 8, 5 * 16}, "Channel:", Color::light_grey()},
|
||||
{{0 * 8, 6 * 16}, "Battery:", Color::light_grey()},
|
||||
{{0 * 8, 7 * 16}, "Age:", Color::light_grey()},
|
||||
};
|
||||
|
||||
Button button_done{
|
||||
{screen_width - 96 - 4, screen_height - 32 - 12, 96, 32},
|
||||
"Done"};
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
|
||||
#endif /*__UI_WEATHER_H__*/
|
||||
@@ -198,8 +198,8 @@ void update_audio_mute() {
|
||||
|
||||
namespace input {
|
||||
|
||||
void start(int8_t alc_mode) {
|
||||
audio_codec->microphone_enable(alc_mode); // added user-GUI selection for AK4951, ALC mode parameter.
|
||||
void start(int8_t alc_mode, bool mic_to_HP_enabled) {
|
||||
audio_codec->microphone_enable(alc_mode, mic_to_HP_enabled); // added user-GUI selection for AK4951, ALC mode parameter. and the check box "Hear Mic"
|
||||
i2s::i2s0::rx_start();
|
||||
}
|
||||
|
||||
@@ -208,6 +208,14 @@ void stop() {
|
||||
audio_codec->microphone_disable();
|
||||
}
|
||||
|
||||
void loopback_mic_to_hp_enable() {
|
||||
audio_codec->microphone_to_HP_enable();
|
||||
}
|
||||
|
||||
void loopback_mic_to_hp_disable() {
|
||||
audio_codec->microphone_to_HP_disable();
|
||||
}
|
||||
|
||||
} /* namespace input */
|
||||
|
||||
namespace headphone {
|
||||
@@ -232,6 +240,10 @@ uint32_t reg_read(const size_t register_number) {
|
||||
return audio_codec->reg_read(register_number);
|
||||
}
|
||||
|
||||
void reg_write(const size_t register_number, uint32_t value) {
|
||||
audio_codec->reg_write(register_number, value);
|
||||
}
|
||||
|
||||
std::string codec_name() {
|
||||
return audio_codec->name();
|
||||
}
|
||||
|
||||
@@ -50,12 +50,16 @@ class Codec {
|
||||
virtual volume_range_t headphone_gain_range() const = 0;
|
||||
virtual void set_headphone_volume(const volume_t volume) = 0;
|
||||
|
||||
virtual void microphone_enable(int8_t alc_mode) = 0; // added user-GUI AK4951 ,selected ALC mode.
|
||||
virtual void microphone_enable(int8_t alc_mode, bool mic_to_HP_enabled) = 0; // added user-GUI AK4951 ,selected ALC mode.
|
||||
virtual void microphone_disable() = 0;
|
||||
|
||||
virtual void microphone_to_HP_enable() = 0;
|
||||
virtual void microphone_to_HP_disable() = 0;
|
||||
|
||||
virtual size_t reg_count() const = 0;
|
||||
virtual size_t reg_bits() const = 0;
|
||||
virtual uint32_t reg_read(const size_t register_number) = 0;
|
||||
virtual void reg_write(const size_t register_number, const uint32_t value) = 0;
|
||||
};
|
||||
|
||||
namespace output {
|
||||
@@ -76,9 +80,12 @@ void update_audio_mute();
|
||||
|
||||
namespace input {
|
||||
|
||||
void start(int8_t alc_mode); // added parameter user-GUI select AK4951-ALC mode for config mic path,(recording mode in datasheet),
|
||||
void start(int8_t alc_mode, bool mic_to_HP_enabled); // added parameters -GUI select AK4951-ALC mode for config mic path,(recording mode),and the check box "Hear Mic"
|
||||
void stop();
|
||||
|
||||
void loopback_mic_to_hp_enable();
|
||||
void loopback_mic_to_hp_disable();
|
||||
|
||||
} /* namespace input */
|
||||
|
||||
namespace headphone {
|
||||
@@ -101,6 +108,7 @@ namespace debug {
|
||||
|
||||
size_t reg_count();
|
||||
uint32_t reg_read(const size_t register_number);
|
||||
void reg_write(const size_t register_number, uint32_t value);
|
||||
std::string codec_name();
|
||||
size_t reg_bits();
|
||||
|
||||
|
||||
@@ -149,12 +149,18 @@ void set_aprs(const uint32_t baudrate) {
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
void set_btle(const uint32_t baudrate, const uint32_t word_length, const uint32_t trigger_value, const bool trigger_word) {
|
||||
void set_btlerx(uint8_t channel_number) {
|
||||
const BTLERxConfigureMessage message{
|
||||
baudrate,
|
||||
word_length,
|
||||
trigger_value,
|
||||
trigger_word};
|
||||
channel_number};
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
void set_btletx(uint8_t channel_number, char* macAddress, char* advertisementData, uint8_t pduType) {
|
||||
const BTLETxConfigureMessage message{
|
||||
channel_number,
|
||||
macAddress,
|
||||
advertisementData,
|
||||
pduType};
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
@@ -167,6 +173,17 @@ 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) {
|
||||
const FSKRxConfigureMessage message{
|
||||
taps_200k_decim_0,
|
||||
taps_16k0_decim_1,
|
||||
taps_11k0_channel,
|
||||
2,
|
||||
deviation};
|
||||
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
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) {
|
||||
const AFSKTxConfigureMessage message{
|
||||
afsk_samples_per_bit,
|
||||
@@ -261,8 +278,7 @@ void set_pocsag() {
|
||||
}
|
||||
|
||||
void set_adsb() {
|
||||
const ADSBConfigureMessage message{
|
||||
1};
|
||||
const ADSBConfigureMessage message{};
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
@@ -303,6 +319,11 @@ void set_spectrum_painter_config(const uint16_t width, const uint16_t height, bo
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
void set_weather() {
|
||||
const WeatherRxConfigureMessage message{};
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
static bool baseband_image_running = false;
|
||||
|
||||
void run_image(const spi_flash::image_tag_t image_tag) {
|
||||
|
||||
@@ -69,9 +69,11 @@ 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_aprs(const uint32_t baudrate);
|
||||
|
||||
void set_btle(const uint32_t baudrate, const uint32_t word_length, const uint32_t trigger_value, const bool trigger_word);
|
||||
void set_btlerx(uint8_t channel_number);
|
||||
void set_btletx(uint8_t channel_number, char* macAddress, char* advertisementData, uint8_t pduType);
|
||||
|
||||
void set_nrf(const uint32_t baudrate, const uint32_t word_length, const uint32_t trigger_value, const bool trigger_word);
|
||||
|
||||
@@ -86,6 +88,7 @@ void set_spectrum(const size_t sampling_rate, const size_t trigger);
|
||||
void set_siggen_tone(const uint32_t tone);
|
||||
void set_siggen_config(const uint32_t bw, const uint32_t shape, const uint32_t duration);
|
||||
void set_spectrum_painter_config(const uint16_t width, const uint16_t height, bool update, int32_t bw);
|
||||
void set_weather();
|
||||
void request_beep();
|
||||
|
||||
void run_image(const portapack::spi_flash::image_tag_t image_tag);
|
||||
|
||||
@@ -5683,6 +5683,44 @@ static constexpr Bitmap bitmap_icon_trim{
|
||||
{16, 16},
|
||||
bitmap_icon_trim_data};
|
||||
|
||||
static constexpr uint8_t bitmap_icon_hide_data[] = {
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x40,
|
||||
0x00,
|
||||
0x20,
|
||||
0xE0,
|
||||
0x17,
|
||||
0x18,
|
||||
0x18,
|
||||
0xC4,
|
||||
0x27,
|
||||
0x62,
|
||||
0x42,
|
||||
0x21,
|
||||
0x85,
|
||||
0xA1,
|
||||
0x84,
|
||||
0x62,
|
||||
0x46,
|
||||
0xA4,
|
||||
0x23,
|
||||
0x18,
|
||||
0x18,
|
||||
0xE8,
|
||||
0x07,
|
||||
0x04,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
};
|
||||
static constexpr Bitmap bitmap_icon_hide{
|
||||
{16, 16},
|
||||
bitmap_icon_hide_data};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
#endif /*__BITMAP_HPP__*/
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
*/
|
||||
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
|
||||
#ifndef __DE_BRUIJN_H__
|
||||
#define __DE_BRUIJN_H__
|
||||
|
||||
@@ -258,38 +258,43 @@ void draw_stack_dump() {
|
||||
}
|
||||
}
|
||||
|
||||
// Disk I/O in this function doesn't work after a fault
|
||||
// Using the stack while dumping the stack isn't ideal, but hopefully anything imporant is still on the call stack.
|
||||
bool stack_dump() {
|
||||
uint32_t num_words = &__process_stack_end__ - &__process_stack_base__;
|
||||
return memory_dump(&__process_stack_base__, num_words, true);
|
||||
}
|
||||
|
||||
bool memory_dump(uint32_t* addr_start, uint32_t num_words, bool stack_flag) {
|
||||
Painter painter;
|
||||
std::string debug_dir = "DEBUG";
|
||||
std::filesystem::path filename{};
|
||||
File stack_dump_file{};
|
||||
File dump_file{};
|
||||
bool error;
|
||||
std::string str;
|
||||
uint32_t* addr_end;
|
||||
uint32_t* p;
|
||||
int n{0};
|
||||
bool data_found{false};
|
||||
|
||||
make_new_directory(debug_dir);
|
||||
filename = next_filename_matching_pattern(debug_dir + "/STACK_DUMP_????.TXT");
|
||||
filename = next_filename_matching_pattern(debug_dir + "/" + (stack_flag ? "STACK" : "MEMORY") + "_DUMP_????.TXT");
|
||||
error = filename.empty();
|
||||
if (!error)
|
||||
error = stack_dump_file.create(filename) != 0;
|
||||
error = dump_file.create(filename) != 0;
|
||||
if (error) {
|
||||
painter.draw_string({16, 320 - 32}, ui::Styles::red, "ERROR DUMPING " + filename.filename().string() + "!");
|
||||
return false;
|
||||
}
|
||||
|
||||
for (p = &__process_stack_base__; p < &__process_stack_end__; p++) {
|
||||
addr_end = addr_start + num_words;
|
||||
for (p = addr_start; p < addr_end; p++) {
|
||||
// skip past unused stack words
|
||||
if (!data_found) {
|
||||
if (stack_flag && !data_found) {
|
||||
if (*p == CRT0_STACKS_FILL_PATTERN)
|
||||
continue;
|
||||
else {
|
||||
data_found = true;
|
||||
auto stack_space_left = p - &__process_stack_base__;
|
||||
stack_dump_file.write_line(to_string_hex((uint32_t)&__process_stack_base__, 8) + ": Unused words " + to_string_dec_uint(stack_space_left));
|
||||
auto stack_space_left = p - addr_start;
|
||||
dump_file.write_line(to_string_hex((uint32_t)addr_start, 8) + ": Unused words " + to_string_dec_uint(stack_space_left));
|
||||
|
||||
// align subsequent lines to start on 16-byte boundaries
|
||||
p -= (stack_space_left & 3);
|
||||
@@ -299,20 +304,20 @@ bool stack_dump() {
|
||||
// write address
|
||||
if (n++ == 0) {
|
||||
str = to_string_hex((uint32_t)p, 8) + ":";
|
||||
stack_dump_file.write(str.data(), 9);
|
||||
dump_file.write(str.data(), 9);
|
||||
}
|
||||
|
||||
// write stack dword
|
||||
str = " " + to_string_hex(*p, 8);
|
||||
stack_dump_file.write(str.data(), 9);
|
||||
dump_file.write(str.data(), 9);
|
||||
|
||||
if (n == 4) {
|
||||
stack_dump_file.write("\r\n", 2);
|
||||
dump_file.write("\r\n", 2);
|
||||
n = 0;
|
||||
}
|
||||
}
|
||||
|
||||
painter.draw_string({16, 320 - 32}, ui::Styles::green, filename.filename().string() + " dumped!");
|
||||
painter.draw_string({0, 320 - 16}, ui::Styles::green, filename.filename().string() + " dumped!");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -48,5 +48,6 @@ inline uint32_t get_free_stack_space() {
|
||||
}
|
||||
|
||||
bool stack_dump();
|
||||
bool memory_dump(uint32_t* addr_start, uint32_t num_words, bool stack_flag);
|
||||
|
||||
#endif /*__DEBUG_H__*/
|
||||
|
||||
@@ -321,7 +321,7 @@ void MAX2837::set_rx_buff_vcm(const size_t v) {
|
||||
flush();
|
||||
}
|
||||
|
||||
reg_t MAX2837::temp_sense() {
|
||||
int8_t MAX2837::temp_sense() {
|
||||
if (!_map.r.rx_top.ts_en) {
|
||||
_map.r.rx_top.ts_en = 1;
|
||||
flush_one(Register::RX_TOP);
|
||||
@@ -334,12 +334,15 @@ reg_t MAX2837::temp_sense() {
|
||||
|
||||
halPolledDelay(ticks_for_temperature_sense_adc_conversion);
|
||||
|
||||
const auto value = read(Register::TEMP_SENSE);
|
||||
/*
|
||||
* Conversion to degrees C determined by testing - does not match data sheet.
|
||||
*/
|
||||
reg_t value = read(Register::TEMP_SENSE) & 0x1F;
|
||||
|
||||
_map.r.rx_top.ts_adc_trigger = 0;
|
||||
flush_one(Register::RX_TOP);
|
||||
|
||||
return value;
|
||||
return std::min(127, (int)(value * 4.31 - 40)); // reg value is 0 to 31; possible return range is -40 C to 127 C
|
||||
}
|
||||
|
||||
} // namespace max2837
|
||||
|
||||
@@ -833,9 +833,10 @@ class MAX2837 : public MAX283x {
|
||||
void set_vco_bias(const size_t v);
|
||||
void set_rx_buff_vcm(const size_t v) override;
|
||||
|
||||
reg_t temp_sense() override;
|
||||
int8_t temp_sense() override;
|
||||
|
||||
reg_t read(const address_t reg_num) override;
|
||||
void write(const address_t reg_num, const reg_t value) override;
|
||||
|
||||
private:
|
||||
spi::arbiter::Target& _target;
|
||||
@@ -845,8 +846,6 @@ class MAX2837 : public MAX283x {
|
||||
|
||||
void flush_one(const Register reg);
|
||||
|
||||
void write(const address_t reg_num, const reg_t value);
|
||||
|
||||
void write(const Register reg, const reg_t value);
|
||||
reg_t read(const Register reg);
|
||||
|
||||
|
||||
@@ -351,7 +351,7 @@ void MAX2839::set_rx_buff_vcm(const size_t v) {
|
||||
flush();
|
||||
}
|
||||
|
||||
reg_t MAX2839::temp_sense() {
|
||||
int8_t MAX2839::temp_sense() {
|
||||
if (!_map.r.rx_top_2.ts_en) {
|
||||
_map.r.rx_top_2.ts_en = 1;
|
||||
flush_one(Register::RX_TOP_2);
|
||||
@@ -365,15 +365,14 @@ reg_t MAX2839::temp_sense() {
|
||||
halPolledDelay(ticks_for_temperature_sense_adc_conversion);
|
||||
|
||||
/*
|
||||
* Things look very similar to MAX2837, so this probably works, but the
|
||||
* MAX2839 data sheet does not describe the TEMP_SENSE register contents.
|
||||
* Conversion to degrees C determined by testing - does not match data sheet.
|
||||
*/
|
||||
const auto value = read(Register::TEMP_SENSE);
|
||||
reg_t value = read(Register::TEMP_SENSE) & 0x1F;
|
||||
|
||||
_map.r.rx_top_2.ts_adc_trigger = 0;
|
||||
flush_one(Register::RX_TOP_2);
|
||||
|
||||
return value;
|
||||
return std::min(127, (int)(value * 4.31 - 40)); // reg value is 0 to 31; possible return range is -40 C to 127 C
|
||||
}
|
||||
|
||||
} // namespace max2839
|
||||
@@ -692,9 +692,10 @@ class MAX2839 : public MAX283x {
|
||||
void set_rx_lo_iq_calibration(const size_t v) override;
|
||||
void set_rx_buff_vcm(const size_t v) override;
|
||||
|
||||
reg_t temp_sense() override;
|
||||
int8_t temp_sense() override;
|
||||
|
||||
reg_t read(const address_t reg_num) override;
|
||||
void write(const address_t reg_num, const reg_t value) override;
|
||||
|
||||
private:
|
||||
spi::arbiter::Target& _target;
|
||||
@@ -704,8 +705,6 @@ class MAX2839 : public MAX283x {
|
||||
|
||||
void flush_one(const Register reg);
|
||||
|
||||
void write(const address_t reg_num, const reg_t value);
|
||||
|
||||
void write(const Register reg, const reg_t value);
|
||||
reg_t read(const Register reg);
|
||||
|
||||
|
||||
@@ -131,9 +131,10 @@ class MAX283x {
|
||||
virtual void set_rx_lo_iq_calibration(const size_t v);
|
||||
virtual void set_rx_buff_vcm(const size_t v);
|
||||
|
||||
virtual reg_t temp_sense();
|
||||
virtual int8_t temp_sense();
|
||||
|
||||
virtual reg_t read(const address_t reg_num);
|
||||
virtual void write(const address_t reg_num, const reg_t value);
|
||||
};
|
||||
|
||||
} // namespace max283x
|
||||
|
||||
@@ -841,6 +841,7 @@ class RFFC507x {
|
||||
void set_gpo1(const bool new_value);
|
||||
|
||||
reg_t read(const address_t reg_num);
|
||||
void write(const address_t reg_num, const reg_t value);
|
||||
|
||||
private:
|
||||
spi::SPI _bus{};
|
||||
@@ -848,8 +849,6 @@ class RFFC507x {
|
||||
RegisterMap _map{default_hackrf_one};
|
||||
DirtyRegisters<Register, reg_count> _dirty{};
|
||||
|
||||
void write(const address_t reg_num, const reg_t value);
|
||||
|
||||
void write(const Register reg, const reg_t value);
|
||||
reg_t read(const Register reg);
|
||||
|
||||
|
||||
@@ -36,6 +36,13 @@ uint32_t power(T value) {
|
||||
return (real * real) + (imag * imag);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
uint32_t iq_max(T value) {
|
||||
auto real = abs(value.real());
|
||||
auto imag = abs(value.imag());
|
||||
return (real > imag) ? real : imag;
|
||||
}
|
||||
|
||||
/* Collects capture file metadata and sample power buckets. */
|
||||
template <typename T>
|
||||
Optional<CaptureInfo> profile_capture(
|
||||
@@ -51,7 +58,8 @@ Optional<CaptureInfo> profile_capture(
|
||||
.file_size = f.size(),
|
||||
.sample_count = f.size() / sizeof(T),
|
||||
.sample_size = sizeof(T),
|
||||
.max_power = 0};
|
||||
.max_power = 0,
|
||||
.max_iq = 0};
|
||||
|
||||
auto profile_samples = buckets.size * samples_per_bucket;
|
||||
auto sample_interval = info.sample_count / profile_samples;
|
||||
@@ -67,8 +75,11 @@ Optional<CaptureInfo> profile_capture(
|
||||
if (*result != info.sample_size)
|
||||
break; // EOF
|
||||
|
||||
auto mag_squared = power(value);
|
||||
auto max_iq = iq_max(value);
|
||||
if (max_iq > info.max_iq)
|
||||
info.max_iq = max_iq;
|
||||
|
||||
auto mag_squared = power(value);
|
||||
if (mag_squared > info.max_power)
|
||||
info.max_power = mag_squared;
|
||||
|
||||
@@ -133,13 +144,50 @@ TrimRange compute_trim_range(
|
||||
info.sample_size};
|
||||
}
|
||||
|
||||
void amplify_iq_buffer(uint8_t* buffer, uint32_t length, uint32_t amplification, uint8_t sample_size) {
|
||||
uint32_t mult_count = length / sample_size / 2;
|
||||
|
||||
switch (sample_size) {
|
||||
case sizeof(complex16_t): {
|
||||
int16_t* buf_ptr = (int16_t*)buffer;
|
||||
for (uint32_t i = 0; i < mult_count; i++) {
|
||||
int32_t val = *buf_ptr * amplification;
|
||||
if (val > 0x7FFF)
|
||||
val = 0x7FFF;
|
||||
else if (val < -0x7FFF)
|
||||
val = -0x7FFF;
|
||||
*buf_ptr++ = val;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case sizeof(complex8_t): {
|
||||
int8_t* buf_ptr = (int8_t*)buffer;
|
||||
for (uint32_t i = 0; i < mult_count; i++) {
|
||||
int32_t val = *buf_ptr * amplification;
|
||||
if (val > 0x7F)
|
||||
val = 0x7F;
|
||||
else if (val < -0x7F)
|
||||
val = -0x7F;
|
||||
*buf_ptr++ = val;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool trim_capture_with_range(
|
||||
const fs::path& path,
|
||||
TrimRange range,
|
||||
const std::function<void(uint8_t)>& on_progress) {
|
||||
const std::function<void(uint8_t)>& on_progress,
|
||||
const uint32_t amplification) {
|
||||
constexpr size_t buffer_size = std::filesystem::max_file_block_size;
|
||||
uint8_t buffer[buffer_size];
|
||||
auto temp_path = path + u"-tmp";
|
||||
auto sample_size = fs::capture_file_sample_size(path);
|
||||
|
||||
// end_sample is the first sample to _not_ include.
|
||||
auto start_byte = range.start_sample * range.sample_size;
|
||||
@@ -168,6 +216,9 @@ bool trim_capture_with_range(
|
||||
auto remaining = length - processed;
|
||||
auto to_write = std::min(remaining, *result);
|
||||
|
||||
if (amplification > 1)
|
||||
amplify_iq_buffer(buffer, to_write, amplification, sample_size);
|
||||
|
||||
result = dst->write(buffer, to_write);
|
||||
if (result.is_error()) return false;
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ struct CaptureInfo {
|
||||
uint64_t sample_count;
|
||||
uint8_t sample_size;
|
||||
uint32_t max_power;
|
||||
uint32_t max_iq;
|
||||
};
|
||||
|
||||
/* Holds sample average power by bucket. */
|
||||
@@ -82,11 +83,19 @@ TrimRange compute_trim_range(
|
||||
const PowerBuckets& buckets,
|
||||
uint8_t cutoff_percent);
|
||||
|
||||
/* Multiplies samples in an IQ buffer by amplification value */
|
||||
void amplify_iq_buffer(
|
||||
uint8_t* buffer,
|
||||
uint32_t length,
|
||||
uint32_t amplification,
|
||||
uint8_t sample_size);
|
||||
|
||||
/* Trims the capture file with the specified range. */
|
||||
bool trim_capture_with_range(
|
||||
const std::filesystem::path& path,
|
||||
TrimRange range,
|
||||
const std::function<void(uint8_t)>& on_progress);
|
||||
const std::function<void(uint8_t)>& on_progress,
|
||||
const uint32_t amplification);
|
||||
|
||||
} // namespace iq
|
||||
|
||||
|
||||
@@ -186,6 +186,8 @@ int main(void) {
|
||||
sdcStop(&SDCD1);
|
||||
|
||||
portapack::shutdown();
|
||||
} else {
|
||||
config_mode_clear();
|
||||
}
|
||||
|
||||
m4_init(portapack::spi_flash::image_tag_hackrf, portapack::memory::map::m4_code_hackrf, true);
|
||||
|
||||
@@ -467,6 +467,20 @@ bool init() {
|
||||
i2c0.start(i2c_config_fast_clock);
|
||||
chThdSleepMilliseconds(10);
|
||||
|
||||
/* Check if portapack is attached by checking if any of the two audio chips is present. */
|
||||
systime_t timeout = 50;
|
||||
uint8_t wm8731_reset_command[] = {0x0f, 0x00};
|
||||
if (i2c0.transmit(0x1a /* wm8731 */, wm8731_reset_command, 2, timeout) == false) {
|
||||
audio_codec_ak4951.reset();
|
||||
uint8_t ak4951_init_command[] = {0x00, 0x00};
|
||||
i2c0.transmit(0x12 /* ak4951 */, ak4951_init_command, 2, timeout);
|
||||
chThdSleepMilliseconds(10);
|
||||
if (i2c0.transmit(0x12 /* ak4951 */, ak4951_init_command, 2, timeout) == false) {
|
||||
shutdown_base();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
touch::adc::init();
|
||||
controls_init();
|
||||
chThdSleepMilliseconds(10);
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "aprs.hpp"
|
||||
#include "ax25.hpp"
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ static constexpr SPIConfig ssp_config_max283x = {
|
||||
.ssport = gpio_max283x_select.port(),
|
||||
.sspad = gpio_max283x_select.pad(),
|
||||
.cr0 =
|
||||
CR0_CLOCKRATE(ssp_scr(ssp1_pclk_f, ssp1_cpsr, max283x_spi_f)) | CR0_FRFSPI | CR0_DSS16BIT,
|
||||
CR0_CLOCKRATE(ssp_scr(ssp1_pclk_f, ssp1_cpsr, max283x_spi_f) + 1) | CR0_FRFSPI | CR0_DSS16BIT,
|
||||
.cpsr = ssp1_cpsr,
|
||||
};
|
||||
|
||||
@@ -287,6 +287,10 @@ uint32_t register_read(const size_t register_number) {
|
||||
return radio::first_if.read(register_number);
|
||||
}
|
||||
|
||||
void register_write(const size_t register_number, uint32_t value) {
|
||||
radio::first_if.write(register_number, value);
|
||||
}
|
||||
|
||||
} /* namespace first_if */
|
||||
|
||||
namespace second_if {
|
||||
@@ -295,8 +299,12 @@ uint32_t register_read(const size_t register_number) {
|
||||
return radio::second_if->read(register_number);
|
||||
}
|
||||
|
||||
uint8_t temp_sense() {
|
||||
return radio::second_if->temp_sense() & 0x1f;
|
||||
void register_write(const size_t register_number, uint32_t value) {
|
||||
radio::second_if->write(register_number, value);
|
||||
}
|
||||
|
||||
int8_t temp_sense() {
|
||||
return radio::second_if->temp_sense();
|
||||
}
|
||||
|
||||
} /* namespace second_if */
|
||||
|
||||
@@ -65,15 +65,17 @@ namespace debug {
|
||||
namespace first_if {
|
||||
|
||||
uint32_t register_read(const size_t register_number);
|
||||
void register_write(const size_t register_number, uint32_t value);
|
||||
|
||||
} /* namespace first_if */
|
||||
|
||||
namespace second_if {
|
||||
|
||||
uint32_t register_read(const size_t register_number);
|
||||
void register_write(const size_t register_number, uint32_t value);
|
||||
|
||||
// TODO: This belongs somewhere else.
|
||||
uint8_t temp_sense();
|
||||
int8_t temp_sense();
|
||||
|
||||
} /* namespace second_if */
|
||||
|
||||
|
||||
@@ -24,13 +24,13 @@
|
||||
|
||||
#include "ui_widget.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <list>
|
||||
#include <utility>
|
||||
#include <functional>
|
||||
#include <iterator>
|
||||
#include <algorithm>
|
||||
#include <list>
|
||||
#include <utility>
|
||||
|
||||
template <class Entry>
|
||||
using RecentEntries = std::list<Entry>;
|
||||
@@ -42,6 +42,13 @@ typename ContainerType::const_iterator find(const ContainerType& entries, const
|
||||
[key](typename ContainerType::const_reference e) { return e.key() == key; });
|
||||
}
|
||||
|
||||
template <typename ContainerType, typename Key>
|
||||
typename ContainerType::iterator find(ContainerType& entries, const Key key) {
|
||||
return std::find_if(
|
||||
std::begin(entries), std::end(entries),
|
||||
[key](typename ContainerType::const_reference e) { return e.key() == key; });
|
||||
}
|
||||
|
||||
template <typename ContainerType>
|
||||
static void truncate_entries(ContainerType& entries, const size_t entries_max = 64) {
|
||||
while (entries.size() > entries_max) {
|
||||
@@ -88,6 +95,36 @@ static std::pair<typename ContainerType::const_iterator, typename ContainerType:
|
||||
return {start, end};
|
||||
}
|
||||
|
||||
template <typename ContainerType, typename KeySelector, typename SortOrder>
|
||||
void sortEntriesBy(ContainerType& entries, KeySelector keySelector, SortOrder ascending) {
|
||||
entries.sort([keySelector, ascending](const auto& a, const auto& b) {
|
||||
return ascending ? keySelector(a) < keySelector(b) : keySelector(a) > keySelector(b);
|
||||
});
|
||||
}
|
||||
|
||||
template <typename ContainerType, typename KeySelector>
|
||||
void resetFilteredEntries(ContainerType& entries, KeySelector keySelector) {
|
||||
// Clear the filteredEntries container
|
||||
auto it = entries.begin();
|
||||
while (it != entries.end()) {
|
||||
if (keySelector(*it)) {
|
||||
entries.erase(it); // Add a new entry to filteredEntries
|
||||
}
|
||||
++it; // Move to the next element, outside of the if block
|
||||
}
|
||||
}
|
||||
|
||||
template <typename ContainerType, typename MemberPtr, typename KeyValue>
|
||||
void setAllMembersToValue(ContainerType& entries, MemberPtr memberPtr, const KeyValue& keyValue) {
|
||||
for (auto& entry : entries) {
|
||||
// Check if the member specified by memberPtr is equal to keyValue
|
||||
if (entry.*memberPtr != keyValue) {
|
||||
// Update the member with keyValue
|
||||
entry.*memberPtr = keyValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace ui {
|
||||
|
||||
using RecentEntriesColumn = std::pair<std::string, size_t>;
|
||||
@@ -272,6 +309,10 @@ class RecentEntriesView : public View {
|
||||
_table.focus();
|
||||
}
|
||||
|
||||
void set_table(Entries& new_table) {
|
||||
_table = new_table;
|
||||
}
|
||||
|
||||
private:
|
||||
RecentEntriesHeader _header;
|
||||
RecentEntriesTable<Entries> _table;
|
||||
|
||||
@@ -311,6 +311,72 @@ std::string to_string_file_size(uint32_t file_size) {
|
||||
return to_string_dec_uint(file_size) + suffix[suffix_index];
|
||||
}
|
||||
|
||||
std::string to_string_mac_address(const uint8_t* macAddress, uint8_t length, bool noColon) {
|
||||
std::string string;
|
||||
|
||||
string += to_string_hex(macAddress[0], 2);
|
||||
|
||||
for (int i = 1; i < length; i++) {
|
||||
string += noColon ? to_string_hex(macAddress[i], 2) : ":" + to_string_hex(macAddress[i], 2);
|
||||
}
|
||||
|
||||
return string;
|
||||
}
|
||||
|
||||
std::string to_string_formatted_mac_address(const char* macAddress) {
|
||||
std::string formattedAddress;
|
||||
|
||||
for (int i = 0; i < 12; i += 2) {
|
||||
if (i > 0) {
|
||||
formattedAddress += ':';
|
||||
}
|
||||
formattedAddress += macAddress[i];
|
||||
formattedAddress += macAddress[i + 1];
|
||||
}
|
||||
|
||||
return formattedAddress;
|
||||
}
|
||||
|
||||
void generateRandomMacAddress(char* macAddress) {
|
||||
const char hexDigits[] = "0123456789ABCDEF";
|
||||
|
||||
// Generate 12 random hexadecimal characters
|
||||
for (int i = 0; i < 12; i++) {
|
||||
int randomIndex = rand() % 16;
|
||||
macAddress[i] = hexDigits[randomIndex];
|
||||
}
|
||||
macAddress[12] = '\0'; // Null-terminate the string
|
||||
}
|
||||
|
||||
uint64_t readUntil(File& file, char* result, std::size_t maxBufferSize, char delimiter) {
|
||||
std::size_t bytesRead = 0;
|
||||
|
||||
while (true) {
|
||||
char ch;
|
||||
File::Result<File::Size> readResult = file.read(&ch, 1);
|
||||
|
||||
if (readResult.is_ok() && readResult.value() > 0) {
|
||||
if (ch == delimiter) {
|
||||
// Found a space character, stop reading
|
||||
break;
|
||||
} else if (bytesRead < maxBufferSize) {
|
||||
// Append the character to the result if there's space
|
||||
result[bytesRead++] = ch;
|
||||
} else {
|
||||
// Buffer is full, break to prevent overflow
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break; // End of file or error
|
||||
}
|
||||
}
|
||||
|
||||
// Null-terminate the result string
|
||||
result[bytesRead] = '\0';
|
||||
|
||||
return bytesRead;
|
||||
}
|
||||
|
||||
std::string unit_auto_scale(double n, const uint32_t base_unit, uint32_t precision) {
|
||||
const uint32_t powers_of_ten[5] = {1, 10, 100, 1000, 10000};
|
||||
std::string string{""};
|
||||
|
||||
@@ -76,6 +76,13 @@ std::string to_string_FAT_timestamp(const FATTimestamp& timestamp);
|
||||
// Gets a human readable file size string.
|
||||
std::string to_string_file_size(uint32_t file_size);
|
||||
|
||||
// Converts Mac Address to string.
|
||||
std::string to_string_mac_address(const uint8_t* macAddress, uint8_t length, bool noColon);
|
||||
std::string to_string_formatted_mac_address(const char* macAddress);
|
||||
void generateRandomMacAddress(char* macAddress);
|
||||
|
||||
uint64_t readUntil(File& file, char* result, std::size_t maxBufferSize, char delimiter);
|
||||
|
||||
/* Scales 'n' to be a value less than 1000. 'base_unit' is the index of the unit from
|
||||
* 'unit_prefix' that 'n' is in initially. 3 is the index of the '1s' unit. */
|
||||
std::string unit_auto_scale(double n, const uint32_t base_unit, uint32_t precision);
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
class TemperatureLogger {
|
||||
public:
|
||||
using sample_t = uint8_t;
|
||||
using sample_t = int8_t;
|
||||
|
||||
void second_tick();
|
||||
|
||||
|
||||
@@ -118,7 +118,12 @@ void AlphanumView::refresh_keys() {
|
||||
|
||||
size_t n = 0;
|
||||
for (auto& button : buttons) {
|
||||
button.set_text(std::string{key_list[n]});
|
||||
if (n > strlen(key_list)) {
|
||||
button.set_text(std::string{'\0'});
|
||||
} else {
|
||||
button.set_text(std::string{key_list[n]});
|
||||
}
|
||||
|
||||
n++;
|
||||
}
|
||||
|
||||
@@ -137,7 +142,9 @@ void AlphanumView::refresh_keys() {
|
||||
|
||||
void AlphanumView::on_button(Button& button) {
|
||||
const auto c = button.text()[0];
|
||||
char_add(c);
|
||||
if (c != '\0') {
|
||||
char_add(c);
|
||||
}
|
||||
|
||||
// TODO: Consolidate shift handling.
|
||||
if (shift_mode == ShiftMode::Shift) {
|
||||
|
||||
@@ -57,6 +57,7 @@ class AlphanumView : public TextEntryView {
|
||||
const char* const keys_upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ, .";
|
||||
const char* const keys_digit = "1234567890()'`\"+-*/=<>_\\!?, .";
|
||||
const char* const keys_symbl = "!@#$%^&*()[]'`\"{}|:;<>-_~?, .";
|
||||
const char* const keys_hex = "1234567890ABCDEF ";
|
||||
|
||||
struct key_set_t {
|
||||
const char* name;
|
||||
@@ -64,9 +65,10 @@ class AlphanumView : public TextEntryView {
|
||||
const char* shifted;
|
||||
};
|
||||
|
||||
const key_set_t key_sets[2] = {
|
||||
const key_set_t key_sets[3] = {
|
||||
{"abc", keys_lower, keys_upper},
|
||||
{"123", keys_digit, keys_symbl}};
|
||||
{"123", keys_digit, keys_symbl},
|
||||
{"hex", keys_hex, keys_hex}};
|
||||
|
||||
int16_t focused_button = 0;
|
||||
uint32_t mode = 0; // Letters.
|
||||
|
||||
@@ -246,13 +246,24 @@ void load_blacklist() {
|
||||
if (error)
|
||||
return;
|
||||
|
||||
blacklist_ptr = std::unique_ptr<char>(new char[f.size()]);
|
||||
if (f.read(blacklist_ptr.get(), f.size()))
|
||||
blacklist_len = f.size();
|
||||
// allocating two extra bytes for leading & trailing commas
|
||||
blacklist_ptr = std::unique_ptr<char>(new char[f.size() + 2]);
|
||||
if (f.read(blacklist_ptr.get() + 1, f.size())) {
|
||||
blacklist_len = f.size() + 2;
|
||||
|
||||
// replace any CR/LF characters with comma delineator, and add comma prefix/suffix, to simplify searching
|
||||
char* ptr = blacklist_ptr.get();
|
||||
*ptr = ',';
|
||||
*(ptr + blacklist_len - 1) = ',';
|
||||
for (size_t i = 0; i < blacklist_len; i++, ptr++) {
|
||||
if (*ptr == 0x0D || *ptr == 0x0A)
|
||||
*ptr = ',';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool BtnGridView::blacklisted_app(GridItem new_item) {
|
||||
std::string app_name = new_item.text;
|
||||
std::string app_name = "," + new_item.text + ",";
|
||||
|
||||
if (blacklist_len < app_name.size())
|
||||
return false;
|
||||
|
||||
@@ -42,6 +42,16 @@ FrequencyField::FrequencyField(
|
||||
set_focusable(true);
|
||||
}
|
||||
|
||||
FrequencyField::FrequencyField(
|
||||
const Point parent_pos,
|
||||
const rf::FrequencyRange range)
|
||||
: Widget{{parent_pos, {8 * 10, 16}}},
|
||||
length_{11},
|
||||
range_{range} {
|
||||
initial_switch_config_ = get_switches_long_press_config();
|
||||
set_focusable(true);
|
||||
}
|
||||
|
||||
FrequencyField::~FrequencyField() {
|
||||
reset_switch_config();
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ class FrequencyField : public Widget {
|
||||
using range_t = rf::FrequencyRange;
|
||||
|
||||
FrequencyField(Point parent_pos);
|
||||
FrequencyField(Point parent_pos, rf::FrequencyRange range);
|
||||
~FrequencyField();
|
||||
|
||||
rf::Frequency value() const;
|
||||
@@ -66,8 +67,7 @@ class FrequencyField : public Widget {
|
||||
|
||||
private:
|
||||
const size_t length_;
|
||||
const range_t range_;
|
||||
|
||||
range_t range_;
|
||||
rf::Frequency value_{0};
|
||||
rf::Frequency step_{25000};
|
||||
uint64_t last_ms_{0};
|
||||
|
||||
@@ -30,6 +30,10 @@
|
||||
#define max(a, b) ((a) > (b) ? (a) : (b))
|
||||
#define abs(x) ((x) > 0 ? (x) : -(x))
|
||||
|
||||
// flag value for graph min
|
||||
// If it does not change, then all graph min values are is zero
|
||||
#define GRAPH_MIN_ALL_ZERO_FLAG 666
|
||||
|
||||
namespace ui {
|
||||
|
||||
RSSI::RSSI(
|
||||
@@ -209,17 +213,48 @@ void RSSI::on_statistics_update(const RSSIStatistics& statistics) {
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
int16_t RSSIGraph::get_graph_min() {
|
||||
return graph_min_;
|
||||
}
|
||||
|
||||
int16_t RSSIGraph::get_graph_avg() {
|
||||
return graph_avg_;
|
||||
}
|
||||
|
||||
int16_t RSSIGraph::get_graph_max() {
|
||||
return graph_max_;
|
||||
}
|
||||
|
||||
int16_t RSSIGraph::get_graph_delta() {
|
||||
return graph_max_ - graph_min_;
|
||||
}
|
||||
|
||||
void RSSIGraph::paint(Painter& painter) {
|
||||
const auto r = screen_rect();
|
||||
|
||||
RSSIGraph_entry& prev_entry = graph_list[0];
|
||||
int xpos = 0, prev_xpos = r.width();
|
||||
if (graph_list.size() == 0)
|
||||
return;
|
||||
|
||||
int xpos = 0, prev_xpos = r.width();
|
||||
RSSIGraph_entry& prev_entry = graph_list[0];
|
||||
|
||||
graph_min_ = GRAPH_MIN_ALL_ZERO_FLAG; // if it stays at that value the whole graphlist min are zero
|
||||
graph_max_ = 0;
|
||||
int avg_sum = 0;
|
||||
for (int n = 1; (unsigned)n <= graph_list.size(); n++) {
|
||||
xpos = (r.width() * (graph_list.size() - n)) / graph_list.size();
|
||||
int size = abs(xpos - prev_xpos);
|
||||
RSSIGraph_entry& entry = graph_list[n - 1];
|
||||
|
||||
// stats
|
||||
if (entry.rssi_min != 0 && entry.rssi_min < graph_min_) {
|
||||
graph_min_ = entry.rssi_min;
|
||||
}
|
||||
if (entry.rssi_max > graph_max_) {
|
||||
graph_max_ = entry.rssi_max;
|
||||
}
|
||||
avg_sum += entry.rssi_avg;
|
||||
|
||||
// black
|
||||
const Rect r0{r.right() - prev_xpos, r.top(), size, r.height()};
|
||||
painter.fill_rectangle(
|
||||
@@ -293,6 +328,10 @@ void RSSIGraph::paint(Painter& painter) {
|
||||
prev_entry = entry;
|
||||
prev_xpos = xpos;
|
||||
}
|
||||
graph_avg_ = (avg_sum / graph_list.size());
|
||||
// hack to only set to 0 if all graphlist min values are 0
|
||||
if (graph_min_ == GRAPH_MIN_ALL_ZERO_FLAG)
|
||||
graph_min_ = 0;
|
||||
}
|
||||
|
||||
/*void RSSIGraph::paintOld(Painter& painter) {
|
||||
|
||||
@@ -49,6 +49,7 @@ class RSSI : public Widget {
|
||||
: RSSI{{}, {}} {
|
||||
}
|
||||
|
||||
// get last used/received min/avg/max/delta
|
||||
int16_t get_min();
|
||||
int16_t get_avg();
|
||||
int16_t get_max();
|
||||
@@ -113,8 +114,16 @@ class RSSIGraph : public Widget {
|
||||
|
||||
void on_hide() override;
|
||||
void on_show() override;
|
||||
// get whole graph_list min/avg/max/delta
|
||||
int16_t get_graph_min();
|
||||
int16_t get_graph_avg();
|
||||
int16_t get_graph_max();
|
||||
int16_t get_graph_delta();
|
||||
|
||||
private:
|
||||
int16_t graph_min_ = 0;
|
||||
int16_t graph_avg_ = 0;
|
||||
int16_t graph_max_ = 0;
|
||||
uint16_t nb_columns_before_hide = 16;
|
||||
uint16_t nb_columns = 16;
|
||||
RSSIGraphList graph_list{};
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
#include "ui_flash_utility.hpp"
|
||||
#include "ui_font_fixed_8x16.hpp"
|
||||
#include "ui_freqman.hpp"
|
||||
#include "ui_fsk_rx.hpp"
|
||||
#include "ui_iq_trim.hpp"
|
||||
#include "ui_jammer.hpp"
|
||||
// #include "ui_keyfob.hpp"
|
||||
@@ -78,6 +79,7 @@
|
||||
#include "ui_tone_search.hpp"
|
||||
#include "ui_touchtunes.hpp"
|
||||
#include "ui_view_wav.hpp"
|
||||
#include "ui_weatherstation.hpp"
|
||||
#include "ui_whipcalc.hpp"
|
||||
#include "ui_external_items_menu_loader.hpp"
|
||||
|
||||
@@ -85,6 +87,9 @@
|
||||
#include "ais_app.hpp"
|
||||
#include "analog_audio_app.hpp"
|
||||
#include "analog_tv_app.hpp"
|
||||
#include "ble_comm_app.hpp"
|
||||
#include "ble_rx_app.hpp"
|
||||
#include "ble_tx_app.hpp"
|
||||
#include "capture_app.hpp"
|
||||
#include "ert_app.hpp"
|
||||
#include "gps_sim_app.hpp"
|
||||
@@ -167,6 +172,9 @@ SystemStatusView::SystemStatusView(
|
||||
pmem::load_persistent_settings_from_file();
|
||||
}
|
||||
|
||||
// force apply of selected sdcard speed override at UI startup
|
||||
pmem::set_config_sdcard_high_speed_io(pmem::config_sdcard_high_speed_io(), false);
|
||||
|
||||
button_back.id = -1; // Special ID used by FocusManager
|
||||
title.set_style(&Styles::bg_dark_grey);
|
||||
|
||||
@@ -395,6 +403,7 @@ void SystemStatusView::rtc_battery_workaround() {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
make_new_directory(SETTINGS_DIR);
|
||||
make_new_file(DATE_FILEFLAG);
|
||||
|
||||
year = 1980;
|
||||
@@ -546,7 +555,9 @@ ReceiversMenuView::ReceiversMenuView(NavigationView& nav) {
|
||||
{"Analog TV", Color::yellow(), &bitmap_icon_sstv, [&nav]() { nav.push<AnalogTvView>(); }},
|
||||
{"APRS", Color::green(), &bitmap_icon_aprs, [&nav]() { nav.push<APRSRXView>(); }},
|
||||
{"Audio", Color::green(), &bitmap_icon_speaker, [&nav]() { nav.push<AnalogAudioView>(); }},
|
||||
{"BTLE", Color::yellow(), &bitmap_icon_btle, [&nav]() { nav.push<BTLERxView>(); }},
|
||||
//{"BTLE", Color::yellow(), &bitmap_icon_btle, [&nav]() { nav.push<BTLERxView>(); }},
|
||||
//{"BLE Comm", ui::Color::orange(), &bitmap_icon_btle, [&nav]() { nav.push<BLECommView>(); }},
|
||||
{"BLE Rx", Color::green(), &bitmap_icon_btle, [&nav]() { nav.push<BLERxView>(); }},
|
||||
{"ERT Meter", Color::green(), &bitmap_icon_ert, [&nav]() { nav.push<ERTAppView>(); }},
|
||||
{"Level", Color::green(), &bitmap_icon_options_radio, [&nav]() { nav.push<LevelView>(); }},
|
||||
{"NRF", Color::yellow(), &bitmap_icon_nrf, [&nav]() { nav.push<NRFRxView>(); }},
|
||||
@@ -555,6 +566,8 @@ ReceiversMenuView::ReceiversMenuView(NavigationView& nav) {
|
||||
{"Recon", Color::green(), &bitmap_icon_scanner, [&nav]() { nav.push<ReconView>(); }},
|
||||
{"Search", Color::yellow(), &bitmap_icon_search, [&nav]() { nav.push<SearchView>(); }},
|
||||
{"TPMS Cars", Color::green(), &bitmap_icon_tpms, [&nav]() { nav.push<TPMSAppView>(); }},
|
||||
{"Weather", Color::yellow(), &bitmap_icon_lge, [&nav]() { nav.push<WeatherView>(); }},
|
||||
// {"FSK RX", Color::yellow(), &bitmap_icon_remote, [&nav]() { nav.push<FskxRxMainView>(); }},
|
||||
// {"DMR", Color::dark_grey(), &bitmap_icon_dmr, [&nav](){ nav.push<NotImplementedView>(); }},
|
||||
// {"SIGFOX", Color::dark_grey(), &bitmap_icon_fox, [&nav](){ nav.push<NotImplementedView>(); }},
|
||||
// {"LoRa", Color::dark_grey(), &bitmap_icon_lora, [&nav](){ nav.push<NotImplementedView>(); }},
|
||||
@@ -577,6 +590,7 @@ TransmittersMenuView::TransmittersMenuView(NavigationView& nav) {
|
||||
{"ADS-B TX", ui::Color::green(), &bitmap_icon_adsb, [&nav]() { nav.push<ADSBTxView>(); }},
|
||||
{"APRS TX", ui::Color::green(), &bitmap_icon_aprs, [&nav]() { nav.push<APRSTXView>(); }},
|
||||
{"BHT Xy/EP", ui::Color::green(), &bitmap_icon_bht, [&nav]() { nav.push<BHTView>(); }},
|
||||
{"BLE Tx", ui::Color::green(), &bitmap_icon_btle, [&nav]() { nav.push<BLETxView>(); }},
|
||||
{"BurgerPgr", ui::Color::yellow(), &bitmap_icon_burger, [&nav]() { nav.push<CoasterPagerView>(); }},
|
||||
{"GPS Sim", ui::Color::green(), &bitmap_icon_gps_sim, [&nav]() { nav.push<GpsSimAppView>(); }},
|
||||
{"Jammer", ui::Color::green(), &bitmap_icon_jammer, [&nav]() { nav.push<JammerView>(); }},
|
||||
|
||||
@@ -322,7 +322,7 @@ void RecordView::trim_capture() {
|
||||
auto trim_range = iq::compute_trim_range(*info, power_buckets, 7);
|
||||
|
||||
trim_ui.show_trimming();
|
||||
iq::trim_capture_with_range(trim_path, trim_range, trim_ui.get_callback());
|
||||
iq::trim_capture_with_range(trim_path, trim_range, trim_ui.get_callback(), 1);
|
||||
}
|
||||
|
||||
trim_ui.clear();
|
||||
|
||||
@@ -360,6 +360,14 @@ set(MODE_CPPSRC
|
||||
proc_btlerx.cpp
|
||||
)
|
||||
DeclareTargets(PBTR btlerx)
|
||||
|
||||
### BTLE TX
|
||||
|
||||
set(MODE_CPPSRC
|
||||
proc_ble_tx.cpp
|
||||
)
|
||||
DeclareTargets(PBTT btletx)
|
||||
|
||||
### AIS
|
||||
|
||||
set(MODE_CPPSRC
|
||||
@@ -416,6 +424,13 @@ set(MODE_CPPSRC
|
||||
)
|
||||
DeclareTargets(PFSK fsktx)
|
||||
|
||||
### FSK RX
|
||||
|
||||
set(MODE_CPPSRC
|
||||
proc_fsk_rx.cpp
|
||||
)
|
||||
DeclareTargets(PFSR fskrx)
|
||||
|
||||
### Jammer
|
||||
|
||||
set(MODE_CPPSRC
|
||||
@@ -535,6 +550,14 @@ set(MODE_CPPSRC
|
||||
)
|
||||
DeclareTargets(PWFM wfm_audio)
|
||||
|
||||
### Weather Stations
|
||||
|
||||
set(MODE_CPPSRC
|
||||
proc_weather.cpp
|
||||
)
|
||||
DeclareTargets(PWTH weather)
|
||||
|
||||
|
||||
### Flash Utility
|
||||
|
||||
include(${CHIBIOS_PORTAPACK}/os/various/fatfs_bindings/fatfs.cmake)
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
|
||||
#ifndef __FPROTO_Acurite_592TXR_H__
|
||||
#define __FPROTO_Acurite_592TXR_H__
|
||||
|
||||
#include "weatherbase.hpp"
|
||||
|
||||
typedef enum {
|
||||
Acurite_592TXRDecoderStepReset = 0,
|
||||
Acurite_592TXRDecoderStepCheckPreambule,
|
||||
Acurite_592TXRDecoderStepSaveDuration,
|
||||
Acurite_592TXRDecoderStepCheckDuration,
|
||||
} Acurite_592TXRDecoderStep;
|
||||
|
||||
class FProtoWeatherAcurite592TXR : public FProtoWeatherBase {
|
||||
public:
|
||||
FProtoWeatherAcurite592TXR() {
|
||||
sensorType = FPW_Acurite592TXR;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
switch (parser_step) {
|
||||
case Acurite_592TXRDecoderStepReset:
|
||||
if ((level) && (DURATION_DIFF(duration, te_short * 3) <
|
||||
te_delta * 2)) {
|
||||
parser_step = Acurite_592TXRDecoderStepCheckPreambule;
|
||||
te_last = duration;
|
||||
header_count = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case Acurite_592TXRDecoderStepCheckPreambule:
|
||||
if (level) {
|
||||
te_last = duration;
|
||||
} else {
|
||||
if ((DURATION_DIFF(
|
||||
te_last, te_short * 3) <
|
||||
te_delta * 2) &&
|
||||
(DURATION_DIFF(duration, te_short * 3) <
|
||||
te_delta * 2)) {
|
||||
// Found preambule
|
||||
header_count++;
|
||||
} else if ((header_count > 2) && (header_count < 5)) {
|
||||
if ((DURATION_DIFF(te_last, te_short) < te_delta) &&
|
||||
(DURATION_DIFF(duration, te_long) < te_delta)) {
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
subghz_protocol_blocks_add_bit(0);
|
||||
parser_step = Acurite_592TXRDecoderStepSaveDuration;
|
||||
} else if (
|
||||
(DURATION_DIFF(
|
||||
te_last, te_long) <
|
||||
te_delta) &&
|
||||
(DURATION_DIFF(duration, te_short) <
|
||||
te_delta)) {
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
parser_step = Acurite_592TXRDecoderStepSaveDuration;
|
||||
} else {
|
||||
parser_step = Acurite_592TXRDecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
parser_step = Acurite_592TXRDecoderStepReset;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case Acurite_592TXRDecoderStepSaveDuration:
|
||||
if (level) {
|
||||
te_last = duration;
|
||||
parser_step = Acurite_592TXRDecoderStepCheckDuration;
|
||||
} else {
|
||||
parser_step = Acurite_592TXRDecoderStepReset;
|
||||
}
|
||||
break;
|
||||
|
||||
case Acurite_592TXRDecoderStepCheckDuration:
|
||||
if (!level) {
|
||||
if (duration >= ((uint32_t)te_short * 5)) {
|
||||
if ((decode_count_bit ==
|
||||
min_count_bit_for_found) &&
|
||||
ws_protocol_acurite_592txr_check_crc()) {
|
||||
data = decode_data;
|
||||
data_count_bit = decode_count_bit;
|
||||
ws_protocol_acurite_592txr_remote_controller();
|
||||
if (callback) callback(this);
|
||||
}
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
parser_step = Acurite_592TXRDecoderStepReset;
|
||||
break;
|
||||
} else if (
|
||||
(DURATION_DIFF(
|
||||
te_last, te_short) <
|
||||
te_delta) &&
|
||||
(DURATION_DIFF(duration, te_long) <
|
||||
te_delta)) {
|
||||
subghz_protocol_blocks_add_bit(0);
|
||||
parser_step = Acurite_592TXRDecoderStepSaveDuration;
|
||||
} else if (
|
||||
(DURATION_DIFF(
|
||||
te_last, te_long) <
|
||||
te_delta) &&
|
||||
(DURATION_DIFF(duration, te_short) <
|
||||
te_delta)) {
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
parser_step = Acurite_592TXRDecoderStepSaveDuration;
|
||||
} else {
|
||||
parser_step = Acurite_592TXRDecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
parser_step = Acurite_592TXRDecoderStepReset;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
uint32_t te_short = 200;
|
||||
uint32_t te_long = 400;
|
||||
uint32_t te_delta = 90;
|
||||
uint32_t min_count_bit_for_found = 56;
|
||||
|
||||
bool ws_protocol_acurite_592txr_check_crc() {
|
||||
uint8_t msg[] = {
|
||||
static_cast<uint8_t>(decode_data >> 48),
|
||||
static_cast<uint8_t>(decode_data >> 40),
|
||||
static_cast<uint8_t>(decode_data >> 32),
|
||||
static_cast<uint8_t>(decode_data >> 24),
|
||||
static_cast<uint8_t>(decode_data >> 16),
|
||||
static_cast<uint8_t>(decode_data >> 8)};
|
||||
|
||||
if ((subghz_protocol_blocks_add_bytes(msg, 6) ==
|
||||
(uint8_t)(decode_data & 0xFF)) &&
|
||||
(!subghz_protocol_blocks_parity_bytes(&msg[2], 4))) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void ws_protocol_acurite_592txr_remote_controller() {
|
||||
uint8_t channel_[] = {3, 0, 2, 1};
|
||||
uint8_t channel_raw = ((data >> 54) & 0x03);
|
||||
channel = channel_[channel_raw];
|
||||
id = (data >> 40) & 0x3FFF;
|
||||
battery_low = !((data >> 38) & 1);
|
||||
humidity = (data >> 24) & 0x7F;
|
||||
uint16_t temp_raw = ((data >> 9) & 0xF80) | ((data >> 8) & 0x7F);
|
||||
temp = ((float)(temp_raw)-1000) / 10.0f;
|
||||
btn = WS_NO_BTN;
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,110 @@
|
||||
|
||||
#ifndef __FPROTO_Acurite_606TX_H__
|
||||
#define __FPROTO_Acurite_606TX_H__
|
||||
|
||||
#include "weatherbase.hpp"
|
||||
|
||||
typedef enum {
|
||||
Acurite_606TXDecoderStepReset = 0,
|
||||
Acurite_606TXDecoderStepSaveDuration,
|
||||
Acurite_606TXDecoderStepCheckDuration,
|
||||
} Acurite_606TXDecoderStep;
|
||||
|
||||
class FProtoWeatherAcurite606TX : public FProtoWeatherBase {
|
||||
public:
|
||||
FProtoWeatherAcurite606TX() {
|
||||
sensorType = FPW_Acurite606TX;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
switch (parser_step) {
|
||||
case Acurite_606TXDecoderStepReset:
|
||||
if ((!level) && (DURATION_DIFF(duration, te_short * 17) < te_delta * 8)) {
|
||||
// Found syncPrefix
|
||||
parser_step = Acurite_606TXDecoderStepSaveDuration;
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case Acurite_606TXDecoderStepSaveDuration:
|
||||
if (level) {
|
||||
te_last = duration;
|
||||
parser_step = Acurite_606TXDecoderStepCheckDuration;
|
||||
} else {
|
||||
parser_step = Acurite_606TXDecoderStepReset;
|
||||
}
|
||||
break;
|
||||
|
||||
case Acurite_606TXDecoderStepCheckDuration:
|
||||
if (!level) {
|
||||
if (DURATION_DIFF(te_last, te_short) <
|
||||
te_delta) {
|
||||
if ((DURATION_DIFF(duration, te_short) <
|
||||
te_delta) ||
|
||||
(duration > te_long * 3)) {
|
||||
// Found syncPostfix
|
||||
parser_step = Acurite_606TXDecoderStepReset;
|
||||
if ((decode_count_bit ==
|
||||
min_count_bit_for_found) &&
|
||||
ws_protocol_acurite_606tx_check()) {
|
||||
data = decode_data;
|
||||
data_count_bit = decode_count_bit;
|
||||
ws_protocol_acurite_606tx_remote_controller();
|
||||
if (callback) callback(this);
|
||||
}
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_long) <
|
||||
te_delta * 2) {
|
||||
subghz_protocol_blocks_add_bit(0);
|
||||
parser_step = Acurite_606TXDecoderStepSaveDuration;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_long * 2) < te_delta * 4) {
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
parser_step = Acurite_606TXDecoderStepSaveDuration;
|
||||
} else {
|
||||
parser_step = Acurite_606TXDecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
parser_step = Acurite_606TXDecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
parser_step = Acurite_606TXDecoderStepReset;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
uint32_t te_short = 500;
|
||||
uint32_t te_long = 2000;
|
||||
uint32_t te_delta = 150;
|
||||
uint32_t min_count_bit_for_found = 32;
|
||||
|
||||
void ws_protocol_acurite_606tx_remote_controller() {
|
||||
id = (data >> 24) & 0xFF;
|
||||
battery_low = (data >> 23) & 1;
|
||||
channel = WS_NO_CHANNEL;
|
||||
if (!((data >> 19) & 1)) {
|
||||
temp = (float)((data >> 8) & 0x07FF) / 10.0f;
|
||||
} else {
|
||||
temp = (float)((~(data >> 8) & 0x07FF) + 1) / -10.0f;
|
||||
}
|
||||
btn = WS_NO_BTN;
|
||||
humidity = WS_NO_HUMIDITY;
|
||||
}
|
||||
bool ws_protocol_acurite_606tx_check() {
|
||||
if (!decode_data) return false;
|
||||
uint8_t msg[] = {
|
||||
static_cast<uint8_t>(decode_data >> 24),
|
||||
static_cast<uint8_t>(decode_data >> 16),
|
||||
static_cast<uint8_t>(decode_data >> 8)};
|
||||
|
||||
uint8_t crc = subghz_protocol_blocks_lfsr_digest8(msg, 3, 0x98, 0xF1);
|
||||
return (crc == (decode_data & 0xFF));
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,112 @@
|
||||
|
||||
#ifndef __FPROTO_Acurite_609TX_H__
|
||||
#define __FPROTO_Acurite_609TX_H__
|
||||
|
||||
#include "weatherbase.hpp"
|
||||
|
||||
typedef enum {
|
||||
Acurite_609TXCDecoderStepReset = 0,
|
||||
Acurite_609TXCDecoderStepSaveDuration,
|
||||
Acurite_609TXCDecoderStepCheckDuration,
|
||||
} Acurite_609TXCDecoderStep;
|
||||
|
||||
class FProtoWeatherAcurite609TX : public FProtoWeatherBase {
|
||||
public:
|
||||
FProtoWeatherAcurite609TX() {
|
||||
sensorType = FPW_Acurite609TX;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
switch (parser_step) {
|
||||
case Acurite_609TXCDecoderStepReset:
|
||||
if ((!level) && (DURATION_DIFF(duration, te_short * 17) <
|
||||
te_delta * 8)) {
|
||||
// Found syncPrefix
|
||||
parser_step = Acurite_609TXCDecoderStepSaveDuration;
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case Acurite_609TXCDecoderStepSaveDuration:
|
||||
if (level) {
|
||||
te_last = duration;
|
||||
parser_step = Acurite_609TXCDecoderStepCheckDuration;
|
||||
} else {
|
||||
parser_step = Acurite_609TXCDecoderStepReset;
|
||||
}
|
||||
break;
|
||||
|
||||
case Acurite_609TXCDecoderStepCheckDuration:
|
||||
if (!level) {
|
||||
if (DURATION_DIFF(te_last, te_short) <
|
||||
te_delta) {
|
||||
if ((DURATION_DIFF(duration, te_short) <
|
||||
te_delta) ||
|
||||
(duration > te_long * 3)) {
|
||||
// Found syncPostfix
|
||||
parser_step = Acurite_609TXCDecoderStepReset;
|
||||
if ((decode_count_bit ==
|
||||
min_count_bit_for_found) &&
|
||||
ws_protocol_acurite_609txc_check()) {
|
||||
data = decode_data;
|
||||
data_count_bit = decode_count_bit;
|
||||
ws_protocol_acurite_609txc_remote_controller();
|
||||
if (callback) callback(this);
|
||||
}
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_long) <
|
||||
te_delta * 2) {
|
||||
subghz_protocol_blocks_add_bit(0);
|
||||
parser_step = Acurite_609TXCDecoderStepSaveDuration;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_long * 2) <
|
||||
te_delta * 4) {
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
parser_step = Acurite_609TXCDecoderStepSaveDuration;
|
||||
} else {
|
||||
parser_step = Acurite_609TXCDecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
parser_step = Acurite_609TXCDecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
parser_step = Acurite_609TXCDecoderStepReset;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
uint32_t te_short = 500;
|
||||
uint32_t te_long = 2000;
|
||||
uint32_t te_delta = 150;
|
||||
uint32_t min_count_bit_for_found = 32;
|
||||
|
||||
void ws_protocol_acurite_609txc_remote_controller() {
|
||||
id = (data >> 32) & 0xFF;
|
||||
battery_low = (data >> 31) & 1;
|
||||
channel = WS_NO_CHANNEL;
|
||||
|
||||
// Temperature in Celsius is encoded as a 12 bit integer value
|
||||
// multiplied by 10 using the 4th - 6th nybbles (bytes 1 & 2)
|
||||
// negative values are recovered by sign extend from int16_t.
|
||||
int16_t temp_raw =
|
||||
(int16_t)(((data >> 12) & 0xf000) | ((data >> 16) << 4));
|
||||
temp = (temp_raw >> 4) * 0.1f;
|
||||
humidity = (data >> 8) & 0xff;
|
||||
btn = WS_NO_BTN;
|
||||
}
|
||||
bool ws_protocol_acurite_609txc_check() {
|
||||
if (!decode_data) return false;
|
||||
uint8_t crc = (uint8_t)(decode_data >> 32) +
|
||||
(uint8_t)(decode_data >> 24) +
|
||||
(uint8_t)(decode_data >> 16) +
|
||||
(uint8_t)(decode_data >> 8);
|
||||
return (crc == (decode_data & 0xFF));
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,138 @@
|
||||
|
||||
#ifndef __FPROTO_Acurite_986_H__
|
||||
#define __FPROTO_Acurite_986_H__
|
||||
|
||||
#include "weatherbase.hpp"
|
||||
|
||||
typedef enum {
|
||||
Acurite_986DecoderStepReset = 0,
|
||||
Acurite_986DecoderStepSync1,
|
||||
Acurite_986DecoderStepSync2,
|
||||
Acurite_986DecoderStepSync3,
|
||||
Acurite_986DecoderStepSaveDuration,
|
||||
Acurite_986DecoderStepCheckDuration,
|
||||
} Acurite_986DecoderStep;
|
||||
|
||||
class FProtoWeatherAcurite986 : public FProtoWeatherBase {
|
||||
public:
|
||||
FProtoWeatherAcurite986() {
|
||||
sensorType = FPW_Acurite986;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
switch (parser_step) {
|
||||
case Acurite_986DecoderStepReset:
|
||||
if ((!level) && (DURATION_DIFF(duration, te_long) < te_delta * 15)) {
|
||||
// Found 1st sync bit
|
||||
parser_step = Acurite_986DecoderStepSync1;
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case Acurite_986DecoderStepSync1:
|
||||
if (DURATION_DIFF(duration, te_long) < te_delta * 15) {
|
||||
if (!level) {
|
||||
parser_step = Acurite_986DecoderStepSync2;
|
||||
}
|
||||
} else {
|
||||
parser_step = Acurite_986DecoderStepReset;
|
||||
}
|
||||
break;
|
||||
|
||||
case Acurite_986DecoderStepSync2:
|
||||
if (DURATION_DIFF(duration, te_long) < te_delta * 15) {
|
||||
if (!level) {
|
||||
parser_step = Acurite_986DecoderStepSync3;
|
||||
}
|
||||
} else {
|
||||
parser_step = Acurite_986DecoderStepReset;
|
||||
}
|
||||
break;
|
||||
|
||||
case Acurite_986DecoderStepSync3:
|
||||
if (DURATION_DIFF(duration, te_long) < te_delta * 15) {
|
||||
if (!level) {
|
||||
parser_step = Acurite_986DecoderStepSaveDuration;
|
||||
}
|
||||
} else {
|
||||
parser_step = Acurite_986DecoderStepReset;
|
||||
}
|
||||
break;
|
||||
|
||||
case Acurite_986DecoderStepSaveDuration:
|
||||
if (level) {
|
||||
te_last = duration;
|
||||
parser_step = Acurite_986DecoderStepCheckDuration;
|
||||
} else {
|
||||
parser_step = Acurite_986DecoderStepReset;
|
||||
}
|
||||
break;
|
||||
|
||||
case Acurite_986DecoderStepCheckDuration:
|
||||
if (!level) {
|
||||
if (DURATION_DIFF(duration, te_short) <
|
||||
te_delta * 10) {
|
||||
if (duration < te_short) {
|
||||
subghz_protocol_blocks_add_bit(0);
|
||||
parser_step = Acurite_986DecoderStepSaveDuration;
|
||||
} else {
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
parser_step = Acurite_986DecoderStepSaveDuration;
|
||||
}
|
||||
} else {
|
||||
// Found syncPostfix
|
||||
parser_step = Acurite_986DecoderStepReset;
|
||||
if ((decode_count_bit == min_count_bit_for_found) && ws_protocol_acurite_986_check()) {
|
||||
data = decode_data;
|
||||
data_count_bit = decode_count_bit;
|
||||
ws_protocol_acurite_986_remote_controller();
|
||||
if (callback) callback(this);
|
||||
}
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
}
|
||||
} else {
|
||||
parser_step = Acurite_986DecoderStepReset;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
uint32_t te_short = 800;
|
||||
uint32_t te_long = 1750;
|
||||
uint32_t te_delta = 50;
|
||||
uint32_t min_count_bit_for_found = 40;
|
||||
|
||||
void ws_protocol_acurite_986_remote_controller() {
|
||||
int temp;
|
||||
|
||||
id = subghz_protocol_blocks_reverse_key(data >> 24, 8);
|
||||
id = (id << 8) | subghz_protocol_blocks_reverse_key(data >> 16, 8);
|
||||
battery_low = (data >> 14) & 1;
|
||||
channel = ((data >> 15) & 1) + 1;
|
||||
|
||||
temp = subghz_protocol_blocks_reverse_key(data >> 32, 8);
|
||||
if (temp & 0x80) {
|
||||
temp = -(temp & 0x7F);
|
||||
}
|
||||
temp = locale_fahrenheit_to_celsius((float)temp);
|
||||
btn = WS_NO_BTN;
|
||||
humidity = WS_NO_HUMIDITY;
|
||||
}
|
||||
|
||||
bool ws_protocol_acurite_986_check() {
|
||||
if (!decode_data) return false;
|
||||
uint8_t msg[] = {
|
||||
(uint8_t)(decode_data >> 32),
|
||||
(uint8_t)(decode_data >> 24),
|
||||
(uint8_t)(decode_data >> 16),
|
||||
(uint8_t)(decode_data >> 8)};
|
||||
|
||||
uint8_t crc = subghz_protocol_blocks_crc8(msg, 4, 0x07, 0x00);
|
||||
return (crc == (decode_data & 0xFF));
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,88 @@
|
||||
|
||||
#ifndef __FPROTO_Ambient_H__
|
||||
#define __FPROTO_Ambient_H__
|
||||
|
||||
#include "weatherbase.hpp"
|
||||
|
||||
#define AMBIENT_WEATHER_PACKET_HEADER_1 0xFFD440000000000 // 0xffd45 .. 0xffd46
|
||||
#define AMBIENT_WEATHER_PACKET_HEADER_2 0x001440000000000 // 0x00145 .. 0x00146
|
||||
#define AMBIENT_WEATHER_PACKET_HEADER_MASK 0xFFFFC0000000000
|
||||
|
||||
class FProtoWeatherAmbient : public FProtoWeatherBase {
|
||||
public:
|
||||
FProtoWeatherAmbient() {
|
||||
sensorType = FPW_Ambient;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
ManchesterEvent event = ManchesterEventReset;
|
||||
if (!level) {
|
||||
if (DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
event = ManchesterEventShortLow;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_long) < te_delta * 2) {
|
||||
event = ManchesterEventLongLow;
|
||||
}
|
||||
} else {
|
||||
if (DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
event = ManchesterEventShortHigh;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_long) < te_delta * 2) {
|
||||
event = ManchesterEventLongHigh;
|
||||
}
|
||||
}
|
||||
if (event != ManchesterEventReset) {
|
||||
bool data;
|
||||
bool data_ok = manchester_advance(manchester_saved_state, event, &manchester_saved_state, &data);
|
||||
|
||||
if (data_ok) {
|
||||
decode_data = (decode_data << 1) | !data;
|
||||
}
|
||||
|
||||
if (((decode_data & AMBIENT_WEATHER_PACKET_HEADER_MASK) == AMBIENT_WEATHER_PACKET_HEADER_1) ||
|
||||
((decode_data & AMBIENT_WEATHER_PACKET_HEADER_MASK) == AMBIENT_WEATHER_PACKET_HEADER_2)) {
|
||||
if (ws_protocol_ambient_weather_check_crc()) {
|
||||
data = decode_data;
|
||||
data_count_bit = min_count_bit_for_found;
|
||||
ws_protocol_ambient_weather_remote_controller();
|
||||
if (callback) callback(this);
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
manchester_advance(manchester_saved_state, ManchesterEventReset, &manchester_saved_state, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
uint32_t te_short = 500;
|
||||
uint32_t te_long = 1000;
|
||||
uint32_t te_delta = 120;
|
||||
uint32_t min_count_bit_for_found = 48;
|
||||
|
||||
bool ws_protocol_ambient_weather_check_crc() {
|
||||
uint8_t msg[] = {
|
||||
static_cast<uint8_t>(decode_data >> 40),
|
||||
static_cast<uint8_t>(decode_data >> 32),
|
||||
static_cast<uint8_t>(decode_data >> 24),
|
||||
static_cast<uint8_t>(decode_data >> 16),
|
||||
static_cast<uint8_t>(decode_data >> 8)};
|
||||
|
||||
uint8_t crc = subghz_protocol_blocks_lfsr_digest8(msg, 5, 0x98, 0x3e) ^ 0x64;
|
||||
return (crc == (uint8_t)(decode_data & 0xFF));
|
||||
}
|
||||
|
||||
void ws_protocol_ambient_weather_remote_controller() {
|
||||
id = (data >> 32) & 0xFF;
|
||||
battery_low = (data >> 31) & 1;
|
||||
channel = ((data >> 28) & 0x07) + 1;
|
||||
temp = locale_fahrenheit_to_celsius(((float)((data >> 16) & 0x0FFF) - 400.0f) / 10.0f);
|
||||
humidity = (data >> 8) & 0xFF;
|
||||
btn = WS_NO_BTN;
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,121 @@
|
||||
|
||||
#ifndef __FPROTO_AuriolAhfl_H__
|
||||
#define __FPROTO_AuriolAhfl_H__
|
||||
|
||||
#include "weatherbase.hpp"
|
||||
|
||||
#define AURIOL_AHFL_CONST_DATA 0b0100
|
||||
|
||||
typedef enum {
|
||||
auriol_AHFLDecoderStepReset = 0,
|
||||
auriol_AHFLDecoderStepSaveDuration,
|
||||
auriol_AHFLDecoderStepCheckDuration,
|
||||
} auriol_AHFLDecoderStep;
|
||||
|
||||
class FProtoWeatherAuriolAhfl : public FProtoWeatherBase {
|
||||
public:
|
||||
FProtoWeatherAuriolAhfl() {
|
||||
sensorType = FPW_AuriolAhfl;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
switch (parser_step) {
|
||||
case auriol_AHFLDecoderStepReset:
|
||||
if ((!level) && (DURATION_DIFF(duration, te_short * 18) <
|
||||
te_delta)) {
|
||||
// Found syncPrefix
|
||||
parser_step = auriol_AHFLDecoderStepSaveDuration;
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
}
|
||||
break;
|
||||
case auriol_AHFLDecoderStepSaveDuration:
|
||||
if (level) {
|
||||
te_last = duration;
|
||||
parser_step = auriol_AHFLDecoderStepCheckDuration;
|
||||
} else {
|
||||
parser_step = auriol_AHFLDecoderStepReset;
|
||||
}
|
||||
break;
|
||||
case auriol_AHFLDecoderStepCheckDuration:
|
||||
if (!level) {
|
||||
if (DURATION_DIFF(te_last, te_short) <
|
||||
te_delta) {
|
||||
if (DURATION_DIFF(duration, te_short * 18) <
|
||||
te_delta * 8) {
|
||||
// Found syncPostfix
|
||||
parser_step = auriol_AHFLDecoderStepReset;
|
||||
if ((decode_count_bit ==
|
||||
min_count_bit_for_found) &&
|
||||
ws_protocol_auriol_ahfl_check()) {
|
||||
data = decode_data;
|
||||
data_count_bit = decode_count_bit;
|
||||
ws_protocol_auriol_ahfl_remote_controller();
|
||||
if (callback) {
|
||||
callback(this);
|
||||
}
|
||||
} else if (decode_count_bit == 1) {
|
||||
parser_step = auriol_AHFLDecoderStepSaveDuration;
|
||||
}
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_long) <
|
||||
te_delta * 2) {
|
||||
subghz_protocol_blocks_add_bit(0);
|
||||
parser_step = auriol_AHFLDecoderStepSaveDuration;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_long * 2) <
|
||||
te_delta * 4) {
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
parser_step = auriol_AHFLDecoderStepSaveDuration;
|
||||
} else {
|
||||
parser_step = auriol_AHFLDecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
parser_step = auriol_AHFLDecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
parser_step = auriol_AHFLDecoderStepReset;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
uint32_t te_short = 500;
|
||||
uint32_t te_long = 2000;
|
||||
uint32_t te_delta = 150;
|
||||
uint32_t min_count_bit_for_found = 42;
|
||||
|
||||
void ws_protocol_auriol_ahfl_remote_controller() {
|
||||
id = data >> 34;
|
||||
battery_low = (data >> 33) & 1;
|
||||
btn = (data >> 32) & 1;
|
||||
channel = ((data >> 30) & 0x3) + 1;
|
||||
if (!((data >> 29) & 1)) {
|
||||
temp = (float)((data >> 18) & 0x07FF) / 10.0f;
|
||||
} else {
|
||||
temp = (float)((~(data >> 18) & 0x07FF) + 1) / -10.0f;
|
||||
}
|
||||
humidity = (data >> 11) & 0x7F;
|
||||
}
|
||||
|
||||
bool ws_protocol_auriol_ahfl_check() {
|
||||
uint8_t type = (decode_data >> 6) & 0x0F;
|
||||
if (type != AURIOL_AHFL_CONST_DATA) {
|
||||
// Fail const data check
|
||||
return false;
|
||||
}
|
||||
uint64_t payload = decode_data >> 6;
|
||||
// Checksum is the last 6 bits of data
|
||||
uint8_t checksum_received = decode_data & 0x3F;
|
||||
uint8_t checksum_calculated = 0;
|
||||
for (uint8_t i = 0; i < 9; i++) {
|
||||
checksum_calculated += (payload >> (i * 4)) & 0xF;
|
||||
}
|
||||
return checksum_received == checksum_calculated;
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,115 @@
|
||||
|
||||
#ifndef __FPROTO_AuriolTH_H__
|
||||
#define __FPROTO_AuriolTH_H__
|
||||
|
||||
#include "weatherbase.hpp"
|
||||
|
||||
#define AURIOL_TH_CONST_DATA 0b1110
|
||||
|
||||
typedef enum {
|
||||
auriol_THDecoderStepReset = 0,
|
||||
auriol_THDecoderStepSaveDuration,
|
||||
auriol_THDecoderStepCheckDuration,
|
||||
} auriol_THDecoderStep;
|
||||
|
||||
class FProtoWeatherAuriolTh : public FProtoWeatherBase {
|
||||
public:
|
||||
FProtoWeatherAuriolTh() {
|
||||
sensorType = FPW_AuriolTH;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
switch (parser_step) {
|
||||
case auriol_THDecoderStepReset:
|
||||
if ((!level) && (DURATION_DIFF(duration, te_short * 8) < te_delta)) {
|
||||
// Found sync
|
||||
parser_step = auriol_THDecoderStepSaveDuration;
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case auriol_THDecoderStepSaveDuration:
|
||||
if (level) {
|
||||
te_last = duration;
|
||||
parser_step = auriol_THDecoderStepCheckDuration;
|
||||
} else {
|
||||
parser_step = auriol_THDecoderStepReset;
|
||||
}
|
||||
break;
|
||||
|
||||
case auriol_THDecoderStepCheckDuration:
|
||||
if (!level) {
|
||||
if (DURATION_DIFF(duration, te_short * 8) <
|
||||
te_delta) {
|
||||
// Found sync
|
||||
parser_step = auriol_THDecoderStepReset;
|
||||
if ((decode_count_bit ==
|
||||
min_count_bit_for_found) &&
|
||||
ws_protocol_auriol_th_check()) {
|
||||
data = decode_data;
|
||||
data_count_bit = decode_count_bit;
|
||||
ws_protocol_auriol_th_remote_controller();
|
||||
if (callback) callback(this);
|
||||
parser_step = auriol_THDecoderStepCheckDuration;
|
||||
}
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
|
||||
break;
|
||||
} else if (
|
||||
(DURATION_DIFF(te_last, te_short) <
|
||||
te_delta) &&
|
||||
(DURATION_DIFF(duration, te_short * 2) <
|
||||
te_delta)) {
|
||||
subghz_protocol_blocks_add_bit(0);
|
||||
parser_step = auriol_THDecoderStepSaveDuration;
|
||||
} else if (
|
||||
(DURATION_DIFF(te_last, te_short) <
|
||||
te_delta) &&
|
||||
(DURATION_DIFF(duration, te_short * 4) <
|
||||
te_delta * 2)) {
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
parser_step = auriol_THDecoderStepSaveDuration;
|
||||
} else {
|
||||
parser_step = auriol_THDecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
parser_step = auriol_THDecoderStepReset;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
uint32_t te_short = 500;
|
||||
uint32_t te_long = 2000;
|
||||
uint32_t te_delta = 150;
|
||||
uint32_t min_count_bit_for_found = 37;
|
||||
|
||||
void ws_protocol_auriol_th_remote_controller() {
|
||||
id = (data >> 31) & 0xFF;
|
||||
battery_low = ((data >> 30) & 1);
|
||||
channel = ((data >> 25) & 0x03) + 1;
|
||||
btn = WS_NO_BTN;
|
||||
if (!((data >> 23) & 1)) {
|
||||
temp = (float)((data >> 13) & 0x07FF) / 10.0f;
|
||||
} else {
|
||||
temp = (float)((~(data >> 13) & 0x07FF) + 1) / -10.0f;
|
||||
}
|
||||
|
||||
humidity = (data >> 1) & 0x7F;
|
||||
}
|
||||
bool ws_protocol_auriol_th_check() {
|
||||
uint8_t type = (decode_data >> 8) & 0x0F;
|
||||
|
||||
if ((type == AURIOL_TH_CONST_DATA) && ((decode_data >> 4) != 0xffffffff)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,121 @@
|
||||
|
||||
#ifndef __FPROTO_GTWT02_H__
|
||||
#define __FPROTO_GTWT02_H__
|
||||
|
||||
#include "weatherbase.hpp"
|
||||
|
||||
#define AURIOL_AHFL_CONST_DATA 0b0100
|
||||
|
||||
typedef enum {
|
||||
GT_WT02DecoderStepReset = 0,
|
||||
GT_WT02DecoderStepSaveDuration,
|
||||
GT_WT02DecoderStepCheckDuration,
|
||||
} GT_WT02DecoderStep;
|
||||
|
||||
class FProtoWeatherGTWT02 : public FProtoWeatherBase {
|
||||
public:
|
||||
FProtoWeatherGTWT02() {
|
||||
sensorType = FPW_GTWT02;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
switch (parser_step) {
|
||||
case GT_WT02DecoderStepReset:
|
||||
if ((!level) && (DURATION_DIFF(duration, te_short * 18) <
|
||||
te_delta * 8)) {
|
||||
// Found syncPrefix
|
||||
parser_step = GT_WT02DecoderStepSaveDuration;
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case GT_WT02DecoderStepSaveDuration:
|
||||
if (level) {
|
||||
te_last = duration;
|
||||
parser_step = GT_WT02DecoderStepCheckDuration;
|
||||
} else {
|
||||
parser_step = GT_WT02DecoderStepReset;
|
||||
}
|
||||
break;
|
||||
|
||||
case GT_WT02DecoderStepCheckDuration:
|
||||
if (!level) {
|
||||
if (DURATION_DIFF(te_last, te_short) <
|
||||
te_delta) {
|
||||
if (DURATION_DIFF(duration, te_short * 18) <
|
||||
te_delta * 8) {
|
||||
// Found syncPostfix
|
||||
parser_step = GT_WT02DecoderStepReset;
|
||||
if ((decode_count_bit ==
|
||||
min_count_bit_for_found) &&
|
||||
ws_protocol_gt_wt_02_check()) {
|
||||
data = decode_data;
|
||||
data_count_bit = decode_count_bit;
|
||||
ws_protocol_gt_wt_02_remote_controller();
|
||||
if (callback) callback(this);
|
||||
} else if (decode_count_bit == 1) {
|
||||
parser_step = GT_WT02DecoderStepSaveDuration;
|
||||
}
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_long) <
|
||||
te_delta * 2) {
|
||||
subghz_protocol_blocks_add_bit(0);
|
||||
parser_step = GT_WT02DecoderStepSaveDuration;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_long * 2) <
|
||||
te_delta * 4) {
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
parser_step = GT_WT02DecoderStepSaveDuration;
|
||||
} else {
|
||||
parser_step = GT_WT02DecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
parser_step = GT_WT02DecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
parser_step = GT_WT02DecoderStepReset;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
uint32_t te_short = 500;
|
||||
uint32_t te_long = 2000;
|
||||
uint32_t te_delta = 150;
|
||||
uint32_t min_count_bit_for_found = 37;
|
||||
|
||||
void ws_protocol_gt_wt_02_remote_controller() {
|
||||
id = (data >> 29) & 0xFF;
|
||||
battery_low = (data >> 28) & 1;
|
||||
btn = (data >> 27) & 1;
|
||||
channel = ((data >> 25) & 0x3) + 1;
|
||||
|
||||
if (!((data >> 24) & 1)) {
|
||||
temp = (float)((data >> 13) & 0x07FF) / 10.0f;
|
||||
} else {
|
||||
temp = (float)((~(data >> 13) & 0x07FF) + 1) / -10.0f;
|
||||
}
|
||||
|
||||
humidity = (data >> 6) & 0x7F;
|
||||
if (humidity <= 10) // actually the sensors sends 10 below working range of 20%
|
||||
humidity = 0;
|
||||
else if (humidity > 90) // actually the sensors sends 110 above working range of 90%
|
||||
humidity = 100;
|
||||
}
|
||||
|
||||
bool ws_protocol_gt_wt_02_check() {
|
||||
if (!decode_data) return false;
|
||||
uint8_t sum = (decode_data >> 5) & 0xe;
|
||||
uint64_t temp_data = decode_data >> 9;
|
||||
for (uint8_t i = 0; i < 7; i++) {
|
||||
sum += (temp_data >> (i * 4)) & 0xF;
|
||||
}
|
||||
return ((uint8_t)(decode_data & 0x3F) == (sum & 0x3F));
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,159 @@
|
||||
|
||||
#ifndef __FPROTO_GTWT03_H__
|
||||
#define __FPROTO_GTWT03_H__
|
||||
|
||||
#include "weatherbase.hpp"
|
||||
|
||||
#define AURIOL_AHFL_CONST_DATA 0b0100
|
||||
|
||||
typedef enum {
|
||||
GT_WT03DecoderStepReset = 0,
|
||||
GT_WT03DecoderStepCheckPreambule,
|
||||
GT_WT03DecoderStepSaveDuration,
|
||||
GT_WT03DecoderStepCheckDuration,
|
||||
} GT_WT03DecoderStep;
|
||||
|
||||
class FProtoWeatherGTWT03 : public FProtoWeatherBase {
|
||||
public:
|
||||
FProtoWeatherGTWT03() {
|
||||
sensorType = FPW_GTWT03;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
switch (parser_step) {
|
||||
case GT_WT03DecoderStepReset:
|
||||
if ((level) && (DURATION_DIFF(duration, te_short * 3) <
|
||||
te_delta * 2)) {
|
||||
parser_step = GT_WT03DecoderStepCheckPreambule;
|
||||
te_last = duration;
|
||||
header_count = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case GT_WT03DecoderStepCheckPreambule:
|
||||
if (level) {
|
||||
te_last = duration;
|
||||
} else {
|
||||
if ((DURATION_DIFF(te_last, te_short * 3) < te_delta * 2) &&
|
||||
(DURATION_DIFF(duration, te_short * 3) < te_delta * 2)) {
|
||||
// Found preambule
|
||||
header_count++;
|
||||
} else if (header_count == 4) {
|
||||
if ((DURATION_DIFF(te_last, te_short) < te_delta) &&
|
||||
(DURATION_DIFF(duration, te_long) < te_delta)) {
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
subghz_protocol_blocks_add_bit(0);
|
||||
parser_step = GT_WT03DecoderStepSaveDuration;
|
||||
} else if (
|
||||
(DURATION_DIFF(te_last, te_long) < te_delta) &&
|
||||
(DURATION_DIFF(duration, te_short) < te_delta)) {
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
parser_step = GT_WT03DecoderStepSaveDuration;
|
||||
} else {
|
||||
parser_step = GT_WT03DecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
parser_step = GT_WT03DecoderStepReset;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case GT_WT03DecoderStepSaveDuration:
|
||||
if (level) {
|
||||
te_last = duration;
|
||||
parser_step = GT_WT03DecoderStepCheckDuration;
|
||||
} else {
|
||||
parser_step = GT_WT03DecoderStepReset;
|
||||
}
|
||||
break;
|
||||
|
||||
case GT_WT03DecoderStepCheckDuration:
|
||||
if (!level) {
|
||||
if (((DURATION_DIFF(te_last, te_short * 3) < te_delta * 2) &&
|
||||
(DURATION_DIFF(duration, te_short * 3) < te_delta * 2))) {
|
||||
if ((decode_count_bit ==
|
||||
min_count_bit_for_found) &&
|
||||
ws_protocol_gt_wt_03_check_crc()) {
|
||||
data = decode_data;
|
||||
data_count_bit = decode_count_bit;
|
||||
ws_protocol_gt_wt_03_remote_controller();
|
||||
if (callback) callback(this);
|
||||
}
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
header_count = 1;
|
||||
parser_step = GT_WT03DecoderStepCheckPreambule;
|
||||
break;
|
||||
} else if (
|
||||
(DURATION_DIFF(te_last, te_short) < te_delta) &&
|
||||
(DURATION_DIFF(duration, te_long) < te_delta)) {
|
||||
subghz_protocol_blocks_add_bit(0);
|
||||
parser_step = GT_WT03DecoderStepSaveDuration;
|
||||
} else if (
|
||||
(DURATION_DIFF(te_last, te_long) < te_delta) &&
|
||||
(DURATION_DIFF(duration, te_short) < te_delta)) {
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
parser_step = GT_WT03DecoderStepSaveDuration;
|
||||
} else {
|
||||
parser_step = GT_WT03DecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
parser_step = GT_WT03DecoderStepReset;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
uint32_t te_short = 285;
|
||||
uint32_t te_long = 570;
|
||||
uint32_t te_delta = 120;
|
||||
uint32_t min_count_bit_for_found = 41;
|
||||
|
||||
void ws_protocol_gt_wt_03_remote_controller() {
|
||||
id = data >> 33;
|
||||
humidity = (data >> 25) & 0xFF;
|
||||
|
||||
if (humidity <= 10) { // actually the sensors sends 10 below working range of 20%
|
||||
humidity = 0;
|
||||
} else if (humidity > 95) { // actually the sensors sends 110 above working range of 90%
|
||||
humidity = 100;
|
||||
}
|
||||
|
||||
battery_low = (data >> 24) & 1;
|
||||
btn = (data >> 23) & 1;
|
||||
channel = ((data >> 21) & 0x03) + 1;
|
||||
|
||||
if (!((data >> 20) & 1)) {
|
||||
temp = (float)((data >> 9) & 0x07FF) / 10.0f;
|
||||
} else {
|
||||
temp = (float)((~(data >> 9) & 0x07FF) + 1) / -10.0f;
|
||||
}
|
||||
}
|
||||
|
||||
bool ws_protocol_gt_wt_03_check_crc() {
|
||||
uint8_t msg[] = {
|
||||
static_cast<uint8_t>(decode_data >> 33),
|
||||
static_cast<uint8_t>(decode_data >> 25),
|
||||
static_cast<uint8_t>(decode_data >> 17),
|
||||
static_cast<uint8_t>(decode_data >> 9)};
|
||||
|
||||
uint8_t sum = 0;
|
||||
for (unsigned k = 0; k < sizeof(msg); ++k) {
|
||||
uint8_t data = msg[k];
|
||||
uint16_t key = 0x3100;
|
||||
for (int i = 7; i >= 0; --i) {
|
||||
// XOR key into sum if data bit is set
|
||||
if ((data >> i) & 1) sum ^= key & 0xff;
|
||||
// roll the key right
|
||||
key = (key >> 1);
|
||||
}
|
||||
}
|
||||
return ((sum ^ (uint8_t)((decode_data >> 1) & 0xFF)) == 0x2D);
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,138 @@
|
||||
|
||||
#ifndef __FPROTO_Infactory_H__
|
||||
#define __FPROTO_Infactory_H__
|
||||
|
||||
#include "weatherbase.hpp"
|
||||
|
||||
typedef enum {
|
||||
InfactoryDecoderStepReset = 0,
|
||||
InfactoryDecoderStepCheckPreambule,
|
||||
InfactoryDecoderStepSaveDuration,
|
||||
InfactoryDecoderStepCheckDuration,
|
||||
} InfactoryDecoderStep;
|
||||
|
||||
class FProtoWeatherInfactory : public FProtoWeatherBase {
|
||||
public:
|
||||
FProtoWeatherInfactory() {
|
||||
sensorType = FPW_INFACTORY;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
switch (parser_step) {
|
||||
case InfactoryDecoderStepReset:
|
||||
if ((level) && (DURATION_DIFF(duration, te_short * 2) <
|
||||
te_delta * 2)) {
|
||||
parser_step = InfactoryDecoderStepCheckPreambule;
|
||||
te_last = duration;
|
||||
header_count = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case InfactoryDecoderStepCheckPreambule:
|
||||
if (level) {
|
||||
te_last = duration;
|
||||
} else {
|
||||
if ((DURATION_DIFF(te_last, te_short * 2) <
|
||||
te_delta * 2) &&
|
||||
(DURATION_DIFF(duration, te_short * 2) <
|
||||
te_delta * 2)) {
|
||||
// Found preambule
|
||||
header_count++;
|
||||
} else if (
|
||||
(DURATION_DIFF(te_last, te_short) <
|
||||
te_delta) &&
|
||||
(DURATION_DIFF(duration, te_short * 16) <
|
||||
te_delta * 8)) {
|
||||
// Found syncPrefix
|
||||
if (header_count > 3) {
|
||||
parser_step = InfactoryDecoderStepSaveDuration;
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
}
|
||||
} else {
|
||||
parser_step = InfactoryDecoderStepReset;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case InfactoryDecoderStepSaveDuration:
|
||||
if (level) {
|
||||
te_last = duration;
|
||||
parser_step = InfactoryDecoderStepCheckDuration;
|
||||
} else {
|
||||
parser_step = InfactoryDecoderStepReset;
|
||||
}
|
||||
break;
|
||||
|
||||
case InfactoryDecoderStepCheckDuration:
|
||||
if (!level) {
|
||||
if (duration >= ((uint32_t)te_short * 30)) {
|
||||
// Found syncPostfix
|
||||
if ((decode_count_bit ==
|
||||
min_count_bit_for_found) &&
|
||||
ws_protocol_infactory_check_crc()) {
|
||||
data = decode_data;
|
||||
data_count_bit = decode_count_bit;
|
||||
ws_protocol_infactory_remote_controller();
|
||||
if (callback) callback(this);
|
||||
}
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
parser_step = InfactoryDecoderStepReset;
|
||||
break;
|
||||
} else if (
|
||||
(DURATION_DIFF(te_last, te_short) <
|
||||
te_delta) &&
|
||||
(DURATION_DIFF(duration, te_long) <
|
||||
te_delta * 2)) {
|
||||
subghz_protocol_blocks_add_bit(0);
|
||||
parser_step = InfactoryDecoderStepSaveDuration;
|
||||
} else if (
|
||||
(DURATION_DIFF(te_last, te_short) <
|
||||
te_delta) &&
|
||||
(DURATION_DIFF(duration, te_long * 2) <
|
||||
te_delta * 4)) {
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
parser_step = InfactoryDecoderStepSaveDuration;
|
||||
} else {
|
||||
parser_step = InfactoryDecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
parser_step = InfactoryDecoderStepReset;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
uint32_t te_short = 500;
|
||||
uint32_t te_long = 2000;
|
||||
uint32_t te_delta = 150;
|
||||
uint32_t min_count_bit_for_found = 40;
|
||||
|
||||
bool ws_protocol_infactory_check_crc() {
|
||||
uint8_t msg[] = {
|
||||
static_cast<uint8_t>(decode_data >> 32),
|
||||
static_cast<uint8_t>(((decode_data >> 24) & 0x0F) | (decode_data & 0x0F) << 4),
|
||||
static_cast<uint8_t>(decode_data >> 16),
|
||||
static_cast<uint8_t>(decode_data >> 8),
|
||||
static_cast<uint8_t>(decode_data)};
|
||||
|
||||
uint8_t crc =
|
||||
subghz_protocol_blocks_crc4(msg, 4, 0x13, 0); // Koopmann 0x9, CCITT-4; FP-4; ITU-T G.704
|
||||
crc ^= msg[4] >> 4; // last nibble is only XORed
|
||||
return (crc == ((decode_data >> 28) & 0x0F));
|
||||
}
|
||||
void ws_protocol_infactory_remote_controller() {
|
||||
id = data >> 32;
|
||||
battery_low = (data >> 26) & 1;
|
||||
btn = WS_NO_BTN;
|
||||
temp =
|
||||
locale_fahrenheit_to_celsius(((float)((data >> 12) & 0x0FFF) - 900.0f) / 10.0f);
|
||||
humidity =
|
||||
(((data >> 8) & 0x0F) * 10) + ((data >> 4) & 0x0F); // BCD, 'A0'=100%rH
|
||||
channel = data & 0x03;
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,163 @@
|
||||
|
||||
#ifndef __FPROTO_LACROSSE_TX_H__
|
||||
#define __FPROTO_LACROSSE_TX_H__
|
||||
|
||||
#include "weatherbase.hpp"
|
||||
|
||||
#define LACROSSE_TX_GAP 1000
|
||||
#define LACROSSE_TX_BIT_SIZE 44
|
||||
#define LACROSSE_TX_SUNC_PATTERN 0x0A000000000
|
||||
#define LACROSSE_TX_SUNC_MASK 0x0F000000000
|
||||
#define LACROSSE_TX_MSG_TYPE_TEMP 0x00
|
||||
#define LACROSSE_TX_MSG_TYPE_HUM 0x0E
|
||||
typedef enum {
|
||||
LaCrosse_TXDecoderStepReset = 0,
|
||||
LaCrosse_TXDecoderStepCheckPreambule,
|
||||
LaCrosse_TXDecoderStepSaveDuration,
|
||||
LaCrosse_TXDecoderStepCheckDuration,
|
||||
} LaCrosse_TXDecoderStep;
|
||||
|
||||
class FProtoWeatherLaCrosseTx : public FProtoWeatherBase {
|
||||
public:
|
||||
FProtoWeatherLaCrosseTx() {
|
||||
sensorType = FPW_LACROSSETX;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
switch (parser_step) {
|
||||
case LaCrosse_TXDecoderStepReset:
|
||||
if ((!level) && (DURATION_DIFF(duration, LACROSSE_TX_GAP) < te_delta * 2)) {
|
||||
parser_step = LaCrosse_TXDecoderStepCheckPreambule;
|
||||
header_count = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case LaCrosse_TXDecoderStepCheckPreambule:
|
||||
|
||||
if (level) {
|
||||
if ((DURATION_DIFF(duration, te_short) < te_delta) &&
|
||||
(header_count > 1)) {
|
||||
parser_step = LaCrosse_TXDecoderStepCheckDuration;
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
te_last = duration;
|
||||
} else if (duration > (te_long * 2)) {
|
||||
parser_step = LaCrosse_TXDecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
if (DURATION_DIFF(duration, LACROSSE_TX_GAP) < te_delta * 2) {
|
||||
te_last = duration;
|
||||
header_count++;
|
||||
} else {
|
||||
parser_step = LaCrosse_TXDecoderStepReset;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case LaCrosse_TXDecoderStepSaveDuration:
|
||||
if (level) {
|
||||
te_last = duration;
|
||||
parser_step = LaCrosse_TXDecoderStepCheckDuration;
|
||||
} else {
|
||||
parser_step = LaCrosse_TXDecoderStepReset;
|
||||
}
|
||||
break;
|
||||
|
||||
case LaCrosse_TXDecoderStepCheckDuration:
|
||||
|
||||
if (!level) {
|
||||
if (duration > LACROSSE_TX_GAP * 3) {
|
||||
if (DURATION_DIFF(te_last, te_short) < te_delta) {
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
parser_step = LaCrosse_TXDecoderStepSaveDuration;
|
||||
} else if (
|
||||
DURATION_DIFF(te_last, te_long) < te_delta) {
|
||||
subghz_protocol_blocks_add_bit(0);
|
||||
parser_step = LaCrosse_TXDecoderStepSaveDuration;
|
||||
}
|
||||
if ((decode_data & LACROSSE_TX_SUNC_MASK) ==
|
||||
LACROSSE_TX_SUNC_PATTERN) {
|
||||
if (ws_protocol_lacrosse_tx_check_crc()) {
|
||||
data = decode_data;
|
||||
data_count_bit = LACROSSE_TX_BIT_SIZE;
|
||||
ws_protocol_lacrosse_tx_remote_controller();
|
||||
if (callback) callback(this);
|
||||
}
|
||||
}
|
||||
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
header_count = 0;
|
||||
parser_step = LaCrosse_TXDecoderStepReset;
|
||||
break;
|
||||
} else if (
|
||||
(DURATION_DIFF(te_last, te_short) < te_delta) &&
|
||||
(DURATION_DIFF(duration, LACROSSE_TX_GAP) < te_delta * 2)) {
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
parser_step = LaCrosse_TXDecoderStepSaveDuration;
|
||||
} else if (
|
||||
(DURATION_DIFF(te_last, te_long) < te_delta) &&
|
||||
(DURATION_DIFF(duration, LACROSSE_TX_GAP) < te_delta * 2)) {
|
||||
subghz_protocol_blocks_add_bit(0);
|
||||
parser_step = LaCrosse_TXDecoderStepSaveDuration;
|
||||
} else {
|
||||
parser_step = LaCrosse_TXDecoderStepReset;
|
||||
}
|
||||
|
||||
} else {
|
||||
parser_step = LaCrosse_TXDecoderStepReset;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
uint32_t te_short = 550;
|
||||
uint32_t te_long = 1300;
|
||||
uint32_t te_delta = 120;
|
||||
uint32_t min_count_bit_for_found = 40;
|
||||
|
||||
void ws_protocol_lacrosse_tx_remote_controller() {
|
||||
uint8_t msg_type = (data >> 32) & 0x0F;
|
||||
id = (((data >> 28) & 0x0F) << 3) | (((data >> 24) & 0x0F) >> 1);
|
||||
|
||||
float msg_value = (float)((data >> 20) & 0x0F) * 10.0f +
|
||||
(float)((data >> 16) & 0x0F) +
|
||||
(float)((data >> 12) & 0x0F) * 0.1f;
|
||||
|
||||
if (msg_type == LACROSSE_TX_MSG_TYPE_TEMP) { //-V1051
|
||||
temp = msg_value - 50.0f;
|
||||
humidity = WS_NO_HUMIDITY;
|
||||
} else if (msg_type == LACROSSE_TX_MSG_TYPE_HUM) {
|
||||
// ToDo for verification, records are needed with sensors maintaining temperature and temperature for this standard
|
||||
humidity = (uint8_t)msg_value;
|
||||
} else {
|
||||
// furi_crash("WS: WSProtocolLaCrosse_TX incorrect msg_type.");
|
||||
}
|
||||
|
||||
btn = WS_NO_BTN;
|
||||
battery_low = WS_NO_BATT;
|
||||
channel = WS_NO_CHANNEL;
|
||||
}
|
||||
|
||||
bool ws_protocol_lacrosse_tx_check_crc() {
|
||||
if (!decode_data) return false;
|
||||
uint8_t msg[] = {
|
||||
static_cast<uint8_t>((decode_data >> 36) & 0x0F),
|
||||
static_cast<uint8_t>((decode_data >> 32) & 0x0F),
|
||||
static_cast<uint8_t>((decode_data >> 28) & 0x0F),
|
||||
static_cast<uint8_t>((decode_data >> 24) & 0x0F),
|
||||
static_cast<uint8_t>((decode_data >> 20) & 0x0F),
|
||||
static_cast<uint8_t>((decode_data >> 16) & 0x0F),
|
||||
static_cast<uint8_t>((decode_data >> 12) & 0x0F),
|
||||
static_cast<uint8_t>((decode_data >> 8) & 0x0F),
|
||||
static_cast<uint8_t>((decode_data >> 4) & 0x0F)};
|
||||
|
||||
uint8_t crc = subghz_protocol_blocks_add_bytes(msg, 9);
|
||||
return ((crc & 0x0F) == (decode_data & 0x0F));
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,135 @@
|
||||
|
||||
#ifndef __FPROTO_LACROSSE_TX141thbv2_H__
|
||||
#define __FPROTO_LACROSSE_TX141thbv2_H__
|
||||
|
||||
#include "weatherbase.hpp"
|
||||
|
||||
#define LACROSSE_TX141TH_BV2_BIT_COUNT 41
|
||||
|
||||
typedef enum {
|
||||
LaCrosse_TX141THBv2DecoderStepReset = 0,
|
||||
LaCrosse_TX141THBv2DecoderStepCheckPreambule,
|
||||
LaCrosse_TX141THBv2DecoderStepSaveDuration,
|
||||
LaCrosse_TX141THBv2DecoderStepCheckDuration,
|
||||
} LaCrosse_TX141THBv2DecoderStep;
|
||||
|
||||
class FProtoWeatherLaCrosseTx141thbv2 : public FProtoWeatherBase {
|
||||
public:
|
||||
FProtoWeatherLaCrosseTx141thbv2() {
|
||||
sensorType = FPW_LACROSSETX141thbv2;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
switch (parser_step) {
|
||||
case LaCrosse_TX141THBv2DecoderStepReset:
|
||||
if ((level) && (DURATION_DIFF(duration, te_short * 4) < te_delta * 2)) {
|
||||
parser_step = LaCrosse_TX141THBv2DecoderStepCheckPreambule;
|
||||
te_last = duration;
|
||||
header_count = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case LaCrosse_TX141THBv2DecoderStepCheckPreambule:
|
||||
if (level) {
|
||||
te_last = duration;
|
||||
} else {
|
||||
if ((DURATION_DIFF(te_last, te_short * 4) < te_delta * 2) &&
|
||||
(DURATION_DIFF(duration, te_short * 4) < te_delta * 2)) {
|
||||
// Found preambule
|
||||
header_count++;
|
||||
} else if (header_count == 4) {
|
||||
if (ws_protocol_decoder_lacrosse_tx141thbv2_add_bit(te_last, duration)) {
|
||||
decode_data = decode_data & 1;
|
||||
decode_count_bit = 1;
|
||||
parser_step = LaCrosse_TX141THBv2DecoderStepSaveDuration;
|
||||
} else {
|
||||
parser_step = LaCrosse_TX141THBv2DecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
parser_step = LaCrosse_TX141THBv2DecoderStepReset;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case LaCrosse_TX141THBv2DecoderStepSaveDuration:
|
||||
if (level) {
|
||||
te_last = duration;
|
||||
parser_step = LaCrosse_TX141THBv2DecoderStepCheckDuration;
|
||||
} else {
|
||||
parser_step = LaCrosse_TX141THBv2DecoderStepReset;
|
||||
}
|
||||
break;
|
||||
|
||||
case LaCrosse_TX141THBv2DecoderStepCheckDuration:
|
||||
if (!level) {
|
||||
if (((DURATION_DIFF(te_last, te_short * 3) < te_delta * 2) &&
|
||||
(DURATION_DIFF(duration, te_short * 4) < te_delta * 2))) {
|
||||
if ((decode_count_bit == min_count_bit_for_found) ||
|
||||
(decode_count_bit == LACROSSE_TX141TH_BV2_BIT_COUNT)) {
|
||||
if (ws_protocol_lacrosse_tx141thbv2_check_crc()) {
|
||||
data = decode_data;
|
||||
data_count_bit = decode_count_bit;
|
||||
ws_protocol_lacrosse_tx141thbv2_remote_controller();
|
||||
if (callback) callback(this);
|
||||
}
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
header_count = 1;
|
||||
parser_step = LaCrosse_TX141THBv2DecoderStepCheckPreambule;
|
||||
break;
|
||||
}
|
||||
} else if (ws_protocol_decoder_lacrosse_tx141thbv2_add_bit(te_last, duration)) {
|
||||
parser_step = LaCrosse_TX141THBv2DecoderStepSaveDuration;
|
||||
} else {
|
||||
parser_step = LaCrosse_TX141THBv2DecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
parser_step = LaCrosse_TX141THBv2DecoderStepReset;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
uint32_t te_short = 208;
|
||||
uint32_t te_long = 417;
|
||||
uint32_t te_delta = 120;
|
||||
uint32_t min_count_bit_for_found = 40;
|
||||
|
||||
bool ws_protocol_lacrosse_tx141thbv2_check_crc() {
|
||||
if (!decode_data) return false;
|
||||
uint64_t data = decode_data;
|
||||
if (decode_count_bit == LACROSSE_TX141TH_BV2_BIT_COUNT) {
|
||||
data >>= 1;
|
||||
}
|
||||
uint8_t msg[] = {static_cast<uint8_t>(data >> 32), static_cast<uint8_t>(data >> 24), static_cast<uint8_t>(data >> 16), static_cast<uint8_t>(data >> 8)};
|
||||
|
||||
uint8_t crc = subghz_protocol_blocks_lfsr_digest8_reflect(msg, 4, 0x31, 0xF4);
|
||||
return (crc == (data & 0xFF));
|
||||
}
|
||||
void ws_protocol_lacrosse_tx141thbv2_remote_controller() {
|
||||
if (data_count_bit == LACROSSE_TX141TH_BV2_BIT_COUNT) {
|
||||
data >>= 1;
|
||||
}
|
||||
id = data >> 32;
|
||||
battery_low = (data >> 31) & 1;
|
||||
btn = (data >> 30) & 1;
|
||||
channel = ((data >> 28) & 0x03) + 1;
|
||||
temp = ((float)((data >> 16) & 0x0FFF) - 500.0f) / 10.0f;
|
||||
humidity = (data >> 8) & 0xFF;
|
||||
}
|
||||
bool ws_protocol_decoder_lacrosse_tx141thbv2_add_bit(uint32_t te_last, uint32_t te_current) {
|
||||
bool ret = false;
|
||||
if (DURATION_DIFF(te_last + te_current, te_short + te_long) < te_delta * 2) {
|
||||
if (te_last > te_current) {
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
} else {
|
||||
subghz_protocol_blocks_add_bit(0);
|
||||
}
|
||||
ret = true;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,121 @@
|
||||
|
||||
#ifndef __FPROTO_NEXUSTH_H__
|
||||
#define __FPROTO_NEXUSTH_H__
|
||||
|
||||
#include "weatherbase.hpp"
|
||||
|
||||
// For the state machine
|
||||
typedef enum {
|
||||
Nexus_THDecoderStepReset = 0,
|
||||
Nexus_THDecoderStepSaveDuration,
|
||||
Nexus_THDecoderStepCheckDuration,
|
||||
} Nexus_THDecoderStep;
|
||||
|
||||
// we will look for this value
|
||||
#define NEXUS_TH_CONST_DATA 0b1111
|
||||
|
||||
class FProtoWeatherNexusTH : public FProtoWeatherBase {
|
||||
public:
|
||||
FProtoWeatherNexusTH() {
|
||||
// must set it's value from the "weathertypes.hpp". getWeatherSensorTypeName() will work with this.
|
||||
sensorType = FPW_NexusTH;
|
||||
}
|
||||
|
||||
// Here we will got a level and duration. eg HIGH (true) for 500. This function must be as fast as possible, to keep the core happy.
|
||||
// From this function we will call the "callback", that is responsible for transmitting the parsed data to the ui. Before calling it, we must populate the base class's variables, like temp, humidity, ...
|
||||
void feed(bool level, uint32_t duration) override {
|
||||
switch (parser_step) {
|
||||
case Nexus_THDecoderStepReset:
|
||||
if ((!level) && (DURATION_DIFF(duration, te_short * 8) <
|
||||
te_delta * 4)) {
|
||||
// Found sync
|
||||
parser_step = Nexus_THDecoderStepSaveDuration;
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case Nexus_THDecoderStepSaveDuration:
|
||||
if (level) {
|
||||
te_last = duration;
|
||||
parser_step = Nexus_THDecoderStepCheckDuration;
|
||||
} else {
|
||||
parser_step = Nexus_THDecoderStepReset;
|
||||
}
|
||||
break;
|
||||
|
||||
case Nexus_THDecoderStepCheckDuration:
|
||||
if (!level) {
|
||||
if (DURATION_DIFF(duration, te_short * 8) < te_delta * 4) {
|
||||
// Found sync
|
||||
parser_step = Nexus_THDecoderStepReset;
|
||||
if ((decode_count_bit == min_count_bit_for_found) &&
|
||||
ws_protocol_nexus_th_check()) {
|
||||
data = decode_data;
|
||||
data_count_bit = decode_count_bit;
|
||||
ws_protocol_nexus_th_remote_controller();
|
||||
if (callback) callback((FProtoWeatherBase*)this);
|
||||
parser_step = Nexus_THDecoderStepCheckDuration;
|
||||
}
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
|
||||
break;
|
||||
} else if ((DURATION_DIFF(te_last, te_short) <
|
||||
te_delta) &&
|
||||
(DURATION_DIFF(duration, te_short * 2) < te_delta * 2)) {
|
||||
subghz_protocol_blocks_add_bit(0);
|
||||
parser_step = Nexus_THDecoderStepSaveDuration;
|
||||
} else if (
|
||||
(DURATION_DIFF(te_last, te_short) < te_delta) &&
|
||||
(DURATION_DIFF(duration, te_short * 4) < te_delta * 4)) {
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
parser_step = Nexus_THDecoderStepSaveDuration;
|
||||
} else {
|
||||
parser_step = Nexus_THDecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
parser_step = Nexus_THDecoderStepReset;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
// timing values
|
||||
uint32_t te_short = 490;
|
||||
uint32_t te_long = 1980;
|
||||
uint32_t te_delta = 150;
|
||||
uint32_t min_count_bit_for_found = 36;
|
||||
|
||||
// sanity check
|
||||
bool ws_protocol_nexus_th_check() {
|
||||
uint8_t type = (decode_data >> 8) & 0x0F;
|
||||
if ((type == NEXUS_TH_CONST_DATA) && ((decode_data >> 4) != 0xffffffff)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// fill the base class's variables before calling the callback
|
||||
void ws_protocol_nexus_th_remote_controller() {
|
||||
id = (data >> 28) & 0xFF;
|
||||
battery_low = !((data >> 27) & 1);
|
||||
channel = ((data >> 24) & 0x03) + 1;
|
||||
btn = WS_NO_BTN;
|
||||
if (!((data >> 23) & 1)) {
|
||||
temp = (float)((data >> 12) & 0x07FF) / 10.0f;
|
||||
} else {
|
||||
temp = (float)((~(data >> 12) & 0x07FF) + 1) / -10.0f;
|
||||
}
|
||||
humidity = data & 0xFF;
|
||||
if (humidity > 95)
|
||||
humidity = 95;
|
||||
else if (humidity < 20)
|
||||
humidity = 20;
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,248 @@
|
||||
|
||||
#ifndef __FPROTO_OREGON2_H__
|
||||
#define __FPROTO_OREGON2_H__
|
||||
|
||||
#include "weatherbase.hpp"
|
||||
|
||||
#define OREGON2_PREAMBLE_BITS 19
|
||||
#define OREGON2_PREAMBLE_MASK 0b1111111111111111111
|
||||
#define OREGON2_SENSOR_ID(d) (((d) >> 16) & 0xFFFF)
|
||||
#define OREGON2_CHECKSUM_BITS 8
|
||||
|
||||
// 15 ones + 0101 (inverted A)
|
||||
#define OREGON2_PREAMBLE 0b1111111111111110101
|
||||
|
||||
// bit indicating the low battery
|
||||
#define OREGON2_FLAG_BAT_LOW 0x4
|
||||
|
||||
/// Documentation for Oregon Scientific protocols can be found here:
|
||||
/// http://wmrx00.sourceforge.net/Arduino/OregonScientific-RF-Protocols.pdf
|
||||
// Sensors ID
|
||||
#define ID_THGR122N 0x1d20
|
||||
#define ID_THGR968 0x1d30
|
||||
#define ID_BTHR918 0x5d50
|
||||
#define ID_BHTR968 0x5d60
|
||||
#define ID_RGR968 0x2d10
|
||||
#define ID_THR228N 0xec40
|
||||
#define ID_THN132N 0xec40 // same as THR228N but different packet size
|
||||
#define ID_RTGN318 0x0cc3 // warning: id is from 0x0cc3 and 0xfcc3
|
||||
#define ID_RTGN129 0x0cc3 // same as RTGN318 but different packet size
|
||||
#define ID_THGR810 0xf824 // This might be ID_THGR81, but what's true is lost in (git) history
|
||||
#define ID_THGR810a 0xf8b4 // unconfirmed version
|
||||
#define ID_THN802 0xc844
|
||||
#define ID_PCR800 0x2914
|
||||
#define ID_PCR800a 0x2d14 // Different PCR800 ID - AU version I think
|
||||
#define ID_WGR800 0x1984
|
||||
#define ID_WGR800a 0x1994 // unconfirmed version
|
||||
#define ID_WGR968 0x3d00
|
||||
#define ID_UV800 0xd874
|
||||
#define ID_THN129 0xcc43 // THN129 Temp only
|
||||
#define ID_RTHN129 0x0cd3 // RTHN129 Temp, clock sensors
|
||||
#define ID_RTHN129_1 0x9cd3
|
||||
#define ID_RTHN129_2 0xacd3
|
||||
#define ID_RTHN129_3 0xbcd3
|
||||
#define ID_RTHN129_4 0xccd3
|
||||
#define ID_RTHN129_5 0xdcd3
|
||||
#define ID_BTHGN129 0x5d53 // Baro, Temp, Hygro sensor
|
||||
#define ID_UVR128 0xec70
|
||||
#define ID_THGR328N 0xcc23 // Temp & Hygro sensor similar to THR228N with 5 channel instead of 3
|
||||
#define ID_RTGR328N_1 0xdcc3 // RTGR328N_[1-5] RFclock(date &time)&Temp&Hygro sensor
|
||||
#define ID_RTGR328N_2 0xccc3
|
||||
#define ID_RTGR328N_3 0xbcc3
|
||||
#define ID_RTGR328N_4 0xacc3
|
||||
#define ID_RTGR328N_5 0x9cc3
|
||||
#define ID_RTGR328N_6 0x8ce3 // RTGR328N_6&7 RFclock(date &time)&Temp&Hygro sensor like THGR328N
|
||||
#define ID_RTGR328N_7 0x8ae3
|
||||
typedef enum {
|
||||
Oregon2DecoderStepReset = 0,
|
||||
Oregon2DecoderStepFoundPreamble,
|
||||
Oregon2DecoderStepVarData,
|
||||
} Oregon2DecoderStep;
|
||||
|
||||
class FProtoWeatherOregon2 : public FProtoWeatherBase {
|
||||
public:
|
||||
FProtoWeatherOregon2() {
|
||||
sensorType = FPW_OREGON2;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) override {
|
||||
// oregon v2.1 signal is inverted
|
||||
ManchesterEvent event = level_and_duration_to_event(!level, duration);
|
||||
bool data;
|
||||
|
||||
// low-level bit sequence decoding
|
||||
if (event == ManchesterEventReset) {
|
||||
parser_step = Oregon2DecoderStepReset;
|
||||
have_bit = false;
|
||||
decode_data = 0UL;
|
||||
decode_count_bit = 0;
|
||||
}
|
||||
if (manchester_advance(manchester_saved_state, event, &manchester_saved_state, &data)) {
|
||||
if (have_bit) {
|
||||
if (!prev_bit && data) {
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
} else if (prev_bit && !data) {
|
||||
subghz_protocol_blocks_add_bit(0);
|
||||
} else {
|
||||
ws_protocol_decoder_oregon2_reset();
|
||||
}
|
||||
have_bit = false;
|
||||
} else {
|
||||
prev_bit = data;
|
||||
have_bit = true;
|
||||
}
|
||||
}
|
||||
|
||||
switch (parser_step) {
|
||||
case Oregon2DecoderStepReset:
|
||||
// waiting for fixed oregon2 preamble
|
||||
if (decode_count_bit >= OREGON2_PREAMBLE_BITS &&
|
||||
((decode_data & OREGON2_PREAMBLE_MASK) == OREGON2_PREAMBLE)) {
|
||||
parser_step = Oregon2DecoderStepFoundPreamble;
|
||||
decode_count_bit = 0;
|
||||
decode_data = 0UL;
|
||||
}
|
||||
break;
|
||||
case Oregon2DecoderStepFoundPreamble:
|
||||
// waiting for fixed oregon2 data
|
||||
if (decode_count_bit == 32) {
|
||||
data = decode_data;
|
||||
data_count_bit = decode_count_bit;
|
||||
decode_data = 0UL;
|
||||
decode_count_bit = 0;
|
||||
|
||||
// reverse nibbles in decoded data
|
||||
data = (data & 0x55555555) << 1 |
|
||||
(data & 0xAAAAAAAA) >> 1;
|
||||
data = (data & 0x33333333) << 2 |
|
||||
(data & 0xCCCCCCCC) >> 2;
|
||||
|
||||
ws_oregon2_decode_const_data();
|
||||
var_bits =
|
||||
oregon2_sensor_id_var_bits(OREGON2_SENSOR_ID(data));
|
||||
|
||||
if (!var_bits) {
|
||||
// sensor is not supported, stop decoding, but showing the decoded fixed part
|
||||
parser_step = Oregon2DecoderStepReset;
|
||||
if (callback) callback(this);
|
||||
} else {
|
||||
parser_step = Oregon2DecoderStepVarData;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Oregon2DecoderStepVarData:
|
||||
// waiting for variable (sensor-specific data)
|
||||
if (decode_count_bit == (uint32_t)var_bits + OREGON2_CHECKSUM_BITS) {
|
||||
var_data = decode_data & 0xFFFFFFFF;
|
||||
|
||||
// reverse nibbles in var data
|
||||
var_data = (var_data & 0x55555555) << 1 |
|
||||
(var_data & 0xAAAAAAAA) >> 1;
|
||||
var_data = (var_data & 0x33333333) << 2 |
|
||||
(var_data & 0xCCCCCCCC) >> 2;
|
||||
|
||||
ws_oregon2_decode_var_data(OREGON2_SENSOR_ID(data), var_data >> OREGON2_CHECKSUM_BITS);
|
||||
|
||||
parser_step = Oregon2DecoderStepReset;
|
||||
if (callback) callback(this);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
// timing values
|
||||
uint32_t te_short = 500;
|
||||
uint32_t te_long = 1000;
|
||||
uint32_t te_delta = 200;
|
||||
uint32_t min_count_bit_for_found = 32;
|
||||
|
||||
bool have_bit = false;
|
||||
bool prev_bit = 0;
|
||||
uint8_t var_bits{0};
|
||||
uint32_t var_data{0};
|
||||
|
||||
void ws_protocol_decoder_oregon2_reset() {
|
||||
parser_step = Oregon2DecoderStepReset;
|
||||
decode_data = 0UL;
|
||||
decode_count_bit = 0;
|
||||
manchester_advance(manchester_saved_state, ManchesterEventReset, &manchester_saved_state, NULL);
|
||||
have_bit = false;
|
||||
var_data = 0;
|
||||
var_bits = 0;
|
||||
}
|
||||
|
||||
ManchesterEvent level_and_duration_to_event(bool level, uint32_t duration) {
|
||||
bool is_long = false;
|
||||
if (DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
is_long = true;
|
||||
} else if (DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
is_long = false;
|
||||
} else {
|
||||
return ManchesterEventReset;
|
||||
}
|
||||
|
||||
if (level)
|
||||
return is_long ? ManchesterEventLongHigh : ManchesterEventShortHigh;
|
||||
else
|
||||
return is_long ? ManchesterEventLongLow : ManchesterEventShortLow;
|
||||
}
|
||||
|
||||
uint8_t oregon2_sensor_id_var_bits(uint16_t sensor_id) {
|
||||
switch (sensor_id) {
|
||||
case ID_THR228N:
|
||||
case ID_RTHN129_1:
|
||||
case ID_RTHN129_2:
|
||||
case ID_RTHN129_3:
|
||||
case ID_RTHN129_4:
|
||||
case ID_RTHN129_5:
|
||||
return 16;
|
||||
case ID_THGR122N:
|
||||
return 24;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
void ws_oregon2_decode_const_data() {
|
||||
id = OREGON2_SENSOR_ID(data);
|
||||
uint8_t ch_bits = (data >> 12) & 0xF;
|
||||
channel = 1;
|
||||
while (ch_bits > 1) {
|
||||
channel++;
|
||||
ch_bits >>= 1;
|
||||
}
|
||||
battery_low = (data & OREGON2_FLAG_BAT_LOW) ? 1 : 0;
|
||||
}
|
||||
uint16_t bcd_decode_short(uint32_t data) {
|
||||
return (data & 0xF) * 10 + ((data >> 4) & 0xF);
|
||||
}
|
||||
float ws_oregon2_decode_temp(uint32_t data) {
|
||||
int32_t temp_val;
|
||||
temp_val = bcd_decode_short(data >> 4);
|
||||
temp_val *= 10;
|
||||
temp_val += (data >> 12) & 0xF;
|
||||
if (data & 0xF) temp_val = -temp_val;
|
||||
return (float)temp_val / 10.0;
|
||||
}
|
||||
void ws_oregon2_decode_var_data(uint16_t sensor_id, uint32_t data) {
|
||||
switch (sensor_id) {
|
||||
case ID_THR228N:
|
||||
case ID_RTHN129_1:
|
||||
case ID_RTHN129_2:
|
||||
case ID_RTHN129_3:
|
||||
case ID_RTHN129_4:
|
||||
case ID_RTHN129_5:
|
||||
temp = ws_oregon2_decode_temp(data);
|
||||
humidity = WS_NO_HUMIDITY;
|
||||
return;
|
||||
case ID_THGR122N:
|
||||
humidity = bcd_decode_short(data);
|
||||
temp = ws_oregon2_decode_temp(data >> 8);
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,173 @@
|
||||
|
||||
#ifndef __FPROTO_OREGON3_H__
|
||||
#define __FPROTO_OREGON3_H__
|
||||
|
||||
#include "weatherbase.hpp"
|
||||
|
||||
#define OREGON3_PREAMBLE_BITS 28
|
||||
#define OREGON3_PREAMBLE_MASK 0b1111111111111111111111111111
|
||||
// 24 ones + 0101 (inverted A)
|
||||
#define OREGON3_PREAMBLE 0b1111111111111111111111110101
|
||||
|
||||
// Fixed part contains:
|
||||
// - Sensor type: 16 bits
|
||||
// - Channel: 4 bits
|
||||
// - ID (changes when batteries are changed): 8 bits
|
||||
// - Battery status: 4 bits
|
||||
#define OREGON3_FIXED_PART_BITS (16 + 4 + 8 + 4)
|
||||
#define OREGON3_SENSOR_ID(d) (((d) >> 16) & 0xFFFF)
|
||||
#define OREGON3_CHECKSUM_BITS 8
|
||||
|
||||
// bit indicating the low battery
|
||||
#define OREGON3_FLAG_BAT_LOW 0x4
|
||||
|
||||
/// Documentation for Oregon Scientific protocols can be found here:
|
||||
/// https://www.osengr.org/Articles/OS-RF-Protocols-IV.pdf
|
||||
// Sensors ID
|
||||
#define ID_THGR221 0xf824
|
||||
typedef enum {
|
||||
Oregon3DecoderStepReset = 0,
|
||||
Oregon3DecoderStepFoundPreamble,
|
||||
Oregon3DecoderStepVarData,
|
||||
} Oregon3DecoderStep;
|
||||
|
||||
class FProtoWeatherOregon3 : public FProtoWeatherBase {
|
||||
public:
|
||||
FProtoWeatherOregon3() {
|
||||
sensorType = FPW_OREGON3;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) override {
|
||||
ManchesterEvent event = level_and_duration_to_event(!level, duration);
|
||||
|
||||
// low-level bit sequence decoding
|
||||
if (event == ManchesterEventReset) {
|
||||
parser_step = Oregon3DecoderStepReset;
|
||||
prev_bit = false;
|
||||
decode_data = 0UL;
|
||||
decode_count_bit = 0;
|
||||
}
|
||||
if (manchester_advance(
|
||||
manchester_saved_state, event, &manchester_saved_state, &prev_bit)) {
|
||||
subghz_protocol_blocks_add_bit(prev_bit);
|
||||
}
|
||||
|
||||
switch (parser_step) {
|
||||
case Oregon3DecoderStepReset:
|
||||
// waiting for fixed oregon3 preamble
|
||||
if (decode_count_bit >= OREGON3_PREAMBLE_BITS &&
|
||||
((decode_data & OREGON3_PREAMBLE_MASK) == OREGON3_PREAMBLE)) {
|
||||
parser_step = Oregon3DecoderStepFoundPreamble;
|
||||
decode_count_bit = 0;
|
||||
decode_data = 0UL;
|
||||
}
|
||||
break;
|
||||
case Oregon3DecoderStepFoundPreamble:
|
||||
// waiting for fixed oregon3 data
|
||||
if (decode_count_bit == OREGON3_FIXED_PART_BITS) {
|
||||
data = decode_data;
|
||||
data_count_bit = decode_count_bit;
|
||||
decode_data = 0UL;
|
||||
decode_count_bit = 0;
|
||||
|
||||
// reverse nibbles in decoded data as oregon v3.0 is LSB first
|
||||
data = (data & 0x55555555) << 1 |
|
||||
(data & 0xAAAAAAAA) >> 1;
|
||||
data = (data & 0x33333333) << 2 |
|
||||
(data & 0xCCCCCCCC) >> 2;
|
||||
|
||||
ws_oregon3_decode_const_data();
|
||||
var_bits =
|
||||
oregon3_sensor_id_var_bits(OREGON3_SENSOR_ID(data));
|
||||
|
||||
if (!var_bits) {
|
||||
// sensor is not supported, stop decoding
|
||||
parser_step = Oregon3DecoderStepReset;
|
||||
} else {
|
||||
parser_step = Oregon3DecoderStepVarData;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Oregon3DecoderStepVarData:
|
||||
// waiting for variable (sensor-specific data)
|
||||
if (decode_count_bit == (uint32_t)var_bits + OREGON3_CHECKSUM_BITS) {
|
||||
var_data = decode_data & 0xFFFFFFFFFFFFFFFF;
|
||||
|
||||
// reverse nibbles in var data
|
||||
var_data = (var_data & 0x5555555555555555) << 1 |
|
||||
(var_data & 0xAAAAAAAAAAAAAAAA) >> 1;
|
||||
var_data = (var_data & 0x3333333333333333) << 2 |
|
||||
(var_data & 0xCCCCCCCCCCCCCCCC) >> 2;
|
||||
|
||||
ws_oregon3_decode_var_data(OREGON3_SENSOR_ID(data), var_data >> OREGON3_CHECKSUM_BITS);
|
||||
parser_step = Oregon3DecoderStepReset;
|
||||
if (callback) callback(this);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
// timing values
|
||||
uint32_t te_short = 500;
|
||||
uint32_t te_long = 1100;
|
||||
uint32_t te_delta = 300;
|
||||
uint32_t min_count_bit_for_found = 32;
|
||||
|
||||
bool prev_bit = false;
|
||||
uint8_t var_bits{0};
|
||||
uint64_t var_data{0};
|
||||
|
||||
ManchesterEvent level_and_duration_to_event(bool level, uint32_t duration) {
|
||||
bool is_long = false;
|
||||
|
||||
if (DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
is_long = true;
|
||||
} else if (DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
is_long = false;
|
||||
} else {
|
||||
return ManchesterEventReset;
|
||||
}
|
||||
|
||||
if (level)
|
||||
return is_long ? ManchesterEventLongHigh : ManchesterEventShortHigh;
|
||||
else
|
||||
return is_long ? ManchesterEventLongLow : ManchesterEventShortLow;
|
||||
}
|
||||
uint8_t oregon3_sensor_id_var_bits(uint16_t sensor_id) {
|
||||
switch (sensor_id) {
|
||||
case ID_THGR221:
|
||||
// nibbles: temp + hum + '0'
|
||||
return (4 + 2 + 1) * 4;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
void ws_oregon3_decode_const_data() {
|
||||
id = OREGON3_SENSOR_ID(data);
|
||||
channel = (data >> 12) & 0xF;
|
||||
battery_low = (data & OREGON3_FLAG_BAT_LOW) ? 1 : 0;
|
||||
}
|
||||
uint16_t ws_oregon3_bcd_decode_short(uint32_t data) {
|
||||
return (data & 0xF) * 10 + ((data >> 4) & 0xF);
|
||||
}
|
||||
float ws_oregon3_decode_temp(uint32_t data) {
|
||||
int32_t temp_val;
|
||||
temp_val = ws_oregon3_bcd_decode_short(data >> 4);
|
||||
temp_val *= 10;
|
||||
temp_val += (data >> 12) & 0xF;
|
||||
if (data & 0xF) temp_val = -temp_val;
|
||||
return (float)temp_val / 10.0;
|
||||
}
|
||||
void ws_oregon3_decode_var_data(uint16_t sensor_id, uint32_t data) {
|
||||
switch (sensor_id) {
|
||||
case ID_THGR221:
|
||||
default:
|
||||
humidity = ws_oregon3_bcd_decode_short(data >> 4);
|
||||
temp = ws_oregon3_decode_temp(data >> 12);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,176 @@
|
||||
|
||||
#ifndef __FPROTO_OREGONv1_H__
|
||||
#define __FPROTO_OREGONv1_H__
|
||||
|
||||
#include "weatherbase.hpp"
|
||||
|
||||
typedef enum {
|
||||
Oregon_V1DecoderStepReset = 0,
|
||||
Oregon_V1DecoderStepFoundPreamble,
|
||||
Oregon_V1DecoderStepParse,
|
||||
} Oregon_V1DecoderStep;
|
||||
|
||||
#define OREGON_V1_HEADER_OK 0xFF
|
||||
|
||||
class FProtoWeatherOregonV1 : public FProtoWeatherBase {
|
||||
public:
|
||||
FProtoWeatherOregonV1() {
|
||||
sensorType = FPW_OREGONv1;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) override {
|
||||
ManchesterEvent event = ManchesterEventReset;
|
||||
switch (parser_step) {
|
||||
case Oregon_V1DecoderStepReset:
|
||||
if ((level) && (DURATION_DIFF(duration, te_short) <
|
||||
te_delta)) {
|
||||
parser_step = Oregon_V1DecoderStepFoundPreamble;
|
||||
te_last = duration;
|
||||
header_count = 0;
|
||||
}
|
||||
break;
|
||||
case Oregon_V1DecoderStepFoundPreamble:
|
||||
if (level) {
|
||||
// keep high levels, if they suit our durations
|
||||
if ((DURATION_DIFF(duration, te_short) <
|
||||
te_delta) ||
|
||||
(DURATION_DIFF(duration, te_short * 4) <
|
||||
te_delta)) {
|
||||
te_last = duration;
|
||||
} else {
|
||||
parser_step = Oregon_V1DecoderStepReset;
|
||||
}
|
||||
} else if (
|
||||
// checking low levels
|
||||
(DURATION_DIFF(duration, te_short) <
|
||||
te_delta) &&
|
||||
(DURATION_DIFF(te_last, te_short) <
|
||||
te_delta)) {
|
||||
// Found header
|
||||
header_count++;
|
||||
} else if (
|
||||
(DURATION_DIFF(duration, te_short * 3) <
|
||||
te_delta) &&
|
||||
(DURATION_DIFF(te_last, te_short) <
|
||||
te_delta)) {
|
||||
// check header
|
||||
if (header_count > 7) {
|
||||
header_count = OREGON_V1_HEADER_OK;
|
||||
}
|
||||
} else if (
|
||||
(header_count == OREGON_V1_HEADER_OK) &&
|
||||
(DURATION_DIFF(te_last, te_short * 4) <
|
||||
te_delta)) {
|
||||
// found all the necessary patterns
|
||||
decode_data = 0;
|
||||
decode_count_bit = 1;
|
||||
manchester_advance(
|
||||
manchester_saved_state,
|
||||
ManchesterEventReset,
|
||||
&manchester_saved_state,
|
||||
NULL);
|
||||
parser_step = Oregon_V1DecoderStepParse;
|
||||
if (duration < te_short * 4) {
|
||||
first_bit = 1;
|
||||
} else {
|
||||
first_bit = 0;
|
||||
}
|
||||
} else {
|
||||
parser_step = Oregon_V1DecoderStepReset;
|
||||
}
|
||||
break;
|
||||
case Oregon_V1DecoderStepParse:
|
||||
if (level) {
|
||||
if (DURATION_DIFF(duration, te_short) <
|
||||
te_delta) {
|
||||
event = ManchesterEventShortHigh;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_long) <
|
||||
te_delta) {
|
||||
event = ManchesterEventLongHigh;
|
||||
} else {
|
||||
parser_step = Oregon_V1DecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
if (DURATION_DIFF(duration, te_short) <
|
||||
te_delta) {
|
||||
event = ManchesterEventShortLow;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_long) <
|
||||
te_delta) {
|
||||
event = ManchesterEventLongLow;
|
||||
} else if (duration >= ((uint32_t)te_long * 2)) {
|
||||
if (decode_count_bit ==
|
||||
min_count_bit_for_found) {
|
||||
if (first_bit) {
|
||||
decode_data = ~decode_data | (1 << 31);
|
||||
}
|
||||
if (ws_protocol_oregon_v1_check()) {
|
||||
data = decode_data;
|
||||
data_count_bit = decode_count_bit;
|
||||
ws_protocol_oregon_v1_remote_controller();
|
||||
if (callback) callback(this);
|
||||
}
|
||||
}
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
manchester_advance(
|
||||
manchester_saved_state,
|
||||
ManchesterEventReset,
|
||||
&manchester_saved_state,
|
||||
NULL);
|
||||
} else {
|
||||
parser_step = Oregon_V1DecoderStepReset;
|
||||
}
|
||||
}
|
||||
if (event != ManchesterEventReset) {
|
||||
bool data;
|
||||
bool data_ok = manchester_advance(
|
||||
manchester_saved_state, event, &manchester_saved_state, &data);
|
||||
|
||||
if (data_ok) {
|
||||
decode_data = (decode_data << 1) | !data;
|
||||
decode_count_bit++;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
// timing values
|
||||
uint32_t te_short = 1465;
|
||||
uint32_t te_long = 2930;
|
||||
uint32_t te_delta = 350;
|
||||
uint32_t min_count_bit_for_found = 32;
|
||||
|
||||
uint8_t first_bit{0};
|
||||
|
||||
bool ws_protocol_oregon_v1_check() {
|
||||
if (!decode_data) return false;
|
||||
uint64_t data = subghz_protocol_blocks_reverse_key(decode_data, 32);
|
||||
uint16_t crc = (data & 0xff) + ((data >> 8) & 0xff) + ((data >> 16) & 0xff);
|
||||
crc = (crc & 0xff) + ((crc >> 8) & 0xff);
|
||||
return (crc == ((data >> 24) & 0xFF));
|
||||
}
|
||||
void ws_protocol_oregon_v1_remote_controller() {
|
||||
uint64_t data2 = subghz_protocol_blocks_reverse_key(data, 32);
|
||||
|
||||
id = data2 & 0xFF;
|
||||
channel = ((data2 >> 6) & 0x03) + 1;
|
||||
|
||||
float temp_raw =
|
||||
((data2 >> 8) & 0x0F) * 0.1f + ((data2 >> 12) & 0x0F) + ((data2 >> 16) & 0x0F) * 10.0f;
|
||||
if (!((data2 >> 21) & 1)) {
|
||||
temp = temp_raw;
|
||||
} else {
|
||||
temp = -temp_raw;
|
||||
}
|
||||
battery_low = !((data2 >> 23) & 1ULL);
|
||||
btn = WS_NO_BTN;
|
||||
humidity = WS_NO_HUMIDITY;
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,110 @@
|
||||
|
||||
#ifndef __FPROTO_THERMOPROTX4_H__
|
||||
#define __FPROTO_THERMOPROTX4_H__
|
||||
|
||||
#include "weatherbase.hpp"
|
||||
#define THERMO_PRO_TX4_TYPE_1 0b1001
|
||||
#define THERMO_PRO_TX4_TYPE_2 0b0110
|
||||
|
||||
typedef enum {
|
||||
ThermoPRO_TX4DecoderStepReset = 0,
|
||||
ThermoPRO_TX4DecoderStepSaveDuration,
|
||||
ThermoPRO_TX4DecoderStepCheckDuration,
|
||||
} ThermoPRO_TX4DecoderStep;
|
||||
|
||||
class FProtoWeatherThermoProTx4 : public FProtoWeatherBase {
|
||||
public:
|
||||
FProtoWeatherThermoProTx4() {
|
||||
sensorType = FPW_THERMOPROTX4;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) override {
|
||||
switch (parser_step) {
|
||||
case ThermoPRO_TX4DecoderStepReset:
|
||||
if ((!level) && (DURATION_DIFF(duration, te_short * 18) < te_delta * 10)) {
|
||||
// Found sync
|
||||
parser_step = ThermoPRO_TX4DecoderStepSaveDuration;
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case ThermoPRO_TX4DecoderStepSaveDuration:
|
||||
if (level) {
|
||||
te_last = duration;
|
||||
parser_step = ThermoPRO_TX4DecoderStepCheckDuration;
|
||||
} else {
|
||||
parser_step = ThermoPRO_TX4DecoderStepReset;
|
||||
}
|
||||
break;
|
||||
|
||||
case ThermoPRO_TX4DecoderStepCheckDuration:
|
||||
if (!level) {
|
||||
if (DURATION_DIFF(duration, te_short * 18) < te_delta * 10) {
|
||||
// Found sync
|
||||
parser_step = ThermoPRO_TX4DecoderStepReset;
|
||||
if ((decode_count_bit == min_count_bit_for_found) &&
|
||||
ws_protocol_thermopro_tx4_check()) {
|
||||
data = decode_data;
|
||||
data_count_bit = decode_count_bit;
|
||||
ws_protocol_thermopro_tx4_remote_controller();
|
||||
if (callback) callback(this);
|
||||
parser_step = ThermoPRO_TX4DecoderStepCheckDuration;
|
||||
}
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
|
||||
break;
|
||||
} else if (
|
||||
(DURATION_DIFF(
|
||||
te_last, te_short) < te_delta) &&
|
||||
(DURATION_DIFF(duration, te_long) < te_delta * 2)) {
|
||||
subghz_protocol_blocks_add_bit(0);
|
||||
parser_step = ThermoPRO_TX4DecoderStepSaveDuration;
|
||||
} else if (
|
||||
(DURATION_DIFF(
|
||||
te_last, te_short) < te_delta) &&
|
||||
(DURATION_DIFF(duration, te_long * 2) < te_delta * 4)) {
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
parser_step = ThermoPRO_TX4DecoderStepSaveDuration;
|
||||
} else {
|
||||
parser_step = ThermoPRO_TX4DecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
parser_step = ThermoPRO_TX4DecoderStepReset;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
// timing values
|
||||
uint32_t te_short = 500;
|
||||
uint32_t te_long = 2000;
|
||||
uint32_t te_delta = 150;
|
||||
uint32_t min_count_bit_for_found = 37;
|
||||
|
||||
bool ws_protocol_thermopro_tx4_check() {
|
||||
uint8_t type = decode_data >> 33;
|
||||
if ((type == THERMO_PRO_TX4_TYPE_1) || (type == THERMO_PRO_TX4_TYPE_2)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void ws_protocol_thermopro_tx4_remote_controller() {
|
||||
id = (data >> 25) & 0xFF;
|
||||
battery_low = (data >> 24) & 1;
|
||||
btn = (data >> 23) & 1;
|
||||
channel = ((data >> 21) & 0x03) + 1;
|
||||
if (!((data >> 20) & 1)) {
|
||||
temp = (float)((data >> 9) & 0x07FF) / 10.0f;
|
||||
} else {
|
||||
temp = (float)((~(data >> 9) & 0x07FF) + 1) / -10.0f;
|
||||
}
|
||||
humidity = (data >> 1) & 0xFF;
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,141 @@
|
||||
|
||||
#ifndef __FPROTO_TX8300_H__
|
||||
#define __FPROTO_TX8300_H__
|
||||
|
||||
#include "weatherbase.hpp"
|
||||
typedef enum {
|
||||
TX_8300DecoderStepReset = 0,
|
||||
TX_8300DecoderStepCheckPreambule,
|
||||
TX_8300DecoderStepSaveDuration,
|
||||
TX_8300DecoderStepCheckDuration,
|
||||
} TX_8300DecoderStep;
|
||||
#define TX_8300_PACKAGE_SIZE 32
|
||||
|
||||
class FProtoWeatherTX8300 : public FProtoWeatherBase {
|
||||
public:
|
||||
FProtoWeatherTX8300() {
|
||||
sensorType = FPW_TX_8300;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) override {
|
||||
switch (parser_step) {
|
||||
case TX_8300DecoderStepReset:
|
||||
if ((level) && (DURATION_DIFF(duration, te_short * 2) < te_delta)) {
|
||||
parser_step = TX_8300DecoderStepCheckPreambule;
|
||||
}
|
||||
break;
|
||||
|
||||
case TX_8300DecoderStepCheckPreambule:
|
||||
if ((!level) && ((DURATION_DIFF(duration, te_short * 2) < te_delta) ||
|
||||
(DURATION_DIFF(duration, te_short * 3) < te_delta))) {
|
||||
parser_step = TX_8300DecoderStepSaveDuration;
|
||||
decode_data = 0;
|
||||
decode_count_bit = 1;
|
||||
package_1 = 0;
|
||||
package_2 = 0;
|
||||
} else {
|
||||
parser_step = TX_8300DecoderStepReset;
|
||||
}
|
||||
break;
|
||||
|
||||
case TX_8300DecoderStepSaveDuration:
|
||||
if (level) {
|
||||
te_last = duration;
|
||||
parser_step = TX_8300DecoderStepCheckDuration;
|
||||
} else {
|
||||
parser_step = TX_8300DecoderStepReset;
|
||||
}
|
||||
break;
|
||||
|
||||
case TX_8300DecoderStepCheckDuration:
|
||||
if (!level) {
|
||||
if (duration >= ((uint32_t)te_short * 5)) {
|
||||
// Found syncPostfix
|
||||
if ((decode_count_bit ==
|
||||
min_count_bit_for_found) &&
|
||||
ws_protocol_tx_8300_check_crc()) {
|
||||
data = package_1;
|
||||
data_count_bit = decode_count_bit;
|
||||
ws_protocol_tx_8300_remote_controller();
|
||||
if (callback) callback(this);
|
||||
}
|
||||
decode_data = 0;
|
||||
decode_count_bit = 1;
|
||||
parser_step = TX_8300DecoderStepReset;
|
||||
break;
|
||||
} else if (
|
||||
(DURATION_DIFF(te_last, te_short) <
|
||||
te_delta) &&
|
||||
(DURATION_DIFF(duration, te_long) <
|
||||
te_delta * 2)) {
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
parser_step = TX_8300DecoderStepSaveDuration;
|
||||
} else if (
|
||||
(DURATION_DIFF(te_last, te_short) <
|
||||
te_delta) &&
|
||||
(DURATION_DIFF(duration, te_short) <
|
||||
te_delta)) {
|
||||
subghz_protocol_blocks_add_bit(0);
|
||||
parser_step = TX_8300DecoderStepSaveDuration;
|
||||
} else {
|
||||
parser_step = TX_8300DecoderStepReset;
|
||||
}
|
||||
|
||||
if (decode_count_bit == TX_8300_PACKAGE_SIZE) {
|
||||
package_1 = decode_data;
|
||||
decode_data = 0;
|
||||
} else if (decode_count_bit == TX_8300_PACKAGE_SIZE * 2) {
|
||||
package_2 = decode_data;
|
||||
decode_data = 0;
|
||||
}
|
||||
|
||||
} else {
|
||||
parser_step = TX_8300DecoderStepReset;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
// timing values
|
||||
uint32_t te_short = 1940;
|
||||
uint32_t te_long = 3880;
|
||||
uint32_t te_delta = 250;
|
||||
uint32_t min_count_bit_for_found = 72;
|
||||
|
||||
uint32_t package_1{0};
|
||||
uint32_t package_2{0};
|
||||
bool ws_protocol_tx_8300_check_crc() {
|
||||
if (!package_2) return false;
|
||||
if (package_1 != ~package_2) return false;
|
||||
|
||||
uint16_t x = 0;
|
||||
uint16_t y = 0;
|
||||
for (int i = 0; i < 32; i += 4) {
|
||||
x += (package_1 >> i) & 0x0F;
|
||||
y += (package_1 >> i) & 0x05;
|
||||
}
|
||||
uint8_t crc = (~x & 0xF) << 4 | (~y & 0xF);
|
||||
return (crc == (decode_data & 0xFF));
|
||||
}
|
||||
void ws_protocol_tx_8300_remote_controller() {
|
||||
humidity = (((data >> 28) & 0x0F) * 10) + ((data >> 24) & 0x0F);
|
||||
btn = WS_NO_BTN;
|
||||
if (!((data >> 22) & 0x03))
|
||||
battery_low = 0;
|
||||
else
|
||||
battery_low = 1;
|
||||
channel = (data >> 20) & 0x03;
|
||||
id = (data >> 12) & 0x7F;
|
||||
|
||||
float temp_raw = ((data >> 8) & 0x0F) * 10.0f + ((data >> 4) & 0x0F) +
|
||||
(data & 0x0F) * 0.1f;
|
||||
if (!((data >> 19) & 1)) {
|
||||
temp = temp_raw;
|
||||
} else {
|
||||
temp = -temp_raw;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,156 @@
|
||||
|
||||
#ifndef __FPROTO_WENDOX_W6726_H__
|
||||
#define __FPROTO_WENDOX_W6726_H__
|
||||
|
||||
#include "weatherbase.hpp"
|
||||
typedef enum {
|
||||
WendoxW6726DecoderStepReset = 0,
|
||||
WendoxW6726DecoderStepCheckPreambule,
|
||||
WendoxW6726DecoderStepSaveDuration,
|
||||
WendoxW6726DecoderStepCheckDuration,
|
||||
} WendoxW6726DecoderStep;
|
||||
|
||||
class FProtoWeatherWendoxW6726 : public FProtoWeatherBase {
|
||||
public:
|
||||
FProtoWeatherWendoxW6726() {
|
||||
sensorType = FPW_WENDOX_W6726;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) override {
|
||||
switch (parser_step) {
|
||||
case WendoxW6726DecoderStepReset:
|
||||
if ((level) && (DURATION_DIFF(duration, te_short) <
|
||||
te_delta)) {
|
||||
parser_step = WendoxW6726DecoderStepCheckPreambule;
|
||||
te_last = duration;
|
||||
header_count = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case WendoxW6726DecoderStepCheckPreambule:
|
||||
if (level) {
|
||||
te_last = duration;
|
||||
} else {
|
||||
if ((DURATION_DIFF(te_last, te_short) <
|
||||
te_delta * 1) &&
|
||||
(DURATION_DIFF(duration, te_long) <
|
||||
te_delta * 2)) {
|
||||
header_count++;
|
||||
} else if ((header_count > 4) && (header_count < 12)) {
|
||||
if ((DURATION_DIFF(
|
||||
te_last, te_long) <
|
||||
te_delta * 2) &&
|
||||
(DURATION_DIFF(duration, te_short) <
|
||||
te_delta)) {
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
parser_step = WendoxW6726DecoderStepSaveDuration;
|
||||
} else {
|
||||
parser_step = WendoxW6726DecoderStepReset;
|
||||
}
|
||||
|
||||
} else {
|
||||
parser_step = WendoxW6726DecoderStepReset;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case WendoxW6726DecoderStepSaveDuration:
|
||||
if (level) {
|
||||
te_last = duration;
|
||||
parser_step = WendoxW6726DecoderStepCheckDuration;
|
||||
} else {
|
||||
parser_step = WendoxW6726DecoderStepReset;
|
||||
}
|
||||
break;
|
||||
|
||||
case WendoxW6726DecoderStepCheckDuration:
|
||||
if (!level) {
|
||||
if (duration >
|
||||
te_short + te_long) {
|
||||
if (DURATION_DIFF(te_last, te_short) < te_delta) {
|
||||
subghz_protocol_blocks_add_bit(0);
|
||||
parser_step = WendoxW6726DecoderStepSaveDuration;
|
||||
} else if (
|
||||
DURATION_DIFF(te_last, te_long) < te_delta * 2) {
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
parser_step = WendoxW6726DecoderStepSaveDuration;
|
||||
} else {
|
||||
parser_step = WendoxW6726DecoderStepReset;
|
||||
}
|
||||
if ((decode_count_bit ==
|
||||
min_count_bit_for_found) &&
|
||||
ws_protocol_wendox_w6726_check()) {
|
||||
data = decode_data;
|
||||
data_count_bit = decode_count_bit;
|
||||
ws_protocol_wendox_w6726_remote_controller();
|
||||
if (callback) callback(this);
|
||||
}
|
||||
|
||||
parser_step = WendoxW6726DecoderStepReset;
|
||||
} else if (
|
||||
(DURATION_DIFF(te_last, te_short) <
|
||||
te_delta) &&
|
||||
(DURATION_DIFF(duration, te_long) <
|
||||
te_delta * 3)) {
|
||||
subghz_protocol_blocks_add_bit(0);
|
||||
parser_step = WendoxW6726DecoderStepSaveDuration;
|
||||
} else if (
|
||||
(DURATION_DIFF(te_last, te_long) <
|
||||
te_delta * 2) &&
|
||||
(DURATION_DIFF(duration, te_short) <
|
||||
te_delta)) {
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
parser_step = WendoxW6726DecoderStepSaveDuration;
|
||||
} else {
|
||||
parser_step = WendoxW6726DecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
parser_step = WendoxW6726DecoderStepReset;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
// timing values
|
||||
uint32_t te_short = 1955;
|
||||
uint32_t te_long = 5865;
|
||||
uint32_t te_delta = 300;
|
||||
uint32_t min_count_bit_for_found = 29;
|
||||
|
||||
bool ws_protocol_wendox_w6726_check() {
|
||||
if (!decode_data) return false;
|
||||
uint8_t msg[] = {
|
||||
static_cast<uint8_t>(decode_data >> 28),
|
||||
static_cast<uint8_t>(decode_data >> 20),
|
||||
static_cast<uint8_t>(decode_data >> 12),
|
||||
static_cast<uint8_t>(decode_data >> 4)};
|
||||
|
||||
uint8_t crc = subghz_protocol_blocks_crc4(msg, 4, 0x9, 0xD);
|
||||
return (crc == (decode_data & 0x0F));
|
||||
}
|
||||
void ws_protocol_wendox_w6726_remote_controller() {
|
||||
id = (data >> 24) & 0xFF;
|
||||
battery_low = (data >> 6) & 1;
|
||||
channel = WS_NO_CHANNEL;
|
||||
|
||||
if (((data >> 23) & 1)) {
|
||||
temp = (float)(((data >> 14) & 0x1FF) + 12) / 10.0f;
|
||||
} else {
|
||||
temp = (float)((~(data >> 14) & 0x1FF) + 1 - 12) / -10.0f;
|
||||
}
|
||||
|
||||
if (temp < -50.0f) {
|
||||
temp = -50.0f;
|
||||
} else if (temp > 70.0f) {
|
||||
temp = 70.0f;
|
||||
}
|
||||
|
||||
btn = WS_NO_BTN;
|
||||
humidity = WS_NO_HUMIDITY;
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,230 @@
|
||||
/*
|
||||
Base class for all weather protocols.
|
||||
This and most of the weather protocols uses code from Flipper XTreme codebase ( https://github.com/Flipper-XFW/Xtreme-Firmware/tree/dev/lib/subghz ). Thanks for their work!
|
||||
For comments in a protocol implementation check w-nexus-th.hpp
|
||||
*/
|
||||
|
||||
#ifndef __FPROTO_BASE_H__
|
||||
#define __FPROTO_BASE_H__
|
||||
|
||||
#define bit_read(value, bit) (((value) >> (bit)) & 0x01)
|
||||
|
||||
#include "weathertypes.hpp"
|
||||
|
||||
#include <string>
|
||||
// default walues to indicate 'no value'
|
||||
#define WS_NO_ID 0xFFFFFFFF
|
||||
#define WS_NO_BATT 0xFF
|
||||
#define WS_NO_HUMIDITY 0xFF
|
||||
#define WS_NO_CHANNEL 0xFF
|
||||
#define WS_NO_BTN 0xFF
|
||||
#define WS_NO_TEMPERATURE -273.0f
|
||||
|
||||
#define DURATION_DIFF(x, y) (((x) < (y)) ? ((y) - (x)) : ((x) - (y)))
|
||||
typedef enum {
|
||||
ManchesterStateStart1 = 0,
|
||||
ManchesterStateMid1 = 1,
|
||||
ManchesterStateMid0 = 2,
|
||||
ManchesterStateStart0 = 3
|
||||
} ManchesterState;
|
||||
typedef enum {
|
||||
ManchesterEventShortLow = 0,
|
||||
ManchesterEventShortHigh = 2,
|
||||
ManchesterEventLongLow = 4,
|
||||
ManchesterEventLongHigh = 6,
|
||||
ManchesterEventReset = 8
|
||||
} ManchesterEvent;
|
||||
class FProtoWeatherBase;
|
||||
typedef void (*SubGhzProtocolDecoderBaseRxCallback)(FProtoWeatherBase* instance);
|
||||
|
||||
class FProtoWeatherBase {
|
||||
public:
|
||||
FProtoWeatherBase() {}
|
||||
virtual ~FProtoWeatherBase() {}
|
||||
virtual void feed(bool level, uint32_t duration) = 0; // need to be implemented on each protocol handler.
|
||||
void setCallback(SubGhzProtocolDecoderBaseRxCallback cb) { callback = cb; } // this is called when there is a hit.
|
||||
|
||||
uint8_t getSensorType() { return sensorType; }
|
||||
uint32_t getSensorId() { return id; }
|
||||
float getTemp() { return temp; }
|
||||
uint8_t getHumidity() { return humidity; }
|
||||
uint8_t getBattLow() { return battery_low; }
|
||||
uint32_t getTimestamp() { return timestamp; }
|
||||
uint8_t getChannel() { return channel; }
|
||||
uint8_t getButton() { return btn; }
|
||||
|
||||
protected:
|
||||
// Helper functions to keep it as compatible with flipper as we can, so adding new protos will be easy.
|
||||
void subghz_protocol_blocks_add_bit(uint8_t bit) {
|
||||
decode_data = decode_data << 1 | bit;
|
||||
decode_count_bit++;
|
||||
}
|
||||
uint8_t subghz_protocol_blocks_add_bytes(uint8_t const message[], size_t size) {
|
||||
uint32_t result = 0;
|
||||
for (size_t i = 0; i < size; ++i) {
|
||||
result += message[i];
|
||||
}
|
||||
return (uint8_t)result;
|
||||
}
|
||||
uint8_t subghz_protocol_blocks_parity8(uint8_t byte) {
|
||||
byte ^= byte >> 4;
|
||||
byte &= 0xf;
|
||||
return (0x6996 >> byte) & 1;
|
||||
}
|
||||
uint8_t subghz_protocol_blocks_parity_bytes(uint8_t const message[], size_t size) {
|
||||
uint8_t result = 0;
|
||||
for (size_t i = 0; i < size; ++i) {
|
||||
result ^= subghz_protocol_blocks_parity8(message[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
uint8_t subghz_protocol_blocks_lfsr_digest8(
|
||||
uint8_t const message[],
|
||||
size_t size,
|
||||
uint8_t gen,
|
||||
uint8_t key) {
|
||||
uint8_t sum = 0;
|
||||
for (size_t byte = 0; byte < size; ++byte) {
|
||||
uint8_t data = message[byte];
|
||||
for (int i = 7; i >= 0; --i) {
|
||||
// XOR key into sum if data bit is set
|
||||
if ((data >> i) & 1) sum ^= key;
|
||||
// roll the key right (actually the LSB is dropped here)
|
||||
// and apply the gen (needs to include the dropped LSB as MSB)
|
||||
if (key & 1)
|
||||
key = (key >> 1) ^ gen;
|
||||
else
|
||||
key = (key >> 1);
|
||||
}
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
float locale_fahrenheit_to_celsius(float temp_f) {
|
||||
return (temp_f - 32.f) / 1.8f;
|
||||
}
|
||||
bool manchester_advance(
|
||||
ManchesterState state,
|
||||
ManchesterEvent event,
|
||||
ManchesterState* next_state,
|
||||
bool* data) {
|
||||
bool result = false;
|
||||
ManchesterState new_state;
|
||||
|
||||
if (event == ManchesterEventReset) {
|
||||
new_state = manchester_reset_state;
|
||||
} else {
|
||||
new_state = (ManchesterState)(transitions[state] >> event & 0x3);
|
||||
if (new_state == state) {
|
||||
new_state = manchester_reset_state;
|
||||
} else {
|
||||
if (new_state == ManchesterStateMid0) {
|
||||
if (data) *data = false;
|
||||
result = true;
|
||||
} else if (new_state == ManchesterStateMid1) {
|
||||
if (data) *data = true;
|
||||
result = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*next_state = new_state;
|
||||
return result;
|
||||
}
|
||||
uint8_t subghz_protocol_blocks_crc4(
|
||||
uint8_t const message[],
|
||||
size_t size,
|
||||
uint8_t polynomial,
|
||||
uint8_t init) {
|
||||
uint8_t remainder = init << 4; // LSBs are unused
|
||||
uint8_t poly = polynomial << 4;
|
||||
uint8_t bit;
|
||||
|
||||
while (size--) {
|
||||
remainder ^= *message++;
|
||||
for (bit = 0; bit < 8; bit++) {
|
||||
if (remainder & 0x80) {
|
||||
remainder = (remainder << 1) ^ poly;
|
||||
} else {
|
||||
remainder = (remainder << 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
return remainder >> 4 & 0x0f; // discard the LSBs
|
||||
}
|
||||
uint8_t subghz_protocol_blocks_lfsr_digest8_reflect(
|
||||
uint8_t const message[],
|
||||
size_t size,
|
||||
uint8_t gen,
|
||||
uint8_t key) {
|
||||
uint8_t sum = 0;
|
||||
// Process message from last byte to first byte (reflected)
|
||||
for (int byte = size - 1; byte >= 0; --byte) {
|
||||
uint8_t data = message[byte];
|
||||
// Process individual bits of each byte (reflected)
|
||||
for (uint8_t i = 0; i < 8; ++i) {
|
||||
// XOR key into sum if data bit is set
|
||||
if ((data >> i) & 1) {
|
||||
sum ^= key;
|
||||
}
|
||||
// roll the key left (actually the LSB is dropped here)
|
||||
// and apply the gen (needs to include the dropped lsb as MSB)
|
||||
if (key & 0x80)
|
||||
key = (key << 1) ^ gen;
|
||||
else
|
||||
key = (key << 1);
|
||||
}
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
uint64_t subghz_protocol_blocks_reverse_key(uint64_t key, uint8_t bit_count) {
|
||||
uint64_t reverse_key = 0;
|
||||
for (uint8_t i = 0; i < bit_count; i++) {
|
||||
reverse_key = reverse_key << 1 | bit_read(key, i);
|
||||
}
|
||||
return reverse_key;
|
||||
}
|
||||
uint8_t subghz_protocol_blocks_crc8(
|
||||
uint8_t const message[],
|
||||
size_t size,
|
||||
uint8_t polynomial,
|
||||
uint8_t init) {
|
||||
uint8_t remainder = init;
|
||||
|
||||
for (size_t byte = 0; byte < size; ++byte) {
|
||||
remainder ^= message[byte];
|
||||
for (uint8_t bit = 0; bit < 8; ++bit) {
|
||||
if (remainder & 0x80) {
|
||||
remainder = (remainder << 1) ^ polynomial;
|
||||
} else {
|
||||
remainder = (remainder << 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
return remainder;
|
||||
}
|
||||
|
||||
// General weather data holder
|
||||
uint8_t sensorType = FPW_Invalid;
|
||||
uint32_t id = WS_NO_ID;
|
||||
float temp = WS_NO_TEMPERATURE;
|
||||
uint8_t humidity = WS_NO_HUMIDITY;
|
||||
uint8_t battery_low = WS_NO_BATT;
|
||||
uint32_t timestamp = 0;
|
||||
uint8_t channel = WS_NO_CHANNEL;
|
||||
uint8_t btn = WS_NO_BTN;
|
||||
|
||||
// inner logic stuff, also for flipper compatibility. //todo revork a bit, so won't have dupes (decode_data + data, ..), but check if any of the protos uses it in the same time or not. (shouldn't)
|
||||
SubGhzProtocolDecoderBaseRxCallback callback = NULL;
|
||||
uint16_t header_count = 0;
|
||||
uint8_t parser_step = 0;
|
||||
uint32_t te_last = 0;
|
||||
uint64_t data = 0;
|
||||
uint32_t data_count_bit = 0;
|
||||
uint64_t decode_data = 0;
|
||||
uint32_t decode_count_bit = 0;
|
||||
ManchesterState manchester_saved_state = ManchesterStateMid1;
|
||||
static const ManchesterState manchester_reset_state = ManchesterStateMid1;
|
||||
static inline const uint8_t transitions[] = {0b00000001, 0b10010001, 0b10011011, 0b11111011};
|
||||
};
|
||||
|
||||
#endif
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user