diff --git a/src/main.c b/src/main.c index a698222..2cc2f01 100644 --- a/src/main.c +++ b/src/main.c @@ -105,10 +105,19 @@ static volatile int rock_minutes = 5; // 0 = continuous, 1-30 = timer static volatile int rock_intensity = 100; // 0-100% // Auto-renew rocking mode +#define AUTO_START_MINUTES 30 +#define PENDING_COMMAND_RETRY_INTERVAL_MS 3000 +#define ROCK_NOTIFY_TIMEOUT_SEC 15 + static volatile bool auto_renew_enabled = false; static volatile int64_t rock_start_time = 0; +static volatile int64_t last_rock_notify_time = 0; static volatile int auto_renew_duration = 120; // 2 hours default static volatile int auto_renew_threshold = 10; // Renew when 10 min left +static bool autostart_enabled = false; +static int autostart_intensity = 50; +static bool auto_start_sent = false; +static volatile bool processing_commands = false; // MQTT static esp_mqtt_client_handle_t mqtt_client = NULL; @@ -142,15 +151,29 @@ static EventGroupHandle_t s_wifi_event_group; #define WIFI_CONNECTED_BIT BIT0 static httpd_handle_t server = NULL; +static int clamp_int(int value, int min, int max); static void ble_app_scan(void); static int ble_gap_event(struct ble_gap_event *event, void *arg); static void read_all_characteristics(void); +static void read_drive_or_led_or_subscribe(void); +static void read_led_or_subscribe(void); +static void finish_initial_reads(void); static void process_pending_commands(void); static void mqtt_publish_state(void); static void mqtt_publish_discovery(void); static void auto_renew_task(void *arg); +static void pending_command_retry_task(void *arg); static void load_entity_names(void); static void save_entity_names(void); +static void save_drive_mode(void); +static void start_auto_renew_rocking(const char *source); +static void wifi_services_task(void *arg); + +static int clamp_int(int value, int min, int max) { + if (value < min) return min; + if (value > max) return max; + return value; +} static void wifi_event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) { if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { @@ -254,23 +277,26 @@ static int on_status_read(uint16_t ch, const struct ble_gatt_error *e, struct bl mqtt_publish_state(); // Update HA when battery changes } } + read_drive_or_led_or_subscribe(); return 0; } static int on_drive_read(uint16_t ch, const struct ble_gatt_error *e, struct ble_gatt_attr *a, void *arg) { ESP_LOGI(TAG, "on_drive_read: status=%d", e->status); if (e->status == 0 && a) { - uint8_t d[8]; + uint8_t d[8] = {0}; uint16_t l = OS_MBUF_PKTLEN(a->om); if (l > 8) l = 8; os_mbuf_copydata(a->om, 0, l, d); ESP_LOGI(TAG, "Mode raw: len=%d, [%02X %02X %02X %02X]", l, d[0], d[1], d[2], d[3]); - if (l >= 1) { + if (l >= 1 && d[0] >= 1 && d[0] <= 3) { drive_mode = d[0]; ESP_LOGI(TAG, "Mode: %d", drive_mode); + save_drive_mode(); mqtt_publish_state(); // Update HA when mode changes } } + read_led_or_subscribe(); return 0; } @@ -285,7 +311,7 @@ static int on_led_read(uint16_t ch, const struct ble_gatt_error *e, struct ble_g ESP_LOGI(TAG, "LEDs: %d", battery_leds); } } - process_pending_commands(); + finish_initial_reads(); return 0; } @@ -324,46 +350,110 @@ static void subscribe_to_notifications(void) { static void read_all_characteristics(void) { if (!ble_connected || !chars_discovered) return; - if (status_val_handle) ble_gattc_read(conn_handle, status_val_handle, on_status_read, NULL); - vTaskDelay(pdMS_TO_TICKS(100)); - if (battery_led_val_handle) ble_gattc_read(conn_handle, battery_led_val_handle, on_led_read, NULL); - vTaskDelay(pdMS_TO_TICKS(100)); - - // Subscribe to notifications for live updates + if (status_val_handle) { + int rc = ble_gattc_read(conn_handle, status_val_handle, on_status_read, NULL); + ESP_LOGI(TAG, "Status read rc=%d", rc); + if (rc == 0) return; + } + read_drive_or_led_or_subscribe(); +} + +static void read_drive_or_led_or_subscribe(void) { + if (!ble_connected || !chars_discovered) return; + if (drive_mode_val_handle) { + int rc = ble_gattc_read(conn_handle, drive_mode_val_handle, on_drive_read, NULL); + ESP_LOGI(TAG, "Mode read rc=%d", rc); + if (rc == 0) return; + } + read_led_or_subscribe(); +} + +static void read_led_or_subscribe(void) { + if (!ble_connected || !chars_discovered) return; + if (battery_led_val_handle) { + int rc = ble_gattc_read(conn_handle, battery_led_val_handle, on_led_read, NULL); + ESP_LOGI(TAG, "LED read rc=%d", rc); + if (rc == 0) return; + } + finish_initial_reads(); +} + +static void finish_initial_reads(void) { + if (!ble_connected || !chars_discovered) return; subscribe_to_notifications(); + if (autostart_enabled && !auto_start_sent) { + auto_start_sent = true; + rock_intensity = clamp_int(autostart_intensity, 0, 100); + vTaskDelay(pdMS_TO_TICKS(500)); + start_auto_renew_rocking("BLE connect"); + } else { + process_pending_commands(); + } +} + +static void start_auto_renew_rocking(const char *source) { + rock_minutes = AUTO_START_MINUTES; + auto_renew_duration = AUTO_START_MINUTES; + auto_renew_enabled = true; + pending_rock_stop = 0; + pending_rock_start = 1; + ESP_LOGI(TAG, "Auto mode start from %s: %d min, %d%%", source, rock_minutes, rock_intensity); + web_log_add("Auto mode start: %s", source); + if (ble_connected && chars_discovered) process_pending_commands(); + mqtt_publish_state(); } static void process_pending_commands(void) { + if (processing_commands) return; + processing_commands = true; ESP_LOGI(TAG, "process_pending: mode=%d, rock_start=%d, rock_stop=%d, connected=%d, chars=%d", pending_mode, pending_rock_start, pending_rock_stop, ble_connected, chars_discovered); - if (!ble_connected || !chars_discovered) return; + if (!ble_connected || !chars_discovered) { + processing_commands = false; + return; + } if (pending_mode > 0 && drive_mode_val_handle) { uint8_t m = pending_mode; - pending_mode = 0; ESP_LOGI(TAG, "Writing mode %d to handle %d (conn=%d)", m, drive_mode_val_handle, conn_handle); last_write_rc = ble_gattc_write_flat(conn_handle, drive_mode_val_handle, &m, 1, on_write, NULL); ESP_LOGI(TAG, "Write rc=%d", last_write_rc); if (last_write_rc == 0) { + pending_mode = 0; drive_mode = m; // Update local state on successful write + save_drive_mode(); mqtt_publish_state(); + } else { + web_log_add("Mode write busy: rc=%d", last_write_rc); } vTaskDelay(pdMS_TO_TICKS(300)); } if (pending_rock_start && rocking_val_handle) { - pending_rock_start = 0; // Format: [0x01, minutes (0=continuous), intensity%] uint8_t cmd[3] = {0x01, (uint8_t)rock_minutes, (uint8_t)rock_intensity}; ESP_LOGI(TAG, "Rock start: %d min, %d%%", rock_minutes, rock_intensity); - ble_gattc_write_flat(conn_handle, rocking_val_handle, cmd, 3, on_write, NULL); - is_rocking = true; - rock_start_time = esp_timer_get_time() / 1000000; // Set start time in seconds + last_write_rc = ble_gattc_write_flat(conn_handle, rocking_val_handle, cmd, 3, on_write, NULL); + ESP_LOGI(TAG, "Rock write rc=%d", last_write_rc); + if (last_write_rc == 0) { + pending_rock_start = 0; + is_rocking = true; + rock_start_time = esp_timer_get_time() / 1000000; // Set start time in seconds + last_rock_notify_time = rock_start_time; + } else { + web_log_add("Rock start busy: rc=%d", last_write_rc); + } } if (pending_rock_stop && rocking_val_handle) { - pending_rock_stop = 0; uint8_t cmd = 0x00; - ble_gattc_write_flat(conn_handle, rocking_val_handle, &cmd, 1, on_write, NULL); - is_rocking = false; + last_write_rc = ble_gattc_write_flat(conn_handle, rocking_val_handle, &cmd, 1, on_write, NULL); + ESP_LOGI(TAG, "Rock stop rc=%d", last_write_rc); + if (last_write_rc == 0) { + pending_rock_stop = 0; + is_rocking = false; + } else { + web_log_add("Rock stop busy: rc=%d", last_write_rc); + } } + processing_commands = false; } static int on_chr(uint16_t ch, const struct ble_gatt_error *e, const struct ble_gatt_chr *c, void *arg) { @@ -437,6 +527,7 @@ static void connect_to_priam(ble_addr_t *addr) { web_log_add("Connect failed: rc=%d", last_connect_rc); priam_found = false; have_candidate = false; + auto_start_sent = false; } } @@ -528,6 +619,7 @@ static int ble_gap_event(struct ble_gap_event *event, void *arg) { if (mode >= 1 && mode <= 3 && drive_mode != mode) { drive_mode = mode; ESP_LOGI(TAG, "Mode notify: %d", drive_mode); + save_drive_mode(); mqtt_publish_state(); } } else if (attr_handle == rocking_val_handle && len >= 3) { @@ -537,6 +629,7 @@ static int ble_gap_event(struct ble_gap_event *event, void *arg) { int time_left = data[1] | (data[2] << 8); bool was_rocking = is_rocking; is_rocking = (intensity > 0 || time_left > 0); + last_rock_notify_time = esp_timer_get_time() / 1000000; ESP_LOGI(TAG, "Rock notify: intensity=%d, time_left=%d", intensity, time_left); if (is_rocking != was_rocking) mqtt_publish_state(); } @@ -549,6 +642,7 @@ static int ble_gap_event(struct ble_gap_event *event, void *arg) { ble_connected = false; priam_found = false; have_candidate = false; + auto_start_sent = false; chars_discovered = false; battery_percent = -1; battery_leds = -1; @@ -628,7 +722,6 @@ static void ble_init(void) { if (nimble_port_init() != ESP_OK) return; ble_hs_cfg.sync_cb = ble_on_sync; ble_hs_cfg.reset_cb = ble_on_reset; - ble_svc_gap_device_name_set("EPriam-Bridge"); nimble_port_freertos_init(ble_host_task); } @@ -643,6 +736,15 @@ static void auto_renew_task(void *arg) { int remaining = (auto_renew_duration * 60) - (int)elapsed; ESP_LOGI(TAG, "Auto-renew check: elapsed=%llds, remaining=%ds", elapsed, remaining); + + if (last_rock_notify_time > 0 && (now - last_rock_notify_time) > ROCK_NOTIFY_TIMEOUT_SEC) { + ESP_LOGW(TAG, "No rock notifications for %llds, retrying start", now - last_rock_notify_time); + web_log_add("Rock notify timeout, retrying"); + last_rock_notify_time = now; + pending_rock_start = 1; + process_pending_commands(); + mqtt_publish_state(); + } // Renew when threshold minutes remaining if (remaining <= (auto_renew_threshold * 60) && remaining > 0) { @@ -657,6 +759,16 @@ static void auto_renew_task(void *arg) { } } +static void pending_command_retry_task(void *arg) { + while (1) { + vTaskDelay(pdMS_TO_TICKS(PENDING_COMMAND_RETRY_INTERVAL_MS)); + if (ble_connected && chars_discovered && + (pending_mode > 0 || pending_rock_start || pending_rock_stop)) { + process_pending_commands(); + } + } +} + // MQTT event handler static void mqtt_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data) { esp_mqtt_event_handle_t event = data; @@ -894,6 +1006,7 @@ static void load_entity_names(void) { nvs_handle_t nvs; if (nvs_open("epriam", NVS_READONLY, &nvs) == ESP_OK) { size_t len; + int32_t stored_i32; len = sizeof(name_device); nvs_get_str(nvs, "n_device", name_device, &len); len = sizeof(name_battery); nvs_get_str(nvs, "n_battery", name_battery, &len); len = sizeof(name_rocking); nvs_get_str(nvs, "n_rocking", name_rocking, &len); @@ -901,12 +1014,21 @@ static void load_entity_names(void) { len = sizeof(name_mode); nvs_get_str(nvs, "n_mode", name_mode, &len); len = sizeof(name_intensity); nvs_get_str(nvs, "n_intensity", name_intensity, &len); len = sizeof(name_connected); nvs_get_str(nvs, "n_connected", name_connected, &len); + if (nvs_get_i32(nvs, "autostart_en", &stored_i32) == ESP_OK) { + autostart_enabled = stored_i32 != 0; + } + if (nvs_get_i32(nvs, "autostart_int", &stored_i32) == ESP_OK) { + autostart_intensity = clamp_int(stored_i32, 0, 100); + } + if (nvs_get_i32(nvs, "drive_mode", &stored_i32) == ESP_OK && stored_i32 >= 1 && stored_i32 <= 3) { + drive_mode = stored_i32; + } nvs_close(nvs); - ESP_LOGI(TAG, "Loaded entity names from NVS"); + ESP_LOGI(TAG, "Loaded config from NVS"); } } -// Save entity names to NVS +// Save entity names and autostart settings to NVS static void save_entity_names(void) { nvs_handle_t nvs; if (nvs_open("epriam", NVS_READWRITE, &nvs) == ESP_OK) { @@ -917,9 +1039,11 @@ static void save_entity_names(void) { nvs_set_str(nvs, "n_mode", name_mode); nvs_set_str(nvs, "n_intensity", name_intensity); nvs_set_str(nvs, "n_connected", name_connected); + nvs_set_i32(nvs, "autostart_en", autostart_enabled ? 1 : 0); + nvs_set_i32(nvs, "autostart_int", clamp_int(autostart_intensity, 0, 100)); nvs_commit(nvs); nvs_close(nvs); - ESP_LOGI(TAG, "Saved entity names to NVS"); + ESP_LOGI(TAG, "Saved config to NVS"); } } @@ -939,15 +1063,26 @@ static void mqtt_init(void) { } } +static void save_drive_mode(void) { + if (drive_mode < 1 || drive_mode > 3) return; + nvs_handle_t nvs; + if (nvs_open("epriam", NVS_READWRITE, &nvs) == ESP_OK) { + nvs_set_i32(nvs, "drive_mode", drive_mode); + nvs_commit(nvs); + nvs_close(nvs); + } +} + static esp_err_t root_handler(httpd_req_t *req) { char batt[16]; if (battery_percent >= 0) snprintf(batt, 16, "%d%%", battery_percent); else strcpy(batt, "?"); const char* mode = drive_mode == 1 ? "ECO" : drive_mode == 2 ? "TOUR" : drive_mode == 3 ? "BOOST" : "?"; - const char* auto_status = auto_renew_enabled ? "on" : "off"; + const char* rock_status = auto_renew_enabled ? "Auto" : is_rocking ? "On" : "Off"; + const char* rock_status_class = is_rocking ? "on" : "off"; - static char r[3400]; + static char r[4096]; snprintf(r, sizeof(r), "
" "