#include "comms.h" #include "cJSON.h" #include "control_fsm.h" #include "esp_log.h" #include "esp_system.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "power_mgmt.h" #include "rf_433.h" #include "rtc.h" #include "sensors.h" #include "storage.h" #include "version.h" #include static const char *TAG = "COMMS"; /** * Build a JSON object containing complete system status */ cJSON* comms_handle_get(void) { //ESP_LOGI(TAG, "GET request"); rtc_reset_shutdown_timer(); // Create root JSON object cJSON *root = cJSON_CreateObject(); if (root == NULL) { ESP_LOGE(TAG, "Failed to create JSON object"); return NULL; } // Add basic system info cJSON_AddStringToObject(root, "build_version", FIRMWARE_VERSION); cJSON_AddStringToObject(root, "build_date", BUILD_DATE); cJSON_AddNumberToObject(root, "time", (double)rtc_get_s()); cJSON_AddBoolToObject(root, "rtc_set", rtc_is_set()); cJSON_AddNumberToObject(root, "state", fsm_get_state()); cJSON_AddNumberToObject(root, "voltage", get_battery_V()); cJSON_AddNumberToObject(root, "remaining_dist", fsm_get_remaining_distance()); cJSON_AddNumberToObject(root, "next_alarm", (double)rtc_get_next_alarm_s()); cJSON_AddNumberToObject(root, "board_rev", hw_get_board_rev()); cJSON_AddNumberToObject(root, "fsm_error", fsm_get_error()); // Structured error flags (match LED error code bits) cJSON *errors = cJSON_CreateObject(); bool efuse_trip = efuse_get(BRIDGE_AUX) || efuse_get(BRIDGE_JACK) || efuse_get(BRIDGE_DRIVE); float bat_v = get_battery_V(); float low_v = get_param_value_t(PARAM_LOW_PROTECTION_V).f32; bool low_bat = (bat_v > 0 && bat_v < low_v); bool safety_trip = !get_is_safe(); bool leash_hit = (fsm_get_remaining_distance() <= 0); cJSON_AddBoolToObject(errors, "efuse_aux", efuse_get(BRIDGE_AUX) != 0); cJSON_AddBoolToObject(errors, "efuse_jack", efuse_get(BRIDGE_JACK) != 0); cJSON_AddBoolToObject(errors, "efuse_drive", efuse_get(BRIDGE_DRIVE) != 0); cJSON_AddBoolToObject(errors, "low_battery", low_bat); cJSON_AddBoolToObject(errors, "rtc_not_set", !rtc_is_set()); cJSON_AddBoolToObject(errors, "safety_trip", safety_trip); cJSON_AddBoolToObject(errors, "leash_hit", leash_hit); // LED error code: bit0=efuse/battery, bit1=RTC, bit2=safety/leash uint8_t led_code = 0; if (efuse_trip || low_bat) led_code |= 0b001; if (!rtc_is_set()) led_code |= 0b010; if (safety_trip || leash_hit) led_code |= 0b100; if (fsm_get_error() != 0 && led_code == 0) led_code = 0b111; cJSON_AddNumberToObject(errors, "led_code", led_code); cJSON_AddItemToObject(root, "errors", errors); // Status message array cJSON *msg_array = cJSON_CreateArray(); if (msg_array == NULL) { ESP_LOGE(TAG, "Failed to create msg array"); cJSON_Delete(root); return NULL; } switch(fsm_get_state()) { case STATE_IDLE: cJSON_AddItemToArray(msg_array, cJSON_CreateString("IDLE")); break; case STATE_UNDO_JACK_START: cJSON_AddItemToArray(msg_array, cJSON_CreateString("CANCELLING MOVE")); break; default: cJSON_AddItemToArray(msg_array, cJSON_CreateString("MOVING...")); break; } if (leash_hit) cJSON_AddItemToArray(msg_array, cJSON_CreateString("DISTANCE LIMIT HIT")); if (efuse_get(BRIDGE_AUX)) cJSON_AddItemToArray(msg_array, cJSON_CreateString("AUX EFUSE TRIP")); if (efuse_get(BRIDGE_JACK)) cJSON_AddItemToArray(msg_array, cJSON_CreateString("JACK EFUSE TRIP")); if (efuse_get(BRIDGE_DRIVE)) cJSON_AddItemToArray(msg_array, cJSON_CreateString("DRIVE EFUSE TRIP")); if (low_bat) cJSON_AddItemToArray(msg_array, cJSON_CreateString("LOW BATTERY")); if (!rtc_is_set()) cJSON_AddItemToArray(msg_array, cJSON_CreateString("CLOCK NOT SET")); if (safety_trip) cJSON_AddItemToArray(msg_array, cJSON_CreateString("SAFETY SENSOR BREAK")); cJSON_AddItemToObject(root, "msg", msg_array); // Add parameters object cJSON *parameters = cJSON_CreateObject(); if (parameters == NULL) { ESP_LOGE(TAG, "Failed to create parameters object"); cJSON_Delete(root); return NULL; } // Add all parameters for (param_idx_t i = 0; i < NUM_PARAMS; i++) { const char *name = get_param_name(i); param_value_t value = get_param_value_t(i); switch (get_param_type(i)) { case PARAM_TYPE_f32: cJSON_AddNumberToObject(parameters, name, value.f32); break; case PARAM_TYPE_f64: cJSON_AddNumberToObject(parameters, name, value.f64); break; case PARAM_TYPE_i32: cJSON_AddNumberToObject(parameters, name, value.i32); break; case PARAM_TYPE_i16: cJSON_AddNumberToObject(parameters, name, value.i16); break; case PARAM_TYPE_u32: cJSON_AddNumberToObject(parameters, name, value.u32); break; case PARAM_TYPE_u16: cJSON_AddNumberToObject(parameters, name, value.u16); break; case PARAM_TYPE_str: cJSON_AddStringToObject(parameters, name, get_param_string(i)); break; default: cJSON_AddNullToObject(parameters, name); break; } } cJSON_AddItemToObject(root, "parameters", parameters); return root; } /** * Process a POST request with JSON data */ esp_err_t comms_handle_post(cJSON *root, cJSON **response_json) { //ESP_LOGI(TAG, "POST request"); if (root == NULL || response_json == NULL) { ESP_LOGE(TAG, "Invalid arguments to comms_handle_post"); return ESP_FAIL; } rtc_reset_shutdown_timer(); bool cmd_executed = false; bool sleep_requested = false; bool reboot_requested = false; bool wifi_params_changed = false; bool wifi_restart_requested = false; const char *error_msg = NULL; int params_updated = 0; int params_failed = 0; // Process time if present cJSON *time = cJSON_GetObjectItem(root, "time"); if (cJSON_IsNumber(time)) { int64_t new_time = (int64_t)cJSON_GetNumberValue(time); ESP_LOGI(TAG, "Setting time to %lld", (long long)new_time); rtc_set_s(new_time); } // Process remaining_dist if present cJSON *remaining_dist = cJSON_GetObjectItem(root, "remaining_dist"); if (cJSON_IsNumber(remaining_dist)) { int64_t new_dist = (int64_t)cJSON_GetNumberValue(remaining_dist); ESP_LOGI(TAG, "Setting remaining_dist to %lld", new_dist); fsm_set_remaining_distance(new_dist); } // Process command if present cJSON *cmd = cJSON_GetObjectItem(root, "cmd"); if (cJSON_IsString(cmd)) { const char *cmd_str = cmd->valuestring; // ESP_LOGI(TAG, "Executing command: %s", cmd_str); if (strcmp(cmd_str, "start") == 0) { fsm_request(FSM_CMD_START); cmd_executed = true; } else if (strcmp(cmd_str, "stop") == 0) { fsm_request(FSM_CMD_STOP); cmd_executed = true; } else if (strcmp(cmd_str, "undo") == 0) { fsm_request(FSM_CMD_UNDO); cmd_executed = true; } else if (strcmp(cmd_str, "fwd") == 0) { pulse_override(FSM_OVERRIDE_DRIVE_FWD); cmd_executed = true; } else if (strcmp(cmd_str, "rev") == 0) { pulse_override(FSM_OVERRIDE_DRIVE_REV); cmd_executed = true; } else if (strcmp(cmd_str, "up") == 0) { pulse_override(FSM_OVERRIDE_JACK_UP); cmd_executed = true; } else if (strcmp(cmd_str, "down") == 0) { pulse_override(FSM_OVERRIDE_JACK_DOWN); cmd_executed = true; } else if (strcmp(cmd_str, "aux") == 0) { pulse_override(FSM_OVERRIDE_AUX); cmd_executed = true; } else if (strcmp(cmd_str, "reboot") == 0) { reboot_requested = true; cmd_executed = true; } else if (strcmp(cmd_str, "sleep") == 0) { sleep_requested = true; cmd_executed = true; } else if (strcmp(cmd_str, "rf_clear_temp") == 0) { rf_433_clear_temp_keycodes(); cmd_executed = true; } else if (strcmp(cmd_str, "rf_disable") == 0) { rf_433_disable_controls(); cmd_executed = true; } else if (strcmp(cmd_str, "rf_enable") == 0) { rf_433_enable_controls(); cmd_executed = true; } else if (strcmp(cmd_str, "rf_learn") == 0) { // Start learning for a specific channel cJSON *channel = cJSON_GetObjectItem(root, "channel"); if (cJSON_IsNumber(channel)) { int ch = (int)cJSON_GetNumberValue(channel); if (ch >= 0 && ch < NUM_RF_BUTTONS) { rf_433_learn_keycode(ch); cmd_executed = true; } else if (ch == -1) { rf_433_cancel_learn_keycode(); cmd_executed = true; } else { error_msg = "Invalid channel number"; } } else { error_msg = "rf_learn requires channel parameter"; } } else if (strcmp(cmd_str, "rf_set_temp") == 0) { cJSON *index = cJSON_GetObjectItem(root, "index"); cJSON *code = cJSON_GetObjectItem(root, "code"); if (cJSON_IsNumber(index) && cJSON_IsNumber(code)) { int idx = (int)cJSON_GetNumberValue(index); int32_t rf_code = (int32_t)cJSON_GetNumberValue(code); rf_433_set_temp_keycode(idx, rf_code); cmd_executed = true; } else { error_msg = "rf_set_temp requires index and code parameters"; } } else if (strcmp(cmd_str, "rf_status") == 0) { // Return current temp keycodes cJSON *response = cJSON_CreateObject(); cJSON *codes_array = cJSON_CreateArray(); for (int i = 0; i < 4; i++) { // Only return first 4 for web UI int32_t code = rf_433_get_temp_keycode(i); cJSON_AddItemToArray(codes_array, cJSON_CreateNumber(code)); } cJSON_AddItemToObject(response, "codes", codes_array); *response_json = response; return ESP_OK; } else if (strcmp(cmd_str, "cal_jack_start") == 0) { fsm_request(FSM_CMD_CALIBRATE_JACK_PREP); ESP_LOGI(TAG, "FSM_CMD_CALIBRATE_JACK_PREP"); cmd_executed = true; } else if (strcmp(cmd_str, "cal_drive_start") == 0) { fsm_request(FSM_CMD_CALIBRATE_DRIVE_PREP); ESP_LOGI(TAG, "FSM_CMD_CALIBRATE_DRIVE_PREP"); cmd_executed = true; } else if (strcmp(cmd_str, "cal_get") == 0) { ESP_LOGI(TAG, "CAL_GET"); // Build JSON response with calibration values cJSON *response = cJSON_CreateObject(); cJSON_AddItemToObject(response, "e", cJSON_CreateNumber(fsm_get_cal_e())); cJSON_AddItemToObject(response, "t", cJSON_CreateNumber(fsm_get_cal_t())); *response_json = response; return ESP_OK; } else if (strcmp(cmd_str, "set_board_rev") == 0) { cJSON *rev = cJSON_GetObjectItem(root, "board_rev"); if (cJSON_IsNumber(rev)) { uint16_t r = (uint16_t)cJSON_GetNumberValue(rev); if (hw_set_board_rev(r) == ESP_OK) { ESP_LOGI(TAG, "Board rev set to %u", r); cmd_executed = true; } else { error_msg = "Failed to write board_rev to NVS"; } } else { error_msg = "set_board_rev requires board_rev parameter"; } } else { ESP_LOGW(TAG, "Unknown command: %s", cmd_str); error_msg = "Unknown command"; } } // Process batch parameter updates if present cJSON *parameters = cJSON_GetObjectItem(root, "parameters"); if (cJSON_IsObject(parameters)) { // Process individual parameter updates (direct key-value pairs) cJSON *item = NULL; cJSON_ArrayForEach(item, parameters) { const char *key = item->string; ESP_LOGI(TAG, "Processing parameter: %s", key); // Find parameter index by name param_idx_t param_idx = NUM_PARAMS; for (param_idx_t i = 0; i < NUM_PARAMS; i++) { if (strcmp(get_param_name(i), key) == 0) { param_idx = i; break; } } if (param_idx == NUM_PARAMS) { ESP_LOGW(TAG, "Unknown parameter: %s", key); params_failed++; continue; } if (param_idx == PARAM_WIFI_SSID || param_idx == PARAM_WIFI_PASS || param_idx == PARAM_WIFI_CHANNEL || param_idx == PARAM_NET_SSID || param_idx == PARAM_NET_PASS) { wifi_params_changed = true; } cJSON *value_json = cJSON_GetObjectItem(parameters, key); // Set parameter value based on type switch (get_param_type(param_idx)) { case PARAM_TYPE_f32: if (cJSON_IsNumber(value_json)) { set_param_value_t(param_idx, (param_value_t){.f32 = value_json->valuedouble}); params_updated++; } else { ESP_LOGW(TAG, "Type mismatch for parameter: %s", key); params_failed++; } break; case PARAM_TYPE_f64: if (cJSON_IsNumber(value_json)) { set_param_value_t(param_idx, (param_value_t){.f64 = value_json->valuedouble}); params_updated++; } else { ESP_LOGW(TAG, "Type mismatch for parameter: %s", key); params_failed++; } break; case PARAM_TYPE_i32: if (cJSON_IsNumber(value_json)) { set_param_value_t(param_idx, (param_value_t){.i32 = value_json->valueint}); params_updated++; } else { ESP_LOGW(TAG, "Type mismatch for parameter: %s", key); params_failed++; } break; case PARAM_TYPE_i16: if (cJSON_IsNumber(value_json)) { set_param_value_t(param_idx, (param_value_t){.i16 = value_json->valueint}); params_updated++; } else { ESP_LOGW(TAG, "Type mismatch for parameter: %s", key); params_failed++; } break; case PARAM_TYPE_u32: if (cJSON_IsNumber(value_json)) { set_param_value_t(param_idx, (param_value_t){.u32 = value_json->valueint}); params_updated++; } else { ESP_LOGW(TAG, "Type mismatch for parameter: %s", key); params_failed++; } break; case PARAM_TYPE_u16: if (cJSON_IsNumber(value_json)) { set_param_value_t(param_idx, (param_value_t){.u16 = value_json->valueint}); params_updated++; } else { ESP_LOGW(TAG, "Type mismatch for parameter: %s", key); params_failed++; } break; case PARAM_TYPE_str: if (cJSON_IsString(value_json)) { set_param_string(param_idx, value_json->valuestring); params_updated++; } else { ESP_LOGW(TAG, "Type mismatch for parameter: %s", key); params_failed++; } break; default: ESP_LOGW(TAG, "Unknown type for parameter: %s", key); params_failed++; break; } } if (params_updated > 0) { rtc_schedule_next_alarm(); commit_params(); if (wifi_params_changed) { ESP_LOGI(TAG, "WiFi params changed — restarting WiFi AP"); wifi_restart_requested = true; } } } // Build response cJSON *response = cJSON_CreateObject(); if (response == NULL) { ESP_LOGE(TAG, "Failed to create response JSON"); return ESP_FAIL; } if (wifi_restart_requested) { cJSON_AddStringToObject(response, "status", "ok"); cJSON_AddStringToObject(response, "message", "Restarting WiFi AP..."); cJSON_AddBoolToObject(response, "wifi_restart", true); *response_json = response; return ESP_OK; } if (reboot_requested) { cJSON_AddStringToObject(response, "status", "ok"); cJSON_AddStringToObject(response, "message", "Rebooting..."); cJSON_AddBoolToObject(response, "reboot", true); *response_json = response; return ESP_OK; } if (sleep_requested) { cJSON_AddStringToObject(response, "status", "ok"); cJSON_AddStringToObject(response, "message", "Sleeping..."); cJSON_AddBoolToObject(response, "sleep", true); *response_json = response; return ESP_OK; } if (error_msg != NULL) { cJSON_AddStringToObject(response, "status", "error"); cJSON_AddStringToObject(response, "message", error_msg); *response_json = response; return ESP_FAIL; } cJSON_AddStringToObject(response, "status", "ok"); cJSON_AddNumberToObject(response, "params_updated", params_updated); cJSON_AddNumberToObject(response, "params_failed", params_failed); cJSON_AddBoolToObject(response, "cmd_executed", cmd_executed); *response_json = response; return ESP_OK; }