mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-09-16 03:22:59 +00:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bfbbacd43c | |||
| 065d01b591 | |||
| b197a4b1de | |||
| 85eaa9d800 | |||
| 1ad092f341 | |||
| 0fe51fa7e3 | |||
| 4cc2ba690b | |||
| abee022000 | |||
| 430c8e5e79 | |||
| 7ea3eec016 | |||
| eb524f7110 | |||
| 3413d71bb3 | |||
| c64dddf5cc | |||
| 3d56bccd49 | |||
| 705e4f125f | |||
| 363b27efe8 | |||
| f8ed7489c9 | |||
| 0685fb7a9f | |||
| 6498f5a1b2 | |||
| 77f747aab2 | |||
| 819b4d0fed | |||
| 15df51f976 | |||
| e9e78609b7 | |||
| 785bd06938 | |||
| 300a4b03c6 | |||
| eaa07f8369 | |||
| c31a978e6f | |||
| 266db4b158 | |||
| 290bf543a1 | |||
| e17f4c4ab4 | |||
| 83a4580353 | |||
| 00f40bf171 | |||
| 794daf5257 |
@@ -83,5 +83,3 @@ venv/
|
||||
# generated bitmap arr file
|
||||
# TODO: generate bitmap during build, since we use python during build anyway, lemme know if this is a bad idea @zxkmm
|
||||
/firmware/tools/bitmap.hpp
|
||||
# flashsize is generated by cmake
|
||||
/firmware/flashsize.h
|
||||
|
||||
+1
-4
@@ -43,7 +43,7 @@ if(NOT DEFINED FLASH_MB_LIMIT_SIZE)
|
||||
set(FLASH_MB_LIMIT_SIZE 3.5) #PRALINE 4 MB = 3.5 firmware + 0.5 MB fpga bitstream.
|
||||
else()
|
||||
set(FLASH_MB_LIMIT_SIZE 1) #TODO: should be ${FLASH_MB_SIZE} remove once we got a stable build for all devices we support with bigger than 1 mb flash
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
message("Configuring for FLASH_MB_LIMIT_SIZE=${FLASH_MB_LIMIT_SIZE}MB")
|
||||
|
||||
@@ -58,9 +58,6 @@ else()
|
||||
math(EXPR FLASH_BYTES_LIMIT_SIZE "${FLASH_MB_LIMIT_SIZE} * 1024 * 1024")
|
||||
endif()
|
||||
|
||||
#generate h files
|
||||
configure_file(flashsize.h.in ${CMAKE_CURRENT_SOURCE_DIR}/firmware/flashsize.h)
|
||||
|
||||
option(USE_CCACHE "Enable ccache, please keep an eye on this because sometimes it's not quite stable and generated unusable binary" OFF)
|
||||
|
||||
project(portapack-h1)
|
||||
|
||||
+5
-2
@@ -15,8 +15,11 @@
|
||||
# - Get a shell inside the build image to
|
||||
# inspect problems: ./dockerize.sh shell
|
||||
# - Give additional parameters to the container:
|
||||
# ./dockerize.sh -j10
|
||||
# ./dockerize.sh ninja -j10
|
||||
# Additional parameters to make: ./dockerize.sh -j10
|
||||
# Use ninja instead of make: ./dockerize.sh ninja -j10
|
||||
# Addotopnal CMake parameters: ./dockerize.sh -DBOARD=PRALINE
|
||||
# Change the default build directory: ./dockerize.sh -B build-alt
|
||||
# (Alternatively --workdir works as well)
|
||||
# - Use a different dockerfile:
|
||||
# ./dockerize.sh build dockerfile-other
|
||||
# - Use a different cpu architecture:
|
||||
|
||||
+33
-26
@@ -48,43 +48,50 @@ if(NOT ${CMAKE_CXX_COMPILER_VERSION} VERSION_EQUAL ${EXPECTED_GCC_VERSION})
|
||||
set(GCC_VERSION_MISMATCH 1)
|
||||
endif()
|
||||
|
||||
#generate h files
|
||||
configure_file(flashsize.h.in flashsize.h)
|
||||
add_compile_options(-include ${CMAKE_CURRENT_BINARY_DIR}/flashsize.h)
|
||||
|
||||
add_subdirectory(application)
|
||||
add_subdirectory(baseband)
|
||||
add_subdirectory(standalone)
|
||||
add_subdirectory(test)
|
||||
|
||||
# NOTE: Dependencies break if the .bin files aren't included in DEPENDS. WTF, CMake?
|
||||
add_custom_command(
|
||||
OUTPUT ${FIRMWARE_FILENAME}
|
||||
COMMAND ${MAKE_SPI_IMAGE} ${application_BINARY_DIR}/application.bin ${baseband_BINARY_DIR}/baseband.img ${FIRMWARE_FILENAME} ${FLASH_BYTES_LIMIT_SIZE}
|
||||
DEPENDS baseband application ${MAKE_SPI_IMAGE}
|
||||
${baseband_BINARY_DIR}/baseband.img ${application_BINARY_DIR}/application.bin
|
||||
VERBATIM
|
||||
)
|
||||
if(BOARD STREQUAL "PRALINE")
|
||||
math(EXPR PRALINE_FINAL_SIZE "4 * 1024 * 1024")
|
||||
set(PRALINE_FPGA_BIN ${CMAKE_CURRENT_SOURCE_DIR}/../hackrf/firmware/fpga/build/praline_fpga.bin)
|
||||
set(APPEND_FPGA_SCRIPT ${CMAKE_CURRENT_SOURCE_DIR}/tools/append_fpga_bitstream.py)
|
||||
|
||||
# NOTE: Dependencies break if the .bin files aren't included in DEPENDS. WTF, CMake?
|
||||
add_custom_command(
|
||||
OUTPUT ${FIRMWARE_FILENAME}
|
||||
COMMAND ${MAKE_SPI_IMAGE} ${application_BINARY_DIR}/application.bin ${baseband_BINARY_DIR}/baseband.img ${FIRMWARE_FILENAME} ${FLASH_BYTES_LIMIT_SIZE}
|
||||
# PRALINE: Append FPGA bitstream to firmware at offset 0x380000
|
||||
# The base firmware is 3.5MB, FPGA bitstream gets appended at the end of it, final size is 4MB
|
||||
COMMAND ${CMAKE_COMMAND} -E echo "Appending PRALINE FPGA bitstream to firmware..."
|
||||
COMMAND python3 ${APPEND_FPGA_SCRIPT} ${FIRMWARE_FILENAME} ${PRALINE_FPGA_BIN} ${FIRMWARE_FILENAME}.tmp ${PRALINE_FINAL_SIZE}
|
||||
COMMAND ${CMAKE_COMMAND} -E rename ${FIRMWARE_FILENAME}.tmp ${FIRMWARE_FILENAME}
|
||||
DEPENDS baseband application ${MAKE_SPI_IMAGE}
|
||||
${baseband_BINARY_DIR}/baseband.img ${application_BINARY_DIR}/application.bin
|
||||
${PRALINE_FPGA_BIN} ${APPEND_FPGA_SCRIPT}
|
||||
VERBATIM
|
||||
)
|
||||
else() # Sadly this part had to be duplicated (or at least I couldn't find a better way to do it)
|
||||
# NOTE: Dependencies break if the .bin files aren't included in DEPENDS. WTF, CMake?
|
||||
add_custom_command(
|
||||
OUTPUT ${FIRMWARE_FILENAME}
|
||||
COMMAND ${MAKE_SPI_IMAGE} ${application_BINARY_DIR}/application.bin ${baseband_BINARY_DIR}/baseband.img ${FIRMWARE_FILENAME} ${FLASH_BYTES_LIMIT_SIZE}
|
||||
DEPENDS baseband application ${MAKE_SPI_IMAGE}
|
||||
${baseband_BINARY_DIR}/baseband.img ${application_BINARY_DIR}/application.bin
|
||||
VERBATIM
|
||||
)
|
||||
endif()
|
||||
|
||||
add_custom_target(
|
||||
firmware
|
||||
DEPENDS ${FIRMWARE_FILENAME} ${HACKRF_FIRMWARE_DFU_FILENAME} ${HACKRF_FIRMWARE_FILENAME}
|
||||
)
|
||||
|
||||
# PRALINE: Append FPGA bitstream to firmware at offset 0x180000
|
||||
# The base firmware is 1MB, FPGA goes at 1MB offset, final size is 2MB
|
||||
if(BOARD STREQUAL "PRALINE")
|
||||
math(EXPR PRALINE_FINAL_SIZE "4 * 1024 * 1024")
|
||||
set(PRALINE_FPGA_BIN ${CMAKE_CURRENT_SOURCE_DIR}/../hackrf/firmware/fpga/build/praline_fpga.bin)
|
||||
set(APPEND_FPGA_SCRIPT ${CMAKE_CURRENT_SOURCE_DIR}/tools/append_fpga_bitstream.py)
|
||||
add_custom_command(
|
||||
TARGET firmware POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E echo "Appending PRALINE FPGA bitstream to firmware..."
|
||||
COMMAND python3 ${APPEND_FPGA_SCRIPT} ${FIRMWARE_FILENAME} ${PRALINE_FPGA_BIN} ${FIRMWARE_FILENAME}.tmp ${PRALINE_FINAL_SIZE}
|
||||
COMMAND ${CMAKE_COMMAND} -E rename ${FIRMWARE_FILENAME}.tmp ${FIRMWARE_FILENAME}
|
||||
COMMAND ${CMAKE_COMMAND} -E echo "PRALINE firmware with FPGA bitstream created:"
|
||||
COMMAND ls -la ${FIRMWARE_FILENAME}
|
||||
COMMENT "Appending PRALINE FPGA bitstream"
|
||||
VERBATIM
|
||||
)
|
||||
endif()
|
||||
|
||||
if(${GCC_VERSION_MISMATCH})
|
||||
set(COMPILER_MISMATCH_MESSAGE "WARNING: Compiler version mismatch, please use the official compiler version ${EXPECTED_GCC_VERSION} when sharing builds! Current compiler version: ${CMAKE_CXX_COMPILER_VERSION}")
|
||||
message(${COMPILER_MISMATCH_MESSAGE})
|
||||
|
||||
@@ -213,6 +213,7 @@ set(CPPSRC
|
||||
irq_controls.cpp
|
||||
irq_lcd_frame.cpp
|
||||
irq_rtc.cpp
|
||||
keeloq_common.cpp
|
||||
keeloq_file.cpp
|
||||
keeloq_keystore.cpp
|
||||
log_file.cpp
|
||||
@@ -257,6 +258,7 @@ set(CPPSRC
|
||||
ook_file.cpp
|
||||
ui_baseband_stats_view.cpp
|
||||
ui_navigation.cpp
|
||||
ui_notifications.cpp
|
||||
ui_record_view.cpp
|
||||
ui_sd_card_status_view.cpp
|
||||
ui/ui_alphanum.cpp
|
||||
@@ -303,7 +305,6 @@ set(CPPSRC
|
||||
apps/ui_mictx.cpp
|
||||
apps/ui_modemsetup.cpp
|
||||
apps/ui_playlist.cpp
|
||||
apps/ui_pocsag_tx.cpp
|
||||
apps/ui_rds.cpp
|
||||
apps/ui_recon_settings.cpp
|
||||
apps/ui_recon.cpp
|
||||
|
||||
@@ -225,7 +225,7 @@ void BLETxView::send_packet() {
|
||||
uint8_t min = 0x00;
|
||||
uint8_t max = 0x0F;
|
||||
|
||||
hexDigit = min + std::rand() % (max - min + 1);
|
||||
hexDigit = min + rand() % (max - min + 1);
|
||||
} break;
|
||||
default:
|
||||
hexDigit = 0;
|
||||
|
||||
@@ -271,10 +271,17 @@ ADSBRxDetailsView::ADSBRxDetailsView(
|
||||
&text_frame_pos_even,
|
||||
&text_frame_pos_odd,
|
||||
&button_aircraft_details,
|
||||
&button_see_map});
|
||||
&button_see_map,
|
||||
&opt_map_list});
|
||||
|
||||
text_icao_address.set(entry_.icao_str);
|
||||
|
||||
opt_map_list.on_change = [this](size_t, uint32_t mf) {
|
||||
map_filter = mf;
|
||||
clear_map_markers(); // reset them
|
||||
pos_history.clear(); // erase the trail
|
||||
};
|
||||
|
||||
button_aircraft_details.on_select = [this, &nav](Button&) {
|
||||
aircraft_details_view_ = nav.push<ADSBRxAircraftDetailsView>(entry_);
|
||||
nav.set_on_pop([this]() {
|
||||
@@ -288,7 +295,7 @@ ADSBRxDetailsView::ADSBRxDetailsView(
|
||||
get_map_tag(entry_),
|
||||
entry_.pos.altitude,
|
||||
GeoPos::alt_unit::FEET,
|
||||
GeoPos::spd_unit::HIDDEN,
|
||||
GeoPos::spd_unit::KNOTS,
|
||||
entry_.pos.latitude,
|
||||
entry_.pos.longitude,
|
||||
entry_.velo.heading);
|
||||
@@ -312,17 +319,107 @@ void ADSBRxDetailsView::update(const AircraftRecentEntry& entry) {
|
||||
// AC Details view is showing, nothing to update.
|
||||
} else if (geomap_view_) {
|
||||
// Map is showing, update the current item.
|
||||
if (map_filter == 1) {
|
||||
// add to map trail history
|
||||
add_map_trail(entry);
|
||||
}
|
||||
geomap_view_->update_tag(get_map_tag(entry_));
|
||||
geomap_view_->update_position(entry.pos.latitude, entry.pos.longitude, entry.velo.heading, entry.pos.altitude, entry.velo.speed);
|
||||
geomap_view_->update_position(entry.pos.latitude, entry.pos.longitude, entry.velo.heading, entry.pos.altitude, entry.get_ground_speed());
|
||||
} else {
|
||||
// Details is showing, update details.
|
||||
refresh_ui();
|
||||
}
|
||||
}
|
||||
|
||||
void ADSBRxDetailsView::get_altitude_color(int32_t alt_ft, uint8_t* r, uint8_t* g, uint8_t* b) {
|
||||
static const struct {
|
||||
int32_t alt;
|
||||
uint8_t r, g, b;
|
||||
} stops[] = {
|
||||
{0, 220, 220, 220}, // 0 ft: White/Grey (Ground)
|
||||
{2000, 255, 255, 0}, // 2k ft: Yellow
|
||||
{10000, 0, 255, 0}, // 10k ft: Green
|
||||
{20000, 0, 255, 255}, // 20k ft: Cyan
|
||||
{30000, 60, 60, 255}, // 30k ft: Blue (Lightened for visibility on Black)
|
||||
{40000, 255, 0, 255} // 40k+ ft: Magenta
|
||||
};
|
||||
if (alt_ft <= stops[0].alt) {
|
||||
*r = stops[0].r;
|
||||
*g = stops[0].g;
|
||||
*b = stops[0].b;
|
||||
return;
|
||||
}
|
||||
const int count = sizeof(stops) / sizeof(stops[0]);
|
||||
if (alt_ft >= stops[count - 1].alt) {
|
||||
*r = stops[count - 1].r;
|
||||
*g = stops[count - 1].g;
|
||||
*b = stops[count - 1].b;
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < count - 1; i++) {
|
||||
if (alt_ft < stops[i + 1].alt) {
|
||||
float t = (float)(alt_ft - stops[i].alt) / (stops[i + 1].alt - stops[i].alt);
|
||||
|
||||
*r = (uint8_t)(stops[i].r + (stops[i + 1].r - stops[i].r) * t);
|
||||
*g = (uint8_t)(stops[i].g + (stops[i + 1].g - stops[i].g) * t);
|
||||
*b = (uint8_t)(stops[i].b + (stops[i + 1].b - stops[i].b) * t);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Fallback: in case no interval matched, default to ground color.
|
||||
*r = stops[0].r;
|
||||
*g = stops[0].g;
|
||||
*b = stops[0].b;
|
||||
}
|
||||
|
||||
void ADSBRxDetailsView::clear_map_markers() {
|
||||
if (geomap_view_)
|
||||
geomap_view_->clear_markers();
|
||||
if (map_filter == 1)
|
||||
refresh_trail_markers(); // re add trail
|
||||
}
|
||||
|
||||
void ADSBRxDetailsView::refresh_trail_markers() {
|
||||
if (!geomap_view_)
|
||||
return;
|
||||
|
||||
geomap_view_->clear_markers();
|
||||
for (const auto& pos : pos_history) {
|
||||
GeoMarker marker{};
|
||||
marker.lon = pos.lon;
|
||||
marker.lat = pos.lat;
|
||||
marker.angle = pos.heading;
|
||||
marker.tag = ""; // No tag for trail points
|
||||
uint8_t r, g, b;
|
||||
get_altitude_color(pos.altitude, &r, &g, &b);
|
||||
marker.color = Color(r, g, b);
|
||||
geomap_view_->store_marker(marker);
|
||||
}
|
||||
}
|
||||
|
||||
void ADSBRxDetailsView::add_map_trail(const AircraftRecentEntry& entry) {
|
||||
if (!geomap_view_)
|
||||
return;
|
||||
if (entry.pos.pos_valid == false)
|
||||
return;
|
||||
|
||||
if (!pos_history.empty()) {
|
||||
const auto& last_pos = pos_history.back();
|
||||
const float THRESH_DEG = 0.009f;
|
||||
const float THRESH_SQ = THRESH_DEG * THRESH_DEG; // 0.00002025f
|
||||
|
||||
float d_lat = entry.pos.latitude - last_pos.lat;
|
||||
float d_lon = entry.pos.longitude - last_pos.lon;
|
||||
if ((d_lat * d_lat) + (d_lon * d_lon) < THRESH_SQ) {
|
||||
return; // Moved less than ~1000m, skip (rough estimate, varies with latitude but good enough for our purposes). This prevents adding too many points when the plane is circling or taxiing.
|
||||
}
|
||||
}
|
||||
|
||||
// Only keep the last 30 positions in the trail.
|
||||
if (pos_history.size() >= 30)
|
||||
pos_history.erase(pos_history.begin());
|
||||
pos_history.push_back({entry.pos.latitude, entry.pos.longitude, entry.velo.heading, entry.pos.altitude});
|
||||
refresh_trail_markers();
|
||||
}
|
||||
|
||||
bool ADSBRxDetailsView::add_map_marker(const AircraftRecentEntry& entry) {
|
||||
@@ -330,11 +427,16 @@ bool ADSBRxDetailsView::add_map_marker(const AircraftRecentEntry& entry) {
|
||||
if (!geomap_view_)
|
||||
return false;
|
||||
|
||||
if (map_filter == 1) return false; // this is the 'only me' option, so skip all others
|
||||
|
||||
GeoMarker marker{};
|
||||
marker.lon = entry.pos.longitude;
|
||||
marker.lat = entry.pos.latitude;
|
||||
marker.angle = entry.velo.heading;
|
||||
marker.tag = get_map_tag(entry);
|
||||
uint8_t r, g, b;
|
||||
get_altitude_color(entry.pos.altitude, &r, &g, &b);
|
||||
marker.color = Color(r, g, b);
|
||||
|
||||
auto markerStored = geomap_view_->store_marker(marker);
|
||||
return markerStored == MARKER_STORED;
|
||||
@@ -651,14 +753,15 @@ void ADSBRxView::refresh_ui() {
|
||||
// Process the entries list.
|
||||
for (const auto& entry : recent) {
|
||||
// Found the entry being shown in details view. Update it.
|
||||
if (entry.key() == detail_key) {
|
||||
if (entry.key() == detail_key) { // we don't add it to the markers, since this is the currently selected and shown in the middle one. This eliminates the "shadow" marker, and saves ram
|
||||
details_view->update(entry);
|
||||
current_updated = true;
|
||||
}
|
||||
|
||||
// NB: current entry also gets a marker so it shows up if map is panned.
|
||||
if (map_needs_update && entry.pos.pos_valid && entry.state <= ADSBAgeState::Recent) {
|
||||
map_needs_update = details_view->add_map_marker(entry);
|
||||
} else {
|
||||
// Add others only
|
||||
// NB: current entry also gets a marker so it shows up if map is panned.
|
||||
if (map_needs_update && entry.pos.pos_valid && entry.state <= ADSBAgeState::Recent) {
|
||||
map_needs_update = details_view->add_map_marker(entry);
|
||||
}
|
||||
}
|
||||
|
||||
// Any work left to do?
|
||||
|
||||
@@ -64,9 +64,9 @@ namespace ui {
|
||||
#define VEL_AIR_SUBSONIC 3
|
||||
#define VEL_AIR_SUPERSONIC 4
|
||||
|
||||
#define O_E_FRAME_TIMEOUT 20 // timeout between odd and even frames
|
||||
#define MARKER_UPDATE_SECONDS_OSM 10 // "other" map marker redraw interval for osm
|
||||
#define MARKER_UPDATE_SECONDS_BIN 5 // "other" map marker redraw interval for bin map
|
||||
#define O_E_FRAME_TIMEOUT 20 // timeout between odd and even frames
|
||||
#define MARKER_UPDATE_SECONDS_OSM 8 // "other" map marker redraw interval for osm
|
||||
#define MARKER_UPDATE_SECONDS_BIN 5 // "other" map marker redraw interval for bin map
|
||||
|
||||
/* Thresholds (in seconds) that define the transition between ages. */
|
||||
struct ADSBAgeLimit {
|
||||
@@ -150,6 +150,19 @@ struct AircraftRecentEntry {
|
||||
age = 0;
|
||||
}
|
||||
|
||||
int32_t get_ground_speed() const {
|
||||
if (velo.valid == false) return 0;
|
||||
if (velo.type == SPD_GND)
|
||||
return velo.speed;
|
||||
else if (velo.type == SPD_IAS) {
|
||||
if (!pos.alt_valid) return velo.speed; // can't correct without altitude, so just return IAS
|
||||
return (int32_t)(velo.speed * (1.0f + (0.02f * (pos.altitude / 1000.0f))));
|
||||
} else if (velo.type == SPD_TAS)
|
||||
return velo.speed; // We don't know the wind speed
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
void inc_age(int delta) {
|
||||
age += delta;
|
||||
|
||||
@@ -208,50 +221,50 @@ class ADSBRxAircraftDetailsView : public View {
|
||||
|
||||
private:
|
||||
Labels labels{
|
||||
{{UI_POS_X(0), 1 * 16}, "ICAO:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), 2 * 16}, "Registration:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), 3 * 16}, "Manufacturer:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), 5 * 16}, "Model:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), 7 * 16}, "Type:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), 8 * 16}, "Number of engines:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), 9 * 16}, "Engine type:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), 11 * 16}, "Owner:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), 13 * 16}, "Operator:", Theme::getInstance()->fg_light->foreground}};
|
||||
{{UI_POS_X(0), UI_POS_Y(1)}, "ICAO:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(2)}, "Registration:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(3)}, "Manufacturer:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(5)}, "Model:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(7)}, "Type:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(8)}, "Number of engines:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(9)}, "Engine type:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(11)}, "Owner:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(13)}, "Operator:", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
Text text_icao_address{
|
||||
{5 * 8, 1 * 16, 6 * 8, UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(5), UI_POS_Y(1), 6 * 8, UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
|
||||
Text text_registration{
|
||||
{13 * 8, 2 * 16, 8 * 8, UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(13), UI_POS_Y(2), 8 * 8, UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
|
||||
Text text_manufacturer{
|
||||
{UI_POS_X(0), 4 * 16, 19 * 8, UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(0), UI_POS_Y(4), 19 * 8, UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
|
||||
Text text_model{
|
||||
{UI_POS_X(0), 6 * 16, screen_width, UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(0), UI_POS_Y(6), screen_width, UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
|
||||
Text text_type{
|
||||
{5 * 8, 7 * 16, 22 * 8, UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(5), UI_POS_Y(7), 22 * 8, UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
|
||||
Text text_number_of_engines{
|
||||
{18 * 8, 8 * 16, screen_width, UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(18), UI_POS_Y(8), screen_width, UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
|
||||
Text text_engine_type{
|
||||
{UI_POS_X(0), 10 * 16, screen_width, UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(0), UI_POS_Y(10), screen_width, UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
|
||||
Text text_owner{
|
||||
{UI_POS_X(0), 12 * 16, screen_width, UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(0), UI_POS_Y(12), screen_width, UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
|
||||
Text text_operator{
|
||||
{UI_POS_X(0), 14 * 16, screen_width, UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(0), UI_POS_Y(14), screen_width, UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
|
||||
Button button_close{
|
||||
@@ -270,11 +283,14 @@ class ADSBRxDetailsView : public View {
|
||||
void focus() override;
|
||||
void update(const AircraftRecentEntry& entry);
|
||||
|
||||
void get_altitude_color(int32_t alt_ft, uint8_t* r, uint8_t* g, uint8_t* b);
|
||||
/* Calls forwarded to map view if shown. */
|
||||
bool map_active() const { return geomap_view_; }
|
||||
void clear_map_markers();
|
||||
/* Adds a marker for the entry to the map. Returns true on success. */
|
||||
bool add_map_marker(const AircraftRecentEntry& entry);
|
||||
void add_map_trail(const AircraftRecentEntry& entry);
|
||||
void refresh_trail_markers();
|
||||
|
||||
std::string title() const override { return "Details"; }
|
||||
|
||||
@@ -297,57 +313,72 @@ class ADSBRxDetailsView : public View {
|
||||
// if removed from the recent entries list.
|
||||
AircraftRecentEntry entry_{AircraftRecentEntry::invalid_key};
|
||||
bool airline_checked{false};
|
||||
uint8_t map_filter{0}; // 0: all, 1: only this, set by opt_map_list.
|
||||
struct PosHistory {
|
||||
float lat;
|
||||
float lon;
|
||||
uint16_t heading;
|
||||
int32_t altitude;
|
||||
};
|
||||
std::vector<PosHistory> pos_history{};
|
||||
|
||||
Labels labels{
|
||||
{{UI_POS_X(0), 1 * 16}, "ICAO:", Theme::getInstance()->fg_light->foreground},
|
||||
{{13 * 8, 1 * 16}, "Callsign:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), 2 * 16}, "Last seen:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), 3 * 16}, "Airline:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), 5 * 16}, "Country:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), 13 * 16}, "Even position frame:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), 15 * 16}, "Odd position frame:", Theme::getInstance()->fg_light->foreground}};
|
||||
{{UI_POS_X(0), UI_POS_Y(0)}, "ICAO:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(13), UI_POS_Y(0)}, "Callsign:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(1)}, "Last seen:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(2)}, "Airline:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(4)}, "Country:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(7)}, "Map filter:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(13)}, "Even position frame:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(15)}, "Odd position frame:", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
Text text_icao_address{
|
||||
{5 * 8, 1 * 16, 6 * 8, UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(5), UI_POS_Y(0), 6 * 8, UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
|
||||
Text text_callsign{
|
||||
{22 * 8, 1 * 16, 8 * 8, UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(22), UI_POS_Y(0), 8 * 8, UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
|
||||
Text text_last_seen{
|
||||
{11 * 8, 2 * 16, 19 * 8, UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(11), UI_POS_Y(1), 19 * 8, UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
|
||||
Text text_airline{
|
||||
{UI_POS_X(0), 4 * 16, screen_width, UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(0), UI_POS_Y(3), screen_width, UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
|
||||
Text text_country{
|
||||
{8 * 8, 5 * 16, 22 * 8, UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(8), UI_POS_Y(4), 22 * 8, UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
|
||||
Text text_infos{
|
||||
{UI_POS_X(0), 6 * 16, screen_width, UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(0), UI_POS_Y(5), screen_width, UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
|
||||
Text text_info2{
|
||||
{UI_POS_X(0), 7 * 16, screen_width, UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(0), UI_POS_Y(6), screen_width, UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
|
||||
Text text_frame_pos_even{
|
||||
{UI_POS_X(0), 14 * 16, screen_width, UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(0), UI_POS_Y(14), screen_width, UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
Text text_frame_pos_odd{
|
||||
{UI_POS_X(0), 16 * 16, screen_width, UI_POS_HEIGHT(1)},
|
||||
{UI_POS_X(0), UI_POS_Y(16), screen_width, UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
|
||||
Button button_aircraft_details{
|
||||
{UI_POS_X_CENTER(12) - UI_POS_X(8), UI_POS_Y(9), UI_POS_WIDTH(12), UI_POS_HEIGHT(3)},
|
||||
{UI_POS_X_CENTER(12) - UI_POS_X(8), UI_POS_Y(8), UI_POS_WIDTH(12), UI_POS_HEIGHT(3)},
|
||||
"A/C details"};
|
||||
|
||||
OptionsField opt_map_list{
|
||||
{UI_POS_X_CENTER(12) + UI_POS_X(8), UI_POS_Y(7)},
|
||||
12,
|
||||
{{"All", 0},
|
||||
{"Only this", 1}}};
|
||||
|
||||
Button button_see_map{
|
||||
{UI_POS_X_CENTER(12) + UI_POS_X(8), UI_POS_Y(9), UI_POS_WIDTH(12), UI_POS_HEIGHT(3)},
|
||||
{UI_POS_X_CENTER(12) + UI_POS_X(8), UI_POS_Y(8), UI_POS_WIDTH(12), UI_POS_HEIGHT(3)},
|
||||
"See on map"};
|
||||
|
||||
MessageHandlerRegistration message_handler_gps{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -643,34 +643,36 @@ class Si5351DebugView : public View {
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
|
||||
Text text_title{{8, 16, 200, 16}, "Si5351 Clock Generator"};
|
||||
Text text_title{{0, 0, 200, 16}, "Si5351 Clock Generator"};
|
||||
|
||||
Text text_status_label{{8, 40, 80, 16}, "Status Reg:"};
|
||||
Text text_status_value{{96, 40, 144, 16}, ""};
|
||||
Text text_status_label{{0, 16, 80, 16}, "Status Reg:"};
|
||||
Text text_status_value{{96, 16, 144, 16}, ""};
|
||||
|
||||
Text text_pll_a_label{{8, 60, 80, 16}, "PLL A:"};
|
||||
Text text_pll_a_status{{96, 60, 144, 16}, ""};
|
||||
Text text_pll_a_label{{0, 32, 80, 16}, "PLL A:"};
|
||||
Text text_pll_a_status{{96, 32, 144, 16}, ""};
|
||||
|
||||
Text text_pll_b_label{{8, 80, 80, 16}, "PLL B:"};
|
||||
Text text_pll_b_status{{96, 80, 144, 16}, ""};
|
||||
Text text_pll_b_label{{0, 48, 80, 16}, "PLL B:"};
|
||||
Text text_pll_b_status{{96, 48, 144, 16}, ""};
|
||||
|
||||
Text text_sys_init_label{{8, 100, 80, 16}, "SYS_INIT:"};
|
||||
Text text_sys_init_status{{96, 100, 144, 16}, ""};
|
||||
Text text_sys_init_label{{0, 64, 80, 16}, "SYS_INIT:"};
|
||||
Text text_sys_init_status{{96, 64, 144, 16}, ""};
|
||||
|
||||
Text text_xtal_cap_label{{8, 120, 80, 16}, "XTAL Cap:"};
|
||||
Text text_xtal_cap_value{{96, 120, 144, 16}, ""};
|
||||
Text text_xtal_cap_label{{0, 80, 80, 16}, "XTAL Cap:"};
|
||||
Text text_xtal_cap_value{{96, 80, 144, 16}, ""};
|
||||
|
||||
Text text_clk0_label{{8, 150, 72, 16}, "CLK0:"};
|
||||
Text text_clk0_status{{88, 150, 152, 16}, ""};
|
||||
Text text_clk0_label{{0, 96, 48, 16}, "CLK0:"};
|
||||
Text text_clk0_status{{50, 96, 28, 16}, ""};
|
||||
Text text_clk0_freq_value{{80, 96, 160, 16}, ""};
|
||||
Text text_clk0_div_value{{50, 112, 190, 16}, ""};
|
||||
|
||||
Text text_clk0_freq_label{{8, 170, 72, 16}, " Freq:"};
|
||||
Text text_clk0_freq_value{{88, 170, 152, 16}, ""};
|
||||
Text text_clk1_label{{0, 128, 96, 16}, "CLK1 (SCT):"};
|
||||
Text text_clk1_status{{112, 128, 128, 16}, ""};
|
||||
|
||||
Text text_clk0_div_label{{8, 190, 72, 16}, " Div:"};
|
||||
Text text_clk0_div_value{{88, 190, 152, 16}, ""};
|
||||
Text text_clk4_label{{0, 144, 96, 16}, "CLK4 (MAX):"};
|
||||
Text text_clk4_status{{112, 144, 128, 16}, ""};
|
||||
|
||||
Text text_clk1_label{{8, 210, 96, 16}, "CLK1 (SCT):"};
|
||||
Text text_clk1_status{{112, 210, 128, 16}, ""};
|
||||
Text text_clk5_label{{0, 160, 96, 16}, "CLK5 (RFFC):"};
|
||||
Text text_clk5_status{{112, 160, 128, 16}, ""};
|
||||
|
||||
Button button_refresh{{8, 240, 72, 24}, "Refresh"};
|
||||
Button button_reset_pll{{88, 240, 72, 24}, "Reset PLL"};
|
||||
@@ -694,7 +696,7 @@ class SignalPathStatusView : public View {
|
||||
|
||||
Text text_title{{0, 0, 240, 16}, "=== Signal Path Status ==="};
|
||||
|
||||
Text text_lbl_max_enable{{0, 20, 1114, 16}, "MAX2831:"};
|
||||
Text text_lbl_max_enable{{0, 20, 114, 16}, "MAX2831:"};
|
||||
Text text_max_enable{{116, 20, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_max_mode{{0, 36, 114, 16}, "RX Mode:"};
|
||||
@@ -703,20 +705,88 @@ class SignalPathStatusView : public View {
|
||||
Text text_lbl_rf_path{{0, 52, 114, 16}, "RF Path:"};
|
||||
Text text_rf_path{{116, 52, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_rf_amp{{0, 68, 114, 16}, "RF Amp:"};
|
||||
Text text_rf_amp{{116, 68, 124, 16}, "---"};
|
||||
Text text_lbl_filter{{0, 68, 114, 16}, "Filter Band:"};
|
||||
Text text_filter{{116, 68, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_lna{{0, 84, 114, 16}, "LNA Gain:"};
|
||||
Text text_lna{{116, 84, 124, 16}, "---"};
|
||||
Text text_lbl_mixer{{0, 84, 114, 16}, "Mixer Enable:"};
|
||||
Text text_mixer{{116, 84, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_vga{{0, 100, 114, 16}, "VGA Gain:"};
|
||||
Text text_vga{{116, 100, 124, 16}, "---"};
|
||||
Text text_lbl_rf_amp{{0, 100, 114, 16}, "RF Amp:"};
|
||||
Text text_rf_amp{{116, 100, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_fpga_decim{{0, 116, 114, 16}, "FPGA Decim:"};
|
||||
Text text_fpga_decim{{116, 116, 124, 16}, "---"};
|
||||
Text text_lbl_lna{{0, 116, 114, 16}, "LNA Gain:"};
|
||||
Text text_lna{{116, 116, 124, 16}, "---"};
|
||||
|
||||
Text text_status{{0, 140, 240, 32}, ""};
|
||||
Text text_lbl_vga{{0, 132, 114, 16}, "VGA Gain:"};
|
||||
Text text_vga{{116, 132, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_fpga_decim{{0, 148, 114, 16}, "FPGA Decim:"};
|
||||
Text text_fpga_decim{{116, 148, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_fpga_ctrl_dc_q{{0, 164, 114, 16}, "FPGA Ctrl:"};
|
||||
Text text_fpga_ctrl_dc_q{{116, 164, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_fpga_ctrl_qs{{0, 180, 114, 16}, ""};
|
||||
Text text_fpga_ctrl_qs{{116, 180, 124, 16}, "---"};
|
||||
|
||||
Text text_status{{0, 200, 240, 32}, ""};
|
||||
|
||||
Button button_refresh{{0, 280, 72, 24}, "Refresh"};
|
||||
Button button_toggle_q{{88, 280, 72, 24}, "Q Inv"};
|
||||
Button button_done{{176, 280, 64, 24}, "Done"};
|
||||
};
|
||||
#endif
|
||||
|
||||
#ifdef PRALINE
|
||||
class GPIODebugView : public View {
|
||||
public:
|
||||
GPIODebugView(NavigationView& nav);
|
||||
void focus() override;
|
||||
std::string title() const override { return "GPIO Debug"; };
|
||||
|
||||
private:
|
||||
void refresh();
|
||||
|
||||
// GPIO4 (LPF and RF Amp)
|
||||
Text text_lbl_gpio4{{0, 0, 240, 16}, "Pin Diag: GPIO4 (LPF|Amp|Mix)"};
|
||||
|
||||
Text text_lbl_mixr1{{0, 18, 56, 16}, "MixR1:"};
|
||||
Text text_mixr1{{58, 18, 180, 16}, "---"};
|
||||
|
||||
Text text_lbl_pin4{{0, 36, 114, 16}, "PIN[4] (read):"};
|
||||
Text text_pin4{{116, 36, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_set4{{0, 54, 114, 16}, "SET[4] (write):"};
|
||||
Text text_set4{{116, 54, 124, 16}, "---"};
|
||||
|
||||
// Bit 8 (LPF)
|
||||
Text text_lbl_lpf_bit{{0, 78, 240, 16}, "Bit 8 (LPF - GPIO4[8]):"};
|
||||
Text text_lpf_dir{{0, 96, 80, 16}, "DIR: ?"};
|
||||
Text text_lpf_pin{{82, 96, 78, 16}, "PIN: ?"};
|
||||
Text text_lpf_set{{162, 96, 78, 16}, "SET: ?"};
|
||||
|
||||
Button button_lpf_toggle{{8, 114, 110, 24}, "Toggle LPF"};
|
||||
Button button_lpf_on{{122, 114, 50, 24}, "ON"};
|
||||
Button button_lpf_off{{176, 114, 50, 24}, "OFF"};
|
||||
|
||||
// Bit 9 (RF Amp)
|
||||
Text text_lbl_amp_bit{{0, 146, 240, 16}, "Bit 9 (Amp - GPIO4[9]):"};
|
||||
Text text_amp_dir{{0, 164, 80, 16}, "DIR: ?"};
|
||||
Text text_amp_pin{{82, 164, 78, 16}, "PIN: ?"};
|
||||
Text text_amp_set{{162, 164, 78, 16}, "SET: ?"};
|
||||
|
||||
Button button_amp_toggle{{8, 182, 110, 24}, "Toggle Amp"};
|
||||
Button button_amp_on{{122, 182, 50, 24}, "ON"};
|
||||
Button button_amp_off{{176, 182, 50, 24}, "OFF"};
|
||||
|
||||
// GPIO3 (Mixer)
|
||||
Text text_lbl_gpio3{{0, 214, 240, 16}, "--- GPIO3 (Mixer) ---"};
|
||||
Text text_lbl_mix_bit{{0, 232, 240, 16}, "Bit 2 (MIX - GPIO3[2]):"};
|
||||
Text text_mix_dir{{0, 250, 80, 16}, "DIR: ?"};
|
||||
Text text_mix_pin{{82, 250, 78, 16}, "PIN: ?"};
|
||||
Text text_mix_set{{162, 250, 78, 16}, "SET: ?"};
|
||||
|
||||
// Controls
|
||||
Button button_refresh{{8, 280, 72, 24}, "Refresh"};
|
||||
Button button_done{{168, 280, 64, 24}, "Done"};
|
||||
};
|
||||
@@ -734,44 +804,50 @@ class RFFC5072StatusView : public View {
|
||||
NavigationView& nav_;
|
||||
void refresh_status();
|
||||
|
||||
Text text_title{{0, 0, 240, 16}, "=== RFFC5072 (1st IF) ==="};
|
||||
Text text_status{{0, 0, 240, 16}, "---"};
|
||||
|
||||
Text text_lbl_enabled{{0, 20, 114, 16}, "Status:"};
|
||||
Text text_enabled{{116, 20, 124, 16}, "---"};
|
||||
Text text_gpio4{{0, 16, 240, 16}, "---"};
|
||||
|
||||
Text text_lbl_freq{{0, 36, 114, 16}, "LO Freq:"};
|
||||
Text text_freq{{116, 36, 124, 16}, "---"};
|
||||
Text text_ctrl{{0, 32, 240, 16}, "---"};
|
||||
|
||||
Text text_lbl_path{{0, 52, 114, 16}, "Path:"};
|
||||
Text text_path{{116, 52, 124, 16}, "---"};
|
||||
Text text_lbl_enabled{{0, 48, 114, 16}, "Status:"};
|
||||
Text text_enabled{{116, 48, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_mixer{{0, 68, 114, 16}, "Mixer:"};
|
||||
Text text_mixer{{116, 68, 124, 16}, "---"};
|
||||
Text text_lbl_freq{{0, 64, 114, 16}, "LO Freq:"};
|
||||
Text text_freq{{116, 64, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_r0{{0, 92, 114, 16}, "Reg 0:"};
|
||||
Text text_r0{{116, 92, 124, 16}, "---"};
|
||||
Text text_lbl_path{{0, 80, 114, 16}, "Path:"};
|
||||
Text text_path{{116, 80, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_r1{{0, 108, 114, 16}, "Reg 1 (N):"};
|
||||
Text text_r1{{116, 108, 124, 16}, "---"};
|
||||
Text text_lbl_mixer{{0, 96, 114, 16}, "Mixer:"};
|
||||
Text text_mixer{{116, 96, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_r2{{0, 124, 114, 16}, "Reg 2:"};
|
||||
Text text_r2{{116, 124, 124, 16}, "---"};
|
||||
Text text_lbl_r0{{0, 112, 114, 16}, "Reg 0:"};
|
||||
Text text_r0{{116, 112, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_decode{{0, 148, 240, 16}, "--- Decoded Values ---"};
|
||||
Text text_lbl_r1{{0, 128, 114, 16}, "Reg 1 (N):"};
|
||||
Text text_r1{{116, 128, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_n{{0, 168, 114, 16}, "N divider:"};
|
||||
Text text_n{{116, 168, 124, 16}, "---"};
|
||||
Text text_lbl_r2{{0, 144, 114, 16}, "Reg 2:"};
|
||||
Text text_r2{{116, 144, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_lodiv{{0, 184, 114, 16}, "LO divider:"};
|
||||
Text text_lodiv{{116, 184, 124, 16}, "---"};
|
||||
Text text_lbl_n{{0, 160, 114, 16}, "N divider:"};
|
||||
Text text_n{{116, 160, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_calc{{0, 200, 114, 16}, "Calc freq:"};
|
||||
Text text_calc{{116, 200, 124, 16}, "---"};
|
||||
Text text_lbl_lodiv{{0, 176, 114, 16}, "LO divider:"};
|
||||
Text text_lodiv{{116, 176, 124, 16}, "---"};
|
||||
|
||||
Text text_status{{0, 224, 240, 32}, ""};
|
||||
Text text_lbl_calc{{0, 192, 114, 16}, "Calc freq:"};
|
||||
Text text_calc{{116, 192, 124, 16}, "---"};
|
||||
|
||||
Button button_refresh{{8, 280, 72, 24}, "Refresh"};
|
||||
Button button_done{{168, 280, 64, 24}, "Done"};
|
||||
Text text_regs_status{{0, 208, 240, 16}, "---"};
|
||||
|
||||
Text text_status2{{0, 224, 240, 16}, ""};
|
||||
Text text_status3{{0, 240, 240, 16}, ""};
|
||||
|
||||
Button button_refresh{{2, 280, 72, 24}, "Refresh"};
|
||||
Button button_force{{98, 280, 60, 24}, "SPI"};
|
||||
Button button_done{{182, 280, 56, 24}, "Done"};
|
||||
};
|
||||
|
||||
#ifdef PRALINE
|
||||
@@ -828,6 +904,120 @@ class RFFCTuningDebugView : public View {
|
||||
|
||||
#endif
|
||||
|
||||
#ifdef PRALINE
|
||||
/* MAX2831DebugView *************************************************/
|
||||
|
||||
class MAX2831DebugView : public View {
|
||||
public:
|
||||
MAX2831DebugView(NavigationView& nav);
|
||||
void focus() override;
|
||||
std::string title() const override { return "MAX2831 Debug"; };
|
||||
|
||||
private:
|
||||
void refresh();
|
||||
|
||||
Text text_title{{0, 0, 240, 16}, "MAX2831 (2nd IF) Debug"};
|
||||
|
||||
Text text_lbl_called{{0, 24, 120, 16}, "Freq Set:"};
|
||||
Text text_called{{122, 24, 118, 16}, "NO"};
|
||||
|
||||
Text text_lbl_valid{{0, 42, 120, 16}, "In Range:"};
|
||||
Text text_valid{{122, 42, 118, 16}, "---"};
|
||||
|
||||
Text text_lbl_req{{0, 60, 120, 16}, "Requested:"};
|
||||
Text text_req{{122, 60, 118, 16}, "---"};
|
||||
|
||||
Text text_lbl_calc_n{{0, 78, 120, 16}, "Calc N:"};
|
||||
Text text_calc_n{{122, 78, 118, 16}, "---"};
|
||||
|
||||
Text text_lbl_calc_frac{{0, 96, 120, 16}, "Calc Frac:"};
|
||||
Text text_calc_frac{{122, 96, 118, 16}, "---"};
|
||||
|
||||
Text text_spacer{{0, 114, 240, 16}, "--- Hardware Regs ---"};
|
||||
|
||||
Text text_lbl_r3{{0, 132, 120, 16}, "Reg 3:"};
|
||||
Text text_r3{{122, 132, 118, 16}, "---"};
|
||||
|
||||
Text text_lbl_r4{{0, 150, 120, 16}, "Reg 4:"};
|
||||
Text text_r4{{122, 150, 118, 16}, "---"};
|
||||
|
||||
Text text_lbl_act_n{{0, 168, 120, 16}, "Actual N:"};
|
||||
Text text_act_n{{122, 168, 118, 16}, "---"};
|
||||
|
||||
Text text_lbl_act_frac{{0, 186, 120, 16}, "Actual Frac:"};
|
||||
Text text_act_frac{{122, 186, 118, 16}, "---"};
|
||||
|
||||
Text text_lbl_calc_freq{{0, 204, 120, 16}, "Calc Freq:"};
|
||||
Text text_calc_freq{{122, 204, 118, 16}, "---"};
|
||||
|
||||
Text text_status{{0, 228, 240, 44}, ""};
|
||||
|
||||
Button button_refresh{{8, 280, 72, 24}, "Refresh"};
|
||||
Button button_done{{168, 280, 64, 24}, "Done"};
|
||||
};
|
||||
#endif
|
||||
|
||||
#ifdef PRALINE
|
||||
class SystemDiagnosticsView : public View {
|
||||
public:
|
||||
SystemDiagnosticsView(NavigationView& nav);
|
||||
void focus() override;
|
||||
std::string title() const override { return "System Diagnostics"; };
|
||||
|
||||
private:
|
||||
void refresh();
|
||||
void read_gpio_states();
|
||||
void set_sample_rate(uint32_t rate);
|
||||
|
||||
Text text_title{{0, 0, 240, 16}, "System Diagnostics"};
|
||||
|
||||
// Sample Rate Section
|
||||
Text text_lbl_sample{{0, 24, 42, 16}, "Rate:"};
|
||||
Text text_sample_rate{{44, 24, 50, 16}, "---"};
|
||||
Text text_lbl_fpga_decode{{96, 24, 144, 16}, "DC:? Q:? QS:?"};
|
||||
|
||||
Button button_sample_2m{{2, 44, 56, 24}, "2 MSPS"};
|
||||
Button button_sample_4m{{62, 44, 56, 24}, "4 MSPS"};
|
||||
Button button_sample_8m{{122, 44, 56, 24}, "8 MSPS"};
|
||||
Button button_sample_20m{{182, 44, 56, 24}, "20 MSPS"};
|
||||
|
||||
// Baseband Filter Section
|
||||
Text text_lbl_bb_filter{{0, 76, 114, 16}, "BB Filter BW:"};
|
||||
Text text_bb_filter{{116, 76, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_reg8{{0, 94, 114, 16}, "MAX2831 R8:"};
|
||||
Text text_reg8{{116, 94, 124, 16}, "---"};
|
||||
|
||||
// GPIO States Section
|
||||
Text text_lbl_gpio{{0, 118, 240, 16}, "--- GPIO Pin States ---"};
|
||||
|
||||
Text text_lbl_lpf{{0, 136, 56, 16}, "LPF:"};
|
||||
Text text_gpio_lpf{{58, 136, 181, 16}, "---"};
|
||||
|
||||
Text text_lbl_mix{{0, 154, 56, 16}, "Mixer:"};
|
||||
Text text_gpio_mix{{58, 154, 181, 16}, "---"};
|
||||
|
||||
Text text_lbl_amp{{0, 172, 56, 16}, "RF Amp:"};
|
||||
Text text_gpio_amp{{58, 172, 181, 16}, "---"};
|
||||
|
||||
// FPGA Control Section
|
||||
Text text_lbl_fpga{{0, 196, 240, 16}, "--- FPGA Control ---"};
|
||||
|
||||
Text text_lbl_fpga_ctrl{{0, 214, 114, 16}, "FPGA Reg 1:"};
|
||||
Text text_fpga_ctrl{{116, 214, 124, 16}, "---"};
|
||||
|
||||
Text text_lbl_band{{0, 232, 48, 16}, "Band:"};
|
||||
Text text_band{{50, 232, 190, 16}, "---"};
|
||||
|
||||
Button button_toggle_q{{2, 252, 110, 24}, "Toggle Q Inv"};
|
||||
Button button_toggle_dc{{122, 252, 110, 24}, "Toggle DC Blk"};
|
||||
|
||||
// Control Buttons
|
||||
Button button_refresh{{2, 280, 72, 24}, "Refresh"};
|
||||
Button button_done{{168, 280, 64, 24}, "Done"};
|
||||
};
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
class DebugPeripheralsMenuView : public BtnGridView {
|
||||
|
||||
@@ -46,11 +46,10 @@ bool valid_firmware_file(std::filesystem::path::string_type path) {
|
||||
auto result = firmware_file.open(path.c_str());
|
||||
if (!result.is_valid()) {
|
||||
uint64_t file_size = firmware_file.size();
|
||||
if (portapack::device_type == portapack::DeviceType::DEV_PORTAPACK && file_size > 1 * 1024 * 1024) {
|
||||
// Portapack firmware files must not be larger than 1MB
|
||||
if (file_size > FLASH_ROM_SIZE) {
|
||||
// Firmware file is larger than the flash size for this device
|
||||
return false;
|
||||
}
|
||||
// May need to add a check to portarf and portarf pro too.
|
||||
checksum = 0;
|
||||
for (uint64_t offset = 0; offset < FLASH_ROM_SIZE && offset < file_size; offset += sizeof(read_buffer)) {
|
||||
auto readResult = firmware_file.read(&read_buffer, sizeof(read_buffer));
|
||||
|
||||
@@ -32,8 +32,6 @@
|
||||
#include "untar.hpp"
|
||||
#include <cstdint>
|
||||
|
||||
#include "../../flashsize.h"
|
||||
|
||||
#define FLASH_ROM_SIZE (FLASH_SIZE_MB * 1024 * 1024)
|
||||
#define FLASH_STARTING_ADDRESS 0x00000000
|
||||
#define FLASH_EXPECTED_CHECKSUM 0x00000000
|
||||
|
||||
@@ -304,7 +304,8 @@ class MicTXView : public View {
|
||||
{
|
||||
{" 8k5 ", 0}, // Initial dynamic values when we start Mic App.
|
||||
{" 11k ", 1},
|
||||
{" 16k ", 2},
|
||||
{" 12k5 ", 2},
|
||||
{" 16k ", 3},
|
||||
}};
|
||||
|
||||
NumberField field_squelch{
|
||||
|
||||
@@ -33,6 +33,8 @@
|
||||
#include "ui_touch_calibration.hpp"
|
||||
#include "ui_text_editor.hpp"
|
||||
#include "ui_external_items_menu_loader.hpp"
|
||||
#include "ui_ss_viewer.hpp"
|
||||
#include "ui_fileman.hpp"
|
||||
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include "lpc43xx_cpp.hpp"
|
||||
@@ -348,7 +350,6 @@ SetUIView::SetUIView(NavigationView& nav) {
|
||||
add_children({&checkbox_disable_touchscreen,
|
||||
&checkbox_bloff,
|
||||
&options_bloff,
|
||||
&checkbox_showsplash,
|
||||
&checkbox_showclock,
|
||||
&options_clockformat,
|
||||
&checkbox_guireturnflag,
|
||||
@@ -375,7 +376,6 @@ SetUIView::SetUIView(NavigationView& nav) {
|
||||
}
|
||||
|
||||
checkbox_disable_touchscreen.set_value(pmem::disable_touchscreen());
|
||||
checkbox_showsplash.set_value(pmem::config_splash());
|
||||
checkbox_showclock.set_value(!pmem::hide_clock());
|
||||
checkbox_guireturnflag.set_value(pmem::show_gui_return_icon());
|
||||
|
||||
@@ -414,7 +414,6 @@ SetUIView::SetUIView(NavigationView& nav) {
|
||||
pmem::set_clock_with_date(false);
|
||||
}
|
||||
|
||||
pmem::set_config_splash(checkbox_showsplash.value());
|
||||
pmem::set_clock_hidden(!checkbox_showclock.value());
|
||||
pmem::set_gui_return_icon(checkbox_guireturnflag.value());
|
||||
pmem::set_disable_touchscreen(checkbox_disable_touchscreen.value());
|
||||
@@ -1113,6 +1112,46 @@ void SetBatteryView::focus() {
|
||||
button_cancel.focus();
|
||||
}
|
||||
|
||||
/* SetSlpash *********************************************/
|
||||
|
||||
SetSplash::SetSplash(NavigationView& nav) {
|
||||
add_children({&checkbox_showsplash,
|
||||
&checkbox_randomsplash,
|
||||
&message,
|
||||
&button_picture_select,
|
||||
&button_save,
|
||||
&button_cancel});
|
||||
|
||||
checkbox_showsplash.set_value(pmem::config_splash());
|
||||
splash_bmp_exists = file_exists(splash_dot_bmp);
|
||||
checkbox_randomsplash.set_value(!splash_bmp_exists);
|
||||
message.hidden(splash_bmp_exists);
|
||||
|
||||
checkbox_randomsplash.on_select = [this](Checkbox&, bool v) {
|
||||
random_enabled = v;
|
||||
};
|
||||
|
||||
button_picture_select.on_select = [this, &nav](Button&) {
|
||||
auto ret = nav.push<FileManagerView>();
|
||||
ret->push_dir(splash_dir);
|
||||
};
|
||||
|
||||
button_save.on_select = [&nav, this](Button&) {
|
||||
if (random_enabled == true) delete_file(splash_dot_bmp);
|
||||
pmem::set_config_splash(checkbox_showsplash.value());
|
||||
send_system_refresh();
|
||||
nav.pop();
|
||||
};
|
||||
|
||||
button_cancel.on_select = [&nav, this](Button&) {
|
||||
nav.pop();
|
||||
};
|
||||
}
|
||||
|
||||
void SetSplash::focus() {
|
||||
button_save.focus();
|
||||
}
|
||||
|
||||
/* SettingsMenuView **************************************/
|
||||
|
||||
SettingsMenuView::SettingsMenuView(NavigationView& nav)
|
||||
@@ -1146,6 +1185,7 @@ void SettingsMenuView::on_populate() {
|
||||
{"Display", ui::Color::dark_cyan(), &bitmap_icon_brightness, [this]() { nav_.push<SetDisplayView>(); }},
|
||||
{"Menu Color", ui::Color::dark_cyan(), &bitmap_icon_brightness, [this]() { nav_.push<SetMenuColorView>(); }},
|
||||
{"Theme", ui::Color::dark_cyan(), &bitmap_icon_setup, [this]() { nav_.push<SetThemeView>(); }},
|
||||
{"Splash settings", ui::Color::dark_cyan(), &bitmap_icon_file_image, [this]() { nav_.push<SetSplash>(); }},
|
||||
});
|
||||
|
||||
if (battery::BatteryManagement::isDetected()) add_item({"Battery", ui::Color::dark_cyan(), &bitmap_icon_batt_icon, [this]() { nav_.push<SetBatteryView>(); }});
|
||||
|
||||
@@ -343,77 +343,72 @@ class SetUIView : public View {
|
||||
{"1 hour", backlight_timeout_t::Timeout3600Sec},
|
||||
}};
|
||||
|
||||
Checkbox checkbox_showsplash{
|
||||
{3 * 8, 6 * 16},
|
||||
20,
|
||||
"Show splash"};
|
||||
|
||||
Checkbox checkbox_showclock{
|
||||
{3 * 8, 8 * 16},
|
||||
{3 * 8, 6 * 16},
|
||||
20,
|
||||
"Show clock with:"};
|
||||
|
||||
OptionsField options_clockformat{
|
||||
{60, 9 * 16 + 8},
|
||||
{60, 7 * 16 + 8},
|
||||
20,
|
||||
{{"time only", 0},
|
||||
{"time and date", 1}}};
|
||||
|
||||
Checkbox checkbox_guireturnflag{
|
||||
{3 * 8, 11 * 16},
|
||||
{3 * 8, 9 * 16},
|
||||
20,
|
||||
"Back button in menu"};
|
||||
|
||||
Labels labels{
|
||||
{{3 * 8, 13 * 16}, "Show/Hide Status Icons", Theme::getInstance()->fg_light->foreground},
|
||||
{{3 * 8, 11 * 16}, "Show/Hide Status Icons", Theme::getInstance()->fg_light->foreground},
|
||||
};
|
||||
|
||||
ImageToggle toggle_camera{
|
||||
{2 * 8, 14 * 16 + 2, 16, 16},
|
||||
{2 * 8, 12 * 16 + 2, 16, 16},
|
||||
&bitmap_icon_camera};
|
||||
|
||||
ImageToggle toggle_sleep{
|
||||
{4 * 8, 14 * 16 + 2, 16, 16},
|
||||
{4 * 8, 12 * 16 + 2, 16, 16},
|
||||
&bitmap_icon_sleep};
|
||||
|
||||
ImageToggle toggle_stealth{
|
||||
{6 * 8, 14 * 16 + 2, 16, 16},
|
||||
{6 * 8, 12 * 16 + 2, 16, 16},
|
||||
&bitmap_icon_stealth};
|
||||
|
||||
ImageToggle toggle_converter{
|
||||
{8 * 8, 14 * 16 + 2, 16, 16},
|
||||
{8 * 8, 12 * 16 + 2, 16, 16},
|
||||
&bitmap_icon_upconvert};
|
||||
|
||||
ImageToggle toggle_bias_tee{
|
||||
{10 * 8, 14 * 16 + 2, 16, 16},
|
||||
{10 * 8, 12 * 16 + 2, 16, 16},
|
||||
&bitmap_icon_biast_off};
|
||||
|
||||
ImageToggle toggle_clock{
|
||||
{12 * 8, 14 * 16 + 2, 8, 16},
|
||||
{12 * 8, 12 * 16 + 2, 8, 16},
|
||||
&bitmap_icon_clk_ext};
|
||||
|
||||
ImageToggle toggle_mute{
|
||||
{13 * 8, 14 * 16 + 2, 16, 16},
|
||||
{13 * 8, 12 * 16 + 2, 16, 16},
|
||||
&bitmap_icon_speaker_and_headphones_mute};
|
||||
|
||||
ImageToggle toggle_speaker{
|
||||
{15 * 8, 14 * 16 + 2, 16, 16},
|
||||
{15 * 8, 12 * 16 + 2, 16, 16},
|
||||
&bitmap_icon_speaker_mute};
|
||||
|
||||
ImageToggle toggle_battery_icon{
|
||||
{17 * 8, 14 * 16 + 2, 16, 16},
|
||||
{17 * 8, 12 * 16 + 2, 16, 16},
|
||||
&bitmap_icon_batt_icon};
|
||||
|
||||
ImageToggle toggle_battery_text{
|
||||
{19 * 8, 14 * 16 + 2, 16, 16},
|
||||
{19 * 8, 12 * 16 + 2, 16, 16},
|
||||
&bitmap_icon_batt_text};
|
||||
|
||||
ImageToggle toggle_fake_brightness{
|
||||
{21 * 8, 14 * 16 + 2, 16, 16},
|
||||
{21 * 8, 12 * 16 + 2, 16, 16},
|
||||
&bitmap_icon_brightness};
|
||||
|
||||
ImageToggle toggle_sd_card{
|
||||
{23 * 8, 14 * 16 + 2, 16, 16},
|
||||
{23 * 8, 12 * 16 + 2, 16, 16},
|
||||
&bitmap_sd_card_ok};
|
||||
|
||||
Button button_save{
|
||||
@@ -1069,6 +1064,43 @@ class SettingsMenuView : public BtnGridView {
|
||||
void on_populate() override;
|
||||
};
|
||||
|
||||
class SetSplash : public View {
|
||||
public:
|
||||
SetSplash(NavigationView& nav);
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "Splash"; };
|
||||
|
||||
private:
|
||||
bool splash_bmp_exists = false;
|
||||
bool random_enabled = false;
|
||||
|
||||
Checkbox checkbox_showsplash{
|
||||
{3 * 8, 1 * 16},
|
||||
20,
|
||||
"Show splash"};
|
||||
|
||||
Checkbox checkbox_randomsplash{
|
||||
{3 * 8, 3 * 16},
|
||||
20,
|
||||
"Random splash on boot"};
|
||||
|
||||
Text message{{UI_POS_X(1), UI_POS_Y(6), UI_POS_WIDTH(26), UI_POS_HEIGHT(1)}, "Pick image to stop random."};
|
||||
|
||||
Button button_picture_select{
|
||||
{UI_POS_X_CENTER(8) - UI_POS_WIDTH(4), UI_POS_Y_BOTTOM(7), UI_POS_WIDTH(13), UI_POS_HEIGHT(2)},
|
||||
"Select pict."};
|
||||
|
||||
Button button_save{
|
||||
{UI_POS_X_CENTER(12) - UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(12), UI_POS_HEIGHT(2)},
|
||||
"Save"};
|
||||
|
||||
Button button_cancel{
|
||||
{UI_POS_X_CENTER(16) + UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(12), UI_POS_HEIGHT(2)},
|
||||
"Cancel"};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
#endif /*__UI_SETTINGS_H__*/
|
||||
|
||||
@@ -27,8 +27,6 @@ namespace fs = std::filesystem;
|
||||
|
||||
namespace ui {
|
||||
|
||||
const std::filesystem::path splash_dot_bmp{u"/splash.bmp"};
|
||||
|
||||
ScreenshotViewer::ScreenshotViewer(
|
||||
NavigationView& nav,
|
||||
const std::filesystem::path& path)
|
||||
@@ -44,7 +42,6 @@ bool ScreenshotViewer::on_key(KeyEvent) {
|
||||
|
||||
void ScreenshotViewer::paint(Painter& painter) {
|
||||
File file{};
|
||||
|
||||
painter.fill_rectangle({0, 0, screen_width, screen_height}, Color::black());
|
||||
|
||||
auto show_invalid = [&]() {
|
||||
|
||||
@@ -28,11 +28,10 @@
|
||||
#include "ui_painter.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "file.hpp"
|
||||
#include "file_path.hpp"
|
||||
|
||||
namespace ui {
|
||||
|
||||
extern const std::filesystem::path splash_dot_bmp;
|
||||
|
||||
class ScreenshotViewer : public View {
|
||||
public:
|
||||
ScreenshotViewer(NavigationView& nav, const std::filesystem::path& path);
|
||||
@@ -49,7 +48,6 @@ class SplashViewer : public View {
|
||||
SplashViewer(NavigationView& nav, const std::filesystem::path& path);
|
||||
bool on_key(KeyEvent key) override;
|
||||
void paint(Painter& painter) override;
|
||||
void update_ss(void);
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "ui_textentry.hpp"
|
||||
#include "../keeloq_keystore.hpp"
|
||||
#include "../keeloq_file.hpp"
|
||||
#include "../keeloq_common.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
@@ -60,6 +61,19 @@ void SubGhzDRecentEntryDetailView::update_data() {
|
||||
|
||||
if (entry_.data != 0) console.writeln("Data: " + to_string_hex(entry_.data));
|
||||
|
||||
if (entry_.sensorType == FPS_RESTAURANT_PAGER) {
|
||||
uint8_t pager_addr = (entry_.data >> 5) & 0x0F;
|
||||
uint8_t func_code = (entry_.data >> 1) & 0x0F;
|
||||
console.writeln("Station: 0x" + to_string_hex(serial) + " (" + to_string_dec_uint(serial) + ")");
|
||||
console.writeln("Pager: " + to_string_dec_uint(pager_addr));
|
||||
if (func_code == 0x0D)
|
||||
console.writeln("Action: Buzz");
|
||||
else if (func_code == 0x0F)
|
||||
console.writeln("Action: Sync");
|
||||
else
|
||||
console.writeln("Action: 0x" + to_string_hex(func_code));
|
||||
}
|
||||
|
||||
if (entry_.sensorType == FPS_KEELOQ) {
|
||||
console.writeln("Fix: " + to_string_hex(fix));
|
||||
console.writeln("Encrypted: " + to_string_hex(encrypted));
|
||||
@@ -82,7 +96,7 @@ void SubGhzDRecentEntryDetailView::update_data() {
|
||||
KeeloqData params{
|
||||
mf_name,
|
||||
serial,
|
||||
cnt,
|
||||
(uint16_t)cnt,
|
||||
btn};
|
||||
|
||||
ensure_directory(keeloq_remotes_dir);
|
||||
@@ -279,6 +293,8 @@ const char* SubGhzDView::getSensorTypeName(FPROTO_SUBGHZD_SENSOR type) {
|
||||
return "Marantec24";
|
||||
case FPS_HOLTEKHT6P20B:
|
||||
return "Holtek HT6P20B";
|
||||
case FPS_RESTAURANT_PAGER:
|
||||
return "Rest. Pager";
|
||||
case FPS_Invalid:
|
||||
default:
|
||||
return "Unknown";
|
||||
@@ -305,7 +321,14 @@ void RecentEntriesTable<ui::SubGhzDRecentEntries>::draw(
|
||||
line.reserve(30);
|
||||
|
||||
line = SubGhzDView::getSensorTypeName((FPROTO_SUBGHZD_SENSOR)entry.sensorType);
|
||||
line = line + " " + to_string_hex(entry.data << 32);
|
||||
if (entry.sensorType == FPS_RESTAURANT_PAGER) {
|
||||
uint8_t pgr = (entry.data >> 5) & 0x0F;
|
||||
uint8_t func = (entry.data >> 1) & 0x0F;
|
||||
line += " P:" + to_string_dec_uint(pgr) + (func == 0x0D ? " Buzz" : func == 0x0F ? " Sync"
|
||||
: "");
|
||||
} else {
|
||||
line = line + " " + to_string_hex(entry.data << 32);
|
||||
}
|
||||
line.resize(columns.at(0).second, ' ');
|
||||
std::string ageStr = to_string_dec_uint(entry.age);
|
||||
std::string bitsStr = to_string_dec_uint(entry.bits);
|
||||
@@ -361,28 +384,6 @@ bool SubGhzDRecentEntryDetailView::keeloq_check_decrypt_centurion(uint32_t decry
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t keeloq_decrypt(const uint32_t data, const uint64_t key) {
|
||||
uint32_t x = data, r;
|
||||
for (r = 0; r < 528; r++)
|
||||
x = (x << 1) ^ bit(x, 31) ^ bit(x, 15) ^ (uint32_t)bit(key, (15 - r) & 63) ^
|
||||
bit(KEELOQ_NLF, g5(x, 0, 8, 19, 25, 30));
|
||||
return x;
|
||||
}
|
||||
|
||||
uint64_t keeloq_normal_learning(uint32_t data, const uint64_t key) {
|
||||
uint32_t k1, k2;
|
||||
|
||||
data &= 0x0FFFFFFF;
|
||||
data |= 0x20000000;
|
||||
k1 = keeloq_decrypt(data, key);
|
||||
|
||||
data &= 0x0FFFFFFF;
|
||||
data |= 0x60000000;
|
||||
k2 = keeloq_decrypt(data, key);
|
||||
|
||||
return ((uint64_t)k2 << 32) | k1;
|
||||
}
|
||||
|
||||
const uint32_t came_twee_magic_numbers_xor[15] = {
|
||||
0x0E0E0E00,
|
||||
0x1D1D1D11,
|
||||
@@ -894,5 +895,12 @@ void SubGhzDRecentEntryDetailView::parseProtocol() {
|
||||
btn = (entry_.data >> 4) & 0xF;
|
||||
return;
|
||||
}
|
||||
|
||||
if (entry_.sensorType == FPS_RESTAURANT_PAGER) {
|
||||
// 25-bit EV1527-variant: [sysid:16][pager:4][func:4][stop:1]
|
||||
serial = (entry_.data >> 9) & 0xFFFF; // System ID
|
||||
// btn/cnt left as SD_NO values; friendly output in update_data()
|
||||
return;
|
||||
}
|
||||
}
|
||||
} // namespace ui
|
||||
|
||||
@@ -31,12 +31,6 @@
|
||||
#define SD_NO_FIX 0xFFFFFFFF
|
||||
#define SD_NO_ENCRYPTED 0xFFFFFFFF
|
||||
|
||||
#define bit(x, n) (((x) >> (n)) & 1)
|
||||
#define g5(x, a, b, c, d, e) \
|
||||
(bit(x, a) + bit(x, b) * 2 + bit(x, c) * 4 + bit(x, d) * 8 + bit(x, e) * 16)
|
||||
|
||||
#define KEELOQ_NLF 0x3A5C742E
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
|
||||
@@ -397,6 +397,21 @@ void set_bitstream_config(uint32_t deviation, uint8_t mode) {
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
void set_rtty_config(uint16_t baud, uint16_t shift, uint8_t* payload, uint16_t payload_length) {
|
||||
RTTYDataMessage message{baud, shift};
|
||||
if (payload && payload_length > 0) {
|
||||
message.data_len = payload_length > message.max_len ? message.max_len : payload_length;
|
||||
for (size_t i = 0; i < message.data_len; ++i) {
|
||||
message.data[i] = payload[i];
|
||||
}
|
||||
}
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
void set_rtty_config(RTTYDataMessage& message) {
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
static bool baseband_image_running = false;
|
||||
|
||||
void run_image(const spi_flash::image_tag_t image_tag) {
|
||||
@@ -458,7 +473,12 @@ void shutdown() {
|
||||
send_message(&message);
|
||||
|
||||
shared_memory.application_queue.reset();
|
||||
|
||||
// Allow time for the shutdown message to be processed and for the baseband
|
||||
// core to stop before starting another image. Otherwise, the M4 may still be
|
||||
// running and cause a crash when the next image is started.
|
||||
#ifdef PRALINE
|
||||
chThdSleepMilliseconds(20);
|
||||
#endif
|
||||
baseband_image_running = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -108,7 +108,9 @@ void set_morsetx_key(bool key_down);
|
||||
void set_wefax_config(uint8_t lpm, uint8_t ioc);
|
||||
void set_noaaapt_config();
|
||||
void set_flex_config();
|
||||
void set_bitstream_config(uint32_t deviation, uint8_t mode); // mode 0 for am, 1 for 2fsk
|
||||
void set_bitstream_config(uint32_t deviation, uint8_t mode); // mode 0 for am, 1 for 2fsk
|
||||
void set_rtty_config(uint16_t baud, uint16_t shift, uint8_t* payload = nullptr, uint16_t payload_length = 0); // baud*100
|
||||
void set_rtty_config(RTTYDataMessage& message);
|
||||
|
||||
void request_roger_beep();
|
||||
void request_rssi_beep();
|
||||
|
||||
@@ -237,19 +237,40 @@ constexpr ClockControls si5351c_clock_control_common{{
|
||||
}};
|
||||
|
||||
constexpr ClockControls si5351a_clock_control_common{{
|
||||
#ifndef PRALINE
|
||||
#ifdef PRALINE
|
||||
// CLK0: MAX5864 (ADC)
|
||||
{ClockControl::ClockCurrentDrive::_4mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
|
||||
// CLK1: SCT_CLK - iCE40 FPGA timing clock
|
||||
{ClockControl::ClockCurrentDrive::_6mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
|
||||
// CLK2: LPC43xx MCU
|
||||
{ClockControl::ClockCurrentDrive::_4mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
|
||||
// CLK3: CLKOUT (optional) SMA Port P1
|
||||
{ClockControl::ClockCurrentDrive::_8mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
|
||||
// CLK4: PRALINE RFFC5072 reference (40 MHz) - INVERTED, 6mA, Integer mode
|
||||
// This matches HackRF One OG configuration for RFFC5072
|
||||
{ClockControl::ClockCurrentDrive::_6mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Invert, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
|
||||
// CLK5: PRALINE MAX2831 reference (40 MHz) - INVERTED per hackrf_usb, 4mA, Integer mode
|
||||
{ClockControl::ClockCurrentDrive::_4mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Invert, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
|
||||
// CLK6: SMA Port P2
|
||||
{ClockControl::ClockCurrentDrive::_8mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
|
||||
#else
|
||||
{ClockControl::ClockCurrentDrive::_6mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Fractional, ClockControl::ClockPowerDown::Power_Off}, // CLK0: MUST be Fractional for 8 MHz!
|
||||
#endif
|
||||
{ClockControl::ClockCurrentDrive::_6mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
|
||||
{ClockControl::ClockCurrentDrive::_4mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Fractional, ClockControl::ClockPowerDown::Power_Off},
|
||||
{ClockControl::ClockCurrentDrive::_8mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
|
||||
{ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
|
||||
// CLK4: HackRF r9 - not inverted
|
||||
{ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
|
||||
// CLK5: HackRF r9 - not used
|
||||
{ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
|
||||
// CLK6: Not used
|
||||
{ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
|
||||
#endif
|
||||
|
||||
// CLK7: Not used
|
||||
{ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
|
||||
|
||||
}};
|
||||
|
||||
ClockManager::Reference ClockManager::get_reference() const {
|
||||
return reference;
|
||||
}
|
||||
@@ -659,6 +680,8 @@ void ClockManager::set_sampling_frequency(const uint32_t frequency) {
|
||||
/* PRALINE: Match HackRF USB sample_rate_frac_set()
|
||||
* Reference: hackrf_usb radio.c lines 29-91, hackrf_core.c lines 501-685 */
|
||||
|
||||
_base_band_frequency = frequency;
|
||||
|
||||
// Set FPGA decimation to 0 (no decimation) for direct passthrough
|
||||
fpga_debug_register_write(2, 0);
|
||||
radio::invalidate_spi_config();
|
||||
|
||||
@@ -70,6 +70,10 @@ class ClockManager {
|
||||
|
||||
void set_sampling_frequency(const uint32_t frequency);
|
||||
|
||||
uint32_t get_sampling_frequency() const {
|
||||
return _base_band_frequency;
|
||||
};
|
||||
|
||||
void set_reference_ppb(const int32_t ppb);
|
||||
|
||||
#ifdef PRALINE
|
||||
@@ -92,6 +96,8 @@ class ClockManager {
|
||||
si5351::Si5351& clock_generator;
|
||||
Reference reference;
|
||||
|
||||
uint32_t _base_band_frequency{20000000};
|
||||
|
||||
void set_gp_clkin_to_clkin_direct();
|
||||
|
||||
void start_frequency_monitor_measurement(const cgu::CLK_SEL clk_sel);
|
||||
|
||||
@@ -256,7 +256,9 @@ ui::Widget* EventDispatcher::touch_widget(ui::Widget* const w, ui::TouchEvent ev
|
||||
if (!w->hidden()) {
|
||||
// To achieve reverse depth ordering (last object drawn is
|
||||
// considered "top"), descend first.
|
||||
for (const auto child : w->children()) {
|
||||
auto& children = w->children();
|
||||
for (auto it = children.rbegin(); it != children.rend(); ++it) { // reverse, bc the lastly added will be "top" if overlaps
|
||||
const auto& child = *it;
|
||||
const auto touched_widget = touch_widget(child, event);
|
||||
if (touched_widget) {
|
||||
return touched_widget;
|
||||
|
||||
+73
-4
@@ -33,6 +33,76 @@ using namespace portapack;
|
||||
|
||||
namespace ui::external_app::acars_rx {
|
||||
|
||||
// ACARS frame field layout (0-based byte offsets):
|
||||
// [0] SOH framing byte (skipped)
|
||||
// [1..7] Aircraft registration (7 chars)
|
||||
// [8] STX framing byte (skipped)
|
||||
// [9..10] Label (2 chars)
|
||||
// [11] Block ID (1 char)
|
||||
// [12..14] Message number (3 chars)
|
||||
// [15..20] Flight ID (6 chars)
|
||||
// [21..N-3] Free-text payload (variable)
|
||||
// [N-2..N-1] CRC-16/CCITT (2 bytes, MSB first, NOT part of payload)
|
||||
//
|
||||
// Minimum valid frame: 21-byte fixed header + 2 CRC bytes = 23 bytes.
|
||||
// (A zero-length payload is legal per spec; we require at least 23 bytes.)
|
||||
static constexpr std::string::size_type kAcarsCrcLen = 2;
|
||||
static constexpr std::string::size_type kAcarsHeaderLen = 21;
|
||||
static constexpr std::string::size_type kAcarsMinLen = kAcarsHeaderLen + kAcarsCrcLen;
|
||||
|
||||
// CRC-16/CCITT: poly 0x1021, init 0x0000, no reflection, no final XOR.
|
||||
// This is the variant used by ACARS (same as XMODEM CRC).
|
||||
static uint16_t acars_crc16(const std::string& data, std::string::size_type len) {
|
||||
uint16_t crc = 0x0000;
|
||||
for (std::string::size_type i = 0; i < len; ++i) {
|
||||
crc ^= static_cast<uint16_t>(static_cast<uint8_t>(data[i])) << 8;
|
||||
for (int bit = 0; bit < 8; ++bit) {
|
||||
if (crc & 0x8000)
|
||||
crc = static_cast<uint16_t>((crc << 1) ^ 0x1021);
|
||||
else
|
||||
crc = static_cast<uint16_t>(crc << 1);
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
AcarsDecoded acars_decode(const std::string& raw) {
|
||||
AcarsDecoded result;
|
||||
if (raw.size() < kAcarsMinLen) {
|
||||
result.txt = "ACARS message too short (" + std::to_string(raw.size()) +
|
||||
" bytes, need " + std::to_string(kAcarsMinLen) + ")";
|
||||
return result;
|
||||
}
|
||||
|
||||
// Verify CRC: computed over everything except the last 2 bytes,
|
||||
// then compared against those 2 bytes (MSB first).
|
||||
const std::string::size_type payload_len = raw.size() - kAcarsCrcLen;
|
||||
const uint16_t computed = acars_crc16(raw, payload_len);
|
||||
const uint16_t received = (static_cast<uint16_t>(static_cast<uint8_t>(raw[raw.size() - 2])) << 8) |
|
||||
static_cast<uint16_t>(static_cast<uint8_t>(raw[raw.size() - 1]));
|
||||
result.crc_ok = (computed == received);
|
||||
|
||||
result.reg = raw.substr(1, 7);
|
||||
result.label = raw.substr(9, 2);
|
||||
result.block_id = raw[11];
|
||||
result.msg_num = raw.substr(12, 3);
|
||||
result.flight_id = raw.substr(15, 6);
|
||||
// Payload sits between end of fixed header and the 2 trailing CRC bytes.
|
||||
if (payload_len > kAcarsHeaderLen)
|
||||
result.txt = raw.substr(kAcarsHeaderLen, payload_len - kAcarsHeaderLen);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string acars_format(const AcarsDecoded& msg) {
|
||||
return std::string("ACARS Decoded Result\nCRC: ") + (msg.crc_ok ? "OK" : "FAIL") +
|
||||
"\nRegistration: " + msg.reg +
|
||||
"\nLabel: " + msg.label +
|
||||
"\nBlockID: " + msg.block_id +
|
||||
"\nMsgNum: " + msg.msg_num +
|
||||
"\nFlightID: " + msg.flight_id +
|
||||
"\nMessage: " + msg.txt;
|
||||
}
|
||||
|
||||
void ACARSLogger::log_str(std::string msg) {
|
||||
log_file.write_entry(msg);
|
||||
}
|
||||
@@ -77,17 +147,16 @@ void ACARSAppView::focus() {
|
||||
|
||||
void ACARSAppView::on_packet(const ACARSPacketMessage* packet) {
|
||||
std::string console_info;
|
||||
|
||||
if (packet->state == 255) {
|
||||
// got a packet, parse it, and display
|
||||
rtc::RTC datetime;
|
||||
rtc_time::now(datetime);
|
||||
// todo parity error recovery
|
||||
console_info = to_string_datetime(datetime, HMS);
|
||||
console_info += ": ";
|
||||
console_info += packet->message;
|
||||
std::string message{packet->message, packet->message + packet->msg_len};
|
||||
AcarsDecoded decoded = acars_decode(message);
|
||||
console_info += acars_format(decoded);
|
||||
console.writeln(console_info);
|
||||
// Log raw data whatever it contains
|
||||
if (logger && logging)
|
||||
logger->log_str(console_info);
|
||||
} else {
|
||||
|
||||
+21
-1
@@ -33,6 +33,26 @@
|
||||
|
||||
namespace ui::external_app::acars_rx {
|
||||
|
||||
// Decoded ACARS message fields extracted from a raw frame.
|
||||
// CRC-16/CCITT (poly 0x1021, init 0x0000) is verified against the two
|
||||
// trailing bytes of the raw frame; crc_ok reflects that result.
|
||||
struct AcarsDecoded {
|
||||
bool crc_ok{false};
|
||||
std::string reg{};
|
||||
std::string label{};
|
||||
std::string flight_id{};
|
||||
std::string msg_num{};
|
||||
char block_id{'\0'};
|
||||
std::string txt{};
|
||||
};
|
||||
|
||||
// Decode a raw ACARS frame: verify CRC-16/CCITT and extract fixed-offset fields.
|
||||
// Returns a partially-filled AcarsDecoded (txt error only) if the frame is too short.
|
||||
AcarsDecoded acars_decode(const std::string& raw);
|
||||
|
||||
// Format a decoded ACARS message for display or logging.
|
||||
std::string acars_format(const AcarsDecoded& msg);
|
||||
|
||||
class ACARSLogger {
|
||||
public:
|
||||
Optional<File::Error> append(const std::filesystem::path& filename) {
|
||||
@@ -106,4 +126,4 @@ class ACARSAppView : public View {
|
||||
|
||||
} // namespace ui::external_app::acars_rx
|
||||
|
||||
#endif /*__ACARS_APP_H__*/
|
||||
#endif /*__ACARS_APP_H__*/
|
||||
|
||||
+83
-83
@@ -1,83 +1,83 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Bernd Herzog
|
||||
*
|
||||
* 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.hpp"
|
||||
#include "acars_app.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::acars_rx {
|
||||
void initialize_app(ui::NavigationView& nav) {
|
||||
nav.push<ACARSAppView>();
|
||||
}
|
||||
} // namespace ui::external_app::acars_rx
|
||||
|
||||
extern "C" {
|
||||
|
||||
__attribute__((section(".external_app.app_acars_rx.application_information"), used)) application_information_t _application_information_acars_rx = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||
/*.externalAppEntry = */ ui::external_app::acars_rx::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "ACARS",
|
||||
/*.bitmap_data = */ {
|
||||
0x80,
|
||||
0x01,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xE0,
|
||||
0x07,
|
||||
0xF8,
|
||||
0x1F,
|
||||
0xFE,
|
||||
0x7F,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xE0,
|
||||
0x07,
|
||||
0xF0,
|
||||
0x0F,
|
||||
0xF8,
|
||||
0x1F,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::orange().v,
|
||||
/*.menu_location = */ app_location_t::RX,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_acars */ {'P', 'A', 'C', 'A'},
|
||||
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
|
||||
};
|
||||
}
|
||||
/*
|
||||
* Copyright (C) 2023 Bernd Herzog
|
||||
*
|
||||
* 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.hpp"
|
||||
#include "acars_app.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::acars_rx {
|
||||
void initialize_app(ui::NavigationView& nav) {
|
||||
nav.push<ACARSAppView>();
|
||||
}
|
||||
} // namespace ui::external_app::acars_rx
|
||||
|
||||
extern "C" {
|
||||
|
||||
__attribute__((section(".external_app.app_acars_rx.application_information"), used)) application_information_t _application_information_acars_rx = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||
/*.externalAppEntry = */ ui::external_app::acars_rx::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "ACARS",
|
||||
/*.bitmap_data = */ {
|
||||
0x80,
|
||||
0x01,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xE0,
|
||||
0x07,
|
||||
0xF8,
|
||||
0x1F,
|
||||
0xFE,
|
||||
0x7F,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xE0,
|
||||
0x07,
|
||||
0xF0,
|
||||
0x0F,
|
||||
0xF8,
|
||||
0x1F,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::orange().v,
|
||||
/*.menu_location = */ app_location_t::RX,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_acars */ {'P', 'A', 'C', 'A'},
|
||||
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
|
||||
};
|
||||
}
|
||||
|
||||
+1
-1
@@ -339,7 +339,7 @@ void AdultToysView::randomizeMac() {
|
||||
}
|
||||
|
||||
void AdultToysView::randomChn() {
|
||||
channel_number = 37 + std::rand() % (39 - 37 + 1);
|
||||
channel_number = 37 + rand() % (39 - 37 + 1);
|
||||
field_frequency.set_value(get_freq_by_channel_number(channel_number));
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ void BlackjackView::paint(Painter& painter) {
|
||||
|
||||
if (!initialized) {
|
||||
initialized = true;
|
||||
std::srand(LPC_RTC->CTIME0);
|
||||
srand(LPC_RTC->CTIME0);
|
||||
init_deck();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -132,7 +132,7 @@ void BLESpamView::reset() {
|
||||
}
|
||||
|
||||
void BLESpamView::randomChn() {
|
||||
channel_number = 37 + std::rand() % (39 - 37 + 1);
|
||||
channel_number = 37 + rand() % (39 - 37 + 1);
|
||||
field_frequency.set_value(get_freq_by_channel_number(channel_number));
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -655,7 +655,7 @@ void BreakoutView::paint(Painter& painter) {
|
||||
|
||||
if (!initialized) {
|
||||
initialized = true;
|
||||
std::srand(LPC_RTC->CTIME0);
|
||||
srand(LPC_RTC->CTIME0);
|
||||
init_game();
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -125,7 +125,7 @@ void DinoGameView::paint(Painter& painter) {
|
||||
|
||||
if (!initialized) {
|
||||
initialized = true;
|
||||
std::srand(LPC_RTC->CTIME0);
|
||||
srand(LPC_RTC->CTIME0);
|
||||
init_game();
|
||||
}
|
||||
}
|
||||
@@ -789,8 +789,8 @@ void DinoGameView::draw_current_score() {
|
||||
|
||||
void DinoGameView::draw_high_score() {
|
||||
auto style = *ui::Theme::getInstance()->fg_light;
|
||||
painter.fill_rectangle({UI_POS_X_RIGHT(90), 28, 90, 18}, Color::black());
|
||||
painter.draw_string({UI_POS_X_RIGHT(90), 30}, style, "HI " + score_to_string(highScore));
|
||||
painter.fill_rectangle({UI_POS_X_RIGHT(10), 28, 90, 18}, Color::black());
|
||||
painter.draw_string({UI_POS_X_RIGHT(10), 30}, style, "HI " + score_to_string(highScore));
|
||||
}
|
||||
|
||||
void DinoGameView::jump() {
|
||||
|
||||
+15
-15
@@ -204,11 +204,11 @@ void spawn_entity(uint8_t type, uint8_t x, uint8_t y) {
|
||||
|
||||
void spawn_random_entity(uint8_t type) {
|
||||
if (num_entities >= MAX_ENTITIES) return;
|
||||
std::srand(LPC_RTC->CTIME0);
|
||||
srand(LPC_RTC->CTIME0);
|
||||
uint8_t spawn_x, spawn_y;
|
||||
do {
|
||||
spawn_x = std::rand() % LEVEL_WIDTH;
|
||||
spawn_y = std::rand() % LEVEL_HEIGHT;
|
||||
spawn_x = rand() % LEVEL_WIDTH;
|
||||
spawn_y = rand() % LEVEL_HEIGHT;
|
||||
} while (get_block_at(spawn_x, spawn_y) == 0xF ||
|
||||
(spawn_x == (uint8_t)player.pos.x && spawn_y == (uint8_t)player.pos.y));
|
||||
|
||||
@@ -227,9 +227,9 @@ void remove_entity(uint8_t index) {
|
||||
bool spawned = false;
|
||||
|
||||
while (!spawned && attempts < 50) {
|
||||
std::srand(LPC_RTC->CTIME0 + attempts);
|
||||
uint8_t spawn_x = std::rand() % LEVEL_WIDTH;
|
||||
uint8_t spawn_y = std::rand() % LEVEL_HEIGHT;
|
||||
srand(LPC_RTC->CTIME0 + attempts);
|
||||
uint8_t spawn_x = rand() % LEVEL_WIDTH;
|
||||
uint8_t spawn_y = rand() % LEVEL_HEIGHT;
|
||||
|
||||
int16_t dx = (int16_t)spawn_x - (int16_t)player.pos.x;
|
||||
int16_t dy = (int16_t)spawn_y - (int16_t)player.pos.y;
|
||||
@@ -270,9 +270,9 @@ void initialize_level() {
|
||||
uint8_t attempts = 0;
|
||||
|
||||
while (initial_enemies < 2 && num_entities < MAX_ENTITIES && attempts < 50) {
|
||||
std::srand(LPC_RTC->CTIME0 + attempts);
|
||||
int8_t offset_x = (std::rand() % 11) - 5;
|
||||
int8_t offset_y = (std::rand() % 11) - 5;
|
||||
srand(LPC_RTC->CTIME0 + attempts);
|
||||
int8_t offset_x = (rand() % 11) - 5;
|
||||
int8_t offset_y = (rand() % 11) - 5;
|
||||
|
||||
if (abs(offset_x) < 3 && abs(offset_y) < 3) {
|
||||
attempts++;
|
||||
@@ -307,10 +307,10 @@ void initialize_level() {
|
||||
|
||||
attempts = 0;
|
||||
while (num_entities < MIN_ENTITIES && attempts < 100) {
|
||||
std::srand(LPC_RTC->CTIME0 + attempts + 100);
|
||||
srand(LPC_RTC->CTIME0 + attempts + 100);
|
||||
|
||||
uint8_t spawn_x = std::rand() % LEVEL_WIDTH;
|
||||
uint8_t spawn_y = std::rand() % LEVEL_HEIGHT;
|
||||
uint8_t spawn_x = rand() % LEVEL_WIDTH;
|
||||
uint8_t spawn_y = rand() % LEVEL_HEIGHT;
|
||||
|
||||
int16_t dx = (int16_t)spawn_x - (int16_t)player.pos.x;
|
||||
int16_t dy = (int16_t)spawn_y - (int16_t)player.pos.y;
|
||||
@@ -1045,7 +1045,7 @@ void DoomView::on_show() {
|
||||
void DoomView::paint(Painter& painter) {
|
||||
if (!initialized) {
|
||||
initialized = true;
|
||||
std::srand(LPC_RTC->CTIME0);
|
||||
srand(LPC_RTC->CTIME0);
|
||||
scene = 0;
|
||||
up = down = left = right = fired = false;
|
||||
jogging = view_height = 0;
|
||||
@@ -1081,7 +1081,7 @@ void DoomView::paint(Painter& painter) {
|
||||
int hud_y = RENDER_HEIGHT + 5;
|
||||
painter.draw_string({5, hud_y}, style_yellow, "Health: " + std::to_string(player.health));
|
||||
painter.draw_string({UI_POS_X_CENTER(15), hud_y}, style_red, "Kills: " + std::to_string(kills));
|
||||
painter.draw_string({UI_POS_X_RIGHT(80), hud_y}, style_blue, "Ammo: " + std::to_string(player.ammo));
|
||||
painter.draw_string({UI_POS_X_RIGHT(10), hud_y}, style_blue, "Ammo: " + std::to_string(player.ammo));
|
||||
|
||||
prev_velocity_moving = (player.velocity != 0);
|
||||
needs_redraw = false;
|
||||
@@ -1113,7 +1113,7 @@ void DoomView::paint(Painter& painter) {
|
||||
int hud_y = RENDER_HEIGHT + 5;
|
||||
painter.draw_string({5, hud_y}, style_yellow, "Health: " + std::to_string(player.health));
|
||||
painter.draw_string({UI_POS_X_CENTER(15), hud_y}, style_red, "Kills: " + std::to_string(kills));
|
||||
painter.draw_string({UI_POS_X_RIGHT(80), hud_y}, style_blue, "Ammo: " + std::to_string(player.ammo));
|
||||
painter.draw_string({UI_POS_X_RIGHT(10), hud_y}, style_blue, "Ammo: " + std::to_string(player.ammo));
|
||||
|
||||
needs_gun_redraw = false;
|
||||
}
|
||||
|
||||
+28
-3
@@ -83,6 +83,10 @@ set(EXTCPPSRC
|
||||
external/tpmsrx/main.cpp
|
||||
external/tpmsrx/tpms_app.cpp
|
||||
|
||||
#tpmstx 800 bytes - TPMS transmit with editable fields
|
||||
external/tpmstx/main.cpp
|
||||
external/tpmstx/tpms_tx_app.cpp
|
||||
|
||||
#protoview 8 byte
|
||||
external/protoview/main.cpp
|
||||
external/protoview/ui_protoview.cpp
|
||||
@@ -109,8 +113,8 @@ set(EXTCPPSRC
|
||||
external/random_password/sha512.cpp
|
||||
|
||||
#acars
|
||||
#external/acars_rx/main.cpp
|
||||
#external/acars_rx/acars_app.cpp
|
||||
external/acars_rx/main.cpp
|
||||
external/acars_rx/acars_app.cpp
|
||||
|
||||
#wefax_rx 192 bytes
|
||||
external/wefax_rx/main.cpp
|
||||
@@ -292,6 +296,22 @@ set(EXTCPPSRC
|
||||
#morseradiotx
|
||||
external/morseradiotx/main.cpp
|
||||
external/morseradiotx/ui_morse_radiotx.cpp
|
||||
|
||||
external/keeloqtx/main.cpp
|
||||
external/keeloqtx/ui_keeloqtx.cpp
|
||||
#rtty_rx
|
||||
external/rtty_rx/main.cpp
|
||||
external/rtty_rx/ui_rtty_rx.cpp
|
||||
external/rtty_rx/baudot.cpp
|
||||
|
||||
#rtty_tx
|
||||
external/rtty_tx/main.cpp
|
||||
external/rtty_tx/ui_rtty_tx.cpp
|
||||
external/rtty_tx/baudot.cpp
|
||||
|
||||
#pocsag_tx
|
||||
external/pocsag_tx/main.cpp
|
||||
external/pocsag_tx/ui_pocsag_tx.cpp
|
||||
)
|
||||
|
||||
set(EXTAPPLIST
|
||||
@@ -314,13 +334,14 @@ set(EXTAPPLIST
|
||||
audio_test
|
||||
wardrivemap
|
||||
tpmsrx
|
||||
tpmstx
|
||||
protoview
|
||||
adsbtx
|
||||
#morse_tx
|
||||
sstvtx
|
||||
sstvrx
|
||||
random_password
|
||||
# acars_rx --not working
|
||||
acars_rx
|
||||
wefax_rx
|
||||
noaaapt_rx
|
||||
shoppingcart_lock
|
||||
@@ -365,6 +386,10 @@ set(EXTAPPLIST
|
||||
siggen
|
||||
morse_radio
|
||||
morseradiotx
|
||||
keeloqtx
|
||||
rtty_rx
|
||||
rtty_tx
|
||||
pocsag_tx
|
||||
)
|
||||
|
||||
# sdusb has type conflicts with PRALINE (HackRF Pro) - add only for non-PRALINE builds
|
||||
|
||||
+33
@@ -94,6 +94,11 @@ MEMORY
|
||||
ram_external_app_morse_radio (rwx) : org = 0xADF50000, len = 32k
|
||||
ram_external_app_waterfall_designer (rwx) : org = 0xADF60000, len = 32k
|
||||
ram_external_app_morseradiotx (rwx) : org = 0xADF70000, len = 32k
|
||||
ram_external_app_keeloqtx (rwx) : org = 0xADF80000, len = 32k
|
||||
ram_external_app_rtty_rx (rwx) : org = 0xADF90000, len = 32k
|
||||
ram_external_app_rtty_tx (rwx) : org = 0xADFA0000, len = 32k
|
||||
ram_external_app_tpmstx (rwx) : org = 0xADFB0000, len = 32k
|
||||
ram_external_app_pocsag_tx (rwx) : org = 0xADFC0000, len = 32k
|
||||
}
|
||||
|
||||
SECTIONS
|
||||
@@ -212,6 +217,12 @@ SECTIONS
|
||||
*(*ui*external_app*tpmsrx*);
|
||||
} > ram_external_app_tpmsrx
|
||||
|
||||
.external_app_tpmstx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_tpmstx.application_information));
|
||||
*(*ui*external_app*tpmstx*);
|
||||
} > ram_external_app_tpmstx
|
||||
|
||||
.external_app_protoview : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_protoview.application_information));
|
||||
@@ -526,5 +537,27 @@ SECTIONS
|
||||
*(*ui*external_app*morseradiotx*);
|
||||
} > ram_external_app_morseradiotx
|
||||
|
||||
.external_app_keeloqtx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_keeloqtx.application_information));
|
||||
*(*ui*external_app*keeloqtx*);
|
||||
} > ram_external_app_keeloqtx
|
||||
.external_app_rtty_rx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_rtty_rx.application_information));
|
||||
*(*ui*external_app*rtty_rx*);
|
||||
} > ram_external_app_rtty_rx
|
||||
|
||||
.external_app_rtty_tx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_rtty_tx.application_information));
|
||||
*(*ui*external_app*rtty_tx*);
|
||||
} > ram_external_app_rtty_tx
|
||||
|
||||
.external_app_pocsag_tx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_pocsag_tx.application_information));
|
||||
*(*ui*external_app*pocsag_tx*);
|
||||
} > ram_external_app_pocsag_tx
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ Game2048View::Game2048View(NavigationView& nav)
|
||||
|
||||
void Game2048View::on_show() {
|
||||
if (!initialized) {
|
||||
std::srand(LPC_RTC->CTIME0);
|
||||
srand(LPC_RTC->CTIME0);
|
||||
reset_game();
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright (C) 2026 lifegame1lu111
|
||||
*
|
||||
* 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.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_keeloqtx.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::ui_keeloqtx {
|
||||
void initialize_app(ui::NavigationView& nav) {
|
||||
nav.push<KeeloqTXView>();
|
||||
}
|
||||
} // namespace ui::external_app::ui_keeloqtx
|
||||
|
||||
extern "C" {
|
||||
|
||||
__attribute__((section(".external_app.app_keeloqtx.application_information"), used)) application_information_t _application_information_keeloqtx = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||
/*.externalAppEntry = */ ui::external_app::ui_keeloqtx::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
/*.app_name = */ "KeeLoq TX",
|
||||
/*.bitmap_data = */ {
|
||||
0x20,
|
||||
0x00,
|
||||
0x20,
|
||||
0x00,
|
||||
0x20,
|
||||
0x00,
|
||||
0x20,
|
||||
0x00,
|
||||
0xE0,
|
||||
0x07,
|
||||
0xF0,
|
||||
0x0F,
|
||||
0x30,
|
||||
0x0C,
|
||||
0x30,
|
||||
0x0C,
|
||||
0xF0,
|
||||
0x0F,
|
||||
0xF0,
|
||||
0x0F,
|
||||
0x70,
|
||||
0x0D,
|
||||
0xB0,
|
||||
0x0E,
|
||||
0x70,
|
||||
0x0D,
|
||||
0xB0,
|
||||
0x0E,
|
||||
0xF0,
|
||||
0x0F,
|
||||
0xE0,
|
||||
0x07,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::yellow().v,
|
||||
/*.menu_location = */ app_location_t::TX,
|
||||
/*.desired_menu_position = */ -1,
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_ook */ {'P', 'O', 'O', 'K'},
|
||||
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
#include "ui_keeloqtx.hpp"
|
||||
#include "ui_fileman.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "../../keeloq_common.hpp"
|
||||
#include "../../baseband/fprotos/fprotogeneral.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
using namespace ui;
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace ui::external_app::ui_keeloqtx {
|
||||
|
||||
void KeeloqTXView::focus() {
|
||||
button_open.focus();
|
||||
}
|
||||
|
||||
void KeeloqTXView::update_hop() {
|
||||
hop = data.btn << 28 | (data.serial & 0x3FF) << 16 | data.counter;
|
||||
|
||||
if (data.mf_name == "Aprimatic") {
|
||||
uint32_t apri_serial = data.serial;
|
||||
uint8_t apr1 = 0;
|
||||
|
||||
for (uint16_t i = 1; i != 0b10000000000; i <<= 1) {
|
||||
if (apri_serial & i) apr1++;
|
||||
}
|
||||
|
||||
apri_serial &= 0b00001111111111;
|
||||
|
||||
if (apr1 % 2 == 0) {
|
||||
apri_serial |= 0b110000000000;
|
||||
}
|
||||
|
||||
hop = data.btn << 28 | (apri_serial & 0xFFF) << 16 | data.counter;
|
||||
} else if (
|
||||
data.mf_name == "DTM_Neo" || data.mf_name == "FAAC_RC,XT" || data.mf_name == "Mutanco_Mutancode" || data.mf_name == "Came_Space" || data.mf_name == "Genius_Bravo" || data.mf_name == "GSN" || data.mf_name == "Rosh" || data.mf_name == "Rossi" || data.mf_name == "Peccinin" || data.mf_name == "Steelmate" || data.mf_name == "Cardin_S449") {
|
||||
hop = data.btn << 28 | (data.serial & 0xFFF) << 16 | data.counter;
|
||||
} else if (
|
||||
data.mf_name == "NICE_Smilo" || data.mf_name == "NICE_MHOUSE" || data.mf_name == "JCM_Tech") {
|
||||
hop = data.btn << 28 | (data.serial & 0xFF) << 16 | data.counter;
|
||||
} else if (data.mf_name == "Merlin") {
|
||||
hop = data.btn << 28 | (0x000) << 16 | data.counter;
|
||||
} else if (data.mf_name == "Centurion") {
|
||||
hop = data.btn << 28 | (0x1CE) << 16 | data.counter;
|
||||
} else if (data.mf_name == "Monarch") {
|
||||
hop = data.btn << 28 | (0x100) << 16 | data.counter;
|
||||
} else if (data.mf_name == "Dea_Mio") {
|
||||
uint8_t first_disc_num = (data.serial >> 8) & 0xF;
|
||||
uint8_t result_disc = (0xC + (first_disc_num % 4));
|
||||
|
||||
uint32_t dea_serial = (data.serial & 0xFF) | (((uint32_t)result_disc) << 8);
|
||||
|
||||
hop = data.btn << 28 | (dea_serial & 0xFFF) << 16 | data.counter;
|
||||
}
|
||||
|
||||
text_hop.set(to_string_hex(hop));
|
||||
}
|
||||
|
||||
void KeeloqTXView::update_payload() {
|
||||
uint64_t encrypt = 0;
|
||||
|
||||
switch (current_key.type) {
|
||||
case KEELOQ_SIMPLE_LEARNING: {
|
||||
encrypt = keeloq_encrypt(hop, current_key.key);
|
||||
|
||||
break;
|
||||
}
|
||||
case KEELOQ_NORMAL_LEARNING: {
|
||||
uint64_t man = keeloq_normal_learning(fix, current_key.key);
|
||||
|
||||
encrypt = keeloq_encrypt(hop, man);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
payload = (uint64_t)fix << 32 | encrypt;
|
||||
|
||||
uint64_t preview_payload = FProtoGeneral::subghz_protocol_blocks_reverse_key(payload, 64);
|
||||
|
||||
text_payload.set(to_string_hex(preview_payload));
|
||||
encode_data();
|
||||
}
|
||||
|
||||
void KeeloqTXView::encode_data() {
|
||||
std::string fragments{};
|
||||
|
||||
for (uint32_t i = 0; i < 64; ++i) {
|
||||
fragments += keeloq_fragments[bit(payload, i)];
|
||||
}
|
||||
|
||||
encoded_data = KEELOQ_HEADER + fragments + "1001";
|
||||
|
||||
if (data.mf_name == "Sommer") {
|
||||
pause_duration = 28;
|
||||
} else {
|
||||
pause_duration = 39;
|
||||
}
|
||||
}
|
||||
|
||||
KeeloqTXView::KeeloqTXView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_ook);
|
||||
|
||||
add_children({&labels,
|
||||
&text_mf_name,
|
||||
&text_serial,
|
||||
&text_fix,
|
||||
&text_hop,
|
||||
&field_counter,
|
||||
&field_button,
|
||||
&field_repeat,
|
||||
&text_payload,
|
||||
&button_open,
|
||||
&text_status,
|
||||
&progressbar,
|
||||
&tx_view});
|
||||
|
||||
field_counter.on_change = [this](uint32_t value) {
|
||||
data.counter = (uint16_t)value;
|
||||
|
||||
update_hop();
|
||||
update_payload();
|
||||
};
|
||||
|
||||
field_button.on_change = [this](uint32_t value) {
|
||||
data.btn = (uint8_t)value;
|
||||
|
||||
fix &= 0xFFFFFFF;
|
||||
fix |= data.btn << 28;
|
||||
|
||||
text_fix.set(to_string_hex(fix));
|
||||
|
||||
update_hop();
|
||||
update_payload();
|
||||
};
|
||||
|
||||
field_repeat.on_change = [this](uint32_t value) {
|
||||
repeat = value;
|
||||
};
|
||||
|
||||
field_repeat.set_value(4, true);
|
||||
|
||||
tx_view.on_edit_frequency = [this, &nav]() {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(transmitter_model.target_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
transmitter_model.set_target_frequency(f);
|
||||
};
|
||||
};
|
||||
|
||||
tx_view.on_start = [this]() {
|
||||
start_tx();
|
||||
};
|
||||
|
||||
tx_view.on_stop = [this]() {
|
||||
baseband::kill_ook();
|
||||
stop_tx();
|
||||
};
|
||||
|
||||
button_open.on_select = [this](const Button&) {
|
||||
auto open_view = nav_.push<FileLoadView>(".KEELOQ");
|
||||
|
||||
ensure_directory(keeloq_remotes_dir);
|
||||
open_view->push_dir(keeloq_remotes_dir);
|
||||
open_view->on_changed = [this](fs::path path) {
|
||||
nav_.set_on_pop([this, path]() {
|
||||
if (!read_keeloq_file(path, data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
file_path = path;
|
||||
|
||||
for (const auto& key : keystore.get_keys()) {
|
||||
if (key.mf_name == data.mf_name) {
|
||||
current_key = key;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
fix = data.btn << 28 | data.serial;
|
||||
|
||||
update_hop();
|
||||
|
||||
text_mf_name.set(data.mf_name);
|
||||
text_serial.set(to_string_hex(data.serial));
|
||||
text_fix.set(to_string_hex(fix));
|
||||
|
||||
field_counter.set_value(data.counter, false);
|
||||
field_button.set_value(data.btn, false);
|
||||
|
||||
update_payload();
|
||||
|
||||
field_counter.focus();
|
||||
});
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
void KeeloqTXView::start_tx() {
|
||||
size_t bitstream_length = encoders::make_bitstream(encoded_data);
|
||||
|
||||
progressbar.set_max(repeat);
|
||||
tx_view.set_transmitting(true);
|
||||
|
||||
transmitter_model.enable();
|
||||
|
||||
baseband::set_ook_data(
|
||||
bitstream_length,
|
||||
OOK_SAMPLERATE * (400.0 / 1000000.0),
|
||||
repeat,
|
||||
pause_duration);
|
||||
}
|
||||
|
||||
void KeeloqTXView::stop_tx() {
|
||||
transmitter_model.disable();
|
||||
text_status.set("Ready");
|
||||
progressbar.set_value(0);
|
||||
tx_view.set_transmitting(false);
|
||||
|
||||
data.counter += 1;
|
||||
|
||||
field_counter.set_value(data.counter, false);
|
||||
|
||||
update_hop();
|
||||
update_payload();
|
||||
|
||||
write_keeloq_file(file_path, data);
|
||||
}
|
||||
|
||||
void KeeloqTXView::on_tx_progress(uint32_t progress, bool done) {
|
||||
if (!done) {
|
||||
text_status.set(to_string_dec_uint(progress + 1) + "/" + to_string_dec_uint(repeat));
|
||||
progressbar.set_value(progress + 1);
|
||||
} else {
|
||||
chThdSleepMilliseconds(10);
|
||||
|
||||
stop_tx();
|
||||
}
|
||||
}
|
||||
|
||||
KeeloqTXView::~KeeloqTXView() {
|
||||
transmitter_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::ui_keeloqtx
|
||||
@@ -0,0 +1,129 @@
|
||||
#ifndef __KEELOQTX__
|
||||
#define __KEELOQTX__
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_transmitter.hpp"
|
||||
#include "transmitter_model.hpp"
|
||||
#include "file_path.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "encoders.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "../../keeloq_file.hpp"
|
||||
#include "../../keeloq_keystore.hpp"
|
||||
|
||||
#define KEELOQ_HEADER "101010101010101010101010000000000"
|
||||
|
||||
namespace ui::external_app::ui_keeloqtx {
|
||||
|
||||
class KeeloqTXView : public View {
|
||||
public:
|
||||
KeeloqTXView(NavigationView& nav);
|
||||
~KeeloqTXView();
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override {
|
||||
return "KeeLoqTX";
|
||||
}
|
||||
|
||||
private:
|
||||
const std::string keeloq_fragments[2] = {"110", "100"};
|
||||
|
||||
NavigationView& nav_;
|
||||
|
||||
TxRadioState radio_state_{
|
||||
433920000,
|
||||
1750000,
|
||||
OOK_SAMPLERATE};
|
||||
|
||||
app_settings::SettingsManager settings_{
|
||||
"tx_keeloq", app_settings::Mode::TX};
|
||||
|
||||
std::filesystem::path file_path{};
|
||||
|
||||
KeeloqKeystore keystore{};
|
||||
KeeloqKey current_key{};
|
||||
KeeloqData data{};
|
||||
|
||||
uint32_t fix = 0;
|
||||
uint32_t hop = 0;
|
||||
uint64_t payload = 0;
|
||||
|
||||
void update_hop();
|
||||
|
||||
Labels labels{
|
||||
{{UI_POS_X(1), UI_POS_Y(0)}, "Manufacturer:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(1), UI_POS_Y(1)}, "Serial:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(1), UI_POS_Y(2)}, "Fix:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(1), UI_POS_Y(3)}, "Hop:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(1), UI_POS_Y(4)}, "Counter:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(1), UI_POS_Y(5)}, "Button:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(1), UI_POS_Y(6)}, "Repeat:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(2), UI_POS_Y(8)}, "Payload:", Theme::getInstance()->fg_light->foreground},
|
||||
};
|
||||
|
||||
Text text_mf_name{{UI_POS_X(15), UI_POS_Y(0), 128, 16}, ""};
|
||||
Text text_serial{{UI_POS_X(9), UI_POS_Y(1), 64, 16}, "00000000"};
|
||||
Text text_fix{{UI_POS_X(6), UI_POS_Y(2), 64, 16}, "00000000"};
|
||||
Text text_hop{{UI_POS_X(6), UI_POS_Y(3), 64, 16}, "00000000"};
|
||||
|
||||
NumberField field_counter{
|
||||
{UI_POS_X(10), UI_POS_Y(4)},
|
||||
5,
|
||||
{0, 65535},
|
||||
1,
|
||||
' '};
|
||||
NumberField field_button{
|
||||
{UI_POS_X(9), UI_POS_Y(5)},
|
||||
2,
|
||||
{0, 15},
|
||||
1,
|
||||
' '};
|
||||
NumberField field_repeat{
|
||||
{UI_POS_X(10), UI_POS_Y(6)},
|
||||
3,
|
||||
{0, 100},
|
||||
1,
|
||||
' '};
|
||||
|
||||
Text text_payload{{UI_POS_X(2), UI_POS_Y(9), 128, 16}, "0000000000000000"};
|
||||
|
||||
void update_payload();
|
||||
|
||||
Button button_open{{UI_POS_X(0), UI_POS_Y(11), screen_width, 32}, "Open file"};
|
||||
|
||||
Text text_status{{UI_POS_X(2), UI_POS_Y_BOTTOM(7), 128, 16}, "Ready"};
|
||||
|
||||
ProgressBar progressbar{
|
||||
{UI_POS_X(2), UI_POS_Y_BOTTOM(7) + 20, UI_POS_WIDTH_REMAINING(4), 16}};
|
||||
|
||||
TransmitterView tx_view{
|
||||
(int16_t)UI_POS_Y_BOTTOM(4),
|
||||
50000,
|
||||
9};
|
||||
|
||||
std::string encoded_data{};
|
||||
|
||||
void encode_data();
|
||||
|
||||
uint32_t repeat = 4;
|
||||
uint32_t pause_duration = 0;
|
||||
|
||||
void start_tx();
|
||||
void stop_tx();
|
||||
|
||||
void update_progress();
|
||||
void on_tx_progress(uint32_t progress, bool done);
|
||||
|
||||
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, message.done);
|
||||
}};
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::ui_keeloqtx
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Bernd Herzog
|
||||
*
|
||||
* 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.hpp"
|
||||
#include "ui_pocsag_tx.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::pocsag_tx {
|
||||
void initialize_app(ui::NavigationView& nav) {
|
||||
nav.push<POCSAGTXView>();
|
||||
}
|
||||
} // namespace ui::external_app::pocsag_tx
|
||||
|
||||
extern "C" {
|
||||
|
||||
__attribute__((section(".external_app.app_pocsag_tx.application_information"), used)) application_information_t _application_information_pocsag_tx = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||
/*.externalAppEntry = */ ui::external_app::pocsag_tx::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "POCSG TX",
|
||||
/*.bitmap_data = */ {
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0xFC,
|
||||
0x3F,
|
||||
0xFE,
|
||||
0x7F,
|
||||
0x02,
|
||||
0x40,
|
||||
0xBA,
|
||||
0x45,
|
||||
0x02,
|
||||
0x40,
|
||||
0xFE,
|
||||
0x7F,
|
||||
0xFE,
|
||||
0x7F,
|
||||
0x92,
|
||||
0x7C,
|
||||
0x92,
|
||||
0x7C,
|
||||
0xFC,
|
||||
0x3F,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::green().v,
|
||||
/*.menu_location = */ app_location_t::TX,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_fsktx */ {'P', 'F', 'S', 'K'},
|
||||
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
|
||||
};
|
||||
}
|
||||
+3
-3
@@ -31,7 +31,7 @@
|
||||
using namespace portapack;
|
||||
using namespace pocsag;
|
||||
|
||||
namespace ui {
|
||||
namespace ui::external_app::pocsag_tx {
|
||||
|
||||
#define MAX_POCSAG_LENGTH 80
|
||||
|
||||
@@ -159,7 +159,7 @@ void POCSAGTXView::on_set_text(NavigationView& nav) {
|
||||
POCSAGTXView::POCSAGTXView(
|
||||
NavigationView& nav)
|
||||
: nav_(nav) {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_fsktx);
|
||||
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
|
||||
|
||||
add_children({&labels,
|
||||
&options_bitrate,
|
||||
@@ -205,4 +205,4 @@ POCSAGTXView::POCSAGTXView(
|
||||
};
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
} /* namespace ui::external_app::pocsag_tx */
|
||||
+2
-2
@@ -37,7 +37,7 @@
|
||||
|
||||
using namespace pocsag;
|
||||
|
||||
namespace ui {
|
||||
namespace ui::external_app::pocsag_tx {
|
||||
|
||||
class POCSAGTXView : public View {
|
||||
public:
|
||||
@@ -155,6 +155,6 @@ class POCSAGTXView : public View {
|
||||
}};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
} /* namespace ui::external_app::pocsag_tx */
|
||||
|
||||
#endif /*__POCSAG_TX_H__*/
|
||||
@@ -239,10 +239,10 @@ void RandomPasswordView::on_freqchg(int64_t freq) {
|
||||
}
|
||||
|
||||
void RandomPasswordView::set_random_freq() {
|
||||
std::srand(LPC_RTC->CTIME0);
|
||||
srand(LPC_RTC->CTIME0);
|
||||
// this is only for seed to visit random freq, the radio is still real random
|
||||
|
||||
auto random_freq = 100000000 + (std::rand() % 900000000); // 100mhz to 1ghz
|
||||
auto random_freq = 100000000 + (rand() % 900000000); // 100mhz to 1ghz
|
||||
receiver_model.set_target_frequency(random_freq);
|
||||
field_frequency.set_value(random_freq);
|
||||
}
|
||||
@@ -297,12 +297,12 @@ void RandomPasswordView::new_password() {
|
||||
/// roll worker
|
||||
for (int i = 0; i < password_length * 2; i += 2) {
|
||||
unsigned int seed = seeds_deque[i];
|
||||
std::srand(seed);
|
||||
srand(seed);
|
||||
uint8_t rollnum = (uint8_t)(seeds_deque[i + 1] % 128);
|
||||
uint8_t nu = 0;
|
||||
for (uint8_t o = 0; o < rollnum; ++o) nu = std::rand();
|
||||
for (uint8_t o = 0; o < rollnum; ++o) nu = rand();
|
||||
nu++;
|
||||
char c = charset[std::rand() % charset.length()];
|
||||
char c = charset[rand() % charset.length()];
|
||||
initial_password += c;
|
||||
}
|
||||
|
||||
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
#include "baudot.hpp"
|
||||
#include <cctype>
|
||||
|
||||
namespace ui::external_app::rtty_rx {
|
||||
|
||||
constexpr uint8_t CODE_FIGS = 0x1B;
|
||||
constexpr uint8_t CODE_LTRS = 0x1F;
|
||||
constexpr uint8_t CODE_SPACE = 0x04;
|
||||
|
||||
char BaudotCoder::get_char_mapping(bool is_figures, uint8_t index) const {
|
||||
if (index >= 32) return 0; // Safety check
|
||||
|
||||
if (is_figures) {
|
||||
// ITA2 FIGURES Table
|
||||
const char figures[32] = {
|
||||
0, '3', '\n', '-', ' ', '\'', '8', '7',
|
||||
'\r', '$', '4', '\a', ',', '!', ':', '(',
|
||||
'5', '+', ')', '2', '#', '6', '0', '1',
|
||||
'9', '?', '&', 0, '.', '/', '=', 0};
|
||||
return figures[index];
|
||||
} else {
|
||||
// ITA2 LETTERS Table
|
||||
const char letters[32] = {
|
||||
0, 'E', '\n', 'A', ' ', 'S', 'I', 'U',
|
||||
'\r', 'D', 'R', 'J', 'N', 'F', 'C', 'K',
|
||||
'T', 'Z', 'L', 'W', 'H', 'Y', 'P', 'Q',
|
||||
'O', 'B', 'G', 0, 'M', 'X', 'V', 0};
|
||||
return letters[index];
|
||||
}
|
||||
}
|
||||
|
||||
// --- DECODE ---
|
||||
char BaudotCoder::decode(uint8_t baudotCode) {
|
||||
uint8_t code = baudotCode & 0x1F;
|
||||
if (code == CODE_FIGS) {
|
||||
shiftState = FIGURES;
|
||||
return 0;
|
||||
}
|
||||
if (code == CODE_LTRS) {
|
||||
shiftState = LETTERS;
|
||||
return 0;
|
||||
}
|
||||
if (usos_enabled && shiftState == FIGURES && code == CODE_SPACE) {
|
||||
shiftState = LETTERS;
|
||||
return ' ';
|
||||
}
|
||||
return get_char_mapping((shiftState == FIGURES), code);
|
||||
}
|
||||
|
||||
void BaudotCoder::encode(const std::string& src, uint8_t* dest, uint16_t* dest_length, uint16_t dest_max_size) {
|
||||
uint16_t idx = 0;
|
||||
|
||||
for (char c : src) {
|
||||
if (idx >= dest_max_size) break;
|
||||
|
||||
char upper_c = std::toupper(c);
|
||||
uint8_t code = 0;
|
||||
bool found = false;
|
||||
bool target_is_figures = false;
|
||||
if (upper_c == ' ') {
|
||||
code = CODE_SPACE;
|
||||
found = true;
|
||||
} else {
|
||||
for (int i = 1; i < 32; i++) {
|
||||
if (get_char_mapping(false, i) == upper_c) {
|
||||
code = i;
|
||||
found = true;
|
||||
target_is_figures = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
for (int i = 1; i < 32; i++) {
|
||||
if (get_char_mapping(true, i) == upper_c) {
|
||||
code = i;
|
||||
found = true;
|
||||
target_is_figures = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (found) {
|
||||
if (target_is_figures && shiftState != FIGURES) {
|
||||
if (idx + 1 >= dest_max_size) break;
|
||||
dest[idx++] = CODE_FIGS;
|
||||
shiftState = FIGURES;
|
||||
} else if (!target_is_figures && shiftState != LETTERS && code != CODE_SPACE) {
|
||||
if (idx + 1 >= dest_max_size) break;
|
||||
dest[idx++] = CODE_LTRS;
|
||||
shiftState = LETTERS;
|
||||
}
|
||||
|
||||
dest[idx++] = code;
|
||||
}
|
||||
}
|
||||
|
||||
if (dest_length) {
|
||||
*dest_length = idx;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::rtty_rx
|
||||
@@ -0,0 +1,28 @@
|
||||
#ifndef BAUDOT_HPP
|
||||
#define BAUDOT_HPP
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace ui::external_app::rtty_rx {
|
||||
|
||||
class BaudotCoder {
|
||||
public:
|
||||
enum ShiftState { LETTERS,
|
||||
FIGURES };
|
||||
|
||||
BaudotCoder()
|
||||
: shiftState(LETTERS) {}
|
||||
char decode(uint8_t baudotCode);
|
||||
void encode(const std::string& src, uint8_t* dest, uint16_t* dest_length, uint16_t dest_max_size);
|
||||
void set_usos(bool enable) { usos_enabled = enable; } // shift == set to letters too
|
||||
|
||||
private:
|
||||
ShiftState shiftState;
|
||||
bool usos_enabled = true;
|
||||
char get_char_mapping(bool is_figures, uint8_t index) const;
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::rtty_rx
|
||||
|
||||
#endif // BAUDOT_HPP
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright (C) 2026 HTotoo
|
||||
*
|
||||
* 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.hpp"
|
||||
#include "ui_rtty_rx.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::rtty_rx {
|
||||
void initialize_app(ui::NavigationView& nav) {
|
||||
nav.push<RttyRxView>();
|
||||
}
|
||||
} // namespace ui::external_app::rtty_rx
|
||||
|
||||
extern "C" {
|
||||
|
||||
__attribute__((section(".external_app.app_rtty_rx.application_information"), used)) application_information_t _application_information_rtty_rx = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||
/*.externalAppEntry = */ ui::external_app::rtty_rx::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "RTTY",
|
||||
/*.bitmap_data = */ {
|
||||
0x00,
|
||||
0x00,
|
||||
0xFE,
|
||||
0x1F,
|
||||
0xFE,
|
||||
0x3F,
|
||||
0x0E,
|
||||
0x78,
|
||||
0x0E,
|
||||
0x70,
|
||||
0x0E,
|
||||
0x70,
|
||||
0x0E,
|
||||
0x70,
|
||||
0x0E,
|
||||
0x78,
|
||||
0xFE,
|
||||
0x3F,
|
||||
0xFE,
|
||||
0x1F,
|
||||
0x8E,
|
||||
0x07,
|
||||
0x0E,
|
||||
0x0F,
|
||||
0x0E,
|
||||
0x1E,
|
||||
0x0E,
|
||||
0x3C,
|
||||
0x0E,
|
||||
0x78,
|
||||
0x00,
|
||||
0x00,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::orange().v,
|
||||
/*.menu_location = */ app_location_t::RX,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_rttyrx */ {'P', 'R', 'T', 'R'},
|
||||
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright (C) 2026 HTotoo
|
||||
*
|
||||
* 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_rtty_rx.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 ui;
|
||||
|
||||
// todo add timeout, so add newline and time when new data arrives after a time.
|
||||
|
||||
namespace ui::external_app::rtty_rx {
|
||||
|
||||
void RttyRxView::focus() {
|
||||
field_frequency.focus();
|
||||
}
|
||||
|
||||
RttyRxView::RttyRxView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
|
||||
add_children({
|
||||
&rssi,
|
||||
&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&field_volume,
|
||||
&field_frequency,
|
||||
&labels,
|
||||
&options_baud,
|
||||
&console,
|
||||
});
|
||||
field_frequency.set_step(100);
|
||||
audio::set_rate(audio::Rate::Hz_24000);
|
||||
audio::output::start();
|
||||
|
||||
receiver_model.set_hidden_offset(0);
|
||||
receiver_model.set_sampling_rate(3072000); // set the needed baseband SR.
|
||||
receiver_model.set_baseband_bandwidth(1750000); // set the front-end RF BW filter.
|
||||
receiver_model.enable();
|
||||
console.enable_scrolling(false);
|
||||
|
||||
options_baud.on_change = [this](size_t, int32_t value) {
|
||||
baseband::set_rtty_config(value, 170);
|
||||
baud_conf = value;
|
||||
};
|
||||
options_baud.set_by_value(baud_conf);
|
||||
if (baud_conf == 0) { // to trigger it when not changed
|
||||
baseband::set_rtty_config(0, 170);
|
||||
}
|
||||
}
|
||||
|
||||
void RttyRxView::got_message(std::string msg) {
|
||||
console.write(msg);
|
||||
}
|
||||
|
||||
RttyRxView::~RttyRxView() {
|
||||
receiver_model.set_hidden_offset(0);
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
audio::output::stop();
|
||||
}
|
||||
|
||||
void RttyRxView::on_data(const RTTYDataMessage* message) {
|
||||
std::string msg = "";
|
||||
for (size_t i = 0; i < message->data_len; i++) {
|
||||
const auto c = baudot_decoder.decode(message->data[i]);
|
||||
if (c != 0)
|
||||
msg += c;
|
||||
}
|
||||
got_message(msg);
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::rtty_rx
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright (C) 2026 HTotoo
|
||||
*
|
||||
* 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_RTTY_RX_H__
|
||||
#define __UI_RTTY_RX_H__
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_language.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "log_file.hpp"
|
||||
#include "utility.hpp"
|
||||
#include "ui_fileman.hpp"
|
||||
#include "file_path.hpp"
|
||||
#include "baudot.hpp"
|
||||
|
||||
using namespace ui;
|
||||
|
||||
namespace ui::external_app::rtty_rx {
|
||||
|
||||
class RttyRxView : public View {
|
||||
public:
|
||||
RttyRxView(NavigationView& nav);
|
||||
~RttyRxView();
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "RTTY"; };
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
RxRadioState radio_state_{};
|
||||
uint32_t baud_conf = 0;
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_rtty",
|
||||
app_settings::Mode::RX,
|
||||
{
|
||||
{"baud_conf"sv, &baud_conf},
|
||||
}};
|
||||
|
||||
RxFrequencyField field_frequency{
|
||||
{UI_POS_X(0), UI_POS_Y(0)},
|
||||
nav_};
|
||||
RFAmpField field_rf_amp{
|
||||
{UI_POS_X(13), UI_POS_Y(0)}};
|
||||
LNAGainField field_lna{
|
||||
{UI_POS_X(15), UI_POS_Y(0)}};
|
||||
VGAGainField field_vga{
|
||||
{UI_POS_X(18), UI_POS_Y(0)}};
|
||||
RSSI rssi{
|
||||
{UI_POS_X(21), UI_POS_Y(0), UI_POS_WIDTH_REMAINING(24), 4}};
|
||||
AudioVolumeField field_volume{{UI_POS_X_RIGHT(2), UI_POS_Y(0)}};
|
||||
|
||||
Labels labels{
|
||||
{{UI_POS_X(0), UI_POS_Y(1)}, "Baud:", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
OptionsField options_baud{
|
||||
{UI_POS_X(7), UI_POS_Y(1)},
|
||||
5,
|
||||
{{"Auto", 0},
|
||||
{"45", 4500},
|
||||
{"45.45", 4545},
|
||||
{"50", 5000},
|
||||
{"75", 7500},
|
||||
{"100", 10000},
|
||||
{"110", 11000},
|
||||
{"150", 15000},
|
||||
{"200", 20000}}};
|
||||
|
||||
Console console{
|
||||
{UI_POS_X(0), UI_POS_Y(2), UI_POS_MAXWIDTH, UI_POS_HEIGHT_REMAINING(3)}};
|
||||
|
||||
BaudotCoder baudot_decoder{};
|
||||
|
||||
void on_data(const RTTYDataMessage* message);
|
||||
void got_message(std::string msg);
|
||||
|
||||
MessageHandlerRegistration message_handler_data{
|
||||
Message::ID::RTTYData,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const RTTYDataMessage*>(p);
|
||||
this->on_data(message);
|
||||
}};
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::rtty_rx
|
||||
|
||||
#endif /*__UI_RTTY_RX_H__*/
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
#include "baudot.hpp"
|
||||
#include <cctype>
|
||||
|
||||
namespace ui::external_app::rtty_tx {
|
||||
|
||||
constexpr uint8_t CODE_FIGS = 0x1B;
|
||||
constexpr uint8_t CODE_LTRS = 0x1F;
|
||||
constexpr uint8_t CODE_SPACE = 0x04;
|
||||
|
||||
char BaudotCoder::get_char_mapping(bool is_figures, uint8_t index) const {
|
||||
if (index >= 32) return 0; // Safety check
|
||||
|
||||
if (is_figures) {
|
||||
// ITA2 FIGURES Table
|
||||
const char figures[32] = {
|
||||
0, '3', '\n', '-', ' ', '\'', '8', '7',
|
||||
'\r', '$', '4', '\a', ',', '!', ':', '(',
|
||||
'5', '+', ')', '2', '#', '6', '0', '1',
|
||||
'9', '?', '&', 0, '.', '/', '=', 0};
|
||||
return figures[index];
|
||||
} else {
|
||||
// ITA2 LETTERS Table
|
||||
const char letters[32] = {
|
||||
0, 'E', '\n', 'A', ' ', 'S', 'I', 'U',
|
||||
'\r', 'D', 'R', 'J', 'N', 'F', 'C', 'K',
|
||||
'T', 'Z', 'L', 'W', 'H', 'Y', 'P', 'Q',
|
||||
'O', 'B', 'G', 0, 'M', 'X', 'V', 0};
|
||||
return letters[index];
|
||||
}
|
||||
}
|
||||
|
||||
// --- DECODE ---
|
||||
char BaudotCoder::decode(uint8_t baudotCode) {
|
||||
uint8_t code = baudotCode & 0x1F;
|
||||
if (code == CODE_FIGS) {
|
||||
shiftState = FIGURES;
|
||||
return 0;
|
||||
}
|
||||
if (code == CODE_LTRS) {
|
||||
shiftState = LETTERS;
|
||||
return 0;
|
||||
}
|
||||
if (usos_enabled && shiftState == FIGURES && code == CODE_SPACE) {
|
||||
shiftState = LETTERS;
|
||||
return ' ';
|
||||
}
|
||||
return get_char_mapping((shiftState == FIGURES), code);
|
||||
}
|
||||
|
||||
void BaudotCoder::encode(const std::string& src, uint8_t* dest, uint16_t* dest_length, uint16_t dest_max_size) {
|
||||
uint16_t idx = 0;
|
||||
|
||||
for (char c : src) {
|
||||
if (idx >= dest_max_size) break;
|
||||
|
||||
char upper_c = std::toupper(c);
|
||||
uint8_t code = 0;
|
||||
bool found = false;
|
||||
bool target_is_figures = false;
|
||||
if (upper_c == ' ') {
|
||||
code = CODE_SPACE;
|
||||
found = true;
|
||||
} else {
|
||||
for (int i = 1; i < 32; i++) {
|
||||
if (get_char_mapping(false, i) == upper_c) {
|
||||
code = i;
|
||||
found = true;
|
||||
target_is_figures = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
for (int i = 1; i < 32; i++) {
|
||||
if (get_char_mapping(true, i) == upper_c) {
|
||||
code = i;
|
||||
found = true;
|
||||
target_is_figures = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (found) {
|
||||
if (target_is_figures && shiftState != FIGURES) {
|
||||
if (idx + 1 >= dest_max_size) break;
|
||||
dest[idx++] = CODE_FIGS;
|
||||
shiftState = FIGURES;
|
||||
} else if (!target_is_figures && shiftState != LETTERS && code != CODE_SPACE) {
|
||||
if (idx + 1 >= dest_max_size) break;
|
||||
dest[idx++] = CODE_LTRS;
|
||||
shiftState = LETTERS;
|
||||
}
|
||||
|
||||
dest[idx++] = code;
|
||||
}
|
||||
}
|
||||
|
||||
if (dest_length) {
|
||||
*dest_length = idx;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::rtty_tx
|
||||
@@ -0,0 +1,28 @@
|
||||
#ifndef BAUDOT_HPP
|
||||
#define BAUDOT_HPP
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace ui::external_app::rtty_tx {
|
||||
|
||||
class BaudotCoder {
|
||||
public:
|
||||
enum ShiftState { LETTERS,
|
||||
FIGURES };
|
||||
|
||||
BaudotCoder()
|
||||
: shiftState(LETTERS) {}
|
||||
char decode(uint8_t baudotCode);
|
||||
void encode(const std::string& src, uint8_t* dest, uint16_t* dest_length, uint16_t dest_max_size);
|
||||
void set_usos(bool enable) { usos_enabled = enable; } // shift == set to letters too
|
||||
|
||||
private:
|
||||
ShiftState shiftState;
|
||||
bool usos_enabled = true;
|
||||
char get_char_mapping(bool is_figures, uint8_t index) const;
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::rtty_tx
|
||||
|
||||
#endif // BAUDOT_HPP
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright (C) 2026 HTotoo
|
||||
*
|
||||
* 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.hpp"
|
||||
#include "ui_rtty_tx.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::rtty_tx {
|
||||
void initialize_app(ui::NavigationView& nav) {
|
||||
nav.push<RttyTxView>();
|
||||
}
|
||||
} // namespace ui::external_app::rtty_tx
|
||||
|
||||
extern "C" {
|
||||
|
||||
__attribute__((section(".external_app.app_rtty_tx.application_information"), used)) application_information_t _application_information_rtty_tx = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||
/*.externalAppEntry = */ ui::external_app::rtty_tx::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "RTTY",
|
||||
/*.bitmap_data = */ {
|
||||
0x00,
|
||||
0x00,
|
||||
0xFE,
|
||||
0x1F,
|
||||
0xFE,
|
||||
0x3F,
|
||||
0x0E,
|
||||
0x78,
|
||||
0x0E,
|
||||
0x70,
|
||||
0x0E,
|
||||
0x70,
|
||||
0x0E,
|
||||
0x70,
|
||||
0x0E,
|
||||
0x78,
|
||||
0xFE,
|
||||
0x3F,
|
||||
0xFE,
|
||||
0x1F,
|
||||
0x8E,
|
||||
0x07,
|
||||
0x0E,
|
||||
0x0F,
|
||||
0x0E,
|
||||
0x1E,
|
||||
0x0E,
|
||||
0x3C,
|
||||
0x0E,
|
||||
0x78,
|
||||
0x00,
|
||||
0x00,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::orange().v,
|
||||
/*.menu_location = */ app_location_t::TX,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_rttytx */ {'P', 'R', 'T', 'T'},
|
||||
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* Copyright (C) 2026 HTotoo
|
||||
*
|
||||
* 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_rtty_tx.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 ui;
|
||||
|
||||
namespace ui::external_app::rtty_tx {
|
||||
|
||||
void RttyTxView::focus() {
|
||||
options_baud.focus();
|
||||
}
|
||||
|
||||
RttyTxView::RttyTxView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
add_children({&tx_view,
|
||||
&labels,
|
||||
&options_baud,
|
||||
&options_shift,
|
||||
&btn_message,
|
||||
&text_message,
|
||||
&check_inverted,
|
||||
&options_tone,
|
||||
&text_tones});
|
||||
|
||||
options_baud.on_change = [this](size_t, int32_t value) {
|
||||
baud_conf = value;
|
||||
};
|
||||
options_baud.set_by_value(baud_conf);
|
||||
|
||||
options_shift.on_change = [this](size_t, int32_t value) {
|
||||
shift = value;
|
||||
refresh_tones();
|
||||
};
|
||||
options_shift.set_by_value(shift);
|
||||
|
||||
btn_message.on_select = [this](Button&) {
|
||||
text_prompt(nav_, message, 64, ENTER_KEYBOARD_MODE_ALPHA, [this](std::string& new_message) {
|
||||
message = new_message;
|
||||
text_message.set(message); // maybe replace to console
|
||||
});
|
||||
};
|
||||
text_message.set(message);
|
||||
options_tone.set_by_value(mark_tone);
|
||||
options_tone.on_change = [this](size_t, int32_t value) {
|
||||
mark_tone = value;
|
||||
refresh_tones();
|
||||
};
|
||||
check_inverted.on_select = [this](Checkbox&, bool) {
|
||||
refresh_tones();
|
||||
};
|
||||
|
||||
tx_view.on_edit_frequency = [this, &nav]() {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(transmitter_model.target_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
transmitter_model.set_target_frequency(f);
|
||||
};
|
||||
};
|
||||
|
||||
tx_view.on_start = [this]() {
|
||||
if (message.empty()) {
|
||||
nav_.display_modal("Error", "Message is empty. Please set a message to transmit.");
|
||||
return;
|
||||
}
|
||||
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
|
||||
transmitter_model.set_baseband_bandwidth(2048000);
|
||||
baseband::set_sample_rate(2048000, OversampleRate::None);
|
||||
transmitter_model.set_sampling_rate(2048000);
|
||||
transmitter_model.set_baseband_bandwidth(2048000);
|
||||
transmitter_model.enable();
|
||||
rtty_message.baud = baud_conf;
|
||||
rtty_message.shift = shift;
|
||||
rtty_message.inverted = false; // check_inverted.value(); //i already invert it with the mark and space calculation
|
||||
rtty_message.mark_tone = mark;
|
||||
rtty_message.space_tone = space;
|
||||
rtty_message.stopbits = stop_bits;
|
||||
baudot_encoder.set_usos(false); // to be compatible with all
|
||||
baudot_encoder.encode(message, rtty_message.data, &rtty_message.data_len, rtty_message.max_len);
|
||||
baseband::set_rtty_config(rtty_message);
|
||||
tx_view.set_transmitting(true);
|
||||
};
|
||||
|
||||
tx_view.on_stop = [this]() {
|
||||
stop();
|
||||
};
|
||||
|
||||
refresh_tones();
|
||||
}
|
||||
|
||||
void RttyTxView::refresh_tones() {
|
||||
std::string tones_str = "";
|
||||
|
||||
int16_t shift = options_shift.selected_index_value();
|
||||
if (mark_tone == -32000) {
|
||||
mark = -shift / 2;
|
||||
space = shift / 2;
|
||||
} else {
|
||||
if (mark_tone < 0) {
|
||||
space = -(shift + abs(mark_tone));
|
||||
} else {
|
||||
space = shift + mark_tone;
|
||||
}
|
||||
mark = mark_tone;
|
||||
}
|
||||
|
||||
if (check_inverted.value()) {
|
||||
int16_t tmp = mark;
|
||||
mark = space;
|
||||
space = tmp;
|
||||
}
|
||||
tones_str = "Mark:" + to_string_dec_int(mark) + " Hz, Space:" + to_string_dec_int(space) + " Hz";
|
||||
text_tones.set(tones_str);
|
||||
}
|
||||
|
||||
void RttyTxView::on_tx_progress(bool done) {
|
||||
if (done) stop();
|
||||
}
|
||||
|
||||
void RttyTxView::stop() {
|
||||
tx_view.set_transmitting(false);
|
||||
transmitter_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
RttyTxView::~RttyTxView() {
|
||||
stop();
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::rtty_tx
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* Copyright (C) 2026 HTotoo
|
||||
*
|
||||
* 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_RTTY_TX_H__
|
||||
#define __UI_RTTY_TX_H__
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_language.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_transmitter.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "log_file.hpp"
|
||||
#include "utility.hpp"
|
||||
#include "ui_fileman.hpp"
|
||||
#include "file_path.hpp"
|
||||
#include "baudot.hpp"
|
||||
|
||||
using namespace ui;
|
||||
|
||||
namespace ui::external_app::rtty_tx {
|
||||
|
||||
class RttyTxView : public View {
|
||||
public:
|
||||
RttyTxView(NavigationView& nav);
|
||||
~RttyTxView();
|
||||
void focus() override;
|
||||
std::string title() const override { return "RTTY"; };
|
||||
|
||||
private:
|
||||
void on_tx_progress(bool done);
|
||||
void stop();
|
||||
void refresh_tones();
|
||||
|
||||
NavigationView& nav_;
|
||||
RxRadioState radio_state_{};
|
||||
uint32_t baud_conf = 4545; // default to 45.45 baud, which is common for RTTY.
|
||||
uint32_t shift = 170; // 170 hz
|
||||
std::string message = "PORTAPACK";
|
||||
int32_t mark_tone = 0; // for option
|
||||
int16_t mark = 0, space = 170;
|
||||
uint8_t stop_bits = 3; // 2=1.0, 3=1.5, 4=2.0
|
||||
app_settings::SettingsManager settings_{
|
||||
"tx_rtty",
|
||||
app_settings::Mode::TX,
|
||||
{
|
||||
{"baud_conf"sv, &baud_conf},
|
||||
{"shift"sv, &shift},
|
||||
{"message"sv, &message},
|
||||
{"mark_tone"sv, &mark_tone},
|
||||
}};
|
||||
|
||||
Labels labels{
|
||||
{{UI_POS_X(0), UI_POS_Y(0)}, "Baud:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(14), UI_POS_Y(0)}, "Shift:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(15), UI_POS_Y(1)}, "Mark t:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(3)}, "Message:", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
OptionsField options_baud{
|
||||
{UI_POS_X(7), UI_POS_Y(0)},
|
||||
5,
|
||||
{{"45", 4500},
|
||||
{"45.45", 4545},
|
||||
{"50", 5000},
|
||||
{"75", 7500},
|
||||
{"100", 10000},
|
||||
{"110", 11000},
|
||||
{"150", 15000},
|
||||
{"200", 20000}}};
|
||||
|
||||
OptionsField options_shift{
|
||||
{UI_POS_X(21), UI_POS_Y(0)},
|
||||
5,
|
||||
{{"170", 170},
|
||||
{"85", 85},
|
||||
{"200", 200},
|
||||
{"450", 450},
|
||||
{"850", 850}}};
|
||||
|
||||
Checkbox check_inverted{
|
||||
{UI_POS_X(0), UI_POS_Y(1)},
|
||||
10,
|
||||
"Inverted",
|
||||
true};
|
||||
|
||||
OptionsField options_tone{
|
||||
{UI_POS_X(25), UI_POS_Y(1)},
|
||||
5,
|
||||
{{"CNT", -32000},
|
||||
{"0", 0},
|
||||
{"2125", 2125},
|
||||
{"-2125", -2125},
|
||||
{"1275", 1275},
|
||||
{"-1275", -1275}}};
|
||||
|
||||
Text text_tones{
|
||||
{UI_POS_X(0), UI_POS_Y(2), UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
|
||||
Button btn_message{
|
||||
{UI_POS_X(0), UI_POS_Y(3), UI_POS_WIDTH(15), UI_POS_HEIGHT(2)},
|
||||
"Set message"};
|
||||
|
||||
Text text_message{
|
||||
{UI_POS_X(0), UI_POS_Y(5), UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)},
|
||||
""};
|
||||
|
||||
TransmitterView tx_view{
|
||||
(int16_t)UI_POS_Y_BOTTOM(4),
|
||||
1000,
|
||||
0};
|
||||
|
||||
RTTYDataMessage rtty_message{};
|
||||
BaudotCoder baudot_encoder{};
|
||||
|
||||
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.done);
|
||||
}};
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::rtty_tx
|
||||
|
||||
#endif /*__UI_RTTY_TX_H__*/
|
||||
+1
-1
@@ -221,7 +221,7 @@ void SnakeView::paint(Painter& painter) {
|
||||
(void)painter;
|
||||
if (!initialized) {
|
||||
initialized = true;
|
||||
std::srand(LPC_RTC->CTIME0);
|
||||
srand(LPC_RTC->CTIME0);
|
||||
init_game();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -605,7 +605,7 @@ void pause_game() {
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::srand(GenerateRandomSeed());
|
||||
srand(GenerateRandomSeed());
|
||||
Init();
|
||||
ShowLevelMenu();
|
||||
joystick.attach(&ReadJoystickForLevel, 0.3);
|
||||
|
||||
+1
-1
@@ -75,7 +75,7 @@ __attribute__((section(".external_app.app_tpmsrx.application_information"), used
|
||||
},
|
||||
/*.icon_color = */ ui::Color::green().v,
|
||||
/*.menu_location = */ app_location_t::RX,
|
||||
/*.desired_menu_position = */ -1,
|
||||
/*.desired_menu_position = */ 7,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_tpms */ {'P', 'T', 'P', 'M'},
|
||||
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
|
||||
|
||||
+146
-8
@@ -42,16 +42,37 @@ std::string type(tpms::Reading::Type type) {
|
||||
return to_string_dec_uint(toUType(type), 2);
|
||||
}
|
||||
|
||||
std::string type_name(tpms::Reading::Type type) {
|
||||
switch (type) {
|
||||
case tpms::Reading::Type::None:
|
||||
return "None";
|
||||
case tpms::Reading::Type::FLM_64:
|
||||
return "FLM_64";
|
||||
case tpms::Reading::Type::FLM_72:
|
||||
return "FLM_72";
|
||||
case tpms::Reading::Type::FLM_80:
|
||||
return "FLM_80";
|
||||
case tpms::Reading::Type::Schrader:
|
||||
return "Schrader";
|
||||
case tpms::Reading::Type::GMC_96:
|
||||
return "GMC_96";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
std::string id(tpms::TransponderID id) {
|
||||
return to_string_hex(id.value(), 8);
|
||||
}
|
||||
|
||||
std::string pressure(Pressure pressure) {
|
||||
return to_string_dec_int(units_psi ? pressure.psi() : pressure.kilopascal(), 3);
|
||||
return to_string_dec_int(pressure_unit == PRESSURE_UNIT_PSI ? pressure.psi() : pressure_unit == PRESSURE_UNIT_BAR ? pressure.bar()
|
||||
: pressure.kilopascal(),
|
||||
3);
|
||||
}
|
||||
|
||||
std::string temperature(Temperature temperature) {
|
||||
return to_string_dec_int(units_fahr ? temperature.fahrenheit() : temperature.celsius(), 3);
|
||||
return to_string_dec_int(temp_unit == TEMP_UNIT_CELSIUS ? temperature.celsius() : temperature.fahrenheit(), 3);
|
||||
}
|
||||
|
||||
std::string flags(tpms::Flags flags) {
|
||||
@@ -99,7 +120,8 @@ void TPMSRecentEntry::update(const tpms::Reading& reading) {
|
||||
}
|
||||
}
|
||||
|
||||
TPMSAppView::TPMSAppView(NavigationView&) {
|
||||
TPMSAppView::TPMSAppView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
// baseband::run_image(portapack::spi_flash::image_tag_tpms);
|
||||
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
|
||||
|
||||
@@ -122,16 +144,21 @@ TPMSAppView::TPMSAppView(NavigationView&) {
|
||||
options_band.set_by_value(receiver_model.target_frequency());
|
||||
|
||||
options_pressure.on_change = [this](size_t, int32_t i) {
|
||||
format::units_psi = (bool)i;
|
||||
format::pressure_unit = (uint8_t)i;
|
||||
update_view();
|
||||
};
|
||||
options_pressure.set_selected_index(format::units_psi, true);
|
||||
options_pressure.set_by_value(format::pressure_unit);
|
||||
|
||||
options_temperature.on_change = [this](size_t, int32_t i) {
|
||||
format::units_fahr = (bool)i;
|
||||
format::temp_unit = (uint8_t)i;
|
||||
update_view();
|
||||
};
|
||||
options_temperature.set_selected_index(format::units_fahr, true);
|
||||
options_temperature.set_by_value(format::temp_unit);
|
||||
|
||||
// Add on_select handler for entries
|
||||
recent_entries_view.on_select = [this](const TPMSRecentEntry& entry) {
|
||||
on_show_detail(entry);
|
||||
};
|
||||
|
||||
logger = std::make_unique<TPMSLogger>();
|
||||
if (logger) {
|
||||
@@ -176,6 +203,7 @@ void TPMSAppView::on_packet(const tpms::Packet& packet) {
|
||||
const auto reading = reading_opt.value();
|
||||
auto& entry = ::on_packet(recent, TPMSRecentEntry::Key{reading.type(), reading.id()});
|
||||
entry.update(reading);
|
||||
entry.signal_type = packet.signal_type();
|
||||
recent_entries_view.set_dirty();
|
||||
}
|
||||
|
||||
@@ -189,6 +217,116 @@ void TPMSAppView::on_show_list() {
|
||||
recent_entries_view.focus();
|
||||
}
|
||||
|
||||
void TPMSAppView::on_show_detail(const TPMSRecentEntry& entry) {
|
||||
nav_.push<TPMSRecentEntryDetailView>(entry);
|
||||
}
|
||||
|
||||
// Detail view implementation
|
||||
TPMSRecentEntryDetailView::TPMSRecentEntryDetailView(NavigationView& nav, const TPMSRecentEntry& entry)
|
||||
: nav_{nav},
|
||||
entry_{entry} {
|
||||
add_children({&labels,
|
||||
&text_type,
|
||||
&text_id,
|
||||
&text_pressure,
|
||||
&text_temperature,
|
||||
&text_flags,
|
||||
&text_count,
|
||||
&button_done,
|
||||
&button_save});
|
||||
|
||||
// Display entry data
|
||||
text_type.set(format::type_name(entry.type));
|
||||
text_id.set(to_string_hex(entry.id.value(), 8));
|
||||
|
||||
if (entry.last_pressure.is_valid()) {
|
||||
std::string pressure_str = format::pressure(entry.last_pressure.value());
|
||||
std::string unit_str = format::pressure_unit == PRESSURE_UNIT_PSI ? " PSI" : format::pressure_unit == PRESSURE_UNIT_BAR ? " BAR"
|
||||
: " kPa";
|
||||
text_pressure.set(pressure_str + unit_str);
|
||||
} else {
|
||||
text_pressure.set("---");
|
||||
}
|
||||
|
||||
if (entry.last_temperature.is_valid()) {
|
||||
text_temperature.set(to_string_dec_int(entry.last_temperature.value().celsius(), 3) + " C");
|
||||
} else {
|
||||
text_temperature.set("---");
|
||||
}
|
||||
|
||||
if (entry.last_flags.is_valid()) {
|
||||
text_flags.set(to_string_hex(entry.last_flags.value(), 2));
|
||||
} else {
|
||||
text_flags.set("--");
|
||||
}
|
||||
|
||||
text_count.set(to_string_dec_uint(entry.received_count, 4));
|
||||
|
||||
button_done.on_select = [&nav](Button&) {
|
||||
nav.pop();
|
||||
};
|
||||
|
||||
button_save.on_select = [this](Button&) {
|
||||
on_save();
|
||||
};
|
||||
}
|
||||
|
||||
void TPMSRecentEntryDetailView::focus() {
|
||||
button_done.focus();
|
||||
}
|
||||
|
||||
void TPMSRecentEntryDetailView::set_entry(const TPMSRecentEntry& entry) {
|
||||
entry_ = entry;
|
||||
}
|
||||
|
||||
void TPMSRecentEntryDetailView::on_save() {
|
||||
auto timestamp = to_string_timestamp(rtc_time::now());
|
||||
std::string file_name = "TPMS_" + timestamp + ".TXT";
|
||||
ensure_directory(tpms_dir);
|
||||
auto file_path = tpms_dir / file_name;
|
||||
|
||||
if (save_file(file_path)) {
|
||||
nav_.display_modal("Saved", "Packet saved to:\n" + file_name);
|
||||
} else {
|
||||
nav_.display_modal("Error", "Failed to save\npacket");
|
||||
}
|
||||
}
|
||||
|
||||
bool TPMSRecentEntryDetailView::save_file(const std::filesystem::path& path) {
|
||||
File f;
|
||||
auto error = f.create(path);
|
||||
if (error.is_valid())
|
||||
return false;
|
||||
|
||||
// Save in format compatible with TX app
|
||||
std::string content = "Type=" + to_string_dec_uint(toUType(entry_.type), 1) + "\n";
|
||||
content += "ID=" + to_string_hex(entry_.id.value(), 8) + "\n";
|
||||
|
||||
if (entry_.last_pressure.is_valid()) {
|
||||
content += "Pressure=" + to_string_dec_int(entry_.last_pressure.value().kilopascal(), 1) + "\n";
|
||||
} else {
|
||||
content += "Pressure=240\n"; // Default value
|
||||
}
|
||||
|
||||
if (entry_.last_temperature.is_valid()) {
|
||||
content += "Temperature=" + to_string_dec_int(entry_.last_temperature.value().celsius(), 1) + "\n";
|
||||
} else {
|
||||
content += "Temperature=25\n"; // Default value
|
||||
}
|
||||
|
||||
if (entry_.last_flags.is_valid()) {
|
||||
content += "Flags=" + to_string_hex(entry_.last_flags.value(), 2) + "\n";
|
||||
} else {
|
||||
content += "Flags=00\n";
|
||||
}
|
||||
|
||||
// Save signal type for proper retransmission
|
||||
content += "SignalType=" + to_string_dec_uint(toUType(entry_.signal_type), 1) + "\n";
|
||||
|
||||
f.write(content.c_str(), content.length());
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::tpmsrx
|
||||
|
||||
namespace ui {
|
||||
@@ -239,4 +377,4 @@ void RecentEntriesTable<ui::external_app::tpmsrx::TPMSRecentEntries>::draw(
|
||||
painter.draw_string(target_rect.location(), style, line);
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace ui
|
||||
|
||||
+73
-11
@@ -42,10 +42,9 @@ namespace ui::external_app::tpmsrx {
|
||||
|
||||
namespace format {
|
||||
|
||||
static bool units_psi{false};
|
||||
static bool units_fahr{false};
|
||||
|
||||
} /* namespace format */
|
||||
static uint8_t pressure_unit{PRESSURE_UNIT_KPA};
|
||||
static uint8_t temp_unit{TEMP_UNIT_CELSIUS};
|
||||
} // namespace format
|
||||
|
||||
struct TPMSRecentEntry {
|
||||
using Key = std::pair<tpms::Reading::Type, tpms::TransponderID>;
|
||||
@@ -54,6 +53,7 @@ struct TPMSRecentEntry {
|
||||
|
||||
tpms::Reading::Type type{invalid_key.first};
|
||||
tpms::TransponderID id{invalid_key.second};
|
||||
tpms::SignalType signal_type{tpms::SignalType::OOK_8k192_Schrader};
|
||||
|
||||
size_t received_count{0};
|
||||
|
||||
@@ -90,6 +90,64 @@ class TPMSLogger {
|
||||
|
||||
using TPMSRecentEntriesView = RecentEntriesView<TPMSRecentEntries>;
|
||||
|
||||
class TPMSRecentEntryDetailView : public View {
|
||||
public:
|
||||
TPMSRecentEntryDetailView(NavigationView& nav, const TPMSRecentEntry& entry);
|
||||
|
||||
void set_entry(const TPMSRecentEntry& entry);
|
||||
const TPMSRecentEntry& entry() const { return entry_; }
|
||||
|
||||
void focus() override;
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
TPMSRecentEntry entry_;
|
||||
|
||||
void on_save();
|
||||
bool save_file(const std::filesystem::path& path);
|
||||
|
||||
Labels labels{
|
||||
{{0 * 8, 1 * 16}, "Type:", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 3 * 16}, "ID:", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 5 * 16}, "Pressure:", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 7 * 16}, "Temperature:", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 9 * 16}, "Flags:", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 11 * 16}, "Count:", Theme::getInstance()->fg_light->foreground},
|
||||
};
|
||||
|
||||
Text text_type{
|
||||
{8 * 8, 1 * 16, 20 * 8, 16},
|
||||
""};
|
||||
|
||||
Text text_id{
|
||||
{8 * 8, 3 * 16, 20 * 8, 16},
|
||||
""};
|
||||
|
||||
Text text_pressure{
|
||||
{12 * 8, 5 * 16, 16 * 8, 16},
|
||||
""};
|
||||
|
||||
Text text_temperature{
|
||||
{14 * 8, 7 * 16, 14 * 8, 16},
|
||||
""};
|
||||
|
||||
Text text_flags{
|
||||
{8 * 8, 9 * 16, 20 * 8, 16},
|
||||
""};
|
||||
|
||||
Text text_count{
|
||||
{8 * 8, 11 * 16, 20 * 8, 16},
|
||||
""};
|
||||
|
||||
Button button_save{
|
||||
{0 * 8, 13 * 16, 14 * 8, 32},
|
||||
"Save"};
|
||||
|
||||
Button button_done{
|
||||
{16 * 8, 13 * 16, 14 * 8, 32},
|
||||
"Done"};
|
||||
};
|
||||
|
||||
class TPMSAppView : public View {
|
||||
public:
|
||||
TPMSAppView(NavigationView& nav);
|
||||
@@ -106,6 +164,8 @@ class TPMSAppView : public View {
|
||||
std::string title() const override { return "TPMS RX"; };
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
|
||||
RxRadioState radio_state_{
|
||||
314900000 /* frequency*/
|
||||
,
|
||||
@@ -117,8 +177,8 @@ class TPMSAppView : public View {
|
||||
"rx_tpms",
|
||||
app_settings::Mode::RX,
|
||||
{
|
||||
{"units_psi"sv, &format::units_psi},
|
||||
{"units_fahr"sv, &format::units_fahr},
|
||||
{"pressure_unit"sv, &format::pressure_unit},
|
||||
{"temp_unit"sv, &format::temp_unit},
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_packet{
|
||||
@@ -154,15 +214,16 @@ class TPMSAppView : public View {
|
||||
|
||||
OptionsField options_pressure{
|
||||
{6 * 8, UI_POS_Y(0)},
|
||||
3,
|
||||
{{"kPa", 0},
|
||||
{"PSI", 1}}};
|
||||
4,
|
||||
{{"kPa", PRESSURE_UNIT_KPA},
|
||||
{"PSI", PRESSURE_UNIT_PSI},
|
||||
{"BAR", PRESSURE_UNIT_BAR}}};
|
||||
|
||||
OptionsField options_temperature{
|
||||
{10 * 8, UI_POS_Y(0)},
|
||||
2,
|
||||
{{STR_DEGREES_C, 0},
|
||||
{STR_DEGREES_F, 1}}};
|
||||
{{STR_DEGREES_C, TEMP_UNIT_CELSIUS},
|
||||
{STR_DEGREES_F, TEMP_UNIT_FAHRENHEIT}}};
|
||||
|
||||
RFAmpField field_rf_amp{
|
||||
{13 * 8, UI_POS_Y(0)}};
|
||||
@@ -188,6 +249,7 @@ class TPMSAppView : public View {
|
||||
|
||||
void on_packet(const tpms::Packet& packet);
|
||||
void on_show_list();
|
||||
void on_show_detail(const TPMSRecentEntry& entry);
|
||||
void update_view();
|
||||
};
|
||||
|
||||
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright (C) 2026
|
||||
*
|
||||
* 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.hpp"
|
||||
#include "tpms_tx_app.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::tpmstx {
|
||||
void initialize_app(ui::NavigationView& nav) {
|
||||
nav.push<TPMSTXView>();
|
||||
}
|
||||
} // namespace ui::external_app::tpmstx
|
||||
|
||||
extern "C" {
|
||||
|
||||
__attribute__((section(".external_app.app_tpmstx.application_information"), used)) application_information_t _application_information_tpmstx = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000, // will be filled at compile time
|
||||
/*.externalAppEntry = */ ui::external_app::tpmstx::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "TPMS TX",
|
||||
/*.bitmap_data = */ {
|
||||
0xC0,
|
||||
0x03,
|
||||
0xF0,
|
||||
0x0F,
|
||||
0x18,
|
||||
0x18,
|
||||
0xEC,
|
||||
0x37,
|
||||
0x36,
|
||||
0x6D,
|
||||
0x3A,
|
||||
0x59,
|
||||
0x4B,
|
||||
0xD5,
|
||||
0x8B,
|
||||
0xD3,
|
||||
0xCB,
|
||||
0xD1,
|
||||
0xAB,
|
||||
0xD2,
|
||||
0x9A,
|
||||
0x5C,
|
||||
0xB6,
|
||||
0x6C,
|
||||
0xEC,
|
||||
0x37,
|
||||
0x18,
|
||||
0x18,
|
||||
0xF0,
|
||||
0x0F,
|
||||
0xC0,
|
||||
0x03,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::green().v,
|
||||
/*.menu_location = */ app_location_t::TX,
|
||||
/*.desired_menu_position = */ 6,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_ook */ {'P', 'O', 'O', 'K'},
|
||||
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,812 @@
|
||||
/*
|
||||
* Copyright (C) 2026
|
||||
*
|
||||
* 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 "tpms_tx_app.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "spi_image.hpp"
|
||||
#include "ui_fileman.hpp"
|
||||
#include "ui_freqman.hpp"
|
||||
#include "manchester.hpp"
|
||||
#include "rtc_time.hpp"
|
||||
#include "file_path.hpp"
|
||||
#include "encoders.hpp"
|
||||
#include "crc.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
using namespace tpms;
|
||||
|
||||
namespace ui::external_app::tpmstx {
|
||||
|
||||
void TPMSTXView::focus() {
|
||||
options_packet_type.focus();
|
||||
}
|
||||
|
||||
void TPMSTXView::update_signal_type_from_packet() {
|
||||
// Auto-select signal type based on packet type
|
||||
switch (packet_type_) {
|
||||
case tpms::Reading::Type::Schrader:
|
||||
signal_type_ = tpms::SignalType::OOK_8k192_Schrader;
|
||||
break;
|
||||
case tpms::Reading::Type::FLM_64:
|
||||
case tpms::Reading::Type::FLM_72:
|
||||
case tpms::Reading::Type::FLM_80:
|
||||
signal_type_ = tpms::SignalType::FSK_19k2_Schrader;
|
||||
break;
|
||||
case tpms::Reading::Type::GMC_96:
|
||||
signal_type_ = tpms::SignalType::OOK_8k4_Schrader;
|
||||
break;
|
||||
default:
|
||||
signal_type_ = tpms::SignalType::OOK_8k192_Schrader;
|
||||
break;
|
||||
}
|
||||
// Note: Don't call switch_baseband() here - it's called when needed
|
||||
}
|
||||
|
||||
void TPMSTXView::switch_baseband() {
|
||||
// Switch baseband image based on signal type
|
||||
// Must shutdown first to avoid BBRunning error
|
||||
baseband::shutdown();
|
||||
chThdSleepMilliseconds(100);
|
||||
|
||||
if (signal_type_ == tpms::SignalType::FSK_19k2_Schrader) {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_fsktx);
|
||||
} else {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_ook);
|
||||
}
|
||||
|
||||
chThdSleepMilliseconds(100);
|
||||
}
|
||||
|
||||
void TPMSTXView::update_packet_display() {
|
||||
// Only update status text - field values are already set by user interaction
|
||||
// or by explicit set_value calls when loading from file
|
||||
std::string status = "ID:" + to_string_hex(transponder_id_, 8);
|
||||
|
||||
// Note protocol-specific limitations
|
||||
if (packet_type_ == tpms::Reading::Type::Schrader) {
|
||||
status = "ID:" + to_string_hex(transponder_id_ & 0x00FFFFFF, 6) + " (24bit)";
|
||||
}
|
||||
|
||||
status += " " + to_string_dec_uint(pressure_kpa_) + "kPa";
|
||||
|
||||
// Check for pressure overflow and warn user
|
||||
if (packet_type_ == tpms::Reading::Type::GMC_96) {
|
||||
if (pressure_kpa_ > 701) {
|
||||
status += " !MAX"; // GMC_96 max is 701 kPa (101.7 PSI)
|
||||
}
|
||||
} else {
|
||||
if (pressure_kpa_ > 340) {
|
||||
status += " !MAX"; // Schrader/FLM max is 340 kPa (49.3 PSI)
|
||||
}
|
||||
}
|
||||
|
||||
// Temperature note for protocols that support it
|
||||
if (packet_type_ == tpms::Reading::Type::GMC_96 ||
|
||||
packet_type_ == tpms::Reading::Type::FLM_64 ||
|
||||
packet_type_ == tpms::Reading::Type::FLM_72 ||
|
||||
packet_type_ == tpms::Reading::Type::FLM_80) {
|
||||
status += " " + to_string_dec_int(temperature_c_) + "C";
|
||||
} else {
|
||||
status += " (no temp)";
|
||||
}
|
||||
|
||||
text_status.set(status);
|
||||
}
|
||||
|
||||
void TPMSTXView::update_field_visibility() {
|
||||
// Schrader: Show Func, Hide Temp (no temperature support)
|
||||
// GMC_96, FLM_xx: Hide Func, Show Temp (temperature supported, no func field)
|
||||
if (packet_type_ == tpms::Reading::Type::Schrader) {
|
||||
// Show Func field and label
|
||||
label_flags.hidden(false);
|
||||
field_flags.hidden(false);
|
||||
// Hide Temp field, label, and unit selector
|
||||
label_temperature.hidden(true);
|
||||
field_temperature.hidden(true);
|
||||
options_temperature.hidden(true);
|
||||
// Show 24-bit ID field, hide 32-bit ID field
|
||||
field_transponder_id_24.hidden(false);
|
||||
field_transponder_id_32.hidden(true);
|
||||
} else {
|
||||
// Hide Func field and label
|
||||
label_flags.hidden(true);
|
||||
field_flags.hidden(true);
|
||||
// Show Temp field, label, and unit selector
|
||||
label_temperature.hidden(false);
|
||||
field_temperature.hidden(false);
|
||||
options_temperature.hidden(false);
|
||||
// Show 32-bit ID field, hide 24-bit ID field
|
||||
field_transponder_id_24.hidden(true);
|
||||
field_transponder_id_32.hidden(false);
|
||||
}
|
||||
// Force screen refresh to clear hidden widgets
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
void TPMSTXView::on_pressure_unit_change() {
|
||||
// Convert displayed pressure to new unit and update field
|
||||
units::Pressure pressure(pressure_kpa_);
|
||||
int display_value = 0;
|
||||
|
||||
if (format::pressure_unit == PRESSURE_UNIT_PSI) {
|
||||
display_value = pressure.psi();
|
||||
} else if (format::pressure_unit == PRESSURE_UNIT_BAR) {
|
||||
display_value = pressure.bar();
|
||||
} else {
|
||||
display_value = pressure.kilopascal();
|
||||
}
|
||||
|
||||
field_pressure.set_value(display_value);
|
||||
}
|
||||
|
||||
void TPMSTXView::on_temperature_unit_change() {
|
||||
// Convert displayed temperature to new unit and update field
|
||||
units::Temperature temperature(temperature_c_);
|
||||
int display_value = 0;
|
||||
|
||||
if (format::temp_unit == TEMP_UNIT_FAHRENHEIT) {
|
||||
display_value = temperature.fahrenheit();
|
||||
} else {
|
||||
display_value = temperature.celsius();
|
||||
}
|
||||
|
||||
field_temperature.set_value(display_value);
|
||||
}
|
||||
|
||||
void TPMSTXView::encode_and_transmit() {
|
||||
if (!is_transmitting_) return;
|
||||
|
||||
// Build TPMS packet based on signal type
|
||||
std::string binary_string;
|
||||
uint32_t symbol_rate;
|
||||
uint32_t sample_rate;
|
||||
|
||||
// Set sample rate based on signal type to match transmitter config
|
||||
if (signal_type_ == tpms::SignalType::FSK_19k2_Schrader) {
|
||||
sample_rate = 2280000; // 2.28 MHz for FSK (matches transmitter_model config)
|
||||
} else {
|
||||
sample_rate = 2000000; // 2 MHz for OOK
|
||||
}
|
||||
|
||||
if (signal_type_ == tpms::SignalType::OOK_8k192_Schrader) {
|
||||
// Schrader OOK 8k192 format (Type = Schrader)
|
||||
// Preamble: 11*2, 01*14, 11, 10
|
||||
// Data: 3 bits function code, 24 bits ID, 8 bits pressure, 2 bits checksum
|
||||
// Total: 37 Manchester symbols (74 bits after encoding)
|
||||
// NOTE: This protocol does NOT support temperature transmission
|
||||
// NOTE: Only lower 24 bits of ID are transmitted
|
||||
// NOTE: Function code (flags_) is stored as 3-bit value (0-7)
|
||||
// RX will display it combined with checksum in upper nibble
|
||||
|
||||
symbol_rate = 8192;
|
||||
|
||||
// Preamble as expected by RX decoder
|
||||
binary_string = "1111"; // 11*2
|
||||
for (int i = 0; i < 14; i++) {
|
||||
binary_string += "01";
|
||||
}
|
||||
binary_string += "1110"; // 11, 10
|
||||
|
||||
// Build data bits (pre-Manchester)
|
||||
uint64_t data = 0;
|
||||
|
||||
// 3-bit flags (0-7, stored directly)
|
||||
uint8_t flags_3bit = flags_ & 0x07;
|
||||
data |= ((uint64_t)flags_3bit << 34);
|
||||
|
||||
// 24-bit ID (only use lower 24 bits - protocol limitation)
|
||||
uint32_t id_24bit = transponder_id_ & 0x00FFFFFF;
|
||||
data |= ((uint64_t)id_24bit << 10);
|
||||
|
||||
// 8-bit pressure (convert kPa to raw: kPa * 3/4)
|
||||
// Clamp to prevent overflow: max raw = 255, max kPa = 255 * 4/3 = 340 (49.3 PSI)
|
||||
uint16_t pressure_clamped = (pressure_kpa_ > 340) ? 340 : pressure_kpa_;
|
||||
uint8_t pressure_raw = (pressure_clamped * 3 / 4);
|
||||
data |= ((uint64_t)pressure_raw << 2);
|
||||
|
||||
// Calculate 2-bit checksum: sum all 2-bit pairs, result & 3 must equal 3
|
||||
uint32_t checksum_calc = (data >> 36) & 1; // First bit
|
||||
for (size_t i = 1; i < 37; i += 2) {
|
||||
checksum_calc += (data >> (37 - i - 2)) & 3;
|
||||
}
|
||||
uint8_t checksum_2bit = (3 - (checksum_calc & 3)) & 3;
|
||||
data |= checksum_2bit;
|
||||
|
||||
// Manchester encode the 37 data bits
|
||||
for (int i = 36; i >= 0; i--) {
|
||||
if ((data >> i) & 1) {
|
||||
binary_string += "10"; // '1' = 10 in Manchester
|
||||
} else {
|
||||
binary_string += "01"; // '0' = 01 in Manchester
|
||||
}
|
||||
}
|
||||
|
||||
} else if (signal_type_ == tpms::SignalType::OOK_8k4_Schrader) {
|
||||
// GMC_96 OOK 8k4 format
|
||||
symbol_rate = 8400;
|
||||
|
||||
// Preamble: 01*40
|
||||
for (int i = 0; i < 40; i++) {
|
||||
binary_string += "01";
|
||||
}
|
||||
|
||||
// First nibble of System ID (0x4) - part of preamble pattern match
|
||||
// RX looks for pattern ending with: 01010101...01100101
|
||||
// The "01100101" = Manchester for 0x4, and is consumed by pattern matcher
|
||||
binary_string += "01"; // 0
|
||||
binary_string += "10"; // 1
|
||||
binary_string += "01"; // 0
|
||||
binary_string += "01"; // 0 (0100 = 0x4)
|
||||
|
||||
// Remaining 20 bits of system ID (padding with zeros)
|
||||
// These are the first 20 bits of the 76-bit payload
|
||||
for (int i = 0; i < 20; i++) {
|
||||
binary_string += "01"; // All zeros for padding
|
||||
}
|
||||
|
||||
// 32-bit ID (Manchester encoded)
|
||||
for (int i = 31; i >= 0; i--) {
|
||||
if ((transponder_id_ >> i) & 1) {
|
||||
binary_string += "10";
|
||||
} else {
|
||||
binary_string += "01";
|
||||
}
|
||||
}
|
||||
|
||||
// Pressure: kPa * 4/11
|
||||
// Clamp to prevent overflow: max raw = 255, max kPa = 255 * 11/4 = 701.25 (101.7 PSI)
|
||||
uint16_t pressure_clamped = (pressure_kpa_ > 701) ? 701 : pressure_kpa_;
|
||||
uint8_t pressure_gmc = (pressure_clamped * 4 / 11);
|
||||
for (int i = 7; i >= 0; i--) {
|
||||
if ((pressure_gmc >> i) & 1) {
|
||||
binary_string += "10";
|
||||
} else {
|
||||
binary_string += "01";
|
||||
}
|
||||
}
|
||||
|
||||
// Temperature: temp + 61
|
||||
uint8_t temp_gmc = (temperature_c_ + 61) & 0xFF;
|
||||
for (int i = 7; i >= 0; i--) {
|
||||
if ((temp_gmc >> i) & 1) {
|
||||
binary_string += "10";
|
||||
} else {
|
||||
binary_string += "01";
|
||||
}
|
||||
}
|
||||
|
||||
// Checksum: sum of all data bytes (system ID + ID + pressure + temp)
|
||||
// System ID byte 0: (0x4 << 4) | 0x0 = 0x40
|
||||
// System ID byte 1: 0x00
|
||||
// System ID byte 2: 0x00
|
||||
uint8_t checksum = 0x40; // First byte of system ID
|
||||
checksum += 0x00; // Second byte of system ID
|
||||
checksum += 0x00; // Third byte of system ID
|
||||
|
||||
// Add all 4 bytes of the ID
|
||||
checksum += (transponder_id_ >> 24) & 0xFF;
|
||||
checksum += (transponder_id_ >> 16) & 0xFF;
|
||||
checksum += (transponder_id_ >> 8) & 0xFF;
|
||||
checksum += transponder_id_ & 0xFF;
|
||||
|
||||
// Add pressure and temperature
|
||||
checksum += pressure_gmc;
|
||||
checksum += temp_gmc;
|
||||
|
||||
for (int i = 7; i >= 0; i--) {
|
||||
if ((checksum >> i) & 1) {
|
||||
binary_string += "10";
|
||||
} else {
|
||||
binary_string += "01";
|
||||
}
|
||||
}
|
||||
|
||||
} else if (signal_type_ == tpms::SignalType::FSK_19k2_Schrader) {
|
||||
// FSK 19.2k for FLM variants
|
||||
// Data is Manchester encoded: each data bit becomes two FSK symbols
|
||||
// Preamble: 14 x '01' + '10' = 30 bits (matches RX pattern exactly)
|
||||
// Payload: 160 Manchester-encoded bits (80 data bits max)
|
||||
|
||||
symbol_rate = 19200;
|
||||
|
||||
// Build preamble: 14 pairs of "01" followed by "10"
|
||||
for (int i = 0; i < 14; i++) {
|
||||
binary_string += "01";
|
||||
}
|
||||
binary_string += "10";
|
||||
|
||||
std::array<uint8_t, 20> data_bytes = {0}; // 160 bits = 20 bytes max
|
||||
size_t num_data_bits = 0;
|
||||
|
||||
if (packet_type_ == tpms::Reading::Type::FLM_64) {
|
||||
// FLM_64: 64 bits (8 bytes)
|
||||
// Bytes 0-3: ID (32 bits)
|
||||
// Byte 4: Pressure (8 bits)
|
||||
// Byte 5: Temperature (8 bits, LSB 7 bits used)
|
||||
// Byte 6: Padding
|
||||
// Byte 7: Sum checksum of bytes 0-6 (low 8 bits)
|
||||
|
||||
num_data_bits = 64;
|
||||
|
||||
data_bytes[0] = (transponder_id_ >> 24) & 0xFF;
|
||||
data_bytes[1] = (transponder_id_ >> 16) & 0xFF;
|
||||
data_bytes[2] = (transponder_id_ >> 8) & 0xFF;
|
||||
data_bytes[3] = transponder_id_ & 0xFF;
|
||||
// Clamp pressure to prevent overflow: max 340 kPa (49.3 PSI)
|
||||
uint16_t pressure_clamped_flm64 = (pressure_kpa_ > 340) ? 340 : pressure_kpa_;
|
||||
data_bytes[4] = (pressure_clamped_flm64 * 3 / 4);
|
||||
data_bytes[5] = ((temperature_c_ + 56) & 0x7F); // LSB 7 bits only
|
||||
data_bytes[6] = 0x00; // Padding
|
||||
|
||||
// Addition checksum over bytes 0-6
|
||||
uint32_t checksum = 0;
|
||||
for (int i = 0; i < 7; i++) {
|
||||
checksum += data_bytes[i];
|
||||
}
|
||||
data_bytes[7] = checksum & 0xFF;
|
||||
|
||||
} else if (packet_type_ == tpms::Reading::Type::FLM_72) {
|
||||
// FLM_72: 72 bits (9 bytes)
|
||||
// Bytes 0-3: ID (32 bits)
|
||||
// Byte 4: Padding
|
||||
// Byte 5: Pressure (8 bits)
|
||||
// Byte 6: Temperature (8 bits)
|
||||
// Bytes 7-8: CRC field - RX processes ALL 9 bytes and expects CRC result = 0
|
||||
|
||||
num_data_bits = 72;
|
||||
|
||||
data_bytes[0] = (transponder_id_ >> 24) & 0xFF;
|
||||
data_bytes[1] = (transponder_id_ >> 16) & 0xFF;
|
||||
data_bytes[2] = (transponder_id_ >> 8) & 0xFF;
|
||||
data_bytes[3] = transponder_id_ & 0xFF;
|
||||
data_bytes[4] = 0x00; // Padding
|
||||
// Clamp pressure to prevent overflow: max 340 kPa (49.3 PSI)
|
||||
uint16_t pressure_clamped_flm = (pressure_kpa_ > 340) ? 340 : pressure_kpa_;
|
||||
data_bytes[5] = (pressure_clamped_flm * 3 / 4);
|
||||
data_bytes[6] = (temperature_c_ + 56) & 0xFF;
|
||||
data_bytes[7] = 0x00; // Set to 0 initially
|
||||
|
||||
// Calculate CRC over bytes 0-7, place result in byte 8
|
||||
// When RX calculates CRC over bytes 0-8, it should get residual 0
|
||||
CRC<8> crc{0x01, 0x00};
|
||||
for (int i = 0; i < 8; i++) {
|
||||
crc.process_byte(data_bytes[i]);
|
||||
}
|
||||
data_bytes[8] = crc.checksum() & 0xFF;
|
||||
|
||||
} else if (packet_type_ == tpms::Reading::Type::FLM_80) {
|
||||
// FLM_80: 80 bits (10 bytes)
|
||||
// Byte 0: Padding
|
||||
// Bytes 1-4: ID (32 bits)
|
||||
// Byte 5: Padding
|
||||
// Byte 6: Pressure (8 bits)
|
||||
// Byte 7: Temperature (8 bits)
|
||||
// Bytes 8-9: CRC field - RX processes bytes 1-9 and expects CRC result = 0
|
||||
|
||||
num_data_bits = 80;
|
||||
|
||||
data_bytes[0] = 0x00; // Padding (not included in CRC)
|
||||
data_bytes[1] = (transponder_id_ >> 24) & 0xFF;
|
||||
data_bytes[2] = (transponder_id_ >> 16) & 0xFF;
|
||||
data_bytes[3] = (transponder_id_ >> 8) & 0xFF;
|
||||
data_bytes[4] = transponder_id_ & 0xFF;
|
||||
data_bytes[5] = 0x00; // Padding
|
||||
// Clamp pressure to prevent overflow: max 340 kPa (49.3 PSI)
|
||||
uint16_t pressure_clamped_flm80 = (pressure_kpa_ > 340) ? 340 : pressure_kpa_;
|
||||
data_bytes[6] = (pressure_clamped_flm80 * 3 / 4);
|
||||
data_bytes[7] = (temperature_c_ + 56) & 0xFF;
|
||||
data_bytes[8] = 0x00; // Padding/Reserved
|
||||
data_bytes[9] = 0x00; // CRC byte
|
||||
|
||||
// Calculate CRC over bytes 1-8 (all bytes except 0 and 9)
|
||||
// When RX calculates CRC over bytes 1-9, it should get residual 0
|
||||
CRC<8> crc{0x01, 0x00};
|
||||
for (int i = 1; i <= 8; i++) {
|
||||
crc.process_byte(data_bytes[i]);
|
||||
}
|
||||
data_bytes[9] = crc.checksum() & 0xFF;
|
||||
}
|
||||
|
||||
// Manchester-encode data bits for FSK
|
||||
// RX ManchesterDecoder with sense=0: '1' = "10", '0' = "01"
|
||||
size_t num_bytes = num_data_bits / 8;
|
||||
for (size_t byte_idx = 0; byte_idx < num_bytes; byte_idx++) {
|
||||
uint8_t byte_val = data_bytes[byte_idx];
|
||||
for (int bit_idx = 7; bit_idx >= 0; bit_idx--) {
|
||||
if ((byte_val >> bit_idx) & 1) {
|
||||
binary_string += "10"; // Manchester '1'
|
||||
} else {
|
||||
binary_string += "01"; // Manchester '0'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pad payload to 160 bits with valid Manchester pairs ("01" = 0)
|
||||
// Using proper Manchester encoding maintains clock recovery sync
|
||||
while (binary_string.length() < 190) { // 30 preamble + 160 payload
|
||||
binary_string += "01"; // Manchester-encoded zero
|
||||
}
|
||||
|
||||
} else {
|
||||
text_status.set("Unknown signal type");
|
||||
stop_tx();
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert binary string to bitstream
|
||||
size_t bitstream_length = encoders::make_bitstream(binary_string);
|
||||
|
||||
// Calculate samples per bit (round to nearest for best timing accuracy)
|
||||
uint32_t samples_per_bit = (sample_rate + symbol_rate / 2) / symbol_rate;
|
||||
|
||||
// Update status to show we're sending
|
||||
text_status.set("TX: " + to_string_dec_uint(binary_string.length()) + " bits");
|
||||
|
||||
// Send via appropriate baseband (OOK or FSK)
|
||||
if (signal_type_ == tpms::SignalType::FSK_19k2_Schrader) {
|
||||
// FSK mode: 19.2 kbaud, 38.4 kHz shift
|
||||
baseband::set_fsk_data(
|
||||
bitstream_length,
|
||||
samples_per_bit,
|
||||
38400, // 38.4 kHz shift
|
||||
256); // Progress notice interval
|
||||
} else {
|
||||
// OOK mode
|
||||
baseband::set_ook_data(
|
||||
bitstream_length,
|
||||
samples_per_bit,
|
||||
repeat_count_,
|
||||
pause_duration_);
|
||||
}
|
||||
}
|
||||
|
||||
void TPMSTXView::handle_tx_complete() {
|
||||
// For FSK (FLM packets), handle repeats at application level
|
||||
// OOK repeats are handled by the baseband processor
|
||||
if (signal_type_ == tpms::SignalType::FSK_19k2_Schrader) {
|
||||
fsk_repeat_counter_++;
|
||||
|
||||
if (fsk_repeat_counter_ < repeat_count_) {
|
||||
// More repeats needed - send the next one
|
||||
// Update progress bar
|
||||
progressbar.set_value(fsk_repeat_counter_);
|
||||
|
||||
// Wait for FSK processor to fully complete and reset
|
||||
// This allows the shared memory bitstream to be safely rewritten
|
||||
chThdSleepMilliseconds(50);
|
||||
|
||||
encode_and_transmit();
|
||||
} else {
|
||||
// All FSK repeats complete
|
||||
stop_tx();
|
||||
}
|
||||
} else {
|
||||
// OOK repeats are handled by baseband, just stop here
|
||||
stop_tx();
|
||||
}
|
||||
}
|
||||
|
||||
void TPMSTXView::start_tx() {
|
||||
if (is_transmitting_)
|
||||
return;
|
||||
|
||||
is_transmitting_ = true;
|
||||
|
||||
progressbar.set_max(repeat_count_);
|
||||
progressbar.set_value(0);
|
||||
|
||||
button_transmit.set_text("STOP TX");
|
||||
text_status.set("Transmitting...");
|
||||
|
||||
// Initialize FSK repeat counter for application-level repeat handling
|
||||
fsk_repeat_counter_ = 0;
|
||||
|
||||
// Switch to appropriate baseband before transmission
|
||||
switch_baseband();
|
||||
|
||||
// Configure transmitter based on modulation type
|
||||
if (signal_type_ == tpms::SignalType::FSK_19k2_Schrader) {
|
||||
// FSK configuration
|
||||
transmitter_model.set_sampling_rate(2280000); // 2.28 MHz for FSK
|
||||
transmitter_model.set_baseband_bandwidth(1750000);
|
||||
} else {
|
||||
// OOK configuration
|
||||
transmitter_model.set_sampling_rate(2000000); // 2 MHz for OOK
|
||||
transmitter_model.set_baseband_bandwidth(1750000);
|
||||
}
|
||||
|
||||
transmitter_model.enable();
|
||||
|
||||
// Start transmission
|
||||
encode_and_transmit();
|
||||
}
|
||||
|
||||
void TPMSTXView::stop_tx() {
|
||||
if (!is_transmitting_)
|
||||
return;
|
||||
|
||||
is_transmitting_ = false;
|
||||
transmitter_model.disable();
|
||||
|
||||
button_transmit.set_text("START TX");
|
||||
text_status.set("Transmission complete");
|
||||
progressbar.set_value(0);
|
||||
}
|
||||
|
||||
TPMSTXView::TPMSTXView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
// Start with OOK baseband (will switch if needed)
|
||||
baseband::run_image(portapack::spi_flash::image_tag_ook);
|
||||
|
||||
add_children({&labels,
|
||||
&label_temperature,
|
||||
&label_flags,
|
||||
&options_frequency,
|
||||
&options_packet_type,
|
||||
&field_transponder_id_24,
|
||||
&field_transponder_id_32,
|
||||
&field_pressure,
|
||||
&options_pressure,
|
||||
&field_temperature,
|
||||
&options_temperature,
|
||||
&field_flags,
|
||||
&field_repeat,
|
||||
&button_load,
|
||||
&button_save,
|
||||
&tx_view,
|
||||
&button_transmit,
|
||||
&text_status,
|
||||
&progressbar});
|
||||
|
||||
// Set label styles to match other labels (light grey)
|
||||
label_temperature.set_style(Theme::getInstance()->fg_light);
|
||||
label_flags.set_style(Theme::getInstance()->fg_light);
|
||||
|
||||
// Initialize values
|
||||
options_frequency.set_by_value(transmitter_model.target_frequency());
|
||||
options_packet_type.set_selected_index(0);
|
||||
field_repeat.set_value(repeat_count_);
|
||||
|
||||
// Set initial signal type based on packet type
|
||||
update_signal_type_from_packet();
|
||||
|
||||
// Initialize unit selection
|
||||
options_pressure.set_by_value(format::pressure_unit);
|
||||
options_temperature.set_by_value(format::temp_unit);
|
||||
|
||||
// Initialize field values from default member variables
|
||||
// For pressure and temperature, use unit conversion to display in selected units
|
||||
field_transponder_id_24.set_value(transponder_id_ & 0x00FFFFFF);
|
||||
field_transponder_id_32.set_value(transponder_id_);
|
||||
on_pressure_unit_change();
|
||||
on_temperature_unit_change();
|
||||
field_flags.set_value(flags_);
|
||||
|
||||
update_field_visibility();
|
||||
update_packet_display();
|
||||
|
||||
// Event handlers
|
||||
options_frequency.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
transmitter_model.set_target_frequency(v);
|
||||
};
|
||||
|
||||
options_packet_type.on_change = [this](size_t, int32_t value) {
|
||||
packet_type_ = static_cast<tpms::Reading::Type>(value);
|
||||
update_signal_type_from_packet();
|
||||
|
||||
// Sync field values when switching packet types
|
||||
if (packet_type_ == tpms::Reading::Type::Schrader) {
|
||||
// Switching to Schrader: copy from 32-bit field to 24-bit field (masked)
|
||||
transponder_id_ = field_transponder_id_32.to_integer() & 0x00FFFFFF;
|
||||
field_transponder_id_24.set_value(transponder_id_);
|
||||
} else {
|
||||
// Switching from Schrader: copy from 24-bit field to 32-bit field
|
||||
transponder_id_ = field_transponder_id_24.to_integer();
|
||||
field_transponder_id_32.set_value(transponder_id_);
|
||||
}
|
||||
|
||||
update_field_visibility();
|
||||
update_packet_display();
|
||||
};
|
||||
|
||||
options_pressure.on_change = [this](size_t, int32_t i) {
|
||||
format::pressure_unit = (uint8_t)i;
|
||||
on_pressure_unit_change();
|
||||
};
|
||||
|
||||
options_temperature.on_change = [this](size_t, int32_t i) {
|
||||
format::temp_unit = (uint8_t)i;
|
||||
on_temperature_unit_change();
|
||||
};
|
||||
|
||||
field_transponder_id_24.on_change = [this](SymField&) {
|
||||
transponder_id_ = field_transponder_id_24.to_integer() & 0x00FFFFFF;
|
||||
update_packet_display();
|
||||
};
|
||||
|
||||
field_transponder_id_32.on_change = [this](SymField&) {
|
||||
transponder_id_ = field_transponder_id_32.to_integer();
|
||||
update_packet_display();
|
||||
};
|
||||
|
||||
field_pressure.on_change = [this](int32_t value) {
|
||||
// Convert from displayed unit to kPa for internal storage
|
||||
if (format::pressure_unit == PRESSURE_UNIT_PSI) {
|
||||
pressure_kpa_ = value * 6895 / 1000;
|
||||
} else if (format::pressure_unit == PRESSURE_UNIT_BAR) {
|
||||
pressure_kpa_ = value * 100;
|
||||
} else {
|
||||
pressure_kpa_ = value;
|
||||
}
|
||||
update_packet_display();
|
||||
};
|
||||
|
||||
field_temperature.on_change = [this](int32_t value) {
|
||||
// Convert from displayed unit to Celsius for internal storage
|
||||
if (format::temp_unit == TEMP_UNIT_FAHRENHEIT) {
|
||||
temperature_c_ = (value - 32) * 5 / 9;
|
||||
} else {
|
||||
temperature_c_ = value;
|
||||
}
|
||||
update_packet_display();
|
||||
};
|
||||
|
||||
field_flags.on_change = [this](int32_t value) {
|
||||
flags_ = value & 0x07;
|
||||
update_packet_display();
|
||||
};
|
||||
|
||||
field_repeat.on_change = [this](int32_t value) {
|
||||
repeat_count_ = value;
|
||||
};
|
||||
|
||||
button_transmit.on_select = [this](Button&) {
|
||||
if (is_transmitting_) {
|
||||
stop_tx();
|
||||
} else {
|
||||
start_tx();
|
||||
}
|
||||
};
|
||||
|
||||
button_load.on_select = [this, &nav](Button&) {
|
||||
auto open_view = nav.push<FileLoadView>(".TXT");
|
||||
open_view->on_changed = [this](std::filesystem::path file_path) {
|
||||
// Load TPMS packet from file
|
||||
File f;
|
||||
auto error = f.open(file_path);
|
||||
if (!error.is_valid()) {
|
||||
char buffer[512];
|
||||
auto result = f.read(buffer, sizeof(buffer) - 1);
|
||||
if (result.is_ok()) {
|
||||
buffer[result.value()] = 0;
|
||||
|
||||
// Parse the file format (key=value format, one per line)
|
||||
std::string content(buffer);
|
||||
size_t pos = 0;
|
||||
|
||||
auto parse_line = [&content, &pos](const std::string& key) -> std::string {
|
||||
size_t key_pos = content.find(key + "=", pos);
|
||||
if (key_pos != std::string::npos) {
|
||||
size_t value_start = key_pos + key.length() + 1;
|
||||
size_t value_end = content.find('\n', value_start);
|
||||
if (value_end == std::string::npos) value_end = content.length();
|
||||
return content.substr(value_start, value_end - value_start);
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
std::string type_str = parse_line("Type");
|
||||
std::string id_str = parse_line("ID");
|
||||
std::string pressure_str = parse_line("Pressure");
|
||||
std::string temp_str = parse_line("Temperature");
|
||||
std::string flags_str = parse_line("Flags");
|
||||
std::string signal_type_str = parse_line("SignalType");
|
||||
|
||||
if (!type_str.empty()) {
|
||||
int type = std::stoi(type_str);
|
||||
if (type >= static_cast<int>(tpms::Reading::Type::FLM_64) &&
|
||||
type <= static_cast<int>(tpms::Reading::Type::GMC_96)) {
|
||||
packet_type_ = static_cast<tpms::Reading::Type>(type);
|
||||
options_packet_type.set_by_value(type);
|
||||
}
|
||||
}
|
||||
|
||||
if (!id_str.empty()) {
|
||||
transponder_id_ = std::stoul(id_str, nullptr, 16);
|
||||
field_transponder_id_24.set_value(transponder_id_ & 0x00FFFFFF);
|
||||
field_transponder_id_32.set_value(transponder_id_);
|
||||
}
|
||||
|
||||
if (!pressure_str.empty()) {
|
||||
pressure_kpa_ = std::stoi(pressure_str);
|
||||
on_pressure_unit_change();
|
||||
}
|
||||
|
||||
if (!temp_str.empty()) {
|
||||
temperature_c_ = std::stoi(temp_str);
|
||||
field_temperature.set_value(temperature_c_);
|
||||
}
|
||||
|
||||
if (!flags_str.empty()) {
|
||||
uint8_t loaded_flags = std::stoul(flags_str, nullptr, 16);
|
||||
// Handle old format files: extract bits 6-4 if value > 7
|
||||
flags_ = (loaded_flags > 7) ? ((loaded_flags >> 4) & 0x07) : (loaded_flags & 0x07);
|
||||
field_flags.set_value(flags_);
|
||||
}
|
||||
|
||||
// Load signal type if available, otherwise auto-select based on packet type
|
||||
if (!signal_type_str.empty()) {
|
||||
int signal_type = std::stoi(signal_type_str);
|
||||
if (signal_type >= 1 && signal_type <= 3) {
|
||||
signal_type_ = static_cast<tpms::SignalType>(signal_type);
|
||||
} else {
|
||||
// Fall back to auto-detect
|
||||
update_signal_type_from_packet();
|
||||
}
|
||||
} else {
|
||||
// Fall back to auto-detect for backwards compatibility
|
||||
update_signal_type_from_packet();
|
||||
}
|
||||
|
||||
update_packet_display();
|
||||
update_field_visibility();
|
||||
text_status.set("Loaded: " + file_path.filename().string());
|
||||
} else {
|
||||
text_status.set("Error reading file");
|
||||
}
|
||||
} else {
|
||||
text_status.set("Error opening file");
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
button_save.on_select = [this](Button&) {
|
||||
// Save current TPMS packet to file
|
||||
auto timestamp = to_string_timestamp(rtc_time::now());
|
||||
std::string file_name = "TPMS_" + timestamp + ".TXT";
|
||||
ensure_directory(tpms_dir);
|
||||
auto file_path = tpms_dir / file_name;
|
||||
|
||||
File f;
|
||||
auto error = f.create(file_path);
|
||||
if (!error.is_valid()) {
|
||||
std::string content = "Type=" + to_string_dec_uint(toUType(packet_type_), 1) + "\n";
|
||||
content += "ID=" + to_string_hex(transponder_id_, 8) + "\n";
|
||||
content += "Pressure=" + to_string_dec_uint(pressure_kpa_) + "\n";
|
||||
content += "Temperature=" + to_string_dec_int(temperature_c_) + "\n";
|
||||
content += "Flags=" + to_string_hex(flags_ & 0x07, 1) + "\n";
|
||||
|
||||
f.write(content.c_str(), content.length());
|
||||
text_status.set("Saved: " + file_name);
|
||||
} else {
|
||||
text_status.set("Error saving file");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
TPMSTXView::~TPMSTXView() {
|
||||
stop_tx();
|
||||
transmitter_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::tpmstx
|
||||
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
* Copyright (C) 2026
|
||||
*
|
||||
* 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 __TPMS_TX_APP_H__
|
||||
#define __TPMS_TX_APP_H__
|
||||
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_transmitter.hpp"
|
||||
#include "transmitter_model.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "message.hpp"
|
||||
#include "tpms_packet.hpp"
|
||||
#include "file_path.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "units.hpp"
|
||||
|
||||
namespace ui::external_app::tpmstx {
|
||||
|
||||
namespace format {
|
||||
static uint8_t pressure_unit{PRESSURE_UNIT_PSI};
|
||||
static uint8_t temp_unit{TEMP_UNIT_CELSIUS};
|
||||
} // namespace format
|
||||
|
||||
class TPMSTXView : public View {
|
||||
public:
|
||||
TPMSTXView(NavigationView& nav);
|
||||
~TPMSTXView();
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "TPMS TX"; };
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
|
||||
TxRadioState radio_state_{
|
||||
314900000, /* frequency */
|
||||
1750000, /* bandwidth */
|
||||
2457600 /* sampling rate */
|
||||
};
|
||||
|
||||
app_settings::SettingsManager settings_{
|
||||
"tx_tpms",
|
||||
app_settings::Mode::TX,
|
||||
{
|
||||
{"pressure_unit"sv, &format::pressure_unit},
|
||||
{"temp_unit"sv, &format::temp_unit},
|
||||
}};
|
||||
|
||||
// TPMS packet data
|
||||
tpms::Reading::Type packet_type_{tpms::Reading::Type::Schrader};
|
||||
uint32_t transponder_id_{0x12345678};
|
||||
uint16_t pressure_kpa_{240}; // Default ~35 PSI
|
||||
int16_t temperature_c_{25}; // Default 25°C
|
||||
uint8_t flags_{0x00}; // 3-bit function code (0-7), checksum is auto-calculated
|
||||
tpms::SignalType signal_type_{tpms::SignalType::FSK_19k2_Schrader};
|
||||
|
||||
uint8_t repeat_count_{5};
|
||||
uint32_t pause_duration_{50}; // ms between repeats
|
||||
|
||||
bool is_transmitting_{false};
|
||||
|
||||
// FSK-specific repeat tracking (OOK repeats are handled by baseband)
|
||||
uint8_t fsk_repeat_counter_{0};
|
||||
uint32_t fsk_repeat_timer_id_{0};
|
||||
|
||||
void update_signal_type_from_packet();
|
||||
void switch_baseband();
|
||||
|
||||
void start_tx();
|
||||
void stop_tx();
|
||||
void send_packet();
|
||||
void encode_and_transmit();
|
||||
void update_packet_display();
|
||||
void on_pressure_unit_change();
|
||||
void on_temperature_unit_change();
|
||||
void update_field_visibility();
|
||||
|
||||
MessageHandlerRegistration message_handler_tx_progress{
|
||||
Message::ID::TXProgress,
|
||||
[this](const Message* const p) {
|
||||
const auto message = *reinterpret_cast<const TXProgressMessage*>(p);
|
||||
progressbar.set_value(message.progress);
|
||||
if (message.done) {
|
||||
handle_tx_complete();
|
||||
}
|
||||
}};
|
||||
|
||||
void handle_tx_complete();
|
||||
|
||||
Labels labels{
|
||||
{{0 * 8, 0 * 16}, "Freq:", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 1 * 16}, "Type:", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 2 * 16}, "ID:", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 3 * 16}, "Pres:", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 5 * 16}, "Rpt:", Theme::getInstance()->fg_light->foreground},
|
||||
};
|
||||
|
||||
Text label_temperature{
|
||||
{0 * 8, 4 * 16, 5 * 8, 16},
|
||||
"Temp:"};
|
||||
|
||||
Text label_flags{
|
||||
{0 * 8, 4 * 16, 5 * 8, 16},
|
||||
"Func:"};
|
||||
|
||||
OptionsField options_frequency{
|
||||
{6 * 8, 0 * 16},
|
||||
5,
|
||||
{
|
||||
{"314.9", 314900000},
|
||||
{"315.0", 315000000},
|
||||
{"433.9", 433920000},
|
||||
}};
|
||||
|
||||
OptionsField options_packet_type{
|
||||
{6 * 8, 1 * 16},
|
||||
10,
|
||||
{
|
||||
{"Schrader", (int32_t)tpms::Reading::Type::Schrader},
|
||||
{"FLM_64", (int32_t)tpms::Reading::Type::FLM_64},
|
||||
{"FLM_72", (int32_t)tpms::Reading::Type::FLM_72},
|
||||
{"FLM_80", (int32_t)tpms::Reading::Type::FLM_80},
|
||||
{"GMC_96", (int32_t)tpms::Reading::Type::GMC_96},
|
||||
}};
|
||||
|
||||
OptionsField options_pressure{
|
||||
{11 * 8, 3 * 16},
|
||||
4,
|
||||
{{"kPa", PRESSURE_UNIT_KPA},
|
||||
{"PSI", PRESSURE_UNIT_PSI},
|
||||
{"BAR", PRESSURE_UNIT_BAR}}};
|
||||
|
||||
OptionsField options_temperature{
|
||||
{11 * 8, 4 * 16},
|
||||
2,
|
||||
{{STR_DEGREES_C, TEMP_UNIT_CELSIUS},
|
||||
{STR_DEGREES_F, TEMP_UNIT_FAHRENHEIT}}};
|
||||
|
||||
SymField field_transponder_id_24{
|
||||
{6 * 8, 2 * 16},
|
||||
6,
|
||||
SymField::Type::Hex};
|
||||
|
||||
SymField field_transponder_id_32{
|
||||
{6 * 8, 2 * 16},
|
||||
8,
|
||||
SymField::Type::Hex};
|
||||
|
||||
NumberField field_pressure{
|
||||
{6 * 8, 3 * 16},
|
||||
4,
|
||||
{0, 9999},
|
||||
1,
|
||||
' '};
|
||||
|
||||
NumberField field_temperature{
|
||||
{6 * 8, 4 * 16},
|
||||
4,
|
||||
{-99, 999},
|
||||
1,
|
||||
' '};
|
||||
|
||||
NumberField field_flags{
|
||||
{6 * 8, 4 * 16},
|
||||
1,
|
||||
{0, 7},
|
||||
1,
|
||||
' '};
|
||||
|
||||
NumberField field_repeat{
|
||||
{6 * 8, 5 * 16},
|
||||
3,
|
||||
{1, 100},
|
||||
1,
|
||||
' '};
|
||||
|
||||
Button button_load{
|
||||
{0 * 8, 8 * 16 + 4, 7 * 8, 24},
|
||||
"Load"};
|
||||
|
||||
Button button_save{
|
||||
{8 * 8, 8 * 16 + 4, 7 * 8, 24},
|
||||
"Save"};
|
||||
|
||||
TransmitterView2 tx_view{
|
||||
{16 * 8, 0 * 16},
|
||||
true // Short UI format
|
||||
};
|
||||
|
||||
Button button_transmit{
|
||||
{0 * 8, 11 * 16, 15 * 8, 32},
|
||||
"START TX"};
|
||||
|
||||
Text text_status{
|
||||
{0 * 8, 13 * 16 + 4, 30 * 8, 16},
|
||||
"Ready"};
|
||||
|
||||
ProgressBar progressbar{
|
||||
{0 * 8, 14 * 16 + 8, 30 * 8, 16}};
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::tpmstx
|
||||
|
||||
#endif /*__TPMS_TX_APP_H__*/
|
||||
@@ -51,7 +51,9 @@ const std::filesystem::path whipcalc_dir = u"WHIPCALC";
|
||||
const std::filesystem::path ook_editor_dir = u"OOKFILES";
|
||||
const std::filesystem::path hopper_dir = u"HOPPER";
|
||||
const std::filesystem::path subghz_dir = u"SUBGHZ";
|
||||
const std::filesystem::path tpms_dir = u"TPMS";
|
||||
const std::filesystem::path waterfalls_dir = u"WATERFALLS";
|
||||
const std::filesystem::path macaddress_dir = u"MACADDRESS";
|
||||
const std::filesystem::path splash_dot_bmp = u"/splash.bmp";
|
||||
const std::filesystem::path keeloq_keys_dir = u"KEELOQKEYS";
|
||||
const std::filesystem::path keeloq_remotes_dir = u"KEELOQREMOTES";
|
||||
|
||||
@@ -53,9 +53,12 @@ extern const std::filesystem::path whipcalc_dir;
|
||||
extern const std::filesystem::path ook_editor_dir;
|
||||
extern const std::filesystem::path hopper_dir;
|
||||
extern const std::filesystem::path subghz_dir;
|
||||
extern const std::filesystem::path tpms_dir;
|
||||
extern const std::filesystem::path waterfalls_dir;
|
||||
extern const std::filesystem::path macaddress_dir;
|
||||
extern const std::filesystem::path keeloq_keys_dir;
|
||||
extern const std::filesystem::path keeloq_remotes_dir;
|
||||
|
||||
extern const std::filesystem::path splash_dot_bmp;
|
||||
|
||||
#endif /* __FILE_PATH_H__ */
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
#include "../../hackrf/firmware/common/firmware_info.h"
|
||||
#include "../../hackrf/firmware/common/platform_detect.h"
|
||||
|
||||
// Use the same logic as official HackRF firmware
|
||||
#ifdef JAWBREAKER
|
||||
#define SUPPORTED_PLATFORM PLATFORM_JAWBREAKER
|
||||
#elif HACKRF_ONE
|
||||
#define SUPPORTED_PLATFORM (PLATFORM_HACKRF1_OG | PLATFORM_HACKRF1_R9)
|
||||
#elif RAD1O
|
||||
#define SUPPORTED_PLATFORM PLATFORM_RAD1O
|
||||
#elif PRALINE
|
||||
#define SUPPORTED_PLATFORM PLATFORM_PRALINE
|
||||
#else
|
||||
#define SUPPORTED_PLATFORM 0
|
||||
#endif
|
||||
|
||||
#define DFU_MODE_VALUE 0
|
||||
|
||||
__attribute__((section(".firmware_info"))) const struct firmware_info_t firmware_info = {
|
||||
|
||||
@@ -66,7 +66,8 @@ options_t freqman_bandwidths[6] = {
|
||||
// NFM
|
||||
{"8k5", 0},
|
||||
{"11k", 1},
|
||||
{"16k", 2},
|
||||
{"12k5", 2},
|
||||
{"16k", 3},
|
||||
},
|
||||
{
|
||||
// WFM
|
||||
|
||||
@@ -42,6 +42,17 @@ namespace max2831 {
|
||||
|
||||
using namespace max283x;
|
||||
|
||||
static MAX2831Info max2831_info = {0, 0, 0, false, false};
|
||||
|
||||
MAX2831Info get_max2831_info() {
|
||||
return {
|
||||
max2831_info.requested_freq_mhz,
|
||||
max2831_info.calculated_n,
|
||||
max2831_info.calculated_frac,
|
||||
max2831_info.set_frequency_called,
|
||||
max2831_info.frequency_valid};
|
||||
}
|
||||
|
||||
/*
|
||||
* MAX2831 uses 9-bit SPI transfers.
|
||||
* An 18-bit word is sent as two 9-bit transfers:
|
||||
@@ -120,7 +131,8 @@ void MAX2831::init() {
|
||||
// set_reg_field(11, REG11_RXVGA_GAIN_MASK, 0x1F); // 62 dB VGA = MAX
|
||||
|
||||
/* Configure baseband filter for 8 MHz TX - matches GSG reference */
|
||||
set_reg_field(8, REG8_LPF_COARSE_MASK, REG8_RX_LPF_7_5M);
|
||||
// set_reg_field(8, REG8_LPF_COARSE_MASK, REG8_RX_LPF_7_5M);
|
||||
set_reg_field(8, REG8_LPF_COARSE_MASK, REG8_RX_LPF_15M);
|
||||
set_reg_field(7, REG7_RX_LPF_FINE_MASK, REG7_RX_LPF_FINE_100);
|
||||
set_reg_field(7, REG7_TX_LPF_FINE_MASK, REG7_TX_LPF_FINE_100);
|
||||
|
||||
@@ -329,9 +341,18 @@ uint32_t MAX2831::set_lpf_bandwidth_internal(const uint32_t bandwidth_hz) {
|
||||
|
||||
void MAX2831::set_lpf_rf_bandwidth_rx(const uint32_t bandwidth_minimum) {
|
||||
_desired_lpf_bw = bandwidth_minimum;
|
||||
#ifdef PRALINE
|
||||
uint32_t actual_bw = bandwidth_minimum;
|
||||
|
||||
_desired_lpf_bw = actual_bw;
|
||||
if (_mode == Mode::Receive || _mode == Mode::Rx_Calibration) {
|
||||
set_lpf_bandwidth_internal(actual_bw);
|
||||
}
|
||||
#else
|
||||
if (_mode == Mode::Receive || _mode == Mode::Rx_Calibration) {
|
||||
set_lpf_bandwidth_internal(bandwidth_minimum);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void MAX2831::set_lpf_rf_bandwidth_tx(const uint32_t bandwidth_minimum) {
|
||||
@@ -355,7 +376,20 @@ bool MAX2831::set_frequency(const rf::Frequency lo_frequency) {
|
||||
*/
|
||||
|
||||
/* MAX2831 supports 2.3-2.6 GHz */
|
||||
if (lo_frequency < 2300000000ULL || lo_frequency > 2600000000ULL) {
|
||||
// if (lo_frequency < MAX2831_MIN_LO_FREQUENCY_HZ || lo_frequency > MAX2831_MAX_LO_FREQUENCY_HZ) {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
bool valid = (lo_frequency >= MAX2831_MIN_LO_FREQUENCY_HZ && lo_frequency <= MAX2831_MAX_LO_FREQUENCY_HZ);
|
||||
|
||||
// TRACK REQUEST IMMEDIATELY
|
||||
max2831_info.requested_freq_mhz = lo_frequency / 1000000;
|
||||
max2831_info.set_frequency_called = true;
|
||||
max2831_info.frequency_valid = valid;
|
||||
|
||||
if (!valid) {
|
||||
max2831_info.calculated_n = 0;
|
||||
max2831_info.calculated_frac = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -377,6 +411,10 @@ bool MAX2831::set_frequency(const rf::Frequency lo_frequency) {
|
||||
}
|
||||
}
|
||||
|
||||
// TRACK CALCULATED VALUES
|
||||
max2831_info.calculated_n = div_int;
|
||||
max2831_info.calculated_frac = div_frac;
|
||||
|
||||
/* Write order matters - matches GSG reference */
|
||||
/* REG 3: SYN_INT (bits 7:0) and SYN_FRAC_LO (bits 13:8) */
|
||||
uint16_t reg3_val = (div_int & 0xFF) | ((div_frac & 0x3F) << 8);
|
||||
|
||||
@@ -38,6 +38,21 @@ namespace max2831 {
|
||||
|
||||
using namespace max283x;
|
||||
|
||||
// Global debug tracking for MAX2831
|
||||
struct MAX2831Info {
|
||||
uint32_t requested_freq_mhz;
|
||||
uint32_t calculated_n;
|
||||
uint32_t calculated_frac;
|
||||
bool set_frequency_called;
|
||||
bool frequency_valid;
|
||||
};
|
||||
|
||||
MAX2831Info get_max2831_info();
|
||||
|
||||
/* minumum and maximum lo_frequencies supported by max2831 */
|
||||
constexpr rf::Frequency MAX2831_MIN_LO_FREQUENCY_HZ = 2300000000LL;
|
||||
constexpr rf::Frequency MAX2831_MAX_LO_FREQUENCY_HZ = 2600000000LL;
|
||||
|
||||
/* MAX2831 has 16 registers, each containing 14 bits of data */
|
||||
constexpr size_t reg_count = 16;
|
||||
|
||||
|
||||
@@ -287,6 +287,7 @@ void RFFC507x::set_frequency(const rf::Frequency lo_frequency) {
|
||||
const SynthConfig synth_config = SynthConfig::calculate(lo_frequency);
|
||||
|
||||
#ifdef PRALINE
|
||||
|
||||
// Calculate VCO frequency from LO frequency and divider
|
||||
const size_t lo_divider = 1U << synth_config.lo_divider_log2; // 2^lodiv_log2
|
||||
const rf::Frequency vco_freq = lo_frequency * lo_divider;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
#include "keeloq_common.hpp"
|
||||
|
||||
uint32_t keeloq_encrypt(const uint32_t data, const uint64_t key) {
|
||||
uint32_t x = data, r;
|
||||
|
||||
for (r = 0; r < 528; r++)
|
||||
x = (x >> 1) ^ ((bit(x, 0) ^ bit(x, 16) ^ (uint32_t)bit(key, r & 63) ^
|
||||
bit(KEELOQ_NLF, g5(x, 1, 9, 20, 26, 31)))
|
||||
<< 31);
|
||||
|
||||
return x;
|
||||
}
|
||||
|
||||
uint32_t keeloq_decrypt(const uint32_t data, const uint64_t key) {
|
||||
uint32_t x = data, r;
|
||||
|
||||
for (r = 0; r < 528; r++)
|
||||
x = (x << 1) ^ bit(x, 31) ^ bit(x, 15) ^ (uint32_t)bit(key, (15 - r) & 63) ^
|
||||
bit(KEELOQ_NLF, g5(x, 0, 8, 19, 25, 30));
|
||||
|
||||
return x;
|
||||
}
|
||||
|
||||
uint64_t keeloq_normal_learning(uint32_t data, const uint64_t key) {
|
||||
uint32_t k1, k2;
|
||||
|
||||
data &= 0x0FFFFFFF;
|
||||
data |= 0x20000000;
|
||||
k1 = keeloq_decrypt(data, key);
|
||||
|
||||
data &= 0x0FFFFFFF;
|
||||
data |= 0x60000000;
|
||||
k2 = keeloq_decrypt(data, key);
|
||||
|
||||
return ((uint64_t)k2 << 32) | k1;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#ifndef __KEELOQ_COMMON__
|
||||
#define __KEELOQ_COMMON__
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#define bit(x, n) (((x) >> (n)) & 1)
|
||||
#define g5(x, a, b, c, d, e) \
|
||||
(bit(x, a) + bit(x, b) * 2 + bit(x, c) * 4 + bit(x, d) * 8 + bit(x, e) * 16)
|
||||
|
||||
#define KEELOQ_NLF 0x3A5C742E
|
||||
|
||||
uint32_t keeloq_encrypt(const uint32_t data, const uint64_t key);
|
||||
uint32_t keeloq_decrypt(const uint32_t data, const uint64_t key);
|
||||
uint64_t keeloq_normal_learning(uint32_t data, const uint64_t key);
|
||||
|
||||
#endif
|
||||
@@ -43,7 +43,7 @@ bool read_keeloq_file(const fs::path& path, KeeloqData& data) {
|
||||
|
||||
data.mf_name = std::string{chunks[0]};
|
||||
data.serial = std::strtoul(chunks[1].data(), NULL, 16);
|
||||
data.counter = std::atoi(chunks[2].data());
|
||||
data.counter = (uint16_t)std::atoi(chunks[2].data());
|
||||
data.btn = (uint8_t)std::atoi(chunks[3].data());
|
||||
|
||||
return true;
|
||||
@@ -60,7 +60,10 @@ bool write_keeloq_file(const fs::path& path, const KeeloqData& data) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string formatted = data.mf_name + ";" + to_string_hex(data.serial) + ";" + to_string_dec_uint(data.counter) + ";" + to_string_dec_uint(data.btn);
|
||||
std::string formatted = data.mf_name + ";" +
|
||||
to_string_hex(data.serial) + ";" +
|
||||
to_string_dec_uint(data.counter) + ";" +
|
||||
to_string_dec_uint(data.btn);
|
||||
|
||||
file.write_line(formatted);
|
||||
file.close();
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
struct KeeloqData {
|
||||
std::string mf_name{};
|
||||
uint32_t serial = 0;
|
||||
uint32_t counter = 0;
|
||||
uint16_t counter = 0;
|
||||
uint8_t btn = 0;
|
||||
};
|
||||
|
||||
|
||||
Executable → Regular
+17
@@ -145,6 +145,21 @@ Continuous (Fox-oring)
|
||||
rffc507x::RFFC507x first_if;
|
||||
ui::SystemView* system_view_ptr;
|
||||
|
||||
static uint32_t random_seed_state = 123456789;
|
||||
extern "C" int rand(void) {
|
||||
random_seed_state ^= random_seed_state << 13;
|
||||
random_seed_state ^= random_seed_state >> 17;
|
||||
random_seed_state ^= random_seed_state << 5;
|
||||
return (int)(random_seed_state & 0x7FFFFFFF);
|
||||
}
|
||||
extern "C" void srand(unsigned int seed) {
|
||||
if (seed == 0) {
|
||||
random_seed_state = 123456789;
|
||||
} else {
|
||||
random_seed_state = seed;
|
||||
}
|
||||
}
|
||||
|
||||
static void event_loop() {
|
||||
static ui::Context context;
|
||||
static ui::SystemView system_view{
|
||||
@@ -166,7 +181,9 @@ static void event_loop() {
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
#ifndef PRALINE // Do not perform quick set up of GP01_RFF507X = 1 for PRALINE
|
||||
first_if.init(); /* To avoid initial short Ant_DC_Bias pulse ,we need quick set up GP01_RFF507X =1 */
|
||||
#endif
|
||||
|
||||
if (config_mode_should_enter()) {
|
||||
config_mode_clear();
|
||||
|
||||
@@ -56,11 +56,9 @@ using asahi_kasei::ak4951::AK4951;
|
||||
#include "i2cdevmanager.hpp"
|
||||
#include "battery.hpp"
|
||||
|
||||
#ifndef PRALINE
|
||||
extern "C" {
|
||||
#include "platform_detect.h"
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace portapack {
|
||||
|
||||
@@ -555,12 +553,10 @@ init_status_t init() {
|
||||
|
||||
chThdSleepMilliseconds(100);
|
||||
|
||||
#ifndef PRALINE
|
||||
detect_hardware_platform();
|
||||
finalize_detect_hardware_platform();
|
||||
|
||||
chThdSleepMilliseconds(100);
|
||||
#endif
|
||||
|
||||
configure_pins_portapack();
|
||||
|
||||
|
||||
@@ -32,10 +32,11 @@
|
||||
extern "C" {
|
||||
#include "fpga_bridge.h"
|
||||
}
|
||||
#else
|
||||
#include "baseband_cpld.hpp"
|
||||
#endif
|
||||
|
||||
#include "max5864.hpp"
|
||||
#include "baseband_cpld.hpp"
|
||||
|
||||
#include "tuning.hpp"
|
||||
|
||||
@@ -116,9 +117,10 @@ max2837::MAX2837 second_if_max2837{ssp1_target_max283x};
|
||||
max2839::MAX2839 second_if_max2839{ssp1_target_max283x};
|
||||
#ifdef PRALINE
|
||||
max2831::MAX2831 second_if_max2831{ssp1_target_max283x};
|
||||
#else
|
||||
static baseband::CPLD baseband_cpld;
|
||||
#endif
|
||||
static max5864::MAX5864 baseband_codec{ssp1_target_max5864};
|
||||
static baseband::CPLD baseband_cpld;
|
||||
|
||||
// load_sram() is called at boot in portapack.cpp, including verify CPLD part, so default direction is Receive
|
||||
static rf::Direction direction{rf::Direction::Receive};
|
||||
@@ -145,15 +147,13 @@ void init() {
|
||||
? (max283x::MAX283x*)&second_if_max2839
|
||||
: (max283x::MAX283x*)&second_if_max2837;
|
||||
#endif
|
||||
|
||||
rf_path.init();
|
||||
first_if.init();
|
||||
second_if->init();
|
||||
baseband_codec.init();
|
||||
#ifndef PRALINE
|
||||
/* HackRF One uses CPLD for Q inversion control.
|
||||
* PRALINE uses FPGA and the pin (P2_3) is used for LCD_TE on H4M. */
|
||||
baseband_cpld.init();
|
||||
#else
|
||||
|
||||
#ifdef PRALINE
|
||||
/* Initialize FPGA registers - DC_BLOCK must be enabled for RX */
|
||||
// debug::fpga::init();
|
||||
fpga_debug_register_write(1, 0x01); // DC_BLOCK=1, QUARTER_SHIFT=0, Q_INVERT=0
|
||||
@@ -164,6 +164,10 @@ void init() {
|
||||
|
||||
ssp1_arbiter.invalidate();
|
||||
chThdSleepMilliseconds(10); // Let FPGA registers settle
|
||||
#else
|
||||
/* HackRF One uses CPLD for Q inversion control.
|
||||
* PRALINE uses FPGA and the pin (P2_3) is used for LCD_TE on H4M. */
|
||||
baseband_cpld.init();
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -202,17 +206,23 @@ void set_direction(const rf::Direction new_direction) {
|
||||
*/
|
||||
baseband_invert = false;
|
||||
}
|
||||
#ifndef PRALINE
|
||||
baseband_cpld.set_invert(mixer_invert ^ baseband_invert);
|
||||
#else
|
||||
// Praline: Control Q inversion via FPGA register
|
||||
// Assuming register 1 bit 1 controls Q inversion
|
||||
uint8_t ctrl_reg = 0x01; // DC_BLOCK enabled
|
||||
if (mixer_invert ^ baseband_invert) {
|
||||
ctrl_reg |= 0x02; // Set Q_INVERT bit
|
||||
|
||||
#ifdef PRALINE
|
||||
|
||||
// CORRECT: FPGA register 1 only controls DC_BLOCK
|
||||
fpga_debug_register_write(1, 0x01); // DC_BLOCK only, no QUARTER_SHIFT!
|
||||
|
||||
// Q inversion controlled by GPIO0[13] (SGPIO12), not FPGA register
|
||||
bool q_invert = mixer_invert ^ baseband_invert;
|
||||
if (q_invert) {
|
||||
LPC_GPIO->SET[0] = (1 << 13); // SGPIO12 = 1 (Q inverted)
|
||||
} else {
|
||||
LPC_GPIO->CLR[0] = (1 << 13); // SGPIO12 = 0 (Q normal)
|
||||
}
|
||||
fpga_debug_register_write(1, ctrl_reg);
|
||||
|
||||
ssp1_arbiter.invalidate();
|
||||
#else
|
||||
baseband_cpld.set_invert(mixer_invert ^ baseband_invert);
|
||||
#endif
|
||||
|
||||
second_if->set_mode((direction == rf::Direction::Transmit) ? max283x::Mode::Transmit : max283x::Mode::Receive);
|
||||
@@ -224,16 +234,6 @@ void set_direction(const rf::Direction new_direction) {
|
||||
led_rx.on();
|
||||
else
|
||||
led_tx.on();
|
||||
|
||||
// #ifdef PRALINE
|
||||
// Try with Q inversion OFF
|
||||
// fpga_debug_register_write(1, 0x01); // DC_BLOCK=1, Q_INVERT=0
|
||||
// ssp1_arbiter.invalidate();
|
||||
|
||||
// If no signals, try with Q inversion ON
|
||||
// fpga_debug_register_write(1, 0x03); // DC_BLOCK=1, Q_INVERT=1
|
||||
// ssp1_arbiter.invalidate();
|
||||
// #endif
|
||||
}
|
||||
|
||||
bool set_tuning_frequency(const rf::Frequency frequency) {
|
||||
@@ -269,6 +269,10 @@ bool set_tuning_frequency(const rf::Frequency frequency) {
|
||||
if (tuning_config.first_lo_frequency) {
|
||||
first_if.set_frequency(tuning_config.first_lo_frequency);
|
||||
first_if.enable();
|
||||
#ifdef PRALINE
|
||||
first_if.flush(); // Force register write with reference clock present
|
||||
chThdSleepMilliseconds(10); // Allow PLL to settle
|
||||
#endif
|
||||
}
|
||||
|
||||
// Program second local oscillator frequency into MAX283x
|
||||
@@ -276,7 +280,24 @@ bool set_tuning_frequency(const rf::Frequency frequency) {
|
||||
|
||||
rf_path.set_band(tuning_config.rf_path_band);
|
||||
mixer_invert = tuning_config.mixer_invert;
|
||||
#ifndef PRALINE
|
||||
|
||||
#ifdef PRALINE
|
||||
// TEST: Force baseband invert for Praline (like r9)
|
||||
// baseband_invert = (direction == rf::Direction::Receive);
|
||||
|
||||
// CORRECT: FPGA register 1 only controls DC_BLOCK
|
||||
fpga_debug_register_write(1, 0x01); // DC_BLOCK only, no QUARTER_SHIFT!
|
||||
|
||||
// Q inversion controlled by GPIO0[13] (SGPIO12), not FPGA register
|
||||
bool q_invert = mixer_invert ^ baseband_invert;
|
||||
if (q_invert) {
|
||||
LPC_GPIO->SET[0] = (1 << 13); // SGPIO12 = 1 (Q inverted)
|
||||
} else {
|
||||
LPC_GPIO->CLR[0] = (1 << 13); // SGPIO12 = 0 (Q normal)
|
||||
}
|
||||
|
||||
ssp1_arbiter.invalidate();
|
||||
#else
|
||||
baseband_cpld.set_invert(mixer_invert ^ baseband_invert);
|
||||
#endif
|
||||
|
||||
@@ -347,20 +368,6 @@ void set_rx_max283x_iq_phase_calibration(const size_t v) {
|
||||
second_if->set_rx_LO_iq_phase_calibration(v);
|
||||
}
|
||||
|
||||
/*void enable(Configuration configuration) {
|
||||
configure(configuration);
|
||||
}
|
||||
|
||||
void configure(Configuration configuration) {
|
||||
set_tuning_frequency(configuration.tuning_frequency);
|
||||
set_rf_amp(configuration.rf_amp);
|
||||
set_lna_gain(configuration.lna_gain);
|
||||
set_vga_gain(configuration.vga_gain);
|
||||
set_baseband_rate(configuration.baseband_rate);
|
||||
set_baseband_filter_bandwidth(configuration.baseband_filter_bandwidth);
|
||||
set_direction(configuration.direction);
|
||||
}*/
|
||||
|
||||
void disable() {
|
||||
set_antenna_bias(false);
|
||||
baseband_codec.set_mode(max5864::Mode::Shutdown);
|
||||
@@ -468,6 +475,12 @@ int8_t temp_sense() {
|
||||
|
||||
} /* namespace second_if */
|
||||
|
||||
namespace rf_path_info {
|
||||
rf::path::Band get_current_band() {
|
||||
return radio::rf_path.get_band();
|
||||
}
|
||||
} /* namespace rf_path_info */
|
||||
|
||||
#ifdef PRALINE
|
||||
namespace fpga {
|
||||
|
||||
|
||||
@@ -103,6 +103,12 @@ int8_t temp_sense();
|
||||
|
||||
} /* namespace second_if */
|
||||
|
||||
namespace rf_path_info {
|
||||
|
||||
rf::path::Band get_current_band();
|
||||
|
||||
} /* namespace rf_path_info */
|
||||
|
||||
#ifdef PRALINE
|
||||
namespace fpga {
|
||||
|
||||
|
||||
@@ -63,9 +63,10 @@ static constexpr std::array<baseband::AMConfig, 12> am_configs{{
|
||||
{taps_6k0_narrow_decim_1, taps_6k0_decim_2, taps_2k6_usb_wefax_channel, AMConfigureMessage::Modulation::SSB_FM, apt_audio_12k_lpf_1500hz_config, (int)AMConfigureMessage::Zoom_waterfall::ZOOM_x_2}, // SSB USB+FM to demod. Subcarrier FM Audio Tones to get APT Weather Fax with waterfall zoom x 2 (we need taps_6k0_narrow_decim_1 to minimize aliasing)
|
||||
}};
|
||||
|
||||
static constexpr std::array<baseband::NBFMConfig, 3> nbfm_configs{{
|
||||
static constexpr std::array<baseband::NBFMConfig, 4> nbfm_configs{{
|
||||
{taps_4k25_decim_0, taps_4k25_decim_1, taps_4k25_channel, 2500},
|
||||
{taps_11k0_decim_0, taps_11k0_decim_1, taps_11k0_channel, 2500},
|
||||
{taps_12k5_decim_0, taps_12k5_decim_1, taps_12k5_channel, 2500},
|
||||
{taps_16k0_decim_0, taps_16k0_decim_1, taps_16k0_channel, 5000},
|
||||
}};
|
||||
|
||||
|
||||
@@ -41,14 +41,14 @@ namespace {
|
||||
*/
|
||||
struct PralineConfig {
|
||||
bool tx_en;
|
||||
bool mix_en_n; // Inverted: 0 = mixer enabled
|
||||
bool mix_bypass; // RF path mixer bypass (GPIO3[2])
|
||||
bool lpf_en;
|
||||
bool rf_amp_en;
|
||||
bool ant_bias_en_n; // Inverted: 0 = bias enabled
|
||||
|
||||
static void gpio_init() {
|
||||
gpio_tx_enable.output();
|
||||
gpio_mix_enable_n.output();
|
||||
gpio_mix_bypass.output();
|
||||
gpio_lpf_enable.output();
|
||||
gpio_rf_amp_enable.output();
|
||||
gpio_ant_bias_disable.output();
|
||||
@@ -56,7 +56,7 @@ struct PralineConfig {
|
||||
|
||||
void apply() const {
|
||||
gpio_tx_enable.write(tx_en);
|
||||
gpio_mix_enable_n.write(mix_en_n);
|
||||
gpio_mix_bypass.write(mix_bypass); // Control RF path mixer
|
||||
gpio_lpf_enable.write(lpf_en);
|
||||
gpio_rf_amp_enable.write(rf_amp_en);
|
||||
gpio_ant_bias_disable.write(ant_bias_en_n);
|
||||
@@ -239,7 +239,7 @@ void Path::init() {
|
||||
/* Set safe initial state: RX mode, mixer enabled, LPF on, amp off, no bias */
|
||||
PralineConfig config = {
|
||||
.tx_en = false,
|
||||
.mix_en_n = false, // Mixer enabled (inverted)
|
||||
.mix_bypass = false, // RF path mixer bypass (GPIO3[2])
|
||||
.lpf_en = true, // LPF on for low band
|
||||
.rf_amp_en = false, // Amp off
|
||||
.ant_bias_en_n = true // Bias off (inverted)
|
||||
@@ -258,6 +258,7 @@ void Path::set_direction(const Direction new_direction) {
|
||||
|
||||
void Path::set_band(const Band new_band) {
|
||||
band = new_band;
|
||||
_band = new_band;
|
||||
update();
|
||||
}
|
||||
|
||||
@@ -275,7 +276,7 @@ void Path::update() {
|
||||
#ifdef PRALINE
|
||||
/* PRALINE RF path control:
|
||||
* - tx_en: 1 for TX, 0 for RX
|
||||
* - mix_en_n: 0 to enable mixer (inverted), 1 to bypass
|
||||
* - mix_bypass: 0 to enable RF path mixer bypass (GPIO3[2])
|
||||
* - lpf_en: 1 for low band (< 2.4 GHz), 0 for high band
|
||||
* - rf_amp_en: 1 to enable RF amplifier
|
||||
* - ant_bias_en_n: 0 to enable antenna bias (inverted)
|
||||
@@ -293,11 +294,12 @@ void Path::update() {
|
||||
|
||||
config.tx_en = (direction == Direction::Transmit);
|
||||
|
||||
/* Mixer bypass for mid band (2.3-2.7 GHz direct to MAX2831) */
|
||||
config.mix_en_n = (band == Band::Mid); // 1 = bypass (disabled)
|
||||
// RF path mixer bypass: 0=enabled, 1=bypassed
|
||||
config.mix_bypass = (band == Band::Mid);
|
||||
|
||||
/* Move to the final state by turning on required signals. */
|
||||
/* LPF for low band */
|
||||
|
||||
config.lpf_en = (band == Band::Low);
|
||||
|
||||
/* RF amp when amplification requested */
|
||||
@@ -307,6 +309,7 @@ void Path::update() {
|
||||
config.ant_bias_en_n = true;
|
||||
|
||||
config.apply();
|
||||
|
||||
#else
|
||||
/* HackRF One RF path control */
|
||||
const auto config = get_config(direction, band, rf_amp);
|
||||
|
||||
@@ -39,9 +39,17 @@ enum class Direction {
|
||||
|
||||
namespace path {
|
||||
|
||||
#ifdef PRALINE
|
||||
/* PRALINE: MAX2831 direct path is 2320-2740 MHz */
|
||||
constexpr FrequencyRange band_low{0, 2320'000'000};
|
||||
constexpr FrequencyRange band_high{2740'000'000, 7250'000'000};
|
||||
constexpr FrequencyRange band_mid{band_low.maximum, band_high.minimum};
|
||||
#else
|
||||
/* HackRF One: Original band boundaries */
|
||||
constexpr FrequencyRange band_low{0, 2170'000'000};
|
||||
constexpr FrequencyRange band_high{2740'000'000, 7250'000'000};
|
||||
constexpr FrequencyRange band_mid{band_low.maximum, band_high.minimum};
|
||||
#endif
|
||||
|
||||
enum class Band {
|
||||
/* Zero-based, used as index into frequency_bands table */
|
||||
@@ -58,12 +66,16 @@ class Path {
|
||||
void set_band(const Band band);
|
||||
void set_rf_amp(const bool rf_amp);
|
||||
|
||||
Band get_band() const { return _band; } //_band is used solely for debugging purposes.
|
||||
|
||||
private:
|
||||
Direction direction{Direction::Receive};
|
||||
Band band{Band::Mid};
|
||||
bool rf_amp{false};
|
||||
|
||||
void update();
|
||||
|
||||
Band _band{Band::Mid}; //_band is solely used of debugging purposes
|
||||
};
|
||||
|
||||
} // namespace path
|
||||
|
||||
@@ -93,6 +93,22 @@ static const char* get_board_revision_string(board_rev_t rev) {
|
||||
return "GSG HackRF R9";
|
||||
case BOARD_REV_GSG_HACKRF1_R10:
|
||||
return "GSG HackRF R10";
|
||||
case BOARD_REV_PRALINE_R0_1:
|
||||
case BOARD_REV_PRALINE_R0_2:
|
||||
case BOARD_REV_PRALINE_R0_3:
|
||||
return "HackRF Pro R0";
|
||||
case BOARD_REV_PRALINE_R1_0:
|
||||
case BOARD_REV_PRALINE_R1_1:
|
||||
case BOARD_REV_PRALINE_R1_2:
|
||||
return "HackRF Pro R1";
|
||||
case BOARD_REV_GSG_PRALINE_R0_1:
|
||||
case BOARD_REV_GSG_PRALINE_R0_2:
|
||||
case BOARD_REV_GSG_PRALINE_R0_3:
|
||||
return "GSG HackRF Pro R0";
|
||||
case BOARD_REV_GSG_PRALINE_R1_0:
|
||||
case BOARD_REV_GSG_PRALINE_R1_1:
|
||||
case BOARD_REV_GSG_PRALINE_R1_2:
|
||||
return "GSG HackRF Pro R1";
|
||||
case BOARD_REV_UNRECOGNIZED:
|
||||
return "Unrecognized";
|
||||
case BOARD_REV_UNDETECTED:
|
||||
@@ -134,6 +150,36 @@ static void cmd_info(BaseSequentialStream* chp, int argc, char* argv[]) {
|
||||
board_rev_t revision = detected_revision();
|
||||
const char* revision_string = get_board_revision_string(revision);
|
||||
chprintf(chp, "HackRF Board Rev: %s\r\n", revision_string);
|
||||
|
||||
// Determine device type string
|
||||
const char* device_str;
|
||||
#ifdef PRALINE
|
||||
device_str = "HackRF Pro";
|
||||
#else
|
||||
if (portapack::device_type == portapack::DEV_PORTARF) {
|
||||
device_str = "PortaRF";
|
||||
} else {
|
||||
device_str = "HackRF One";
|
||||
}
|
||||
#endif
|
||||
chprintf(chp, "Device: %s\r\n", device_str);
|
||||
|
||||
// Flash info: build-time allowed MB, runtime-detected limit, current firmware size
|
||||
// Use explicit uint32_t cast: FLASH_SIZE_LIMIT_MB may be a float (e.g. 3.5 for HPro)
|
||||
uint8_t flash_allowrun;
|
||||
#ifdef PRALINE
|
||||
flash_allowrun = 4; // HackRF Pro
|
||||
#else
|
||||
if (portapack::device_type == portapack::DeviceType::DEV_PORTAPACK) {
|
||||
flash_allowrun = 1; // PortaPack classic
|
||||
} else {
|
||||
flash_allowrun = FLASH_SIZE_MB; // PortaRF
|
||||
}
|
||||
#endif
|
||||
chprintf(chp, "Flash Allowed: %dMB\r\n", (uint32_t)FLASH_SIZE_MB);
|
||||
chprintf(chp, "Flash Runtime: %dMB\r\n", (uint32_t)flash_allowrun);
|
||||
chprintf(chp, "Flash Current: %dMB\r\n", (uint32_t)FLASH_SIZE_LIMIT_MB);
|
||||
|
||||
chprintf(chp, "Reference Source: %s\r\n", portapack::clock_manager.get_source().c_str());
|
||||
chprintf(chp, "Reference Freq: %s\r\n", portapack::clock_manager.get_freq().c_str());
|
||||
#ifdef __DATE__
|
||||
|
||||
+104
-13
@@ -31,18 +31,86 @@ Config low_band(const rf::Frequency target_frequency);
|
||||
Config mid_band(const rf::Frequency target_frequency);
|
||||
Config high_band(const rf::Frequency target_frequency);
|
||||
|
||||
// Low band <2170 Mhz:
|
||||
#ifdef PRALINE
|
||||
/*
|
||||
* PRALINE Tuning Configuration
|
||||
* ============================
|
||||
*
|
||||
* Reference: hackrf_usb/common/tune_config.h praline_tune_config_rx[]
|
||||
*
|
||||
* The hackrf_usb firmware uses a table-driven approach where each entry
|
||||
* specifies:
|
||||
* - rf_range_end_mhz: Upper frequency limit for this config
|
||||
* - if_mhz: IF frequency (what MAX2831 tunes to)
|
||||
* - high_lo: true = high-side injection, false = low-side
|
||||
* - shift: FPGA quarter-shift mode (not implemented in Mayhem yet)
|
||||
*
|
||||
* Key insight: The IF frequency varies to keep the RFFC5072 VCO in a
|
||||
* safe operating range (ideally 3500-5000 MHz, avoiding extremes).
|
||||
*
|
||||
* RFFC5072 VCO calculation:
|
||||
* High-side injection: LO = IF + RF, VCO = LO × lodiv
|
||||
* Low-side injection: LO = IF - RF, VCO = LO × lodiv
|
||||
* Where lodiv = 2 for frequencies where VCO > 2700 MHz
|
||||
*
|
||||
* From hackrf_usb tune_config_rx (simplified):
|
||||
* 0-2100 MHz: IF=2375, high_lo=true → VCO = (2375+RF)×2
|
||||
* 2105-2115: IF=2375, high_lo=false → VCO = (2375-RF)×2
|
||||
* 2115-2130: IF=2425, high_lo=false → VCO = (2425-RF)×2
|
||||
* ... (more entries for fine-grained control)
|
||||
* 2320-2580: IF=0 (bypass mode, no mixer)
|
||||
* 2580+: High-pass mode
|
||||
*/
|
||||
|
||||
// Simplified tune_config lookup for Mayhem
|
||||
// Returns the IF frequency in Hz for a given target frequency
|
||||
constexpr rf::Frequency praline_get_if_frequency(const rf::Frequency target_frequency) {
|
||||
const uint32_t freq_mhz = target_frequency / 1'000'000;
|
||||
|
||||
// Based on hackrf_usb tune_config_rx table
|
||||
if (freq_mhz < 2100) {
|
||||
// Most low-band frequencies: use 2375 MHz IF
|
||||
// This keeps VCO around 4750-4950 MHz for FM band
|
||||
return 2375'000'000;
|
||||
} else if (freq_mhz < 2320) {
|
||||
// Transition zone: use varying IF to avoid VCO edges
|
||||
// These frequencies are tricky - near MAX2831 minimum
|
||||
// Use 2425 MHz to give some margin
|
||||
return 2425'000'000;
|
||||
} else {
|
||||
// Bypass mode or high-band - IF not used for mixer
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Returns true for high-side injection, false for low-side
|
||||
constexpr bool praline_use_high_side_injection(const rf::Frequency target_frequency) {
|
||||
const uint32_t freq_mhz = target_frequency / 1'000'000;
|
||||
|
||||
// Based on hackrf_usb tune_config_rx table
|
||||
if (freq_mhz < 2100) {
|
||||
// Standard low-band: high-side injection
|
||||
// LO = IF + RF, mixer inverts spectrum
|
||||
return true;
|
||||
} else if (freq_mhz < 2105) {
|
||||
// Narrow transition: still high-side
|
||||
return true;
|
||||
} else if (freq_mhz < 2320) {
|
||||
// Near MAX2831 minimum: use low-side injection
|
||||
// LO = IF - RF, no spectrum inversion
|
||||
return false;
|
||||
} else {
|
||||
// Bypass/high-band - doesn't matter, mixer bypassed
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif // PRALINE
|
||||
|
||||
// Low band <2170 Mhz (HackRF One) or <2320 MHz (PRALINE):
|
||||
constexpr rf::Frequency low_band_second_lo_frequency(const rf::Frequency target_frequency) {
|
||||
#ifdef PRALINE
|
||||
// Praline-specific formula for MAX2831 (2.3-2.6 GHz range)
|
||||
// Use a fixed second_lo that:
|
||||
// 1. Falls in MAX2831's sweet spot (2.3-2.6 GHz)
|
||||
// 2. Gives RFFC5072 a VCO frequency in its range (2700-5400 MHz)
|
||||
|
||||
// For most low-band frequencies, use 2500 MHz as second_lo
|
||||
// This gives RFFC5072 plenty of headroom
|
||||
(void)target_frequency; // Unused in fixed formula
|
||||
return 2500'000'000;
|
||||
// Use the tune_config lookup for PRALINE
|
||||
return praline_get_if_frequency(target_frequency);
|
||||
#else
|
||||
return 2650'000'000 - (target_frequency / 7);
|
||||
#endif
|
||||
@@ -50,18 +118,36 @@ constexpr rf::Frequency low_band_second_lo_frequency(const rf::Frequency target_
|
||||
|
||||
Config low_band(const rf::Frequency target_frequency) {
|
||||
const rf::Frequency second_lo_frequency = low_band_second_lo_frequency(target_frequency);
|
||||
|
||||
#ifdef PRALINE
|
||||
rf::Frequency first_lo_frequency;
|
||||
bool mixer_invert;
|
||||
|
||||
if (praline_use_high_side_injection(target_frequency)) {
|
||||
// High-side injection: LO = IF + RF
|
||||
first_lo_frequency = second_lo_frequency + target_frequency;
|
||||
mixer_invert = true;
|
||||
} else {
|
||||
// Low-side injection: LO = IF - RF
|
||||
first_lo_frequency = second_lo_frequency - target_frequency;
|
||||
mixer_invert = false;
|
||||
}
|
||||
|
||||
return {first_lo_frequency, second_lo_frequency, rf::path::Band::Low, mixer_invert};
|
||||
#else
|
||||
const rf::Frequency first_lo_frequency = target_frequency + second_lo_frequency;
|
||||
const bool mixer_invert = true;
|
||||
return {first_lo_frequency, second_lo_frequency, rf::path::Band::Low, mixer_invert};
|
||||
#endif
|
||||
}
|
||||
|
||||
// Mid band 2170-2740 Mhz:
|
||||
// Mid band 2170-2740 Mhz (HackRF One) or 2320-2580 MHz (PRALINE):
|
||||
Config mid_band(const rf::Frequency target_frequency) {
|
||||
#ifdef PRALINE
|
||||
// For Praline with MAX2831 (2.3-2.6 GHz range)
|
||||
// Frequencies 2170-2300 MHz need upconversion since they're below MAX2831 minimum
|
||||
if (target_frequency < 2300'000'000) {
|
||||
// Treat as low band
|
||||
// Treat as low band - need mixer
|
||||
return low_band(target_frequency);
|
||||
}
|
||||
// Frequencies 2300-2600 MHz can go direct (no RFFC5072)
|
||||
@@ -84,11 +170,16 @@ Config mid_band(const rf::Frequency target_frequency) {
|
||||
#endif
|
||||
}
|
||||
|
||||
// High band >2740 Mhz:
|
||||
// High band >2740 Mhz (HackRF One) or >2580 MHz (PRALINE):
|
||||
constexpr rf::Frequency high_band_second_lo_frequency(const rf::Frequency target_frequency) {
|
||||
#ifdef PRALINE
|
||||
// Praline formula tuned for MAX2831 (2.3-2.6 GHz range)
|
||||
// Keep second_lo in MAX2831's range while allowing RFFC5072 to work
|
||||
//
|
||||
// For high-band, we use LOW-side injection: LO = RF - IF
|
||||
// So IF should be chosen to keep LO (and thus VCO) in a good range
|
||||
//
|
||||
// Based on hackrf_usb tune_config_tx patterns:
|
||||
if (target_frequency < 3600'000'000)
|
||||
return 2400'000'000 + ((target_frequency - 2740'000'000) / 4);
|
||||
else if (target_frequency < 5100'000'000)
|
||||
|
||||
@@ -265,7 +265,7 @@ void GeoMap::map_read_line_bin(ui::Color* buffer, uint16_t pixels) {
|
||||
|
||||
void GeoMap::draw_markers(Painter& painter) {
|
||||
for (int i = 0; i < markerListLen; ++i) {
|
||||
draw_marker_item(painter, markerList[i], Color::blue(), Color::blue(), Color::magenta());
|
||||
draw_marker_item(painter, markerList[i], markerList[i].color, markerList[i].color, Color::black());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -826,7 +826,7 @@ void GeoMap::update_my_position(float lat, float lon, int32_t altitude) {
|
||||
my_pos.lat = lat;
|
||||
my_pos.lon = lon;
|
||||
my_altitude = altitude;
|
||||
redraw_map = is_changed;
|
||||
redraw_map |= is_changed;
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
@@ -876,8 +876,10 @@ void GeoMapView::update_position(float lat, float lon, uint16_t angle, int32_t a
|
||||
geopos.set_report_change(true);
|
||||
|
||||
geomap.set_angle(angle);
|
||||
if (is_changed) geomap.move(lon_, lat_);
|
||||
geomap.set_dirty();
|
||||
if (is_changed) {
|
||||
geomap.move(lon_, lat_);
|
||||
geomap.set_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
void GeoMapView::update_tag(const std::string tag) {
|
||||
|
||||
@@ -62,13 +62,14 @@ struct GeoMarker {
|
||||
float lon{0};
|
||||
uint16_t angle{0};
|
||||
std::string tag{""};
|
||||
Color color{Color::blue()};
|
||||
|
||||
GeoMarker& operator=(GeoMarker& rhs) {
|
||||
GeoMarker& operator=(const GeoMarker& rhs) {
|
||||
lat = rhs.lat;
|
||||
lon = rhs.lon;
|
||||
angle = rhs.angle;
|
||||
tag = rhs.tag;
|
||||
|
||||
color = rhs.color;
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -45,11 +45,8 @@
|
||||
#include "ui_mictx.hpp"
|
||||
|
||||
#include "ui_playlist.hpp"
|
||||
#include "ui_pocsag_tx.hpp"
|
||||
#include "ui_rds.hpp"
|
||||
#include "ui_recon.hpp"
|
||||
// #include "ui_scanner.hpp"
|
||||
// #include "ui_sd_over_usb.hpp"
|
||||
#include "ui_search.hpp"
|
||||
#include "ui_settings.hpp"
|
||||
#include "ui_sonde.hpp"
|
||||
@@ -64,7 +61,6 @@
|
||||
|
||||
#include "ais_app.hpp"
|
||||
#include "analog_audio_app.hpp"
|
||||
// #include "ble_comm_app.hpp"
|
||||
#include "ble_rx_app.hpp"
|
||||
#include "ble_tx_app.hpp"
|
||||
#include "capture_app.hpp"
|
||||
@@ -136,7 +132,6 @@ const NavigationView::AppList NavigationView::appList = {
|
||||
{"aprstx", "APRS TX", TX, ui::Color::green(), &bitmap_icon_aprs, new ViewFactory<APRSTXView>()},
|
||||
{"bletx", "BLE Tx", TX, ui::Color::green(), &bitmap_icon_btle, new ViewFactory<BLETxView>()},
|
||||
{"ooktx", "OOK", TX, ui::Color::yellow(), &bitmap_icon_remote, new ViewFactory<EncodersView>()},
|
||||
{"pocsagtx", "POCSAG TX", TX, ui::Color::green(), &bitmap_icon_pocsag, new ViewFactory<POCSAGTXView>()},
|
||||
{"rdstx", "RDS", TX, ui::Color::green(), &bitmap_icon_rds, new ViewFactory<RDSView>()},
|
||||
{"touchtune", "TouchTune", TX, ui::Color::green(), &bitmap_icon_touchtunes, new ViewFactory<TouchTunesView>()},
|
||||
/* TRX ********************************************************************/
|
||||
@@ -146,7 +141,6 @@ const NavigationView::AppList NavigationView::appList = {
|
||||
{"freqman", "Freq. Manager", UTILITIES, Color::green(), &bitmap_icon_freqman, new ViewFactory<FrequencyManagerView>()},
|
||||
{"iqtrim", "IQ Trim", UTILITIES, Color::orange(), &bitmap_icon_trim, new ViewFactory<IQTrimView>()},
|
||||
{"notepad", "Notepad", UTILITIES, Color::dark_cyan(), &bitmap_icon_notepad, new ViewFactory<TextEditorView>()},
|
||||
//{nullptr, "SD Over USB", UTILITIES, Color::yellow(), &bitmap_icon_hackrf, new ViewFactory<SdOverUsbView>()},
|
||||
{nullptr, "Debug", UTILITIES, Color::light_grey(), &bitmap_icon_debug, new ViewFactory<DebugMenuView>()},
|
||||
//{"testapp", "Test App", UTILITIES, Color::dark_grey(), nullptr, new ViewFactory<TestView>()},
|
||||
// Dangerous apps.
|
||||
@@ -607,7 +601,14 @@ bool InformationView::firmware_checksum_error() {
|
||||
|
||||
// only checking firmware checksum once per boot
|
||||
if (!fw_checksum_checked) {
|
||||
#ifdef PRALINE
|
||||
fw_checksum_error = (simple_checksum(FLASH_STARTING_ADDRESS, 4 * 1024 * 1024) != FLASH_EXPECTED_CHECKSUM);
|
||||
// TODO: This is a minimal workaround to fix the FLASH ERR checksum, bc GSG's cmake doesn't define the PortaRF board,
|
||||
// so if we do, we need to patch many codes. so we can't define the PortaRF board currently in our cmake.
|
||||
// discuss needed to find a better way but currently we need CI works and make hackrf pro works as much as possible.
|
||||
#else
|
||||
fw_checksum_error = (simple_checksum(FLASH_STARTING_ADDRESS, FLASH_SIZE_LIMIT_MB * 1024 * 1024) != FLASH_EXPECTED_CHECKSUM);
|
||||
#endif
|
||||
}
|
||||
return fw_checksum_error;
|
||||
}
|
||||
@@ -969,6 +970,8 @@ SystemView::SystemView(
|
||||
|
||||
navigation_view.push<SystemMenuView>();
|
||||
|
||||
add_child(¬ification_view);
|
||||
|
||||
if (pmem::config_splash()) {
|
||||
navigation_view.push<SplashScreenView>();
|
||||
}
|
||||
@@ -1054,17 +1057,56 @@ SplashScreenView::SplashScreenView(NavigationView& nav)
|
||||
};
|
||||
}
|
||||
|
||||
void SplashScreenView::get_random_splash_file(std::filesystem::path& path) {
|
||||
path = u"";
|
||||
|
||||
srand(LPC_RTC->CTIME0);
|
||||
|
||||
DIR dir;
|
||||
FILINFO fno;
|
||||
int32_t valid_count = 0;
|
||||
|
||||
// Open directory
|
||||
if (f_opendir(&dir, (const TCHAR*)splash_dir.c_str()) == FR_OK) {
|
||||
while (f_readdir(&dir, &fno) == FR_OK && fno.fname[0] != 0) {
|
||||
// Skip directories
|
||||
if (fno.fattrib & AM_DIR) continue;
|
||||
int len = 0;
|
||||
while (fno.fname[len]) len++; // Get length
|
||||
if (len >= 4) {
|
||||
const TCHAR* ext = &fno.fname[len - 4];
|
||||
// Check extension case-insensitively
|
||||
if (ext[0] == '.' &&
|
||||
(ext[1] == 'B' || ext[1] == 'b') &&
|
||||
(ext[2] == 'M' || ext[2] == 'm') &&
|
||||
(ext[3] == 'P' || ext[3] == 'p')) {
|
||||
valid_count++;
|
||||
// Reservoir Sampling:
|
||||
if ((rand() % valid_count) == 0) {
|
||||
path = splash_dir / fno.fname;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
f_closedir(&dir);
|
||||
}
|
||||
}
|
||||
|
||||
void SplashScreenView::paint(Painter&) {
|
||||
constexpr const uint8_t zoom = 3;
|
||||
set_clean();
|
||||
// if (!bmp_view.load_bmp(splash_dot_bmp)) { //--too slow drawing, bc of the more bmp format support, and up-> down drawing
|
||||
if (!portapack::display.draw_bmp_from_sdcard_file({0, 0}, splash_dot_bmp))
|
||||
// ^ try draw bmp file from sdcard at (0,0), and the (0,0) already bypassed the status bar, so actual pos is (0, STATUS_BAR_HEIGHT)
|
||||
portapack::display.draw_bitmap({screen_width / 2 - ((bitmap_titlebar_image.size.width() * zoom) / 2),
|
||||
screen_height / 2},
|
||||
bitmap_titlebar_image.size,
|
||||
bitmap_titlebar_image.data,
|
||||
Theme::getInstance()->bg_darkest->foreground,
|
||||
Theme::getInstance()->bg_darkest->background, zoom);
|
||||
if (portapack::display.draw_bmp_from_sdcard_file({0, 0}, splash_dot_bmp)) return;
|
||||
// ^ try draw bmp file from sdcard at (0,0), and the (0,0) already bypassed the status bar, so actual pos is (0, STATUS_BAR_HEIGHT)
|
||||
|
||||
std::filesystem::path path{};
|
||||
get_random_splash_file(path);
|
||||
if (portapack::display.draw_bmp_from_sdcard_file({0, 0}, path)) return;
|
||||
portapack::display.draw_bitmap({screen_width / 2 - ((bitmap_titlebar_image.size.width() * 3) / 2),
|
||||
screen_height / 2},
|
||||
bitmap_titlebar_image.size,
|
||||
bitmap_titlebar_image.data,
|
||||
Theme::getInstance()->bg_darkest->foreground,
|
||||
Theme::getInstance()->bg_darkest->background, 3);
|
||||
// ^ draw BMP HEX arr in firmware, note that the BMP HEX arr only cover the image part (instead of fill the screen with background, this position is located it in the center)
|
||||
}
|
||||
|
||||
@@ -1082,7 +1124,6 @@ bool SplashScreenView::on_touch(const TouchEvent event) {
|
||||
case TouchEvent::Type::Start:
|
||||
handle_pop();
|
||||
return false;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
#include "ui_audio.hpp"
|
||||
#include "ui_sd_card_status_view.hpp"
|
||||
#include "ui_dfu_menu.hpp"
|
||||
|
||||
#include "ui_notifications.hpp"
|
||||
#include "bitmap.hpp"
|
||||
#include "ui_bmpview.hpp"
|
||||
#include "ff.h"
|
||||
@@ -366,7 +366,7 @@ class SplashScreenView : public View {
|
||||
Button button_done{
|
||||
{screen_width, 0, 1, 1},
|
||||
""};
|
||||
// BMPViewer bmp_view{ {0, 0, screen_width, screen_height - 16}};
|
||||
void get_random_splash_file(std::filesystem::path& path);
|
||||
};
|
||||
|
||||
class ReceiversMenuView : public BtnGridView {
|
||||
@@ -446,11 +446,12 @@ class SystemView : public View {
|
||||
private:
|
||||
uint8_t overlay_active{0};
|
||||
|
||||
NavigationView navigation_view{};
|
||||
SystemStatusView status_view{navigation_view};
|
||||
InformationView info_view{navigation_view};
|
||||
NotificationView notification_view{navigation_view};
|
||||
DfuMenu overlay{navigation_view};
|
||||
DfuMenu2 overlay2{navigation_view};
|
||||
NavigationView navigation_view{};
|
||||
Context& context_;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
#include "ui_notifications.hpp"
|
||||
|
||||
#include "ui_navigation.hpp"
|
||||
|
||||
extern ui::SystemView* system_view_ptr;
|
||||
|
||||
namespace ui {
|
||||
|
||||
NotificationEntryView::NotificationEntryView(const NotificationEntry& entry, NotificationView* notifhandler)
|
||||
: entry_(entry), notifhandler_(notifhandler) {
|
||||
add_children({&background, &border, &title_text, &message_text, &close_button});
|
||||
border.set_outline(true);
|
||||
if (entry_.icon != NOTIF_ICON_NONE) {
|
||||
add_child(&icon_image);
|
||||
icon_image.set_parent_rect({UI_POS_X(0) + 2, UI_POS_Y(1), UI_POS_WIDTH(2), UI_POS_HEIGHT(1)});
|
||||
message_text.set_parent_rect({UI_POS_X(2) + 2, UI_POS_Y(1), UI_POS_WIDTH_REMAINING(6) - 2, UI_POS_HEIGHT(2)});
|
||||
if (entry_.icon == NOTIF_ICON_MESSAGE) {
|
||||
icon_image.set_bitmap(&bitmap_icon_pocsag);
|
||||
}
|
||||
|
||||
} else {
|
||||
message_text.set_parent_rect({UI_POS_X(1), UI_POS_Y(1), UI_POS_WIDTH_REMAINING(5) - 1, UI_POS_HEIGHT(2)});
|
||||
}
|
||||
title_text.set_style(Theme::getInstance()->bg_dark);
|
||||
title_text.set(entry_.title);
|
||||
message_text.set_style(Theme::getInstance()->bg_darkest);
|
||||
message_text.set(entry_.message);
|
||||
|
||||
close_button.on_select = [this](Button&) {
|
||||
if (notifhandler_) {
|
||||
notifhandler_->remove_notification(entry_.id);
|
||||
}
|
||||
};
|
||||
|
||||
message_text.on_select = [this](Text&) {
|
||||
if (!entry_.source_app.empty() && notifhandler_) {
|
||||
notifhandler_->open_notification(entry_.source_app);
|
||||
// notifhandler_->remove_notification(entry_.id);
|
||||
}
|
||||
};
|
||||
title_text.on_select = message_text.on_select;
|
||||
}
|
||||
|
||||
NotificationView::NotificationView(NavigationView& nav)
|
||||
: nav_(nav) {
|
||||
signal_token_tick_second = rtc_time::signal_tick_second += [this]() {
|
||||
this->on_tick_second();
|
||||
};
|
||||
}
|
||||
|
||||
NotificationView::~NotificationView() {
|
||||
rtc_time::signal_tick_second -= signal_token_tick_second;
|
||||
}
|
||||
|
||||
void NotificationView::on_tick_second() {
|
||||
bool changed = false;
|
||||
for (size_t i = 0; i < notification_views_.size();) {
|
||||
if (notification_views_[i]->entry()->increase_time(1000)) {
|
||||
notification_views_.erase(notification_views_.begin() + i);
|
||||
changed = true;
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
rearrange_notifications();
|
||||
}
|
||||
}
|
||||
|
||||
void NotificationView::rearrange_notifications() {
|
||||
size_t count = children().size();
|
||||
while (children().size() > 0) {
|
||||
remove_child(children().back());
|
||||
}
|
||||
|
||||
const int entry_height = UI_POS_HEIGHT(3) + 1;
|
||||
int index = 0;
|
||||
|
||||
for (auto& view_ptr : notification_views_) {
|
||||
view_ptr->set_parent_rect({UI_POS_X(0),
|
||||
(Coord)(UI_POS_Y(0) + (index * entry_height)),
|
||||
UI_POS_MAXWIDTH,
|
||||
entry_height});
|
||||
add_child(view_ptr.get());
|
||||
index++;
|
||||
}
|
||||
|
||||
set_parent_rect({UI_POS_X(0), UI_POS_Y(0), UI_POS_MAXWIDTH, (Coord)(notification_views_.size() * entry_height)});
|
||||
set_dirty();
|
||||
if (count != children().size()) {
|
||||
// refresh screen!!!
|
||||
system_view_ptr->set_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
void NotificationView::add_notification(NotificationEntry& entry) {
|
||||
if (notification_views_.size() >= max_notifications) {
|
||||
notification_views_.erase(notification_views_.begin());
|
||||
}
|
||||
|
||||
entry.id = ++curr_not_id;
|
||||
notification_views_.push_back(std::make_unique<NotificationEntryView>(entry, this));
|
||||
|
||||
rearrange_notifications();
|
||||
}
|
||||
|
||||
void NotificationView::remove_notification(uint16_t id) {
|
||||
bool found = false;
|
||||
size_t found_index = 0;
|
||||
|
||||
for (size_t i = 0; i < notification_views_.size(); i++) {
|
||||
if (notification_views_[i]->id() == id) {
|
||||
found = true;
|
||||
found_index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found) {
|
||||
notification_views_.erase(notification_views_.begin() + found_index);
|
||||
rearrange_notifications();
|
||||
}
|
||||
}
|
||||
|
||||
void NotificationView::open_notification(std::string app_name) {
|
||||
if (app_name.empty()) return;
|
||||
nav_.StartAppByName(app_name.c_str());
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
@@ -0,0 +1,101 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include "ui.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "signal.hpp"
|
||||
#include "rtc_time.hpp"
|
||||
#include "event_m0.hpp"
|
||||
|
||||
namespace ui {
|
||||
|
||||
class NotificationView;
|
||||
class NavigationView;
|
||||
|
||||
enum notification_icon_t : uint8_t {
|
||||
NOTIF_ICON_NONE = 0,
|
||||
NOTIF_ICON_MESSAGE
|
||||
};
|
||||
|
||||
class NotificationEntry {
|
||||
public:
|
||||
std::string source_app{};
|
||||
std::string title{};
|
||||
std::string message{};
|
||||
notification_icon_t icon = NOTIF_ICON_NONE;
|
||||
uint16_t timeout = 10000;
|
||||
uint16_t id = 0;
|
||||
|
||||
NotificationEntry() = default;
|
||||
NotificationEntry(const std::string& source_app, const std::string& title, const std::string& message, notification_icon_t icon = NOTIF_ICON_NONE, uint16_t timeout = 10000, uint16_t id = 0)
|
||||
: source_app(source_app), title(title), message(message), icon(icon), timeout(timeout), id(id) {}
|
||||
|
||||
static NotificationEntry build(const std::string& source_app, const std::string& title, const std::string& message, notification_icon_t icon = NOTIF_ICON_NONE, uint16_t timeout = 10000) {
|
||||
return NotificationEntry{source_app, title, message, icon, timeout, 0};
|
||||
}
|
||||
|
||||
bool increase_time(uint16_t delta) {
|
||||
current_time += delta;
|
||||
return (current_time >= timeout);
|
||||
}
|
||||
|
||||
private:
|
||||
uint16_t current_time = 0;
|
||||
};
|
||||
|
||||
class NotificationEntryView : public View {
|
||||
public:
|
||||
NotificationEntryView(const NotificationEntry& entry, NotificationView* notifhandler);
|
||||
|
||||
NotificationEntryView(const NotificationEntryView&) = delete;
|
||||
NotificationEntryView& operator=(const NotificationEntryView&) = delete;
|
||||
|
||||
ui::Rectangle background{{1, 1, UI_POS_MAXWIDTH - 2, UI_POS_HEIGHT(3) - 2}, Theme::getInstance()->bg_darkest->background};
|
||||
ui::Rectangle border{{0, 0, UI_POS_MAXWIDTH, UI_POS_HEIGHT(3) + 1}, Theme::getInstance()->bg_light->background};
|
||||
ui::Text title_text{{UI_POS_X(0) + 1, UI_POS_Y(0) + 1, UI_POS_WIDTH_REMAINING(4) - 3, UI_POS_HEIGHT(1)}, ""};
|
||||
ui::Text message_text{{UI_POS_X(1), UI_POS_Y(1), UI_POS_WIDTH_REMAINING(5) - 1, UI_POS_HEIGHT(2)}, ""};
|
||||
ui::Image icon_image{}; // 16*16 bitmap
|
||||
ui::Button close_button{{UI_POS_X_RIGHT(4) - 2, UI_POS_Y(0) + 1, UI_POS_WIDTH(4), UI_POS_HEIGHT(2)}, "X"};
|
||||
|
||||
uint16_t id() const { return entry_.id; }
|
||||
NotificationEntry* entry() { return &entry_; }
|
||||
|
||||
private:
|
||||
NotificationEntry entry_;
|
||||
NotificationView* notifhandler_;
|
||||
};
|
||||
|
||||
class NotificationView : public View {
|
||||
public:
|
||||
NotificationView(NavigationView& nav);
|
||||
~NotificationView();
|
||||
|
||||
void add_notification(NotificationEntry& entry);
|
||||
void remove_notification(uint16_t id);
|
||||
void open_notification(std::string app_name);
|
||||
|
||||
private:
|
||||
void on_tick_second();
|
||||
void rearrange_notifications();
|
||||
|
||||
NavigationView& nav_;
|
||||
|
||||
// Changed to unique_ptr to handle non-copyable Views safely
|
||||
std::vector<std::unique_ptr<NotificationEntryView>> notification_views_{};
|
||||
|
||||
SignalToken signal_token_tick_second{};
|
||||
|
||||
uint16_t curr_not_id = 0;
|
||||
constexpr static uint8_t max_notifications = 4;
|
||||
|
||||
MessageHandlerRegistration message_handler_notifs{
|
||||
Message::ID::NotificationData, [this](Message* const p) {
|
||||
const auto message = static_cast<const NotificationDataMessage*>(p);
|
||||
NotificationEntry entry = NotificationEntry::build(message->source_app, message->title, message->message, static_cast<notification_icon_t>(message->icon), message->timeout);
|
||||
this->add_notification(entry);
|
||||
}};
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
@@ -105,26 +105,23 @@ static void cmd_sd_over_usb(BaseSequentialStream* chp, int argc, char* argv[]) {
|
||||
(void)chp;
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
|
||||
ui::Painter painter;
|
||||
painter.fill_rectangle(
|
||||
{0, 0, portapack::display.width(), portapack::display.height()},
|
||||
Theme::getInstance()->fg_yellow->background);
|
||||
|
||||
painter.draw_bitmap(
|
||||
{portapack::display.width() / 2 - 8, portapack::display.height() / 2 - 8},
|
||||
ui::bitmap_icon_hackrf,
|
||||
Theme::getInstance()->fg_yellow->foreground,
|
||||
Theme::getInstance()->fg_yellow->background);
|
||||
|
||||
sdcDisconnect(&SDCD1);
|
||||
sdcStop(&SDCD1);
|
||||
|
||||
m4_request_shutdown();
|
||||
chThdSleepMilliseconds(50);
|
||||
portapack::shutdown(true);
|
||||
m4_init(portapack::spi_flash::image_tag_usb_sd, portapack::memory::map::m4_code, false);
|
||||
m0_halt();
|
||||
auto evtd = getEventDispatcherInstance();
|
||||
if (!evtd) return;
|
||||
auto top_widget = evtd->getTopWidget();
|
||||
if (!top_widget) return;
|
||||
auto nav = static_cast<ui::SystemView*>(top_widget)->get_navigation_view();
|
||||
if (!nav) return;
|
||||
nav->home(false);
|
||||
std::string appwithpath = "/" + apps_dir.string() + "/";
|
||||
appwithpath += "sdusb.ppma";
|
||||
bool ret = ui::ExternalItemsMenuLoader::run_external_app(*nav, path_from_string8((char*)appwithpath.c_str()));
|
||||
if (ret) {
|
||||
chprintf(chp, "ok\r\n");
|
||||
} else {
|
||||
chprintf(chp, "Failed to start SD over USB\r\n");
|
||||
}
|
||||
evtd->wait_finish_frame();
|
||||
evtd->emulateKeyboard('\n');
|
||||
}
|
||||
|
||||
bool strEndsWith(const std::u16string& str, const std::u16string& suffix) {
|
||||
@@ -1386,22 +1383,76 @@ static void cmd_getres(BaseSequentialStream* chp, int argc, char* argv[]) {
|
||||
static void cmd_getflash(BaseSequentialStream* chp, int argc, char* argv[]) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
uint8_t allowrun = FLASH_SIZE_MB;
|
||||
// FLASH_SIZE_LIMIT_MB may be a float (e.g. 3.5 for HPro), always cast explicitly.
|
||||
uint8_t allowrun;
|
||||
#ifdef PRALINE
|
||||
allowrun = 4; // HackRF Pro
|
||||
#else
|
||||
if (portapack::device_type == portapack::DeviceType::DEV_PORTAPACK) {
|
||||
allowrun = 1;
|
||||
allowrun = 1; // PortaPack classic
|
||||
} else {
|
||||
allowrun = FLASH_SIZE_MB; // PortaRF
|
||||
}
|
||||
std::string res = "ALLOWEDFW:" + to_string_dec_uint(FLASH_SIZE_MB) + "\r\nALLOWEDRUNTIME:" + to_string_dec_uint(allowrun) + "\r\nCURRENT:" + to_string_dec_uint(FLASH_SIZE_LIMIT_MB) + "\r\nok\r\n";
|
||||
#endif
|
||||
std::string res = "ALLOWEDFW:" + to_string_dec_uint((uint32_t)FLASH_SIZE_MB) + "\r\nALLOWEDRUNTIME:" + to_string_dec_uint((uint32_t)allowrun) + "\r\nCURRENT:" + to_string_dec_uint((uint32_t)FLASH_SIZE_LIMIT_MB) + "\r\nok\r\n";
|
||||
chprintf(chp, res.c_str());
|
||||
}
|
||||
|
||||
static void cmd_getdevtype(BaseSequentialStream* chp, int argc, char* argv[]) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
std::string res = portapack::device_type == portapack::DeviceType::DEV_PORTARF ? "PORTARF" : "PORTAPACK";
|
||||
std::string res;
|
||||
#ifdef PRALINE
|
||||
res = "HPRO";
|
||||
#else
|
||||
if (portapack::device_type == portapack::DeviceType::DEV_PORTARF) {
|
||||
res = "PORTARF";
|
||||
} else {
|
||||
res = "PORTAPACK";
|
||||
}
|
||||
#endif
|
||||
res += "\r\nok\r\n";
|
||||
chprintf(chp, res.c_str());
|
||||
}
|
||||
|
||||
static void cmd_notification(BaseSequentialStream* chp, int argc, char* argv[]) {
|
||||
const char* usage = "usage: notif <iconindex> [appname]\r\n";
|
||||
if (argc < 1) {
|
||||
chprintf(chp, usage);
|
||||
return;
|
||||
}
|
||||
int iconindex = atoi(argv[0]);
|
||||
std::string appname = (argc > 1) ? argv[1] : "";
|
||||
chprintf(chp, "Send title, and a <CR>\r\n");
|
||||
std::string title{};
|
||||
uint8_t msg[1]{0};
|
||||
do {
|
||||
size_t bytes_read = chSequentialStreamRead(chp, &msg[0], 1);
|
||||
if (bytes_read != 1)
|
||||
break;
|
||||
if (msg[0] == '\r') // end of title
|
||||
break;
|
||||
if (msg[0] == '\n') continue;
|
||||
title += (char)msg[0];
|
||||
} while (title.size() < 48);
|
||||
std::string message{};
|
||||
chprintf(chp, "Send message, and a <CR>\r\n");
|
||||
do {
|
||||
size_t bytes_read = chSequentialStreamRead(chp, &msg[0], 1);
|
||||
if (bytes_read != 1)
|
||||
break;
|
||||
if (msg[0] == '\r') // end of message
|
||||
break;
|
||||
if (msg[0] == '\n') continue;
|
||||
message += (char)msg[0];
|
||||
} while (message.size() < 298);
|
||||
|
||||
NotificationDataMessage msgn{appname.c_str(), title.c_str(), message.c_str(), (uint8_t)iconindex};
|
||||
EventDispatcher::send_message(msgn);
|
||||
|
||||
chprintf(chp, "ok\r\n");
|
||||
}
|
||||
|
||||
static const ShellCommand commands[] = {
|
||||
{"reboot", cmd_reboot},
|
||||
{"dfu", cmd_dfu},
|
||||
@@ -1440,6 +1491,7 @@ static const ShellCommand commands[] = {
|
||||
{"getres", cmd_getres},
|
||||
{"getflash", cmd_getflash},
|
||||
{"getdevtype", cmd_getdevtype},
|
||||
{"notif", cmd_notification},
|
||||
{NULL, NULL}};
|
||||
|
||||
static const ShellConfig shell_cfg1 = {
|
||||
|
||||
@@ -674,6 +674,22 @@ set(MODE_CPPSRC
|
||||
)
|
||||
DeclareTargets(PMRS morse)
|
||||
|
||||
|
||||
### RTTY RX
|
||||
|
||||
set(MODE_CPPSRC
|
||||
proc_rtty_rx.cpp
|
||||
)
|
||||
DeclareTargets(PRTR rtty_rx)
|
||||
|
||||
|
||||
### RTTY TX
|
||||
|
||||
set(MODE_CPPSRC
|
||||
proc_rtty_tx.cpp
|
||||
)
|
||||
DeclareTargets(PRTT rtty_tx)
|
||||
|
||||
### SD over USB
|
||||
|
||||
set(MODE_INCDIR
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
|
||||
#ifndef __FPROTO_RESTAURANT_PAGER_H__
|
||||
#define __FPROTO_RESTAURANT_PAGER_H__
|
||||
|
||||
#include "subghzdbase.hpp"
|
||||
|
||||
typedef enum : uint8_t {
|
||||
RestaurantPagerDecoderStepReset = 0,
|
||||
RestaurantPagerDecoderStepSaveDuration,
|
||||
RestaurantPagerDecoderStepCheckDuration,
|
||||
} RestaurantPagerDecoderStep;
|
||||
|
||||
class FProtoSubGhzDRestaurantPager : public FProtoSubGhzDBase {
|
||||
public:
|
||||
FProtoSubGhzDRestaurantPager() {
|
||||
sensorType = FPS_RESTAURANT_PAGER;
|
||||
te_short = 204;
|
||||
te_long = 636;
|
||||
te_delta = 100;
|
||||
min_count_bit_for_found = 25;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
switch (parser_step) {
|
||||
case RestaurantPagerDecoderStepReset:
|
||||
if ((!level) && (DURATION_DIFF(duration, te_short * 36) < te_delta * 36)) {
|
||||
// Found preamble
|
||||
parser_step = RestaurantPagerDecoderStepSaveDuration;
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
}
|
||||
break;
|
||||
case RestaurantPagerDecoderStepSaveDuration:
|
||||
if (level) {
|
||||
te_last = duration;
|
||||
parser_step = RestaurantPagerDecoderStepCheckDuration;
|
||||
}
|
||||
break;
|
||||
case RestaurantPagerDecoderStepCheckDuration:
|
||||
if (!level) {
|
||||
if (duration >= ((uint32_t)te_long * 2)) {
|
||||
parser_step = RestaurantPagerDecoderStepSaveDuration;
|
||||
if (decode_count_bit == min_count_bit_for_found) {
|
||||
// Skip all-ones preamble frames
|
||||
if ((decode_data >> 1) != 0xFFFFFF) {
|
||||
data_count_bit = decode_count_bit;
|
||||
if (callback) callback(this);
|
||||
}
|
||||
}
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
if ((DURATION_DIFF(te_last, te_short) < te_delta) &&
|
||||
(DURATION_DIFF(duration, te_long) < te_delta * 3)) {
|
||||
subghz_protocol_blocks_add_bit(0);
|
||||
parser_step = RestaurantPagerDecoderStepSaveDuration;
|
||||
} else if (
|
||||
(DURATION_DIFF(te_last, te_long) < te_delta * 3) &&
|
||||
(DURATION_DIFF(duration, te_short) < te_delta)) {
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
parser_step = RestaurantPagerDecoderStepSaveDuration;
|
||||
} else {
|
||||
parser_step = RestaurantPagerDecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
parser_step = RestaurantPagerDecoderStepReset;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -55,6 +55,7 @@ So include here the .hpp, and add a new element to the protos vector in the cons
|
||||
#include "s-marantec24.hpp"
|
||||
// GENIE FROM PR
|
||||
#include "s-holtek_ht6p20b.hpp"
|
||||
#include "s-restaurant_pager.hpp"
|
||||
|
||||
#ifndef __FPROTO_PROTOLISTSGZ_H__
|
||||
#define __FPROTO_PROTOLISTSGZ_H__
|
||||
@@ -109,6 +110,7 @@ class SubGhzDProtos : public FProtoListGeneral {
|
||||
protos[FPS_GANGQI] = new FProtoSubGhzDGangqi();
|
||||
protos[FPS_MARANTEC24] = new FProtoSubGhzDMarantec24();
|
||||
protos[FPS_HOLTEKHT6P20B] = new FProtoSubGhzDHoltekHt6p20b();
|
||||
protos[FPS_RESTAURANT_PAGER] = new FProtoSubGhzDRestaurantPager();
|
||||
|
||||
for (uint8_t i = 0; i < FPS_COUNT; ++i) {
|
||||
if (protos[i] != NULL) protos[i]->setCallback(callbackTarget);
|
||||
|
||||
@@ -59,6 +59,7 @@ enum FPROTO_SUBGHZD_SENSOR : uint8_t {
|
||||
FPS_GANGQI,
|
||||
FPS_MARANTEC24,
|
||||
FPS_HOLTEKHT6P20B,
|
||||
FPS_RESTAURANT_PAGER,
|
||||
FPS_COUNT
|
||||
};
|
||||
|
||||
|
||||
@@ -28,8 +28,6 @@
|
||||
#include "debug.hpp"
|
||||
#include "portapack_shared_memory.hpp"
|
||||
|
||||
#include "../flashsize.h"
|
||||
|
||||
#define PAGE_LEN 256U
|
||||
#define NUM_PAGES (4096U * FLASH_SIZE_MB)
|
||||
|
||||
|
||||
@@ -0,0 +1,334 @@
|
||||
/*
|
||||
* Copyright (C) 2026 HTotoo
|
||||
*
|
||||
* 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_rtty_rx.hpp"
|
||||
#include "portapack_shared_memory.hpp"
|
||||
#include "audio_dma.hpp"
|
||||
#include "event_m4.hpp"
|
||||
|
||||
// RTTY Timing Limits (at 24kHz)
|
||||
static constexpr uint32_t MIN_VALID_PULSE = 200;
|
||||
static constexpr uint32_t MAX_VALID_PULSE = 800;
|
||||
|
||||
void RTTYRxProcessor::configure() {
|
||||
configured = false;
|
||||
|
||||
baseband_thread.set_sampling_rate(baseband_fs);
|
||||
|
||||
// 1. 3.072M -> 384k
|
||||
decim_0.configure(taps_4k25_decim_0.taps);
|
||||
|
||||
// 2. 384k -> 48k
|
||||
decim_1.configure(taps_4k25_decim_1.taps);
|
||||
|
||||
// 3. 48k -> 24k
|
||||
channel_filter.configure(taps_11k0_channel.taps, 2);
|
||||
|
||||
// FM Demodulator
|
||||
demod.configure(24000, 9000);
|
||||
|
||||
// Audio Output
|
||||
audio_output.configure(iir_config_passthrough, iir_config_passthrough, 1.0f);
|
||||
|
||||
// Reset State
|
||||
val_max = -200000;
|
||||
val_min = 200000;
|
||||
|
||||
uart_state = WAIT_START;
|
||||
|
||||
inverted_polarity = false;
|
||||
// Default to standard 45.45 baud
|
||||
estimated_bit_width = 528;
|
||||
samples_per_bit = 528;
|
||||
pulse_measure_counter = 0;
|
||||
|
||||
configured = true;
|
||||
}
|
||||
|
||||
// Variables for Fast-Lock Auto Baud
|
||||
uint32_t candidate_width = 0;
|
||||
uint8_t candidate_hits = 0;
|
||||
uint32_t squelch_closed_timer = 0;
|
||||
bool is_squelched = true;
|
||||
|
||||
void RTTYRxProcessor::execute(const buffer_c8_t& buffer) {
|
||||
if (!configured) return;
|
||||
|
||||
const auto decim_0_out = decim_0.execute(buffer, dst_buffer);
|
||||
buffer_c16_t decim_1_target{dst_buffer_data.data() + 256, 256};
|
||||
const auto decim_1_out = decim_1.execute(decim_0_out, decim_1_target);
|
||||
const auto channel_out = channel_filter.execute(decim_1_out, dst_buffer);
|
||||
|
||||
auto audio = demod.execute(channel_out, audio_buffer);
|
||||
|
||||
feed_channel_stats(channel_out);
|
||||
|
||||
for (size_t i = 0; i < audio.count; i++) {
|
||||
int16_t sample = audio.p[i];
|
||||
|
||||
// DECODER INPUT
|
||||
int32_t fm_val = (int32_t)sample * 32;
|
||||
|
||||
// 1. ENVELOPE TRACKING (Floating)
|
||||
if (fm_val > val_max) val_max = fm_val;
|
||||
if (fm_val < val_min) val_min = fm_val;
|
||||
|
||||
// 2. DECAY
|
||||
// Shrink the envelope spread (Max - Min) slowly.
|
||||
if (++decay_timer == 0) {
|
||||
int32_t spread = val_max - val_min;
|
||||
if (spread > 200) {
|
||||
int32_t decay = (spread >> 7) + 1;
|
||||
if (val_max > val_min + decay) val_max -= decay;
|
||||
if (val_min < val_max - decay) val_min += decay;
|
||||
} else {
|
||||
// val_max += 100;
|
||||
// val_min -= 100;
|
||||
// temp off. but kept it for memory
|
||||
}
|
||||
}
|
||||
|
||||
// 3. OFFSET CALCULATION
|
||||
int32_t midpoint = (val_max + val_min) / 2;
|
||||
|
||||
// 4. SIGNAL CENTERING
|
||||
int32_t centered_val = fm_val - midpoint;
|
||||
|
||||
// LPF for Slicer
|
||||
fm_val_smoothed += (centered_val - fm_val_smoothed) >> LPF_ALPHA_SHIFT;
|
||||
|
||||
// 5. SQUELCH
|
||||
int32_t spread = val_max - val_min;
|
||||
|
||||
if (is_squelched) {
|
||||
if (spread > 600) is_squelched = false;
|
||||
} else {
|
||||
if (spread < 300) is_squelched = true;
|
||||
}
|
||||
|
||||
process_demodulated_sample(fm_val_smoothed);
|
||||
|
||||
// 6. AUDIO PATH
|
||||
if (is_squelched) {
|
||||
audio.p[i] = 0;
|
||||
} else {
|
||||
int32_t audio_boost = sample * 48;
|
||||
if (audio_boost > 32767)
|
||||
audio_boost = 32767;
|
||||
else if (audio_boost < -32768)
|
||||
audio_boost = -32768;
|
||||
audio.p[i] = (int16_t)audio_boost;
|
||||
}
|
||||
}
|
||||
|
||||
audio_output.write(audio);
|
||||
|
||||
// UI Update
|
||||
if (tx_message.data_len > 0) {
|
||||
if (baud_rate == 0 && samples_per_bit > 0) {
|
||||
uint32_t b = (final_fs * 100) / samples_per_bit;
|
||||
if (b > 4300 && b < 4700)
|
||||
b = 4500;
|
||||
else if (b > 4800 && b < 5200)
|
||||
b = 5000;
|
||||
else if (b > 7200 && b < 7800)
|
||||
b = 7500;
|
||||
tx_message.baud = (uint16_t)b;
|
||||
} else {
|
||||
tx_message.baud = baud_rate;
|
||||
}
|
||||
tx_message.shift = shift_hz;
|
||||
|
||||
if (shared_memory.application_queue.push(tx_message)) {
|
||||
tx_message.data_len = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RTTYRxProcessor::process_demodulated_sample(int32_t sample) {
|
||||
// 1. Squelch Check
|
||||
if (is_squelched) {
|
||||
squelch_closed_timer++;
|
||||
if (squelch_closed_timer > 12000) {
|
||||
uart_state = WAIT_START;
|
||||
current_slicer_bit = 1;
|
||||
pulse_measure_counter = 0;
|
||||
inverted_polarity = false;
|
||||
if (baud_rate == 0) {
|
||||
estimated_bit_width = 528;
|
||||
samples_per_bit = 528;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
squelch_closed_timer = 0;
|
||||
|
||||
// 2. Schmitt Trigger
|
||||
int32_t hysteresis = (val_max - val_min) / 8;
|
||||
|
||||
uint8_t raw_bit = current_slicer_bit;
|
||||
if (inverted_polarity) raw_bit = !raw_bit;
|
||||
|
||||
if (sample > hysteresis)
|
||||
raw_bit = 1;
|
||||
else if (sample < -hysteresis)
|
||||
raw_bit = 0;
|
||||
|
||||
// Polarity Check
|
||||
if (raw_bit == 0) {
|
||||
if (++polarity_timer > 7200) {
|
||||
inverted_polarity = !inverted_polarity;
|
||||
polarity_timer = 0;
|
||||
val_max = -200000;
|
||||
val_min = 200000;
|
||||
uart_state = WAIT_START;
|
||||
}
|
||||
} else {
|
||||
polarity_timer = 0;
|
||||
}
|
||||
|
||||
current_slicer_bit = inverted_polarity ? !raw_bit : raw_bit;
|
||||
|
||||
// 3. Auto Baud
|
||||
if (baud_rate == 0) {
|
||||
pulse_measure_counter++;
|
||||
if (current_slicer_bit != last_bit_state) {
|
||||
update_baud_estimation(pulse_measure_counter);
|
||||
pulse_measure_counter = 0;
|
||||
last_bit_state = current_slicer_bit;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. UART State Machine
|
||||
switch (uart_state) {
|
||||
case WAIT_START:
|
||||
if (current_slicer_bit == 0) {
|
||||
phase_counter = samples_per_bit / 2;
|
||||
uart_state = CHECK_START;
|
||||
}
|
||||
break;
|
||||
|
||||
case CHECK_START:
|
||||
if (--phase_counter == 0) {
|
||||
if (current_slicer_bit == 0) {
|
||||
phase_counter = samples_per_bit;
|
||||
bit_counter = 0;
|
||||
shift_reg = 0;
|
||||
uart_state = READ_BITS;
|
||||
} else {
|
||||
uart_state = WAIT_START;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case READ_BITS:
|
||||
if (--phase_counter == 0) {
|
||||
if (current_slicer_bit) shift_reg |= (1 << bit_counter);
|
||||
|
||||
phase_counter = samples_per_bit;
|
||||
bit_counter++;
|
||||
|
||||
if (bit_counter >= 5) {
|
||||
uart_state = WAIT_STOP;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case WAIT_STOP:
|
||||
if (--phase_counter == 0) {
|
||||
// Accept data even if stop bit is noisy (0)
|
||||
// This improves reception during fades
|
||||
// if (current_slicer_bit == 1) {
|
||||
append_data(shift_reg & 0x1F);
|
||||
//}
|
||||
uart_state = WAIT_START;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void RTTYRxProcessor::update_baud_estimation(uint32_t pulse_width) {
|
||||
if (pulse_width < MIN_VALID_PULSE || pulse_width > MAX_VALID_PULSE) return;
|
||||
|
||||
int32_t diff = (int32_t)pulse_width - (int32_t)estimated_bit_width;
|
||||
if (diff < 0) diff = -diff;
|
||||
|
||||
if (diff < (int32_t)(estimated_bit_width / 6)) {
|
||||
estimated_bit_width = (estimated_bit_width * 7 + pulse_width) / 8;
|
||||
samples_per_bit = estimated_bit_width;
|
||||
candidate_hits = 0;
|
||||
} else {
|
||||
int32_t cand_diff = (int32_t)pulse_width - (int32_t)candidate_width;
|
||||
if (cand_diff < 0) cand_diff = -cand_diff;
|
||||
|
||||
if (cand_diff < (int32_t)(candidate_width / 8)) {
|
||||
candidate_hits++;
|
||||
if (candidate_hits >= 3) {
|
||||
estimated_bit_width = (candidate_width + pulse_width) / 2;
|
||||
samples_per_bit = estimated_bit_width;
|
||||
candidate_hits = 0;
|
||||
uart_state = WAIT_START;
|
||||
}
|
||||
} else {
|
||||
candidate_width = pulse_width;
|
||||
candidate_hits = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RTTYRxProcessor::append_data(uint8_t raw_baudot_code) {
|
||||
if (tx_message.data_len < tx_message.max_len) {
|
||||
tx_message.data[tx_message.data_len] = raw_baudot_code;
|
||||
tx_message.data_len++;
|
||||
}
|
||||
}
|
||||
|
||||
void RTTYRxProcessor::on_message(const Message* const message) {
|
||||
if (message->id == Message::ID::RTTYData) {
|
||||
const auto& rtty_msg = static_cast<const RTTYDataMessage&>(*message);
|
||||
|
||||
if (rtty_msg.baud != baud_rate) {
|
||||
baud_rate = rtty_msg.baud;
|
||||
if (baud_rate > 0) {
|
||||
const float real_baud = (float)baud_rate / 100.0f;
|
||||
samples_per_bit = (uint32_t)((float)final_fs / real_baud);
|
||||
estimated_bit_width = samples_per_bit;
|
||||
} else {
|
||||
estimated_bit_width = 528;
|
||||
samples_per_bit = 528;
|
||||
inverted_polarity = false;
|
||||
}
|
||||
uart_state = WAIT_START;
|
||||
}
|
||||
shift_hz = rtty_msg.shift;
|
||||
if (!configured) {
|
||||
configure();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
audio::dma::init_audio_out();
|
||||
EventDispatcher event_dispatcher{std::make_unique<RTTYRxProcessor>()};
|
||||
event_dispatcher.run();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright (C) 2026 HTotoo
|
||||
*
|
||||
* 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_RTTY_RX_H__
|
||||
#define __PROC_RTTY_RX_H__
|
||||
|
||||
#include "baseband_processor.hpp"
|
||||
#include "baseband_thread.hpp"
|
||||
#include "rssi_thread.hpp"
|
||||
#include "message.hpp"
|
||||
#include "dsp_decimate.hpp"
|
||||
#include "dsp_demodulate.hpp"
|
||||
#include "audio_output.hpp"
|
||||
#include "dsp_fir_taps.hpp"
|
||||
|
||||
class RTTYRxProcessor : 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 = 3072000;
|
||||
|
||||
// RTTY Config
|
||||
uint16_t baud_rate = 0; // 0 = Auto-detect
|
||||
uint16_t shift_hz = 170;
|
||||
bool configured = false;
|
||||
|
||||
// DSP Rates
|
||||
// Stage 0: 3.072M / 8 = 384k
|
||||
// Stage 1: 384k / 8 = 48k
|
||||
// Stage 2: 48k / 2 = 24k (Audio/Demod)
|
||||
static constexpr uint32_t decim_0_out_fs = baseband_fs / 8;
|
||||
static constexpr uint32_t decim_1_out_fs = decim_0_out_fs / 8;
|
||||
static constexpr uint32_t final_fs = decim_1_out_fs / 2; // 24000 Hz
|
||||
|
||||
// Tuning Constants
|
||||
static constexpr int32_t LPF_ALPHA_SHIFT = 2;
|
||||
static constexpr int32_t MIN_SHIFT_SPREAD = 400;
|
||||
|
||||
// Decimation Chain
|
||||
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{};
|
||||
dsp::decimate::FIRC16xR16x32Decim8 decim_1{};
|
||||
dsp::decimate::FIRAndDecimateComplex channel_filter{};
|
||||
|
||||
// Demodulator & Audio
|
||||
dsp::demodulate::FM demod{};
|
||||
AudioOutput audio_output{};
|
||||
|
||||
// Buffers
|
||||
std::array<complex16_t, 512> dst_buffer_data{};
|
||||
const buffer_c16_t dst_buffer{dst_buffer_data.data(), dst_buffer_data.size()};
|
||||
|
||||
std::array<int16_t, 32> audio_data{};
|
||||
const buffer_s16_t audio_buffer{audio_data.data(), audio_data.size()};
|
||||
|
||||
// Output Message
|
||||
RTTYDataMessage tx_message{};
|
||||
|
||||
// Demodulator State
|
||||
int32_t fm_val_smoothed = 0;
|
||||
|
||||
// Tracker State
|
||||
int32_t val_max = 0;
|
||||
int32_t val_min = 0;
|
||||
uint8_t decay_timer = 0;
|
||||
|
||||
// Auto-Baud State
|
||||
uint32_t pulse_measure_counter = 0;
|
||||
uint8_t last_bit_state = 0;
|
||||
uint32_t estimated_bit_width = 528; // ~45 baud @ 24k
|
||||
|
||||
// UART State
|
||||
enum UartState {
|
||||
WAIT_START,
|
||||
CHECK_START,
|
||||
READ_BITS,
|
||||
WAIT_STOP
|
||||
};
|
||||
|
||||
UartState uart_state = WAIT_START;
|
||||
uint32_t samples_per_bit = 528;
|
||||
uint32_t phase_counter = 0;
|
||||
uint8_t bit_counter = 0;
|
||||
uint8_t shift_reg = 0;
|
||||
uint8_t current_slicer_bit = 1;
|
||||
|
||||
// Polarity
|
||||
bool inverted_polarity = true;
|
||||
uint32_t polarity_timer = 0;
|
||||
|
||||
void configure();
|
||||
void process_demodulated_sample(int32_t sample);
|
||||
void update_baud_estimation(uint32_t pulse_width);
|
||||
void append_data(uint8_t raw_baudot_code);
|
||||
|
||||
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive};
|
||||
RSSIThread rssi_thread{};
|
||||
};
|
||||
|
||||
#endif /*__PROC_RTTY_RX_H__*/
|
||||
@@ -0,0 +1,245 @@
|
||||
/*
|
||||
* Copyright (C) 2026 HTotoo
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "proc_rtty_tx.hpp"
|
||||
#include "sine_table_int8.hpp"
|
||||
#include "event_m4.hpp"
|
||||
#include <algorithm>
|
||||
|
||||
static constexpr uint32_t LEAD_IN_SAMPLES = 204800;
|
||||
|
||||
static inline uint32_t hz_to_delta(int32_t hz, uint32_t fs) {
|
||||
int64_t delta = ((int64_t)hz * (int64_t)UINT32_MAX) / (int64_t)fs;
|
||||
return (uint32_t)delta;
|
||||
}
|
||||
|
||||
void RTTYTXProcessor::execute(const buffer_c8_t& buffer) {
|
||||
if (!configured) {
|
||||
for (size_t i = 0; i < buffer.count; i++) {
|
||||
buffer.p[i] = {0, 0};
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < buffer.count; i++) {
|
||||
bool advance = false;
|
||||
|
||||
if (state == State::LeadIn) {
|
||||
lead_counter++;
|
||||
if (lead_counter >= LEAD_IN_SAMPLES) {
|
||||
advance = true;
|
||||
}
|
||||
} else if (state == State::LeadOut) {
|
||||
lead_counter++;
|
||||
if (lead_counter >= 460000) {
|
||||
txprogress_message.done = true;
|
||||
shared_memory.application_queue.push(txprogress_message);
|
||||
configured = false;
|
||||
state = State::Idle;
|
||||
}
|
||||
} else if (state != State::Idle) {
|
||||
uint32_t previous_phase = baud_phase;
|
||||
baud_phase += baud_phase_increment;
|
||||
|
||||
if (baud_phase < previous_phase) {
|
||||
advance = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (advance) {
|
||||
advance_state();
|
||||
}
|
||||
|
||||
// Tone selection
|
||||
uint32_t target_delta;
|
||||
bool is_mark = true;
|
||||
|
||||
switch (state) {
|
||||
case State::StartBit:
|
||||
is_mark = false;
|
||||
break;
|
||||
case State::DataBits:
|
||||
is_mark = (current_char >> bit_pos) & 1;
|
||||
break;
|
||||
default:
|
||||
is_mark = true;
|
||||
break;
|
||||
}
|
||||
|
||||
target_delta = is_mark ? delta_mark : delta_space;
|
||||
|
||||
// Slew limiter
|
||||
int32_t diff = (int32_t)target_delta - (int32_t)current_delta;
|
||||
int32_t abs_diff = diff < 0 ? -diff : diff;
|
||||
|
||||
if (abs_diff <= (int32_t)slew_rate) {
|
||||
current_delta = target_delta;
|
||||
} else {
|
||||
current_delta += (diff > 0) ? slew_rate : -slew_rate;
|
||||
}
|
||||
|
||||
phase += current_delta;
|
||||
int8_t re = sine_table_i8[((phase + 0x40000000) & 0xFF000000) >> 24];
|
||||
int8_t im = sine_table_i8[(phase & 0xFF000000) >> 24];
|
||||
|
||||
buffer.p[i] = {re, im};
|
||||
}
|
||||
}
|
||||
|
||||
void RTTYTXProcessor::advance_state() {
|
||||
switch (state) {
|
||||
case State::Idle:
|
||||
state = State::LeadIn;
|
||||
lead_counter = 0;
|
||||
break;
|
||||
|
||||
case State::LeadIn:
|
||||
if (buffer_pop(current_char)) {
|
||||
state = State::StartBit;
|
||||
baud_phase = 0;
|
||||
baud_phase_increment = base_baud_phase_increment;
|
||||
} else {
|
||||
state = State::LeadOut;
|
||||
lead_counter = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case State::StartBit:
|
||||
state = State::DataBits;
|
||||
bit_pos = 0;
|
||||
break;
|
||||
|
||||
case State::DataBits:
|
||||
bit_pos++;
|
||||
if (bit_pos >= 5) {
|
||||
state = State::StopBit;
|
||||
}
|
||||
break;
|
||||
|
||||
case State::StopBit:
|
||||
if (buffer_pop(current_char)) {
|
||||
state = State::StartBit;
|
||||
} else {
|
||||
state = State::LeadOut;
|
||||
lead_counter = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case State::LeadOut:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void RTTYTXProcessor::configure(
|
||||
uint16_t baud,
|
||||
uint16_t shift,
|
||||
int16_t mark_tone_,
|
||||
int16_t space_tone_,
|
||||
uint8_t stop_bits_,
|
||||
bool inverted_) {
|
||||
if (baud == 0) return;
|
||||
|
||||
// baud phase increment
|
||||
uint32_t new_base_baud_inc = (uint32_t)((uint64_t)baud * UINT32_MAX / (baseband_fs * 100ULL));
|
||||
|
||||
// Stop bits
|
||||
configured_stop_bits = stop_bits_;
|
||||
if (configured_stop_bits < 2) configured_stop_bits = 2;
|
||||
|
||||
int32_t freq_mark = mark_tone_;
|
||||
int32_t freq_space = space_tone_;
|
||||
|
||||
if (inverted_) {
|
||||
std::swap(freq_mark, freq_space);
|
||||
}
|
||||
|
||||
uint32_t new_delta_mark = hz_to_delta(freq_mark, baseband_fs);
|
||||
uint32_t new_delta_space = hz_to_delta(freq_space, baseband_fs);
|
||||
|
||||
// Slew rate
|
||||
uint32_t samples_per_bit = (uint32_t)((uint64_t)UINT32_MAX / new_base_baud_inc);
|
||||
uint32_t transition_samples = samples_per_bit / 10;
|
||||
if (transition_samples == 0) transition_samples = 1;
|
||||
|
||||
uint32_t shift_delta = hz_to_delta(shift, baseband_fs);
|
||||
uint32_t new_slew_rate = shift_delta / transition_samples;
|
||||
if (new_slew_rate == 0) new_slew_rate = 1;
|
||||
|
||||
base_baud_phase_increment = new_base_baud_inc;
|
||||
delta_mark = new_delta_mark;
|
||||
delta_space = new_delta_space;
|
||||
slew_rate = new_slew_rate;
|
||||
|
||||
if (!configured) {
|
||||
current_delta = delta_mark;
|
||||
lead_counter = 0;
|
||||
phase = 0;
|
||||
baud_phase = 0;
|
||||
baud_phase_increment = base_baud_phase_increment;
|
||||
configured = true;
|
||||
}
|
||||
}
|
||||
|
||||
void RTTYTXProcessor::on_message(const Message* const msg) {
|
||||
if (msg->id == Message::ID::RTTYData) {
|
||||
const auto& rtty_msg = *reinterpret_cast<const RTTYDataMessage*>(msg);
|
||||
configure(rtty_msg.baud,
|
||||
rtty_msg.shift,
|
||||
rtty_msg.mark_tone,
|
||||
rtty_msg.space_tone,
|
||||
rtty_msg.stopbits,
|
||||
rtty_msg.inverted);
|
||||
|
||||
for (int i = 0; i < 15; i++) {
|
||||
buffer_push(0x1F); // LTRS
|
||||
}
|
||||
|
||||
buffer_push(0x08); // CR
|
||||
buffer_push(0x02); // LF
|
||||
|
||||
for (uint16_t i = 0; i < rtty_msg.data_len && i < rtty_msg.max_len; i++) {
|
||||
buffer_push(rtty_msg.data[i]);
|
||||
}
|
||||
buffer_push(0x08); // CR
|
||||
buffer_push(0x02); // LF
|
||||
|
||||
if (state == State::Idle) {
|
||||
state = State::LeadIn;
|
||||
lead_counter = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ring Buffer Logic
|
||||
bool RTTYTXProcessor::buffer_push(uint8_t byte) {
|
||||
size_t next_head = (head + 1) % data_buffer.size();
|
||||
if (next_head == tail) return false;
|
||||
data_buffer[head] = byte;
|
||||
head = next_head;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RTTYTXProcessor::buffer_pop(uint8_t& byte) {
|
||||
if (head == tail) return false;
|
||||
byte = data_buffer[tail];
|
||||
tail = (tail + 1) % data_buffer.size();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RTTYTXProcessor::buffer_empty() const {
|
||||
return head == tail;
|
||||
}
|
||||
|
||||
int main() {
|
||||
EventDispatcher event_dispatcher{std::make_unique<RTTYTXProcessor>()};
|
||||
event_dispatcher.run();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright (C) 2026 HTotoo
|
||||
*
|
||||
* 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_RTTY_TX_H__
|
||||
#define __PROC_RTTY_TX_H__
|
||||
|
||||
#include "baseband_processor.hpp"
|
||||
#include "baseband_thread.hpp"
|
||||
#include "portapack_shared_memory.hpp"
|
||||
#include <array>
|
||||
|
||||
class RTTYTXProcessor : public BasebandProcessor {
|
||||
public:
|
||||
void execute(const buffer_c8_t& buffer) override;
|
||||
void on_message(const Message* const msg) override;
|
||||
|
||||
private:
|
||||
static constexpr uint32_t baseband_fs = 2048000;
|
||||
|
||||
bool configured{false};
|
||||
|
||||
// RTTY Configuration
|
||||
uint32_t samples_per_bit{0};
|
||||
|
||||
// Stop bits configuration: 2=1.0, 3=1.5, 4=2.0
|
||||
uint8_t configured_stop_bits{2};
|
||||
|
||||
// FSK State
|
||||
uint32_t delta_mark{0}; // Phase step for Mark
|
||||
uint32_t delta_space{0}; // Phase step for Space
|
||||
uint32_t current_delta{0}; // Smoothed delta
|
||||
uint32_t slew_rate{0}; // Max change per sample
|
||||
uint32_t phase{0}; // Phase accumulator
|
||||
|
||||
// Precision Timing
|
||||
uint32_t baud_phase{0};
|
||||
uint32_t baud_phase_increment{0};
|
||||
uint32_t base_baud_phase_increment{0}; // Store the standard 1.0 bit rate
|
||||
|
||||
// State Machine
|
||||
enum class State {
|
||||
Idle,
|
||||
LeadIn,
|
||||
StartBit,
|
||||
DataBits,
|
||||
StopBit,
|
||||
LeadOut
|
||||
};
|
||||
State state{State::Idle};
|
||||
|
||||
uint32_t lead_counter{0};
|
||||
uint8_t current_char{0};
|
||||
uint8_t bit_pos{0};
|
||||
|
||||
// Ring Buffer
|
||||
std::array<uint8_t, 1024> data_buffer{};
|
||||
volatile size_t head{0};
|
||||
volatile size_t tail{0};
|
||||
|
||||
void configure(uint16_t baud, uint16_t shift, int16_t mark_tone_, int16_t space_tone_, uint8_t stop_bits_, bool inverted_);
|
||||
void advance_state();
|
||||
|
||||
bool buffer_push(uint8_t byte);
|
||||
bool buffer_pop(uint8_t& byte);
|
||||
bool buffer_empty() const;
|
||||
|
||||
TXProgressMessage txprogress_message{};
|
||||
|
||||
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Transmit};
|
||||
};
|
||||
|
||||
#endif /* __PROC_RTTY_TX_H__ */
|
||||
@@ -61,7 +61,11 @@ const PALConfig pal_default_config = {
|
||||
.P = {
|
||||
{ // GPIO0
|
||||
.data
|
||||
#ifdef PRALINE
|
||||
= (0 << 15) // P1_20: CLKIN_CTRL - start low
|
||||
#else
|
||||
= (1 << 15) // P1_20: CS_XCVR
|
||||
#endif
|
||||
| (1 << 14) // P2_10: AMP_BYPASS
|
||||
| (0 << 13) // P1_18: SGPIO12, HOST_Q_INVERT
|
||||
| (0 << 12) // P1_17: SGPIO11, HOST_DIRECTION
|
||||
@@ -79,8 +83,12 @@ const PALConfig pal_default_config = {
|
||||
| (1 << 0) // P0_0: SGPIO0, HOST_DATA0
|
||||
,
|
||||
.dir
|
||||
#ifdef PRALINE
|
||||
= (1 << 15) // P1_20: CLKIN_CTRL - output
|
||||
#else
|
||||
= (1 << 15) // P1_20: CS_XCVR
|
||||
| (1 << 14) // P2_10: AMP_BYPASS
|
||||
#endif
|
||||
| (1 << 14) // P2_10: AMP_BYPASS
|
||||
| (1 << 13) // P1_18: SGPIO12, HOST_Q_INVERT
|
||||
| (0 << 12) // P1_17: SGPIO11, HOST_DIRECTION
|
||||
| (0 << 11) // P1_4: SSP1_MOSI
|
||||
@@ -137,7 +145,11 @@ const PALConfig pal_default_config = {
|
||||
.data
|
||||
= (0 << 15) // P5_6: TX_AMP
|
||||
| (1 << 14) // P5_5: MIXER_RESETX, 10K PU
|
||||
#ifdef PRALINE
|
||||
| (0 << 13) // P5_4: MIXER_ENX, 10K PU
|
||||
#else
|
||||
| (1 << 13) // P5_4: MIXER_ENX, 10K PU
|
||||
#endif
|
||||
| (1 << 12) // P5_3: RX_MIX_BP
|
||||
| (0 << 11) // P5_2: TX_MIX_BP
|
||||
| (0 << 10) // P5_1: LP
|
||||
@@ -154,11 +166,19 @@ const PALConfig pal_default_config = {
|
||||
,
|
||||
.dir
|
||||
= (1 << 15) // P5_6: TX_AMP
|
||||
| (1 << 14) // P5_5: MIXER_RESETX, 10K PU
|
||||
| (1 << 13) // P5_4: MIXER_ENX, 10K PU
|
||||
#ifdef PRALINE
|
||||
| (0 << 14) // P5_5: MIXER_RESETX, 10K PU
|
||||
| (1 << 13) // P5_4: MIXER_ENX - OUTPUT (MCU controlled)
|
||||
| (0 << 12) // P5_3: RX_MIX_BP - unused on PRALINE
|
||||
| (0 << 11) // P5_2: FPGA_CRESET - INPUT initially
|
||||
| (0 << 10) // P5_1: FPGA_SPI_CS - INPUT initially
|
||||
#else
|
||||
| (1 << 14) // P5_5: MIXER_RESETX, 10K PU
|
||||
| (1 << 13) // P5_4: MIXER_ENX, 10K PU
|
||||
| (1 << 12) // P5_3: RX_MIX_BP
|
||||
| (1 << 11) // P5_2: TX_MIX_BP
|
||||
| (1 << 10) // P5_1: LP
|
||||
#endif
|
||||
| (0 << 9) // P5_0: Varies by revision, float until detection
|
||||
| (1 << 8) // P6_12: LED3 (TX)
|
||||
| (1 << 7) // P5_7: CS_AD
|
||||
@@ -306,7 +326,13 @@ const PALConfig pal_default_config = {
|
||||
{ 1, 7, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* !MIX_BYPASS/P35: U1.VCTL1(I), U11.VCTL2(I), U9.V2(I) */
|
||||
{ 1, 19, scu_config_normal_drive_t { .mode=1, .epd=0, .epun=0, .ehs=0, .ezi=0, .zif=0 } }, /* SSP1_SCK/P39: MAX2837.SCLK(I), MAX5864.SCLK(I) */
|
||||
{ 1, 20, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* CS_XCVR/P53: MAX2837.CS(I) */
|
||||
|
||||
#ifdef PRALINE
|
||||
{ 2, 6, scu_config_normal_drive_t { .mode=4, .epd=0, .epun=1, .ehs=1, .ezi=0, .zif=1 } }, /* TRIGGER_OUT / MIX_EN_N_R1_0: GPIO5[6] - PRALINE */
|
||||
#else
|
||||
/* HackRF One RFFC5072 SPI pins */
|
||||
{ 2, 6, scu_config_normal_drive_t { .mode=4, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* MIXER_SCLK/P31: 33pF, RFFC5072.SCLK(I) */
|
||||
#endif
|
||||
{ 2, 10, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* AMP_BYPASS/P50: U14.V2(I), U12.V2(I) */
|
||||
{ 2, 11, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* RX_AMP/P49: U12.V1(I), U14.V3(I) */
|
||||
{ 2, 12, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* !RX_AMP_PWR/P52: 10K PU, Q1.G(I), power to U13 (RX amp) */
|
||||
@@ -319,8 +345,16 @@ const PALConfig pal_default_config = {
|
||||
{ 5, 4, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* MIXER_ENX/P32: 10K PU, 33pF, RFFC5072.ENX(I) */
|
||||
{ 5, 5, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* MIXER_RESETX/P33: 10K PU, 33pF, RFFC5072.RESETX(I) */
|
||||
{ 5, 6, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* TX_AMP/P48: U12.V3(I), U14.V1(I) */
|
||||
#ifdef PRALINE
|
||||
/* PRALINE RFFC5072 SPI pins - different from HackRF One */
|
||||
{ 5, 7, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* P5_7: GPIO2[7], RFFC5072 CS */
|
||||
{ 9, 5, scu_config_normal_drive_t { .mode=4, .epd=0, .epun=0, .ehs=1, .ezi=0, .zif=0 } }, /* P9_5: GPIO5[18], RFFC5072 SCLK */
|
||||
{ 9, 2, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=0, .ehs=1, .ezi=1, .zif=0 } }, /* P9_2: GPIO4[14], RFFC5072 SDATA (bidirectional) */
|
||||
#else
|
||||
/* HackRF One RFFC5072 SPI pins - NOT used on PRALINE */
|
||||
{ 5, 7, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* CS_AD/P54: MAX5864.CS(I) */
|
||||
{ 6, 4, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=0, .ehs=0, .ezi=1, .zif=0 } }, /* MIXER_SDATA/P27: 33pF, RFFC5072.SDATA(IO) */
|
||||
#endif
|
||||
{ 6, 8, scu_config_normal_drive_t { .mode=4, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* MIX_BYPASS/P34: U1.VCTL2(I), U11.VCTL1(I) */
|
||||
{ 6, 9, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* !TX_AMP_PWR/P51: 10K PU, Q2.G(I), power to U25 (TX amp) */
|
||||
|
||||
@@ -346,7 +380,9 @@ const PALConfig pal_default_config = {
|
||||
{ 6, 1, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* CPLD_TCK: PortaPack CPLD.TCK(I) */
|
||||
{ 6, 2, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=1, .zif=0 } }, /* CPLD_TDI: PortaPack CPLD.TDI(I), I2S0_RX_SDA(O) */
|
||||
{ 6, 5, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* CPLD_TMS: HackRF CPLD.TMS(I) */
|
||||
#ifndef PRALINE
|
||||
{ 9, 5, scu_config_normal_drive_t { .mode=4, .epd=0, .epun=0, .ehs=0, .ezi=1, .zif=0 } }, /* CPLD_TDO: HackRF CPLD.TDO(O) */
|
||||
#endif
|
||||
|
||||
/* PortaPack CPLD */
|
||||
{ 1, 5, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=0, .ehs=0, .ezi=1, .zif=0 } }, /* SD_POW: PortaPack CPLD.TDO(O) */
|
||||
@@ -870,40 +906,55 @@ extern "C" void boardInit(void) {
|
||||
LPC_SCU->SFSP[6][7] = 0xF4; /* SCU_GPIO_FAST | FUNCTION4 */
|
||||
LPC_GPIO->DIR[5] |= (1 << 15);
|
||||
LPC_GPIO->CLR[5] = (1 << 15); /* Clear = enable 3.3V aux */
|
||||
{ volatile uint32_t delay = 100000; while(delay--); }
|
||||
{ volatile uint32_t delay = 200000; while(delay--); }
|
||||
|
||||
/* Enable 1.2V for FPGA - P8_7 = GPIO4[7], active high */
|
||||
LPC_SCU->SFSP[8][7] = 0x10;
|
||||
LPC_GPIO->DIR[4] |= (1 << 7);
|
||||
LPC_GPIO->SET[4] = (1 << 7);
|
||||
{ volatile uint32_t delay = 100000; while(delay--); }
|
||||
{ volatile uint32_t delay = 200000; while(delay--); }
|
||||
|
||||
/* Enable VAA for RF - P8_1 = GPIO4[1], active low */
|
||||
LPC_SCU->SFSP[8][1] = 0x10;
|
||||
LPC_GPIO->DIR[4] |= (1 << 1);
|
||||
LPC_GPIO->CLR[4] = (1 << 1);
|
||||
{ volatile uint32_t delay = 100000; while(delay--); }
|
||||
{ volatile uint32_t delay = 200000; while(delay--); }
|
||||
|
||||
/* Configure RFFC5072 pins for PRALINE */
|
||||
/* P9_2 = GPIO4[14] RFFC5072 data (SCU_GPIO_FAST | FUNCTION0 = 0xF0) */
|
||||
LPC_SCU->SFSP[9][2] = 0xF0;
|
||||
/* P9_5 = GPIO5[18] RFFC5072 clock (SCU_GPIO_FAST | FUNCTION4 = 0xF4) */
|
||||
LPC_SCU->SFSP[9][5] = 0xF4;
|
||||
LPC_GPIO->DIR[5] |= (1 << 18); /* Clock as output */
|
||||
/* Set GPIO directions for RFFC5072 SPI pins */
|
||||
/* P5_7 = GPIO2[7] RFFC5072 CS */
|
||||
LPC_GPIO->SET[2] = (1 << 7); /* CS high (deselected) */
|
||||
LPC_GPIO->DIR[2] |= (1 << 7); /* CS as output */
|
||||
|
||||
/* P9_5 = GPIO5[18] RFFC5072 SCLK */
|
||||
LPC_GPIO->CLR[5] = (1 << 18); /* CLK low */
|
||||
LPC_GPIO->DIR[5] |= (1 << 18); /* CLK as output */
|
||||
|
||||
/* P9_2 = GPIO4[14] RFFC5072 DATA (bidirectional) */
|
||||
LPC_GPIO->DIR[4] |= (1 << 14); /* DATA as output initially */
|
||||
|
||||
/* P2_6 = GPIO5[6] TRIGGER_OUT / MIX_EN_N_R1_0 (PRALINE R1.0 mixer bypass) */
|
||||
/* SCU configured in PAL array above with mode=4 */
|
||||
LPC_GPIO->CLR[5] = (1 << 6); /* Default low (mixer enabled) */
|
||||
LPC_GPIO->DIR[5] |= (1 << 6); /* Output */
|
||||
{ volatile uint32_t delay = 200000; while(delay--); }
|
||||
|
||||
/* Configure Port D pins for PRALINE (use SFSPD registers) */
|
||||
/* PD_14 = GPIO6[28] MAX2831 chip select */
|
||||
LPC_SCU->SFSPD[14] = 0xF4; /* SCU_GPIO_FAST | FUNCTION4 */
|
||||
LPC_GPIO->SET[6] = (1 << 28); /* CS high (inactive) */
|
||||
LPC_GPIO->DIR[6] |= (1 << 28); /* Output */
|
||||
|
||||
/* PD_15 = GPIO6[29] MAX2831 RXHP control */
|
||||
LPC_SCU->SFSPD[15] = 0xF4; /* SCU_GPIO_FAST | FUNCTION4 */
|
||||
LPC_GPIO->CLR[6] = (1 << 29); /* RXHP low = 100 Hz HPF */
|
||||
LPC_GPIO->DIR[6] |= (1 << 29); /* Output */
|
||||
|
||||
/* PD_16 = GPIO6[30] MAX5864 chip select */
|
||||
LPC_SCU->SFSPD[16] = 0xF4; /* SCU_GPIO_FAST | FUNCTION4 */
|
||||
LPC_GPIO->SET[6] |= (1 << 30); /* CS high (inactive) */
|
||||
LPC_GPIO->DIR[6] |= (1 << 30); /* Output */
|
||||
{ volatile uint32_t delay = 200000; while(delay--); }
|
||||
|
||||
/* Configure Port E pins for MAX2831 control (use SFSPE registers) */
|
||||
/* PE_1 = GPIO7[1] MAX2831 ENABLE */
|
||||
@@ -914,6 +965,7 @@ extern "C" void boardInit(void) {
|
||||
LPC_SCU->SFSPE[2] = 0xF4; /* SCU_GPIO_FAST | FUNCTION4 */
|
||||
LPC_GPIO->CLR[7] = (1 << 2); /* Start in shutdown mode */
|
||||
LPC_GPIO->DIR[7] |= (1 << 2); /* Output */
|
||||
{ volatile uint32_t delay = 200000; while(delay--); }
|
||||
|
||||
/* Configure Port 6 pins for RF path control */
|
||||
/* P6_3 = GPIO3[2] Mixer enable (inverted: 0 = mixer ON) */
|
||||
@@ -924,16 +976,41 @@ extern "C" void boardInit(void) {
|
||||
LPC_SCU->SFSP[6][5] = 0xF0; /* SCU_GPIO_FAST | FUNCTION0 */
|
||||
LPC_GPIO->CLR[3] = (1 << 4); /* TX off by default (RX mode) */
|
||||
LPC_GPIO->DIR[3] |= (1 << 4); /* Output */
|
||||
{ volatile uint32_t delay = 200000; while(delay--); }
|
||||
|
||||
/* Configure Port A pins for RF path control */
|
||||
/* PA_1 = GPIO4[8] LPF enable */
|
||||
LPC_SCU->SFSP[0xA][1] = 0xF4; /* SCU_GPIO_FAST | FUNCTION4 */
|
||||
LPC_SCU->SFSP[0xA][1] = 0xF0; /* SCU_GPIO_FAST | FUNCTION0 */
|
||||
LPC_GPIO->SET[4] = (1 << 8); /* LPF enabled by default (low band) */
|
||||
LPC_GPIO->DIR[4] |= (1 << 8); /* Output */
|
||||
/* PA_2 = GPIO4[9] RF amp enable */
|
||||
LPC_SCU->SFSP[0xA][2] = 0xF4; /* SCU_GPIO_FAST | FUNCTION4 */
|
||||
LPC_SCU->SFSP[0xA][2] = 0xF0; /* SCU_GPIO_FAST | FUNCTION0 */
|
||||
LPC_GPIO->CLR[4] = (1 << 9); /* RF amp off by default */
|
||||
LPC_GPIO->DIR[4] |= (1 << 9); /* Output */
|
||||
{ volatile uint32_t delay = 200000; while(delay--); }
|
||||
|
||||
/* Configure RFFC5072 control pins for PRALINE */
|
||||
/* P5_4 = GPIO2[13] RFFC5072 ENX - SPI chip select (managed by SPI driver) */
|
||||
LPC_SCU->SFSP[5][4] = 0x10; /* FUNCTION0 (GPIO), pull-up, slow mode (matches HackRF USB) */
|
||||
LPC_GPIO->DIR[2] |= (1 << 13); /* ENX: OUTPUT */
|
||||
LPC_GPIO->SET[2] = (1 << 13); /* ENX = 1 (deselected initially) */
|
||||
{ volatile uint32_t delay = 200000; while(delay--); }
|
||||
|
||||
/* P5_5 = GPIO2[14] RFFC5072 RESETX (active high: 1=running) */
|
||||
LPC_SCU->SFSP[5][5] = 0x10; /* FUNCTION0 (GPIO), pull-up, slow mode (matches HackRF USB) */
|
||||
LPC_GPIO->DIR[2] |= (1 << 14); /* RESETX: OUTPUT */
|
||||
LPC_GPIO->SET[2] = (1 << 14); /* RESETX = 1 (RUNNING) */
|
||||
{ volatile uint32_t delay = 200000; while(delay--); }
|
||||
|
||||
/* Ensure RESETX is stable */
|
||||
for (volatile int i = 0; i < 10; i++) {
|
||||
LPC_GPIO->W2[14] = 1;
|
||||
}
|
||||
|
||||
/* PD_11 = GPIO6[25] RFFC5072 Lock Detect (input) */
|
||||
LPC_SCU->SFSPD[11] = 0x10; /* FUNCTION0 (GPIO), pull-up (matches HackRF USB) */
|
||||
LPC_GPIO->DIR[6] &= ~(1 << 25); /* LD: INPUT */
|
||||
{ volatile uint32_t delay = 200000; while(delay--); }
|
||||
|
||||
/* Configure PRALINE-specific SGPIO pins for FPGA sample interface.
|
||||
* These override the HackRF One pin config from pins_setup.
|
||||
@@ -953,6 +1030,7 @@ extern "C" void boardInit(void) {
|
||||
LPC_SCU->SFSP[8][2] = 0xF4; /* SCU_GPIO_FAST | func 4 */
|
||||
/* SGPIO11 = P1_17 function 6 (HOST_DIRECTION - output to FPGA, tells FPGA TX vs RX) */
|
||||
LPC_SCU->SFSP[1][17] = 0xF6; /* SCU_GPIO_FAST | func 6 */
|
||||
{ volatile uint32_t delay = 200000; while(delay--); }
|
||||
|
||||
/* SGPIO data pins (SGPIO0-7) - all 8 bits required for sample data */
|
||||
LPC_SCU->SFSP[0][0] = 0xF3; /* SGPIO0: P0_0 function 3, HOST_DATA0 */
|
||||
@@ -963,9 +1041,7 @@ extern "C" void boardInit(void) {
|
||||
LPC_SCU->SFSP[6][6] = 0xF2; /* SGPIO5: P6_6 function 2, HOST_DATA5 */
|
||||
LPC_SCU->SFSP[2][2] = 0xF0; /* SGPIO6: P2_2 function 0, HOST_DATA6 */
|
||||
LPC_SCU->SFSP[1][0] = 0xF6; /* SGPIO7: P1_0 function 6, HOST_DATA7 */
|
||||
|
||||
/* NOTE: P9_5 is RFFC5072 mixer clock (SCU_MIXER_SCLK), NOT SGPIO!
|
||||
* Do NOT override P9_5 here. */
|
||||
{ volatile uint32_t delay = 200000; while(delay--); }
|
||||
|
||||
// Trigger FPGA bitstream loading via fpga bridge
|
||||
// Attempt to load the FPGA bitstream
|
||||
@@ -979,11 +1055,13 @@ extern "C" void boardInit(void) {
|
||||
// Turn off all LEDs to start
|
||||
// PRALINE LEDs are active-low: SET (HIGH) = OFF, CLR (LOW) = ON
|
||||
LPC_GPIO->SET[2] = (1 << 1) | (1 << 2) | (1 << 8);
|
||||
{ volatile uint32_t delay = 200000; while(delay--); }
|
||||
|
||||
// Call fpga_bridge_init and continue boot regardless of result
|
||||
// (Watchdog was resetting device when we halted with while(1))
|
||||
int load_result = fpga_bridge_init();
|
||||
(void)load_result; // Ignore result for now, just let boot continue
|
||||
{ volatile uint32_t delay = 200000; while(delay--); }
|
||||
|
||||
// Keep LEDs off after FPGA load
|
||||
LPC_GPIO->SET[2] = (1 << 1) | (1 << 2) | (1 << 8);
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
// Check if PRALINE was passed from CMake
|
||||
#ifdef PRALINE
|
||||
#warning "Building for HackRF_PRO with FPGA."
|
||||
|
||||
// Necessary headers
|
||||
#include "lz4_blk.h"
|
||||
@@ -297,7 +296,6 @@
|
||||
|
||||
// Cached register values for debug reads (since reads may require mode switch)
|
||||
static uint8_t fpga_reg_cache[6] = {0, 0x01, 0x00, 0x00, 0x00, 0x00};
|
||||
static uint8_t fpga_reg_cache_valid = 0;
|
||||
|
||||
// Public function to read FPGA register (callable from C++ application code)
|
||||
// Switches SPI mode, reads register, switches back
|
||||
|
||||
@@ -213,6 +213,24 @@ CpldUpdateStatus update_autodetect(const Config config_rev_20150901, const Confi
|
||||
namespace hackrf {
|
||||
namespace cpld {
|
||||
|
||||
#ifdef PRALINE
|
||||
|
||||
/* PRALINE has no HackRF CPLD - stub these functions out */
|
||||
bool load_sram() {
|
||||
return true;
|
||||
}
|
||||
void load_sram_no_verify() {
|
||||
return;
|
||||
}
|
||||
bool verify_eeprom() {
|
||||
return true;
|
||||
}
|
||||
void init_from_eeprom() {
|
||||
return;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
static jtag::GPIOTarget jtag_target_hackrf() {
|
||||
return {
|
||||
hackrf::one::gpio_cpld_tck,
|
||||
@@ -262,5 +280,7 @@ void init_from_eeprom() {
|
||||
hackrf_cpld.init_from_eeprom();
|
||||
}
|
||||
|
||||
#endif // PRALINE
|
||||
|
||||
} /* namespace cpld */
|
||||
} /* namespace hackrf */
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user