Files
LoraTestSystemRX_TX/apps/rx/main/rx_main.c
2026-01-17 09:53:08 +02:00

449 lines
13 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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();
}