Compare commits

2 Commits
v1.0.0 ... main

Author SHA1 Message Date
286028b6a8 fix evse_link 2026-01-24 16:56:51 +00:00
023644a887 new upgrade 2025-12-21 23:28:26 +00:00
123 changed files with 10151 additions and 7949 deletions

209
README.md
View File

@@ -1,85 +1,180 @@
![ESP32 EVSE](https://github.com/dzurikmiroslav/esp32-evse/wiki/images/logo-full.svg)
# ChargeFlow EVSE Firmware (ESP32, ESP-IDF 5.x)
J1772 EVSE firmware for ESP32 based devices.
Firmware for an AC EVSE (EV charger) based on ESP32 and ESP-IDF 5.x, with:
![Build with ESP-IDF](https://github.com/dzurikmiroslav/esp32-evse/workflows/Build%20with%20ESP-IDF/badge.svg)
[![License](https://img.shields.io/github/license/dzurikmiroslav/esp32-evse.svg)](LICENSE.md)
- IEC-style EVSE state machine (Control Pilot A/B/C/D)
- Wi-Fi (STA + AP for local configuration)
- REST API served from SPIFFS
- Local authentication and OCPP integration
- Load balancing (master + slaves)
- Scheduler (time windows)
- Audible feedback (buzzer) and RGB LED status
- On-device ring-buffer logger
## Key features
- Hardware abstraction for device design
- Responsive web-interface
- OTA update
- Integrated energy meter
- Energy detection for relay control
- [REST](https://github.com/dzurikmiroslav/esp32-evse/wiki/Rest) API
- MQTT API
- [Modbus](https://github.com/dzurikmiroslav/esp32-evse/wiki/Modbus) (RS485, TCP)
- [Scripting](https://github.com/dzurikmiroslav/esp32-evse/wiki/Script)
- [Nextion HMI](https://github.com/dzurikmiroslav/esp32-evse/wiki/Nextion)
---
### Device definition method
## Features
_One firmware to rule them all._ Not really :-) one per device platform (ESP32, ESP32-S2...).
### Core EVSE
There is no need to compile the firmware for your EVSE design.
Source code ist not hardcoded to GPIOs or other hardware design features.
All code is written in ESP-IDF without additional mapping layer like Arduino.
- EVSE manager (`evse_manager`) coordinating:
- Hardware layer (`evse_hardware`)
- State machine (`evse_state`)
- Error handling (`evse_error`)
- Energy metering (`evse_meter` / `meter_manager`)
- Session tracking (`evse_session`)
- Runs a periodic tick (`evse_manager_tick()`) in its own FreeRTOS task.
- Supports multiple auth modes (OPEN / RFID / OCPP), with scheduling and load-balancer aware logic.
All configuration is written outside firmware in configuration file named _board.cfg_ on dedicated partition.
For example, on following scheme is minimal EVSE circuit with ESP32 devkit.
### Networking & REST
![Minimal circuit](https://github.com/dzurikmiroslav/esp32-evse/wiki/images/minimal-circuit.png)
- Wi-Fi:
- Station mode for normal operation.
- Access point mode for local configuration, enabled by a physical button.
- REST server (`rest_main`) serving from `/data` SPIFFS mount:
- For configuration, status, logs, etc. (exact endpoints depend on your REST implementation).
For this circuit there is _board.cfg_, for more information's see [Wiki](https://github.com/dzurikmiroslav/esp32-evse/wiki/Board-config).
### Button & User Input
- One physical button (configured via `board_config`):
- **Short press** → Starts Wi-Fi AP mode for configuration.
- **Long press (~30s)** → Erases NVS and reboots (factory-like reset).
- Robust handling:
- ISR with software debounce and spinlock.
- Dedicated `user_input_task` that receives button press/release notifications via `xTaskNotify`.
```bash
#Device name
DEVICE_NAME=ESP32 minimal EVSE
#Button
BUTTON_WIFI_GPIO=0
#Pilot
PILOT_PWM_GPIO=33
PILOT_ADC_CHANNEL=7
PILOT_DOWN_THRESHOLD_12=2410
PILOT_DOWN_THRESHOLD_9=2104
PILOT_DOWN_THRESHOLD_6=1797
PILOT_DOWN_THRESHOLD_3=1491
PILOT_DOWN_THRESHOLD_N12=265
#AC relay
AC_RELAY_GPIO=32
```
### Storage
### Web interface
- SPIFFS used for:
- `/cfg` partition: persistent configuration.
- `/data` partition: web assets, runtime data, logs, etc.
- Two separate mounts:
- `cfg_conf``/cfg` (label: `cfg`)
- `data_conf``/data` (label: `data`)
Fully responsive web interface is accessible local network IP address on port 80.
### LED Subsystem
Dashboard page
- RGB LED driven by LEDC:
- `ledc_driver` abstracts LEDC timer + channels.
- `led` module maps EVSE state & sessions to colors/patterns.
- LED patterns per EVSE state:
- **IDLE** → Green solid.
- **WAITING** (vehicle plugged, not charging) → Blue slow blink.
- **CHARGING** → Blue “breathing” effect.
- **FAULT** → Red fast blink.
- Session effects:
- Distinct visual patterns when a session starts/finishes.
- Uses a one-shot timer and a dedicated effect state machine.
![Dashboard](https://github.com/dzurikmiroslav/esp32-evse/wiki/images/web-dashboard.png)
### Buzzer
Settings page
- Buzzer with multiple patterns (`buzzer` + `buzzer_events`):
- Plugged/unplugged, card read/denied, AP start, charging, fault, etc.
- Supported modes:
- Active buzzer (ON/OFF).
- Passive buzzer with LEDC PWM (frequency & duty configurable).
- Features:
- Central queue + dedicated `buzzer_task`.
- Quiet hours support (optionally suppress non-critical sounds at night).
- Anti-spam mechanism to avoid excessively frequent beeps.
- Integrated with:
- EVSE events (state changes & faults)
- Auth events (RFID card success/denied/added)
- Network events (AP/STA up)
![Settings](https://github.com/dzurikmiroslav/esp32-evse/wiki/images/web-settings.png)
### Load Balancer
Mobile dashboard page
- `loadbalancer` component:
- Monitors GRID meter and EVSE meter via `meter_events`.
- Supports one master + up to 255 slaves (connectors array).
- Fair distribution of current with:
- Headroom calculation based on grid limit and measured current.
- Min current guarantees (e.g. 6 A) using a “water-filling” algorithm.
- Session-age based priority (oldest sessions first).
- Per-connector hysteresis and LB suspension/resume flags.
- Publishes limits via `LOADBALANCER_EVENTS`:
- `LOADBALANCER_EVENT_MASTER_CURRENT_LIMIT`
- `LOADBALANCER_EVENT_SLAVE_CURRENT_LIMIT`
- Fail-safe behavior:
- If GRID meter data times out, clamps connectors to minimum safe current instead of ramping up.
![Dashboard mobile](https://github.com/dzurikmiroslav/esp32-evse/wiki/images/web-dashboard-mobile.png)
### Scheduler
## Hardware
- Scheduler component (`scheduler`) emits `SCHED_EVENTS` with `allowed_now` flag:
- EVSE manager revokes authorization when the window closes.
- In OPEN mode, automatic re-authorization only happens when scheduler allows.
### ESP32DevkitC
### OCPP
Dev board with basic functionality, single phase energy meter, RS485. One side pcb, for DIY makers easy to make at home conditions ;-)
- `ocpp` module integration:
- Listens to OCPP events (`OCPP_EVENTS`).
- Handles:
- RemoteStart/Stop
- Authorization results
- ChangeAvailability (operative/inoperative) → mapped into local `enabled` config.
- EVSE manager mediates OCPP decisions with scheduler + load balancer.
[EasyEDA project](https://oshwlab.com/dzurik.miroslav/esp32-devkit-evse)
### Logger
![ESP32DevkitC](https://github.com/dzurikmiroslav/esp32-evse/wiki/images/esp32devkitc.jpg)
- `logger` + `output_buffer` components:
- Central log sink with ring buffer in RAM.
- Thread-safe via FreeRTOS mutex.
- Integrated with ESP log system via `esp_log_set_vprintf(logger_vprintf);`
- Optionally mirrors to UART (controlled via `CONFIG_ESP_CONSOLE_UART`).
- Simple reader API:
- Iterate entries using an index.
- Handy for exposing logs over REST/Web UI.
### ESP32-S2 DIY ALPHA
---
ESP32-S2 based EVSE with advanced functionality, three phase energy meter, RS485, UART, 1WIRE, RCM, socket lock.
## Project Structure (Relevant Parts)
[EasyEDA project](https://oshwlab.com/dzurik.miroslav/esp32s2-diy-evse)
Approximate layout (names may vary slightly in your repo):
![ESP32-S2-DA](https://github.com/dzurikmiroslav/esp32-evse/wiki/images/esp32s2da.jpg)
```text
main/
main.c # System entrypoint, button setup, module init
components/
evse/
evse_manager.c/.h # High-level EVSE orchestration
evse_state.c/.h # State machine & events
evse_error.c/.h # Error handling
evse_hardware.c/.h # Hardware abstraction
evse_session.c/.h # Session metrics
loadbalancer/
src/
loadbalancer.c
loadbalancer_events.c
input_filter.c
include/
loadbalancer.h
loadbalancer_events.h
input_filter.h
buzzer/
src/
buzzer.c
buzzer_events.c
include/
buzzer.h
buzzer_events.h
led/
src/
led.c
ledc_driver.c
include/
led.h
ledc_driver.h
logger/
src/
logger.c
output_buffer.c
include/
logger.h
output_buffer.h
# ... other modules: auth, ocpp, scheduler, meter_manager, evse_link, etc.

View File

@@ -3,5 +3,5 @@ set(srcs "src/auth_types.c" "src/auth.c" "src/wiegand.c" "src/wiegand_reader.c"
idf_component_register(SRCS "${srcs}"
INCLUDE_DIRS "include"
PRIV_INCLUDE_DIRS "src"
PRIV_REQUIRES nvs_flash driver esp_timer
REQUIRES esp_event evse ocpp evse_link)
PRIV_REQUIRES driver esp_timer
REQUIRES esp_event evse ocpp evse_link storage_service)

View File

@@ -4,19 +4,22 @@
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include <freertos/task.h>
#include <esp_log.h>
#include <esp_err.h>
#include <string.h>
#include <strings.h> // strcasecmp
#include <strings.h>
#include <stdio.h>
#include "wiegand_reader.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "storage_service.h"
#include "evse_link.h"
#include "evse_link_events.h"
#define MAX_TAGS 50
static const char *TAG = "Auth";
/* ===== Estado ===== */
@@ -25,162 +28,16 @@ static bool waiting_for_registration = false;
static char valid_tags[MAX_TAGS][AUTH_TAG_MAX_LEN];
static int tag_count = 0;
static uint32_t s_next_req_id = 1;
static bool s_wiegand_started = false; // controla se o Wiegand já foi iniciado
static bool s_wiegand_started = false;
/* ===== NVS keys ===== */
/* ===== Storage keys ===== */
#define NVS_NAMESPACE "auth"
#define NVS_TAG_PREFIX "tag_"
#define NVS_TAG_COUNT_KEY "count"
#define NVS_MODE_KEY "mode" // uint8_t
/* =========================
* NVS Persistence (tags)
* ========================= */
static void load_tags_from_nvs(void)
{
nvs_handle_t handle;
esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &handle);
if (err != ESP_OK)
{
ESP_LOGW(TAG, "No stored tags in NVS (nvs_open: %s)", esp_err_to_name(err));
return;
}
uint8_t count = 0;
err = nvs_get_u8(handle, NVS_TAG_COUNT_KEY, &count);
if (err != ESP_OK)
{
ESP_LOGW(TAG, "No tag count key in NVS (nvs_get_u8: %s)", esp_err_to_name(err));
nvs_close(handle);
return;
}
tag_count = 0;
for (int i = 0; i < count && i < MAX_TAGS; i++)
{
char key[16];
char tag_buf[AUTH_TAG_MAX_LEN];
size_t len = sizeof(tag_buf);
snprintf(key, sizeof(key), "%s%d", NVS_TAG_PREFIX, i);
err = nvs_get_str(handle, key, tag_buf, &len);
if (err == ESP_OK)
{
strncpy(valid_tags[tag_count], tag_buf, AUTH_TAG_MAX_LEN - 1);
valid_tags[tag_count][AUTH_TAG_MAX_LEN - 1] = '\0';
tag_count++;
}
else
{
ESP_LOGW(TAG, "Failed to load tag %d from NVS (%s)", i, esp_err_to_name(err));
}
}
nvs_close(handle);
ESP_LOGI(TAG, "Loaded %d tags from NVS", tag_count);
}
static void save_tags_to_nvs(void)
{
nvs_handle_t handle;
esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to open NVS to save tags: %s", esp_err_to_name(err));
return;
}
err = nvs_set_u8(handle, NVS_TAG_COUNT_KEY, tag_count);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "nvs_set_u8(count) failed: %s", esp_err_to_name(err));
nvs_close(handle);
return;
}
for (int i = 0; i < tag_count; i++)
{
char key[16];
snprintf(key, sizeof(key), "%s%d", NVS_TAG_PREFIX, i);
err = nvs_set_str(handle, key, valid_tags[i]);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "nvs_set_str(%s) failed: %s", key, esp_err_to_name(err));
nvs_close(handle);
return;
}
}
err = nvs_commit(handle);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "nvs_commit failed when saving tags: %s", esp_err_to_name(err));
nvs_close(handle);
return;
}
nvs_close(handle);
ESP_LOGI(TAG, "Tags saved to NVS (%d tags)", tag_count);
}
/* =========================
* NVS Persistence (mode)
* ========================= */
static void load_mode_from_nvs(void)
{
nvs_handle_t h;
esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &h);
if (err == ESP_OK)
{
uint8_t u = (uint8_t)AUTH_MODE_OPEN;
err = nvs_get_u8(h, NVS_MODE_KEY, &u);
if (err == ESP_OK)
{
if (u <= (uint8_t)AUTH_MODE_OCPP_RFID)
s_mode = (auth_mode_t)u;
}
else
{
ESP_LOGW(TAG, "No stored auth mode in NVS (nvs_get_u8: %s). Default OPEN", esp_err_to_name(err));
}
nvs_close(h);
}
else
{
ESP_LOGW(TAG, "No stored auth mode in NVS (nvs_open: %s). Default OPEN", esp_err_to_name(err));
}
ESP_LOGI(TAG, "Loaded mode = %d (%s)", (int)s_mode, auth_mode_to_str(s_mode));
}
static void save_mode_to_nvs(auth_mode_t mode)
{
nvs_handle_t h;
esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &h);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to open NVS to save auth mode: %s", esp_err_to_name(err));
return;
}
err = nvs_set_u8(h, NVS_MODE_KEY, (uint8_t)mode);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "nvs_set_u8(mode) failed: %s", esp_err_to_name(err));
nvs_close(h);
return;
}
err = nvs_commit(h);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "nvs_commit failed when saving mode: %s", esp_err_to_name(err));
nvs_close(h);
return;
}
nvs_close(h);
ESP_LOGI(TAG, "Saved mode = %d (%s)", (int)mode, auth_mode_to_str(mode));
}
// timeout para operações sync do storage
#define STORAGE_TO pdMS_TO_TICKS(2000)
/* =========================
* Helpers
@@ -190,37 +47,155 @@ static bool is_tag_valid(const char *tag)
for (int i = 0; i < tag_count; i++)
{
if (strncmp(valid_tags[i], tag, AUTH_TAG_MAX_LEN) == 0)
{
return true;
}
}
return false;
}
/* =========================
* Storage Persistence (tags)
* ========================= */
static void load_tags_from_storage(void)
{
uint8_t count = 0;
esp_err_t err = storage_get_u8_sync(NVS_NAMESPACE, NVS_TAG_COUNT_KEY, &count, STORAGE_TO);
if (err == ESP_ERR_NOT_FOUND)
{
ESP_LOGD(TAG, "No stored tags (count not found)");
tag_count = 0;
return;
}
if (err != ESP_OK)
{
ESP_LOGW(TAG, "Failed to read tag count (%s)", esp_err_to_name(err));
tag_count = 0;
return;
}
tag_count = 0;
for (int i = 0; i < (int)count && i < MAX_TAGS; i++)
{
char key[16];
char tag_buf[AUTH_TAG_MAX_LEN] = {0};
snprintf(key, sizeof(key), "%s%d", NVS_TAG_PREFIX, i);
err = storage_get_str_sync(NVS_NAMESPACE, key, tag_buf, sizeof(tag_buf), STORAGE_TO);
if (err == ESP_OK)
{
if (tag_buf[0] != '\0')
{
strncpy(valid_tags[tag_count], tag_buf, AUTH_TAG_MAX_LEN - 1);
valid_tags[tag_count][AUTH_TAG_MAX_LEN - 1] = '\0';
tag_count++;
}
}
else if (err == ESP_ERR_NOT_FOUND)
{
// pode acontecer se count estiver desfasado; ignora
continue;
}
else
{
ESP_LOGW(TAG, "Failed to load tag %d (%s)", i, esp_err_to_name(err));
}
}
ESP_LOGI(TAG, "Loaded %d tags from storage", tag_count);
}
static void save_tags_to_storage(void)
{
// ler count antigo (para apagar keys antigas se removemos tags)
uint8_t old_count = 0;
esp_err_t err = storage_get_u8_sync(NVS_NAMESPACE, NVS_TAG_COUNT_KEY, &old_count, STORAGE_TO);
if (err == ESP_ERR_NOT_FOUND)
old_count = 0;
// grava count + tags
(void)storage_set_u8_async(NVS_NAMESPACE, NVS_TAG_COUNT_KEY, (uint8_t)tag_count);
for (int i = 0; i < tag_count; i++)
{
char key[16];
snprintf(key, sizeof(key), "%s%d", NVS_TAG_PREFIX, i);
(void)storage_set_str_async(NVS_NAMESPACE, key, valid_tags[i]);
}
// se removemos tags: apagar chaves antigas
if (old_count > (uint8_t)tag_count)
{
for (int i = tag_count; i < (int)old_count && i < MAX_TAGS; i++)
{
char key[16];
snprintf(key, sizeof(key), "%s%d", NVS_TAG_PREFIX, i);
(void)storage_erase_key_async(NVS_NAMESPACE, key);
}
}
// opcional: forçar commit “já”
(void)storage_flush_async();
ESP_LOGD(TAG, "Tags saved to storage (%d tags)", tag_count);
}
/* =========================
* Storage Persistence (mode)
* ========================= */
static void load_mode_from_storage(void)
{
uint8_t u = (uint8_t)AUTH_MODE_OPEN;
esp_err_t err = storage_get_u8_sync(NVS_NAMESPACE, NVS_MODE_KEY, &u, STORAGE_TO);
if (err == ESP_OK)
{
if (u <= (uint8_t)AUTH_MODE_OCPP_RFID)
s_mode = (auth_mode_t)u;
else
s_mode = AUTH_MODE_OPEN;
}
else if (err == ESP_ERR_NOT_FOUND)
{
s_mode = AUTH_MODE_OPEN;
ESP_LOGD(TAG, "No stored mode -> default OPEN");
}
else
{
s_mode = AUTH_MODE_OPEN;
ESP_LOGW(TAG, "Failed to read mode (%s) -> default OPEN", esp_err_to_name(err));
}
ESP_LOGI(TAG, "Loaded mode = %d (%s)", (int)s_mode, auth_mode_to_str(s_mode));
}
static void save_mode_to_storage(auth_mode_t mode)
{
(void)storage_set_u8_async(NVS_NAMESPACE, NVS_MODE_KEY, (uint8_t)mode);
(void)storage_flush_async(); // opcional: commit mais rápido
ESP_LOGD(TAG, "Saved mode = %d (%s)", (int)mode, auth_mode_to_str(mode));
}
/* =========================
* Bridge: EVSE-Link -> AUTH (remote AUTH_GRANTED no slave)
* ========================= */
static void on_remote_auth_grant(void *arg, esp_event_base_t base, int32_t id, void *data)
{
(void)arg;
if (base != EVSE_LINK_EVENTS || id != LINK_EVENT_REMOTE_AUTH_GRANTED || data == NULL)
{
return;
}
const evse_link_auth_grant_event_t *src = (const evse_link_auth_grant_event_t *)data;
// Só faz sentido em SLAVE; em MASTER este evento não deve aparecer
if (evse_link_get_mode() != EVSE_LINK_MODE_SLAVE)
{
return;
}
auth_tag_event_data_t ev = {0};
strncpy(ev.tag, src->tag, AUTH_TAG_MAX_LEN - 1);
ev.tag[AUTH_TAG_MAX_LEN - 1] = '\0';
ev.authorized = true;
ESP_LOGI(TAG, "Remote auth grant on SLAVE for tag=%s", ev.tag);
ESP_LOGD(TAG, "Remote auth grant on SLAVE for tag=%s", ev.tag);
esp_err_t err = esp_event_post(
AUTH_EVENTS,
@@ -241,8 +216,11 @@ static void on_remote_auth_grant(void *arg, esp_event_base_t base, int32_t id, v
* ========================= */
void auth_init(void)
{
load_mode_from_nvs();
load_tags_from_nvs();
// garantir que o storage service está pronto
ESP_ERROR_CHECK(storage_service_init());
load_mode_from_storage();
load_tags_from_storage();
bool need_wiegand = (s_mode == AUTH_MODE_LOCAL_RFID || s_mode == AUTH_MODE_OCPP_RFID);
if (need_wiegand)
@@ -256,7 +234,7 @@ void auth_init(void)
ESP_LOGI(TAG, "Mode OPEN: Wiegand not started");
}
// Registar bridge para autorizações remotas vindas do EVSE-Link
// bridge EVSE-Link -> AUTH
{
esp_err_t err = esp_event_handler_register(
EVSE_LINK_EVENTS,
@@ -285,44 +263,34 @@ void auth_set_mode(auth_mode_t mode)
if (mode == s_mode)
{
ESP_LOGI(TAG, "Mode unchanged: %s", auth_mode_to_str(mode));
ESP_LOGD(TAG, "Mode unchanged: %s", auth_mode_to_str(mode));
return;
}
auth_mode_t old = s_mode;
s_mode = mode;
save_mode_to_nvs(mode);
save_mode_to_storage(mode);
bool need_wiegand = (s_mode == AUTH_MODE_LOCAL_RFID || s_mode == AUTH_MODE_OCPP_RFID);
if (need_wiegand && !s_wiegand_started)
{
ESP_LOGI(TAG, "Mode changed %s -> %s, starting Wiegand",
ESP_LOGD(TAG, "Mode changed %s -> %s, starting Wiegand",
auth_mode_to_str(old), auth_mode_to_str(s_mode));
initWiegand();
s_wiegand_started = true;
}
else if (!need_wiegand && s_wiegand_started)
{
// Aqui poderias implementar um wiegand_deinit() se o driver o expuser.
ESP_LOGI(TAG, "Mode changed %s -> %s, Wiegand remains started (no deinit implemented)",
ESP_LOGD(TAG, "Mode changed %s -> %s, Wiegand remains started (no deinit implemented)",
auth_mode_to_str(old), auth_mode_to_str(s_mode));
}
else
{
ESP_LOGI(TAG, "Mode changed %s -> %s, no change in Wiegand state",
ESP_LOGD(TAG, "Mode changed %s -> %s, no change in Wiegand state",
auth_mode_to_str(old), auth_mode_to_str(s_mode));
}
if (s_mode == AUTH_MODE_OPEN)
{
ESP_LOGI(TAG, "Mode set to OPEN");
}
else
{
ESP_LOGI(TAG, "Mode set to %s", auth_mode_to_str(s_mode));
}
auth_mode_event_data_t evt = {.mode = s_mode};
esp_event_post(AUTH_EVENTS, AUTH_EVENT_MODE_CHANGED, &evt, sizeof(evt), portMAX_DELAY);
}
@@ -338,15 +306,16 @@ bool auth_add_tag(const char *tag)
return false;
if (tag_count >= MAX_TAGS)
return false;
if (is_tag_valid(tag))
return true; // já existe
return true;
strncpy(valid_tags[tag_count], tag, AUTH_TAG_MAX_LEN - 1);
valid_tags[tag_count][AUTH_TAG_MAX_LEN - 1] = '\0';
tag_count++;
save_tags_to_nvs();
ESP_LOGI(TAG, "Tag added: %s", tag);
save_tags_to_storage();
ESP_LOGD(TAG, "Tag added: %s", tag);
return true;
}
@@ -366,8 +335,8 @@ bool auth_remove_tag(const char *tag)
}
tag_count--;
save_tags_to_nvs();
ESP_LOGI(TAG, "Tag removed: %s", tag);
save_tags_to_storage();
ESP_LOGD(TAG, "Tag removed: %s", tag);
return true;
}
}
@@ -383,11 +352,9 @@ bool auth_tag_exists(const char *tag)
void auth_list_tags(void)
{
ESP_LOGI(TAG, "Registered Tags (%d):", tag_count);
ESP_LOGD(TAG, "Registered Tags (%d):", tag_count);
for (int i = 0; i < tag_count; i++)
{
ESP_LOGI(TAG, "- %s", valid_tags[i]);
}
ESP_LOGD(TAG, "- %s", valid_tags[i]);
}
void auth_wait_for_tag_registration(void)
@@ -398,7 +365,7 @@ void auth_wait_for_tag_registration(void)
return;
}
waiting_for_registration = true;
ESP_LOGI(TAG, "Tag registration mode enabled.");
ESP_LOGD(TAG, "Tag registration mode enabled.");
}
void auth_process_tag(const char *tag)
@@ -412,11 +379,8 @@ void auth_process_tag(const char *tag)
switch (s_mode)
{
case AUTH_MODE_OPEN:
{
// Sem verificação; normalmente nem é necessário evento.
ESP_LOGI(TAG, "Mode OPEN: tag=%s (no verification)", tag);
ESP_LOGD(TAG, "Mode OPEN: tag=%s (no verification)", tag);
break;
}
case AUTH_MODE_LOCAL_RFID:
{
@@ -430,9 +394,10 @@ void auth_process_tag(const char *tag)
strncpy(ev.tag, tag, AUTH_TAG_MAX_LEN - 1);
ev.tag[AUTH_TAG_MAX_LEN - 1] = '\0';
ev.authorized = true;
esp_event_post(AUTH_EVENTS, AUTH_EVENT_TAG_SAVED,
&ev, sizeof(ev), portMAX_DELAY);
ESP_LOGI(TAG, "Tag registered: %s", tag);
ESP_LOGD(TAG, "Tag registered: %s", tag);
}
else
{
@@ -446,8 +411,9 @@ void auth_process_tag(const char *tag)
ev.tag[AUTH_TAG_MAX_LEN - 1] = '\0';
ev.authorized = is_tag_valid(tag);
ESP_LOGI(TAG, "LOCAL tag %s: %s", tag,
ESP_LOGD(TAG, "LOCAL tag %s: %s", tag,
ev.authorized ? "AUTHORIZED" : "DENIED");
esp_event_post(AUTH_EVENTS, AUTH_EVENT_TAG_PROCESSED,
&ev, sizeof(ev), portMAX_DELAY);
break;
@@ -455,13 +421,14 @@ void auth_process_tag(const char *tag)
case AUTH_MODE_OCPP_RFID:
{
// Não decide localmente. Pede validação ao OCPP.
auth_tag_verify_event_t rq = {0};
strncpy(rq.tag, tag, AUTH_TAG_MAX_LEN - 1);
rq.tag[AUTH_TAG_MAX_LEN - 1] = '\0';
rq.req_id = s_next_req_id++;
ESP_LOGI(TAG, "OCPP VERIFY requested for tag=%s (req_id=%u)",
ESP_LOGD(TAG, "OCPP VERIFY requested for tag=%s (req_id=%u)",
rq.tag, (unsigned)rq.req_id);
esp_event_post(AUTH_EVENTS, AUTH_EVENT_TAG_VERIFY,
&rq, sizeof(rq), portMAX_DELAY);
break;

View File

@@ -301,7 +301,7 @@ void initWiegand(void)
xTaskCreate(wiegand_task, TAG, configMINIMAL_STACK_SIZE * 4, NULL, 4, NULL);
// Para testes, podes ativar o simulador:
//ESP_LOGI(TAG, "Inicializando Wiegand simulado");
//xTaskCreate(wiegand_sim_task, "WiegandSim",
// ESP_LOGI(TAG, "Inicializando Wiegand simulado");
// xTaskCreate(wiegand_sim_task, "WiegandSim",
// configMINIMAL_STACK_SIZE * 3, NULL, 3, NULL);
}

View File

@@ -8,5 +8,5 @@ idf_component_register(
INCLUDE_DIRS "include"
PRIV_INCLUDE_DIRS "src"
REQUIRES esp_event
PRIV_REQUIRES driver nvs_flash esp_timer evse
PRIV_REQUIRES driver esp_timer evse network
)

View File

@@ -398,7 +398,7 @@ static void network_event_handler(void *handler_args, esp_event_base_t base, int
if (base != NETWORK_EVENTS)
return;
ESP_LOGI(TAG, "Network event id=%d", (int)id);
ESP_LOGD(TAG, "Network event id=%d", (int)id);
buzzer_event_data_t evt = {0};

View File

@@ -2,5 +2,5 @@ set(srcs
"board_config.c")
idf_component_register(SRCS "${srcs}"
PRIV_REQUIRES nvs_flash
PRIV_REQUIRES
INCLUDE_DIRS "include")

View File

@@ -17,6 +17,6 @@ set(srcs
idf_component_register(
SRCS ${srcs}
INCLUDE_DIRS "include"
PRIV_REQUIRES nvs_flash driver
REQUIRES peripherals auth loadbalancer scheduler
PRIV_REQUIRES driver
REQUIRES peripherals auth loadbalancer scheduler storage_service
)

View File

@@ -1,215 +1,289 @@
#include <inttypes.h> // For PRI macros
#include <inttypes.h>
#include <stdbool.h>
#include "freertos/FreeRTOS.h"
#include "evse_config.h"
#include "board_config.h"
#include "evse_limits.h"
#include "evse_api.h"
#include "evse_state.h"
#include "esp_log.h"
#include "nvs.h"
#include "esp_timer.h"
#include "esp_err.h"
#include "esp_check.h"
#include "storage_service.h"
static const char *TAG = "evse_config";
static nvs_handle_t nvs;
#define NVS_NAMESPACE "evse_config"
// ========================
// Configurable parameters
// 3 variáveis (semântica simples)
// ========================
static uint8_t max_charging_current = MAX_CHARGING_CURRENT_LIMIT;
static uint16_t charging_current; // Persisted (NVS)
static uint16_t charging_current_runtime = 0; // Runtime only
static bool socket_outlet;
static bool rcm;
// 1) Hardware (FIXO)
static const uint8_t max_charging_current = MAX_CHARGING_CURRENT_LIMIT;
// 2) Configurável (persistido)
static uint16_t charging_current = MAX_CHARGING_CURRENT_LIMIT;
// 3) Runtime (RAM)
static uint16_t charging_current_runtime = 0;
// Outros parâmetros (persistidos)
static bool socket_outlet = false;
static bool rcm = false;
static uint8_t temp_threshold = 60;
static bool require_auth;
// Availability / Enable flags
// Availability / Enable flags (persistidos)
static bool is_available = true;
static bool is_enabled = true;
static inline TickType_t TO_TICKS_MS(uint32_t ms) { return pdMS_TO_TICKS(ms); }
// Ajusta conforme o teu boot:
// 1000ms pode ser curto com Wi-Fi/FS/tasks; 2000ms é mais robusto em produto.
static inline TickType_t BOOT_TO(void) { return TO_TICKS_MS(2000); }
// ========================
// Initialization
// ========================
esp_err_t evse_config_init(void)
{
ESP_LOGD(TAG, "Initializing NVS configuration...");
return nvs_open("evse", NVS_READWRITE, &nvs);
// garante storage iniciado
ESP_RETURN_ON_ERROR(storage_service_init(), TAG, "storage init failed");
ESP_LOGI(TAG, "EVSE config init OK (storage-backed)");
return ESP_OK;
}
void evse_check_defaults(void)
{
esp_err_t err;
uint8_t u8;
uint16_t u16;
uint32_t u32;
bool needs_commit = false;
uint8_t u8_bool;
uint8_t u8 = 0;
uint16_t u16 = 0;
ESP_LOGD(TAG, "Checking default parameters...");
// Timeouts: leitura e escrita no boot
const TickType_t rd_to = BOOT_TO();
const TickType_t wr_to = TO_TICKS_MS(2000);
// Max charging current
err = nvs_get_u8(nvs, "max_chrg_curr", &u8);
if (err != ESP_OK || u8 < MIN_CHARGING_CURRENT_LIMIT || u8 > MAX_CHARGING_CURRENT_LIMIT)
{
max_charging_current = MAX_CHARGING_CURRENT_LIMIT;
nvs_set_u8(nvs, "max_chrg_curr", max_charging_current);
needs_commit = true;
ESP_LOGW(TAG, "Invalid or missing max_chrg_curr, resetting to %d", max_charging_current);
}
else
{
max_charging_current = u8;
}
ESP_LOGD(TAG, "Checking default parameters (sync persistence)...");
// -----------------------------------------
// Charging current (default, persisted)
err = nvs_get_u16(nvs, "def_chrg_curr", &u16);
if (err != ESP_OK || u16 < (MIN_CHARGING_CURRENT_LIMIT) || u16 > (max_charging_current))
// -----------------------------------------
err = storage_get_u16_sync(NVS_NAMESPACE, "def_chrg_curr", &u16, rd_to);
if (err != ESP_OK || u16 < MIN_CHARGING_CURRENT_LIMIT || u16 > max_charging_current)
{
charging_current = max_charging_current;
nvs_set_u16(nvs, "def_chrg_curr", charging_current);
needs_commit = true;
ESP_LOGW(TAG, "Invalid or missing def_chrg_curr, resetting to %d", charging_current);
esp_err_t se = storage_set_u16_sync(NVS_NAMESPACE, "def_chrg_curr", charging_current, wr_to);
if (se != ESP_OK)
{
ESP_LOGE(TAG, "Failed to persist def_chrg_curr=%u: %s",
(unsigned)charging_current, esp_err_to_name(se));
// seguimos com RAM correta; persist pode falhar por flash/partição
}
ESP_LOGW(TAG, "Invalid/missing def_chrg_curr (%s) -> reset to %u (sync persisted)",
esp_err_to_name(err), (unsigned)charging_current);
}
else
{
charging_current = u16;
}
// Runtime charging current inicializado a partir do default persistido
// runtime inicializa a partir do default
charging_current_runtime = charging_current;
ESP_LOGD(TAG, "Runtime charging current initialized to: %d", charging_current_runtime);
ESP_LOGD(TAG, "Runtime charging current initialized from default: %u",
(unsigned)charging_current_runtime);
// Auth required
err = nvs_get_u8(nvs, "require_auth", &u8);
require_auth = (err == ESP_OK && u8 <= 1) ? u8 : false;
if (err != ESP_OK)
// -----------------------------------------
// Socket outlet (persisted) + capability gate
// -----------------------------------------
err = storage_get_u8_sync(NVS_NAMESPACE, "socket_outlet", &u8, rd_to);
if (err == ESP_OK && u8 <= 1)
{
nvs_set_u8(nvs, "require_auth", require_auth);
needs_commit = true;
bool wanted = (u8 != 0);
if (wanted && !board_config.proximity)
{
// NVS dizia 1, mas HW não suporta -> runtime false e persistir 0
socket_outlet = false;
esp_err_t se = storage_set_u8_sync(NVS_NAMESPACE, "socket_outlet", 0, wr_to);
if (se != ESP_OK)
{
ESP_LOGE(TAG, "Failed to persist socket_outlet=0 (capability mismatch): %s",
esp_err_to_name(se));
}
// Socket outlet
err = nvs_get_u8(nvs, "socket_outlet", &u8);
socket_outlet = (err == ESP_OK && u8) && board_config.proximity;
if (err != ESP_OK)
{
nvs_set_u8(nvs, "socket_outlet", socket_outlet);
needs_commit = true;
}
// RCM
err = nvs_get_u8(nvs, "rcm", &u8);
rcm = (err == ESP_OK && u8) && board_config.rcm;
if (err != ESP_OK)
{
nvs_set_u8(nvs, "rcm", rcm);
needs_commit = true;
}
// Temp threshold
err = nvs_get_u8(nvs, "temp_threshold", &u8);
temp_threshold = (err == ESP_OK && u8 >= 40 && u8 <= 80) ? u8 : 60;
if (err != ESP_OK)
{
nvs_set_u8(nvs, "temp_threshold", temp_threshold);
needs_commit = true;
}
// Optional limits
if (nvs_get_u32(nvs, "def_cons_lim", &u32) == ESP_OK)
evse_set_consumption_limit(u32);
if (nvs_get_u32(nvs, "def_ch_time_lim", &u32) == ESP_OK)
evse_set_charging_time_limit(u32);
if (nvs_get_u16(nvs, "def_un_pwr_lim", &u16) == ESP_OK)
evse_set_under_power_limit(u16);
// Availability (persist)
if (nvs_get_u8(nvs, "available", &u8_bool) == ESP_OK && u8_bool <= 1)
{
is_available = (u8_bool != 0);
ESP_LOGW(TAG, "socket_outlet requested but HW has no proximity -> forcing false (sync persisted)");
}
else
{
is_available = true; // default
nvs_set_u8(nvs, "available", (uint8_t)is_available);
needs_commit = true;
ESP_LOGW(TAG, "Missing 'available' -> default=true (persisted).");
socket_outlet = wanted;
}
// Enabled (persist)
if (nvs_get_u8(nvs, "enabled", &u8_bool) == ESP_OK && u8_bool <= 1)
{
is_enabled = (u8_bool != 0);
}
else
{
is_enabled = true; // default
nvs_set_u8(nvs, "enabled", (uint8_t)is_enabled);
needs_commit = true;
ESP_LOGW(TAG, "Missing 'enabled' -> default=true (persisted).");
socket_outlet = false;
esp_err_t se = storage_set_u8_sync(NVS_NAMESPACE, "socket_outlet", 0, wr_to);
if (se != ESP_OK)
{
ESP_LOGE(TAG, "Failed to persist socket_outlet default=0: %s", esp_err_to_name(se));
}
if (needs_commit)
ESP_LOGW(TAG, "Missing/invalid socket_outlet (%s) -> default=false (sync persisted).",
esp_err_to_name(err));
}
// -----------------------------------------
// RCM (persisted) + capability gate
// -----------------------------------------
err = storage_get_u8_sync(NVS_NAMESPACE, "rcm", &u8, rd_to);
if (err == ESP_OK && u8 <= 1)
{
err = nvs_commit(nvs);
if (err == ESP_OK)
bool wanted = (u8 != 0);
if (wanted && !board_config.rcm)
{
ESP_LOGD(TAG, "Configuration committed to NVS.");
rcm = false;
esp_err_t se = storage_set_u8_sync(NVS_NAMESPACE, "rcm", 0, wr_to);
if (se != ESP_OK)
{
ESP_LOGE(TAG, "Failed to persist rcm=0 (capability mismatch): %s",
esp_err_to_name(se));
}
ESP_LOGW(TAG, "rcm requested but HW has no RCM -> forcing false (sync persisted)");
}
else
{
ESP_LOGE(TAG, "Failed to commit configuration to NVS: %s", esp_err_to_name(err));
rcm = wanted;
}
}
else
{
rcm = false;
esp_err_t se = storage_set_u8_sync(NVS_NAMESPACE, "rcm", 0, wr_to);
if (se != ESP_OK)
{
ESP_LOGE(TAG, "Failed to persist rcm default=0: %s", esp_err_to_name(se));
}
ESP_LOGW(TAG, "Missing/invalid rcm (%s) -> default=false (sync persisted).",
esp_err_to_name(err));
}
// -----------------------------------------
// Temp threshold (persisted)
// -----------------------------------------
err = storage_get_u8_sync(NVS_NAMESPACE, "temp_threshold", &u8, rd_to);
if (err == ESP_OK && u8 >= 40 && u8 <= 80)
{
temp_threshold = u8;
}
else
{
temp_threshold = 60;
esp_err_t se = storage_set_u8_sync(NVS_NAMESPACE, "temp_threshold", temp_threshold, wr_to);
if (se != ESP_OK)
{
ESP_LOGE(TAG, "Failed to persist temp_threshold=%u: %s",
(unsigned)temp_threshold, esp_err_to_name(se));
}
ESP_LOGW(TAG, "Invalid/missing temp_threshold (%s) -> default=60 (sync persisted).",
esp_err_to_name(err));
}
// -----------------------------------------
// Availability (persisted) [0/1]
// -----------------------------------------
err = storage_get_u8_sync(NVS_NAMESPACE, "available", &u8, rd_to);
if (err == ESP_OK && u8 <= 1)
{
is_available = (u8 != 0);
}
else
{
is_available = true;
esp_err_t se = storage_set_u8_sync(NVS_NAMESPACE, "available", 1, wr_to);
if (se != ESP_OK)
{
ESP_LOGE(TAG, "Failed to persist available=1: %s", esp_err_to_name(se));
}
ESP_LOGW(TAG, "Missing/invalid 'available' (%s) -> default=true (sync persisted).",
esp_err_to_name(err));
}
// -----------------------------------------
// Enabled (persisted) [0/1]
// -----------------------------------------
err = storage_get_u8_sync(NVS_NAMESPACE, "enabled", &u8, rd_to);
if (err == ESP_OK && u8 <= 1)
{
is_enabled = (u8 != 0);
}
else
{
is_enabled = true;
esp_err_t se = storage_set_u8_sync(NVS_NAMESPACE, "enabled", 1, wr_to);
if (se != ESP_OK)
{
ESP_LOGE(TAG, "Failed to persist enabled=1: %s", esp_err_to_name(se));
}
ESP_LOGW(TAG, "Missing/invalid 'enabled' (%s) -> default=true (sync persisted).",
esp_err_to_name(err));
}
// Flush explícito no boot:
// - ajuda a garantir commit determinístico antes do resto do sistema avançar
// - mantém-se útil mesmo com setters sync se o teu storage ainda estiver com debounce interno
esp_err_t fe = storage_flush_sync(wr_to);
if (fe != ESP_OK)
ESP_LOGE(TAG, "storage_flush_sync failed: %s", esp_err_to_name(fe));
}
// ========================
// Charging current getters/setters
// ========================
uint8_t evse_get_max_charging_current(void)
{
return max_charging_current;
}
uint8_t evse_get_max_charging_current(void) { return max_charging_current; }
esp_err_t evse_set_max_charging_current(uint8_t value)
{
if (value < MIN_CHARGING_CURRENT_LIMIT || value > MAX_CHARGING_CURRENT_LIMIT)
return ESP_ERR_INVALID_ARG;
max_charging_current = value;
evse_set_runtime_charging_current(value);
nvs_set_u8(nvs, "max_chrg_curr", value);
return nvs_commit(nvs);
}
uint16_t evse_get_charging_current(void)
{
return charging_current;
}
uint16_t evse_get_charging_current(void) { return charging_current; }
esp_err_t evse_set_charging_current(uint16_t value)
{
if (value < (MIN_CHARGING_CURRENT_LIMIT) || value > (max_charging_current))
if (value < MIN_CHARGING_CURRENT_LIMIT || value > max_charging_current)
return ESP_ERR_INVALID_ARG;
if (value == charging_current)
{
evse_set_runtime_charging_current(value);
return ESP_OK;
}
charging_current = value;
nvs_set_u16(nvs, "def_chrg_curr", value);
return nvs_commit(nvs);
}
uint16_t evse_get_default_charging_current(void)
{
uint16_t value;
if (nvs_get_u16(nvs, "def_chrg_curr", &value) == ESP_OK)
return value;
return charging_current;
}
esp_err_t err = storage_set_u16_async(NVS_NAMESPACE, "def_chrg_curr", value);
if (err != ESP_OK)
{
// Em runtime, isto pode falhar por fila cheia. RAM fica correta; persistência é best-effort.
ESP_LOGE(TAG, "Failed to persist def_chrg_curr async=%u: %s", (unsigned)value, esp_err_to_name(err));
return err;
}
esp_err_t evse_set_default_charging_current(uint16_t value)
{
if (value < (MIN_CHARGING_CURRENT_LIMIT) || value > (max_charging_current))
return ESP_ERR_INVALID_ARG;
nvs_set_u16(nvs, "def_chrg_curr", value);
return nvs_commit(nvs);
evse_set_runtime_charging_current(value);
return ESP_OK;
}
// ========================
@@ -218,135 +292,119 @@ esp_err_t evse_set_default_charging_current(uint16_t value)
void evse_set_runtime_charging_current(uint16_t value)
{
if (value > max_charging_current)
{
value = max_charging_current;
}
else if (value < MIN_CHARGING_CURRENT_LIMIT)
{
value = MIN_CHARGING_CURRENT_LIMIT;
}
charging_current_runtime = value;
ESP_LOGI(TAG, "Runtime charging current updated: %d", charging_current_runtime);
evse_config_event_data_t evt = {
.charging = evse_state_is_charging(evse_get_state()),
.hw_max_current = (float)evse_get_max_charging_current(),
.runtime_current = (float)evse_get_runtime_charging_current(),
.timestamp_us = esp_timer_get_time()};
esp_event_post(EVSE_EVENTS,
EVSE_EVENT_CONFIG_UPDATED,
&evt,
sizeof(evt),
portMAX_DELAY);
}
uint16_t evse_get_runtime_charging_current(void)
{
return charging_current_runtime;
}
uint16_t evse_get_runtime_charging_current(void) { return charging_current_runtime; }
// ========================
// Socket outlet
// ========================
bool evse_get_socket_outlet(void)
{
return socket_outlet;
}
bool evse_get_socket_outlet(void) { return socket_outlet; }
esp_err_t evse_set_socket_outlet(bool value)
{
if (value && !board_config.proximity)
return ESP_ERR_INVALID_ARG;
if (value == socket_outlet)
return ESP_OK;
socket_outlet = value;
nvs_set_u8(nvs, "socket_outlet", value);
return nvs_commit(nvs);
esp_err_t err = storage_set_u8_async(NVS_NAMESPACE, "socket_outlet", (uint8_t)value);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to persist socket_outlet async=%u: %s", (unsigned)value, esp_err_to_name(err));
return err;
}
return ESP_OK;
}
// ========================
// RCM
// ========================
bool evse_is_rcm(void)
{
return rcm;
}
bool evse_is_rcm(void) { return rcm; }
esp_err_t evse_set_rcm(bool value)
{
if (value && !board_config.rcm)
return ESP_ERR_INVALID_ARG;
if (value == rcm)
return ESP_OK;
rcm = value;
nvs_set_u8(nvs, "rcm", value);
return nvs_commit(nvs);
esp_err_t err = storage_set_u8_async(NVS_NAMESPACE, "rcm", (uint8_t)value);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to persist rcm async=%u: %s", (unsigned)value, esp_err_to_name(err));
return err;
}
return ESP_OK;
}
// ========================
// Temperature
// ========================
uint8_t evse_get_temp_threshold(void)
{
return temp_threshold;
}
uint8_t evse_get_temp_threshold(void) { return temp_threshold; }
esp_err_t evse_set_temp_threshold(uint8_t value)
{
if (value < 40 || value > 80)
return ESP_ERR_INVALID_ARG;
if (value == temp_threshold)
return ESP_OK;
temp_threshold = value;
nvs_set_u8(nvs, "temp_threshold", value);
return nvs_commit(nvs);
esp_err_t err = storage_set_u8_async(NVS_NAMESPACE, "temp_threshold", value);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to persist temp_threshold async=%u: %s", (unsigned)value, esp_err_to_name(err));
return err;
}
return ESP_OK;
}
// ========================
// Availability
// ========================
bool evse_config_is_available(void)
{
return is_available;
}
bool evse_config_is_available(void) { return is_available; }
void evse_config_set_available(bool available)
{
is_available = available ? true : false;
bool newv = available;
if (newv == is_available)
return;
esp_err_t err = nvs_set_u8(nvs, "available", (uint8_t)is_available);
if (err == ESP_OK)
err = nvs_commit(nvs);
is_available = newv;
esp_err_t err = storage_set_u8_async(NVS_NAMESPACE, "available", (uint8_t)is_available);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to persist 'available': %s", esp_err_to_name(err));
}
evse_available_event_data_t e = {
.available = is_available,
.timestamp_us = esp_timer_get_time()};
esp_event_post(EVSE_EVENTS, EVSE_EVENT_AVAILABLE_UPDATED, &e, sizeof(e), portMAX_DELAY);
ESP_LOGE(TAG, "Failed to persist 'available' async=%u: %s", (unsigned)is_available, esp_err_to_name(err));
}
// ========================
// Enable/Disable
// ========================
bool evse_config_is_enabled(void)
{
return is_enabled;
}
bool evse_config_is_enabled(void) { return is_enabled; }
void evse_config_set_enabled(bool enabled)
{
is_enabled = enabled ? true : false;
bool newv = enabled;
if (newv == is_enabled)
return;
esp_err_t err = nvs_set_u8(nvs, "enabled", (uint8_t)is_enabled);
if (err == ESP_OK)
err = nvs_commit(nvs);
is_enabled = newv;
esp_err_t err = storage_set_u8_async(NVS_NAMESPACE, "enabled", (uint8_t)is_enabled);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to persist 'enabled': %s", esp_err_to_name(err));
}
evse_enable_event_data_t e = {
.enabled = is_enabled,
.timestamp_us = esp_timer_get_time()};
esp_event_post(EVSE_EVENTS, EVSE_EVENT_ENABLE_UPDATED, &e, sizeof(e), portMAX_DELAY);
ESP_LOGE(TAG, "Failed to persist 'enabled' async=%u: %s", (unsigned)is_enabled, esp_err_to_name(err));
}

View File

@@ -1,3 +1,4 @@
// components/evse/evse_core.c
#include "evse_fsm.h"
#include "evse_error.h"
#include "evse_limits.h"
@@ -14,6 +15,36 @@ static const char *TAG = "evse_core";
static SemaphoreHandle_t mutex;
static evse_state_t last_state = EVSE_STATE_A;
// Filtro simples de histerese no pilot
#define PILOT_STABLE_SAMPLES 2
static pilot_voltage_t s_last_raw = PILOT_VOLTAGE_12;
static pilot_voltage_t s_filtered = PILOT_VOLTAGE_12;
static int s_stable_count = 0;
static pilot_voltage_t filter_pilot_voltage(pilot_voltage_t raw)
{
if (raw == s_last_raw)
{
if (s_stable_count < PILOT_STABLE_SAMPLES)
{
s_stable_count++;
}
}
else
{
s_last_raw = raw;
s_stable_count = 1;
}
if (s_stable_count >= PILOT_STABLE_SAMPLES && raw != s_filtered)
{
s_filtered = raw;
}
return s_filtered;
}
static void evse_process(void);
static void evse_core_task(void *arg);
@@ -32,7 +63,8 @@ void evse_init(void)
evse_fsm_reset();
pilot_set_level(true);
xTaskCreate(evse_core_task, "evse_core_task", 4096, NULL, 5, NULL);
BaseType_t rc = xTaskCreate(evse_core_task, "evse_core_task", 4096, NULL, 6, NULL);
configASSERT(rc == pdPASS);
}
static void evse_process(void)
@@ -44,19 +76,27 @@ static void evse_process(void)
xSemaphoreTake(mutex, portMAX_DELAY);
pilot_voltage_t pilot_voltage;
pilot_voltage_t pilot_raw;
bool is_n12v = false;
pilot_measure(&pilot_voltage, &is_n12v);
ESP_LOGD(TAG, "Pilot: %d, -12V: %s", pilot_voltage, is_n12v ? "yes" : "no");
pilot_measure(&pilot_raw, &is_n12v);
pilot_voltage_t pilot_voltage = filter_pilot_voltage(pilot_raw);
ESP_LOGD(TAG, "Pilot(raw=%d, filt=%d), -12V: %s",
pilot_raw, pilot_voltage, is_n12v ? "yes" : "no");
// raw set/clear; erro visível mantém holdoff interno (60s após sumir)
evse_error_check(pilot_voltage, is_n12v);
// ✅ Sem cooldown externo: disponibilidade depende só do erro "visível"
bool available = evse_config_is_available() && (evse_get_error() == 0);
bool enabled = evse_config_is_enabled();
evse_fsm_process(
pilot_voltage,
evse_state_get_authorized(),
evse_config_is_available(),
evse_config_is_enabled());
available,
enabled);
evse_limits_check();

View File

@@ -1,5 +1,4 @@
#include "evse_error.h"
#include "evse_config.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
@@ -7,151 +6,147 @@
#include "esp_log.h"
#include "ntc_sensor.h"
#include "esp_event.h"
#include "esp_timer.h"
#include "evse_events.h"
#include "evse_config.h"
static const char *TAG = "evse_error";
// Estado global de erros
static uint32_t error_bits = 0;
static TickType_t auto_clear_timeout = 0;
// ----------------------------------------------------
// Estado interno
// ----------------------------------------------------
// raw_bits = erros “instantâneos” conforme checks (set/clear)
// visible_bits = erros expostos ao resto do sistema (com holdoff)
// clear_deadline = quando pode finalmente limpar visible_bits para 0
static uint32_t raw_bits = 0;
static uint32_t visible_bits = 0;
static TickType_t clear_deadline = 0;
// Sticky flag: "todos erros foram limpos"
// Sticky flag: "todos erros visíveis foram limpos"
static bool error_cleared = false;
// Proteção contra concorrência
static portMUX_TYPE error_mux = portMUX_INITIALIZER_UNLOCKED;
void evse_error_init(void)
// ----------------------------------------------------
// Helper: publicar evento de alteração de erro (visible_bits)
// ----------------------------------------------------
static void evse_error_post_event(uint32_t new_bits, uint32_t changed_mask)
{
portENTER_CRITICAL(&error_mux);
error_bits = 0;
auto_clear_timeout = 0;
error_cleared = false;
portEXIT_CRITICAL(&error_mux);
}
evse_error_event_data_t ev = {
.error_bits = new_bits,
.changed_mask = changed_mask,
.timestamp_us = esp_timer_get_time(),
};
void evse_error_check(pilot_voltage_t pilot_voltage, bool is_n12v)
{
ESP_LOGD(TAG, "Verificando erro: pilot_voltage=%d, is_n12v=%s",
pilot_voltage, is_n12v ? "true" : "false");
esp_err_t err = esp_event_post(
EVSE_EVENTS,
EVSE_EVENT_ERROR_CHANGED,
&ev,
sizeof(ev),
portMAX_DELAY);
// 1) Falha elétrica geral no pilot
if (pilot_voltage == PILOT_VOLTAGE_1)
if (err != ESP_OK)
{
bool first_time = false;
portENTER_CRITICAL(&error_mux);
if (!(error_bits & EVSE_ERR_PILOT_FAULT_BIT))
{
error_cleared = false;
error_bits |= EVSE_ERR_PILOT_FAULT_BIT;
first_time = true;
}
portEXIT_CRITICAL(&error_mux);
if (first_time)
{
ESP_LOGW(TAG, "Erro: pilot abaixo de 2V (falha)");
}
}
else
{
// Pilot voltou a nível válido → limpa erro de pilot fault
evse_error_clear(EVSE_ERR_PILOT_FAULT_BIT);
}
// 2) Falta de -12V durante PWM (C ou D)
if ((pilot_voltage == PILOT_VOLTAGE_6 || pilot_voltage == PILOT_VOLTAGE_3) && !is_n12v)
{
bool first_time = false;
portENTER_CRITICAL(&error_mux);
if (!(error_bits & EVSE_ERR_DIODE_SHORT_BIT))
{
error_cleared = false;
error_bits |= EVSE_ERR_DIODE_SHORT_BIT;
auto_clear_timeout = xTaskGetTickCount() + pdMS_TO_TICKS(60000);
first_time = true;
}
portEXIT_CRITICAL(&error_mux);
if (first_time)
{
ESP_LOGW(TAG, "Erro: ausência de -12V no PWM (sem diodo)");
}
}
else
{
// Se já não estamos em C/D sem -12V, limpa o erro de diodo curto
evse_error_clear(EVSE_ERR_DIODE_SHORT_BIT);
ESP_LOGW(TAG, "Falha ao publicar EVSE_EVENT_ERROR_CHANGED: %s",
esp_err_to_name(err));
}
}
void evse_temperature_check(void)
// ----------------------------------------------------
// Helpers internos
// ----------------------------------------------------
static bool raw_has_bit(uint32_t bit)
{
float temp_c = ntc_temp_sensor();
uint8_t threshold = evse_get_temp_threshold();
ESP_LOGD(TAG, "Verificando temperatura: atual=%.2f °C, limite=%d °C",
temp_c, threshold);
// Temperatura inválida -> erro de sensor
if (temp_c < -40.0f || temp_c > 150.0f)
{
bool first_time = false;
bool v;
portENTER_CRITICAL(&error_mux);
if (!(error_bits & EVSE_ERR_TEMPERATURE_FAULT_BIT))
{
error_cleared = false;
error_bits |= EVSE_ERR_TEMPERATURE_FAULT_BIT;
first_time = true;
}
v = ((raw_bits & bit) != 0);
portEXIT_CRITICAL(&error_mux);
return v;
}
if (first_time)
static void reconcile_visible_locked(TickType_t now)
{
// Se existem erros reais, o visível segue imediatamente
if (raw_bits != 0)
{
ESP_LOGW(TAG, "Sensor NTC falhou ou está desconectado");
}
visible_bits = raw_bits;
clear_deadline = 0;
error_cleared = false;
return;
}
// Leitura válida -> limpa erro de sensor
evse_error_clear(EVSE_ERR_TEMPERATURE_FAULT_BIT);
// Temperatura máxima
if (temp_c >= threshold)
// raw_bits == 0
if (visible_bits == 0)
{
bool first_time = false;
clear_deadline = 0;
return;
}
// Ainda há erro visível (holdoff). Arma deadline 1x.
if (clear_deadline == 0)
{
clear_deadline = now + pdMS_TO_TICKS(EVSE_ERROR_COOLDOWN_MS);
return;
}
// Expirou -> limpar finalmente
if ((int32_t)(now - clear_deadline) >= 0)
{
visible_bits = 0;
clear_deadline = 0;
error_cleared = true;
}
}
// ----------------------------------------------------
// API pública
// ----------------------------------------------------
void evse_error_init(void)
{
uint32_t old_vis, new_vis, changed;
bool post = false;
portENTER_CRITICAL(&error_mux);
if (!(error_bits & EVSE_ERR_TEMPERATURE_HIGH_BIT))
{
old_vis = visible_bits;
raw_bits = 0;
visible_bits = 0;
clear_deadline = 0;
error_cleared = false;
error_bits |= EVSE_ERR_TEMPERATURE_HIGH_BIT;
auto_clear_timeout = xTaskGetTickCount() + pdMS_TO_TICKS(60000);
first_time = true;
}
new_vis = visible_bits;
changed = old_vis ^ new_vis;
post = (changed != 0);
portEXIT_CRITICAL(&error_mux);
if (first_time)
if (post)
{
ESP_LOGW(TAG, "Temperatura acima do limite: %.2f °C ≥ %d °C",
temp_c, threshold);
}
}
else
{
evse_error_clear(EVSE_ERR_TEMPERATURE_HIGH_BIT);
evse_error_post_event(new_vis, changed);
}
}
uint32_t evse_get_error(void)
{
portENTER_CRITICAL(&error_mux);
uint32_t val = error_bits;
uint32_t val = visible_bits;
portEXIT_CRITICAL(&error_mux);
return val;
}
bool evse_error_is_active(void)
{
return evse_get_error() != 0;
}
uint32_t evse_error_get_bits(void)
{
return evse_get_error();
}
bool evse_error_cleared_flag(void)
{
portENTER_CRITICAL(&error_mux);
@@ -169,61 +164,147 @@ void evse_error_reset_flag(void)
void evse_error_set(uint32_t bitmask)
{
uint32_t old_vis, new_vis, changed;
TickType_t now = xTaskGetTickCount();
portENTER_CRITICAL(&error_mux);
error_cleared = false;
error_bits |= bitmask;
old_vis = visible_bits;
if (bitmask & EVSE_ERR_AUTO_CLEAR_BITS)
{
auto_clear_timeout = xTaskGetTickCount() + pdMS_TO_TICKS(60000); // 60s
}
raw_bits |= bitmask;
// se aparece qualquer erro, o "cleared" deixa de ser verdade
error_cleared = false;
reconcile_visible_locked(now);
new_vis = visible_bits;
changed = old_vis ^ new_vis;
portEXIT_CRITICAL(&error_mux);
if (changed != 0)
{
evse_error_post_event(new_vis, changed);
}
}
void evse_error_clear(uint32_t bitmask)
{
uint32_t old_vis, new_vis, changed;
TickType_t now = xTaskGetTickCount();
portENTER_CRITICAL(&error_mux);
bool had_error = (error_bits != 0);
error_bits &= ~bitmask;
old_vis = visible_bits;
if (had_error && error_bits == 0)
{
error_cleared = true;
}
raw_bits &= ~bitmask;
// ✅ Aqui é onde o “60s depois do erro desaparecer” é armado:
// quando raw_bits chega a 0, reconcile arma clear_deadline (uma vez)
reconcile_visible_locked(now);
new_vis = visible_bits;
changed = old_vis ^ new_vis;
portEXIT_CRITICAL(&error_mux);
if (changed != 0)
{
evse_error_post_event(new_vis, changed);
}
}
void evse_error_tick(void)
{
uint32_t old_vis, new_vis, changed;
TickType_t now = xTaskGetTickCount();
portENTER_CRITICAL(&error_mux);
if ((error_bits & EVSE_ERR_AUTO_CLEAR_BITS) &&
auto_clear_timeout != 0 &&
xTaskGetTickCount() >= auto_clear_timeout)
{
error_bits &= ~EVSE_ERR_AUTO_CLEAR_BITS;
if (error_bits == 0)
{
error_cleared = true;
}
auto_clear_timeout = 0;
}
old_vis = visible_bits;
reconcile_visible_locked(now);
new_vis = visible_bits;
changed = old_vis ^ new_vis;
portEXIT_CRITICAL(&error_mux);
if (changed != 0)
{
evse_error_post_event(new_vis, changed);
}
}
bool evse_error_is_active(void)
// ----------------------------------------------------
// Checks (raw -> set/clear)
// ----------------------------------------------------
void evse_error_check(pilot_voltage_t pilot_voltage, bool is_n12v)
{
return evse_get_error() != 0;
ESP_LOGD(TAG, "Verificando erro: pilot_voltage=%d, is_n12v=%s",
pilot_voltage, is_n12v ? "true" : "false");
// 1) Falha elétrica geral no pilot
if (pilot_voltage == PILOT_VOLTAGE_1)
{
if (!raw_has_bit(EVSE_ERR_PILOT_FAULT_BIT))
{
ESP_LOGW(TAG, "Erro: pilot abaixo de 2V (falha)");
}
evse_error_set(EVSE_ERR_PILOT_FAULT_BIT);
}
else
{
evse_error_clear(EVSE_ERR_PILOT_FAULT_BIT);
}
// 2) Falta de -12V durante PWM (C ou D)
if ((pilot_voltage == PILOT_VOLTAGE_6 || pilot_voltage == PILOT_VOLTAGE_3) && !is_n12v)
{
if (!raw_has_bit(EVSE_ERR_DIODE_SHORT_BIT))
{
ESP_LOGW(TAG, "Erro: ausência de -12V no PWM (sem diodo)");
}
evse_error_set(EVSE_ERR_DIODE_SHORT_BIT);
}
else
{
evse_error_clear(EVSE_ERR_DIODE_SHORT_BIT);
}
}
uint32_t evse_error_get_bits(void)
void evse_temperature_check(void)
{
return evse_get_error();
float temp_c = ntc_temp_sensor();
uint8_t threshold = evse_get_temp_threshold();
ESP_LOGD(TAG, "Verificando temperatura: atual=%.2f °C, limite=%d °C",
temp_c, threshold);
// Temperatura inválida -> erro de sensor
if (temp_c < -40.0f || temp_c > 150.0f)
{
if (!raw_has_bit(EVSE_ERR_TEMPERATURE_FAULT_BIT))
{
ESP_LOGW(TAG, "Sensor NTC falhou ou está desconectado");
}
evse_error_set(EVSE_ERR_TEMPERATURE_FAULT_BIT);
return;
}
// Leitura válida -> limpa erro de sensor
evse_error_clear(EVSE_ERR_TEMPERATURE_FAULT_BIT);
// Temperatura máxima
if (temp_c >= threshold)
{
if (!raw_has_bit(EVSE_ERR_TEMPERATURE_HIGH_BIT))
{
ESP_LOGW(TAG, "Temperatura acima do limite: %.2f °C ≥ %d °C",
temp_c, threshold);
}
evse_error_set(EVSE_ERR_TEMPERATURE_HIGH_BIT);
}
else
{
evse_error_clear(EVSE_ERR_TEMPERATURE_HIGH_BIT);
}
}

View File

@@ -1,3 +1,4 @@
// components/evse/evse_fsm.c
#include "evse_fsm.h"
#include "evse_api.h"
#include "evse_pilot.h"
@@ -17,16 +18,14 @@ static const char *TAG = "evse_fsm";
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#endif
static bool c1_d1_waiting = false;
static TickType_t c1_d1_relay_to = 0;
void evse_fsm_reset(void)
{
evse_set_state(EVSE_STATE_A);
c1_d1_waiting = false;
c1_d1_relay_to = 0;
}
/**
* @brief Atualiza saídas de hardware (pilot, relé, trava) em função do estado lógico.
*/
static void update_outputs(evse_state_t state)
{
const uint16_t current = evse_get_runtime_charging_current();
@@ -38,7 +37,7 @@ static void update_outputs(evse_state_t state)
cable_max_current = proximity_get_max_current();
}
// Segurança: relé sempre off e outputs seguros em caso de erro
// Segurança total: qualquer erro ativo força saída segura
if (evse_get_error() != 0)
{
if (ac_relay_get_state())
@@ -46,8 +45,14 @@ static void update_outputs(evse_state_t state)
ac_relay_set_state(false);
ESP_LOGW(TAG, "ERRO ativo: relé estava ligado, agora desligado por segurança!");
}
else
{
ac_relay_set_state(false);
}
// Em erro, garantir pilot OFF (não PWM / não +12V)
pilot_set_level(true);
if (board_config.socket_lock && socket_outlet)
{
socket_lock_set_locked(false);
@@ -55,14 +60,16 @@ static void update_outputs(evse_state_t state)
return;
}
// Fluxo normal
switch (state)
{
case EVSE_STATE_A:
case EVSE_STATE_E:
case EVSE_STATE_F:
ac_relay_set_state(false);
// A → pilot alto (+12V), E/F → pilot OFF
pilot_set_level(state == EVSE_STATE_A);
if (board_config.socket_lock && socket_outlet)
{
socket_lock_set_locked(false);
@@ -72,66 +79,77 @@ static void update_outputs(evse_state_t state)
case EVSE_STATE_B1:
pilot_set_level(true);
ac_relay_set_state(false);
if (board_config.socket_lock && socket_outlet)
{
socket_lock_set_locked(true);
}
if (rcm_test())
{
// ESP_LOGI(TAG, "RCM self test passed");
}
else
{
// ESP_LOGW(TAG, "RCM self test failed");
}
(void)rcm_test();
break;
case EVSE_STATE_B2:
pilot_set_amps(MIN(current, cable_max_current));
ac_relay_set_state(false);
if (board_config.socket_lock && socket_outlet)
{
socket_lock_set_locked(true);
}
break;
case EVSE_STATE_C1:
case EVSE_STATE_D1:
{
pilot_set_amps(MIN(current, cable_max_current));
ac_relay_set_state(false);
c1_d1_waiting = true;
c1_d1_relay_to = xTaskGetTickCount() + pdMS_TO_TICKS(6000);
break;
if (board_config.socket_lock && socket_outlet)
{
socket_lock_set_locked(true);
}
break;
case EVSE_STATE_C2:
case EVSE_STATE_D2:
pilot_set_amps(MIN(current, cable_max_current));
ac_relay_set_state(true);
if (board_config.socket_lock && socket_outlet)
{
socket_lock_set_locked(true);
}
break;
}
}
// FSM principal
/**
* @brief Máquina de estados principal do EVSE (IEC 61851).
*/
void evse_fsm_process(
pilot_voltage_t pilot_voltage,
bool authorized,
bool available,
bool enabled)
{
// Proteção total: erro força F sempre!
if (evse_get_error() != 0)
// 1) Erros globais: dominam qualquer outra lógica
uint32_t err_bits = evse_get_error();
if (err_bits != 0)
{
if (evse_get_state() != EVSE_STATE_F)
evse_state_t forced_state =
(err_bits & EVSE_ERR_PILOT_FAULT_BIT) ? EVSE_STATE_E : EVSE_STATE_F;
if (evse_get_state() != forced_state)
{
ESP_LOGW(TAG, "Erro ativo detectado: forçando estado FAULT (F)");
evse_set_state(EVSE_STATE_F);
ESP_LOGW(TAG, "Erro ativo detectado: forçando estado %s",
evse_state_to_str(forced_state));
evse_set_state(forced_state);
}
update_outputs(EVSE_STATE_F);
update_outputs(forced_state);
return;
}
TickType_t now = xTaskGetTickCount();
evse_state_t prev = evse_get_state();
evse_state_t curr = prev;
evse_state_t curr = evse_get_state();
switch (curr)
{
@@ -153,17 +171,25 @@ void evse_fsm_process(
evse_set_state(EVSE_STATE_F);
break;
}
switch (pilot_voltage)
{
case PILOT_VOLTAGE_12:
evse_set_state(EVSE_STATE_A);
break;
case PILOT_VOLTAGE_9:
evse_set_state((authorized && enabled) ? EVSE_STATE_B2 : EVSE_STATE_B1);
break;
case PILOT_VOLTAGE_6:
evse_set_state((authorized && enabled) ? EVSE_STATE_C2 : EVSE_STATE_C1);
break;
case PILOT_VOLTAGE_3:
evse_set_state((authorized && enabled) ? EVSE_STATE_D2 : EVSE_STATE_D1);
break;
default:
break;
}
@@ -171,52 +197,59 @@ void evse_fsm_process(
case EVSE_STATE_C1:
case EVSE_STATE_D1:
if (c1_d1_waiting && now >= c1_d1_relay_to)
{
ac_relay_set_state(false);
c1_d1_waiting = false;
case EVSE_STATE_C2:
case EVSE_STATE_D2:
if (!available)
{
evse_set_state(EVSE_STATE_F);
break;
}
}
__attribute__((fallthrough));
case EVSE_STATE_C2:
case EVSE_STATE_D2:
if (!enabled || !available)
if (!enabled)
{
evse_set_state((curr == EVSE_STATE_D2 || curr == EVSE_STATE_D1)
? EVSE_STATE_D1
: EVSE_STATE_C1);
if (curr == EVSE_STATE_C2)
{
evse_set_state(EVSE_STATE_C1);
}
else if (curr == EVSE_STATE_D2)
{
evse_set_state(EVSE_STATE_D1);
}
break;
}
switch (pilot_voltage)
{
case PILOT_VOLTAGE_6:
evse_set_state((authorized && enabled) ? EVSE_STATE_C2 : EVSE_STATE_C1);
break;
case PILOT_VOLTAGE_3:
evse_set_state((authorized && enabled) ? EVSE_STATE_D2 : EVSE_STATE_D1);
break;
case PILOT_VOLTAGE_9:
evse_set_state((authorized && enabled) ? EVSE_STATE_B2 : EVSE_STATE_B1);
break;
case PILOT_VOLTAGE_12:
evse_set_state(EVSE_STATE_A);
break;
default:
break;
}
break;
case EVSE_STATE_E:
// Estado elétrico grave: só reset manual
// ✅ Agora recupera como F: se disponível e sem erro -> volta a A
if (available && evse_get_error() == 0)
{
evse_set_state(EVSE_STATE_A);
}
break;
case EVSE_STATE_F:
// Fault: só sai se disponível e sem erro
if (available && evse_get_error() == 0)
{
evse_set_state(EVSE_STATE_A);

View File

@@ -1,34 +1,123 @@
#include <inttypes.h> // for PRIu32
#include <inttypes.h>
#include <stdbool.h>
#include "evse_state.h"
#include "evse_api.h"
#include "evse_limits.h"
#include "evse_meter.h"
#include "evse_session.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_err.h"
#include "nvs.h"
#include "esp_check.h"
// ========================
// Concurrency protection
// ========================
#include "storage_service.h"
#define NVS_NAMESPACE "evse_limits"
static const char *TAG = "evse_limits";
static portMUX_TYPE evse_mux = portMUX_INITIALIZER_UNLOCKED;
// ========================
// Runtime state (volatile)
// ========================
static bool limit_reached = false;
static uint32_t consumption_limit = 0; // Energy limit in Wh
static uint32_t charging_time_limit = 0; // Time limit in seconds
static uint16_t under_power_limit = 0; // Minimum acceptable power in W
static uint32_t consumption_limit = 0; // Wh
static uint32_t charging_time_limit = 0; // seconds
static uint16_t under_power_limit = 0; // W
// ========================
// Limit status flag
// ========================
static inline TickType_t TO_TICKS_MS(uint32_t ms) { return pdMS_TO_TICKS(ms); }
static inline TickType_t BOOT_TO(void) { return TO_TICKS_MS(1000); }
// ---------------------------------
// Init + defaults
// ---------------------------------
esp_err_t evse_limits_init(void)
{
ESP_RETURN_ON_ERROR(storage_service_init(), TAG, "storage init failed");
ESP_LOGI(TAG, "EVSE limits init OK (storage-backed)");
return ESP_OK;
}
void evse_limits_check_defaults(void)
{
esp_err_t err;
bool needs_flush = false;
uint32_t u32 = 0;
uint16_t u16 = 0;
ESP_LOGD(TAG, "Checking default limits...");
// Consumption limit (Wh) default = 0 (disabled)
err = storage_get_u32_sync(NVS_NAMESPACE, "def_cons_lim", &u32, BOOT_TO());
if (err == ESP_OK)
{
portENTER_CRITICAL(&evse_mux);
consumption_limit = u32;
portEXIT_CRITICAL(&evse_mux);
}
else
{
portENTER_CRITICAL(&evse_mux);
consumption_limit = 0;
portEXIT_CRITICAL(&evse_mux);
(void)storage_set_u32_async(NVS_NAMESPACE, "def_cons_lim", 0);
needs_flush = true;
ESP_LOGW(TAG, "Missing def_cons_lim (%s) -> default=0 (persisted).", esp_err_to_name(err));
}
// Charging time limit (s) default = 0 (disabled)
err = storage_get_u32_sync(NVS_NAMESPACE, "def_ch_time_lim", &u32, BOOT_TO());
if (err == ESP_OK)
{
portENTER_CRITICAL(&evse_mux);
charging_time_limit = u32;
portEXIT_CRITICAL(&evse_mux);
}
else
{
portENTER_CRITICAL(&evse_mux);
charging_time_limit = 0;
portEXIT_CRITICAL(&evse_mux);
(void)storage_set_u32_async(NVS_NAMESPACE, "def_ch_time_lim", 0);
needs_flush = true;
ESP_LOGW(TAG, "Missing def_ch_time_lim (%s) -> default=0 (persisted).", esp_err_to_name(err));
}
// Under-power limit (W) default = 0 (disabled)
err = storage_get_u16_sync(NVS_NAMESPACE, "def_un_pwr_lim", &u16, BOOT_TO());
if (err == ESP_OK)
{
portENTER_CRITICAL(&evse_mux);
under_power_limit = u16;
portEXIT_CRITICAL(&evse_mux);
}
else
{
portENTER_CRITICAL(&evse_mux);
under_power_limit = 0;
portEXIT_CRITICAL(&evse_mux);
(void)storage_set_u16_async(NVS_NAMESPACE, "def_un_pwr_lim", 0);
needs_flush = true;
ESP_LOGW(TAG, "Missing def_un_pwr_lim (%s) -> default=0 (persisted).", esp_err_to_name(err));
}
if (needs_flush)
{
esp_err_t fe = storage_flush_sync(TO_TICKS_MS(2000));
if (fe != ESP_OK)
ESP_LOGE(TAG, "storage_flush_sync failed: %s", esp_err_to_name(fe));
else
ESP_LOGD(TAG, "Defaults committed (flush).");
}
}
// ---------------------------------
// Limit reached flag
// ---------------------------------
bool evse_get_limit_reached(void)
{
bool val;
@@ -50,10 +139,9 @@ bool evse_is_limit_reached(void)
return evse_get_limit_reached();
}
// ========================
// Runtime limit accessors
// ========================
// ---------------------------------
// Consumption limit
// ---------------------------------
uint32_t evse_get_consumption_limit(void)
{
uint32_t val;
@@ -78,30 +166,18 @@ void evse_set_consumption_limit(uint32_t value)
if (!changed)
return;
nvs_handle_t h;
esp_err_t err = nvs_open("evse", NVS_READWRITE, &h);
if (err == ESP_OK)
{
err = nvs_set_u32(h, "def_cons_lim", value);
if (err == ESP_OK)
err = nvs_commit(h);
nvs_close(h);
esp_err_t err = storage_set_u32_async(NVS_NAMESPACE, "def_cons_lim", value);
if (err != ESP_OK)
{
ESP_LOGE("EVSE_LIMITS",
ESP_LOGE(TAG,
"Failed to persist consumption limit (%" PRIu32 " Wh): %s",
value, esp_err_to_name(err));
}
}
else
{
ESP_LOGE("EVSE_LIMITS",
"Failed to open NVS for consumption limit: %s",
esp_err_to_name(err));
}
}
// ---------------------------------
// Charging time limit
// ---------------------------------
uint32_t evse_get_charging_time_limit(void)
{
uint32_t val;
@@ -126,30 +202,18 @@ void evse_set_charging_time_limit(uint32_t value)
if (!changed)
return;
nvs_handle_t h;
esp_err_t err = nvs_open("evse", NVS_READWRITE, &h);
if (err == ESP_OK)
{
err = nvs_set_u32(h, "def_ch_time_lim", value);
if (err == ESP_OK)
err = nvs_commit(h);
nvs_close(h);
esp_err_t err = storage_set_u32_async(NVS_NAMESPACE, "def_ch_time_lim", value);
if (err != ESP_OK)
{
ESP_LOGE("EVSE_LIMITS",
ESP_LOGE(TAG,
"Failed to persist charging time limit (%" PRIu32 " s): %s",
value, esp_err_to_name(err));
}
}
else
{
ESP_LOGE("EVSE_LIMITS",
"Failed to open NVS for charging time limit: %s",
esp_err_to_name(err));
}
}
// ---------------------------------
// Under-power limit
// ---------------------------------
uint16_t evse_get_under_power_limit(void)
{
uint16_t val;
@@ -174,82 +238,64 @@ void evse_set_under_power_limit(uint16_t value)
if (!changed)
return;
nvs_handle_t h;
esp_err_t err = nvs_open("evse", NVS_READWRITE, &h);
if (err == ESP_OK)
{
err = nvs_set_u16(h, "def_un_pwr_lim", value);
if (err == ESP_OK)
err = nvs_commit(h);
nvs_close(h);
esp_err_t err = storage_set_u16_async(NVS_NAMESPACE, "def_un_pwr_lim", value);
if (err != ESP_OK)
{
ESP_LOGE("EVSE_LIMITS",
ESP_LOGE(TAG,
"Failed to persist under-power limit (%" PRIu32 " W): %s",
(uint32_t)value, esp_err_to_name(err));
}
}
else
{
ESP_LOGE("EVSE_LIMITS",
"Failed to open NVS for under-power limit: %s",
esp_err_to_name(err));
}
}
// ========================
// Limit checking logic
// ========================
// ---------------------------------
// Runtime check
// ---------------------------------
void evse_limits_check(void)
{
// Só faz sentido durante carregamento
// Só faz sentido quando há energia ativa (C2/D2)
if (!evse_state_is_charging(evse_get_state()))
{
return;
}
evse_session_t sess;
if (!evse_session_get(&sess) || !sess.is_current)
{
// Sem sessão ativa → nada a fazer
return;
}
uint32_t cons_lim;
uint32_t time_lim;
uint16_t unp_lim;
portENTER_CRITICAL(&evse_mux);
cons_lim = consumption_limit;
time_lim = charging_time_limit;
unp_lim = under_power_limit;
portEXIT_CRITICAL(&evse_mux);
bool reached = false;
// 1) Limite de energia (Wh)
if (consumption_limit > 0 && sess.energy_wh >= consumption_limit)
if (cons_lim > 0 && sess.energy_wh >= cons_lim)
{
ESP_LOGW("EVSE_LIMITS",
"Energy limit reached: %" PRIu32 " Wh ≥ %" PRIu32 " Wh",
sess.energy_wh, consumption_limit);
ESP_LOGW(TAG, "Energy limit reached: %" PRIu32 " Wh ≥ %" PRIu32 " Wh",
sess.energy_wh, cons_lim);
reached = true;
}
// 2) Limite de tempo (s)
if (charging_time_limit > 0 && sess.duration_s >= charging_time_limit)
if (time_lim > 0 && sess.duration_s >= time_lim)
{
ESP_LOGW("EVSE_LIMITS",
"Charging time limit reached: %" PRIu32 " s ≥ %" PRIu32 " s",
sess.duration_s, charging_time_limit);
ESP_LOGW(TAG, "Charging time limit reached: %" PRIu32 " s ≥ %" PRIu32 " s",
sess.duration_s, time_lim);
reached = true;
}
// 3) Under-power (potência instantânea)
uint32_t inst_power = evse_meter_get_instant_power();
if (under_power_limit > 0 && inst_power < under_power_limit)
int32_t p = evse_meter_get_instant_power();
uint32_t inst_power = (p > 0) ? (uint32_t)p : 0;
if (unp_lim > 0 && inst_power < (uint32_t)unp_lim)
{
ESP_LOGW("EVSE_LIMITS",
"Under-power limit reached: %" PRIu32 " W < %" PRIu32 " W",
(uint32_t)inst_power,
(uint32_t)under_power_limit);
ESP_LOGW(TAG, "Under-power limit reached: %" PRIu32 " W < %" PRIu32 " W",
inst_power, (uint32_t)unp_lim);
reached = true;
}
if (reached)
{
evse_set_limit_reached(true);
}
}

View File

@@ -1,4 +1,3 @@
// === Início de: components/evse/evse_manager.c ===
#include "evse_manager.h"
#include "evse_state.h"
#include "evse_error.h"
@@ -11,10 +10,10 @@
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "freertos/queue.h"
#include "esp_log.h"
#include "esp_event.h"
#include "esp_err.h"
#include <string.h>
#include <inttypes.h>
@@ -27,27 +26,29 @@
static const char *TAG = "EVSE_Manager";
static SemaphoreHandle_t evse_mutex;
static volatile bool auth_enabled = false;
// ✅ Proteção para flags partilhadas (event handlers vs task)
static portMUX_TYPE s_mgr_mux = portMUX_INITIALIZER_UNLOCKED;
static bool auth_enabled = false;
// Estado de pausa controlado pelo Load Balancer
static volatile bool lb_paused = false;
static volatile bool lb_prev_authorized = false;
static bool lb_paused = false;
static bool lb_prev_authorized = false;
// Estado de janela do scheduler
static volatile bool s_sched_allowed = true;
static bool s_sched_allowed = true;
static portMUX_TYPE s_sched_mux = portMUX_INITIALIZER_UNLOCKED;
#define EVSE_MANAGER_TICK_PERIOD_MS 1000 // 1 segundo
// ================= Helpers internos =================
static void lb_clear_pause_state(void)
{
portENTER_CRITICAL(&s_mgr_mux);
lb_paused = false;
lb_prev_authorized = false;
portEXIT_CRITICAL(&s_mgr_mux);
}
// Exposto para outros módulos (se quiserem saber se o scheduler permite)
bool evse_sched_is_allowed(void)
{
bool v;
@@ -60,19 +61,35 @@ bool evse_sched_is_allowed(void)
static void evse_manager_handle_auth_on_tick(void)
{
bool sched_allowed = evse_sched_is_allowed();
uint32_t err_bits = evse_get_error(); // inclui holdoff interno
bool has_error = (err_bits != 0);
if (auth_enabled)
bool local_auth_enabled;
bool local_lb_paused;
portENTER_CRITICAL(&s_mgr_mux);
local_auth_enabled = auth_enabled;
local_lb_paused = lb_paused;
portEXIT_CRITICAL(&s_mgr_mux);
if (local_auth_enabled)
{
// Se o carro foi desconectado, revoga autorização
if (evse_state_get_authorized() && evse_get_state() == EVSE_STATE_A)
{
ESP_LOGI(TAG, "Vehicle disconnected → revoking authorization.");
evse_state_set_authorized(false);
// Desconexão física invalida qualquer pausa pendente do LB
lb_clear_pause_state();
}
// Em modos RFID/OCPP, o scheduler pode também forçar paragem
if (has_error && evse_state_get_authorized())
{
ESP_LOGI(TAG,
"[AUTH] error active (err=0x%08" PRIx32 ") → revoking authorization.",
err_bits);
evse_state_set_authorized(false);
lb_clear_pause_state();
}
if (!sched_allowed && evse_state_get_authorized())
{
ESP_LOGI(TAG, "[SCHED] window closed (auth mode) → revoking authorization.");
@@ -81,28 +98,32 @@ static void evse_manager_handle_auth_on_tick(void)
}
else
{
// Modo OPEN: só autoriza se LB e Scheduler permitirem
if (!lb_paused && sched_allowed && !evse_state_get_authorized())
bool limit_hit = evse_is_limit_reached();
bool can_operate = evse_config_is_available() && evse_config_is_enabled();
if ((has_error || limit_hit || !sched_allowed || !can_operate || local_lb_paused) &&
evse_state_get_authorized())
{
evse_state_set_authorized(true);
ESP_LOGI(TAG, "Authentication disabled → forced authorization (within schedule).");
lb_clear_pause_state();
ESP_LOGI(TAG,
"[OPEN] blocking (err=%d limit=%d sched=%d operate=%d lb_paused=%d) → revoking authorization.",
(int)has_error, (int)limit_hit, (int)sched_allowed, (int)can_operate, (int)local_lb_paused);
evse_state_set_authorized(false);
}
// Fora da janela, garantir que não fica autorizado
if (!sched_allowed && evse_state_get_authorized())
if (!local_lb_paused && sched_allowed && can_operate &&
!has_error && !limit_hit &&
!evse_state_get_authorized())
{
ESP_LOGI(TAG, "[SCHED] window closed (OPEN mode) → revoking authorization.");
evse_state_set_authorized(false);
evse_state_set_authorized(true);
ESP_LOGI(TAG, "Authentication disabled → forced authorization (schedule ok, no error/limits).");
lb_clear_pause_state();
}
}
}
// ===== Task de ciclo principal =====
static void evse_manager_task(void *arg)
{
(void)arg;
while (true)
{
evse_manager_tick();
@@ -110,15 +131,10 @@ static void evse_manager_task(void *arg)
}
}
// ===== Tratador de eventos de AUTH =====
static void on_auth_event(void *arg, esp_event_base_t base, int32_t id, void *data)
{
(void)arg;
if (base != AUTH_EVENTS || !data)
return;
auth_mode_t g_mode = AUTH_MODE_OPEN;
if (base != AUTH_EVENTS || !data) return;
switch (id)
{
@@ -127,8 +143,6 @@ static void on_auth_event(void *arg, esp_event_base_t base, int32_t id, void *da
const auth_tag_event_data_t *evt = (const auth_tag_event_data_t *)data;
ESP_LOGI(TAG, "Tag %s -> %s", evt->tag, evt->authorized ? "AUTHORIZED" : "DENIED");
evse_state_set_authorized(evt->authorized);
// Qualquer alteração explícita de auth invalida pausa do LB
lb_clear_pause_state();
break;
}
@@ -137,37 +151,26 @@ static void on_auth_event(void *arg, esp_event_base_t base, int32_t id, void *da
case AUTH_EVENT_INIT:
{
const auth_mode_event_data_t *evt = (const auth_mode_event_data_t *)data;
g_mode = evt->mode;
ESP_LOGI(TAG, "Auth mode = %s", auth_mode_to_str(g_mode));
if (g_mode == AUTH_MODE_OPEN)
{
// Em OPEN, a autorização passa a ser gerida por evse_manager_handle_auth_on_tick(),
// que também respeita o scheduler.
evse_state_set_authorized(false); // vai ser forçado no próximo tick se permitido
auth_enabled = false;
}
else
{
evse_state_set_authorized(false);
auth_enabled = true;
}
ESP_LOGI(TAG, "Auth mode = %s", auth_mode_to_str(evt->mode));
// Modo mudou -> qualquer pausa antiga deixa de fazer sentido
portENTER_CRITICAL(&s_mgr_mux);
auth_enabled = (evt->mode != AUTH_MODE_OPEN);
portEXIT_CRITICAL(&s_mgr_mux);
evse_state_set_authorized(false);
lb_clear_pause_state();
break;
}
}
}
// ===== Tratador de eventos de Load Balancer =====
static void on_loadbalancer_event(void *handler_arg, esp_event_base_t event_base,
int32_t event_id, void *event_data)
{
(void)handler_arg;
(void)event_base;
if (!event_data)
return;
if (!event_data) return;
if (event_id == LOADBALANCER_EVENT_INIT || event_id == LOADBALANCER_EVENT_STATE_CHANGED)
{
@@ -181,98 +184,86 @@ static void on_loadbalancer_event(void *handler_arg, esp_event_base_t event_base
const loadbalancer_master_limit_event_t *evt =
(const loadbalancer_master_limit_event_t *)event_data;
ESP_LOGI(TAG,
"Novo limite de corrente (master): %u A (ts: %lld)",
ESP_LOGI(TAG, "Novo limite de corrente (master): %u A (ts: %lld)",
evt->max_current, (long long)evt->timestamp_us);
if (evt->max_current == 0)
{
// Suspensão por LB (não interessa se é OPEN ou RFID/OCPP)
lb_paused = true;
lb_prev_authorized = evse_state_get_authorized();
bool prev_auth = evse_state_get_authorized();
if (lb_prev_authorized)
portENTER_CRITICAL(&s_mgr_mux);
lb_paused = true;
lb_prev_authorized = prev_auth;
portEXIT_CRITICAL(&s_mgr_mux);
if (prev_auth)
{
ESP_LOGI(TAG, "[LB] limit=0A → pausando sessão (authorized=false)");
evse_state_set_authorized(false);
}
else
{
ESP_LOGD(TAG, "[LB] limit=0A → já não estava autorizado");
}
}
else
{
// Ajusta corrente em runtime
evse_set_runtime_charging_current(evt->max_current);
if (lb_paused)
{
lb_paused = false;
bool was_paused;
bool prev_auth;
// Só retomamos se EVSE estiver operacional e scheduler permitir
portENTER_CRITICAL(&s_mgr_mux);
was_paused = lb_paused;
prev_auth = lb_prev_authorized;
portEXIT_CRITICAL(&s_mgr_mux);
if (was_paused)
{
bool can_resume =
(evse_get_error() == 0) &&
evse_config_is_available() &&
evse_config_is_enabled() &&
evse_sched_is_allowed();
evse_sched_is_allowed() &&
!evse_is_limit_reached();
if (!can_resume)
{
ESP_LOGW(TAG,
"[LB] limit=%uA → não retoma automaticamente (erro/indisponível/desabilitado/fora de horário)",
"[LB] limit=%uA → não retoma automaticamente (erro/indisp/desab/fora de horário/limite)",
evt->max_current);
lb_clear_pause_state();
return;
}
if (!auth_enabled)
bool local_auth_enabled;
portENTER_CRITICAL(&s_mgr_mux);
local_auth_enabled = auth_enabled;
lb_paused = false; // já vai tentar retomar
portEXIT_CRITICAL(&s_mgr_mux);
if (!local_auth_enabled)
{
// Modo OPEN: retoma sempre (se dentro da janela do scheduler)
ESP_LOGI(TAG,
"[LB] limit=%uA → modo OPEN, reautorizando (authorized=true)",
evt->max_current);
ESP_LOGI(TAG, "[LB] limit=%uA → modo OPEN, reautorizando", evt->max_current);
evse_state_set_authorized(true);
}
else
{
// RFID/OCPP: só retoma se havia autorização antes da pausa
if (lb_prev_authorized)
if (prev_auth)
{
ESP_LOGI(TAG,
"[LB] limit=%uA → RFID/OCPP, retomando autorização anterior (auto-resume)",
evt->max_current);
ESP_LOGI(TAG, "[LB] limit=%uA → RFID/OCPP, retomando autorização anterior", evt->max_current);
evse_state_set_authorized(true);
}
else
{
ESP_LOGI(TAG,
"[LB] limit=%uA → RFID/OCPP, sem autorização prévia, mantendo estado atual",
evt->max_current);
}
}
// Limpa estado prévio (não reaplicar em pausas futuras)
portENTER_CRITICAL(&s_mgr_mux);
lb_prev_authorized = false;
}
else
{
// Caso normal: apenas ajuste de corrente, sem mexer em auth
ESP_LOGD(TAG,
"[LB] limit=%uA → ajustando corrente runtime (sem mudança de autorização)",
evt->max_current);
portEXIT_CRITICAL(&s_mgr_mux);
}
}
}
}
// ===== Tratador de eventos de OCPP =====
static void on_ocpp_event(void *arg, esp_event_base_t base, int32_t id, void *data)
{
(void)arg;
if (base != OCPP_EVENTS)
return;
if (base != OCPP_EVENTS) return;
switch (id)
{
@@ -283,13 +274,10 @@ static void on_ocpp_event(void *arg, esp_event_base_t base, int32_t id, void *da
break;
case OCPP_EVENT_AUTH_REJECTED:
ESP_LOGW(TAG, "[OCPP] Authorization rejected");
evse_state_set_authorized(false);
lb_clear_pause_state();
break;
case OCPP_EVENT_AUTH_TIMEOUT:
ESP_LOGW(TAG, "[OCPP] Authorization timeout");
case OCPP_EVENT_REMOTE_STOP:
case OCPP_EVENT_STOP_TX:
ESP_LOGW(TAG, "[OCPP] Authorization/Stop");
evse_state_set_authorized(false);
lb_clear_pause_state();
break;
@@ -300,24 +288,11 @@ static void on_ocpp_event(void *arg, esp_event_base_t base, int32_t id, void *da
lb_clear_pause_state();
break;
case OCPP_EVENT_REMOTE_STOP:
ESP_LOGI(TAG, "[OCPP] RemoteStop");
evse_state_set_authorized(false);
lb_clear_pause_state();
break;
case OCPP_EVENT_START_TX:
ESP_LOGI(TAG, "[OCPP] StartTx");
lb_clear_pause_state();
break;
case OCPP_EVENT_STOP_TX:
ESP_LOGI(TAG, "[OCPP] StopTx");
evse_state_set_authorized(false);
lb_clear_pause_state();
break;
// ChangeAvailability remoto (operative/inoperative)
case OCPP_EVENT_OPERATIVE_UPDATED:
{
if (!data)
@@ -329,7 +304,6 @@ static void on_ocpp_event(void *arg, esp_event_base_t base, int32_t id, void *da
ESP_LOGI(TAG, "[OCPP] OperativeUpdated: operative=%d ts=%lld",
(int)ev->operative, (long long)ev->timestamp_us);
// Mapear operative → enabled local (persiste e emite EVSE_EVENT_ENABLE_UPDATED)
evse_config_set_enabled(ev->operative);
break;
}
@@ -340,16 +314,10 @@ static void on_ocpp_event(void *arg, esp_event_base_t base, int32_t id, void *da
}
}
// ===== Tratador de eventos de Scheduler =====
static void on_sched_event(void *arg,
esp_event_base_t base,
int32_t id,
void *data)
static void on_sched_event(void *arg, esp_event_base_t base, int32_t id, void *data)
{
(void)arg;
if (base != SCHED_EVENTS || data == NULL)
return;
if (base != SCHED_EVENTS || data == NULL) return;
const sched_event_state_t *ev = (const sched_event_state_t *)data;
@@ -357,29 +325,26 @@ static void on_sched_event(void *arg,
s_sched_allowed = ev->allowed_now;
portEXIT_CRITICAL(&s_sched_mux);
ESP_LOGI(TAG,
"[SCHED] event id=%" PRIi32 " allowed_now=%d",
id, (int)ev->allowed_now);
ESP_LOGI(TAG, "[SCHED] allowed_now=%d", (int)ev->allowed_now);
// Se a janela fechou, parar sessão (revogar autorização)
if (!ev->allowed_now && evse_state_get_authorized())
{
ESP_LOGI(TAG, "[SCHED] window closed → stopping session (authorized=false)");
evse_state_set_authorized(false);
}
// Se a janela abriu de novo, não auto-reautorizamos aqui.
// Deixamos que o utilizador / OCPP decida iniciar nova sessão.
// (Em modo OPEN, o tick trata disso respeitando o scheduler.)
}
// ===== Inicialização =====
void evse_manager_init(void)
{
evse_mutex = xSemaphoreCreateMutex();
configASSERT(evse_mutex != NULL);
evse_config_init();
esp_err_t err = evse_config_init();
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to init EVSE config NVS: %s", esp_err_to_name(err));
}
evse_error_init();
evse_hardware_init();
evse_state_init();
@@ -393,11 +358,10 @@ void evse_manager_init(void)
ESP_LOGI(TAG, "EVSE Manager inicializado.");
BaseType_t rc = xTaskCreate(evse_manager_task, "evse_manager_task", 4096, NULL, 5, NULL);
BaseType_t rc = xTaskCreate(evse_manager_task, "evse_manager_task", 8192, NULL, 4, NULL);
configASSERT(rc == pdPASS);
}
// ===== Main Tick =====
void evse_manager_tick(void)
{
xSemaphoreTake(evse_mutex, portMAX_DELAY);
@@ -412,5 +376,3 @@ void evse_manager_tick(void)
xSemaphoreGive(evse_mutex);
}
// === Fim de: components/evse/evse_manager.c ===

View File

@@ -1,8 +1,7 @@
// components/evse/evse_pilot.c
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <math.h>
#include <string.h>
#include "driver/ledc.h"
#include "esp_err.h"
@@ -19,33 +18,63 @@
#define PILOT_PWM_DUTY_RES LEDC_TIMER_10_BIT
#define PILOT_PWM_MAX_DUTY 1023
// --- Configuração de amostragem do Pilot ---
#define NUM_PILOT_SAMPLES 100
#define MAX_SAMPLE_ATTEMPTS 1000
#define PILOT_SAMPLE_DELAY_US 10
// Percentagem para descartar extremos superior/inferior (ruído)
#define PILOT_EXTREME_PERCENT 10 // 10% superior e inferior
// ADC referência
#define ADC121_VREF_MV 3300
#define ADC121_MAX 4095
static const char *TAG = "evse_pilot";
static int last_pilot_level = -1;
typedef enum {
PILOT_MODE_DC_HIGH = 0, // +12V (nível alto)
PILOT_MODE_DC_LOW, // nível baixo / pilot desligado (dependente do hardware)
PILOT_MODE_PWM // PWM ativo
} pilot_mode_t;
static pilot_mode_t s_mode = PILOT_MODE_DC_LOW;
static uint32_t last_pwm_duty = 0;
static int adc_raw_to_mv(uint16_t raw) {
return (raw * ADC121_VREF_MV) / ADC121_MAX;
// ---------------------
// Helpers internos
// ---------------------
static int adc_raw_to_mv(uint16_t raw)
{
return (int)((raw * ADC121_VREF_MV) / ADC121_MAX);
}
static int compare_uint16(const void *a, const void *b)
{
uint16_t va = *(const uint16_t *)a;
uint16_t vb = *(const uint16_t *)b;
if (va < vb) return -1;
if (va > vb) return 1;
return 0;
}
// ---------------------
// Inicialização PWM + ADC
// ---------------------
void pilot_init(void)
{
// Configura timer do PWM do Pilot (1 kHz)
ledc_timer_config_t ledc_timer = {
.speed_mode = PILOT_PWM_SPEED_MODE,
.timer_num = PILOT_PWM_TIMER,
.duty_resolution = PILOT_PWM_DUTY_RES,
.freq_hz = 1000,
.freq_hz = 1000, // 1 kHz (IEC 61851)
.clk_cfg = LEDC_AUTO_CLK
};
ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer));
// Canal do PWM no pino configurado em board_config
ledc_channel_config_t ledc_channel = {
.speed_mode = PILOT_PWM_SPEED_MODE,
.channel = PILOT_PWM_CHANNEL,
@@ -56,110 +85,159 @@ void pilot_init(void)
.hpoint = 0
};
ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel));
ESP_ERROR_CHECK(ledc_stop(PILOT_PWM_SPEED_MODE, PILOT_PWM_CHANNEL, 0));
// Garante que começa parado e em idle baixo (pilot off)
ESP_ERROR_CHECK(ledc_stop(PILOT_PWM_SPEED_MODE, PILOT_PWM_CHANNEL, 0));
s_mode = PILOT_MODE_DC_LOW;
last_pwm_duty = 0;
// Inicializa driver do ADC121S021
adc121s021_dma_init();
}
void pilot_set_level(bool level)
// ---------------------
// Controlo do modo do Pilot
// ---------------------
void pilot_set_level(bool high)
{
if (last_pilot_level == level) return;
last_pilot_level = level;
pilot_mode_t target = high ? PILOT_MODE_DC_HIGH : PILOT_MODE_DC_LOW;
ESP_LOGI(TAG, "Set level %d", level);
ledc_stop(PILOT_PWM_SPEED_MODE, PILOT_PWM_CHANNEL, level ? 1 : 0);
// Se já estiver no modo DC desejado e sem PWM ativo, ignora
if (s_mode == target && last_pwm_duty == 0) {
return;
}
ESP_LOGI(TAG, "Pilot set DC level: %s", high ? "HIGH(+12V)" : "LOW/OFF");
// Para PWM e fixa o nível idle do GPIO
ESP_ERROR_CHECK(ledc_stop(PILOT_PWM_SPEED_MODE, PILOT_PWM_CHANNEL, high ? 1 : 0));
s_mode = target;
last_pwm_duty = 0;
}
void pilot_set_amps(uint16_t amps)
{
if (amps < 6 || amps > 80) {
if (amps < 6 || amps > 80)
{
ESP_LOGE(TAG, "Invalid ampere value: %d A (valid: 680 A)", amps);
return;
}
uint32_t duty_percent;
if (amps <= 51) {
if (amps <= 51)
{
duty_percent = (amps * 10) / 6;
} else {
}
else
{
duty_percent = (amps * 10) / 25 + 64;
}
if (duty_percent > 100) duty_percent = 100;
uint32_t duty = (PILOT_PWM_MAX_DUTY * duty_percent) / 100;
if (last_pilot_level == 0 && last_pwm_duty == duty) return;
last_pilot_level = 0;
// Se já estiver em PWM com o mesmo duty, ignora
if (s_mode == PILOT_MODE_PWM && last_pwm_duty == duty) {
return;
}
s_mode = PILOT_MODE_PWM;
last_pwm_duty = duty;
ESP_LOGI(TAG, "Pilot set: %d A → %d/%d (≈ %d%% duty)",
ESP_LOGI(TAG, "Pilot set PWM: %d A → %d/%d (≈ %d%% duty)",
amps, (int)duty, PILOT_PWM_MAX_DUTY, (int)duty_percent);
ledc_set_duty(PILOT_PWM_SPEED_MODE, PILOT_PWM_CHANNEL, duty);
ledc_update_duty(PILOT_PWM_SPEED_MODE, PILOT_PWM_CHANNEL);
ESP_ERROR_CHECK(ledc_set_duty(PILOT_PWM_SPEED_MODE, PILOT_PWM_CHANNEL, duty));
ESP_ERROR_CHECK(ledc_update_duty(PILOT_PWM_SPEED_MODE, PILOT_PWM_CHANNEL));
}
bool pilot_get_state(void) {
return (last_pilot_level == 1) && (last_pwm_duty == 0);
}
static int compare_int(const void *a, const void *b) {
return (*(const int *)a - *(const int *)b);
bool pilot_get_state(void)
{
// "Alto" significa DC +12V (estado A). PWM não conta como DC high.
return (s_mode == PILOT_MODE_DC_HIGH);
}
// ---------------------
// Medição do sinal de Pilot (PWM 1 kHz J1772)
// ---------------------
void pilot_measure(pilot_voltage_t *up_voltage, bool *down_voltage_n12)
{
ESP_LOGD(TAG, "pilot_measure");
int samples[NUM_PILOT_SAMPLES];
int collected = 0, attempts = 0;
uint16_t adc_sample = 0;
uint16_t samples[NUM_PILOT_SAMPLES];
int collected = 0;
int attempts = 0;
while (collected < NUM_PILOT_SAMPLES && attempts < MAX_SAMPLE_ATTEMPTS) {
adc_sample = 0;
if (adc121s021_dma_get_sample(&adc_sample)) {
while (collected < NUM_PILOT_SAMPLES && attempts < MAX_SAMPLE_ATTEMPTS)
{
uint16_t adc_sample;
if (adc121s021_dma_get_sample(&adc_sample))
{
samples[collected++] = adc_sample;
esp_rom_delay_us(10);
} else {
esp_rom_delay_us(PILOT_SAMPLE_DELAY_US);
}
else
{
esp_rom_delay_us(100);
attempts++;
}
}
if (collected < NUM_PILOT_SAMPLES) {
if (collected < NUM_PILOT_SAMPLES)
{
ESP_LOGW(TAG, "Timeout on sample read (%d/%d)", collected, NUM_PILOT_SAMPLES);
*up_voltage = PILOT_VOLTAGE_1;
*down_voltage_n12 = false;
return;
}
qsort(samples, collected, sizeof(int), compare_int);
// Ordena as amostras para eliminar extremos (ruído/espúrios)
qsort(samples, collected, sizeof(uint16_t), compare_uint16);
int k = (collected * PILOT_EXTREME_PERCENT) / 100;
if (k == 0) k = 1;
if (k < 2) k = 2; // garante margem mínima
// descarta k/2 em cada lado (aprox. 10% total, mantendo simetria)
int low_index = k / 2;
int high_index = collected - k + (k / 2);
if (high_index >= collected) high_index = collected - 1;
int high_index = collected - 1 - (k / 2);
int low_raw = samples[low_index];
int high_raw = samples[high_index];
if (low_index < 0) low_index = 0;
if (high_index >= collected) high_index = collected - 1;
if (high_index <= low_index) high_index = low_index;
uint16_t low_raw = samples[low_index];
uint16_t high_raw = samples[high_index];
int high_mv = adc_raw_to_mv(high_raw);
int low_mv = adc_raw_to_mv(low_raw);
// Determina o nível positivo (+12, +9, +6, +3 ou <3 V)
if (high_mv >= board_config.pilot_down_threshold_12)
{
*up_voltage = PILOT_VOLTAGE_12;
}
else if (high_mv >= board_config.pilot_down_threshold_9)
{
*up_voltage = PILOT_VOLTAGE_9;
}
else if (high_mv >= board_config.pilot_down_threshold_6)
{
*up_voltage = PILOT_VOLTAGE_6;
}
else if (high_mv >= board_config.pilot_down_threshold_3)
{
*up_voltage = PILOT_VOLTAGE_3;
}
else
{
*up_voltage = PILOT_VOLTAGE_1;
}
// Verifica se o nível negativo atinge -12 V (diodo presente, C/D válidos)
*down_voltage_n12 = (low_mv <= board_config.pilot_down_threshold_n12);
ESP_LOGD(TAG, "Final: up_voltage=%d, down_voltage_n12=%d", *up_voltage, *down_voltage_n12);
ESP_LOGD(TAG, "Final: up_voltage=%d, down_voltage_n12=%d (high=%d mV, low=%d mV)",
*up_voltage, *down_voltage_n12, high_mv, low_mv);
}

View File

@@ -7,22 +7,49 @@
#include "evse_events.h"
#include "esp_event.h"
#include "evse_limits.h"
#include "esp_timer.h"
#define EVSE_EVENT_POST_TIMEOUT_MS 50
static const char *TAG = "evse_session";
static TickType_t session_start_tick = 0;
static uint32_t watt_seconds = 0;
// Tempo real (microsegundos)
static int64_t session_start_us = 0;
static int64_t last_tick_us = 0;
// Energia integrada com tempo real: soma de (W * us)
static uint64_t watt_microseconds = 0;
static evse_session_t last_session;
static bool last_session_valid = false;
static uint32_t session_counter = 0;
static portMUX_TYPE session_mux = portMUX_INITIALIZER_UNLOCKED;
static void post_session_event(const evse_session_event_data_t *evt)
{
esp_err_t err = esp_event_post(
EVSE_EVENTS,
EVSE_EVENT_SESSION,
evt,
sizeof(*evt),
portMAX_DELAY);
if (err != ESP_OK)
{
ESP_LOGW(TAG, "esp_event_post(EVSE_EVENT_SESSION) failed: %s", esp_err_to_name(err));
}
}
void evse_session_init(void)
{
portENTER_CRITICAL(&session_mux);
session_start_tick = 0;
watt_seconds = 0;
session_start_us = 0;
last_tick_us = 0;
watt_microseconds = 0;
last_session_valid = false;
session_counter = 0;
portEXIT_CRITICAL(&session_mux);
@@ -31,55 +58,65 @@ void evse_session_init(void)
void evse_session_start(void)
{
TickType_t tick = xTaskGetTickCount();
int64_t now_us = esp_timer_get_time();
portENTER_CRITICAL(&session_mux);
session_start_tick = tick;
watt_seconds = 0;
session_start_us = now_us;
last_tick_us = now_us;
watt_microseconds = 0;
session_counter++;
uint32_t id = session_counter;
portEXIT_CRITICAL(&session_mux);
evse_set_limit_reached(false);
ESP_LOGI(TAG, "Session started at tick %u", (unsigned)tick);
ESP_LOGI(TAG, "Session started (id=%" PRIu32 ") tick=%u us=%" PRId64,
id, (unsigned)tick, now_us);
evse_session_event_data_t evt = {
.type = EVSE_SESSION_EVENT_STARTED,
.session_id = session_counter,
.session_id = id,
.duration_s = 0,
.energy_wh = 0,
.avg_power_w = 0,
.is_current = true,
};
esp_event_post(EVSE_EVENTS,
EVSE_EVENT_SESSION,
&evt,
sizeof(evt),
portMAX_DELAY);
post_session_event(&evt);
}
void evse_session_end(void)
{
TickType_t start_tick;
uint32_t ws;
int64_t start_us;
uint64_t w_us;
uint32_t id;
int64_t end_us = esp_timer_get_time();
portENTER_CRITICAL(&session_mux);
if (session_start_tick == 0) {
if (session_start_tick == 0)
{
portEXIT_CRITICAL(&session_mux);
ESP_LOGW(TAG, "evse_session_end called without active session");
return;
}
start_tick = session_start_tick;
ws = watt_seconds;
start_us = session_start_us;
w_us = watt_microseconds;
id = session_counter;
session_start_tick = 0;
session_start_us = 0;
last_tick_us = 0;
watt_microseconds = 0;
portEXIT_CRITICAL(&session_mux);
TickType_t now = xTaskGetTickCount();
uint32_t duration_s = (now - start_tick) / configTICK_RATE_HZ;
uint32_t energy_wh = ws / 3600U;
uint32_t avg_power = duration_s > 0 ? ws / duration_s : 0;
uint32_t duration_s = (end_us > start_us) ? (uint32_t)((end_us - start_us) / 1000000LL) : 0;
uint32_t energy_wh = (uint32_t)(w_us / (3600ULL * 1000000ULL));
uint64_t watt_seconds = (uint64_t)(w_us / 1000000ULL);
uint32_t avg_power = (duration_s > 0) ? (uint32_t)(watt_seconds / duration_s) : 0;
portENTER_CRITICAL(&session_mux);
last_session.start_tick = start_tick;
@@ -91,9 +128,9 @@ void evse_session_end(void)
portEXIT_CRITICAL(&session_mux);
ESP_LOGI(TAG,
"Session ended: duration=%" PRIu32 " s, energy=%" PRIu32
"Session ended (id=%" PRIu32 "): duration=%" PRIu32 " s, energy=%" PRIu32
" Wh, avg_power=%" PRIu32 " W",
duration_s, energy_wh, avg_power);
id, duration_s, energy_wh, avg_power);
evse_session_event_data_t evt = {
.type = EVSE_SESSION_EVENT_FINISHED,
@@ -103,21 +140,36 @@ void evse_session_end(void)
.avg_power_w = avg_power,
.is_current = false,
};
esp_event_post(EVSE_EVENTS,
EVSE_EVENT_SESSION,
&evt,
sizeof(evt),
portMAX_DELAY);
post_session_event(&evt);
}
void evse_session_tick(void)
{
uint32_t power_w = evse_meter_get_instant_power();
// Potência instantânea pode ser negativa (ruído/overflow de sensor) -> clamp
int p = evse_meter_get_instant_power();
uint32_t power_w = (p > 0) ? (uint32_t)p : 0;
int64_t now_us = esp_timer_get_time();
portENTER_CRITICAL(&session_mux);
if (session_start_tick != 0) {
watt_seconds += power_w;
if (session_start_tick != 0)
{
if (last_tick_us == 0)
{
last_tick_us = now_us;
}
int64_t dt_us = now_us - last_tick_us;
if (dt_us > 0)
{
// Energia incremental: W * us (64-bit)
watt_microseconds += ((uint64_t)power_w * (uint64_t)dt_us);
last_tick_us = now_us;
}
else
{
// relógio não devia andar para trás; ignora
last_tick_us = now_us;
}
}
portEXIT_CRITICAL(&session_mux);
}
@@ -127,15 +179,19 @@ bool evse_session_get(evse_session_t *out)
if (out == NULL)
return false;
TickType_t start;
uint32_t ws;
TickType_t start_tick;
int64_t start_us;
uint64_t w_us;
bool has_current;
evse_session_t last_copy;
bool last_valid;
int64_t now_us = esp_timer_get_time();
portENTER_CRITICAL(&session_mux);
start = session_start_tick;
ws = watt_seconds;
start_tick = session_start_tick;
start_us = session_start_us;
w_us = watt_microseconds;
has_current = (session_start_tick != 0);
last_copy = last_session;
last_valid = last_session_valid;
@@ -143,12 +199,12 @@ bool evse_session_get(evse_session_t *out)
if (has_current)
{
TickType_t now = xTaskGetTickCount();
uint32_t duration_s = (now - start) / configTICK_RATE_HZ;
uint32_t energy_wh = ws / 3600U;
uint32_t avg_power = duration_s > 0 ? ws / duration_s : 0;
uint32_t duration_s = (now_us > start_us) ? (uint32_t)((now_us - start_us) / 1000000LL) : 0;
uint32_t energy_wh = (uint32_t)(w_us / (3600ULL * 1000000ULL));
uint64_t watt_seconds = (uint64_t)(w_us / 1000000ULL);
uint32_t avg_power = (duration_s > 0) ? (uint32_t)(watt_seconds / duration_s) : 0;
out->start_tick = start;
out->start_tick = start_tick;
out->duration_s = duration_s;
out->energy_wh = energy_wh;
out->avg_power_w = avg_power;

View File

@@ -5,150 +5,203 @@
#include "freertos/FreeRTOS.h"
#include "freertos/portmacro.h"
#include "esp_log.h"
#include "esp_event.h"
// =========================
// Internal State Variables
// =========================
#define EVSE_EVENT_POST_TIMEOUT_MS 50
static evse_state_t current_state = EVSE_STATE_A;
static bool is_authorized = false;
static portMUX_TYPE state_mux = portMUX_INITIALIZER_UNLOCKED;
static const char *TAG = "evse_state";
// =========================
// Internal Mapping
// =========================
static evse_state_event_t map_state_to_event(evse_state_t s)
{
switch (s)
{
case EVSE_STATE_A:
return EVSE_STATE_EVENT_IDLE;
static evse_state_event_t map_state_to_event(evse_state_t s) {
switch (s) {
case EVSE_STATE_A: return EVSE_STATE_EVENT_IDLE;
case EVSE_STATE_B1:
case EVSE_STATE_B2: return EVSE_STATE_EVENT_WAITING;
case EVSE_STATE_B2:
return EVSE_STATE_EVENT_WAITING;
case EVSE_STATE_C1:
case EVSE_STATE_C2: return EVSE_STATE_EVENT_CHARGING;
case EVSE_STATE_C2:
case EVSE_STATE_D1:
case EVSE_STATE_D2:
return EVSE_STATE_EVENT_CHARGING;
case EVSE_STATE_E:
case EVSE_STATE_F: return EVSE_STATE_EVENT_FAULT;
default: return EVSE_STATE_EVENT_IDLE;
case EVSE_STATE_F:
return EVSE_STATE_EVENT_FAULT;
default:
return EVSE_STATE_EVENT_IDLE;
}
}
// =========================
// Public API
// =========================
static void post_evse_event(evse_event_id_t id, const void *data, size_t len)
{
esp_err_t err = esp_event_post(
EVSE_EVENTS,
id,
data,
len,
portMAX_DELAY);
void evse_set_state(evse_state_t new_state) {
if (err != ESP_OK)
{
ESP_LOGW(TAG, "esp_event_post(id=%d) failed: %s", (int)id, esp_err_to_name(err));
}
}
bool evse_state_is_charging(evse_state_t state)
{
// “charging” == energia efetiva (relé ON)
return (state == EVSE_STATE_C2 || state == EVSE_STATE_D2);
}
bool evse_state_is_power_flowing(evse_state_t state)
{
return evse_state_is_charging(state);
}
bool evse_state_is_requesting(evse_state_t state)
{
// EV pediu carga mas o relé ainda está OFF
return (state == EVSE_STATE_C1 || state == EVSE_STATE_D1);
}
bool evse_state_is_plugged(evse_state_t state)
{
return state == EVSE_STATE_B1 || state == EVSE_STATE_B2 ||
state == EVSE_STATE_C1 || state == EVSE_STATE_C2 ||
state == EVSE_STATE_D1 || state == EVSE_STATE_D2;
}
bool evse_state_is_session(evse_state_t state)
{
// Sessão lógica: “autorizado/pronto” ou “a pedir/a fornecer energia”
return (state == EVSE_STATE_B2 ||
state == EVSE_STATE_C1 || state == EVSE_STATE_C2 ||
state == EVSE_STATE_D1 || state == EVSE_STATE_D2);
}
void evse_set_state(evse_state_t new_state)
{
bool changed = false;
evse_state_t prev_state;
bool start_session = false;
bool end_session = false;
// 1) Detecta transição de estado dentro da região crítica
portENTER_CRITICAL(&state_mux);
prev_state = current_state;
if (new_state != current_state) {
// se entrou em charging pela primeira vez
if (evse_state_is_charging(new_state) && !evse_state_is_charging(prev_state)) {
if (new_state != current_state)
{
// Sessão começa quando entra em energia (relé ON)
if (evse_state_is_power_flowing(new_state) && !evse_state_is_power_flowing(prev_state))
{
start_session = true;
}
// se saiu de charging para qualquer outro
else if (!evse_state_is_charging(new_state) && evse_state_is_charging(prev_state)) {
// Sessão termina quando sai de energia
else if (!evse_state_is_power_flowing(new_state) && evse_state_is_power_flowing(prev_state))
{
end_session = true;
}
current_state = new_state;
changed = true;
}
portEXIT_CRITICAL(&state_mux);
// 2) Executa start/end de sessão FORA da região crítica, evitando logs/alloc dentro dela
if (start_session) {
// Fora da região crítica
if (start_session)
{
evse_session_start();
}
if (end_session) {
if (end_session)
{
evse_session_end();
}
// 3) Se mudou o estado, faz log e dispara evento
if (changed) {
const char *prev_str = evse_state_to_str(prev_state);
const char *curr_str = evse_state_to_str(new_state);
ESP_LOGI(TAG, "State changed: %s → %s", prev_str, curr_str);
if (changed)
{
ESP_LOGI(TAG, "State changed: %s → %s",
evse_state_to_str(prev_state),
evse_state_to_str(new_state));
evse_state_event_data_t evt = {
.state = map_state_to_event(new_state)
};
esp_event_post(EVSE_EVENTS,
EVSE_EVENT_STATE_CHANGED,
&evt,
sizeof(evt),
portMAX_DELAY);
.state = map_state_to_event(new_state)};
post_evse_event(EVSE_EVENT_STATE_CHANGED, &evt, sizeof(evt));
}
}
evse_state_t evse_get_state(void) {
evse_state_t evse_get_state(void)
{
portENTER_CRITICAL(&state_mux);
evse_state_t s = current_state;
portEXIT_CRITICAL(&state_mux);
return s;
}
const char* evse_state_to_str(evse_state_t state) {
switch (state) {
case EVSE_STATE_A: return "A - EV Not Connected (12V)";
case EVSE_STATE_B1: return "B1 - EV Connected (9V, Not Authorized)";
case EVSE_STATE_B2: return "B2 - EV Connected (9V, Authorized and Ready)";
case EVSE_STATE_C1: return "C1 - Charging Requested (6V, Relay Off)";
case EVSE_STATE_C2: return "C2 - Charging Active (6V, Relay On)";
case EVSE_STATE_D1: return "D1 - Ventilation Required (3V, Relay Off)";
case EVSE_STATE_D2: return "D2 - Ventilation Active (3V, Relay On)";
case EVSE_STATE_E: return "E - Error: Control Pilot Shorted to Ground (0V)";
case EVSE_STATE_F: return "F - Fault: EVSE Unavailable or No Pilot Signal";
default: return "Unknown State";
const char *evse_state_to_str(evse_state_t state)
{
switch (state)
{
case EVSE_STATE_A:
return "A - EV Not Connected (12V)";
case EVSE_STATE_B1:
return "B1 - EV Connected (9V, Not Authorized)";
case EVSE_STATE_B2:
return "B2 - EV Connected (9V, Authorized and Ready)";
case EVSE_STATE_C1:
return "C1 - Charging Requested (6V, Relay Off)";
case EVSE_STATE_C2:
return "C2 - Charging Active (6V, Relay On)";
case EVSE_STATE_D1:
return "D1 - Ventilation Required (3V, Relay Off)";
case EVSE_STATE_D2:
return "D2 - Ventilation Active (3V, Relay On)";
case EVSE_STATE_E:
return "E - Error: Control Pilot Shorted to Ground (0V)";
case EVSE_STATE_F:
return "F - Fault: EVSE Unavailable or No Pilot Signal";
default:
return "Unknown State";
}
}
void evse_state_init(void) {
void evse_state_init(void)
{
portENTER_CRITICAL(&state_mux);
current_state = EVSE_STATE_A;
is_authorized = true;
is_authorized = false;
portEXIT_CRITICAL(&state_mux);
ESP_LOGI("EVSE_STATE", "Initialized in state: %s", evse_state_to_str(current_state));
ESP_LOGI(TAG, "Initialized in state: %s", evse_state_to_str(current_state));
evse_state_event_data_t evt = {
.state = map_state_to_event(current_state)
};
esp_event_post(EVSE_EVENTS, EVSE_EVENT_INIT, &evt, sizeof(evt), portMAX_DELAY);
.state = map_state_to_event(current_state)};
post_evse_event(EVSE_EVENT_INIT, &evt, sizeof(evt));
}
void evse_state_tick(void) {
// Placeholder for future state logic
void evse_state_tick(void)
{
// placeholder
}
bool evse_state_is_charging(evse_state_t state) {
return state == EVSE_STATE_C1 || state == EVSE_STATE_C2;
}
bool evse_state_is_plugged(evse_state_t state) {
return state == EVSE_STATE_B1 || state == EVSE_STATE_B2 ||
state == EVSE_STATE_C1 || state == EVSE_STATE_C2 ||
state == EVSE_STATE_D1 || state == EVSE_STATE_D2;
}
bool evse_state_is_session(evse_state_t state) {
return state == EVSE_STATE_B2 || state == EVSE_STATE_C1 || state == EVSE_STATE_C2;
}
void evse_state_set_authorized(bool authorized) {
void evse_state_set_authorized(bool authorized)
{
portENTER_CRITICAL(&state_mux);
is_authorized = authorized;
portEXIT_CRITICAL(&state_mux);
}
bool evse_state_get_authorized(void) {
bool evse_state_get_authorized(void)
{
portENTER_CRITICAL(&state_mux);
bool result = is_authorized;
portEXIT_CRITICAL(&state_mux);

View File

@@ -15,11 +15,9 @@ extern "C" {
// Limites Globais (Defines)
// ========================
// Corrente máxima de carregamento (configurável pelo usuário)
#define MIN_CHARGING_CURRENT_LIMIT 6 // A
#define MAX_CHARGING_CURRENT_LIMIT 32 // A
// Corrente via cabo (proximity) — se configurável
#define MIN_CABLE_CURRENT_LIMIT 6 // A
#define MAX_CABLE_CURRENT_LIMIT 63 // A
@@ -31,23 +29,20 @@ extern "C" {
esp_err_t evse_config_init(void);
void evse_check_defaults(void);
// Corrente de carregamento
// Corrente máxima de hardware (fixa)
uint8_t evse_get_max_charging_current(void);
esp_err_t evse_set_max_charging_current(uint8_t value);
// Corrente configurável (persistida) <= max hardware
uint16_t evse_get_charging_current(void);
esp_err_t evse_set_charging_current(uint16_t value);
uint16_t evse_get_default_charging_current(void);
esp_err_t evse_set_default_charging_current(uint16_t value);
// Configuração de socket outlet
bool evse_get_socket_outlet(void);
esp_err_t evse_set_socket_outlet(bool socket_outlet);
// Corrente runtime (RAM) <= max hardware (load balancer pode alterar)
void evse_set_runtime_charging_current(uint16_t value);
uint16_t evse_get_runtime_charging_current(void);
// Socket outlet
bool evse_get_socket_outlet(void);
esp_err_t evse_set_socket_outlet(bool socket_outlet);
// RCM
bool evse_is_rcm(void);

View File

@@ -1,4 +1,3 @@
// === Início de: components/evse/include/evse_error.h ===
#ifndef EVSE_ERROR_H
#define EVSE_ERROR_H
@@ -6,11 +5,13 @@
#include <stdbool.h>
#include "evse_pilot.h"
// Bits que auto-limpam passado um timeout
#define EVSE_ERR_AUTO_CLEAR_BITS ( \
EVSE_ERR_DIODE_SHORT_BIT | \
EVSE_ERR_TEMPERATURE_HIGH_BIT | \
EVSE_ERR_RCM_TRIGGERED_BIT)
// ----------------------------------------------------
// Holdoff interno pós-erro (sem expor "cooldown" ao resto)
// ----------------------------------------------------
// Após TODOS os erros reais desaparecerem (raw_bits == 0),
// o módulo mantém o erro "visível" durante este tempo.
// Durante este período, evse_get_error() continua != 0.
#define EVSE_ERROR_COOLDOWN_MS 60000
// Error bits
#define EVSE_ERR_DIODE_SHORT_BIT (1 << 0)
@@ -30,7 +31,7 @@ void evse_error_check(pilot_voltage_t pilot_voltage, bool is_n12v);
void evse_temperature_check(void);
void evse_error_tick(void);
// Leitura e controle de erros
// Leitura e controle de erros (estado "visível" com holdoff)
uint32_t evse_get_error(void);
void evse_error_set(uint32_t bitmask);
void evse_error_clear(uint32_t bitmask);
@@ -40,12 +41,9 @@ uint32_t evse_error_get_bits(void);
// ----------------------------------------------------
// Semântica sticky: flag "todos erros limpos"
// (fica true quando o erro visível chega a 0; pode ser útil para UI/logs)
// ----------------------------------------------------
// Fica true quando TODOS os erros são limpos.
// Volta a false assim que qualquer erro novo aparece.
// Permanece true até o consumidor limpar explicitamente.
bool evse_error_cleared_flag(void);
void evse_error_reset_flag(void);
#endif // EVSE_ERROR_H
// === Fim de: components/evse/include/evse_error.h ===

View File

@@ -16,6 +16,7 @@ typedef enum {
EVSE_EVENT_ENABLE_UPDATED,
EVSE_EVENT_AVAILABLE_UPDATED,
EVSE_EVENT_SESSION,
EVSE_EVENT_ERROR_CHANGED,
} evse_event_id_t;
// -----------------
@@ -43,35 +44,41 @@ typedef enum {
typedef struct {
evse_session_event_type_t type; ///< STARTED / FINISHED
// campos básicos da sessão, em tipos simples:
uint32_t session_id; ///< opcional, se tiveres um ID
uint32_t duration_s; ///< duração em segundos (0 no STARTED)
uint32_t energy_wh; ///< energia em Wh (0 no STARTED)
uint32_t avg_power_w; ///< potência média em W (0 no STARTED)
uint32_t session_id;
uint32_t duration_s;
uint32_t energy_wh;
uint32_t avg_power_w;
bool is_current; ///< true se ainda estiver em curso
bool is_current;
} evse_session_event_data_t;
// -----------------
// Eventos de CONFIG
// -----------------
typedef struct {
bool charging; // Estado de carregamento
float hw_max_current; // Corrente máxima suportada pelo hardware
float runtime_current; // Corrente de carregamento em uso
int64_t timestamp_us; // Momento da atualização
bool charging;
float hw_max_current;
float runtime_current;
int64_t timestamp_us;
} evse_config_event_data_t;
// Eventos simples e específicos
typedef struct {
bool enabled; // novo estado de enabled
int64_t timestamp_us; // epoch micros
bool enabled;
int64_t timestamp_us;
} evse_enable_event_data_t;
typedef struct {
bool available; // novo estado de available
int64_t timestamp_us; // epoch micros
bool available;
int64_t timestamp_us;
} evse_available_event_data_t;
// -----------------
// Eventos de ERRO
// -----------------
typedef struct {
uint32_t error_bits; ///< estado atual (todos os bits de erro)
uint32_t changed_mask; ///< bits que mudaram nesta notificação
int64_t timestamp_us; ///< esp_timer_get_time()
} evse_error_event_data_t;
#endif // EVSE_EVENTS_H

View File

@@ -2,65 +2,43 @@
#define PILOT_H_
#ifdef __cplusplus
extern "C" {
extern "C"
{
#endif
#include <stdbool.h>
#include <stdint.h>
/**
* @brief Níveis categóricos de tensão no sinal CP (Control Pilot)
*/
typedef enum
{
PILOT_VOLTAGE_12, ///< Estado A: +12V
PILOT_VOLTAGE_9, ///< Estado B: +9V
PILOT_VOLTAGE_6, ///< Estado C: +6V
PILOT_VOLTAGE_3, ///< Estado D: +3V
PILOT_VOLTAGE_1 ///< Estado E/F: abaixo de 3V
} pilot_voltage_t;
typedef enum
{
PILOT_VOLTAGE_12,
PILOT_VOLTAGE_9,
PILOT_VOLTAGE_6,
PILOT_VOLTAGE_3,
PILOT_VOLTAGE_1
} pilot_voltage_t;
/**
* @brief Inicializa o driver do sinal Pilot
*/
void pilot_init(void);
void pilot_init(void);
/**
* @brief Define o nível do Pilot: +12V ou -12V
/**
* @brief Define o pilot em modo DC.
*
* @param level true = +12V, false = -12V
* @param high true = nível alto (+12V)
* false = nível baixo (-12V)
*/
void pilot_set_level(bool level);
void pilot_set_level(bool high);
/**
* @brief Ativa o PWM do Pilot com corrente limitada
*
* @param amps Corrente em ampères (ex: 16 = 16A)
*/
void pilot_set_amps(uint16_t amps);
void pilot_set_amps(uint16_t amps);
/**
* @brief Mede o nível de tensão do Pilot e detecta -12V
*
* @param up_voltage Valor categórico da tensão positiva
* @param down_voltage_n12 true se o nível negativo atingir -12V
*/
void pilot_measure(pilot_voltage_t *up_voltage, bool *down_voltage_n12);
void pilot_measure(pilot_voltage_t *up_voltage, bool *down_voltage_n12);
/**
* @brief Retorna o estado lógico atual do Pilot (nível alto = +12V)
*
* @return true se nível atual for +12V, false se for -12V
*/
bool pilot_get_state(void);
bool pilot_get_state(void);
/**
* @brief Cache interno opcional dos níveis de tensão reais do Pilot
*/
typedef struct {
uint16_t high_mv; ///< Pico positivo medido (mV)
uint16_t low_mv; ///< Pico negativo medido (mV)
} pilot_voltage_cache_t;
typedef struct
{
uint16_t high_mv;
uint16_t low_mv;
} pilot_voltage_cache_t;
#ifdef __cplusplus
}

View File

@@ -1,9 +1,3 @@
/*
* evse_session.h
* Module to track and retrieve charging session data (current or last completed),
* accumulating energy via periodic tick of instantaneous power.
*/
#ifndef EVSE_SESSION_H
#define EVSE_SESSION_H
@@ -15,39 +9,23 @@
* @brief Charging session statistics
*/
typedef struct {
TickType_t start_tick; ///< tick when session began
uint32_t duration_s; ///< total duration in seconds
uint32_t energy_wh; ///< total energy consumed in Wh
TickType_t start_tick; ///< tick when session began (debug/trace)
uint32_t duration_s; ///< total duration in seconds (tempo real)
uint32_t energy_wh; ///< total energy consumed in Wh (tempo real)
uint32_t avg_power_w; ///< average power in W
bool is_current; ///< true if session still in progress
} evse_session_t;
/**
* @brief Initialize the session module
*/
void evse_session_init(void);
/**
* @brief Mark the beginning of a charging session
*/
void evse_session_start(void);
/**
* @brief Mark the end of the charging session and store it as "last session"
*/
void evse_session_end(void);
/**
* @brief Periodic tick: must be called (e.g., each 1s) to accumulate energy from instant power
* @brief Periodic tick: called (e.g., each 1s) to accumulate energy from instant power.
* Implementação usa esp_timer (não assume 1s exato).
*/
void evse_session_tick(void);
/**
* @brief Retrieve statistics of either the current ongoing session (if any) or
* the last completed session.
* @param out pointer to evse_session_t to be filled
* @return true if there is a current or last session available, false otherwise
*/
bool evse_session_get(evse_session_t *out);
#endif // EVSE_SESSION_H

View File

@@ -6,14 +6,12 @@
#include "evse_events.h"
#ifdef __cplusplus
extern "C" {
extern "C"
{
#endif
// ============================
// EVSE Pilot Signal States
// ============================
typedef enum {
typedef enum
{
EVSE_STATE_A, // EV Not Connected (12V)
EVSE_STATE_B1, // EV Connected (9V, Not Authorized)
EVSE_STATE_B2, // EV Connected (9V, Authorized and Ready)
@@ -23,73 +21,53 @@ typedef enum {
EVSE_STATE_D2, // Ventilation Active (3V, Relay On)
EVSE_STATE_E, // Error: Pilot Short to Ground (0V)
EVSE_STATE_F // Fault: No Pilot or EVSE Unavailable
} evse_state_t;
} evse_state_t;
// ============================
// Initialization
// ============================
// Initialization
void evse_state_init(void);
void evse_state_tick(void);
/**
* @brief Initializes the EVSE state machine and default state.
// State Access & Control
evse_state_t evse_get_state(void);
void evse_set_state(evse_state_t state);
const char *evse_state_to_str(evse_state_t state);
// ---------------------------
// State Evaluation Helpers
// ---------------------------
/**
* @brief True se existe uma sessão "lógica" ativa (carro ligado e autorizado/pronto ou a carregar).
* Inclui B2, C1/C2, D1/D2.
*/
void evse_state_init(void);
bool evse_state_is_session(evse_state_t state);
/**
* @brief Periodic tick for state handling (optional hook).
/**
* @brief True se o EVSE está a fornecer energia (relé ON).
* Estados com energia: C2 e D2.
*
* Nota: isto substitui a antiga interpretação “C1/C2”.
*/
void evse_state_tick(void);
bool evse_state_is_charging(evse_state_t state);
// ============================
// State Access & Control
// ============================
/**
* @brief Returns the current EVSE state.
/**
* @brief True se o EV pediu carga mas o relé ainda está OFF (C1/D1).
*/
evse_state_t evse_get_state(void);
bool evse_state_is_requesting(evse_state_t state);
/**
* @brief Sets the current EVSE state and emits a change event if needed.
/**
* @brief True se há fluxo de energia (alias explícito para charging).
*/
void evse_set_state(evse_state_t state);
bool evse_state_is_power_flowing(evse_state_t state);
/**
* @brief Converts the state enum into a human-readable string.
/**
* @brief True se o EV está fisicamente ligado (B1 e além).
*/
const char* evse_state_to_str(evse_state_t state);
bool evse_state_is_plugged(evse_state_t state);
// ============================
// State Evaluation Helpers
// ============================
/**
* @brief True if EV is in an active session (B2, C1, C2).
*/
bool evse_state_is_session(evse_state_t state);
/**
* @brief True if EV is actively charging (C1, C2).
*/
bool evse_state_is_charging(evse_state_t state);
/**
* @brief True if EV is physically plugged in (B1 and beyond).
*/
bool evse_state_is_plugged(evse_state_t state);
// ============================
// Authorization Control
// ============================
/**
* @brief Sets whether the EV is authorized to charge.
*/
void evse_state_set_authorized(bool authorized);
/**
* @brief Gets whether the EV is currently authorized.
*/
bool evse_state_get_authorized(void);
// Authorization Control
void evse_state_set_authorized(bool authorized);
bool evse_state_get_authorized(void);
#ifdef __cplusplus
}

View File

@@ -9,8 +9,8 @@ set(srcs
idf_component_register(SRCS "${srcs}"
INCLUDE_DIRS "include"
PRIV_REQUIRES driver esp_timer nvs_flash
REQUIRES config evse loadbalancer)
PRIV_REQUIRES driver esp_timer
REQUIRES config evse loadbalancer storage_service)

View File

@@ -1,3 +1,4 @@
// === Início de: components/evse_link/include/evse_link.h ===
#ifndef EVSE_LINK_H_
#define EVSE_LINK_H_
@@ -43,3 +44,5 @@ void evse_link_set_enabled(bool enabled);
bool evse_link_is_enabled(void);
#endif // EVSE_LINK_H_
// === Fim de: components/evse_link/include/evse_link.h ===

View File

@@ -1,31 +1,29 @@
// === Início de: components/evse_link/include/evse_link_events.h ===
#ifndef EVSE_LINK_EVENTS_H_
#define EVSE_LINK_EVENTS_H_
#include "esp_event.h"
#include <stdint.h>
// Base de eventos do EVSE-Link
ESP_EVENT_DECLARE_BASE(EVSE_LINK_EVENTS);
// Tamanho máximo de tag propagada via EVSE-Link (inclui NUL)
#define EVSE_LINK_TAG_MAX_LEN 32
// IDs de eventos EVSE-Link
typedef enum {
LINK_EVENT_FRAME_RECEIVED, // qualquer frame válido
LINK_EVENT_SLAVE_ONLINE, // heartbeat recebido primeira vez
LINK_EVENT_SLAVE_OFFLINE, // sem heartbeat no timeout
LINK_EVENT_MASTER_POLL_SENT, // opcional: poll enviado pelo master
LINK_EVENT_FRAME_RECEIVED,
LINK_EVENT_SLAVE_ONLINE, // payload: evse_link_slave_presence_event_t
LINK_EVENT_SLAVE_OFFLINE, // payload: evse_link_slave_presence_event_t (master-side) ou NULL (slave-side fallback)
LINK_EVENT_MASTER_POLL_SENT,
LINK_EVENT_CURRENT_LIMIT_APPLIED,
LINK_EVENT_SLAVE_CONFIG_UPDATED, // config atualizada pelo master
LINK_EVENT_REMOTE_AUTH_GRANTED // autorização remota (master -> slave)
LINK_EVENT_SLAVE_CONFIG_UPDATED,
LINK_EVENT_REMOTE_AUTH_GRANTED
} evse_link_event_t;
// Payload para LINK_EVENT_REMOTE_AUTH_GRANTED
typedef struct {
char tag[EVSE_LINK_TAG_MAX_LEN]; // idTag enviada pelo master
char tag[EVSE_LINK_TAG_MAX_LEN];
} evse_link_auth_grant_event_t;
#endif // EVSE_LINK_EVENTS_H_
typedef struct {
uint8_t slave_id;
} evse_link_slave_presence_event_t;
// === Fim de: components/evse_link/include/evse_link_events.h ===
#endif // EVSE_LINK_EVENTS_H_

View File

@@ -6,19 +6,19 @@
#include "driver/uart.h"
// UART instance and configuration
#define UART_PORT UART_NUM_2 // Usa a UART2
#define UART_BAUDRATE 115200
#define UART_PORT UART_NUM_2
#define UART_BAUDRATE 9600
#define UART_RX_BUF_SIZE 256
// GPIO pin assignments for UART (ajuste conforme o hardware)
#define UART_TXD 17 // TX -> DI do MAX3485
#define UART_RXD 16 // RX -> RO do MAX3485
#define UART_RTS 2 // RTS -> DE+RE do MAX3485
// GPIO pin assignments for RS-485 UART
// Ajuste conforme seu hardware
#define MB_UART_TXD 17
#define MB_UART_RXD 16
#define MB_UART_RTS 2 // pino DE/RE do transceiver RS-485
// Conveniência: nomes usados no .c
#define TX_PIN UART_TXD
#define RX_PIN UART_RXD
#define RTS_PIN UART_RTS
#define TX_PIN MB_UART_TXD
#define RX_PIN MB_UART_RXD
#define RTS_PIN MB_UART_RTS
// Frame delimiters
#define MAGIC_START 0x7E

View File

@@ -1,177 +1,200 @@
// components/evse_link/src/evse_link.c
//
// Camada de transporte EVSE-Link:
// - carrega config (mode/self_id/enabled)
// - init do framing
// - task RX (UART -> framing)
// - entrega frames completos ao callback registado
//
// NOTA: a logica de protocolo (CMD_POLL / ACK / etc.) deve ficar em
// evse_link_master.c / evse_link_slave.c.
#include "evse_link.h"
#include "evse_link_framing.h"
#include "driver/uart.h"
#include "nvs.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include <stdbool.h>
#include <stdint.h>
#include "storage_service.h"
static const char *TAG = "evse_link";
// NVS keys
#define _NVS_NAMESPACE "evse_link"
#define _NVS_MODE_KEY "mode"
#define _NVS_ID_KEY "self_id"
#define _NVS_ENABLED_KEY "enabled"
#define _KEY_MODE "mode"
#define _KEY_SELF_ID "self_id"
#define _KEY_ENABLED "enabled"
// UART parameters
#define UART_PORT UART_NUM_2
#define UART_RX_BUF_SIZE 256
// Runtime config
static evse_link_mode_t _mode = EVSE_LINK_MODE_MASTER;
static uint8_t _self_id = 0x01;
static bool _enabled = false;
// Registered Rx callback
static evse_link_rx_cb_t _rx_cb = NULL;
static bool s_evse_link_inited = false;
// Forward declarations
extern void evse_link_master_init(void);
extern void evse_link_slave_init(void);
static void framing_rx_cb(uint8_t src, uint8_t dest,
const uint8_t *payload, uint8_t len)
{
ESP_LOGD(TAG, "framing_rx_cb: src=0x%02X dest=0x%02X len=%u", src, dest, len);
if (_rx_cb)
{
_rx_cb(src, dest, payload, len);
}
}
// Register protocol-level Rx callback
void evse_link_register_rx_cb(evse_link_rx_cb_t cb)
{
_rx_cb = cb;
}
// Load config from NVS
enum
{
EV_OK = ESP_OK
};
static void load_link_config(void)
{
nvs_handle_t handle;
if (nvs_open(_NVS_NAMESPACE, NVS_READONLY, &handle) != EV_OK)
{
ESP_LOGW(TAG, "NVS open failed, using defaults");
return;
}
uint8_t mode, id, en;
if (nvs_get_u8(handle, _NVS_MODE_KEY, &mode) == EV_OK &&
(mode == EVSE_LINK_MODE_MASTER || mode == EVSE_LINK_MODE_SLAVE))
{
_mode = (evse_link_mode_t)mode;
}
if (nvs_get_u8(handle, _NVS_ID_KEY, &id) == EV_OK)
{
_self_id = id;
}
if (nvs_get_u8(handle, _NVS_ENABLED_KEY, &en) == EV_OK)
{
_enabled = (en != 0);
}
nvs_close(handle);
}
uint8_t u8 = 0;
// Save config to NVS
static void save_link_config(void)
{
nvs_handle_t handle;
if (nvs_open(_NVS_NAMESPACE, NVS_READWRITE, &handle) == EV_OK)
{
nvs_set_u8(handle, _NVS_MODE_KEY, (uint8_t)_mode);
nvs_set_u8(handle, _NVS_ID_KEY, _self_id);
nvs_set_u8(handle, _NVS_ENABLED_KEY, _enabled ? 1 : 0);
nvs_commit(handle);
nvs_close(handle);
}
esp_err_t err = storage_get_u8_sync(_NVS_NAMESPACE, _KEY_MODE, &u8, pdMS_TO_TICKS(500));
if (err == ESP_OK && (u8 == (uint8_t)EVSE_LINK_MODE_MASTER || u8 == (uint8_t)EVSE_LINK_MODE_SLAVE))
_mode = (evse_link_mode_t)u8;
else
{
ESP_LOGE(TAG, "Failed to save NVS");
_mode = EVSE_LINK_MODE_MASTER;
(void)storage_set_u8_async(_NVS_NAMESPACE, _KEY_MODE, (uint8_t)_mode);
ESP_LOGW(TAG, "Missing/invalid mode (%s) -> default MASTER", esp_err_to_name(err));
}
err = storage_get_u8_sync(_NVS_NAMESPACE, _KEY_SELF_ID, &u8, pdMS_TO_TICKS(500));
if (err == ESP_OK)
_self_id = u8;
else
{
_self_id = 0x01;
(void)storage_set_u8_async(_NVS_NAMESPACE, _KEY_SELF_ID, _self_id);
ESP_LOGW(TAG, "Missing self_id (%s) -> default 0x%02X", esp_err_to_name(err), _self_id);
}
err = storage_get_u8_sync(_NVS_NAMESPACE, _KEY_ENABLED, &u8, pdMS_TO_TICKS(500));
if (err == ESP_OK && u8 <= 1)
_enabled = (u8 != 0);
else
{
_enabled = false;
(void)storage_set_u8_async(_NVS_NAMESPACE, _KEY_ENABLED, 0);
ESP_LOGW(TAG, "Missing/invalid enabled (%s) -> default false", esp_err_to_name(err));
}
}
// Getters/setters
static void save_link_config(void)
{
(void)storage_set_u8_async(_NVS_NAMESPACE, _KEY_MODE, (uint8_t)_mode);
(void)storage_set_u8_async(_NVS_NAMESPACE, _KEY_SELF_ID, _self_id);
(void)storage_set_u8_async(_NVS_NAMESPACE, _KEY_ENABLED, _enabled ? 1 : 0);
}
void evse_link_set_mode(evse_link_mode_t m)
{
if (m != EVSE_LINK_MODE_MASTER && m != EVSE_LINK_MODE_SLAVE)
{
ESP_LOGW(TAG, "Invalid link mode: %d", (int)m);
return;
}
if (_mode == m)
return;
_mode = m;
save_link_config();
}
evse_link_mode_t evse_link_get_mode(void) { return _mode; }
void evse_link_set_self_id(uint8_t id)
{
if (_self_id == id)
return;
_self_id = id;
save_link_config();
}
uint8_t evse_link_get_self_id(void) { return _self_id; }
void evse_link_set_enabled(bool en)
{
if (_enabled == en)
return;
_enabled = en;
save_link_config();
}
bool evse_link_is_enabled(void) { return _enabled; }
// RX task: reads bytes from UART and feeds framing
static void evse_link_rx_task(void *arg)
{
(void)arg;
ESP_LOGI(TAG, "evse_link_rx_task started");
uint8_t buf[UART_RX_BUF_SIZE];
while (true)
{
int len = uart_read_bytes(UART_PORT, buf, sizeof(buf), pdMS_TO_TICKS(1000));
if (len > 0)
{
ESP_LOGD(TAG, "UART RX: len=%d first=0x%02X last=0x%02X", len, buf[0], buf[len - 1]);
for (int i = 0; i < len; ++i)
{
evse_link_recv_byte(buf[i]);
}
}
}
}
// Initialize EVSE-Link component
void evse_link_init(void)
{
if (s_evse_link_inited)
{
ESP_LOGW(TAG, "evse_link_init called twice; ignoring");
return;
}
s_evse_link_inited = true;
esp_err_t se = storage_service_init();
if (se == ESP_OK)
load_link_config();
else
ESP_LOGE(TAG, "storage_service_init failed: %s (defaults in RAM)", esp_err_to_name(se));
ESP_LOGI(TAG, "Link init: mode=%c id=0x%02X enabled=%d",
_mode == EVSE_LINK_MODE_MASTER ? 'M' : 'S',
_self_id, _enabled);
if (!_enabled)
return;
// 1) framing layer init (sets up mutex, UART driver, etc.)
evse_link_framing_init();
evse_link_framing_register_cb(framing_rx_cb);
// 2) start RX task
xTaskCreate(evse_link_rx_task, "evse_link_rx", 4096, NULL, 4, NULL);
if (xTaskCreate(evse_link_rx_task, "evse_link_rx", 4096, NULL, 4, NULL) != pdPASS)
{
ESP_LOGE(TAG, "Failed to create evse_link_rx task");
return;
}
// 3) delegate to master or slave
if (_mode == EVSE_LINK_MODE_MASTER)
{
evse_link_master_init();
}
else
{
evse_link_slave_init();
}
}
// Send a frame (delegates to framing module)
bool evse_link_send(uint8_t dest, const uint8_t *payload, uint8_t len)
{
if (!evse_link_is_enabled())
return false;
uint8_t src = evse_link_get_self_id();
return evse_link_framing_send(dest, src, payload, len);
}
// Receive byte (delegates to framing module)
void evse_link_recv_byte(uint8_t byte)
{
evse_link_framing_recv_byte(byte);

View File

@@ -1,8 +1,29 @@
// components/evse_link/src/evse_link_framing.c
//
// EVSE-Link framing (RS-485 HALF DUPLEX via UART driver)
// - Usa UART_MODE_RS485_HALF_DUPLEX (driver controla RTS => DE//RE)
// - Configura RX timeout + RX full threshold para evitar “len=120”
// - Remove controlo manual de GPIO do RTS
//
// Requisitos:
// - MAX3485 com DE e /RE juntos ligados ao RTS_PIN
// - RTS_PIN definido em evse_link_framing.h (ex.: GPIO2; recomendado mudar no futuro)
//
// Notas:
// - Se o teu hardware inverter a lógica do RTS (raro), define EVSE_LINK_RTS_INVERT=1
#include "evse_link_framing.h"
#include "driver/uart.h"
#include "freertos/semphr.h"
#include "esp_log.h"
#include "esp_err.h"
#include "esp_timer.h"
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
static const char *TAG = "evse_framing";
@@ -10,36 +31,60 @@ static SemaphoreHandle_t tx_mutex = NULL;
static uint8_t seq = 0;
static evse_link_frame_cb_t rx_cb = NULL;
// CRC-8 (polynomial 0x07)
static uint8_t crc8(const uint8_t *data, uint8_t len)
static bool s_framing_inited = false;
// ---- Tunables (fallbacks) ----
#ifndef EVSE_LINK_INTERBYTE_TIMEOUT_US
// Timeout inter-byte: se um frame morrer a meio, reseta o parser
#define EVSE_LINK_INTERBYTE_TIMEOUT_US 5000
#endif
// Rate-limit para warnings (em microsegundos)
#define LOG_RATELIMIT_US 1000000 // 1s
// RX tuning (evita acumular ~120 bytes antes de "acordar")
#ifndef EVSE_LINK_RX_TIMEOUT
// Timeout do UART TOUT feature (em "character times"). 3..10 funciona bem.
#define EVSE_LINK_RX_TIMEOUT 3
#endif
#ifndef EVSE_LINK_RX_FULL_THRESH
// Gera interrupção quando FIFO tem pelo menos N bytes (1..120). 4 é um bom default.
#define EVSE_LINK_RX_FULL_THRESH 1
#endif
#ifndef EVSE_LINK_RTS_INVERT
// Se precisares inverter RTS (muito raro), define para 1 no build.
#define EVSE_LINK_RTS_INVERT 0
#endif
static inline bool log_ratelimit_ok(int64_t *last_us, int64_t interval_us)
{
uint8_t crc = 0;
for (uint8_t i = 0; i < len; ++i) {
crc ^= data[i];
for (uint8_t b = 0; b < 8; ++b) {
if (crc & 0x80) {
const int64_t now = esp_timer_get_time();
if (*last_us == 0 || (now - *last_us) > interval_us)
{
*last_us = now;
return true;
}
return false;
}
// CRC-8 (poly 0x07), MSB-first, init=0x00
static uint8_t crc8_update(uint8_t crc, uint8_t data)
{
crc ^= data;
for (uint8_t b = 0; b < 8; ++b)
{
if (crc & 0x80)
crc = (uint8_t)((crc << 1) ^ 0x07);
} else {
else
crc <<= 1;
}
}
}
return crc;
}
void evse_link_framing_init(void)
static esp_err_t configure_uart(void)
{
// Mutex para proteger TX (framings de várias tasks)
tx_mutex = xSemaphoreCreateMutex();
// Instala driver UART
uart_driver_install(UART_PORT,
UART_RX_BUF_SIZE * 2, // RX buffer
0, // TX buffer (0 = usa buffer interno)
0,
NULL,
0);
uart_config_t cfg = {
.baud_rate = UART_BAUDRATE,
.data_bits = UART_DATA_8_BITS,
@@ -48,80 +93,190 @@ void evse_link_framing_init(void)
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_DEFAULT,
};
uart_param_config(UART_PORT, &cfg);
// Define pinos: TX, RX e RTS (RTS ligado a DE+RE do transceiver RS485)
uart_set_pin(UART_PORT,
TX_PIN, // MB_UART_TXD (ex: GPIO17)
RX_PIN, // MB_UART_RXD (ex: GPIO16)
RTS_PIN, // MB_UART_RTS (ex: GPIO2, DE+RE)
UART_PIN_NO_CHANGE);
esp_err_t err = uart_param_config(UART_PORT, &cfg);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "uart_param_config failed: %s", esp_err_to_name(err));
return err;
}
// Modo RS485 half-duplex: driver controla RTS/DE/RE automaticamente
uart_set_mode(UART_PORT, UART_MODE_RS485_HALF_DUPLEX);
// TX/RX/RTS na UART (RTS controla DE//RE em RS485 half-duplex)
err = uart_set_pin(UART_PORT, TX_PIN, RX_PIN, RTS_PIN, UART_PIN_NO_CHANGE);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "uart_set_pin failed: %s", esp_err_to_name(err));
return err;
}
ESP_LOGI(TAG, "Framing init: UART%d TX=%d RX=%d RTS(DE/RE)=%d baud=%d",
// RS-485 HALF DUPLEX (driver controla RTS automaticamente)
err = uart_set_mode(UART_PORT, UART_MODE_RS485_HALF_DUPLEX);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "uart_set_mode(RS485_HALF_DUPLEX) failed: %s", esp_err_to_name(err));
return err;
}
// Ajustes para RX responsivo (evita "len=120")
(void)uart_set_rx_full_threshold(UART_PORT, EVSE_LINK_RX_FULL_THRESH);
(void)uart_set_rx_timeout(UART_PORT, EVSE_LINK_RX_TIMEOUT);
// Opcional: inverter RTS se hardware exigir
if (EVSE_LINK_RTS_INVERT)
{
(void)uart_set_line_inverse(UART_PORT, UART_SIGNAL_RTS_INV);
ESP_LOGW(TAG, "RS485 driver: RTS inverted");
}
ESP_LOGW(TAG, "RS485 driver enabled: UART%d TX=%d RX=%d RTS=%d baud=%d rx_to=%d rx_thresh=%d",
UART_PORT, TX_PIN, RX_PIN, RTS_PIN, UART_BAUDRATE,
EVSE_LINK_RX_TIMEOUT, EVSE_LINK_RX_FULL_THRESH);
return ESP_OK;
}
void evse_link_framing_init(void)
{
if (s_framing_inited)
{
ESP_LOGI(TAG, "Framing already initialized");
return;
}
if (!tx_mutex)
{
tx_mutex = xSemaphoreCreateMutex();
if (!tx_mutex)
{
ESP_LOGE(TAG, "Failed to create TX mutex");
return;
}
}
// Se o driver já estiver instalado, só reconfigura e aplica RX tuning.
if (uart_is_driver_installed(UART_PORT))
{
esp_err_t err = configure_uart();
if (err == ESP_OK)
{
s_framing_inited = true;
ESP_LOGW(TAG, "UART%d driver already installed -> configured for RS485 HALF DUPLEX", UART_PORT);
(void)uart_flush_input(UART_PORT);
}
else
{
ESP_LOGE(TAG, "Failed to configure already-installed UART%d", UART_PORT);
}
return;
}
// Instala driver UART
esp_err_t err = uart_driver_install(UART_PORT,
UART_RX_BUF_SIZE * 2, // RX buffer (ringbuffer)
0, // TX buffer (não usado)
0, // event queue size
NULL, // event queue
0);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "uart_driver_install failed: %s", esp_err_to_name(err));
return;
}
err = configure_uart();
if (err != ESP_OK)
return;
(void)uart_flush_input(UART_PORT);
s_framing_inited = true;
ESP_LOGI(TAG, "Framing init (RS485 driver): UART%d TX=%d RX=%d RTS=%d baud=%d",
UART_PORT, TX_PIN, RX_PIN, RTS_PIN, UART_BAUDRATE);
}
bool evse_link_framing_send(uint8_t dest, uint8_t src,
const uint8_t *payload, uint8_t len)
{
if (len > EVSE_LINK_MAX_PAYLOAD) {
ESP_LOGW(TAG, "Payload too large: %u (max=%u)",
len, EVSE_LINK_MAX_PAYLOAD);
if (!s_framing_inited)
{
ESP_LOGE(TAG, "Framing not initialized");
return false;
}
if (xSemaphoreTake(tx_mutex, portMAX_DELAY) != pdTRUE) {
if (len > EVSE_LINK_MAX_PAYLOAD)
{
ESP_LOGW(TAG, "Payload too large: %u (max=%u)", len, EVSE_LINK_MAX_PAYLOAD);
return false;
}
if (len > 0 && payload == NULL)
{
ESP_LOGW(TAG, "Invalid send: len=%u but payload=NULL", len);
return false;
}
if (!tx_mutex)
{
ESP_LOGE(TAG, "TX mutex is NULL (framing_init not called?)");
return false;
}
if (xSemaphoreTake(tx_mutex, portMAX_DELAY) != pdTRUE)
{
ESP_LOGW(TAG, "Failed to take TX mutex");
return false;
}
// Frame: START | DEST | SRC | LEN | SEQ | PAYLOAD | CRC | END
// LEN on wire = SEQ + PAYLOAD (>=1)
uint8_t frame[EVSE_LINK_MAX_PAYLOAD + 7];
int idx = 0;
frame[idx++] = MAGIC_START;
frame[idx++] = dest;
frame[idx++] = src;
frame[idx++] = (uint8_t)(len + 1); // LEN = SEQ + payload
frame[idx++] = (uint8_t)(len + 1);
frame[idx++] = seq;
if (len > 0 && payload != NULL) {
if (len > 0)
{
memcpy(&frame[idx], payload, len);
idx += len;
}
// CRC cobre: DEST + SRC + LEN + SEQ + PAYLOAD
uint8_t crc_input[3 + 1 + EVSE_LINK_MAX_PAYLOAD];
memcpy(crc_input, &frame[1], 3 + 1 + len);
uint8_t crc = crc8(crc_input, (uint8_t)(3 + 1 + len));
frame[idx++] = crc;
// CRC: DEST + SRC + LEN + SEQ + PAYLOAD
uint8_t crc = 0;
crc = crc8_update(crc, dest);
crc = crc8_update(crc, src);
crc = crc8_update(crc, (uint8_t)(len + 1));
crc = crc8_update(crc, seq);
for (uint8_t i = 0; i < len; ++i)
crc = crc8_update(crc, payload[i]);
frame[idx++] = crc;
frame[idx++] = MAGIC_END;
// Envia frame completo
int written = uart_write_bytes(UART_PORT, (const char *)frame, idx);
if (written != idx) {
if (written != idx)
{
ESP_LOGW(TAG, "uart_write_bytes wrote %d of %d", written, idx);
}
// Aguarda TX terminar (o driver controla DE/RE via RTS)
uart_wait_tx_done(UART_PORT, pdMS_TO_TICKS(20));
// Aguarda TX terminar; driver RS485 devolve RTS para RX automaticamente.
(void)uart_wait_tx_done(UART_PORT, pdMS_TO_TICKS(700));
xSemaphoreGive(tx_mutex);
ESP_LOGI(TAG, "Sent frame dest=0x%02X src=0x%02X len=%u seq=%u",
ESP_LOGD(TAG, "Sent frame dest=0x%02X src=0x%02X len=%u seq=%u",
dest, src, len, seq);
seq++; // incrementa sequência após envio
seq++;
return true;
}
void evse_link_framing_recv_byte(uint8_t b)
{
// Máquina de estados para parsing do frame
static enum {
ST_WAIT_START = 0,
ST_WAIT_DEST,
@@ -135,15 +290,44 @@ void evse_link_framing_recv_byte(uint8_t b)
static uint8_t rx_dest;
static uint8_t rx_src;
static uint8_t rx_len; // inclui SEQ + payload
static uint8_t rx_len; // inclui SEQ + payload (>=1)
static uint8_t rx_seq;
static uint8_t rx_buf[EVSE_LINK_MAX_PAYLOAD];
static uint8_t rx_pos;
static uint8_t rx_crc;
switch (rx_state) {
static int64_t s_last_byte_us = 0;
static int64_t s_last_bad_len_log_us = 0;
static int64_t s_last_bad_crc_log_us = 0;
#define RESET_PARSER() \
do \
{ \
rx_state = ST_WAIT_START; \
rx_dest = 0; \
rx_src = 0; \
rx_len = 0; \
rx_seq = 0; \
rx_pos = 0; \
rx_crc = 0; \
} while (0)
const int64_t now_us = esp_timer_get_time();
// Timeout inter-byte: frame morreu a meio -> reseta
if (rx_state != ST_WAIT_START && s_last_byte_us != 0 &&
(now_us - s_last_byte_us) > EVSE_LINK_INTERBYTE_TIMEOUT_US)
{
RESET_PARSER();
}
s_last_byte_us = now_us;
switch (rx_state)
{
case ST_WAIT_START:
if (b == MAGIC_START) {
if (b == MAGIC_START)
{
rx_pos = 0;
rx_state = ST_WAIT_DEST;
}
break;
@@ -159,28 +343,50 @@ void evse_link_framing_recv_byte(uint8_t b)
break;
case ST_WAIT_LEN:
rx_len = b; // LEN = SEQ + payload
rx_len = b;
// rx_len = SEQ + payload => >=1 e <= MAX+1
if (rx_len < 1 || rx_len > (uint8_t)(EVSE_LINK_MAX_PAYLOAD + 1))
{
if (log_ratelimit_ok(&s_last_bad_len_log_us, LOG_RATELIMIT_US))
{
ESP_LOGW(TAG, "Invalid LEN=%u (max=%u), dropping frame",
rx_len, (unsigned)(EVSE_LINK_MAX_PAYLOAD + 1));
}
RESET_PARSER();
break;
}
rx_pos = 0;
rx_state = ST_WAIT_SEQ;
break;
case ST_WAIT_SEQ:
rx_seq = b;
if (rx_len > 1) {
rx_state = ST_READING;
} else {
rx_state = ST_WAIT_CRC;
}
rx_state = (rx_len > 1) ? ST_READING : ST_WAIT_CRC;
break;
case ST_READING:
if (rx_pos < EVSE_LINK_MAX_PAYLOAD) {
rx_buf[rx_pos++] = b;
}
if (rx_pos >= (uint8_t)(rx_len - 1)) { // payload completo
rx_state = ST_WAIT_CRC;
{
const uint8_t payload_len = (uint8_t)(rx_len - 1);
if (payload_len > EVSE_LINK_MAX_PAYLOAD)
{
if (log_ratelimit_ok(&s_last_bad_len_log_us, LOG_RATELIMIT_US))
{
ESP_LOGW(TAG, "Payload len too big: %u", (unsigned)payload_len);
}
RESET_PARSER();
break;
}
if (rx_pos < EVSE_LINK_MAX_PAYLOAD)
rx_buf[rx_pos++] = b;
if (rx_pos >= payload_len)
rx_state = ST_WAIT_CRC;
break;
}
case ST_WAIT_CRC:
rx_crc = b;
@@ -188,41 +394,44 @@ void evse_link_framing_recv_byte(uint8_t b)
break;
case ST_WAIT_END:
if (b == MAGIC_END) {
// Monta buffer para verificar CRC:
// DEST + SRC + LEN + SEQ + PAYLOAD
uint8_t temp[3 + 1 + EVSE_LINK_MAX_PAYLOAD];
int temp_len = 0;
temp[temp_len++] = rx_dest;
temp[temp_len++] = rx_src;
temp[temp_len++] = rx_len;
temp[temp_len++] = rx_seq;
if (rx_len > 1) {
memcpy(&temp[temp_len], rx_buf, rx_len - 1);
temp_len += rx_len - 1;
}
if (b == MAGIC_END)
{
uint8_t expected = 0;
expected = crc8_update(expected, rx_dest);
expected = crc8_update(expected, rx_src);
expected = crc8_update(expected, rx_len);
expected = crc8_update(expected, rx_seq);
uint8_t expected = crc8(temp, (uint8_t)temp_len);
if (expected == rx_crc) {
uint8_t payload_len = (uint8_t)(rx_len - 1); // exclui SEQ
if (rx_cb) {
const uint8_t payload_len = (uint8_t)(rx_len - 1);
for (uint8_t i = 0; i < payload_len; ++i)
expected = crc8_update(expected, rx_buf[i]);
if (expected == rx_crc)
{
if (rx_cb)
rx_cb(rx_src, rx_dest, rx_buf, payload_len);
}
ESP_LOGD(TAG, "Frame OK src=0x%02X dest=0x%02X len=%u seq=%u",
rx_src, rx_dest, payload_len, rx_seq);
} else {
}
else
{
if (log_ratelimit_ok(&s_last_bad_crc_log_us, LOG_RATELIMIT_US))
{
ESP_LOGW(TAG, "CRC mismatch: expected=0x%02X got=0x%02X",
expected, rx_crc);
}
}
// Em qualquer caso, volta a esperar novo frame
rx_state = ST_WAIT_START;
}
RESET_PARSER();
break;
default:
rx_state = ST_WAIT_START;
RESET_PARSER();
break;
}
#undef RESET_PARSER
}
void evse_link_framing_register_cb(evse_link_frame_cb_t cb)

View File

@@ -1,10 +1,27 @@
// components/evse_link/src/evse_link_master.c
//
// Correções aplicadas:
// 1) Evitar TX dentro do callback RX (ACK deferido para task -> não bloqueia RX)
// 2) Dedupe de ACK por slave (evita rajadas se RX vier em chunks / queue acumular)
// 3) Log quando ACK queue enche (antes era silencioso)
// 4) Proteção concorrente no s_presence (int64_t não é atómico no ESP32 32-bit)
// 5) Comentário do poll corrigido + opção de jitter no ACK
//
// NOTA: Mantive o teu comportamento (POLL a cada 10s). Se quiseres, muda para 30s.
#include "evse_link.h"
#include "evse_link_events.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/timers.h"
#include "freertos/queue.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_event.h"
#include "esp_timer.h"
#include "esp_random.h"
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
@@ -14,37 +31,180 @@
static const char *TAG = "evse_link_master";
// Link commands
// Link commands (opcode no payload[0])
#define CMD_POLL 0x01
#define CMD_HEARTBEAT 0x02
#define CMD_HEARTBEAT_ACK 0x09
#define CMD_CONFIG_BROADCAST 0x03
#define CMD_SET_CURRENT 0x08
#define CMD_AUTH_GRANTED 0x0A // novo: master concede autorização a slave
#define CMD_HEARTBEAT_ACK 0x09
#define CMD_AUTH_GRANTED 0x0A
// payload lengths (exclui byte de opcode)
#define LEN_POLL_REQ 1 // [ CMD_POLL ]
#define LEN_POLL_RESP 9 // [ CMD_POLL, float V(4), float I(4) ]
// payload lengths (INCLUI opcode)
#define LEN_HEARTBEAT 6 // [ CMD_HEARTBEAT, charging, hw_max_lo, hw_max_hi, run_lo, run_hi ]
#define LEN_CONFIG_BROADCAST 2 // [ CMD_CONFIG_BROADCAST, new_max_current ]
#define LEN_SET_CURRENT 3 // [ CMD_SET_CURRENT, limit_lo, limit_hi ]
#define LEN_HEARTBEAT_ACK 1
#define LEN_CONFIG_BROADCAST 2 // [ CMD_CONFIG_BROADCAST, ... ]
#define LEN_SET_CURRENT 3 // [ CMD_SET_CURRENT, amps_lo, amps_hi ]
// Presence monitoring
#define PRESENCE_CHECK_MS 5000
#define SLAVE_OFFLINE_TIMEOUT_MS 180000
// ACK defer
#define ACK_QUEUE_LEN 16
// backoff para espaçar ACK (evita burst)
// antes era 0..15ms; aqui fica ligeiramente mais suave.
#define ACK_BACKOFF_MIN_MS 5
#define ACK_BACKOFF_MAX_MS 35 // jitter 5..35ms
typedef struct
{
int64_t last_seen_us;
bool online;
} slave_presence_t;
static slave_presence_t s_presence[256];
static TimerHandle_t s_presence_timer = NULL;
// Proteção concorrente (int64_t não é atómico no ESP32)
static portMUX_TYPE s_presence_mux = portMUX_INITIALIZER_UNLOCKED;
// polling / heartbeat timers interval
typedef struct
{
TimerHandle_t timer;
TickType_t interval;
} timer_def_t;
static timer_def_t poll_timer = {.timer = NULL, .interval = pdMS_TO_TICKS(30000)};
static timer_def_t hb_timer = {.timer = NULL, .interval = pdMS_TO_TICKS(30000)};
// POLL a cada 60s (comentário corrigido)
static timer_def_t poll_timer = {.timer = NULL, .interval = pdMS_TO_TICKS(60000)};
static bool s_handlers_registered = false;
// ACK task/queue
static QueueHandle_t s_ack_q = NULL;
static TaskHandle_t s_ack_task = NULL;
// Dedupe: 1 ACK pendente por slave
static bool s_ack_pending[256] = {0};
static void post_presence_event(evse_link_event_t evt, uint8_t slave_id)
{
evse_link_slave_presence_event_t p = {.slave_id = slave_id};
(void)esp_event_post(EVSE_LINK_EVENTS, evt, &p, sizeof(p), portMAX_DELAY);
}
static void mark_slave_seen(uint8_t slave_id)
{
const int64_t now = esp_timer_get_time();
bool was_offline = false;
portENTER_CRITICAL(&s_presence_mux);
slave_presence_t *p = &s_presence[slave_id];
p->last_seen_us = now;
if (!p->online)
{
p->online = true;
was_offline = true;
}
portEXIT_CRITICAL(&s_presence_mux);
if (was_offline)
{
ESP_LOGI(TAG, "Slave 0x%02X ONLINE", slave_id);
post_presence_event(LINK_EVENT_SLAVE_ONLINE, slave_id);
}
}
static void presence_timer_cb(TimerHandle_t xTimer)
{
(void)xTimer;
const int64_t now = esp_timer_get_time();
const int64_t timeout_us = (int64_t)SLAVE_OFFLINE_TIMEOUT_MS * 1000;
const uint8_t self = evse_link_get_self_id();
for (int i = 0; i < 256; ++i)
{
if ((uint8_t)i == self)
continue;
bool online;
int64_t last_seen;
portENTER_CRITICAL(&s_presence_mux);
online = s_presence[i].online;
last_seen = s_presence[i].last_seen_us;
portEXIT_CRITICAL(&s_presence_mux);
if (!online)
continue;
if (last_seen > 0 && (now - last_seen) > timeout_us)
{
portENTER_CRITICAL(&s_presence_mux);
s_presence[i].online = false;
portEXIT_CRITICAL(&s_presence_mux);
ESP_LOGW(TAG, "Slave 0x%02X OFFLINE (no heartbeat for %d ms)", i, SLAVE_OFFLINE_TIMEOUT_MS);
post_presence_event(LINK_EVENT_SLAVE_OFFLINE, (uint8_t)i);
}
}
}
// Enfileira ACK sem duplicar por slave
static void enqueue_ack(uint8_t slave_id)
{
if (!s_ack_q)
return;
// Dedupe: se já existe ACK pendente para este slave, não enfileira outro
if (s_ack_pending[slave_id])
return;
s_ack_pending[slave_id] = true;
if (xQueueSendToBack(s_ack_q, &slave_id, 0) != pdTRUE)
{
s_ack_pending[slave_id] = false;
ESP_LOGW(TAG, "ACK queue full, dropping ACK for 0x%02X", slave_id);
}
}
// --- ACK task (não bloqueia RX) ---
static void ack_task(void *arg)
{
(void)arg;
for (;;)
{
uint8_t slave_id = 0;
if (xQueueReceive(s_ack_q, &slave_id, portMAX_DELAY) != pdTRUE)
continue;
// libera dedupe
s_ack_pending[slave_id] = false;
// backoff com jitter
uint32_t backoff = ACK_BACKOFF_MIN_MS +
(esp_random() % (ACK_BACKOFF_MAX_MS - ACK_BACKOFF_MIN_MS + 1));
vTaskDelay(pdMS_TO_TICKS(backoff));
uint8_t ack[] = {CMD_HEARTBEAT_ACK};
bool ok = evse_link_send(slave_id, ack, sizeof(ack));
ESP_LOGI(TAG, "CMD_HEARTBEAT_ACK to 0x%02X ok=%d", slave_id, ok);
}
}
// --- Send new limit to slave ---
static void on_new_limit(void *arg, esp_event_base_t base, int32_t id, void *data)
{
if (id != LOADBALANCER_EVENT_SLAVE_CURRENT_LIMIT)
(void)arg;
(void)base;
if (id != LOADBALANCER_EVENT_SLAVE_CURRENT_LIMIT || data == NULL)
return;
const loadbalancer_slave_limit_event_t *evt = data;
const loadbalancer_slave_limit_event_t *evt = (const loadbalancer_slave_limit_event_t *)data;
uint8_t slave_id = evt->slave_id;
uint16_t max_current = evt->max_current;
@@ -52,156 +212,179 @@ static void on_new_limit(void *arg, esp_event_base_t base, int32_t id, void *dat
CMD_SET_CURRENT,
(uint8_t)(max_current & 0xFF),
(uint8_t)(max_current >> 8)};
evse_link_send(slave_id, buf, sizeof(buf));
ESP_LOGI(TAG, "Sent SET_CURRENT to 0x%02X: %uA", slave_id, max_current);
(void)evse_link_send(slave_id, buf, sizeof(buf));
ESP_LOGI(TAG, "Sent SET_CURRENT to 0x%02X: %uA", slave_id, (unsigned)max_current);
}
// --- Bridge AUTH -> EVSE-Link: enviar AUTH_GRANTED para slaves ---
// --- Bridge AUTH -> EVSE-Link ---
static void on_auth_result(void *arg, esp_event_base_t base, int32_t id, void *data)
{
if (base != AUTH_EVENTS || id != AUTH_EVENT_TAG_PROCESSED || data == NULL) {
(void)arg;
if (base != AUTH_EVENTS || id != AUTH_EVENT_TAG_PROCESSED || data == NULL)
return;
}
const auth_tag_event_data_t *ev = (const auth_tag_event_data_t *)data;
if (!ev->authorized) {
if (!ev->authorized)
{
ESP_LOGI(TAG, "Tag %s not authorized, not propagating to slaves", ev->tag);
return;
}
// Construir payload: [ CMD_AUTH_GRANTED, tag..., '\0' ]
uint8_t buf[1 + EVSE_LINK_TAG_MAX_LEN];
buf[0] = CMD_AUTH_GRANTED;
// Copiar tag e garantir NUL
// Copia tag e garante NUL
strncpy((char *)&buf[1], ev->tag, EVSE_LINK_TAG_MAX_LEN - 1);
((char *)&buf[1])[EVSE_LINK_TAG_MAX_LEN - 1] = '\0';
uint8_t payload_len = 1 + (uint8_t)(strlen((char *)&buf[1]) + 1); // opcode + tag + '\0'
// Payload inclui opcode + string + NUL
uint8_t payload_len = 1 + (uint8_t)(strlen((char *)&buf[1]) + 1);
// Neste exemplo: broadcast para todos os slaves (0xFF)
uint8_t dest = 0xFF;
if (!evse_link_send(dest, buf, payload_len)) {
ESP_LOGW(TAG, "Failed to send CMD_AUTH_GRANTED to dest=0x%02X for tag=%s",
dest, (char *)&buf[1]);
} else {
ESP_LOGI(TAG, "Sent CMD_AUTH_GRANTED to dest=0x%02X for tag=%s",
dest, (char *)&buf[1]);
}
(void)evse_link_send(0xFF, buf, payload_len);
ESP_LOGI(TAG, "Sent CMD_AUTH_GRANTED (broadcast) tag=%s", (char *)&buf[1]);
}
// --- Polling broadcast callback ---
static void poll_timer_cb(TimerHandle_t xTimer)
{
ESP_LOGD(TAG, "Broadcasting CMD_POLL to all slaves");
;
// Optionally post event LINK_EVENT_MASTER_POLL_SENT
}
(void)xTimer;
// --- Heartbeat timeout callback ---
static void hb_timer_cb(TimerHandle_t xTimer)
{
ESP_LOGW(TAG, "Heartbeat timeout: possible slave offline");
// post event LINK_EVENT_SLAVE_OFFLINE ???
uint8_t poll[] = {CMD_POLL};
bool ok = evse_link_send(0xFF, poll, sizeof(poll));
ESP_LOGI(TAG, "POLL send ok=%d", ok);
}
static void on_frame_master(uint8_t src, uint8_t dest,
const uint8_t *payload, uint8_t len)
{
if (len < 1)
const uint8_t self = evse_link_get_self_id();
// ignora eco do próprio master e frames que não são para nós nem broadcast
if (src == self)
return;
if (dest != self && dest != 0xFF)
return;
if (payload == NULL || len < 1)
return;
uint8_t cmd = payload[0];
switch (cmd)
{
case CMD_HEARTBEAT:
{
ESP_LOGD(TAG, "HEARTBEAT from 0x%02X: %u bytes", src, len);
if (len != LEN_HEARTBEAT)
{ // CMD + charging + hw_max_lo + hw_max_hi + runtime_lo + runtime_hi
{
ESP_LOGW(TAG, "HEARTBEAT len invalid from 0x%02X: %u bytes", src, len);
return;
}
bool charging = payload[1] != 0;
uint16_t hw_max = payload[2] | (payload[3] << 8);
uint16_t runtime = payload[4] | (payload[5] << 8);
ESP_LOGI(TAG, "Heartbeat from 0x%02X: charging=%d hw_max=%uA runtime=%uA",
src, charging, hw_max, runtime);
bool charging = payload[1] != 0;
uint16_t hw_max = (uint16_t)(payload[2] | ((uint16_t)payload[3] << 8));
uint16_t runtime = (uint16_t)(payload[4] | ((uint16_t)payload[5] << 8));
mark_slave_seen(src);
loadbalancer_slave_status_event_t status = {
.slave_id = src,
.charging = charging,
.hw_max_current = (float)hw_max,
.runtime_current = (float)runtime, // corrente real medida no slave
.runtime_current = (float)runtime,
.timestamp_us = esp_timer_get_time()};
esp_event_post(LOADBALANCER_EVENTS,
(void)esp_event_post(LOADBALANCER_EVENTS,
LOADBALANCER_EVENT_SLAVE_STATUS,
&status, sizeof(status), portMAX_DELAY);
// Enviar ACK de volta
uint8_t ack[] = {CMD_HEARTBEAT_ACK};
evse_link_send(src, ack, sizeof(ack));
ESP_LOGD(TAG, "Sent HEARTBEAT_ACK to 0x%02X", src);
// ACK deferido e deduplicado
enqueue_ack(src);
break;
}
case CMD_POLL:
ESP_LOGD(TAG, "Received POLL_RESP from 0x%02X", src);
break;
case CMD_CONFIG_BROADCAST:
ESP_LOGI(TAG, "Slave 0x%02X acked CONFIG_BROADCAST: new_max=%uA",
src, payload[1]);
if (len >= LEN_CONFIG_BROADCAST)
ESP_LOGI(TAG, "Slave 0x%02X acked CONFIG_BROADCAST: new_max=%uA", src, payload[1]);
else
ESP_LOGW(TAG, "CONFIG_BROADCAST ack short len=%u from 0x%02X", len, src);
break;
default:
ESP_LOGW(TAG, "Unknown cmd 0x%02X from 0x%02X", cmd, src);
ESP_LOGD(TAG, "Cmd 0x%02X from 0x%02X (ignored/unknown)", cmd, src);
break;
}
}
// --- Master initialization ---
void evse_link_master_init(void)
{
if (evse_link_get_mode() != EVSE_LINK_MODE_MASTER || !evse_link_is_enabled())
{
return;
}
ESP_LOGI(TAG, "Initializing MASTER (ID=0x%02X)", evse_link_get_self_id());
// register frame callback
evse_link_register_rx_cb(on_frame_master);
// register loadbalancer event
ESP_ERROR_CHECK(
esp_event_handler_register(
// Cria queue/task de ACK uma vez
if (s_ack_q == NULL)
{
s_ack_q = xQueueCreate(ACK_QUEUE_LEN, sizeof(uint8_t));
if (!s_ack_q)
{
ESP_LOGE(TAG, "Failed to create ACK queue");
}
else
{
if (xTaskCreate(ack_task, "evse_ack", 4096, NULL, 4, &s_ack_task) != pdPASS)
{
ESP_LOGE(TAG, "Failed to create ACK task");
vQueueDelete(s_ack_q);
s_ack_q = NULL;
s_ack_task = NULL;
}
}
}
if (!s_handlers_registered)
{
s_handlers_registered = true;
ESP_ERROR_CHECK(esp_event_handler_register(
LOADBALANCER_EVENTS,
LOADBALANCER_EVENT_SLAVE_CURRENT_LIMIT,
on_new_limit,
NULL));
// escutar resultado do AUTH para propagar autorização aos slaves
ESP_ERROR_CHECK(
esp_event_handler_register(
ESP_ERROR_CHECK(esp_event_handler_register(
AUTH_EVENTS,
AUTH_EVENT_TAG_PROCESSED,
on_auth_result,
NULL));
}
// create and start poll timer
if (poll_timer.timer == NULL)
{
poll_timer.timer = xTimerCreate("poll_tmr",
poll_timer.interval,
pdTRUE, NULL,
poll_timer_cb);
xTimerStart(poll_timer.timer, 0);
if (poll_timer.timer)
(void)xTimerStart(poll_timer.timer, 0);
}
// create and start heartbeat monitor timer
hb_timer.timer = xTimerCreate("hb_tmr",
hb_timer.interval,
pdFALSE, NULL,
hb_timer_cb);
xTimerStart(hb_timer.timer, 0);
if (s_presence_timer == NULL)
{
s_presence_timer = xTimerCreate("presence_tmr",
pdMS_TO_TICKS(PRESENCE_CHECK_MS),
pdTRUE, NULL,
presence_timer_cb);
if (s_presence_timer)
(void)xTimerStart(s_presence_timer, 0);
else
ESP_LOGE(TAG, "Failed to create presence timer");
}
}

View File

@@ -1,199 +1,477 @@
// === components/evse_link/src/evse_link_slave.c ===
// components/evse_link/src/evse_link_slave.c
//
// Correções aplicadas:
// 1) Evitar TX dentro do callback RX: confirmação (heartbeat) é deferida via queue/task.
// 2) Proteger safe_mode (e flags relacionadas) com mux (evita race entre RX e timer).
// 3) Opção de política no fallback: por default faz PAUSE (mais seguro). Pode ser alterado por macro.
// 4) Reduzir ruído de logs em caminho quente (RX frames em DEBUG).
// 5) Manter semântica: só sai de safe_mode com comando explícito de potência (SET_CURRENT / RESUME).
// 6) Mantém lógica de pause SET_CURRENT=0 e resume quando >0.
//
// Nota: A enum do evse_state_event_data_t que enviaste está correta para o handler.
#include "evse_link.h"
#include "evse_link_events.h"
#include "loadbalancer_events.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/timers.h"
#include "freertos/queue.h"
#include "freertos/portmacro.h"
#include "esp_log.h"
#include "esp_event.h"
#include "esp_random.h"
#include "esp_err.h"
#include "evse_events.h"
#include "evse_state.h"
#include "evse_config.h"
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
static const char *TAG = "evse_link_slave";
// Link commands
#define MASTER_ID 0x01
// Commands (opcode no payload[0])
#define CMD_POLL 0x01
#define CMD_HEARTBEAT 0x02 // not used by slave
#define CMD_HEARTBEAT 0x02
#define CMD_CONFIG_BROADCAST 0x03
#define CMD_SET_CURRENT 0x08
#define CMD_HEARTBEAT_ACK 0x09
#define CMD_AUTH_GRANTED 0x0A // novo: master concede autorização
#define CMD_AUTH_GRANTED 0x0A
// #define CMD_RESUME 0x0B // se existir
// payload lengths (exclui seq byte)
#define LEN_POLL_REQ 1 // [ CMD_POLL ]
#define LEN_CONFIG_BROADCAST 2 // [ CMD_CONFIG_BROADCAST, new_max_current ]
#define LEN_SET_CURRENT 3 // [ CMD_SET_CURRENT, limit_lo, limit_hi ]
#define LEN_HEARTBEAT_ACK 1 // [ CMD_HEARTBEAT_ACK ]
#define LEN_HEARTBEAT 6 // CMD_HEARTBEAT + charging + hw_max_lo + hw_max_hi + runtime_lo + runtime_hi
// lengths (INCLUI opcode)
#define LEN_SET_CURRENT 3
// Timing
#define FALLBACK_TIMEOUT_MS 120000
#define FALLBACK_TIMEOUT_MS 180000
// --- Política de fallback ---
// 1 = mais seguro: PAUSE (revoga autorização)
// 0 = força corrente mínima (pode continuar a carregar dependendo do core)
#ifndef EVSE_LINK_FALLBACK_PAUSE
#define EVSE_LINK_FALLBACK_PAUSE 1
#endif
// --- Confirmações via heartbeat (deferidas) ---
#define HB_REQ_QUEUE_LEN 8
typedef enum
{
HB_REQ_SEND = 1,
} hb_req_t;
static TimerHandle_t fallback_timer = NULL;
static bool safe_mode = false;
static TaskHandle_t hb_task_handle = NULL;
// --- Helper to send a heartbeat frame ---
static void send_heartbeat_frame(void) {
// Task para enviar heartbeat sem bloquear RX callback
static TaskHandle_t hb_sender_task_handle = NULL;
static QueueHandle_t hb_req_q = NULL;
// "safe mode" (master offline): aplica fallback local e nao sai com POLL/ACK.
// Só sai com comando explícito de potência (SET_CURRENT / RESUME).
static bool safe_mode = false;
static uint16_t saved_runtime_limit = 0; // informativo
// "remote pause" (SET_CURRENT=0)
static bool paused_by_master = false;
static bool paused_prev_authorized = false;
static portMUX_TYPE s_state_mux = portMUX_INITIALIZER_UNLOCKED;
static bool evse_handler_registered = false;
static size_t bounded_strlen_u8(const uint8_t *s, size_t max_len)
{
size_t i = 0;
if (!s)
return 0;
while (i < max_len && s[i] != 0)
i++;
return i;
}
static void send_heartbeat_frame_now(void)
{
bool charging = evse_state_is_charging(evse_get_state());
uint16_t hw_max = evse_get_max_charging_current();
uint16_t runtime = evse_get_runtime_charging_current();
ESP_LOGI(TAG, "Sending HEARTBEAT: charging=%d hw_max=%uA runtime=%uA",
charging, hw_max, runtime);
uint8_t hb[] = {
CMD_HEARTBEAT,
charging ? 1 : 0,
(uint8_t)(hw_max & 0xFF), (uint8_t)(hw_max >> 8),
(uint8_t)(runtime & 0xFF), (uint8_t)(runtime >> 8)
};
// Broadcast to master (0xFF)
evse_link_send(0xFF, hb, sizeof(hb));
(uint8_t)(runtime & 0xFF), (uint8_t)(runtime >> 8)};
(void)evse_link_send(MASTER_ID, hb, sizeof(hb)); // UNICAST
ESP_LOGI(TAG, "Send Heartbeat Frame");
}
// --- EVSE state change handler ---
static void evse_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data) {
if (base!=EVSE_EVENTS || id!=EVSE_EVENT_STATE_CHANGED || data==NULL) return;
const evse_state_event_data_t *evt = data;
if (evt->state==EVSE_STATE_EVENT_IDLE || evt->state==EVSE_STATE_EVENT_CHARGING) {
send_heartbeat_frame();
// pede heartbeat sem bloquear quem chama (RX callback, event handler, etc.)
static void request_heartbeat_send(void)
{
if (hb_req_q)
{
hb_req_t req = HB_REQ_SEND;
// não bloqueia; se encher, apenas ignora (heartbeat periódico já existe)
(void)xQueueSendToBack(hb_req_q, &req, 0);
}
else
{
// fallback: se queue ainda não existe, manda direto
send_heartbeat_frame_now();
}
}
static void hb_sender_task(void *arg)
{
(void)arg;
hb_req_t req;
for (;;)
{
if (xQueueReceive(hb_req_q, &req, portMAX_DELAY) != pdTRUE)
continue;
if (req == HB_REQ_SEND)
{
send_heartbeat_frame_now();
}
}
}
static void evse_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data)
{
(void)arg;
if (base != EVSE_EVENTS || id != EVSE_EVENT_STATE_CHANGED || data == NULL)
return;
const evse_state_event_data_t *evt = (const evse_state_event_data_t *)data;
// Enforce pause: se algo tentar voltar a carregar enquanto paused_by_master, revoga auth.
bool paused;
portENTER_CRITICAL(&s_state_mux);
paused = paused_by_master;
portEXIT_CRITICAL(&s_state_mux);
if (paused && evt->state == EVSE_STATE_EVENT_CHARGING)
{
// Garante que não continua a carregar por clamp/auto-auth
evse_state_set_authorized(false);
}
// Envia heartbeat quando entra em IDLE ou CHARGING (estado relevante)
if (evt->state == EVSE_STATE_EVENT_IDLE || evt->state == EVSE_STATE_EVENT_CHARGING)
request_heartbeat_send();
}
// Sai de safe-mode APENAS com comando explícito (SET_CURRENT / RESUME).
static void maybe_exit_safe_mode_on_explicit_power_cmd(uint8_t cmd)
{
bool in_safe;
portENTER_CRITICAL(&s_state_mux);
in_safe = safe_mode;
portEXIT_CRITICAL(&s_state_mux);
if (!in_safe)
return;
if (cmd == CMD_SET_CURRENT /*|| cmd == CMD_RESUME*/)
{
portENTER_CRITICAL(&s_state_mux);
safe_mode = false;
portEXIT_CRITICAL(&s_state_mux);
ESP_LOGI(TAG, "Exiting safe mode due to explicit cmd 0x%02X", cmd);
}
}
static void apply_pause_by_master(void)
{
bool prev_auth = evse_state_get_authorized();
portENTER_CRITICAL(&s_state_mux);
paused_by_master = true;
paused_prev_authorized = prev_auth;
portEXIT_CRITICAL(&s_state_mux);
// Revoga autorização para parar contactor/pilot via core
if (prev_auth)
evse_state_set_authorized(false);
// Mantém runtime num valor seguro (não 0, pois clamp -> 6A).
// O "pause" efetivo é pela autorização=false.
evse_set_runtime_charging_current(MIN_CHARGING_CURRENT_LIMIT);
}
static void clear_pause_by_master_if_any(void)
{
bool was_paused;
bool prev_auth;
portENTER_CRITICAL(&s_state_mux);
was_paused = paused_by_master;
prev_auth = paused_prev_authorized;
paused_by_master = false;
paused_prev_authorized = false;
portEXIT_CRITICAL(&s_state_mux);
// Se estava autorizado antes do pause, tenta reautorizar
if (was_paused && prev_auth)
evse_state_set_authorized(true);
}
static void on_frame_slave(uint8_t src, uint8_t dest,
const uint8_t *payload, uint8_t len) {
if (dest != evse_link_get_self_id() && dest != 0xFF) return;
if (len < 1) return;
const uint8_t *payload, uint8_t len)
{
const uint8_t self = evse_link_get_self_id();
// Muito verboso em caminho quente; deixa em DEBUG
ESP_LOGD(TAG, "RX frames (src=0x%02X dest=0x%02X len=%u self=0x%02X)", src, dest, len, self);
if (src == self)
return;
if (dest != self && dest != 0xFF)
return;
if (payload == NULL || len < 1)
{
ESP_LOGW(TAG, "RX invalid: payload NULL or len<1 (len=%u)", len);
return;
}
// Só aceitar comandos do master
if (src != MASTER_ID)
{
ESP_LOGW(TAG, "RX ignore: non-master src=0x%02X", src);
return;
}
// Qualquer frame válido do master => link vivo (reset do fallback)
if (fallback_timer)
(void)xTimerReset(fallback_timer, 0);
uint8_t cmd = payload[0];
switch (cmd) {
switch (cmd)
{
case CMD_POLL:
ESP_LOGD(TAG, "Received CMD_POLL from master 0x%02X", src);
// Liveness only. Não sai de safe-mode e não restaura limites.
ESP_LOGI(TAG, "CMD_POLL from 0x%02X", src);
break;
case CMD_CONFIG_BROADCAST:
ESP_LOGD(TAG, "Received CMD_CONFIG_BROADCAST from master 0x%02X", src);
ESP_LOGI(TAG, "CMD_CONFIG_BROADCAST from 0x%02X", src);
break;
case CMD_SET_CURRENT: {
if (len < LEN_SET_CURRENT) {
ESP_LOGW(TAG, "SET_CURRENT from 0x%02X with invalid length %u", src, len);
case CMD_HEARTBEAT_ACK:
ESP_LOGI(TAG, "HEARTBEAT_ACK from 0x%02X", src);
break;
case CMD_SET_CURRENT:
{
ESP_LOGI(TAG, "SET_CURRENT from 0x%02X", src);
if (len < LEN_SET_CURRENT)
{
ESP_LOGW(TAG, "SET_CURRENT invalid len=%u from 0x%02X", len, src);
break;
}
uint16_t amps = payload[1] | (payload[2] << 8);
evse_set_runtime_charging_current(amps);
ESP_LOGI(TAG, "Applied runtime limit: %uA from master 0x%02X", amps, src);
esp_event_post(EVSE_LINK_EVENTS, LINK_EVENT_CURRENT_LIMIT_APPLIED,
uint16_t amps = (uint16_t)(payload[1] | ((uint16_t)payload[2] << 8));
// Comando explícito => pode sair de safe-mode (mesmo se for pause)
maybe_exit_safe_mode_on_explicit_power_cmd(cmd);
if (amps == 0)
{
// PAUSE explícito
ESP_LOGI(TAG, "SET_CURRENT=0 => PAUSE (src=0x%02X)", src);
apply_pause_by_master();
// Confirma sem bloquear RX
request_heartbeat_send();
// Publica evento com 0A (semântica: pause)
(void)esp_event_post(EVSE_LINK_EVENTS, LINK_EVENT_CURRENT_LIMIT_APPLIED,
&amps, sizeof(amps), portMAX_DELAY);
break;
}
case CMD_HEARTBEAT_ACK:
ESP_LOGI(TAG, "Received HEARTBEAT_ACK from master 0x%02X", src);
if (fallback_timer) {
xTimerReset(fallback_timer, 0);
if (safe_mode) {
safe_mode = false;
uint16_t current = evse_get_runtime_charging_current();
evse_set_runtime_charging_current(current);
ESP_LOGI(TAG, "Exiting safe mode, restoring %uA", current);
}
}
break;
clear_pause_by_master_if_any();
case CMD_AUTH_GRANTED: {
if (len < 2) {
ESP_LOGW(TAG, "CMD_AUTH_GRANTED from 0x%02X with invalid length %u", src, len);
evse_set_runtime_charging_current(amps);
// confirma sem bloquear RX
request_heartbeat_send();
ESP_LOGI(TAG, "Applied runtime limit: %uA from 0x%02X", (unsigned)amps, src);
(void)esp_event_post(EVSE_LINK_EVENTS, LINK_EVENT_CURRENT_LIMIT_APPLIED,
&amps, sizeof(amps), portMAX_DELAY);
break;
}
const char *tag = (const char *)&payload[1];
case CMD_AUTH_GRANTED:
{
if (len < 2)
{
ESP_LOGW(TAG, "AUTH_GRANTED invalid len=%u from 0x%02X", len, src);
break;
}
const uint8_t *tag_ptr = &payload[1];
size_t tag_buf_len = (size_t)(len - 1);
size_t tag_len = bounded_strlen_u8(tag_ptr, tag_buf_len);
evse_link_auth_grant_event_t ev = {0};
strncpy(ev.tag, tag, EVSE_LINK_TAG_MAX_LEN - 1);
ev.tag[EVSE_LINK_TAG_MAX_LEN - 1] = '\0';
size_t copy_len = tag_len;
if (copy_len > (EVSE_LINK_TAG_MAX_LEN - 1))
copy_len = EVSE_LINK_TAG_MAX_LEN - 1;
ESP_LOGI(TAG, "Received CMD_AUTH_GRANTED from master 0x%02X, tag='%s'", src, ev.tag);
if (copy_len > 0)
memcpy(ev.tag, tag_ptr, copy_len);
ev.tag[copy_len] = '\0';
esp_err_t err = esp_event_post(
EVSE_LINK_EVENTS,
// AUTH_GRANTED não deve sair de safe-mode automaticamente
ESP_LOGI(TAG, "AUTH_GRANTED from 0x%02X tag='%s'", src, ev.tag);
esp_err_t err = esp_event_post(EVSE_LINK_EVENTS,
LINK_EVENT_REMOTE_AUTH_GRANTED,
&ev,
sizeof(ev),
&ev, sizeof(ev),
portMAX_DELAY);
if (err != ESP_OK) {
if (err != ESP_OK)
ESP_LOGE(TAG, "Failed to post LINK_EVENT_REMOTE_AUTH_GRANTED: %s", esp_err_to_name(err));
}
break;
}
default:
ESP_LOGW(TAG, "Unknown command 0x%02X from master 0x%02X", cmd, src);
ESP_LOGW(TAG, "Unknown cmd 0x%02X from 0x%02X", cmd, src);
break;
}
}
static void slave_heartbeat_task(void *arg)
{
(void)arg;
const uint32_t period_ms = 60000;
uint8_t id = evse_link_get_self_id();
// --- Periodic heartbeat task ---
static void slave_heartbeat_task(void *arg) {
const TickType_t interval = pdMS_TO_TICKS(10000);
for (;;) {
send_heartbeat_frame();
vTaskDelay(interval);
// desfasamento por ID: 2s, 4s, 6s...
vTaskDelay(pdMS_TO_TICKS((uint32_t)id * 2000));
for (;;)
{
request_heartbeat_send();
// jitter opcional
uint32_t jitter_ms = esp_random() % 201; // 0..200ms
vTaskDelay(pdMS_TO_TICKS(period_ms + jitter_ms));
}
}
// --- Fallback safe mode callback ---
static void fallback_timer_cb(TimerHandle_t xTimer) {
if (!safe_mode) {
static void fallback_timer_cb(TimerHandle_t xTimer)
{
(void)xTimer;
// entra safe_mode uma vez
bool already_safe;
portENTER_CRITICAL(&s_state_mux);
already_safe = safe_mode;
if (!safe_mode)
safe_mode = true;
ESP_LOGW(TAG, "Fallback timeout: entering safe mode");
portEXIT_CRITICAL(&s_state_mux);
if (already_safe)
return;
saved_runtime_limit = evse_get_runtime_charging_current();
#if EVSE_LINK_FALLBACK_PAUSE
ESP_LOGW(TAG, "Fallback timeout: entering safe mode (saved %uA). Policy=PAUSE",
(unsigned)saved_runtime_limit);
// pausar é mais seguro quando o master “morre”
apply_pause_by_master();
#else
ESP_LOGW(TAG, "Fallback timeout: entering safe mode (saved %uA, forcing %uA). Policy=MIN",
(unsigned)saved_runtime_limit, (unsigned)MIN_CHARGING_CURRENT_LIMIT);
evse_set_runtime_charging_current(MIN_CHARGING_CURRENT_LIMIT);
esp_event_post(EVSE_LINK_EVENTS,
LINK_EVENT_SLAVE_OFFLINE,
#endif
(void)esp_event_post(EVSE_LINK_EVENTS, LINK_EVENT_SLAVE_OFFLINE,
NULL, 0, portMAX_DELAY);
}
// opcional: manda heartbeat para indicar estado atual
request_heartbeat_send();
}
// --- Slave initialization ---
void evse_link_slave_init(void) {
if (evse_link_get_mode()!=EVSE_LINK_MODE_SLAVE || !evse_link_is_enabled()) return;
void evse_link_slave_init(void)
{
if (evse_link_get_mode() != EVSE_LINK_MODE_SLAVE || !evse_link_is_enabled())
return;
ESP_LOGI(TAG, "Initializing SLAVE mode (ID=0x%02X)", evse_link_get_self_id());
// register frame callback
evse_link_register_rx_cb(on_frame_slave);
// start periodic heartbeat
xTaskCreate(slave_heartbeat_task, "slave_hb", 4096, NULL, 5, NULL);
// cria queue/task do sender (para não mandar UART TX no callback RX)
if (hb_req_q == NULL)
{
hb_req_q = xQueueCreate(HB_REQ_QUEUE_LEN, sizeof(hb_req_t));
if (!hb_req_q)
{
ESP_LOGE(TAG, "Failed to create HB request queue (fallback to direct send)");
}
else
{
if (xTaskCreate(hb_sender_task, "hb_sender", 3072, NULL, 3, &hb_sender_task_handle) != pdPASS)
{
ESP_LOGE(TAG, "Failed to create hb_sender task");
vQueueDelete(hb_req_q);
hb_req_q = NULL;
hb_sender_task_handle = NULL;
}
}
}
// fallback timer
if (hb_task_handle == NULL)
{
if (xTaskCreate(slave_heartbeat_task, "slave_hb", 4096, NULL, 3, &hb_task_handle) != pdPASS)
{
ESP_LOGE(TAG, "Failed to create slave_heartbeat_task");
hb_task_handle = NULL;
}
}
if (fallback_timer == NULL)
{
fallback_timer = xTimerCreate("fallback_tmr",
pdMS_TO_TICKS(FALLBACK_TIMEOUT_MS),
pdFALSE, NULL,
fallback_timer_cb);
if (fallback_timer) {
xTimerStart(fallback_timer, 0);
if (fallback_timer)
(void)xTimerStart(fallback_timer, 0);
else
ESP_LOGE(TAG, "Failed to create fallback timer");
}
else
{
(void)xTimerReset(fallback_timer, 0);
}
// react to EVSE state changes
ESP_ERROR_CHECK(
esp_event_handler_register(
EVSE_EVENTS,
EVSE_EVENT_STATE_CHANGED,
evse_event_handler,
NULL
)
);
if (!evse_handler_registered)
{
ESP_ERROR_CHECK(esp_event_handler_register(
EVSE_EVENTS, EVSE_EVENT_STATE_CHANGED,
evse_event_handler, NULL));
evse_handler_registered = true;
}
}
// === Fim de: components/evse_link/src/evse_link_slave.c ===

View File

@@ -13,7 +13,7 @@
#include "evse_state.h"
#include "ledc_driver.h"
#define BLOCK_TIME pdMS_TO_TICKS(10)
#define BLOCK_TIME portMAX_DELAY
static const char *TAG = "led";
@@ -350,7 +350,7 @@ static void evse_led_event_handler(void *arg, esp_event_base_t base, int32_t id,
const evse_state_event_data_t *evt = (const evse_state_event_data_t *)data;
ESP_LOGI(TAG, "EVSE State Changed: state=%d", evt->state);
ESP_LOGD(TAG, "EVSE State Changed: state=%d", evt->state);
// Atualiza o estado base
current_state_mode = evt->state;
@@ -378,7 +378,7 @@ static void evse_session_led_event_handler(void *arg, esp_event_base_t base, int
const evse_session_event_data_t *evt =
(const evse_session_event_data_t *)data;
ESP_LOGI(TAG,
ESP_LOGD(TAG,
"EVSE Session Event: type=%d, id=%" PRIu32
", duration=%" PRIu32 " s, energy=%" PRIu32 " Wh, avg=%" PRIu32 " W, current=%d",
(int)evt->type,

View File

@@ -1,8 +1,7 @@
set(srcs
"src/input_filter.c" "src/loadbalancer.c" "src/loadbalancer_events.c"
"src/input_filter.c" "src/loadbalancer.c" "src/pv_optimizer.c" "src/grid_limiter.c" "src/loadbalancer_events.c"
)
idf_component_register(SRCS "${srcs}"
INCLUDE_DIRS "include"
PRIV_REQUIRES nvs_flash
REQUIRES esp_event esp_timer meter_manager evse)

View File

@@ -0,0 +1,42 @@
#ifndef GRID_LIMITER_H_
#define GRID_LIMITER_H_
#ifdef __cplusplus
extern "C"
{
#endif
#include <stdbool.h>
#include <stdint.h>
#include "esp_err.h"
#include "meter_events.h"
void grid_limiter_init(void);
void grid_limiter_set_enabled(bool en);
bool grid_limiter_is_enabled(void);
esp_err_t grid_limiter_set_max_import_a(uint8_t a);
uint8_t grid_limiter_get_max_import_a(void);
/**
* @brief Calcula um novo "total_budget_a" (<= current_total_a) para respeitar max_import_a.
*
* Preferência:
* - Usa watt_total (+import / -export) se existir
* - Caso watt_total==0, usa fallback_grid_current_a (magnitude)
*
* @param grid_evt último evento do GRID
* @param fallback_grid_current_a corrente filtrada (magnitude) como fallback
* @param current_total_a total atual a atribuir aos EVSE (A)
* @return total_budget_a (<= current_total_a)
*/
float grid_limiter_limit_total_a(const meter_event_data_t *grid_evt,
float fallback_grid_current_a,
float current_total_a);
#ifdef __cplusplus
}
#endif
#endif /* GRID_LIMITER_H_ */

View File

@@ -9,35 +9,25 @@ extern "C" {
#include <stdint.h>
#include "esp_err.h"
/**
* @brief Inicializa o módulo de load balancer
*/
void loadbalancer_init(void);
/**
* @brief Task contínua do algoritmo de balanceamento
*/
void loadbalancer_task(void *param);
/**
* @brief Ativa ou desativa o load balancing
*/
void loadbalancer_set_enabled(bool value);
/**
* @brief Verifica se o load balancing está ativo
*/
void loadbalancer_set_enabled(bool enabled);
bool loadbalancer_is_enabled(void);
/**
* @brief Define a corrente máxima do grid
*/
esp_err_t load_balancing_set_max_grid_current(uint8_t max_grid_current);
// GRID limit (A)
void loadbalancer_grid_set_enabled(bool en);
bool loadbalancer_grid_is_enabled(void);
esp_err_t loadbalancer_grid_set_max_import_a(uint8_t a);
uint8_t loadbalancer_grid_get_max_import_a(void);
/**
* @brief Obtém a corrente máxima do grid
*/
// PV optimizer (W)
void loadbalancer_pv_set_enabled(bool en);
bool loadbalancer_pv_is_enabled(void);
esp_err_t loadbalancer_pv_set_max_import_w(int32_t w);
int32_t loadbalancer_pv_get_max_import_w(void);
// Aliases legacy (se quiseres manter chamadas antigas)
esp_err_t load_balancing_set_max_grid_current(uint8_t value);
uint8_t load_balancing_get_max_grid_current(void);
#ifdef __cplusplus

View File

@@ -1,17 +1,26 @@
// components/loadbalancer/include/loadbalancer_events.h
#pragma once
#include "esp_event.h"
#include <stdint.h>
#include <stdbool.h>
#include "esp_timer.h"
#ifdef __cplusplus
extern "C" {
#endif
ESP_EVENT_DECLARE_BASE(LOADBALANCER_EVENTS);
typedef enum {
LOADBALANCER_EVENT_INIT,
LOADBALANCER_EVENT_INIT = 0,
LOADBALANCER_EVENT_STATE_CHANGED,
LOADBALANCER_EVENT_GLOBAL_CURRENT_LIMIT,
// IMPORTANT: eventos separados e payloads diferentes
LOADBALANCER_EVENT_MASTER_CURRENT_LIMIT,
LOADBALANCER_EVENT_SLAVE_CURRENT_LIMIT,
LOADBALANCER_EVENT_SLAVE_STATUS
} loadbalancer_event_id_t;
@@ -20,18 +29,18 @@ typedef struct {
int64_t timestamp_us;
} loadbalancer_state_event_t;
// (opcional)
typedef struct {
float limit;
int64_t timestamp_us;
} loadbalancer_global_limit_event_t;
// MASTER: NÃO tem slave_id
typedef struct {
uint8_t slave_id;
uint16_t max_current;
int64_t timestamp_us;
} loadbalancer_master_limit_event_t;
// SLAVE: tem slave_id
typedef struct {
uint8_t slave_id;
uint16_t max_current;
@@ -39,9 +48,13 @@ typedef struct {
} loadbalancer_slave_limit_event_t;
typedef struct {
uint8_t slave_id; // ID do slave que reportou
bool charging; // Status de carregamento
float hw_max_current; // Limite máximo de corrente do hardware informado
float runtime_current; // Corrente atual de carregamento (A)
int64_t timestamp_us; // Momento em que o status foi coletado
uint8_t slave_id;
bool charging;
float hw_max_current;
float runtime_current;
int64_t timestamp_us;
} loadbalancer_slave_status_event_t;
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,40 @@
#ifndef PV_OPTIMIZER_H_
#define PV_OPTIMIZER_H_
#ifdef __cplusplus
extern "C" {
#endif
#include <stdbool.h>
#include <stdint.h>
#include "esp_err.h"
#include "meter_events.h"
void pv_optimizer_init(void);
void pv_optimizer_set_enabled(bool en);
bool pv_optimizer_is_enabled(void);
esp_err_t pv_optimizer_set_max_import_w(int32_t w);
int32_t pv_optimizer_get_max_import_w(void);
/**
* @brief Calcula o budget TOTAL (A) para todos os EVSEs, para manter importação <= max_import_w.
*
* - max_import_w = 0 => modo "Só PV": tenta manter importação ~0 (só consome quando há exportação).
* - max_import_w > 0 => modo "PV-Grid": permite importar até esse valor.
*
* @param grid_evt Último evento do medidor GRID (watt_total assinado).
* @param last_total_cmd_a Soma da corrente comandada no ciclo anterior (A).
* @param total_hw_max_a Soma dos hw_max_current dos conectores ativos (A).
* @return budget_total_a (0..total_hw_max_a)
*/
float pv_optimizer_compute_budget_a(const meter_event_data_t *grid_evt,
float last_total_cmd_a,
float total_hw_max_a);
#ifdef __cplusplus
}
#endif
#endif /* PV_OPTIMIZER_H_ */

View File

@@ -0,0 +1,123 @@
#include "grid_limiter.h"
#include "esp_log.h"
#include <math.h>
static const char *TAG = "grid_limiter";
#define DEFAULT_VOLTAGE_V (230.0f)
typedef struct
{
bool enabled;
uint8_t max_import_a;
} grid_cfg_t;
static grid_cfg_t s_cfg = {
.enabled = false,
.max_import_a = 32};
static float clamp_pf(float pf)
{
if (pf < 0.05f || pf > 1.2f)
return 1.0f;
return pf;
}
static void estimate_v_and_phases(const meter_event_data_t *m, float *v_avg, int *nph)
{
float sum = 0.0f;
int cnt = 0;
if (!m)
{
*v_avg = DEFAULT_VOLTAGE_V;
*nph = 1;
return;
}
for (int i = 0; i < 3; i++)
{
if (m->vrms[i] > 80.0f)
{
sum += m->vrms[i];
cnt++;
}
}
if (cnt == 0)
{
*v_avg = DEFAULT_VOLTAGE_V;
*nph = 1;
return;
}
*v_avg = sum / (float)cnt;
*nph = cnt;
}
void grid_limiter_init(void) { /* nada */ }
void grid_limiter_set_enabled(bool en) { s_cfg.enabled = en; }
bool grid_limiter_is_enabled(void) { return s_cfg.enabled; }
esp_err_t grid_limiter_set_max_import_a(uint8_t a)
{
if (a < 6 || a > 100)
return ESP_ERR_INVALID_ARG;
s_cfg.max_import_a = a;
return ESP_OK;
}
uint8_t grid_limiter_get_max_import_a(void) { return s_cfg.max_import_a; }
float grid_limiter_limit_total_a(const meter_event_data_t *grid_evt,
float fallback_grid_current_a,
float current_total_a)
{
if (!s_cfg.enabled)
return current_total_a;
if (current_total_a <= 0.0f)
return 0.0f;
float i_import = 0.0f;
if (grid_evt && grid_evt->watt_total > 0)
{
float v_avg;
int nph;
estimate_v_and_phases(grid_evt, &v_avg, &nph);
const float pf = clamp_pf(grid_evt->power_factor);
const float denom = v_avg * (float)nph * pf;
if (denom > 10.0f)
{
i_import = ((float)grid_evt->watt_total) / denom;
}
else
{
i_import = fallback_grid_current_a;
}
}
else
{
// export (<=0) => import=0; ou sem potência => fallback
if (grid_evt && grid_evt->watt_total < 0)
i_import = 0.0f;
else
i_import = fallback_grid_current_a;
}
if (i_import <= (float)s_cfg.max_import_a + 0.01f)
return current_total_a;
const float over = i_import - (float)s_cfg.max_import_a;
const float cut_a = ceilf(over); // conservador
float new_total = current_total_a - cut_a;
if (new_total < 0.0f)
new_total = 0.0f;
ESP_LOGD(TAG, "cap: i_import=%.2fA max=%uA over=%.2fA total=%.1fA -> %.1fA",
i_import, (unsigned)s_cfg.max_import_a, over, current_total_a, new_total);
return new_total;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,165 @@
#include "pv_optimizer.h"
#include "esp_log.h"
#include <math.h>
static const char *TAG = "pv_optimizer";
// internos (fixos, como pediste)
#define PV_MIN_EXPORT_W (50) // deadband export (anti-oscilações)
#define PV_TOTAL_RAMP_STEP_A (2.0f) // step total por ciclo (como tens loop 5s)
#define DEFAULT_VOLTAGE_V (230.0f)
typedef struct
{
bool enabled;
int32_t max_import_w; // >=0
} pv_cfg_t;
static pv_cfg_t s_cfg = {
.enabled = false,
.max_import_w = 0};
static float clamp_pf(float pf)
{
if (pf < 0.05f || pf > 1.2f)
return 1.0f;
return pf;
}
static void estimate_v_and_phases(const meter_event_data_t *m, float *v_avg, int *nph)
{
float sum = 0.0f;
int cnt = 0;
if (!m)
{
*v_avg = DEFAULT_VOLTAGE_V;
*nph = 1;
return;
}
for (int i = 0; i < 3; i++)
{
if (m->vrms[i] > 80.0f)
{
sum += m->vrms[i];
cnt++;
}
}
if (cnt == 0)
{
*v_avg = DEFAULT_VOLTAGE_V;
*nph = 1;
return;
}
*v_avg = sum / (float)cnt;
*nph = cnt;
}
void pv_optimizer_init(void)
{
// nada a fazer
}
void pv_optimizer_set_enabled(bool en) { s_cfg.enabled = en; }
bool pv_optimizer_is_enabled(void) { return s_cfg.enabled; }
esp_err_t pv_optimizer_set_max_import_w(int32_t w)
{
if (w < 0)
return ESP_ERR_INVALID_ARG;
s_cfg.max_import_w = w;
return ESP_OK;
}
int32_t pv_optimizer_get_max_import_w(void) { return s_cfg.max_import_w; }
static float ramp_total(float last_a, float target_a)
{
if (target_a > last_a + PV_TOTAL_RAMP_STEP_A)
return last_a + PV_TOTAL_RAMP_STEP_A;
if (target_a < last_a - PV_TOTAL_RAMP_STEP_A)
return last_a - PV_TOTAL_RAMP_STEP_A;
return target_a;
}
float pv_optimizer_compute_budget_a(const meter_event_data_t *grid_evt,
float last_total_cmd_a,
float total_hw_max_a)
{
if (!s_cfg.enabled)
return total_hw_max_a;
if (!grid_evt)
return 0.0f;
// se meter não fornece potência (fica 0) não dá para PV -> conservador: não importa
// (podes mudar para "mantém last" se preferires)
if (grid_evt->watt_total == 0)
{
return ramp_total(last_total_cmd_a, 0.0f);
}
float v_avg;
int nph;
estimate_v_and_phases(grid_evt, &v_avg, &nph);
const float pf = clamp_pf(grid_evt->power_factor);
const float w_per_a = v_avg * (float)nph * pf;
if (w_per_a < 10.0f)
{
return ramp_total(last_total_cmd_a, 0.0f);
}
const int32_t p_grid_w = grid_evt->watt_total; // +import / -export
const int32_t target_import_w = s_cfg.max_import_w; // >=0
// deadband só para o "Só PV"
if (target_import_w == 0)
{
if (p_grid_w < 0)
{
int32_t export_w = -p_grid_w;
if (export_w < PV_MIN_EXPORT_W)
{
return ramp_total(last_total_cmd_a, 0.0f);
}
}
else
{
// está a importar
if (p_grid_w < PV_MIN_EXPORT_W)
{
return ramp_total(last_total_cmd_a, 0.0f);
}
}
}
// estima base-load com o comando anterior
const float p_evse_last_w = last_total_cmd_a * w_per_a;
const float p_base_w = (float)p_grid_w - p_evse_last_w;
// queremos p_grid -> target_import_w
float p_evse_target_w = (float)target_import_w - p_base_w;
// clamp [0..max]
if (p_evse_target_w < 0.0f)
p_evse_target_w = 0.0f;
const float p_evse_max_w = total_hw_max_a * w_per_a;
if (p_evse_target_w > p_evse_max_w)
p_evse_target_w = p_evse_max_w;
float target_total_a = p_evse_target_w / w_per_a;
if (target_total_a < 0.0f)
target_total_a = 0.0f;
if (target_total_a > total_hw_max_a)
target_total_a = total_hw_max_a;
float ramped = ramp_total(last_total_cmd_a, target_total_a);
ESP_LOGD(TAG, "pv: p_grid=%ldW target_imp=%ldW base=%.1fW last=%.1fA -> target=%.1fA (v=%.1f nph=%d pf=%.2f)",
(long)p_grid_w, (long)target_import_w, p_base_w, last_total_cmd_a, ramped, v_avg, nph, pf);
return ramped;
}

View File

@@ -1,7 +0,0 @@
set(srcs
"src/logger.c"
"src/output_buffer.c"
)
idf_component_register(SRCS "${srcs}"
INCLUDE_DIRS "include")

View File

@@ -1,58 +0,0 @@
#ifndef LOGGER_H_
#define LOGGER_H_
#include <stdint.h>
#include <stdbool.h>
#include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"
#define LOGGER_SERIAL_BIT BIT0
/**
* @brief Logger event group LOGGER_SERIAL_BIT
*
*/
extern EventGroupHandle_t logger_event_group;
/**
* @brief Initialize logger
*
*/
void logger_init(void);
/**
* @brief Print
*
* @param str
*/
void logger_print(const char* str);
/**
* @brief Print va
*
* @param str
* @param l
* @return int
*/
int logger_vprintf(const char* str, va_list l);
/**
* @brief Get entries count
*
* @return uint16_t
*/
uint16_t logger_count(void);
/**
* @brief Read line from index, set index for reading next entry
*
* @param index
* @param str
* @param v
* @return true When has next entry
* @return false When no entry left
*/
bool logger_read(uint16_t *index, char **str, uint16_t* len);
#endif /* LOGGER_H_ */

View File

@@ -1,24 +0,0 @@
#ifndef OUTPUT_BUFFER_H_
#define OUTPUT_BUFFER_H_
#include <stdint.h>
#include <stdbool.h>
typedef struct {
uint16_t size;
uint16_t count;
uint8_t* data;
uint8_t* append;
} output_buffer_t;
output_buffer_t* output_buffer_create(uint16_t size);
void output_buffer_delete(output_buffer_t* buffer);
void output_buffer_append_buf(output_buffer_t* buffer, const char* buf, uint16_t len);
void output_buffer_append_str(output_buffer_t* buffer, const char* str);
bool output_buffer_read(output_buffer_t* buffer, uint16_t *index, char **str, uint16_t* len);
#endif /* OUTPUT_BUFFER_H_ */

View File

@@ -1,71 +0,0 @@
#include <stdio.h>
#include <memory.h>
#include <sys/param.h>
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "logger.h"
#include "output_buffer.h"
#define LOG_BUFFER_SIZE 6096 //4096
#define MAX_LOG_SIZE 512
static SemaphoreHandle_t mutex;
static output_buffer_t * buffer = NULL;
EventGroupHandle_t logger_event_group = NULL;
void logger_init(void)
{
mutex = xSemaphoreCreateMutex();
logger_event_group = xEventGroupCreate();
buffer = output_buffer_create(LOG_BUFFER_SIZE);
}
uint16_t logger_count(void)
{
return buffer->count;
}
void logger_print(const char* str)
{
xSemaphoreTake(mutex, portMAX_DELAY);
output_buffer_append_str(buffer, str);
xEventGroupSetBits(logger_event_group, 0xFF);
xSemaphoreGive(mutex);
}
int logger_vprintf(const char* str, va_list l)
{
#ifdef CONFIG_ESP_CONSOLE_UART
vprintf(str, l);
#endif
xSemaphoreTake(mutex, portMAX_DELAY);
static char log[MAX_LOG_SIZE];
int len = vsnprintf(log, MAX_LOG_SIZE, str, l);
output_buffer_append_buf(buffer, log, len);
xEventGroupSetBits(logger_event_group, 0xFF);
xSemaphoreGive(mutex);
return len;
}
bool logger_read(uint16_t* index, char** str, uint16_t* len)
{
xSemaphoreTake(mutex, portMAX_DELAY);
bool has_next = output_buffer_read(buffer, index, str, len);
xSemaphoreGive(mutex);
return has_next;
}

View File

@@ -1,86 +0,0 @@
#include <memory.h>
#include "output_buffer.h"
output_buffer_t* output_buffer_create(uint16_t size)
{
output_buffer_t* buffer = (output_buffer_t*)malloc(sizeof(output_buffer_t));
buffer->size = size;
buffer->count = 0;
buffer->data = (uint8_t*)malloc(sizeof(uint8_t) * size);
buffer->append = buffer->data;
return buffer;
}
void output_buffer_delete(output_buffer_t* buffer)
{
free((void*)buffer->data);
free((void*)buffer);
}
void output_buffer_append_buf(output_buffer_t* buffer, const char* str, uint16_t len)
{
if (((buffer->append - buffer->data) + sizeof(uint16_t) + len) >= buffer->size) {
//rotate buffer
uint8_t* pos = buffer->data;
uint16_t rotate_count = 0;
while ((pos - buffer->data) < buffer->size / 2) {
//seek first half
uint16_t entry_len;
memcpy((void*)&entry_len, (void*)pos, sizeof(uint16_t));
pos += entry_len + sizeof(uint16_t);
rotate_count++;
}
memmove((void*)buffer->data, (void*)pos, buffer->size - (pos - buffer->data));
buffer->count -= rotate_count;
buffer->append -= (pos - buffer->data);
}
memcpy((void*)buffer->append, (void*)&len, sizeof(uint16_t));
buffer->append += sizeof(uint16_t);
memcpy((void*)buffer->append, (void*)str, len);
buffer->append += len;
buffer->count++;
}
void output_buffer_append_str(output_buffer_t* buffer, const char* str)
{
output_buffer_append_buf(buffer, str, strlen(str));
}
bool output_buffer_read(output_buffer_t* buffer, uint16_t* index, char** str, uint16_t* len)
{
if (*index > buffer->count) {
*index = buffer->count;
}
bool has_next = false;
if (*index < buffer->count) {
uint8_t* pos = buffer->data;
uint16_t current = 0;
while (current != *index) {
uint16_t entry_len;
memcpy((void*)&entry_len, (void*)pos, sizeof(uint16_t));
pos += entry_len + sizeof(uint16_t);
current++;
}
memcpy((void*)len, (void*)pos, sizeof(uint16_t));
pos += sizeof(uint16_t);
*str = (char*)pos;
(*index)++;
has_next = true;
}
return has_next;
}

View File

@@ -1,29 +1,31 @@
# List the source files to be compiled
# components/meter_manager/CMakeLists.txt
set(srcs
"driver/meter_ade7758/meter_ade7758.c"
"driver/meter_ade7758/ade7758.c"
"driver/meter_orno/meter_orno513.c"
"driver/meter_orno/meter_orno526.c"
"driver/meter_orno/meter_orno516.c"
"driver/meter_orno/meter_dts6619.c"
"driver/meter_orno/meter_dds661.c"
"driver/meter_orno/meter_ea777.c"
"driver/meter_orno/modbus_params.c"
"driver/meter_zigbee/meter_zigbee.c"
"src/meter_manager.c"
"src/meter_events.c"
driver/meter_ade7758/meter_ade7758.c
driver/meter_ade7758/ade7758.c
driver/meter_modbus/meter_orno513.c
driver/meter_modbus/meter_orno526.c
driver/meter_modbus/meter_orno516.c
driver/meter_modbus/meter_dts6619.c
driver/meter_modbus/meter_dds661.c
driver/meter_modbus/meter_ea777.c
driver/meter_modbus/meter_dts024m.c
driver/meter_modbus/modbus_params.c
driver/meter_zigbee/meter_zigbee.c
src/meter_manager.c
src/meter_events.c
)
# List the include directories
set(includes
"include"
"driver/meter_ade7758"
"driver/meter_orno"
"driver/meter_zigbee"
include
driver/meter_ade7758
driver/meter_modbus
driver/meter_zigbee
)
# Register the component with the ESP-IDF build system
idf_component_register(SRCS "${srcs}"
INCLUDE_DIRS "${includes}"
PRIV_REQUIRES nvs_flash
REQUIRES esp_event esp-modbus spi_bus_manager network)
idf_component_register(
SRCS ${srcs}
INCLUDE_DIRS ${includes}
REQUIRES esp_event
PRIV_REQUIRES esp-modbus spi_bus_manager storage_service network
)

View File

@@ -53,7 +53,7 @@ static void meter_ade7758_post_event(const meter_ade7758_internal_data_t *data)
memcpy(evt.irms, data->irms, sizeof(evt.irms));
memcpy(evt.watt, data->watt, sizeof(evt.watt));
esp_err_t err = esp_event_post(METER_EVENT, METER_EVENT_DATA_READY, &evt, sizeof(evt), pdMS_TO_TICKS(10));
esp_err_t err = esp_event_post(METER_EVENT, METER_EVENT_DATA_READY, &evt, sizeof(evt), portMAX_DELAY);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Falha ao emitir evento: %s", esp_err_to_name(err));
}

View File

@@ -197,7 +197,7 @@ static void serial_mdb_task(void *param)
memcpy(evt.irms, current, sizeof(evt.irms));
memcpy(evt.watt, watt, sizeof(evt.watt));
esp_event_post(METER_EVENT, METER_EVENT_DATA_READY, &evt, sizeof(evt), pdMS_TO_TICKS(10));
esp_event_post(METER_EVENT, METER_EVENT_DATA_READY, &evt, sizeof(evt), portMAX_DELAY);
vTaskDelay(UPDATE_INTERVAL);
}
}

View File

@@ -0,0 +1,542 @@
// meter_dts024m.c — Driver Modbus RTU para DTS024M (ESP-IDF / esp-modbus)
// Versão PRODUÇÃO (SEM AUTO-PROBE): parâmetros fixos (baud/parity/id/FC/base).
// Ajusta os #defines DTS024M_PROD_* conforme o teu medidor.
#include "meter_events.h"
#include "modbus_params.h"
#include "mbcontroller.h"
#include "esp_log.h"
#include "esp_err.h"
#include "driver/uart.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <stddef.h>
#include <string.h>
#include "meter_dts024m.h"
#define TAG "serial_mdb_dts024m"
// ===== UART / RS-485 =====
#define MB_PORT_NUM 2
// Ajuste os pinos conforme seu hardware
#define MB_UART_TXD 17
#define MB_UART_RXD 16
#define MB_UART_RTS 2 // pino DE/RE do transceiver RS-485
// ===== Timings =====
#define UPDATE_INTERVAL (5000 / portTICK_PERIOD_MS)
#define POLL_INTERVAL (200 / portTICK_PERIOD_MS)
// ===== Helpers =====
#define STR(fieldname) ((const char *)(fieldname))
#define OPTS(min_val, max_val, step_val) {.opt1 = (min_val), .opt2 = (max_val), .opt3 = (step_val)}
#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
// ===== Config PRODUÇÃO (sem AUTO-PROBE) =====
// Ajusta estes valores:
#define DTS024M_PROD_BAUD 2400
#define DTS024M_PROD_PARITY UART_PARITY_DISABLE // 0 = none; UART_PARITY_EVEN se 8E1
#define DTS024M_PROD_SLAVE_ID 1 // endereço Modbus (1..247)
#define DTS024M_PROD_AREA MB_PARAM_INPUT // MB_PARAM_INPUT (FC04) ou MB_PARAM_HOLDING (FC03)
#define DTS024M_PROD_BASE_OFFSET 0 // 0 ou 1 (depende se o mapa é 0-based ou 1-based)
// ===== Estado =====
static bool is_initialized = false;
static bool mb_started = false;
static TaskHandle_t meter_task = NULL;
// ============================================================================
// MAPA DE REGISTROS (template) — pode variar conforme firmware.
// Estes endereços são um “perfil” comum.
// ============================================================================
#define DTS024M_L1_VOLTAGE 0x0000 // U32, 0.01 V (2 regs)
#define DTS024M_L2_VOLTAGE 0x0002
#define DTS024M_L3_VOLTAGE 0x0004
#define DTS024M_L1_CURRENT 0x0006 // U32, 0.001 A (2 regs)
#define DTS024M_L2_CURRENT 0x0008
#define DTS024M_L3_CURRENT 0x000A
#define DTS024M_L1_ACTIVE_P 0x000C // I32 (twos complement), (depende do modelo/escala)
#define DTS024M_L2_ACTIVE_P 0x000E
#define DTS024M_L3_ACTIVE_P 0x0010
#define DTS024M_PF_L1 0x001E // I16 (twos complement), 0.001
#define DTS024M_PF_L2 0x001F
#define DTS024M_PF_L3 0x0020
#define DTS024M_FREQUENCY 0x002A // U16, 0.01 Hz
#define DTS024M_TOTAL_ACTIVE_E 0x0404 // U32, 0.01 kWh (2 regs)
// ============================================================================
// Conversões signed (twos complement) — porque o projeto não tem PARAM_TYPE_I*
// ============================================================================
static inline int32_t s32_from_u32(uint32_t x)
{
return (x & 0x80000000u) ? (int32_t)(x - 0x100000000ULL) : (int32_t)x;
}
static inline int16_t s16_from_u16(uint16_t x)
{
return (x & 0x8000u) ? (int16_t)(x - 0x10000u) : (int16_t)x;
}
// ============================================================================
// CIDs
// ============================================================================
enum
{
CID_DTS024M_L1_VOLTAGE = 0,
CID_DTS024M_L2_VOLTAGE,
CID_DTS024M_L3_VOLTAGE,
CID_DTS024M_L1_CURRENT,
CID_DTS024M_L2_CURRENT,
CID_DTS024M_L3_CURRENT,
CID_DTS024M_L1_ACTIVE_P,
CID_DTS024M_L2_ACTIVE_P,
CID_DTS024M_L3_ACTIVE_P,
CID_DTS024M_PF_L1,
CID_DTS024M_PF_L2,
CID_DTS024M_PF_L3,
CID_DTS024M_FREQUENCY,
CID_DTS024M_TOTAL_ACTIVE_E,
};
// ============================================================================
// DESCRIPTORS (TEMPLATE) — copiamos para RAM e ajustamos:
// - slave_id
// - base offset (0/1)
// - mb_param_type (HOLDING/INPUT)
// ============================================================================
static const mb_parameter_descriptor_t device_parameters_dts024m_tmpl[] = {
// Tensões (U32 / 2 regs) — 0.01 V
{CID_DTS024M_L1_VOLTAGE, STR("L1 Voltage"), STR("V"), 1,
MB_PARAM_HOLDING, DTS024M_L1_VOLTAGE, 2,
0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ},
{CID_DTS024M_L2_VOLTAGE, STR("L2 Voltage"), STR("V"), 1,
MB_PARAM_HOLDING, DTS024M_L2_VOLTAGE, 2,
0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ},
{CID_DTS024M_L3_VOLTAGE, STR("L3 Voltage"), STR("V"), 1,
MB_PARAM_HOLDING, DTS024M_L3_VOLTAGE, 2,
0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ},
// Correntes (U32 / 2 regs) — 0.001 A
{CID_DTS024M_L1_CURRENT, STR("L1 Current"), STR("A"), 1,
MB_PARAM_HOLDING, DTS024M_L1_CURRENT, 2,
0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ},
{CID_DTS024M_L2_CURRENT, STR("L2 Current"), STR("A"), 1,
MB_PARAM_HOLDING, DTS024M_L2_CURRENT, 2,
0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ},
{CID_DTS024M_L3_CURRENT, STR("L3 Current"), STR("A"), 1,
MB_PARAM_HOLDING, DTS024M_L3_CURRENT, 2,
0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ},
// Potência ativa por fase (U32 / 2 regs no descriptor; interpretamos como signed I32)
{CID_DTS024M_L1_ACTIVE_P, STR("L1 Active Power"), STR("W"), 1,
MB_PARAM_HOLDING, DTS024M_L1_ACTIVE_P, 2,
0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ},
{CID_DTS024M_L2_ACTIVE_P, STR("L2 Active Power"), STR("W"), 1,
MB_PARAM_HOLDING, DTS024M_L2_ACTIVE_P, 2,
0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ},
{CID_DTS024M_L3_ACTIVE_P, STR("L3 Active Power"), STR("W"), 1,
MB_PARAM_HOLDING, DTS024M_L3_ACTIVE_P, 2,
0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ},
// PF (U16 / 1 reg; interpretamos como signed I16) — 0.001
{CID_DTS024M_PF_L1, STR("L1 PF"), STR(""), 1,
MB_PARAM_HOLDING, DTS024M_PF_L1, 1,
0, PARAM_TYPE_U16, 2, OPTS(0, 65535, 1), PAR_PERMS_READ},
{CID_DTS024M_PF_L2, STR("L2 PF"), STR(""), 1,
MB_PARAM_HOLDING, DTS024M_PF_L2, 1,
0, PARAM_TYPE_U16, 2, OPTS(0, 65535, 1), PAR_PERMS_READ},
{CID_DTS024M_PF_L3, STR("L3 PF"), STR(""), 1,
MB_PARAM_HOLDING, DTS024M_PF_L3, 1,
0, PARAM_TYPE_U16, 2, OPTS(0, 65535, 1), PAR_PERMS_READ},
// Frequência (U16 / 1 reg) — 0.01 Hz
{CID_DTS024M_FREQUENCY, STR("Frequency"), STR("Hz"), 1,
MB_PARAM_HOLDING, DTS024M_FREQUENCY, 1,
0, PARAM_TYPE_U16, 2, OPTS(0, 10000, 1), PAR_PERMS_READ},
// Energia ativa total (U32 / 2 regs) — 0.01 kWh
{CID_DTS024M_TOTAL_ACTIVE_E, STR("Total Active Energy"), STR("kWh"), 1,
MB_PARAM_HOLDING, DTS024M_TOTAL_ACTIVE_E, 2,
0, PARAM_TYPE_U32, 4, OPTS(0, 0xFFFFFFFF, 1), PAR_PERMS_READ},
};
static mb_parameter_descriptor_t device_parameters_dts024m[ARRAY_SIZE(device_parameters_dts024m_tmpl)];
static const uint16_t num_device_parameters_dts024m = ARRAY_SIZE(device_parameters_dts024m);
static void dts024m_build_descriptors(uint8_t slave_id, uint16_t base_offset, mb_param_type_t area)
{
memcpy(device_parameters_dts024m,
device_parameters_dts024m_tmpl,
sizeof(device_parameters_dts024m));
for (uint16_t i = 0; i < num_device_parameters_dts024m; ++i)
{
device_parameters_dts024m[i].mb_slave_addr = slave_id;
device_parameters_dts024m[i].mb_reg_start =
(uint16_t)(device_parameters_dts024m[i].mb_reg_start + base_offset);
device_parameters_dts024m[i].mb_param_type = area; // HOLDING (FC03) ou INPUT (FC04)
}
}
// ============================================================================
// Modbus master init (fixo) — garante ordem correta (start -> uart_set_mode)
// ============================================================================
static esp_err_t dts024m_master_reinit(uint32_t baud, uart_parity_t parity)
{
if (mb_started)
{
(void)mbc_master_destroy();
mb_started = false;
}
if (uart_is_driver_installed(MB_PORT_NUM))
{
uart_driver_delete(MB_PORT_NUM);
}
mb_communication_info_t comm = {
.port = MB_PORT_NUM,
.mode = MB_MODE_RTU,
.baudrate = baud,
.parity = parity};
void *handler = NULL;
esp_err_t err = mbc_master_init(MB_PORT_SERIAL_MASTER, &handler);
if (err != ESP_OK)
return err;
err = mbc_master_setup(&comm);
if (err != ESP_OK)
{
(void)mbc_master_destroy();
return err;
}
err = uart_set_pin(MB_PORT_NUM, MB_UART_TXD, MB_UART_RXD, MB_UART_RTS, UART_PIN_NO_CHANGE);
if (err != ESP_OK)
{
(void)mbc_master_destroy();
return err;
}
// IMPORTANTE: start antes de uart_set_mode (driver UART costuma ser instalado no start)
err = mbc_master_start();
if (err != ESP_OK)
{
(void)mbc_master_destroy();
return err;
}
mb_started = true;
err = uart_set_mode(MB_PORT_NUM, UART_MODE_RS485_HALF_DUPLEX);
if (err != ESP_OK)
{
(void)mbc_master_destroy();
mb_started = false;
return err;
}
vTaskDelay(pdMS_TO_TICKS(40));
return ESP_OK;
}
// ============================================================================
// Post do evento de medição
// ============================================================================
static void meter_dts024m_post_event(float *voltage, float *current, int *power_w,
float freq_hz, float pf_avg, float total_kwh)
{
meter_event_data_t evt = {
.source = "GRID",
.frequency = freq_hz,
.power_factor = pf_avg,
.total_energy = total_kwh};
memcpy(evt.vrms, voltage, sizeof(evt.vrms));
memcpy(evt.irms, current, sizeof(evt.irms));
memcpy(evt.watt, power_w, sizeof(evt.watt));
esp_err_t err = esp_event_post(METER_EVENT, METER_EVENT_DATA_READY,
&evt, sizeof(evt), portMAX_DELAY);
if (err != ESP_OK)
{
ESP_LOGW(TAG, "Falha ao emitir evento: %s", esp_err_to_name(err));
}
}
// ============================================================================
// Task de polling
// ============================================================================
static void serial_mdb_dts024m_task(void *param)
{
(void)param;
esp_err_t err;
const mb_parameter_descriptor_t *desc = NULL;
float v[3] = {0};
float i[3] = {0};
float pf[3] = {0};
float freq = 0.0f;
float total_kwh = 0.0f;
int p_w[3] = {0};
vTaskDelay(pdMS_TO_TICKS(200)); // settle
while (1)
{
for (uint16_t cid = 0; cid < num_device_parameters_dts024m; cid++)
{
err = mbc_master_get_cid_info(cid, &desc);
if (err != ESP_OK || !desc)
{
continue;
}
uint8_t type = 0;
uint16_t raw_u16 = 0;
uint32_t raw_u32 = 0;
void *value_ptr = &raw_u16;
// U32
switch (cid)
{
case CID_DTS024M_L1_VOLTAGE:
case CID_DTS024M_L2_VOLTAGE:
case CID_DTS024M_L3_VOLTAGE:
case CID_DTS024M_L1_CURRENT:
case CID_DTS024M_L2_CURRENT:
case CID_DTS024M_L3_CURRENT:
case CID_DTS024M_L1_ACTIVE_P:
case CID_DTS024M_L2_ACTIVE_P:
case CID_DTS024M_L3_ACTIVE_P:
case CID_DTS024M_TOTAL_ACTIVE_E:
value_ptr = &raw_u32;
break;
default:
value_ptr = &raw_u16;
break;
}
// 1 retry simples em caso de timeout (podes remover se quiseres menos carga)
err = mbc_master_get_parameter(cid,
(char *)desc->param_key,
(uint8_t *)value_ptr,
&type);
if (err == ESP_ERR_TIMEOUT)
{
vTaskDelay(pdMS_TO_TICKS(60));
err = mbc_master_get_parameter(cid,
(char *)desc->param_key,
(uint8_t *)value_ptr,
&type);
}
if (err == ESP_OK)
{
switch (cid)
{
// V (0.01V)
case CID_DTS024M_L1_VOLTAGE:
v[0] = ((float)raw_u32) * 0.01f;
break;
case CID_DTS024M_L2_VOLTAGE:
v[1] = ((float)raw_u32) * 0.01f;
break;
case CID_DTS024M_L3_VOLTAGE:
v[2] = ((float)raw_u32) * 0.01f;
break;
// I (0.001A)
case CID_DTS024M_L1_CURRENT:
i[0] = ((float)raw_u32) * 0.001f;
break;
case CID_DTS024M_L2_CURRENT:
i[1] = ((float)raw_u32) * 0.001f;
break;
case CID_DTS024M_L3_CURRENT:
i[2] = ((float)raw_u32) * 0.001f;
break;
// P ativa (twos complement I32) — atenção: escala depende do modelo
case CID_DTS024M_L1_ACTIVE_P:
p_w[0] = (int)s32_from_u32(raw_u32);
break;
case CID_DTS024M_L2_ACTIVE_P:
p_w[1] = (int)s32_from_u32(raw_u32);
break;
case CID_DTS024M_L3_ACTIVE_P:
p_w[2] = (int)s32_from_u32(raw_u32);
break;
// PF (twos complement I16; 0.001)
case CID_DTS024M_PF_L1:
pf[0] = ((float)s16_from_u16(raw_u16)) * 0.001f;
break;
case CID_DTS024M_PF_L2:
pf[1] = ((float)s16_from_u16(raw_u16)) * 0.001f;
break;
case CID_DTS024M_PF_L3:
pf[2] = ((float)s16_from_u16(raw_u16)) * 0.001f;
break;
// Freq (0.01Hz)
case CID_DTS024M_FREQUENCY:
freq = ((float)raw_u16) * 0.01f;
break;
// Energia (0.01kWh)
case CID_DTS024M_TOTAL_ACTIVE_E:
total_kwh = ((float)raw_u32) * 0.01f;
break;
default:
break;
}
ESP_LOGD(TAG, "%s (cid=%u) ok (u16=%u u32=%u)",
desc->param_key, cid, (unsigned)raw_u16, (unsigned)raw_u32);
}
else
{
ESP_LOGE(TAG, "CID %u (%s) read failed: %s",
cid, desc->param_key, esp_err_to_name(err));
}
vTaskDelay(POLL_INTERVAL);
}
// PF médio simples (ignora zeros)
float pf_sum = 0.0f;
int pf_cnt = 0;
for (int k = 0; k < 3; ++k)
{
if (pf[k] != 0.0f)
{
pf_sum += pf[k];
pf_cnt++;
}
}
float pf_avg = (pf_cnt ? pf_sum / pf_cnt : 0.0f);
meter_dts024m_post_event(v, i, p_w, freq, pf_avg, total_kwh);
vTaskDelay(UPDATE_INTERVAL);
}
}
// ============================================================================
// Init / Start / Stop
// ============================================================================
esp_err_t meter_dts024m_init(void)
{
if (is_initialized)
{
ESP_LOGW(TAG, "Already initialized");
return ESP_ERR_INVALID_STATE;
}
// init fixo (produção)
esp_err_t err = dts024m_master_reinit(DTS024M_PROD_BAUD, DTS024M_PROD_PARITY);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "master_reinit failed: %s", esp_err_to_name(err));
return err;
}
// monta descriptors reais com ID/offset/area fixos
dts024m_build_descriptors(DTS024M_PROD_SLAVE_ID, DTS024M_PROD_BASE_OFFSET, DTS024M_PROD_AREA);
// aplica descriptors reais
esp_err_t derr = mbc_master_set_descriptor(device_parameters_dts024m,
num_device_parameters_dts024m);
if (derr != ESP_OK)
{
ESP_LOGE(TAG, "set_descriptor failed: %s", esp_err_to_name(derr));
return derr;
}
is_initialized = true;
ESP_LOGI(TAG, "DTS024M initialized (PROD) baud=%d parity=%d id=%d area=%s base=%d",
DTS024M_PROD_BAUD,
(int)DTS024M_PROD_PARITY,
DTS024M_PROD_SLAVE_ID,
(DTS024M_PROD_AREA == MB_PARAM_HOLDING ? "FC03" : "FC04"),
DTS024M_PROD_BASE_OFFSET);
return ESP_OK;
}
esp_err_t meter_dts024m_start(void)
{
if (!is_initialized)
{
ESP_LOGE(TAG, "Not initialized");
return ESP_ERR_INVALID_STATE;
}
if (meter_task == NULL)
{
xTaskCreate(serial_mdb_dts024m_task,
"meter_dts024m_task",
4096, NULL, 3, &meter_task);
ESP_LOGI(TAG, "DTS024M task started");
}
return ESP_OK;
}
void meter_dts024m_stop(void)
{
if (!is_initialized)
{
ESP_LOGW(TAG, "Not initialized, skipping stop");
return;
}
if (meter_task)
{
vTaskDelete(meter_task);
meter_task = NULL;
ESP_LOGI(TAG, "DTS024M task stopped");
}
if (mb_started)
{
(void)mbc_master_destroy();
mb_started = false;
}
if (uart_is_driver_installed(MB_PORT_NUM))
{
uart_driver_delete(MB_PORT_NUM);
ESP_LOGI(TAG, "UART driver deleted");
}
is_initialized = false;
ESP_LOGI(TAG, "Meter DTS024M cleaned up");
}

View File

@@ -0,0 +1,35 @@
#ifndef METER_DTS024M_H_
#define METER_DTS024M_H_
#include <stdint.h>
#include <stdbool.h>
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Inicializa o driver do medidor DTS024M (UART RS485, Modbus, registradores).
*
* @return esp_err_t Retorna ESP_OK se a inicialização for bem-sucedida, caso contrário retorna um erro.
*/
esp_err_t meter_dts024m_init(void);
/**
* @brief Inicia a tarefa de leitura de dados do medidor DTS024M.
*
* @return esp_err_t Retorna ESP_OK se a tarefa for iniciada com sucesso, caso contrário retorna um erro.
*/
esp_err_t meter_dts024m_start(void);
/**
* @brief Para a tarefa de leitura e limpa os dados internos do medidor DTS024M.
*/
void meter_dts024m_stop(void);
#ifdef __cplusplus
}
#endif
#endif /* METER_DTS024M_H_ */

View File

@@ -138,7 +138,7 @@ static void meter_dts6619_post_event(float *voltage, float *current, int *power_
memcpy(evt.watt, power_w, sizeof(evt.watt));
esp_err_t err = esp_event_post(METER_EVENT, METER_EVENT_DATA_READY,
&evt, sizeof(evt), pdMS_TO_TICKS(10));
&evt, sizeof(evt), portMAX_DELAY);
if (err != ESP_OK)
{
ESP_LOGW(TAG, "Falha ao emitir evento: %s", esp_err_to_name(err));

View File

@@ -24,7 +24,6 @@ esp_err_t meter_dts6619_start(void);
*/
void meter_dts6619_stop(void);
#ifdef __cplusplus
}
#endif

View File

@@ -7,17 +7,18 @@
#include "driver/uart.h"
#include <stddef.h>
#include <string.h>
#include "meter_ea777.h"
#define TAG "serial_mdb_ea777"
// ===== UART / RS-485 =====
#define MB_PORT_NUM 2
#define MB_PORT_NUM 1
#define MB_DEV_SPEED 9600
// Ajuste os pinos conforme seu hardware
#define MB_UART_TXD 17
#define MB_UART_RXD 16
#define MB_UART_RTS 2 // pino DE/RE do transceiver RS-485
#define MB_UART_TXD 21
#define MB_UART_RXD 22
#define MB_UART_RTS UART_PIN_NO_CHANGE // sem DE/RE
// ===== Timings =====
#define UPDATE_INTERVAL (5000 / portTICK_PERIOD_MS)
@@ -148,7 +149,7 @@ static void meter_ea777_post_event(float *voltage, float *current, int *power_w,
memcpy(evt.watt, power_w, sizeof(evt.watt));
esp_err_t err = esp_event_post(METER_EVENT, METER_EVENT_DATA_READY,
&evt, sizeof(evt), pdMS_TO_TICKS(10));
&evt, sizeof(evt), portMAX_DELAY);
if (err != ESP_OK)
{
ESP_LOGW(TAG, "Falha ao emitir evento: %s", esp_err_to_name(err));
@@ -321,9 +322,10 @@ esp_err_t meter_ea777_init(void)
ESP_ERROR_CHECK(mbc_master_setup(&comm));
ESP_ERROR_CHECK(uart_set_pin(MB_PORT_NUM,
MB_UART_TXD, MB_UART_RXD,
MB_UART_RTS, UART_PIN_NO_CHANGE));
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
ESP_ERROR_CHECK(mbc_master_start());
ESP_ERROR_CHECK(uart_set_mode(MB_PORT_NUM, UART_MODE_RS485_HALF_DUPLEX));
ESP_ERROR_CHECK(uart_set_mode(MB_PORT_NUM, UART_MODE_UART));
// ESP_ERROR_CHECK(uart_set_mode(MB_PORT_NUM, UART_MODE_UART));
vTaskDelay(pdMS_TO_TICKS(50));
ESP_ERROR_CHECK(mbc_master_set_descriptor(device_parameters_ea777,

View File

@@ -8,11 +8,12 @@
#define TAG "serial_mdb_orno513"
#define MB_PORT_NUM 2
#define MB_DEV_SPEED 9600
#define MB_UART_TXD 17
#define MB_UART_RXD 16
#define MB_UART_RTS 5
#define MB_UART_RTS 2
#define UPDATE_INTERVAL (3000 / portTICK_PERIOD_MS)
#define POLL_INTERVAL (100 / portTICK_PERIOD_MS)
@@ -129,7 +130,7 @@ static void serial_mdb_task(void *param) {
memcpy(evt.irms, current, sizeof(evt.irms));
memcpy(evt.watt, watt, sizeof(evt.watt));
esp_event_post(METER_EVENT, METER_EVENT_DATA_READY, &evt, sizeof(evt), pdMS_TO_TICKS(10));
esp_event_post(METER_EVENT, METER_EVENT_DATA_READY, &evt, sizeof(evt), portMAX_DELAY);
vTaskDelay(UPDATE_INTERVAL);

View File

@@ -91,7 +91,7 @@ static void meter_orno516_post_event(float *voltage, float *current, int *power)
memcpy(evt.watt, power, sizeof(evt.watt));
esp_err_t err = esp_event_post(METER_EVENT, METER_EVENT_DATA_READY,
&evt, sizeof(evt), pdMS_TO_TICKS(10));
&evt, sizeof(evt), portMAX_DELAY);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Falha ao emitir evento: %s", esp_err_to_name(err));

View File

@@ -207,7 +207,7 @@ static void serial_mdb_task(void *param)
memcpy(evt.irms, current, sizeof(evt.irms));
memcpy(evt.watt, watt, sizeof(evt.watt));
esp_event_post(METER_EVENT, METER_EVENT_DATA_READY, &evt, sizeof(evt), pdMS_TO_TICKS(10));
esp_event_post(METER_EVENT, METER_EVENT_DATA_READY, &evt, sizeof(evt), portMAX_DELAY);
vTaskDelay(UPDATE_INTERVAL);
}
}

View File

@@ -12,7 +12,7 @@
#define TAG "meter_zigbee"
// UART config
#define UART_PORT UART_NUM_1
#define UART_PORT UART_NUM_2
#define TXD_PIN GPIO_NUM_17
#define RXD_PIN GPIO_NUM_16
#define UART_BUF_SIZE 128
@@ -41,7 +41,8 @@
#define PHASE_L3 2
// Internal meter state
typedef struct {
typedef struct
{
float vrms[PHASE_COUNT];
float irms[PHASE_COUNT];
int watt[PHASE_COUNT];
@@ -58,24 +59,28 @@ static meter_zigbee_data_t meter_data = {0};
static SemaphoreHandle_t meter_mutex = NULL;
static TaskHandle_t meter_zigbee_task = NULL;
bool meter_zigbee_is_running(void) {
bool meter_zigbee_is_running(void)
{
return meter_zigbee_task != NULL;
}
void send_stop_command(void) {
//const char *cmd = "stop\n"; // Comando enviado para o outro lado interpretar e dormir
//uart_write_bytes(UART_PORT, cmd, strlen(cmd));
//uart_wait_tx_done(UART_PORT, pdMS_TO_TICKS(100)); // Aguarda envio terminar
static inline int32_t tuya_power16_to_signed(uint16_t p)
{
// Igual ao quirk multi_dp_to_power()
if (p > 0x7FFF)
{
return (int32_t)((0x999A - p) * -1);
}
return (int32_t)p;
}
static void meter_zigbee_post_event(void) {
static void meter_zigbee_post_event(void)
{
meter_event_data_t evt = {
.source = "GRID",
.frequency = meter_data.frequency,
.power_factor = meter_data.power_factor,
.total_energy = meter_data.total_energy
};
.total_energy = meter_data.total_energy};
memcpy(evt.vrms, meter_data.vrms, sizeof(evt.vrms));
memcpy(evt.irms, meter_data.irms, sizeof(evt.irms));
@@ -85,19 +90,21 @@ static void meter_zigbee_post_event(void) {
METER_EVENT_DATA_READY,
&evt,
sizeof(evt),
pdMS_TO_TICKS(10));
portMAX_DELAY);
if (err != ESP_OK) {
if (err != ESP_OK)
{
ESP_LOGW(TAG, "Falha ao emitir evento: %s", esp_err_to_name(err));
}
}
static void handle_zigbee_frame(const uint8_t *buf, size_t len)
{
ESP_LOGD(TAG, "Received UART frame (%d bytes):", len);
// ESP_LOG_BUFFER_HEX(TAG, buf, len);
static void handle_zigbee_frame(const uint8_t *buf, size_t len) {
ESP_LOGI(TAG, "Received UART frame (%d bytes):", len);
ESP_LOG_BUFFER_HEX(TAG, buf, len);
if (len < RX_FRAME_SIZE) {
if (len < RX_FRAME_SIZE)
{
ESP_LOGW(TAG, "Invalid frame: too short (len = %d)", len);
return;
}
@@ -105,23 +112,35 @@ static void handle_zigbee_frame(const uint8_t *buf, size_t len) {
uint16_t attr = buf[2] | (buf[3] << 8);
uint8_t size = buf[5];
if (size != 8) {
if (size != 8)
{
ESP_LOGW(TAG, "Unsupported payload size: %d", size);
return;
}
uint16_t volt_raw = (buf[6] << 8) | buf[7];
uint32_t current_raw = (buf[8] << 16) | (buf[9] << 8) | buf[10];
uint32_t power_raw = (buf[11] << 16) | (buf[12] << 8) | buf[13];
// payload 8 bytes começa em buf[6]
const uint8_t *p = &buf[6];
uint16_t volt_raw = ((uint16_t)p[0] << 8) | p[1];
uint16_t curr_raw_u16 = ((uint16_t)p[3] << 8) | p[4]; // 2 bytes
uint16_t pow_raw_u16 = ((uint16_t)p[6] << 8) | p[7]; // 2 bytes
int32_t power = tuya_power16_to_signed(pow_raw_u16);
float volt = volt_raw / 10.0f;
float current = current_raw / 1000.0f;
float power = power_raw;
float curr = curr_raw_u16 / 1000.0f;
ESP_LOGI(TAG, "Parsed Attr 0x%04X: V=%.1fV I=%.2fA P=%.1fW", attr, volt, current, power);
// Se queres “corrente com sinal”, deriva pelo sinal da potência:
float current = (power < 0) ? -curr : curr;
if (xSemaphoreTake(meter_mutex, pdMS_TO_TICKS(10)) == pdTRUE) {
switch (attr) {
ESP_LOGD(TAG, "Attr 0x%04X: V=%.1fV I=%.3fA (signed=%+.3fA) P=%+ldW",
attr, volt, curr, current, (long)power);
if (xSemaphoreTake(meter_mutex, pdMS_TO_TICKS(10)) == pdTRUE)
{
switch (attr)
{
case ATTR_CURRENT_L1:
case ATTR_CURRENT_L1_ALT:
meter_data.irms[PHASE_L1] = current;
@@ -160,15 +179,18 @@ static void handle_zigbee_frame(const uint8_t *buf, size_t len) {
}
// Verifica se todas as 3 fases foram atualizadas
if (phase_updated[PHASE_L1] && phase_updated[PHASE_L2] && phase_updated[PHASE_L3]) {
if (phase_updated[PHASE_L1] && phase_updated[PHASE_L2] && phase_updated[PHASE_L3])
{
meter_zigbee_post_event();
memset(phase_updated, 0, sizeof(phase_updated));
}
}
static void meter_zigbee_task_func(void *param) {
static void meter_zigbee_task_func(void *param)
{
uint8_t *buf = malloc(RX_FRAME_SIZE);
if (!buf) {
if (!buf)
{
ESP_LOGE(TAG, "Failed to allocate buffer");
vTaskDelete(NULL);
return;
@@ -176,13 +198,19 @@ static void meter_zigbee_task_func(void *param) {
ESP_LOGI(TAG, "Zigbee meter task started");
while (1) {
while (1)
{
int len = uart_read_bytes(UART_PORT, buf, RX_FRAME_SIZE, pdMS_TO_TICKS(5000));
if (len == RX_FRAME_SIZE) {
if (len == RX_FRAME_SIZE)
{
handle_zigbee_frame(buf, len);
} else if (len == 0) {
}
else if (len == 0)
{
ESP_LOGD(TAG, "UART timeout with no data");
} else {
}
else
{
ESP_LOGW(TAG, "Incomplete frame received (%d bytes)", len);
}
}
@@ -191,12 +219,15 @@ static void meter_zigbee_task_func(void *param) {
vTaskDelete(NULL);
}
esp_err_t meter_zigbee_init(void) {
esp_err_t meter_zigbee_init(void)
{
ESP_LOGI(TAG, "Initializing Zigbee meter");
if (!meter_mutex) {
if (!meter_mutex)
{
meter_mutex = xSemaphoreCreateMutex();
if (!meter_mutex) return ESP_ERR_NO_MEM;
if (!meter_mutex)
return ESP_ERR_NO_MEM;
}
uart_config_t config = {
@@ -205,8 +236,7 @@ esp_err_t meter_zigbee_init(void) {
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_DEFAULT
};
.source_clk = UART_SCLK_DEFAULT};
ESP_ERROR_CHECK(uart_param_config(UART_PORT, &config));
ESP_ERROR_CHECK(uart_set_pin(UART_PORT, TXD_PIN, RXD_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
@@ -215,30 +245,28 @@ esp_err_t meter_zigbee_init(void) {
return ESP_OK;
}
esp_err_t meter_zigbee_start(void) {
if (meter_zigbee_task) return ESP_ERR_INVALID_STATE;
esp_err_t meter_zigbee_start(void)
{
if (meter_zigbee_task)
return ESP_ERR_INVALID_STATE;
xTaskCreate(meter_zigbee_task_func, "meter_zigbee_task", 4096, NULL, 3, &meter_zigbee_task);
return ESP_OK;
}
void meter_zigbee_stop(void)
{
void meter_zigbee_stop(void) {
//send_stop_command();
//vTaskDelay(pdMS_TO_TICKS(100)); // Aguarda o outro lado processar
if (meter_zigbee_task) {
if (meter_zigbee_task)
{
vTaskDelete(meter_zigbee_task);
meter_zigbee_task = NULL;
}
uart_driver_delete(UART_PORT);
if (meter_mutex) {
if (meter_mutex)
{
vSemaphoreDelete(meter_mutex);
meter_mutex = NULL;
}

View File

@@ -2,34 +2,53 @@
#define METER_EVENTS_H
#include "esp_event.h"
#include "meter_manager.h" // Para meter_type_t
#include "meter_manager.h" // meter_type_t
#include <stdint.h> // int32_t, int64_t
#ifdef __cplusplus
extern "C" {
extern "C"
{
#endif
// Base de eventos dos medidores
ESP_EVENT_DECLARE_BASE(METER_EVENT);
// Base de eventos dos medidores
ESP_EVENT_DECLARE_BASE(METER_EVENT);
// IDs de eventos emitidos por medidores
typedef enum {
// IDs de eventos emitidos por medidores
typedef enum
{
METER_EVENT_DATA_READY = 0,
METER_EVENT_ERROR,
METER_EVENT_STARTED,
METER_EVENT_STOPPED
} meter_event_id_t;
METER_EVENT_STOPPED,
METER_EVENT_CONFIG_UPDATED
} meter_event_id_t;
// Estrutura de dados enviados com METER_EVENT_DATA_READY
typedef struct {
// Estrutura de dados enviados com METER_EVENT_DATA_READY
// NOTA: campos não suportados pelo meter devem ficar a 0.
typedef struct
{
const char *source; // "GRID" ou "EVSE"
float vrms[3]; // Tensão por fase
float irms[3]; // Corrente por fase
int watt[3]; // Potência ativa por fase
float frequency; // Frequência da rede (Hz)
float power_factor; // Fator de potência
float total_energy; // Energia acumulada (kWh)
} meter_event_data_t;
float vrms[3]; // V por fase (0 se não existir)
float irms[3]; // A por fase (0 se não existir)
int32_t watt[3]; // W por fase (0 se não existir)
int32_t watt_total; // W total ASSINADO: +import / -export (0 se não existir)
float frequency; // Hz (0 se não existir)
float power_factor; // (0 se não existir)
float total_energy; // kWh (0 se não existir)
int64_t timestamp_us; // esp_timer_get_time() (0 => consumidor pode usar "now")
} meter_event_data_t;
// Estrutura de dados enviados com METER_EVENT_CONFIG_UPDATED
typedef struct
{
meter_type_t grid_type;
meter_type_t evse_type;
int64_t timestamp_us; // esp_timer_get_time()
} meter_config_event_t;
#ifdef __cplusplus
}

View File

@@ -17,7 +17,8 @@ typedef enum {
METER_TYPE_DTS6619, // dts6619
METER_TYPE_MONO_ZIGBEE, // Zigbee single-phase
METER_TYPE_TRIF_ZIGBEE, // Zigbee three-phase
METER_TYPE_EA777 // EA777
METER_TYPE_EA777, // EA777
METER_TYPE_DTS024M,
} meter_type_t;
/**

View File

@@ -9,11 +9,17 @@
#include "meter_dds661.h"
#include "meter_zigbee.h"
#include "meter_ea777.h"
#include "meter_dts024m.h"
#include "nvs_flash.h"
#include "nvs.h"
#include <string.h>
#include "network_events.h"
#include "meter_events.h"
#include "esp_event.h"
#include "esp_timer.h"
// NEW:
#include "storage_service.h"
static const char *TAG = "meter_manager";
@@ -21,179 +27,200 @@ static const char *TAG = "meter_manager";
static meter_type_t meter_evse_type = METER_TYPE_NONE;
static meter_type_t meter_grid_type = METER_TYPE_NONE;
#define NVS_NAMESPACE "meterconfig"
#define NVS_EVSE_MODEL "evse_model"
#define NVS_GRID_MODEL "grid_model"
#define STORE_NAMESPACE "meterconfig"
#define STORE_EVSE_MODEL "evse_model"
#define STORE_GRID_MODEL "grid_model"
// timeouts storage
#define STORAGE_TO pdMS_TO_TICKS(800)
// ------------------------------------------------------------------
// Helpers storage (robustos a queue cheia)
// ------------------------------------------------------------------
static esp_err_t storage_try_flush(void)
{
return storage_flush_sync(pdMS_TO_TICKS(2000));
}
static esp_err_t storage_try_set_u8(const char *ns, const char *key, uint8_t v)
{
for (int attempt = 0; attempt < 3; ++attempt)
{
esp_err_t err = storage_set_u8_async(ns, key, v);
if (err == ESP_OK)
return ESP_OK;
if (err == ESP_ERR_TIMEOUT)
{
(void)storage_try_flush();
continue;
}
return err;
}
return ESP_ERR_TIMEOUT;
}
static esp_err_t storage_get_u8_default(const char *ns, const char *key, uint8_t *out, uint8_t def)
{
if (!out)
return ESP_ERR_INVALID_ARG;
uint8_t v = def;
esp_err_t err = storage_get_u8_sync(ns, key, &v, STORAGE_TO);
if (err == ESP_OK)
{
*out = v;
return ESP_OK;
}
// se não existir / erro, devolve default
*out = def;
return err;
}
// ------------------------------------------------------------------
/*
static void meter_manager_network_event_handler(void *arg, esp_event_base_t base, int32_t event_id, void *data)
{
(void)arg;
(void)data;
if (base != NETWORK_EVENTS)
return;
switch (event_id)
{
case NETWORK_EVENT_AP_STARTED:
ESP_LOGI(TAG, "Recebido NETWORK_EVENT_AP_STARTED, parando medidor de grid");
// meter_manager_grid_stop();
ESP_LOGD(TAG, "Recebido NETWORK_EVENT_AP_STARTED, parando medidor de grid");
break;
case NETWORK_EVENT_AP_STOP:
ESP_LOGI(TAG, "Recebido NETWORK_EVENT_AP_STOP, reiniciando medidor de grid");
// meter_manager_grid_start();
ESP_LOGD(TAG, "Recebido NETWORK_EVENT_AP_STOP, reiniciando medidor de grid");
break;
case NETWORK_EVENT_STA_GOT_IP:
ESP_LOGI(TAG, "Recebido NETWORK_EVENT_STA_GOT_IP");
// opcional: reiniciar ou logar
ESP_LOGD(TAG, "Recebido NETWORK_EVENT_STA_GOT_IP");
break;
default:
break;
}
}
*/
// Função unificada para ler ou inicializar um modelo de medidor
// Função unificada para ler ou inicializar um modelo de medidor (via storage_service)
static esp_err_t load_or_init_meter_model(const char *key, meter_type_t *type)
{
nvs_handle_t handle;
esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to open NVS handle for %s: %s", key, esp_err_to_name(err));
return err;
}
if (!type)
return ESP_ERR_INVALID_ARG;
// tenta ler
uint8_t value = 0xFF;
esp_err_t err = storage_get_u8_sync(STORE_NAMESPACE, key, &value, STORAGE_TO);
uint8_t value = 0;
err = nvs_get_u8(handle, key, &value);
if (err == ESP_OK && value < 255)
{
*type = (meter_type_t)value;
ESP_LOGI(TAG, "Loaded meter type %d from NVS key '%s'", value, key);
}
else
{
*type = METER_TYPE_NONE;
nvs_set_u8(handle, key, *type);
nvs_commit(handle);
ESP_LOGW(TAG, "Invalid or missing key '%s', setting default (NONE)", key);
ESP_LOGD(TAG, "Loaded meter type %u from storage key '%s/%s'", (unsigned)value, STORE_NAMESPACE, key);
return ESP_OK;
}
nvs_close(handle);
// se não existir / inválido -> default NONE e grava
*type = METER_TYPE_NONE;
esp_err_t w = storage_try_set_u8(STORE_NAMESPACE, key, (uint8_t)(*type));
if (w != ESP_OK)
ESP_LOGE(TAG, "Failed to init key '%s/%s' to NONE: %s", STORE_NAMESPACE, key, esp_err_to_name(w));
(void)storage_try_flush();
ESP_LOGW(TAG, "Invalid/missing key '%s/%s' (read=%s), setting default (NONE)",
STORE_NAMESPACE, key, esp_err_to_name(err));
return ESP_OK; // seguimos com default
}
static esp_err_t write_meter_model_to_storage(const char *key, meter_type_t meter_type)
{
esp_err_t err = storage_try_set_u8(STORE_NAMESPACE, key, (uint8_t)meter_type);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to write meter type to storage key '%s/%s': %s",
STORE_NAMESPACE, key, esp_err_to_name(err));
return err;
}
(void)storage_try_flush();
ESP_LOGD(TAG, "Saved meter type %d to storage key '%s/%s'", (int)meter_type, STORE_NAMESPACE, key);
return ESP_OK;
}
static esp_err_t write_meter_model_to_nvs(const char *key, meter_type_t meter_type)
{
nvs_handle_t handle;
esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to open NVS handle for writing");
return err;
}
err = nvs_set_u8(handle, key, (uint8_t)meter_type);
if (err == ESP_OK)
{
err = nvs_commit(handle);
ESP_LOGI(TAG, "Saved meter type %d to NVS key '%s'", meter_type, key);
}
else
{
ESP_LOGE(TAG, "Failed to write meter type to NVS key '%s'", key);
}
nvs_close(handle);
return err;
}
/**
* @brief Initializes the meter manager system.
*
* This function initializes both the EVSE and GRID meters,
* and registers the event handler to listen for NETWORK_EVENTS
* (e.g., AP started/stopped, STA got IP).
*
* @return esp_err_t ESP_OK on success, or an error code.
* @brief Inicializa o sistema de meter manager.
*/
esp_err_t meter_manager_init(void)
{
esp_err_t err;
// garantir storage pronto
esp_err_t s = storage_service_init();
if (s != ESP_OK)
ESP_LOGE(TAG, "storage_service_init failed: %s", esp_err_to_name(s));
// Initialize EVSE meter (habilite quando quiser)
// err = meter_manager_evse_init();
// if (err != ESP_OK) return err;
esp_err_t err;
// Initialize GRID meter
err = meter_manager_grid_init();
if (err != ESP_OK)
return err;
// Register handler for custom network events
ESP_LOGI(TAG, "Registering network event handler");
return esp_event_handler_register(NETWORK_EVENTS,
// Regista handler para eventos de rede
/*
ESP_LOGD(TAG, "Registering network event handler");
err = esp_event_handler_register(
NETWORK_EVENTS,
ESP_EVENT_ANY_ID,
meter_manager_network_event_handler,
NULL);
*/
if (err != ESP_OK)
return err;
// Emite um evento inicial de configuração
meter_config_event_t ev = {
.grid_type = meter_manager_grid_get_model(),
.evse_type = meter_manager_evse_get_model(),
.timestamp_us = esp_timer_get_time()};
esp_event_post(METER_EVENT,
METER_EVENT_CONFIG_UPDATED,
&ev,
sizeof(ev),
0);
return ESP_OK;
}
/**
* @brief Starts all configured meters (EVSE and GRID).
*
* This function starts the EVSE and GRID meters based on their configured types.
* It does not register event handlers — that is handled by `meter_manager_init()`.
*
* @return esp_err_t ESP_OK on success, or an error code from one of the start calls.
*/
esp_err_t meter_manager_start(void)
{
esp_err_t err;
// Start EVSE meter (habilite quando quiser)
// err = meter_manager_evse_start();
// if (err != ESP_OK) return err;
// Start GRID meter
err = meter_manager_grid_start();
if (err != ESP_OK)
return err;
return ESP_OK;
return meter_manager_grid_start();
}
/**
* @brief Stops all meters and unregisters event handlers.
*
* This function gracefully stops the EVSE and GRID meters
* and unregisters the previously registered network event handler.
*
* @return esp_err_t ESP_OK on success, or an error code.
*/
esp_err_t meter_manager_stop(void)
{
esp_err_t err;
// Stop EVSE meter
// err = meter_manager_evse_stop();
// if (err != ESP_OK) return err;
// Stop GRID meter
err = meter_manager_grid_stop();
if (err != ESP_OK)
return err;
return ESP_OK;
return meter_manager_grid_stop();
}
// ---------- EVSE ----------
esp_err_t meter_manager_evse_init()
{
esp_err_t err = load_or_init_meter_model(NVS_EVSE_MODEL, &meter_evse_type);
esp_err_t err = load_or_init_meter_model(STORE_EVSE_MODEL, &meter_evse_type);
if (err != ESP_OK)
return err;
ESP_LOGI(TAG, "Initializing EVSE meter of type %s", meter_type_to_str(meter_evse_type));
ESP_LOGD(TAG, "Initializing EVSE meter of type %s", meter_type_to_str(meter_evse_type));
switch (meter_evse_type)
{
@@ -213,6 +240,8 @@ esp_err_t meter_manager_evse_init()
return meter_dds661_init();
case METER_TYPE_EA777:
return meter_ea777_init();
case METER_TYPE_DTS024M:
return meter_dts024m_init();
case METER_TYPE_MONO_ZIGBEE:
case METER_TYPE_TRIF_ZIGBEE:
return meter_zigbee_init();
@@ -242,6 +271,8 @@ esp_err_t meter_manager_evse_start()
return meter_dds661_start();
case METER_TYPE_EA777:
return meter_ea777_start();
case METER_TYPE_DTS024M:
return meter_dts024m_start();
case METER_TYPE_MONO_ZIGBEE:
case METER_TYPE_TRIF_ZIGBEE:
return meter_zigbee_start();
@@ -278,6 +309,9 @@ esp_err_t meter_manager_evse_stop(void)
case METER_TYPE_EA777:
meter_ea777_stop();
break;
case METER_TYPE_DTS024M:
meter_dts024m_stop();
break;
case METER_TYPE_MONO_ZIGBEE:
case METER_TYPE_TRIF_ZIGBEE:
meter_zigbee_stop();
@@ -292,13 +326,13 @@ esp_err_t meter_manager_evse_stop(void)
esp_err_t meter_manager_grid_init()
{
esp_err_t err = load_or_init_meter_model(NVS_GRID_MODEL, &meter_grid_type);
esp_err_t err = load_or_init_meter_model(STORE_GRID_MODEL, &meter_grid_type);
if (err != ESP_OK)
return err;
ESP_LOGI(TAG, "Initializing GRID meter of type %s", meter_type_to_str(meter_grid_type));
ESP_LOGD(TAG, "Initializing GRID meter of type %s", meter_type_to_str(meter_grid_type));
switch (meter_grid_type) // corrigido: ORNO-513 -> driver orno513
switch (meter_grid_type)
{
case METER_TYPE_NONE:
return ESP_OK;
@@ -316,6 +350,8 @@ esp_err_t meter_manager_grid_init()
return meter_dds661_init();
case METER_TYPE_EA777:
return meter_ea777_init();
case METER_TYPE_DTS024M:
return meter_dts024m_init();
case METER_TYPE_MONO_ZIGBEE:
case METER_TYPE_TRIF_ZIGBEE:
return meter_zigbee_init();
@@ -334,7 +370,7 @@ esp_err_t meter_manager_grid_start()
case METER_TYPE_ADE7758:
return meter_ade7758_start();
case METER_TYPE_ORNO513:
return meter_orno513_start(); // corrigido
return meter_orno513_start();
case METER_TYPE_ORNO516:
return meter_orno516_start();
case METER_TYPE_ORNO526:
@@ -345,6 +381,8 @@ esp_err_t meter_manager_grid_start()
return meter_dds661_start();
case METER_TYPE_EA777:
return meter_ea777_start();
case METER_TYPE_DTS024M:
return meter_dts024m_start();
case METER_TYPE_MONO_ZIGBEE:
case METER_TYPE_TRIF_ZIGBEE:
return meter_zigbee_start();
@@ -364,7 +402,7 @@ esp_err_t meter_manager_grid_stop(void)
meter_ade7758_stop();
break;
case METER_TYPE_ORNO513:
meter_orno513_stop(); // corrigido
meter_orno513_stop();
break;
case METER_TYPE_ORNO516:
meter_orno516_stop();
@@ -381,6 +419,9 @@ esp_err_t meter_manager_grid_stop(void)
case METER_TYPE_EA777:
meter_ea777_stop();
break;
case METER_TYPE_DTS024M:
meter_dts024m_stop();
break;
case METER_TYPE_MONO_ZIGBEE:
case METER_TYPE_TRIF_ZIGBEE:
meter_zigbee_stop();
@@ -391,18 +432,64 @@ esp_err_t meter_manager_grid_stop(void)
return ESP_OK;
}
// ---------- Utilidades ----------
// ---------- Utilidades / setters com evento ----------
esp_err_t meter_manager_evse_set_model(meter_type_t meter_type)
{
esp_err_t err;
err = meter_manager_evse_stop();
if (err != ESP_OK)
ESP_LOGW(TAG, "Failed to stop EVSE meter before changing model: %s", esp_err_to_name(err));
meter_evse_type = meter_type;
return write_meter_model_to_nvs(NVS_EVSE_MODEL, meter_evse_type);
err = write_meter_model_to_storage(STORE_EVSE_MODEL, meter_evse_type);
if (err != ESP_OK)
return err;
err = meter_manager_evse_init();
if (err == ESP_OK)
err = meter_manager_evse_start();
meter_config_event_t ev = {
.grid_type = meter_manager_grid_get_model(),
.evse_type = meter_manager_evse_get_model(),
.timestamp_us = esp_timer_get_time()};
esp_err_t evt_err = esp_event_post(METER_EVENT, METER_EVENT_CONFIG_UPDATED, &ev, sizeof(ev), portMAX_DELAY);
if (evt_err != ESP_OK)
ESP_LOGW(TAG, "Failed to post METER_EVENT_CONFIG_UPDATED (EVSE): %s", esp_err_to_name(evt_err));
return err;
}
esp_err_t meter_manager_grid_set_model(meter_type_t meter_type)
{
esp_err_t err;
err = meter_manager_grid_stop();
if (err != ESP_OK)
ESP_LOGW(TAG, "Failed to stop GRID meter before changing model: %s", esp_err_to_name(err));
meter_grid_type = meter_type;
return write_meter_model_to_nvs(NVS_GRID_MODEL, meter_grid_type);
err = write_meter_model_to_storage(STORE_GRID_MODEL, meter_grid_type);
if (err != ESP_OK)
return err;
err = meter_manager_grid_init();
if (err == ESP_OK)
err = meter_manager_grid_start();
meter_config_event_t ev = {
.grid_type = meter_manager_grid_get_model(),
.evse_type = meter_manager_evse_get_model(),
.timestamp_us = esp_timer_get_time()};
esp_err_t evt_err = esp_event_post(METER_EVENT, METER_EVENT_CONFIG_UPDATED, &ev, sizeof(ev), portMAX_DELAY);
if (evt_err != ESP_OK)
ESP_LOGW(TAG, "Failed to post METER_EVENT_CONFIG_UPDATED (GRID): %s", esp_err_to_name(evt_err));
return err;
}
bool meter_manager_evse_is_enabled(void)
@@ -410,15 +497,8 @@ bool meter_manager_evse_is_enabled(void)
return meter_manager_evse_get_model() != METER_TYPE_NONE;
}
meter_type_t meter_manager_evse_get_model(void)
{
return meter_evse_type;
}
meter_type_t meter_manager_grid_get_model(void)
{
return meter_grid_type;
}
meter_type_t meter_manager_evse_get_model(void) { return meter_evse_type; }
meter_type_t meter_manager_grid_get_model(void) { return meter_grid_type; }
const char *meter_type_to_str(meter_type_t type)
{
@@ -444,6 +524,8 @@ const char *meter_type_to_str(meter_type_t type)
return "TRIF-ZIGBEE";
case METER_TYPE_EA777:
return "EA-777";
case METER_TYPE_DTS024M:
return "DTS-024M";
default:
return "NENHUM";
}
@@ -453,6 +535,7 @@ meter_type_t string_to_meter_type(const char *str)
{
if (!str)
return METER_TYPE_NONE;
if (strcmp(str, "IC ADE") == 0)
return METER_TYPE_ADE7758;
if (strcmp(str, "ORNO-513") == 0)
@@ -471,5 +554,8 @@ meter_type_t string_to_meter_type(const char *str)
return METER_TYPE_TRIF_ZIGBEE;
if (strcmp(str, "EA-777") == 0)
return METER_TYPE_EA777;
if (strcmp(str, "DTS-024M") == 0)
return METER_TYPE_DTS024M;
return METER_TYPE_NONE;
}

View File

@@ -5,4 +5,4 @@ set(srcs
idf_component_register(SRCS "${srcs}"
INCLUDE_DIRS "include"
PRIV_REQUIRES nvs_flash esp_netif esp_wifi mdns esp_event)
PRIV_REQUIRES nvs_flash esp_netif esp_wifi storage_service mdns esp_event)

View File

@@ -11,22 +11,24 @@
#include "freertos/event_groups.h"
#include "esp_log.h"
#include "esp_err.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_netif.h"
#include "esp_mac.h"
#include "nvs.h"
#include "nvs_flash.h"
#include "mdns.h"
#include "network_events.h"
#include "network.h"
// NEW:
#include "storage_service.h"
// -----------------------------------------------------------------------------
// Config
// -----------------------------------------------------------------------------
#define AP_SSID "plx-%02x%02x%02x" // SSID do AP (usa 3 bytes do MAC)
#define MDNS_SSID "plx%02x" // hostname mDNS (usa 2 bytes do MAC)
#define AP_SSID "plx-%02x%02x%02x"
#define MDNS_SSID "plx%02x"
#define NVS_NAMESPACE "wifi"
#define NVS_ENABLED "enabled"
@@ -35,60 +37,106 @@
// Comprimentos com terminador
#define SSID_MAX_LEN 32
#define PASS_MAX_LEN 64 // 63 chars + '\0'
#define SSID_BUF_SZ (SSID_MAX_LEN + 1) // 33
#define PASS_BUF_SZ (PASS_MAX_LEN + 1) // 65
#define PASS_MAX_LEN 64
#define SSID_BUF_SZ (SSID_MAX_LEN + 1)
#define PASS_BUF_SZ (PASS_MAX_LEN + 1)
static const char *TAG = "wifi";
// Storage timeouts
#define STORE_TO pdMS_TO_TICKS(800)
#define STORE_FLUSH_TO pdMS_TO_TICKS(2000)
// -----------------------------------------------------------------------------
// Estado global
// -----------------------------------------------------------------------------
static nvs_handle_t nvs;
static esp_netif_t *sta_netif;
static esp_netif_t *ap_netif;
EventGroupHandle_t wifi_event_group;
// Backoff simples para reconexão STA (agora sem delay no event handler)
// Backoff simples para reconexão STA
static int s_retry_count = 0;
static const int s_retry_max = 7;
// -----------------------------------------------------------------------------
// Helpers
// Helpers storage (robustos)
// -----------------------------------------------------------------------------
// Lê string do NVS com segurança (trunca se necessário)
static esp_err_t nvs_get_str_safe(nvs_handle_t h, const char *key, char *out, size_t out_sz)
static esp_err_t store_flush_best_effort(void)
{
esp_err_t e = storage_flush_sync(STORE_FLUSH_TO);
if (e != ESP_OK)
ESP_LOGW(TAG, "storage_flush_sync failed: %s", esp_err_to_name(e));
return e;
}
static esp_err_t store_set_u8_best_effort(const char *ns, const char *key, uint8_t v)
{
for (int attempt = 0; attempt < 3; ++attempt)
{
esp_err_t e = storage_set_u8_async(ns, key, v);
if (e == ESP_OK)
return ESP_OK;
if (e == ESP_ERR_TIMEOUT)
{
(void)store_flush_best_effort();
vTaskDelay(pdMS_TO_TICKS(10));
continue;
}
return e;
}
return ESP_ERR_TIMEOUT;
}
static esp_err_t store_set_str_best_effort(const char *ns, const char *key, const char *s)
{
for (int attempt = 0; attempt < 3; ++attempt)
{
esp_err_t e = storage_set_str_async(ns, key, s);
if (e == ESP_OK)
return ESP_OK;
if (e == ESP_ERR_TIMEOUT)
{
(void)store_flush_best_effort();
vTaskDelay(pdMS_TO_TICKS(10));
continue;
}
return e;
}
return ESP_ERR_TIMEOUT;
}
// Lê string de forma segura (lê para buffer grande e depois trunca para out)
// Nota: isto ajuda se houver lixo antigo no NVS com strings maiores do que o esperado.
static esp_err_t store_get_str_safe(const char *ns, const char *key, char *out, size_t out_sz)
{
if (!out || out_sz == 0)
return ESP_ERR_INVALID_ARG;
out[0] = '\0';
size_t need = 0;
esp_err_t err = nvs_get_str(h, key, NULL, &need);
if (err == ESP_ERR_NVS_NOT_FOUND)
// buffer grande (sem heap): usa o máximo definido no storage_service
char tmp[STORAGE_MAX_VALUE_BYTES + 1];
memset(tmp, 0, sizeof(tmp));
esp_err_t e = storage_get_str_sync(ns, key, tmp, sizeof(tmp), STORE_TO);
if (e == ESP_ERR_NOT_FOUND)
{
out[0] = '\0';
return ESP_OK;
if (err != ESP_OK)
return err;
if (need == 0)
return ESP_OK; // vazio
if (need > out_sz)
{
// Truncar de forma segura
char *tmp = (char *)malloc(need);
if (!tmp)
return ESP_ERR_NO_MEM;
err = nvs_get_str(h, key, tmp, &need);
if (err == ESP_OK)
{
snprintf(out, out_sz, "%s", tmp);
}
free(tmp);
return err;
if (e != ESP_OK)
{
out[0] = '\0';
return e;
}
return nvs_get_str(h, key, out, &need);
size_t n = strnlen(tmp, out_sz - 1); // no máximo out_sz-1
memcpy(out, tmp, n);
out[n] = '\0';
return ESP_OK;
}
// -----------------------------------------------------------------------------
@@ -96,6 +144,8 @@ static esp_err_t nvs_get_str_safe(nvs_handle_t h, const char *key, char *out, si
// -----------------------------------------------------------------------------
static void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
{
(void)arg;
if (event_base == WIFI_EVENT)
{
switch (event_id)
@@ -128,7 +178,6 @@ static void event_handler(void *arg, esp_event_base_t event_base, int32_t event_
case WIFI_EVENT_STA_CONNECTED:
{
ESP_LOGI(TAG, "STA associated (L2 connected)");
// dispara evento genérico de STA_CONNECTED
esp_event_post(NETWORK_EVENTS,
NETWORK_EVENT_STA_CONNECTED,
NULL,
@@ -144,14 +193,12 @@ static void event_handler(void *arg, esp_event_base_t event_base, int32_t event_
wifi_event_sta_disconnected_t *ev = (wifi_event_sta_disconnected_t *)event_data;
ESP_LOGW(TAG, "STA disconnected, reason=%d", ev ? ev->reason : -1);
// dispara evento genérico de “STA desconectou”
esp_event_post(NETWORK_EVENTS,
NETWORK_EVENT_STA_DISCONNECTED,
ev,
ev ? sizeof(*ev) : 0,
portMAX_DELAY);
// NÃO bloquear o event loop com vTaskDelay
if (s_retry_count < s_retry_max)
{
esp_err_t err = esp_wifi_connect();
@@ -162,7 +209,7 @@ static void event_handler(void *arg, esp_event_base_t event_base, int32_t event_
}
else
{
ESP_LOGW(TAG, "esp_wifi_connect failed (%d)", err);
ESP_LOGW(TAG, "esp_wifi_connect failed (%s)", esp_err_to_name(err));
}
}
else
@@ -185,22 +232,12 @@ static void event_handler(void *arg, esp_event_base_t event_base, int32_t event_
xEventGroupSetBits(wifi_event_group, WIFI_STA_CONNECTED_BIT);
s_retry_count = 0;
// Dispara evento “STA GOT IP”
esp_event_post(NETWORK_EVENTS,
NETWORK_EVENT_STA_GOT_IP,
&event->ip_info,
sizeof(event->ip_info),
portMAX_DELAY);
}
else if (event_id == IP_EVENT_GOT_IP6)
{
ip_event_got_ip6_t *event = (ip_event_got_ip6_t *)event_data;
ESP_LOGI(TAG, "WiFi STA got ip6: " IPV6STR, IPV62STR(event->ip6_info.ip));
xEventGroupClearBits(wifi_event_group, WIFI_STA_DISCONNECTED_BIT);
xEventGroupSetBits(wifi_event_group, WIFI_STA_CONNECTED_BIT);
s_retry_count = 0;
// se quiseres, podes também propagar um NETWORK_EVENT_STA_GOT_IP aqui
}
else if (event_id == IP_EVENT_STA_LOST_IP)
{
ESP_LOGW(TAG, "STA lost IP");
@@ -227,28 +264,21 @@ static void sta_set_config(void)
wifi_config.sta.pmf_cfg.capable = true;
wifi_config.sta.pmf_cfg.required = false;
// buffers "seguros" vindos da NVS (estes sim têm terminador)
char ssid_buf[SSID_BUF_SZ] = {0}; // 33 (32 + '\0')
char pass_buf[PASS_BUF_SZ] = {0}; // 65 (64 + '\0')
char ssid_buf[SSID_BUF_SZ] = {0};
char pass_buf[PASS_BUF_SZ] = {0};
wifi_get_ssid(ssid_buf);
wifi_get_password(pass_buf);
// Copiar **sem** terminador para os campos do esp_wifi (32/64 bytes)
// SSID: max 32
size_t ssid_len = strnlen(ssid_buf, SSID_MAX_LEN); // até 32
size_t ssid_len = strnlen(ssid_buf, SSID_MAX_LEN);
memcpy(wifi_config.sta.ssid, ssid_buf, ssid_len);
// Password WPA/WPA2: 8..63 chars (campo tem 64 bytes)
// (se usares rede aberta, pass_len pode ser 0)
size_t pass_len = strnlen(pass_buf, 63); // até 63
size_t pass_len = strnlen(pass_buf, 63);
memcpy(wifi_config.sta.password, pass_buf, pass_len);
if (pass_len <= 63)
{
wifi_config.sta.password[pass_len] = '\0';
}
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config)); // v5.x
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
}
static void ap_set_config(void)
@@ -257,14 +287,13 @@ static void ap_set_config(void)
wifi_config_t wifi_ap_config = {0};
wifi_ap_config.ap.max_connection = 1;
wifi_ap_config.ap.authmode = WIFI_AUTH_OPEN; // para portal cativo, por exemplo
wifi_ap_config.ap.authmode = WIFI_AUTH_OPEN;
uint8_t mac[6];
esp_wifi_get_mac(WIFI_IF_AP, mac);
snprintf((char *)wifi_ap_config.ap.ssid, sizeof(wifi_ap_config.ap.ssid),
AP_SSID, mac[3], mac[4], mac[5]); // "plx-XXXXXX"
AP_SSID, mac[3], mac[4], mac[5]);
// Só AP (não mexer na config STA aqui para não a limpar)
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_ap_config));
}
@@ -283,7 +312,7 @@ static void sta_try_start(void)
esp_err_t e = esp_wifi_start();
if (e != ESP_OK && e != ESP_ERR_WIFI_CONN)
{
ESP_LOGW(TAG, "esp_wifi_start returned %d", e);
ESP_LOGW(TAG, "esp_wifi_start returned %s", esp_err_to_name(e));
}
xEventGroupSetBits(wifi_event_group, WIFI_STA_MODE_BIT);
}
@@ -296,8 +325,10 @@ void wifi_ini(void)
{
ESP_LOGI(TAG, "Wifi init");
// Abre NVS (assume que nvs_flash_init() já foi chamado no boot geral)
ESP_ERROR_CHECK(nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs));
// garante storage pronto (não assume NVS handle aberto aqui)
esp_err_t se = storage_service_init();
if (se != ESP_OK)
ESP_LOGW(TAG, "storage_service_init failed: %s", esp_err_to_name(se));
wifi_event_group = xEventGroupCreate();
@@ -310,14 +341,11 @@ void wifi_ini(void)
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL));
// mDNS: usa dois bytes do MAC para identificar
uint8_t mac[6];
esp_wifi_get_mac(WIFI_IF_STA, mac);
char chargeid[16];
// ex.: "plx00"
snprintf(chargeid, sizeof(chargeid), MDNS_SSID, 0);
// Hostname das interfaces alinhado com o mDNS
ESP_ERROR_CHECK(esp_netif_set_hostname(sta_netif, chargeid));
ESP_ERROR_CHECK(esp_netif_set_hostname(ap_netif, chargeid));
@@ -335,10 +363,9 @@ esp_err_t wifi_set_config(bool enabled, const char *ssid, const char *password)
{
ESP_LOGI(TAG, "wifi_set_config(enabled=%d)", enabled);
// Validação (quando habilitar STA)
// Validação
if (enabled)
{
// SSID 1..32
if (ssid && (strlen(ssid) == 0 || strlen(ssid) > SSID_MAX_LEN))
{
ESP_LOGE(TAG, "SSID out of range");
@@ -346,15 +373,15 @@ esp_err_t wifi_set_config(bool enabled, const char *ssid, const char *password)
}
if (!ssid)
{
size_t len = 0;
esp_err_t e = nvs_get_str(nvs, NVS_SSID, NULL, &len);
if (e != ESP_OK || len <= 1)
char cur_ssid[SSID_BUF_SZ] = {0};
wifi_get_ssid(cur_ssid);
if (strlen(cur_ssid) == 0)
{
ESP_LOGE(TAG, "Required SSID");
return ESP_ERR_INVALID_ARG;
}
}
// Password: 8..63 (se não for vazia). Aceita "" para rede open (caso uses).
if (password)
{
size_t lp = strlen(password);
@@ -366,13 +393,27 @@ esp_err_t wifi_set_config(bool enabled, const char *ssid, const char *password)
}
}
// Persiste no NVS
ESP_ERROR_CHECK(nvs_set_u8(nvs, NVS_ENABLED, enabled));
// Persiste via storage_service
esp_err_t err = store_set_u8_best_effort(NVS_NAMESPACE, NVS_ENABLED, enabled ? 1 : 0);
if (err != ESP_OK)
return err;
if (ssid)
ESP_ERROR_CHECK(nvs_set_str(nvs, NVS_SSID, ssid));
{
err = store_set_str_best_effort(NVS_NAMESPACE, NVS_SSID, ssid);
if (err != ESP_OK)
return err;
}
if (password)
ESP_ERROR_CHECK(nvs_set_str(nvs, NVS_PASSWORD, password));
ESP_ERROR_CHECK(nvs_commit(nvs));
{
err = store_set_str_best_effort(NVS_NAMESPACE, NVS_PASSWORD, password);
if (err != ESP_OK)
return err;
}
// Força persistência (para sobreviver a reboot imediato)
(void)store_flush_best_effort();
// Reinicia modo
ESP_LOGI(TAG, "Stopping AP/STA");
@@ -380,7 +421,7 @@ esp_err_t wifi_set_config(bool enabled, const char *ssid, const char *password)
esp_err_t e = esp_wifi_stop();
if (e != ESP_OK && e != ESP_ERR_WIFI_NOT_INIT && e != ESP_ERR_WIFI_STOP_STATE)
{
ESP_LOGW(TAG, "esp_wifi_stop returned %d", e);
ESP_LOGW(TAG, "esp_wifi_stop returned %s", esp_err_to_name(e));
}
sta_try_start();
@@ -402,21 +443,21 @@ uint16_t wifi_scan(wifi_scan_ap_t *scan_aps)
esp_err_t err = esp_wifi_scan_start(NULL, true);
if (err != ESP_OK)
{
ESP_LOGW(TAG, "esp_wifi_scan_start failed (%d)", err);
ESP_LOGW(TAG, "esp_wifi_scan_start failed (%s)", esp_err_to_name(err));
return 0;
}
err = esp_wifi_scan_get_ap_records(&number, ap_info);
if (err != ESP_OK)
{
ESP_LOGW(TAG, "esp_wifi_scan_get_ap_records failed (%d)", err);
ESP_LOGW(TAG, "esp_wifi_scan_get_ap_records failed (%s)", esp_err_to_name(err));
return 0;
}
err = esp_wifi_scan_get_ap_num(&ap_count);
if (err != ESP_OK)
{
ESP_LOGW(TAG, "esp_wifi_scan_get_ap_num failed (%d)", err);
ESP_LOGW(TAG, "esp_wifi_scan_get_ap_num failed (%s)", esp_err_to_name(err));
return 0;
}
@@ -424,7 +465,6 @@ uint16_t wifi_scan(wifi_scan_ap_t *scan_aps)
for (int i = 0; (i < WIFI_SCAN_SCAN_LIST_SIZE) && (i < ap_count); i++)
{
// garante que scan_aps[i].ssid tenha pelo menos 33 bytes
snprintf(scan_aps[i].ssid, sizeof(scan_aps[i].ssid), "%s", (const char *)ap_info[i].ssid);
scan_aps[i].rssi = ap_info[i].rssi;
scan_aps[i].auth = ap_info[i].authmode != WIFI_AUTH_OPEN;
@@ -436,29 +476,29 @@ uint16_t wifi_scan(wifi_scan_ap_t *scan_aps)
bool wifi_get_enabled(void)
{
uint8_t value = 0;
esp_err_t e = nvs_get_u8(nvs, NVS_ENABLED, &value);
if (e == ESP_ERR_NVS_NOT_FOUND)
esp_err_t e = storage_get_u8_sync(NVS_NAMESPACE, NVS_ENABLED, &value, STORE_TO);
if (e == ESP_ERR_NOT_FOUND)
return false;
if (e != ESP_OK)
{
ESP_LOGW(TAG, "nvs_get_u8(NVS_ENABLED) failed (%d), assuming disabled", e);
ESP_LOGW(TAG, "storage_get_u8_sync(enabled) failed (%s), assuming disabled", esp_err_to_name(e));
return false;
}
return value;
return (value != 0);
}
void wifi_get_ssid(char *value)
{
if (!value)
return;
nvs_get_str_safe(nvs, NVS_SSID, value, SSID_BUF_SZ); // 33
(void)store_get_str_safe(NVS_NAMESPACE, NVS_SSID, value, SSID_BUF_SZ);
}
void wifi_get_password(char *value)
{
if (!value)
return;
nvs_get_str_safe(nvs, NVS_PASSWORD, value, PASS_BUF_SZ); // 65
(void)store_get_str_safe(NVS_NAMESPACE, NVS_PASSWORD, value, PASS_BUF_SZ);
}
void wifi_ap_start(void)
@@ -474,7 +514,7 @@ void wifi_ap_start(void)
esp_err_t e = esp_wifi_start();
if (e != ESP_OK && e != ESP_ERR_WIFI_CONN)
{
ESP_LOGW(TAG, "esp_wifi_start (AP) returned %d", e);
ESP_LOGW(TAG, "esp_wifi_start (AP) returned %s", esp_err_to_name(e));
}
xEventGroupSetBits(wifi_event_group, WIFI_AP_MODE_BIT);

View File

@@ -5,5 +5,4 @@ set(srcs
idf_component_register(SRCS "${srcs}"
INCLUDE_DIRS "include"
PRIV_REQUIRES nvs_flash
REQUIRES esp_event config esp_wifi evse mongoose MicroOcpp MicroOcppMongoose)
REQUIRES esp_event storage_service config esp_wifi evse mongoose MicroOcpp MicroOcppMongoose)

View File

@@ -1,32 +1,37 @@
// components/ocpp/src/ocpp.c
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
#include <inttypes.h>
#include <math.h>
#include "esp_log.h"
#include "esp_err.h"
#include "esp_timer.h"
#include "esp_event.h"
#include "esp_wifi.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "ocpp.h"
#include "ocpp_events.h"
#include "esp_wifi.h"
#include "nvs.h"
#include "nvs_flash.h"
#include "evse_error.h"
#include "auth_events.h"
#include "evse_events.h"
#include "evse_state.h"
#include "meter_events.h"
#include "esp_timer.h"
#include <math.h>
/* MicroOcpp includes */
#include <mongoose.h>
#include <MicroOcpp_c.h> // C-facade of MicroOcpp
#include <MicroOcppMongooseClient_c.h> // WebSocket integration for ESP-IDF
// NEW storage layer
#include "storage_service.h"
#define NVS_NAMESPACE "ocpp"
#define NVS_OCPP_ENABLED "enabled"
#define NVS_OCPP_SERVER "ocpp_server"
@@ -59,19 +64,18 @@ static esp_event_handler_instance_t s_evse_available_inst = NULL;
// --- cache de medições vindas de METER_EVENT_DATA_READY ---
typedef struct
{
// dados por fase
float vrms[3];
float irms[3];
int32_t watt[3]; // ativo por fase (W)
float frequency;
float power_factor;
// acumulados
float total_energy_Wh;
// derivados práticos
int32_t sum_watt; // soma das 3 fases
float avg_voltage; // média das 3 fases
float sum_current; // soma das 3 fases
// flag de validade
int32_t sum_watt;
float avg_voltage;
float sum_current;
bool have_data;
} ocpp_meter_cache_t;
@@ -82,18 +86,101 @@ static esp_event_handler_instance_t s_meter_inst = NULL;
// valor de oferta (A por conector)
static float s_current_offered_A = 16.0f;
// novo input apropriado
static float getCurrentOffered(void)
{
return s_current_offered_A;
}
/* =========================
* Task / Main Loop
* ========================= *
*/
// -----------------------------------------------------------------------------
// Storage helpers (robustos)
// -----------------------------------------------------------------------------
#define STORE_TO pdMS_TO_TICKS(800)
#define STORE_FLUSH_TO pdMS_TO_TICKS(2000)
static void storage_init_best_effort(void)
{
esp_err_t e = storage_service_init();
if (e != ESP_OK)
ESP_LOGW(TAG, "storage_service_init failed: %s", esp_err_to_name(e));
}
static esp_err_t store_flush_best_effort(void)
{
esp_err_t e = storage_flush_sync(STORE_FLUSH_TO);
if (e != ESP_OK)
ESP_LOGW(TAG, "storage_flush_sync failed: %s", esp_err_to_name(e));
return e;
}
static esp_err_t store_set_u8_best_effort(const char *ns, const char *key, uint8_t v)
{
for (int attempt = 0; attempt < 3; ++attempt)
{
esp_err_t e = storage_set_u8_async(ns, key, v);
if (e == ESP_OK)
return ESP_OK;
if (e == ESP_ERR_TIMEOUT)
{
(void)store_flush_best_effort();
vTaskDelay(pdMS_TO_TICKS(10));
continue;
}
return e;
}
return ESP_ERR_TIMEOUT;
}
static esp_err_t store_set_str_best_effort(const char *ns, const char *key, const char *s)
{
for (int attempt = 0; attempt < 3; ++attempt)
{
esp_err_t e = storage_set_str_async(ns, key, s ? s : "");
if (e == ESP_OK)
return ESP_OK;
if (e == ESP_ERR_TIMEOUT)
{
(void)store_flush_best_effort();
vTaskDelay(portMAX_DELAY);
continue;
}
return e;
}
return ESP_ERR_TIMEOUT;
}
// Lê string de forma segura (buffer grande -> truncagem segura para out)
static esp_err_t store_get_str_safe(const char *ns, const char *key, char *out, size_t out_sz)
{
if (!out || out_sz == 0)
return ESP_ERR_INVALID_ARG;
out[0] = '\0';
char tmp[STORAGE_MAX_VALUE_BYTES + 1];
memset(tmp, 0, sizeof(tmp));
esp_err_t e = storage_get_str_sync(ns, key, tmp, sizeof(tmp), STORE_TO);
if (e == ESP_ERR_NOT_FOUND)
return ESP_OK;
if (e != ESP_OK)
return e;
size_t n = strnlen(tmp, out_sz - 1);
memcpy(out, tmp, n);
out[n] = '\0';
return ESP_OK;
}
// -----------------------------------------------------------------------------
// Task / Main Loop
// -----------------------------------------------------------------------------
static void ocpp_task_func(void *param)
{
(void)param;
while (true)
{
if (enabled)
@@ -106,7 +193,6 @@ static void ocpp_task_func(void *param)
{
s_evse_enabled = operative;
// >>> enviar OCPP_EVENT (remoto → local)
ocpp_operative_event_t ev = {
.operative = operative,
.timestamp_us = esp_timer_get_time()};
@@ -115,8 +201,7 @@ static void ocpp_task_func(void *param)
&ev, sizeof(ev),
portMAX_DELAY);
ESP_LOGI(TAG, "[OCPP] ChangeAvailability remoto → operative=%d",
(int)operative);
ESP_LOGI(TAG, "[OCPP] ChangeAvailability remoto → operative=%d", (int)operative);
}
}
else
@@ -126,33 +211,21 @@ static void ocpp_task_func(void *param)
}
}
/* =========================
* NVS GETs
* ========================= */
// -----------------------------------------------------------------------------
// Storage GETs
// -----------------------------------------------------------------------------
bool ocpp_get_enabled(void)
{
nvs_handle_t h;
esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &h);
if (err == ESP_ERR_NVS_NOT_FOUND)
{
// namespace ainda não existe -> default: disabled
return false;
}
if (err != ESP_OK)
{
ESP_LOGW(TAG, "nvs_open(%s) RO failed: %s", NVS_NAMESPACE, esp_err_to_name(err));
return false;
}
storage_init_best_effort();
uint8_t value = 0;
err = nvs_get_u8(h, NVS_OCPP_ENABLED, &value);
nvs_close(h);
esp_err_t err = storage_get_u8_sync(NVS_NAMESPACE, NVS_OCPP_ENABLED, &value, STORE_TO);
if (err == ESP_ERR_NVS_NOT_FOUND)
return false; // default
if (err == ESP_ERR_NOT_FOUND)
return false;
if (err != ESP_OK)
{
ESP_LOGW(TAG, "nvs_get_u8(enabled) failed: %s", esp_err_to_name(err));
ESP_LOGW(TAG, "storage_get_u8_sync(enabled) failed: %s", esp_err_to_name(err));
return false;
}
return value != 0;
@@ -164,31 +237,12 @@ void ocpp_get_server(char *value /* out, size>=64 */)
return;
value[0] = '\0';
nvs_handle_t h;
esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &h);
if (err == ESP_ERR_NVS_NOT_FOUND)
{
// namespace ainda não existe -> default: ""
return;
}
if (err != ESP_OK)
{
ESP_LOGW(TAG, "nvs_open(%s) RO failed: %s", NVS_NAMESPACE, esp_err_to_name(err));
return;
}
storage_init_best_effort();
size_t len = 64;
err = nvs_get_str(h, NVS_OCPP_SERVER, value, &len);
nvs_close(h);
if (err == ESP_ERR_NVS_NOT_FOUND)
esp_err_t e = store_get_str_safe(NVS_NAMESPACE, NVS_OCPP_SERVER, value, 64);
if (e != ESP_OK)
{
value[0] = '\0';
return;
}
if (err != ESP_OK)
{
ESP_LOGW(TAG, "nvs_get_str(server) failed: %s", esp_err_to_name(err));
ESP_LOGW(TAG, "store_get_str_safe(server) failed: %s", esp_err_to_name(e));
value[0] = '\0';
}
}
@@ -199,92 +253,81 @@ void ocpp_get_charge_id(char *value /* out, size>=64 */)
return;
value[0] = '\0';
nvs_handle_t h;
esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &h);
if (err == ESP_ERR_NVS_NOT_FOUND)
{
// namespace ainda não existe -> default: ""
return;
}
if (err != ESP_OK)
{
ESP_LOGW(TAG, "nvs_open(%s) RO failed: %s", NVS_NAMESPACE, esp_err_to_name(err));
return;
}
storage_init_best_effort();
size_t len = 64;
err = nvs_get_str(h, NVS_OCPP_CHARGE_ID, value, &len);
nvs_close(h);
if (err == ESP_ERR_NVS_NOT_FOUND)
esp_err_t e = store_get_str_safe(NVS_NAMESPACE, NVS_OCPP_CHARGE_ID, value, 64);
if (e != ESP_OK)
{
value[0] = '\0';
return;
}
if (err != ESP_OK)
{
ESP_LOGW(TAG, "nvs_get_str(charge_id) failed: %s", esp_err_to_name(err));
ESP_LOGW(TAG, "store_get_str_safe(charge_id) failed: %s", esp_err_to_name(e));
value[0] = '\0';
}
}
/* =========================
* NVS SETs
* ========================= */
// --- SETTERS: RW (cria o namespace na 1ª escrita), commit e fecha ---
// -----------------------------------------------------------------------------
// Storage SETs
// -----------------------------------------------------------------------------
void ocpp_set_enabled(bool value)
{
nvs_handle_t h;
esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &h);
if (err != ESP_OK)
storage_init_best_effort();
ESP_LOGI(TAG, "set enabled %d", value);
esp_err_t e = store_set_u8_best_effort(NVS_NAMESPACE, NVS_OCPP_ENABLED, value ? 1 : 0);
if (e != ESP_OK)
{
ESP_LOGE(TAG, "nvs_open(%s) RW failed: %s", NVS_NAMESPACE, esp_err_to_name(err));
ESP_LOGE(TAG, "store_set_u8_best_effort(enabled) failed: %s", esp_err_to_name(e));
return;
}
ESP_LOGI(TAG, "set enabled %d", value);
ESP_ERROR_CHECK(nvs_set_u8(h, NVS_OCPP_ENABLED, value ? 1 : 0));
ESP_ERROR_CHECK(nvs_commit(h));
nvs_close(h);
(void)store_flush_best_effort();
enabled = value;
}
void ocpp_set_server(char *value)
{
nvs_handle_t h;
esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &h);
if (err != ESP_OK)
storage_init_best_effort();
ESP_LOGI(TAG, "set server %s", value ? value : "(null)");
esp_err_t e = store_set_str_best_effort(NVS_NAMESPACE, NVS_OCPP_SERVER, value ? value : "");
if (e != ESP_OK)
{
ESP_LOGE(TAG, "nvs_open(%s) RW failed: %s", NVS_NAMESPACE, esp_err_to_name(err));
ESP_LOGE(TAG, "store_set_str_best_effort(server) failed: %s", esp_err_to_name(e));
return;
}
ESP_LOGI(TAG, "set server %s", value ? value : "(null)");
ESP_ERROR_CHECK(nvs_set_str(h, NVS_OCPP_SERVER, value ? value : ""));
ESP_ERROR_CHECK(nvs_commit(h));
nvs_close(h);
(void)store_flush_best_effort();
}
void ocpp_set_charge_id(char *value)
{
nvs_handle_t h;
esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &h);
if (err != ESP_OK)
storage_init_best_effort();
ESP_LOGI(TAG, "set charge_id %s", value ? value : "(null)");
esp_err_t e = store_set_str_best_effort(NVS_NAMESPACE, NVS_OCPP_CHARGE_ID, value ? value : "");
if (e != ESP_OK)
{
ESP_LOGE(TAG, "nvs_open(%s) RW failed: %s", NVS_NAMESPACE, esp_err_to_name(err));
ESP_LOGE(TAG, "store_set_str_best_effort(charge_id) failed: %s", esp_err_to_name(e));
return;
}
ESP_LOGI(TAG, "set charge_id %s", value ? value : "(null)");
ESP_ERROR_CHECK(nvs_set_str(h, NVS_OCPP_CHARGE_ID, value ? value : ""));
ESP_ERROR_CHECK(nvs_commit(h));
nvs_close(h);
(void)store_flush_best_effort();
}
// -----------------------------------------------------------------------------
// Event handlers (AUTH / EVSE / METER)
// -----------------------------------------------------------------------------
static void ocpp_on_auth_verify(void *arg, esp_event_base_t base, int32_t id, void *event_data)
{
(void)arg;
(void)base;
(void)id;
const auth_tag_verify_event_t *rq = (const auth_tag_verify_event_t *)event_data;
if (!rq)
return;
// Sanitizar/copiar a idTag
char idtag[AUTH_TAG_MAX_LEN];
if (rq->tag[0] == '\0')
{
@@ -299,16 +342,13 @@ static void ocpp_on_auth_verify(void *arg, esp_event_base_t base, int32_t id, vo
ESP_LOGI(TAG, "AUTH_EVENT_TAG_VERIFY: tag=%s req_id=%u", idtag, (unsigned)rq->req_id);
// Se não está pronto, apenas regista e sai (podes adaptar conforme política)
if (!enabled || g_ocpp_conn == NULL)
{
ESP_LOGW(TAG, "OCPP not ready (enabled=%d, conn=%p) ignoring verify", enabled, (void *)g_ocpp_conn);
ESP_LOGW(TAG, "OCPP not ready (enabled=%d, conn=%p) ignoring verify",
enabled, (void *)g_ocpp_conn);
return;
}
// Regra pedida:
// - se já existe transação/charge em andamento -> terminar
// - senão -> iniciar com a IDTAG recebida
if (ocpp_isTransactionActive())
{
ESP_LOGI(TAG, "Transaction active -> ocpp_end_transaction(\"%s\")", idtag);
@@ -323,6 +363,7 @@ static void ocpp_on_auth_verify(void *arg, esp_event_base_t base, int32_t id, vo
static void evse_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data)
{
(void)arg;
if (base != EVSE_EVENTS || id != EVSE_EVENT_STATE_CHANGED || data == NULL)
return;
@@ -343,12 +384,11 @@ static void evse_event_handler(void *arg, esp_event_base_t base, int32_t id, voi
case EVSE_STATE_EVENT_CHARGING:
s_ev_plugged = true;
s_ev_ready = true; // EV está a pedir/receber energia
s_ev_ready = true;
break;
case EVSE_STATE_EVENT_FAULT:
default:
// em falha, considera não pronto (mantém plugged se quiseres)
s_ev_ready = false;
break;
}
@@ -356,6 +396,7 @@ static void evse_event_handler(void *arg, esp_event_base_t base, int32_t id, voi
static void evse_enable_available_handler(void *arg, esp_event_base_t base, int32_t id, void *data)
{
(void)arg;
if (base != EVSE_EVENTS || data == NULL)
return;
@@ -363,7 +404,8 @@ static void evse_enable_available_handler(void *arg, esp_event_base_t base, int3
{
const evse_enable_event_data_t *e = (const evse_enable_event_data_t *)data;
s_evse_enabled = e->enabled;
ESP_LOGI(TAG, "[EVSE] ENABLE_UPDATED: enabled=%d (ts=%lld)", (int)e->enabled, (long long)e->timestamp_us);
ESP_LOGI(TAG, "[EVSE] ENABLE_UPDATED: enabled=%d (ts=%lld)",
(int)e->enabled, (long long)e->timestamp_us);
return;
}
@@ -371,23 +413,24 @@ static void evse_enable_available_handler(void *arg, esp_event_base_t base, int3
{
const evse_available_event_data_t *e = (const evse_available_event_data_t *)data;
s_evse_available = e->available;
ESP_LOGI(TAG, "[EVSE] AVAILABLE_UPDATED: available=%d (ts=%lld)", (int)e->available, (long long)e->timestamp_us);
ESP_LOGI(TAG, "[EVSE] AVAILABLE_UPDATED: available=%d (ts=%lld)",
(int)e->available, (long long)e->timestamp_us);
return;
}
}
static void on_meter_event(void *arg, esp_event_base_t base, int32_t id, void *data)
{
(void)arg;
if (base != METER_EVENT || id != METER_EVENT_DATA_READY || !data)
return;
const meter_event_data_t *evt = (const meter_event_data_t *)data;
// Só queremos o medidor do EVSE (não o GRID)
if (!evt->source || strcmp(evt->source, "EVSE") != 0)
return;
// Derivados simples
int32_t sum_w = (int32_t)evt->watt[0] + (int32_t)evt->watt[1] + (int32_t)evt->watt[2];
float avg_v = (evt->vrms[0] + evt->vrms[1] + evt->vrms[2]) / 3.0f;
float sum_i = evt->irms[0] + evt->irms[1] + evt->irms[2];
@@ -408,22 +451,21 @@ static void on_meter_event(void *arg, esp_event_base_t base, int32_t id, void *d
portEXIT_CRITICAL(&s_meter_mux);
}
/* =========================
* MicroOCPP Inputs/CBs
* ========================= */
// -----------------------------------------------------------------------------
// MicroOCPP Inputs/CBs
// -----------------------------------------------------------------------------
bool setConnectorPluggedInput(void)
{
return s_ev_plugged; // EV fisicamente ligado
return s_ev_plugged;
}
bool setEvReadyInput(void)
{
return s_ev_ready; // EV pede / pronto a carregar
return s_ev_ready;
}
bool setEvseReadyInput(void)
{
// EVSE autorizado / operacional
return s_evse_enabled && s_evse_available;
}
@@ -439,13 +481,10 @@ float setPowerMeterInput(void)
portEXIT_CRITICAL(&s_meter_mux);
if (!have)
{
ESP_LOGW(TAG, "[METER] PowerMeterInput: no data (return 0)");
}
else
{
ESP_LOGD(TAG, "[METER] PowerMeterInput: %" PRId32 " W", w);
}
return (float)w;
}
@@ -461,14 +500,11 @@ float setEnergyMeterInput(void)
portEXIT_CRITICAL(&s_meter_mux);
if (!have)
{
ESP_LOGW(TAG, "[METER] EnergyMeterInput: no data (return 0)");
}
else
{
ESP_LOGD(TAG, "[METER] EnergyMeterInput: (%.1f Wh)", wh);
}
return wh; // agora devolve Wh
return wh;
}
int setEnergyInput(void)
@@ -491,13 +527,10 @@ float setCurrentInput(void)
portEXIT_CRITICAL(&s_meter_mux);
if (!have)
{
ESP_LOGW(TAG, "[METER] CurrentInput: no data (return 0)");
}
else
{
ESP_LOGD(TAG, "[METER] CurrentInput: %.2f A (total)", a);
}
return a;
}
@@ -513,19 +546,16 @@ float setVoltageInput(void)
portEXIT_CRITICAL(&s_meter_mux);
if (!have)
{
ESP_LOGW(TAG, "[METER] VoltageInput: no data (return 0)");
}
else
{
ESP_LOGD(TAG, "[METER] VoltageInput: %.1f V (avg)", v);
}
return v;
}
float setPowerInput(void)
{
float w = setPowerMeterInput(); // alias
float w = setPowerMeterInput();
ESP_LOGD(TAG, "[METER] PowerInput: %.1f W", w);
return w;
}
@@ -598,111 +628,63 @@ bool setOnResetNotify(bool value)
void notificationOutput(OCPP_Transaction *transaction, enum OCPP_TxNotification txNotification)
{
(void)transaction;
ESP_LOGI(TAG, "TxNotification: %d", txNotification);
switch (txNotification)
{
case Authorized:
ESP_LOGI(TAG, "Authorized");
// TODO: send event ocpp Authorized
// evse_authorize();
// Opcional: enviar idTag no payload (se tiveres como obter do transaction)
// ocpp_idtag_event_t ev = {0};
// strlcpy(ev.idTag, ocpp_tx_get_idTag(transaction), sizeof(ev.idTag));
// esp_event_post(OCPP_EVENTS, OCPP_EVENT_AUTHORIZED, &ev, sizeof(ev), portMAX_DELAY);
esp_event_post(OCPP_EVENTS, OCPP_EVENT_AUTHORIZED, NULL, 0, portMAX_DELAY);
break;
case AuthorizationRejected:
ESP_LOGI(TAG, "AuthorizationRejected");
// TODO: send event ocpp AuthorizationRejected
// ocpp_idtag_event_t ev = {0};
// strlcpy(ev.idTag, ocpp_tx_get_idTag(transaction), sizeof(ev.idTag));
// esp_event_post(OCPP_EVENTS, OCPP_EVENT_AUTH_REJECTED, &ev, sizeof(ev), portMAX_DELAY);
esp_event_post(OCPP_EVENTS, OCPP_EVENT_AUTH_REJECTED, NULL, 0, portMAX_DELAY);
break;
case AuthorizationTimeout:
ESP_LOGI(TAG, "AuthorizationTimeout");
// TODO: send event ocpp AuthorizationTimeout
esp_event_post(OCPP_EVENTS, OCPP_EVENT_AUTH_TIMEOUT, NULL, 0, portMAX_DELAY);
break;
case ReservationConflict:
ESP_LOGI(TAG, "ReservationConflict");
// TODO: send event ocpp ReservationConflict
// (Se quiseres, cria um ID específico no enum e publica aqui)
break;
case ConnectionTimeout:
ESP_LOGI(TAG, "ConnectionTimeout");
// TODO: send event ocpp ConnectionTimeout
// (Se quiseres, cria um ID específico no enum e publica aqui)
break;
case DeAuthorized:
ESP_LOGI(TAG, "DeAuthorized");
// TODO: send event ocpp DeAuthorized
// TODO: adapt to the new interface
// evse_set_authorized(false);
// evse_set_limit_reached(2);
// Poderias mapear para AUTH_REJECTED ou STOP_TX por política da aplicação:
// esp_event_post(OCPP_EVENTS, OCPP_EVENT_AUTH_REJECTED, NULL, 0, portMAX_DELAY);
break;
case RemoteStart:
ESP_LOGI(TAG, "RemoteStart");
// TODO: send event ocpp RemoteStart
// ocpp_idtag_event_t ev = {0};
// strlcpy(ev.idTag, ocpp_tx_get_idTag(transaction), sizeof(ev.idTag));
// esp_event_post(OCPP_EVENTS, OCPP_EVENT_REMOTE_START, &ev, sizeof(ev), portMAX_DELAY);
esp_event_post(OCPP_EVENTS, OCPP_EVENT_REMOTE_START, NULL, 0, portMAX_DELAY);
break;
case RemoteStop:
ESP_LOGI(TAG, "RemoteStop");
// TODO: send event ocpp RemoteStop
esp_event_post(OCPP_EVENTS, OCPP_EVENT_REMOTE_STOP, NULL, 0, portMAX_DELAY);
break;
case StartTx:
ESP_LOGI(TAG, "StartTx");
// TODO: send event ocpp StartTx
// ocpp_tx_event_t tx = { .tx_id = ocpp_tx_get_id(transaction) };
// esp_event_post(OCPP_EVENTS, OCPP_EVENT_START_TX, &tx, sizeof(tx), portMAX_DELAY);
esp_event_post(OCPP_EVENTS, OCPP_EVENT_START_TX, NULL, 0, portMAX_DELAY);
break;
case StopTx:
ESP_LOGI(TAG, "StopTx");
// TODO: send event ocpp StopTx
// TODO: adapt to the new interface
// evse_set_authorized(false);
// evse_set_limit_reached(2);
// ocpp_reason_event_t rs = {0};
// strlcpy(rs.reason, "Local", sizeof(rs.reason));
// esp_event_post(OCPP_EVENTS, OCPP_EVENT_STOP_TX, &rs, sizeof(rs), portMAX_DELAY);
esp_event_post(OCPP_EVENTS, OCPP_EVENT_STOP_TX, NULL, 0, portMAX_DELAY);
break;
}
}
// Estado de conexão simples do OCPP
bool ocpp_is_connected(void)
{
// Se quiser algo mais preciso (WS aberto), substitui por uma chamada da MicroOcpp,
// mas como fallback isto já evita o undefined symbol.
return g_ocpp_conn != NULL;
}
@@ -728,12 +710,12 @@ const char *addErrorCodeInput(void)
else if (error & EVSE_ERR_TEMPERATURE_FAULT_BIT)
ptr = "OtherError";
return ptr; // NULL => sem erro
return ptr;
}
/* =========================
* Start / Stop OCPP
* ========================= */
// -----------------------------------------------------------------------------
// Start / Stop OCPP
// -----------------------------------------------------------------------------
void ocpp_start(void)
{
ESP_LOGI(TAG, "Starting OCPP");
@@ -744,6 +726,8 @@ void ocpp_start(void)
return;
}
storage_init_best_effort();
enabled = ocpp_get_enabled();
if (!enabled)
{
@@ -767,15 +751,14 @@ void ocpp_start(void)
return;
}
/* Inicializar Mongoose + MicroOcpp */
mg_mgr_init(&mgr);
mg_log_set(MG_LL_ERROR);
struct OCPP_FilesystemOpt fsopt = {.use = true, .mount = true, .formatFsOnFail = true};
g_ocpp_conn = ocpp_makeConnection(&mgr,
serverstr, /* ex: ws://host:port/OCPP16/... */
charge_id, /* ChargeBoxId / identity */
serverstr,
charge_id,
"",
"",
fsopt);
@@ -786,21 +769,24 @@ void ocpp_start(void)
return;
}
//chargePointModel: "EPower M1"
//chargePointVendor: "Plixin"
//firmwareVersion: "FW-PLXV1.0"
//chargePointSerialNumber: "SN001"
ocpp_initialize(g_ocpp_conn, "EPower M1", "Plixin", fsopt, false);
/* Inputs/outputs e callbacks */
ocpp_setEvReadyInput(&setEvReadyInput);
ocpp_setEvseReadyInput(&setEvseReadyInput);
ocpp_setConnectorPluggedInput(&setConnectorPluggedInput);
ocpp_setOnResetExecute(&OnResetExecute);
ocpp_setTxNotificationOutput(&notificationOutput);
// ocpp_setStartTxReadyInput(&setStartTxReadyInput);
ocpp_setStopTxReadyInput(&setStopTxReadyInput);
ocpp_setOnResetNotify(&setOnResetNotify);
ocpp_setEnergyMeterInput(&setEnergyInput); // inteiro Wh
ocpp_setEnergyMeterInput(&setEnergyInput);
/* Metering */
ocpp_addMeterValueInputFloat(&setCurrentInput, "Current.Import", "A", NULL, NULL);
ocpp_addMeterValueInputFloat(&getCurrentOffered, "Current.Offered", "A", NULL, NULL);
ocpp_addMeterValueInputFloat(&setVoltageInput, "Voltage", "V", NULL, NULL);
@@ -810,8 +796,7 @@ void ocpp_start(void)
ocpp_addErrorCodeInput(&addErrorCodeInput);
/* Task */
xTaskCreate(ocpp_task_func, "ocpp_task", 5 * 1024, NULL, 5, &ocpp_task);
xTaskCreate(ocpp_task_func, "ocpp_task", 5 * 1024, NULL, 3, &ocpp_task);
if (!s_auth_verify_inst)
{
@@ -821,7 +806,6 @@ void ocpp_start(void)
ESP_LOGI(TAG, "Registered AUTH_EVENT_TAG_VERIFY listener");
}
// ouvir mudanças de estado do EVSE
if (!s_evse_state_inst)
{
ESP_ERROR_CHECK(esp_event_handler_instance_register(
@@ -829,7 +813,6 @@ void ocpp_start(void)
&evse_event_handler, NULL, &s_evse_state_inst));
}
// ouvir mudanças de ENABLE / AVAILABLE do EVSE (Local → OCPP)
if (!s_evse_enable_inst)
{
ESP_ERROR_CHECK(esp_event_handler_instance_register(

View File

@@ -6,13 +6,11 @@ set(srcs
"src/ac_relay.c"
"src/socket_lock.c"
"src/rcm.c"
"src/onewire.c"
"src/ds18x20.c"
"src/temp_sensor.c"
"src/ntc_sensor.c"
)
idf_component_register(SRCS "${srcs}"
INCLUDE_DIRS "include"
PRIV_REQUIRES nvs_flash driver esp_adc esp_timer
REQUIRES config evse ntc_driver spi_bus_manager)
PRIV_REQUIRES driver esp_adc esp_timer
REQUIRES config evse ntc_driver spi_bus_manager storage_service)

View File

@@ -1,12 +1,35 @@
// components/peripherals/include/adc121s021_dma.h
#ifndef ADC_DMA_H_
#define ADC_DMA_H_
#include <stdint.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Inicializa o ADC121S021 no barramento SPI partilhado.
*
* - Garante que o spi_bus_manager foi inicializado.
* - Regista o dispositivo ADC no bus.
*/
void adc121s021_dma_init(void);
/**
* @brief Lê uma única amostra (12 bits) do ADC121S021.
*
* Esta função faz uma transação SPI bloqueante (polling), suficientemente
* rápida para uso em burst (100 amostras em ~23 ms).
*
* @param[out] sample Ponteiro onde será escrito o valor lido (0..4095).
* @return true em caso de sucesso, false se ocorrer erro.
*/
bool adc121s021_dma_get_sample(uint16_t *sample);
#ifdef __cplusplus
}
#endif
#endif /* ADC_DMA_h_ */
#endif /* ADC_DMA_H_ */

View File

@@ -1,254 +0,0 @@
/*
* Copyright (c) 2016 Grzegorz Hetman <ghetman@gmail.com>
* Copyright (c) 2016 Alex Stewart <foogod@gmail.com>
* Copyright (c) 2018 Ruslan V. Uss <unclerus@gmail.com>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of the copyright holder nor the names of itscontributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef _DS18X20_H
#define _DS18X20_H
#include <esp_err.h>
#include "onewire.h"
typedef onewire_addr_t ds18x20_addr_t;
/** An address value which can be used to indicate "any device on the bus" */
#define DS18X20_ANY ONEWIRE_NONE
/** Family ID (lower address byte) of DS18B20 sensors */
#define DS18B20_FAMILY_ID 0x28
/** Family ID (lower address byte) of DS18S20 sensors */
#define DS18S20_FAMILY_ID 0x10
/**
* @brief Find the addresses of all ds18x20 devices on the bus.
*
* Scans the bus for all devices and places their addresses in the supplied
* array. If there are more than `addr_count` devices on the bus, only the
* first `addr_count` are recorded.
*
* @param pin The GPIO pin connected to the ds18x20 bus
* @param addr_list A pointer to an array of ::ds18x20_addr_t values.
* This will be populated with the addresses of the found
* devices.
* @param addr_count Number of slots in the `addr_list` array. At most this
* many addresses will be returned.
* @param found The number of devices found. Note that this may be less
* than, equal to, or more than `addr_count`, depending on
* how many ds18x20 devices are attached to the bus.
*
* @returns `ESP_OK` if the command was successfully issued
*/
esp_err_t ds18x20_scan_devices(gpio_num_t pin, ds18x20_addr_t *addr_list, size_t addr_count, size_t *found);
/**
* @brief Tell one or more sensors to perform a temperature measurement and
* conversion (CONVERT_T) operation.
*
* This operation can take up to 750ms to complete.
*
* If `wait=true`, this routine will automatically drive the pin high for the
* necessary 750ms after issuing the command to ensure parasitically-powered
* devices have enough power to perform the conversion operation (for
* non-parasitically-powered devices, this is not necessary but does not
* hurt). If `wait=false`, this routine will drive the pin high, but will
* then return immediately. It is up to the caller to wait the requisite time
* and then depower the bus using onewire_depower() or by issuing another
* command once conversion is done.
*
* @param pin The GPIO pin connected to the ds18x20 device
* @param addr The 64-bit address of the device on the bus. This can be set
* to ::DS18X20_ANY to send the command to all devices on the bus
* at the same time.
* @param wait Whether to wait for the necessary 750ms for the ds18x20 to
* finish performing the conversion before returning to the
* caller (You will normally want to do this).
*
* @returns `ESP_OK` if the command was successfully issued
*/
esp_err_t ds18x20_measure(gpio_num_t pin, ds18x20_addr_t addr, bool wait);
/**
* @brief Read the value from the last CONVERT_T operation.
*
* This should be called after ds18x20_measure() to fetch the result of the
* temperature measurement.
*
* @param pin The GPIO pin connected to the ds18x20 device
* @param addr The 64-bit address of the device to read. This can be set
* to ::DS18X20_ANY to read any device on the bus (but note
* that this will only work if there is exactly one device
* connected, or they will corrupt each others' transmissions)
* @param temperature The temperature in degrees Celsius
*
* @returns `ESP_OK` if the command was successfully issued
*/
esp_err_t ds18x20_read_temperature(gpio_num_t pin, ds18x20_addr_t addr, int16_t *temperature);
/**
* @brief Read the value from the last CONVERT_T operation (ds18b20 version).
*
* This should be called after ds18x20_measure() to fetch the result of the
* temperature measurement.
*
* @param pin The GPIO pin connected to the ds18x20 device
* @param addr The 64-bit address of the device to read. This can be set
* to ::DS18X20_ANY to read any device on the bus (but note
* that this will only work if there is exactly one device
* connected, or they will corrupt each others' transmissions)
* @param temperature The temperature in degrees Celsius
*
* @returns `ESP_OK` if the command was successfully issued
*/
esp_err_t ds18b20_read_temperature(gpio_num_t pin, ds18x20_addr_t addr, int16_t *temperature);
/**
* @brief Read the value from the last CONVERT_T operation (ds18s20 version).
*
* This should be called after ds18x20_measure() to fetch the result of the
* temperature measurement.
*
* @param pin The GPIO pin connected to the ds18x20 device
* @param addr The 64-bit address of the device to read. This can be set
* to ::DS18X20_ANY to read any device on the bus (but note
* that this will only work if there is exactly one device
* connected, or they will corrupt each others' transmissions)
* @param temperature The temperature in degrees Celsius
*
* @returns `ESP_OK` if the command was successfully issued
*/
esp_err_t ds18s20_read_temperature(gpio_num_t pin, ds18x20_addr_t addr, int16_t *temperature);
/**
* @brief Read the value from the last CONVERT_T operation for multiple devices.
*
* This should be called after ds18x20_measure() to fetch the result of the
* temperature measurement.
*
* @param pin The GPIO pin connected to the ds18x20 bus
* @param addr_list A list of addresses for devices to read.
* @param addr_count The number of entries in `addr_list`.
* @param result_list An array of int16_ts to hold the returned temperature
* values. It should have at least `addr_count` entries.
*
* @returns `ESP_OK` if all temperatures were fetched successfully
*/
esp_err_t ds18x20_read_temp_multi(gpio_num_t pin, ds18x20_addr_t *addr_list, size_t addr_count, int16_t *result_list);
/** Perform a ds18x20_measure() followed by ds18s20_read_temperature()
*
* @param pin The GPIO pin connected to the ds18s20 device
* @param addr The 64-bit address of the device to read. This can be set
* to ::DS18X20_ANY to read any device on the bus (but note
* that this will only work if there is exactly one device
* connected, or they will corrupt each others' transmissions)
* @param temperature The temperature in degrees Celsius
*/
esp_err_t ds18s20_measure_and_read(gpio_num_t pin, ds18x20_addr_t addr, int16_t *temperature);
/** Perform a ds18x20_measure() followed by ds18b20_read_temperature()
*
* @param pin The GPIO pin connected to the ds18x20 device
* @param addr The 64-bit address of the device to read. This can be set
* to ::DS18X20_ANY to read any device on the bus (but note
* that this will only work if there is exactly one device
* connected, or they will corrupt each others' transmissions)
* @param temperature The temperature in degrees Celsius
*/
esp_err_t ds18b20_measure_and_read(gpio_num_t pin, ds18x20_addr_t addr, int16_t *temperature);
/** Perform a ds18x20_measure() followed by ds18x20_read_temperature()
*
* @param pin The GPIO pin connected to the ds18x20 device
* @param addr The 64-bit address of the device to read. This can be set
* to ::DS18X20_ANY to read any device on the bus (but note
* that this will only work if there is exactly one device
* connected, or they will corrupt each others' transmissions)
* @param temperature The temperature in degrees Celsius
*/
esp_err_t ds18x20_measure_and_read(gpio_num_t pin, ds18x20_addr_t addr, int16_t *temperature);
/**
* @brief Perform a ds18x20_measure() followed by ds18x20_read_temp_multi()
*
* @param pin The GPIO pin connected to the ds18x20 bus
* @param addr_list A list of addresses for devices to read.
* @param addr_count The number of entries in `addr_list`.
* @param result_list An array of int16_ts to hold the returned temperature
* values. It should have at least `addr_count` entries.
*
* @returns `ESP_OK` if all temperatures were fetched successfully
*/
esp_err_t ds18x20_measure_and_read_multi(gpio_num_t pin, ds18x20_addr_t *addr_list, size_t addr_count, int16_t *result_list);
/**
* @brief Read the scratchpad data for a particular ds18x20 device.
*
* This is not generally necessary to do directly. It is done automatically
* as part of ds18x20_read_temperature().
*
* @param pin The GPIO pin connected to the ds18x20 device
* @param addr The 64-bit address of the device to read. This can be set
* to ::DS18X20_ANY to read any device on the bus (but note
* that this will only work if there is exactly one device
* connected, or they will corrupt each others' transmissions)
* @param buffer An 8-byte buffer to hold the read data.
*
* @returns `ESP_OK` if the command was successfully issued
*/
esp_err_t ds18x20_read_scratchpad(gpio_num_t pin, ds18x20_addr_t addr, uint8_t *buffer);
/**
* @brief Write the scratchpad data for a particular ds18x20 device.
*
* @param pin The GPIO pin connected to the ds18x20 device
* @param addr The 64-bit address of the device to write. This can be set
* to ::DS18X20_ANY to read any device on the bus (but note
* that this will only work if there is exactly one device
* connected, or they will corrupt each others' transmissions)
* @param buffer An 3-byte buffer to hold the data to write
*
* @returns `ESP_OK` if the command was successfully issued
*/
esp_err_t ds18x20_write_scratchpad(gpio_num_t pin, ds18x20_addr_t addr, uint8_t *buffer);
/**
* @brief Issue the copy scratchpad command, copying current scratchpad to
* EEPROM.
*
* @param pin The GPIO pin connected to the ds18x20 device
* @param addr The 64-bit address of the device to command. This can be set
* to ::DS18X20_ANY to read any device on the bus (but note
* that this will only work if there is exactly one device
* connected, or they will corrupt each others' transmissions)
*
* @returns `ESP_OK` if the command was successfully issued
*/
esp_err_t ds18x20_copy_scratchpad(gpio_num_t pin, ds18x20_addr_t addr);
#endif /* _DS18X20_H */

View File

@@ -1,277 +0,0 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2014 zeroday nodemcu.com
*
* 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.
* -------------------------------------------------------------------------------
* Portions copyright (C) 2000 Dallas Semiconductor Corporation, under the
* following additional terms:
*
* Except as contained in this notice, the name of Dallas Semiconductor
* shall not be used except as stated in the Dallas Semiconductor
* Branding Policy.
*/
#ifndef ONEWIRE_H_
#define ONEWIRE_H_
#include <stdbool.h>
#include <stdint.h>
#include "driver/gpio.h"
/**
* Type used to hold all 1-Wire device ROM addresses (64-bit)
*/
typedef uint64_t onewire_addr_t;
/**
* Structure to contain the current state for onewire_search_next(), etc
*/
typedef struct
{
uint8_t rom_no[8];
uint8_t last_discrepancy;
bool last_device_found;
} onewire_search_t;
/**
* ::ONEWIRE_NONE is an invalid ROM address that will never occur in a device
* (CRC mismatch), and so can be useful as an indicator for "no-such-device",
* etc.
*/
#define ONEWIRE_NONE ((onewire_addr_t)(0xffffffffffffffffLL))
/**
* @brief Perform a 1-Wire reset cycle.
*
* @param pin The GPIO pin connected to the 1-Wire bus.
*
* @return `true` if at least one device responds with a presence pulse,
* `false` if no devices were detected (or the bus is shorted, etc)
*/
bool onewire_reset(gpio_num_t pin);
/**
* @brief Issue a 1-Wire "ROM select" command to select a particular device.
*
* It is necessary to call ::onewire_reset() before calling this function.
*
* @param pin The GPIO pin connected to the 1-Wire bus.
* @param addr The ROM address of the device to select
*
* @return `true` if the "ROM select" command could be successfully issued,
* `false` if there was an error.
*/
bool onewire_select(gpio_num_t pin, const onewire_addr_t addr);
/**
* @brief Issue a 1-Wire "skip ROM" command to select *all* devices on the bus.
*
* It is necessary to call ::onewire_reset() before calling this function.
*
* @param pin The GPIO pin connected to the 1-Wire bus.
*
* @return `true` if the "skip ROM" command could be successfully issued,
* `false` if there was an error.
*/
bool onewire_skip_rom(gpio_num_t pin);
/**
* @brief Write a byte on the onewire bus.
*
* The writing code uses open-drain mode and expects the pullup resistor to
* pull the line high when not driven low. If you need strong power after the
* write (e.g. DS18B20 in parasite power mode) then call ::onewire_power()
* after this is complete to actively drive the line high.
*
* @param pin The GPIO pin connected to the 1-Wire bus.
* @param v The byte value to write
*
* @return `true` if successful, `false` on error.
*/
bool onewire_write(gpio_num_t pin, uint8_t v);
/**
* @brief Write multiple bytes on the 1-Wire bus.
*
* See ::onewire_write() for more info.
*
* @param pin The GPIO pin connected to the 1-Wire bus.
* @param buf A pointer to the buffer of bytes to be written
* @param count Number of bytes to write
*
* @return `true` if all bytes written successfully, `false` on error.
*/
bool onewire_write_bytes(gpio_num_t pin, const uint8_t *buf, size_t count);
/**
* @brief Read a byte from a 1-Wire device.
*
* @param pin The GPIO pin connected to the 1-Wire bus.
*
* @return the read byte on success, negative value on error.
*/
int onewire_read(gpio_num_t pin);
/**
* @brief Read multiple bytes from a 1-Wire device.
*
* @param pin The GPIO pin connected to the 1-Wire bus.
* @param[out] buf A pointer to the buffer to contain the read bytes
* @param count Number of bytes to read
*
* @return `true` on success, `false` on error.
*/
bool onewire_read_bytes(gpio_num_t pin, uint8_t *buf, size_t count);
/**
* @brief Actively drive the bus high to provide extra power for certain
* operations of parasitically-powered devices.
*
* For parasitically-powered devices which need more power than can be
* provided via the normal pull-up resistor, it may be necessary for some
* operations to drive the bus actively high. This function can be used to
* perform that operation.
*
* The bus can be depowered once it is no longer needed by calling
* ::onewire_depower(), or it will be depowered automatically the next time
* ::onewire_reset() is called to start another command.
*
* @note Make sure the device(s) you are powering will not pull more current
* than the ESP32/ESP8266 is able to supply via its GPIO pins (this is
* especially important when multiple devices are on the same bus and
* they are all performing a power-intensive operation at the same time
* (i.e. multiple DS18B20 sensors, which have all been given a
* "convert T" operation by using ::onewire_skip_rom())).
*
* @note This routine will check to make sure that the bus is already high
* before driving it, to make sure it doesn't attempt to drive it high
* while something else is pulling it low (which could cause a reset or
* damage the ESP32/ESP8266).
*
* @param pin The GPIO pin connected to the 1-Wire bus.
*
* @return `true` on success, `false` on error.
*/
bool onewire_power(gpio_num_t pin);
/**
* @brief Stop forcing power onto the bus.
*
* You only need to do this if you previously called ::onewire_power() to drive
* the bus high and now want to allow it to float instead. Note that
* onewire_reset() will also automatically depower the bus first, so you do
* not need to call this first if you just want to start a new operation.
*
* @param pin The GPIO pin connected to the 1-Wire bus.
*/
void onewire_depower(gpio_num_t pin);
/**
* @brief Clear the search state so that it will start from the beginning on
* the next call to ::onewire_search_next().
*
* @param[out] search The onewire_search_t structure to reset.
*/
void onewire_search_start(onewire_search_t *search);
/**
* @brief Setup the search to search for devices with the specified
* "family code".
*
* @param[out] search The onewire_search_t structure to update.
* @param family_code The "family code" to search for.
*/
void onewire_search_prefix(onewire_search_t *search, uint8_t family_code);
/**
* @brief Search for the next device on the bus.
*
* The order of returned device addresses is deterministic. You will always
* get the same devices in the same order.
*
* @note It might be a good idea to check the CRC to make sure you didn't get
* garbage.
*
* @return the address of the next device on the bus, or ::ONEWIRE_NONE if
* there is no next address. ::ONEWIRE_NONE might also mean that
* the bus is shorted, there are no devices, or you have already
* retrieved all of them.
*/
onewire_addr_t onewire_search_next(onewire_search_t *search, gpio_num_t pin);
/**
* @brief Compute a Dallas Semiconductor 8 bit CRC.
*
* These are used in the ROM address and scratchpad registers to verify the
* transmitted data is correct.
*/
uint8_t onewire_crc8(const uint8_t *data, uint8_t len);
/**
* @brief Compute the 1-Wire CRC16 and compare it against the received CRC.
*
* Example usage (reading a DS2408):
* @code{.c}
* // Put everything in a buffer so we can compute the CRC easily.
* uint8_t buf[13];
* buf[0] = 0xF0; // Read PIO Registers
* buf[1] = 0x88; // LSB address
* buf[2] = 0x00; // MSB address
* onewire_write_bytes(pin, buf, 3); // Write 3 cmd bytes
* onewire_read_bytes(pin, buf+3, 10); // Read 6 data bytes, 2 0xFF, 2 CRC16
* if (!onewire_check_crc16(buf, 11, &buf[11])) {
* // TODO: Handle error.
* }
* @endcode
*
* @param input Array of bytes to checksum.
* @param len Number of bytes in `input`
* @param inverted_crc The two CRC16 bytes in the received data.
* This should just point into the received data,
* *not* at a 16-bit integer.
* @param crc_iv The crc starting value (optional)
*
* @return `true` if the CRC matches, `false` otherwise.
*/
bool onewire_check_crc16(const uint8_t* input, size_t len, const uint8_t* inverted_crc, uint16_t crc_iv);
/**
* @brief Compute a Dallas Semiconductor 16 bit CRC.
*
* This is required to check the integrity of data received from many 1-Wire
* devices. Note that the CRC computed here is *not* what you'll get from the
* 1-Wire network, for two reasons:
*
* 1. The CRC is transmitted bitwise inverted.
* 2. Depending on the endian-ness of your processor, the binary
* representation of the two-byte return value may have a different
* byte order than the two bytes you get from 1-Wire.
*
* @param input Array of bytes to checksum.
* @param len How many bytes are in `input`.
* @param crc_iv The crc starting value (optional)
*
* @return the CRC16, as defined by Dallas Semiconductor.
*/
uint16_t onewire_crc16(const uint8_t* input, size_t len, uint16_t crc_iv);
#endif /* ONEWIRE_H_ */

View File

@@ -1,28 +1,41 @@
// components/peripherals/src/adc121s021_dma.c
#include "driver/spi_master.h"
#include "esp_log.h"
#include "esp_err.h"
#include "adc121s021_dma.h"
#include "spi_bus_manager.h"
#define TAG "adc_dma"
// Pino de chip-select do ADC121S021 (ajusta se necessário)
#define PIN_NUM_CS 5
#define SAMPLE_SIZE_BYTES 2
// ADC é 12-bit, mas transferimos 16 bits via SPI
#define ADC_BITS 12
#define SPI_CLOCK_HZ (6 * 1000 * 1000) // 6 MHz
// Clock SPI: 1 MHz → ~16 µs de transferência por amostra.
// Com um pequeno delay entre leituras, 100 amostras ficam em ~23 ms,
// o que é perfeito para analisar um PWM de 1 kHz a cada 100 ms.
#define SPI_CLOCK_HZ (1 * 1000 * 1000)
static spi_device_handle_t adc_spi = NULL;
void adc121s021_dma_init(void)
{
if (adc_spi) {
if (adc_spi)
{
ESP_LOGW(TAG, "ADC121S021 já foi inicializado.");
return;
}
if (!spi_bus_manager_is_initialized()) {
// Garante que o SPI bus partilhado está configurado
if (!spi_bus_manager_is_initialized())
{
ESP_LOGI(TAG, "SPI bus não inicializado. Inicializando...");
esp_err_t err = spi_bus_manager_init(); // 🔧 CORRIGIDO: sem argumentos
if (err != ESP_OK) {
esp_err_t err = spi_bus_manager_init();
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Falha ao inicializar o SPI bus: %s", esp_err_to_name(err));
return;
}
@@ -32,44 +45,55 @@ void adc121s021_dma_init(void)
.clock_speed_hz = SPI_CLOCK_HZ,
.mode = 0,
.spics_io_num = PIN_NUM_CS,
.queue_size = 2,
.queue_size = 2, // suficiente para uso em burst
.flags = SPI_DEVICE_NO_DUMMY,
.pre_cb = NULL,
.post_cb = NULL,
};
esp_err_t err = spi_bus_add_device(spi_bus_manager_get_host(), &devcfg, &adc_spi);
if (err != ESP_OK) {
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Falha ao registrar ADC121S021 no SPI: %s", esp_err_to_name(err));
adc_spi = NULL;
return;
}
ESP_LOGI(TAG, "ADC121S021 registrado no SPI com sucesso.");
ESP_LOGI(TAG, "ADC121S021 registrado no SPI (CS=%d, fSPI=%d Hz).",
PIN_NUM_CS, SPI_CLOCK_HZ);
}
bool adc121s021_dma_get_sample(uint16_t *sample)
{
if (!adc_spi) {
ESP_LOGE(TAG, "ADC SPI não inicializado!");
if (!sample)
{
return false;
}
uint8_t tx_buffer[2] = {0x00, 0x00}; // Dummy
if (!adc_spi)
{
ESP_LOGE(TAG, "ADC SPI não inicializado! Chama adc121s021_dma_init() primeiro.");
return false;
}
uint8_t tx_buffer[2] = {0x00, 0x00}; // Dummy (ADC só precisa de clock)
uint8_t rx_buffer[2] = {0};
spi_transaction_t t = {
.length = 16,
.length = 16, // 16 bits
.tx_buffer = tx_buffer,
.rx_buffer = rx_buffer,
.flags = 0
};
.flags = 0};
esp_err_t err = spi_device_transmit(adc_spi, &t);
if (err != ESP_OK) {
// Polling transmit → menor overhead que fila + espera.
esp_err_t err = spi_device_polling_transmit(adc_spi, &t);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Erro na transmissão SPI: %s", esp_err_to_name(err));
return false;
}
*sample = ((rx_buffer[0] << 8) | rx_buffer[1]) & 0x0FFF;
// ADC121S021 devolve os 12 bits mais significativos em 16 bits.
*sample = (uint16_t)(((rx_buffer[0] << 8) | rx_buffer[1]) & 0x0FFF);
return true;
}

View File

@@ -1,265 +0,0 @@
/*
* Copyright (c) 2016 Grzegorz Hetman <ghetman@gmail.com>
* Copyright (c) 2016 Alex Stewart <foogod@gmail.com>
* Copyright (c) 2018 Ruslan V. Uss <unclerus@gmail.com>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of the copyright holder nor the names of itscontributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include <math.h>
#include <esp_log.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include "ds18x20.h"
#define ds18x20_WRITE_SCRATCHPAD 0x4E
#define ds18x20_READ_SCRATCHPAD 0xBE
#define ds18x20_COPY_SCRATCHPAD 0x48
#define ds18x20_READ_EEPROM 0xB8
#define ds18x20_READ_PWRSUPPLY 0xB4
#define ds18x20_SEARCHROM 0xF0
#define ds18x20_SKIP_ROM 0xCC
#define ds18x20_READROM 0x33
#define ds18x20_MATCHROM 0x55
#define ds18x20_ALARMSEARCH 0xEC
#define ds18x20_CONVERT_T 0x44
#define CHECK(x) do { esp_err_t __; if ((__ = x) != ESP_OK) return __; } while (0)
#define CHECK_ARG(VAL) do { if (!(VAL)) return ESP_ERR_INVALID_ARG; } while (0)
static portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;
static const char* TAG = "ds18x20";
esp_err_t ds18x20_measure(gpio_num_t pin, ds18x20_addr_t addr, bool wait)
{
if (!onewire_reset(pin))
return ESP_ERR_INVALID_RESPONSE;
if (addr == DS18X20_ANY)
onewire_skip_rom(pin);
else
onewire_select(pin, addr);
portENTER_CRITICAL(&mux);
onewire_write(pin, ds18x20_CONVERT_T);
// For parasitic devices, power must be applied within 10us after issuing
// the convert command.
onewire_power(pin);
portEXIT_CRITICAL(&mux);
if (wait){
vTaskDelay(pdMS_TO_TICKS(750));
onewire_depower(pin);
}
return ESP_OK;
}
esp_err_t ds18x20_read_scratchpad(gpio_num_t pin, ds18x20_addr_t addr, uint8_t* buffer)
{
CHECK_ARG(buffer);
uint8_t crc;
uint8_t expected_crc;
if (!onewire_reset(pin))
return ESP_ERR_INVALID_RESPONSE;
if (addr == DS18X20_ANY)
onewire_skip_rom(pin);
else
onewire_select(pin, addr);
onewire_write(pin, ds18x20_READ_SCRATCHPAD);
for (int i = 0; i < 8; i++)
buffer[i] = onewire_read(pin);
crc = onewire_read(pin);
expected_crc = onewire_crc8(buffer, 8);
if (crc != expected_crc)
{
ESP_LOGE(TAG, "CRC check failed reading scratchpad: %02x %02x %02x %02x %02x %02x %02x %02x : %02x (expected %02x)", buffer[0], buffer[1],
buffer[2], buffer[3], buffer[4], buffer[5], buffer[6], buffer[7], crc, expected_crc);
return ESP_ERR_INVALID_CRC;
}
return ESP_OK;
}
esp_err_t ds18x20_write_scratchpad(gpio_num_t pin, ds18x20_addr_t addr, uint8_t* buffer)
{
CHECK_ARG(buffer);
if (!onewire_reset(pin))
return ESP_ERR_INVALID_RESPONSE;
if (addr == DS18X20_ANY)
onewire_skip_rom(pin);
else
onewire_select(pin, addr);
onewire_write(pin, ds18x20_WRITE_SCRATCHPAD);
for (int i = 0; i < 3; i++)
onewire_write(pin, buffer[i]);
return ESP_OK;
}
esp_err_t ds18x20_copy_scratchpad(gpio_num_t pin, ds18x20_addr_t addr)
{
if (!onewire_reset(pin))
return ESP_ERR_INVALID_RESPONSE;
if (addr == DS18X20_ANY)
onewire_skip_rom(pin);
else
onewire_select(pin, addr);
portENTER_CRITICAL(&mux);
onewire_write(pin, ds18x20_COPY_SCRATCHPAD);
// For parasitic devices, power must be applied within 10us after issuing
// the convert command.
onewire_power(pin);
portEXIT_CRITICAL(&mux);
// And then it needs to keep that power up for 10ms.
vTaskDelay(pdMS_TO_TICKS(10));
onewire_depower(pin);
return ESP_OK;
}
esp_err_t ds18b20_read_temperature(gpio_num_t pin, ds18x20_addr_t addr, int16_t* temperature)
{
CHECK_ARG(temperature);
uint8_t scratchpad[8];
int16_t temp;
CHECK(ds18x20_read_scratchpad(pin, addr, scratchpad));
temp = scratchpad[1] << 8 | scratchpad[0];
*temperature = ((int16_t)temp * 625.0) / 100;
return ESP_OK;
}
esp_err_t ds18s20_read_temperature(gpio_num_t pin, ds18x20_addr_t addr, int16_t* temperature)
{
CHECK_ARG(temperature);
uint8_t scratchpad[8];
int16_t temp;
CHECK(ds18x20_read_scratchpad(pin, addr, scratchpad));
temp = scratchpad[1] << 8 | scratchpad[0];
temp = ((temp & 0xfffe) << 3) + (16 - scratchpad[6]) - 4;
*temperature = (temp * 625) / 100 - 25;
return ESP_OK;
}
esp_err_t ds18x20_read_temperature(gpio_num_t pin, ds18x20_addr_t addr, int16_t* temperature)
{
if ((uint8_t)addr == DS18B20_FAMILY_ID) {
return ds18b20_read_temperature(pin, addr, temperature);
} else {
return ds18s20_read_temperature(pin, addr, temperature);
}
}
esp_err_t ds18b20_measure_and_read(gpio_num_t pin, ds18x20_addr_t addr, int16_t* temperature)
{
CHECK_ARG(temperature);
CHECK(ds18x20_measure(pin, addr, true));
return ds18b20_read_temperature(pin, addr, temperature);
}
esp_err_t ds18s20_measure_and_read(gpio_num_t pin, ds18x20_addr_t addr, int16_t* temperature)
{
CHECK_ARG(temperature);
CHECK(ds18x20_measure(pin, addr, true));
return ds18s20_read_temperature(pin, addr, temperature);
}
esp_err_t ds18x20_measure_and_read(gpio_num_t pin, ds18x20_addr_t addr, int16_t* temperature)
{
CHECK_ARG(temperature);
CHECK(ds18x20_measure(pin, addr, true));
return ds18x20_read_temperature(pin, addr, temperature);
}
esp_err_t ds18x20_measure_and_read_multi(gpio_num_t pin, ds18x20_addr_t* addr_list, size_t addr_count, int16_t* result_list)
{
CHECK_ARG(result_list && addr_count);
CHECK(ds18x20_measure(pin, DS18X20_ANY, true));
return ds18x20_read_temp_multi(pin, addr_list, addr_count, result_list);
}
esp_err_t ds18x20_scan_devices(gpio_num_t pin, ds18x20_addr_t* addr_list, size_t addr_count, size_t* found)
{
CHECK_ARG(addr_list && addr_count);
onewire_search_t search;
onewire_addr_t addr;
*found = 0;
onewire_search_start(&search);
while ((addr = onewire_search_next(&search, pin)) != ONEWIRE_NONE)
{
uint8_t family_id = (uint8_t)addr;
if (family_id == DS18B20_FAMILY_ID || family_id == DS18S20_FAMILY_ID)
{
if (*found < addr_count)
addr_list[*found] = addr;
*found += 1;
}
}
return ESP_OK;
}
esp_err_t ds18x20_read_temp_multi(gpio_num_t pin, ds18x20_addr_t* addr_list, size_t addr_count, int16_t* result_list)
{
CHECK_ARG(result_list);
esp_err_t res = ESP_OK;
for (size_t i = 0; i < addr_count; i++)
{
esp_err_t tmp = ds18x20_read_temperature(pin, addr_list[i], &result_list[i]);
if (tmp != ESP_OK)
res = tmp;
}
return res;
}

View File

@@ -1,498 +0,0 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2014 zeroday nodemcu.com
*
* 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.
* -------------------------------------------------------------------------------
* Portions copyright (C) 2000 Dallas Semiconductor Corporation, under the
* following additional terms:
*
* Except as contained in this notice, the name of Dallas Semiconductor
* shall not be used except as stated in the Dallas Semiconductor
* Branding Policy.
*/
#include <string.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include "rom/ets_sys.h"
#include "onewire.h"
#define ONEWIRE_SELECT_ROM 0x55
#define ONEWIRE_SKIP_ROM 0xcc
#define ONEWIRE_SEARCH 0xf0
#define ONEWIRE_CRC8_TABLE
static portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;
// Waits up to `max_wait` microseconds for the specified pin to go high.
// Returns true if successful, false if the bus never comes high (likely
// shorted).
static inline bool _onewire_wait_for_bus(gpio_num_t pin, int max_wait)
{
bool state;
for (int i = 0; i < ((max_wait + 4) / 5); i++) {
if (gpio_get_level(pin))
break;
ets_delay_us(5);
}
state = gpio_get_level(pin);
// Wait an extra 1us to make sure the devices have an adequate recovery
// time before we drive things low again.
ets_delay_us(1);
return state;
}
static void setup_pin(gpio_num_t pin, bool open_drain)
{
gpio_set_direction(pin, open_drain ? GPIO_MODE_INPUT_OUTPUT_OD : GPIO_MODE_OUTPUT);
// gpio_set_pull_mode(pin, GPIO_PULLUP_ONLY);
}
// Perform the onewire reset function. We will wait up to 250uS for
// the bus to come high, if it doesn't then it is broken or shorted
// and we return false;
//
// Returns true if a device asserted a presence pulse, false otherwise.
//
bool onewire_reset(gpio_num_t pin)
{
setup_pin(pin, true);
gpio_set_level(pin, 1);
// wait until the wire is high... just in case
if (!_onewire_wait_for_bus(pin, 250))
return false;
gpio_set_level(pin, 0);
ets_delay_us(480);
portENTER_CRITICAL(&mux);
gpio_set_level(pin, 1); // allow it to float
ets_delay_us(70);
bool r = !gpio_get_level(pin);
portEXIT_CRITICAL(&mux);
// Wait for all devices to finish pulling the bus low before returning
if (!_onewire_wait_for_bus(pin, 410))
return false;
return r;
}
static bool _onewire_write_bit(gpio_num_t pin, bool v)
{
if (!_onewire_wait_for_bus(pin, 10))
return false;
portENTER_CRITICAL(&mux);
if (v) {
gpio_set_level(pin, 0); // drive output low
ets_delay_us(10);
gpio_set_level(pin, 1); // allow output high
ets_delay_us(55);
} else {
gpio_set_level(pin, 0); // drive output low
ets_delay_us(65);
gpio_set_level(pin, 1); // allow output high
}
ets_delay_us(1);
portEXIT_CRITICAL(&mux);
return true;
}
static int _onewire_read_bit(gpio_num_t pin)
{
if (!_onewire_wait_for_bus(pin, 10))
return -1;
portENTER_CRITICAL(&mux);
gpio_set_level(pin, 0);
ets_delay_us(2);
gpio_set_level(pin, 1); // let pin float, pull up will raise
ets_delay_us(11);
int r = gpio_get_level(pin); // Must sample within 15us of start
ets_delay_us(48);
portEXIT_CRITICAL(&mux);
return r;
}
// Write a byte. The writing code uses open-drain mode and expects the pullup
// resistor to pull the line high when not driven low. If you need strong
// power after the write (e.g. DS18B20 in parasite power mode) then call
// onewire_power() after this is complete to actively drive the line high.
//
bool onewire_write(gpio_num_t pin, uint8_t v)
{
for (uint8_t bitMask = 0x01; bitMask; bitMask <<= 1)
if (!_onewire_write_bit(pin, (bitMask & v)))
return false;
return true;
}
bool onewire_write_bytes(gpio_num_t pin, const uint8_t* buf, size_t count)
{
for (size_t i = 0; i < count; i++)
if (!onewire_write(pin, buf[i]))
return false;
return true;
}
// Read a byte
//
int onewire_read(gpio_num_t pin)
{
int r = 0;
for (uint8_t bitMask = 0x01; bitMask; bitMask <<= 1) {
int bit = _onewire_read_bit(pin);
if (bit < 0)
return -1;
else if (bit)
r |= bitMask;
}
return r;
}
bool onewire_read_bytes(gpio_num_t pin, uint8_t* buf, size_t count)
{
size_t i;
int b;
for (i = 0; i < count; i++) {
b = onewire_read(pin);
if (b < 0)
return false;
buf[i] = b;
}
return true;
}
bool onewire_select(gpio_num_t pin, onewire_addr_t addr)
{
uint8_t i;
if (!onewire_write(pin, ONEWIRE_SELECT_ROM))
return false;
for (i = 0; i < 8; i++) {
if (!onewire_write(pin, addr & 0xff))
return false;
addr >>= 8;
}
return true;
}
bool onewire_skip_rom(gpio_num_t pin)
{
return onewire_write(pin, ONEWIRE_SKIP_ROM);
}
bool onewire_power(gpio_num_t pin)
{
// Make sure the bus is not being held low before driving it high, or we
// may end up shorting ourselves out.
if (!_onewire_wait_for_bus(pin, 10))
return false;
setup_pin(pin, false);
gpio_set_level(pin, 1);
return true;
}
void onewire_depower(gpio_num_t pin)
{
setup_pin(pin, true);
}
void onewire_search_start(onewire_search_t* search)
{
// reset the search state
memset(search, 0, sizeof(*search));
}
void onewire_search_prefix(onewire_search_t* search, uint8_t family_code)
{
uint8_t i;
search->rom_no[0] = family_code;
for (i = 1; i < 8; i++) {
search->rom_no[i] = 0;
}
search->last_discrepancy = 64;
search->last_device_found = false;
}
// Perform a search. If the next device has been successfully enumerated, its
// ROM address will be returned. If there are no devices, no further
// devices, or something horrible happens in the middle of the
// enumeration then ONEWIRE_NONE is returned. Use OneWire::reset_search() to
// start over.
//
// --- Replaced by the one from the Dallas Semiconductor web site ---
//--------------------------------------------------------------------------
// Perform the 1-Wire Search Algorithm on the 1-Wire bus using the existing
// search state.
// Return 1 : device found, ROM number in ROM_NO buffer
// 0 : device not found, end of search
//
onewire_addr_t onewire_search_next(onewire_search_t* search, gpio_num_t pin)
{
//TODO: add more checking for read/write errors
uint8_t id_bit_number;
uint8_t last_zero, search_result;
int rom_byte_number;
int8_t id_bit, cmp_id_bit;
onewire_addr_t addr;
unsigned char rom_byte_mask;
bool search_direction;
// initialize for search
id_bit_number = 1;
last_zero = 0;
rom_byte_number = 0;
rom_byte_mask = 1;
search_result = 0;
// if the last call was not the last one
if (!search->last_device_found) {
// 1-Wire reset
if (!onewire_reset(pin)) {
// reset the search
search->last_discrepancy = 0;
search->last_device_found = false;
return ONEWIRE_NONE;
}
// issue the search command
onewire_write(pin, ONEWIRE_SEARCH);
// loop to do the search
do {
// read a bit and its complement
id_bit = _onewire_read_bit(pin);
cmp_id_bit = _onewire_read_bit(pin);
if ((id_bit == 1) && (cmp_id_bit == 1))
break;
else {
// all devices coupled have 0 or 1
if (id_bit != cmp_id_bit)
search_direction = id_bit; // bit write value for search
else {
// if this discrepancy if before the Last Discrepancy
// on a previous next then pick the same as last time
if (id_bit_number < search->last_discrepancy)
search_direction = ((search->rom_no[rom_byte_number] & rom_byte_mask) > 0);
else
// if equal to last pick 1, if not then pick 0
search_direction = (id_bit_number == search->last_discrepancy);
// if 0 was picked then record its position in LastZero
if (!search_direction)
last_zero = id_bit_number;
}
// set or clear the bit in the ROM byte rom_byte_number
// with mask rom_byte_mask
if (search_direction)
search->rom_no[rom_byte_number] |= rom_byte_mask;
else
search->rom_no[rom_byte_number] &= ~rom_byte_mask;
// serial number search direction write bit
_onewire_write_bit(pin, search_direction);
// increment the byte counter id_bit_number
// and shift the mask rom_byte_mask
id_bit_number++;
rom_byte_mask <<= 1;
// if the mask is 0 then go to new SerialNum byte rom_byte_number and reset mask
if (rom_byte_mask == 0) {
rom_byte_number++;
rom_byte_mask = 1;
}
}
} while (rom_byte_number < 8); // loop until through all ROM bytes 0-7
// if the search was successful then
if (!(id_bit_number < 65)) {
// search successful so set last_discrepancy,last_device_found,search_result
search->last_discrepancy = last_zero;
// check for last device
if (search->last_discrepancy == 0)
search->last_device_found = true;
search_result = 1;
}
}
// if no device found then reset counters so next 'search' will be like a first
if (!search_result || !search->rom_no[0]) {
search->last_discrepancy = 0;
search->last_device_found = false;
return ONEWIRE_NONE;
} else {
addr = 0;
for (rom_byte_number = 7; rom_byte_number >= 0; rom_byte_number--) {
addr = (addr << 8) | search->rom_no[rom_byte_number];
}
//printf("Ok I found something at %08x%08x...\n", (uint32_t)(addr >> 32), (uint32_t)addr);
}
return addr;
}
// The 1-Wire CRC scheme is described in Maxim Application Note 27:
// "Understanding and Using Cyclic Redundancy Checks with Maxim iButton Products"
//
#ifdef ONEWIRE_CRC8_TABLE
// This table comes from Dallas sample code where it is freely reusable,
// though Copyright (c) 2000 Dallas Semiconductor Corporation
static const uint8_t dscrc_table[] = {
0, 94, 188, 226, 97, 63, 221, 131, 194, 156, 126, 32, 163, 253, 31, 65,
157, 195, 33, 127, 252, 162, 64, 30, 95, 1, 227, 189, 62, 96, 130, 220,
35, 125, 159, 193, 66, 28, 254, 160, 225, 191, 93, 3, 128, 222, 60, 98,
190, 224, 2, 92, 223, 129, 99, 61, 124, 34, 192, 158, 29, 67, 161, 255,
70, 24, 250, 164, 39, 121, 155, 197, 132, 218, 56, 102, 229, 187, 89, 7,
219, 133, 103, 57, 186, 228, 6, 88, 25, 71, 165, 251, 120, 38, 196, 154,
101, 59, 217, 135, 4, 90, 184, 230, 167, 249, 27, 69, 198, 152, 122, 36,
248, 166, 68, 26, 153, 199, 37, 123, 58, 100, 134, 216, 91, 5, 231, 185,
140, 210, 48, 110, 237, 179, 81, 15, 78, 16, 242, 172, 47, 113, 147, 205,
17, 79, 173, 243, 112, 46, 204, 146, 211, 141, 111, 49, 178, 236, 14, 80,
175, 241, 19, 77, 206, 144, 114, 44, 109, 51, 209, 143, 12, 82, 176, 238,
50, 108, 142, 208, 83, 13, 239, 177, 240, 174, 76, 18, 145, 207, 45, 115,
202, 148, 118, 40, 171, 245, 23, 73, 8, 86, 180, 234, 105, 55, 213, 139,
87, 9, 235, 181, 54, 104, 138, 212, 149, 203, 41, 119, 244, 170, 72, 22,
233, 183, 85, 11, 136, 214, 52, 106, 43, 117, 151, 201, 74, 20, 246, 168,
116, 42, 200, 150, 21, 75, 169, 247, 182, 232, 10, 84, 215, 137, 107, 53
};
//
// Compute a Dallas Semiconductor 8 bit CRC. These show up in the ROM
// and the registers. (note: this might better be done without to
// table, it would probably be smaller and certainly fast enough
// compared to all those delayMicrosecond() calls. But I got
// confused, so I use this table from the examples.)
//
uint8_t onewire_crc8(const uint8_t* data, uint8_t len)
{
uint8_t crc = 0;
while (len--)
crc = dscrc_table[crc ^ *data++];
return crc;
}
#else
//
// Compute a Dallas Semiconductor 8 bit CRC directly.
// this is much slower, but much smaller, than the lookup table.
//
uint8_t onewire_crc8(const uint8_t* data, uint8_t len)
{
uint8_t crc = 0;
while (len--)
{
uint8_t inbyte = *data++;
for (int i = 8; i; i--)
{
uint8_t mix = (crc ^ inbyte) & 0x01;
crc >>= 1;
if (mix)
crc ^= 0x8C;
inbyte >>= 1;
}
}
return crc;
}
#endif /* ONEWIRE_CRC8_TABLE */
// Compute the 1-Wire CRC16 and compare it against the received CRC.
// Example usage (reading a DS2408):
// // Put everything in a buffer so we can compute the CRC easily.
// uint8_t buf[13];
// buf[0] = 0xF0; // Read PIO Registers
// buf[1] = 0x88; // LSB address
// buf[2] = 0x00; // MSB address
// WriteBytes(net, buf, 3); // Write 3 cmd bytes
// ReadBytes(net, buf+3, 10); // Read 6 data bytes, 2 0xFF, 2 CRC16
// if (!CheckCRC16(buf, 11, &buf[11])) {
// // Handle error.
// }
//
// @param input - Array of bytes to checksum.
// @param len - How many bytes to use.
// @param inverted_crc - The two CRC16 bytes in the received data.
// This should just point into the received data,
// *not* at a 16-bit integer.
// @param crc - The crc starting value (optional)
// @return 1, iff the CRC matches.
bool onewire_check_crc16(const uint8_t* input, size_t len, const uint8_t* inverted_crc, uint16_t crc_iv)
{
uint16_t crc = ~onewire_crc16(input, len, crc_iv);
return (crc & 0xFF) == inverted_crc[0] && (crc >> 8) == inverted_crc[1];
}
// Compute a Dallas Semiconductor 16 bit CRC. This is required to check
// the integrity of data received from many 1-Wire devices. Note that the
// CRC computed here is *not* what you'll get from the 1-Wire network,
// for two reasons:
// 1) The CRC is transmitted bitwise inverted.
// 2) Depending on the endian-ness of your processor, the binary
// representation of the two-byte return value may have a different
// byte order than the two bytes you get from 1-Wire.
// @param input - Array of bytes to checksum.
// @param len - How many bytes to use.
// @param crc - The crc starting value (optional)
// @return The CRC16, as defined by Dallas Semiconductor.
uint16_t onewire_crc16(const uint8_t* input, size_t len, uint16_t crc_iv)
{
uint16_t crc = crc_iv;
static const uint8_t oddparity[16] = { 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0 };
uint16_t i;
for (i = 0; i < len; i++) {
// Even though we're just copying a byte from the input,
// we'll be doing 16-bit computation with it.
uint16_t cdata = input[i];
cdata = (cdata ^ crc) & 0xff;
crc >>= 8;
if (oddparity[cdata & 0x0F] ^ oddparity[cdata >> 4])
crc ^= 0xC001;
cdata <<= 6;
crc ^= cdata;
cdata <<= 1;
crc ^= cdata;
}
return crc;
}

View File

@@ -1,7 +1,5 @@
#include "peripherals.h"
#include "adc.h"
//#include "led.h"
// #include "buzzer.h"
#include "proximity.h"
#include "ac_relay.h"
#include "socket_lock.h"
@@ -11,13 +9,9 @@
void peripherals_init(void)
{
ac_relay_init();
// led_init();
// buzzer_init();
adc_init();
proximity_init();
// socket_lock_init();
// rcm_init();
// energy_meter_init();
// aux_init();
ntc_sensor_init();
}

View File

@@ -4,12 +4,15 @@
#include "freertos/semphr.h"
#include "freertos/timers.h"
#include "esp_log.h"
#include "esp_err.h"
#include "driver/gpio.h"
#include "nvs.h"
#include "socket_lock.h"
#include "board_config.h"
// NEW:
#include "storage_service.h"
#define NVS_NAMESPACE "socket_lock"
#define NVS_OPERATING_TIME "op_time"
#define NVS_BREAK_TIME "break_time"
@@ -27,20 +30,68 @@
static const char* TAG = "socket_lock";
static nvs_handle_t nvs;
// Storage timeouts (ajusta se quiseres)
#define STORE_TO pdMS_TO_TICKS(800)
#define STORE_FLUSH_TO pdMS_TO_TICKS(2000)
static uint16_t operating_time = 300;
static uint16_t break_time = 1000;
static bool detection_high;
static bool detection_high = false;
static uint8_t retry_count = 5;
static socket_lock_status_t status;
static TaskHandle_t socket_lock_task;
// -----------------------------------------------------------------------------
// Helpers storage (best effort) - iguais ao estilo do wifi.c
// -----------------------------------------------------------------------------
static esp_err_t store_flush_best_effort(void)
{
esp_err_t e = storage_flush_sync(STORE_FLUSH_TO);
if (e != ESP_OK)
ESP_LOGW(TAG, "storage_flush_sync failed: %s", esp_err_to_name(e));
return e;
}
static esp_err_t store_set_u8_best_effort(const char *ns, const char *key, uint8_t v)
{
for (int attempt = 0; attempt < 3; ++attempt)
{
esp_err_t e = storage_set_u8_async(ns, key, v);
if (e == ESP_OK) return ESP_OK;
if (e == ESP_ERR_TIMEOUT)
{
(void)store_flush_best_effort();
vTaskDelay(pdMS_TO_TICKS(10));
continue;
}
return e;
}
return ESP_ERR_TIMEOUT;
}
static esp_err_t store_set_u16_best_effort(const char *ns, const char *key, uint16_t v)
{
for (int attempt = 0; attempt < 3; ++attempt)
{
esp_err_t e = storage_set_u16_async(ns, key, v);
if (e == ESP_OK) return ESP_OK;
if (e == ESP_ERR_TIMEOUT)
{
(void)store_flush_best_effort();
vTaskDelay(pdMS_TO_TICKS(10));
continue;
}
return e;
}
return ESP_ERR_TIMEOUT;
}
// -----------------------------------------------------------------------------
// Lock logic
// -----------------------------------------------------------------------------
static bool is_locked(void)
{
gpio_set_level(board_config.socket_lock_a_gpio, 1);
@@ -58,31 +109,42 @@ bool socket_lock_is_locked_state(void)
static void socket_lock_task_func(void* param)
{
uint32_t notification;
(void)param;
uint32_t notification;
TickType_t previous_tick = 0;
uint8_t attempt = 0;
while (true) {
if (xTaskNotifyWait(0x00, 0xff, &notification, portMAX_DELAY)) {
if (notification & (LOCK_BIT | UNLOCK_BIT)) {
while (true)
{
if (xTaskNotifyWait(0x00, 0xff, &notification, portMAX_DELAY))
{
if (notification & (LOCK_BIT | UNLOCK_BIT))
{
attempt = retry_count;
}
if (notification & (UNLOCK_BIT | REPEAT_UNLOCK_BIT)) {
if (notification & (UNLOCK_BIT | REPEAT_UNLOCK_BIT))
{
gpio_set_level(board_config.socket_lock_a_gpio, 0);
gpio_set_level(board_config.socket_lock_b_gpio, 1);
vTaskDelay(pdMS_TO_TICKS(operating_time));
if (!is_locked()) {
if (!is_locked())
{
ESP_LOGI(TAG, "Unlock OK");
status = SOCKED_LOCK_STATUS_IDLE;
} else {
if (attempt > 1) {
}
else
{
if (attempt > 1)
{
ESP_LOGW(TAG, "Not unlocked yet, repeating...");
attempt--;
xTaskNotify(socket_lock_task, REPEAT_UNLOCK_BIT, eSetBits);
} else {
}
else
{
ESP_LOGE(TAG, "Not unlocked");
status = SOCKED_LOCK_STATUS_UNLOCKING_FAIL;
}
@@ -90,23 +152,33 @@ static void socket_lock_task_func(void* param)
gpio_set_level(board_config.socket_lock_a_gpio, 0);
gpio_set_level(board_config.socket_lock_b_gpio, 0);
} else if (notification & (LOCK_BIT | REPEAT_LOCK_BIT)) {
if (notification & LOCK_BIT) {
vTaskDelay(pdMS_TO_TICKS(LOCK_DELAY)); //delay before first lock attempt
}
else if (notification & (LOCK_BIT | REPEAT_LOCK_BIT))
{
if (notification & LOCK_BIT)
{
vTaskDelay(pdMS_TO_TICKS(LOCK_DELAY)); // delay before first lock attempt
}
gpio_set_level(board_config.socket_lock_a_gpio, 1);
gpio_set_level(board_config.socket_lock_b_gpio, 0);
vTaskDelay(pdMS_TO_TICKS(operating_time));
if (is_locked()) {
if (is_locked())
{
ESP_LOGI(TAG, "Lock OK");
status = SOCKED_LOCK_STATUS_IDLE;
} else {
if (attempt > 1) {
}
else
{
if (attempt > 1)
{
ESP_LOGW(TAG, "Not locked yet, repeating...");
attempt--;
xTaskNotify(socket_lock_task, REPEAT_LOCK_BIT, eSetBits);
} else {
}
else
{
ESP_LOGE(TAG, "Not locked");
status = SOCKED_LOCK_STATUS_LOCKING_FAIL;
}
@@ -117,7 +189,8 @@ static void socket_lock_task_func(void* param)
}
TickType_t delay_tick = xTaskGetTickCount() - previous_tick;
if (delay_tick < pdMS_TO_TICKS(break_time)) {
if (delay_tick < pdMS_TO_TICKS(break_time))
{
vTaskDelay(pdMS_TO_TICKS(break_time) - delay_tick);
}
previous_tick = xTaskGetTickCount();
@@ -125,20 +198,43 @@ static void socket_lock_task_func(void* param)
}
}
// -----------------------------------------------------------------------------
// Init / API pública
// -----------------------------------------------------------------------------
void socket_lock_init(void)
{
if (board_config.socket_lock) {
ESP_ERROR_CHECK(nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs));
if (!board_config.socket_lock)
return;
nvs_get_u16(nvs, NVS_OPERATING_TIME, &operating_time);
// garante storage pronto
esp_err_t se = storage_service_init();
if (se != ESP_OK)
ESP_LOGW(TAG, "storage_service_init failed: %s", esp_err_to_name(se));
nvs_get_u16(nvs, NVS_BREAK_TIME, &break_time);
// Load config (best effort; se não existir, fica default)
{
uint16_t u16 = 0;
uint8_t u8 = 0;
nvs_get_u8(nvs, NVS_RETRY_COUNT, &retry_count);
esp_err_t e = storage_get_u16_sync(NVS_NAMESPACE, NVS_OPERATING_TIME, &u16, STORE_TO);
if (e == ESP_OK) operating_time = u16;
else if (e != ESP_ERR_NOT_FOUND)
ESP_LOGW(TAG, "load %s failed: %s", NVS_OPERATING_TIME, esp_err_to_name(e));
uint8_t u8;
if (nvs_get_u8(nvs, NVS_DETECTION_HIGH, &u8) == ESP_OK) {
detection_high = u8;
e = storage_get_u16_sync(NVS_NAMESPACE, NVS_BREAK_TIME, &u16, STORE_TO);
if (e == ESP_OK) break_time = u16;
else if (e != ESP_ERR_NOT_FOUND)
ESP_LOGW(TAG, "load %s failed: %s", NVS_BREAK_TIME, esp_err_to_name(e));
e = storage_get_u8_sync(NVS_NAMESPACE, NVS_RETRY_COUNT, &u8, STORE_TO);
if (e == ESP_OK) retry_count = u8;
else if (e != ESP_ERR_NOT_FOUND)
ESP_LOGW(TAG, "load %s failed: %s", NVS_RETRY_COUNT, esp_err_to_name(e));
e = storage_get_u8_sync(NVS_NAMESPACE, NVS_DETECTION_HIGH, &u8, STORE_TO);
if (e == ESP_OK) detection_high = (u8 != 0);
else if (e != ESP_ERR_NOT_FOUND)
ESP_LOGW(TAG, "load %s failed: %s", NVS_DETECTION_HIGH, esp_err_to_name(e));
}
gpio_config_t io_conf = {};
@@ -151,8 +247,7 @@ void socket_lock_init(void)
io_conf.pin_bit_mask = BIT64(board_config.socket_lock_detection_gpio);
ESP_ERROR_CHECK(gpio_config(&io_conf));
xTaskCreate(socket_lock_task_func, "socket_lock_task", 2 * 1024, NULL, 10, &socket_lock_task);
}
xTaskCreate(socket_lock_task_func, "socket_lock_task", 2 * 1024, NULL, 2, &socket_lock_task);
}
bool socket_lock_is_detection_high(void)
@@ -164,8 +259,11 @@ void socket_lock_set_detection_high(bool _detection_high)
{
detection_high = _detection_high;
nvs_set_u8(nvs, NVS_DETECTION_HIGH, detection_high);
nvs_commit(nvs);
esp_err_t e = store_set_u8_best_effort(NVS_NAMESPACE, NVS_DETECTION_HIGH, detection_high ? 1 : 0);
if (e != ESP_OK)
ESP_LOGW(TAG, "persist detect_hi failed: %s", esp_err_to_name(e));
(void)store_flush_best_effort();
}
uint16_t socket_lock_get_operating_time(void)
@@ -175,15 +273,22 @@ uint16_t socket_lock_get_operating_time(void)
esp_err_t socket_lock_set_operating_time(uint16_t _operating_time)
{
if (_operating_time < OPERATING_TIME_MIN || _operating_time > OPERATING_TIME_MAX) {
if (_operating_time < OPERATING_TIME_MIN || _operating_time > OPERATING_TIME_MAX)
{
ESP_LOGE(TAG, "Operating time out of range");
return ESP_ERR_INVALID_ARG;
}
operating_time = _operating_time;
nvs_set_u16(nvs, NVS_OPERATING_TIME, operating_time);
nvs_commit(nvs);
esp_err_t e = store_set_u16_best_effort(NVS_NAMESPACE, NVS_OPERATING_TIME, operating_time);
if (e != ESP_OK)
{
ESP_LOGW(TAG, "persist op_time failed: %s", esp_err_to_name(e));
return e;
}
(void)store_flush_best_effort();
return ESP_OK;
}
@@ -195,8 +300,12 @@ uint8_t socket_lock_get_retry_count(void)
void socket_lock_set_retry_count(uint8_t _retry_count)
{
retry_count = _retry_count;
nvs_set_u8(nvs, NVS_RETRY_COUNT, retry_count);
nvs_commit(nvs);
esp_err_t e = store_set_u8_best_effort(NVS_NAMESPACE, NVS_RETRY_COUNT, retry_count);
if (e != ESP_OK)
ESP_LOGW(TAG, "persist retry_count failed: %s", esp_err_to_name(e));
(void)store_flush_best_effort();
}
uint16_t socket_lock_get_break_time(void)
@@ -206,15 +315,22 @@ uint16_t socket_lock_get_break_time(void)
esp_err_t socket_lock_set_break_time(uint16_t _break_time)
{
if (_break_time < board_config.socket_lock_min_break_time) {
ESP_LOGE(TAG, "Operating time out of range");
if (_break_time < board_config.socket_lock_min_break_time)
{
ESP_LOGE(TAG, "Break time out of range");
return ESP_ERR_INVALID_ARG;
}
break_time = _break_time;
nvs_set_u16(nvs, NVS_BREAK_TIME, break_time);
nvs_commit(nvs);
esp_err_t e = store_set_u16_best_effort(NVS_NAMESPACE, NVS_BREAK_TIME, break_time);
if (e != ESP_OK)
{
ESP_LOGW(TAG, "persist break_time failed: %s", esp_err_to_name(e));
return e;
}
(void)store_flush_best_effort();
return ESP_OK;
}

View File

@@ -38,7 +38,7 @@ void temp_sensor_init(void)
lm75a_init();
xTaskCreate(temp_sensor_task_func, "temp_sensor_task", 5 * 1024, NULL, 5, NULL);
xTaskCreate(temp_sensor_task_func, "temp_sensor_task", 5 * 1024, NULL, 2, NULL);
}
uint8_t temp_sensor_get_count(void)

View File

@@ -1,24 +1,22 @@
idf_component_register(
SRCS
"src/protocols.c"
"src/json.c"
"src/mqtt.c"
INCLUDE_DIRS
"include"
PRIV_INCLUDE_DIRS
"src"
PRIV_REQUIRES
nvs_flash
mqtt
cjson
vfs
spiffs
REQUIRES
logger
network
config
evse
peripherals
meter_manager
ocpp
auth
)

View File

@@ -1,38 +0,0 @@
#ifndef JSON_H_
#define JSON_H_
#include <stdbool.h>
#include "esp_err.h"
#include "cJSON.h"
/**
* @brief Gera um objeto JSON com a configuração atual do EVSE.
*
* Contém parâmetros como corrente máxima, limites de tempo,
* trava do conector, temperatura e configuração do OCPP.
*
* @return Ponteiro para cJSON (deve ser liberado com cJSON_Delete()).
*/
cJSON* json_get_evse_config(void);
/**
* @brief Define a configuração do EVSE a partir de um objeto JSON.
*
* Aplica valores recebidos de protocolos como MQTT ou REST.
*
* @param root Objeto JSON com os campos válidos.
* @return ESP_OK se todos os parâmetros foram aplicados com sucesso.
*/
esp_err_t json_set_evse_config(cJSON* root);
/**
* @brief Retorna o estado atual do EVSE em formato JSON.
*
* Inclui estado de operação, erros, limites, sessão atual e medições elétricas.
*
* @return Ponteiro para cJSON (deve ser liberado com cJSON_Delete()).
*/
cJSON* json_get_state(void);
#endif /* JSON_H_ */

View File

@@ -1,114 +1,115 @@
#ifndef MQTT_H_
#define MQTT_H_
// === Início de: components/mqtt/include/mqtt.h ===
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <stdbool.h>
#include <stdint.h>
#include "esp_err.h"
// Tamanhos máximos esperados pelos getters (buffers do chamador)
#define MQTT_SERVER_MAX_LEN 64 ///< host ou URI do broker
#define MQTT_BASE_TOPIC_MAX_LEN 64 ///< tópico base (ex: "evse")
#define MQTT_USERNAME_MAX_LEN 32
#define MQTT_PASSWORD_MAX_LEN 64
/**
* @file mqtt.h
* @brief MQTT configuration and control interface.
* @brief Inicializa o módulo MQTT.
*
* This module provides initialization, configuration,
* and runtime access functions for the MQTT client.
* - Carrega configuração da NVS
* - Regista handlers de eventos (AUTH, SCHED, LOADBALANCER, METER, EVSE, NETWORK)
* - Cria a task de telemetria periódica
*
* Configuration is persisted in NVS under namespace "mqtt".
* Não inicia a ligação ao broker imediatamente; isso acontece quando existir IP
* (NETWORK_EVENT_STA_GOT_IP) e `enabled == true`.
*/
/* -------------------------------------------------------------------------- */
/* Definitions */
/* -------------------------------------------------------------------------- */
#define MQTT_MAX_SERVER_LEN 64 /**< Max length for MQTT server URI */
#define MQTT_MAX_USER_LEN 32 /**< Max length for MQTT username */
#define MQTT_MAX_PASSWORD_LEN 64 /**< Max length for MQTT password */
#define MQTT_MAX_BASE_TOPIC_LEN 64 /**< Max length for MQTT base topic */
/* -------------------------------------------------------------------------- */
/* Public Functions */
/* -------------------------------------------------------------------------- */
esp_err_t mqtt_init(void);
/**
* @brief Initialize MQTT subsystem.
* @brief Força tentativa de ligação ao broker (normalmente não é necessário chamar).
*
* Loads configuration from NVS and starts background publish task
* if MQTT is enabled. Must be called once during system startup.
* Útil apenas se quiseres forçar manualmente depois de mudares config.
*/
void mqtt_init(void);
esp_err_t mqtt_start(void);
/**
* @brief Restart the MQTT client safely.
*
* Stops the current MQTT client (if running) and starts a new one
* with the configuration currently stored in NVS.
*
* Useful when changing Wi-Fi networks, credentials, or broker settings.
*
* @return ESP_OK on success, or an ESP_ERR_* code otherwise.
* @brief Pára o cliente MQTT e destrói o handle.
*/
esp_err_t mqtt_restart(void);
void mqtt_stop(void);
// =============================
// Getters de configuração
// =============================
/**
* @brief Set and persist MQTT configuration parameters in NVS.
*
* Any NULL parameter will be skipped (the previous value remains stored).
*
* @param enabled Whether MQTT should be enabled (true/false).
* @param server Broker URI (e.g. "mqtt://192.168.1.10").
* @param base_topic Base topic prefix for publish/subscribe.
* @param user MQTT username (optional).
* @param password MQTT password (optional).
* @param periodicity Publish interval (in seconds). Must be >0 when enabled.
*
* @return ESP_OK on success, or an ESP_ERR_* code otherwise.
*/
esp_err_t mqtt_set_config(bool enabled,
const char *server,
const char *base_topic,
const char *user,
const char *password,
uint16_t periodicity);
/**
* @brief Get whether MQTT is enabled.
*
* @return true if enabled, false otherwise.
* @brief Indica se o MQTT está ativo (flag em config).
*/
bool mqtt_get_enabled(void);
/**
* @brief Get MQTT broker URI stored in NVS.
* @brief Obtém o servidor MQTT (host ou URI).
*
* @param[out] value Buffer to receive the URI (min length: MQTT_MAX_SERVER_LEN).
* @param server buffer de saída, com pelo menos MQTT_SERVER_MAX_LEN bytes.
*/
void mqtt_get_server(char *value);
void mqtt_get_server(char *server);
/**
* @brief Get MQTT base topic stored in NVS.
* @brief Obtém o tópico base (ex.: "evse").
*
* @param[out] value Buffer to receive the base topic (min length: MQTT_MAX_BASE_TOPIC_LEN).
* @param base_topic buffer de saída, com pelo menos MQTT_BASE_TOPIC_MAX_LEN bytes.
*/
void mqtt_get_base_topic(char *value);
void mqtt_get_base_topic(char *base_topic);
/**
* @brief Get MQTT username stored in NVS.
* @brief Obtém o username MQTT (se configurado).
*
* @param[out] value Buffer to receive the username (min length: MQTT_MAX_USER_LEN).
* @param username buffer de saída, com pelo menos MQTT_USERNAME_MAX_LEN bytes.
*/
void mqtt_get_user(char *value);
void mqtt_get_user(char *username);
/**
* @brief Get MQTT password stored in NVS.
* @brief Obtém a password MQTT (se configurada).
*
* @param[out] value Buffer to receive the password (min length: MQTT_MAX_PASSWORD_LEN).
* @param password buffer de saída, com pelo menos MQTT_PASSWORD_MAX_LEN bytes.
*/
void mqtt_get_password(char *value);
void mqtt_get_password(char *password);
/**
* @brief Get MQTT publish periodicity in seconds.
*
* @return Publish interval in seconds (default: 30).
* @brief Obtém a periodicidade da telemetria periódica (em segundos).
*/
uint16_t mqtt_get_periodicity(void);
#endif /* MQTT_H_ */
// =============================
// Setter de configuração
// =============================
/**
* @brief Atualiza a configuração MQTT (tipicamente chamado pelo endpoint REST).
*
* - Grava em NVS
* - Se já existir cliente ligado, pára e recria com a nova config
* - Se `enabled == true` e houver IP, tenta ligar ao broker
*
* @param enabled true para ativar MQTT
* @param host hostname ou URI (ex: "broker.hivemq.com" ou "mqtt://x.y.z")
* @param topic tópico base (ex: "evse")
* @param username username MQTT (ou NULL para manter/limpar)
* @param password password MQTT (ou NULL para manter/limpar)
* @param periodicity período da task de telemetria em segundos (ex: 30)
*
* @return ESP_OK em caso de sucesso, ou erro de NVS / MQTT.
*/
esp_err_t mqtt_set_config(bool enabled,
const char *host,
const char *topic,
const char *username,
const char *password,
int periodicity);
#ifdef __cplusplus
}
#endif
// === Fim de: components/mqtt/include/mqtt.h ===

View File

@@ -1,204 +0,0 @@
// === Início de: components/protocols/src/json.c ===
#include <string.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "esp_wifi.h"
#include "esp_timer.h"
#include "esp_chip_info.h"
#include "esp_mac.h"
#include "esp_log.h"
#include "json.h"
#include "mqtt.h"
#include "network.h"
#include "evse_error.h"
#include "evse_api.h"
#include "auth.h"
#include "evse_limits.h"
#include "evse_state.h"
#include "evse_config.h"
#include "ocpp.h"
#include "board_config.h"
#include "socket_lock.h"
#include "proximity.h"
#include "temp_sensor.h"
#include "evse_meter.h"
static const char *TAG = "json";
//
// ===== EVSE CONFIG JSON =====
//
cJSON *json_get_evse_config(void)
{
cJSON *root = cJSON_CreateObject();
if (!root)
return NULL;
cJSON_AddNumberToObject(root, "maxChargingCurrent", evse_get_max_charging_current());
cJSON_AddNumberToObject(root, "chargingCurrent", evse_get_charging_current());
cJSON_AddNumberToObject(root, "defaultChargingCurrent", evse_get_default_charging_current());
cJSON_AddBoolToObject(root, "requireAuth", auth_get_mode());
cJSON_AddBoolToObject(root, "socketOutlet", evse_get_socket_outlet());
cJSON_AddBoolToObject(root, "rcm", evse_is_rcm());
cJSON_AddNumberToObject(root, "temperatureThreshold", evse_get_temp_threshold());
cJSON_AddNumberToObject(root, "consumptionLimit", evse_get_consumption_limit());
//cJSON_AddNumberToObject(root, "defaultConsumptionLimit", evse_get_default_consumption_limit());
cJSON_AddNumberToObject(root, "chargingTimeLimit", evse_get_charging_time_limit());
//cJSON_AddNumberToObject(root, "defaultChargingTimeLimit", evse_get_default_charging_time_limit());
cJSON_AddNumberToObject(root, "underPowerLimit", evse_get_under_power_limit());
//cJSON_AddNumberToObject(root, "defaultUnderPowerLimit", evse_get_default_under_power_limit());
cJSON_AddNumberToObject(root, "socketLockOperatingTime", socket_lock_get_operating_time());
cJSON_AddNumberToObject(root, "socketLockBreakTime", socket_lock_get_break_time());
cJSON_AddBoolToObject(root, "socketLockDetectionHigh", socket_lock_is_detection_high());
cJSON_AddNumberToObject(root, "socketLockRetryCount", socket_lock_get_retry_count());
char str[64];
cJSON_AddBoolToObject(root, "enabledocpp", ocpp_get_enabled());
ocpp_get_server(str);
cJSON_AddStringToObject(root, "serverocpp", str);
return root;
}
//
// ===== SET EVSE CONFIG (from MQTT or REST) =====
//
esp_err_t json_set_evse_config(cJSON *root)
{
if (!root)
return ESP_ERR_INVALID_ARG;
// Alguns setters retornam esp_err_t, outros void. Para manter compatibilidade,
// chamamos sem propagar erro (se existir, será tratado internamente).
#define SET_NUM(key, fn) \
do \
{ \
const cJSON *item = cJSON_GetObjectItem(root, key); \
if (cJSON_IsNumber(item)) \
{ \
fn(item->valuedouble); \
} \
} while (0)
#define SET_BOOL(key, fn) \
do \
{ \
const cJSON *item = cJSON_GetObjectItem(root, key); \
if (cJSON_IsBool(item)) \
{ \
fn(cJSON_IsTrue(item)); \
} \
} while (0)
SET_NUM("maxChargingCurrent", evse_set_max_charging_current);
SET_NUM("chargingCurrent", evse_set_charging_current);
SET_NUM("defaultChargingCurrent", evse_set_default_charging_current);
SET_BOOL("socketOutlet", evse_set_socket_outlet);
SET_BOOL("rcm", evse_set_rcm);
SET_NUM("temperatureThreshold", evse_set_temp_threshold);
SET_NUM("consumptionLimit", evse_set_consumption_limit);
//SET_NUM("defaultConsumptionLimit", evse_set_default_consumption_limit);
SET_NUM("chargingTimeLimit", evse_set_charging_time_limit);
//SET_NUM("defaultChargingTimeLimit", evse_set_default_charging_time_limit);
SET_NUM("underPowerLimit", evse_set_under_power_limit);
//SET_NUM("defaultUnderPowerLimit", evse_set_default_under_power_limit);
SET_NUM("socketLockOperatingTime", socket_lock_set_operating_time);
SET_NUM("socketLockBreakTime", socket_lock_set_break_time);
const cJSON *retry = cJSON_GetObjectItem(root, "socketLockRetryCount");
if (cJSON_IsNumber(retry))
socket_lock_set_retry_count(retry->valueint);
const cJSON *detect = cJSON_GetObjectItem(root, "socketLockDetectionHigh");
if (cJSON_IsBool(detect))
socket_lock_set_detection_high(cJSON_IsTrue(detect));
const cJSON *ocpp_enabled = cJSON_GetObjectItem(root, "enabledocpp");
if (cJSON_IsBool(ocpp_enabled))
ocpp_set_enabled(cJSON_IsTrue(ocpp_enabled));
const cJSON *ocpp_server = cJSON_GetObjectItem(root, "serverocpp");
if (cJSON_IsString(ocpp_server))
ocpp_set_server(cJSON_GetStringValue(ocpp_server));
ESP_LOGI(TAG, "EVSE configuration updated successfully");
return ESP_OK;
}
//
// ===== EVSE STATE JSON =====
//
cJSON *json_get_state(void)
{
cJSON *root = cJSON_CreateObject();
if (!root)
return NULL;
cJSON_AddStringToObject(root, "state", evse_state_to_str(evse_get_state()));
cJSON_AddBoolToObject(root, "available", evse_config_is_available());
cJSON_AddBoolToObject(root, "enabled", evse_config_is_enabled());
cJSON_AddBoolToObject(root, "pendingAuth", auth_get_mode());
cJSON_AddBoolToObject(root, "limitReached", evse_is_limit_reached());
// Add error list
uint32_t error = evse_error_get_bits();
if (error == 0)
{
cJSON_AddNullToObject(root, "errors");
}
else
{
cJSON *errors = cJSON_CreateArray();
if (error & EVSE_ERR_PILOT_FAULT_BIT)
cJSON_AddItemToArray(errors, cJSON_CreateString("pilot_fault"));
if (error & EVSE_ERR_DIODE_SHORT_BIT)
cJSON_AddItemToArray(errors, cJSON_CreateString("diode_short"));
if (error & EVSE_ERR_LOCK_FAULT_BIT)
cJSON_AddItemToArray(errors, cJSON_CreateString("lock_fault"));
if (error & EVSE_ERR_UNLOCK_FAULT_BIT)
cJSON_AddItemToArray(errors, cJSON_CreateString("unlock_fault"));
if (error & EVSE_ERR_RCM_TRIGGERED_BIT)
cJSON_AddItemToArray(errors, cJSON_CreateString("rcm_triggered"));
if (error & EVSE_ERR_RCM_SELFTEST_FAULT_BIT)
cJSON_AddItemToArray(errors, cJSON_CreateString("rcm_selftest_fault"));
if (error & EVSE_ERR_TEMPERATURE_HIGH_BIT)
cJSON_AddItemToArray(errors, cJSON_CreateString("temperature_high"));
if (error & EVSE_ERR_TEMPERATURE_FAULT_BIT)
cJSON_AddItemToArray(errors, cJSON_CreateString("temperature_fault"));
cJSON_AddItemToObject(root, "errors", errors);
}
// Session info
evse_session_t sess;
if (evse_get_session(&sess))
{
cJSON_AddNumberToObject(root, "sessionTime", (double)sess.start_tick);
cJSON_AddNumberToObject(root, "chargingTime", (double)sess.duration_s);
cJSON_AddNumberToObject(root, "consumption", (double)sess.energy_wh);
}
else
{
cJSON_AddNullToObject(root, "sessionTime");
cJSON_AddNumberToObject(root, "chargingTime", 0);
cJSON_AddNumberToObject(root, "consumption", 0);
}
// Meter readings
float voltage[EVSE_METER_PHASE_COUNT];
float current[EVSE_METER_PHASE_COUNT];
int power[EVSE_METER_PHASE_COUNT];
evse_meter_get_voltage(voltage);
evse_meter_get_current(current);
evse_meter_get_power(power);
cJSON_AddItemToObject(root, "power", cJSON_CreateIntArray(power, EVSE_METER_PHASE_COUNT));
cJSON_AddItemToObject(root, "voltage", cJSON_CreateFloatArray(voltage, EVSE_METER_PHASE_COUNT));
cJSON_AddItemToObject(root, "current", cJSON_CreateFloatArray(current, EVSE_METER_PHASE_COUNT));
return root;
}
// === Fim de: components/protocols/src/json.c ===

File diff suppressed because it is too large Load Diff

View File

@@ -25,7 +25,7 @@ static esp_err_t dashboard_get_handler(httpd_req_t *req) {
cJSON_AddNumberToObject(charger1, "id", 1);
cJSON_AddStringToObject(charger1, "status", evse_state_to_str(state));
cJSON_AddNumberToObject(charger1, "current", evse_get_runtime_charging_current());
cJSON_AddNumberToObject(charger1, "maxCurrent", evse_get_max_charging_current());
cJSON_AddNumberToObject(charger1, "maxCurrent", evse_get_charging_current());
// Calcular a potência com base na corrente (considerando 230V)
int power = (evse_get_runtime_charging_current()) * 230;

View File

@@ -11,7 +11,7 @@ static esp_err_t link_config_get_handler(httpd_req_t *req) {
uint8_t mode = evse_link_get_mode(); // 0=MASTER,1=SLAVE
uint8_t self_id = evse_link_get_self_id();
ESP_LOGI(TAG, "GET link config: enabled=%d mode=%u id=%u",
ESP_LOGD(TAG, "GET link config: enabled=%d mode=%u id=%u",
enabled, mode, self_id);
httpd_resp_set_type(req, "application/json");
@@ -23,7 +23,7 @@ static esp_err_t link_config_get_handler(httpd_req_t *req) {
char *s = cJSON_Print(root);
httpd_resp_sendstr(req, s);
ESP_LOGI(TAG, " payload: %s", s);
ESP_LOGD(TAG, " payload: %s", s);
free(s);
cJSON_Delete(root);
return ESP_OK;
@@ -38,7 +38,7 @@ static esp_err_t link_config_post_handler(httpd_req_t *req) {
return ESP_FAIL;
}
buf[len] = '\0';
ESP_LOGI(TAG, "POST link config: %s", buf);
ESP_LOGD(TAG, "POST link config: %s", buf);
cJSON *json = cJSON_Parse(buf);
if (!json) {
@@ -50,7 +50,7 @@ static esp_err_t link_config_post_handler(httpd_req_t *req) {
cJSON *j_en = cJSON_GetObjectItem(json, "linkEnabled");
if (j_en && cJSON_IsBool(j_en)) {
evse_link_set_enabled(cJSON_IsTrue(j_en));
ESP_LOGI(TAG, " set enabled = %d", cJSON_IsTrue(j_en));
ESP_LOGD(TAG, " set enabled = %d", cJSON_IsTrue(j_en));
}
// linkMode
@@ -67,7 +67,7 @@ static esp_err_t link_config_post_handler(httpd_req_t *req) {
"Invalid linkMode (must be MASTER or SLAVE)");
return ESP_FAIL;
}
ESP_LOGI(TAG, " set mode = %s", m);
ESP_LOGD(TAG, " set mode = %s", m);
}
// linkSelfId
@@ -81,7 +81,7 @@ static esp_err_t link_config_post_handler(httpd_req_t *req) {
return ESP_FAIL;
}
evse_link_set_self_id((uint8_t)id);
ESP_LOGI(TAG, " set self_id = %d", id);
ESP_LOGD(TAG, " set self_id = %d", id);
}
cJSON_Delete(json);

View File

@@ -12,7 +12,7 @@ static const char *TAG = "evse_settings_api";
static esp_err_t config_settings_get_handler(httpd_req_t *req) {
httpd_resp_set_type(req, "application/json");
cJSON *config = cJSON_CreateObject();
cJSON_AddNumberToObject(config, "currentLimit", evse_get_max_charging_current());
cJSON_AddNumberToObject(config, "currentLimit", evse_get_charging_current());
cJSON_AddNumberToObject(config, "temperatureLimit", evse_get_temp_threshold());
const char *json_str = cJSON_Print(config);
httpd_resp_sendstr(req, json_str);
@@ -36,7 +36,7 @@ static esp_err_t config_settings_post_handler(httpd_req_t *req) {
}
cJSON *current = cJSON_GetObjectItem(json, "currentLimit");
if (current) evse_set_max_charging_current(current->valueint);
if (current) evse_set_charging_current(current->valueint);
cJSON *temp = cJSON_GetObjectItem(json, "temperatureLimit");
if (temp) evse_set_temp_threshold(temp->valueint);

View File

@@ -1,108 +1,216 @@
#include "loadbalancing_settings_api.h"
#include "loadbalancer.h"
#include "esp_log.h"
#include "esp_err.h"
#include "esp_http_server.h"
#include "cJSON.h"
#include <stdlib.h>
#include <string.h>
static const char *TAG = "loadbalancing_settings_api";
// GET Handler: Retorna configurações atuais de load balancing
static esp_err_t loadbalancing_config_get_handler(httpd_req_t *req) {
bool enabled = loadbalancer_is_enabled();
uint8_t currentLimit = load_balancing_get_max_grid_current();
// limites simples
#define MIN_GRID_A 6
#define MAX_GRID_A 100
ESP_LOGI(TAG, "Fetching load balancing settings: enabled = %d, currentLimit = %u", enabled, currentLimit);
#define MIN_PV_W 0
#define MAX_PV_W 100000 // ajusta se quiseres
static esp_err_t send_json(httpd_req_t *req, cJSON *root)
{
httpd_resp_set_type(req, "application/json");
cJSON *config = cJSON_CreateObject();
cJSON_AddBoolToObject(config, "loadBalancingEnabled", enabled);
cJSON_AddNumberToObject(config, "loadBalancingCurrentLimit", currentLimit);
char *json_str = cJSON_PrintUnformatted(root);
if (!json_str)
{
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "JSON encode failed");
return ESP_FAIL;
}
const char *json_str = cJSON_Print(config);
httpd_resp_sendstr(req, json_str);
esp_err_t err = httpd_resp_sendstr(req, json_str);
free(json_str);
return err;
}
ESP_LOGI(TAG, "Returned config: %s", json_str);
// GET -> payload novo
static esp_err_t loadbalancing_config_get_handler(httpd_req_t *req)
{
cJSON *root = cJSON_CreateObject();
if (!root)
{
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "No mem");
return ESP_FAIL;
}
free((void *)json_str);
cJSON_Delete(config);
cJSON_AddBoolToObject(root, "enabled", loadbalancer_is_enabled());
cJSON *grid = cJSON_CreateObject();
cJSON_AddItemToObject(root, "gridLimit", grid);
cJSON_AddBoolToObject(grid, "enabled", loadbalancer_grid_is_enabled());
cJSON_AddNumberToObject(grid, "maxImportA", loadbalancer_grid_get_max_import_a());
cJSON *pv = cJSON_CreateObject();
cJSON_AddItemToObject(root, "pv", pv);
cJSON_AddBoolToObject(pv, "enabled", loadbalancer_pv_is_enabled());
cJSON_AddNumberToObject(pv, "maxImportW", loadbalancer_pv_get_max_import_w());
esp_err_t err = send_json(req, root);
cJSON_Delete(root);
return err;
}
// lê body de forma robusta (httpd_req_recv pode devolver parcial)
static esp_err_t read_body(httpd_req_t *req, char *buf, size_t buf_sz)
{
if (req->content_len <= 0)
return ESP_FAIL;
if ((size_t)req->content_len >= buf_sz)
return ESP_ERR_NO_MEM;
int remaining = req->content_len;
int off = 0;
while (remaining > 0)
{
int r = httpd_req_recv(req, buf + off, remaining);
if (r <= 0)
return ESP_FAIL;
off += r;
remaining -= r;
}
buf[off] = '\0';
return ESP_OK;
}
// POST Handler: Atualiza configurações de load balancing
static esp_err_t loadbalancing_config_post_handler(httpd_req_t *req) {
char buf[512];
int len = httpd_req_recv(req, buf, sizeof(buf) - 1);
if (len <= 0) {
ESP_LOGE(TAG, "Received empty POST body");
// POST -> updates parciais aceites
static esp_err_t loadbalancing_config_post_handler(httpd_req_t *req)
{
if (req->content_len <= 0)
{
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Empty body");
return ESP_FAIL;
}
buf[len] = '\0';
ESP_LOGI(TAG, "Received POST data: %s", buf);
if (req->content_len >= 512)
{
httpd_resp_send_err(req, HTTPD_413_CONTENT_TOO_LARGE, "Body too large");
return ESP_FAIL;
}
char buf[512];
esp_err_t rb = read_body(req, buf, sizeof(buf));
if (rb == ESP_ERR_NO_MEM)
{
httpd_resp_send_err(req, HTTPD_413_CONTENT_TOO_LARGE, "Body too large");
return ESP_FAIL;
}
if (rb != ESP_OK)
{
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Failed to read body");
return ESP_FAIL;
}
ESP_LOGD(TAG, "POST: %s", buf);
cJSON *json = cJSON_Parse(buf);
if (!json) {
ESP_LOGE(TAG, "Invalid JSON");
if (!json)
{
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON");
return ESP_FAIL;
}
// Atualizar estado habilitado
cJSON *enabled_item = cJSON_GetObjectItem(json, "loadBalancingEnabled");
if (enabled_item && cJSON_IsBool(enabled_item)) {
bool isEnabled = cJSON_IsTrue(enabled_item);
loadbalancer_set_enabled(isEnabled);
ESP_LOGI(TAG, "Updated loadBalancingEnabled to: %d", isEnabled);
// enabled (top-level)
cJSON *enabled_item = cJSON_GetObjectItem(json, "enabled");
if (enabled_item && cJSON_IsBool(enabled_item))
{
loadbalancer_set_enabled(cJSON_IsTrue(enabled_item));
}
// Atualizar limite de corrente
cJSON *limit_item = cJSON_GetObjectItem(json, "loadBalancingCurrentLimit");
if (limit_item && cJSON_IsNumber(limit_item)) {
uint8_t currentLimit = (uint8_t)limit_item->valuedouble;
// gridLimit
cJSON *grid = cJSON_GetObjectItem(json, "gridLimit");
if (grid && cJSON_IsObject(grid))
{
cJSON *g_en = cJSON_GetObjectItem(grid, "enabled");
if (g_en && cJSON_IsBool(g_en))
{
loadbalancer_grid_set_enabled(cJSON_IsTrue(g_en));
}
// Validar intervalo
if (currentLimit < 6 || currentLimit > 100) {
ESP_LOGW(TAG, "Rejected invalid currentLimit: %d", currentLimit);
cJSON *g_maxA = cJSON_GetObjectItem(grid, "maxImportA");
if (g_maxA && cJSON_IsNumber(g_maxA))
{
int maxA = (int)g_maxA->valuedouble;
if (maxA < MIN_GRID_A || maxA > MAX_GRID_A)
{
cJSON_Delete(json);
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid currentLimit (must be 6-100)");
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "gridLimit.maxImportA must be 6-100");
return ESP_FAIL;
}
esp_err_t err = load_balancing_set_max_grid_current(currentLimit);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to save currentLimit: %s", esp_err_to_name(err));
esp_err_t e = loadbalancer_grid_set_max_import_a((uint8_t)maxA);
if (e != ESP_OK)
{
cJSON_Delete(json);
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to save setting");
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to set gridLimit.maxImportA");
return ESP_FAIL;
}
}
}
// pv
cJSON *pv = cJSON_GetObjectItem(json, "pv");
if (pv && cJSON_IsObject(pv))
{
cJSON *p_en = cJSON_GetObjectItem(pv, "enabled");
if (p_en && cJSON_IsBool(p_en))
{
loadbalancer_pv_set_enabled(cJSON_IsTrue(p_en));
}
cJSON *p_maxW = cJSON_GetObjectItem(pv, "maxImportW");
if (p_maxW && cJSON_IsNumber(p_maxW))
{
int32_t maxW = (int32_t)p_maxW->valuedouble;
if (maxW < MIN_PV_W || maxW > MAX_PV_W)
{
cJSON_Delete(json);
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "pv.maxImportW out of range");
return ESP_FAIL;
}
ESP_LOGI(TAG, "Updated loadBalancingCurrentLimit to: %d", currentLimit);
esp_err_t e = loadbalancer_pv_set_max_import_w(maxW);
if (e != ESP_OK)
{
cJSON_Delete(json);
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to set pv.maxImportW");
return ESP_FAIL;
}
}
}
cJSON_Delete(json);
httpd_resp_sendstr(req, "Load balancing settings updated successfully");
httpd_resp_sendstr(req, "OK");
return ESP_OK;
}
// Registro dos handlers na API HTTP
void register_loadbalancing_settings_handlers(httpd_handle_t server, void *ctx) {
// GET
void register_loadbalancing_settings_handlers(httpd_handle_t server, void *ctx)
{
httpd_uri_t get_uri = {
.uri = "/api/v1/config/loadbalancing",
.method = HTTP_GET,
.handler = loadbalancing_config_get_handler,
.user_ctx = ctx
};
.user_ctx = ctx};
httpd_register_uri_handler(server, &get_uri);
// POST
httpd_uri_t post_uri = {
.uri = "/api/v1/config/loadbalancing",
.method = HTTP_POST,
.handler = loadbalancing_config_post_handler,
.user_ctx = ctx
};
.user_ctx = ctx};
httpd_register_uri_handler(server, &post_uri);
}

View File

@@ -158,13 +158,13 @@ void register_meters_data_handlers(httpd_handle_t server, void *ctx)
.user_ctx = ctx};
httpd_register_uri_handler(server, &live);
ESP_LOGI(TAG, "REST /api/v1/meters/live registrado");
ESP_LOGD(TAG, "REST /api/v1/meters/live registrado");
}
// Função para recuperar as configurações dos contadores
static esp_err_t meters_config_get_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "Received GET request for /api/v1/config/meters");
ESP_LOGD(TAG, "Received GET request for /api/v1/config/meters");
httpd_resp_set_type(req, "application/json");
@@ -174,8 +174,8 @@ static esp_err_t meters_config_get_handler(httpd_req_t *req)
meter_type_t gridmeterType = meter_manager_grid_get_model();
meter_type_t evsemeterType = meter_manager_evse_get_model();
ESP_LOGI(TAG, "Grid meter type: %s", meter_type_to_str(gridmeterType));
ESP_LOGI(TAG, "EVSE meter type: %s", meter_type_to_str(evsemeterType));
ESP_LOGD(TAG, "Grid meter type: %s", meter_type_to_str(gridmeterType));
ESP_LOGD(TAG, "EVSE meter type: %s", meter_type_to_str(evsemeterType));
// Adicionando os tipos de contadores ao objeto JSON
cJSON_AddStringToObject(config, "gridmeter", meter_type_to_str(gridmeterType));
@@ -183,7 +183,7 @@ static esp_err_t meters_config_get_handler(httpd_req_t *req)
// Convertendo o objeto JSON para uma string
const char *json_str = cJSON_Print(config);
ESP_LOGI(TAG, "Returning meters config: %s", json_str);
ESP_LOGD(TAG, "Returning meters config: %s", json_str);
httpd_resp_sendstr(req, json_str);
@@ -197,7 +197,7 @@ static esp_err_t meters_config_get_handler(httpd_req_t *req)
// Função para atualizar as configurações dos contadores
static esp_err_t meters_config_post_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "Received POST request for /api/v1/config/meters");
ESP_LOGD(TAG, "Received POST request for /api/v1/config/meters");
char buf[512];
int len = httpd_req_recv(req, buf, sizeof(buf) - 1);
@@ -211,7 +211,7 @@ static esp_err_t meters_config_post_handler(httpd_req_t *req)
buf[len] = '\0'; // Garantir que a string está terminada
ESP_LOGI(TAG, "Received POST data: %s", buf);
ESP_LOGD(TAG, "Received POST data: %s", buf);
cJSON *json = cJSON_Parse(buf);
if (!json)
@@ -227,7 +227,7 @@ static esp_err_t meters_config_post_handler(httpd_req_t *req)
if (gridmeter)
{
meter_type_t gridType = string_to_meter_type(gridmeter->valuestring); // Usando a função string_to_meter_type
ESP_LOGI(TAG, "Updating grid meter type to: %s", gridmeter->valuestring);
ESP_LOGD(TAG, "Updating grid meter type to: %s", gridmeter->valuestring);
meter_manager_grid_set_model(gridType);
}
@@ -235,14 +235,14 @@ static esp_err_t meters_config_post_handler(httpd_req_t *req)
if (evsemeter)
{
meter_type_t evseType = string_to_meter_type(evsemeter->valuestring); // Usando a função string_to_meter_type
ESP_LOGI(TAG, "Updating EVSE meter type to: %s", evsemeter->valuestring);
ESP_LOGD(TAG, "Updating EVSE meter type to: %s", evsemeter->valuestring);
meter_manager_evse_set_model(evseType);
}
cJSON_Delete(json);
httpd_resp_sendstr(req, "Meters updated successfully");
ESP_LOGI(TAG, "Meters configuration updated successfully");
ESP_LOGD(TAG, "Meters configuration updated successfully");
return ESP_OK;
}

View File

@@ -8,29 +8,33 @@
#include "network.h"
#include "mqtt.h"
#include <string.h>
#include <stdlib.h>
static const char *TAG = "network_api";
typedef struct {
typedef struct
{
bool enabled;
char ssid[33];
char password[65];
} wifi_task_data_t;
static void wifi_apply_config_task(void *param) {
static void wifi_apply_config_task(void *param)
{
wifi_task_data_t *data = (wifi_task_data_t *)param;
ESP_LOGI("wifi_task", "Applying Wi-Fi config in background task");
ESP_LOGD("wifi_task", "Applying Wi-Fi config in background task");
wifi_set_config(data->enabled, data->ssid, data->password);
free(data);
vTaskDelete(NULL);
}
static esp_err_t wifi_get_handler(httpd_req_t *req) {
ESP_LOGI(TAG, "Handling GET /api/v1/config/wifi");
static esp_err_t wifi_get_handler(httpd_req_t *req)
{
ESP_LOGD(TAG, "Handling GET /api/v1/config/wifi");
httpd_resp_set_type(req, "application/json");
// Obter dados da NVS via wifi.c
bool enabled = wifi_get_enabled();
char ssid[33] = {0};
char password[65] = {0};
@@ -38,81 +42,85 @@ static esp_err_t wifi_get_handler(httpd_req_t *req) {
wifi_get_ssid(ssid);
wifi_get_password(password);
// Criar JSON
cJSON *json = cJSON_CreateObject();
cJSON_AddBoolToObject(json, "enabled", enabled);
cJSON_AddStringToObject(json, "ssid", ssid);
cJSON_AddStringToObject(json, "password", password);
// Enviar resposta
char *response = cJSON_Print(json);
httpd_resp_sendstr(req, response);
// Limpeza
free(response);
cJSON_Delete(json);
return ESP_OK;
}
static esp_err_t wifi_post_handler(httpd_req_t *req) {
ESP_LOGI(TAG, "Handling POST /api/v1/config/wifi");
static esp_err_t wifi_post_handler(httpd_req_t *req)
{
ESP_LOGD(TAG, "Handling POST /api/v1/config/wifi");
char buf[512];
int len = httpd_req_recv(req, buf, sizeof(buf) - 1);
if (len <= 0) return ESP_FAIL;
if (len <= 0)
return ESP_FAIL;
buf[len] = '\0';
cJSON *json = cJSON_Parse(buf);
if (!json) return ESP_FAIL;
if (!json)
return ESP_FAIL;
// Valores padrão
bool enabled = false;
const char *ssid = NULL;
const char *password = NULL;
cJSON *j_enabled = cJSON_GetObjectItem(json, "enabled");
if (cJSON_IsBool(j_enabled)) enabled = j_enabled->valueint;
if (cJSON_IsBool(j_enabled))
enabled = j_enabled->valueint;
cJSON *j_ssid = cJSON_GetObjectItem(json, "ssid");
if (cJSON_IsString(j_ssid)) ssid = j_ssid->valuestring;
if (cJSON_IsString(j_ssid))
ssid = j_ssid->valuestring;
cJSON *j_password = cJSON_GetObjectItem(json, "password");
if (cJSON_IsString(j_password)) password = j_password->valuestring;
if (cJSON_IsString(j_password))
password = j_password->valuestring;
// Enviar resposta antes de alterar Wi-Fi
// Resposta imediata
httpd_resp_sendstr(req, "Wi-Fi config atualizada com sucesso");
// Alocar struct para passar para a task
wifi_task_data_t *task_data = malloc(sizeof(wifi_task_data_t));
if (!task_data) {
wifi_task_data_t *task_data = (wifi_task_data_t *)malloc(sizeof(wifi_task_data_t));
if (!task_data)
{
cJSON_Delete(json);
ESP_LOGE(TAG, "Memory allocation failed for Wi-Fi task");
return ESP_ERR_NO_MEM;
}
task_data->enabled = enabled;
strncpy(task_data->ssid, ssid ? ssid : "", sizeof(task_data->ssid));
strncpy(task_data->password, password ? password : "", sizeof(task_data->password));
// Criar task normal com função C
// Copias seguras (garante null-termination)
strncpy(task_data->ssid, ssid ? ssid : "", sizeof(task_data->ssid) - 1);
task_data->ssid[sizeof(task_data->ssid) - 1] = '\0';
strncpy(task_data->password, password ? password : "", sizeof(task_data->password) - 1);
task_data->password[sizeof(task_data->password) - 1] = '\0';
xTaskCreate(
wifi_apply_config_task,
"wifi_config_task",
4096,
task_data,
3,
NULL
);
NULL);
cJSON_Delete(json);
return ESP_OK;
}
static esp_err_t config_mqtt_get_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "Handling GET /api/v1/config/mqtt");
ESP_LOGD(TAG, "Handling GET /api/v1/config/mqtt");
httpd_resp_set_type(req, "application/json");
@@ -128,85 +136,128 @@ static esp_err_t config_mqtt_get_handler(httpd_req_t *req)
mqtt_get_user(username);
mqtt_get_password(password);
ESP_LOGI(TAG, "MQTT Config:");
ESP_LOGI(TAG, " Enabled: %s", enabled ? "true" : "false");
ESP_LOGI(TAG, " Server: %s", server);
ESP_LOGI(TAG, " Topic: %s", base_topic);
ESP_LOGI(TAG, " Username: %s", username);
ESP_LOGI(TAG, " Password: %s", password);
ESP_LOGI(TAG, " Periodicity: %d", periodicity);
ESP_LOGD(TAG, "MQTT Config:");
ESP_LOGD(TAG, " Enabled: %s", enabled ? "true" : "false");
ESP_LOGD(TAG, " Server: %s", server);
ESP_LOGD(TAG, " Topic: %s", base_topic);
ESP_LOGD(TAG, " Username: %s", username);
ESP_LOGD(TAG, " Password: %s", password);
ESP_LOGD(TAG, " Periodicity: %d", periodicity);
cJSON *config = cJSON_CreateObject();
cJSON_AddBoolToObject(config, "enabled", enabled);
cJSON_AddStringToObject(config, "host", server);
cJSON_AddNumberToObject(config, "port", 1883);
cJSON_AddNumberToObject(config, "port", 1883); // fixo (se não usas no mqtt_set_config)
cJSON_AddStringToObject(config, "username", username);
cJSON_AddStringToObject(config, "password", password);
cJSON_AddStringToObject(config, "topic", base_topic);
cJSON_AddNumberToObject(config, "periodicity", periodicity);
const char *config_str = cJSON_Print(config);
char *config_str = cJSON_Print(config);
httpd_resp_sendstr(req, config_str);
free((void *)config_str);
free(config_str);
cJSON_Delete(config);
return ESP_OK;
}
static esp_err_t config_mqtt_post_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "Handling POST /api/v1/config/mqtt");
ESP_LOGD(TAG, "Handling POST /api/v1/config/mqtt");
char buf[512];
int len = httpd_req_recv(req, buf, sizeof(buf) - 1);
if (len <= 0) {
if (len <= 0)
{
ESP_LOGE(TAG, "Failed to read request body");
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid request body");
return ESP_FAIL;
}
buf[len] = '\0';
ESP_LOGI(TAG, "Received JSON: %s", buf);
ESP_LOGD(TAG, "Received JSON: %s", buf);
cJSON *json = cJSON_Parse(buf);
if (!json) {
if (!json)
{
ESP_LOGE(TAG, "Invalid JSON format");
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON");
return ESP_FAIL;
}
bool enabled = false;
const char *host = NULL, *topic = NULL, *username = NULL, *password = NULL;
int periodicity = 30;
// --- Ler config atual (para permitir "partial update" e evitar strings vazias)
bool current_enabled = mqtt_get_enabled();
char current_host[64] = {0};
char current_topic[32] = {0};
char current_user[32] = {0};
char current_pass[64] = {0};
uint16_t current_periodicity = mqtt_get_periodicity();
if (cJSON_IsBool(cJSON_GetObjectItem(json, "enabled")))
enabled = cJSON_GetObjectItem(json, "enabled")->valueint;
mqtt_get_server(current_host);
mqtt_get_base_topic(current_topic);
mqtt_get_user(current_user);
mqtt_get_password(current_pass);
bool enabled = current_enabled;
const char *host_in = NULL, *topic_in = NULL, *user_in = NULL, *pass_in = NULL;
int periodicity = (int)current_periodicity;
cJSON *j_enabled = cJSON_GetObjectItem(json, "enabled");
if (cJSON_IsBool(j_enabled))
enabled = j_enabled->valueint;
cJSON *j_host = cJSON_GetObjectItem(json, "host");
if (cJSON_IsString(j_host)) host = j_host->valuestring;
if (cJSON_IsString(j_host))
host_in = j_host->valuestring;
cJSON *j_topic = cJSON_GetObjectItem(json, "topic");
if (cJSON_IsString(j_topic)) topic = j_topic->valuestring;
if (cJSON_IsString(j_topic))
topic_in = j_topic->valuestring;
cJSON *j_user = cJSON_GetObjectItem(json, "username");
if (cJSON_IsString(j_user)) username = j_user->valuestring;
if (cJSON_IsString(j_user))
user_in = j_user->valuestring;
cJSON *j_pass = cJSON_GetObjectItem(json, "password");
if (cJSON_IsString(j_pass)) password = j_pass->valuestring;
if (cJSON_IsString(j_pass))
pass_in = j_pass->valuestring;
cJSON *j_periodicity = cJSON_GetObjectItem(json, "periodicity");
if (cJSON_IsNumber(j_periodicity)) periodicity = j_periodicity->valueint;
if (cJSON_IsNumber(j_periodicity))
periodicity = j_periodicity->valueint;
ESP_LOGI(TAG, "Applying MQTT config:");
ESP_LOGI(TAG, " Enabled: %s", enabled ? "true" : "false");
ESP_LOGI(TAG, " Host: %s", host);
ESP_LOGI(TAG, " Topic: %s", topic);
ESP_LOGI(TAG, " Username: %s", username);
ESP_LOGI(TAG, " Password: %s", password);
ESP_LOGI(TAG, " Periodicity: %d", periodicity);
// --- Regras: se vier NULL ou "" mantém o atual; se atual também estiver vazio, usa default
const char *host =
(host_in && host_in[0] != '\0') ? host_in : (current_host[0] != '\0') ? current_host
: "mqtt.plixin.com";
const char *topic =
(topic_in && topic_in[0] != '\0') ? topic_in : (current_topic[0] != '\0') ? current_topic
: "";
const char *username =
(user_in && user_in[0] != '\0') ? user_in : (current_user[0] != '\0') ? current_user
: "";
const char *password =
(pass_in && pass_in[0] != '\0') ? pass_in : (current_pass[0] != '\0') ? current_pass
: "";
if (periodicity <= 0)
periodicity = (int)current_periodicity;
if (periodicity <= 0)
periodicity = 30;
ESP_LOGD(TAG, "Applying MQTT config:");
ESP_LOGD(TAG, " Enabled: %s", enabled ? "true" : "false");
ESP_LOGD(TAG, " Host: %s", host);
ESP_LOGD(TAG, " Topic: %s", topic);
ESP_LOGD(TAG, " Username: %s", username);
ESP_LOGD(TAG, " Password: %s", password);
ESP_LOGD(TAG, " Periodicity: %d", periodicity);
esp_err_t err = mqtt_set_config(enabled, host, topic, username, password, periodicity);
if (err != ESP_OK) {
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to apply MQTT config (code %d)", err);
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to apply config");
cJSON_Delete(json);
@@ -218,40 +269,33 @@ static esp_err_t config_mqtt_post_handler(httpd_req_t *req)
return ESP_OK;
}
void register_network_handlers(httpd_handle_t server, void *ctx) {
void register_network_handlers(httpd_handle_t server, void *ctx)
{
httpd_uri_t wifi_get = {
.uri = "/api/v1/config/wifi",
.method = HTTP_GET,
.handler = wifi_get_handler,
.user_ctx = ctx
};
.user_ctx = ctx};
httpd_register_uri_handler(server, &wifi_get);
httpd_uri_t wifi_post = {
.uri = "/api/v1/config/wifi",
.method = HTTP_POST,
.handler = wifi_post_handler,
.user_ctx = ctx
};
.user_ctx = ctx};
httpd_register_uri_handler(server, &wifi_post);
// URI handler for getting MQTT config
httpd_uri_t config_mqtt_get_uri = {
.uri = "/api/v1/config/mqtt",
.method = HTTP_GET,
.handler = config_mqtt_get_handler,
.user_ctx = ctx
};
.user_ctx = ctx};
httpd_register_uri_handler(server, &config_mqtt_get_uri);
// URI handler for posting MQTT config
httpd_uri_t config_mqtt_post_uri = {
.uri = "/api/v1/config/mqtt",
.method = HTTP_POST,
.handler = config_mqtt_post_handler,
.user_ctx = ctx
};
.user_ctx = ctx};
httpd_register_uri_handler(server, &config_mqtt_post_uri);
}

View File

@@ -11,6 +11,9 @@
#include "static_file_api.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
static const char *TAG = "rest_main";
esp_err_t rest_server_init(const char *base_path)
@@ -30,6 +33,13 @@ esp_err_t rest_server_init(const char *base_path)
config.uri_match_fn = httpd_uri_match_wildcard;
config.max_uri_handlers = 32;
// Aumenta stack do httpd (handlers + cJSON + etc)
config.stack_size = 8192; // tenta 8192; se necessário 12288
config.task_priority = tskIDLE_PRIORITY + 5;
// Opcional (ajuda com muitas conexões/keep-alive)
config.lru_purge_enable = true;
httpd_handle_t server = NULL;
esp_err_t err = httpd_start(&server, &config);
if (err != ESP_OK)
@@ -41,21 +51,19 @@ esp_err_t rest_server_init(const char *base_path)
ESP_LOGI(TAG, "HTTP server started successfully");
// Register endpoint groups
register_evse_settings_handlers(server, ctx); // Apenas chamando a função sem comparação
register_network_handlers(server, ctx); // Apenas chamando a função sem comparação
register_ocpp_handlers(server, ctx); // Apenas chamando a função sem comparação
register_auth_handlers(server, ctx); // Apenas chamando a função sem comparação
register_dashboard_handlers(server, ctx); // Apenas chamando a função sem comparação
register_meters_settings_handlers(server, ctx); // Apenas chamando a função sem comparação
register_loadbalancing_settings_handlers(server, ctx); // Apenas chamando a função sem comparação
// Registar handlers
register_evse_settings_handlers(server, ctx);
register_network_handlers(server, ctx);
register_ocpp_handlers(server, ctx);
register_auth_handlers(server, ctx);
register_dashboard_handlers(server, ctx);
register_meters_settings_handlers(server, ctx);
register_loadbalancing_settings_handlers(server, ctx);
register_link_config_handlers(server, ctx);
register_meters_data_handlers(server, ctx);
register_scheduler_settings_handlers(server, ctx);
register_static_file_handlers(server, ctx); // Apenas chamando a função sem comparação
register_static_file_handlers(server, ctx);
ESP_LOGI(TAG, "All REST API endpoint groups registered successfully");
return ESP_OK;
}

View File

@@ -50,7 +50,7 @@ static void format_hhmm(uint16_t minutes, char *buf, size_t buf_sz)
* ========================= */
static esp_err_t scheduler_config_get_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "GET /api/v1/config/scheduler");
ESP_LOGD(TAG, "GET /api/v1/config/scheduler");
httpd_resp_set_type(req, "application/json");
@@ -95,7 +95,7 @@ static esp_err_t scheduler_config_get_handler(httpd_req_t *req)
* ========================= */
static esp_err_t scheduler_config_post_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "POST /api/v1/config/scheduler");
ESP_LOGD(TAG, "POST /api/v1/config/scheduler");
// NOTA: para payloads pequenos 512 bytes chega; se quiseres robustez total,
// usa req->content_len e faz um loop com httpd_req_recv.
@@ -108,7 +108,7 @@ static esp_err_t scheduler_config_post_handler(httpd_req_t *req)
return ESP_FAIL;
}
buf[len] = '\0';
ESP_LOGI(TAG, "Body: %s", buf);
ESP_LOGD(TAG, "Body: %s", buf);
cJSON *json = cJSON_Parse(buf);
if (!json)
@@ -126,7 +126,7 @@ static esp_err_t scheduler_config_post_handler(httpd_req_t *req)
if (cJSON_IsBool(j_enabled))
{
cfg.enabled = cJSON_IsTrue(j_enabled);
ESP_LOGI(TAG, " enabled = %d", cfg.enabled);
ESP_LOGD(TAG, " enabled = %d", cfg.enabled);
}
// mode
@@ -143,7 +143,7 @@ static esp_err_t scheduler_config_post_handler(httpd_req_t *req)
return ESP_FAIL;
}
cfg.mode = m;
ESP_LOGI(TAG, " mode = %s", sched_mode_to_str(cfg.mode));
ESP_LOGD(TAG, " mode = %s", sched_mode_to_str(cfg.mode));
}
// startTime (string "HH:MM")
@@ -160,7 +160,7 @@ static esp_err_t scheduler_config_post_handler(httpd_req_t *req)
return ESP_FAIL;
}
cfg.start_min = minutes;
ESP_LOGI(TAG, " start_min = %u", (unsigned)cfg.start_min);
ESP_LOGD(TAG, " start_min = %u", (unsigned)cfg.start_min);
}
// endTime (string "HH:MM")
@@ -177,7 +177,7 @@ static esp_err_t scheduler_config_post_handler(httpd_req_t *req)
return ESP_FAIL;
}
cfg.end_min = minutes;
ESP_LOGI(TAG, " end_min = %u", (unsigned)cfg.end_min);
ESP_LOGD(TAG, " end_min = %u", (unsigned)cfg.end_min);
}
// (Opcional) validações extra:
@@ -220,5 +220,5 @@ void register_scheduler_settings_handlers(httpd_handle_t server, void *ctx)
.user_ctx = ctx};
httpd_register_uri_handler(server, &post_uri);
ESP_LOGI(TAG, "Scheduler REST handlers registered");
ESP_LOGD(TAG, "Scheduler REST handlers registered");
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -13,8 +13,8 @@
}
</style>
<title>ChargeFlow</title>
<script type="module" crossorigin src="/assets/index-19gq1t3T.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-SX00HfRO.css">
<script type="module" crossorigin src="/assets/index-0q0tbwk5.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BIZ-rt0x.css">
</head>
<body>
<div id="root"></div>

View File

@@ -3,5 +3,5 @@ set(srcs "src/scheduler_types.c" "src/scheduler.c" "src/scheduler_events.c")
idf_component_register(SRCS "${srcs}"
INCLUDE_DIRS "include"
PRIV_INCLUDE_DIRS "src"
PRIV_REQUIRES nvs_flash esp_timer
PRIV_REQUIRES esp_timer
REQUIRES esp_event evse)

Some files were not shown because too many files have changed in this diff Show More