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