diff --git a/firmware/application/CMakeLists.txt b/firmware/application/CMakeLists.txt
index 86b6f3fdda393e32629f74e322d33477ef7e7357..721bc2799b81f187d7b77269f213aff5e9d375cc 100644
--- a/firmware/application/CMakeLists.txt
+++ b/firmware/application/CMakeLists.txt
@@ -260,6 +260,7 @@ set(CPPSRC
 	apps/lge_app.cpp
 	apps/pocsag_app.cpp
 	apps/replay_app.cpp
+	apps/gps_sim_app.cpp
 	apps/soundboard_app.cpp
 	apps/tpms_app.cpp
 	protocols/aprs.cpp
diff --git a/firmware/application/apps/gps_sim_app.cpp b/firmware/application/apps/gps_sim_app.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..83f721aae078d7a70c29c82313cb3f92d6c8dbe9
--- /dev/null
+++ b/firmware/application/apps/gps_sim_app.cpp
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2016 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 "gps_sim_app.hpp"
+#include "string_format.hpp"
+
+#include "ui_fileman.hpp"
+#include "io_file.hpp"
+
+#include "baseband_api.hpp"
+#include "portapack.hpp"
+#include "portapack_persistent_memory.hpp"
+
+using namespace portapack;
+
+namespace ui {
+
+void GpsSimAppView::set_ready() {
+	ready_signal = true;
+}
+
+void GpsSimAppView::on_file_changed(std::filesystem::path new_file_path) {
+	File data_file, info_file;
+	char file_data[257];
+	
+	// Get file size
+	auto data_open_error = data_file.open("/" + new_file_path.string());
+	if (data_open_error.is_valid()) {
+		file_error();
+		return;
+	}
+	
+	file_path = new_file_path;
+	
+	// Get original record frequency if available
+	std::filesystem::path info_file_path = file_path;
+	info_file_path.replace_extension(u".TXT");
+	
+	sample_rate = 500000;
+	
+	auto info_open_error = info_file.open("/" + info_file_path.string());
+	if (!info_open_error.is_valid()) {
+		memset(file_data, 0, 257);
+		auto read_size = info_file.read(file_data, 256);
+		if (!read_size.is_error()) {
+			auto pos1 = strstr(file_data, "center_frequency=");
+			if (pos1) {
+				pos1 += 17;
+				field_frequency.set_value(strtoll(pos1, nullptr, 10));
+			}
+			
+			auto pos2 = strstr(file_data, "sample_rate=");
+			if (pos2) {
+				pos2 += 12;
+				sample_rate = strtoll(pos2, nullptr, 10);
+			}
+		}
+	}
+	
+	text_sample_rate.set(unit_auto_scale(sample_rate, 3, 1) + "Hz");
+	
+	auto file_size = data_file.size();
+	auto duration = (file_size * 1000) / (1 * 2 * sample_rate);
+	
+	progressbar.set_max(file_size);
+	text_filename.set(file_path.filename().string().substr(0, 12));
+	text_duration.set(to_string_time_ms(duration));
+	
+	button_play.focus();
+}
+
+void GpsSimAppView::on_tx_progress(const uint32_t progress) {
+	progressbar.set_value(progress);
+}
+
+void GpsSimAppView::focus() {
+	button_open.focus();
+}
+
+void GpsSimAppView::file_error() {
+	nav_.display_modal("Error", "File read error.");
+}
+
+bool GpsSimAppView::is_active() const {
+	return (bool)replay_thread;
+}
+
+void GpsSimAppView::toggle() {
+	if( is_active() ) {
+		stop(false);
+	} else {
+		start();
+	}
+}
+
+void GpsSimAppView::start() {
+	stop(false);
+
+	std::unique_ptr<stream::Reader> reader;
+	
+	auto p = std::make_unique<FileReader>();
+	auto open_error = p->open(file_path);
+	if( open_error.is_valid() ) {
+		file_error();
+	} else {
+		reader = std::move(p);
+	}
+
+	if( reader ) {
+		button_play.set_bitmap(&bitmap_stop);
+		baseband::set_sample_rate(sample_rate );
+		
+		replay_thread = std::make_unique<ReplayThread>(
+			std::move(reader),
+			read_size, buffer_count,
+			&ready_signal,
+			[](uint32_t return_code) {
+				ReplayThreadDoneMessage message { return_code };
+				EventDispatcher::send_message(message);
+			}
+		);
+	}
+	
+	radio::enable({
+		receiver_model.tuning_frequency(),
+		sample_rate ,
+		baseband_bandwidth,
+		rf::Direction::Transmit,
+		receiver_model.rf_amp(),
+		static_cast<int8_t>(receiver_model.lna()),
+		static_cast<int8_t>(receiver_model.vga())
+	});
+}
+
+void GpsSimAppView::stop(const bool do_loop) {
+	if( is_active() )
+		replay_thread.reset();
+	
+	if (do_loop && check_loop.value()) {
+		start();
+	} else {
+		radio::disable();
+		button_play.set_bitmap(&bitmap_play);
+	}
+	
+	ready_signal = false;
+}
+
+void GpsSimAppView::handle_replay_thread_done(const uint32_t return_code) {
+	if (return_code == ReplayThread::END_OF_FILE) {
+		stop(true);
+	} else if (return_code == ReplayThread::READ_ERROR) {
+		stop(false);
+		file_error();
+	}
+	
+	progressbar.set_value(0);
+}
+
+GpsSimAppView::GpsSimAppView(
+	NavigationView& nav
+) : nav_ (nav)
+{
+	baseband::run_image(portapack::spi_flash::image_tag_gps);
+
+	add_children({
+		&labels,
+		&button_open,
+		&text_filename,
+		&text_sample_rate,
+		&text_duration,
+		&progressbar,
+		&field_frequency,
+		&field_lna,
+		&field_rf_amp,
+		&check_loop,
+		&button_play,
+		&waterfall,
+	});
+	
+	field_frequency.set_value(target_frequency());
+	field_frequency.set_step(receiver_model.frequency_step());
+	field_frequency.on_change = [this](rf::Frequency f) {
+		this->on_target_frequency_changed(f);
+	};
+	field_frequency.on_edit = [this, &nav]() {
+		// TODO: Provide separate modal method/scheme?
+		auto new_view = nav.push<FrequencyKeypadView>(this->target_frequency());
+		new_view->on_changed = [this](rf::Frequency f) {
+			this->on_target_frequency_changed(f);
+			this->field_frequency.set_value(f);
+		};
+	};
+
+	field_frequency.set_step(5000);
+	
+	button_play.on_select = [this](ImageButton&) {
+		this->toggle();
+	};
+	
+	button_open.on_select = [this, &nav](Button&) {
+		auto open_view = nav.push<FileLoadView>(".C8");
+		open_view->on_changed = [this](std::filesystem::path new_file_path) {
+			on_file_changed(new_file_path);
+		};
+	};
+}
+
+GpsSimAppView::~GpsSimAppView() {
+	radio::disable();
+	baseband::shutdown();
+}
+
+void GpsSimAppView::on_hide() {
+	// TODO: Terrible kludge because widget system doesn't notify Waterfall that
+	// it's being shown or hidden.
+	waterfall.on_hide();
+	View::on_hide();
+}
+
+void GpsSimAppView::set_parent_rect(const Rect new_parent_rect) {
+	View::set_parent_rect(new_parent_rect);
+
+	const ui::Rect waterfall_rect { 0, header_height, new_parent_rect.width(), new_parent_rect.height() - header_height };
+	waterfall.set_parent_rect(waterfall_rect);
+}
+
+void GpsSimAppView::on_target_frequency_changed(rf::Frequency f) {
+	set_target_frequency(f);
+}
+
+void GpsSimAppView::set_target_frequency(const rf::Frequency new_value) {
+	persistent_memory::set_tuned_frequency(new_value);;
+}
+
+rf::Frequency GpsSimAppView::target_frequency() const {
+	return persistent_memory::tuned_frequency();
+}
+
+} /* namespace ui */
diff --git a/firmware/application/apps/gps_sim_app.hpp b/firmware/application/apps/gps_sim_app.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..4f26c11a6d88f036e4051b9eeac8870eb67e99e8
--- /dev/null
+++ b/firmware/application/apps/gps_sim_app.hpp
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2016 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 __GPS_SIM_APP_HPP__
+#define __GPS_SIM_APP_HPP__
+
+#include "ui_widget.hpp"
+#include "ui_navigation.hpp"
+#include "ui_receiver.hpp"
+#include "replay_thread.hpp"
+#include "ui_spectrum.hpp"
+
+#include <string>
+#include <memory>
+
+namespace ui {
+
+class GpsSimAppView : public View {
+public:
+	GpsSimAppView(NavigationView& nav);
+	~GpsSimAppView();
+
+	void on_hide() override;
+	void set_parent_rect(const Rect new_parent_rect) override;
+	void focus() override;
+
+	std::string title() const override { return "GPS Simulator"; };
+	
+private:
+	NavigationView& nav_;
+	
+	static constexpr ui::Dim header_height = 3 * 16;
+	
+	uint32_t sample_rate = 0;
+	static constexpr uint32_t baseband_bandwidth = 3000000; //filter bandwidth
+	const size_t read_size { 16384 };
+	const size_t buffer_count { 3 };
+
+	void on_file_changed(std::filesystem::path new_file_path);
+	void on_target_frequency_changed(rf::Frequency f);
+	void on_tx_progress(const uint32_t progress);
+	
+	void set_target_frequency(const rf::Frequency new_value);
+	rf::Frequency target_frequency() const;
+
+	void toggle();
+	void start();
+	void stop(const bool do_loop);
+	bool is_active() const;
+	void set_ready();
+	void handle_replay_thread_done(const uint32_t return_code);
+	void file_error();
+
+	std::filesystem::path file_path { };
+	std::unique_ptr<ReplayThread> replay_thread { };
+	bool ready_signal { false };
+
+	Labels labels {
+		{ { 10 * 8, 2 * 16 }, "LNA:   A:", Color::light_grey() }
+	};
+	
+	Button button_open {
+		{ 0 * 8, 0 * 16, 10 * 8, 2 * 16 },
+		"Open file"
+	};
+	
+	Text text_filename {
+		{ 11 * 8, 0 * 16, 12 * 8, 16 },
+		"-"
+	};
+	Text text_sample_rate {
+		{ 24 * 8, 0 * 16, 6 * 8, 16 },
+		"-"
+	};
+	
+	Text text_duration {
+		{ 11 * 8, 1 * 16, 6 * 8, 16 },
+		"-"
+	};
+	ProgressBar progressbar {
+		{ 18 * 8, 1 * 16, 12 * 8, 16 }
+	};
+	
+	FrequencyField field_frequency {
+		{ 0 * 8, 2 * 16 },
+	};
+	LNAGainField field_lna {
+		{ 14 * 8, 2 * 16 }
+	};
+	RFAmpField field_rf_amp {
+		{ 19 * 8, 2 * 16 }
+	};
+	Checkbox check_loop {
+		{ 21 * 8, 2 * 16 },
+		4,
+		"Loop",
+		true
+	};
+	ImageButton button_play {
+		{ 28 * 8, 2 * 16, 2 * 8, 1 * 16 },
+		&bitmap_play,
+		Color::green(),
+		Color::black()
+	};
+
+	spectrum::WaterfallWidget waterfall { };
+
+	MessageHandlerRegistration message_handler_replay_thread_error {
+		Message::ID::ReplayThreadDone,
+		[this](const Message* const p) {
+			const auto message = *reinterpret_cast<const ReplayThreadDoneMessage*>(p);
+			this->handle_replay_thread_done(message.return_code);
+		}
+	};
+	
+	MessageHandlerRegistration message_handler_fifo_signal {
+		Message::ID::RequestSignal,
+		[this](const Message* const p) {
+			const auto message = static_cast<const RequestSignalMessage*>(p);
+			if (message->signal == RequestSignalMessage::Signal::FillRequest) {
+				this->set_ready();
+			}
+		}
+	};
+	
+	MessageHandlerRegistration message_handler_tx_progress {
+		Message::ID::TXProgress,
+		[this](const Message* const p) {
+			const auto message = *reinterpret_cast<const TXProgressMessage*>(p);
+			this->on_tx_progress(message.progress);
+		}
+	};
+};
+
+} /* namespace ui */
+
+#endif/*__GPS_SIM_APP_HPP__*/
diff --git a/firmware/application/bitmap.hpp b/firmware/application/bitmap.hpp
index 9626bedecc5fb5ac52e030416537e24394bff3c2..b837d3d2f353a58ea313b87f339a6abb04e02777 100644
--- a/firmware/application/bitmap.hpp
+++ b/firmware/application/bitmap.hpp
@@ -1503,6 +1503,28 @@ static constexpr Bitmap bitmap_icon_replay {
 	{ 16, 16 }, bitmap_icon_replay_data
 };
 
+static constexpr uint8_t bitmap_gps_sim_data[] = {
+	0x00, 0x00, 
+	0x00, 0x00, 
+	0x00, 0x00, 
+	0xF0, 0x0F, 
+	0x4C, 0x32, 
+	0xFE, 0x7F, 
+	0x25, 0xA4, 
+	0x25, 0xA4, 
+	0xFF, 0xFF, 
+	0x25, 0xA4, 
+	0x25, 0xA4, 
+	0xFE, 0x7F, 
+	0x4C, 0x32, 
+	0xF0, 0x0F, 
+	0x00, 0x00, 
+	0x00, 0x00, 
+};
+static constexpr Bitmap bitmap_gps_sim {
+	{ 16, 16 }, bitmap_gps_sim_data
+};
+
 static constexpr uint8_t bitmap_icon_btle_data[] = {
 	0x00, 0x00, 
 	0x80, 0x00, 
diff --git a/firmware/application/ui_navigation.cpp b/firmware/application/ui_navigation.cpp
index bd1eb0aa85bcd416a6f3154c2fe25bb6b155bee4..7eea7a76349b67d7021b6b321407de1939ebb1a3 100644
--- a/firmware/application/ui_navigation.cpp
+++ b/firmware/application/ui_navigation.cpp
@@ -76,6 +76,7 @@
 #include "lge_app.hpp"
 #include "pocsag_app.hpp"
 #include "replay_app.hpp"
+#include "gps_sim_app.hpp"
 #include "soundboard_app.hpp"
 #include "tpms_app.hpp"
 
@@ -377,6 +378,7 @@ TransmittersMenuView::TransmittersMenuView(NavigationView& nav) {
 		{ "ADS-B [S]",		ui::Color::yellow(), 	&bitmap_icon_adsb,		[&nav](){ nav.push<ADSBTxView>(); } },
 		{ "APRS", 					ui::Color::orange(),	&bitmap_icon_aprs,		[&nav](){ nav.push<APRSTXView>(); } },
 		{ "BHT Xy/EP", 				ui::Color::green(), 	&bitmap_icon_bht,		[&nav](){ nav.push<BHTView>(); } },
+		{ "GPS Sim",				ui::Color::yellow(),	&bitmap_gps_sim,			[&nav](){ nav.push<GpsSimAppView>(); } },
 		{ "Jammer", 				ui::Color::yellow(),	&bitmap_icon_jammer,	[&nav](){ nav.push<JammerView>(); } },
 		{ "Key fob", 				ui::Color::orange(),	&bitmap_icon_keyfob,	[&nav](){ nav.push<KeyfobView>(); } },
 		{ "LGE tool", 				ui::Color::yellow(),	&bitmap_icon_lge,		[&nav](){ nav.push<LGEView>(); } },
diff --git a/firmware/baseband/CMakeLists.txt b/firmware/baseband/CMakeLists.txt
index 028e62e408cb1f98071549a37e4591d9d86ba101..c4aafa564eebecdb2507d8521228d1c21e0b81bf 100644
--- a/firmware/baseband/CMakeLists.txt
+++ b/firmware/baseband/CMakeLists.txt
@@ -437,6 +437,13 @@ set(MODE_CPPSRC
 )
 DeclareTargets(PREP replay)
 
+### GPS Simulator
+
+set(MODE_CPPSRC
+	proc_gps_sim.cpp
+)
+DeclareTargets(PGPS gps_sim)
+
 ### Signal generator
 
 set(MODE_CPPSRC
diff --git a/firmware/baseband/proc_gps_sim.cpp b/firmware/baseband/proc_gps_sim.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..230c0bda75573b729f69606526408faa44bb5118
--- /dev/null
+++ b/firmware/baseband/proc_gps_sim.cpp
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2016 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_gps_sim.hpp"
+#include "sine_table_int8.hpp"
+#include "portapack_shared_memory.hpp"
+
+#include "event_m4.hpp"
+
+#include "utility.hpp"
+
+ReplayProcessor::ReplayProcessor() {
+	channel_filter_pass_f = taps_200k_decim_1.pass_frequency_normalized * 1000000;	// 162760.416666667
+	channel_filter_stop_f = taps_200k_decim_1.stop_frequency_normalized * 1000000;	// 337239.583333333
+	
+	spectrum_samples = 0;
+
+	channel_spectrum.set_decimation_factor(1);
+	
+	configured = false;
+}
+
+void ReplayProcessor::execute(const buffer_c8_t& buffer) {
+	/* 4MHz, 2048 samples */
+	
+	if (!configured) return;
+	
+	// File data is in C16 format, we need C8
+	// File samplerate is 500kHz, we're at 4MHz
+	// iq_buffer can only be 512 C16 samples (RAM limitation)
+	// To fill up the 2048-sample C8 buffer, we need:
+	// 2048 samples * 2 bytes per sample = 4096 bytes
+	// Since we're oversampling by 4M/500k = 8, we only need 2048/8 = 256 samples from the file and duplicate them 8 times each
+	// So 256 * 4 bytes per sample (C16) = 1024 bytes from the file
+	if( stream ) {                             //sizeof(*buffer.p) = sizeof(C8) = 2*int8 = 2 bytes //buffer.count = 2048
+		const size_t bytes_to_read = sizeof(*buffer.p) * 1 * (buffer.count );	// *2 (C16), /8 (oversampling) should be == 1024
+		bytes_read += stream->read(iq_buffer.p, bytes_to_read);
+	}
+	
+	// Fill and "stretch"
+	for (size_t i = 0; i < buffer.count; i++) {
+		/*if (i & 3) {
+			buffer.p[i] = buffer.p[i - 1];
+		} else {
+			auto re_out = iq_buffer.p[i >> 3].real() ;
+			auto im_out = iq_buffer.p[i >> 3].imag() ;
+			buffer.p[i] = { (int8_t)re_out, (int8_t)im_out };
+		}*/
+                /*
+                if (i % 8 != 0) {
+			buffer.p[i] = buffer.p[i - 1];
+		} else {
+			auto re_out = iq_buffer.p[i/8].real() ;
+			auto im_out = iq_buffer.p[i/8].imag() ;
+			buffer.p[i] = { (int8_t)re_out, (int8_t)im_out };
+		}*/
+                
+                auto re_out = iq_buffer.p[i].real() ;
+	        auto im_out = iq_buffer.p[i].imag() ;
+		buffer.p[i] = { (int8_t)re_out, (int8_t)im_out };
+	}
+	
+	spectrum_samples += buffer.count;
+	if( spectrum_samples >= spectrum_interval_samples ) {
+		spectrum_samples -= spectrum_interval_samples;
+		//channel_spectrum.feed(iq_buffer, channel_filter_pass_f, channel_filter_stop_f);
+		
+		txprogress_message.progress = bytes_read;	// Inform UI about progress
+		txprogress_message.done = false;
+		shared_memory.application_queue.push(txprogress_message);
+	}
+}
+
+void ReplayProcessor::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::SamplerateConfig:
+		samplerate_config(*reinterpret_cast<const SamplerateConfigMessage*>(message));
+		break;
+	
+	case Message::ID::ReplayConfig:
+		configured = false;
+		bytes_read = 0;
+		replay_config(*reinterpret_cast<const ReplayConfigMessage*>(message));
+		break;
+		
+	// App has prefilled the buffers, we're ready to go now
+	case Message::ID::FIFOData:
+		configured = true;
+		break;
+
+	default:
+		break;
+	}
+}
+
+void ReplayProcessor::samplerate_config(const SamplerateConfigMessage& message) {
+	baseband_fs = message.sample_rate;
+	baseband_thread.set_sampling_rate(baseband_fs);
+	spectrum_interval_samples = baseband_fs / spectrum_rate_hz;
+}
+
+void ReplayProcessor::replay_config(const ReplayConfigMessage& message) {
+	if( message.config ) {
+		
+		stream = std::make_unique<StreamOutput>(message.config);
+		
+		// Tell application that the buffers and FIFO pointers are ready, prefill
+		shared_memory.application_queue.push(sig_message);
+	} else {
+		stream.reset();
+	}
+}
+
+int main() {
+	EventDispatcher event_dispatcher { std::make_unique<ReplayProcessor>() };
+	event_dispatcher.run();
+	return 0;
+}
diff --git a/firmware/baseband/proc_gps_sim.hpp b/firmware/baseband/proc_gps_sim.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..440e79de5297442ec5eac3260765992b4504697c
--- /dev/null
+++ b/firmware/baseband/proc_gps_sim.hpp
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2016 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_GPS_SIM_HPP__
+#define __PROC_GPS_SIM_HPP__
+
+#include "baseband_processor.hpp"
+#include "baseband_thread.hpp"
+
+#include "spectrum_collector.hpp"
+
+#include "stream_output.hpp"
+
+#include <array>
+#include <memory>
+
+class ReplayProcessor : public BasebandProcessor {
+public:
+	ReplayProcessor();
+
+	void execute(const buffer_c8_t& buffer) override;
+
+	void on_message(const Message* const message) override;
+
+private:
+	size_t baseband_fs = 0;
+	static constexpr auto spectrum_rate_hz = 50.0f;
+
+	BasebandThread baseband_thread { baseband_fs, this, NORMALPRIO + 20, baseband::Direction::Transmit };
+
+	std::array<complex8_t, 2048> iq { };
+	const buffer_c8_t iq_buffer {
+		iq.data(),
+		iq.size(),
+		baseband_fs 
+	};
+	
+	uint32_t channel_filter_pass_f = 0;
+	uint32_t channel_filter_stop_f = 0;
+
+	std::unique_ptr<StreamOutput> stream { };
+
+	SpectrumCollector channel_spectrum { };
+	size_t spectrum_interval_samples = 0;
+	size_t spectrum_samples = 0;
+	
+	bool configured { false };
+	uint32_t bytes_read { 0 };
+
+	void samplerate_config(const SamplerateConfigMessage& message);
+	void replay_config(const ReplayConfigMessage& message);
+	
+	TXProgressMessage txprogress_message { };
+	RequestSignalMessage sig_message { RequestSignalMessage::Signal::FillRequest };
+};
+
+#endif/*__PROC_GPS_SIM_HPP__*/
diff --git a/firmware/common/spi_image.hpp b/firmware/common/spi_image.hpp
index 023c1317d3e5016f901590950e46bf57be1541cf..896340be5a53ebd7d09dbfeea91948d1e3eca83c 100644
--- a/firmware/common/spi_image.hpp
+++ b/firmware/common/spi_image.hpp
@@ -92,6 +92,7 @@ constexpr image_tag_t image_tag_mic_tx				{ 'P', 'M', 'T', 'X' };
 constexpr image_tag_t image_tag_ook					{ 'P', 'O', 'O', 'K' };
 constexpr image_tag_t image_tag_rds					{ 'P', 'R', 'D', 'S' };
 constexpr image_tag_t image_tag_replay				{ 'P', 'R', 'E', 'P' };
+constexpr image_tag_t image_tag_gps					{ 'P', 'G', 'P', 'S' };
 constexpr image_tag_t image_tag_siggen				{ 'P', 'S', 'I', 'G' };
 constexpr image_tag_t image_tag_sstv_tx				{ 'P', 'S', 'T', 'X' };
 constexpr image_tag_t image_tag_tones				{ 'P', 'T', 'O', 'N' };