diff --git a/firmware/application/CMakeLists.txt b/firmware/application/CMakeLists.txt index dc9e41d5c6e5c88e11c99fffc2f6aafcdb7ac3d2..f551f676ce8fab64c024edb1f4f3d00ed7fbb36c 100644 --- a/firmware/application/CMakeLists.txt +++ b/firmware/application/CMakeLists.txt @@ -214,7 +214,7 @@ set(CPPSRC ui/ui_tabview.cpp ui/ui_textentry.cpp ui/ui_transmitter.cpp - apps/ui_about.cpp + apps/ui_about_simple.cpp apps/ui_adsb_rx.cpp apps/ui_adsb_tx.cpp apps/ui_afsk_rx.cpp diff --git a/firmware/application/apps/ais_app.cpp b/firmware/application/apps/ais_app.cpp index 89244e87e7da56524b517610222255423388a950..7d42be8606f74fa2a08824b08e5d087c2b242db0 100644 --- a/firmware/application/apps/ais_app.cpp +++ b/firmware/application/apps/ais_app.cpp @@ -240,7 +240,7 @@ AISRecentEntryDetailView::AISRecentEntryDetailView(NavigationView& nav) { void AISRecentEntryDetailView::update_position() { if (send_updates) - geomap_view->update_position(ais::format::latlon_float(entry_.last_position.latitude.normalized()), ais::format::latlon_float(entry_.last_position.longitude.normalized())); + geomap_view->update_position(ais::format::latlon_float(entry_.last_position.latitude.normalized()), ais::format::latlon_float(entry_.last_position.longitude.normalized()), (float)entry_.last_position.true_heading); } void AISRecentEntryDetailView::focus() { diff --git a/firmware/application/apps/analog_audio_app.cpp b/firmware/application/apps/analog_audio_app.cpp index 4f3d724ab733f5aadc388b1ada54f8ce99d8ad75..b8d79e3ec51ab173dfcfc561270b6b3dd880dd24 100644 --- a/firmware/application/apps/analog_audio_app.cpp +++ b/firmware/application/apps/analog_audio_app.cpp @@ -83,6 +83,32 @@ NBFMOptionsView::NBFMOptionsView( }; } +/* SPECOptionsView *******************************************************/ + +SPECOptionsView::SPECOptionsView( + AnalogAudioView* view, const Rect parent_rect, const Style* const style +) : View { parent_rect } +{ + set_style(style); + + add_children({ + &label_config, + &options_config, + &text_speed, + &field_speed + }); + + options_config.set_selected_index(view->get_spec_bw_index()); + options_config.on_change = [this, view](size_t n, OptionsField::value_t bw) { + view->set_spec_bw(n, bw); + }; + + field_speed.set_value(view->get_spec_trigger()); + field_speed.on_change = [this, view](int32_t v) { + view->set_spec_trigger(v); + }; +} + /* AnalogAudioView *******************************************************/ AnalogAudioView::AnalogAudioView( @@ -157,6 +183,29 @@ AnalogAudioView::AnalogAudioView( on_modulation_changed(static_cast<ReceiverModel::Mode>(modulation)); } +size_t AnalogAudioView::get_spec_bw_index() { + return spec_bw_index; +} + +void AnalogAudioView::set_spec_bw(size_t index, uint32_t bw) { + spec_bw_index = index; + spec_bw = bw; + + baseband::set_spectrum(bw, spec_trigger); + receiver_model.set_sampling_rate(bw); + receiver_model.set_baseband_bandwidth(bw/2); +} + +uint16_t AnalogAudioView::get_spec_trigger() { + return spec_trigger; +} + +void AnalogAudioView::set_spec_trigger(uint16_t trigger) { + spec_trigger = trigger; + + baseband::set_spectrum(spec_bw, spec_trigger); +} + AnalogAudioView::~AnalogAudioView() { // TODO: Manipulating audio codec here, and in ui_receiver.cpp. Good to do // both? @@ -272,6 +321,7 @@ void AnalogAudioView::on_show_options_modulation() { break; case ReceiverModel::Mode::SpectrumAnalysis: + widget = std::make_unique<SPECOptionsView>(this, nbfm_view_rect, &style_options_group); waterfall.show_audio_spectrum_view(false); text_ctcss.hidden(true); break; @@ -315,15 +365,17 @@ void AnalogAudioView::update_modulation(const ReceiverModel::Mode modulation) { } baseband::run_image(image_tag); - + if (modulation == ReceiverModel::Mode::SpectrumAnalysis) { - baseband::set_spectrum(20000000, 127); + baseband::set_spectrum(spec_bw, spec_trigger); } const auto is_wideband_spectrum_mode = (modulation == ReceiverModel::Mode::SpectrumAnalysis); receiver_model.set_modulation(modulation); - receiver_model.set_sampling_rate(is_wideband_spectrum_mode ? 20000000 : 3072000); - receiver_model.set_baseband_bandwidth(is_wideband_spectrum_mode ? 12000000 : 1750000); + + receiver_model.set_sampling_rate(is_wideband_spectrum_mode ? spec_bw : 3072000); + receiver_model.set_baseband_bandwidth(is_wideband_spectrum_mode ? spec_bw/2 : 1750000); + receiver_model.enable(); // TODO: This doesn't belong here! There's a better way. diff --git a/firmware/application/apps/analog_audio_app.hpp b/firmware/application/apps/analog_audio_app.hpp index 993e611e0ce9b535326ecf5fb103972672c61f8e..80f3bbe6dff648de6373ca7b04899707ae8f9659 100644 --- a/firmware/application/apps/analog_audio_app.hpp +++ b/firmware/application/apps/analog_audio_app.hpp @@ -94,6 +94,43 @@ private: }; }; +class AnalogAudioView; + +class SPECOptionsView : public View { +public: + SPECOptionsView(AnalogAudioView* view, const Rect parent_rect, const Style* const style); + +private: + Text label_config { + { 0 * 8, 0 * 16, 2 * 8, 1 * 16 }, + "BW", + }; + OptionsField options_config { + { 3 * 8, 0 * 16 }, + 4, + { + { "20m ", 20000000 }, + { "10m ", 10000000 }, + { " 5m ", 5000000 }, + { " 2m ", 2000000 }, + { " 1m ", 1000000 }, + { "500k", 500000 }, + } + }; + + Text text_speed { + { 9 * 8, 0 * 16, 8 * 8, 1 * 16 }, + "SP /63" + }; + NumberField field_speed { + { 12 * 8, 0 * 16 }, + 2, + { 0, 63 }, + 1, + ' ', + }; +}; + class AnalogAudioView : public View { public: AnalogAudioView(NavigationView& nav); @@ -106,13 +143,23 @@ public: void focus() override; std::string title() const override { return "Analog audio"; }; - + + size_t get_spec_bw_index(); + void set_spec_bw(size_t index, uint32_t bw); + + uint16_t get_spec_trigger(); + void set_spec_trigger(uint16_t trigger); + private: static constexpr ui::Dim header_height = 3 * 16; const Rect options_view_rect { 0 * 8, 1 * 16, 30 * 8, 1 * 16 }; const Rect nbfm_view_rect { 0 * 8, 1 * 16, 18 * 8, 1 * 16 }; + size_t spec_bw_index = 0; + uint32_t spec_bw = 20000000; + uint16_t spec_trigger = 63; + NavigationView& nav_; //bool exit_on_squelch { false }; diff --git a/firmware/application/apps/ui_about_simple.cpp b/firmware/application/apps/ui_about_simple.cpp new file mode 100644 index 0000000000000000000000000000000000000000..2c6f796ed8539e9ad1d171201499878ade3031c9 --- /dev/null +++ b/firmware/application/apps/ui_about_simple.cpp @@ -0,0 +1,75 @@ +#include "ui_about_simple.hpp" + +namespace ui +{ + AboutView::AboutView(NavigationView &nav) + { + add_children({&console, &button_ok}); + + button_ok.on_select = [&nav](Button &) { + nav.pop(); + }; + + console.writeln("\x1B\x07List of contributors:\x1B\x10"); + console.writeln(""); + } + + void AboutView::update() + { + if (++timer > 200) + { + timer = 0; + + switch (++frame) + { + case 1: + // TODO: Generate this automatically from github + // https://github.com/eried/portapack-mayhem/graphs/contributors?to=2022-01-01&from=2020-04-12&type=c + console.writeln("\x1B\x06Mayhem:\x1B\x10"); + console.writeln("eried,euquiq,gregoryfenton"); + console.writeln("johnelder,jwetzell,nnemanjan00"); + console.writeln("N0vaPixel,klockee,jamesshao8"); + console.writeln(""); + break; + + case 2: + // https://github.com/eried/portapack-mayhem/graphs/contributors?to=2020-04-12&from=2015-07-31&type=c + console.writeln("\x1B\x06Havoc:\x1B\x10"); + console.writeln("furrtek,mrmookie,notpike"); + console.writeln("mjwaxios,ImDroided,Giorgiofox"); + console.writeln("F4GEV,z4ziggy,xmycroftx"); + console.writeln("troussos,silascutler"); + console.writeln("nickbouwhuis,msoose,leres"); + console.writeln("joakar,dhoetger,clem-42"); + console.writeln("brianlechthaler,ZeroChaos-..."); + console.writeln(""); + break; + + case 3: + // https://github.com/eried/portapack-mayhem/graphs/contributors?from=2014-07-05&to=2015-07-31&type=c + console.writeln("\x1B\x06PortaPack:\x1B\x10"); + console.writeln("jboone,argilo"); + console.writeln(""); + break; + + case 4: + // https://github.com/mossmann/hackrf/graphs/contributors + console.writeln("\x1B\x06HackRF:\x1B\x10"); + console.writeln("mossmann,dominicgs,bvernoux"); + console.writeln("bgamari,schneider42,miek"); + console.writeln("willcode,hessu,Sec42"); + console.writeln("yhetti,ckuethe,smunaut"); + console.writeln("wishi,mrbubble62,scateu..."); + console.writeln(""); + frame = 0; // Loop + break; + } + } + } + + void AboutView::focus() + { + button_ok.focus(); + } + +} /* namespace ui */ \ No newline at end of file diff --git a/firmware/application/apps/ui_about_simple.hpp b/firmware/application/apps/ui_about_simple.hpp new file mode 100644 index 0000000000000000000000000000000000000000..a059011907ec2d333436566a68c87764375e2095 --- /dev/null +++ b/firmware/application/apps/ui_about_simple.hpp @@ -0,0 +1,40 @@ +#ifndef __UI_ABOUT_SIMPLE_H__ +#define __UI_ABOUT_SIMPLE_H__ + +#include "ui_widget.hpp" +#include "ui_navigation.hpp" +#include "ui_font_fixed_8x16.hpp" + +#include <cstdint> + +namespace ui +{ + class AboutView : public View + { + public: + AboutView(NavigationView &nav); + void focus() override; + std::string title() const override { return "About"; }; + int32_t timer{180}; + short frame{0}; + + private: + void update(); + + Console console{ + {0, 10, 240, 240}}; + + Button button_ok{ + {240/3, 270, 240/3, 24}, + "OK", + }; + + MessageHandlerRegistration message_handler_update{ + Message::ID::DisplayFrameSync, + [this](const Message *const) { + this->update(); + }}; + }; +} // namespace ui + +#endif /*__UI_ABOUT_SIMPLE_H__*/ diff --git a/firmware/application/apps/ui_adsb_rx.cpp b/firmware/application/apps/ui_adsb_rx.cpp index 76d22730c68b46d06a8bf76fb30d1ad5538ad770..ba08048129954ea195b44a6f871a886666c79abd 100644 --- a/firmware/application/apps/ui_adsb_rx.cpp +++ b/firmware/application/apps/ui_adsb_rx.cpp @@ -92,12 +92,16 @@ void ADSBRxDetailsView::update(const AircraftRecentEntry& entry) { 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_position(entry_copy.pos.latitude, entry_copy.pos.longitude); + geomap_view->update_position(entry_copy.pos.latitude, entry_copy.pos.longitude, entry_copy.velo.heading); } ADSBRxDetailsView::~ADSBRxDetailsView() { @@ -123,6 +127,7 @@ ADSBRxDetailsView::ADSBRxDetailsView( &text_airline, &text_country, &text_infos, + &text_info2, &text_frame_pos_even, &text_frame_pos_odd, &button_see_map @@ -172,7 +177,7 @@ ADSBRxDetailsView::ADSBRxDetailsView( GeoPos::alt_unit::FEET, entry_copy.pos.latitude, entry_copy.pos.longitude, - 0, + entry_copy.velo.heading, [this]() { send_updates = false; }); @@ -199,7 +204,7 @@ void ADSBRxView::on_frame(const ADSBFrameMessage * message) { auto frame = message->frame; uint32_t ICAO_address = frame.get_ICAO_address(); - + if (frame.check_CRC() && frame.get_ICAO_address()) { rtcGetTime(&RTCD1, &datetime); auto& entry = ::on_packet(recent, ICAO_address); @@ -214,6 +219,7 @@ void ADSBRxView::on_frame(const ADSBFrameMessage * message) { 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 ((msg_type >= 1) && (msg_type <= 4)) { @@ -224,10 +230,10 @@ void ADSBRxView::on_frame(const ADSBFrameMessage * message) { entry.set_frame_pos(frame, raw_data[6] & 4); if (entry.pos.valid) { - str_info = "Alt:" + to_string_dec_uint(entry.pos.altitude) + - " Lat" + to_string_dec_int(entry.pos.latitude) + + str_info = "Alt:" + to_string_dec_int(entry.pos.altitude) + + " Lat:" + to_string_dec_int(entry.pos.latitude) + "." + to_string_dec_int((int)abs(entry.pos.latitude * 1000) % 100, 2, '0') + - " Lon" + to_string_dec_int(entry.pos.longitude) + + " Lon:" + to_string_dec_int(entry.pos.longitude) + "." + to_string_dec_int((int)abs(entry.pos.longitude * 1000) % 100, 2, '0'); entry.set_info_string(str_info); @@ -236,6 +242,13 @@ void ADSBRxView::on_frame(const ADSBFrameMessage * message) { if (send_updates) details_view->update(entry); } + } else if(msg_type == 19 && msg_sub >= 1 && msg_sub <= 4){ + entry.set_frame_velo(frame); + logentry += "Type:" + to_string_dec_uint(msg_sub) + + " Hdg:" + to_string_dec_uint(entry.velo.heading) + + " Spd: "+ to_string_dec_int(entry.velo.speed); + if (send_updates) + details_view->update(entry); } } recent_entries_view.set_dirty(); diff --git a/firmware/application/apps/ui_adsb_rx.hpp b/firmware/application/apps/ui_adsb_rx.hpp index c0316623394bdbc794d3f6a9351aa7e7e3154312..dd7adb5a5d5d33b46b3d1c9ff3719cd3302b382b 100644 --- a/firmware/application/apps/ui_adsb_rx.hpp +++ b/firmware/application/apps/ui_adsb_rx.hpp @@ -49,7 +49,7 @@ struct AircraftRecentEntry { uint16_t hits { 0 }; uint32_t age { 0 }; adsb_pos pos { false, 0, 0, 0 }; - + adsb_vel velo { false, 0, 999 }; ADSBFrame frame_pos_even { }; ADSBFrame frame_pos_odd { }; @@ -86,6 +86,10 @@ struct AircraftRecentEntry { pos = decode_frame_pos(frame_pos_even, frame_pos_odd); } } + + void set_frame_velo(ADSBFrame& frame){ + velo = decode_frame_velo(frame); + } void set_info_string(std::string& new_info_string) { info_string = new_info_string; @@ -146,8 +150,8 @@ private: { { 0 * 8, 2 * 16 }, "Last seen:", Color::light_grey() }, { { 0 * 8, 3 * 16 }, "Airline:", Color::light_grey() }, { { 0 * 8, 5 * 16 }, "Country:", Color::light_grey() }, - { { 0 * 8, 12 * 16 }, "Even position frame:", Color::light_grey() }, - { { 0 * 8, 14 * 16 }, "Odd position frame:", Color::light_grey() } + { { 0 * 8, 13 * 16 }, "Even position frame:", Color::light_grey() }, + { { 0 * 8, 15 * 16 }, "Odd position frame:", Color::light_grey() } }; Text text_callsign { @@ -174,17 +178,23 @@ private: { 0 * 8, 6 * 16, 30 * 8, 16 }, "-" }; + + Text text_info2 { + {0*8, 7*16, 30*8, 16}, + "-" + }; + Text text_frame_pos_even { - { 0 * 8, 13 * 16, 30 * 8, 16 }, + { 0 * 8, 14 * 16, 30 * 8, 16 }, "-" }; Text text_frame_pos_odd { - { 0 * 8, 15 * 16, 30 * 8, 16 }, + { 0 * 8, 16 * 16, 30 * 8, 16 }, "-" }; Button button_see_map { - { 8 * 8, 8 * 16, 14 * 8, 3 * 16 }, + { 8 * 8, 9 * 16, 14 * 8, 3 * 16 }, "See on map" }; }; diff --git a/firmware/application/apps/ui_debug.cpp b/firmware/application/apps/ui_debug.cpp index 6d81f620f229e8a22c1250b7946daf8dfc114093..8622911c9f7f133fe3f8a049d91ed20006bf2835 100644 --- a/firmware/application/apps/ui_debug.cpp +++ b/firmware/application/apps/ui_debug.cpp @@ -121,7 +121,7 @@ void TemperatureWidget::paint(Painter& painter) { } TemperatureWidget::temperature_t TemperatureWidget::temperature(const sample_t sensor_value) const { - return -45 + sensor_value * 5; + return -35 + sensor_value * 4; //max2837 datasheet temp 25ºC has sensor value: 15 } std::string TemperatureWidget::temperature_str(const temperature_t temperature) const { diff --git a/firmware/application/apps/ui_debug.hpp b/firmware/application/apps/ui_debug.hpp index 6589969841b91d11ad411f7455c2d9b9a1d9378b..1e3d60035eba60a794ebcda613c2b624745ebbe9 100644 --- a/firmware/application/apps/ui_debug.hpp +++ b/firmware/application/apps/ui_debug.hpp @@ -101,10 +101,10 @@ private: std::string temperature_str(const temperature_t temperature) const; - static constexpr temperature_t display_temp_min = 0; + static constexpr temperature_t display_temp_min = -10; //Accomodate negative values, present in cold startup cases static constexpr temperature_t display_temp_scale = 3; static constexpr int bar_width = 1; - static constexpr int temp_len = 3; + static constexpr int temp_len = 4; //Now scale shows up to 4 chars ("-10C") }; class TemperatureView : public View { diff --git a/firmware/application/apps/ui_mictx.cpp b/firmware/application/apps/ui_mictx.cpp index 8145b28eca4b82c81cf0095b8017ecb5e872aa0d..0d7b23672cd38f1b702bc23e2bc0c5108bdd5f10 100644 --- a/firmware/application/apps/ui_mictx.cpp +++ b/firmware/application/apps/ui_mictx.cpp @@ -61,22 +61,22 @@ void MicTXView::configure_baseband() { void MicTXView::set_tx(bool enable) { if (enable) { + if (rx_enabled) //If audio RX is enabled + rxaudio(false); //Then turn off audio RX transmitting = true; configure_baseband(); transmitter_model.enable(); portapack::pin_i2s0_rx_sda.mode(3); // This is already done in audio::init but gets changed by the CPLD overlay reprogramming - //gpio_tx.write(1); - //led_tx.on(); } else { if (transmitting && rogerbeep_enabled) { - baseband::request_beep(); - transmitting = false; - } else { + baseband::request_beep(); //Transmit the roger beep + transmitting = false; //And flag the end of the transmission so ... + } else { // (if roger beep was enabled, this will be executed after the beep ends transmitting. transmitting = false; configure_baseband(); transmitter_model.disable(); - //gpio_tx.write(0); - //led_tx.off(); + if (rx_enabled) //If audio RX is enabled and we've been transmitting + rxaudio(true); //Turn back on audio RX } } } @@ -120,6 +120,39 @@ void MicTXView::do_timing() { void MicTXView::on_tuning_frequency_changed(rf::Frequency f) { transmitter_model.set_tuning_frequency(f); + //if ( rx_enabled ) + receiver_model.set_tuning_frequency(f); //Update freq also for RX +} + +void MicTXView::rxaudio(bool is_on) { + if (is_on) { + audio::input::stop(); + baseband::shutdown(); + baseband::run_image(portapack::spi_flash::image_tag_nfm_audio); + receiver_model.set_modulation(ReceiverModel::Mode::NarrowbandFMAudio); + receiver_model.set_sampling_rate(3072000); + receiver_model.set_baseband_bandwidth(1750000); + receiver_model.set_tuning_frequency(field_frequency.value()); //probably this too can be commented out. + receiver_model.enable(); + audio::output::start(); + } else { //These incredibly convoluted steps are required for the vumeter to reappear when stopping RX. + receiver_model.disable(); + baseband::shutdown(); + baseband::run_image(portapack::spi_flash::image_tag_mic_tx); + audio::input::start(); + transmitter_model.enable(); + portapack::pin_i2s0_rx_sda.mode(3); + transmitting = false; + configure_baseband(); + transmitter_model.disable(); + } +} + +void MicTXView::on_headphone_volume_changed(int32_t v) { + //if (rx_enabled) { + const auto new_volume = volume_t::decibel(v - 99) + audio::headphone::volume_range().max; + receiver_model.set_headphone_volume(new_volume); + //} } MicTXView::MicTXView( @@ -142,9 +175,12 @@ MicTXView::MicTXView( &field_frequency, &options_tone_key, &check_rogerbeep, + &check_rxactive, + &field_volume, + &field_squelch, &text_ptt }); - + tone_keys_populate(options_tone_key); options_tone_key.on_change = [this](size_t i, int32_t) { tone_key_index = i; @@ -168,6 +204,7 @@ MicTXView::MicTXView( new_view->on_changed = [this](rf::Frequency f) { this->on_tuning_frequency_changed(f); this->field_frequency.set_value(f); + set_dirty(); }; }; @@ -178,16 +215,15 @@ MicTXView::MicTXView( check_va.on_select = [this](Checkbox&, bool v) { va_enabled = v; - text_ptt.hidden(v); - set_dirty(); + text_ptt.hidden(v); //hide / show PTT text + check_rxactive.hidden(v); //hide / show the RX AUDIO + set_dirty(); //Refresh display }; - check_va.set_value(false); check_rogerbeep.on_select = [this](Checkbox&, bool v) { rogerbeep_enabled = v; }; - check_rogerbeep.set_value(false); - + field_va_level.on_change = [this](int32_t v) { va_level = v; vumeter.set_mark(v); @@ -203,7 +239,24 @@ MicTXView::MicTXView( 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_volume.set_value((receiver_model.headphone_volume() - audio::headphone::volume_range().max).decibel() + 99); + field_volume.on_change = [this](int32_t v) { this->on_headphone_volume_changed(v); }; + + 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); + transmitter_model.set_sampling_rate(sampling_rate); transmitter_model.set_baseband_bandwidth(1750000); @@ -216,6 +269,8 @@ MicTXView::MicTXView( MicTXView::~MicTXView() { audio::input::stop(); transmitter_model.disable(); + if (rx_enabled) //Also turn off audio rx if enabled + rxaudio(false); baseband::shutdown(); } diff --git a/firmware/application/apps/ui_mictx.hpp b/firmware/application/apps/ui_mictx.hpp index 4cebe343c7ca62f7cac5ce476d903f3f7fa83935..0f27206c2f288eefa04bbdadeeb80beb34297f9f 100644 --- a/firmware/application/apps/ui_mictx.hpp +++ b/firmware/application/apps/ui_mictx.hpp @@ -30,6 +30,7 @@ #include "transmitter_model.hpp" #include "tone_key.hpp" #include "message.hpp" +#include "receiver_model.hpp" namespace ui { @@ -54,11 +55,11 @@ public: return false; }; - std::string title() const override { return "Microphone TX"; }; + std::string title() const override { return "Mic TX RX"; }; private: static constexpr uint32_t sampling_rate = 1536000U; - static constexpr uint32_t lcd_frame_duration = (256 * 1000UL) / 60; // 1 frame @ 60fps in ms .8 fixed point + static constexpr uint32_t lcd_frame_duration = (256 * 1000UL) / 60; // 1 frame @ 60fps in ms .8 fixed point /60 void update_vumeter(); void do_timing(); @@ -66,10 +67,14 @@ private: void on_tuning_frequency_changed(rf::Frequency f); void on_tx_progress(const bool done); void configure_baseband(); + + void rxaudio(bool is_on); + void on_headphone_volume_changed(int32_t v); bool transmitting { false }; - bool va_enabled { }; - bool rogerbeep_enabled { }; + bool va_enabled { false }; + bool rogerbeep_enabled { false }; + bool rx_enabled { false }; uint32_t tone_key_index { }; float mic_gain { 1.0 }; uint32_t audio_level { 0 }; @@ -80,23 +85,25 @@ private: uint32_t decay_timer { 0 }; Labels labels { - { { 7 * 8, 1 * 8 }, "Mic. gain:", Color::light_grey() }, - { { 7 * 8, 4 * 8 }, "Frequency:", Color::light_grey() }, - { { 7 * 8, 6 * 8 }, "Bandwidth: kHz", Color::light_grey() }, - { { 9 * 8, 13 * 8 }, "Level: /255", Color::light_grey() }, - { { 9 * 8, 15 * 8 }, "Attack: ms", Color::light_grey() }, - { { 9 * 8, 17 * 8 }, "Decay: ms", Color::light_grey() }, - { { 7 * 8, 21 * 8 }, "Tone key:", Color::light_grey() } + { { 3 * 8, 1 * 8 }, "MIC. GAIN:", Color::light_grey() }, + { { 3 * 8, 3 * 8 }, "FREQUENCY:", Color::light_grey() }, + { { 3 * 8, 5 * 8 }, "BANDWIDTH: kHz", Color::light_grey() }, + { { 7 * 8, 11 * 8 }, "LEVEL: /255", Color::light_grey() }, + { { 6 * 8, 13 * 8 }, "ATTACK: ms", Color::light_grey() }, + { { 7 * 8, 15 * 8 }, "DECAY: ms", Color::light_grey() }, + { { 4 * 8, 18 * 8 }, "TONE KEY:", Color::light_grey() }, + { { 9 * 8, 30 * 8 }, "VOL:", Color::light_grey() }, + { { 5 * 8, 32 * 8 }, "SQUELCH:", Color::light_grey() } }; VuMeter vumeter { - { 1 * 8, 2 * 8, 5 * 8, 32 * 8 }, - 20, - false + { 0 * 8, 1 * 8, 2 * 8, 33 * 8 }, + 12, + true }; OptionsField options_gain { - { 17 * 8, 1 * 8 }, + { 13 * 8, 1 * 8 }, 4, { { "x0.5", 5 }, @@ -107,10 +114,10 @@ private: }; FrequencyField field_frequency { - { 17 * 8, 4 * 8 }, + { 13 * 8, 3 * 8 }, }; NumberField field_bw { - { 17 * 8, 6 * 8 }, + { 13 * 8, 5 * 8 }, 3, { 0, 150 }, 1, @@ -118,28 +125,28 @@ private: }; Checkbox check_va { - { 7 * 8, 10 * 8 }, + { 3 * 8, (9 * 8) - 4 }, 7, "Voice activation", false }; NumberField field_va_level { - { 15 * 8, 13 * 8 }, + { 13 * 8, 11 * 8 }, 3, { 0, 255 }, 2, ' ' }; NumberField field_va_attack { - { 16 * 8, 15 * 8 }, + { 13 * 8, 13 * 8 }, 3, { 0, 999 }, 20, ' ' }; NumberField field_va_decay { - { 15 * 8, 17 * 8 }, + { 13 * 8, 15 * 8 }, 4, { 0, 9999 }, 100, @@ -147,28 +154,52 @@ private: }; OptionsField options_tone_key { - { 7 * 8, 23 * 8 }, + { 10 * 8, 20 * 8 }, 23, { } }; Checkbox check_rogerbeep { - { 7 * 8, 26 * 8 }, + { 3 * 8, 23 * 8 }, 10, "Roger beep", false }; + + Checkbox check_rxactive { + { 3 * 8, (27 * 8) + 4 }, + 8, + "RX audio listening", + false + }; + + NumberField field_volume { + { 13 * 8, 30 * 8 }, + 2, + { 0, 99 }, + 1, + ' ', + }; + NumberField field_squelch { + { 13 * 8, 32 * 8 }, + 2, + { 0, 99 }, + 1, + ' ', + }; + Text text_ptt { - { 7 * 8, 17 * 16, 16 * 8, 16 }, + { 7 * 8, 35 * 8, 16 * 8, 16 }, "PTT: RIGHT BUTTON" }; - + + MessageHandlerRegistration message_handler_lcd_sync { Message::ID::DisplayFrameSync, [this](const Message* const) { - this->update_vumeter(); this->do_timing(); + this->update_vumeter(); } }; diff --git a/firmware/application/apps/ui_scanner.cpp b/firmware/application/apps/ui_scanner.cpp index 26c00728662ef863bf9d2b704c3c0fa8352feabd..65c4788df405e73767ce3a88861c27460c8bd19f 100644 --- a/firmware/application/apps/ui_scanner.cpp +++ b/firmware/application/apps/ui_scanner.cpp @@ -22,10 +22,6 @@ #include "ui_scanner.hpp" -#include "baseband_api.hpp" -#include "string_format.hpp" -#include "audio.hpp" - using namespace portapack; namespace ui { @@ -38,6 +34,10 @@ ScannerThread::ScannerThread( } ScannerThread::~ScannerThread() { + stop(); +} + +void ScannerThread::stop() { if( thread ) { chThdTerminate(thread); chThdWait(thread); @@ -49,6 +49,28 @@ void ScannerThread::set_scanning(const bool v) { _scanning = v; } +bool ScannerThread::is_scanning() { + return _scanning; +} + +void ScannerThread::set_freq_lock(const uint32_t v) { + _freq_lock = v; +} + +uint32_t ScannerThread::is_freq_lock() { + return _freq_lock; +} + +void ScannerThread::set_freq_del(const uint32_t v) { + _freq_del = v; +} + +void ScannerThread::change_scanning_direction() { + _fwd = !_fwd; + chThdSleepMilliseconds(300); //Give some pause after reversing scanning direction + +} + msg_t ScannerThread::static_fn(void* arg) { auto obj = static_cast<ScannerThread*>(arg); obj->run(); @@ -56,36 +78,78 @@ msg_t ScannerThread::static_fn(void* arg) { } void ScannerThread::run() { - RetuneMessage message { }; - uint32_t frequency_index = 0; - - while( !chThdShouldTerminate() ) { - if (_scanning) { - // Retune - receiver_model.set_tuning_frequency(frequency_list_[frequency_index]); - - message.range = frequency_index; - EventDispatcher::send_message(message); - - - frequency_index++; - if (frequency_index >= frequency_list_.size()) - frequency_index = 0; + if (frequency_list_.size()) { //IF THERE IS A FREQUENCY LIST ... + RetuneMessage message { }; + uint32_t frequency_index = frequency_list_.size(); + bool restart_scan = false; //Flag whenever scanning is restarting after a pause + while( !chThdShouldTerminate() ) { + if (_scanning) { //Scanning + if (_freq_lock == 0) { //normal scanning (not performing freq_lock) + if (!restart_scan) { //looping at full speed + if (_fwd) { //forward + frequency_index++; + if (frequency_index >= frequency_list_.size()) + frequency_index = 0; + + } else { //reverse + if (frequency_index < 1) + frequency_index = frequency_list_.size(); + frequency_index--; + } + receiver_model.set_tuning_frequency(frequency_list_[frequency_index]); // Retune + } + else + restart_scan=false; //Effectively skipping first retuning, giving system time + } + message.range = frequency_index; //Inform freq (for coloring purposes also!) + EventDispatcher::send_message(message); + } + else { //NOT scanning + if (_freq_del != 0) { //There is a frequency to delete + for (uint16_t i = 0; i < frequency_list_.size(); i++) { //Search for the freq to delete + if (frequency_list_[i] == _freq_del) + { //found: Erase it + frequency_list_.erase(frequency_list_.begin() + i); + if (i==0) //set scan index one place back to compensate + i=frequency_list_.size(); + else + i--; + break; + } + } + _freq_del = 0; //deleted. + } + else { + restart_scan=true; //Flag the need for skipping a cycle when restarting scan + } + } + chThdSleepMilliseconds(50); //Needed to (eventually) stabilize the receiver into new freq } - - chThdSleepMilliseconds(50); } } void ScannerView::handle_retune(uint32_t i) { - text_cycle.set( to_string_dec_uint(i) + "/" + - to_string_dec_uint(frequency_list.size()) + " : " + - to_string_dec_uint(frequency_list[i]) ); - desc_cycle.set( description_list[i] ); + switch (scan_thread->is_freq_lock()) + { + case 0: //NO FREQ LOCK, ONGOING STANDARD SCANNING + text_cycle.set( to_string_dec_uint(i + 1,3) ); + current_index = i; //since it is an ongoing scan, this is a new index + if (description_list[current_index].size() > 0) desc_cycle.set( description_list[current_index] ); //Show new description + break; + case 1: //STARTING LOCK FREQ + big_display.set_style(&style_yellow); + break; + case MAX_FREQ_LOCK: //FREQ IS STRONG: GREEN and scanner will pause when on_statistics_update() + big_display.set_style(&style_green); + break; + default: //freq lock is checking the signal, do not update display + return; + } + big_display.set(frequency_list[current_index]); //UPDATE the big Freq after 0, 1 or MAX_FREQ_LOCK (at least, for color synching) } void ScannerView::focus() { - field_lna.focus(); + field_mode.focus(); } ScannerView::~ScannerView() { @@ -94,9 +158,20 @@ ScannerView::~ScannerView() { baseband::shutdown(); } +void ScannerView::show_max() { //show total number of freqs to scan + if (frequency_list.size() == MAX_DB_ENTRY) { + text_max.set_style(&style_red); + text_max.set( "/ " + to_string_dec_uint(MAX_DB_ENTRY) + " (DB MAX!)"); + } + else { + text_max.set_style(&style_grey); + text_max.set( "/ " + to_string_dec_uint(frequency_list.size())); + } +} + ScannerView::ScannerView( - NavigationView& -) + NavigationView& nav + ) : nav_ { nav } { add_children({ &labels, @@ -105,110 +180,340 @@ ScannerView::ScannerView( &field_rf_amp, &field_volume, &field_bw, - &field_trigger, &field_squelch, &field_wait, - //&record_view, + &rssi, &text_cycle, + &text_max, &desc_cycle, - //&waterfall, + &big_display, + &button_manual_start, + &button_manual_end, + &field_mode, + &step_mode, + &button_manual_scan, + &button_pause, + &button_dir, + &button_audio_app, + &button_mic_app, + &button_add, + &button_remove + }); - std::string scanner_file = "SCANNER"; - if (load_freqman_file(scanner_file, database)) { - for(auto& entry : database) { - // FIXME - if (entry.type == RANGE) { - for (uint32_t i=entry.frequency_a; i < entry.frequency_b; i+= 1000000) { - frequency_list.push_back(i); - description_list.push_back("RNG " + to_string_dec_uint(entry.frequency_a) + ">" + to_string_dec_uint(entry.frequency_b)); - } - } else { - frequency_list.push_back(entry.frequency_a); - description_list.push_back(entry.description); - } + def_step = change_mode(AM); //Start on AM + field_mode.set_by_value(AM); //Reflect the mode into the manual selector + + //HELPER: Pre-setting a manual range, based on stored frequency + rf::Frequency stored_freq = persistent_memory::tuned_frequency(); + frequency_range.min = stored_freq - 1000000; + button_manual_start.set_text(to_string_short_freq(frequency_range.min)); + frequency_range.max = stored_freq + 1000000; + button_manual_end.set_text(to_string_short_freq(frequency_range.max)); + + button_manual_start.on_select = [this, &nav](Button& button) { + auto new_view = nav_.push<FrequencyKeypadView>(frequency_range.min); + new_view->on_changed = [this, &button](rf::Frequency f) { + frequency_range.min = f; + button_manual_start.set_text(to_string_short_freq(f)); + }; + }; + + button_manual_end.on_select = [this, &nav](Button& button) { + auto new_view = nav.push<FrequencyKeypadView>(frequency_range.max); + new_view->on_changed = [this, &button](rf::Frequency f) { + frequency_range.max = f; + button_manual_end.set_text(to_string_short_freq(f)); + }; + }; + + button_pause.on_select = [this](Button&) { + if ( userpause ) + user_resume(); + else { + scan_pause(); + button_pause.set_text("RESUME"); //PAUSED, show resume + userpause=true; } - } else { - // DEBUG - // TODO: Clean this - frequency_list.push_back(466025000); - description_list.push_back("POCSAG-France"); - frequency_list.push_back(466050000); - description_list.push_back("POCSAG-France"); - frequency_list.push_back(466075000); - description_list.push_back("POCSAG-France"); - frequency_list.push_back(466175000); - description_list.push_back("POCSAG-France"); - frequency_list.push_back(466206250); - description_list.push_back("POCSAG-France"); - frequency_list.push_back(466231250); - description_list.push_back("POCSAG-France"); - } + }; - field_bw.set_selected_index(2); - field_bw.on_change = [this](size_t n, OptionsField::value_t) { - receiver_model.set_nbfm_configuration(n); + button_audio_app.on_select = [this](Button&) { + scan_thread->stop(); + nav_.pop(); + nav_.push<AnalogAudioView>(); }; - field_wait.on_change = [this](int32_t v) { - wait = v; + button_mic_app.on_select = [this](Button&) { + scan_thread->stop(); + nav_.pop(); + nav_.push<MicTXView>(); }; - field_wait.set_value(5); - field_trigger.on_change = [this](int32_t v) { - trigger = v; + button_remove.on_select = [this](Button&) { + if (frequency_list.size() > current_index) { + if (scan_thread->is_scanning()) //STOP Scanning if necessary + scan_thread->set_scanning(false); + scan_thread->set_freq_del(frequency_list[current_index]); + description_list.erase(description_list.begin() + current_index); + frequency_list.erase(frequency_list.begin() + current_index); + show_max(); //UPDATE new list size on screen + desc_cycle.set(" "); //Clean up description (cosmetic detail) + scan_thread->set_freq_lock(0); //Reset the scanner lock + if ( userpause ) //If user-paused, resume + user_resume(); + } }; - field_trigger.set_value(30); - - field_squelch.set_value(receiver_model.squelch_level()); - field_squelch.on_change = [this](int32_t v) { - squelch = v; - receiver_model.set_squelch_level(v); + + button_manual_scan.on_select = [this](Button&) { + if (!frequency_range.min || !frequency_range.max) { + nav_.display_modal("Error", "Both START and END freqs\nneed a value"); + } else if (frequency_range.min > frequency_range.max) { + nav_.display_modal("Error", "END freq\nis lower than START"); + } else { + audio::output::stop(); + scan_thread->stop(); //STOP SCANNER THREAD + frequency_list.clear(); + description_list.clear(); + def_step = step_mode.selected_index_value(); //Use def_step from manual selector + + description_list.push_back( + "M:" + to_string_short_freq(frequency_range.min) + " >" + + to_string_short_freq(frequency_range.max) + " S:" + + to_string_short_freq(def_step) + ); + + rf::Frequency frequency = frequency_range.min; + while (frequency_list.size() < MAX_DB_ENTRY && frequency <= frequency_range.max) { //add manual range + frequency_list.push_back(frequency); + description_list.push_back(""); //If empty, will keep showing the last description + frequency+=def_step; + } + show_max(); + if ( userpause ) //If user-paused, resume + user_resume(); + big_display.set_style(&style_grey); //Back to grey color + start_scan_thread(); //RESTART SCANNER THREAD + } }; + field_mode.on_change = [this](size_t, OptionsField::value_t v) { + receiver_model.disable(); + baseband::shutdown(); + change_mode(v); + if ( !scan_thread->is_scanning() ) //for some motive, audio output gets stopped. + audio::output::start(); //So if scan was stopped we resume audio + receiver_model.enable(); + }; - field_volume.set_value((receiver_model.headphone_volume() - audio::headphone::volume_range().max).decibel() + 99); - field_volume.on_change = [this](int32_t v) { - this->on_headphone_volume_changed(v); + button_dir.on_select = [this](Button&) { + scan_thread->change_scanning_direction(); + if ( userpause ) //If user-paused, resume + user_resume(); + big_display.set_style(&style_grey); //Back to grey color }; - - audio::output::start(); - - audio::output::mute(); - baseband::run_image(portapack::spi_flash::image_tag_nfm_audio); - receiver_model.set_modulation(ReceiverModel::Mode::NarrowbandFMAudio); - receiver_model.set_sampling_rate(3072000); - receiver_model.set_baseband_bandwidth(1750000); - receiver_model.enable(); - receiver_model.set_squelch_level(0); - receiver_model.set_nbfm_configuration(field_bw.selected_index()); - audio::output::unmute(); - - // TODO: Scanning thread here - scan_thread = std::make_unique<ScannerThread>(frequency_list); + + button_add.on_select = [this](Button&) { //frequency_list[current_index] + File scanner_file; + auto result = scanner_file.open("FREQMAN/SCANNER.TXT"); //First search if freq is already in txt + if (!result.is_valid()) { + std::string frequency_to_add = "f=" + + to_string_dec_uint(frequency_list[current_index] / 1000) + + to_string_dec_uint(frequency_list[current_index] % 1000UL, 3, '0'); + char one_char[1]; //Read it char by char + std::string line; //and put read line in here + bool found=false; + for (size_t pointer=0; pointer < scanner_file.size();pointer++) { + + scanner_file.seek(pointer); + scanner_file.read(one_char, 1); + if ((int)one_char[0] > 31) { //ascii space upwards + line += one_char[0]; //Add it to the textline + } + else if (one_char[0] == '\n') { //New Line + if (line.compare(0, frequency_to_add.size(),frequency_to_add) == 0) { + found=true; + break; + } + line.clear(); //Ready for next textline + } + } + if (found) { + nav_.display_modal("Error", "Frequency already exists"); + big_display.set(frequency_list[current_index]); //After showing an error + } + else { + auto result = scanner_file.append("FREQMAN/SCANNER.TXT"); //Second: append if it is not there + scanner_file.write_line(frequency_to_add + ",d=ADD FQ"); + } + } else + { + nav_.display_modal("Error", "Cannot open SCANNER.TXT\nfor appending freq."); + big_display.set(frequency_list[current_index]); //After showing an error + } + }; + + //PRE-CONFIGURATION: + field_wait.on_change = [this](int32_t v) { wait = v; }; field_wait.set_value(5); + field_squelch.on_change = [this](int32_t v) { squelch = v; }; field_squelch.set_value(-10); + field_volume.set_value((receiver_model.headphone_volume() - audio::headphone::volume_range().max).decibel() + 99); + field_volume.on_change = [this](int32_t v) { this->on_headphone_volume_changed(v); }; + // LEARN FREQUENCIES + std::string scanner_txt = "SCANNER"; + if ( load_freqman_file(scanner_txt, database) ) { + for(auto& entry : database) { // READ LINE PER LINE + if (frequency_list.size() < MAX_DB_ENTRY) { //We got space! + if (entry.type == RANGE) { //RANGE + switch (entry.step) { + case AM_US: def_step = 10000; break ; + case AM_EUR:def_step = 9000; break ; + case NFM_1: def_step = 12500; break ; + case NFM_2: def_step = 6250; break ; + case FM_1: def_step = 100000; break ; + case FM_2: def_step = 50000; break ; + case N_1: def_step = 25000; break ; + case N_2: def_step = 250000; break ; + case AIRBAND:def_step= 8330; break ; + } + frequency_list.push_back(entry.frequency_a); //Store starting freq and description + description_list.push_back("R:" + to_string_short_freq(entry.frequency_a) + + " >" + to_string_short_freq(entry.frequency_b) + + " S:" + to_string_short_freq(def_step)); + while (frequency_list.size() < MAX_DB_ENTRY && entry.frequency_a <= entry.frequency_b) { //add the rest of the range + entry.frequency_a+=def_step; + frequency_list.push_back(entry.frequency_a); + description_list.push_back(""); //Token (keep showing the last description) + } + } else if ( entry.type == SINGLE) { + frequency_list.push_back(entry.frequency_a); + description_list.push_back("S: " + entry.description); + } + show_max(); + } + else + { + break; //No more space: Stop reading the txt file ! + } + } + } + else + { + desc_cycle.set(" NO SCANNER.TXT FILE ..." ); + } + audio::output::stop(); + step_mode.set_by_value(def_step); //Impose the default step into the manual step selector + start_scan_thread(); } void ScannerView::on_statistics_update(const ChannelStatistics& statistics) { - int32_t max_db = statistics.max_db; - - if (timer <= wait) - timer++; - - if (max_db < -trigger) { - if (timer == wait) { - //audio::output::stop(); - scan_thread->set_scanning(true); + if ( !userpause ) //Scanning not user-paused + { + if (timer >= (wait * 10) ) + { + timer = 0; + scan_resume(); + } + else if (!timer) + { + if (statistics.max_db > squelch ) { //There is something on the air...(statistics.max_db > -squelch) + if (scan_thread->is_freq_lock() >= MAX_FREQ_LOCK) { //checking time reached + scan_pause(); + timer++; + } else { + scan_thread->set_freq_lock( scan_thread->is_freq_lock() + 1 ); //in lock period, still analyzing the signal + } + } else { //There is NOTHING on the air + if (scan_thread->is_freq_lock() > 0) { //But are we already in freq_lock ? + big_display.set_style(&style_grey); //Back to grey color + scan_thread->set_freq_lock(0); //Reset the scanner lock, since there is no signal + } + } + } + else //Ongoing wait time + { + timer++; } - } else { - //audio::output::start(); - scan_thread->set_scanning(false); - timer = 0; } } +void ScannerView::scan_pause() { + if (scan_thread->is_scanning()) { + scan_thread->set_freq_lock(0); //Reset the scanner lock (because user paused, or MAX_FREQ_LOCK reached) for next freq scan + scan_thread->set_scanning(false); // WE STOP SCANNING + audio::output::start(); + } +} + +void ScannerView::scan_resume() { + audio::output::stop(); + big_display.set_style(&style_grey); //Back to grey color + if (!scan_thread->is_scanning()) + scan_thread->set_scanning(true); // RESUME! +} + +void ScannerView::user_resume() { + timer = wait * 10; //Will trigger a scan_resume() on_statistics_update, also advancing to next freq. + button_pause.set_text("PAUSE"); //Show button for pause + userpause=false; //Resume scanning +} + void ScannerView::on_headphone_volume_changed(int32_t v) { const auto new_volume = volume_t::decibel(v - 99) + audio::headphone::volume_range().max; receiver_model.set_headphone_volume(new_volume); } -} /* namespace ui */ +size_t ScannerView::change_mode(uint8_t new_mod) { //Before this, do a scan_thread->stop(); After this do a start_scan_thread() + using option_t = std::pair<std::string, int32_t>; + using options_t = std::vector<option_t>; + options_t bw; + field_bw.on_change = [this](size_t n, OptionsField::value_t) { }; + + switch (new_mod) { + case NFM: //bw 16k (2) default + bw.emplace_back("8k5", 0); + bw.emplace_back("11k", 0); + bw.emplace_back("16k", 0); + field_bw.set_options(bw); + + baseband::run_image(portapack::spi_flash::image_tag_nfm_audio); + receiver_model.set_modulation(ReceiverModel::Mode::NarrowbandFMAudio); + field_bw.set_selected_index(2); + receiver_model.set_nbfm_configuration(field_bw.selected_index()); + field_bw.on_change = [this](size_t n, OptionsField::value_t) { receiver_model.set_nbfm_configuration(n); }; + receiver_model.set_sampling_rate(3072000); receiver_model.set_baseband_bandwidth(1750000); + break; + case AM: + bw.emplace_back("DSB", 0); + bw.emplace_back("USB", 0); + bw.emplace_back("LSB", 0); + field_bw.set_options(bw); + + baseband::run_image(portapack::spi_flash::image_tag_am_audio); + receiver_model.set_modulation(ReceiverModel::Mode::AMAudio); + field_bw.set_selected_index(0); + receiver_model.set_am_configuration(field_bw.selected_index()); + field_bw.on_change = [this](size_t n, OptionsField::value_t) { receiver_model.set_am_configuration(n); }; + receiver_model.set_sampling_rate(2000000);receiver_model.set_baseband_bandwidth(2000000); + break; + case WFM: + bw.emplace_back("16k", 0); + field_bw.set_options(bw); + + baseband::run_image(portapack::spi_flash::image_tag_wfm_audio); + receiver_model.set_modulation(ReceiverModel::Mode::WidebandFMAudio); + field_bw.set_selected_index(0); + receiver_model.set_wfm_configuration(field_bw.selected_index()); + field_bw.on_change = [this](size_t n, OptionsField::value_t) { receiver_model.set_wfm_configuration(n); }; + receiver_model.set_sampling_rate(3072000); receiver_model.set_baseband_bandwidth(2000000); + break; + } + + return mod_step[new_mod]; +} + +void ScannerView::start_scan_thread() { + receiver_model.enable(); + receiver_model.set_squelch_level(0); + scan_thread = std::make_unique<ScannerThread>(frequency_list); +} + +} /* namespace ui */ \ No newline at end of file diff --git a/firmware/application/apps/ui_scanner.hpp b/firmware/application/apps/ui_scanner.hpp index b5d17c228a0347c2d769c2e92f23d00cc13040d2..d2befbfe9fdf876d7ef97353c34610e89c8022a5 100644 --- a/firmware/application/apps/ui_scanner.hpp +++ b/firmware/application/apps/ui_scanner.hpp @@ -20,20 +20,46 @@ * Boston, MA 02110-1301, USA. */ +#include "ui.hpp" #include "receiver_model.hpp" - #include "ui_receiver.hpp" #include "ui_font_fixed_8x16.hpp" #include "freqman.hpp" +#include "analog_audio_app.hpp" +#include "audio.hpp" +#include "ui_mictx.hpp" +#include "portapack_persistent_memory.hpp" +#include "baseband_api.hpp" +#include "string_format.hpp" +#include "file.hpp" + + +#define MAX_DB_ENTRY 500 +#define MAX_FREQ_LOCK 10 //50ms cycles scanner locks into freq when signal detected, to verify signal is not spureous namespace ui { +enum modulation_type { AM = 0,WFM,NFM }; + +string const mod_name[3] = {"AM", "WFM", "NFM"}; +size_t const mod_step[3] = {9000, 100000, 12500 }; + class ScannerThread { public: ScannerThread(std::vector<rf::Frequency> frequency_list); ~ScannerThread(); - + void set_scanning(const bool v); + bool is_scanning(); + + void set_freq_lock(const uint32_t v); + uint32_t is_freq_lock(); + + void set_freq_del(const uint32_t v); + + void change_scanning_direction(); + + void stop(); ScannerThread(const ScannerThread&) = delete; ScannerThread(ScannerThread&&) = delete; @@ -45,38 +71,81 @@ private: Thread* thread { nullptr }; bool _scanning { true }; - + bool _fwd { true }; + uint32_t _freq_lock { 0 }; + uint32_t _freq_del { 0 }; static msg_t static_fn(void* arg); - void run(); }; class ScannerView : public View { public: - ScannerView(NavigationView&); + ScannerView(NavigationView& nav); ~ScannerView(); void focus() override; + + void big_display_freq(rf::Frequency f); + + const Style style_grey { // scanning + .font = font::fixed_8x16, + .background = Color::black(), + .foreground = Color::grey(), + }; - std::string title() const override { return "Scanner"; }; + const Style style_yellow { //Found signal + .font = font::fixed_8x16, + .background = Color::black(), + .foreground = Color::dark_yellow(), + }; + + const Style style_green { //Found signal + .font = font::fixed_8x16, + .background = Color::black(), + .foreground = Color::green(), + }; + + const Style style_red { //erasing freq + .font = font::fixed_8x16, + .background = Color::black(), + .foreground = Color::red(), + }; + + std::string title() const override { return "SCANNER"; }; + std::vector<rf::Frequency> frequency_list{ }; + std::vector<string> description_list { }; + +//void set_parent_rect(const Rect new_parent_rect) override; private: + NavigationView& nav_; + + void start_scan_thread(); + size_t change_mode(uint8_t mod_type); + void show_max(); + void scan_pause(); + void scan_resume(); + void user_resume(); + void on_statistics_update(const ChannelStatistics& statistics); void on_headphone_volume_changed(int32_t v); void handle_retune(uint32_t i); - - std::vector<rf::Frequency> frequency_list { }; - std::vector<string> description_list { }; - int32_t trigger { 0 }; + + jammer::jammer_range_t frequency_range { false, 0, 0 }; //perfect for manual scan task too... int32_t squelch { 0 }; uint32_t timer { 0 }; uint32_t wait { 0 }; + size_t def_step { 0 }; freqman_db database { }; + uint32_t current_index { 0 }; + bool userpause { false }; Labels labels { - { { 0 * 8, 0 * 16 }, "LNA: TRIGGER: /99 VOL:", Color::light_grey() }, - { { 0 * 8, 1 * 16 }, "VGA: SQUELCH: /99 AMP:", Color::light_grey() }, - { { 0 * 8, 2 * 16 }, " BW: WAIT:", Color::light_grey() }, + { { 0 * 8, 0 * 16 }, "LNA: VGA: AMP: VOL:", Color::light_grey() }, + { { 0 * 8, 1* 16 }, "BW: SQUELCH: db WAIT:", Color::light_grey() }, + { { 3 * 8, 10 * 16 }, "START END MANUAL", Color::light_grey() }, + { { 0 * 8, (26 * 8) + 4 }, "MODE:", Color::light_grey() }, + { { 11 * 8, (26 * 8) + 4 }, "STEP:", Color::light_grey() }, }; LNAGainField field_lna { @@ -84,15 +153,15 @@ private: }; VGAGainField field_vga { - { 4 * 8, 1 * 16 } + { 11 * 8, 0 * 16 } }; RFAmpField field_rf_amp { - { 28 * 8, 1 * 16 } + { 18 * 8, 0 * 16 } }; NumberField field_volume { - { 28 * 8, 0 * 16 }, + { 24 * 8, 0 * 16 }, 2, { 0, 99 }, 1, @@ -100,46 +169,118 @@ private: }; OptionsField field_bw { - { 4 * 8, 2 * 16 }, - 3, - { - { "8k5", 0 }, - { "11k", 0 }, - { "16k", 0 }, - } - }; + { 3 * 8, 1 * 16 }, + 4, + { } + }; - NumberField field_trigger { - { 16 * 8, 0 * 16 }, - 2, - { 0, 99 }, - 1, - ' ', - }; - NumberField field_squelch { - { 16 * 8, 1 * 16 }, - 2, - { 0, 99 }, + { 15 * 8, 1 * 16 }, + 3, + { -90, 20 }, 1, ' ', }; NumberField field_wait { - { 16 * 8, 2 * 16 }, + { 26 * 8, 1 * 16 }, 2, { 0, 99 }, 1, ' ', }; + RSSI rssi { + { 0 * 16, 2 * 16, 15 * 16, 8 }, + }; + Text text_cycle { - { 0, 5 * 16, 240, 16 }, - "--/--" + { 0, 3 * 16, 3 * 8, 16 }, + }; + + Text text_max { + { 4 * 8, 3 * 16, 18 * 8, 16 }, }; + Text desc_cycle { - {0, 6 * 16, 240, 16 }, - " " + {0, 4 * 16, 240, 16 }, + }; + + BigFrequency big_display { //Show frequency in glamour + { 4, 6 * 16, 28 * 8, 52 }, + 0 + }; + + Button button_manual_start { + { 0 * 8, 11 * 16, 11 * 8, 28 }, + "" + }; + + Button button_manual_end { + { 12 * 8, 11 * 16, 11 * 8, 28 }, + "" + }; + + Button button_manual_scan { + { 24 * 8, 11 * 16, 6 * 8, 28 }, + "SCAN" + }; + + OptionsField field_mode { + { 5 * 8, (26 * 8) + 4 }, + 6, + { + { " AM ", 0 }, + { " WFM ", 1 }, + { " NFM ", 2 }, + } + }; + + OptionsField step_mode { + { 17 * 8, (26 * 8) + 4 }, + 12, + { + { "5Khz (SA AM)", 5000 }, + { "9Khz (EU AM)", 9000 }, + { "10Khz(US AM)", 10000 }, + { "50Khz (FM1)", 50000 }, + { "100Khz(FM2)", 100000 }, + { "6.25khz(NFM)", 6250 }, + { "12.5khz(NFM)", 12500 }, + { "25khz (N1)", 25000 }, + { "250khz (N2)", 250000 }, + { "8.33khz(AIR)", 8330 } + } + }; + + Button button_pause { + { 0, (15 * 16) - 4, 72, 28 }, + "PAUSE" + }; + + Button button_dir { + { 0, (35 * 8) - 4, 72, 28 }, + "FW><RV" + }; + + Button button_audio_app { + { 84, (15 * 16) - 4, 72, 28 }, + "AUDIO" + }; + + Button button_mic_app { + { 84, (35 * 8) - 4, 72, 28 }, + "MIC TX" + }; + + Button button_add { + { 168, (15 * 16) - 4, 72, 28 }, + "ADD FQ" + }; + + Button button_remove { + { 168, (35 * 8) - 4, 72, 28 }, + "DEL FQ" }; std::unique_ptr<ScannerThread> scan_thread { }; @@ -160,4 +301,4 @@ private: }; }; -} /* namespace ui */ +} /* namespace ui */ \ No newline at end of file diff --git a/firmware/application/apps/ui_sonde.cpp b/firmware/application/apps/ui_sonde.cpp index 16506b3f179fa9dcf7704d78767fb59b5a1fece3..e61139acdcf74529124248c2790dd7fd72657b98 100644 --- a/firmware/application/apps/ui_sonde.cpp +++ b/firmware/application/apps/ui_sonde.cpp @@ -54,7 +54,7 @@ SondeView::SondeView(NavigationView& nav) { }); field_frequency.set_value(target_frequency_); - field_frequency.set_step(10000); + field_frequency.set_step(500); //euquiq: was 10000, but we are using this for fine-tunning field_frequency.on_change = [this](rf::Frequency f) { set_target_frequency(f); field_frequency.set_value(f); @@ -86,12 +86,12 @@ SondeView::SondeView(NavigationView& nav) { button_see_map.on_select = [this, &nav](Button&) { nav.push<GeoMapView>( - "", - altitude, + sonde_id, + gps_info.alt, GeoPos::alt_unit::METERS, - latitude, - longitude, - 0); + gps_info.lat, + gps_info.lon, + 999); //set a dummy heading out of range to draw a cross...probably not ideal? }; logger = std::make_unique<SondeLogger>(); @@ -113,16 +113,15 @@ void SondeView::on_packet(const sonde::Packet& packet) { //const auto hex_formatted = packet.symbols_formatted(); text_signature.set(packet.type_string()); - text_serial.set(packet.serial_number()); + sonde_id = packet.serial_number(); //used also as tag on the geomap + text_serial.set(sonde_id); text_voltage.set(unit_auto_scale(packet.battery_voltage(), 2, 3) + "V"); + + gps_info = packet.get_GPS_data(); - altitude = packet.GPS_altitude(); - latitude = packet.GPS_latitude(); - longitude = packet.GPS_longitude(); - - geopos.set_altitude(altitude); - geopos.set_lat(latitude); - geopos.set_lon(longitude); + geopos.set_altitude(gps_info.alt); + geopos.set_lat(gps_info.lat); + geopos.set_lon(gps_info.lon); if (logger && logging) { logger->on_packet(packet); diff --git a/firmware/application/apps/ui_sonde.hpp b/firmware/application/apps/ui_sonde.hpp index 5dc7fe86a4a2c17e8fbb593c00fad08a4e01c98b..9e7743b1a8ae0f4af0d14561e7927102bea5d0a6 100644 --- a/firmware/application/apps/ui_sonde.hpp +++ b/firmware/application/apps/ui_sonde.hpp @@ -65,11 +65,10 @@ public: private: std::unique_ptr<SondeLogger> logger { }; - uint32_t target_frequency_ { 402000000 }; + uint32_t target_frequency_ { 402700000 }; bool logging { false }; - int32_t altitude { 0 }; - float latitude { 0 }; - float longitude { 0 }; + sonde::GPS_data gps_info; + std::string sonde_id; Labels labels { { { 0 * 8, 2 * 16 }, "Signature:", Color::light_grey() }, diff --git a/firmware/application/apps/ui_view_wav.cpp b/firmware/application/apps/ui_view_wav.cpp index daa12a8b33e4849b20c5f21244c34d6cebe3f0ad..0ec1dba696332c1e62810309f6b0b2898d1bdb36 100644 --- a/firmware/application/apps/ui_view_wav.cpp +++ b/firmware/application/apps/ui_view_wav.cpp @@ -81,15 +81,6 @@ void ViewWavView::load_wav(std::filesystem::path file_path) { int16_t sample; uint32_t average; - if (!wav_reader->open(file_path)) { - nav_.display_modal("Error", "Couldn't open file.", INFO, nullptr); - return; - } - - if ((wav_reader->channels() != 1) || (wav_reader->bits_per_sample() != 16)) { - nav_.display_modal("Error", "Wrong format.\nWav viewer only accepts\n16-bit mono files.", INFO, nullptr); - return; - } text_filename.set(file_path.filename().string()); auto ms_duration = wav_reader->ms_duration(); @@ -148,10 +139,18 @@ ViewWavView::ViewWavView( &field_cursor_b, &text_delta }); - + reset_controls(); button_open.on_select = [this, &nav](Button&) { auto open_view = nav.push<FileLoadView>(".WAV"); open_view->on_changed = [this](std::filesystem::path file_path) { + if (!wav_reader->open(file_path)) { + nav_.display_modal("Error", "Couldn't open file.", INFO, nullptr); + return; + } + if ((wav_reader->channels() != 1) || (wav_reader->bits_per_sample() != 16)) { + nav_.display_modal("Error", "Wrong format.\nWav viewer only accepts\n16-bit mono files.", INFO, nullptr); + return; + } load_wav(file_path); field_pos_seconds.focus(); }; @@ -176,7 +175,6 @@ ViewWavView::ViewWavView( refresh_measurements(); }; - reset_controls(); } void ViewWavView::focus() { diff --git a/firmware/application/freqman.hpp b/firmware/application/freqman.hpp index daaad1efb98db4d63b41d4b925f516fea9846e6f..dca1f3645e8883152031c5e88751cdc79d447f8e 100644 --- a/firmware/application/freqman.hpp +++ b/firmware/application/freqman.hpp @@ -48,11 +48,28 @@ enum freqman_entry_type { RANGE }; +//Entry step placed for AlainD freqman version (or any other enhanced version) +enum freqman_entry_step { + STEP_DEF = 0, // default + AM_US, // 10 Khz AM/CB + AM_EUR, // 9 Khz LW/MW + NFM_1, // 12,5 Khz (Analogic PMR 446) + NFM_2, // 6,25 Khz (Digital PMR 446) + FM_1, // 100 Khz + FM_2, // 50 Khz + N_1, // 25 Khz + N_2, // 250 Khz + AIRBAND, // AIRBAND 8,33 Khz + ERROR_STEP +}; + +// freqman_entry_step step added, as above, to provide compatibility / future enhancement. struct freqman_entry { rf::Frequency frequency_a { 0 }; rf::Frequency frequency_b { 0 }; std::string description { }; freqman_entry_type type { }; + freqman_entry_step step { }; }; using freqman_db = std::vector<freqman_entry>; diff --git a/firmware/application/string_format.cpp b/firmware/application/string_format.cpp index a62ce1ae32e312ddfd7a14888220ccb60305ddc5..fd7fb1a3c96c8b179d7855691cd88b47e126bc72 100644 --- a/firmware/application/string_format.cpp +++ b/firmware/application/string_format.cpp @@ -113,7 +113,8 @@ std::string to_string_dec_int( } std::string to_string_short_freq(const uint64_t f) { - auto final_str = to_string_dec_int(f / 1000000, 4) + "." + to_string_dec_int((f / 100) % 10000, 4, '0'); + //was... to_string_dec_int(f / 1000000,4) + auto final_str = to_string_dec_int(f / 1000000) + "." + to_string_dec_int((f / 100) % 10000, 4, '0'); return final_str; } diff --git a/firmware/application/ui/ui_btngrid.cpp b/firmware/application/ui/ui_btngrid.cpp index 0c756af4e03660247755dfbbc289b055e053756d..2ad7907eb55524615a912563be7d54c0710546f1 100644 --- a/firmware/application/ui/ui_btngrid.cpp +++ b/firmware/application/ui/ui_btngrid.cpp @@ -95,6 +95,15 @@ void BtnGridView::set_parent_rect(const Rect new_parent_rect) { update_items(); } +void BtnGridView::set_arrow_enabled(bool new_value) { + if(new_value){ + add_child(&arrow_more); + } + else{ + remove_child(&arrow_more); + } +}; + void BtnGridView::on_tick_second() { if (more && blink) arrow_more.set_foreground(Color::white()); diff --git a/firmware/application/ui/ui_btngrid.hpp b/firmware/application/ui/ui_btngrid.hpp index 1ce14ccacf7bacd793c990b64ebf6fa6179c3c5e..5f922f78b11a6aca0ed5da85972bcf9c7d121677 100644 --- a/firmware/application/ui/ui_btngrid.hpp +++ b/firmware/application/ui/ui_btngrid.hpp @@ -62,6 +62,7 @@ public: uint32_t highlighted_index(); void set_parent_rect(const Rect new_parent_rect) override; + void set_arrow_enabled(bool new_value); void on_focus() override; void on_blur() override; bool on_key(const KeyEvent event) override; diff --git a/firmware/application/ui/ui_geomap.cpp b/firmware/application/ui/ui_geomap.cpp index f907825a7466204ffb30928e91f355456f337eff..91fcb545e648737c5c1bc70478b794944e0759fb 100644 --- a/firmware/application/ui/ui_geomap.cpp +++ b/firmware/application/ui/ui_geomap.cpp @@ -59,7 +59,7 @@ GeoPos::GeoPos( set_altitude(0); set_lat(0); set_lon(0); - + const auto changed_fn = [this](int32_t) { float lat_value = lat(); float lon_value = lon(); @@ -163,15 +163,22 @@ void GeoMap::paint(Painter& painter) { prev_x_pos = x_pos; prev_y_pos = y_pos; } - + //center tag above point + if(tag_.find_first_not_of(' ') != tag_.npos){ //only draw tag if we have something other than spaces + painter.draw_string(r.center() - Point(((int)tag_.length() * 8 / 2), 2 * 16), style(), tag_); + } if (mode_ == PROMPT) { // Cross display.fill_rectangle({ r.center() - Point(16, 1), { 32, 2 } }, Color::red()); display.fill_rectangle({ r.center() - Point(1, 16), { 2, 32 } }, Color::red()); - } else { + } else if (angle_ < 360){ + //if we have a valid angle draw bearing draw_bearing(r.center(), angle_, 10, Color::red()); - //center tag above bearing - painter.draw_string(r.center() - Point(((int)tag_.length() * 8 / 2), 2 * 16), style(), tag_); + } + else { + //draw a small cross + display.fill_rectangle({ r.center() - Point(8, 1), { 16, 2 } }, Color::red()); + display.fill_rectangle({ r.center() - Point(1, 8), { 2, 16 } }, Color::red()); } } @@ -231,7 +238,7 @@ void GeoMap::set_mode(GeoMapMode mode) { mode_ = mode; } -void GeoMap::draw_bearing(const Point origin, const uint32_t angle, uint32_t size, const Color color) { +void GeoMap::draw_bearing(const Point origin, const uint16_t angle, uint32_t size, const Color color) { Point arrow_a, arrow_b, arrow_c; for (size_t thickness = 0; thickness < 3; thickness++) { @@ -254,11 +261,12 @@ void GeoMapView::focus() { nav_.display_modal("No map", "No world_map.bin file in\n/ADSB/ directory", ABORT, nullptr); } -void GeoMapView::update_position(float lat, float lon) { +void GeoMapView::update_position(float lat, float lon, uint16_t angle) { lat_ = lat; lon_ = lon; geopos.set_lat(lat_); geopos.set_lon(lon_); + geomap.set_angle(angle); geomap.move(lon_, lat_); geomap.set_dirty(); } @@ -269,7 +277,7 @@ void GeoMapView::setup() { geopos.set_altitude(altitude_); geopos.set_lat(lat_); geopos.set_lon(lon_); - + geopos.on_change = [this](int32_t altitude, float lat, float lon) { altitude_ = altitude; lat_ = lat; @@ -307,7 +315,7 @@ GeoMapView::GeoMapView( GeoPos::alt_unit altitude_unit, float lat, float lon, - float angle, + uint16_t angle, const std::function<void(void)> on_close ) : nav_ (nav), altitude_ (altitude), @@ -328,6 +336,7 @@ GeoMapView::GeoMapView( geomap.set_mode(mode_); geomap.set_tag(tag); + geomap.set_angle(angle); geomap.move(lon_, lat_); geopos.set_read_only(true); diff --git a/firmware/application/ui/ui_geomap.hpp b/firmware/application/ui/ui_geomap.hpp index 45a9479b1046db1ab0acbdb83e0e8cb5a20ec132..30fd69830436364a5df3e5ef6d6fc1f603e7ee84 100644 --- a/firmware/application/ui/ui_geomap.hpp +++ b/firmware/application/ui/ui_geomap.hpp @@ -129,8 +129,12 @@ public: tag_ = new_tag; } + void set_angle(uint16_t new_angle){ + angle_ = new_angle; + } + private: - void draw_bearing(const Point origin, const uint32_t angle, uint32_t size, const Color color); + void draw_bearing(const Point origin, const uint16_t angle, uint32_t size, const Color color); GeoMapMode mode_ { }; File map_file { }; @@ -141,7 +145,7 @@ private: int32_t prev_x_pos { 0xFFFF }, prev_y_pos { 0xFFFF }; float lat_ { }; float lon_ { }; - float angle_ { }; + uint16_t angle_ { }; std::string tag_ { }; }; @@ -154,7 +158,7 @@ public: GeoPos::alt_unit altitude_unit, float lat, float lon, - float angle, + uint16_t angle, const std::function<void(void)> on_close = nullptr ); GeoMapView(NavigationView& nav, @@ -173,7 +177,7 @@ public: void focus() override; - void update_position(float lat, float lon); + void update_position(float lat, float lon, uint16_t angle); std::string title() const override { return "Map view"; }; @@ -190,7 +194,7 @@ private: GeoPos::alt_unit altitude_unit_ { }; float lat_ { }; float lon_ { }; - float angle_ { }; + uint16_t angle_ { }; std::function<void(void)> on_close_ { nullptr }; bool map_opened { }; diff --git a/firmware/application/ui_navigation.cpp b/firmware/application/ui_navigation.cpp index e4ee6490babe8bd438cc10dd0ec8b8fc33844d95..01782d891aeabe66a5ceeba2e0e91e449be504d4 100644 --- a/firmware/application/ui_navigation.cpp +++ b/firmware/application/ui_navigation.cpp @@ -30,7 +30,7 @@ #include "bmp_modal_warning.hpp" #include "portapack_persistent_memory.hpp" -#include "ui_about.hpp" +#include "ui_about_simple.hpp" #include "ui_adsb_rx.hpp" #include "ui_adsb_tx.hpp" #include "ui_afsk_rx.hpp" @@ -303,7 +303,36 @@ void SystemStatusView::on_camera() { } void SystemStatusView::on_title() { - nav_.push<AboutView>(); + if(nav_.is_top()) + nav_.push<AboutView>(); + else + nav_.pop(); +} + +/* Information View *****************************************************/ + +InformationView::InformationView( + NavigationView& nav +) : nav_ (nav) +{ + static constexpr Style style_infobar { + .font = font::fixed_8x16, + .background = {33, 33, 33}, + .foreground = Color::white(), + }; + + add_children({ + &backdrop, + &version, + <ime + }); + + version.set_style(&style_infobar); + ltime.set_style(&style_infobar); + ltime.set_seconds_enabled(true); + ltime.set_date_enabled(false); + + set_dirty(); } /* Navigation ************************************************************/ @@ -416,7 +445,7 @@ ReceiversMenuView::ReceiversMenuView(NavigationView& nav) { { "Analog TV", ui::Color::yellow(), &bitmap_icon_sstv, [&nav](){ nav.push<AnalogTvView>(); } }, { "ERT Meter", ui::Color::green(), &bitmap_icon_ert, [&nav](){ nav.push<ERTAppView>(); } }, { "POCSAG", ui::Color::green(), &bitmap_icon_pocsag, [&nav](){ nav.push<POCSAGAppView>(); } }, - { "Radiosnde", ui::Color::yellow(), &bitmap_icon_sonde, [&nav](){ nav.push<SondeView>(); } }, + { "Radiosnde", ui::Color::green(), &bitmap_icon_sonde, [&nav](){ nav.push<SondeView>(); } }, { "TPMS Cars", ui::Color::green(), &bitmap_icon_tpms, [&nav](){ nav.push<TPMSAppView>(); } }, /*{ "APRS", ui::Color::dark_grey(), &bitmap_icon_aprs, [&nav](){ nav.push<NotImplementedView>(); } }, { "DMR", ui::Color::dark_grey(), &bitmap_icon_dmr, [&nav](){ nav.push<NotImplementedView>(); } }, @@ -502,6 +531,7 @@ SystemMenuView::SystemMenuView(NavigationView& nav) { //{ "About", ui::Color::cyan(), nullptr, [&nav](){ nav.push<AboutView>(); } } }); set_max_rows(2); // allow wider buttons + set_arrow_enabled(false); //set_highlighted(1); // Startup selection } @@ -522,6 +552,7 @@ SystemView::SystemView( set_style(&style_default); constexpr ui::Dim status_view_height = 16; + constexpr ui::Dim info_view_height = 16; add_child(&status_view); status_view.set_parent_rect({ @@ -537,13 +568,29 @@ SystemView::SystemView( { 0, status_view_height }, { parent_rect.width(), static_cast<ui::Dim>(parent_rect.height() - status_view_height) } }); + + add_child(&info_view); + info_view.set_parent_rect({ + {0, 19 * 16}, + { parent_rect.width(), info_view_height } + }); + navigation_view.on_view_changed = [this](const View& new_view) { + + if(!this->navigation_view.is_top()){ + remove_child(&info_view); + } + else{ + add_child(&info_view); + } + this->status_view.set_back_enabled(!this->navigation_view.is_top()); this->status_view.set_title_image_enabled(this->navigation_view.is_top()); - this->status_view.set_dirty(); this->status_view.set_title(new_view.title()); + this->status_view.set_dirty(); + }; - + // portapack::persistent_memory::set_playdead_sequence(0x8D1); @@ -557,6 +604,9 @@ SystemView::SystemView( if (portapack::persistent_memory::config_splash()) navigation_view.push<BMPView>(); + status_view.set_back_enabled(false); + status_view.set_title_image_enabled(true); + status_view.set_dirty(); //else // navigation_view.push<SystemMenuView>(); diff --git a/firmware/application/ui_navigation.hpp b/firmware/application/ui_navigation.hpp index 8754d35d9547ddab86454b4d74114facc3b3877e..6d95eefe2c8d351a8b0f28b1e799d22dffb46f08 100644 --- a/firmware/application/ui_navigation.hpp +++ b/firmware/application/ui_navigation.hpp @@ -110,7 +110,7 @@ public: void set_title(const std::string new_value); private: - static constexpr auto default_title = " v1.1.1"; // TODO: Move the version somewhere + static constexpr auto default_title = ""; NavigationView& nav_; @@ -208,6 +208,29 @@ private: }; }; +class InformationView : public View { +public: + InformationView(NavigationView& nav); + +private: + static constexpr auto version_string = "v1.2"; + NavigationView& nav_; + + Rectangle backdrop { + { 0, 0 * 16, 240, 16 }, + {33, 33, 33} + }; + + Text version { + {2, 0, 11 * 8, 16}, + version_string + }; + + LiveDateTime ltime { + {174, 0, 8 * 8, 16} + }; +}; + class BMPView : public View { public: BMPView(NavigationView& nav); @@ -262,6 +285,7 @@ public: private: SystemStatusView status_view { navigation_view }; + InformationView info_view { navigation_view }; NavigationView navigation_view { }; Context& context_; }; diff --git a/firmware/baseband/proc_sonde.hpp b/firmware/baseband/proc_sonde.hpp index de15fff0818c4a4be0c9597265fc49c77d52e3fe..92e535803d83eb92bd6744d56cc299661b4248c6 100644 --- a/firmware/baseband/proc_sonde.hpp +++ b/firmware/baseband/proc_sonde.hpp @@ -140,7 +140,8 @@ private: } }; PacketBuilder<BitPattern, NeverMatch, FixedLength> packet_builder_fsk_4800_Vaisala { - { 0b00001000011011010101001110001000, 32, 1 }, + { 0b00001000011011010101001110001000, 32, 1 }, //euquiq Header detects 4 of 8 bytes 0x10B6CA11 /this is in raw format) (these bits are not passed at the beginning of packet) + //{ 0b0000100001101101010100111000100001000100011010010100100000011111, 64, 1 }, //euquiq whole header detection would be 8 bytes. { }, { 320 * 8 }, [this](const baseband::Packet& packet) { diff --git a/firmware/common/adsb.cpp b/firmware/common/adsb.cpp index c730bbecbb97bd18c434b056e52a4f2cbf492099..3f8f4c23118cac696de720a8a580e23763d2ec1c 100644 --- a/firmware/common/adsb.cpp +++ b/firmware/common/adsb.cpp @@ -300,8 +300,8 @@ void encode_frame_velo(ADSBFrame& frame, const uint32_t ICAO_address, const uint v_rate_coded = (v_rate / 64) + 1; - velo_ew_abs = abs(velo_ew); - velo_ns_abs = abs(velo_ns); + velo_ew_abs = abs(velo_ew) + 1; + velo_ns_abs = abs(velo_ns) + 1; v_rate_coded_abs = abs(v_rate_coded); make_frame_adsb(frame, ICAO_address); @@ -317,4 +317,52 @@ void encode_frame_velo(ADSBFrame& frame, const uint32_t ICAO_address, const uint frame.make_CRC(); } +// Decoding method from dump1090 +adsb_vel decode_frame_velo(ADSBFrame& frame){ + adsb_vel velo {false, 0, 0}; + + uint8_t * frame_data = frame.get_raw_data(); + uint8_t velo_type = frame.get_msg_sub(); + + if(velo_type >= 1 && velo_type <= 4){ //vertical rate is always present + + velo.v_rate = (((frame_data[8] & 0x07 ) << 6) | ((frame_data[9]) >> 2) - 1) * 64; + + if((frame_data[8] & 0x8) >> 3) velo.v_rate *= -1; //check v_rate sign + } + + if(velo_type == 1 || velo_type == 2){ //Ground Speed + int32_t raw_ew = ((frame_data[5] & 0x03) << 8) | frame_data[6]; + int32_t velo_ew = raw_ew - 1; //velocities are all offset by one (this is part of the spec) + + int32_t raw_ns = ((frame_data[7] & 0x7f) << 3) | (frame_data[8] >> 5); + int32_t velo_ns = raw_ns - 1; + + if (velo_type == 2){ // supersonic indicator so multiply by 4 + velo_ew = velo_ew << 2; + velo_ns = velo_ns << 2; + } + + if(frame_data[5]&0x04) velo_ew *= -1; //check ew direction sign + if(frame_data[7]&0x80) velo_ns *= -1; //check ns direction sign + + velo.speed = sqrt(velo_ns*velo_ns + velo_ew*velo_ew); + + if(velo.speed){ + //calculate heading in degrees from ew/ns velocities + int16_t heading_temp = (int16_t)(atan2(velo_ew,velo_ns) * 180.0 / pi); + // We don't want negative values but a 0-360 scale. + if (heading_temp < 0) heading_temp += 360.0; + velo.heading = (uint16_t)heading_temp; + } + + }else if(velo_type == 3 || velo_type == 4){ //Airspeed + velo.valid = frame_data[5] & (1<<2); + velo.heading = ((((frame_data[5] & 0x03)<<8) | frame_data[6]) * 45) << 7; + } + + return velo; + +} + } /* namespace adsb */ diff --git a/firmware/common/adsb.hpp b/firmware/common/adsb.hpp index 4e042ff21f6f17ee83c6df62ff30fe8212d6afe8..82d177e496e383e84bc7d14542e28e1d6f6d74ac 100644 --- a/firmware/common/adsb.hpp +++ b/firmware/common/adsb.hpp @@ -56,6 +56,13 @@ struct adsb_pos { int32_t altitude; }; +struct adsb_vel { + bool valid; + int32_t speed; //knot + uint16_t heading; //degree + int32_t v_rate; //ft/min +}; + const float CPR_MAX_VALUE = 131072.0; const float adsb_lat_lut[58] = { @@ -89,6 +96,8 @@ adsb_pos decode_frame_pos(ADSBFrame& frame_even, ADSBFrame& frame_odd); void encode_frame_velo(ADSBFrame& frame, const uint32_t ICAO_address, const uint32_t speed, const float angle, const int32_t v_rate); +adsb_vel decode_frame_velo(ADSBFrame& frame); + //void encode_frame_emergency(ADSBFrame& frame, const uint32_t ICAO_address, const uint8_t code); void encode_frame_squawk(ADSBFrame& frame, const uint32_t squawk); diff --git a/firmware/common/adsb_frame.hpp b/firmware/common/adsb_frame.hpp index 36685843d0e849d3262068bd5ec3b91c300487d9..a93abe6fb5403578dc009e0886f3e74a15222a49 100644 --- a/firmware/common/adsb_frame.hpp +++ b/firmware/common/adsb_frame.hpp @@ -41,6 +41,10 @@ public: return (raw_data[4] >> 3); } + uint8_t get_msg_sub() { + return (raw_data[4] & 7); + } + uint32_t get_ICAO_address() { return (raw_data[1] << 16) + (raw_data[2] << 8) + raw_data[3]; } diff --git a/firmware/common/sonde_packet.cpp b/firmware/common/sonde_packet.cpp index 22f547938ab7376ce3daffea29cc0b1481513fd5..b1c22430cddf9232cc02f8d13ecc7f362846c9d3 100644 --- a/firmware/common/sonde_packet.cpp +++ b/firmware/common/sonde_packet.cpp @@ -22,9 +22,25 @@ #include "sonde_packet.hpp" #include "string_format.hpp" +#include <cstring> +//#include <complex> namespace sonde { +//Defines for Vaisala RS41, from https://github.com/rs1729/RS/blob/master/rs41/rs41sg.c +#define MASK_LEN 64 +#define pos_FrameNb 0x37 //0x03B // 2 byte +#define pos_SondeID 0x39 //0x03D // 8 byte +#define pos_Voltage 0x041 //0x045 // 3 bytes (but first one is the important one) voltage x 10 ie: 26 = 2.6v +#define pos_CalData 0x04E //0x052 // 1 byte, counter 0x00..0x32 +#define pos_GPSweek 0x091 //0x095 // 2 byte +#define pos_GPSTOW 0x093 //0x097 // 4 byte +#define pos_GPSecefX 0x110 //0x114 // 4 byte +#define pos_GPSecefY 0x114 //0x118 // 4 byte (not actually used since Y and Z are following X, and grabbed in that same loop) +#define pos_GPSecefZ 0x118 //0x11C // 4 byte (same as Y) + +#define PI 3.1415926535897932384626433832795 //3.1416 //(3.1415926535897932384626433832795) + Packet::Packet( const baseband::Packet& packet, const Type type @@ -60,37 +76,65 @@ Packet::Type Packet::type() const { return type_; } -/*uint8_t Packet::vaisala_descramble(const uint32_t pos) { - return reader_raw.read(pos * 8, 8) ^ vaisala_mask[pos & 63]; -};*/ - -uint32_t Packet::GPS_altitude() const { - if ((type_ == Type::Meteomodem_M10) || (type_ == Type::Meteomodem_M2K2)) - return (reader_bi_m.read(22 * 8, 32) / 1000) - 48; - else if (type_ == Type::Vaisala_RS41_SG) { - /*uint32_t altitude_ecef = 0; - for (uint32_t i = 0; i < 4; i++) - altitude_ecef = (altitude_ecef << 8) + vaisala_descramble(0x11C + i);*/ - // TODO: and a bunch of maths (see ecef2elli() from RS1729) - return 0; - } else - return 0; // Unknown -} - -float Packet::GPS_latitude() const { - if ((type_ == Type::Meteomodem_M10) || (type_ == Type::Meteomodem_M2K2)) - return reader_bi_m.read(14 * 8, 32) / ((1ULL << 32) / 360.0); - //else if (type_ == Type::Vaisala_RS41_SG) - // return vaisala_descramble(); - else - return 0; // Unknown -} +//euquiq here: +//RS41SG 320 bits header, 320bytes frame (or more if it is an "extended frame") +//The raw data is xor-scrambled with the values in the 64 bytes vaisala_mask (see.hpp) + + +uint8_t Packet::vaisala_descramble(const uint32_t pos) const { + //return reader_raw.read(pos * 8, 8) ^ vaisala_mask[pos & 63]; + // packet_[i]; its a bit; packet_.size the total (should be 2560 bits) + uint8_t value = 0; + for (uint8_t i = 0; i < 8; i++) + value = (value << 1) | packet_[(pos * 8) + (7 -i)]; //get the byte from the bits collection + + //packetReader reader { packet_ }; //This works just as above. + //value = reader.read(pos * 8,8); + //shift pos because first 4 bytes are consumed by proc_sonde in finding the vaisala signature + uint32_t mask_pos = pos + 4; + value = value ^ vaisala_mask[mask_pos % MASK_LEN]; //descramble with the xor pseudorandom table + return value; +}; + +GPS_data Packet::get_GPS_data() const { + GPS_data result; + if ((type_ == Type::Meteomodem_M10) || (type_ == Type::Meteomodem_M2K2)) { + + result.alt = (reader_bi_m.read(22 * 8, 32) / 1000) - 48; + result.lat = reader_bi_m.read(14 * 8, 32) / ((1ULL << 32) / 360.0); + result.lon = reader_bi_m.read(18 * 8, 32) / ((1ULL << 32) / 360.0); + + } else if (type_ == Type::Vaisala_RS41_SG) { + + uint8_t XYZ_bytes[4]; + int32_t XYZ; // 32bit + double_t X[3]; + for (int32_t k = 0; k < 3; k++) { //Get X,Y,Z ECEF position from GPS + for (int32_t i = 0; i < 4; i++) //each one is 4 bytes (32 bits) + XYZ_bytes[i] = vaisala_descramble(pos_GPSecefX + (4*k) + i); + memcpy(&XYZ, XYZ_bytes, 4); + X[k] = XYZ / 100.0; + } + + double_t a = 6378137.0; + double_t b = 6356752.31424518; + double_t e = sqrt( (a*a - b*b) / (a*a) ); + double_t ee = sqrt( (a*a - b*b) / (b*b) ); + + double_t lam = atan2( X[1] , X[0] ); + double_t p = sqrt( X[0]*X[0] + X[1]*X[1] ); + double_t t = atan2( X[2]*a , p*b ); + double_t phi = atan2( X[2] + ee*ee * b * sin(t)*sin(t)*sin(t) , + p - e*e * a * cos(t)*cos(t)*cos(t) ); + + double_t R = a / sqrt( 1 - e*e*sin(phi)*sin(phi) ); + + result.alt = p / cos(phi) - R; + result.lat = phi*180/PI; + result.lon = lam*180/PI; -float Packet::GPS_longitude() const { - if ((type_ == Type::Meteomodem_M10) || (type_ == Type::Meteomodem_M2K2)) - return reader_bi_m.read(18 * 8, 32) / ((1ULL << 32) / 360.0); - else - return 0; // Unknown + } + return result; } uint32_t Packet::battery_voltage() const { @@ -98,8 +142,13 @@ uint32_t Packet::battery_voltage() const { return (reader_bi_m.read(69 * 8, 8) + (reader_bi_m.read(70 * 8, 8) << 8)) * 1000 / 150; else if (type_ == Type::Meteomodem_M2K2) return reader_bi_m.read(69 * 8, 8) * 66; // Actually 65.8 - else + else if (type_ == Type::Vaisala_RS41_SG) { + uint32_t voltage = vaisala_descramble(pos_Voltage) * 100; //byte 69 = voltage * 10 (check if this value needs to be multiplied) + return voltage; + } + else { return 0; // Unknown + } } std::string Packet::type_string() const { @@ -127,12 +176,33 @@ std::string Packet::serial_number() const { to_string_dec_uint(reader_bi_m.read(93 * 8 + 24, 3), 1) + to_string_dec_uint(reader_bi_m.read(93 * 8 + 27, 13), 4, '0'); - } else + } else if(type() == Type::Vaisala_RS41_SG) { + std::string serial_id = ""; + uint8_t achar; + for (uint8_t i=0; i<8; i++) { //euquiq: Serial ID is 8 bytes long, each byte a char + achar = vaisala_descramble(pos_SondeID + i); + if (achar < 32 || achar > 126) return "?"; //Maybe there are ids with less than 8 bytes and this is not OK. + serial_id += (char)achar; + } + return serial_id; + } else return "?"; } FormattedSymbols Packet::symbols_formatted() const { - return format_symbols(decoder_); + if (type() == Type::Vaisala_RS41_SG) { //Euquiq: now we distinguish different types + uint32_t bytes = packet_.size() / 8; //Need the byte amount, which if full, it SHOULD be 320 size() should return 2560 + std::string hex_data; + std::string hex_error; + hex_data.reserve(bytes * 2); //2 hexa chars per byte + hex_error.reserve(1); + for (uint32_t i=0; i < bytes; i++) //log will show the packet starting on the last 4 bytes from signature 93DF1A60 + hex_data += to_string_hex(vaisala_descramble(i),2); + return { hex_data, hex_error }; + + } else { + return format_symbols(decoder_); + } } bool Packet::crc_ok() const { diff --git a/firmware/common/sonde_packet.hpp b/firmware/common/sonde_packet.hpp index 4ecbc08a475e09e8337e72b2029fdc227c859294..746d42e3117d2a20bd0ba8a6c159c5d41bc5b1b8 100644 --- a/firmware/common/sonde_packet.hpp +++ b/firmware/common/sonde_packet.hpp @@ -32,6 +32,12 @@ namespace sonde { + struct GPS_data { + uint32_t alt { 0 }; + float lat { 0 }; + float lon { 0 }; + }; + class Packet { public: enum class Type : uint32_t { @@ -41,7 +47,7 @@ public: Meteomodem_M2K2 = 3, Vaisala_RS41_SG = 4, }; - + Packet(const baseband::Packet& packet, const Type type); size_t length() const; @@ -56,9 +62,7 @@ public: std::string serial_number() const; uint32_t battery_voltage() const; - uint32_t GPS_altitude() const; - float GPS_latitude() const; - float GPS_longitude() const; + GPS_data get_GPS_data() const; FormattedSymbols symbols_formatted() const; @@ -75,17 +79,20 @@ private: 0xD0, 0xBC, 0xB4, 0xB6, 0x06, 0xAA, 0xF4, 0x23, 0x78, 0x6E, 0x3B, 0xAE, 0xBF, 0x7B, 0x4C, 0xC1 }; + + GPS_data ecef_to_gps() const; - //uint8_t vaisala_descramble(const uint32_t pos); + uint8_t vaisala_descramble(uint32_t pos) const; const baseband::Packet packet_; const BiphaseMDecoder decoder_; const FieldReader<BiphaseMDecoder, BitRemapNone> reader_bi_m; Type type_; + using packetReader = FieldReader<baseband::Packet, BitRemapByteReverse>; //baseband::Packet instead of BiphaseMDecoder bool crc_ok_M10() const; }; } /* namespace sonde */ -#endif/*__SONDE_PACKET_H__*/ +#endif/*__SONDE_PACKET_H__*/ \ No newline at end of file diff --git a/firmware/common/ui_widget.cpp b/firmware/common/ui_widget.cpp index c7427d478a058f0aecc0386dfe0b26e826a07728..a9333eae3511ac8c27d39964f14cdaf4226f8fb0 100644 --- a/firmware/common/ui_widget.cpp +++ b/firmware/common/ui_widget.cpp @@ -409,10 +409,26 @@ void Labels::paint(Painter& painter) { void LiveDateTime::on_tick_second() { rtcGetTime(&RTCD1, &datetime); + text = ""; - text = to_string_dec_uint(datetime.month(), 2, '0') + "/" + to_string_dec_uint(datetime.day(), 2, '0') + " " + - to_string_dec_uint(datetime.hour(), 2, '0') + ":" + to_string_dec_uint(datetime.minute(), 2, '0'); + if(date_enabled){ + text = to_string_dec_uint(datetime.month(), 2, '0') + "/" + to_string_dec_uint(datetime.day(), 2, '0') + " "; + } + text = text + to_string_dec_uint(datetime.hour(), 2, '0') + ":" + to_string_dec_uint(datetime.minute(), 2, '0'); + + if(seconds_enabled){ + text += ":"; + + if(init_delay==0) + text += to_string_dec_uint(datetime.second(), 2, '0'); + else + { + // Placeholder while the seconds are not updated + text += "XX"; + init_delay--; + } + } set_dirty(); } @@ -444,6 +460,14 @@ void LiveDateTime::paint(Painter& painter) { ); } +void LiveDateTime::set_date_enabled(bool new_value){ + this->date_enabled = new_value; +} + +void LiveDateTime::set_seconds_enabled(bool new_value) { + this->seconds_enabled = new_value; +} + /* BigFrequency **********************************************************/ BigFrequency::BigFrequency( @@ -1760,13 +1784,13 @@ void VuMeter::paint(Painter& painter) { lit = true; if (bar == 0) - color = lit ? Color::red() : Color::dark_red(); + color = lit ? Color::red() : Color::dark_grey(); else if (bar == 1) - color = lit ? Color::orange() : Color::dark_orange(); + color = lit ? Color::orange() : Color::dark_grey(); else if ((bar == 2) || (bar == 3)) - color = lit ? Color::yellow() : Color::dark_yellow(); + color = lit ? Color::yellow() : Color::dark_grey(); else - color = lit ? Color::green() : Color::dark_green(); + color = lit ? Color::green() : Color::dark_grey(); painter.fill_rectangle({ pos.x(), pos.y() + (Coord)(bar * (LED_height + 1)), width, (Coord)LED_height }, color); } diff --git a/firmware/common/ui_widget.hpp b/firmware/common/ui_widget.hpp index 61ffeb7258125103085e40dbb03f4cd8e8cb192c..51de42773a9539f299ff32ce9696f1325e7cdde2 100644 --- a/firmware/common/ui_widget.hpp +++ b/firmware/common/ui_widget.hpp @@ -243,7 +243,10 @@ public: ~LiveDateTime(); void paint(Painter& painter) override; - + + void set_seconds_enabled(bool new_value); + void set_date_enabled(bool new_value); + std::string& string() { return text; } @@ -251,6 +254,10 @@ public: private: void on_tick_second(); + uint16_t init_delay = 4; + bool date_enabled = true; + bool seconds_enabled = false; + rtc::RTC datetime { }; SignalToken signal_token_tick_second { }; std::string text { };