diff --git a/firmware/application/CMakeLists.txt b/firmware/application/CMakeLists.txt
index 270ea676ea348dae0dd924b82644f6ead48066cc..721bc2799b81f187d7b77269f213aff5e9d375cc 100644
--- a/firmware/application/CMakeLists.txt
+++ b/firmware/application/CMakeLists.txt
@@ -209,6 +209,7 @@ set(CPPSRC
 	ui/ui_btngrid.cpp
 	ui/ui_receiver.cpp
 	ui/ui_rssi.cpp
+	ui/ui_tv.cpp
 	ui/ui_spectrum.cpp
 	ui/ui_tabview.cpp
 	ui/ui_textentry.cpp
@@ -217,6 +218,8 @@ set(CPPSRC
 	apps/ui_adsb_rx.cpp
 	apps/ui_adsb_tx.cpp
 	apps/ui_afsk_rx.cpp
+	apps/ui_btle_rx.cpp
+	apps/ui_nrf_rx.cpp
 	apps/ui_aprs_tx.cpp
 	apps/ui_bht_tx.cpp
 	apps/ui_coasterp.cpp
@@ -251,6 +254,7 @@ set(CPPSRC
 	apps/acars_app.cpp
 	apps/ais_app.cpp
 	apps/analog_audio_app.cpp
+	apps/analog_tv_app.cpp
 	apps/capture_app.cpp
 	apps/ert_app.cpp
 	apps/lge_app.cpp
diff --git a/firmware/application/apps/analog_tv_app.cpp b/firmware/application/apps/analog_tv_app.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..8f4980788650dc4114cfc40f6eb729fe2cd5d2d2
--- /dev/null
+++ b/firmware/application/apps/analog_tv_app.cpp
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
+ * Copyright (C) 2018 Furrtek
+ * Copyright (C) 2020 Shao
+ *
+ * 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 "analog_tv_app.hpp"
+
+#include "baseband_api.hpp"
+
+#include "portapack.hpp"
+#include "portapack_persistent_memory.hpp"
+using namespace portapack;
+using namespace tonekey;
+
+#include "audio.hpp"
+#include "file.hpp"
+
+#include "utility.hpp"
+
+#include "string_format.hpp"
+
+namespace ui {
+
+/* AnalogTvView *******************************************************/
+
+AnalogTvView::AnalogTvView(
+	NavigationView& nav
+) : nav_ (nav)
+{
+	add_children({
+		&rssi,
+		&channel,
+		&audio,
+		&field_frequency,
+		&field_lna,
+		&field_vga,
+		&options_modulation,
+		&field_volume,
+		&tv
+	});
+
+	field_frequency.set_value(receiver_model.tuning_frequency());
+	field_frequency.set_step(receiver_model.frequency_step());
+	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>(receiver_model.tuning_frequency());
+		new_view->on_changed = [this](rf::Frequency f) {
+			this->on_tuning_frequency_changed(f);
+			this->field_frequency.set_value(f);
+		};
+	};
+
+	field_frequency.on_show_options = [this]() {
+		this->on_show_options_frequency();
+	};
+
+	field_lna.on_show_options = [this]() {
+		this->on_show_options_rf_gain();
+	};
+
+	field_vga.on_show_options = [this]() {
+		this->on_show_options_rf_gain();
+	};
+
+	const auto modulation = receiver_model.modulation();
+	options_modulation.set_by_value(toUType(ReceiverModel::Mode::WidebandFMAudio));
+	options_modulation.on_change = [this](size_t, OptionsField::value_t v) {
+		this->on_modulation_changed(static_cast<ReceiverModel::Mode>(v));
+	};
+	options_modulation.on_show_options = [this]() {
+		this->on_show_options_modulation();
+	};
+
+	field_volume.set_value(0);
+	field_volume.on_change = [this](int32_t v) {
+		this->on_headphone_volume_changed(v);
+	};
+	
+	tv.on_select = [this](int32_t offset) {
+		field_frequency.set_value(receiver_model.tuning_frequency() + offset);
+	};
+
+	update_modulation(static_cast<ReceiverModel::Mode>(modulation));
+        on_modulation_changed(ReceiverModel::Mode::WidebandFMAudio);
+}
+
+AnalogTvView::~AnalogTvView() {
+	// TODO: Manipulating audio codec here, and in ui_receiver.cpp. Good to do
+	// both?
+	audio::output::stop();
+
+	receiver_model.disable();
+
+	baseband::shutdown();
+}
+
+void AnalogTvView::on_hide() {
+	// TODO: Terrible kludge because widget system doesn't notify Waterfall that
+	// it's being shown or hidden.
+	tv.on_hide();
+	View::on_hide();
+}
+
+void AnalogTvView::set_parent_rect(const Rect new_parent_rect) {
+	View::set_parent_rect(new_parent_rect);
+	
+	const ui::Rect tv_rect { 0, header_height, new_parent_rect.width(), new_parent_rect.height() - header_height };
+	tv.set_parent_rect(tv_rect);
+}
+
+void AnalogTvView::focus() {
+	field_frequency.focus();
+}
+
+void AnalogTvView::on_tuning_frequency_changed(rf::Frequency f) {
+	receiver_model.set_tuning_frequency(f);
+}
+
+void AnalogTvView::on_baseband_bandwidth_changed(uint32_t bandwidth_hz) {
+	receiver_model.set_baseband_bandwidth(bandwidth_hz);
+}
+
+void AnalogTvView::on_modulation_changed(const ReceiverModel::Mode modulation) {
+	// TODO: Terrible kludge because widget system doesn't notify Waterfall that
+	// it's being shown or hidden.
+	tv.on_hide();
+	update_modulation(modulation);
+	on_show_options_modulation();
+	tv.on_show();
+}
+
+void AnalogTvView::remove_options_widget() {
+	if( options_widget ) {
+		remove_child(options_widget.get());
+		options_widget.reset();
+	}
+	
+	field_lna.set_style(nullptr);
+	options_modulation.set_style(nullptr);
+	field_frequency.set_style(nullptr);
+}
+
+void AnalogTvView::set_options_widget(std::unique_ptr<Widget> new_widget) {
+	remove_options_widget();
+
+	if( new_widget ) {
+		options_widget = std::move(new_widget);
+	} else {
+		// TODO: Lame hack to hide options view due to my bad paint/damage algorithm.
+		options_widget = std::make_unique<Rectangle>(options_view_rect, style_options_group_new.background);
+	}
+	add_child(options_widget.get());
+}
+
+void AnalogTvView::on_show_options_frequency() {
+	auto widget = std::make_unique<FrequencyOptionsView>(options_view_rect, &style_options_group_new);
+
+	widget->set_step(receiver_model.frequency_step());
+	widget->on_change_step = [this](rf::Frequency f) {
+		this->on_frequency_step_changed(f);
+	};
+	widget->set_reference_ppm_correction(persistent_memory::correction_ppb() / 1000);
+	widget->on_change_reference_ppm_correction = [this](int32_t v) {
+		this->on_reference_ppm_correction_changed(v);
+	};
+
+	set_options_widget(std::move(widget));
+	field_frequency.set_style(&style_options_group_new);
+}
+
+void AnalogTvView::on_show_options_rf_gain() {
+	auto widget = std::make_unique<RadioGainOptionsView>(options_view_rect, &style_options_group_new);
+
+	set_options_widget(std::move(widget));
+	field_lna.set_style(&style_options_group_new);
+}
+
+void AnalogTvView::on_show_options_modulation() {
+	std::unique_ptr<Widget> widget;
+
+	const auto modulation = static_cast<ReceiverModel::Mode>(receiver_model.modulation());
+	tv.show_audio_spectrum_view(true);
+	
+	set_options_widget(std::move(widget));
+	options_modulation.set_style(&style_options_group_new);
+}
+
+void AnalogTvView::on_frequency_step_changed(rf::Frequency f) {
+	receiver_model.set_frequency_step(f);
+	field_frequency.set_step(f);
+}
+
+void AnalogTvView::on_reference_ppm_correction_changed(int32_t v) {
+	persistent_memory::set_correction_ppb(v * 1000);
+}
+
+void AnalogTvView::on_headphone_volume_changed(int32_t v) {
+	//tv::TVView::set_headphone_volume(this,v);
+}
+
+void AnalogTvView::update_modulation(const ReceiverModel::Mode modulation) {
+	audio::output::mute();
+
+	baseband::shutdown();
+
+	portapack::spi_flash::image_tag_t image_tag;
+	image_tag = portapack::spi_flash::image_tag_am_tv;	
+
+	baseband::run_image(image_tag);
+
+	receiver_model.set_modulation(modulation);
+        receiver_model.set_sampling_rate(2000000);
+        receiver_model.set_baseband_bandwidth(2000000);
+	receiver_model.enable();
+}
+
+} /* namespace ui */
diff --git a/firmware/application/apps/analog_tv_app.hpp b/firmware/application/apps/analog_tv_app.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..552f7f6ba4fab5235c63de5c272a06919ace0495
--- /dev/null
+++ b/firmware/application/apps/analog_tv_app.hpp
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
+ * Copyright (C) 2018 Furrtek
+ * Copyright (C) 2020 Shao
+ *
+ * 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 __ANALOG_TV_APP_H__
+#define __ANALOG_TV_APP_H__
+
+#include "receiver_model.hpp"
+
+#include "ui_receiver.hpp"
+#include "ui_tv.hpp"
+#include "ui_record_view.hpp"
+
+#include "ui_font_fixed_8x16.hpp"
+
+#include "tone_key.hpp"
+
+namespace ui {
+
+constexpr Style style_options_group_new {
+	.font = font::fixed_8x16,
+	.background = Color::blue(),
+	.foreground = Color::white(),
+};
+
+class AnalogTvView : public View {
+public:
+	AnalogTvView(NavigationView& nav);
+	~AnalogTvView();
+
+	void on_hide() override;
+
+	void set_parent_rect(const Rect new_parent_rect) override;
+
+	void focus() override;
+
+	std::string title() const override { return "Analog TV"; };
+	
+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 };
+
+	NavigationView& nav_;
+	
+	RSSI rssi {
+		{ 21 * 8, 0, 6 * 8, 4 },
+	};
+
+	Channel channel {
+		{ 21 * 8, 5, 6 * 8, 4 },
+	};
+
+	Audio audio {
+		{ 21 * 8, 10, 6 * 8, 4 },
+	};
+
+	FrequencyField field_frequency {
+		{ 5 * 8, 0 * 16 },
+	};
+
+	LNAGainField field_lna {
+		{ 15 * 8, 0 * 16 }
+	};
+
+	VGAGainField field_vga {
+		{ 18 * 8, 0 * 16 }
+	};
+
+	OptionsField options_modulation {
+		{ 0 * 8, 0 * 16 },
+		4,
+		{
+			{ "TV ", toUType(ReceiverModel::Mode::WidebandFMAudio) },
+			{ "TV ", toUType(ReceiverModel::Mode::WidebandFMAudio) },
+			{ "TV ", toUType(ReceiverModel::Mode::WidebandFMAudio) },
+		}
+	};
+
+	NumberField field_volume {
+		{ 27 * 8, 0 * 16 },
+		3,
+		{ 0, 255 },
+		1,
+		' ',
+	};
+
+	std::unique_ptr<Widget> options_widget { };
+
+	tv::TVWidget tv { true };
+
+	void on_tuning_frequency_changed(rf::Frequency f);
+	void on_baseband_bandwidth_changed(uint32_t bandwidth_hz);
+	void on_modulation_changed(const ReceiverModel::Mode modulation);
+	void on_show_options_frequency();
+	void on_show_options_rf_gain();
+	void on_show_options_modulation();
+	void on_frequency_step_changed(rf::Frequency f);
+	void on_reference_ppm_correction_changed(int32_t v);
+	void on_headphone_volume_changed(int32_t v);
+	void on_edit_frequency();
+
+	void remove_options_widget();
+	void set_options_widget(std::unique_ptr<Widget> new_widget);
+
+	void update_modulation(const ReceiverModel::Mode modulation);
+	
+
+};
+
+} /* namespace ui */
+
+#endif/*__ANALOG_TV_APP_H__*/
diff --git a/firmware/application/apps/ui_btle_rx.cpp b/firmware/application/apps/ui_btle_rx.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..d4b9afeed50235fa393b75b190ce658024c74ffe
--- /dev/null
+++ b/firmware/application/apps/ui_btle_rx.cpp
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
+ * Copyright (C) 2017 Furrtek
+ * Copyright (C) 2020 Shao
+ *
+ * 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_btle_rx.hpp"
+#include "ui_modemsetup.hpp"
+
+#include "modems.hpp"
+#include "audio.hpp"
+#include "rtc_time.hpp"
+#include "baseband_api.hpp"
+#include "string_format.hpp"
+#include "portapack_persistent_memory.hpp"
+
+using namespace portapack;
+using namespace modems;
+
+namespace ui {
+
+void BTLERxView::focus() {
+	field_frequency.focus();
+}
+
+void BTLERxView::update_freq(rf::Frequency f) {
+	receiver_model.set_tuning_frequency(f);
+}
+
+BTLERxView::BTLERxView(NavigationView& nav) {
+	baseband::run_image(portapack::spi_flash::image_tag_btle_rx);
+	
+	add_children({
+		&rssi,
+		&channel,
+		&field_rf_amp,
+		&field_lna,
+		&field_vga,
+		&field_frequency,
+		&text_debug,
+		&button_modem_setup,
+		&record_view,
+		&console
+	});
+	
+	// DEBUG
+	record_view.on_error = [&nav](std::string message) {
+		nav.display_modal("Error", message);
+	};
+	record_view.set_sampling_rate(24000);
+	
+	// Auto-configure modem for LCR RX (will be removed later)
+	update_freq(2426000000);
+	auto def_bell202 = &modem_defs[0];
+	persistent_memory::set_modem_baudrate(def_bell202->baudrate);
+	serial_format_t serial_format;
+	serial_format.data_bits = 7;
+	serial_format.parity = EVEN;
+	serial_format.stop_bits = 1;
+	serial_format.bit_order = LSB_FIRST;
+	persistent_memory::set_serial_format(serial_format);
+	
+	field_frequency.set_value(receiver_model.tuning_frequency());
+	field_frequency.set_step(100);
+	field_frequency.on_change = [this](rf::Frequency f) {
+		update_freq(f);
+	};
+	field_frequency.on_edit = [this, &nav]() {
+		auto new_view = nav.push<FrequencyKeypadView>(receiver_model.tuning_frequency());
+		new_view->on_changed = [this](rf::Frequency f) {
+			update_freq(f);
+			field_frequency.set_value(f);
+		};
+	};
+
+	button_modem_setup.on_select = [&nav](Button&) {
+		nav.push<ModemSetupView>();
+	};
+	
+	
+	// Auto-configure modem for LCR RX (will be removed later)
+	baseband::set_btle(persistent_memory::modem_baudrate(), 8, 0, false);
+	
+	audio::set_rate(audio::Rate::Hz_24000);
+	audio::output::start();
+	
+	receiver_model.set_sampling_rate(4000000);
+	receiver_model.set_baseband_bandwidth(4000000);
+	receiver_model.set_modulation(ReceiverModel::Mode::WidebandFMAudio);
+	receiver_model.enable();
+}
+
+void BTLERxView::on_data(uint32_t value, bool is_data) {
+	//std::string str_console = "\x1B";
+	std::string str_console = "";
+	if (is_data) {
+		// Colorize differently after message splits
+		//str_console += (char)((console_color & 3) + 9);
+		
+		//value &= 0xFF;											// ABCDEFGH
+		//value = ((value & 0xF0) >> 4) | ((value & 0x0F) << 4);	// EFGHABCD
+		//value = ((value & 0xCC) >> 2) | ((value & 0x33) << 2);	// GHEFCDAB
+		//value = ((value & 0xAA) >> 1) | ((value & 0x55) << 1);	// HGFEDCBA
+		//value &= 0x7F;											// Ignore parity, which is the MSB now
+		
+		//if ((value >= 32) && (value < 127)) {
+		//	str_console += (char)value;							// Printable
+		//} 
+		
+		//str_console += (char)'A';
+		//str_console += (char)value;
+		//str_console += "[" + to_string_hex(value, 2) + "]";	
+		str_console += ":" + to_string_hex(value, 2) ;	
+		console.write(str_console);
+		
+		
+		
+		/*if ((value != 0x7F) && (prev_value == 0x7F)) {
+			// Message split
+			console.writeln("");
+			console_color++;
+			
+			
+		}*/
+		//prev_value = value;
+	} else {
+		// Baudrate estimation
+		//text_debug.set("~" + to_string_dec_uint(value)); 
+		if (value == 'A')
+		{console.write("mac");}
+		else if (value == 'B')
+		{console.writeln("");}
+		//console.writeln("");
+	}
+}
+
+BTLERxView::~BTLERxView() {
+	audio::output::stop();
+	receiver_model.disable();
+	baseband::shutdown();
+}
+
+} /* namespace ui */
diff --git a/firmware/application/apps/ui_btle_rx.hpp b/firmware/application/apps/ui_btle_rx.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..9e331eefcee58c8274c9213f744778a292bc32f2
--- /dev/null
+++ b/firmware/application/apps/ui_btle_rx.hpp
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
+ * Copyright (C) 2017 Furrtek
+ * Copyright (C) 2020 Shao
+ *
+ * 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_BTLE_RX_H__
+#define __UI_BTLE_RX_H__
+
+#include "ui.hpp"
+#include "ui_navigation.hpp"
+#include "ui_receiver.hpp"
+#include "ui_record_view.hpp"	// DEBUG
+
+#include "utility.hpp"
+
+namespace ui {
+
+class BTLERxView : public View {
+public:
+	BTLERxView(NavigationView& nav);
+	~BTLERxView();
+
+	void focus() override;
+
+	std::string title() const override { return "BTLE RX"; };
+	
+private:
+	void on_data(uint32_t value, bool is_data);
+	
+	uint8_t console_color { 0 };
+	uint32_t prev_value { 0 };
+	std::string str_log { "" };
+
+	RFAmpField field_rf_amp {
+		{ 13 * 8, 0 * 16 }
+	};
+	LNAGainField field_lna {
+		{ 15 * 8, 0 * 16 }
+	};
+	VGAGainField field_vga {
+		{ 18 * 8, 0 * 16 }
+	};
+	RSSI rssi {
+		{ 21 * 8, 0, 6 * 8, 4 },
+	};
+	Channel channel {
+		{ 21 * 8, 5, 6 * 8, 4 },
+	};
+	
+	FrequencyField field_frequency {
+		{ 0 * 8, 0 * 16 },
+	};
+	
+	Text text_debug {
+		{ 0 * 8, 1 * 16, 10 * 8, 16 },
+		"DEBUG"
+	};
+	
+	
+	Button button_modem_setup {
+		{ 12 * 8, 1 * 16, 96, 24 },
+		"Modem setup"
+	};
+	
+	// DEBUG
+	RecordView record_view {
+		{ 0 * 8, 3 * 16, 30 * 8, 1 * 16 },
+		u"AFS_????", RecordView::FileType::WAV, 4096, 4
+	};
+	
+	Console console {
+		{ 0, 4 * 16, 240, 240 }
+	};
+
+	void update_freq(rf::Frequency f);
+	//void on_data_afsk(const AFSKDataMessage& message);
+	
+	MessageHandlerRegistration message_handler_packet {
+		Message::ID::AFSKData,
+		[this](Message* const p) {
+			const auto message = static_cast<const AFSKDataMessage*>(p);
+			this->on_data(message->value, message->is_data);
+		}
+	};
+};
+
+} /* namespace ui */
+
+#endif/*__UI_BTLE_RX_H__*/
diff --git a/firmware/application/apps/ui_nrf_rx.cpp b/firmware/application/apps/ui_nrf_rx.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..32982ae6aca4caf9ed8d385d370b574998e4e891
--- /dev/null
+++ b/firmware/application/apps/ui_nrf_rx.cpp
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
+ * Copyright (C) 2017 Furrtek
+ * Copyright (C) 2020 Shao
+ *
+ * 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_nrf_rx.hpp"
+#include "ui_modemsetup.hpp"
+
+#include "modems.hpp"
+#include "audio.hpp"
+#include "rtc_time.hpp"
+#include "baseband_api.hpp"
+#include "string_format.hpp"
+#include "portapack_persistent_memory.hpp"
+
+using namespace portapack;
+using namespace modems;
+
+namespace ui {
+
+void NRFRxView::focus() {
+	field_frequency.focus();
+}
+
+void NRFRxView::update_freq(rf::Frequency f) {
+	receiver_model.set_tuning_frequency(f);
+}
+
+NRFRxView::NRFRxView(NavigationView& nav) {
+	baseband::run_image(portapack::spi_flash::image_tag_nrf_rx);
+	
+	add_children({
+		&rssi,
+		&channel,
+		&field_rf_amp,
+		&field_lna,
+		&field_vga,
+		&field_frequency,
+		&text_debug,
+		&button_modem_setup,
+		&record_view,
+		&console
+	});
+	
+	// DEBUG
+	record_view.on_error = [&nav](std::string message) {
+		nav.display_modal("Error", message);
+	};
+	record_view.set_sampling_rate(24000);
+	
+	// Auto-configure modem for LCR RX (will be removed later)
+	update_freq(2480000000);
+	auto def_bell202 = &modem_defs[0];
+	persistent_memory::set_modem_baudrate(def_bell202->baudrate);
+	serial_format_t serial_format;
+	serial_format.data_bits = 7;
+	serial_format.parity = EVEN;
+	serial_format.stop_bits = 1;
+	serial_format.bit_order = LSB_FIRST;
+	persistent_memory::set_serial_format(serial_format);
+	
+	field_frequency.set_value(receiver_model.tuning_frequency());
+	field_frequency.set_step(100);
+	field_frequency.on_change = [this](rf::Frequency f) {
+		update_freq(f);
+	};
+	field_frequency.on_edit = [this, &nav]() {
+		auto new_view = nav.push<FrequencyKeypadView>(receiver_model.tuning_frequency());
+		new_view->on_changed = [this](rf::Frequency f) {
+			update_freq(f);
+			field_frequency.set_value(f);
+		};
+	};
+
+	button_modem_setup.on_select = [&nav](Button&) {
+		nav.push<ModemSetupView>();
+	};
+	
+	
+	// Auto-configure modem for LCR RX (will be removed later)
+	baseband::set_nrf(persistent_memory::modem_baudrate(), 8, 0, false);
+	
+	audio::set_rate(audio::Rate::Hz_24000);
+	audio::output::start();
+	
+	receiver_model.set_sampling_rate(4000000);
+	receiver_model.set_baseband_bandwidth(4000000);
+	receiver_model.set_modulation(ReceiverModel::Mode::WidebandFMAudio);
+	receiver_model.enable();
+}
+
+void NRFRxView::on_data(uint32_t value, bool is_data) {
+	//std::string str_console = "\x1B";
+	std::string str_console = "";
+	if (is_data) {
+		// Colorize differently after message splits
+		//str_console += (char)((console_color & 3) + 9);
+		
+		//value &= 0xFF;											// ABCDEFGH
+		//value = ((value & 0xF0) >> 4) | ((value & 0x0F) << 4);	// EFGHABCD
+		//value = ((value & 0xCC) >> 2) | ((value & 0x33) << 2);	// GHEFCDAB
+		//value = ((value & 0xAA) >> 1) | ((value & 0x55) << 1);	// HGFEDCBA
+		//value &= 0x7F;											// Ignore parity, which is the MSB now
+		
+		//if ((value >= 32) && (value < 127)) {
+		//	str_console += (char)value;							// Printable
+		//} 
+		
+		//str_console += (char)'A';
+		//str_console += (char)value;
+		//str_console += "[" + to_string_hex(value, 2) + "]";	
+		str_console += " " + to_string_hex(value, 2) ;	
+		console.write(str_console);
+		
+		
+		
+		/*if ((value != 0x7F) && (prev_value == 0x7F)) {
+			// Message split
+			console.writeln("");
+			console_color++;
+			
+			
+		}*/
+		//prev_value = value;
+	} else {
+		// Baudrate estimation
+		//text_debug.set("~" + to_string_dec_uint(value)); 
+		if (value == 'A')
+		{console.write("addr:");}
+		else if (value == 'B')
+		{console.write(" data:");}
+		else if (value == 'C')
+		{
+			console.writeln("");
+			console.writeln("");
+		}
+		//console.writeln("");
+	}
+}
+
+NRFRxView::~NRFRxView() {
+	audio::output::stop();
+	receiver_model.disable();
+	baseband::shutdown();
+}
+
+} /* namespace ui */
diff --git a/firmware/application/apps/ui_nrf_rx.hpp b/firmware/application/apps/ui_nrf_rx.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..4f72b25b5d2eba22b7c4338473d0a03007022b8e
--- /dev/null
+++ b/firmware/application/apps/ui_nrf_rx.hpp
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
+ * Copyright (C) 2017 Furrtek
+ * Copyright (C) 2020 Shao
+ *
+ * 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_NRF_RX_H__
+#define __UI_NRF_RX_H__
+
+#include "ui.hpp"
+#include "ui_navigation.hpp"
+#include "ui_receiver.hpp"
+#include "ui_record_view.hpp"	// DEBUG
+
+#include "utility.hpp"
+
+namespace ui {
+
+class NRFRxView : public View {
+public:
+	NRFRxView(NavigationView& nav);
+	~NRFRxView();
+
+	void focus() override;
+
+	std::string title() const override { return "NRF RX"; };
+	
+private:
+	void on_data(uint32_t value, bool is_data);
+	
+	uint8_t console_color { 0 };
+	uint32_t prev_value { 0 };
+	std::string str_log { "" };
+
+	RFAmpField field_rf_amp {
+		{ 13 * 8, 0 * 16 }
+	};
+	LNAGainField field_lna {
+		{ 15 * 8, 0 * 16 }
+	};
+	VGAGainField field_vga {
+		{ 18 * 8, 0 * 16 }
+	};
+	RSSI rssi {
+		{ 21 * 8, 0, 6 * 8, 4 },
+	};
+	Channel channel {
+		{ 21 * 8, 5, 6 * 8, 4 },
+	};
+	
+	FrequencyField field_frequency {
+		{ 0 * 8, 0 * 16 },
+	};
+	
+	Text text_debug {
+		{ 0 * 8, 1 * 16, 10 * 8, 16 },
+		"DEBUG"
+	};
+	
+	
+	Button button_modem_setup {
+		{ 12 * 8, 1 * 16, 96, 24 },
+		"Modem setup"
+	};
+	
+	// DEBUG
+	RecordView record_view {
+		{ 0 * 8, 3 * 16, 30 * 8, 1 * 16 },
+		u"AFS_????", RecordView::FileType::WAV, 4096, 4
+	};
+	
+	Console console {
+		{ 0, 4 * 16, 240, 240 }
+	};
+
+	void update_freq(rf::Frequency f);
+	//void on_data_afsk(const AFSKDataMessage& message);
+	
+	MessageHandlerRegistration message_handler_packet {
+		Message::ID::AFSKData,
+		[this](Message* const p) {
+			const auto message = static_cast<const AFSKDataMessage*>(p);
+			this->on_data(message->value, message->is_data);
+		}
+	};
+};
+
+} /* namespace ui */
+
+#endif/*__UI_NRF_RX_H__*/
diff --git a/firmware/application/baseband_api.cpp b/firmware/application/baseband_api.cpp
index beb95371bec35c676e905a16188ae78ed26732cf..502047d2374993fd41cada71c15a426932dec93e 100644
--- a/firmware/application/baseband_api.cpp
+++ b/firmware/application/baseband_api.cpp
@@ -130,6 +130,26 @@ void set_afsk(const uint32_t baudrate, const uint32_t word_length, const uint32_
 	send_message(&message);
 }
 
+void set_btle(const uint32_t baudrate, const uint32_t word_length, const uint32_t trigger_value, const bool trigger_word) {
+	const BTLERxConfigureMessage message {
+		baudrate,
+		word_length,
+		trigger_value,
+		trigger_word
+	};
+	send_message(&message);
+}
+    
+void set_nrf(const uint32_t baudrate, const uint32_t word_length, const uint32_t trigger_value, const bool trigger_word) {
+	const NRFRxConfigureMessage message {
+		baudrate,
+		word_length,
+		trigger_value,
+		trigger_word
+	};
+	send_message(&message);
+}
+    
 void set_afsk_data(const uint32_t afsk_samples_per_bit, const uint32_t afsk_phase_inc_mark, const uint32_t afsk_phase_inc_space,
 					const uint8_t afsk_repeat, const uint32_t afsk_bw, const uint8_t symbol_count) {
 	const AFSKTxConfigureMessage message {
diff --git a/firmware/application/baseband_api.hpp b/firmware/application/baseband_api.hpp
index 8562a90ce6a05f020c27119d7cb5cc0f6a5a9642..9592f8ca84b2aea8d462836f22b5d091462582b6 100644
--- a/firmware/application/baseband_api.hpp
+++ b/firmware/application/baseband_api.hpp
@@ -68,6 +68,11 @@ void set_afsk_data(const uint32_t afsk_samples_per_bit, const uint32_t afsk_phas
 					const uint8_t afsk_repeat, const uint32_t afsk_bw, const uint8_t symbol_count);
 void kill_afsk();
 void set_afsk(const uint32_t baudrate, const uint32_t word_length, const uint32_t trigger_value, const bool trigger_word);
+
+void set_btle(const uint32_t baudrate, const uint32_t word_length, const uint32_t trigger_value, const bool trigger_word);
+
+void set_nrf(const uint32_t baudrate, const uint32_t word_length, const uint32_t trigger_value, const bool trigger_word);
+
 void set_ook_data(const uint32_t stream_length, const uint32_t samples_per_bit, const uint8_t repeat,
 					const uint32_t pause_symbols);
 void set_fsk_data(const uint32_t stream_length, const uint32_t samples_per_bit, const uint32_t shift,
diff --git a/firmware/application/bitmap.hpp b/firmware/application/bitmap.hpp
index 7cab6b24a1dd1c4a63503319dd15f691adabf3eb..b837d3d2f353a58ea313b87f339a6abb04e02777 100644
--- a/firmware/application/bitmap.hpp
+++ b/firmware/application/bitmap.hpp
@@ -1524,6 +1524,51 @@ static constexpr uint8_t bitmap_gps_sim_data[] = {
 static constexpr Bitmap bitmap_gps_sim {
 	{ 16, 16 }, bitmap_gps_sim_data
 };
+
+static constexpr uint8_t bitmap_icon_btle_data[] = {
+	0x00, 0x00, 
+	0x80, 0x00, 
+	0x80, 0x01, 
+	0x80, 0x07, 
+	0x80, 0x0C, 
+	0x98, 0x06, 
+	0xF0, 0x03, 
+	0xE0, 0x01, 
+	0xE0, 0x01, 
+	0xF0, 0x03, 
+	0x98, 0x06, 
+	0x80, 0x0C, 
+	0x80, 0x07, 
+	0x80, 0x01, 
+	0x80, 0x00, 
+	0x00, 0x00, 
+};
+static constexpr Bitmap bitmap_icon_btle {
+	{ 16, 16 }, bitmap_icon_btle_data
+};
+
+static constexpr uint8_t bitmap_icon_nrf_data[] = {
+	0x00, 0x00, 
+	0x00, 0x00, 
+	0x00, 0x00, 
+	0x07, 0xE0, 
+	0x05, 0x20, 
+	0xCC, 0x33, 
+	0xF4, 0x2F, 
+	0xE6, 0x67, 
+	0xE2, 0x47, 
+	0x36, 0x5C, 
+	0x0E, 0xE0, 
+	0x06, 0x60, 
+	0x00, 0x00, 
+	0x00, 0x00, 
+	0x00, 0x00, 
+	0x00, 0x00, 
+};
+static constexpr Bitmap bitmap_icon_nrf {
+	{ 16, 16 }, bitmap_icon_nrf_data
+};
+
 static constexpr uint8_t bitmap_sig_sine_data[] = {
 	0x00, 0x00, 0x00, 0x00, 
 	0x00, 0x00, 0x00, 0x00, 
diff --git a/firmware/application/spectrum_color_lut.cpp b/firmware/application/spectrum_color_lut.cpp
index bd2379c28bbf88944fecba3d8365ead33a43a72e..e78a5322f74174b7f30ccf3a851c939d6d548b8a 100644
--- a/firmware/application/spectrum_color_lut.cpp
+++ b/firmware/application/spectrum_color_lut.cpp
@@ -538,3 +538,262 @@ const std::array<ui::Color, 256> spectrum_rgb3_lut { {
 	{ 252,   3,   0 },
 	{ 255,   0,   0 },
 } };
+
+const std::array<ui::Color, 256> spectrum_rgb4_lut { {
+	{   0,   0,   0 },
+	{   1,   1,   1 },
+	{   2,   2,   2 },
+	{   3,   3,   3 },
+	{   4,   4,   4 },
+	{   5,   5,   5 },
+	{   6,   6,   6 },
+	{   7,   7,   7 },
+	{   8,   8,   8 },
+	{   9,   9,   9 },
+	{   10,  10,  10 },
+	{   11,  11,  11 },
+	{   12,  12,  12 },
+	{   13,  13,  13 },
+	{   14,  14,  14 },
+	{   15,  15,  15 },
+	{   16,  16,  16 },
+	{   17,  17,  17 },
+	{   18,  18,  18 },
+	{   19,  19,  19 },
+	{   20,  20,  20 },
+	{   21,  21,  21 },
+	{   22,  22,  22 },
+	{   23,  23,  23 },
+	{   24,  24,  24 },
+	{   25,  25,  25 },
+	{   26,  26,  26 },
+	{   27,  27,  27 },
+	{   28,  28,  28 },
+	{   29,  29,  29 },
+	{   30,  30,  30 },
+	{   31,  31,  31 },
+	{   32,  32,  32 },
+	{   33,  33,  33 },
+	{   34,  34,  34 },
+	{   35,  35,  35 },
+	{   36,  36,  36 },
+	{   37,  37,  37 },
+	{   38,  38,  38 },
+	{   39,  39,  39 },
+	{   40,  40,  40 },
+	{   41,  41,  41 },
+	{   42,  42,  42 },
+	{   43,  43,  43 },
+	{   44,  44,  44 },
+	{   45,  45,  45 },
+	{   46,  46,  46 },
+	{   47,  47,  47 },
+	{   48,  48,  48 },
+	{   49,  49,  49 },
+	{   50,  50,  50 },
+	{   51,  51,  51 },
+	{   52,  52,  52 },
+	{   53,  53,  53 },
+	{   54,  54,  54 },
+	{   55,  55,  55 },
+	{   56,  56,  56 },
+	{   57,  57,  57 },
+	{   58,  58,  58 },
+	{   59,  59,  59 },
+	{   60,  60,  60 },
+	{   61,  61,  61 },
+	{   62,  62,  62 },
+	{   63,  63,  63 },
+	{   64,  64,  64 },
+	{   65,  65,  65 },
+	{   66,  66,  66 },
+	{   67,  67,  67 },
+	{   68,  68,  68 },
+	{   69,  69,  69 },
+	{   70,  70,  70 },
+	{   71,  71,  71 },
+	{   72,  72,  72 },
+	{   73,  73,  73 },
+	{   74,  74,  74 },
+	{   75,  75,  75 },
+	{   76,  76,  76 },
+	{   77,  77,  77 },
+	{   78,  78,  78 },
+	{   79,  79,  79 },
+	{   80,  80,  80 },
+	{   81,  81,  81 },
+	{   82,  82,  82 },
+	{   83,  83,  83 },
+	{   84,  84,  84 },
+	{   85,  85,  85 },
+	{   86,  86,  86 },
+	{   87,  87,  87 },
+	{   88,  88,  88 },
+	{   89,  89,  89 },
+	{   90,  90,  90 },
+	{   91,  91,  91 },
+	{   92,  92,  92 },
+	{   93,  93,  93 },
+	{   94,  94,  94 },
+	{   95,  95,  95 },
+	{   96,  96,  96 },
+	{   97,  97,  97 },
+	{   98,  98,  98 },
+	{   99,  99,  99 },
+	{   100, 100, 100 },
+	{   101, 101, 101 },
+	{   102, 102, 102 },
+	{   103, 103, 103 },
+	{   104, 104, 104 },
+	{   105, 105, 105 },
+	{   106, 106, 106 },
+	{   107, 107, 107 },
+	{   108, 108, 108 },
+	{   109, 109, 109 },
+	{   110, 110, 110 },
+	{   111, 111, 111 },
+	{   112, 112, 112 },
+	{   113, 113, 113 },
+	{   114, 114, 114 },
+	{   115, 115, 115 },
+	{   116, 116, 116 },
+	{   117, 117, 117 },
+	{   118, 118, 118 },
+	{   119, 119, 119 },
+	{   120, 120, 120 },
+	{   121, 121, 121 },
+	{   122, 122, 122 },
+	{   123, 123, 123 },
+	{   124, 124, 124 },
+	{   125, 125, 125 },
+	{   126, 126, 126 },
+	{   127, 127, 127 },
+	{   128, 128, 128 },
+	{   129, 129, 129 },
+	{   130, 130, 130 },
+	{   131, 131, 131 },
+	{   132, 132, 132 },
+	{   133, 133, 133 },
+	{   134, 134, 134 },
+	{   135, 135, 135 },
+	{   136, 136, 136 },
+	{   137, 137, 137 },
+	{   138, 138, 138 },
+	{   139, 139, 139 },
+	{   140, 140, 140 },
+	{   141, 141, 141 },
+	{   142, 142, 142 },
+	{   143, 143, 143 },
+	{   144, 144, 144 },
+	{   145, 145, 145 },
+	{   146, 146, 146 },
+	{   147, 147, 147 },
+	{   148, 148, 148 },
+	{   149, 149, 149 },
+	{   150, 150, 150 },
+	{   151, 151, 151 },
+	{   152, 152, 152 },
+	{   153, 153, 153 },
+	{   154, 154, 154 },
+	{   155, 155, 155 },
+	{   156, 156, 156 },
+	{   157, 157, 157 },
+	{   158, 158, 158 },
+	{   159, 159, 159 },
+	{   160, 160, 160 },
+	{   161, 161, 161 },
+	{   162, 162, 162 },
+	{   163, 163, 163 },
+	{   164, 164, 164 },
+	{   165, 165, 165 },
+	{   166, 166, 166 },
+	{   167, 167, 167 },
+	{   168, 168, 168 },
+	{   169, 169, 169 },
+	{   170, 170, 170 },
+	{   171, 171, 171 },
+	{   172, 172, 172 },
+	{   173, 173, 173 },
+	{   174, 174, 174 },
+	{   175, 175, 175 },
+	{   176, 176, 176 },
+	{   177, 177, 177 },
+	{   178, 178, 178 },
+	{   179, 179, 179 },
+	{   180, 180, 180 },
+	{   181, 181, 181 },
+	{   182, 182, 182 },
+	{   183, 183, 183 },
+	{   184, 184, 184 },
+	{   185, 185, 185 },
+	{   186, 186, 186 },
+	{   187, 187, 187 },
+	{   188, 188, 188 },
+	{   189, 189, 189 },
+	{   190, 190, 190 },
+	{   191, 191, 191 },
+	{   192, 192, 192 },
+	{   193, 193, 193 },
+	{   194, 194, 194 },
+	{   195, 195, 195 },
+	{   196, 196, 196 },
+	{   197, 197, 197 },
+	{   198, 198, 198 },
+	{   199, 199, 199 },
+	{   200, 200, 200 },
+	{   201, 201, 201 },
+	{   202, 202, 202 },
+	{   203, 203, 203 },
+	{   204, 204, 204 },
+	{   205, 205, 205 },
+	{   206, 206, 206 },
+	{   207, 207, 207 },
+	{   208, 208, 208 },
+	{   209, 209, 209 },
+	{   210, 210, 210 },
+	{   211, 211, 211 },
+	{   212, 212, 212 },
+	{   213, 213, 213 },
+	{   214, 214, 214 },
+	{   215, 215, 215 },
+	{   216, 216, 216 },
+	{   217, 217, 217 },
+	{   218, 218, 218 },
+	{   219, 219, 219 },
+	{   220, 220, 220 },
+	{   221, 221, 221 },
+	{   222, 222, 222 },
+	{   223, 223, 223 },
+	{   224, 224, 224 },
+	{   225, 225, 225 },
+	{   226, 226, 226 },
+	{   227, 227, 227 },
+	{   228, 228, 228 },
+	{   229, 229, 229 },
+	{   230, 230, 230 },
+	{   231, 231, 231 },
+	{   232, 232, 232 },
+	{   233, 233, 233 },
+	{   234, 234, 234 },
+	{   235, 235, 235 },
+	{   236, 236, 236 },
+	{   237, 237, 237 },
+	{   238, 238, 238 },
+	{   239, 239, 239 },
+	{   240, 240, 240 },
+	{   241, 241, 241 },
+	{   242, 242, 242 },
+	{   243, 243, 243 },
+	{   244, 244, 244 },
+	{   245, 245, 245 },
+	{   246, 246, 246 },
+	{   247, 247, 247 },
+	{   248, 248, 248 },
+	{   249, 249, 249 },
+	{   250, 250, 250 },
+	{   251, 251, 251 },
+	{   252, 252, 252 },
+	{   253, 253, 253 },
+	{   254, 254, 254 },
+	{   255, 255, 255 },
+} };
diff --git a/firmware/application/spectrum_color_lut.hpp b/firmware/application/spectrum_color_lut.hpp
index afec033ea7f6c05213f4c594dec430128a5a5582..74c554a81aa243cfd897c1a488f3fd92a24e7135 100644
--- a/firmware/application/spectrum_color_lut.hpp
+++ b/firmware/application/spectrum_color_lut.hpp
@@ -28,5 +28,6 @@
 
 extern const std::array<ui::Color, 256> spectrum_rgb2_lut;
 extern const std::array<ui::Color, 256> spectrum_rgb3_lut;
+extern const std::array<ui::Color, 256> spectrum_rgb4_lut;
 
 #endif/*__SPECTRUM_COLOR_LUT_H__*/
diff --git a/firmware/application/ui/ui_tv.cpp b/firmware/application/ui/ui_tv.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..59c99e073a573a32bd77409ce61dc8abc51f1ed1
--- /dev/null
+++ b/firmware/application/ui/ui_tv.cpp
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
+ * Copyright (C) 2020 Shao
+ *
+ * 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_tv.hpp"
+
+#include "spectrum_color_lut.hpp"
+
+#include "portapack.hpp"
+using namespace portapack;
+
+#include "baseband_api.hpp"
+
+#include "string_format.hpp"
+
+#include <cmath>
+#include <array>
+
+namespace ui {
+namespace tv {
+
+/* TimeScopeView******************************************************/
+
+TimeScopeView::TimeScopeView(
+	const Rect parent_rect
+) : View { parent_rect }
+{
+	set_focusable(true);
+	
+	add_children({
+		//&labels,
+		//&field_frequency,
+		&waveform
+	});
+	
+	/*field_frequency.on_change = [this](int32_t) {
+		set_dirty();
+	};
+	field_frequency.set_value(10);*/
+}
+
+void TimeScopeView::paint(Painter& painter) {
+	const auto r = screen_rect();
+
+	painter.fill_rectangle(r, Color::black());
+	
+	// Cursor
+	/*
+	const Rect r_cursor {
+		field_frequency.value() / (48000 / 240), r.bottom() - 32 - cursor_band_height,
+		1, cursor_band_height
+	};
+	painter.fill_rectangle(
+		r_cursor,
+		Color::red()
+	);*/
+}
+
+void TimeScopeView::on_audio_spectrum(const AudioSpectrum* spectrum) {
+	for (size_t i = 0; i < spectrum->db.size(); i++)
+		audio_spectrum[i] = ((int16_t)spectrum->db[i] - 127) * 256;
+	waveform.set_dirty();
+}
+
+/* TVView *********************************************************/
+
+void TVView::on_show() {
+	clear();
+
+	const auto screen_r = screen_rect();
+	display.scroll_set_area(screen_r.top(), screen_r.bottom());
+}
+
+void TVView::on_hide() {
+	/* TODO: Clear region to eliminate brief flash of content at un-shifted
+	 * position?
+	 */
+	display.scroll_disable();
+}
+
+void TVView::paint(Painter& painter) {
+	// Do nothing.
+	(void)painter;
+}
+
+void TVView::on_adjust_xcorr(uint8_t xcorr){
+	x_correction = xcorr;
+}
+
+void TVView::on_channel_spectrum(
+	const ChannelSpectrum& spectrum
+) {
+	//portapack has limitations
+        // 1.screen resolution (less than 240x320) 2.samples each call back (128 or 256)
+	// 3.memory size (for ui::Color, the buffer size
+	//spectrum.db[i] is 256 long
+	//768x625 ->128x625 ->128x312 -> 128x104
+	//originally @6MHz sample rate, the PAL should be 768x625
+        //I reduced sample rate to 2MHz(3 times less samples), then calculate mag (effectively decimate by 2)
+	//the resolution is now changed to 128x625. The total decimation factor is 6, which changes how many samples in a line
+	//However 625 is too large for the screen, also interlaced scanning is harder to realize in portapack than normal computer.
+        //So I decided to simply drop half of the lines, once y is larger than 625/2=312.5 or 312, I recognize it as a new frame.
+        //then the resolution is changed to 128x312
+        //128x312 is now able to put into a 240x320 screen, but the buffer for a whole frame is 128x312=39936, which is too large
+        //according to my test, I can only make a buffer with a length of 13312 of type ui::Color. which is 1/3 of what I wanted.
+        //So now the resolution is changed to 128x104, the height is shrinked to 1/3 of the original height.
+        //I was expecting to see 1/3 height of original video.
+
+        //Look how nice is that! I am now able to meet the requirements of 1 and 3 for portapack. Also the length of a line is 128
+        //Each call back gives me 256 samples which is exactly 2 lines. What a coincidence!
+
+	//After some experiment, I did some improvements.
+	//1.I found that instead of 1/3 of the frame is shown, I got 3 whole frames shrinked into one window.
+	//So I made the height twice simply by painting 2 identical lines in the place of original lines
+	//2.I found sometimes there is an horizontal offset, so I added x_correction to move the frame back to center manually
+	//3.I changed video_buffer's type, from ui::Color to uint_8, since I don't need 3 digit to represent a grey scale value.
+	//I was hoping that by doing this, I can have a longer buffer like 39936, then the frame will looks better vertically
+	//however this is useless until now.	
+
+	for(size_t i=0; i<256; i++) 
+	{
+		//video_buffer[i+count*256] = spectrum_rgb4_lut[spectrum.db[i]];
+		video_buffer_int[i+count*256] = 255 - spectrum.db[i];
+	}
+	count = count + 1;
+	if (count == 52 -1)
+	{
+		ui::Color line_buffer[128];
+		Coord line;
+		uint32_t bmp_px;
+
+		/*for (line = 0; line < 104; line++)
+		{
+			for (bmp_px = 0; bmp_px < 128; bmp_px++) 
+			{
+				//line_buffer[bmp_px] = video_buffer[bmp_px+line*128];
+				line_buffer[bmp_px] = spectrum_rgb4_lut[video_buffer_int[bmp_px+line*128 + x_correction]];
+			}
+
+			display.render_line({ 0, line + 100 }, 128, line_buffer);
+		}*/
+		for (line = 0; line < 208; line=line+2)
+		{
+			for (bmp_px = 0; bmp_px < 128; bmp_px++) 
+			{
+				//line_buffer[bmp_px] = video_buffer[bmp_px+line*128];
+				line_buffer[bmp_px] = spectrum_rgb4_lut[video_buffer_int[bmp_px+line/2*128 + x_correction]];
+			}
+
+			display.render_line({ 0, line + 100 }, 128, line_buffer);
+			display.render_line({ 0, line + 101 }, 128, line_buffer);
+		}
+		count = 0;
+	}
+
+}
+
+void TVView::clear() {
+	display.fill_rectangle(
+		screen_rect(),
+		Color::black()
+	);
+}
+
+/* TVWidget *******************************************************/
+
+TVWidget::TVWidget(const bool cursor) {
+	add_children({
+		&tv_view,
+		&field_xcorr
+	});
+	field_xcorr.set_value(10);
+}
+
+void TVWidget::on_show() {
+	baseband::spectrum_streaming_start();
+}
+
+void TVWidget::on_hide() {
+	baseband::spectrum_streaming_stop();
+}
+
+void TVWidget::show_audio_spectrum_view(const bool show) {
+	if ((audio_spectrum_view && show) || (!audio_spectrum_view && !show)) return;
+	
+	if (show) {
+		audio_spectrum_view = std::make_unique<TimeScopeView>(audio_spectrum_view_rect);
+		add_child(audio_spectrum_view.get());
+		update_widgets_rect();
+	} else {
+		audio_spectrum_update = false;
+		remove_child(audio_spectrum_view.get());
+		audio_spectrum_view.reset();
+		update_widgets_rect();
+	}
+}
+
+void TVWidget::update_widgets_rect() {
+	if (audio_spectrum_view) {
+		tv_view.set_parent_rect(tv_reduced_rect);
+	} else {
+		tv_view.set_parent_rect(tv_normal_rect);
+	}
+	tv_view.on_show();
+}
+
+void TVWidget::set_parent_rect(const Rect new_parent_rect) {
+	View::set_parent_rect(new_parent_rect);
+	
+	tv_normal_rect = { 0, scale_height, new_parent_rect.width(), new_parent_rect.height() - scale_height};
+	tv_reduced_rect = { 0, audio_spectrum_height + scale_height, new_parent_rect.width(), new_parent_rect.height() - scale_height - audio_spectrum_height };
+	
+	update_widgets_rect();
+}
+
+void TVWidget::paint(Painter& painter) {
+	// TODO:
+	(void)painter;
+}
+
+void TVWidget::on_channel_spectrum(const ChannelSpectrum& spectrum) {
+	tv_view.on_channel_spectrum(spectrum);
+	tv_view.on_adjust_xcorr(field_xcorr.value());
+	sampling_rate = spectrum.sampling_rate;
+	
+}
+
+void TVWidget::on_audio_spectrum() {
+	audio_spectrum_view->on_audio_spectrum(audio_spectrum_data);
+}
+
+} /* namespace tv */
+} /* namespace ui */
diff --git a/firmware/application/ui/ui_tv.hpp b/firmware/application/ui/ui_tv.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..1fb7887a513f28323707624495fa1b5db7df26ea
--- /dev/null
+++ b/firmware/application/ui/ui_tv.hpp
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
+ * Copyright (C) 2020 Shao
+ *
+ * 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_TV_H__
+#define __UI_TV_H__
+
+#include "ui.hpp"
+#include "ui_widget.hpp"
+
+#include "event_m0.hpp"
+
+#include "message.hpp"
+
+#include <cstdint>
+#include <cstddef>
+
+namespace ui {
+namespace tv {
+
+class TimeScopeView : public View {
+public:
+	TimeScopeView(const Rect parent_rect);
+	
+	void paint(Painter& painter) override;
+	
+	void on_audio_spectrum(const AudioSpectrum* spectrum);
+	
+private:
+	static constexpr int cursor_band_height = 4;
+	
+	int16_t audio_spectrum[128] { 0 };
+	
+	/*Labels labels {
+		{ { 6 * 8, 0 * 16 }, "Hz", Color::light_grey() }
+	};*/
+	/*
+	NumberField field_frequency {
+		{ 0 * 8, 0 * 16 },
+		5,
+		{ 0, 48000 },
+		48000 / 240,
+		' '
+	};*/
+	
+	Waveform waveform {
+		{ 0, 1 * 16 + cursor_band_height, 30 * 8, 2 * 16 },
+		audio_spectrum,
+		128,
+		0,
+		false,
+		Color::white()
+	};
+};
+
+class TVView : public Widget {
+public:
+	void on_show() override;
+	void on_hide() override;
+
+	void paint(Painter& painter) override;
+	void on_channel_spectrum(const ChannelSpectrum& spectrum);
+	void on_adjust_xcorr(uint8_t xcorr);
+	//ui::Color video_buffer[13312];
+        uint8_t video_buffer_int[13312+128] { 0 }; //128 is for the over length caused by x_correction
+	uint32_t count=0;
+        uint8_t x_correction=0;
+private:
+	void clear();
+	
+};
+
+class TVWidget : public View {
+public:
+	std::function<void(int32_t offset)> on_select { };
+	
+	TVWidget(const bool cursor = false);
+
+	TVWidget(const TVWidget&) = delete;
+	TVWidget(TVWidget&&) = delete;
+	TVWidget& operator=(const TVWidget&) = delete;
+	TVWidget& operator=(TVWidget&&) = delete;
+
+	void on_show() override;
+	void on_hide() override;
+
+	void set_parent_rect(const Rect new_parent_rect) override;
+	
+	void show_audio_spectrum_view(const bool show);
+
+	void paint(Painter& painter) override;
+	NumberField field_xcorr {
+		{ 0 * 8, 0 * 16 },
+		5,
+		{ 0, 128 },
+		1,
+		' '
+	};
+
+private:
+	void update_widgets_rect();
+	
+	const Rect audio_spectrum_view_rect { 0 * 8, 0 * 16, 30 * 8, 2 * 16 + 20 };
+	static constexpr Dim audio_spectrum_height = 16 * 2 + 20;
+	static constexpr Dim scale_height = 20;
+	
+	TVView tv_view { };
+
+	ChannelSpectrumFIFO* channel_fifo { nullptr };
+	AudioSpectrum* audio_spectrum_data { nullptr };
+	bool audio_spectrum_update { false };
+	
+	std::unique_ptr<TimeScopeView> audio_spectrum_view { };
+	
+	int sampling_rate { 0 };
+	int32_t cursor_position { 0 };
+	ui::Rect tv_normal_rect { };
+	ui::Rect tv_reduced_rect { };
+
+	MessageHandlerRegistration message_handler_channel_spectrum_config {
+		Message::ID::ChannelSpectrumConfig,
+		[this](const Message* const p) {
+			const auto message = *reinterpret_cast<const ChannelSpectrumConfigMessage*>(p);
+			this->channel_fifo = message.fifo;
+		}
+	};
+	MessageHandlerRegistration message_handler_audio_spectrum {
+		Message::ID::AudioSpectrum,
+		[this](const Message* const p) {
+			const auto message = *reinterpret_cast<const AudioSpectrumMessage*>(p);
+			this->audio_spectrum_data = message.data;
+			this->audio_spectrum_update = true;
+		}
+	};
+	MessageHandlerRegistration message_handler_frame_sync {
+		Message::ID::DisplayFrameSync,
+		[this](const Message* const) {
+			if( this->channel_fifo ) {
+				ChannelSpectrum channel_spectrum;
+				while( channel_fifo->out(channel_spectrum) ) {
+					this->on_channel_spectrum(channel_spectrum);
+				}
+			}
+			if (this->audio_spectrum_update) {
+				this->audio_spectrum_update = false;
+				this->on_audio_spectrum();
+			}
+		}
+	};
+
+	void on_channel_spectrum(const ChannelSpectrum& spectrum);
+	void on_audio_spectrum();
+};
+
+} /* namespace tv */
+} /* namespace ui */
+
+#endif/*__UI_TV_H__*/
diff --git a/firmware/application/ui_navigation.cpp b/firmware/application/ui_navigation.cpp
index 01be7b9f91f02e84646993befb4b5abaeebe3267..7eea7a76349b67d7021b6b321407de1939ebb1a3 100644
--- a/firmware/application/ui_navigation.cpp
+++ b/firmware/application/ui_navigation.cpp
@@ -34,6 +34,8 @@
 #include "ui_adsb_rx.hpp"
 #include "ui_adsb_tx.hpp"
 #include "ui_afsk_rx.hpp"
+#include "ui_btle_rx.hpp"
+#include "ui_nrf_rx.hpp"
 #include "ui_aprs_tx.hpp"
 #include "ui_bht_tx.hpp"
 #include "ui_coasterp.hpp"
@@ -68,6 +70,7 @@
 #include "acars_app.hpp"
 #include "ais_app.hpp"
 #include "analog_audio_app.hpp"
+#include "analog_tv_app.hpp"
 #include "capture_app.hpp"
 #include "ert_app.hpp"
 #include "lge_app.hpp"
@@ -348,7 +351,10 @@ ReceiversMenuView::ReceiversMenuView(NavigationView& nav) {
 		{ "ACARS", 		ui::Color::yellow(),	&bitmap_icon_adsb,		[&nav](){ nav.push<ACARSAppView>(); }, },
 		{ "AIS Boats",	ui::Color::green(),		&bitmap_icon_ais,		[&nav](){ nav.push<AISAppView>(); } },
 		{ "AFSK", 		ui::Color::yellow(),	&bitmap_icon_receivers,	[&nav](){ nav.push<AFSKRxView>(); } },
+		{ "BTLE",		ui::Color::yellow(),	&bitmap_icon_btle,		[&nav](){ nav.push<BTLERxView>(); } },
+		{ "NRF", 		ui::Color::yellow(),	&bitmap_icon_nrf,		[&nav](){ nav.push<NRFRxView>(); } }, 
 		{ "Audio", 		ui::Color::green(),		&bitmap_icon_speaker,	[&nav](){ nav.push<AnalogAudioView>(); } },
+		{ "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>(); } },
diff --git a/firmware/baseband/CMakeLists.txt b/firmware/baseband/CMakeLists.txt
index c9bf0c4a9170968f7010da2bb872ed8feffabb89..c4aafa564eebecdb2507d8521228d1c21e0b81bf 100644
--- a/firmware/baseband/CMakeLists.txt
+++ b/firmware/baseband/CMakeLists.txt
@@ -116,6 +116,7 @@ set(CPPSRC
 	dsp_goertzel.cpp
 	matched_filter.cpp
 	spectrum_collector.cpp
+	tv_collector.cpp
 	stream_input.cpp
 	stream_output.cpp
 	dsp_squelch.cpp
@@ -311,7 +312,19 @@ set(MODE_CPPSRC
 	proc_afskrx.cpp
 )
 DeclareTargets(PAFR afskrx)
+### NRF RX
 
+set(MODE_CPPSRC
+	proc_nrfrx.cpp
+)
+DeclareTargets(PNRR nrfrx)
+
+### BTLE RX
+
+set(MODE_CPPSRC
+	proc_btlerx.cpp
+)
+DeclareTargets(PBTR btlerx)
 ### AIS
 
 set(MODE_CPPSRC
@@ -326,6 +339,13 @@ set(MODE_CPPSRC
 )
 DeclareTargets(PAMA am_audio)
 
+### AM TV
+
+set(MODE_CPPSRC
+	proc_am_tv.cpp
+)
+DeclareTargets(PAMT am_tv)
+
 ### Audio transmit
 
 set(MODE_CPPSRC
diff --git a/firmware/baseband/proc_am_tv.cpp b/firmware/baseband/proc_am_tv.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..4c2c7e9dace3048df7694d2ac29f047dead7fbf7
--- /dev/null
+++ b/firmware/baseband/proc_am_tv.cpp
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
+ * Copyright (C) 2016 Furrtek
+ * Copyright (C) 2020 Shao
+ *
+ * This file is part of PortaPack.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; see the file COPYING.  If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#include "proc_am_tv.hpp"
+
+#include "portapack_shared_memory.hpp"
+#include "dsp_fft.hpp"
+#include "event_m4.hpp"
+
+#include <cstdint>
+
+void WidebandFMAudio::execute(const buffer_c8_t& buffer) {
+	if( !configured ) {
+		return;
+	}
+	
+	std::fill(spectrum.begin(), spectrum.end(), 0);
+
+	for(size_t i=0; i<spectrum.size(); i++) {
+		spectrum[i] += buffer.p[i];
+	}
+
+	const buffer_c16_t buffer_c16 {spectrum.data(),spectrum.size(),buffer.sampling_rate};
+	channel_spectrum.feed(buffer_c16);
+
+        int8_t re, im;
+	int8_t mag;
+
+        for (size_t i = 0; i < 128; i++) 
+	{
+		re = buffer.p[i].real();
+		im = buffer.p[i].imag();
+		mag = __builtin_sqrtf((re * re) + (im * im)) ;
+		const unsigned int v =  re + 127.0f; //timescope
+		audio_spectrum.db[i] = std::max(0U, std::min(255U, v));
+	}
+	AudioSpectrumMessage message { &audio_spectrum };
+	shared_memory.application_queue.push(message);
+}
+
+void WidebandFMAudio::on_message(const Message* const message) {
+	switch(message->id) {
+	case Message::ID::UpdateSpectrum:
+	case Message::ID::SpectrumStreamingConfig:
+		channel_spectrum.on_message(message);
+		break;
+
+	case Message::ID::WFMConfigure:
+		configure(*reinterpret_cast<const WFMConfigureMessage*>(message));
+		break;
+		
+	default:
+		break;
+	}
+}
+
+void WidebandFMAudio::configure(const WFMConfigureMessage& message) {
+	configured = true;
+}
+
+
+int main() {
+	EventDispatcher event_dispatcher { std::make_unique<WidebandFMAudio>() };
+	event_dispatcher.run();
+	return 0;
+}
diff --git a/firmware/baseband/proc_am_tv.hpp b/firmware/baseband/proc_am_tv.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..356877b0553ce1e1e62c20e6e773e743e78a25a1
--- /dev/null
+++ b/firmware/baseband/proc_am_tv.hpp
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
+ * Copyright (C) 2016 Furrtek
+ * Copyright (C) 2020 Shao
+ *
+ * This file is part of PortaPack.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; see the file COPYING.  If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#ifndef __PROC_AM_TV_H__
+#define __PROC_AM_TV_H__
+
+#include "baseband_processor.hpp"
+#include "baseband_thread.hpp"
+#include "rssi_thread.hpp"
+
+#include "dsp_types.hpp"
+#include "dsp_decimate.hpp"
+#include "dsp_demodulate.hpp"
+#include "block_decimator.hpp"
+
+#include "tv_collector.hpp"
+
+class WidebandFMAudio : public BasebandProcessor {
+public:
+	void execute(const buffer_c8_t& buffer) override;
+
+	void on_message(const Message* const message) override;
+
+private:
+	static constexpr size_t baseband_fs = 2000000;
+
+	BasebandThread baseband_thread { baseband_fs, this, NORMALPRIO + 20, baseband::Direction::Receive };
+	RSSIThread rssi_thread { NORMALPRIO + 10 };
+
+	std::array<complex16_t, 512> dst { };
+	const buffer_c16_t dst_buffer {
+		dst.data(),
+		dst.size()
+	};
+
+	AudioSpectrum audio_spectrum { };
+	TvCollector channel_spectrum { };
+        std::array<complex16_t, 256> spectrum { };
+
+	bool configured { false };
+	void configure(const WFMConfigureMessage& message);
+
+};
+
+#endif/*__PROC_AM_TV_H__*/
diff --git a/firmware/baseband/proc_btlerx.cpp b/firmware/baseband/proc_btlerx.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..f4e15571b144e664c6388cdf845904f28c912d58
--- /dev/null
+++ b/firmware/baseband/proc_btlerx.cpp
@@ -0,0 +1,339 @@
+/*
+ * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
+ * Copyright (C) 2016 Furrtek
+ * Copyright (C) 2020 Shao
+ *
+ * This file is part of PortaPack.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; see the file COPYING.  If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#include "proc_btlerx.hpp"
+#include "portapack_shared_memory.hpp"
+
+#include "event_m4.hpp"
+
+void BTLERxProcessor::execute(const buffer_c8_t& buffer) {
+	if (!configured) return;
+	
+	// FM demodulation
+
+	/*const auto decim_0_out = decim_0.execute(buffer, dst_buffer);
+	const auto channel = decim_1.execute(decim_0_out, dst_buffer);
+
+	feed_channel_stats(channel);
+	
+	auto audio_oversampled = demod.execute(channel, work_audio_buffer);*/
+
+	const auto decim_0_out = decim_0.execute(buffer, dst_buffer);
+	feed_channel_stats(decim_0_out);
+	
+	auto audio_oversampled = demod.execute(decim_0_out, work_audio_buffer);
+
+	/*std::fill(spectrum.begin(), spectrum.end(), 0);
+	for(size_t i=0; i<spectrum.size(); i++) {
+		spectrum[i] += buffer.p[i];
+	}
+	const buffer_c16_t buffer_c16 {spectrum.data(),spectrum.size(),buffer.sampling_rate};
+	feed_channel_stats(buffer_c16);
+	
+	auto audio_oversampled = demod.execute(buffer_c16, work_audio_buffer);*/
+	// Audio signal processing
+	for (size_t c = 0; c < audio_oversampled.count; c++) {
+		int result;
+
+		/*const int32_t sample_int = audio_oversampled.p[c] * 32768.0f;
+		int32_t current_sample = __SSAT(sample_int, 16);
+		current_sample /= 128;*/
+                
+		int32_t current_sample = audio_oversampled.p[c]; //if I directly use this, some results can pass crc but not correct.
+                rb_head++;
+	        rb_head=(rb_head)%RB_SIZE;
+
+                rb_buf[rb_head] = current_sample;
+
+		skipSamples = skipSamples - 1;
+
+
+		if (skipSamples<1)
+		{
+				
+
+			int32_t threshold_tmp=0;
+			for (int c=0;c<8;c++)
+			{
+				threshold_tmp = threshold_tmp + (int32_t)rb_buf[(rb_head+c)%RB_SIZE];
+			}
+			g_threshold = (int32_t)threshold_tmp/8;
+
+				
+			int transitions=0;
+			if (rb_buf[(rb_head+9)%RB_SIZE] > g_threshold)
+			{
+				for (int c=0;c<8;c++)
+				{
+					if (rb_buf[(rb_head + c)%RB_SIZE] > rb_buf[(rb_head + c + 1)%RB_SIZE])
+					transitions = transitions + 1;
+				}
+			}
+			else
+			{
+				for (int c=0;c<8;c++)
+				{
+					if (rb_buf[(rb_head + c)%RB_SIZE] < rb_buf[(rb_head + c + 1)%RB_SIZE])
+					transitions = transitions + 1;
+				}
+			}
+			
+			bool packet_detected=false;
+			//if ( transitions==4 && abs(g_threshold)<15500)
+			if ( transitions==4)
+			{
+
+				
+				uint8_t packet_data[500];
+				int packet_length;
+				uint32_t packet_crc;
+				uint32_t calced_crc;
+				uint64_t packet_addr_l;
+				uint32_t result;
+				uint8_t crc[3];
+				uint8_t packet_header_arr[2];
+
+				packet_addr_l=0;
+				for (int i=0;i<4;i++) 
+				{                   
+				    bool current_bit;
+				    uint8_t byte=0;
+				    for (int c=0;c<8;c++)
+				    {
+				        if (rb_buf[(rb_head + (i+1)*8 + c)%RB_SIZE] > g_threshold)
+				            current_bit = true;
+				        else
+				            current_bit = false;
+				        byte |= current_bit << (7-c);
+				    }
+				    uint8_t byte_temp = (uint8_t) (((byte * 0x0802LU & 0x22110LU) | (byte * 0x8020LU & 0x88440LU)) * 0x10101LU >> 16);
+				    packet_addr_l|=((uint64_t)byte_temp)<<(8*i);
+				}
+
+				channel_number = 38;
+
+				
+				for (int t=0;t<2;t++)
+				{
+				    bool current_bit;
+				    uint8_t byte=0;
+				    for (int c=0;c<8;c++)
+				    {
+				        if (rb_buf[(rb_head + 5*8+t*8 + c)%RB_SIZE] > g_threshold)
+				            current_bit = true;
+				        else
+				            current_bit = false;
+				        byte |= current_bit << (7-c);
+				    }
+
+				    packet_header_arr[t] = byte;
+				}
+
+				
+
+				uint8_t byte_temp2 = (uint8_t) (((channel_number * 0x0802LU & 0x22110LU) | (channel_number * 0x8020LU & 0x88440LU)) * 0x10101LU >> 16);
+				uint8_t lfsr_1 = byte_temp2 | 2;
+				int header_length = 2;
+				int header_counter = 0;
+				while(header_length--)
+				{
+				    for(uint8_t i = 0x80; i; i >>= 1)
+				    {
+				        if(lfsr_1 & 0x80)
+				        {
+				            lfsr_1 ^= 0x11;
+				            (packet_header_arr[header_counter]) ^= i;
+				        }
+				        lfsr_1 <<= 1;
+				    }
+				    header_counter = header_counter + 1;
+				}
+
+				
+
+				if (packet_addr_l==0x8E89BED6)
+				{  
+
+				    uint8_t byte_temp3 = (uint8_t) (((packet_header_arr[1] * 0x0802LU & 0x22110LU) | (packet_header_arr[1] * 0x8020LU & 0x88440LU)) * 0x10101LU >> 16);
+				    packet_length=byte_temp3&0x3F;
+				
+				} 
+				else 
+				{
+				    packet_length=0;
+				}
+
+				for (int t=0;t<packet_length+2+3;t++)
+				{
+				    bool current_bit;
+				    uint8_t byte=0;
+				    for (int c=0;c<8;c++)
+				    {
+				        if (rb_buf[(rb_head + 5*8+t*8 + c)%RB_SIZE] > g_threshold)
+				            current_bit = true;
+				        else
+				            current_bit = false;
+				        byte |= current_bit << (7-c);
+				    }
+
+				    packet_data[t] = byte;
+				}
+
+				
+
+				uint8_t byte_temp4 = (uint8_t) (((channel_number * 0x0802LU & 0x22110LU) | (channel_number * 0x8020LU & 0x88440LU)) * 0x10101LU >> 16);
+				uint8_t lfsr_2 = byte_temp4 | 2;
+				int pdu_crc_length = packet_length+2+3;
+				int pdu_crc_counter = 0;
+				while(pdu_crc_length--)
+				{
+				    for(uint8_t i = 0x80; i; i >>= 1)
+				    {
+				        if(lfsr_2 & 0x80)
+				        {
+				            lfsr_2 ^= 0x11;
+				            (packet_data[pdu_crc_counter]) ^= i;
+				        }
+				        lfsr_2 <<= 1;
+				    }
+				    pdu_crc_counter = pdu_crc_counter + 1;
+				}
+
+				
+
+				if (packet_addr_l==0x8E89BED6)
+				{  
+				    crc[0]=crc[1]=crc[2]=0x55;
+				}
+				else 
+				{
+				    crc[0]=crc[1]=crc[2]=0;
+				}
+
+				uint8_t v, t, d, crc_length;
+				uint32_t crc_result=0;
+				crc_length = packet_length + 2;
+				int counter = 0;
+				while(crc_length--)
+				{
+				    uint8_t byte_temp5 = (uint8_t) (((packet_data[counter] * 0x0802LU & 0x22110LU) | (packet_data[counter] * 0x8020LU & 0x88440LU)) * 0x10101LU >> 16);
+				    d = byte_temp5;
+				    for(v = 0; v < 8; v++, d >>= 1)
+				    {
+				        t = crc[0] >> 7;
+				        crc[0] <<= 1;
+				        if(crc[1] & 0x80) crc[0] |= 1;
+				        crc[1] <<= 1;
+				        if(crc[2] & 0x80) crc[1] |= 1;
+				        crc[2] <<= 1;
+				        if(t != (d & 1))
+				        {
+				            crc[2] ^= 0x5B;
+				            crc[1] ^= 0x06;
+				        }
+				    }
+				    counter = counter + 1;
+				}
+				for (v=0;v<3;v++) crc_result=(crc_result<<8)|crc[v];
+				calced_crc = crc_result;
+
+				packet_crc=0;
+				for (int c=0;c<3;c++) packet_crc=(packet_crc<<8)|packet_data[packet_length+2+c];
+
+				if (packet_addr_l==0x8E89BED6)
+				//if (packet_crc==calced_crc)
+				{
+				    uint8_t mac_data[6];
+				    int counter = 0;
+				    for (int i = 7; i >= 2; i--) 
+				    {
+				        uint8_t byte_temp6 = (uint8_t) (((packet_data[i] * 0x0802LU & 0x22110LU) | (packet_data[i] * 0x8020LU & 0x88440LU)) * 0x10101LU >> 16);
+					//result = byte_temp6;
+					mac_data[counter] = byte_temp6;
+					counter = counter + 1;
+				    }
+
+				    data_message.is_data = false;
+			            data_message.value = 'A';
+				    shared_memory.application_queue.push(data_message);
+
+				    data_message.is_data = true;
+			            data_message.value = mac_data[0];
+				    shared_memory.application_queue.push(data_message);
+
+				    data_message.is_data = true;
+			            data_message.value = mac_data[1];
+				    shared_memory.application_queue.push(data_message);
+
+				    data_message.is_data = true;
+			            data_message.value = mac_data[2];
+				    shared_memory.application_queue.push(data_message);
+
+				    data_message.is_data = true;
+			            data_message.value = mac_data[3];
+				    shared_memory.application_queue.push(data_message);
+
+				    data_message.is_data = true;
+			            data_message.value = mac_data[4];
+				    shared_memory.application_queue.push(data_message);
+
+				    data_message.is_data = true;
+			            data_message.value = mac_data[5];
+				    shared_memory.application_queue.push(data_message);
+
+				    data_message.is_data = false;
+			            data_message.value = 'B';
+				    shared_memory.application_queue.push(data_message);
+
+				    packet_detected = true;
+				}
+				else
+				    packet_detected = false;
+			}
+
+			if (packet_detected) 
+			{
+				skipSamples=20;
+			}
+		}
+	}
+}
+
+void BTLERxProcessor::on_message(const Message* const message) {
+	if (message->id == Message::ID::BTLERxConfigure)
+		configure(*reinterpret_cast<const BTLERxConfigureMessage*>(message));
+}
+
+void BTLERxProcessor::configure(const BTLERxConfigureMessage& message) {	
+	decim_0.configure(taps_200k_wfm_decim_0.taps, 33554432);
+	decim_1.configure(taps_200k_wfm_decim_1.taps, 131072);
+	demod.configure(audio_fs, 5000);
+
+	configured = true;
+}
+
+int main() {
+	EventDispatcher event_dispatcher { std::make_unique<BTLERxProcessor>() };
+	event_dispatcher.run();
+	return 0;
+}
diff --git a/firmware/baseband/proc_btlerx.hpp b/firmware/baseband/proc_btlerx.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..2bb48872b86885131ad4ef79b5f470924b99dfc4
--- /dev/null
+++ b/firmware/baseband/proc_btlerx.hpp
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
+ * Copyright (C) 2016 Furrtek
+ * Copyright (C) 2020 Shao
+ *
+ * This file is part of PortaPack.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; see the file COPYING.  If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#ifndef __PROC_BTLERX_H__
+#define __PROC_BTLERX_H__
+
+#include "baseband_processor.hpp"
+#include "baseband_thread.hpp"
+#include "rssi_thread.hpp"
+
+#include "dsp_decimate.hpp"
+#include "dsp_demodulate.hpp"
+
+#include "audio_output.hpp"
+
+#include "fifo.hpp"
+#include "message.hpp"
+
+class BTLERxProcessor : public BasebandProcessor {
+public:
+	void execute(const buffer_c8_t& buffer) override;
+
+	void on_message(const Message* const message) override;
+	
+private:
+	static constexpr size_t baseband_fs = 4000000;
+	static constexpr size_t audio_fs = baseband_fs / 8 / 8 / 2;
+	
+	BasebandThread baseband_thread { baseband_fs, this, NORMALPRIO + 20, baseband::Direction::Receive };
+	RSSIThread rssi_thread { NORMALPRIO + 10 };
+	
+	std::array<complex16_t, 512> dst { };
+	const buffer_c16_t dst_buffer {
+		dst.data(),
+		dst.size()
+	};
+
+        std::array<complex16_t, 512> spectrum { };
+	const buffer_c16_t spectrum_buffer {
+		spectrum.data(),
+		spectrum.size()
+	};
+
+	const buffer_s16_t work_audio_buffer {
+		(int16_t*)dst.data(),
+		sizeof(dst) / sizeof(int16_t)
+	};
+
+
+	// Array size ok down to 375 bauds (24000 / 375)
+	std::array<int32_t, 64> delay_line { 0 };
+	std::array<int16_t, 1000> rb_buf { 0 };
+
+	/*dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0 { };
+	dsp::decimate::FIRC16xR16x32Decim8 decim_1 { };
+	dsp::decimate::FIRAndDecimateComplex channel_filter { };*/
+	dsp::decimate::FIRC8xR16x24FS4Decim4 decim_0 { };
+	dsp::decimate::FIRC16xR16x16Decim2 decim_1 { };
+	
+	dsp::demodulate::FM demod { };
+	int rb_head {-1};
+	int32_t g_threshold {0};  
+	uint8_t channel_number {38};
+	int skipSamples {1000};
+	int RB_SIZE {1000};
+
+	bool configured { false };
+
+	
+	void configure(const BTLERxConfigureMessage& message);
+	
+	AFSKDataMessage data_message { false, 0 };
+};
+
+#endif/*__PROC_BTLERX_H__*/
diff --git a/firmware/baseband/proc_nrfrx.cpp b/firmware/baseband/proc_nrfrx.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..277bc7f396852d2061fbdca9c5e0dd1c770f675e
--- /dev/null
+++ b/firmware/baseband/proc_nrfrx.cpp
@@ -0,0 +1,286 @@
+/*
+ * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
+ * Copyright (C) 2016 Furrtek
+ * Copyright (C) 2020 Shao
+ *
+ * This file is part of PortaPack.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; see the file COPYING.  If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#include "proc_nrfrx.hpp"
+#include "portapack_shared_memory.hpp"
+
+#include "event_m4.hpp"
+
+void NRFRxProcessor::execute(const buffer_c8_t& buffer) {
+	if (!configured) return;
+	
+	// FM demodulation
+
+	const auto decim_0_out = decim_0.execute(buffer, dst_buffer);
+	feed_channel_stats(decim_0_out);
+	
+	auto audio_oversampled = demod.execute(decim_0_out, work_audio_buffer);
+	// Audio signal processing
+	for (size_t c = 0; c < audio_oversampled.count; c++) {
+		int result;
+                int g_srate = 4; //4 for 250KPS
+		//int g_srate = 1; //1 for 1MPS, not working yet
+		int32_t current_sample = audio_oversampled.p[c]; //if I directly use this, some results can pass crc but not correct.
+                rb_head++;
+	        rb_head=(rb_head)%RB_SIZE;
+
+                rb_buf[rb_head] = current_sample;
+
+		skipSamples = skipSamples - 1;
+
+
+		if (skipSamples<1)
+		{
+				
+
+			int32_t threshold_tmp=0;
+			for (int c=0;c<8*g_srate;c++)
+			{
+				threshold_tmp = threshold_tmp + (int32_t)rb_buf[(rb_head+c)%RB_SIZE];
+			}
+
+			g_threshold = (int32_t)threshold_tmp/(8*g_srate);
+	
+			int transitions=0;
+			if (rb_buf[(rb_head + 9*g_srate)%RB_SIZE] > g_threshold)
+			{
+				for (int c=0;c<8;c++)
+				{
+					if (rb_buf[(rb_head + c*g_srate)%RB_SIZE] > rb_buf[(rb_head + (c+1)*g_srate)%RB_SIZE])
+						transitions = transitions + 1;
+				}
+			}
+			else
+			{
+				for (int c=0;c<8;c++)
+				{
+					if (rb_buf[(rb_head + c*g_srate)%RB_SIZE] < rb_buf[(rb_head + (c+1)*g_srate)%RB_SIZE])
+						transitions = transitions + 1;
+				}
+			}
+			
+			bool packet_detected=false;
+			//if ( transitions==4 && abs(g_threshold)<15500)
+			if ( transitions==4 && abs(g_threshold)<15500)
+			{
+				int packet_length = 0;
+				uint8_t tmp_buf[10];
+				uint8_t packet_data[500];
+				uint8_t packet_packed[50];
+				uint16_t pcf;
+				uint32_t packet_crc;
+				uint32_t calced_crc;
+				uint64_t packet_addr_l;
+
+				/* extract address */
+				packet_addr_l=0;
+
+				for (int t=0;t<5;t++)
+				{
+					bool current_bit;
+					uint8_t byte=0;
+					for (int c=0;c<8;c++) 
+					{
+						if (rb_buf[(rb_head+(1*8+t*8+c)*g_srate)%RB_SIZE] > g_threshold)
+							current_bit = true;
+						else
+							current_bit = false;
+						byte |= current_bit << (7-c);
+					}
+					tmp_buf[t]=byte;
+				}
+
+				for (int t=0;t<5;t++) packet_addr_l|=((uint64_t)tmp_buf[t])<<(4-t)*8;
+
+				//channel_number = 26;
+
+				
+				/* extract pcf */
+				for (int t=0;t<2;t++)
+				{
+					bool current_bit;
+					uint8_t byte=0;
+					for (int c=0;c<8;c++) 
+					{
+						if (rb_buf[(rb_head+(6*8+t*8+c)*g_srate)%RB_SIZE] > g_threshold)
+							current_bit = true;
+						else
+							current_bit = false;
+						byte |= current_bit << (7-c);
+					}
+					tmp_buf[t]=byte;
+				}
+
+				pcf = tmp_buf[0]<<8 | tmp_buf[1];
+				pcf >>=7;
+
+				/* extract packet length, avoid excessive length packets */
+				if(packet_length == 0)
+					packet_length=(int)pcf>>3;
+				if (packet_length>32) 
+					packet_detected = false;
+
+				/* extract data */
+				for (int t=0;t<packet_length;t++)
+				{
+					bool current_bit;
+					uint8_t byte=0;
+					for (int c=0;c<8;c++) 
+					{
+						if (rb_buf[(rb_head+(6*8+9+t*8+c)*g_srate)%RB_SIZE] > g_threshold)
+							current_bit = true;
+						else
+							current_bit = false;
+						byte |= current_bit << (7-c);
+					}
+					packet_data[t]=byte;
+				}
+
+				/* Prepare packed bit stream for CRC calculation */
+				uint64_t packet_header=packet_addr_l;
+				packet_header<<=9;
+				packet_header|=pcf;
+				for (int c=0;c<7;c++){
+					packet_packed[c]=(packet_header>>((6-c)*8))&0xFF;
+				}
+
+				for (int c=0;c<packet_length;c++){
+					packet_packed[c+7]=packet_data[c];
+				}
+
+				/* calculate packet crc */
+				const uint8_t* data = packet_packed; 
+				size_t data_len =  7+packet_length;
+				bool bit;
+				uint8_t cc;
+				uint_fast16_t crc=0x3C18;
+				while (data_len--) {
+				cc = *data++;
+				for (uint8_t i = 0x80; i > 0; i >>= 1) 
+				{
+					bit = crc & 0x8000;
+					if (cc & i) 
+					{
+						bit = !bit;
+					}
+					crc <<= 1;
+					if (bit) 
+					{
+						crc ^= 0x1021;
+					}
+				}
+					crc &= 0xffff;
+				}
+				calced_crc = (uint16_t)(crc & 0xffff);
+
+				/* extract crc */
+				for (int t=0;t<2;t++)
+				{
+					bool current_bit;
+					uint8_t byte=0;
+					for (int c=0;c<8;c++) 
+					{
+						if (rb_buf[(rb_head+((6+packet_length)*8+9+t*8+c)*g_srate)%RB_SIZE] > g_threshold)
+							current_bit = true;
+						else
+							current_bit = false;
+						byte |= current_bit << (7-c);
+					}
+					tmp_buf[t]=byte;
+				}
+				packet_crc = tmp_buf[0]<<8 | tmp_buf[1];
+
+				/* NRF24L01+ packet found, dump information */
+				//if (packet_addr_l==0xE7E7E7E7)
+				if (packet_crc==calced_crc)
+				{
+					data_message.is_data = false;
+					data_message.value = 'A';
+					shared_memory.application_queue.push(data_message);
+
+					data_message.is_data = true;
+					data_message.value = packet_addr_l;
+					shared_memory.application_queue.push(data_message);
+
+					for (int c=0;c<7;c++)
+					{
+						data_message.is_data = true;
+						data_message.value = packet_addr_l >> 8;
+						shared_memory.application_queue.push(data_message);
+					}
+					/*data_message.is_data = true;
+					data_message.value = packet_addr_l;
+					shared_memory.application_queue.push(data_message);
+
+					data_message.is_data = true;
+					data_message.value = packet_addr_l >> 8;
+					shared_memory.application_queue.push(data_message);*/
+
+					data_message.is_data = false;
+					data_message.value = 'B';
+					shared_memory.application_queue.push(data_message);
+
+					for (int c=0;c<packet_length;c++)
+					{
+						data_message.is_data = true;
+						data_message.value = packet_data[c];
+						shared_memory.application_queue.push(data_message);
+					}
+
+					data_message.is_data = false;
+					data_message.value = 'C';
+					shared_memory.application_queue.push(data_message);
+
+
+					packet_detected = true;
+				}
+				else
+					packet_detected = false;
+			}
+
+			if (packet_detected) 
+			{
+				skipSamples=20;
+			}
+		}
+	}
+}
+
+void NRFRxProcessor::on_message(const Message* const message) {
+	if (message->id == Message::ID::NRFRxConfigure)
+		configure(*reinterpret_cast<const NRFRxConfigureMessage*>(message));
+}
+
+void NRFRxProcessor::configure(const NRFRxConfigureMessage& message) {	
+	decim_0.configure(taps_200k_wfm_decim_0.taps, 33554432);
+	decim_1.configure(taps_200k_wfm_decim_1.taps, 131072);
+	demod.configure(audio_fs, 5000);
+
+	configured = true;
+}
+
+int main() {
+	EventDispatcher event_dispatcher { std::make_unique<NRFRxProcessor>() };
+	event_dispatcher.run();
+	return 0;
+}
diff --git a/firmware/baseband/proc_nrfrx.hpp b/firmware/baseband/proc_nrfrx.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..bfd9e348b5bfe21b36717fb8c500ef691664cf1a
--- /dev/null
+++ b/firmware/baseband/proc_nrfrx.hpp
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
+ * Copyright (C) 2016 Furrtek
+ * Copyright (C) 2020 Shao
+ *
+ * This file is part of PortaPack.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; see the file COPYING.  If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#ifndef __PROC_NRFRX_H__
+#define __PROC_NRFRX_H__
+
+#include "baseband_processor.hpp"
+#include "baseband_thread.hpp"
+#include "rssi_thread.hpp"
+
+#include "dsp_decimate.hpp"
+#include "dsp_demodulate.hpp"
+
+#include "audio_output.hpp"
+
+#include "fifo.hpp"
+#include "message.hpp"
+
+class NRFRxProcessor : public BasebandProcessor {
+public:
+	void execute(const buffer_c8_t& buffer) override;
+
+	void on_message(const Message* const message) override;
+	
+private:
+	static constexpr size_t baseband_fs = 4000000;
+	static constexpr size_t audio_fs = baseband_fs / 8 / 8 / 2;
+	
+	BasebandThread baseband_thread { baseband_fs, this, NORMALPRIO + 20, baseband::Direction::Receive };
+	RSSIThread rssi_thread { NORMALPRIO + 10 };
+	
+	std::array<complex16_t, 512> dst { };
+	const buffer_c16_t dst_buffer {
+		dst.data(),
+		dst.size()
+	};
+
+        std::array<complex16_t, 512> spectrum { };
+	const buffer_c16_t spectrum_buffer {
+		spectrum.data(),
+		spectrum.size()
+	};
+
+	const buffer_s16_t work_audio_buffer {
+		(int16_t*)dst.data(),
+		sizeof(dst) / sizeof(int16_t)
+	};
+
+
+	// Array size ok down to 375 bauds (24000 / 375)
+	std::array<int32_t, 64> delay_line { 0 };
+	std::array<int16_t, 1000> rb_buf { 0 };
+
+	/*dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0 { };
+	dsp::decimate::FIRC16xR16x32Decim8 decim_1 { };
+	dsp::decimate::FIRAndDecimateComplex channel_filter { };*/
+	dsp::decimate::FIRC8xR16x24FS4Decim4 decim_0 { };
+	dsp::decimate::FIRC16xR16x16Decim2 decim_1 { };
+	
+	dsp::demodulate::FM demod { };
+	int rb_head {-1};
+	int32_t g_threshold {0};  
+	//uint8_t g_srate {8}; 
+	uint8_t channel_number {38};
+	int skipSamples {1000};
+	int RB_SIZE {1000};
+
+	bool configured { false };
+
+	
+	void configure(const NRFRxConfigureMessage& message);
+	
+	AFSKDataMessage data_message { false, 0 };
+};
+
+#endif/*__PROC_NRFRX_H__*/
diff --git a/firmware/baseband/tv_collector.cpp b/firmware/baseband/tv_collector.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..a18eaf469149de344df99a42206714b0a1f0fad1
--- /dev/null
+++ b/firmware/baseband/tv_collector.cpp
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
+ * Copyright (C) 2020 Shao
+ *
+ * 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 "tv_collector.hpp"
+
+#include "dsp_fft.hpp"
+
+#include "utility.hpp"
+#include "event_m4.hpp"
+#include "portapack_shared_memory.hpp"
+
+#include "event_m4.hpp"
+
+#include <algorithm>
+
+void TvCollector::on_message(const Message* const message) {
+	switch(message->id) {
+	case Message::ID::UpdateSpectrum:
+		update();
+		break;
+
+	case Message::ID::SpectrumStreamingConfig:
+		set_state(*reinterpret_cast<const SpectrumStreamingConfigMessage*>(message));
+		break;
+
+	default:
+		break;
+	}
+}
+
+void TvCollector::set_state(const SpectrumStreamingConfigMessage& message) {
+	if( message.mode == SpectrumStreamingConfigMessage::Mode::Running ) {
+		start();
+	} else {
+		stop();
+	}
+}
+
+void TvCollector::start() {
+	streaming = true;
+	ChannelSpectrumConfigMessage message { &fifo };
+	shared_memory.application_queue.push(message);
+}
+
+void TvCollector::stop() {
+	streaming = false;
+	fifo.reset_in();
+}
+
+void TvCollector::set_decimation_factor(
+	const size_t decimation_factor
+) {
+	channel_spectrum_decimator.set_factor(decimation_factor);
+}
+
+/* TODO: Refactor to register task with idle thread?
+ * It's sad that the idle thread has to call all the way back here just to
+ * perform the deferred task on the buffer of data we prepared.
+ */
+
+void TvCollector::feed(
+	const buffer_c16_t& channel
+) {
+	// Called from baseband processing thread.
+	channel_spectrum_decimator.feed(channel,[this](const buffer_c16_t& data) {this->post_message(data);});
+}
+
+void TvCollector::post_message(const buffer_c16_t& data) {
+	// Called from baseband processing thread.
+        float re, im;
+	float mag;
+        float max;
+	if( streaming && !channel_spectrum_request_update ) {
+                for(size_t i=0; i<256; i++) 
+		{
+			const auto s = data.p[i];
+                        re = (float)(data.p[i].real());
+			im = (float)(data.p[i].imag());
+			mag = __builtin_sqrtf((re * re) + (im * im)) ;
+			channel_spectrum[i] = {mag, mag};
+		}
+                channel_spectrum_sampling_rate = data.sampling_rate;
+		channel_spectrum_request_update = true;
+		EventDispatcher::events_flag(EVT_MASK_SPECTRUM);
+	}
+}
+
+
+void TvCollector::update() {
+	// Called from idle thread (after EVT_MASK_SPECTRUM is flagged)
+	if( streaming && channel_spectrum_request_update ) {
+		ChannelSpectrum spectrum;
+		spectrum.sampling_rate = channel_spectrum_sampling_rate;
+		for(size_t i=0; i<spectrum.db.size(); i++) {
+                        const auto corrected_sample = channel_spectrum[i].real();
+			const unsigned int v = corrected_sample + 127.0f;
+			spectrum.db[i] = std::max(0U, std::min(255U, v));
+		}
+		fifo.in(spectrum);
+	}
+
+	channel_spectrum_request_update = false;
+}
diff --git a/firmware/baseband/tv_collector.hpp b/firmware/baseband/tv_collector.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..de2041a29d54658a00e8711b2a44c540d4cdf8a7
--- /dev/null
+++ b/firmware/baseband/tv_collector.hpp
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
+ * Copyright (C) 2020 Shao
+ *
+ * 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 __TV_COLLECTOR_H__
+#define __TV_COLLECTOR_H__
+
+#include "dsp_types.hpp"
+#include "complex.hpp"
+
+#include "block_decimator.hpp"
+
+#include <cstdint>
+#include <array>
+
+#include "message.hpp"
+
+class TvCollector {
+public:
+	void on_message(const Message* const message);
+
+	void set_decimation_factor(const size_t decimation_factor);
+
+	void feed(
+		const buffer_c16_t& channel
+	);
+
+private:
+	BlockDecimator<complex16_t, 256> channel_spectrum_decimator { 1 };
+	ChannelSpectrum fifo_data[1 << ChannelSpectrumConfigMessage::fifo_k] { };
+	ChannelSpectrumFIFO fifo { fifo_data, ChannelSpectrumConfigMessage::fifo_k };
+
+	volatile bool channel_spectrum_request_update { false };
+	bool streaming { false };
+	std::array<std::complex<float>, 256> channel_spectrum { };
+	uint32_t channel_spectrum_sampling_rate { 0 };
+
+	void post_message(const buffer_c16_t& data);
+
+	void set_state(const SpectrumStreamingConfigMessage& message);
+	void start();
+	void stop();
+
+	void update();
+
+};
+
+#endif/*__TV_COLLECTOR_H__*/
diff --git a/firmware/common/message.hpp b/firmware/common/message.hpp
index dfcc49ce328eee9f586516fe0f6cd9a25fd4a145..4ca93da9fa3a8b942f3dc7aebf34ce0cb5efc678 100644
--- a/firmware/common/message.hpp
+++ b/firmware/common/message.hpp
@@ -78,6 +78,8 @@ public:
 		AFSKRxConfigure = 22,
 		StatusRefresh = 23,
 		SamplerateConfig = 24,
+		BTLERxConfigure = 25,
+		NRFRxConfigure = 26,
 
 		TXProgress = 30,
 		Retune = 31,
@@ -722,6 +724,46 @@ public:
 	const bool trigger_word;
 };
 
+class BTLERxConfigureMessage : public Message {
+public:
+	constexpr BTLERxConfigureMessage(
+		const uint32_t baudrate,
+		const uint32_t word_length,
+		const uint32_t trigger_value,
+		const bool trigger_word
+	) : Message { ID::BTLERxConfigure },
+		baudrate(baudrate),
+		word_length(word_length),
+		trigger_value(trigger_value),
+		trigger_word(trigger_word)
+	{
+    }
+	const uint32_t baudrate;
+	const uint32_t word_length;
+	const uint32_t trigger_value;
+	const bool trigger_word;
+};
+
+class NRFRxConfigureMessage : public Message {
+public:
+	constexpr NRFRxConfigureMessage(
+		const uint32_t baudrate,
+		const uint32_t word_length,
+		const uint32_t trigger_value,
+		const bool trigger_word
+	) : Message { ID::NRFRxConfigure },
+		baudrate(baudrate),
+		word_length(word_length),
+		trigger_value(trigger_value),
+		trigger_word(trigger_word)
+	{
+    }
+	const uint32_t baudrate;
+	const uint32_t word_length;
+	const uint32_t trigger_value;
+	const bool trigger_word;
+};
+
 class PitchRSSIConfigureMessage : public Message {
 public:
 	constexpr PitchRSSIConfigureMessage(
diff --git a/firmware/common/spi_image.hpp b/firmware/common/spi_image.hpp
index 3c9981b57cec2b1500638caf8ba8742872cb4cec..896340be5a53ebd7d09dbfeea91948d1e3eca83c 100644
--- a/firmware/common/spi_image.hpp
+++ b/firmware/common/spi_image.hpp
@@ -68,8 +68,11 @@ private:
 constexpr image_tag_t image_tag_acars				{ 'P', 'A', 'C', 'A' };
 constexpr image_tag_t image_tag_adsb_rx				{ 'P', 'A', 'D', 'R' };
 constexpr image_tag_t image_tag_afsk_rx				{ 'P', 'A', 'F', 'R' };
+constexpr image_tag_t image_tag_btle_rx				{ 'P', 'B', 'T', 'R' };
+constexpr image_tag_t image_tag_nrf_rx				{ 'P', 'N', 'R', 'R' };
 constexpr image_tag_t image_tag_ais					{ 'P', 'A', 'I', 'S' };
 constexpr image_tag_t image_tag_am_audio			{ 'P', 'A', 'M', 'A' };
+constexpr image_tag_t image_tag_am_tv			        { 'P', 'A', 'M', 'T' };
 constexpr image_tag_t image_tag_capture				{ 'P', 'C', 'A', 'P' };
 constexpr image_tag_t image_tag_ert					{ 'P', 'E', 'R', 'T' };
 constexpr image_tag_t image_tag_nfm_audio			{ 'P', 'N', 'F', 'M' };