B001-V3 works with it now

This commit is contained in:
Thaddeus Hughes
2025-12-16 12:10:10 -06:00
parent ac030005c3
commit 062221dfd3
70 changed files with 3872 additions and 3786 deletions

View File

@@ -2,7 +2,7 @@
# 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
SRCS main.c i2c.c rtc.c storage.c uart_comms.c control_fsm.c power_mgmt.c rf.c rtc.c sensors.c solar.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)

View File

@@ -14,22 +14,27 @@
#include "storage.h"
#include "rtc.h"
#include "sensors.h"
#include "esp_log.h"
#define TRANSITION_DELAY_US 1000000
#define TAG "FSM"
static QueueHandle_t fsm_cmd_queue = NULL;
// map from relay number to bridge
bridge_t bridge_map[] = {
BRIDGE_AUX,
BRIDGE_AUX,
BRIDGE_AUX,
BRIDGE_AUX,
BRIDGE_JACK,
BRIDGE_JACK,
BRIDGE_DRIVE,
BRIDGE_DRIVE };
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 relay_states[8] = {false};
int64_t override_times[8] = {-1};
bool enabled = false;
void setRelay(int8_t relay, bool state) {
@@ -37,36 +42,24 @@ void setRelay(int8_t relay, bool 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]);
uint8_t state = 0x00;
for (uint8_t i=0; i<8; i++) {
// if we command and efuse permits it set the relay
if (relay_states[i] && !efuse_is_tripped(bridge_map[i])) {
state |= 0x01<<i;
set_autozero(bridge_map[i]);
}
}
i2c_set_relays(state);
}
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;
}
@@ -79,18 +72,16 @@ static inline void set_timer(uint64_t us) {
}
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 pulseOverride(relay_t relay) {
if (current_state == STATE_IDLE)
override_times[relay] = current_time + get_param(PARAM_RF_PULSE_LENGTH).u64;
}
void fsm_begin_auto_move() {
/*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)
{
@@ -122,9 +113,10 @@ int8_t fsm_get_current_progress(int8_t denominator) {
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
#define JACK_TIME get_param(PARAM_JACK_MSPI ).u32 * 1000 * get_param(PARAM_JACK_DIST ).u8
#define DRIVE_TIME get_param(PARAM_DRIVE_MSPF).u32 * 1000 * get_param(PARAM_DRIVE_DIST).u8
#define DRIVE_DIST get_param(PARAM_DRIVE_TPDF).u32 / 10 * get_param(PARAM_DRIVE_DIST).u8
void control_task(void *param) {
esp_task_wdt_add(NULL);
@@ -133,14 +125,6 @@ void control_task(void *param) {
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);
@@ -149,13 +133,15 @@ void control_task(void *param) {
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;
case FSM_CMD_START:
if (current_state == STATE_IDLE) {
current_state = STATE_MOVE_START_DELAY;
set_timer(TRANSITION_DELAY_US);
}
break;
case FSM_CMD_STOP:
current_state = STATE_IDLE;
break;
case FSM_CMD_UNDO:
if (current_state != STATE_IDLE &&
current_state != STATE_UNDO_JACK_START &&
@@ -245,12 +231,12 @@ void control_task(void *param) {
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);
*/
//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:
@@ -261,42 +247,69 @@ void control_task(void *param) {
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
//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);
// jack up and fluff
setRelay(RELAY_A1, false);
setRelay(RELAY_B1, false);
setRelay(RELAY_A2, true);
setRelay(RELAY_B2, false);
setRelay(RELAY_A3, true);
reset_shutdown_timer();
break;
case STATE_DRIVE:
setBridge(BRIDGE_DRIVE, +1);
setBridge(BRIDGE_JACK, 0);
setBridge(BRIDGE_AUX, +1);
// drive and fluff
setRelay(RELAY_A1, true);
setRelay(RELAY_B1, false);
setRelay(RELAY_A2, false);
setRelay(RELAY_B2, false);
setRelay(RELAY_A3, true);
reset_shutdown_timer();
break;
case STATE_UNDO_JACK:
case STATE_JACK_DOWN:
setBridge(BRIDGE_DRIVE, 0);
setBridge(BRIDGE_JACK, -1);
setBridge(BRIDGE_AUX, +1);
// jack down and fluffer
setRelay(RELAY_A1, false);
setRelay(RELAY_B1, false);
setRelay(RELAY_A2, false);
setRelay(RELAY_B2, true);
setRelay(RELAY_A3, true);
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);
// only fluffer
setRelay(RELAY_A1, false);
setRelay(RELAY_B1, false);
setRelay(RELAY_A2, false);
setRelay(RELAY_B2, false);
setRelay(RELAY_A3, true);
reset_shutdown_timer();
break;
default: break;
default:
// invalid state; turn all relays off
setRelay(RELAY_A1, false);
setRelay(RELAY_B1, false);
setRelay(RELAY_A2, false);
setRelay(RELAY_B2, false);
setRelay(RELAY_A3, false);
break;
}
driveRelays();
@@ -304,20 +317,20 @@ void control_task(void *param) {
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() {
esp_err_t fsm_init() {
if (fsm_cmd_queue == NULL) {
fsm_cmd_queue = xQueueCreate(8, sizeof(fsm_cmd_t));
}
xTaskCreate(control_task, "FSM", 4096, NULL, 5, NULL);
}
return ESP_OK;
}
esp_err_t fsm_stop() { return ESP_OK; }

View File

@@ -7,7 +7,7 @@
#include "freertos/queue.h"
typedef enum { FSM_CMD_STOP, FSM_CMD_UNDO, FSM_CMD_SHUTDOWN} fsm_cmd_t;
typedef enum { FSM_CMD_START, FSM_CMD_STOP, FSM_CMD_UNDO, FSM_CMD_SHUTDOWN} fsm_cmd_t;
typedef enum {
STATE_IDLE = 0,
@@ -22,21 +22,33 @@ typedef enum {
} fsm_state_t;
typedef enum {
BRIDGE_AUX = 0,
RELAY_NONE = 0,
RELAY_C3,
RELAY_B3,
RELAY_A3,
RELAY_B2,
RELAY_A2,
RELAY_B1,
RELAY_A1
} relay_t;
typedef enum {
BRIDGE_AUX = 2,
BRIDGE_JACK = 1,
BRIDGE_DRIVE = 2,
BRIDGE_DRIVE = 0,
} bridge_t;
#define N_RELAYS 6
#define N_RELAYS 8
#define N_BRIDGES 3
void pulseOverride(bridge_t bridge, int8_t dir, int64_t pulse);
void pulseOverride(relay_t relay/*, int64_t pulse*/);
void start_fsm();
esp_err_t fsm_init();
esp_err_t fsm_stop();
void fsm_request(fsm_cmd_t cmd);
void fsm_begin_auto_move();
//void fsm_begin_auto_move();
int8_t fsm_get_current_progress(int8_t remainder);

View File

@@ -12,6 +12,11 @@
// Debounce & Repeat Settings
#define DEBOUNCE_MS 50
#define REPEAT_MS 200
#define REPEAT_START_MS 700
static uint8_t lcd_col = 0;
static uint8_t lcd_row = 0;

View File

@@ -24,7 +24,7 @@
#define TCA_REG_CONFIG1 0x07
// Debounce & Repeat Settings
#define DEBOUNCE_MS 50
#define DEBOUNCE_MS 50
#define REPEAT_MS 200
#define REPEAT_START_MS 700
@@ -45,7 +45,7 @@ static esp_err_t tca_read_word(uint8_t reg, uint16_t *value) {
return ret;
}
esp_err_t i2cdev_init(void) {
esp_err_t i2c_init(void) {
if (i2c_initted) return ESP_OK;
i2c_config_t conf = {
@@ -75,6 +75,13 @@ esp_err_t i2c_set_led1(uint8_t state) {
return tca_write_word_8(TCA_REG_OUTPUT0, state<<5);
}
esp_err_t i2c_stop() {
if (!i2c_initted) return ESP_OK;
i2c_set_relays(0);
i2c_set_led1(0);
return ESP_OK;
}
#define N_BTNS 2
static bool debounced_state[N_BTNS] = {false};
static bool last_known_state[N_BTNS] = {false};

View File

@@ -6,7 +6,8 @@
#include "esp_err.h"
// Public Functions
esp_err_t i2cdev_init(void);
esp_err_t i2c_init(void);
esp_err_t i2c_stop(void);
esp_err_t i2c_set_relays(uint8_t states);
esp_err_t i2c_set_led1(uint8_t state);

View File

@@ -1,3 +1,5 @@
#include "esp_task_wdt.h"
#include "i2c.h"
#include "storage.h"
#include "uart_comms.h"
#include "esp_err.h"
@@ -7,9 +9,13 @@
#include "power_mgmt.h"
#include "rtc.h"
#include "sensors.h"
#include "solar.h"
#include "rf.h"
#define TAG "MAIN"
int64_t last_log_time = 0;
esp_err_t send_log() {
char entry[LOG_ENTRY_SIZE] = {0};
@@ -20,13 +26,13 @@ esp_err_t send_log() {
memcpy(&entry[1], &be_timestamp, 8);
// Pack 32-bit voltages/currents into bytes 9-24
/*int32_t be_voltage = htobe32(get_battery_mV());
float be_voltage = htobe32(get_battery_V());
memcpy(&entry[9], &be_voltage, 4);
int32_t be_current1 = htobe32(get_bridge_mA(BRIDGE_DRIVE));
float be_current1 = htobe32(get_bridge_A(BRIDGE_DRIVE));
memcpy(&entry[13], &be_current1, 4);
int32_t be_current2 = htobe32(get_bridge_mA(BRIDGE_JACK));
float be_current2 = htobe32(get_bridge_A(BRIDGE_JACK));
memcpy(&entry[17], &be_current2, 4);
int32_t be_current3 = htobe32(get_bridge_mA(BRIDGE_AUX));
float be_current3 = htobe32(get_bridge_A(BRIDGE_AUX));
memcpy(&entry[21], &be_current3, 4);
int32_t be_counter = htobe32(get_sensor_counter(SENSOR_DRIVE));
@@ -34,35 +40,188 @@ esp_err_t send_log() {
entry[29] = get_sensor(SENSOR_DRIVE);
entry[30] = get_sensor(SENSOR_JACK);
entry[31] = fsm_get_state();*/
entry[31] = fsm_get_state();
last_log_time = esp_timer_get_time();
return write_log(entry);
}
typedef enum {
LED_STATE_DRIVING,
LED_STATE_ERROR,
LED_STATE_AWAKE,
LED_STATE_CANCELLING,
LED_STATE_START1,
LED_STATE_START2,
LED_STATE_START3,
LED_STATE_START4,
LED_STATE_BOOTING
} led_state_t;
void driveLEDs(led_state_t state) {
uint8_t patterns[4][12] = {
{1,3,7,6,4,0},
{7,0},
{1,1,1,1,1,1, 1,1,1,3},
{4,2}
};
switch(state) {
case LED_STATE_DRIVING:
i2c_set_led1(patterns[state][(esp_timer_get_time()/100000) % 6]);
break;
case LED_STATE_ERROR:
i2c_set_led1(patterns[state][(esp_timer_get_time()/1000000) % 2]);
break;
case LED_STATE_AWAKE:
i2c_set_led1(patterns[state][(esp_timer_get_time()/200000) % 10]);
break;
case LED_STATE_CANCELLING:
i2c_set_led1(patterns[state][(esp_timer_get_time()/200000) % 2]);
break;
case LED_STATE_BOOTING:
i2c_set_led1(1);
break;
case LED_STATE_START1:
i2c_set_led1(0);
break;
case LED_STATE_START2:
i2c_set_led1(1);
break;
case LED_STATE_START3:
i2c_set_led1(3);
break;
case LED_STATE_START4:
i2c_set_led1(7);
break;
}
}
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");
esp_task_wdt_add(NULL);
// Say hello; turn on the lights
esp_sleep_wakeup_cause_t cause = rtc_wakeup_cause();
if (i2c_init() != ESP_OK) ESP_LOGE(TAG, "I2C FAILED");
i2c_set_relays(0);
driveLEDs(LED_STATE_BOOTING);
// Every boot we load parameters and monitor solar, no matter what
if (adc_init() != ESP_OK) ESP_LOGE(TAG, "ADC FAILED");
if (rtc_xtal_init() != ESP_OK) ESP_LOGE(TAG, "RTC FAILED");
if (storage_init() != ESP_OK) ESP_LOGE(TAG, "STORAGE FAILED");
if (log_init() != ESP_OK) ESP_LOGE(TAG, "LOG FAILED");
if (run_solar_fsm() != ESP_OK) ESP_LOGE(TAG, "SOLAR FAILED");
// TODO: Do a 12V check and enter deep sleep if there's a problem
send_log();
// Check wake reasons
// If button held, we stay #woke
// If not it must've been the RTC - check alarms
// If there's an alarm or the button was held, do a full boot
// Full boot will handle things from there
// only truly wake up if we saw it on EXT0, or from alarm
/*if (!rtc_is_set()) {
//ESP_LOGI("MAIN", "RTC is not set. Can't sleep til then.");
} else */if (cause == ESP_SLEEP_WAKEUP_EXT0) {
ESP_LOGI("MAIN", "Woke from button press");
} else {
if (!alarm_tripped()) {
//enter_deep_sleep();
}
}
// Initialize logging
ESP_LOGI(TAG, "Initializing logging...");
ret = log_init();
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Log init failed");
}
/*** FULL BOOT ***/
if (uart_init() != ESP_OK) ESP_LOGE(TAG, "UART FAILED");
if (power_init() != ESP_OK) ESP_LOGE(TAG, "POWER FAILED");
if (rf_init() != ESP_OK) ESP_LOGE(TAG, "RF FAILED");
if (fsm_init() != ESP_OK) ESP_LOGE(TAG, "FSM FAILED");
if (sensors_init() != ESP_OK) ESP_LOGE(TAG, "SENSORS FAILED");
/*** MAIN LOOP ***/
uart_start();
TickType_t xLastWakeTime = xTaskGetTickCount();
const TickType_t xFrequency = pdMS_TO_TICKS(100);
while(true) {
vTaskDelayUntil(&xLastWakeTime, xFrequency);
send_log();
i2c_poll_buttons();
if (i2c_get_button_state(0))
reset_shutdown_timer();
switch (fsm_get_state()) {
case STATE_IDLE:
// LED cue for user
if (i2c_get_button_ms(0) > 1600){
driveLEDs(LED_STATE_START4);
} else if (i2c_get_button_ms(0) > 1100){
driveLEDs(LED_STATE_START3);
} else if (i2c_get_button_ms(0) > 600){
driveLEDs(LED_STATE_START2);
} else if (i2c_get_button_ms(0) > 100){
driveLEDs(LED_STATE_START1);
} else{
driveLEDs(LED_STATE_AWAKE);
}
// when not actively moving we log at a low frequency
if (esp_timer_get_time() > last_log_time + DEEP_SLEEP_US)
send_log();
if(i2c_get_button_ms(0) > 2100)
fsm_request(FSM_CMD_START);
break;
case STATE_UNDO_JACK:
case STATE_UNDO_JACK_START:
// assume it's running
send_log();
driveLEDs(LED_STATE_CANCELLING);
if (i2c_get_button_tripped(0)) {
ESP_LOGI(TAG, "AAAAH STOP!!!");
fsm_request(FSM_CMD_STOP);
}
break;
default:
// assume it's running in every other case
send_log();
driveLEDs(LED_STATE_DRIVING);
if (i2c_get_button_tripped(0)) {
ESP_LOGI(TAG, "CTRL + Z PLZ!");
fsm_request(FSM_CMD_UNDO);
}
break;
}
if (alarm_tripped()) {
fsm_request(FSM_CMD_START);
set_next_alarm();
}
/*ESP_LOGI(TAG, "VOLTAGE: %2.3f | CURRENTS: %+2.8f %+2.8f %+2.8f",
get_battery_V(),
get_bridge_A(BRIDGE_DRIVE),
get_bridge_A(BRIDGE_JACK),
get_bridge_A(BRIDGE_AUX));*/
run_solar_fsm();
//check_shutdown_timer();
esp_task_wdt_reset();
}
}

View File

@@ -34,194 +34,47 @@
#include "storage.h"
#include "rtc.h"
#define TAG "POWER"
// === GPIO Pin Definitions ===
#define PIN_V_ISENS1 ADC_CHANNEL_6 // GPIO34
#define PIN_V_ISENS2 ADC_CHANNEL_3 // GPIO39 / VN
#define PIN_V_ISENS1 ADC_CHANNEL_0 // GPIO36 / VP
#define PIN_V_ISENS2 ADC_CHANNEL_6 // GPIO34
#define PIN_V_ISENS3 ADC_CHANNEL_7 // GPIO35
#define PIN_V_BATTERY ADC_CHANNEL_0 // GPIO36 / VP
#define PIN_V_BATTERY ADC_CHANNEL_3 // GPIO39 / VN
#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
// update time
#define UPDATE_MS 20
#define UPDATE_S 0.02f
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);
}
int64_t now; // us
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};
int64_t az_enable_time; // Timestamp to enable autozeroing at (negative to disable)
float az_offset; // Accumulated zero offset
bool az_initialized; // First valid zero established
// === 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};
bool ema_init;
float ema_current;
float current; // with all the corrections applied
float heat;
bool tripped;
int64_t trip_time;
} isens_channel_t;
static isens_channel_t isens[N_BRIDGES] = {0};
// === 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
esp_err_t adc_init() {
// ADC1 oneshot mode
adc_oneshot_unit_init_cfg_t init_cfg = {
.unit_id = ADC_UNIT_1,
};
@@ -246,51 +99,62 @@ static esp_err_t adc_init(void) {
};
ESP_ERROR_CHECK(adc_cali_create_scheme_line_fitting(&cali_cfg, &adc_cali_handle));
return ESP_OK;
}
float get_raw_battery_voltage(void) {
int adc_raw = 0;
int voltage_mv = 0;
if (adc_oneshot_read(adc1_handle, PIN_V_SENS_BAT, &adc_raw)
!= ESP_OK) { return NAN; }
if (adc_cali_raw_to_voltage(adc_cali_handle, adc_raw, &voltage_mv)
!= ESP_OK) { return NAN; }
// Voltage divider: 150kohm to 1Mohm -> gain = 1.15 -> scale = 1150/150
return voltage_mv * 0.00766666666; // same as / 1000.0 * 1150.0 / 150.0;
}
esp_err_t process_battery_voltage(void)
{
float raw = get_raw_battery_voltage();
if (!ema_battery_init) {
ema_battery = (float)raw;
ema_battery_init = true;
} else {
float alpha = get_param(PARAM_ADC_ALPHA_BATTERY).f32;
if (isnan(raw)) {
ESP_LOGI(TAG, "RAW BATTERY IS NAN");
} else {
if (isnan(ema_battery) || isnan(alpha))
ema_battery = raw;
else
ema_battery = alpha * (float)raw + (1.0f - alpha) * ema_battery;
}
}
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 set_autozero(bridge_t bridge) {
// enable autozeroing for this bridge 1 second from now
isens[bridge].az_enable_time = now+1000000;
//ESP_LOGI(TAG, "KILLING BRIDGE %d; %lld -> %lld", bridge, (long long int) now, (long long int) isens[bridge].az_enable_time);
}
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;
esp_err_t process_bridge_current(bridge_t bridge) {
int adc_raw = 0;
int voltage_mv = 0;
isens_channel_t *channel = &isens[bridge];
adc_channel_t pin;
switch(bridge) {
case BRIDGE_DRIVE: pin = PIN_V_ISENS3; break;
case BRIDGE_AUX: pin = PIN_V_ISENS1; break;
case BRIDGE_DRIVE: pin = PIN_V_ISENS1; break;
case BRIDGE_JACK: pin = PIN_V_ISENS2; break;
default: return -42069;
case BRIDGE_AUX: pin = PIN_V_ISENS3; break;
default: return -42069; // lol
}
if (adc_oneshot_read(adc1_handle, pin, &adc_raw) != ESP_OK) {
@@ -299,205 +163,171 @@ static int32_t read_bridge_current_raw(bridge_t bridge) {
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;
float raw_a = NAN;
switch (bridge) {
case BRIDGE_JACK:
case BRIDGE_AUX:
current_ma = offset_mv * 1000 / 44; // 44 mV/A
// ACS37042KLHBLT-030B3 is 30A capable and 44 mV/A
raw_a = (voltage_mv - 1650.0f) / 44.0f;
break;
case BRIDGE_DRIVE:
current_ma = offset_mv * 10000 / 132; // 13.2 mV/A
// ACS37220LEZATR-100B3 is 100A capable and 13.2 mV/A
raw_a = (voltage_mv - 1650.0f) / 13.2f;
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;
if (!channel->ema_init) {
channel->ema_current = (float)raw_a;
channel->ema_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;
float alpha = get_param(PARAM_ADC_ALPHA_ISENS).f32;
if (isnan(raw_a)) {
ESP_LOGI(TAG, "RAW BATTERY IS NAN");
channel->ema_current = NAN;
} else {
if (isnan(ema_battery) || isnan(alpha))
channel->ema_current = raw_a;
else
channel->ema_current = alpha * raw_a + (1.0f - alpha) * channel->ema_current;
}
}
// === AUTO-ZERO LEARNING PHASE ===
if (now > channel->az_enable_time) {
//ESP_LOGI(TAG, "AZING %d", bridge);
float db = get_param(PARAM_ADC_DB_IAZ).f32;
if (isnan(db) || fabsf(channel->ema_current) <= db) {
// Valid zero sample
if (!channel->az_initialized) {
channel->az_offset = channel->ema_current;
channel->az_initialized = true;
} else {
float alpha = get_param(PARAM_ADC_ALPHA_IAZ).f32;
if (isnan(raw_a)) {
ESP_LOGI(TAG, "RAW BATTERY IS NAN");
} else {
if (isnan(ema_battery) || isnan(alpha))
channel->az_offset = channel->ema_current;
else
channel->az_offset = alpha * channel->ema_current +
(1.0f - alpha) * channel->az_offset;
}
}
}
}
// Apply the offset
channel->current = channel->ema_current - channel->az_offset;
// PARAMETERS FOR E-FUSING ALGORITHM
// PARAM_EFUSE_KINST : ratio of nominal current that should cause an immediate shutdown
// PARAM_EFUSE_TCOOL : cooldown timer from trip (in microseconds)
// PARAM_EFUSE_TAUCOOL : speed of cooldown for heating (units are 1/s; bigger = faster cooldown)
// Monitor E-fusing
float I_nominal = NAN;
switch(bridge) {
case BRIDGE_DRIVE:
I_nominal = get_param(PARAM_EFUSE_INOM_1).f32;
break;
case BRIDGE_JACK:
I_nominal = get_param(PARAM_EFUSE_INOM_2).f32;
break;
case BRIDGE_AUX:
I_nominal = get_param(PARAM_EFUSE_INOM_3).f32;
break;
}
// Normalize the current as a fraction of rated current
float I_norm = fabsf(channel->current / I_nominal);
// Instant trip on extreme overcurrent
if (I_norm >= get_param(PARAM_EFUSE_KINST).f32) {
channel->tripped = true;
channel->trip_time = now;
ESP_LOGI(TAG, "FUSE TRIP: Inom: %+.5f HEAT:%+2.5f", I_norm, channel->heat);
return ESP_OK; // no more processing, if we're over, we're over
}
// Accumulate heat
channel->heat += (I_norm * I_norm) * UPDATE_S;
// Only do cooling when below threshold
if (I_norm < 1.0f) {
// if we are hot we radiate more heat
// (I^2/I^2*t) * (1/t) * t = I^2/I^2*t
channel->heat -= channel->heat * get_param(PARAM_EFUSE_TAUCOOL).f32 * UPDATE_S;
channel->heat = fmaxf(0.0f, channel->heat); // keep it from going negative
// channel.tripped = false; // Auto-clear if cooled (WTF why this is insane)
}
// If built-up heat exceeds the time limit, trip
// Recall units of heat are (current_actual^2/current_nominal^2)*time
// Ergo, heat is measured in seconds
if (channel->heat > get_param(PARAM_EFUSE_HEAT_THRESH).f32) {
channel->tripped = true;
channel->trip_time = now;
// If we're not overheated
// And enough time has passed
// Go ahead and reset the e-fuse
} else if (channel->tripped &&
(now - channel->trip_time) > get_param(PARAM_EFUSE_TCOOL).i64) {
channel->tripped = false;
// channel.heat = 0.0f // I think we should wait for the e-fuse to catch up
}
if (bridge == BRIDGE_DRIVE)
ESP_LOGI(TAG, "FUSE: Inom: %+.5f HEAT:%+2.5f", I_norm, channel->heat);
return ESP_OK;
}
// === Public Accessors ===
float get_bridge_A(bridge_t bridge)
{
if (bridge >= N_BRIDGES) return NAN;
return isens[bridge].current;
}
float get_battery_V(void)
{
if (ema_battery_init)
return ema_battery;
return get_raw_battery_voltage();
}
// === Public E-Fuse Controls ===
void efuse_reset_all(void)
/*void efuse_reset_all(void)
{
for (uint8_t i = 0; i < N_BRIDGES; i++) {
efuse_heat[i] = 0.0f;
efuse_tripped[i] = false;
isens[i].heat = 0.0f;
isens[i].tripped = false;
}
}
}*/
bool efuse_is_tripped(uint8_t bridge)
bool efuse_is_tripped(bridge_t bridge)
{
if (bridge >= N_BRIDGES) return false;
return efuse_tripped[bridge];
return isens[bridge].tripped;
}
// === 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);
const TickType_t xFrequency = pdMS_TO_TICKS(UPDATE_MS);
//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();
now = esp_timer_get_time(); // us
/*if (now - last_wake_time < period) {
uint32_t delay_us = (period - (now - last_wake_time)) / 1000;
@@ -508,43 +338,20 @@ void power_mgmt_task(void *param) {
// 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);
process_bridge_current(i);
}
/*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);
process_battery_voltage();
esp_task_wdt_reset();
}
}
void start_power() {
esp_err_t power_init() {
xTaskCreate(power_mgmt_task, "PWR", 4096, NULL, 5, NULL);
return ESP_OK;
}
void shutdown_power() {
esp_err_t power_stop() {
return ESP_OK;
}

546
main/power_mgmt.c.old Normal file
View File

@@ -0,0 +1,546 @@
/*
* 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_0 // GPIO36 / VP
#define PIN_V_ISENS2 ADC_CHANNEL_6 // GPIO34
#define PIN_V_ISENS3 ADC_CHANNEL_7 // GPIO35
#define PIN_V_BATTERY ADC_CHANNEL_3 // GPIO39 / VN
#define PIN_V_SENS_BAT PIN_V_BATTERY
#define PIN_CHG_BULK GPIO_NUM_26
#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);
//ESP_LOGI("BAT", "BULK");
break;
case CHG_STATE_FLOAT:
gpio_set_level(PIN_CHG_BULK, 0);
//ESP_LOGI("BAT", "FLOAT");
break;
case CHG_STATE_OFF:
gpio_set_level(PIN_CHG_BULK, 0);
//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_ISENS1; break;
case BRIDGE_AUX: pin = PIN_V_ISENS3; break;
case BRIDGE_JACK: pin = PIN_V_ISENS2; break;
default: return -42069; // lol
}
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() {
}

View File

@@ -8,27 +8,22 @@
#ifndef MAIN_POWER_MGMT_H_
#define MAIN_POWER_MGMT_H_
#include "control_fsm.h"
#include <stdbool.h>
#include <stdint.h>
#include "esp_err.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 efuse_reset_all(void); // Clear all trip states (manual/programmatic reset)
bool efuse_is_tripped(bridge_t bridge); // Query if bridge is currently faulted
void resetBatTimers();
float get_bridge_A(bridge_t bridge);
float get_battery_V();
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
void set_autozero(bridge_t bridge);
int32_t get_bridge_mA(uint8_t bridge);
int32_t get_battery_mV();
void start_power();
void shutdown_power();
esp_err_t adc_init();
esp_err_t power_init();
esp_err_t power_stop();
#endif /* MAIN_POWER_MGMT_H_ */

View File

@@ -1,6 +1,7 @@
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "control_fsm.h"
#include "driver/rmt_rx.h"
#include "driver/gpio.h"
#include "esp_log.h"
@@ -14,16 +15,18 @@
#include "driver/rmt_rx.h"
#include "driver/gpio.h"
#include "esp_log.h"
#include "rf.h"
#include "flash.h"
#include "storage.h"
#define RF_PIN GPIO_NUM_23
#define TAG "RF"
#define RF_PIN GPIO_NUM_25
#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 {
@@ -37,6 +40,8 @@ typedef struct {
// Global queue for passing decoded codes between tasks
static QueueHandle_t g_code_queue = NULL;
int learn_flag = -1;
// 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;
@@ -48,6 +53,7 @@ static bool rfrx_done(rmt_channel_handle_t channel, const rmt_rx_done_event_data
// Task that receives and decodes RF signals
static void rf_receiver_task(void* param) {
esp_task_wdt_add(NULL);
esp_log_level_set("rmt", ESP_LOG_NONE); // disable rmt messages about hw buffer too small
const uint16_t tlow = (P_HIGH - P_LOW - (2 * P_MARGIN));
const uint16_t thigh = (P_HIGH - P_LOW + (2 * P_MARGIN));
@@ -87,7 +93,7 @@ static void rf_receiver_task(void* param) {
ESP_LOGI("RF", "RF receiver task started on core %d", xPortGetCoreID());
for(;;) {
if (xQueueReceive(rx_queue, &rx_data, pdMS_TO_TICKS(1000)) == pdPASS) {
if (xQueueReceive(rx_queue, &rx_data, pdMS_TO_TICKS(500)) == pdPASS) {
size_t len = rx_data.num_symbols;
rmt_symbol_word_t *cur = rx_data.received_symbols;
@@ -126,16 +132,44 @@ static void rf_receiver_task(void* param) {
// 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);
int64_t encoded = ((int64_t)len << 56) | code;
ESP_LOGI(TAG, "GOT KEYCODE 0x%lx [%d]", (long) code, len);
if (learn_flag >= 0) {
set_param(PARAM_KEYCODE_0 + learn_flag,
(param_value_t){.i64 = encoded});
ESP_LOGI(TAG, "LEARNED KEYCODE");
learn_flag = -1;
} else {
rf_code_t rf_msg = {
.code = code,
.high_avg = high / 24,
.low_avg = low / 24,
.errors = err,
.num_symbols = len
};
// Don't do this anymore. No need to pass data between threads. Just act on it.
// Non-blocking send - if queue is full, just drop it
//xQueueSend(g_code_queue, &rf_msg, 0);
for (uint8_t i = 0; i < NUM_RF_BUTTONS; i++) {
int64_t match = get_param(PARAM_KEYCODE_0+i).i64;
if (encoded == match) {
switch (i) {
case 0: pulseOverride(RELAY_A1); pulseOverride(RELAY_A3); break;
case 1: pulseOverride(RELAY_B1); pulseOverride(RELAY_A3); break;
case 2: pulseOverride(RELAY_A2); break;
case 3: pulseOverride(RELAY_B2); break;
default: break;
}
}
}
}
}
// Debug output - print raw symbols
@@ -182,33 +216,36 @@ static void rf_receiver_task(void* param) {
vTaskDelete(NULL);
}
void start_rf() {
esp_err_t rf_init() {
g_code_queue = xQueueCreate(5, sizeof(rf_code_t));
assert(g_code_queue);
xTaskCreate(rf_receiver_task, "RF", 4096, NULL, 10, NULL);
xTaskCreate(rf_receiver_task, TAG, 4096, NULL, 10, NULL);
return ESP_OK;
}
esp_err_t rf_stop() { return ESP_OK; }
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);
set_param(PARAM_KEYCODE_0+index, (param_value_t){.i64=code});
}
void rf_learn_keycode(uint8_t index) {
if (index >= 8) return;
learn_flag = index;
}
void rf_cancel_learn_keycode() {
learn_flag = -1;
}
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))
if (newcode == get_param(PARAM_KEYCODE_0+i).i64)
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);

View File

@@ -10,11 +10,12 @@
#include "freertos/task.h"
#include "freertos/queue.h"
#define NUM_RF_BUTTONS 4
#define NUM_RF_BUTTONS 8
int64_t recieveKeycode();
void start_rf();
esp_err_t rf_init();
esp_err_t rf_stop();
void rf_set_keycode(uint8_t index, int64_t code);
@@ -23,4 +24,7 @@ int64_t rf_get_raw_keycode();
void rf_clear_queue();
void rf_learn_keycode(uint8_t index);
void rf_cancel_learn_keycode();
#endif

View File

@@ -28,16 +28,13 @@
//#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 "solar.h"
#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;
@@ -45,7 +42,7 @@ bool rtc_is_set() {
return rtc_set;
}
esp_err_t start_rtc(void) {
esp_err_t rtc_xtal_init(void) {
/* ---- Wake sources ---- */
esp_sleep_enable_ext0_wakeup(PIN_BTN_INTERRUPT, 0);
gpio_set_direction(PIN_BTN_INTERRUPT, GPIO_MODE_INPUT);
@@ -62,7 +59,7 @@ esp_err_t start_rtc(void) {
// 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));
//vTaskDelay(pdMS_TO_TICKS(1));
//ESP_LOGI("RTC", "Configured with external 32 kHz oscillator (freq: %d Hz)", rtc_clk_slow_freq_get_hz());
@@ -80,9 +77,9 @@ void reset_shutdown_timer(void)
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);
fsm_request(FSM_CMD_STOP);
i2c_stop();
esp_sleep_enable_timer_wakeup(DEEP_SLEEP_US);
esp_deep_sleep_start();
}
@@ -91,7 +88,7 @@ void rtc_set_time(struct tm *tm) {
start_new_log_file();
struct timeval tv = { .tv_sec = mktime(tm), .tv_usec = 0 };
settimeofday(&tv, NULL);
resetBatTimers();
reset_solar_fsm();
}
void rtc_get_time(struct tm *tm)

View File

@@ -18,16 +18,14 @@
#include "esp_err.h"
#define TRANSITION_DELAY_US 1000000
#define OVERRIDE_PULSE_RF 160000
#define OVERRIDE_PULSE_UX 80000
#define POWER_INACTIVITY_TIMEOUT_MS 180000
#define DEEP_SLEEP_US 120000000ULL /* 120 seconds in deep sleep */
/* -------------------------------------------------------------------------- */
/* Public API */
/* -------------------------------------------------------------------------- */
esp_err_t start_rtc();
esp_err_t rtc_xtal_init();
bool rtc_is_set();

View File

@@ -8,7 +8,7 @@
static const char* TAG = "SENS";
uint8_t sensor_pins[N_SENSORS] = {GPIO_NUM_19, GPIO_NUM_25};
uint8_t sensor_pins[N_SENSORS] = {GPIO_NUM_27, GPIO_NUM_14};
volatile int32_t sensor_count[N_SENSORS] = {0};
static volatile uint64_t sensor_last_isr_time[N_SENSORS] = {0};
@@ -43,6 +43,7 @@ static void IRAM_ATTR sensor_isr_handler(void* arg) {
// Debounce task: Processes queue, updates state & count
static void sensor_debounce_task(void* param) {
esp_task_wdt_add(NULL);
sensor_event_t evt;
static uint64_t last_processed_time[N_SENSORS] = {0};
static bool last_raw_state[N_SENSORS] = {false};
@@ -54,8 +55,6 @@ static void sensor_debounce_task(void* param) {
last_raw_state[i] = level;
last_processed_time[i] = esp_timer_get_time();
}
esp_task_wdt_add(NULL);
uint8_t i = 0;
@@ -117,7 +116,7 @@ static void sensor_debounce_task(void* param) {
}
}
void start_sensors() {
esp_err_t sensors_init() {
gpio_config_t io_conf = {
.pin_bit_mask = (1ULL << sensor_pins[0]) | (1ULL << sensor_pins[1]),
.mode = GPIO_MODE_INPUT,
@@ -130,7 +129,7 @@ void start_sensors() {
sensor_event_queue = xQueueCreate(16, sizeof(sensor_event_t));
if (!sensor_event_queue) {
ESP_LOGE(TAG, "Failed to create sensor queue");
return;
return ESP_FAIL;
}
// Install ISR service
@@ -141,15 +140,19 @@ void start_sensors() {
sensor_stable_state[i] = !gpio_get_level(sensor_pins[i]);
}
xTaskCreate(sensor_debounce_task, "SENS_DEBOUNCE", 3072, NULL, 6, NULL);
xTaskCreate(sensor_debounce_task, "SENSORS", 3072, NULL, 6, NULL);
return ESP_OK;
}
void shutdown_sensors() {
esp_err_t sensors_stop() {
for (uint8_t i = 0; i < N_SENSORS; i++) {
gpio_isr_handler_remove(sensor_pins[i]);
}
gpio_uninstall_isr_service();
vQueueDelete(sensor_event_queue);
return ESP_OK;
}
// Public API

View File

@@ -27,7 +27,7 @@ int32_t get_sensor_counter(sensor_t i);
bool get_sensor(sensor_t i);
void start_sensors();
void shutdown_sensors();
esp_err_t sensors_init();
esp_err_t sensors_stop();
#endif /* MAIN_SENSORS_H_ */

91
main/solar.c Normal file
View File

@@ -0,0 +1,91 @@
#include "solar.h"
#include "driver/rtc_io.h"
#include "rtc.h"
#include "power_mgmt.h"
#include "esp_log.h"
#include "storage.h"
#define TAG "SOLAR"
#define PIN_CHG_BULK GPIO_NUM_26
typedef enum {
CHG_STATE_FLOAT = 0,
CHG_STATE_BULK = 1
} charge_state_t;
RTC_DATA_ATTR charge_state_t current_charge_state = CHG_STATE_FLOAT;
RTC_DATA_ATTR int64_t timer;
esp_err_t reset_solar_fsm() {
timer = -1;
current_charge_state = CHG_STATE_FLOAT;
return ESP_OK;
}
RTC_DATA_ATTR bool solar_needs_init = true;
esp_err_t init_solar_gpio() {
if (solar_needs_init) {
rtc_gpio_init(PIN_CHG_BULK);
rtc_gpio_set_direction(PIN_CHG_BULK, RTC_GPIO_MODE_OUTPUT_ONLY);
solar_needs_init = false;
}
return ESP_OK;
}
esp_err_t run_solar_fsm() {
init_solar_gpio();
int64_t now = system_rtc_get_raw_time();
//ESP_LOGI("BAT", "FSM STATE %d", current_charge_state);
float vbat = get_battery_V();
/*
The state machine is simple.
- After a period of time when battery is low, switch to bulk
- After a period of time in bulk, switch to float
*/
//if (rtc_is_set()) {
switch(current_charge_state) {
case CHG_STATE_BULK:
if (now > timer+get_param(PARAM_CHG_BULK_S).i64) {
current_charge_state = CHG_STATE_FLOAT;
}
break;
case CHG_STATE_FLOAT:
// if we have sufficient voltage, reset the timer
if (vbat > get_param(PARAM_CHG_LOW_V).f32) {
timer = now;
}
if (now > timer+get_param(PARAM_CHG_LOW_S).i64) {
timer = now;
current_charge_state = CHG_STATE_BULK;
}
break;
}
/*} else {
reset_solar_fsm();
ESP_LOGI(TAG, "RESET SOLAR FSM");
}*/
rtc_gpio_hold_dis(PIN_CHG_BULK);
switch(current_charge_state) {
case CHG_STATE_BULK:
rtc_gpio_set_level(PIN_CHG_BULK, 1);
//ESP_LOGI(TAG, "BULK");
break;
case CHG_STATE_FLOAT:
rtc_gpio_set_level(PIN_CHG_BULK, 0);
//ESP_LOGI(TAG, "FLOAT");
break;
}
rtc_gpio_hold_en(PIN_CHG_BULK);
//rtc_gpio_hold_en(PIN_CHG_DISABLE);
return ESP_OK;
}

16
main/solar.h Normal file
View File

@@ -0,0 +1,16 @@
/*
* solar.h
*
* Created on: Dec 13, 2025
* Author: Thad
*/
#ifndef MAIN_SOLAR_H_
#define MAIN_SOLAR_H_
#include "esp_err.h"
esp_err_t run_solar_fsm();
esp_err_t reset_solar_fsm();
#endif /* MAIN_SOLAR_H_ */

View File

@@ -5,7 +5,7 @@
#include "esp_crc.h"
#include "storage.h"
#define TAG "PARAMS"
#define TAG "STORAGE"
// ============================================================================
// PARAMETER TABLE GENERATION
@@ -301,7 +301,7 @@ esp_err_t write_log(char* entry) {
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);
//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++;

View File

@@ -67,23 +67,39 @@ typedef enum {
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(DRIVE_DIST, u16, 10) /*3*/\
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)
PARAM_DEF(JACK_MSPI, u16, 600) /*7*/\
PARAM_DEF(KEYCODE_0, i64, 0x19000000005D0C61) \
PARAM_DEF(KEYCODE_1, i64, 0x19000000005D0C62) \
PARAM_DEF(KEYCODE_2, i64, 0x19000000005D0C64) \
PARAM_DEF(KEYCODE_3, i64, 0x19000000005D0C68) /*11*/\
PARAM_DEF(KEYCODE_4, i64, -1) \
PARAM_DEF(KEYCODE_5, i64, -1) \
PARAM_DEF(KEYCODE_6, i64, -1) \
PARAM_DEF(KEYCODE_7, i64, -1) /*15*/\
PARAM_DEF(ADC_ALPHA_BATTERY, f32, 0.02) \
PARAM_DEF(ADC_ALPHA_ISENS, f32, 0.02) \
PARAM_DEF(ADC_ALPHA_IAZ, f32, 0.005) \
PARAM_DEF(ADC_DB_IAZ, f32, 5.0) /*19*/\
PARAM_DEF(EFUSE_INOM_1, f32, 40.0) \
PARAM_DEF(EFUSE_INOM_2, f32, 6.0) \
PARAM_DEF(EFUSE_INOM_3, f32, 2.0) \
PARAM_DEF(EFUSE_HEAT_THRESH, f32, 60.0) /*23*/\
PARAM_DEF(EFUSE_KINST, f32, 4.0) \
PARAM_DEF(EFUSE_TAUCOOL, f32, 0.2) \
PARAM_DEF(EFUSE_TCOOL, i64, 5000000) \
PARAM_DEF(LOW_PROTECTION_V, f32, 10.0) /*27*/\
PARAM_DEF(LOW_PROTECTION_S, i64, 10) \
PARAM_DEF(CHG_LOW_V, f32, 5.0) \
PARAM_DEF(CHG_LOW_S, i64, 5.0) \
PARAM_DEF(CHG_BULK_S, i64, 20) /*31*/\
PARAM_DEF(RF_PULSE_LENGTH, u64, 350000) \
// Generate enum for parameter indices
#define PARAM_DEF(name, type, default_val) PARAM_##name,

View File

@@ -2,12 +2,16 @@
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "esp_task_wdt.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/uart.h"
#include "esp_log.h"
#include "esp_system.h"
#include "storage.h"
#include <errno.h>
#include <ctype.h>
#include "rf.h"
#define TAG "UART"
@@ -19,6 +23,11 @@ static char cmd_buffer[CMD_MAX_LEN];
static int cmd_pos = 0;
static TaskHandle_t uart_task_handle = NULL;
// TODO: Set Time
// TODO: Pair Remote
// TODO: Command Move
// TODO: Show current sensor values
// Parse value as either decimal or hex (0x prefix)
static bool parse_uint64(const char *str, uint64_t *result) {
char *endptr;
@@ -80,7 +89,7 @@ static void print_param_value(param_idx_t id, param_value_t val) {
case PARAM_TYPE_i64:
printf("%lld (0x%016llX)\n",
(long long)val.i64, (unsigned long long)val.u64);
(long long)val.i64, (unsigned long long)val.i64);
break;
case PARAM_TYPE_f32:
@@ -99,6 +108,54 @@ static void print_param_value(param_idx_t id, param_value_t val) {
}
}
static esp_err_t parse_param_value(const char *orig_str, param_type_e type, param_value_t *val) {
const char *str = orig_str;
// Skip leading whitespace
while (isspace((unsigned char)*str)) str++;
// Check for negative sign on unsigned integer types
bool is_unsigned_int = (type == PARAM_TYPE_u8 || type == PARAM_TYPE_u16 || type == PARAM_TYPE_u32 || type == PARAM_TYPE_u64);
if (is_unsigned_int && *str == '-') {
return ESP_FAIL;
}
char *endptr;
errno = 0;
switch (type) {
case PARAM_TYPE_u8:
case PARAM_TYPE_u16:
case PARAM_TYPE_u32:
case PARAM_TYPE_u64:
val->u64 = strtoull(str, &endptr, 0);
break;
case PARAM_TYPE_i8:
case PARAM_TYPE_i16:
case PARAM_TYPE_i32:
case PARAM_TYPE_i64:
val->i64 = strtoll(str, &endptr, 0);
break;
case PARAM_TYPE_f32:
val->f32 = strtof(str, &endptr);
break;
case PARAM_TYPE_f64:
val->f64 = strtod(str, &endptr);
break;
default:
return ESP_FAIL;
}
if (errno == ERANGE || endptr == str || *endptr != '\0') {
return ESP_FAIL;
}
return ESP_OK;
}
// Process set parameter command: sp <id> <value>
static void cmd_set_param(char *args) {
char *id_str = strtok(args, " \t");
@@ -123,17 +180,14 @@ static void cmd_set_param(char *args) {
return;
}
// Parse value
uint64_t value;
if (!parse_uint64(val_str, &value)) {
param_value_t param_val = {0};
param_type_e type = get_param_type(id);
esp_err_t parse_err = parse_param_value(val_str, type, &param_val);
if (parse_err != ESP_OK) {
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 ",
@@ -259,6 +313,30 @@ static void cmd_help(char *args) {
printf("- Parameter IDs range from 0 to %d\n\n", NUM_PARAMS - 1);
}
static void cmd_rf_learn(char *args) {
char *id_str = strtok(args, " \t");
if (id_str == NULL) {
rf_cancel_learn_keycode();
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 < 8) {
printf("Listening for keycode for slot %d\n", id);
rf_learn_keycode(id);
return;
}
printf("ERROR: Keycode slot index out of bounds.\n");
}
// Parse and execute command
static void process_command(char *cmd) {
// Trim leading whitespace
@@ -310,6 +388,8 @@ static void process_command(char *cmd) {
cmd_list_params(cmd);
} else if (strcmp(command, "help") == 0) {
cmd_help(cmd);
} else if (strcmp(command, "rfl") == 0) {
cmd_rf_learn(cmd);
} else {
printf("ERROR: Unknown command '%s' (type 'help' for commands)\n", command);
}
@@ -317,9 +397,11 @@ static void process_command(char *cmd) {
// UART event task
void uart_event_task(void *pvParameters) {
esp_task_wdt_add(NULL);
uint8_t data[BUF_SIZE];
while (1) {
esp_task_wdt_reset();
int len = uart_read_bytes(UART_NUM, data, BUF_SIZE - 1, 20 / portTICK_PERIOD_MS);
if (len > 0) {
@@ -364,14 +446,14 @@ void uart_event_task(void *pvParameters) {
}
}
void uart_start() {
esp_err_t uart_init() {
// 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,
.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,
};
@@ -379,24 +461,25 @@ void uart_start() {
ESP_ERROR_CHECK(uart_param_config(UART_NUM, &uart_config));
// Print startup message
printf("\n\n");
/*printf("\n\n");
printf("=================================\n");
printf(" ESP32 Parameter Manager\n");
printf("=================================\n");
printf("Type 'help' for available commands\n\n");
printf("> ");
fflush(stdout);
fflush(stdout);*/
// Create UART task
xTaskCreate(uart_event_task, "uart_event_task", 4096, NULL, 12, &uart_task_handle);
ESP_LOGI(TAG, "UART interface started");
return ESP_OK;
}
void uart_stop() {
esp_err_t uart_stop() {
if (uart_task_handle == NULL) {
ESP_LOGW(TAG, "UART task not running");
return;
return ESP_OK;
}
ESP_LOGI(TAG, "Shutting down UART...");
@@ -415,4 +498,6 @@ void uart_stop() {
uart_driver_delete(UART_NUM);
ESP_LOGI(TAG, "UART shutdown complete");
return ESP_OK;
}

View File

@@ -8,7 +8,9 @@
#ifndef MAIN_UART_COMMS_H_
#define MAIN_UART_COMMS_H_
void uart_start();
void uart_stop();
#include "esp_err.h"
esp_err_t uart_init();
esp_err_t uart_stop();
#endif /* MAIN_UART_COMMS_H_ */