Initial commit

This commit is contained in:
2026-01-17 09:53:08 +02:00
commit 159633e837
148 changed files with 42795 additions and 0 deletions

9
apps/rx/CMakeLists.txt Normal file
View File

@@ -0,0 +1,9 @@
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
idf_build_set_property(MINIMAL_BUILD ON)
set(EXTRA_COMPONENT_DIRS "${CMAKE_CURRENT_LIST_DIR}/../../components")
set(COMPONENTS main lora_radio common input ui)
project(lora-rx)

10
apps/rx/dependencies.lock Normal file
View File

@@ -0,0 +1,10 @@
dependencies:
idf:
source:
type: idf
version: 5.5.1
direct_dependencies:
- idf
manifest_hash: 9db7a265ef57175d265e0d6eb7847107508a68a5847e4adbd1375406a7c73c51
target: esp32s3
version: 2.0.0

View File

@@ -0,0 +1,5 @@
idf_component_register(
SRCS "rx_main.c"
INCLUDE_DIRS "."
REQUIRES common lora_radio input ui usb_api
)

448
apps/rx/main/rx_main.c Normal file
View File

@@ -0,0 +1,448 @@
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_system.h"
#include "sdkconfig.h"
#include "soc/rtc_cntl_reg.h"
#include "common.h"
#include "input.h"
#include "lora_radio.h"
#include "ui.h"
#include "usb_api.h"
#include <strings.h>
static const char *TAG = "lora_rx";
typedef enum {
FIELD_FREQ = 0,
FIELD_BW,
FIELD_SF,
FIELD_CR,
FIELD_COUNT
} field_t;
typedef enum {
SCREEN_FREQ = 0,
SCREEN_BAND,
SCREEN_BW,
SCREEN_SF,
SCREEN_CR,
SCREEN_PAYLOAD,
SCREEN_BOOT,
SCREEN_COUNT
} screen_t;
static lora_params_t s_params;
static field_t s_field = FIELD_FREQ;
static screen_t s_screen = SCREEN_FREQ;
static const char *s_screen_icon[SCREEN_COUNT] = {"ƒ", "BND", "BW", "SF", "CR", "PKT", "BL"};
static const char *s_screen_label[SCREEN_COUNT] = {"FREQ", "BAND", "BANDW", "SPREAD", "CODERATE", "PAYLOAD", "BOOT"};
static const int s_bw_options[] = {125, 250, 500};
typedef struct {
const char *name;
int min_cmhz;
int max_cmhz;
int default_cmhz;
} band_t;
static const band_t s_bands[] = {
{"430", 43000, 44000, 43300},
{"868", 86300, 87000, 86800},
{"915", 90200, 92800, 91500},
{"L", 152500, 166000, 155000}, // 1.525-1.660 GHz
{"S", 190000, 210000, 200000}, // 1.9-2.1 GHz
{"2.4G", 240000, 248350, 244200}, // 2.4 GHz ISM (2400-2483.5 MHz)
};
static int s_band_idx = 0;
// --- JSON helpers for USB control -------------------------------------------------
static void sanitize_json_string(const char *in, char *out, size_t out_len)
{
if (!out || out_len == 0) {
return;
}
size_t w = 0;
for (size_t i = 0; in && in[i] != '\0' && w + 1 < out_len; i++) {
char c = in[i];
if (c == '\\' || c == '"' || (unsigned char)c < 0x20) {
out[w++] = ' ';
} else {
out[w++] = c;
}
}
out[w] = '\0';
}
static bool parse_int_field(const char *json, const char *key, int *out)
{
if (!json || !key || !out) {
return false;
}
char pattern[32];
snprintf(pattern, sizeof(pattern), "\"%s\"", key);
const char *p = strstr(json, pattern);
if (!p) return false;
p = strchr(p, ':');
if (!p) return false;
p++;
int val = 0;
if (sscanf(p, " %d", &val) == 1) {
*out = val;
return true;
}
return false;
}
static bool parse_string_field(const char *json, const char *key, char *out, size_t out_len)
{
if (!json || !key || !out || out_len == 0) {
return false;
}
char pattern[32];
snprintf(pattern, sizeof(pattern), "\"%s\"", key);
const char *p = strstr(json, pattern);
if (!p) return false;
p = strchr(p, ':');
if (!p) return false;
const char *q = strchr(p, '"');
if (!q) return false;
q++; // after quote
const char *end = strchr(q, '"');
if (!end) return false;
size_t len = (size_t)(end - q);
if (len >= out_len) len = out_len - 1;
memcpy(out, q, len);
out[len] = '\0';
return true;
}
static int find_band_idx(const char *name)
{
if (!name) return -1;
for (size_t i = 0; i < sizeof(s_bands)/sizeof(s_bands[0]); i++) {
if (strcasecmp(name, s_bands[i].name) == 0) {
return (int)i;
}
}
return -1;
}
static int detect_band_from_freq(int cmhz)
{
for (size_t i = 0; i < sizeof(s_bands) / sizeof(s_bands[0]); i++) {
if (cmhz >= s_bands[i].min_cmhz && cmhz <= s_bands[i].max_cmhz) {
return (int)i;
}
}
return 0;
}
static void clamp_freq_to_band(void)
{
const band_t *b = &s_bands[s_band_idx];
if (s_params.freq_centi_mhz < b->min_cmhz) s_params.freq_centi_mhz = b->min_cmhz;
if (s_params.freq_centi_mhz > b->max_cmhz) s_params.freq_centi_mhz = b->max_cmhz;
// Нормалізуємо до цілих MHz
s_params.freq_centi_mhz = ((s_params.freq_centi_mhz + 50) / 100) * 100;
}
static void usb_send_status(void)
{
lora_metrics_t metrics = {0};
char payload[48];
char safe_payload[48];
lora_radio_get_metrics(&metrics);
lora_radio_get_last_payload(payload, sizeof(payload));
sanitize_json_string(payload, safe_payload, sizeof(safe_payload));
char line[256];
snprintf(line, sizeof(line),
"{\"resp\":\"status\",\"role\":\"rx\",\"freq_mhz\":%d,\"bw_khz\":%d,\"sf\":%d,\"cr\":%d,"
"\"band\":\"%s\",\"snr_db\":%d,\"rssi_dbm\":%d,\"last_status\":%u,\"payload\":\"%s\"}\n",
s_params.freq_centi_mhz / 100,
s_params.bw_khz,
s_params.sf,
s_params.cr,
s_bands[s_band_idx].name,
metrics.snr_db,
metrics.rssi_dbm,
(unsigned)metrics.last_status,
safe_payload);
usb_api_send_line(line);
}
static void usb_handle_set_params(const char *json)
{
bool changed = false;
lora_params_t next = s_params;
int band_idx = -1;
char band_name[16] = {0};
if (parse_string_field(json, "band", band_name, sizeof(band_name))) {
band_idx = find_band_idx(band_name);
if (band_idx >= 0) {
s_band_idx = band_idx;
changed = true;
}
}
int val = 0;
if (parse_int_field(json, "freq_mhz", &val)) {
next.freq_centi_mhz = val * 100;
changed = true;
}
if (parse_int_field(json, "bw_khz", &val)) {
next.bw_khz = val;
changed = true;
}
if (parse_int_field(json, "sf", &val)) {
next.sf = val;
changed = true;
}
if (parse_int_field(json, "cr", &val)) {
next.cr = val;
changed = true;
}
if (!changed) {
return;
}
s_params = next;
clamp_freq_to_band();
esp_err_t err = lora_radio_apply_params(&s_params);
if (err != ESP_OK) {
ESP_LOGE(TAG, "USB set_params failed: %s", esp_err_to_name(err));
} else {
ESP_LOGI(TAG, "USB set_params applied");
}
}
static void usb_handle_line(const char *line)
{
if (!line || line[0] == '\0') {
return;
}
ESP_LOGI(TAG, "USB RX: %s", line);
if (strstr(line, "set_params")) {
usb_handle_set_params(line);
return;
}
if (strstr(line, "get_status")) {
usb_send_status();
return;
}
if (strstr(line, "reboot_bootloader")) {
ui_show_status("Bootloader", "Rebooting to USB", "Connect USB", NULL, NULL, NULL);
vTaskDelay(pdMS_TO_TICKS(200));
REG_SET_BIT(RTC_CNTL_OPTIONS0_REG, RTC_CNTL_FORCE_DOWNLOAD_BOOT);
esp_restart();
}
}
static void set_band(int idx)
{
int max_idx = (int)(sizeof(s_bands) / sizeof(s_bands[0])) - 1;
if (idx < 0) idx = 0;
if (idx > max_idx) idx = max_idx;
s_band_idx = idx;
s_params.freq_centi_mhz = s_bands[s_band_idx].default_cmhz;
clamp_freq_to_band();
esp_err_t err = lora_radio_apply_params(&s_params);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Apply params failed: %s", esp_err_to_name(err));
}
}
static void bump_field(int delta)
{
switch (s_field) {
case FIELD_FREQ:
s_params.freq_centi_mhz += delta * 100; // крок 1 MHz
clamp_freq_to_band();
break;
case FIELD_BW: {
int idx = 0;
for (size_t i = 0; i < sizeof(s_bw_options)/sizeof(s_bw_options[0]); i++) {
if (s_bw_options[i] == s_params.bw_khz) {
idx = i;
break;
}
}
idx += delta;
if (idx < 0) idx = 0;
if (idx >= (int)(sizeof(s_bw_options)/sizeof(s_bw_options[0]))) idx = (int)(sizeof(s_bw_options)/sizeof(s_bw_options[0])) - 1;
s_params.bw_khz = s_bw_options[idx];
break;
}
case FIELD_SF:
s_params.sf += delta;
if (s_params.sf < 5) s_params.sf = 5;
if (s_params.sf > 12) s_params.sf = 12;
break;
case FIELD_CR:
s_params.cr += delta;
if (s_params.cr < 5) s_params.cr = 5;
if (s_params.cr > 8) s_params.cr = 8;
break;
default:
break;
}
esp_err_t err = lora_radio_apply_params(&s_params);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Apply params failed: %s", esp_err_to_name(err));
}
}
static void next_screen(int delta)
{
int idx = (int)s_screen + delta;
if (idx < 0) idx = SCREEN_COUNT - 1;
if (idx >= SCREEN_COUNT) idx = 0;
s_screen = (screen_t)idx;
switch (s_screen) {
case SCREEN_FREQ: s_field = FIELD_FREQ; break;
case SCREEN_BAND: s_field = FIELD_FREQ; break;
case SCREEN_BW: s_field = FIELD_BW; break;
case SCREEN_SF: s_field = FIELD_SF; break;
case SCREEN_CR: s_field = FIELD_CR; break;
default: break;
}
}
static void adjust_current(int delta)
{
switch (s_screen) {
case SCREEN_FREQ: s_field = FIELD_FREQ; bump_field(delta); break;
case SCREEN_BAND: set_band(s_band_idx + delta); break;
case SCREEN_BW: s_field = FIELD_BW; bump_field(delta); break;
case SCREEN_SF: s_field = FIELD_SF; bump_field(delta); break;
case SCREEN_CR: s_field = FIELD_CR; bump_field(delta); break;
case SCREEN_PAYLOAD:
case SCREEN_BOOT:
default:
break;
}
}
static void app_loop(void)
{
lora_metrics_t metrics = {0};
while (true) {
input_event_t evt = input_poll();
switch (evt) {
case INPUT_LEFT: next_screen(-1); break;
case INPUT_RIGHT: next_screen(1); break;
case INPUT_UP: adjust_current(1); break;
case INPUT_DOWN: adjust_current(-1); break;
case INPUT_CENTER:
if (s_screen == SCREEN_BOOT) {
ui_show_status("Bootloader", "Rebooting to USB", "Connect USB", NULL, NULL, NULL);
vTaskDelay(pdMS_TO_TICKS(200));
REG_SET_BIT(RTC_CNTL_OPTIONS0_REG, RTC_CNTL_FORCE_DOWNLOAD_BOOT);
esp_restart();
} else {
bump_field(0);
}
break;
default: break;
}
usb_api_tick();
lora_radio_tick_rx();
lora_radio_get_metrics(&metrics);
char line1[32];
char line2[32];
char line3[32];
char line4[32];
char line5[32];
char line6[32];
char payload[48];
memset(line1, 0, sizeof(line1));
memset(line2, 0, sizeof(line2));
memset(line3, 0, sizeof(line3));
memset(line4, 0, sizeof(line4));
memset(line5, 0, sizeof(line5));
memset(line6, 0, sizeof(line6));
switch (s_screen) {
case SCREEN_FREQ:
snprintf(line1, sizeof(line1), "Freq: %d MHz", s_params.freq_centi_mhz / 100);
snprintf(line2, sizeof(line2), "UP/DN to change");
break;
case SCREEN_BAND: {
const band_t *b = &s_bands[s_band_idx];
snprintf(line1, sizeof(line1), "Band %s", b->name);
snprintf(line2, sizeof(line2), "%d-%d MHz", b->min_cmhz / 100, b->max_cmhz / 100);
snprintf(line3, sizeof(line3), "UP/DN to select");
break;
}
case SCREEN_BW:
snprintf(line1, sizeof(line1), "BW: %d kHz", s_params.bw_khz);
snprintf(line2, sizeof(line2), "UP/DN to change");
break;
case SCREEN_SF:
snprintf(line1, sizeof(line1), "SF: %d", s_params.sf);
snprintf(line2, sizeof(line2), "UP/DN to change");
break;
case SCREEN_CR:
snprintf(line1, sizeof(line1), "CR: 4/%d", s_params.cr);
snprintf(line2, sizeof(line2), "UP/DN to change");
break;
case SCREEN_PAYLOAD:
lora_radio_get_last_payload(payload, sizeof(payload));
snprintf(line1, sizeof(line1), "Payload:");
snprintf(line2, sizeof(line2), "%.31s", payload);
break;
case SCREEN_BOOT:
snprintf(line1, sizeof(line1), "USB Bootloader");
snprintf(line2, sizeof(line2), "CENTER to reboot");
snprintf(line3, sizeof(line3), "Plug USB first");
break;
default:
break;
}
// Метрики залишаємо у видимих рядках (5 рядків + заголовок)
snprintf(line4, sizeof(line4), "SNR %ddB", metrics.snr_db);
snprintf(line5, sizeof(line5), "RSSI %ddBm ST 0x%02X", metrics.rssi_dbm, metrics.last_status);
char header[32];
snprintf(header, sizeof(header), "%s %s", s_screen_icon[s_screen], s_screen_label[s_screen]);
ui_show_role(header);
ui_show_status(line1, line2, line3, line4, line5, line6);
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void app_main(void)
{
common_print_boot_info();
input_init();
ui_init();
ui_show_role("RX");
bool radio_ok = lora_radio_init(false);
ui_show_status(radio_ok ? "LR1121 OK" : "LR1121 FAIL", "Listening air", NULL, NULL, NULL, NULL);
ESP_LOGI(TAG, "LR1121 init: %s", radio_ok ? "OK" : "FAIL");
s_params.freq_centi_mhz = CONFIG_LORA_FREQ_MHZ * 100;
s_params.bw_khz = CONFIG_LORA_BW_KHZ;
s_params.sf = CONFIG_LORA_SF;
s_params.cr = CONFIG_LORA_CR;
s_params.tx_power_dbm = 14;
s_params.preamble_syms = 8;
s_params.payload_len = 0;
s_params.crc_on = true;
s_params.iq_invert = false;
s_params.header_implicit = false;
s_band_idx = detect_band_from_freq(s_params.freq_centi_mhz);
clamp_freq_to_band();
esp_err_t err = lora_radio_apply_params(&s_params);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Initial params failed: %s", esp_err_to_name(err));
}
usb_api_init(usb_handle_line);
app_loop();
}

1715
apps/rx/sdkconfig Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,43 @@
# Роль за замовчуванням
CONFIG_LORA_ROLE_RX=y
# Піни RA01 (налаштуйте під свою плату)
CONFIG_LORA_SPI_HOST=2
CONFIG_LORA_PIN_CS=12
CONFIG_LORA_PIN_RST=-1
CONFIG_LORA_PIN_MOSI=10
CONFIG_LORA_PIN_MISO=9
CONFIG_LORA_PIN_SCK=11
CONFIG_LORA_PIN_BUSY=13
CONFIG_LORA_PIN_DIO1=-1
# Радіо
CONFIG_LORA_FREQ_MHZ=433
CONFIG_LORA_BW_KHZ=125
CONFIG_LORA_SF=7
CONFIG_LORA_CR=5
# Консоль через USB Serial/JTAG (CDC), щоб не тримати BOOT
CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y
CONFIG_ESP_CONSOLE_UART_NONE=y
# CONFIG_ESP_CONSOLE_UART_DEFAULT is not set
# CONFIG_ESP_CONSOLE_USB_CDC is not set
# Джойстик
CONFIG_JOYSTICK_ADC_CHANNEL=5
CONFIG_JOYSTICK_ADC_LEVEL_LEFT=600
CONFIG_JOYSTICK_ADC_LEVEL_UP=1200
CONFIG_JOYSTICK_ADC_LEVEL_PRESS=1900
CONFIG_JOYSTICK_ADC_LEVEL_DOWN=2600
CONFIG_JOYSTICK_ADC_LEVEL_RIGHT=3300
CONFIG_JOYSTICK_ADC_TOLERANCE=150
# Дисплей
CONFIG_DISPLAY_I2C_PORT=0
CONFIG_DISPLAY_I2C_ADDR=60
CONFIG_DISPLAY_PIN_SDA=7
CONFIG_DISPLAY_PIN_SCL=8
CONFIG_DISPLAY_PIN_RST=-1
CONFIG_DISPLAY_WIDTH=128
CONFIG_DISPLAY_HEIGHT=64

1548
apps/rx/sdkconfig.old Normal file

File diff suppressed because it is too large Load Diff

9
apps/tx/CMakeLists.txt Normal file
View File

@@ -0,0 +1,9 @@
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
idf_build_set_property(MINIMAL_BUILD ON)
set(EXTRA_COMPONENT_DIRS "${CMAKE_CURRENT_LIST_DIR}/../../components")
set(COMPONENTS main lora_radio common input ui)
project(lora-tx)

10
apps/tx/dependencies.lock Normal file
View File

@@ -0,0 +1,10 @@
dependencies:
idf:
source:
type: idf
version: 5.5.1
direct_dependencies:
- idf
manifest_hash: 9db7a265ef57175d265e0d6eb7847107508a68a5847e4adbd1375406a7c73c51
target: esp32s3
version: 2.0.0

View File

@@ -0,0 +1,5 @@
idf_component_register(
SRCS "tx_main.c"
INCLUDE_DIRS "."
REQUIRES common lora_radio input ui usb_api
)

581
apps/tx/main/tx_main.c Normal file
View File

@@ -0,0 +1,581 @@
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "esp_system.h"
#include "sdkconfig.h"
#include "soc/rtc_cntl_reg.h"
#include "common.h"
#include "input.h"
#include "lora_radio.h"
#include "ui.h"
#include "usb_api.h"
#include <strings.h>
#include <strings.h>
static const char *TAG = "lora_tx";
typedef enum {
FIELD_FREQ = 0,
FIELD_BW,
FIELD_SF,
FIELD_CR,
FIELD_TX_POWER,
FIELD_PERIOD,
FIELD_COUNT
} field_t;
typedef enum {
SCREEN_FREQ = 0,
SCREEN_BAND,
SCREEN_BW,
SCREEN_SF,
SCREEN_CR,
SCREEN_POWER,
SCREEN_PAYLOAD,
SCREEN_PERIOD,
SCREEN_TX_ENABLE,
SCREEN_BOOT,
SCREEN_COUNT
} screen_t;
static lora_params_t s_params;
static field_t s_field = FIELD_FREQ;
static screen_t s_screen = SCREEN_FREQ;
static const char *s_screen_icon[SCREEN_COUNT] = {"ƒ", "BND", "BW", "SF", "CR", "P", "PKT", "PRD", "ON", "BL"};
static const char *s_screen_label[SCREEN_COUNT] = {"FREQ", "BAND", "BANDW", "SPREAD", "CODERATE", "POWER", "PAYLOAD", "PERIOD", "TX", "BOOT"};
static const int s_bw_options[] = {125, 250, 500};
static const char *s_payload_options[] = {
"PING 1", "PING 2", "PING 3", "PING 4", "PING 5",
"PING 6", "PING 7", "PING 8", "PING 9", "PING 10"
};
static int s_payload_idx = 0;
static char s_payload[32] = "PING 1";
static int s_period_ms = 1000;
static int64_t s_last_tx_us = 0;
static bool s_tx_enabled = true;
typedef struct {
const char *name;
int min_cmhz;
int max_cmhz;
int default_cmhz;
} band_t;
static const band_t s_bands[] = {
{"430", 43000, 44000, 43300},
{"868", 86300, 87000, 86800},
{"915", 90200, 92800, 91500},
{"L", 152500, 166000, 155000}, // 1.525-1.660 GHz
{"S", 190000, 210000, 200000}, // 1.9-2.1 GHz
{"2.4G", 240000, 248350, 244200}, // 2.4 GHz ISM (2400-2483.5 MHz)
};
static int s_band_idx = 0;
// --- JSON helpers for USB control -------------------------------------------------
static void sanitize_json_string(const char *in, char *out, size_t out_len)
{
if (!out || out_len == 0) {
return;
}
size_t w = 0;
for (size_t i = 0; in && in[i] != '\0' && w + 1 < out_len; i++) {
char c = in[i];
if (c == '\\' || c == '\"' || (unsigned char)c < 0x20) {
out[w++] = ' ';
} else {
out[w++] = c;
}
}
out[w] = '\0';
}
static bool parse_int_field(const char *json, const char *key, int *out)
{
if (!json || !key || !out) {
return false;
}
char pattern[32];
snprintf(pattern, sizeof(pattern), "\"%s\"", key);
const char *p = strstr(json, pattern);
if (!p) return false;
p = strchr(p, ':');
if (!p) return false;
p++;
int val = 0;
if (sscanf(p, " %d", &val) == 1) {
*out = val;
return true;
}
return false;
}
static bool parse_string_field(const char *json, const char *key, char *out, size_t out_len)
{
if (!json || !key || !out || out_len == 0) {
return false;
}
char pattern[32];
snprintf(pattern, sizeof(pattern), "\"%s\"", key);
const char *p = strstr(json, pattern);
if (!p) return false;
p = strchr(p, ':');
if (!p) return false;
const char *q = strchr(p, '\"');
if (!q) return false;
q++; // after quote
const char *end = strchr(q, '\"');
if (!end) return false;
size_t len = (size_t)(end - q);
if (len >= out_len) len = out_len - 1;
memcpy(out, q, len);
out[len] = '\0';
return true;
}
static bool parse_bool_field(const char *json, const char *key, bool *out)
{
if (!json || !key || !out) {
return false;
}
char pattern[32];
snprintf(pattern, sizeof(pattern), "\"%s\"", key);
const char *p = strstr(json, pattern);
if (!p) return false;
p = strchr(p, ':');
if (!p) return false;
p++;
if (strncmp(p, " true", 5) == 0 || strncmp(p, "true", 4) == 0 || strncmp(p, "1", 1) == 0) {
*out = true;
return true;
}
if (strncmp(p, " false", 6) == 0 || strncmp(p, "false", 5) == 0 || strncmp(p, "0", 1) == 0) {
*out = false;
return true;
}
return false;
}
static int find_band_idx(const char *name)
{
if (!name) return -1;
for (size_t i = 0; i < sizeof(s_bands)/sizeof(s_bands[0]); i++) {
if (strcasecmp(name, s_bands[i].name) == 0) {
return (int)i;
}
}
return -1;
}
static void apply_payload_selection(void)
{
if (s_payload_idx < 0) {
s_payload_idx = 0;
}
int max_idx = (int)(sizeof(s_payload_options) / sizeof(s_payload_options[0])) - 1;
if (s_payload_idx > max_idx) {
s_payload_idx = max_idx;
}
strlcpy(s_payload, s_payload_options[s_payload_idx], sizeof(s_payload));
}
static void change_payload(int delta)
{
s_payload_idx += delta;
apply_payload_selection();
}
static int detect_band_from_freq(int cmhz)
{
for (size_t i = 0; i < sizeof(s_bands) / sizeof(s_bands[0]); i++) {
if (cmhz >= s_bands[i].min_cmhz && cmhz <= s_bands[i].max_cmhz) {
return (int)i;
}
}
return 0;
}
static void clamp_freq_to_band(void)
{
const band_t *b = &s_bands[s_band_idx];
if (s_params.freq_centi_mhz < b->min_cmhz) s_params.freq_centi_mhz = b->min_cmhz;
if (s_params.freq_centi_mhz > b->max_cmhz) s_params.freq_centi_mhz = b->max_cmhz;
s_params.freq_centi_mhz = ((s_params.freq_centi_mhz + 50) / 100) * 100;
}
static void usb_send_status(void)
{
lora_metrics_t metrics = {0};
lora_radio_get_metrics(&metrics);
char line[256];
snprintf(line, sizeof(line),
"{\"resp\":\"status\",\"role\":\"tx\",\"freq_mhz\":%d,\"bw_khz\":%d,\"sf\":%d,\"cr\":%d,"
"\"band\":\"%s\",\"tx_power_dbm\":%d,\"period_ms\":%d,\"tx_enabled\":%s,"
"\"snr_db\":%d,\"rssi_dbm\":%d,\"last_status\":%u}\n",
s_params.freq_centi_mhz / 100,
s_params.bw_khz,
s_params.sf,
s_params.cr,
s_bands[s_band_idx].name,
s_params.tx_power_dbm,
s_period_ms,
s_tx_enabled ? "true" : "false",
metrics.snr_db,
metrics.rssi_dbm,
(unsigned)metrics.last_status);
usb_api_send_line(line);
}
static void usb_handle_set_params(const char *json)
{
bool radio_changed = false;
lora_params_t next = s_params;
int band_idx = -1;
char band_name[16] = {0};
if (parse_string_field(json, "band", band_name, sizeof(band_name))) {
band_idx = find_band_idx(band_name);
if (band_idx >= 0) {
s_band_idx = band_idx;
radio_changed = true;
}
}
int val = 0;
if (parse_int_field(json, "freq_mhz", &val)) {
next.freq_centi_mhz = val * 100;
radio_changed = true;
}
if (parse_int_field(json, "bw_khz", &val)) {
next.bw_khz = val;
radio_changed = true;
}
if (parse_int_field(json, "sf", &val)) {
next.sf = val;
radio_changed = true;
}
if (parse_int_field(json, "cr", &val)) {
next.cr = val;
radio_changed = true;
}
if (parse_int_field(json, "tx_power_dbm", &val)) {
next.tx_power_dbm = val;
radio_changed = true;
}
bool ctrl_changed = false;
if (parse_int_field(json, "period_ms", &val)) {
s_period_ms = val;
if (s_period_ms < 100) s_period_ms = 100;
if (s_period_ms > 60000) s_period_ms = 60000;
ctrl_changed = true;
}
bool bval = false;
if (parse_bool_field(json, "tx_enabled", &bval)) {
s_tx_enabled = bval;
ctrl_changed = true;
}
char payload[64] = {0};
if (parse_string_field(json, "payload", payload, sizeof(payload))) {
sanitize_json_string(payload, payload, sizeof(payload));
strlcpy(s_payload, payload, sizeof(s_payload));
ctrl_changed = true;
}
if (radio_changed) {
s_params = next;
clamp_freq_to_band();
esp_err_t err = lora_radio_apply_params(&s_params);
if (err != ESP_OK) {
ESP_LOGE(TAG, "USB set_params failed: %s", esp_err_to_name(err));
} else {
ESP_LOGI(TAG, "USB set_params applied");
}
} else if (!ctrl_changed) {
// nothing to do
return;
}
}
static void usb_handle_line(const char *line)
{
if (!line || line[0] == '\0') {
return;
}
ESP_LOGI(TAG, "USB RX: %s", line);
if (strstr(line, "set_params")) {
usb_handle_set_params(line);
return;
}
if (strstr(line, "get_status")) {
usb_send_status();
return;
}
if (strstr(line, "reboot_bootloader")) {
ui_show_status("Bootloader", "Rebooting to USB", "Connect USB", NULL, NULL, NULL);
vTaskDelay(pdMS_TO_TICKS(200));
REG_SET_BIT(RTC_CNTL_OPTIONS0_REG, RTC_CNTL_FORCE_DOWNLOAD_BOOT);
esp_restart();
}
}
static void set_band(int idx)
{
int max_idx = (int)(sizeof(s_bands) / sizeof(s_bands[0])) - 1;
if (idx < 0) idx = 0;
if (idx > max_idx) idx = max_idx;
s_band_idx = idx;
s_params.freq_centi_mhz = s_bands[s_band_idx].default_cmhz;
clamp_freq_to_band();
esp_err_t err = lora_radio_apply_params(&s_params);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Apply params failed: %s", esp_err_to_name(err));
}
}
static void bump_field(int delta)
{
switch (s_field) {
case FIELD_FREQ:
s_params.freq_centi_mhz += delta * 100; // крок 1 MHz
clamp_freq_to_band();
break;
case FIELD_BW: {
int idx = 0;
for (size_t i = 0; i < sizeof(s_bw_options)/sizeof(s_bw_options[0]); i++) {
if (s_bw_options[i] == s_params.bw_khz) {
idx = i;
break;
}
}
idx += delta;
if (idx < 0) idx = 0;
if (idx >= (int)(sizeof(s_bw_options)/sizeof(s_bw_options[0]))) idx = (int)(sizeof(s_bw_options)/sizeof(s_bw_options[0])) - 1;
s_params.bw_khz = s_bw_options[idx];
break;
}
case FIELD_SF:
s_params.sf += delta;
if (s_params.sf < 5) s_params.sf = 5;
if (s_params.sf > 12) s_params.sf = 12;
break;
case FIELD_CR:
s_params.cr += delta;
if (s_params.cr < 5) s_params.cr = 5;
if (s_params.cr > 8) s_params.cr = 8;
break;
case FIELD_TX_POWER:
s_params.tx_power_dbm += delta;
break;
case FIELD_PERIOD:
s_period_ms += delta * 100;
if (s_period_ms < 100) s_period_ms = 100;
if (s_period_ms > 60000) s_period_ms = 60000;
break;
default:
break;
}
esp_err_t err = lora_radio_apply_params(&s_params);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Apply params failed: %s", esp_err_to_name(err));
}
}
static void next_screen(int delta)
{
int idx = (int)s_screen + delta;
if (idx < 0) idx = SCREEN_COUNT - 1;
if (idx >= SCREEN_COUNT) idx = 0;
s_screen = (screen_t)idx;
switch (s_screen) {
case SCREEN_FREQ: s_field = FIELD_FREQ; break;
case SCREEN_BAND: s_field = FIELD_FREQ; break;
case SCREEN_BW: s_field = FIELD_BW; break;
case SCREEN_SF: s_field = FIELD_SF; break;
case SCREEN_CR: s_field = FIELD_CR; break;
case SCREEN_POWER: s_field = FIELD_TX_POWER; break;
case SCREEN_PERIOD: s_field = FIELD_PERIOD; break;
case SCREEN_BOOT: s_field = FIELD_FREQ; break;
default: break;
}
}
static void adjust_current(int delta)
{
switch (s_screen) {
case SCREEN_FREQ: s_field = FIELD_FREQ; bump_field(delta); break;
case SCREEN_BAND: set_band(s_band_idx + delta); break;
case SCREEN_BW: s_field = FIELD_BW; bump_field(delta); break;
case SCREEN_SF: s_field = FIELD_SF; bump_field(delta); break;
case SCREEN_CR: s_field = FIELD_CR; bump_field(delta); break;
case SCREEN_POWER: s_field = FIELD_TX_POWER; bump_field(delta); break;
case SCREEN_PERIOD: s_field = FIELD_PERIOD; bump_field(delta); break;
case SCREEN_PAYLOAD: change_payload(delta); break;
case SCREEN_BOOT: break;
default:
break;
}
}
static void app_loop(void)
{
lora_metrics_t metrics = {0};
while (true) {
input_event_t evt = input_poll();
switch (evt) {
case INPUT_LEFT: next_screen(-1); break;
case INPUT_RIGHT: next_screen(1); break;
case INPUT_UP:
if (s_screen == SCREEN_TX_ENABLE) {
s_tx_enabled = true;
} else {
adjust_current(1);
}
break;
case INPUT_DOWN:
if (s_screen == SCREEN_TX_ENABLE) {
s_tx_enabled = false;
} else {
adjust_current(-1);
}
break;
case INPUT_CENTER:
if (s_screen == SCREEN_TX_ENABLE) {
s_tx_enabled = !s_tx_enabled;
} else if (s_screen == SCREEN_BOOT) {
ui_show_status("Bootloader", "Rebooting to USB", "Connect USB", NULL, NULL, NULL);
vTaskDelay(pdMS_TO_TICKS(200));
REG_SET_BIT(RTC_CNTL_OPTIONS0_REG, RTC_CNTL_FORCE_DOWNLOAD_BOOT);
esp_restart();
} else {
bump_field(0);
}
break;
default: break;
}
usb_api_tick();
lora_radio_tick_tx();
lora_radio_get_metrics(&metrics);
// Періодична передача
int64_t now = esp_timer_get_time();
if (s_tx_enabled && now - s_last_tx_us >= (int64_t)s_period_ms * 1000) {
size_t len = strnlen(s_payload, sizeof(s_payload));
if (len > 0) {
esp_err_t tx_err = lora_radio_send((const uint8_t *)s_payload, len);
if (tx_err == ESP_OK) {
s_last_tx_us = now;
ESP_LOGI(TAG, "TX \"%s\" (%u bytes)", s_payload, (unsigned)len);
} else {
ESP_LOGW(TAG, "TX failed: %s", esp_err_to_name(tx_err));
s_last_tx_us = now; // уникнути спаму
}
}
}
char line1[32];
char line2[32];
char line3[32];
char line4[32];
char line5[32];
char line6[32];
memset(line1, 0, sizeof(line1));
memset(line2, 0, sizeof(line2));
memset(line3, 0, sizeof(line3));
memset(line4, 0, sizeof(line4));
memset(line5, 0, sizeof(line5));
memset(line6, 0, sizeof(line6));
switch (s_screen) {
case SCREEN_FREQ:
snprintf(line1, sizeof(line1), "Freq: %d MHz", s_params.freq_centi_mhz / 100);
snprintf(line2, sizeof(line2), "UP/DN to change");
break;
case SCREEN_BAND: {
const band_t *b = &s_bands[s_band_idx];
snprintf(line1, sizeof(line1), "Band %s", b->name);
snprintf(line2, sizeof(line2), "%d-%d MHz", b->min_cmhz / 100, b->max_cmhz / 100);
snprintf(line3, sizeof(line3), "UP/DN to select");
break;
}
case SCREEN_BW:
snprintf(line1, sizeof(line1), "BW: %d kHz", s_params.bw_khz);
snprintf(line2, sizeof(line2), "UP/DN to change");
break;
case SCREEN_SF:
snprintf(line1, sizeof(line1), "SF: %d", s_params.sf);
snprintf(line2, sizeof(line2), "UP/DN to change");
break;
case SCREEN_CR:
snprintf(line1, sizeof(line1), "CR: 4/%d", s_params.cr);
snprintf(line2, sizeof(line2), "UP/DN to change");
break;
case SCREEN_POWER:
snprintf(line1, sizeof(line1), "PWR: %d dBm", s_params.tx_power_dbm);
snprintf(line2, sizeof(line2), "UP/DN to change");
break;
case SCREEN_PAYLOAD:
snprintf(line1, sizeof(line1), "Payload:");
snprintf(line2, sizeof(line2), "%.31s", s_payload);
snprintf(line3, sizeof(line3), "UP/DN pick PING");
break;
case SCREEN_PERIOD:
snprintf(line1, sizeof(line1), "Period: %d ms", s_period_ms);
snprintf(line2, sizeof(line2), "UP/DN to change");
break;
case SCREEN_TX_ENABLE:
snprintf(line1, sizeof(line1), "TX: %s", s_tx_enabled ? "ON" : "OFF");
snprintf(line2, sizeof(line2), "UP=on DN=off CTR=tgl");
break;
case SCREEN_BOOT:
snprintf(line1, sizeof(line1), "USB Bootloader");
snprintf(line2, sizeof(line2), "CENTER to reboot");
snprintf(line3, sizeof(line3), "Plug USB first");
break;
default:
break;
}
snprintf(line4, sizeof(line4), "SNR %ddB", metrics.snr_db);
snprintf(line5, sizeof(line5), "RSSI %ddBm ST 0x%02X", metrics.rssi_dbm, metrics.last_status);
char header[32];
snprintf(header, sizeof(header), "%s %s", s_screen_icon[s_screen], s_screen_label[s_screen]);
ui_show_role(header);
ui_show_status(line1, line2, line3, line4, line5, line6);
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void app_main(void)
{
common_print_boot_info();
input_init();
ui_init();
ui_show_role("TX");
bool radio_ok = lora_radio_init(true);
ui_show_status(radio_ok ? "LR1121 OK" : "LR1121 FAIL", "Ready", NULL, NULL, NULL, NULL);
ESP_LOGI(TAG, "LR1121 init: %s", radio_ok ? "OK" : "FAIL");
s_params.freq_centi_mhz = CONFIG_LORA_FREQ_MHZ * 100;
s_params.bw_khz = CONFIG_LORA_BW_KHZ;
s_params.sf = CONFIG_LORA_SF;
s_params.cr = CONFIG_LORA_CR;
s_params.tx_power_dbm = 14;
s_params.preamble_syms = 8;
s_params.payload_len = 0;
s_params.crc_on = true;
s_params.iq_invert = false;
s_params.header_implicit = false;
apply_payload_selection();
s_band_idx = detect_band_from_freq(s_params.freq_centi_mhz);
clamp_freq_to_band();
esp_err_t err = lora_radio_apply_params(&s_params);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Initial params failed: %s", esp_err_to_name(err));
}
usb_api_init(usb_handle_line);
app_loop();
}

1716
apps/tx/sdkconfig Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,43 @@
# Роль за замовчуванням
CONFIG_LORA_ROLE_TX=y
# Піни RA01 (налаштуйте під свою плату)
CONFIG_LORA_SPI_HOST=2
CONFIG_LORA_PIN_CS=12
CONFIG_LORA_PIN_RST=-1
CONFIG_LORA_PIN_MOSI=10
CONFIG_LORA_PIN_MISO=9
CONFIG_LORA_PIN_SCK=11
CONFIG_LORA_PIN_BUSY=13
CONFIG_LORA_PIN_DIO1=-1
# Радіо
CONFIG_LORA_FREQ_MHZ=433
CONFIG_LORA_BW_KHZ=125
CONFIG_LORA_SF=7
CONFIG_LORA_CR=5
# Консоль через USB Serial/JTAG (CDC), щоб не тримати BOOT
CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y
CONFIG_ESP_CONSOLE_UART_NONE=y
# CONFIG_ESP_CONSOLE_UART_DEFAULT is not set
# CONFIG_ESP_CONSOLE_USB_CDC is not set
# Джойстик
CONFIG_JOYSTICK_ADC_CHANNEL=6
CONFIG_JOYSTICK_ADC_LEVEL_LEFT=600
CONFIG_JOYSTICK_ADC_LEVEL_UP=1200
CONFIG_JOYSTICK_ADC_LEVEL_PRESS=1900
CONFIG_JOYSTICK_ADC_LEVEL_DOWN=2600
CONFIG_JOYSTICK_ADC_LEVEL_RIGHT=3300
CONFIG_JOYSTICK_ADC_TOLERANCE=150
# Дисплей
CONFIG_DISPLAY_I2C_PORT=0
CONFIG_DISPLAY_I2C_ADDR=60
CONFIG_DISPLAY_PIN_SDA=7
CONFIG_DISPLAY_PIN_SCL=8
CONFIG_DISPLAY_PIN_RST=-1
CONFIG_DISPLAY_WIDTH=128
CONFIG_DISPLAY_HEIGHT=64

1549
apps/tx/sdkconfig.old Normal file

File diff suppressed because it is too large Load Diff