From de4cdc2552747cb7e688897235eff9dba0ed97e8 Mon Sep 17 00:00:00 2001 From: Peter Siegmund Date: Sat, 7 Jun 2025 15:43:58 +0200 Subject: [PATCH] copy BLE functions Signed-off-by: Peter Siegmund --- components/persistence/CMakeLists.txt | 5 + components/persistence/include/persistence.h | 12 + components/persistence/persistence.c | 114 ++++++++ components/remote_control/CMakeLists.txt | 11 + .../remote_control/capability_service.c | 156 +++++++++++ components/remote_control/device_service.c | 29 ++ .../include/capability_service.h | 11 + .../remote_control/include/device_service.h | 7 + .../remote_control/include/led_service.h | 12 + .../remote_control/include/remote_control.h | 3 + components/remote_control/led_service.c | 75 ++++++ components/remote_control/remote_control.c | 251 ++++++++++++++++++ components/storage/CMakeLists.txt | 6 + components/storage/include/storage.h | 55 ++++ components/storage/storage.c | 139 ++++++++++ data/capability.json | 2 +- main/beacon.c | 16 +- main/beacon.h | 8 +- main/main.c | 12 +- 19 files changed, 908 insertions(+), 16 deletions(-) create mode 100644 components/persistence/CMakeLists.txt create mode 100644 components/persistence/include/persistence.h create mode 100644 components/persistence/persistence.c create mode 100644 components/remote_control/CMakeLists.txt create mode 100644 components/remote_control/capability_service.c create mode 100644 components/remote_control/device_service.c create mode 100644 components/remote_control/include/capability_service.h create mode 100644 components/remote_control/include/device_service.h create mode 100644 components/remote_control/include/led_service.h create mode 100644 components/remote_control/include/remote_control.h create mode 100644 components/remote_control/led_service.c create mode 100644 components/remote_control/remote_control.c create mode 100644 components/storage/CMakeLists.txt create mode 100644 components/storage/include/storage.h create mode 100644 components/storage/storage.c diff --git a/components/persistence/CMakeLists.txt b/components/persistence/CMakeLists.txt new file mode 100644 index 0000000..9130681 --- /dev/null +++ b/components/persistence/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register(SRCS "persistence.c" + INCLUDE_DIRS "include" + PRIV_REQUIRES + nvs_flash +) diff --git a/components/persistence/include/persistence.h b/components/persistence/include/persistence.h new file mode 100644 index 0000000..ed5ad6d --- /dev/null +++ b/components/persistence/include/persistence.h @@ -0,0 +1,12 @@ +#pragma once + +typedef enum +{ + VALUE_TYPE_STRING, + VALUE_TYPE_INT32, +} persistence_value_type_t; + +void persistence_init(const char *namespace_name); +void persistence_save(persistence_value_type_t value_type, const char *key, const void *value); +void *persistence_load(persistence_value_type_t value_type, const char *key, void *out); +void persistence_deinit(); diff --git a/components/persistence/persistence.c b/components/persistence/persistence.c new file mode 100644 index 0000000..d242f25 --- /dev/null +++ b/components/persistence/persistence.c @@ -0,0 +1,114 @@ +#include "persistence.h" +#include "esp_err.h" +#include "esp_log.h" +#include "esp_mac.h" +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include "nvs_flash.h" + +static const char *TAG = "persistence"; + +static nvs_handle_t persistence_handle; +static SemaphoreHandle_t persistence_mutex; + +void persistence_init(const char *namespace_name) +{ + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) + { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + + ESP_ERROR_CHECK(nvs_open(namespace_name, NVS_READWRITE, &persistence_handle)); + + persistence_mutex = xSemaphoreCreateMutex(); + if (persistence_mutex == NULL) + { + ESP_LOGE(TAG, "Failed to create mutex"); + } +} + +void persistence_save(persistence_value_type_t value_type, const char *key, const void *value) +{ + if (persistence_mutex != NULL) + { + if (xSemaphoreTake(persistence_mutex, portMAX_DELAY) == pdTRUE) + { + esp_err_t err = ESP_ERR_INVALID_ARG; + + switch (value_type) + { + case VALUE_TYPE_STRING: + err = nvs_set_str(persistence_handle, key, (char *)value); + break; + + case VALUE_TYPE_INT32: + err = nvs_set_i32(persistence_handle, key, *(int32_t *)value); + break; + + default: + ESP_LOGE(TAG, "Unsupported value type"); + break; + } + + if (err == ESP_OK) + { + ESP_ERROR_CHECK(nvs_commit(persistence_handle)); + } + else + { + ESP_LOGE(TAG, "Error saving key %s: %s", key, esp_err_to_name(err)); + } + + xSemaphoreGive(persistence_mutex); + } + } +} + +void *persistence_load(persistence_value_type_t value_type, const char *key, void *out) +{ + if (persistence_mutex != NULL) + { + if (xSemaphoreTake(persistence_mutex, portMAX_DELAY) == pdTRUE) + { + esp_err_t err = ESP_ERR_INVALID_ARG; + + switch (value_type) + { + case VALUE_TYPE_STRING: + err = nvs_get_str(persistence_handle, key, (char *)out, NULL); + break; + + case VALUE_TYPE_INT32: + err = nvs_get_i32(persistence_handle, key, (int32_t *)out); + break; + + default: + ESP_LOGE(TAG, "Unsupported value type"); + break; + } + + if (err != ESP_OK) + { + ESP_LOGE(TAG, "Error loading key %s: %s", key, esp_err_to_name(err)); + } + + xSemaphoreGive(persistence_mutex); + } + } + + return out; +} + +void persistence_deinit() +{ + if (persistence_mutex != NULL) + { + vSemaphoreDelete(persistence_mutex); + persistence_mutex = NULL; + } + + nvs_close(persistence_handle); +} diff --git a/components/remote_control/CMakeLists.txt b/components/remote_control/CMakeLists.txt new file mode 100644 index 0000000..698c5a7 --- /dev/null +++ b/components/remote_control/CMakeLists.txt @@ -0,0 +1,11 @@ +idf_component_register(SRCS + "capability_service.c" + "device_service.c" + "led_service.c" + "remote_control.c" + INCLUDE_DIRS "include" + PRIV_REQUIRES + bt + esp_app_format + storage +) diff --git a/components/remote_control/capability_service.c b/components/remote_control/capability_service.c new file mode 100644 index 0000000..9824eb1 --- /dev/null +++ b/components/remote_control/capability_service.c @@ -0,0 +1,156 @@ +#include "capability_service.h" +#include "esp_log.h" +#include "storage.h" +#include +#include // For malloc, free +#include "host/ble_hs.h" // For ble_hs_mbuf_from_flat, ble_att_mtu +#include "host/ble_uuid.h" // For BLE_ATT_MTU_DFLT (often included via ble_hs.h) +#include "nimble/nimble_port.h" // For os_mbuf related functions + +static const char *TAG_CS = "capability_service"; + +#define CAPA_READ_CHUNK_SIZE 200 // Maximale Bytes pro Lesevorgang aus dem Storage + +int capa_read(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg) +{ + esp_err_t storage_status = storage_init(); + if (storage_status != ESP_OK) + { + ESP_LOGE(TAG_CS, "Failed to initialize storage: %s", esp_err_to_name(storage_status)); + const char *err_msg = "Error: Storage init failed"; + os_mbuf_append(ctxt->om, err_msg, strlen(err_msg)); + // storage_uninit() sollte hier nicht aufgerufen werden, da die Initialisierung fehlschlug. + return 0; + } + + char *content = ""; + os_mbuf_append(ctxt->om, content, strlen(content)); + const char *filename = "/storage/capability.json"; // Die zu lesende Datei + char read_buffer[CAPA_READ_CHUNK_SIZE]; + ssize_t bytes_read; + int os_err; + + ESP_LOGI(TAG_CS, "Reading capabilities from %s", filename); + + // Schleife, um die Datei in Chunks zu lesen und an den mbuf anzuhängen + while ((bytes_read = storage_read(filename, read_buffer, sizeof(read_buffer))) > 0) + { + ESP_LOGD(TAG_CS, "Read %zd bytes from storage", bytes_read); + // Den gelesenen Chunk an den BLE-Antwortpuffer anhängen + os_err = os_mbuf_append(ctxt->om, read_buffer, bytes_read); + if (os_err != 0) + { + ESP_LOGE(TAG_CS, "Failed to append to mbuf (error %d). May be out of space.", os_err); + // Der mbuf könnte voll sein. Stoppe das Anhängen. + // Bereits angehängte Daten werden gesendet. + break; + } + } + + // Fehlerbehandlung oder EOF + if (bytes_read < 0) + { + ESP_LOGE(TAG_CS, "Error reading from storage (file: %s, error_code: %zd)", filename, bytes_read); + // Wenn noch nichts angehängt wurde, sende eine Fehlermeldung. + if (ctxt->om->om_len == 0) + { + const char *err_msg = "Error: Failed to read capability data"; + // Hier könnten spezifischere Fehlermeldungen basierend auf bytes_read eingefügt werden + os_mbuf_append(ctxt->om, err_msg, strlen(err_msg)); + } + } + else + { // bytes_read == 0, bedeutet EOF (Ende der Datei) + ESP_LOGI(TAG_CS, "Successfully read and appended all data from %s to mbuf.", filename); + } + + storage_uninit(); + return 0; +} + +int capa_char_1979_user_desc(uint16_t con_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg) +{ + const char *desc = "Capabilities of the device"; + os_mbuf_append(ctxt->om, desc, strlen(desc)); + return 0; +} + +void capa_notify_data(uint16_t conn_handle, uint16_t char_val_handle) +{ + esp_err_t storage_status = storage_init(); + if (storage_status != ESP_OK) + { + ESP_LOGE(TAG_CS, "Notify: Failed to initialize storage: %s", esp_err_to_name(storage_status)); + return; + } + + const char *filename = "/storage/capability.json"; + + uint16_t mtu = ble_att_mtu(conn_handle); + if (mtu == 0) { // Should not happen for an active connection, fallback + ESP_LOGW(TAG_CS, "Notify: ble_att_mtu returned 0, using default MTU %d.", BLE_ATT_MTU_DFLT); + mtu = BLE_ATT_MTU_DFLT; + } + + // Max payload for notification is MTU - 3 (1 byte opcode for Notification, 2 bytes attribute handle) + size_t notify_chunk_size = (mtu > 3) ? (mtu - 3) : (BLE_ATT_MTU_DFLT - 3); // Ensure mtu > 3 + + // Further cap by CAPA_READ_CHUNK_SIZE if it's smaller and meant as an upper limit for any single read op + if (notify_chunk_size > CAPA_READ_CHUNK_SIZE) { + notify_chunk_size = CAPA_READ_CHUNK_SIZE; + } + + if (notify_chunk_size == 0) { // Safety check + ESP_LOGE(TAG_CS, "Notify: Calculated notify_chunk_size is 0. Aborting."); + storage_uninit(); + return; + } + + char *read_buffer = malloc(notify_chunk_size); + if (!read_buffer) + { + ESP_LOGE(TAG_CS, "Notify: Failed to allocate read_buffer (%zu bytes)", notify_chunk_size); + storage_uninit(); + return; + } + + ESP_LOGI(TAG_CS, "Notify: Reading capabilities from %s to notify conn %u, attr %u (chunk size %zu, MTU %u)", + filename, conn_handle, char_val_handle, notify_chunk_size, mtu); + + FILE *fp = fopen(filename, "r"); + if (!fp) + { + ESP_LOGE(TAG_CS, "Notify: Failed to open %s for reading.", filename); + free(read_buffer); + storage_uninit(); + return; + } + + ssize_t bytes_read; + while ((bytes_read = fread(read_buffer, 1, notify_chunk_size, fp)) > 0) + { + ESP_LOGD(TAG_CS, "Notify: Read %zd bytes from storage for notification", bytes_read); + + struct os_mbuf *om = ble_hs_mbuf_from_flat(read_buffer, bytes_read); + if (!om) { + ESP_LOGE(TAG_CS, "Notify: Failed to allocate mbuf for notification. Stopping."); + break; // Stop sending if mbuf allocation fails + } + + int rc = ble_gatts_notify_custom(conn_handle, char_val_handle, om); + if (rc != 0) { + ESP_LOGE(TAG_CS, "Notify: Error sending notification (rc=%d). Stopping.", rc); + // ble_gatts_notify_custom frees 'om' on success or if it takes ownership even on some errors. + // If it returns an error where 'om' is not freed, we might need os_mbuf_free_chain(om); + // For now, assume NimBLE handles 'om' correctly in error cases like BLE_HS_ENOMEM. + break; // Stop if notification fails + } + ESP_LOGD(TAG_CS, "Notify: Sent %zd bytes successfully.", bytes_read); + } + + if (ferror(fp)) { ESP_LOGE(TAG_CS, "Notify: File read error from %s.", filename); } + fclose(fp); + free(read_buffer); + storage_uninit(); + ESP_LOGI(TAG_CS, "Notify: Finished sending capability data for conn %u.", conn_handle); +} diff --git a/components/remote_control/device_service.c b/components/remote_control/device_service.c new file mode 100644 index 0000000..2033d9b --- /dev/null +++ b/components/remote_control/device_service.c @@ -0,0 +1,29 @@ +#include "include/device_service.h" +#include "esp_app_desc.h" +#include +#include + +int device_model_number_read(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg) +{ + char model_number_str[65]; + const esp_app_desc_t *app_desc = esp_app_get_description(); + + if (app_desc->project_name[0] != '\0' && app_desc->version[0] != '\0') + { + snprintf(model_number_str, sizeof(model_number_str), "%s v%s", app_desc->project_name, app_desc->version); + } + else + { + snprintf(model_number_str, sizeof(model_number_str), "undefined"); + } + + os_mbuf_append(ctxt->om, model_number_str, strlen(model_number_str)); + return 0; +} + +int device_manufacturer_read(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg) +{ + char *manufacturer = "mars3142"; + os_mbuf_append(ctxt->om, manufacturer, strlen(manufacturer)); + return 0; +} diff --git a/components/remote_control/include/capability_service.h b/components/remote_control/include/capability_service.h new file mode 100644 index 0000000..8841fcc --- /dev/null +++ b/components/remote_control/include/capability_service.h @@ -0,0 +1,11 @@ +#pragma once + +#include "host/ble_hs.h" +#include + +/// Service Characteristics Callback +int capa_read(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg); +void capa_notify_data(uint16_t conn_handle, uint16_t char_val_handle); + +/// Service Characteristics User Description +int capa_char_1979_user_desc(uint16_t con_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg); diff --git a/components/remote_control/include/device_service.h b/components/remote_control/include/device_service.h new file mode 100644 index 0000000..bcc8229 --- /dev/null +++ b/components/remote_control/include/device_service.h @@ -0,0 +1,7 @@ +#pragma once + +#include "host/ble_hs.h" +#include + +int device_model_number_read(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg); +int device_manufacturer_read(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg); diff --git a/components/remote_control/include/led_service.h b/components/remote_control/include/led_service.h new file mode 100644 index 0000000..566c548 --- /dev/null +++ b/components/remote_control/include/led_service.h @@ -0,0 +1,12 @@ +#pragma once + +#include "host/ble_hs.h" +#include + +/// LED Service Characteristic Callbacks +int led_write(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg); +int led_capabilities_read(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg); + +/// LED Service Characteristic User Description +int led_char_a000_user_desc(uint16_t con_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg); +int led_char_dead_user_desc(uint16_t con_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg); diff --git a/components/remote_control/include/remote_control.h b/components/remote_control/include/remote_control.h new file mode 100644 index 0000000..ac994c8 --- /dev/null +++ b/components/remote_control/include/remote_control.h @@ -0,0 +1,3 @@ +#pragma once + +void remote_control_init(void); diff --git a/components/remote_control/led_service.c b/components/remote_control/led_service.c new file mode 100644 index 0000000..0c1407d --- /dev/null +++ b/components/remote_control/led_service.c @@ -0,0 +1,75 @@ +#include "include/led_service.h" + +static const char *TAG = "led_service"; + +/// Capabilities of Device +int led_capabilities_read(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg) +{ + char *data = "To be implemented later"; + os_mbuf_append(ctxt->om, data, strlen(data)); + return 0; +} + +// Write data to ESP32 defined as server +int led_write(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg) +{ + const char *received_payload = (const char *)ctxt->om->om_data; + uint16_t payload_len = ctxt->om->om_len; + + // Define command strings + const char CMD_LIGHT_ON[] = "LIGHT ON"; + const char CMD_LIGHT_OFF[] = "LIGHT OFF"; + const char CMD_FAN_ON[] = "FAN ON"; + const char CMD_FAN_OFF[] = "FAN OFF"; + + if (payload_len == (sizeof(CMD_LIGHT_ON) - 1) && strncmp(received_payload, CMD_LIGHT_ON, payload_len) == 0) + { + ESP_LOGI(TAG, "LIGHT ON"); + // for (int i = 0; i < led_matrix_get_size(); i++) + { + // led_matrix_set_pixel(i, 10, 10, 0); + } + } + else if (payload_len == (sizeof(CMD_LIGHT_OFF) - 1) && strncmp(received_payload, CMD_LIGHT_OFF, payload_len) == 0) + { + ESP_LOGI(TAG, "LIGHT OFF"); + // for (int i = 0; i < led_matrix_get_size(); i++) + { + // led_matrix_set_pixel(i, 0, 0, 0); + } + } + else if (payload_len == (sizeof(CMD_FAN_ON) - 1) && strncmp(received_payload, CMD_FAN_ON, payload_len) == 0) + { + ESP_LOGI(TAG, "FAN ON"); + // TODO: Implement action for FAN ON + } + else if (payload_len == (sizeof(CMD_FAN_OFF) - 1) && strncmp(received_payload, CMD_FAN_OFF, payload_len) == 0) + { + ESP_LOGI(TAG, "FAN OFF"); + // TODO: Implement action for FAN OFF + } + else + { + char temp_buffer[payload_len + 1]; + memcpy(temp_buffer, received_payload, payload_len); + temp_buffer[payload_len] = '\0'; + + ESP_LOGI(TAG, "Unknown command from client: %s", temp_buffer); + } + + return 0; +} + +int led_char_a000_user_desc(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg) +{ + const char *desc = "Capabilities of Device"; + os_mbuf_append(ctxt->om, desc, strlen(desc)); + return 0; +} + +int led_char_dead_user_desc(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg) +{ + const char *desc = "Readable Data from Server"; + os_mbuf_append(ctxt->om, desc, strlen(desc)); + return 0; +} diff --git a/components/remote_control/remote_control.c b/components/remote_control/remote_control.c new file mode 100644 index 0000000..ad4dbd4 --- /dev/null +++ b/components/remote_control/remote_control.c @@ -0,0 +1,251 @@ +#include +#include + +#include "capability_service.h" +#include "esp_event.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "freertos/task.h" +#include "host/ble_hs.h" +#include "host/ble_sm.h" +#include "host/ble_uuid.h" +#include "include/device_service.h" +#include "include/led_service.h" +#include "nimble/nimble_port.h" +#include "nimble/nimble_port_freertos.h" +#include "sdkconfig.h" +#include "services/gap/ble_svc_gap.h" +#include "services/gatt/ble_svc_gatt.h" + +static const char *TAG = "remote_control"; + +static const ble_uuid16_t device_service_uuid = BLE_UUID16_INIT(0x180A); +static const ble_uuid128_t capability_service_uuid = + BLE_UUID128_INIT(0x91, 0xB6, 0xCA, 0x95, 0xB2, 0xC6, 0x7B, 0x90, 0x31, 0x45, 0x77, 0xE6, 0x67, 0x10, 0x68, 0xB9); +static const ble_uuid16_t led_service_uuid = BLE_UUID16_INIT(0x1007); + +uint8_t ble_addr_type; + +// Handle for the capability characteristic value +static uint16_t g_capa_char_val_handle; + +static void ble_app_advertise(void); + +static struct ble_gatt_dsc_def char_0xA000_descs[] = {{ + .uuid = BLE_UUID16_DECLARE(0x2901), + .att_flags = BLE_ATT_F_READ, + .access_cb = led_char_a000_user_desc, + }, + {0}}; + +static struct ble_gatt_dsc_def char_0xDEAD_descs[] = {{ + .uuid = BLE_UUID16_DECLARE(0x2901), + .att_flags = BLE_ATT_F_WRITE, + .access_cb = led_char_dead_user_desc, + }, + {0}}; + +static struct ble_gatt_dsc_def char_0x1979_desc[] = {{ + .uuid = BLE_UUID16_DECLARE(0x2901), + .att_flags = BLE_ATT_F_READ, + .access_cb = capa_char_1979_user_desc, + }, + {0}}; + +// Array of pointers to other service definitions +static const struct ble_gatt_svc_def gatt_svcs[] = { + { + .type = BLE_GATT_SVC_TYPE_PRIMARY, + .uuid = &device_service_uuid.u, + .characteristics = (struct ble_gatt_chr_def[]){{.uuid = BLE_UUID16_DECLARE(0x2A24), + .flags = BLE_GATT_CHR_F_READ, + .access_cb = device_model_number_read}, + {.uuid = BLE_UUID16_DECLARE(0x2A29), + .flags = BLE_GATT_CHR_F_READ, + .access_cb = device_manufacturer_read}, + {0}}, + }, + { + .type = BLE_GATT_SVC_TYPE_PRIMARY, + .uuid = &capability_service_uuid.u, + .characteristics = (struct ble_gatt_chr_def[]){{ + .uuid = BLE_UUID16_DECLARE(0x1979), + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY, + .access_cb = capa_read, + .val_handle = &g_capa_char_val_handle, + .descriptors = char_0x1979_desc, + }, + {0}}, + }, + { + .type = BLE_GATT_SVC_TYPE_PRIMARY, + .uuid = &led_service_uuid.u, + .characteristics = (struct ble_gatt_chr_def[]){{ + .uuid = BLE_UUID16_DECLARE(0xA000), + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY, + .access_cb = led_capabilities_read, + .descriptors = char_0xA000_descs, + }, + { + .uuid = BLE_UUID16_DECLARE(0xDEAD), + .flags = BLE_GATT_CHR_F_WRITE, + .access_cb = led_write, + .descriptors = char_0xDEAD_descs, + }, + {0}}, + }, + {0}}; + +// BLE event handling +static int ble_gap_event(struct ble_gap_event *event, void *arg) +{ + switch (event->type) + { + case BLE_GAP_EVENT_CONNECT: + ESP_LOGI(TAG, "BLE GAP EVENT CONNECT %s", event->connect.status == 0 ? "OK!" : "FAILED!"); + if (event->connect.status != 0) + { + // Re-advertise if connection failed + ble_app_advertise(); + } + break; + + case BLE_GAP_EVENT_DISCONNECT: + ESP_LOGI(TAG, "BLE GAP EVENT DISCONNECTED"); + // Re-advertise after disconnection + ble_app_advertise(); + break; + + case BLE_GAP_EVENT_ADV_COMPLETE: + ESP_LOGI(TAG, "BLE GAP EVENT ADV COMPLETE"); + // Re-advertise to continue accepting new clients + ble_app_advertise(); + break; + + case BLE_GAP_EVENT_SUBSCRIBE: + ESP_LOGI(TAG, + "BLE GAP EVENT SUBSCRIBE conn_handle=%d attr_handle=%d reason=%d " + "prev_notify=%d cur_notify=%d prev_indicate=%d cur_indicate=%d", + event->subscribe.conn_handle, event->subscribe.attr_handle, event->subscribe.reason, + event->subscribe.prev_notify, event->subscribe.cur_notify, event->subscribe.prev_indicate, + event->subscribe.cur_indicate); + + // Check if subscription is for the capability characteristic's CCCD. + // The CCCD handle is typically the characteristic value handle + 1. + // g_capa_char_val_handle stores the handle of the characteristic value itself. + if (event->subscribe.attr_handle == g_capa_char_val_handle + 1) + { + if (event->subscribe.cur_notify) + { + ESP_LOGI(TAG, "Client subscribed to capability notifications. Sending data..."); + // Call the function to send capability data via notifications + capa_notify_data(event->subscribe.conn_handle, g_capa_char_val_handle); + } + else + { + ESP_LOGI(TAG, "Client unsubscribed from capability notifications."); + } + } + break; + + default: + break; + } + return 0; +} + +// Define the BLE connection +static void ble_app_advertise(void) +{ + int ret; + + // GAP - advertising definition + struct ble_hs_adv_fields fields; + memset(&fields, 0, sizeof(fields)); + fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP; + fields.uuids128 = (ble_uuid128_t[]){capability_service_uuid}; + fields.num_uuids128 = 1; + fields.uuids128_is_complete = 1; + + ret = ble_gap_adv_set_fields(&fields); + if (ret != 0) + { + ESP_LOGE(TAG, "Failed to set advertising data (err: %d)", ret); + return; + } + + // GAP - device connectivity definition + struct ble_gap_adv_params adv_params; + memset(&adv_params, 0, sizeof(adv_params)); + adv_params.conn_mode = BLE_GAP_CONN_MODE_UND; // connectable or non-connectable + adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN; // discoverable or non-discoverable + ret = ble_gap_adv_start(ble_addr_type, NULL, BLE_HS_FOREVER, &adv_params, ble_gap_event, NULL); + if (ret != 0) + { + ESP_LOGE(TAG, "Advertising failed to start (err %d)", ret); + return; + } + + // --- Configure Scan Response Data (SCAN_RSP) --- + struct ble_hs_adv_fields scan_rsp_fields; + memset(&scan_rsp_fields, 0, sizeof(scan_rsp_fields)); + + // Get the device name + const char *device_name; + device_name = ble_svc_gap_device_name(); + scan_rsp_fields.name = (uint8_t *)device_name; + scan_rsp_fields.name_len = strlen(device_name); + scan_rsp_fields.name_is_complete = 1; + + // Optionally, add TX power level to scan response + scan_rsp_fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO; + scan_rsp_fields.tx_pwr_lvl_is_present = 1; + + ret = ble_gap_adv_rsp_set_fields(&scan_rsp_fields); + if (ret != 0) + { + ESP_LOGE(TAG, "Error setting scan response data; rc=%d", ret); + return; + } +} + +// The application +static void ble_app_on_sync(void) +{ + uint8_t ble_addr[6] = {0}; + int ret = ble_hs_id_copy_addr(BLE_ADDR_PUBLIC, ble_addr, NULL); + if (ret != 0) + { + ESP_LOGE(TAG, "Failed to get BLE MAC address (err: %d)", ret); + return; + } + + char formatted_name[32]; + snprintf(formatted_name, sizeof(formatted_name), "Lighthouse %02X%02X", ble_addr[4], ble_addr[5]); + ble_svc_gap_device_name_set(formatted_name); + + // Start Advertising + ble_hs_id_infer_auto(0, &ble_addr_type); // Determines the best address type automatically + ble_app_advertise(); +} + +// The infinite task +static void host_task(void *param) +{ + nimble_port_run(); // This function will return only when nimble_port_stop() is executed +} + +void remote_control_init(void) +{ + nimble_port_init(); + ble_svc_gap_init(); + ble_svc_gatt_init(); + ble_gatts_count_cfg(gatt_svcs); + ble_gatts_add_svcs(gatt_svcs); + + // Callback für Synchronisation + ble_hs_cfg.sync_cb = ble_app_on_sync; + + nimble_port_freertos_init(host_task); // Start BLE-Host-Task +} diff --git a/components/storage/CMakeLists.txt b/components/storage/CMakeLists.txt new file mode 100644 index 0000000..5e9118b --- /dev/null +++ b/components/storage/CMakeLists.txt @@ -0,0 +1,6 @@ +idf_component_register(SRCS + "storage.c" + INCLUDE_DIRS "include" + PRIV_REQUIRES + spiffs +) diff --git a/components/storage/include/storage.h b/components/storage/include/storage.h new file mode 100644 index 0000000..3c1f1d1 --- /dev/null +++ b/components/storage/include/storage.h @@ -0,0 +1,55 @@ +#pragma once + +#include "esp_err.h" +#include // For ssize_t + +/** + * @brief Initializes the SPIFFS filesystem. + * This function should be called once before any file operations. + * @return ESP_OK on success, error code otherwise. + */ +esp_err_t storage_init(void); + +/** + * @brief Unregisters the SPIFFS filesystem. + * This function should be called once when file operations are finished. + */ +void storage_uninit(void); + +/** + * @brief Reads a chunk of data from a file on SPIFFS. + * This function maintains internal state (file pointer) to allow reading + * the same file chunk by chunk across multiple calls. + * + * @param filename The path to the file to read (e.g., "/storage/my_file.txt"). + * Must be the same filename for consecutive calls to read the same file. + * @param buffer Buffer to store the read data. Must be large enough for max_bytes. + * @param max_bytes The maximum number of bytes to read in this call. Must be > 0. + * @return The number of bytes read on success (>= 0). + * Returns 0 when the end of the file is reached. + * Returns a negative value on error: + * -1: Invalid input parameters (filename, buffer is NULL, or max_bytes is 0). + * -2: Failed to open the file (first call for this filename). + * -3: Filename mismatch with the currently open file (subsequent call). + * -4: Read error occurred. + */ +ssize_t storage_read(const char *filename, char *buffer, size_t max_bytes); + +/** + * @brief Reads a chunk of data from a file on SPIFFS, starting at a specific offset. + * This function is stateless regarding an internally managed file pointer for sequential reads; + * it opens, seeks, reads, and closes the file on each call. + * + * @param filename The path to the file to read (e.g., "/storage/my_file.txt"). + * @param buffer Buffer to store the read data. Must be large enough for nbytes. + * @param offset The offset in the file to start reading from. + * @param nbytes The maximum number of bytes to read in this call. Must be > 0. + * @return The number of bytes read on success (>= 0). + * Returns 0 when the end of the file is reached from the given offset. + * Returns a negative value on error: + * -1: Invalid input parameters (filename, buffer is NULL, nbytes is 0, or offset is negative). + * -2: Failed to open the file. + * -5: Seek error occurred. + * -4: Read error occurred. + */ +ssize_t storage_read_at(const char *filename, char *buffer, off_t offset, size_t nbytes); diff --git a/components/storage/storage.c b/components/storage/storage.c new file mode 100644 index 0000000..0f30592 --- /dev/null +++ b/components/storage/storage.c @@ -0,0 +1,139 @@ +#include "storage.h" + +#include "esp_log.h" +#include +#include +#include + +#include "esp_spiffs.h" + +static const char *TAG = "storage"; + +static FILE *s_current_file = NULL; +static char s_current_filename[256] = {0}; // Buffer to store the current filename + +esp_err_t storage_init(void) +{ + ESP_LOGI(TAG, "Initializing SPIFFS"); + + esp_vfs_spiffs_conf_t conf = { + .base_path = "/storage", // Path where the filesystem will be mounted + .partition_label = "storage", // Partition label (must match partitions.csv) + .max_files = 5, // Maximum number of files that can be open at the same time + .format_if_mount_failed = true // Format partition if mount fails + }; + + // Initialize and mount SPIFFS + esp_err_t ret = esp_vfs_spiffs_register(&conf); + + if (ret != ESP_OK) + { + if (ret == ESP_FAIL) + { + ESP_LOGE(TAG, "Failed to mount or format filesystem"); + } + else if (ret == ESP_ERR_NOT_FOUND) + { + ESP_LOGE(TAG, "Failed to find SPIFFS partition"); + } + else + { + ESP_LOGE(TAG, "Failed to initialize SPIFFS (%s)", esp_err_to_name(ret)); + } + return ret; + } + + ESP_LOGI(TAG, "SPIFFS mounted"); + return ESP_OK; +} + +void storage_uninit(void) +{ + if (s_current_file != NULL) + { + // Log before clearing s_current_filename, using its value. + ESP_LOGW(TAG, "File '%s' was still open during uninit. Closing it.", s_current_filename); + fclose(s_current_file); // Close the file + s_current_file = NULL; + s_current_filename[0] = '\0'; + } + esp_vfs_spiffs_unregister("storage"); + ESP_LOGI(TAG, "SPIFFS unmounted"); +} + +ssize_t storage_read(const char *filename, char *buffer, size_t max_bytes) +{ + // Input validation + if (filename == NULL || filename[0] == '\0' || buffer == NULL || max_bytes == 0) + { + ESP_LOGE(TAG, "Invalid input parameters for storage_read"); + return -1; + } + + // If no file is currently open, open the requested one + if (s_current_file == NULL) + { + s_current_file = fopen(filename, "r"); + if (s_current_file == NULL) + { + ESP_LOGE(TAG, "Failed to open file for reading: %s", filename); + s_current_filename[0] = '\0'; // Clear filename as open failed + return -2; // Failed to open file + } + // Store the filename for subsequent calls + strncpy(s_current_filename, filename, sizeof(s_current_filename) - 1); + s_current_filename[sizeof(s_current_filename) - 1] = '\0'; + ESP_LOGI(TAG, "Opened file: %s", s_current_filename); + } + else + { + // If a file is open, ensure it's the same file requested + if (strcmp(filename, s_current_filename) != 0) + { + ESP_LOGE(TAG, "Filename mismatch. Expected '%s', got '%s'. Close the current file first.", + s_current_filename, filename); + // Note: File remains open. Caller might need a separate close function + // or ensure read loop finishes (returns 0 or error) before changing filename. + return -3; // Filename mismatch + } + } + + // Read a chunk + size_t bytes_read = fread(buffer, 1, max_bytes, s_current_file); + + // Check for read errors + if (ferror(s_current_file)) + { + ESP_LOGE(TAG, "Error reading file: %s", s_current_filename); + fclose(s_current_file); + s_current_file = NULL; + s_current_filename[0] = '\0'; + return -4; // Read error + } + + // If fread returns 0 and it's not an error, it means EOF or an empty file. + // The file should be closed only when no more bytes can be read (i.e., bytes_read == 0). + if (bytes_read == 0) // Indicates EOF or empty file (and no ferror) + { + // Log before clearing s_current_filename. + // feof(s_current_file) should be true if end of file was actually reached. + if (feof(s_current_file)) + { + ESP_LOGI(TAG, "EOF reached for file: %s. Closing file.", s_current_filename); + } + else + { + ESP_LOGI(TAG, "Read 0 bytes from file: %s (possibly empty or already at EOF). Closing file.", + s_current_filename); + } + fclose(s_current_file); + s_current_file = NULL; + s_current_filename[0] = '\0'; + // Return 0 as per contract: "Returns 0 when the end of the file is reached." + } + // If bytes_read > 0, return the number of bytes read. + // The file remains open (even if EOF was hit during this read and bytes_read < max_bytes). + // The next call to storage_read for this file will then result in fread returning 0, + // which will then hit the (bytes_read == 0) condition above and close the file. + return bytes_read; +} diff --git a/data/capability.json b/data/capability.json index 2b17391..a922836 100644 --- a/data/capability.json +++ b/data/capability.json @@ -1,5 +1,5 @@ { - "module": "Miniature Town", + "module": "Lighthouse", "capabilities": [ { "id": 0, diff --git a/main/beacon.c b/main/beacon.c index badba11..c19a0ef 100644 --- a/main/beacon.c +++ b/main/beacon.c @@ -21,7 +21,7 @@ static LedMatrix ledMatrix = {.size = 64}; static SemaphoreHandle_t timer_semaphore; gptimer_handle_t gpTimer = NULL; -bool IRAM_ATTR timerCallback(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *userCtx) +bool IRAM_ATTR timer_callback(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *userCtx) { BaseType_t high_task_wakeup = pdFALSE; xSemaphoreGiveFromISR(timer_semaphore, &high_task_wakeup); @@ -32,7 +32,7 @@ bool IRAM_ATTR timerCallback(gptimer_handle_t timer, const gptimer_alarm_event_d return true; } -void timerEventTask(void *arg) +void timer_event_task(void *arg) { while (true) { @@ -53,7 +53,7 @@ void timerEventTask(void *arg) } } -esp_err_t initWled(void) +esp_err_t wled_init(void) { led_strip_config_t stripConfig = {.strip_gpio_num = CONFIG_WLED_DIN_PIN, .max_leds = ledMatrix.size, @@ -81,7 +81,7 @@ esp_err_t initWled(void) return ESP_OK; } -esp_err_t startBeacon(void) +esp_err_t beacon_start(void) { if (gpTimer == NULL) { @@ -100,7 +100,7 @@ esp_err_t startBeacon(void) return ret; } -esp_err_t stopBeacon(void) +esp_err_t beacon_stop(void) { if (gpTimer == NULL) { @@ -119,7 +119,7 @@ esp_err_t stopBeacon(void) return ret; } -esp_err_t initBeacon(void) +esp_err_t beacon_init(void) { esp_err_t ret = ESP_OK; @@ -134,7 +134,7 @@ esp_err_t initBeacon(void) goto exit; } - gptimer_event_callbacks_t callbacks = {.on_alarm = timerCallback}; + gptimer_event_callbacks_t callbacks = {.on_alarm = timer_callback}; ret = gptimer_register_event_callbacks(gpTimer, &callbacks, NULL); if (ret != ESP_OK) { @@ -158,7 +158,7 @@ esp_err_t initBeacon(void) goto cleanupEnabledTimer; } - BaseType_t taskCreated = xTaskCreate(timerEventTask, "timer_event_task", 4096, NULL, 10, NULL); + BaseType_t taskCreated = xTaskCreate(timer_event_task, "timer_event_task", 4096, NULL, 10, NULL); if (taskCreated != pdPASS) { ESP_LOGE(TAG, "Failed to create timer event task"); diff --git a/main/beacon.h b/main/beacon.h index 9225cfb..5158db6 100644 --- a/main/beacon.h +++ b/main/beacon.h @@ -12,7 +12,7 @@ * - ESP_OK: Initialization completed successfully. * - Error codes in case of failure, indicating the specific issue. */ -esp_err_t initBeacon(void); +esp_err_t beacon_init(void); /** * @brief Starts the beacon functionality for broadcasting signals. @@ -24,7 +24,7 @@ esp_err_t initBeacon(void); * - ESP_OK: Beacon started successfully. * - Error codes in case of failure, indicating the specific issue. */ -esp_err_t startBeacon(void); +esp_err_t beacon_start(void); /** * @brief Stops the beacon broadcasting functionality. @@ -36,7 +36,7 @@ esp_err_t startBeacon(void); * - ESP_OK: Broadcasting stopped successfully. * - Error codes in case of failure, indicating the specific issue. */ -esp_err_t stopBeacon(void); +esp_err_t beacon_stop(void); /** * @brief Initializes the WLED module. @@ -48,4 +48,4 @@ esp_err_t stopBeacon(void); * - ESP_OK: Initialization completed successfully. * - Error codes in case of failure, indicating the specific issue. */ -esp_err_t initWled(void); +esp_err_t wled_init(void); diff --git a/main/main.c b/main/main.c index c71095c..b148661 100644 --- a/main/main.c +++ b/main/main.c @@ -1,20 +1,26 @@ #include "beacon.h" +#include "persistence.h" +#include "remote_control.h" void app_main(void) { - if (initBeacon() != ESP_OK) + persistence_init("lighthouse"); + + if (beacon_init() != ESP_OK) { printf("Failed to initialize beacon"); return; } - if (initWled() != ESP_OK) + if (wled_init() != ESP_OK) { printf("Failed to initialize WLED"); return; } - if (startBeacon() != ESP_OK) + if (beacon_start() != ESP_OK) { printf("Failed to start beacon"); return; } + + remote_control_init(); }