SC-F001 way better logging and parameters. not integrated yet but.
This commit is contained in:
10
main/CMakeLists.txt
Normal file
10
main/CMakeLists.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
# See the build system documentation in IDF programming guide
|
||||
# for more information about component CMakeLists.txt files.
|
||||
|
||||
idf_component_register(
|
||||
SRCS main.c i2c.c rtc.c storage.c uart_comms.c # list the source files of this component
|
||||
INCLUDE_DIRS # optional, add here public include directories
|
||||
PRIV_INCLUDE_DIRS # optional, add here private include directories
|
||||
REQUIRES # optional, list the public requirements (component names)
|
||||
PRIV_REQUIRES # optional, list the private requirements
|
||||
)
|
||||
0
main/Kconfig.projbuild
Normal file
0
main/Kconfig.projbuild
Normal file
323
main/control_fsm.c
Normal file
323
main/control_fsm.c
Normal file
@@ -0,0 +1,323 @@
|
||||
/*
|
||||
* control_fsm.c
|
||||
*
|
||||
* Created on: Nov 10, 2025
|
||||
* Author: Thad
|
||||
*/
|
||||
|
||||
#include "control_fsm.h"
|
||||
#include "esp_task_wdt.h"
|
||||
#include "esp_timer.h"
|
||||
#include "power_mgmt.h"
|
||||
#include "rtc_wdt.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "storage.h"
|
||||
#include "rtc.h"
|
||||
#include "sensors.h"
|
||||
|
||||
#define TRANSITION_DELAY_US 1000000
|
||||
|
||||
static QueueHandle_t fsm_cmd_queue = NULL;
|
||||
|
||||
uint8_t relay_pins[N_RELAYS] = {
|
||||
GPIO_NUM_13, // A1
|
||||
GPIO_NUM_14, // B1
|
||||
GPIO_NUM_15, // A2
|
||||
GPIO_NUM_16, // B2
|
||||
GPIO_NUM_17, // A3
|
||||
GPIO_NUM_18 // B3
|
||||
};
|
||||
|
||||
bool relay_states[N_RELAYS] = {false};
|
||||
int64_t override_times[N_RELAYS] = {-1};
|
||||
bool enabled = false;
|
||||
|
||||
void setRelay(int8_t relay, bool state) {
|
||||
relay_states[relay] = state;
|
||||
}
|
||||
|
||||
void driveRelays() {
|
||||
for (uint8_t i=0; i<N_RELAYS; i++) {
|
||||
if (efuse_is_tripped(i/2))
|
||||
gpio_set_level(relay_pins[i], 0);
|
||||
else
|
||||
gpio_set_level(relay_pins[i], relay_states[i]);
|
||||
}
|
||||
}
|
||||
|
||||
int8_t bridge_polarities[3] = {
|
||||
+1,
|
||||
-1,
|
||||
-1
|
||||
};
|
||||
void setBridge(bridge_t bridge, int8_t dir) {
|
||||
dir *= bridge_polarities[bridge];
|
||||
setRelay(bridge*2+0, dir<0);
|
||||
setRelay(bridge*2+1, dir>0);
|
||||
}
|
||||
|
||||
|
||||
int8_t get_bridge_state(bridge_t bridge) {
|
||||
if (relay_states[bridge*2 + 0]) return +1;
|
||||
if (relay_states[bridge*2 + 1]) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
volatile fsm_state_t current_state = STATE_IDLE;
|
||||
volatile int64_t current_time = 0;
|
||||
volatile bool start_running_request = false;
|
||||
|
||||
fsm_state_t fsm_get_state() {
|
||||
return current_state;
|
||||
}
|
||||
|
||||
static uint64_t timer_end = 0;
|
||||
static uint64_t timer_start = 0;
|
||||
static inline void set_timer(uint64_t us) {
|
||||
timer_end = current_time + us;
|
||||
timer_start = current_time;
|
||||
}
|
||||
static inline bool timer_done() { return current_time >= timer_end; }
|
||||
|
||||
void pulseOverride(bridge_t bridge, int8_t dir, int64_t pulse) {
|
||||
if (dir < 0)
|
||||
override_times[bridge*2 + (bridge_polarities[bridge]>0?0:1)] = esp_timer_get_time() + pulse;
|
||||
if (dir > 0)
|
||||
override_times[bridge*2 + (bridge_polarities[bridge]>0?1:0)] = esp_timer_get_time() + pulse;
|
||||
}
|
||||
|
||||
void fsm_begin_auto_move() {
|
||||
if (current_state == STATE_IDLE)
|
||||
current_state = STATE_MOVE_START_DELAY;
|
||||
set_timer(TRANSITION_DELAY_US);
|
||||
}
|
||||
|
||||
void fsm_request(fsm_cmd_t cmd)
|
||||
{
|
||||
if (fsm_cmd_queue != NULL)
|
||||
xQueueSend(fsm_cmd_queue, &cmd, 0); // Safe from any context
|
||||
}
|
||||
|
||||
int8_t fsm_get_current_progress(int8_t denominator) {
|
||||
int8_t x = 0;
|
||||
switch (current_state) {
|
||||
case STATE_DRIVE:
|
||||
case STATE_JACK_UP:
|
||||
case STATE_JACK_DOWN:
|
||||
case STATE_MOVE_START_DELAY:
|
||||
case STATE_DRIVE_START_DELAY:
|
||||
case STATE_DRIVE_END_DELAY:
|
||||
case STATE_UNDO_JACK:
|
||||
if (timer_end != timer_start)
|
||||
x = (current_time-timer_start)*denominator/(timer_end-timer_start);
|
||||
break;
|
||||
case STATE_UNDO_JACK_START:
|
||||
x = 0;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (x<0) x=0;
|
||||
if (x>denominator-1) x=denominator-1;
|
||||
return x;
|
||||
}
|
||||
|
||||
#define JACK_TIME (uint64_t)get_param_i8("jack_dist")*get_param_u32("jack_mspi")*1000
|
||||
#define DRIVE_TIME (uint64_t)get_param_i8("drive_dist")*get_param_u32("drive_mspf")*1000
|
||||
#define DRIVE_DIST ((int32_t)(get_param_i8("drive_dist")))*((int32_t)get_param_u32("drive_tpdf"))/10
|
||||
|
||||
void control_task(void *param) {
|
||||
esp_task_wdt_add(NULL);
|
||||
|
||||
TickType_t xLastWakeTime = xTaskGetTickCount();
|
||||
const TickType_t xFrequency = pdMS_TO_TICKS(50);
|
||||
enabled = true;
|
||||
|
||||
|
||||
|
||||
for (size_t i = 0; i < N_RELAYS; ++i) {
|
||||
gpio_reset_pin( relay_pins[i]);
|
||||
gpio_set_direction(relay_pins[i], GPIO_MODE_OUTPUT);
|
||||
gpio_set_level( relay_pins[i], 0); // Force low
|
||||
gpio_hold_dis( relay_pins[i]); // CRITICAL: Allow control after wake
|
||||
}
|
||||
|
||||
while (enabled) {
|
||||
vTaskDelayUntil(&xLastWakeTime, xFrequency);
|
||||
current_time = esp_timer_get_time();
|
||||
|
||||
fsm_cmd_t cmd;
|
||||
while (xQueueReceive(fsm_cmd_queue, &cmd, 0) == pdTRUE) {
|
||||
switch (cmd) {
|
||||
case FSM_CMD_STOP:
|
||||
if (current_state != STATE_IDLE &&
|
||||
current_state != STATE_UNDO_JACK_START &&
|
||||
current_state != STATE_UNDO_JACK) {
|
||||
current_state = STATE_IDLE;
|
||||
}
|
||||
break;
|
||||
case FSM_CMD_UNDO:
|
||||
if (current_state != STATE_IDLE &&
|
||||
current_state != STATE_UNDO_JACK_START &&
|
||||
current_state != STATE_UNDO_JACK) {
|
||||
current_state = STATE_UNDO_JACK_START;
|
||||
}
|
||||
break;
|
||||
case FSM_CMD_SHUTDOWN:
|
||||
enabled = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!enabled) break;
|
||||
|
||||
|
||||
|
||||
// State transitions
|
||||
switch (current_state) {
|
||||
case STATE_IDLE:
|
||||
break;
|
||||
case STATE_MOVE_START_DELAY:
|
||||
if (timer_done()) {
|
||||
current_state = STATE_JACK_UP;
|
||||
set_timer(JACK_TIME);
|
||||
}
|
||||
break;
|
||||
case STATE_JACK_UP:
|
||||
if (timer_done()) {
|
||||
current_state = STATE_DRIVE_START_DELAY;
|
||||
set_timer(TRANSITION_DELAY_US);
|
||||
}
|
||||
if (efuse_is_tripped(BRIDGE_JACK)) {
|
||||
current_state = STATE_UNDO_JACK_START;
|
||||
}
|
||||
break;
|
||||
case STATE_DRIVE_START_DELAY:
|
||||
if (timer_done()) {
|
||||
current_state = STATE_DRIVE;
|
||||
set_timer(DRIVE_TIME);
|
||||
set_sensor_counter(SENSOR_DRIVE, -DRIVE_DIST);
|
||||
}
|
||||
break;
|
||||
case STATE_DRIVE:
|
||||
if (timer_done() || get_sensor_counter(SENSOR_DRIVE) > 0) {
|
||||
current_state = STATE_DRIVE_END_DELAY;
|
||||
set_timer(TRANSITION_DELAY_US);
|
||||
}
|
||||
if (efuse_is_tripped(BRIDGE_DRIVE)) {
|
||||
current_state = STATE_UNDO_JACK_START;
|
||||
}
|
||||
break;
|
||||
case STATE_DRIVE_END_DELAY:
|
||||
if (timer_done()) {
|
||||
current_state = STATE_JACK_DOWN;
|
||||
set_timer(JACK_TIME);
|
||||
}
|
||||
case STATE_JACK_DOWN:
|
||||
if (timer_done() || get_sensor(SENSOR_JACK)) {
|
||||
current_state = STATE_IDLE;
|
||||
}
|
||||
|
||||
// assume we hit something hard and should stop
|
||||
if (efuse_is_tripped(BRIDGE_JACK)) {
|
||||
current_state = STATE_IDLE;
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case STATE_UNDO_JACK_START:
|
||||
// wait for e-fuse to un-trip
|
||||
if (!efuse_is_tripped(BRIDGE_JACK)) {
|
||||
current_state = STATE_UNDO_JACK;
|
||||
set_timer(JACK_TIME);
|
||||
}
|
||||
break;
|
||||
case STATE_UNDO_JACK:
|
||||
if (timer_done() || get_sensor(SENSOR_JACK)) {
|
||||
current_state = STATE_IDLE;
|
||||
}
|
||||
|
||||
// assume we are jacked up all the way (e.g. sensor broke) and should stop
|
||||
if (efuse_is_tripped(BRIDGE_JACK)) {
|
||||
current_state = STATE_IDLE;
|
||||
}
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
/*
|
||||
int64_t elapsed_t = (current_time-timer_start);
|
||||
int64_t total_t = (timer_end-timer_start);
|
||||
int32_t ticks = get_sensor_counter(SENSOR_DRIVE);
|
||||
ESP_LOGI("FSM", "[%d] %lld / %lld ms, %ld ticks", current_state, (long long) elapsed_t, (long long) total_t, (long) ticks);
|
||||
*/
|
||||
// Output control
|
||||
switch (current_state) {
|
||||
case STATE_IDLE:
|
||||
//ESP_LOGI("FSM", "IDLE @ %lld", current_time);
|
||||
for (uint8_t i = 0; i < N_RELAYS; ++i) {
|
||||
//ESP_LOGI("FSM", "t[%d] %lld", i, override_times[i]);
|
||||
bool active = override_times[i] > current_time;
|
||||
if (active) reset_shutdown_timer();
|
||||
|
||||
// prohibit movement past jack limit switch
|
||||
if (i == BRIDGE_JACK*2+(bridge_polarities[BRIDGE_JACK]>0?0:1) && get_sensor(SENSOR_JACK))
|
||||
setRelay(i, false);
|
||||
else
|
||||
setRelay(i, active);
|
||||
//if (active) ESP_LOGI("FSM", "RUN CHANNEL %d (%lld %c %lld)", i, (long long) override_times[i], active ? '>':'<', (long long) current_time);
|
||||
|
||||
}
|
||||
break;
|
||||
case STATE_JACK_UP:
|
||||
setBridge(BRIDGE_DRIVE, 0);
|
||||
setBridge(BRIDGE_JACK, +1);
|
||||
setBridge(BRIDGE_AUX, +1);
|
||||
reset_shutdown_timer();
|
||||
break;
|
||||
case STATE_DRIVE:
|
||||
setBridge(BRIDGE_DRIVE, +1);
|
||||
setBridge(BRIDGE_JACK, 0);
|
||||
setBridge(BRIDGE_AUX, +1);
|
||||
reset_shutdown_timer();
|
||||
break;
|
||||
case STATE_UNDO_JACK:
|
||||
case STATE_JACK_DOWN:
|
||||
setBridge(BRIDGE_DRIVE, 0);
|
||||
setBridge(BRIDGE_JACK, -1);
|
||||
setBridge(BRIDGE_AUX, +1);
|
||||
reset_shutdown_timer();
|
||||
break;
|
||||
case STATE_UNDO_JACK_START:
|
||||
case STATE_DRIVE_START_DELAY:
|
||||
case STATE_DRIVE_END_DELAY:
|
||||
setBridge(BRIDGE_DRIVE, 0);
|
||||
setBridge(BRIDGE_JACK, 0);
|
||||
setBridge(BRIDGE_AUX, +1);
|
||||
reset_shutdown_timer();
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
driveRelays();
|
||||
|
||||
esp_task_wdt_reset();
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < N_RELAYS; ++i) {
|
||||
gpio_set_level(relay_pins[i], 0);
|
||||
gpio_hold_en(relay_pins[i]);
|
||||
}
|
||||
|
||||
if (fsm_cmd_queue != NULL) {
|
||||
vQueueDelete(fsm_cmd_queue);
|
||||
fsm_cmd_queue = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
void start_fsm() {
|
||||
if (fsm_cmd_queue == NULL) {
|
||||
fsm_cmd_queue = xQueueCreate(8, sizeof(fsm_cmd_t));
|
||||
}
|
||||
xTaskCreate(control_task, "FSM", 4096, NULL, 5, NULL);
|
||||
}
|
||||
47
main/control_fsm.h
Normal file
47
main/control_fsm.h
Normal file
@@ -0,0 +1,47 @@
|
||||
#ifndef MAIN_CONTROL_FSM_H_
|
||||
#define MAIN_CONTROL_FSM_H_
|
||||
|
||||
#include "freertos/FreeRTOS.h" // Must be FIRST
|
||||
#include "freertos/projdefs.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/queue.h"
|
||||
|
||||
|
||||
typedef enum { FSM_CMD_STOP, FSM_CMD_UNDO, FSM_CMD_SHUTDOWN} fsm_cmd_t;
|
||||
|
||||
typedef enum {
|
||||
STATE_IDLE = 0,
|
||||
STATE_MOVE_START_DELAY = 1,
|
||||
STATE_JACK_UP = 2,
|
||||
STATE_DRIVE_START_DELAY = 3,
|
||||
STATE_DRIVE = 4,
|
||||
STATE_DRIVE_END_DELAY = 5,
|
||||
STATE_JACK_DOWN = 6,
|
||||
STATE_UNDO_JACK = 7,
|
||||
STATE_UNDO_JACK_START = 8,
|
||||
} fsm_state_t;
|
||||
|
||||
typedef enum {
|
||||
BRIDGE_AUX = 0,
|
||||
BRIDGE_JACK = 1,
|
||||
BRIDGE_DRIVE = 2,
|
||||
} bridge_t;
|
||||
|
||||
#define N_RELAYS 6
|
||||
#define N_BRIDGES 3
|
||||
|
||||
void pulseOverride(bridge_t bridge, int8_t dir, int64_t pulse);
|
||||
|
||||
void start_fsm();
|
||||
|
||||
void fsm_request(fsm_cmd_t cmd);
|
||||
|
||||
void fsm_begin_auto_move();
|
||||
|
||||
int8_t fsm_get_current_progress(int8_t remainder);
|
||||
|
||||
fsm_state_t fsm_get_state();
|
||||
|
||||
int8_t get_bridge_state(bridge_t bridge);
|
||||
|
||||
#endif /* MAIN_CONTROL_FSM_H_ */
|
||||
328
main/filemgmt.c
Normal file
328
main/filemgmt.c
Normal file
@@ -0,0 +1,328 @@
|
||||
/*
|
||||
* filemgmt.c
|
||||
*
|
||||
* Created on: Nov 20, 2025
|
||||
* Author: Thad
|
||||
*/
|
||||
|
||||
#include "filemgmt.h"
|
||||
#include "endian.h"
|
||||
#include "esp_log.h"
|
||||
#include <stdio.h>
|
||||
#include <inttypes.h>
|
||||
#include <string.h>
|
||||
#include <sys/unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/event_groups.h"
|
||||
#include "esp_system.h"
|
||||
#include "esp_mac.h" // for MACSTR
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_event.h"
|
||||
#include "esp_err.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_vfs.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "esp_vfs_fat.h"
|
||||
#include "esp_littlefs.h"
|
||||
#include "ftp.h"
|
||||
#include "control_fsm.h"
|
||||
#include "power_mgmt.h"
|
||||
#include "rtc.h"
|
||||
#include "sensors.h"
|
||||
|
||||
|
||||
const char* partition_label = "storage";
|
||||
const char *TAG = "MAIN";
|
||||
const char *mount_point = "/root";
|
||||
|
||||
RTC_DATA_ATTR bool find_new_filename = true;
|
||||
|
||||
void start_new_log_file() {
|
||||
find_new_filename = true;
|
||||
}
|
||||
|
||||
EventGroupHandle_t xEventTask;
|
||||
int FTP_TASK_FINISH_BIT = BIT2;
|
||||
|
||||
esp_err_t start_filesystem() {
|
||||
ESP_LOGI(TAG, "Initializing LittleFS on Builtin SPI Flash Memory");
|
||||
|
||||
esp_vfs_littlefs_conf_t conf = {
|
||||
.base_path = mount_point,
|
||||
.partition_label = partition_label,
|
||||
.format_if_mount_failed = true,
|
||||
.dont_mount = false,
|
||||
};
|
||||
|
||||
// Use settings defined above to initialize and mount LittleFS filesystem.
|
||||
// Note: esp_vfs_littlefs_register is an all-in-one convenience function.
|
||||
esp_err_t ret = esp_vfs_littlefs_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 LittleFS partition");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to initialize LittleFS (%s)", esp_err_to_name(ret));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
size_t total = 0, used = 0;
|
||||
ret = esp_littlefs_info(conf.partition_label, &total, &used);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to get LittleFS partition information (%s)", esp_err_to_name(ret));
|
||||
//esp_littlefs_format(conf.partition_label);
|
||||
return ret;
|
||||
}
|
||||
ESP_LOGI(TAG, "Partition size: total: %d, used: %d", total, used);
|
||||
ESP_LOGI(TAG, "Mount LittleFS on %s", mount_point);
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
static void wifi_event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data)
|
||||
{
|
||||
if (event_id == WIFI_EVENT_AP_STACONNECTED) {
|
||||
wifi_event_ap_staconnected_t* event = (wifi_event_ap_staconnected_t*) event_data;
|
||||
ESP_LOGI(TAG, "station "MACSTR" join, AID=%d", MAC2STR(event->mac), event->aid);
|
||||
} else if (event_id == WIFI_EVENT_AP_STADISCONNECTED) {
|
||||
wifi_event_ap_stadisconnected_t* event = (wifi_event_ap_stadisconnected_t*) event_data;
|
||||
ESP_LOGI(TAG, "station "MACSTR" leave, AID=%d", MAC2STR(event->mac), event->aid);
|
||||
}
|
||||
}
|
||||
|
||||
extern void ftp_task (void *pvParameters);
|
||||
// Start WiFi AP and FTP Server
|
||||
// filemgmt.c
|
||||
esp_err_t start_ftp_server(void)
|
||||
{
|
||||
ESP_LOGI("FTP", "START");
|
||||
|
||||
close_current_log();
|
||||
|
||||
ESP_ERROR_CHECK(esp_netif_init());
|
||||
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||
|
||||
// THIS IS THE CORRECT WAY – recreate the default AP netif every time
|
||||
esp_netif_create_default_wifi_ap();
|
||||
|
||||
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
|
||||
|
||||
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
|
||||
ESP_EVENT_ANY_ID,
|
||||
&wifi_event_handler,
|
||||
NULL,
|
||||
NULL));
|
||||
|
||||
wifi_config_t wifi_config = {
|
||||
.ap = {
|
||||
.ssid = ESP_WIFI_AP_SSID,
|
||||
.ssid_len = strlen(ESP_WIFI_AP_SSID),
|
||||
.password = ESP_WIFI_AP_PASSWORD,
|
||||
.max_connection = ESP_WIFI_AP_N_CONNECTIONS,
|
||||
.authmode = strlen(ESP_WIFI_AP_PASSWORD) ? WIFI_AUTH_WPA_WPA2_PSK : WIFI_AUTH_OPEN
|
||||
},
|
||||
};
|
||||
|
||||
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
|
||||
ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_AP, &wifi_config));
|
||||
ESP_ERROR_CHECK(esp_wifi_start());
|
||||
|
||||
ESP_LOGI(TAG, "WiFi AP started – IP 192.168.4.1");
|
||||
|
||||
xEventTask = xEventGroupCreate();
|
||||
xTaskCreate(ftp_task, "FTP", 1024*6, NULL, 2, NULL);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void stop_ftp_server(void)
|
||||
{
|
||||
ESP_LOGI("FTP", "OFF");
|
||||
|
||||
ftp_terminate();
|
||||
vTaskDelay(pdMS_TO_TICKS(500)); // give task time to die
|
||||
|
||||
esp_wifi_stop();
|
||||
esp_wifi_deinit();
|
||||
|
||||
// This single call does everything correctly:
|
||||
// Destroy default Wi-Fi interface (handles AP)
|
||||
esp_netif_t *ap_netif = esp_netif_get_handle_from_ifkey("WIFI_AP_DEF");
|
||||
if (ap_netif) {
|
||||
esp_netif_destroy_default_wifi(ap_netif); // Correct API for AP
|
||||
}
|
||||
|
||||
esp_event_loop_delete_default();
|
||||
esp_netif_deinit();
|
||||
|
||||
ESP_LOGI("FTP", "WiFi + FTP completely stopped – safe to restart");
|
||||
}
|
||||
|
||||
|
||||
|
||||
void stop_filesystem() {
|
||||
esp_vfs_littlefs_unregister(partition_label);
|
||||
ESP_LOGI(TAG, "LittleFS unmounted");
|
||||
}
|
||||
|
||||
#define LOG_ENTRY_SIZE 32
|
||||
#define MAX_LOG_FILES 999 // rotate after N files
|
||||
#define ENTRIES_PER_FILE 8000 // ~256 KB per file → safe
|
||||
|
||||
static FILE* logfile = NULL;
|
||||
RTC_DATA_ATTR char current_log_path[32] = "/root/log000.bin";
|
||||
static uint32_t current_entry_count = 0;
|
||||
|
||||
// Helper: close current file safely
|
||||
void close_current_log(void)
|
||||
{
|
||||
if (logfile) {
|
||||
fflush(logfile);
|
||||
fsync(fileno(logfile)); // CRITICAL: forces write to flash
|
||||
fclose(logfile);
|
||||
logfile = NULL;
|
||||
current_entry_count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: open next log file (rotates 000→009)
|
||||
bool open_next_log_file(void)
|
||||
{
|
||||
close_current_log();
|
||||
|
||||
if (find_new_filename) {
|
||||
// Find next filename
|
||||
for (int i = 0; i < MAX_LOG_FILES; i++) {
|
||||
snprintf(current_log_path, sizeof(current_log_path), "/root/log%03d.bin", i);
|
||||
struct stat st;
|
||||
if (stat(current_log_path, &st) != 0) {
|
||||
break; // file doesn't exist → use this one
|
||||
}
|
||||
if (i == MAX_LOG_FILES-1) {
|
||||
ESP_LOGE("FILE", "RAN OUT OF FILES, GOING TO MAX");
|
||||
// All files exist → overwrite oldest (log000.bin)
|
||||
strcpy(current_log_path, "/root/log999.bin");
|
||||
}
|
||||
}
|
||||
|
||||
logfile = fopen(current_log_path, "ab"); // binary append
|
||||
if (!logfile) {
|
||||
ESP_LOGE("FILE", "Failed to open %s", current_log_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Optional: truncate if too big (safety)
|
||||
fseek(logfile, 0, SEEK_END);
|
||||
if (ftell(logfile) > ENTRIES_PER_FILE * LOG_ENTRY_SIZE) {
|
||||
fclose(logfile);
|
||||
logfile = fopen(current_log_path, "wb"); // truncate
|
||||
}
|
||||
|
||||
find_new_filename = false;
|
||||
} else {
|
||||
logfile = fopen(current_log_path, "ab");
|
||||
}
|
||||
|
||||
|
||||
|
||||
ESP_LOGI("FILE", "Logging to %s", current_log_path);
|
||||
return true;
|
||||
}
|
||||
|
||||
void file_log() {
|
||||
if (!logfile && !open_next_log_file()) {
|
||||
return;
|
||||
}
|
||||
|
||||
char entry[LOG_ENTRY_SIZE] = {0};
|
||||
entry[0] = LOG_ENTRY_SIZE;
|
||||
|
||||
// pack 64-bit timestamp into bytes 1-8
|
||||
uint64_t be_timestamp = htobe64(rtc_time_ms());
|
||||
memcpy(&entry[1], &be_timestamp, 8);
|
||||
|
||||
// pack 32-bit voltages/currents into bytes 9-24
|
||||
int32_t be_voltage = htobe32(get_battery_mV());
|
||||
memcpy(&entry[9], &be_voltage, 4);
|
||||
int32_t be_current1 = htobe32(get_bridge_mA(BRIDGE_DRIVE));
|
||||
memcpy(&entry[13], &be_current1, 4);
|
||||
int32_t be_current2 = htobe32(get_bridge_mA(BRIDGE_JACK));
|
||||
memcpy(&entry[17], &be_current2, 4);
|
||||
int32_t be_current3 = htobe32(get_bridge_mA(BRIDGE_AUX));
|
||||
memcpy(&entry[21], &be_current3, 4);
|
||||
|
||||
|
||||
int32_t be_counter = htobe32(get_sensor_counter(SENSOR_DRIVE));
|
||||
memcpy(&entry[25], &be_counter, 4);
|
||||
|
||||
entry[29] = get_sensor(SENSOR_DRIVE);
|
||||
entry[30] = get_sensor(SENSOR_JACK);
|
||||
entry[31] = fsm_get_state();
|
||||
|
||||
ESP_LOGI("FILE", "LOGGING TO %s: 0x %02x %02x%02x%02x%02x%02x%02x%02x%02x %02x%02x%02x%02x %02x%02x%02x%02x %02x%02x%02x%02x %02x%02x%02x%02x %02x%02x%02x%02x %02x %02x %02x", current_log_path,
|
||||
entry[0],
|
||||
entry[1],
|
||||
entry[2],
|
||||
entry[3],
|
||||
entry[4],
|
||||
entry[5],
|
||||
entry[6],
|
||||
entry[7],
|
||||
entry[8],
|
||||
entry[9],
|
||||
entry[10],
|
||||
entry[11],
|
||||
entry[12],
|
||||
entry[13],
|
||||
entry[14],
|
||||
entry[15],
|
||||
entry[16],
|
||||
entry[17],
|
||||
entry[18],
|
||||
entry[19],
|
||||
entry[20],
|
||||
entry[21],
|
||||
entry[22],
|
||||
entry[23],
|
||||
entry[24],
|
||||
entry[25],
|
||||
entry[26],
|
||||
entry[27],
|
||||
entry[28],
|
||||
entry[29],
|
||||
entry[30],
|
||||
entry[31]);
|
||||
|
||||
|
||||
/* SEND TO FILE */
|
||||
|
||||
size_t written = fwrite(entry, 1, LOG_ENTRY_SIZE, logfile);
|
||||
if (written != LOG_ENTRY_SIZE) {
|
||||
ESP_LOGE("FILE", "Partial write! Closing file.");
|
||||
close_current_log();
|
||||
return;
|
||||
}
|
||||
|
||||
current_entry_count++;
|
||||
|
||||
// Periodic flush (every 50–100 entries) + full sync every 500
|
||||
if (current_entry_count % 100 == 0) {
|
||||
fflush(logfile);
|
||||
}
|
||||
if (current_entry_count % 500 == 0) {
|
||||
fsync(fileno(logfile));
|
||||
}
|
||||
|
||||
// Rotate if getting large
|
||||
if (current_entry_count >= ENTRIES_PER_FILE) {
|
||||
ESP_LOGI("FILE", "Rotating log file");
|
||||
open_next_log_file();
|
||||
}
|
||||
}
|
||||
|
||||
33
main/filemgmt.h
Normal file
33
main/filemgmt.h
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* filemgmt.h
|
||||
*
|
||||
* Created on: Nov 20, 2025
|
||||
* Author: Thad
|
||||
*/
|
||||
|
||||
#ifndef MAIN_FILEMGMT_H_
|
||||
#define MAIN_FILEMGMT_H_
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
|
||||
#define ESP_WIFI_AP_SSID "stockcropper"
|
||||
#define ESP_WIFI_AP_PASSWORD ""
|
||||
#define ESP_WIFI_AP_N_CONNECTIONS 4
|
||||
|
||||
// Open the filesystem (e.g. for logging)
|
||||
esp_err_t start_filesystem();
|
||||
|
||||
// Start WiFi AP and FTP Server
|
||||
esp_err_t start_ftp_server();
|
||||
|
||||
void stop_filesystem();
|
||||
void stop_ftp_server();
|
||||
|
||||
void file_log();
|
||||
|
||||
void close_current_log();
|
||||
|
||||
void start_new_log_file();
|
||||
|
||||
#endif /* MAIN_FILEMGMT_H_ */
|
||||
1413
main/ftp.c
Normal file
1413
main/ftp.c
Normal file
File diff suppressed because it is too large
Load Diff
214
main/ftp.h
Normal file
214
main/ftp.h
Normal file
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
* This file is part of the MicroPython ESP32 project, https://github.com/loboris/MicroPython_ESP32_psRAM_LoBo
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2018 LoBo (https://github.com/loboris)
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
/*
|
||||
* This file is based on 'ftp' from Pycom Limited.
|
||||
*
|
||||
* Author: LoBo, loboris@gmail.com
|
||||
* Copyright (c) 2017, LoBo
|
||||
*/
|
||||
|
||||
#ifndef FTP_H_
|
||||
#define FTP_H_
|
||||
|
||||
#include "sdkconfig.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/queue.h"
|
||||
#include "sys/dirent.h"
|
||||
|
||||
/******************************************************************************
|
||||
DEFINE PRIVATE CONSTANTS
|
||||
******************************************************************************/
|
||||
#define FTP_CMD_PORT 21
|
||||
#define FTP_ACTIVE_DATA_PORT 20
|
||||
#define FTP_PASIVE_DATA_PORT 2024
|
||||
#define FTP_CMD_SIZE_MAX 6
|
||||
#define FTP_CMD_CLIENTS_MAX 1
|
||||
#define FTP_DATA_CLIENTS_MAX 1
|
||||
#define FTP_MAX_PARAM_SIZE (MICROPY_ALLOC_PATH_MAX + 1)
|
||||
#define FTP_UNIX_SECONDS_180_DAYS 15552000
|
||||
#define FTP_DATA_TIMEOUT_MS 10000 // 10 seconds
|
||||
#define FTP_SOCKETFIFO_ELEMENTS_MAX 4
|
||||
|
||||
|
||||
typedef enum {
|
||||
E_FTP_STE_DISABLED = 0,
|
||||
E_FTP_STE_START,
|
||||
E_FTP_STE_READY,
|
||||
E_FTP_STE_END_TRANSFER,
|
||||
E_FTP_STE_CONTINUE_LISTING,
|
||||
E_FTP_STE_CONTINUE_FILE_TX,
|
||||
E_FTP_STE_CONTINUE_FILE_RX,
|
||||
E_FTP_STE_CONNECTED
|
||||
} ftp_state_t;
|
||||
|
||||
typedef enum {
|
||||
E_FTP_STE_SUB_DISCONNECTED = 0,
|
||||
E_FTP_STE_SUB_LISTEN_FOR_DATA,
|
||||
E_FTP_STE_SUB_DATA_CONNECTED
|
||||
} ftp_substate_t;
|
||||
|
||||
|
||||
/******************************************************************************
|
||||
DEFINE PRIVATE TYPES
|
||||
******************************************************************************/
|
||||
typedef enum {
|
||||
E_FTP_RESULT_OK = 0,
|
||||
E_FTP_RESULT_CONTINUE,
|
||||
E_FTP_RESULT_FAILED
|
||||
} ftp_result_t;
|
||||
|
||||
typedef struct {
|
||||
bool uservalid : 1;
|
||||
bool passvalid : 1;
|
||||
} ftp_loggin_t;
|
||||
|
||||
typedef enum {
|
||||
E_FTP_NOTHING_OPEN = 0,
|
||||
E_FTP_FILE_OPEN,
|
||||
E_FTP_DIR_OPEN
|
||||
} ftp_e_open_t;
|
||||
|
||||
typedef enum {
|
||||
E_FTP_CLOSE_NONE = 0,
|
||||
E_FTP_CLOSE_DATA,
|
||||
E_FTP_CLOSE_CMD_AND_DATA,
|
||||
} ftp_e_closesocket_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t *dBuffer;
|
||||
uint32_t ctimeout;
|
||||
union {
|
||||
DIR *dp;
|
||||
FILE *fp;
|
||||
};
|
||||
int32_t lc_sd;
|
||||
int32_t ld_sd;
|
||||
int32_t c_sd;
|
||||
int32_t d_sd;
|
||||
int32_t dtimeout;
|
||||
uint32_t ip_addr;
|
||||
uint8_t state;
|
||||
uint8_t substate;
|
||||
uint8_t txRetries;
|
||||
uint8_t logginRetries;
|
||||
ftp_loggin_t loggin;
|
||||
uint8_t e_open;
|
||||
bool closechild;
|
||||
bool enabled;
|
||||
bool listroot;
|
||||
uint32_t total;
|
||||
uint32_t time;
|
||||
} ftp_data_t;
|
||||
|
||||
typedef struct {
|
||||
char * cmd;
|
||||
} ftp_cmd_t;
|
||||
|
||||
typedef enum {
|
||||
E_FTP_CMD_NOT_SUPPORTED = -1,
|
||||
E_FTP_CMD_FEAT = 0,
|
||||
E_FTP_CMD_SYST,
|
||||
E_FTP_CMD_CDUP,
|
||||
E_FTP_CMD_CWD,
|
||||
E_FTP_CMD_PWD,
|
||||
E_FTP_CMD_XPWD,
|
||||
E_FTP_CMD_SIZE,
|
||||
E_FTP_CMD_MDTM,
|
||||
E_FTP_CMD_TYPE,
|
||||
E_FTP_CMD_USER,
|
||||
E_FTP_CMD_PASS,
|
||||
E_FTP_CMD_PASV,
|
||||
E_FTP_CMD_LIST,
|
||||
E_FTP_CMD_RETR,
|
||||
E_FTP_CMD_STOR,
|
||||
E_FTP_CMD_DELE,
|
||||
E_FTP_CMD_RMD,
|
||||
E_FTP_CMD_MKD,
|
||||
E_FTP_CMD_RNFR,
|
||||
E_FTP_CMD_RNTO,
|
||||
E_FTP_CMD_NOOP,
|
||||
E_FTP_CMD_QUIT,
|
||||
E_FTP_CMD_APPE,
|
||||
E_FTP_CMD_NLST,
|
||||
E_FTP_CMD_AUTH,
|
||||
E_FTP_NUM_FTP_CMDS
|
||||
} ftp_cmd_index_t;
|
||||
|
||||
|
||||
|
||||
#define FTP_USER_PASS_LEN_MAX 32
|
||||
#define FTP_DEF_USER "micro"
|
||||
#define FTP_DEF_PASS "python"
|
||||
#define FTP_MUTEX_TIMEOUT_MS 1000
|
||||
#define FTP_CMD_TIMEOUT_MS (CONFIG_MICROPY_FTPSERVER_TIMEOUT*1000)
|
||||
|
||||
#define CONFIG_MICROPY_FTPSERVER_BUFFER_SIZE 1024
|
||||
#define CONFIG_MICROPY_FTPSERVER_TIMEOUT 300
|
||||
#define ONFIG_MICROPY_FILESYSTEM_TYPE 0
|
||||
#define MICROPY_ALLOC_PATH_MAX (512)
|
||||
|
||||
// #define MAX_ACTIVE_INTERFACES 3
|
||||
//tcpip_adapter_if_t tcpip_if[MAX_ACTIVE_INTERFACES] = {TCPIP_ADAPTER_IF_MAX};
|
||||
|
||||
#define VFS_NATIVE_MOUNT_POINT "/_#!#_spiffs"
|
||||
#define VFS_NATIVE_SDCARD_MOUNT_POINT "/_#!#_sdcard"
|
||||
#define VFS_NATIVE_INTERNAL_PART_LABEL "internalfs"
|
||||
#define VFS_NATIVE_INTERNAL_MP "/flash"
|
||||
#define VFS_NATIVE_EXTERNAL_MP "/sd"
|
||||
#define VFS_NATIVE_TYPE_SPIFLASH 0
|
||||
#define VFS_NATIVE_TYPE_SDCARD 1
|
||||
|
||||
#ifndef MIN
|
||||
#define MIN(x, y) ((x) < (y) ? (x) : (y))
|
||||
#endif
|
||||
#ifndef MAX
|
||||
#define MAX(x, y) ((x) > (y) ? (x) : (y))
|
||||
#endif
|
||||
|
||||
#if 0
|
||||
extern const char *FTP_TAG;
|
||||
extern char ftp_user[FTP_USER_PASS_LEN_MAX + 1];
|
||||
extern char ftp_pass[FTP_USER_PASS_LEN_MAX + 1];
|
||||
extern uint32_t ftp_stack_size;
|
||||
extern QueueHandle_t ftp_mutex;
|
||||
extern int ftp_buff_size;
|
||||
extern int ftp_timeout;
|
||||
#endif
|
||||
|
||||
bool ftp_init (void);
|
||||
void ftp_deinit (void);
|
||||
int ftp_run (uint32_t elapsed);
|
||||
bool ftp_enable (void);
|
||||
bool ftp_isenabled (void);
|
||||
bool ftp_disable (void);
|
||||
bool ftp_reset (void);
|
||||
int ftp_getstate();
|
||||
bool ftp_terminate (void);
|
||||
bool ftp_stop_requested();
|
||||
int32_t ftp_get_maxstack (void);
|
||||
uint64_t mp_hal_ticks_ms(void);
|
||||
|
||||
#endif /* FTP_H_ */
|
||||
792
main/hard_ui.c
Normal file
792
main/hard_ui.c
Normal file
@@ -0,0 +1,792 @@
|
||||
/*
|
||||
* lcd.c
|
||||
*
|
||||
* Created on: Dec 12, 2025
|
||||
* Author: Thad
|
||||
*/
|
||||
|
||||
|
||||
/* NOTICE: THIS IS A DUMPING GROUND FOR OBSOLETE CODE SINCE WE NO LONGER HAVE AN LCD
|
||||
NONE OF THIS IS TESTED.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
static uint8_t lcd_col = 0;
|
||||
static uint8_t lcd_row = 0;
|
||||
|
||||
static bool debounced_state[4] = {false};
|
||||
static bool last_known_state[4] = {false};
|
||||
static uint64_t last_stable_time[4] = {0};
|
||||
static uint64_t last_change_time[4] = {0};
|
||||
static uint8_t claimed_repeats[4] = {0};
|
||||
|
||||
|
||||
// === DELAY HELPERS ===
|
||||
static inline void delay_us(uint32_t us) {
|
||||
esp_rom_delay_us(us);
|
||||
}
|
||||
|
||||
|
||||
static esp_err_t tca_write_word_16(uint8_t reg, uint16_t value) {
|
||||
uint8_t data[3] = { reg, (uint8_t)(value & 0xFF), (uint8_t)(value >> 8) };
|
||||
return i2c_master_write_to_device(I2C_PORT, TCA_ADDR, data, 3, pdMS_TO_TICKS(1000));
|
||||
}
|
||||
|
||||
|
||||
// === TCA9555 PORT CONTROL ===
|
||||
static esp_err_t tca_set_config_port0(uint16_t config_port0) {
|
||||
return tca_write_word_16(TCA_REG_CONFIG0, config_port0);
|
||||
}
|
||||
|
||||
static esp_err_t tca_port_write(uint8_t value) {
|
||||
return tca_write_word_8(TCA_REG_OUTPUT1, value);
|
||||
}
|
||||
|
||||
static esp_err_t tca_port_read(uint16_t *value) {
|
||||
uint16_t low, high;
|
||||
ESP_ERROR_CHECK(tca_read_word(TCA_REG_INPUT0, &low));
|
||||
ESP_ERROR_CHECK(tca_read_word(TCA_REG_INPUT1, &high));
|
||||
*value = low | (high << 8);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
|
||||
// === LCD NIBBLE & COMMAND ===
|
||||
static esp_err_t lcd_write_nibble(uint8_t nibble, bool rs) {
|
||||
uint8_t data_state = 0;
|
||||
if (rs) data_state |= (1 << LCD_RS);
|
||||
if (nibble & 0x01) data_state |= (1 << LCD_D4);
|
||||
if (nibble & 0x02) data_state |= (1 << LCD_D5);
|
||||
if (nibble & 0x04) data_state |= (1 << LCD_D6);
|
||||
if (nibble & 0x08) data_state |= (1 << LCD_D7);
|
||||
|
||||
ESP_ERROR_CHECK(tca_port_write(data_state));
|
||||
ESP_ERROR_CHECK(tca_port_write(data_state | (1 << LCD_E)));
|
||||
ESP_ERROR_CHECK(tca_port_write(data_state));
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
|
||||
static esp_err_t lcd_command(uint8_t cmd) {
|
||||
ESP_ERROR_CHECK(lcd_write_nibble(cmd >> 4, false));
|
||||
ESP_ERROR_CHECK(lcd_write_nibble(cmd & 0x0F, false));
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t lcd_data(uint8_t data) {
|
||||
ESP_ERROR_CHECK(lcd_write_nibble(data >> 4, true));
|
||||
ESP_ERROR_CHECK(lcd_write_nibble(data & 0x0F, true));
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void lcd_set_cursor(uint8_t row, uint8_t col) {
|
||||
uint8_t addr = (row == 0) ? 0x00 : 0x40;
|
||||
addr += col;
|
||||
lcd_row = row;
|
||||
lcd_col = col;
|
||||
lcd_command(0x80 | addr);
|
||||
delay_us(50);
|
||||
}
|
||||
|
||||
void lcd_printf(const char *fmt, ...) {
|
||||
char buf[64];
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
vsnprintf(buf, sizeof(buf), fmt, args);
|
||||
va_end(args);
|
||||
|
||||
lcd_set_cursor(0, 0);
|
||||
for (int i = 0; i < 32 && buf[i]; i++) {
|
||||
if (i == 16) lcd_set_cursor(1, 0);
|
||||
lcd_data((uint8_t)buf[i]);
|
||||
delay_us(50);
|
||||
}
|
||||
}
|
||||
|
||||
void lcd_print(const char *str) {
|
||||
lcd_set_cursor(0, 0);
|
||||
for (int i = 0; i < 32 && str[i]; i++) {
|
||||
if (i == 16) lcd_set_cursor(1, 0);
|
||||
lcd_data((uint8_t)str[i]);
|
||||
delay_us(50);
|
||||
}
|
||||
}
|
||||
|
||||
void lcd_off(void) {
|
||||
if (i2c_initted) lcd_command(0x08);
|
||||
}
|
||||
|
||||
esp_err_t lcd_init_4bit(void) {
|
||||
ESP_LOGI("I2C", "Starting LCD init...");
|
||||
ESP_ERROR_CHECK(tca_set_config_port0(0xFF));
|
||||
tca_port_write(0x00);
|
||||
delay_us(50000);
|
||||
|
||||
ESP_ERROR_CHECK(lcd_write_nibble(0x3, false)); delay_us(4500);
|
||||
ESP_ERROR_CHECK(lcd_write_nibble(0x3, false)); delay_us(150);
|
||||
ESP_ERROR_CHECK(lcd_write_nibble(0x3, false)); delay_us(150);
|
||||
ESP_ERROR_CHECK(lcd_write_nibble(0x2, false)); delay_us(150);
|
||||
|
||||
ESP_ERROR_CHECK(lcd_command(0x28)); delay_us(150);
|
||||
ESP_ERROR_CHECK(lcd_command(0x08)); delay_us(150);
|
||||
ESP_ERROR_CHECK(lcd_command(0x01)); delay_us(2000);
|
||||
ESP_ERROR_CHECK(lcd_command(0x06)); delay_us(150);
|
||||
ESP_ERROR_CHECK(lcd_command(0x0C)); delay_us(150);
|
||||
|
||||
ESP_LOGI("I2C", "LCD init complete.");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
|
||||
// === BUTTON DEBOUNCE & REPEAT ===
|
||||
void update_buttons(void) {
|
||||
for (uint8_t btn = 0; btn < 4; ++btn) {
|
||||
last_known_state[btn] = debounced_state[btn];
|
||||
}
|
||||
|
||||
uint16_t port_val;
|
||||
ESP_ERROR_CHECK(tca_port_read(&port_val));
|
||||
uint8_t raw_buttons = (uint8_t)(port_val & 0x0F);
|
||||
uint8_t raw_states = ~raw_buttons & 0x0F;
|
||||
|
||||
uint64_t now = esp_timer_get_time() / 1000;
|
||||
|
||||
for (uint8_t btn = 0; btn < 4; ++btn) {
|
||||
bool raw_pressed = (raw_states & (1 << btn)) != 0;
|
||||
|
||||
if (raw_pressed != debounced_state[btn]) {
|
||||
if (now - last_stable_time[btn] >= DEBOUNCE_MS) {
|
||||
debounced_state[btn] = raw_pressed;
|
||||
last_stable_time[btn] = now;
|
||||
last_change_time[btn] = now;
|
||||
claimed_repeats[btn] = 0;
|
||||
}
|
||||
} else {
|
||||
last_stable_time[btn] = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool get_button_tripped(uint8_t button) {
|
||||
return (button < 4) && debounced_state[button] && !last_known_state[button];
|
||||
}
|
||||
|
||||
bool get_button_released(uint8_t button) {
|
||||
return (button < 4) && !debounced_state[button] && last_known_state[button];
|
||||
}
|
||||
|
||||
bool get_button_state(uint8_t button) {
|
||||
return (button < 4) && debounced_state[button];
|
||||
}
|
||||
|
||||
bool get_button_repeat(uint8_t btn) {
|
||||
if (btn >= 4 || !debounced_state[btn]) return false;
|
||||
uint64_t now = esp_timer_get_time() / 1000;
|
||||
if (now + DEBOUNCE_MS < last_change_time[btn]) return false;
|
||||
if ((now - last_change_time[btn]) > (REPEAT_START_MS + REPEAT_MS * claimed_repeats[btn])) {
|
||||
claimed_repeats[btn]++;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int8_t get_button_repeats(uint8_t btn) {
|
||||
if (!get_button_state(btn))
|
||||
return 0;
|
||||
|
||||
if (btn >= 4 || !debounced_state[btn]) return false;
|
||||
uint64_t now = esp_timer_get_time() / 1000;
|
||||
if (now + DEBOUNCE_MS < last_change_time[btn]) return false;
|
||||
if ((now - last_change_time[btn]) > (REPEAT_START_MS + REPEAT_MS * claimed_repeats[btn])) {
|
||||
claimed_repeats[btn]++;
|
||||
if (claimed_repeats[btn] > 100)
|
||||
claimed_repeats[btn] = 100;
|
||||
ESP_LOGI("BTN", "RPT %d", (uint8_t)claimed_repeats[btn]+2);
|
||||
return claimed_repeats[btn]+1;
|
||||
}
|
||||
if (debounced_state[btn] && !last_known_state[btn]) {
|
||||
|
||||
ESP_LOGI("BTN", "FST %d", 1);
|
||||
return 1;
|
||||
}
|
||||
|
||||
//ESP_LOGI("BTN", "RPT %d", 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int64_t get_button_ms(uint8_t btn) {
|
||||
if (!get_button_state(btn))
|
||||
return 0;
|
||||
|
||||
uint64_t now = esp_timer_get_time() / 1000;
|
||||
return now - last_change_time[btn];
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Parameter descriptor structure
|
||||
typedef struct {
|
||||
const char key[24]; // NVS key name (null-terminated)
|
||||
uint8_t type_size; // Size in bytes: 1=uint8_t, 2=uint16_t, 4=uint32_t/float, 8=uint64_t/double
|
||||
uint8_t type_flags; // Bitfield: [0:1] signed, [2] float, [3:7] reserved
|
||||
const void *default_val; // Pointer to default value (matches type)
|
||||
} param_desc_t;
|
||||
|
||||
typedef struct param_group_s param_group_t;
|
||||
typedef struct param_group_s {
|
||||
char* (*formatter)(const param_group_t*, uint8_t idx);
|
||||
const uint8_t num_keys;
|
||||
const uint8_t indices[8][2];
|
||||
const char keys[8][20];
|
||||
void (*launch_functions[8])(char* key, int8_t dir);
|
||||
} param_group_t;
|
||||
|
||||
// temp buffer for formatting stuff onto the LCD
|
||||
static char formatting_buf[LCD_BUFLEN];
|
||||
|
||||
/* MENU DIALOG CONFIG */
|
||||
char* schedule_format(const param_group_t *pg, uint8_t idx);
|
||||
char* dist_format (const param_group_t *pg, uint8_t idx);
|
||||
char* reprog_format (const param_group_t *pg, uint8_t idx);
|
||||
char* override_format(const param_group_t *pg, uint8_t idx);
|
||||
char* status_format (const param_group_t *pg, uint8_t idx);
|
||||
char* cal_format (const param_group_t *pg, uint8_t idx);
|
||||
char* efuse_format (const param_group_t *pg, uint8_t idx);
|
||||
char* ftp_format (const param_group_t *pg, uint8_t idx);
|
||||
|
||||
// Launch functions (forward declarations)
|
||||
void trigger_move(char* key, int8_t dir);
|
||||
void rf_reprogram_remote(char* key, int8_t dir);
|
||||
|
||||
void adjust_hour (char* key, int8_t dir);
|
||||
void adjust_i8_0_99 (char* key, int8_t dir);
|
||||
void adjust_generic (int idx, int8_t amt);
|
||||
void dummy_adjuster (char* key, int8_t dir) {}; // do nothing
|
||||
void launch_ftp (char* key, int8_t dir);
|
||||
void adjust_i32_smart_0_99999(char* key, int8_t dir);
|
||||
void adjust_i32_smart_0_999 (char* key, int8_t dir);
|
||||
|
||||
// Parameter table (legible, declarative)
|
||||
const param_desc_t param_table[] = {
|
||||
{
|
||||
.key = "sched_start",
|
||||
.type_size = TYPE_SIZE_1,
|
||||
.type_flags = TYPE_SIGNED,
|
||||
.default_val = &(int8_t){0}
|
||||
},
|
||||
{
|
||||
.key = "sched_end",
|
||||
.type_size = TYPE_SIZE_1,
|
||||
.type_flags = TYPE_SIGNED,
|
||||
.default_val = &(int8_t){0}
|
||||
},
|
||||
{
|
||||
.key = "sched_num",
|
||||
.type_size = TYPE_SIZE_1,
|
||||
.type_flags = TYPE_SIGNED,
|
||||
.default_val = &(int8_t){0}
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
.key = "efuse_drive_A",
|
||||
.type_size = TYPE_SIZE_1,
|
||||
.type_flags = TYPE_SIGNED,
|
||||
.default_val = &(int8_t){99}
|
||||
},{
|
||||
.key = "efuse_jack_A",
|
||||
.type_size = TYPE_SIZE_1,
|
||||
.type_flags = TYPE_SIGNED,
|
||||
.default_val = &(int8_t){99}
|
||||
},{
|
||||
.key = "efuse_aux_A",
|
||||
.type_size = TYPE_SIZE_1,
|
||||
.type_flags = TYPE_SIGNED,
|
||||
.default_val = &(int8_t){99}
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
.key = "drive_dist",
|
||||
.type_size = TYPE_SIZE_1,
|
||||
.type_flags = TYPE_SIGNED,
|
||||
.default_val = &(int16_t){10}
|
||||
},{
|
||||
.key = "drive_tpdf",
|
||||
.type_size = TYPE_SIZE_4,
|
||||
.type_flags = 0,
|
||||
.default_val = &(int32_t){70}
|
||||
},{
|
||||
.key = "drive_mspf",
|
||||
.type_size = TYPE_SIZE_4,
|
||||
.type_flags = 0,
|
||||
.default_val = &(int32_t){1000}
|
||||
},{
|
||||
.key = "jack_mspi",
|
||||
.type_size = TYPE_SIZE_4,
|
||||
.type_flags = 0,
|
||||
.default_val = &(int32_t){1000}
|
||||
},{
|
||||
.key = "jack_dist",
|
||||
.type_size = TYPE_SIZE_1,
|
||||
.type_flags = TYPE_SIGNED,
|
||||
.default_val = &(uint8_t){7}
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{
|
||||
.key = "keycode0",
|
||||
.type_size = TYPE_SIZE_8,
|
||||
.type_flags = 0,
|
||||
.default_val = &(uint8_t){0}
|
||||
},{
|
||||
.key = "keycode1",
|
||||
.type_size = TYPE_SIZE_8,
|
||||
.type_flags = 0,
|
||||
.default_val = &(uint8_t){0}
|
||||
},{
|
||||
.key = "keycode2",
|
||||
.type_size = TYPE_SIZE_8,
|
||||
.type_flags = 0,
|
||||
.default_val = &(uint8_t){0}
|
||||
},{
|
||||
.key = "keycode3",
|
||||
.type_size = TYPE_SIZE_8,
|
||||
.type_flags = 0,
|
||||
.default_val = &(uint8_t){0}
|
||||
}
|
||||
};
|
||||
|
||||
#define PARAM_COUNT (sizeof(param_table)/sizeof(param_table[0]))
|
||||
|
||||
// Runtime parameter values
|
||||
static param_value_t param_values[PARAM_COUNT];
|
||||
|
||||
const param_group_t param_group_table[] = {
|
||||
{
|
||||
.formatter = status_format,
|
||||
.num_keys = 3,
|
||||
.keys = {"","",""},
|
||||
.launch_functions = {trigger_move, adjust_rtc_hour, adjust_rtc_min}
|
||||
},{
|
||||
.formatter = schedule_format,
|
||||
.num_keys = 3,
|
||||
.keys = {"sched_start", "sched_end", "sched_num"},
|
||||
.launch_functions = {adjust_hour, adjust_hour, adjust_i8_0_99}
|
||||
},
|
||||
{
|
||||
.formatter = dist_format,
|
||||
.num_keys = 2,
|
||||
.keys = {"drive_dist", "jack_dist"},
|
||||
.launch_functions = {adjust_i8_0_99, adjust_i8_0_99}
|
||||
},
|
||||
{
|
||||
.formatter = cal_format,
|
||||
.num_keys = 3,
|
||||
.keys = { "jack_mspi", "drive_mspf", "drive_tpdf"},
|
||||
.launch_functions = {adjust_i32_smart_0_99999, adjust_i32_smart_0_99999, adjust_i32_smart_0_999}
|
||||
},
|
||||
{
|
||||
.formatter = efuse_format,
|
||||
.num_keys = 3,
|
||||
.keys = { "efuse_aux_A", "efuse_jack_A", "efuse_drive_A"},
|
||||
.launch_functions = {adjust_i8_0_99, adjust_i8_0_99, adjust_i8_0_99}
|
||||
},
|
||||
{
|
||||
.formatter = override_format,
|
||||
.num_keys = 3,
|
||||
.keys = {"","",""},
|
||||
.launch_functions = {dummy_adjuster, dummy_adjuster, dummy_adjuster}
|
||||
},
|
||||
{
|
||||
.formatter = reprog_format,
|
||||
.num_keys = 1,
|
||||
.keys = {""},
|
||||
.launch_functions = {rf_reprogram_remote}
|
||||
},
|
||||
{
|
||||
.formatter = ftp_format,
|
||||
.num_keys = 1,
|
||||
.keys = {""},
|
||||
.launch_functions = {launch_ftp}
|
||||
}
|
||||
};
|
||||
|
||||
#define PARAM_GROUP_RUNMTR 5
|
||||
#define PARAM_GROUP_FTP 7
|
||||
#define PARAM_GROUP_COUNT (sizeof(param_group_table)/sizeof(param_group_table[0]))
|
||||
|
||||
static const char schedule_fmts[3][3][LCD_BUFLEN] = {
|
||||
{
|
||||
"Start/End xTimes [-] - x%-2d ",
|
||||
"Start/End xTimes - [-] x%-2d ",
|
||||
"Start/End xTimes - - [x%-2d]"
|
||||
},{
|
||||
"Start/End xTimes[%2d%cM] - x%-2d ",
|
||||
"Start/End xTimes %2d%cM [-] x%-2d ",
|
||||
"Start/End xTimes %2d%cM - [x%-2d]"
|
||||
},{
|
||||
"Start/End xTimes[%2d%cM]-%2d%cM x%-2d",
|
||||
"Start/End xTimes %2d%cM-[%2d%cM] x%-2d",
|
||||
"Start/End xTimes %2d%cM-%2d%cM [x%-2d]"
|
||||
}
|
||||
};
|
||||
|
||||
static const char dist_fmts[3][LCD_BUFLEN] = {
|
||||
"Dist. Drive/Jack[%2d ft] / %2d in ",
|
||||
"Dist. Drive/Jack %2d ft / [%2d in]"
|
||||
};
|
||||
|
||||
static const char override_fmts[3][LCD_BUFLEN] = {
|
||||
" Run Motors [AUX]JACK DRIVE ",
|
||||
" Run Motors AUX[JACK]DRIVE ",
|
||||
" Run Motors AUX JACK[DRIVE]"
|
||||
};
|
||||
|
||||
static const char cal_fmts[3][LCD_BUFLEN] = {
|
||||
"Jack ms/in: [%4ld]%4ld %4ld ",
|
||||
"Drive ms/ft: %4ld[%4ld]%4ld ",
|
||||
"Drive t/10ft: %4ld %4ld[%4ld]"
|
||||
};
|
||||
static const char efuse_fmts[3][LCD_BUFLEN] = {
|
||||
"E-fuse Aux: [%2dA] %2dA %2dA ",
|
||||
"E-fuse Jack: %2dA [%2dA] %2dA ",
|
||||
"E-fuse Drive: %2dA %2dA [%2dA]"
|
||||
};
|
||||
|
||||
/* All function implementations remain unchanged and appear here in original form */
|
||||
char* schedule_format(const param_group_t *pg, uint8_t idx)
|
||||
{
|
||||
/* pg->keys[0..2] → "sched_start", "sched_end", "sched_num" */
|
||||
int8_t start = (int8_t)get_param_i8(pg->keys[0]); // helper, see below
|
||||
int8_t end = (int8_t)get_param_i8(pg->keys[1]);
|
||||
int8_t num = (int8_t)get_param_i8(pg->keys[2]);
|
||||
|
||||
|
||||
char startAP = start<12 ? 'A':'P';
|
||||
char endAP = end<12 ? 'A':'P';
|
||||
start %= 12;
|
||||
end %= 12;
|
||||
|
||||
|
||||
if (start == 0) start = 12;
|
||||
if (end == 0) end = 12;
|
||||
|
||||
if (num == 0) {
|
||||
snprintf(formatting_buf, sizeof(formatting_buf),
|
||||
schedule_fmts[0][idx], num);
|
||||
return formatting_buf;
|
||||
} else if (num == 1) {
|
||||
snprintf(formatting_buf, sizeof(formatting_buf),
|
||||
schedule_fmts[1][idx], start, startAP, num);
|
||||
return formatting_buf;
|
||||
} else {
|
||||
snprintf(formatting_buf, sizeof(formatting_buf),
|
||||
schedule_fmts[2][idx], start, startAP, end, endAP, num);
|
||||
return formatting_buf;
|
||||
}
|
||||
}
|
||||
|
||||
char* dist_format(const param_group_t *pg, uint8_t idx) {
|
||||
int8_t drive = (int8_t)get_param_i8(pg->keys[0]); // helper, see below
|
||||
int8_t jack = (int8_t)get_param_i8(pg->keys[1]);
|
||||
|
||||
snprintf(formatting_buf, sizeof(formatting_buf),
|
||||
dist_fmts[idx], drive, jack);
|
||||
return formatting_buf;
|
||||
}
|
||||
|
||||
char* reprog_format(const param_group_t *pg, uint8_t idx) {
|
||||
return "Reprogram Keyfob [Press ^ / v ] ";
|
||||
}
|
||||
|
||||
char* override_format(const param_group_t *pg, uint8_t idx) {
|
||||
return override_fmts[idx];
|
||||
}
|
||||
|
||||
char* ftp_format(const param_group_t *pg, uint8_t idx) {
|
||||
return " Start Wifi/FTP [Press ^ / v ] ";
|
||||
}
|
||||
|
||||
char charge_indicators[N_CHARGE_STATES] = {
|
||||
[CHG_STATE_OFF] ='-',
|
||||
[CHG_STATE_FLOAT] ='F',
|
||||
[CHG_STATE_BULK] ='B'
|
||||
|
||||
};
|
||||
|
||||
static const char status_fmts[4][LCD_BUFLEN] = {
|
||||
"%-6s%2dA %2lu.%02luV[MOVE] %2d:%02d %cM",
|
||||
"%-6s%2dA %2lu.%02luV MOVE [%2d]:%02d %cM",
|
||||
"%-6s%2dA %2lu.%02luV MOVE %2d:[%02d]%cM",
|
||||
"%-6s%2dA %2lu.%02luV[ SET TIME ^/v ]",
|
||||
};
|
||||
|
||||
char* status_format(const param_group_t *pg, uint8_t idx) {
|
||||
uint32_t vbat = get_battery_mV();
|
||||
|
||||
struct tm timeinfo;
|
||||
rtc_get_time(&timeinfo);
|
||||
|
||||
// --- Build 7-char time: " 9:05PM" or "10:05PM" ---
|
||||
int hour12 = timeinfo.tm_hour % 12;
|
||||
if (hour12 == 0) hour12 = 12; // 12-hour format
|
||||
|
||||
int current_draw = abs(get_bridge_mA(BRIDGE_DRIVE)/1000) + abs(get_bridge_mA(BRIDGE_JACK)/1000) + abs(get_bridge_mA(BRIDGE_AUX)/1000);
|
||||
|
||||
if (rtc_is_set())
|
||||
snprintf(formatting_buf, sizeof(formatting_buf),
|
||||
status_fmts[idx],
|
||||
"Idle",
|
||||
current_draw,
|
||||
(unsigned long)(vbat / 1000),
|
||||
(unsigned long)((vbat % 1000) + 99) / 100,
|
||||
hour12,
|
||||
timeinfo.tm_min,
|
||||
timeinfo.tm_hour < 12 ? 'A':'P'
|
||||
);
|
||||
else
|
||||
snprintf(formatting_buf, sizeof(formatting_buf),
|
||||
status_fmts[3],
|
||||
"Idle",
|
||||
current_draw,
|
||||
(unsigned long)(vbat / 1000),
|
||||
(unsigned long)((vbat % 1000) + 99) / 100
|
||||
);
|
||||
|
||||
return formatting_buf;
|
||||
}
|
||||
|
||||
char* cal_format(const param_group_t *pg, uint8_t idx) {
|
||||
int32_t x1 = get_param_i32(pg->keys[0]);
|
||||
int32_t x2 = get_param_i32(pg->keys[1]);
|
||||
int32_t x3 = get_param_i32(pg->keys[2]);
|
||||
|
||||
snprintf(formatting_buf, sizeof(formatting_buf),
|
||||
cal_fmts[idx], x1, x2, x3);
|
||||
return formatting_buf;
|
||||
}
|
||||
|
||||
char* efuse_format(const param_group_t *pg, uint8_t idx) {
|
||||
int32_t x1 = get_param_i32(pg->keys[0]);
|
||||
int32_t x2 = get_param_i32(pg->keys[1]);
|
||||
int32_t x3 = get_param_i32(pg->keys[2]);
|
||||
|
||||
snprintf(formatting_buf, sizeof(formatting_buf),
|
||||
efuse_fmts[idx], x1, x2, x3);
|
||||
return formatting_buf;
|
||||
}
|
||||
|
||||
// Generic adjustment fallback
|
||||
void adjust_generic(int idx, int8_t amt) {
|
||||
const param_desc_t *p = ¶m_table[idx];
|
||||
if (p->type_flags & TYPE_FLOAT) {
|
||||
float step = 0.1f;
|
||||
param_values[idx].f32 += amt;
|
||||
} else {
|
||||
switch (p->type_size) {
|
||||
case 1: {
|
||||
int8_t v = (int8_t)param_values[idx].u8;
|
||||
v += amt;
|
||||
param_values[idx].u8 = (int8_t)v;
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
int16_t v = (int16_t)param_values[idx].u16;
|
||||
v += amt;
|
||||
param_values[idx].u16 = (int16_t)v;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
params_save(idx);
|
||||
}
|
||||
|
||||
/**
|
||||
* adjust_time - Shared adjuster for any time parameter (HH:MM format)
|
||||
* @idx: Index in param_table[]
|
||||
* @dir: +1 = increment, -1 = decrement
|
||||
*
|
||||
* Assumes value stored as minutes since 00:00 (0–1439)
|
||||
* Displays as "HH:MM"
|
||||
*/
|
||||
void adjust_hour(char* key, int8_t dir) {
|
||||
int8_t idx = params_find(key);
|
||||
if (idx<0) return;
|
||||
|
||||
if (dir>0) param_values[idx].i8 += +1;
|
||||
if (dir<0) param_values[idx].i8 += -1;
|
||||
|
||||
// wraparound
|
||||
if (param_values[idx].i8 > 23) param_values[idx].i8 = 0;
|
||||
if (param_values[idx].i8 < 0) param_values[idx].i8 = 23;
|
||||
|
||||
params_save(idx);
|
||||
set_next_alarm();
|
||||
}
|
||||
|
||||
void adjust_i8_0_99(char* key, int8_t dir) {
|
||||
int8_t idx = params_find(key);
|
||||
if (idx<0) return;
|
||||
|
||||
if (dir>0) param_values[idx].i8 += +1;
|
||||
if (dir<0) param_values[idx].i8 += -1;
|
||||
|
||||
// clamp
|
||||
if (param_values[idx].i8 > 99) param_values[idx].i8 = 99;
|
||||
if (param_values[idx].i8 < 0) param_values[idx].i8 = 0;
|
||||
|
||||
params_save(idx);
|
||||
set_next_alarm();
|
||||
}
|
||||
|
||||
void adjust_i16_0_9990_by_10(char* key, int8_t dir) {
|
||||
int8_t idx = params_find(key);
|
||||
if (idx<0) return;
|
||||
|
||||
if (dir>0) param_values[idx].i16 += +1;
|
||||
if (dir<0) param_values[idx].i16 += -1;
|
||||
|
||||
// clamp
|
||||
if (param_values[idx].i16 > 9990) param_values[idx].i16 = 9990;
|
||||
if (param_values[idx].i16 < 0) param_values[idx].i16 = 0;
|
||||
|
||||
params_save(idx);
|
||||
set_next_alarm();
|
||||
}
|
||||
|
||||
//inline static int8_t abs(int8_t x) { return x<0?-x:x; }
|
||||
void adjust_i32_smart_0_99999(char* key, int8_t dir) {
|
||||
int8_t idx = params_find(key);
|
||||
if (idx<0) return;
|
||||
|
||||
int32_t inc = 1;
|
||||
if (abs(dir) > 5) inc = 5;
|
||||
if (abs(dir) > 10) inc = 10;
|
||||
if (abs(dir) > 13) inc = 50;
|
||||
if (abs(dir) > 16) inc = 100;
|
||||
if (abs(dir) > 19) inc = 200;
|
||||
if (abs(dir) > 22) inc = 1000;
|
||||
|
||||
if (dir>0) param_values[idx].i32 += +inc;
|
||||
if (dir<0) param_values[idx].i32 += -inc;
|
||||
param_values[idx].i32 = (param_values[idx].i32/inc)*inc;
|
||||
|
||||
ESP_LOGI("ADJ", "P[%d] += %d => %ld", (int)idx, (int)inc, (long)param_values[idx].i32);
|
||||
|
||||
// clamp
|
||||
if (param_values[idx].i32 > 99999) param_values[idx].i32 = 99999;
|
||||
if (param_values[idx].i32 < 0) param_values[idx].i32 = 0;
|
||||
|
||||
params_save(idx);
|
||||
set_next_alarm();
|
||||
}
|
||||
void adjust_i32_smart_0_999(char* key, int8_t dir) {
|
||||
int8_t idx = params_find(key);
|
||||
if (idx<0) return;
|
||||
|
||||
int32_t inc = 1;
|
||||
if (abs(dir) > 5) inc = 5;
|
||||
if (abs(dir) > 10) inc = 10;
|
||||
if (abs(dir) > 13) inc = 50;
|
||||
if (abs(dir) > 16) inc = 100;
|
||||
if (abs(dir) > 19) inc = 200;
|
||||
if (abs(dir) > 22) inc = 1000;
|
||||
|
||||
if (dir>0) param_values[idx].i32 += +inc;
|
||||
if (dir<0) param_values[idx].i32 += -inc;
|
||||
param_values[idx].i32 = (param_values[idx].i32/inc)*inc;
|
||||
|
||||
ESP_LOGI("ADJ", "p[%d] += %d => %ld", (int)idx, (int)inc, (long)param_values[idx].i32);
|
||||
|
||||
// clamp
|
||||
if (param_values[idx].i32 > 999) param_values[idx].i32 = 999;
|
||||
if (param_values[idx].i32 < 0) param_values[idx].i32 = 0;
|
||||
|
||||
params_save(idx);
|
||||
set_next_alarm();
|
||||
}
|
||||
|
||||
|
||||
|
||||
static int8_t group_idx=0, entry_idx=0;
|
||||
void run_parameter_ui() {
|
||||
|
||||
if (get_button_repeats(BTN_L)) {
|
||||
reset_shutdown_timer();
|
||||
entry_idx--;
|
||||
if (entry_idx < 0) {
|
||||
group_idx--;
|
||||
if (group_idx < 0) {
|
||||
group_idx = PARAM_GROUP_COUNT-1;
|
||||
}
|
||||
entry_idx = param_group_table[group_idx].num_keys-1;
|
||||
}
|
||||
}
|
||||
if (get_button_repeats(BTN_R)) {
|
||||
reset_shutdown_timer();
|
||||
entry_idx++;
|
||||
if (entry_idx >= param_group_table[group_idx].num_keys) {
|
||||
group_idx++;
|
||||
if (group_idx >= PARAM_GROUP_COUNT) {
|
||||
group_idx = 0;
|
||||
}
|
||||
entry_idx = 0;
|
||||
}
|
||||
}
|
||||
// Forbid user from doing anything until they set the time
|
||||
if (!rtc_is_set()) {
|
||||
group_idx=0;
|
||||
entry_idx=1;
|
||||
}
|
||||
|
||||
param_group_t pg = param_group_table[group_idx];
|
||||
|
||||
lcd_print(pg.formatter(&pg, entry_idx)); // Formatted with botfmt + values
|
||||
|
||||
int8_t n;
|
||||
if ((n=get_button_repeats(BTN_U))) {
|
||||
reset_shutdown_timer();
|
||||
pg.launch_functions[entry_idx](
|
||||
pg.keys[entry_idx], +n
|
||||
);
|
||||
}
|
||||
if ((n=get_button_repeats(BTN_D))) {
|
||||
reset_shutdown_timer();
|
||||
pg.launch_functions[entry_idx](
|
||||
pg.keys[entry_idx], -n
|
||||
);
|
||||
}
|
||||
|
||||
/*int64_t ut = get_button_ms(BTN_U);
|
||||
if (ut) {
|
||||
reset_shutdown_timer();
|
||||
pg.launch_functions[entry_idx](
|
||||
pg.keys[entry_idx], +ut
|
||||
);
|
||||
}
|
||||
int64_t dt = get_button_ms(BTN_D);
|
||||
if (ut) {
|
||||
reset_shutdown_timer();
|
||||
pg.launch_functions[entry_idx](
|
||||
pg.keys[entry_idx], -ut
|
||||
);
|
||||
}*/
|
||||
}
|
||||
|
||||
int8_t parameter_ux_in_override() {
|
||||
if(group_idx != PARAM_GROUP_RUNMTR)
|
||||
return -1;
|
||||
return entry_idx;
|
||||
|
||||
}
|
||||
|
||||
bool parameter_ux_in_ftp() {
|
||||
return group_idx == PARAM_GROUP_FTP;
|
||||
}
|
||||
165
main/i2c.c
Normal file
165
main/i2c.c
Normal file
@@ -0,0 +1,165 @@
|
||||
#include "i2c.h"
|
||||
#include <stdio.h>
|
||||
#include <stdarg.h>
|
||||
#include <string.h>
|
||||
#include "esp_timer.h"
|
||||
#include "esp_log.h"
|
||||
#include "driver/i2c.h"
|
||||
#include "esp_rom_sys.h"
|
||||
|
||||
#define I2C_PORT I2C_NUM_0
|
||||
#define TCA_ADDR_READ 0x21
|
||||
#define TCA_ADDR_WRITE 0x21
|
||||
#define I2C_PULLUP GPIO_PULLUP_DISABLE
|
||||
#define I2C_FREQUENCY 400000
|
||||
|
||||
// TCA9555 Registers
|
||||
#define TCA_REG_INPUT0 0x00
|
||||
#define TCA_REG_INPUT1 0x01
|
||||
#define TCA_REG_OUTPUT0 0x02
|
||||
#define TCA_REG_OUTPUT1 0x03
|
||||
#define TCA_REG_POLARITY0 0x04
|
||||
#define TCA_REG_POLARITY1 0x05
|
||||
#define TCA_REG_CONFIG0 0x06
|
||||
#define TCA_REG_CONFIG1 0x07
|
||||
|
||||
// Debounce & Repeat Settings
|
||||
#define DEBOUNCE_MS 50
|
||||
#define REPEAT_MS 200
|
||||
#define REPEAT_START_MS 700
|
||||
|
||||
// Static Variables
|
||||
static bool i2c_initted = false;
|
||||
|
||||
// === I2C LOW-LEVEL ===
|
||||
static esp_err_t tca_write_word_8(uint8_t reg, uint8_t value) {
|
||||
uint8_t data[2] = { reg, value };
|
||||
return i2c_master_write_to_device(I2C_PORT, TCA_ADDR_WRITE, data, 2, pdMS_TO_TICKS(1000));
|
||||
}
|
||||
static esp_err_t tca_read_word(uint8_t reg, uint16_t *value) {
|
||||
uint8_t data[2];
|
||||
esp_err_t ret = i2c_master_write_read_device(I2C_PORT, TCA_ADDR_READ, ®, 1, data, 2, pdMS_TO_TICKS(1000));
|
||||
if (ret == ESP_OK) {
|
||||
*value = data[0] | (data[1] << 8);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
esp_err_t i2cdev_init(void) {
|
||||
if (i2c_initted) return ESP_OK;
|
||||
|
||||
i2c_config_t conf = {
|
||||
.mode = I2C_MODE_MASTER,
|
||||
.sda_io_num = GPIO_NUM_22,
|
||||
.scl_io_num = GPIO_NUM_21,
|
||||
.sda_pullup_en = I2C_PULLUP,
|
||||
.scl_pullup_en = I2C_PULLUP,
|
||||
.master.clk_speed = I2C_FREQUENCY,
|
||||
};
|
||||
ESP_ERROR_CHECK(i2c_param_config(I2C_PORT, &conf));
|
||||
ESP_ERROR_CHECK(i2c_driver_install(I2C_PORT, conf.mode, 0, 0, 0));
|
||||
|
||||
|
||||
ESP_ERROR_CHECK(tca_write_word_8(TCA_REG_CONFIG0, 0b00000011));
|
||||
ESP_ERROR_CHECK(tca_write_word_8(TCA_REG_CONFIG1, 0b00000000));
|
||||
|
||||
i2c_initted = true;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t i2c_set_relays(uint8_t states) {
|
||||
return tca_write_word_8(TCA_REG_OUTPUT1, states);
|
||||
}
|
||||
esp_err_t i2c_set_led1(uint8_t state) {
|
||||
// push 3 LSB to top
|
||||
return tca_write_word_8(TCA_REG_OUTPUT0, state<<5);
|
||||
}
|
||||
|
||||
#define N_BTNS 2
|
||||
static bool debounced_state[N_BTNS] = {false};
|
||||
static bool last_known_state[N_BTNS] = {false};
|
||||
static uint64_t last_stable_time[N_BTNS] = {0};
|
||||
static uint64_t last_change_time[N_BTNS] = {0};
|
||||
static uint8_t claimed_repeats[N_BTNS] = {0};
|
||||
esp_err_t i2c_poll_buttons() {
|
||||
for (uint8_t btn = 0; btn < N_BTNS; ++btn) {
|
||||
last_known_state[btn] = debounced_state[btn];
|
||||
}
|
||||
|
||||
uint16_t port_val;
|
||||
ESP_ERROR_CHECK(tca_read_word(TCA_REG_INPUT0, &port_val));
|
||||
uint8_t raw_buttons = (uint8_t)(port_val & 0x0F);
|
||||
uint8_t raw_states = ~raw_buttons & 0x0F;
|
||||
|
||||
uint64_t now = esp_timer_get_time() / 1000;
|
||||
|
||||
for (uint8_t btn = 0; btn < N_BTNS; ++btn) {
|
||||
bool raw_pressed = (raw_states & (1 << btn)) != 0;
|
||||
|
||||
if (raw_pressed != debounced_state[btn]) {
|
||||
if (now - last_stable_time[btn] >= DEBOUNCE_MS) {
|
||||
debounced_state[btn] = raw_pressed;
|
||||
last_stable_time[btn] = now;
|
||||
last_change_time[btn] = now;
|
||||
claimed_repeats[btn] = 0;
|
||||
}
|
||||
} else {
|
||||
last_stable_time[btn] = now;
|
||||
}
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
bool i2c_get_button_tripped(uint8_t button) {
|
||||
return (button < N_BTNS) && debounced_state[button] && !last_known_state[button];
|
||||
}
|
||||
|
||||
bool i2c_get_button_released(uint8_t button) {
|
||||
return (button < N_BTNS) && !debounced_state[button] && last_known_state[button];
|
||||
}
|
||||
|
||||
bool i2c_get_button_state(uint8_t button) {
|
||||
return (button < N_BTNS) && debounced_state[button];
|
||||
}
|
||||
|
||||
bool i2c_get_button_repeat(uint8_t btn) {
|
||||
if (btn >= N_BTNS || !debounced_state[btn]) return false;
|
||||
uint64_t now = esp_timer_get_time() / 1000;
|
||||
if (now + DEBOUNCE_MS < last_change_time[btn]) return false;
|
||||
if ((now - last_change_time[btn]) > (REPEAT_START_MS + REPEAT_MS * claimed_repeats[btn])) {
|
||||
claimed_repeats[btn]++;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int8_t i2c_get_button_repeats(uint8_t btn) {
|
||||
if (!i2c_get_button_state(btn))
|
||||
return 0;
|
||||
|
||||
if (btn >= N_BTNS || !debounced_state[btn]) return false;
|
||||
uint64_t now = esp_timer_get_time() / 1000;
|
||||
if (now + DEBOUNCE_MS < last_change_time[btn]) return false;
|
||||
if ((now - last_change_time[btn]) > (REPEAT_START_MS + REPEAT_MS * claimed_repeats[btn])) {
|
||||
claimed_repeats[btn]++;
|
||||
if (claimed_repeats[btn] > 100)
|
||||
claimed_repeats[btn] = 100;
|
||||
ESP_LOGI("BTN", "RPT %d", (uint8_t)claimed_repeats[btn]+2);
|
||||
return claimed_repeats[btn]+1;
|
||||
}
|
||||
if (debounced_state[btn] && !last_known_state[btn]) {
|
||||
|
||||
ESP_LOGI("BTN", "FST %d", 1);
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int64_t i2c_get_button_ms(uint8_t btn) {
|
||||
if (!i2c_get_button_state(btn))
|
||||
return 0;
|
||||
|
||||
uint64_t now = esp_timer_get_time() / 1000;
|
||||
return now - last_change_time[btn];
|
||||
}
|
||||
23
main/i2c.h
Normal file
23
main/i2c.h
Normal file
@@ -0,0 +1,23 @@
|
||||
#ifndef I2C_H_
|
||||
#define I2C_H_
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include "esp_err.h"
|
||||
|
||||
// Public Functions
|
||||
esp_err_t i2cdev_init(void);
|
||||
|
||||
esp_err_t i2c_set_relays(uint8_t states);
|
||||
esp_err_t i2c_set_led1(uint8_t state);
|
||||
|
||||
esp_err_t i2c_poll_buttons();
|
||||
|
||||
bool i2c_get_button_tripped(uint8_t button);
|
||||
bool i2c_get_button_released(uint8_t button);
|
||||
bool i2c_get_button_state(uint8_t button);
|
||||
bool i2c_get_button_repeat(uint8_t btn);
|
||||
int8_t i2c_get_button_repeats(uint8_t btn);
|
||||
int64_t i2c_get_button_ms(uint8_t btn);
|
||||
|
||||
#endif // I2C_H_
|
||||
18
main/idf_component.yml
Normal file
18
main/idf_component.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
## IDF Component Manager Manifest File
|
||||
dependencies:
|
||||
joltwallet/littlefs: "==1.20.3"
|
||||
esp-idf-lib/tca95x5: "*"
|
||||
## Required IDF version
|
||||
idf:
|
||||
version: ">=4.1.0"
|
||||
# # Put list of dependencies here
|
||||
# # For components maintained by Espressif:
|
||||
# component: "~1.0.0"
|
||||
# # For 3rd party components:
|
||||
# username/component: ">=1.0.0,<2.0.0"
|
||||
# username2/component2:
|
||||
# version: "~1.0.0"
|
||||
# # For transient dependencies `public` flag can be set.
|
||||
# # `public` flag doesn't have an effect dependencies of the `main` component.
|
||||
# # All dependencies of `main` are public by default.
|
||||
# public: true
|
||||
68
main/main.c
Normal file
68
main/main.c
Normal file
@@ -0,0 +1,68 @@
|
||||
#include "storage.h"
|
||||
#include "uart_comms.h"
|
||||
#include "esp_err.h"
|
||||
#include "esp_log.h"
|
||||
#include "endian.h"
|
||||
#include "control_fsm.h"
|
||||
#include "power_mgmt.h"
|
||||
#include "rtc.h"
|
||||
#include "sensors.h"
|
||||
|
||||
#define TAG "MAIN"
|
||||
|
||||
esp_err_t send_log() {
|
||||
|
||||
char entry[LOG_ENTRY_SIZE] = {0};
|
||||
entry[0] = LOG_ENTRY_SIZE;
|
||||
|
||||
// Pack 64-bit timestamp into bytes 1-8
|
||||
uint64_t be_timestamp = htobe64(rtc_time_ms());
|
||||
memcpy(&entry[1], &be_timestamp, 8);
|
||||
|
||||
// Pack 32-bit voltages/currents into bytes 9-24
|
||||
/*int32_t be_voltage = htobe32(get_battery_mV());
|
||||
memcpy(&entry[9], &be_voltage, 4);
|
||||
int32_t be_current1 = htobe32(get_bridge_mA(BRIDGE_DRIVE));
|
||||
memcpy(&entry[13], &be_current1, 4);
|
||||
int32_t be_current2 = htobe32(get_bridge_mA(BRIDGE_JACK));
|
||||
memcpy(&entry[17], &be_current2, 4);
|
||||
int32_t be_current3 = htobe32(get_bridge_mA(BRIDGE_AUX));
|
||||
memcpy(&entry[21], &be_current3, 4);
|
||||
|
||||
int32_t be_counter = htobe32(get_sensor_counter(SENSOR_DRIVE));
|
||||
memcpy(&entry[25], &be_counter, 4);
|
||||
|
||||
entry[29] = get_sensor(SENSOR_DRIVE);
|
||||
entry[30] = get_sensor(SENSOR_JACK);
|
||||
entry[31] = fsm_get_state();*/
|
||||
|
||||
return write_log(entry);
|
||||
}
|
||||
|
||||
void app_main(void) {
|
||||
// Initialize storage and load parameters
|
||||
ESP_LOGI(TAG, "Initializing storage...");
|
||||
esp_err_t ret = storage_init();
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Storage init failed, using defaults");
|
||||
}
|
||||
|
||||
// Initialize logging
|
||||
ESP_LOGI(TAG, "Initializing logging...");
|
||||
ret = log_init();
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Log init failed");
|
||||
}
|
||||
|
||||
uart_start();
|
||||
|
||||
|
||||
TickType_t xLastWakeTime = xTaskGetTickCount();
|
||||
const TickType_t xFrequency = pdMS_TO_TICKS(100);
|
||||
|
||||
while(true) {
|
||||
vTaskDelayUntil(&xLastWakeTime, xFrequency);
|
||||
|
||||
send_log();
|
||||
}
|
||||
}
|
||||
550
main/power_mgmt.c
Normal file
550
main/power_mgmt.c
Normal file
@@ -0,0 +1,550 @@
|
||||
/*
|
||||
* power_mgmt.c
|
||||
*
|
||||
* 1 kHz power-management task:
|
||||
* • Samples all three H-bridge current sensors (DRIVE, AUX, JACK)
|
||||
* • Samples battery voltage (BAT)
|
||||
* • Applies EMA filtering on every channel
|
||||
* • Updates shared volatile globals for the control FSM
|
||||
* • Handles over-current spike protection
|
||||
*
|
||||
* Updated to modern ESP-IDF ADC API (line fitting)
|
||||
* All variables now defined locally
|
||||
*
|
||||
* Created on: Nov 10, 2025
|
||||
*/
|
||||
|
||||
#include <math.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include "driver/rtc_io.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_task_wdt.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_adc/adc_oneshot.h"
|
||||
#include "esp_adc/adc_cali.h"
|
||||
#include "esp_adc/adc_cali_scheme.h"
|
||||
#include "esp_timer.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "control_fsm.h"
|
||||
#include "soc/rtc_io_reg.h"
|
||||
#include "power_mgmt.h"
|
||||
|
||||
#include "storage.h"
|
||||
#include "rtc.h"
|
||||
|
||||
// === GPIO Pin Definitions ===
|
||||
#define PIN_V_ISENS1 ADC_CHANNEL_6 // GPIO34
|
||||
#define PIN_V_ISENS2 ADC_CHANNEL_3 // GPIO39 / VN
|
||||
#define PIN_V_ISENS3 ADC_CHANNEL_7 // GPIO35
|
||||
#define PIN_V_BATTERY ADC_CHANNEL_0 // GPIO36 / VP
|
||||
#define PIN_V_SENS_BAT PIN_V_BATTERY
|
||||
|
||||
#define PIN_CHG_DISABLE GPIO_NUM_26
|
||||
#define PIN_CHG_BULK GPIO_NUM_27
|
||||
|
||||
#define AUTOZERO_THRESH 2000.0f // mA
|
||||
|
||||
|
||||
typedef enum {
|
||||
CHG_T_LOWBAT = 0,
|
||||
CHG_T_BULK = 1,
|
||||
CHG_T_STEADY = 2,
|
||||
} charge_timer_t;
|
||||
|
||||
#define N_CHG_TIMERS 3
|
||||
RTC_DATA_ATTR charge_state_t current_charge_state = CHG_STATE_BULK;
|
||||
RTC_DATA_ATTR int64_t charge_timers[N_CHG_TIMERS] = {-1};
|
||||
|
||||
int64_t now;
|
||||
|
||||
charge_state_t get_charging_state() { return current_charge_state; }
|
||||
|
||||
void setTimerN(charge_timer_t i, int64_t sec) {
|
||||
// set the timer for <sec> in the future if it's currently less than now
|
||||
if (charge_timers[i] < now) {
|
||||
charge_timers[i] = now + sec;
|
||||
ESP_LOGI("BAT", "Set timer[%d] +%lld", i, (long long)sec);
|
||||
}
|
||||
}
|
||||
|
||||
void resetTimerN(charge_timer_t i) {
|
||||
charge_timers[i] = -1;
|
||||
}
|
||||
void resetBatTimers() {
|
||||
for (uint8_t i=0; i<N_CHG_TIMERS; i++)
|
||||
resetTimerN(i);
|
||||
}
|
||||
|
||||
bool getTimerN(charge_timer_t i) {
|
||||
if (charge_timers[i] < 0) return false;
|
||||
return system_rtc_get_raw_time() > charge_timers[i];
|
||||
}
|
||||
|
||||
#define BULK_CHARGE_S 20 //2*60*60
|
||||
#define FLOAT_STEADY_S 10 //30*60
|
||||
#define LOW_DETECT_S 10 //5*60
|
||||
|
||||
#define STEADY_MV 13000
|
||||
#define LOW_MV 12800
|
||||
|
||||
void run_charge_fsm() {
|
||||
now = system_rtc_get_raw_time();
|
||||
|
||||
//ESP_LOGI("BAT", "FSM STATE %d", current_charge_state);
|
||||
|
||||
if (rtc_is_set()) {
|
||||
switch(current_charge_state) {
|
||||
case CHG_STATE_BULK:
|
||||
// turn off bulk charging and go to float when time is up
|
||||
if (getTimerN(CHG_T_BULK)) {
|
||||
ESP_LOGI("BAT", "BULK -> FLOAT");
|
||||
current_charge_state = CHG_STATE_FLOAT;
|
||||
}
|
||||
break;
|
||||
|
||||
case CHG_STATE_FLOAT:
|
||||
if (getTimerN(CHG_T_STEADY)) {
|
||||
ESP_LOGI("BAT", "FLOAT -> OFF");
|
||||
current_charge_state = CHG_STATE_OFF;
|
||||
}
|
||||
if (get_battery_mV() > STEADY_MV) {
|
||||
setTimerN(CHG_T_STEADY, FLOAT_STEADY_S);
|
||||
} else {
|
||||
resetTimerN(CHG_T_STEADY);
|
||||
}
|
||||
// NO break; !! float should also kick into bulk with same triggers
|
||||
case CHG_STATE_OFF:
|
||||
|
||||
// after 5 minutes of low-ish battery go into bulk charge
|
||||
if (getTimerN(CHG_T_LOWBAT)) {
|
||||
ESP_LOGI("BAT", " -> BULK");
|
||||
current_charge_state = CHG_STATE_BULK;
|
||||
setTimerN(CHG_T_BULK, BULK_CHARGE_S);
|
||||
}
|
||||
if (get_battery_mV() < LOW_MV) {
|
||||
setTimerN(CHG_T_LOWBAT, LOW_DETECT_S);
|
||||
} else {
|
||||
resetTimerN(CHG_T_LOWBAT);
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
//ESP_LOGI("BAT", " -> BULK");
|
||||
current_charge_state = CHG_STATE_BULK;
|
||||
}
|
||||
|
||||
//rtc_gpio_hold_dis(PIN_CHG_BULK);
|
||||
//rtc_gpio_hold_dis(PIN_CHG_DISABLE);
|
||||
switch(current_charge_state) {
|
||||
case CHG_STATE_BULK:
|
||||
gpio_set_level(PIN_CHG_BULK, 1);
|
||||
gpio_set_level(PIN_CHG_DISABLE, 0);
|
||||
//ESP_LOGI("BAT", "BULK");
|
||||
break;
|
||||
case CHG_STATE_FLOAT:
|
||||
gpio_set_level(PIN_CHG_BULK, 0);
|
||||
gpio_set_level(PIN_CHG_DISABLE, 0);
|
||||
//ESP_LOGI("BAT", "FLOAT");
|
||||
break;
|
||||
case CHG_STATE_OFF:
|
||||
gpio_set_level(PIN_CHG_BULK, 0);
|
||||
gpio_set_level(PIN_CHG_DISABLE, 1);
|
||||
//ESP_LOGI("BAT", "OFF");
|
||||
break;
|
||||
}
|
||||
//rtc_gpio_hold_en(PIN_CHG_BULK);
|
||||
//rtc_gpio_hold_en(PIN_CHG_DISABLE);
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
bool enabled; // Auto-zero active for this channel
|
||||
float threshold_ma; // Max current to consider "zero" (mA)
|
||||
float learned_offset_mv; // Accumulated zero offset (mV)
|
||||
bool initialized; // First valid zero established
|
||||
} autozero_t;
|
||||
static autozero_t autozero[N_BRIDGES] = {0};
|
||||
|
||||
// === E-Fuse (Software Breaker) Configuration ===
|
||||
static const char* currentLimits_A[N_BRIDGES] = {
|
||||
[BRIDGE_DRIVE] = "efuse_drive_A", //40000,
|
||||
[BRIDGE_AUX] = "efuse_aux_A", // 5000,
|
||||
[BRIDGE_JACK] = "efuse_jack_A" // 10000
|
||||
};
|
||||
static const float i2t_thresholds[N_BRIDGES] = { // A^2*s (tunable per bridge if needed)
|
||||
[BRIDGE_DRIVE] = 6.0f,
|
||||
[BRIDGE_AUX] = 6.0f,
|
||||
[BRIDGE_JACK] = 6.0f
|
||||
};
|
||||
static const float i_instant[N_BRIDGES] = { // Instant trip multiplier of I_rated
|
||||
[BRIDGE_DRIVE] = 15.0f,
|
||||
[BRIDGE_AUX] = 15.0f,
|
||||
[BRIDGE_JACK] = 15.0f
|
||||
};
|
||||
static const float cool_rate[N_BRIDGES] = { // Cooling constant (1/s)
|
||||
[BRIDGE_DRIVE] = 0.008f,
|
||||
[BRIDGE_AUX] = 0.008f,
|
||||
[BRIDGE_JACK] = 0.008f
|
||||
};
|
||||
static const int32_t cooldown_ms[N_BRIDGES] = { // Auto-reset delay after trip
|
||||
[BRIDGE_DRIVE] = 5000,
|
||||
[BRIDGE_AUX] = 5000,
|
||||
[BRIDGE_JACK] = 5000
|
||||
};
|
||||
|
||||
static float efuse_heat[N_BRIDGES] = {0};
|
||||
static uint64_t efuse_trip_time[N_BRIDGES] = {0}; // Timestamp when tripped
|
||||
static bool efuse_tripped[N_BRIDGES] = {false};
|
||||
|
||||
// === ADC Handles ===
|
||||
static adc_oneshot_unit_handle_t adc1_handle = NULL;
|
||||
static adc_cali_handle_t adc_cali_handle = NULL;
|
||||
|
||||
// === EMA Filter State ===
|
||||
#define EMA_ALPHA_CURRENT 0.5f
|
||||
#define EMA_ALPHA_BATTERY 0.05f
|
||||
|
||||
static float ema_current[N_BRIDGES] = {0};
|
||||
static bool ema_init[N_BRIDGES] = {false};
|
||||
|
||||
static float ema_battery = 0.0f;
|
||||
static bool ema_battery_init = false;
|
||||
|
||||
// === Shared Volatile Outputs ===
|
||||
volatile int32_t bridgeCurrents_mA[N_BRIDGES] = {0};
|
||||
volatile int32_t batteryVoltage_mV = 0;
|
||||
|
||||
// === ADC Initialization ===
|
||||
static esp_err_t adc_init(void) {
|
||||
if (adc1_handle != NULL) {
|
||||
return ESP_OK; // Already initialized
|
||||
}
|
||||
|
||||
// ADC1 oneshot mode
|
||||
adc_oneshot_unit_init_cfg_t init_cfg = {
|
||||
.unit_id = ADC_UNIT_1,
|
||||
};
|
||||
ESP_ERROR_CHECK(adc_oneshot_new_unit(&init_cfg, &adc1_handle));
|
||||
|
||||
// Configure all channels
|
||||
adc_oneshot_chan_cfg_t chan_cfg = {
|
||||
.atten = ADC_ATTEN_DB_11,
|
||||
.bitwidth = ADC_BITWIDTH_12,
|
||||
};
|
||||
|
||||
ESP_ERROR_CHECK(adc_oneshot_config_channel(adc1_handle, PIN_V_ISENS1, &chan_cfg));
|
||||
ESP_ERROR_CHECK(adc_oneshot_config_channel(adc1_handle, PIN_V_ISENS2, &chan_cfg));
|
||||
ESP_ERROR_CHECK(adc_oneshot_config_channel(adc1_handle, PIN_V_ISENS3, &chan_cfg));
|
||||
ESP_ERROR_CHECK(adc_oneshot_config_channel(adc1_handle, PIN_V_SENS_BAT, &chan_cfg));
|
||||
|
||||
// Line fitting calibration (modern scheme)
|
||||
adc_cali_line_fitting_config_t cali_cfg = {
|
||||
.unit_id = ADC_UNIT_1,
|
||||
.atten = ADC_ATTEN_DB_11,
|
||||
.bitwidth = ADC_BITWIDTH_12,
|
||||
};
|
||||
ESP_ERROR_CHECK(adc_cali_create_scheme_line_fitting(&cali_cfg, &adc_cali_handle));
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void autozero_enable(bridge_t bridge, bool enable) {
|
||||
if (bridge >= N_BRIDGES) return;
|
||||
autozero[bridge].enabled = enable;
|
||||
if (!enable) {
|
||||
autozero[bridge].learned_offset_mv = 0.0f;
|
||||
autozero[bridge].initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
void autozero_set_threshold(bridge_t bridge, float threshold_ma) {
|
||||
if (bridge >= N_BRIDGES) return;
|
||||
autozero[bridge].threshold_ma = fmaxf(0.0f, threshold_ma);
|
||||
}
|
||||
|
||||
float autozero_get_offset_mv(bridge_t bridge) {
|
||||
if (bridge >= N_BRIDGES) return 0.0f;
|
||||
return autozero[bridge].learned_offset_mv;
|
||||
}
|
||||
|
||||
void autozero_reset(bridge_t bridge) {
|
||||
if (bridge >= N_BRIDGES) return;
|
||||
autozero[bridge].learned_offset_mv = 0.0f;
|
||||
autozero[bridge].initialized = false;
|
||||
}
|
||||
|
||||
void autozero_reset_all(void) {
|
||||
for (uint8_t i = 0; i < N_BRIDGES; i++) {
|
||||
autozero_reset((bridge_t)i);
|
||||
}
|
||||
}
|
||||
|
||||
// === Raw Current Reading (mA) ===
|
||||
static int32_t read_bridge_current_raw(bridge_t bridge) {
|
||||
int adc_raw = 0;
|
||||
int voltage_mv = 0;
|
||||
|
||||
adc_channel_t pin;
|
||||
switch(bridge) {
|
||||
case BRIDGE_DRIVE: pin = PIN_V_ISENS3; break;
|
||||
case BRIDGE_AUX: pin = PIN_V_ISENS1; break;
|
||||
case BRIDGE_JACK: pin = PIN_V_ISENS2; break;
|
||||
default: return -42069;
|
||||
}
|
||||
|
||||
if (adc_oneshot_read(adc1_handle, pin, &adc_raw) != ESP_OK) {
|
||||
return 0;
|
||||
}
|
||||
if (adc_cali_raw_to_voltage(adc_cali_handle, adc_raw, &voltage_mv) != ESP_OK) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
float current_sense_mv = (float)voltage_mv;
|
||||
autozero_t *az = &autozero[bridge];
|
||||
|
||||
// === AUTO-ZERO LEARNING PHASE ===
|
||||
if (az->enabled && get_bridge_state(bridge)==0) {
|
||||
float raw_current_ma = 0.0f;
|
||||
switch (bridge) {
|
||||
case BRIDGE_JACK:
|
||||
case BRIDGE_AUX:
|
||||
// ACS37042KLHBLT-030B3 is 30A capable and 44 mV/A
|
||||
raw_current_ma = (current_sense_mv - 1650.0f) * 1000.0f / 44.0f;
|
||||
break;
|
||||
case BRIDGE_DRIVE:
|
||||
// ACS37220LEZATR-100B3 is 100A capable and 13.2 mV/A
|
||||
raw_current_ma = (current_sense_mv - 1650.0f) * 1000.0f / 13.20f;
|
||||
break;
|
||||
}
|
||||
|
||||
if (fabsf(raw_current_ma) <= az->threshold_ma) {
|
||||
// Valid zero sample
|
||||
if (!az->initialized) {
|
||||
az->learned_offset_mv = current_sense_mv - 1650.0f;
|
||||
az->initialized = true;
|
||||
} else {
|
||||
// EMA on offset (slow adaptation)
|
||||
float alpha = 0.1f;
|
||||
az->learned_offset_mv = alpha * (current_sense_mv - 1650.0f) +
|
||||
(1.0f - alpha) * az->learned_offset_mv;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === APPLY AUTO-ZERO OFFSET ===
|
||||
float corrected_mv = current_sense_mv - az->learned_offset_mv;
|
||||
|
||||
int32_t offset_mv = (int32_t)(corrected_mv - 1650.0f);
|
||||
int32_t current_ma = 0;
|
||||
|
||||
switch (bridge) {
|
||||
case BRIDGE_JACK:
|
||||
case BRIDGE_AUX:
|
||||
current_ma = offset_mv * 1000 / 44; // 44 mV/A
|
||||
break;
|
||||
case BRIDGE_DRIVE:
|
||||
current_ma = offset_mv * 10000 / 132; // 13.2 mV/A
|
||||
break;
|
||||
}
|
||||
|
||||
return current_ma;
|
||||
}
|
||||
|
||||
// === Raw Battery Voltage Reading (mV) ===
|
||||
static int32_t read_battery_voltage_raw(void)
|
||||
{
|
||||
int adc_raw = 0;
|
||||
int voltage_mv = 0;
|
||||
|
||||
if (adc_oneshot_read(adc1_handle, PIN_V_SENS_BAT, &adc_raw) != ESP_OK) {
|
||||
return 0;
|
||||
}
|
||||
if (adc_cali_raw_to_voltage(adc_cali_handle, adc_raw, &voltage_mv) != ESP_OK) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Voltage divider: 150kΩ to 1MΩ → gain = 1.15 → scale = 1150/150
|
||||
return (int32_t)voltage_mv * 1150 / 150;
|
||||
}
|
||||
|
||||
// === EMA Filter Update ===
|
||||
static void apply_ema(float *state, bool *init, float alpha, int32_t raw, volatile int32_t *out)
|
||||
{
|
||||
if (!*init) {
|
||||
*state = (float)raw;
|
||||
*init = true;
|
||||
} else {
|
||||
*state = alpha * (float)raw + (1.0f - alpha) * *state;
|
||||
}
|
||||
*out = (int32_t)(*state + 0.5f);
|
||||
}
|
||||
|
||||
// === Public Accessors ===
|
||||
int32_t get_bridge_mA(uint8_t bridge)
|
||||
{
|
||||
if (bridge >= N_BRIDGES) return -1;
|
||||
return (int32_t)bridgeCurrents_mA[bridge];
|
||||
}
|
||||
|
||||
int32_t get_battery_mV(void)
|
||||
{
|
||||
return (int32_t)batteryVoltage_mV;
|
||||
}
|
||||
|
||||
// === E-Fuse: Trip Logic (called every cycle) ===
|
||||
static void efuse_update(uint8_t bridge, float I, float dt, uint64_t now)
|
||||
{
|
||||
float I_rated = (float)get_param_i8(currentLimits_A[bridge]);
|
||||
float I_norm = I / I_rated;
|
||||
|
||||
// Instant trip on extreme overcurrent
|
||||
if (I_norm >= i_instant[bridge]) {
|
||||
efuse_tripped[bridge] = true;
|
||||
efuse_trip_time[bridge] = now;
|
||||
return;
|
||||
}
|
||||
|
||||
// Cooling when below threshold
|
||||
if (I_norm < 1.1f) {
|
||||
efuse_heat[bridge] -= efuse_heat[bridge] * cool_rate[bridge] * dt;
|
||||
efuse_heat[bridge] = fmaxf(0.0f, efuse_heat[bridge]);
|
||||
efuse_tripped[bridge] = false; // Auto-clear if cooled
|
||||
return;
|
||||
}
|
||||
|
||||
// Accumulate heat (I²t)
|
||||
efuse_heat[bridge] += (I_norm * I_norm) * dt;
|
||||
|
||||
if (efuse_heat[bridge] >= i2t_thresholds[bridge]) {
|
||||
efuse_tripped[bridge] = true;
|
||||
efuse_trip_time[bridge] = now;
|
||||
}
|
||||
}
|
||||
|
||||
// === E-Fuse: Auto-Reset After Cooldown ===
|
||||
static void efuse_cooldown_check(uint64_t now)
|
||||
{
|
||||
for (uint8_t i = 0; i < N_BRIDGES; i++) {
|
||||
if (efuse_tripped[i] &&
|
||||
(now - efuse_trip_time[i]) >= (cooldown_ms[i] * 1000ULL)) {
|
||||
efuse_heat[i] = 0.0f;
|
||||
efuse_tripped[i] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === Public E-Fuse Controls ===
|
||||
void efuse_reset_all(void)
|
||||
{
|
||||
for (uint8_t i = 0; i < N_BRIDGES; i++) {
|
||||
efuse_heat[i] = 0.0f;
|
||||
efuse_tripped[i] = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool efuse_is_tripped(uint8_t bridge)
|
||||
{
|
||||
if (bridge >= N_BRIDGES) return false;
|
||||
return efuse_tripped[bridge];
|
||||
}
|
||||
|
||||
|
||||
// === Power Management Task ===
|
||||
void power_mgmt_task(void *param) {
|
||||
esp_task_wdt_add(NULL);
|
||||
|
||||
/*gpio_config_t io_conf = {
|
||||
.pin_bit_mask = (1ULL << PIN_CHG_DISABLE) | (1ULL << PIN_CHG_BULK),
|
||||
.mode = GPIO_MODE_OUTPUT,
|
||||
.pull_up_en = GPIO_PULLUP_DISABLE,
|
||||
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||
.intr_type = GPIO_INTR_DISABLE,
|
||||
};
|
||||
gpio_config(&io_conf);*/
|
||||
|
||||
/*// Enable RTC GPIO domain (required for hold)
|
||||
rtc_gpio_init(PIN_CHG_DISABLE);
|
||||
rtc_gpio_init(PIN_CHG_BULK);
|
||||
|
||||
// Set as output
|
||||
rtc_gpio_set_direction(PIN_CHG_DISABLE, RTC_GPIO_MODE_OUTPUT_ONLY);
|
||||
rtc_gpio_set_direction(PIN_CHG_BULK, RTC_GPIO_MODE_OUTPUT_ONLY);
|
||||
|
||||
// Optional: set initial level (will be held)
|
||||
//rtc_gpio_set_level(PIN_CHG_DISABLE, 1); // e.g., start disabled
|
||||
//rtc_gpio_set_level(PIN_CHG_BULK, 0);
|
||||
|
||||
// **Critical: Enable hold function**
|
||||
rtc_gpio_hold_en(PIN_CHG_DISABLE);
|
||||
rtc_gpio_hold_en(PIN_CHG_BULK);*/
|
||||
|
||||
ESP_ERROR_CHECK(adc_init());
|
||||
|
||||
TickType_t xLastWakeTime = xTaskGetTickCount();
|
||||
const TickType_t xFrequency = pdMS_TO_TICKS(20);
|
||||
|
||||
// Optional: Enable auto-zero with default threshold
|
||||
autozero_enable(BRIDGE_DRIVE, true);
|
||||
autozero_enable(BRIDGE_AUX, true);
|
||||
autozero_enable(BRIDGE_JACK, true);
|
||||
|
||||
autozero_set_threshold(BRIDGE_DRIVE, AUTOZERO_THRESH);
|
||||
autozero_set_threshold(BRIDGE_AUX, AUTOZERO_THRESH);
|
||||
autozero_set_threshold(BRIDGE_JACK, AUTOZERO_THRESH);
|
||||
|
||||
//uint64_t last_wake_time = esp_timer_get_time();
|
||||
//const uint64_t period = 5000; // 100 us => 10kHz
|
||||
while (1) {
|
||||
vTaskDelayUntil(&xLastWakeTime, xFrequency);
|
||||
uint64_t now_us = esp_timer_get_time();
|
||||
|
||||
/*if (now - last_wake_time < period) {
|
||||
uint32_t delay_us = (period - (now - last_wake_time)) / 1000;
|
||||
if (delay_us > 0) vTaskDelay(pdMS_TO_TICKS(delay_us));
|
||||
continue;
|
||||
}
|
||||
last_wake_time = now;*/
|
||||
|
||||
// Sample currents
|
||||
for (uint8_t i = 0; i < N_BRIDGES; i++) {
|
||||
int32_t raw_ma = read_bridge_current_raw((bridge_t)i);
|
||||
apply_ema(&ema_current[i], &ema_init[i], EMA_ALPHA_CURRENT,
|
||||
raw_ma, &bridgeCurrents_mA[i]);
|
||||
|
||||
// Reset spike timer if under limit
|
||||
/*if (bridgeCurrents_mA[i] < currentLimits_mA[i]) {
|
||||
currentSpikeSafeTimes[i] = now + CURRENT_SPIKE_TIME_US;
|
||||
}*/
|
||||
|
||||
// === E-FUSE UPDATE ===
|
||||
float I = (float)bridgeCurrents_mA[i] / 1000.0f;
|
||||
float dt = 0.020f; // 20 ms task period
|
||||
efuse_update(i, I, dt, now_us);
|
||||
}
|
||||
|
||||
/*ESP_LOGI("PWR", "[ %6ld | %6ld | %6ld mA ] { %6ld mV }",
|
||||
(long)bridgeCurrents_mA[BRIDGE_DRIVE],
|
||||
(long)bridgeCurrents_mA[BRIDGE_JACK],
|
||||
(long)bridgeCurrents_mA[BRIDGE_AUX],
|
||||
(long)batteryVoltage_mV);*/
|
||||
|
||||
// Sample battery
|
||||
int32_t raw_bat = read_battery_voltage_raw();
|
||||
apply_ema(&ema_battery, &ema_battery_init, EMA_ALPHA_BATTERY,
|
||||
raw_bat, &batteryVoltage_mV);
|
||||
|
||||
|
||||
//run_charge_fsm();
|
||||
efuse_cooldown_check(now_us);
|
||||
esp_task_wdt_reset();
|
||||
}
|
||||
}
|
||||
|
||||
void start_power() {
|
||||
xTaskCreate(power_mgmt_task, "PWR", 4096, NULL, 5, NULL);
|
||||
}
|
||||
|
||||
void shutdown_power() {
|
||||
|
||||
}
|
||||
34
main/power_mgmt.h
Normal file
34
main/power_mgmt.h
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* power_mgmt.h
|
||||
*
|
||||
* Created on: Nov 3, 2025
|
||||
* Author: Thad
|
||||
*/
|
||||
|
||||
#ifndef MAIN_POWER_MGMT_H_
|
||||
#define MAIN_POWER_MGMT_H_
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef enum {
|
||||
CHG_STATE_OFF = 0,
|
||||
CHG_STATE_FLOAT = 1,
|
||||
CHG_STATE_BULK = 2
|
||||
} charge_state_t;
|
||||
#define N_CHARGE_STATES 3
|
||||
|
||||
charge_state_t get_charging_state();
|
||||
|
||||
void resetBatTimers();
|
||||
|
||||
void efuse_reset_all(void); // Clear all trip states (manual/programmatic reset)
|
||||
bool efuse_is_tripped(uint8_t bridge); // Query if bridge is currently faulted
|
||||
|
||||
int32_t get_bridge_mA(uint8_t bridge);
|
||||
int32_t get_battery_mV();
|
||||
|
||||
void start_power();
|
||||
void shutdown_power();
|
||||
|
||||
#endif /* MAIN_POWER_MGMT_H_ */
|
||||
231
main/rf.c
Normal file
231
main/rf.c
Normal file
@@ -0,0 +1,231 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include "driver/rmt_rx.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_log.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include "esp_task_wdt.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/queue.h"
|
||||
#include "driver/rmt_rx.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
#include "flash.h"
|
||||
|
||||
#define RF_PIN GPIO_NUM_23
|
||||
#define P_HIGH 1040
|
||||
#define P_LOW 340
|
||||
#define P_MARGIN 70
|
||||
#define P_SKIPMIN 250
|
||||
#define RF_DEBUG 0
|
||||
#define NUM_RF_BUTTONS 4
|
||||
|
||||
// Struct to hold decoded RF data
|
||||
typedef struct {
|
||||
uint32_t code;
|
||||
uint16_t high_avg;
|
||||
uint16_t low_avg;
|
||||
uint8_t errors;
|
||||
size_t num_symbols;
|
||||
} rf_code_t;
|
||||
|
||||
// Global queue for passing decoded codes between tasks
|
||||
static QueueHandle_t g_code_queue = NULL;
|
||||
|
||||
// For rmt_rx_register_event_callbacks
|
||||
static bool rfrx_done(rmt_channel_handle_t channel, const rmt_rx_done_event_data_t *edata, void *udata) {
|
||||
BaseType_t high_task_wakeup = pdFALSE;
|
||||
QueueHandle_t drx_queue = (QueueHandle_t)udata;
|
||||
xQueueSendFromISR(drx_queue, edata, &high_task_wakeup);
|
||||
return high_task_wakeup == pdTRUE;
|
||||
}
|
||||
|
||||
// Task that receives and decodes RF signals
|
||||
static void rf_receiver_task(void* param) {
|
||||
esp_task_wdt_add(NULL);
|
||||
const uint16_t tlow = (P_HIGH - P_LOW - (2 * P_MARGIN));
|
||||
const uint16_t thigh = (P_HIGH - P_LOW + (2 * P_MARGIN));
|
||||
|
||||
rmt_channel_handle_t rx_channel = NULL;
|
||||
rmt_symbol_word_t symbols[64];
|
||||
rmt_rx_done_event_data_t rx_data;
|
||||
|
||||
rmt_receive_config_t rx_config = {
|
||||
.signal_range_min_ns = 2000,
|
||||
.signal_range_max_ns = 1250000,
|
||||
};
|
||||
|
||||
rmt_rx_channel_config_t rx_ch_conf = {
|
||||
.gpio_num = (gpio_num_t)RF_PIN,
|
||||
.clk_src = RMT_CLK_SRC_DEFAULT,
|
||||
.resolution_hz = 1000000,
|
||||
.mem_block_symbols = 64,
|
||||
.flags = {
|
||||
.invert_in = false,
|
||||
.with_dma = false,
|
||||
}
|
||||
};
|
||||
|
||||
ESP_ERROR_CHECK(rmt_new_rx_channel(&rx_ch_conf, &rx_channel));
|
||||
|
||||
QueueHandle_t rx_queue = xQueueCreate(1, sizeof(rx_data));
|
||||
assert(rx_queue);
|
||||
|
||||
rmt_rx_event_callbacks_t cbs = {
|
||||
.on_recv_done = rfrx_done,
|
||||
};
|
||||
|
||||
ESP_ERROR_CHECK(rmt_rx_register_event_callbacks(rx_channel, &cbs, rx_queue));
|
||||
ESP_ERROR_CHECK(rmt_enable(rx_channel));
|
||||
ESP_ERROR_CHECK(rmt_receive(rx_channel, symbols, sizeof(symbols), &rx_config));
|
||||
|
||||
ESP_LOGI("RF", "RF receiver task started on core %d", xPortGetCoreID());
|
||||
|
||||
for(;;) {
|
||||
if (xQueueReceive(rx_queue, &rx_data, pdMS_TO_TICKS(1000)) == pdPASS) {
|
||||
|
||||
size_t len = rx_data.num_symbols;
|
||||
rmt_symbol_word_t *cur = rx_data.received_symbols;
|
||||
|
||||
if (len > 23) {
|
||||
uint32_t code = 0;
|
||||
uint16_t low = 0, high = 0, err = 0;
|
||||
|
||||
// Decode the 24-bit code
|
||||
for (uint8_t i = 0; i < 24; i++) {
|
||||
uint16_t dur0 = (uint16_t)cur[i].duration0;
|
||||
uint16_t dur1 = (uint16_t)cur[i].duration1;
|
||||
|
||||
// Validate symbol format
|
||||
if (!(cur[i].level0 && !cur[i].level1 && dur0 >= P_SKIPMIN && dur1 >= P_SKIPMIN)) {
|
||||
code = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
// Determine bit value based on pulse duration
|
||||
if ((dur0 - dur1) > 0) {
|
||||
code = (code | (1ULL << (23 - i)));
|
||||
high += dur0;
|
||||
low += dur1;
|
||||
} else {
|
||||
high += dur1;
|
||||
low += dur0;
|
||||
}
|
||||
|
||||
// Check if pulse timing is within expected range
|
||||
int16_t diff = abs(dur0 - dur1);
|
||||
if ((diff < tlow) || (diff > thigh)) {
|
||||
err++;
|
||||
}
|
||||
}
|
||||
|
||||
// If we got a valid code, send it to processing task
|
||||
if (code) {
|
||||
rf_code_t rf_msg = {
|
||||
.code = code,
|
||||
.high_avg = high / 24,
|
||||
.low_avg = low / 24,
|
||||
.errors = err,
|
||||
.num_symbols = len
|
||||
};
|
||||
|
||||
// Non-blocking send - if queue is full, just drop it
|
||||
xQueueSend(g_code_queue, &rf_msg, 0);
|
||||
}
|
||||
|
||||
// Debug output - print raw symbols
|
||||
#if RF_DEBUG
|
||||
char buf[128];
|
||||
char tbuf[30];
|
||||
snprintf(tbuf, 20, "Rf%zu: ", len);
|
||||
strcpy(buf, tbuf);
|
||||
|
||||
for (uint8_t i = 0; i < len; i++) {
|
||||
if (strlen(buf) > 100) {
|
||||
printf("%s", buf);
|
||||
buf[0] = '\0';
|
||||
}
|
||||
|
||||
int d0 = cur[i].duration0;
|
||||
if (!cur[i].level0) {
|
||||
d0 *= -1;
|
||||
}
|
||||
|
||||
int d1 = cur[i].duration1;
|
||||
if (!cur[i].level1) {
|
||||
d1 *= -1;
|
||||
}
|
||||
|
||||
snprintf(tbuf, 30, "%d,%d ", d0, d1);
|
||||
strcat(buf, tbuf);
|
||||
}
|
||||
printf("%s\n\n", buf);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Start next receive
|
||||
|
||||
rmt_receive(rx_channel, symbols, sizeof(symbols), &rx_config);
|
||||
|
||||
esp_task_wdt_reset();
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup (never reached in this case)
|
||||
rmt_disable(rx_channel);
|
||||
rmt_del_channel(rx_channel);
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
void start_rf() {
|
||||
g_code_queue = xQueueCreate(5, sizeof(rf_code_t));
|
||||
assert(g_code_queue);
|
||||
|
||||
xTaskCreate(rf_receiver_task, "RF", 4096, NULL, 10, NULL);
|
||||
}
|
||||
|
||||
void rf_set_keycode(uint8_t index, int64_t code) {
|
||||
char key[] = "keycode0";
|
||||
key[7] = 48+index; // ASCII
|
||||
|
||||
ESP_LOGI("RF", "SET KEYCODE[%d] = 0x%16llx", index, code);
|
||||
|
||||
set_param_i64(key, code);
|
||||
}
|
||||
|
||||
int8_t rf_get_keycode() {
|
||||
rf_code_t received_code;
|
||||
|
||||
char key[] = "keycode0";
|
||||
|
||||
if (xQueueReceive(g_code_queue, &received_code, 0) == pdPASS) {
|
||||
int64_t newcode = ((int64_t)received_code.num_symbols << 56) | received_code.code;
|
||||
|
||||
for (uint8_t i = 0; i < NUM_RF_BUTTONS; i++) {
|
||||
key[7] = 48+i; // ASCII
|
||||
if (newcode == get_param_i64(key))
|
||||
return i;
|
||||
}
|
||||
ESP_LOGI("RF", "Received unknown code 0x%08lx (%d) [0x%16llx]", (unsigned long)received_code.code, received_code.num_symbols, (unsigned long long) newcode);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int64_t rf_get_raw_keycode() {
|
||||
rf_code_t received_code;
|
||||
int64_t code = -1;
|
||||
if (xQueueReceive(g_code_queue, &received_code, 0) == pdPASS) {
|
||||
code = ((int64_t)received_code.num_symbols << 56) | received_code.code;
|
||||
//ESP_LOGI("RF", "Raw Code 0x%08lx (%d) [0x%16llx]", (unsigned long)received_code.code, received_code.num_symbols, (unsigned long long) code);
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
void rf_clear_queue() {
|
||||
xQueueReset(g_code_queue);
|
||||
}
|
||||
26
main/rf.h
Normal file
26
main/rf.h
Normal file
@@ -0,0 +1,26 @@
|
||||
|
||||
#ifndef RF_H
|
||||
#define RF_H
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <stdio.h>
|
||||
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/queue.h"
|
||||
|
||||
#define NUM_RF_BUTTONS 4
|
||||
|
||||
int64_t recieveKeycode();
|
||||
|
||||
void start_rf();
|
||||
|
||||
void rf_set_keycode(uint8_t index, int64_t code);
|
||||
|
||||
int8_t rf_get_keycode();
|
||||
int64_t rf_get_raw_keycode();
|
||||
|
||||
void rf_clear_queue();
|
||||
|
||||
#endif
|
||||
243
main/rtc.c
Normal file
243
main/rtc.c
Normal file
@@ -0,0 +1,243 @@
|
||||
/*
|
||||
* system.c
|
||||
*
|
||||
* Implementation of system.h services.
|
||||
* Battery charge-state machine, deep-sleep, RTC, inactivity handling.
|
||||
*
|
||||
* Battery voltage is read from the shared volatile updated by power_mgmt_task.
|
||||
*/
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <time.h>
|
||||
#include <sys/time.h>
|
||||
|
||||
#include "power_mgmt.h"
|
||||
#include "rtc.h"
|
||||
#include "control_fsm.h"
|
||||
#include "esp_sleep.h"
|
||||
#include "i2c.h" // for lcd_off()
|
||||
#include "driver/gpio.h"
|
||||
#include "rtc_wdt.h"
|
||||
#include "esp_sleep.h"
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "i2c.h"
|
||||
#include "rtc_wdt.h"
|
||||
#include "filemgmt.h"
|
||||
|
||||
//#include "esp32/rtc_clk.h" // For RTC_SLOW_FREQ_32K_XTAL enum and rtc_clk_slow_freq_set()
|
||||
#include "driver/rtc_io.h" // For RTC I/O handling (optional but recommended for pin configuration)
|
||||
#include "storage.h"
|
||||
|
||||
#define PIN_BTN_INTERRUPT GPIO_NUM_13
|
||||
|
||||
#define POWER_INACTIVITY_TIMEOUT_MS 20000
|
||||
#define BATTERY_CHECK_INTERVAL_SEC 30
|
||||
uint64_t last_activity_tick = 0;
|
||||
|
||||
#define DEEP_SLEEP_US 30000000ULL /* 30 seconds in deep sleep */
|
||||
|
||||
// RTC_DATA_ATTR keeps this var in RTC memory; persists across sleeps (but not across boots)
|
||||
RTC_DATA_ATTR int64_t next_alarm_time_s = -1;
|
||||
RTC_DATA_ATTR bool rtc_set = false;
|
||||
bool rtc_is_set() {
|
||||
return rtc_set;
|
||||
}
|
||||
|
||||
esp_err_t start_rtc(void) {
|
||||
/* ---- Wake sources ---- */
|
||||
esp_sleep_enable_ext0_wakeup(PIN_BTN_INTERRUPT, 0);
|
||||
gpio_set_direction(PIN_BTN_INTERRUPT, GPIO_MODE_INPUT);
|
||||
gpio_set_pull_mode(PIN_BTN_INTERRUPT, GPIO_PULLUP_ONLY);
|
||||
esp_sleep_enable_ext0_wakeup(PIN_BTN_INTERRUPT, 0);
|
||||
|
||||
/* ---- Enable External 32 kHz Oscillator ---- */
|
||||
// Configure RTC I/O pins for crystal (hold in reset initially if needed)
|
||||
rtc_gpio_init(GPIO_NUM_32);
|
||||
rtc_gpio_init(GPIO_NUM_33);
|
||||
rtc_gpio_set_direction(GPIO_NUM_32, RTC_GPIO_MODE_DISABLED);
|
||||
rtc_gpio_set_direction(GPIO_NUM_33, RTC_GPIO_MODE_DISABLED);
|
||||
|
||||
// Select 32 kHz XTAL as slow clock source (wait for stabilization)
|
||||
//rtc_clk_slow_freq_set(RTC_SLOW_FREQ_32K_XTAL);
|
||||
// Optional: Brief delay for crystal stabilization (typically <1 ms)
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
|
||||
//ESP_LOGI("RTC", "Configured with external 32 kHz oscillator (freq: %d Hz)", rtc_clk_slow_freq_get_hz());
|
||||
|
||||
// Existing log can now be data-driven
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void reset_shutdown_timer(void)
|
||||
{
|
||||
last_activity_tick = xTaskGetTickCount();
|
||||
rtc_wdt_feed();
|
||||
}
|
||||
|
||||
void enter_deep_sleep(void)
|
||||
{
|
||||
//close_current_log();
|
||||
//fsm_request(FSM_CMD_STOP);
|
||||
i2c_set_relays(0);
|
||||
//esp_sleep_enable_timer_wakeup(DEEP_SLEEP_US);
|
||||
esp_deep_sleep_start();
|
||||
}
|
||||
|
||||
void rtc_set_time(struct tm *tm) {
|
||||
rtc_set = true;
|
||||
start_new_log_file();
|
||||
struct timeval tv = { .tv_sec = mktime(tm), .tv_usec = 0 };
|
||||
settimeofday(&tv, NULL);
|
||||
resetBatTimers();
|
||||
}
|
||||
|
||||
void rtc_get_time(struct tm *tm)
|
||||
{
|
||||
time_t raw;
|
||||
time(&raw);
|
||||
localtime_r(&raw, tm);
|
||||
}
|
||||
|
||||
int64_t system_rtc_get_raw_time(void)
|
||||
{
|
||||
struct timeval tv;
|
||||
gettimeofday(&tv, NULL);
|
||||
return (int64_t)tv.tv_sec;
|
||||
}
|
||||
|
||||
uint64_t rtc_time_ms(void)
|
||||
{
|
||||
struct timeval tv;
|
||||
gettimeofday(&tv, NULL);
|
||||
return (uint64_t)tv.tv_sec * 1000ULL + tv.tv_usec / 1000ULL;
|
||||
}
|
||||
|
||||
int64_t system_rtc_seconds_into_day(void)
|
||||
{
|
||||
return system_rtc_get_raw_time() % 86400UL;
|
||||
}
|
||||
|
||||
esp_sleep_wakeup_cause_t rtc_wakeup_cause(void)
|
||||
{
|
||||
esp_sleep_wakeup_cause_t c = esp_sleep_get_wakeup_cause();
|
||||
switch (c) {
|
||||
case ESP_SLEEP_WAKEUP_EXT0: ESP_LOGI("RTC", "Wakeup: GPIO"); break;
|
||||
case ESP_SLEEP_WAKEUP_TIMER: ESP_LOGI("RTC", "Wakeup: timer"); break;
|
||||
default: ESP_LOGI("RTC", "Wakeup: normal boot"); break;
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Unified periodic update */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
void check_shutdown_timer(void)
|
||||
{
|
||||
|
||||
TickType_t elapsed = xTaskGetTickCount() - last_activity_tick;
|
||||
if (elapsed * portTICK_PERIOD_MS >= POWER_INACTIVITY_TIMEOUT_MS)
|
||||
enter_deep_sleep();
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Time adjustment helpers */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
void adjust_rtc_hour(char *key, int8_t dir)
|
||||
{
|
||||
struct tm t;
|
||||
rtc_get_time(&t);
|
||||
if (dir>0) t.tm_hour ++;
|
||||
if (dir<0) t.tm_hour --;
|
||||
if (t.tm_hour > 23) t.tm_hour = 0;
|
||||
if (t.tm_hour < 0) t.tm_hour = 23;
|
||||
rtc_set_time(&t);
|
||||
set_next_alarm();
|
||||
}
|
||||
|
||||
void adjust_rtc_min(char *key, int8_t dir)
|
||||
{
|
||||
struct tm t;
|
||||
rtc_get_time(&t);
|
||||
if (dir>0) t.tm_min ++;
|
||||
if (dir<0) t.tm_min --;
|
||||
if (t.tm_min > 59) t.tm_min = 0;
|
||||
if (t.tm_min < 0) t.tm_min = 59;
|
||||
rtc_set_time(&t);
|
||||
set_next_alarm();
|
||||
}
|
||||
|
||||
|
||||
void set_next_alarm(void) {
|
||||
int8_t start_h = 0; //get_param_i8("sched_start");
|
||||
int8_t end_h = 23; //get_param_i8("sched_end");
|
||||
int8_t num = 0; //get_param_i8("sched_num");
|
||||
|
||||
if (num <= 0) {
|
||||
next_alarm_time_s = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Current time info
|
||||
uint32_t s_into_day = system_rtc_seconds_into_day();
|
||||
time_t current_time = system_rtc_get_raw_time();
|
||||
time_t today_midnight = current_time - s_into_day;
|
||||
|
||||
int start_sec = start_h * 3600;
|
||||
int end_sec = end_h * 3600;
|
||||
|
||||
bool overnight = (start_h > end_h);
|
||||
int total_duration = overnight ? (86400 - start_sec) + end_sec : end_sec - start_sec;
|
||||
|
||||
// Determine period start
|
||||
time_t period_start;
|
||||
if (overnight && s_into_day < end_sec) {
|
||||
// Current time is within overnight period → started yesterday
|
||||
period_start = (today_midnight - 86400) + start_sec;
|
||||
} else {
|
||||
// Normal or after end → starts today
|
||||
period_start = today_midnight + start_sec;
|
||||
}
|
||||
|
||||
//time_t period_end = period_start + total_duration;
|
||||
|
||||
if (num == 1) {
|
||||
// Single alarm: at period start, if passed, next day
|
||||
next_alarm_time_s = (current_time < period_start) ? period_start : period_start + 86400;
|
||||
ESP_LOGI("ALARM", "SET FOR %lld (in %lld s)", next_alarm_time_s, next_alarm_time_s - current_time);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find next alarm
|
||||
int64_t spacing = total_duration / (num - 1);
|
||||
time_t next_alarm = -1;
|
||||
|
||||
for (int8_t i = 0; i < num; i++) {
|
||||
time_t alarm_time = period_start + spacing * i;
|
||||
if (alarm_time > current_time) {
|
||||
next_alarm = alarm_time;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If all passed, first of next period
|
||||
if (next_alarm == -1) {
|
||||
next_alarm = period_start + 86400;
|
||||
}
|
||||
|
||||
next_alarm_time_s = next_alarm;
|
||||
|
||||
ESP_LOGI("ALARM", "SET FOR %lld (in %lld s)", next_alarm_time_s, next_alarm_time_s - current_time);
|
||||
}
|
||||
|
||||
bool alarm_tripped() {
|
||||
if (!rtc_is_set())
|
||||
return false;
|
||||
if (next_alarm_time_s < 0) {
|
||||
set_next_alarm();
|
||||
return false;
|
||||
}
|
||||
return system_rtc_get_raw_time() > next_alarm_time_s;
|
||||
}
|
||||
53
main/rtc.h
Normal file
53
main/rtc.h
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* system.h
|
||||
*
|
||||
* Public system services:
|
||||
* • Battery charge-state machine
|
||||
* • Inactivity / deep-sleep handling
|
||||
* • RTC time & alarm
|
||||
*
|
||||
* All implementation lives in system.c
|
||||
*/
|
||||
|
||||
#ifndef MAIN_RTC_H_
|
||||
#define MAIN_RTC_H_
|
||||
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_sleep.h"
|
||||
#include "esp_timer.h"
|
||||
#include "esp_err.h"
|
||||
|
||||
|
||||
#define TRANSITION_DELAY_US 1000000
|
||||
#define OVERRIDE_PULSE_RF 160000
|
||||
#define OVERRIDE_PULSE_UX 80000
|
||||
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Public API */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
esp_err_t start_rtc();
|
||||
|
||||
bool rtc_is_set();
|
||||
|
||||
void set_next_alarm(void);
|
||||
|
||||
void reset_shutdown_timer(); // reset shutoff timer
|
||||
void enter_deep_sleep();
|
||||
void check_shutdown_timer();
|
||||
esp_sleep_wakeup_cause_t rtc_wakeup_cause();
|
||||
|
||||
void adjust_rtc_hour(char *key, int8_t dir);
|
||||
void adjust_rtc_min(char *key, int8_t dir);
|
||||
|
||||
void rtc_get_time(struct tm * timeinfo);
|
||||
|
||||
int64_t system_rtc_get_raw_time(void);
|
||||
|
||||
bool alarm_tripped();
|
||||
|
||||
uint64_t rtc_time_ms();
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
#endif /* MAIN_RTC_H_ */
|
||||
166
main/sensors.c
Normal file
166
main/sensors.c
Normal file
@@ -0,0 +1,166 @@
|
||||
#include "sensors.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_task_wdt.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/queue.h"
|
||||
|
||||
static const char* TAG = "SENS";
|
||||
|
||||
uint8_t sensor_pins[N_SENSORS] = {GPIO_NUM_19, GPIO_NUM_25};
|
||||
|
||||
volatile int32_t sensor_count[N_SENSORS] = {0};
|
||||
static volatile uint64_t sensor_last_isr_time[N_SENSORS] = {0};
|
||||
static volatile bool sensor_stable_state[N_SENSORS] = {false};
|
||||
static QueueHandle_t sensor_event_queue = NULL;
|
||||
|
||||
#define DEBOUNCE_TIME_US 2000 // 2 ms debounce (adjust per switch)
|
||||
#define DEBOUNCE_TICKS pdMS_TO_TICKS(DEBOUNCE_TIME_MS)
|
||||
|
||||
typedef struct {
|
||||
uint8_t sensor_id;
|
||||
bool level;
|
||||
} sensor_event_t;
|
||||
|
||||
// ISR: Minimal work — just record timestamp and forward to queue
|
||||
static void IRAM_ATTR sensor_isr_handler(void* arg) {
|
||||
uint32_t gpio_num = (uint32_t)arg;
|
||||
uint8_t i;
|
||||
for (i = 0; i < N_SENSORS; i++) {
|
||||
if (sensor_pins[i] == gpio_num) break;
|
||||
}
|
||||
if (i == N_SENSORS) return;
|
||||
|
||||
uint64_t now = esp_timer_get_time();
|
||||
sensor_last_isr_time[i] = now;
|
||||
|
||||
sensor_event_t evt = {.sensor_id = i, .level = !gpio_get_level(gpio_num)};
|
||||
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
|
||||
xQueueSendFromISR(sensor_event_queue, &evt, &xHigherPriorityTaskWoken);
|
||||
if (xHigherPriorityTaskWoken) portYIELD_FROM_ISR();
|
||||
}
|
||||
|
||||
// Debounce task: Processes queue, updates state & count
|
||||
static void sensor_debounce_task(void* param) {
|
||||
sensor_event_t evt;
|
||||
static uint64_t last_processed_time[N_SENSORS] = {0};
|
||||
static bool last_raw_state[N_SENSORS] = {false};
|
||||
|
||||
// Initialize stable state
|
||||
for (uint8_t i = 0; i < N_SENSORS; i++) {
|
||||
bool level = !gpio_get_level(sensor_pins[i]);
|
||||
sensor_stable_state[i] = level;
|
||||
last_raw_state[i] = level;
|
||||
last_processed_time[i] = esp_timer_get_time();
|
||||
}
|
||||
|
||||
esp_task_wdt_add(NULL);
|
||||
|
||||
|
||||
uint8_t i = 0;
|
||||
int64_t now = -1;
|
||||
|
||||
while (1) {
|
||||
if (xQueueReceive(sensor_event_queue, &evt, pdMS_TO_TICKS(100)) == pdTRUE) {
|
||||
i = evt.sensor_id;
|
||||
|
||||
ESP_LOGI("SENS", "EVENT %d", i);
|
||||
|
||||
bool current_raw = !gpio_get_level(sensor_pins[i]);
|
||||
|
||||
|
||||
sensor_stable_state[i] = current_raw;
|
||||
|
||||
if (current_raw && !last_raw_state[i]){
|
||||
ESP_LOGI("SENS", "FALLING");
|
||||
sensor_count[i]++;
|
||||
}
|
||||
if (!current_raw && last_raw_state[i]){
|
||||
ESP_LOGI("SENS", "RISING");
|
||||
sensor_count[i]++;
|
||||
}
|
||||
|
||||
last_raw_state[i] = current_raw;
|
||||
}
|
||||
|
||||
|
||||
now = esp_timer_get_time();
|
||||
|
||||
/*// Wait for debounce period since last ISR
|
||||
if (now - sensor_last_isr_time[i] >= (DEBOUNCE_TIME_US)) {
|
||||
bool current_raw = !gpio_get_level(sensor_pins[i]);
|
||||
|
||||
// Only update if stable and different from last stable
|
||||
if (current_raw != sensor_stable_state[i]) {
|
||||
bool was_high = sensor_stable_state[i];
|
||||
|
||||
// Count rising OR falling edges
|
||||
if (current_raw && !sensor_stable_state[i]){
|
||||
ESP_LOGI("SENS", "FALLING");
|
||||
sensor_count[i]++;
|
||||
}
|
||||
if (!current_raw && sensor_stable_state[i]){
|
||||
ESP_LOGI("SENS", "RISING");
|
||||
sensor_count[i]++;
|
||||
}
|
||||
|
||||
|
||||
sensor_stable_state[i] = current_raw;
|
||||
|
||||
last_raw_state[i] = current_raw;
|
||||
last_processed_time[i] = now;
|
||||
}
|
||||
}*/
|
||||
|
||||
esp_task_wdt_reset();
|
||||
}
|
||||
}
|
||||
|
||||
void start_sensors() {
|
||||
gpio_config_t io_conf = {
|
||||
.pin_bit_mask = (1ULL << sensor_pins[0]) | (1ULL << sensor_pins[1]),
|
||||
.mode = GPIO_MODE_INPUT,
|
||||
.pull_up_en = GPIO_PULLUP_ENABLE,
|
||||
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||
.intr_type = GPIO_INTR_ANYEDGE,
|
||||
};
|
||||
ESP_ERROR_CHECK(gpio_config(&io_conf));
|
||||
|
||||
sensor_event_queue = xQueueCreate(16, sizeof(sensor_event_t));
|
||||
if (!sensor_event_queue) {
|
||||
ESP_LOGE(TAG, "Failed to create sensor queue");
|
||||
return;
|
||||
}
|
||||
|
||||
// Install ISR service
|
||||
ESP_ERROR_CHECK(gpio_install_isr_service(0));
|
||||
|
||||
for (uint8_t i = 0; i < N_SENSORS; i++) {
|
||||
ESP_ERROR_CHECK(gpio_isr_handler_add(sensor_pins[i], sensor_isr_handler, (void*)sensor_pins[i]));
|
||||
sensor_stable_state[i] = !gpio_get_level(sensor_pins[i]);
|
||||
}
|
||||
|
||||
xTaskCreate(sensor_debounce_task, "SENS_DEBOUNCE", 3072, NULL, 6, NULL);
|
||||
}
|
||||
|
||||
void shutdown_sensors() {
|
||||
for (uint8_t i = 0; i < N_SENSORS; i++) {
|
||||
gpio_isr_handler_remove(sensor_pins[i]);
|
||||
}
|
||||
gpio_uninstall_isr_service();
|
||||
vQueueDelete(sensor_event_queue);
|
||||
}
|
||||
|
||||
// Public API
|
||||
bool get_sensor(sensor_t i) {
|
||||
return sensor_stable_state[i];
|
||||
}
|
||||
|
||||
int32_t get_sensor_counter(sensor_t i) {
|
||||
return sensor_count[i];
|
||||
}
|
||||
|
||||
void set_sensor_counter(sensor_t i, int32_t to) {
|
||||
sensor_count[i] = to;
|
||||
}
|
||||
33
main/sensors.h
Normal file
33
main/sensors.h
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* sensors.h
|
||||
*
|
||||
* Created on: Nov 10, 2025
|
||||
* Author: Thad
|
||||
*/
|
||||
|
||||
#ifndef MAIN_SENSORS_H_
|
||||
#define MAIN_SENSORS_H_
|
||||
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_timer.h"
|
||||
#include "freertos/FreeRTOS.h" // Must be FIRST
|
||||
|
||||
#define SENSOR_SAMPLE_PERIOD_US 100 // 10 kHz → captures 120 Hz easily
|
||||
#define SENSOR_DEBOUNCE_US 500 // Reduced to 0.5 ms for responsiveness
|
||||
|
||||
typedef enum {
|
||||
SENSOR_DRIVE = 0,
|
||||
SENSOR_JACK = 1,
|
||||
N_SENSORS = 2
|
||||
} sensor_t;
|
||||
|
||||
void reset_sensor_counter(sensor_t i);
|
||||
void set_sensor_counter(sensor_t i, int32_t to);
|
||||
int32_t get_sensor_counter(sensor_t i);
|
||||
|
||||
bool get_sensor(sensor_t i);
|
||||
|
||||
void start_sensors();
|
||||
void shutdown_sensors();
|
||||
|
||||
#endif /* MAIN_SENSORS_H_ */
|
||||
319
main/storage.c
Normal file
319
main/storage.c
Normal file
@@ -0,0 +1,319 @@
|
||||
#include <string.h>
|
||||
#include "esp_partition.h"
|
||||
#include "esp_err.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_crc.h"
|
||||
#include "storage.h"
|
||||
|
||||
#define TAG "PARAMS"
|
||||
|
||||
// ============================================================================
|
||||
// PARAMETER TABLE GENERATION
|
||||
// ============================================================================
|
||||
|
||||
// Helper macros to construct initializers
|
||||
#define PARAM_VALUE_INIT(type, val) {.type = val}
|
||||
#define PARAM_TYPE_ENUM(type) PARAM_TYPE_##type
|
||||
#define PARAM_NAME_STR(name) #name
|
||||
|
||||
// Generate parameter table with live values (initialized to defaults)
|
||||
#define PARAM_DEF(name, type, default_val) PARAM_VALUE_INIT(type, default_val),
|
||||
param_value_t parameter_table[NUM_PARAMS] = {
|
||||
PARAM_LIST
|
||||
};
|
||||
#undef PARAM_DEF
|
||||
|
||||
// Generate default values array
|
||||
#define PARAM_DEF(name, type, default_val) PARAM_VALUE_INIT(type, default_val),
|
||||
const param_value_t parameter_defaults[NUM_PARAMS] = {
|
||||
PARAM_LIST
|
||||
};
|
||||
#undef PARAM_DEF
|
||||
|
||||
// Generate parameter types array
|
||||
#define PARAM_DEF(name, type, default_val) PARAM_TYPE_ENUM(type),
|
||||
const param_type_e parameter_types[NUM_PARAMS] = {
|
||||
PARAM_LIST
|
||||
};
|
||||
#undef PARAM_DEF
|
||||
|
||||
// Generate parameter names array
|
||||
#define PARAM_DEF(name, type, default_val) PARAM_NAME_STR(name),
|
||||
const char* parameter_names[NUM_PARAMS] = {
|
||||
PARAM_LIST
|
||||
};
|
||||
#undef PARAM_DEF
|
||||
|
||||
|
||||
// Partition pointer
|
||||
static const esp_partition_t *storage_partition = NULL;
|
||||
|
||||
// Log head tracking
|
||||
static uint32_t log_head_index = 0;
|
||||
static bool log_initialized = false;
|
||||
|
||||
// Calculate offset for log area (after parameters sector)
|
||||
#define LOG_START_OFFSET FLASH_SECTOR_SIZE
|
||||
|
||||
// ============================================================================
|
||||
// PARAMETER FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
param_value_t get_param(param_idx_t id) {
|
||||
if (id >= NUM_PARAMS) {
|
||||
ESP_LOGE(TAG, "Invalid parameter ID: %d", id);
|
||||
param_value_t err = {0};
|
||||
return err;
|
||||
}
|
||||
return parameter_table[id];
|
||||
}
|
||||
|
||||
esp_err_t set_param(param_idx_t id, param_value_t val) {
|
||||
if (id >= NUM_PARAMS) {
|
||||
ESP_LOGE(TAG, "Invalid parameter ID: %d", id);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
parameter_table[id] = val;
|
||||
ESP_LOGI(TAG, "Parameter %d (%s) set (not committed)", id, parameter_names[id]);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
param_type_e get_param_type(param_idx_t id) {
|
||||
if (id >= NUM_PARAMS) {
|
||||
return PARAM_TYPE_u64; // Default fallback
|
||||
}
|
||||
return parameter_types[id];
|
||||
}
|
||||
|
||||
const char* get_param_name(param_idx_t id) {
|
||||
if (id >= NUM_PARAMS) {
|
||||
return "INVALID";
|
||||
}
|
||||
return parameter_names[id];
|
||||
}
|
||||
|
||||
param_value_t get_param_default(param_idx_t id) {
|
||||
if (id >= NUM_PARAMS) {
|
||||
param_value_t err = {0};
|
||||
return err;
|
||||
}
|
||||
return parameter_defaults[id];
|
||||
}
|
||||
|
||||
esp_err_t commit_params() {
|
||||
if (storage_partition == NULL) {
|
||||
ESP_LOGE(TAG, "Storage partition not initialized");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
// Prepare storage buffer with parameters and CRCs
|
||||
param_stored_t params_to_store[NUM_PARAMS];
|
||||
|
||||
for (int i = 0; i < NUM_PARAMS; i++) {
|
||||
params_to_store[i].val = parameter_table[i];
|
||||
// Calculate CRC32 for each parameter value
|
||||
params_to_store[i].crc = esp_crc32_le(0, (uint8_t*)¶meter_table[i], sizeof(param_value_t));
|
||||
}
|
||||
|
||||
// Erase the first sector (4096 bytes)
|
||||
esp_err_t err = esp_partition_erase_range(storage_partition, PARAMS_OFFSET, FLASH_SECTOR_SIZE);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to erase parameter sector: %s", esp_err_to_name(err));
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
// Write parameters to flash
|
||||
err = esp_partition_write(storage_partition, PARAMS_OFFSET, params_to_store, PARAMS_TOTAL_SIZE);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to write parameters: %s", esp_err_to_name(err));
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Parameters committed to flash successfully");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// INITIALIZATION FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
esp_err_t storage_init(void) {
|
||||
// Find the partition labeled "storage"
|
||||
storage_partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA,
|
||||
ESP_PARTITION_SUBTYPE_ANY,
|
||||
"storage");
|
||||
|
||||
if (storage_partition == NULL) {
|
||||
ESP_LOGE(TAG, "Storage partition not found");
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Storage partition found: size=%lu bytes", (unsigned long)storage_partition->size);
|
||||
|
||||
// Load parameters from flash
|
||||
param_stored_t params_stored[NUM_PARAMS];
|
||||
esp_err_t err = esp_partition_read(storage_partition, PARAMS_OFFSET,
|
||||
params_stored, PARAMS_TOTAL_SIZE);
|
||||
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Failed to read parameters, using defaults");
|
||||
return err;
|
||||
}
|
||||
|
||||
// Validate and load each parameter
|
||||
bool all_valid = true;
|
||||
for (int i = 0; i < NUM_PARAMS; i++) {
|
||||
uint32_t calculated_crc = esp_crc32_le(0, (uint8_t*)¶ms_stored[i].val,
|
||||
sizeof(param_value_t));
|
||||
|
||||
if (calculated_crc == params_stored[i].crc) {
|
||||
parameter_table[i] = params_stored[i].val;
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Parameter %d (%s) failed CRC check, using default",
|
||||
i, parameter_names[i]);
|
||||
all_valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (all_valid) {
|
||||
ESP_LOGI(TAG, "All parameters loaded successfully from flash");
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Some parameters failed validation, using defaults");
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// LOGGING FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
static esp_err_t find_log_head(void) {
|
||||
if (storage_partition == NULL) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
// Calculate total log area size
|
||||
uint32_t log_area_size = storage_partition->size - LOG_START_OFFSET;
|
||||
uint32_t max_entries = log_area_size / LOG_ENTRY_SIZE;
|
||||
|
||||
// Read through entries to find first uninitialized (all 0xFF)
|
||||
uint8_t entry[LOG_ENTRY_SIZE];
|
||||
uint8_t empty_entry[LOG_ENTRY_SIZE];
|
||||
memset(empty_entry, 0xFF, LOG_ENTRY_SIZE);
|
||||
|
||||
// Binary search would be faster, but linear is safer for circular buffer
|
||||
for (uint32_t i = 0; i < max_entries; i++) {
|
||||
uint32_t offset = LOG_START_OFFSET + (i * LOG_ENTRY_SIZE);
|
||||
|
||||
esp_err_t err = esp_partition_read(storage_partition, offset, entry, LOG_ENTRY_SIZE);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to read log entry at index %lu", (unsigned long)i);
|
||||
return err;
|
||||
}
|
||||
|
||||
// Check if this entry is uninitialized
|
||||
if (memcmp(entry, empty_entry, LOG_ENTRY_SIZE) == 0) {
|
||||
log_head_index = i;
|
||||
ESP_LOGI(TAG, "Log head found at index %lu", (unsigned long)log_head_index);
|
||||
return ESP_OK;
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, all entries are full - wrap to beginning
|
||||
log_head_index = 0;
|
||||
ESP_LOGI(TAG, "Log is full, wrapping to beginning");
|
||||
|
||||
// Erase the first log sector to start fresh
|
||||
esp_err_t err = esp_partition_erase_range(storage_partition, LOG_START_OFFSET,
|
||||
FLASH_SECTOR_SIZE);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to erase first log sector");
|
||||
return err;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t log_init(void) {
|
||||
if (storage_partition == NULL) {
|
||||
ESP_LOGE(TAG, "Storage partition not initialized, call storage_init() first");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
esp_err_t err = find_log_head();
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
|
||||
log_initialized = true;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t write_log(char* entry) {
|
||||
if (!log_initialized || storage_partition == NULL) {
|
||||
ESP_LOGE(TAG, "Logging not initialized");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
uint32_t log_area_end = storage_partition->size;
|
||||
uint32_t max_entries = (log_area_end - LOG_START_OFFSET) / LOG_ENTRY_SIZE;
|
||||
//uint32_t max_sectors = max_entries / FLASH_SECTOR_SIZE;
|
||||
|
||||
// Calculate current offset
|
||||
uint32_t current_offset = LOG_START_OFFSET + (log_head_index * LOG_ENTRY_SIZE);
|
||||
|
||||
// Check if we need to erase the next sector
|
||||
uint32_t current_sector = current_offset / FLASH_SECTOR_SIZE;
|
||||
uint32_t next_offset = current_offset + LOG_ENTRY_SIZE;
|
||||
if (next_offset >= log_area_end)
|
||||
next_offset = LOG_START_OFFSET;
|
||||
uint32_t next_sector = next_offset / FLASH_SECTOR_SIZE;
|
||||
|
||||
// If we're crossing into a new sector, check if it needs erasing
|
||||
if (next_sector != current_sector) {
|
||||
// Check if next sector is uninitialized
|
||||
uint8_t check_byte;
|
||||
esp_err_t err = esp_partition_read(storage_partition, next_sector * FLASH_SECTOR_SIZE,
|
||||
&check_byte, 1);
|
||||
|
||||
if (err == ESP_OK && check_byte != 0xFF) {
|
||||
// Sector needs erasing
|
||||
ESP_LOGI(TAG, "Erasing sector %lu for log", (unsigned long)next_sector);
|
||||
err = esp_partition_erase_range(storage_partition,
|
||||
next_sector * FLASH_SECTOR_SIZE,
|
||||
FLASH_SECTOR_SIZE);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to erase sector: %s", esp_err_to_name(err));
|
||||
return ESP_FAIL;
|
||||
}
|
||||
} else if (err == ESP_OK) {
|
||||
ESP_LOGI(TAG, "Next sector %ld clear, no erasing needed", (unsigned long)next_sector);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Error checking byte (sector %ld)", (unsigned long)next_sector);
|
||||
}
|
||||
}
|
||||
|
||||
// Write the log entry
|
||||
esp_err_t err = esp_partition_write(storage_partition, current_offset, entry, LOG_ENTRY_SIZE);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to write log entry: %s", esp_err_to_name(err));
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Log @ sector %lu / index %lu / offset %lu", (unsigned long) current_sector, (unsigned long) log_head_index, (unsigned long)current_offset);
|
||||
|
||||
|
||||
log_head_index++;
|
||||
if (log_head_index >= max_entries) {
|
||||
log_head_index = 0;
|
||||
ESP_LOGI(TAG, "Log wrapped to beginning");
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void storage_deinit(void) {
|
||||
storage_partition = NULL;
|
||||
log_initialized = false;
|
||||
}
|
||||
127
main/storage.h
Normal file
127
main/storage.h
Normal file
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* storage.h
|
||||
*
|
||||
* Created on: Nov 5, 2025
|
||||
* Author: Thad
|
||||
*/
|
||||
|
||||
#ifndef MAIN_STORAGE_H_
|
||||
#define MAIN_STORAGE_H_
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <time.h>
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <stdarg.h>
|
||||
#include "i2c.h"
|
||||
|
||||
typedef union {
|
||||
uint8_t u8;
|
||||
int8_t i8;
|
||||
uint16_t u16;
|
||||
int16_t i16;
|
||||
uint32_t u32;
|
||||
int32_t i32;
|
||||
uint64_t u64;
|
||||
int64_t i64;
|
||||
float f32;
|
||||
double f64;
|
||||
} param_value_t;
|
||||
|
||||
typedef struct {
|
||||
param_value_t val;
|
||||
uint32_t crc;
|
||||
} param_stored_t;
|
||||
|
||||
typedef enum {
|
||||
PARAM_TYPE_u8,
|
||||
PARAM_TYPE_i8,
|
||||
PARAM_TYPE_u16,
|
||||
PARAM_TYPE_i16,
|
||||
PARAM_TYPE_u32,
|
||||
PARAM_TYPE_i32,
|
||||
PARAM_TYPE_u64,
|
||||
PARAM_TYPE_i64,
|
||||
PARAM_TYPE_f32,
|
||||
PARAM_TYPE_f64
|
||||
} param_type_e;
|
||||
|
||||
// ============================================================================
|
||||
// PARAMETER DEFINITION MACRO
|
||||
// ============================================================================
|
||||
// Usage: PARAM_DEF(NAME, TYPE, DEFAULT_VALUE)
|
||||
//
|
||||
// Examples:
|
||||
// PARAM_DEF(NUM_MOVES, u32, 0)
|
||||
// PARAM_DEF(EFUSE_1_AS, u16, 2400)
|
||||
// PARAM_DEF(JACK_DIST, u8, 5)
|
||||
// PARAM_DEF(KEYCODE_0, i64, -1)
|
||||
// PARAM_DEF(TEMPERATURE, f32, 25.5)
|
||||
// ============================================================================
|
||||
// REMEMBER: ORDER IS IMPERATIVE! PARAMETERS ARE ENTERED IN THE TABLE BY INDEX!
|
||||
// ============================================================================
|
||||
|
||||
#define PARAM_LIST \
|
||||
PARAM_DEF(NUM_MOVES, u32, 0) \
|
||||
PARAM_DEF(MOVE_START, u32, 0) \
|
||||
PARAM_DEF(MOVE_END, u32, 0) \
|
||||
PARAM_DEF(EFUSE_1_A, u32, 40) \
|
||||
PARAM_DEF(EFUSE_2_A, u32, 10) \
|
||||
PARAM_DEF(EFUSE_3_A, u32, 10) \
|
||||
PARAM_DEF(EFUSE_4_A, u32, 10) \
|
||||
PARAM_DEF(EFUSE_1_AS, u16, 2400) \
|
||||
PARAM_DEF(EFUSE_2_AS, u16, 2400) \
|
||||
PARAM_DEF(EFUSE_3_AS, u16, 600) \
|
||||
PARAM_DEF(EFUSE_4_AS, u16, 600) \
|
||||
PARAM_DEF(DRIVE_DIST, u16, 10) \
|
||||
PARAM_DEF(JACK_DIST, u8, 5) \
|
||||
PARAM_DEF(DRIVE_TPDF, u16, 4000) \
|
||||
PARAM_DEF(DRIVE_MSPF, u16, 600) \
|
||||
PARAM_DEF(JACK_MSPI, u16, 600) \
|
||||
PARAM_DEF(KEYCODE_0, i64, -1) \
|
||||
PARAM_DEF(KEYCODE_1, i64, -1) \
|
||||
PARAM_DEF(KEYCODE_2, i64, -1) \
|
||||
PARAM_DEF(KEYCODE_3, i64, -1)
|
||||
|
||||
// Generate enum for parameter indices
|
||||
#define PARAM_DEF(name, type, default_val) PARAM_##name,
|
||||
typedef enum {
|
||||
PARAM_LIST
|
||||
NUM_PARAMS
|
||||
} param_idx_t;
|
||||
#undef PARAM_DEF
|
||||
|
||||
#define PARAMS_SIZE sizeof(param_stored_t)
|
||||
#define PARAMS_OFFSET 0
|
||||
#define PARAMS_TOTAL_SIZE (NUM_PARAMS * PARAMS_SIZE)
|
||||
|
||||
// External declarations
|
||||
extern param_value_t parameter_table[NUM_PARAMS];
|
||||
extern const param_value_t parameter_defaults[NUM_PARAMS];
|
||||
extern const param_type_e parameter_types[NUM_PARAMS];
|
||||
extern const char* parameter_names[NUM_PARAMS];
|
||||
|
||||
esp_err_t storage_init();
|
||||
esp_err_t log_init();
|
||||
|
||||
param_value_t get_param(param_idx_t id);
|
||||
esp_err_t set_param(param_idx_t id, param_value_t val);
|
||||
param_type_e get_param_type(param_idx_t id);
|
||||
const char* get_param_name(param_idx_t id);
|
||||
param_value_t get_param_default(param_idx_t id);
|
||||
|
||||
esp_err_t commit_params();
|
||||
|
||||
|
||||
|
||||
#define LOG_ENTRY_SIZE 32
|
||||
#define LOG_NUM_ENTRIES 512
|
||||
#define FLASH_SECTOR_SIZE 4096
|
||||
|
||||
esp_err_t write_log(char* entry);
|
||||
|
||||
void storage_deinit();
|
||||
|
||||
#endif /* MAIN_STORAGE_H_ */
|
||||
418
main/uart_comms.c
Normal file
418
main/uart_comms.c
Normal file
@@ -0,0 +1,418 @@
|
||||
#include "uart_comms.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "driver/uart.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_system.h"
|
||||
#include "storage.h"
|
||||
|
||||
#define TAG "UART"
|
||||
|
||||
#define UART_NUM UART_NUM_0
|
||||
#define BUF_SIZE (1024)
|
||||
#define CMD_MAX_LEN (256)
|
||||
|
||||
static char cmd_buffer[CMD_MAX_LEN];
|
||||
static int cmd_pos = 0;
|
||||
static TaskHandle_t uart_task_handle = NULL;
|
||||
|
||||
// Parse value as either decimal or hex (0x prefix)
|
||||
static bool parse_uint64(const char *str, uint64_t *result) {
|
||||
char *endptr;
|
||||
|
||||
if (str == NULL || result == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for hex prefix
|
||||
if (strlen(str) > 2 && str[0] == '0' && (str[1] == 'x' || str[1] == 'X')) {
|
||||
*result = strtoull(str, &endptr, 16);
|
||||
} else {
|
||||
*result = strtoull(str, &endptr, 10);
|
||||
}
|
||||
|
||||
// Check if conversion was successful (endptr should point to null terminator)
|
||||
return (*endptr == '\0' && endptr != str);
|
||||
}
|
||||
|
||||
// Format parameter value for display based on its type
|
||||
static void print_param_value(param_idx_t id, param_value_t val) {
|
||||
param_type_e type = get_param_type(id);
|
||||
|
||||
switch (type) {
|
||||
case PARAM_TYPE_u8:
|
||||
printf("%u (0x%02X)\n", val.u8, val.u8);
|
||||
break;
|
||||
|
||||
case PARAM_TYPE_i8:
|
||||
printf("%d (0x%02X)\n",
|
||||
val.i8, (uint8_t)val.i8);
|
||||
break;
|
||||
|
||||
case PARAM_TYPE_u16:
|
||||
printf("%u (0x%04X)\n",
|
||||
val.u16, val.u16);
|
||||
break;
|
||||
|
||||
case PARAM_TYPE_i16:
|
||||
printf("%d (0x%04X)\n",
|
||||
val.i16, (uint16_t)val.i16);
|
||||
break;
|
||||
|
||||
case PARAM_TYPE_u32:
|
||||
printf("%lu (0x%08lX)\n",
|
||||
(unsigned long)val.u32, (unsigned long)val.u32);
|
||||
break;
|
||||
|
||||
case PARAM_TYPE_i32:
|
||||
printf("%ld (0x%08lX)\n",
|
||||
(long)val.i32, (unsigned long)val.i32);
|
||||
break;
|
||||
|
||||
case PARAM_TYPE_u64:
|
||||
printf("%llu (0x%016llX)\n",
|
||||
(unsigned long long)val.u64,
|
||||
(unsigned long long)val.u64);
|
||||
break;
|
||||
|
||||
case PARAM_TYPE_i64:
|
||||
printf("%lld (0x%016llX)\n",
|
||||
(long long)val.i64, (unsigned long long)val.u64);
|
||||
break;
|
||||
|
||||
case PARAM_TYPE_f32:
|
||||
printf("%.6f (0x%08lX as bits)\n",
|
||||
val.f32, (unsigned long)val.u32);
|
||||
break;
|
||||
|
||||
case PARAM_TYPE_f64:
|
||||
printf("%.6f (0x%016llX as bits)\n",
|
||||
val.f64, (unsigned long long)val.u64);
|
||||
break;
|
||||
|
||||
default:
|
||||
printf("UNKNOWN TYPE\n");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Process set parameter command: sp <id> <value>
|
||||
static void cmd_set_param(char *args) {
|
||||
char *id_str = strtok(args, " \t");
|
||||
char *val_str = strtok(NULL, " \t");
|
||||
|
||||
if (id_str == NULL || val_str == NULL) {
|
||||
printf("ERROR: Usage: sp <id> <value>\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse parameter ID
|
||||
uint64_t id_u64;
|
||||
if (!parse_uint64(id_str, &id_u64)) {
|
||||
printf("ERROR: Invalid parameter ID\n");
|
||||
return;
|
||||
}
|
||||
|
||||
param_idx_t id = (param_idx_t)id_u64;
|
||||
if (id >= NUM_PARAMS) {
|
||||
printf("ERROR: Parameter ID %u out of range (max %d)\n",
|
||||
id, NUM_PARAMS - 1);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse value
|
||||
uint64_t value;
|
||||
if (!parse_uint64(val_str, &value)) {
|
||||
printf("ERROR: Invalid value\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// Set parameter (store as u64, will be cast appropriately when used)
|
||||
param_value_t param_val;
|
||||
param_val.u64 = value;
|
||||
|
||||
esp_err_t err = set_param(id, param_val);
|
||||
if (err == ESP_OK) {
|
||||
printf("OK: Parameter %u (%s) set to ",
|
||||
id, get_param_name(id));
|
||||
print_param_value(id, param_val);
|
||||
printf("(Not committed - use 'cp' to save)\n");
|
||||
} else {
|
||||
printf("ERROR: Failed to set parameter\n");
|
||||
}
|
||||
}
|
||||
|
||||
// Process get parameter command: gp <id>
|
||||
static void cmd_get_param(char *args) {
|
||||
char *id_str = strtok(args, " \t");
|
||||
|
||||
if (id_str == NULL) {
|
||||
printf("ERROR: Usage: gp <id>\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse parameter ID
|
||||
uint64_t id_u64;
|
||||
if (!parse_uint64(id_str, &id_u64)) {
|
||||
printf("ERROR: Invalid parameter ID\n");
|
||||
return;
|
||||
}
|
||||
|
||||
param_idx_t id = (param_idx_t)id_u64;
|
||||
if (id >= NUM_PARAMS) {
|
||||
printf("ERROR: Parameter ID %u out of range (max %d)\n",
|
||||
id, NUM_PARAMS - 1);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get parameter
|
||||
param_value_t val = get_param(id);
|
||||
printf("Parameter %u (%s) = ", id, get_param_name(id));
|
||||
print_param_value(id, val);
|
||||
}
|
||||
|
||||
// Process commit parameters command: cp
|
||||
static void cmd_commit_params(char *args) {
|
||||
(void)args; // Unused
|
||||
|
||||
printf("Committing parameters to flash...\n");
|
||||
commit_params();
|
||||
printf("OK: Parameters committed\n");
|
||||
}
|
||||
|
||||
// Process reset parameter command: rp <id>
|
||||
static void cmd_reset_param(char *args) {
|
||||
char *id_str = strtok(args, " \t");
|
||||
|
||||
if (id_str == NULL) {
|
||||
printf("ERROR: Usage: rp <id>\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse parameter ID
|
||||
uint64_t id_u64;
|
||||
if (!parse_uint64(id_str, &id_u64)) {
|
||||
printf("ERROR: Invalid parameter ID\n");
|
||||
return;
|
||||
}
|
||||
|
||||
param_idx_t id = (param_idx_t)id_u64;
|
||||
if (id >= NUM_PARAMS) {
|
||||
printf("ERROR: Parameter ID %u out of range (max %d)\n",
|
||||
id, NUM_PARAMS - 1);
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset to default
|
||||
param_value_t default_val = get_param_default(id);
|
||||
esp_err_t err = set_param(id, default_val);
|
||||
|
||||
if (err == ESP_OK) {
|
||||
printf("OK: Parameter %u (%s) reset to default: ",
|
||||
id, get_param_name(id));
|
||||
print_param_value(id, default_val);
|
||||
printf("(Not committed - use 'cp' to save)\n");
|
||||
} else {
|
||||
printf("ERROR: Failed to reset parameter\n");
|
||||
}
|
||||
}
|
||||
|
||||
// Process list parameters command: lp
|
||||
static void cmd_list_params(char *args) {
|
||||
(void)args; // Unused
|
||||
|
||||
printf("\n=== Parameter List ===\n");
|
||||
printf("ID | Name | Type | Value\n");
|
||||
printf("----+-------------------+------+------------------\n");
|
||||
|
||||
const char* type_names[] = {
|
||||
"u8 ", "i8 ", "u16", "i16", "u32", "i32", "u64", "i64", "f32", "f64"
|
||||
};
|
||||
|
||||
for (int i = 0; i < NUM_PARAMS; i++) {
|
||||
param_value_t val = get_param(i);
|
||||
param_type_e type = get_param_type(i);
|
||||
|
||||
printf("%-3d | %-17s | %-4s | ", i, get_param_name(i), type_names[type]);
|
||||
print_param_value(i, val);
|
||||
}
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
// Process help command
|
||||
static void cmd_help(char *args) {
|
||||
(void)args; // Unused
|
||||
|
||||
printf("\n=== Available Commands ===\n");
|
||||
printf("sp <id> <value> - Set parameter (e.g., sp 0 42 or sp 14 0xDEADBEEF)\n");
|
||||
printf("gp <id> - Get parameter (e.g., gp 0)\n");
|
||||
printf("rp <id> - Reset parameter to default (e.g., rp 0)\n");
|
||||
printf("cp - Commit parameters to flash\n");
|
||||
printf("lp - List all parameters\n");
|
||||
printf("help - Show this help\n");
|
||||
printf("\nNotes:\n");
|
||||
printf("- Values can be decimal (123) or hex (0xABC)\n");
|
||||
printf("- Changes are not saved until you run 'cp'\n");
|
||||
printf("- Parameter IDs range from 0 to %d\n\n", NUM_PARAMS - 1);
|
||||
}
|
||||
|
||||
// Parse and execute command
|
||||
static void process_command(char *cmd) {
|
||||
// Trim leading whitespace
|
||||
while (*cmd == ' ' || *cmd == '\t') {
|
||||
cmd++;
|
||||
}
|
||||
|
||||
// Ignore empty commands
|
||||
if (*cmd == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract command
|
||||
char *space = strchr(cmd, ' ');
|
||||
char *tab = strchr(cmd, '\t');
|
||||
char *delim = NULL;
|
||||
|
||||
if (space && tab) {
|
||||
delim = (space < tab) ? space : tab;
|
||||
} else if (space) {
|
||||
delim = space;
|
||||
} else if (tab) {
|
||||
delim = tab;
|
||||
}
|
||||
|
||||
char command[16] = {0};
|
||||
if (delim) {
|
||||
int cmd_len = delim - cmd;
|
||||
if (cmd_len >= sizeof(command)) {
|
||||
cmd_len = sizeof(command) - 1;
|
||||
}
|
||||
strncpy(command, cmd, cmd_len);
|
||||
cmd = delim + 1; // Point to arguments
|
||||
} else {
|
||||
strncpy(command, cmd, sizeof(command) - 1);
|
||||
cmd = cmd + strlen(cmd); // Point to empty string
|
||||
}
|
||||
|
||||
// Execute command
|
||||
if (strcmp(command, "sp") == 0) {
|
||||
cmd_set_param(cmd);
|
||||
} else if (strcmp(command, "gp") == 0) {
|
||||
cmd_get_param(cmd);
|
||||
} else if (strcmp(command, "rp") == 0) {
|
||||
cmd_reset_param(cmd);
|
||||
} else if (strcmp(command, "cp") == 0) {
|
||||
cmd_commit_params(cmd);
|
||||
} else if (strcmp(command, "lp") == 0) {
|
||||
cmd_list_params(cmd);
|
||||
} else if (strcmp(command, "help") == 0) {
|
||||
cmd_help(cmd);
|
||||
} else {
|
||||
printf("ERROR: Unknown command '%s' (type 'help' for commands)\n", command);
|
||||
}
|
||||
}
|
||||
|
||||
// UART event task
|
||||
void uart_event_task(void *pvParameters) {
|
||||
uint8_t data[BUF_SIZE];
|
||||
|
||||
while (1) {
|
||||
int len = uart_read_bytes(UART_NUM, data, BUF_SIZE - 1, 20 / portTICK_PERIOD_MS);
|
||||
|
||||
if (len > 0) {
|
||||
for (int i = 0; i < len; i++) {
|
||||
char c = (char)data[i];
|
||||
|
||||
// Echo character
|
||||
printf("%c", c);
|
||||
fflush(stdout);
|
||||
|
||||
// Handle backspace
|
||||
if (c == '\b' || c == 127) {
|
||||
if (cmd_pos > 0) {
|
||||
cmd_pos--;
|
||||
printf(" \b"); // Clear character on screen
|
||||
fflush(stdout);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle newline/carriage return
|
||||
if (c == '\n' || c == '\r') {
|
||||
cmd_buffer[cmd_pos] = '\0';
|
||||
printf("\n");
|
||||
|
||||
if (cmd_pos > 0) {
|
||||
process_command(cmd_buffer);
|
||||
}
|
||||
|
||||
cmd_pos = 0;
|
||||
printf("> ");
|
||||
fflush(stdout);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add to buffer if not full
|
||||
if (cmd_pos < CMD_MAX_LEN - 1) {
|
||||
cmd_buffer[cmd_pos++] = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void uart_start() {
|
||||
// Configure UART
|
||||
uart_config_t uart_config = {
|
||||
.baud_rate = 115200,
|
||||
.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_ERROR_CHECK(uart_driver_install(UART_NUM, BUF_SIZE * 2, 0, 0, NULL, 0));
|
||||
ESP_ERROR_CHECK(uart_param_config(UART_NUM, &uart_config));
|
||||
|
||||
// Print startup message
|
||||
printf("\n\n");
|
||||
printf("=================================\n");
|
||||
printf(" ESP32 Parameter Manager\n");
|
||||
printf("=================================\n");
|
||||
printf("Type 'help' for available commands\n\n");
|
||||
printf("> ");
|
||||
fflush(stdout);
|
||||
|
||||
// Create UART task
|
||||
xTaskCreate(uart_event_task, "uart_event_task", 4096, NULL, 12, &uart_task_handle);
|
||||
|
||||
ESP_LOGI(TAG, "UART interface started");
|
||||
}
|
||||
|
||||
void uart_stop() {
|
||||
if (uart_task_handle == NULL) {
|
||||
ESP_LOGW(TAG, "UART task not running");
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Shutting down UART...");
|
||||
|
||||
// Wait for UART TX to finish
|
||||
uart_wait_tx_done(UART_NUM, pdMS_TO_TICKS(100));
|
||||
|
||||
// Delete the UART task
|
||||
vTaskDelete(uart_task_handle);
|
||||
uart_task_handle = NULL;
|
||||
|
||||
// Small delay to ensure task deletion completes
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
|
||||
// Disable UART driver
|
||||
uart_driver_delete(UART_NUM);
|
||||
|
||||
ESP_LOGI(TAG, "UART shutdown complete");
|
||||
}
|
||||
14
main/uart_comms.h
Normal file
14
main/uart_comms.h
Normal file
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* uart_comms.h
|
||||
*
|
||||
* Created on: Dec 13, 2025
|
||||
* Author: Thad
|
||||
*/
|
||||
|
||||
#ifndef MAIN_UART_COMMS_H_
|
||||
#define MAIN_UART_COMMS_H_
|
||||
|
||||
void uart_start();
|
||||
void uart_stop();
|
||||
|
||||
#endif /* MAIN_UART_COMMS_H_ */
|
||||
Reference in New Issue
Block a user