Compare commits

..

4 Commits

Author SHA1 Message Date
zxkmm e33e697cca remove some bluffing text from fpv detect app (#3114) 2026-03-29 17:24:29 +08:00
zxkmm d7d27d99e7 edit security policy (#3113) 2026-03-28 18:14:22 +01:00
Pezsma 78d3480b68 Morse tx update (#3112)
* loop message, progress bar
2026-03-27 18:29:20 +01:00
Totoo 70b42f4a56 fix audio mode change crash (#3111) 2026-03-26 22:40:27 +01:00
8 changed files with 133 additions and 72 deletions
+17 -1
View File
@@ -1 +1,17 @@
Please check [Intended-Use-and-Legality](https://github.com/portapack-mayhem/mayhem-firmware/wiki/)
# Security Policy
## Scope and Threat Model
This repository hosts the codebase for an embedded system designed for security research. Because the system operates primarily offline, traditional network-based vulnerabilities are generally not applicable. However, we still welcome and review security reports concerning the code within this repository.
## Reporting a Vulnerability
If you discover a vulnerability within this tool itself, please submit a report. Please be aware that the maintainers do not promise assistance with obtaining CVE IDs or managing formal security advisories.
## Out of Scope
* **Vulnerabilities found *using* this tool:** Please do not submit reports here for vulnerabilities you have discovered in other systems or targets using this tool. This channel is strictly for reporting vulnerabilities found *in* the tool itself.
* **Other Repositories:** For security issues concerning related tools, such as MayhemHub, please submit your reports directly to their respective repositories.
## Research Attribution
If you utilize this tool to successfully discover vulnerabilities in external systems, we would greatly appreciate it if you credit or mention our project in your published research or write-ups.
## Legal and Security Disclaimer
For comprehensive guidelines regarding the legal and secure usage of this tool, as well as our full liability disclaimer, please refer to: [Intended-Use-and-Legality](https://github.com/portapack-mayhem/mayhem-firmware/wiki/). Users are expected to comply with all applicable laws and regulations when using this software and/or hardware.
@@ -419,10 +419,10 @@ void AnalogAudioView::on_baseband_bandwidth_changed(uint32_t bandwidth_hz) {
}
void AnalogAudioView::on_modulation_changed(ReceiverModel::Mode modulation) {
baseband::spectrum_streaming_stop();
waterfall.stop();
update_modulation(modulation);
on_show_options_modulation();
baseband::spectrum_streaming_start();
waterfall.start();
}
void AnalogAudioView::remove_options_widget() {
+12 -14
View File
@@ -3,13 +3,11 @@
/*
* FPV RX — how frequency search and lock work
*
* 1. Power metric: We use channelized power (baseband IQ magnitude² in the
* capture bandwidth), not raw RF RSSI. Stats come from ChannelStatsCollector
* over filtered IQ, so we see power in the tuned channel, not wideband.
* 1. Power metric: We use channelized power that portapack RSSI algo included.
*
* 2. Scanning: We step through FPV bands/channels (A/B/E/F/R, 8 ch each). When
* power on the current channel exceeds the detect threshold, we enter
* "Candidate" and run verification we do not lock on a single peak.
* "Candidate" and run verification - with some edge detecting algo.
*
* 3. Verification (making sure the drone is on that freq):
* - Multiple samples: verify_hits / verify_misses over several updates.
@@ -177,7 +175,7 @@ void FpvDetectView::reset_detector(bool retune_current) {
}
}
bool FpvDetectView::is_possible_analog_carrier(const ChannelStatistics& statistics) const {
bool FpvDetectView::is_possible_freq_spike(const ChannelStatistics& statistics) const {
return statistics.max_db >= detect_threshold_db();
}
@@ -392,17 +390,17 @@ void FpvDetectView::update_state_badge() {
text_state.set("SCANNING");
break;
case DetectState::Candidate:
text_state.set("VERIFY FPV");
text_state.set("CHECKING");
break;
case DetectState::Locked:
text_state.set("DRONE FOUND");
text_state.set("FREQ FOUND");
break;
}
}
void FpvDetectView::update_confidence_text() {
char buf[16];
std::snprintf(buf, sizeof(buf), "Conf %u%%", static_cast<unsigned>(candidate_confidence_));
std::snprintf(buf, sizeof(buf), "Posi %u%%", static_cast<unsigned>(candidate_confidence_));
text_confidence.set(buf);
}
@@ -412,21 +410,21 @@ void FpvDetectView::update_status_text() {
switch (detect_state_) {
case DetectState::Scanning:
if (band_mode < FPV_NUM_BANDS) {
std::snprintf(buf, sizeof(buf), "Scanning band %c for analog FPV", band_labels[band_mode]);
std::snprintf(buf, sizeof(buf), "Scanning band %c", band_labels[band_mode]);
} else {
std::snprintf(buf, sizeof(buf), "Scanning all FPV bands");
std::snprintf(buf, sizeof(buf), "Scanning all from list");
}
break;
case DetectState::Candidate:
std::snprintf(buf, sizeof(buf), "VERIFYING %c%d %ld MHz",
std::snprintf(buf, sizeof(buf), "CHKING %c%d %ld MHz",
band_labels[candidate_band_],
static_cast<int>(candidate_ch_ + 1),
static_cast<long>(fpv_frequencies[candidate_band_][candidate_ch_] / 1000000LL));
break;
case DetectState::Locked:
std::snprintf(buf, sizeof(buf), "!!! DRONE FOUND !!! %c%d %ld",
std::snprintf(buf, sizeof(buf), "FREQ FOUND %c%d %ld",
band_labels[candidate_band_],
static_cast<int>(candidate_ch_ + 1),
static_cast<long>(fpv_frequencies[candidate_band_][candidate_ch_] / 1000000LL));
@@ -453,7 +451,7 @@ void FpvDetectView::update_detail_text() {
break;
case DetectState::Locked:
std::snprintf(buf, sizeof(buf), "LOCKED %c%d conf %u%% hold %u",
std::snprintf(buf, sizeof(buf), "LOCKED %c%d posi %u%% hold %u",
band_labels[candidate_band_],
static_cast<int>(candidate_ch_ + 1),
static_cast<unsigned>(candidate_confidence_),
@@ -605,7 +603,7 @@ void FpvDetectView::on_statistics_update(const ChannelStatistics& statistics) {
switch (detect_state_) {
case DetectState::Scanning:
if (is_possible_analog_carrier(statistics)) {
if (is_possible_freq_spike(statistics)) {
enter_candidate(statistics);
}
break;
+4 -4
View File
@@ -72,7 +72,7 @@ class FpvDetectView : public View {
void step_scan();
void reset_detector(bool retune_current);
bool is_possible_analog_carrier(const ChannelStatistics& statistics) const;
bool is_possible_freq_spike(const ChannelStatistics& statistics) const;
void enter_candidate(const ChannelStatistics& statistics);
void evaluate_candidate_sample(const ChannelStatistics& statistics);
void enter_lock();
@@ -162,7 +162,7 @@ class FpvDetectView : public View {
Text text_freq{{UI_POS_X_RIGHT(20), UI_POS_Y(1), UI_POS_WIDTH(20), UI_POS_DEFAULT_HEIGHT}, ""};
Text text_state{{UI_POS_X(0), UI_POS_Y(2), UI_POS_WIDTH(12), UI_POS_DEFAULT_HEIGHT}, "SCANNING"};
Text text_confidence{{UI_POS_X(12), UI_POS_Y(2), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT}, "Conf 0%"};
Text text_confidence{{UI_POS_X(12), UI_POS_Y(2), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT}, "Posi 0%"};
Text text_detect_label{{UI_POS_X_RIGHT(10), UI_POS_Y(2), UI_POS_WIDTH(5), UI_POS_DEFAULT_HEIGHT}, "Thr>"};
NumberField field_detect_threshold{
@@ -176,8 +176,8 @@ class FpvDetectView : public View {
Text freq_stats_rssi{{UI_POS_X(0), UI_POS_Y(3), UI_POS_WIDTH(15), UI_POS_DEFAULT_HEIGHT}, "RSSI 0/0/0"};
Text freq_stats_db{{UI_POS_X_RIGHT(14), UI_POS_Y(3), UI_POS_WIDTH(14), UI_POS_DEFAULT_HEIGHT}, "PWR -120 dB"};
Text text_status{{UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH(30), UI_POS_DEFAULT_HEIGHT}, "SCANNING FOR ANALOG FPV"};
Text text_detail{{UI_POS_X(0), UI_POS_Y(5), UI_POS_WIDTH(30), UI_POS_DEFAULT_HEIGHT}, "Waiting for FPV-like carrier"};
Text text_status{{UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH(30), UI_POS_DEFAULT_HEIGHT}, "SCANNING FOR FREQS"};
Text text_detail{{UI_POS_X(0), UI_POS_Y(5), UI_POS_WIDTH(30), UI_POS_DEFAULT_HEIGHT}, "Waiting for FPV FREQ"};
RSSIGraph rssi_graph{{UI_POS_X(0), UI_POS_Y(6), UI_POS_WIDTH_REMAINING(5), UI_POS_HEIGHT_REMAINING(7)}};
RSSI rssi{{UI_POS_X_RIGHT(5), UI_POS_Y(6), UI_POS_WIDTH(5), UI_POS_HEIGHT_REMAINING(7)}};
@@ -27,6 +27,9 @@ MorseRadiotxView::MorseRadiotxView(ui::NavigationView& nav)
&chk_trans,
&bandwidth,
&chk_callsgn,
&chk_loop,
&wait_time,
&progressbar,
&txt_last,
&console_text,
&btn_clear,
@@ -69,6 +72,7 @@ MorseRadiotxView::MorseRadiotxView(ui::NavigationView& nav)
options_mode.set_selected_index(current_mode, true);
tone_.set_value(tone, true);
wpm_.set_value(wpm, true);
wait_time.set_value(1, true);
bandwidth.set_value(band, true);
btn_ptt.on_select = [this](Button&) {
@@ -156,6 +160,14 @@ MorseRadiotxView::MorseRadiotxView(ui::NavigationView& nav)
transmitter_model.set_target_frequency(f);
};
};
chk_trans.on_select = [this](Checkbox&, bool v) {
if (v) chk_loop.set_value(false);
};
chk_loop.on_select = [this](Checkbox&, bool v) {
if (v) chk_trans.set_value(false);
};
}
MorseRadiotxView::~MorseRadiotxView() {
@@ -197,19 +209,15 @@ MorseRadiotxView::MorseTimings MorseRadiotxView::calculate_morse_timings(uint32_
void MorseRadiotxView::transmit_morse_message() {
std::string full_message = "";
// cal sign
if (chk_callsgn.value() && !call_sign.empty()) {
full_message = call_sign;
if (!chk_trans.value() && !msg_buffer.empty()) {
full_message += " " + msg_buffer;
}
} else {
if (!chk_trans.value()) full_message = msg_buffer;
}
if (full_message.empty() && !chk_trans.value()) {
if (chk_trans.value()) {
ptt_button_visibility(false);
return;
} else {
full_message = msg_buffer;
if (chk_callsgn.value() && !call_sign.empty()) { // call sign
full_message += " " + call_sign;
}
}
// enable transmit
@@ -219,46 +227,63 @@ void MorseRadiotxView::transmit_morse_message() {
ui_toggle();
}
for (size_t i = 0; i < full_message.length(); i++) {
if (chThdShouldTerminate()) break;
uint8_t loop_seconds = static_cast<uint8_t>(wait_time.value());
progressbar.set_max(loop_seconds * 2);
char c = full_message[i];
do {
for (size_t i = 0; i < full_message.length(); i++) {
if (chThdShouldTerminate()) break;
std::string s_char(1, c);
// space
if (c == ' ') {
console_text.write(" ");
chThdSleepMilliseconds(current_timings.word_gap);
continue;
}
char c = full_message[i];
const char* pattern = morse_decoder_.get_morse_pattern(c);
std::string s_char(1, c);
// space
if (c == ' ') {
console_text.write(" ");
chThdSleepMilliseconds(current_timings.word_gap);
continue;
}
if (pattern != nullptr) {
txt_last.set(pattern);
const char* pattern = morse_decoder_.get_morse_pattern(c);
// write blue char to console
console_text.write(STR_COLOR_BLUE + s_char);
if (pattern != nullptr) {
txt_last.set(pattern);
// Morze (dih/dah) send
for (int j = 0; pattern[j] != '\0'; j++) {
baseband::set_morsetx_key(true);
if (pattern[j] == '.') {
chThdSleepMilliseconds(current_timings.dot_ms);
} else if (pattern[j] == '-') {
chThdSleepMilliseconds(current_timings.dash_ms);
// write blue char to console
console_text.write(STR_COLOR_BLUE + s_char);
// Morze (dih/dah) send
for (int j = 0; pattern[j] != '\0'; j++) {
baseband::set_morsetx_key(true);
if (pattern[j] == '.') {
chThdSleepMilliseconds(current_timings.dot_ms);
} else if (pattern[j] == '-') {
chThdSleepMilliseconds(current_timings.dash_ms);
}
if (chThdShouldTerminate()) break;
// pause between signs
baseband::set_morsetx_key(false);
chThdSleepMilliseconds(current_timings.symbol_gap);
}
if (chThdShouldTerminate()) break;
// pause between signs
baseband::set_morsetx_key(false);
chThdSleepMilliseconds(current_timings.symbol_gap);
}
if (chThdShouldTerminate()) break;
if (current_timings.char_gap > current_timings.symbol_gap) {
chThdSleepMilliseconds(current_timings.char_gap - current_timings.symbol_gap);
if (current_timings.char_gap > current_timings.symbol_gap) {
chThdSleepMilliseconds(current_timings.char_gap - current_timings.symbol_gap);
}
}
}
}
if (chThdShouldTerminate()) break;
if (chk_loop.value()) {
console_text.write(" ");
for (uint8_t s = 0; s < (loop_seconds * 2); s++) {
if (chThdShouldTerminate()) break;
progressbar.set_value(s + 1);
if (!chk_loop.value()) break;
chThdSleepMilliseconds(500);
}
progressbar.set_value(0);
}
if (!chk_loop.value()) break;
} while (chk_loop.value() && !chThdShouldTerminate());
if (chk_trans.value()) ptt_button_visibility(false);
baseband::set_morsetx_key(false);
@@ -356,13 +381,17 @@ void MorseRadiotxView::ui_toggle() {
wpm_.set_style(Theme::getInstance()->fg_dark);
wpm_.set_focusable(false);
btn_message.set_style(Theme::getInstance()->fg_dark);
btn_message.set_focusable(false);
btn_calls.set_style(Theme::getInstance()->fg_dark);
btn_calls.set_focusable(false);
chk_trans.set_style(Theme::getInstance()->fg_dark);
chk_trans.set_focusable(false);
bandwidth.set_style(Theme::getInstance()->fg_dark);
bandwidth.set_focusable(false);
chk_callsgn.set_style(Theme::getInstance()->fg_dark);
chk_callsgn.set_focusable(false);
wait_time.set_style(Theme::getInstance()->fg_dark);
wait_time.set_focusable(false);
bandwidth.set_style(Theme::getInstance()->fg_dark);
bandwidth.set_focusable(false);
} else {
options_mode.set_style(Theme::getInstance()->bg_darker);
@@ -370,11 +399,15 @@ void MorseRadiotxView::ui_toggle() {
wpm_.set_style(Theme::getInstance()->bg_darker);
wpm_.set_focusable(true);
btn_message.set_style(Theme::getInstance()->bg_darker);
btn_message.set_focusable(true);
btn_calls.set_style(Theme::getInstance()->bg_darker);
btn_calls.set_focusable(true);
chk_trans.set_style(Theme::getInstance()->bg_darker);
chk_trans.set_focusable(true);
chk_callsgn.set_style(Theme::getInstance()->bg_darker);
chk_callsgn.set_focusable(true);
wait_time.set_style(Theme::getInstance()->bg_darker);
wait_time.set_focusable(true);
ptt_button_visibility(true);
tone_.set_style(Theme::getInstance()->bg_darker);
tone_.set_focusable(true);
@@ -118,15 +118,20 @@ class MorseRadiotxView : public ui::View {
{{"AM", 0}, {"FM", 1}, {"DSB", 2}, {"USB", 3}, {"LSB", 4}}};
NumberField tone_{{UI_POS_X(14), UI_POS_Y(0)}, 4, {400, 1400}, 10, ' ', true};
NumberField wpm_{{UI_POS_X(25), UI_POS_Y(0)}, 2, {10, 45}, 1, ' ', true};
FloatField bandwidth{{UI_POS_X(20), UI_POS_Y(3)}, 4, {1.0, 16.0}, 0.1, ' ', true, 1};
NumberField wait_time{{UI_POS_X(10), UI_POS_Y(4)}, 3, {1, 99}, 1, ' ', true}; // wait between tx-es in loop mode (sec)
FloatField bandwidth{{UI_POS_X(6), UI_POS_Y(5)}, 4, {1.0, 16.0}, 0.1, ' ', true, 1};
ProgressBar progressbar{
{UI_POS_X(0), UI_POS_Y(6), screen_width, 16}};
ui::Text txt_msg{{UI_POS_X(0), UI_POS_Y(1), UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)}, "[" + msg_buffer + "] "};
ui::Button btn_message{{UI_POS_X(0), UI_POS_Y(2), UI_POS_WIDTH(11), UI_POS_HEIGHT(1)}, "Message"};
ui::Button btn_calls{{UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH(11), UI_POS_HEIGHT(1)}, (call_sign.empty()) ? "call sign?" : call_sign};
ui::Button btn_calls{{UI_POS_X(0), UI_POS_Y(3), UI_POS_WIDTH(11), UI_POS_HEIGHT(1)}, (call_sign.empty()) ? "call sign?" : call_sign};
Checkbox chk_trans{{UI_POS_X(14), UI_POS_Y(2)}, 13, "Manual trans.", true};
Checkbox chk_callsgn{{UI_POS_X(14), UI_POS_Y(4)}, 13, "Call sign", true};
ui::Text txt_last{{UI_POS_X(10), UI_POS_Y(5), UI_POS_WIDTH_REMAINING(10), UI_POS_HEIGHT(1)}, ""};
ui::Console console_text{{UI_POS_X(0), UI_POS_Y(7), UI_POS_MAXWIDTH, UI_POS_HEIGHT_REMAINING(14)}};
Checkbox chk_callsgn{{UI_POS_X(14), UI_POS_Y(3)}, 9, "Call sign", true};
Checkbox chk_loop{{UI_POS_X(14), UI_POS_Y(4)}, 4, "loop", true};
ui::Text txt_last{{UI_POS_X(10), UI_POS_Y(7), UI_POS_WIDTH_REMAINING(10), UI_POS_HEIGHT(1)}, ""};
ui::Console console_text{{UI_POS_X(0), UI_POS_Y(9), UI_POS_MAXWIDTH, UI_POS_HEIGHT_REMAINING(14)}};
ui::Button btn_clear{{UI_POS_X(0), UI_POS_Y_BOTTOM(5), UI_POS_WIDTH(5), UI_POS_HEIGHT(1)}, "CLR"};
ui::Button btn_ptt{{UI_POS_X_CENTER(12), UI_POS_Y_BOTTOM(7), UI_POS_WIDTH(12), UI_POS_HEIGHT(3)}, "PTT"};
@@ -135,10 +140,11 @@ class MorseRadiotxView : public ui::View {
{{UI_POS_X(9), UI_POS_Y(0)}, "Tone:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(18), UI_POS_Y(0)}, "Hz", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(21), UI_POS_Y(0)}, "WPM:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(14), UI_POS_Y(3)}, "BandW:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(24), UI_POS_Y(3)}, "kHz", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(5)}, "Last seq:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(6)}, "Sent Message:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(5)}, "BandW:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(10), UI_POS_Y(5)}, "kHz", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(4)}, "Wait time: ", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(7)}, "Last seq:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(8)}, "Sent Message:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X_RIGHT(7), UI_POS_Y_BOTTOM(5)}, "Vol.:", Theme::getInstance()->fg_light->foreground},
};
+2
View File
@@ -361,6 +361,8 @@ void WaterfallView::stop() {
baseband::spectrum_streaming_stop();
running_ = false;
}
this->channel_fifo = nullptr;
this->audio_spectrum_data = nullptr;
}
void WaterfallView::show_audio_spectrum_view(const bool show) {
+6
View File
@@ -173,12 +173,16 @@ class WaterfallView : public View {
MessageHandlerRegistration message_handler_channel_spectrum_config{
Message::ID::ChannelSpectrumConfig,
[this](const Message* const p) {
if (!running_)
return;
const auto message = *reinterpret_cast<const ChannelSpectrumConfigMessage*>(p);
this->channel_fifo = message.fifo;
}};
MessageHandlerRegistration message_handler_audio_spectrum{
Message::ID::AudioSpectrum,
[this](const Message* const p) {
if (!running_)
return;
const auto message = *reinterpret_cast<const AudioSpectrumMessage*>(p);
this->audio_spectrum_data = message.data;
this->audio_spectrum_update = true;
@@ -186,6 +190,8 @@ class WaterfallView : public View {
MessageHandlerRegistration message_handler_frame_sync{
Message::ID::DisplayFrameSync,
[this](const Message* const) {
if (!running_)
return;
if (this->channel_fifo) {
ChannelSpectrum channel_spectrum;
while (channel_fifo->out(channel_spectrum)) {