mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-08-19 06:03:59 +00:00
Numbers station works, very basic
Added utilities, whip antenna length calculator Modal errors/abort
This commit is contained in:
@@ -35,7 +35,7 @@ set(USE_OPT "-Os -g --specs=nano.specs")
|
||||
set(USE_COPT "-std=gnu99")
|
||||
|
||||
# C++ specific options here (added to USE_OPT).
|
||||
set(USE_CPPOPT "-std=c++11 -fno-rtti -fno-exceptions")
|
||||
set(USE_CPPOPT "-Wl,-Map,foo.map -std=c++11 -fno-rtti -fno-exceptions")
|
||||
|
||||
# Enable this if you want the linker to remove unused code and data
|
||||
set(USE_LINK_GC yes)
|
||||
@@ -145,37 +145,38 @@ set(CPPSRC
|
||||
${COMMON}/ui_painter.cpp
|
||||
${COMMON}/ui_focus.cpp
|
||||
${COMMON}/msgpack.cpp
|
||||
ui_navigation.cpp
|
||||
ui_menu.cpp
|
||||
ui_rssi.cpp
|
||||
ui_channel.cpp
|
||||
ui_about.cpp
|
||||
ui_adsbtx.cpp
|
||||
ui_afsksetup.cpp
|
||||
ui_alphanum.cpp
|
||||
ui_audio.cpp
|
||||
ui_font_fixed_8x16.cpp
|
||||
ui_setup.cpp
|
||||
ui_touch_calibration.cpp
|
||||
ui_debug.cpp
|
||||
ui_baseband_stats_view.cpp
|
||||
ui_sd_card_status_view.cpp
|
||||
ui_sd_card_debug.cpp
|
||||
ui_channel.cpp
|
||||
ui_debug.cpp
|
||||
ui_encoders.cpp
|
||||
ui_font_fixed_8x16.cpp
|
||||
ui_freqman.cpp
|
||||
ui_handwrite.cpp
|
||||
ui_jammer.cpp
|
||||
ui_lcr.cpp
|
||||
ui_menu.cpp
|
||||
ui_navigation.cpp
|
||||
ui_numbers.cpp
|
||||
ui_nuoptix.cpp
|
||||
ui_rds.cpp
|
||||
ui_receiver.cpp
|
||||
ui_record_view.cpp
|
||||
ui_textentry.cpp
|
||||
ui_alphanum.cpp
|
||||
ui_spectrum.cpp
|
||||
ui_about.cpp
|
||||
ui_nuoptix.cpp
|
||||
ui_adsbtx.cpp
|
||||
ui_handwrite.cpp
|
||||
ui_afsksetup.cpp
|
||||
# ui_closecall.cpp
|
||||
ui_rssi.cpp
|
||||
ui_sd_card_status_view.cpp
|
||||
ui_sd_card_debug.cpp
|
||||
ui_setup.cpp
|
||||
ui_soundboard.cpp
|
||||
ui_rds.cpp
|
||||
ui_lcr.cpp
|
||||
ui_spectrum.cpp
|
||||
ui_textentry.cpp
|
||||
ui_touch_calibration.cpp
|
||||
ui_whipcalc.cpp
|
||||
ui_xylos.cpp
|
||||
ui_numbers.cpp
|
||||
ui_freqman.cpp
|
||||
ui_encoders.cpp
|
||||
ui_jammer.cpp
|
||||
# ui_closecall.cpp
|
||||
# ui_loadmodule.cpp
|
||||
recent_entries.cpp
|
||||
receiver_model.cpp
|
||||
|
||||
@@ -4302,6 +4302,30 @@ ui_touch_calibration.cpp.s:
|
||||
cd /home/furrtek/portapack-hackrf && $(MAKE) -f firmware/application/CMakeFiles/application.elf.dir/build.make firmware/application/CMakeFiles/application.elf.dir/ui_touch_calibration.cpp.s
|
||||
.PHONY : ui_touch_calibration.cpp.s
|
||||
|
||||
ui_whipcalc.obj: ui_whipcalc.cpp.obj
|
||||
.PHONY : ui_whipcalc.obj
|
||||
|
||||
# target to build an object file
|
||||
ui_whipcalc.cpp.obj:
|
||||
cd /home/furrtek/portapack-hackrf && $(MAKE) -f firmware/application/CMakeFiles/application.elf.dir/build.make firmware/application/CMakeFiles/application.elf.dir/ui_whipcalc.cpp.obj
|
||||
.PHONY : ui_whipcalc.cpp.obj
|
||||
|
||||
ui_whipcalc.i: ui_whipcalc.cpp.i
|
||||
.PHONY : ui_whipcalc.i
|
||||
|
||||
# target to preprocess a source file
|
||||
ui_whipcalc.cpp.i:
|
||||
cd /home/furrtek/portapack-hackrf && $(MAKE) -f firmware/application/CMakeFiles/application.elf.dir/build.make firmware/application/CMakeFiles/application.elf.dir/ui_whipcalc.cpp.i
|
||||
.PHONY : ui_whipcalc.cpp.i
|
||||
|
||||
ui_whipcalc.s: ui_whipcalc.cpp.s
|
||||
.PHONY : ui_whipcalc.s
|
||||
|
||||
# target to generate assembly for a file
|
||||
ui_whipcalc.cpp.s:
|
||||
cd /home/furrtek/portapack-hackrf && $(MAKE) -f firmware/application/CMakeFiles/application.elf.dir/build.make firmware/application/CMakeFiles/application.elf.dir/ui_whipcalc.cpp.s
|
||||
.PHONY : ui_whipcalc.cpp.s
|
||||
|
||||
ui_xylos.obj: ui_xylos.cpp.obj
|
||||
.PHONY : ui_xylos.obj
|
||||
|
||||
@@ -4858,6 +4882,9 @@ help:
|
||||
@echo "... ui_touch_calibration.obj"
|
||||
@echo "... ui_touch_calibration.i"
|
||||
@echo "... ui_touch_calibration.s"
|
||||
@echo "... ui_whipcalc.obj"
|
||||
@echo "... ui_whipcalc.i"
|
||||
@echo "... ui_whipcalc.s"
|
||||
@echo "... ui_xylos.obj"
|
||||
@echo "... ui_xylos.i"
|
||||
@echo "... ui_xylos.s"
|
||||
|
||||
@@ -23,14 +23,15 @@
|
||||
// Bitmaps generated with:
|
||||
// Gimp image > indexed colors (16), then "xxd -i *.bmp"
|
||||
|
||||
//TEST: Imperial in whipcalc
|
||||
//TEST: Numbers
|
||||
//TEST: Jammer
|
||||
//TEST: RDS
|
||||
|
||||
//BUG: Unistroke text entry screen doesn't care about string max length parameter
|
||||
//BUG (fixed ?): Soundboard crashes on exit if no wav files on sd card
|
||||
//BUG (fixed ?): No audio in about when shown second time
|
||||
//BUG: Unistroke text entry screen doesn't care about string max length parameter
|
||||
//BUG: POCSAG RX sometimes misses the first codeword after SYNC
|
||||
//BUG: Soundboard crashes on exit if no wav files on sd card
|
||||
|
||||
//TODO: Use ModalMessageView with yes/no for TX
|
||||
//TODO: Show address/data bit fields in OOK TX
|
||||
|
||||
@@ -33,9 +33,27 @@
|
||||
using namespace portapack;
|
||||
|
||||
namespace ui {
|
||||
|
||||
void FrequencySaveView::focus() {
|
||||
button_exit.focus();
|
||||
}
|
||||
|
||||
FrequencySaveView::FrequencySaveView(
|
||||
NavigationView& nav,
|
||||
const rf::Frequency value
|
||||
) {
|
||||
|
||||
add_children({ {
|
||||
&button_exit
|
||||
} });
|
||||
|
||||
void FreqManView::paint(Painter& painter) {
|
||||
(void)painter;
|
||||
button_exit.on_select = [this, &nav](Button&) {
|
||||
nav.pop();
|
||||
};
|
||||
}
|
||||
|
||||
void FreqManView::focus() {
|
||||
button_exit.focus();
|
||||
}
|
||||
|
||||
FreqManView::FreqManView(
|
||||
@@ -43,7 +61,7 @@ FreqManView::FreqManView(
|
||||
) {
|
||||
|
||||
add_children({ {
|
||||
&button_ok
|
||||
&button_exit
|
||||
} });
|
||||
|
||||
size_t n = 0;
|
||||
@@ -61,7 +79,7 @@ FreqManView::FreqManView(
|
||||
n++;
|
||||
}
|
||||
|
||||
button_ok.on_select = [this, &nav](Button&) {
|
||||
button_exit.on_select = [this, &nav](Button&) {
|
||||
nav.pop();
|
||||
};
|
||||
|
||||
|
||||
@@ -28,23 +28,37 @@
|
||||
|
||||
namespace ui {
|
||||
|
||||
class FrequencySaveView : public View {
|
||||
public:
|
||||
FrequencySaveView(NavigationView& nav, const rf::Frequency value);
|
||||
//~FrequencySaveView();
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "Save frequency"; };
|
||||
|
||||
private:
|
||||
Button button_exit {
|
||||
{ 72, 264, 96, 32 },
|
||||
"Exit"
|
||||
};
|
||||
};
|
||||
|
||||
class FreqManView : public View {
|
||||
public:
|
||||
FreqManView(NavigationView& nav);
|
||||
//~FreqManView();
|
||||
|
||||
std::string title() const override { return "Frequency list"; };
|
||||
void focus() override;
|
||||
|
||||
void paint(Painter& painter) override;
|
||||
//void on_show() override;
|
||||
//void on_hide() override;
|
||||
std::string title() const override { return "Freq. manager"; };
|
||||
|
||||
private:
|
||||
std::array<Text, 10> text_list;
|
||||
|
||||
Button button_ok {
|
||||
Button button_exit {
|
||||
{ 72, 264, 96, 32 },
|
||||
"OK"
|
||||
"Exit"
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ void JammerView::update_text(uint8_t id, rf::Frequency f) {
|
||||
std::string bw;
|
||||
uint8_t c;
|
||||
|
||||
/*
|
||||
auto mhz = to_string_dec_int(f / 1000000, 3);
|
||||
auto hz100 = to_string_dec_int((f / 100) % 10000, 4, '0');
|
||||
|
||||
@@ -63,7 +64,8 @@ void JammerView::update_text(uint8_t id, rf::Frequency f) {
|
||||
strcat(finalstr, " ");
|
||||
|
||||
buttons_freq[id].set_text(finalstr);
|
||||
|
||||
*/
|
||||
|
||||
for (c = 0; c < 3; c++) {
|
||||
center = (frequency_range[c].min + frequency_range[c].max) / 2;
|
||||
bw = to_string_dec_int(abs(frequency_range[c].max - frequency_range[c].min) / 1000, 5);
|
||||
@@ -156,7 +158,7 @@ JammerView::JammerView(NavigationView& nav) {
|
||||
button.on_select = button_freq_fn;
|
||||
button.set_parent_rect({
|
||||
static_cast<Coord>(13 * 8),
|
||||
static_cast<Coord>((n * 52) + 91 + (17 * (n & 1))),
|
||||
static_cast<Coord>(((n >> 1) * 52) + 90 + (18 * (n & 1))),
|
||||
88, 18
|
||||
});
|
||||
button.id = n;
|
||||
@@ -180,6 +182,8 @@ JammerView::JammerView(NavigationView& nav) {
|
||||
checkbox_range1.set_value(range_presets[v][0].enabled);
|
||||
checkbox_range2.set_value(range_presets[v][1].enabled);
|
||||
checkbox_range3.set_value(range_presets[v][2].enabled);
|
||||
|
||||
update_text(0, 0);
|
||||
};
|
||||
|
||||
options_preset.set_selected_index(8); // Sigfox, because they deserve it
|
||||
|
||||
@@ -95,7 +95,7 @@ private:
|
||||
{ false, 0, 0 }},
|
||||
|
||||
// Sigfox
|
||||
{{ true, 868000000, 868220000 }, // Center: 868.2MHz BW: 40kHz
|
||||
{{ true, 868000000, 868200000 }, // Center: 868.2MHz BW: 40kHz
|
||||
{ false, 0, 0 },
|
||||
{ false, 0, 0 }},
|
||||
|
||||
|
||||
@@ -36,28 +36,26 @@
|
||||
#include "ui_debug.hpp"
|
||||
|
||||
#include "ui_numbers.hpp"
|
||||
#include "ui_whipcalc.hpp"
|
||||
//#include "ui_closecall.hpp" // DEBUG
|
||||
#include "ui_freqman.hpp"
|
||||
#include "ui_nuoptix.hpp"
|
||||
#include "ui_soundboard.hpp"
|
||||
|
||||
#include "ui_encoders.hpp"
|
||||
#include "ui_debug.hpp"
|
||||
#include "ui_rds.hpp"
|
||||
#include "ui_xylos.hpp"
|
||||
#include "ui_epar.hpp"
|
||||
#include "ui_lcr.hpp"
|
||||
#include "analog_audio_app.hpp"
|
||||
//#include "ui_audiotx.hpp" // DEBUG
|
||||
#include "ui_adsbtx.hpp"
|
||||
#include "ui_jammer.hpp"
|
||||
|
||||
#include "ais_app.hpp"
|
||||
#include "ert_app.hpp"
|
||||
#include "tpms_app.hpp"
|
||||
#include "pocsag_app.hpp"
|
||||
#include "capture_app.hpp"
|
||||
|
||||
#include "ui_debug.hpp"
|
||||
#include "core_control.hpp"
|
||||
|
||||
#include "file.hpp"
|
||||
@@ -182,13 +180,38 @@ void NavigationView::pop() {
|
||||
}
|
||||
}
|
||||
|
||||
void NavigationView::pop_modal() {
|
||||
if( view() == modal_view ) {
|
||||
modal_view = nullptr;
|
||||
}
|
||||
|
||||
// Pop modal view and underlying app view
|
||||
if( view_stack.size() > 2 ) {
|
||||
free_view();
|
||||
view_stack.pop_back();
|
||||
free_view();
|
||||
view_stack.pop_back();
|
||||
|
||||
update_view();
|
||||
}
|
||||
}
|
||||
|
||||
void NavigationView::display_modal(
|
||||
const std::string& title,
|
||||
const std::string& message
|
||||
) {
|
||||
display_modal(title, message, INFO, nullptr);
|
||||
}
|
||||
|
||||
void NavigationView::display_modal(
|
||||
const std::string& title,
|
||||
const std::string& message,
|
||||
const modal_t type,
|
||||
const std::function<void(bool)> on_choice
|
||||
) {
|
||||
/* If a modal view is already visible, don't display another */
|
||||
if( !modal_view ) {
|
||||
modal_view = push<ModalMessageView>(title, message, false);
|
||||
modal_view = push<ModalMessageView>(title, message, type, on_choice);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,6 +295,15 @@ TransmitterAudioMenuView::TransmitterAudioMenuView(NavigationView& nav) {
|
||||
on_left = [&nav](){ nav.pop(); };
|
||||
}
|
||||
|
||||
/* UtilitiesView *****************************************************************/
|
||||
|
||||
UtilitiesView::UtilitiesView(NavigationView& nav) {
|
||||
add_items<2>({ {
|
||||
{ "Whip antenna calculator", ui::Color::green(), [&nav](){ nav.push<WhipCalcView>(); } },
|
||||
{ "Notepad", ui::Color::grey(), [&nav](){ nav.push<NotImplementedView>(); } },
|
||||
} });
|
||||
on_left = [&nav](){ nav.pop(); };
|
||||
}
|
||||
/* SystemMenuView ********************************************************/
|
||||
|
||||
SystemMenuView::SystemMenuView(NavigationView& nav) {
|
||||
@@ -284,9 +316,10 @@ SystemMenuView::SystemMenuView(NavigationView& nav) {
|
||||
//{ "Close Call RX", ui::Color::cyan(), [&nav](){ nav.push<CloseCallView>(); } },
|
||||
{ "Jammer", ui::Color::orange(), [&nav](){ nav.push<JammerView>(); } },
|
||||
{ "Frequency manager", ui::Color::white(), [&nav](){ nav.push<FreqManView>(); } },
|
||||
{ "Utilities", ui::Color::purple(), [&nav](){ nav.push<UtilitiesView>(); } },
|
||||
//{ "Analyze", ui::Color::white(), [&nav](){ nav.push<NotImplementedView>(); } },
|
||||
{ "Setup", ui::Color::white(), [&nav](){ nav.push<SetupMenuView>(); } },
|
||||
{ "Debug", ui::Color::white(), [&nav](){ nav.push<DebugMenuView>(); } },
|
||||
//{ "Debug", ui::Color::white(), [&nav](){ nav.push<DebugMenuView>(); } },
|
||||
{ "HackRF", ui::Color::white(), [&nav](){ nav.push<HackRFFirmwareView>(); } },
|
||||
{ "About", ui::Color::white(), [&nav](){ nav.push<AboutView>(); } }
|
||||
} });
|
||||
@@ -453,61 +486,66 @@ void NotImplementedView::focus() {
|
||||
}
|
||||
|
||||
/* ModalMessageView ******************************************************/
|
||||
|
||||
ModalMessageView::ModalMessageView(
|
||||
NavigationView& nav,
|
||||
const std::string& title,
|
||||
const std::string& message,
|
||||
bool yesno
|
||||
const modal_t type,
|
||||
const std::function<void(bool)> on_choice
|
||||
) : title_ { title },
|
||||
yesno_ { yesno }
|
||||
type_ { type },
|
||||
on_choice_ { on_choice }
|
||||
{
|
||||
|
||||
if (!yesno) {
|
||||
button_done.on_select = [&nav](Button&){
|
||||
nav.pop();
|
||||
};
|
||||
add_child(&text_message);
|
||||
|
||||
if (type == INFO) {
|
||||
add_child(&button_ok);
|
||||
|
||||
add_children({ {
|
||||
&text_message,
|
||||
&button_done
|
||||
} });
|
||||
} else {
|
||||
button_yes.on_select = [this,&nav](Button&){
|
||||
if (on_choice) on_choice(true);
|
||||
button_ok.on_select = [&nav](Button&){
|
||||
nav.pop();
|
||||
};
|
||||
button_no.on_select = [this,&nav](Button&){
|
||||
if (on_choice) on_choice(false);
|
||||
nav.pop();
|
||||
};
|
||||
|
||||
} else if (type == YESNO) {
|
||||
add_children({ {
|
||||
&text_message,
|
||||
&button_yes,
|
||||
&button_no
|
||||
} });
|
||||
|
||||
button_yes.on_select = [this, &nav](Button&){
|
||||
if (on_choice_) on_choice_(true);
|
||||
nav.pop();
|
||||
};
|
||||
button_no.on_select = [this, &nav](Button&){
|
||||
if (on_choice_) on_choice_(false);
|
||||
nav.pop();
|
||||
};
|
||||
} else {
|
||||
add_child(&button_ok);
|
||||
|
||||
button_ok.on_select = [this, &nav](Button&){
|
||||
if (on_choice_) on_choice_(true);
|
||||
nav.pop_modal();
|
||||
};
|
||||
}
|
||||
|
||||
text_message.set(message);
|
||||
|
||||
const int text_message_width = message.size() * 8;
|
||||
text_message.set_parent_rect({
|
||||
(240 - text_message_width) / 2, 7 * 16,
|
||||
(240 - text_message_width) / 2, 8 * 16,
|
||||
text_message_width, 16
|
||||
});
|
||||
text_message.set(message);
|
||||
}
|
||||
|
||||
void ModalMessageView::paint(Painter& painter) {
|
||||
(void)painter;
|
||||
portapack::display.drawBMP({64, 64}, modal_warning_bmp, false);
|
||||
portapack::display.drawBMP({96, 64}, modal_warning_bmp, false);
|
||||
}
|
||||
|
||||
void ModalMessageView::focus() {
|
||||
if (!yesno_) {
|
||||
button_done.focus();
|
||||
} else {
|
||||
if (type_ == YESNO) {
|
||||
button_yes.focus();
|
||||
} else {
|
||||
button_ok.focus();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,12 @@
|
||||
|
||||
namespace ui {
|
||||
|
||||
enum modal_t {
|
||||
INFO = 0,
|
||||
YESNO,
|
||||
ABORT
|
||||
};
|
||||
|
||||
class SystemStatusView : public View {
|
||||
public:
|
||||
std::function<void(void)> on_back;
|
||||
@@ -112,8 +118,10 @@ public:
|
||||
void push(View* v);
|
||||
|
||||
void pop();
|
||||
void pop_modal();
|
||||
|
||||
void display_modal(const std::string& title, const std::string& message);
|
||||
void display_modal(const std::string& title, const std::string& message, const modal_t type, const std::function<void(bool)> on_choice);
|
||||
|
||||
void focus() override;
|
||||
|
||||
@@ -192,6 +200,12 @@ public:
|
||||
std::string title() const override { return "Audio TX"; };
|
||||
};
|
||||
|
||||
class UtilitiesView : public MenuView {
|
||||
public:
|
||||
UtilitiesView(NavigationView& nav);
|
||||
std::string title() const override { return "Utilities"; };
|
||||
};
|
||||
|
||||
class SystemMenuView : public MenuView {
|
||||
public:
|
||||
SystemMenuView(NavigationView& nav);
|
||||
@@ -279,23 +293,23 @@ public:
|
||||
NavigationView& nav,
|
||||
const std::string& title,
|
||||
const std::string& message,
|
||||
bool yesno
|
||||
const modal_t type,
|
||||
const std::function<void(bool)> on_choice
|
||||
);
|
||||
void paint(Painter& painter) override;
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return title_; };
|
||||
|
||||
std::function<void(bool)> on_choice;
|
||||
// std::string title() const override { return title_; };
|
||||
|
||||
private:
|
||||
const std::string title_;
|
||||
const bool yesno_;
|
||||
|
||||
const modal_t type_;
|
||||
const std::function<void(bool)> on_choice_;
|
||||
|
||||
Text text_message { };
|
||||
|
||||
Button button_done {
|
||||
Button button_ok {
|
||||
{ 10 * 8, 13 * 16, 10 * 8, 24 },
|
||||
"OK",
|
||||
};
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
#include <cstring>
|
||||
#include <stdio.h>
|
||||
|
||||
// TODO: Total transmission time (all durations / 44100)
|
||||
|
||||
using namespace portapack;
|
||||
using namespace hackrf::one;
|
||||
|
||||
@@ -38,6 +40,10 @@ namespace ui {
|
||||
|
||||
void NumbersStationView::focus() {
|
||||
button_exit.focus();
|
||||
|
||||
if (file_error) {
|
||||
nav_.display_modal("No files", "Missing files in /numbers/", ABORT, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
NumbersStationView::~NumbersStationView() {
|
||||
@@ -45,53 +51,85 @@ NumbersStationView::~NumbersStationView() {
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
void NumbersStationView::paint(Painter& painter) {
|
||||
(void)painter;
|
||||
}
|
||||
|
||||
void NumbersStationView::on_tuning_frequency_changed(rf::Frequency f) {
|
||||
transmitter_model.set_tuning_frequency(f);
|
||||
}
|
||||
|
||||
void NumbersStationView::prepare_audio() {
|
||||
uint8_t code;
|
||||
|
||||
if (cnt >= sample_duration) {
|
||||
/*if (!check_loop.value()) {
|
||||
transmitter_model.disable();
|
||||
return;
|
||||
} else {
|
||||
file.seek(44);
|
||||
cnt = 0;
|
||||
}*/
|
||||
if (segment == ANNOUNCE) {
|
||||
if (!announce_loop) {
|
||||
segment = MESSAGE;
|
||||
} else {
|
||||
auto error = file.open("/numbers/announce.wav");
|
||||
if (error.is_valid()) return;
|
||||
|
||||
sample_duration = sound_sizes[10];
|
||||
|
||||
file.seek(44); // Skip header
|
||||
|
||||
announce_loop--;
|
||||
}
|
||||
}
|
||||
|
||||
// DEBUG
|
||||
file.seek(44);
|
||||
cnt = 0;
|
||||
if (segment == MESSAGE) {
|
||||
if (id_test == 10)
|
||||
transmitter_model.disable();
|
||||
|
||||
code = symfield_code.value(id_test);
|
||||
|
||||
if (code == 10) {
|
||||
pause = 11025; // p: 0.25s @ 44100Hz
|
||||
memset(audio_buffer, 0, 1024);
|
||||
} else if (code == 11) {
|
||||
pause = 33075; // P: 0.75s @ 44100Hz
|
||||
memset(audio_buffer, 0, 1024);
|
||||
} else {
|
||||
auto error = file.open("/numbers/" + file_names[code] + ".wav");
|
||||
if (error.is_valid()) return;
|
||||
|
||||
sample_duration = sound_sizes[code];
|
||||
|
||||
file.seek(44); // Skip header
|
||||
}
|
||||
|
||||
id_test++;
|
||||
}
|
||||
|
||||
cnt = 0;
|
||||
}
|
||||
|
||||
file.read(audio_buffer, 1024);
|
||||
|
||||
// Unsigned to signed, pretty stupid :/
|
||||
for (size_t n = 0; n < 1024; n++)
|
||||
audio_buffer[n] -= 0x80;
|
||||
|
||||
cnt += 1024;
|
||||
if (!pause) {
|
||||
size_t bytes_read = file.read(audio_buffer, 1024).value();
|
||||
|
||||
// Unsigned to signed, pretty stupid :/
|
||||
for (size_t n = 0; n < bytes_read; n++)
|
||||
audio_buffer[n] -= 0x80;
|
||||
|
||||
cnt += 1024;
|
||||
} else {
|
||||
if (pause >= 1024) {
|
||||
pause -= 1024;
|
||||
} else {
|
||||
cnt = sample_duration;
|
||||
pause = 0;
|
||||
}
|
||||
}
|
||||
|
||||
baseband::set_fifo_data(audio_buffer);
|
||||
}
|
||||
|
||||
void NumbersStationView::play_sound(uint16_t id) {
|
||||
void NumbersStationView::start_tx() {
|
||||
uint32_t divider;
|
||||
|
||||
if (sounds[id].size == 0) return;
|
||||
|
||||
auto error = file.open("/numbers/" + filenames[id] + ".wav");
|
||||
if (error.is_valid()) return;
|
||||
sample_duration = sound_sizes[0];
|
||||
cnt = sample_duration;
|
||||
|
||||
sample_duration = sounds[id].sample_duration;
|
||||
|
||||
cnt = 0;
|
||||
file.seek(44); // Skip header
|
||||
id_test = 0;
|
||||
announce_loop = 2;
|
||||
segment = ANNOUNCE;
|
||||
|
||||
prepare_audio();
|
||||
|
||||
@@ -116,21 +154,94 @@ void NumbersStationView::play_sound(uint16_t id) {
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Copied from soundboard, make globally available
|
||||
uint16_t NumbersStationView::fb_to_uint16(const std::string& fb) {
|
||||
return (fb[1] << 8) + fb[0];
|
||||
}
|
||||
|
||||
uint32_t NumbersStationView::fb_to_uint32(const std::string& fb) {
|
||||
return (fb[3] << 24) + (fb[2] << 16) + (fb[1] << 8) + fb[0];
|
||||
}
|
||||
|
||||
NumbersStationView::NumbersStationView(
|
||||
NavigationView& nav
|
||||
) {
|
||||
uint8_t m, d, dayofweek;
|
||||
uint16_t y;
|
||||
) : nav_ (nav)
|
||||
{
|
||||
uint8_t c;
|
||||
uint8_t y, m, d, dayofweek;
|
||||
size_t size;
|
||||
|
||||
char file_buffer[32];
|
||||
|
||||
c = 0;
|
||||
for (auto& file_name : file_names) {
|
||||
auto error = file.open("/numbers/" + file_name + ".wav");
|
||||
if (!error.is_valid()) {
|
||||
file.seek(22);
|
||||
file.read(file_buffer, 2);
|
||||
|
||||
// Is file mono ?
|
||||
if (fb_to_uint16(file_buffer) == 1) {
|
||||
file.seek(40);
|
||||
file.read(file_buffer, 4);
|
||||
size = fb_to_uint32(file_buffer);
|
||||
if (!size) break;
|
||||
sound_sizes[c] = size;
|
||||
c++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (c != 11) file_error = true;
|
||||
|
||||
baseband::run_image(portapack::spi_flash::image_tag_audio_tx);
|
||||
|
||||
add_children({ {
|
||||
&text_title,
|
||||
&field_frequency,
|
||||
&number_bw,
|
||||
&symfield_code,
|
||||
&button_tx,
|
||||
&button_exit
|
||||
} });
|
||||
|
||||
number_bw.set_value(15);
|
||||
number_bw.set_value(120);
|
||||
|
||||
field_frequency.set_value(transmitter_model.tuning_frequency());
|
||||
field_frequency.set_step(50000);
|
||||
field_frequency.on_change = [this](rf::Frequency f) {
|
||||
this->on_tuning_frequency_changed(f);
|
||||
};
|
||||
field_frequency.on_edit = [this, &nav]() {
|
||||
// TODO: Provide separate modal method/scheme?
|
||||
auto new_view = nav.push<FrequencyKeypadView>(transmitter_model.tuning_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
this->on_tuning_frequency_changed(f);
|
||||
this->field_frequency.set_value(f);
|
||||
};
|
||||
};
|
||||
|
||||
// DEBUG
|
||||
symfield_code.set_value(0, 10);
|
||||
symfield_code.set_value(1, 3);
|
||||
symfield_code.set_value(2, 4);
|
||||
symfield_code.set_value(3, 11);
|
||||
symfield_code.set_value(4, 6);
|
||||
symfield_code.set_value(5, 1);
|
||||
symfield_code.set_value(6, 9);
|
||||
symfield_code.set_value(7, 7);
|
||||
symfield_code.set_value(8, 8);
|
||||
symfield_code.set_value(9, 0);
|
||||
transmitter_model.set_tuning_frequency(103300000); // 103.3MHz
|
||||
|
||||
for (c = 0; c < 10; c++)
|
||||
symfield_code.set_symbol_list(c, "0123456789pP");
|
||||
|
||||
|
||||
|
||||
rtc::RTC datetime;
|
||||
rtcGetTime(&RTCD1, &datetime);
|
||||
@@ -144,6 +255,10 @@ NumbersStationView::NumbersStationView(
|
||||
|
||||
text_title.set(day_of_week[dayofweek]);
|
||||
|
||||
button_tx.on_select = [this, &nav](Button&){
|
||||
this->start_tx();
|
||||
};
|
||||
|
||||
button_exit.on_select = [&nav](Button&){
|
||||
nav.pop();
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_painter.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_font_fixed_8x16.hpp"
|
||||
#include "clock_manager.hpp"
|
||||
@@ -41,44 +41,58 @@ public:
|
||||
~NumbersStationView();
|
||||
|
||||
void focus() override;
|
||||
void paint(Painter& painter) override;
|
||||
|
||||
|
||||
std::string title() const override { return "Numbers station"; };
|
||||
|
||||
private:
|
||||
// Different from the one in ui_soundboard.hpp, simpler
|
||||
struct sound {
|
||||
uint32_t size = 0;
|
||||
uint32_t sample_duration = 0;
|
||||
// Sequencing state machine
|
||||
enum segments {
|
||||
ANNOUNCE = 0,
|
||||
MESSAGE,
|
||||
SIGNOFF
|
||||
};
|
||||
|
||||
sound sounds[11];
|
||||
Style style_red {
|
||||
.font = font::fixed_8x16,
|
||||
.background = Color::red(),
|
||||
.foreground = Color::black()
|
||||
};
|
||||
|
||||
const std::string filenames[11] = {
|
||||
"zero",
|
||||
"one",
|
||||
"two",
|
||||
"three",
|
||||
"four",
|
||||
"five",
|
||||
"six",
|
||||
"seven",
|
||||
"eight",
|
||||
"nine",
|
||||
"anounce"
|
||||
NavigationView& nav_;
|
||||
|
||||
segments segment;
|
||||
bool file_error = false;
|
||||
uint32_t sound_sizes[11];
|
||||
|
||||
const std::vector<std::string> file_names = {
|
||||
{ "0" },
|
||||
{ "1" },
|
||||
{ "2" },
|
||||
{ "3" },
|
||||
{ "4" },
|
||||
{ "5" },
|
||||
{ "6" },
|
||||
{ "7" },
|
||||
{ "8" },
|
||||
{ "9" },
|
||||
{ "announce" }
|
||||
};
|
||||
|
||||
const uint8_t month_table[12] = { 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4 };
|
||||
const char * day_of_week[7] = { "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" };
|
||||
const char * day_of_week[7] = { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" };
|
||||
|
||||
File file;
|
||||
uint8_t id_test, announce_loop;
|
||||
uint32_t cnt;
|
||||
uint32_t sample_duration;
|
||||
int8_t audio_buffer[1024];
|
||||
uint32_t pause = 0;
|
||||
|
||||
void on_tuning_frequency_changed(rf::Frequency f);
|
||||
void prepare_audio();
|
||||
void play_sound(uint16_t id);
|
||||
void start_tx();
|
||||
uint16_t fb_to_uint16(const std::string& fb);
|
||||
uint32_t fb_to_uint32(const std::string& fb);
|
||||
|
||||
// Schedule: save on sd card
|
||||
// For each day of the week, max 8 messages ?
|
||||
@@ -95,14 +109,27 @@ private:
|
||||
"Schedule:"
|
||||
};
|
||||
|
||||
FrequencyField field_frequency {
|
||||
{ 1 * 8, 4 },
|
||||
};
|
||||
NumberField number_bw {
|
||||
{ 11 * 8, 3 * 16 },
|
||||
{ 12 * 8, 2 * 16 },
|
||||
3,
|
||||
{1, 150},
|
||||
1,
|
||||
' '
|
||||
};
|
||||
|
||||
SymField symfield_code {
|
||||
{ 1*8, 3 * 16 },
|
||||
10,
|
||||
false
|
||||
};
|
||||
|
||||
Button button_tx {
|
||||
{ 21 * 8, 13 * 16, 64, 32 },
|
||||
"TX !"
|
||||
};
|
||||
Button button_exit {
|
||||
{ 21 * 8, 16 * 16, 64, 32 },
|
||||
"Exit"
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_freqman.hpp"
|
||||
|
||||
#include "portapack.hpp"
|
||||
using namespace portapack;
|
||||
@@ -140,14 +141,22 @@ FrequencyKeypadView::FrequencyKeypadView(
|
||||
button.on_select = button_fn;
|
||||
button.set_parent_rect({
|
||||
(n % 3) * button_w,
|
||||
(n / 3) * button_h + button_h,
|
||||
(n / 3) * button_h + 24,
|
||||
button_w, button_h
|
||||
});
|
||||
button.set_text(label);
|
||||
n++;
|
||||
}
|
||||
|
||||
add_children({ {
|
||||
&button_save,
|
||||
&button_close
|
||||
} });
|
||||
|
||||
button_save.on_select = [this, &nav](Button&) {
|
||||
nav.push<FrequencySaveView>(this->value());
|
||||
};
|
||||
|
||||
add_child(&button_close);
|
||||
button_close.on_select = [this, &nav](Button&) {
|
||||
if( on_changed ) {
|
||||
on_changed(this->value());
|
||||
|
||||
@@ -198,13 +198,17 @@ private:
|
||||
static constexpr int text_digits = mhz_digits + 1 + submhz_digits;
|
||||
|
||||
Text text_value {
|
||||
{ 0, 0, text_digits * button_w, button_h }
|
||||
{ 0, 4, 240, 16 }
|
||||
};
|
||||
|
||||
std::array<Button, 12> buttons;
|
||||
|
||||
Button button_save {
|
||||
{ 0, button_h * 4 + button_h, button_w, button_h },
|
||||
"Save"
|
||||
};
|
||||
Button button_close {
|
||||
{ 0, button_h * 4 + button_h, button_w * 3, button_h },
|
||||
{ button_w, button_h * 4 + button_h, button_w * 2, button_h },
|
||||
"Done"
|
||||
};
|
||||
|
||||
|
||||
@@ -74,10 +74,11 @@ void SoundBoardView::prepare_audio() {
|
||||
}
|
||||
|
||||
pbar.set_value(cnt);
|
||||
|
||||
size_t bytes_read = file.read(audio_buffer, 1024).value();
|
||||
|
||||
file.read(audio_buffer, 1024);
|
||||
|
||||
for (size_t n = 0; n < 1024; n++)
|
||||
// Unsigned to signed, pretty stupid :/
|
||||
for (size_t n = 0; n < bytes_read; n++)
|
||||
audio_buffer[n] -= 0x80;
|
||||
|
||||
cnt += 1024;
|
||||
@@ -87,6 +88,10 @@ void SoundBoardView::prepare_audio() {
|
||||
|
||||
void SoundBoardView::focus() {
|
||||
buttons[0].focus();
|
||||
|
||||
if (!max_sound) {
|
||||
nav_.display_modal("No files", "No files in /wav/ directory", ABORT, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void SoundBoardView::on_tuning_frequency_changed(rf::Frequency f) {
|
||||
@@ -202,7 +207,7 @@ void SoundBoardView::on_ctcss_changed(uint32_t v) {
|
||||
|
||||
SoundBoardView::SoundBoardView(
|
||||
NavigationView& nav
|
||||
)
|
||||
) : nav_ (nav)
|
||||
{
|
||||
std::vector<std::string> file_list;
|
||||
std::string file_name;
|
||||
@@ -256,11 +261,6 @@ SoundBoardView::SoundBoardView(
|
||||
}
|
||||
}
|
||||
|
||||
if (!c) {
|
||||
nav.display_modal("No files", "No files in /wav/ directory");
|
||||
nav.pop();
|
||||
}
|
||||
|
||||
max_sound = c;
|
||||
max_page = max_sound / 21; // 21 buttons per page
|
||||
|
||||
|
||||
@@ -44,6 +44,8 @@ public:
|
||||
std::string title() const override { return "Soundboard"; };
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
|
||||
enum tx_modes {
|
||||
NORMAL = 0,
|
||||
RANDOM
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* 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_whipcalc.hpp"
|
||||
|
||||
#include "ch.h"
|
||||
#include "portapack.hpp"
|
||||
#include "event_m0.hpp"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
using namespace portapack;
|
||||
|
||||
namespace ui {
|
||||
|
||||
void WhipCalcView::focus() {
|
||||
field_frequency.focus();
|
||||
}
|
||||
|
||||
void WhipCalcView::update_result() {
|
||||
double length, divider;
|
||||
|
||||
divider = ((double)options_type.selected_index_value() / 8.0);
|
||||
|
||||
// Metric
|
||||
length = (speed_of_light_mps / (double)field_frequency.value()) * divider;
|
||||
|
||||
auto m = to_string_dec_int((int)length, 2);
|
||||
auto cm = to_string_dec_int(int(length * 100.0) % 100, 2);
|
||||
auto mm = to_string_dec_int(int(length * 1000.0) % 10, 1);
|
||||
|
||||
text_result_metric.set(m + "m " + cm + "." + mm + "cm");
|
||||
|
||||
// ANT500 elements for crude adjustment
|
||||
length /= 0.14;
|
||||
if (int(length) <= 4) {
|
||||
auto elements = to_string_dec_int((int)length, 1);
|
||||
text_result_ant500.set(elements + " " + frac_str[((int(length * 10.0) % 10) + 1) / 3] + "elements");
|
||||
} else {
|
||||
text_result_ant500.set("-");
|
||||
}
|
||||
|
||||
// Imperial
|
||||
length = (speed_of_light_fps / (double)field_frequency.value()) * divider;
|
||||
|
||||
auto feet = to_string_dec_int((int)length, 3);
|
||||
auto inch = to_string_dec_int(int(length * 10.0) % 12, 2);
|
||||
auto inch_c = to_string_dec_int(int(length * 100.0) % 10, 1);
|
||||
|
||||
text_result_imperial.set(feet + "ft " + inch + "." + inch_c + "in");
|
||||
}
|
||||
|
||||
WhipCalcView::WhipCalcView(
|
||||
NavigationView& nav
|
||||
) {
|
||||
|
||||
add_children({ {
|
||||
&text_frequency,
|
||||
&field_frequency,
|
||||
&text_type,
|
||||
&options_type,
|
||||
&text_result_metric,
|
||||
&text_result_imperial,
|
||||
&text_result_ant500,
|
||||
&button_exit
|
||||
} });
|
||||
|
||||
options_type.on_change = [this](size_t, OptionsField::value_t) {
|
||||
this->update_result();
|
||||
};
|
||||
options_type.set_selected_index(2); // Quarter wave
|
||||
|
||||
field_frequency.set_value(transmitter_model.tuning_frequency());
|
||||
field_frequency.set_step(500000); // 500kHz step
|
||||
field_frequency.on_change = [this](rf::Frequency) {
|
||||
this->update_result();
|
||||
};
|
||||
field_frequency.on_edit = [this, &nav]() {
|
||||
// TODO: Provide separate modal method/scheme?
|
||||
auto new_view = nav.push<FrequencyKeypadView>(transmitter_model.tuning_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
this->update_result();
|
||||
this->field_frequency.set_value(f);
|
||||
};
|
||||
};
|
||||
|
||||
button_exit.on_select = [this, &nav](Button&) {
|
||||
nav.pop();
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#ifndef __UI_WHIPCALC_H__
|
||||
#define __UI_WHIPCALC_H__
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "string_format.hpp"
|
||||
|
||||
namespace ui {
|
||||
|
||||
class WhipCalcView : public View {
|
||||
public:
|
||||
WhipCalcView(NavigationView& nav);
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "Whip calculator"; };
|
||||
|
||||
private:
|
||||
const double speed_of_light_mps = 299792458.0; // m/s
|
||||
const double speed_of_light_fps = 983571087.90472; // feet/s
|
||||
|
||||
const std::string frac_str[4] = { "", "1/4 ", "1/2 ", "3/4 " };
|
||||
|
||||
void update_result();
|
||||
|
||||
Text text_frequency {
|
||||
{ 2 * 8, 2 * 16, 10 * 16, 16 },
|
||||
"Frequency:"
|
||||
};
|
||||
FrequencyField field_frequency {
|
||||
{ 13 * 8, 2 * 16 },
|
||||
};
|
||||
|
||||
Text text_type {
|
||||
{ 2 * 8, 3 * 16, 5 * 16, 16 },
|
||||
"Type:"
|
||||
};
|
||||
OptionsField options_type {
|
||||
{ 8 * 8, 3 * 16 },
|
||||
12,
|
||||
{
|
||||
{ "Full wave", 8 },
|
||||
{ "Half wave", 4 },
|
||||
{ "Quarter wave", 2 },
|
||||
{ "3/4 wave", 6 },
|
||||
{ "1/8 wave", 1 },
|
||||
{ "3/8 wave", 3 },
|
||||
{ "5/8 wave", 5 },
|
||||
{ "7/8 wave", 7 }
|
||||
}
|
||||
};
|
||||
|
||||
Text text_result_metric {
|
||||
{ 3 * 8, 5 * 16, 10 * 16, 16 },
|
||||
"-"
|
||||
};
|
||||
Text text_result_imperial {
|
||||
{ 2 * 8, 6 * 16, 10 * 16, 16 },
|
||||
"-"
|
||||
};
|
||||
Text text_result_ant500 {
|
||||
{ 2 * 8, 8 * 16, 14 * 16, 16 },
|
||||
"-"
|
||||
};
|
||||
|
||||
Button button_exit {
|
||||
{ 72, 264, 96, 32 },
|
||||
"Exit"
|
||||
};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
#endif/*__UI_WHIPCALC__*/
|
||||
@@ -159,6 +159,12 @@ private:
|
||||
|
||||
tx_modes tx_mode = IDLE;
|
||||
|
||||
// 93.975
|
||||
// 94.1625
|
||||
// 94.3125
|
||||
// 94.425
|
||||
// 95.0625
|
||||
// 95.925
|
||||
const rf::Frequency xylos_freqs[7] = { 31325000, 31387500, 31437500, 31475000, 31687500, 31975000, 88000000 };
|
||||
|
||||
char ccir_message[21];
|
||||
|
||||
@@ -110,12 +110,12 @@ constexpr region_t bootstrap {
|
||||
|
||||
constexpr region_t images {
|
||||
.offset = 0x10000,
|
||||
.size = 0x40000,
|
||||
.size = 0x70000,
|
||||
};
|
||||
|
||||
constexpr region_t application {
|
||||
.offset = 0x80000,
|
||||
.size = 0x40000,
|
||||
.size = 0x50000,
|
||||
};
|
||||
|
||||
} /* namespace spi_flash */
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user