chore: snapshot before shared RS485 bus integration

This commit is contained in:
2026-07-22 15:36:04 +01:00
parent ef02e5c5f5
commit 2a803fcef1
167 changed files with 5749 additions and 1128 deletions

View File

@@ -0,0 +1,10 @@
set(srcs
"src/hmi_link.c"
)
idf_component_register(
SRCS ${srcs}
INCLUDE_DIRS "include"
REQUIRES evse
PRIV_REQUIRES driver esp_timer json
)

View File

@@ -0,0 +1,26 @@
#ifndef HMI_LINK_H
#define HMI_LINK_H
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Start the local HMI UART link.
*
* Default pins:
* EVSE GPIO22 = TX -> LCD RX
* EVSE GPIO21 = RX <- LCD TX
* Baudrate = 115200 8N1
*
* The link uses one JSON object per line ('\n').
*/
esp_err_t hmi_link_init(void);
#ifdef __cplusplus
}
#endif
#endif // HMI_LINK_H

View File

@@ -0,0 +1,406 @@
#include "hmi_link.h"
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "cJSON.h"
#include "driver/gpio.h"
#include "driver/uart.h"
#include "esp_err.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "evse_api.h"
#include "evse_config.h"
#include "evse_error.h"
#include "evse_meter.h"
#include "evse_session.h"
#include "evse_state.h"
#ifndef HMI_LINK_UART_PORT
#define HMI_LINK_UART_PORT UART_NUM_1
#endif
#ifndef HMI_LINK_UART_TX_GPIO
#define HMI_LINK_UART_TX_GPIO GPIO_NUM_22
#endif
#ifndef HMI_LINK_UART_RX_GPIO
#define HMI_LINK_UART_RX_GPIO GPIO_NUM_21
#endif
#ifndef HMI_LINK_UART_BAUDRATE
#define HMI_LINK_UART_BAUDRATE 115200
#endif
#ifndef HMI_LINK_STATUS_PERIOD_MS
#define HMI_LINK_STATUS_PERIOD_MS 1000
#endif
#define HMI_LINK_RX_BUF_SIZE 1024
#define HMI_LINK_TX_BUF_SIZE 1024
#define HMI_LINK_LINE_MAX 512
#define HMI_LINK_JSON_MAX 896
#define HMI_LINK_TASK_STACK 4096
#define HMI_LINK_TASK_PRIO 5
static const char *TAG = "hmi_link";
static TaskHandle_t s_task = NULL;
static uint32_t s_seq = 0;
static void hmi_link_write_line(const char *line)
{
if (!line)
return;
uart_write_bytes(HMI_LINK_UART_PORT, line, strlen(line));
uart_write_bytes(HMI_LINK_UART_PORT, "\n", 1);
}
static void hmi_link_send_ack(const char *cmd, bool ok, esp_err_t err)
{
char out[192];
snprintf(out, sizeof(out),
"{\"type\":\"ack\",\"cmd\":\"%s\",\"ok\":%s,\"err\":\"%s\"}",
cmd ? cmd : "",
ok ? "true" : "false",
ok ? "OK" : esp_err_to_name(err));
hmi_link_write_line(out);
}
static void hmi_link_send_error(const char *reason)
{
char out[192];
snprintf(out, sizeof(out),
"{\"type\":\"error\",\"reason\":\"%s\"}",
reason ? reason : "unknown");
hmi_link_write_line(out);
}
static bool json_bool_value(const cJSON *root, const char *name, bool *out)
{
const cJSON *item = cJSON_GetObjectItemCaseSensitive(root, name);
if (cJSON_IsBool(item))
{
*out = cJSON_IsTrue(item);
return true;
}
return false;
}
static bool json_u16_value_any(const cJSON *root, uint16_t *out, const char *a, const char *b, const char *c)
{
const char *names[3] = {a, b, c};
for (size_t i = 0; i < 3; ++i)
{
if (!names[i])
continue;
const cJSON *item = cJSON_GetObjectItemCaseSensitive(root, names[i]);
if (cJSON_IsNumber(item) && item->valuedouble >= 0.0 && item->valuedouble <= 65535.0)
{
*out = (uint16_t)item->valueint;
return true;
}
}
return false;
}
static void hmi_link_send_status(void)
{
int power[EVSE_METER_PHASE_COUNT] = {0};
float voltage[EVSE_METER_PHASE_COUNT] = {0};
float current[EVSE_METER_PHASE_COUNT] = {0};
evse_session_t session = {0};
bool has_session = evse_get_session(&session);
evse_meter_get_power(power);
evse_meter_get_voltage(voltage);
evse_meter_get_current(current);
evse_state_t state = evse_get_state();
uint32_t error_bits = evse_get_error();
uint64_t uptime_ms = (uint64_t)(esp_timer_get_time() / 1000ULL);
char out[HMI_LINK_JSON_MAX];
snprintf(out, sizeof(out),
"{"
"\"type\":\"status\","
"\"seq\":%lu,"
"\"uptimeMs\":%llu,"
"\"state\":\"%s\","
"\"plugged\":%s,"
"\"charging\":%s,"
"\"enabled\":%s,"
"\"available\":%s,"
"\"authorized\":%s,"
"\"limitA\":%u,"
"\"runtimeA\":%u,"
"\"maxA\":%u,"
"\"tempLimitC\":%u,"
"\"powerW\":%d,"
"\"energyWh\":%d,"
"\"error\":%lu,"
"\"v\":[%.1f,%.1f,%.1f],"
"\"i\":[%.3f,%.3f,%.3f],"
"\"p\":[%d,%d,%d],"
"\"session\":{\"present\":%s,\"current\":%s,\"id\":%lu,\"durationS\":%lu,\"energyWh\":%lu,\"avgPowerW\":%lu}"
"}",
(unsigned long)++s_seq,
(unsigned long long)uptime_ms,
evse_state_to_str(state),
evse_state_is_plugged(state) ? "true" : "false",
evse_state_is_charging(state) ? "true" : "false",
evse_config_is_enabled() ? "true" : "false",
evse_config_is_available() ? "true" : "false",
evse_state_get_authorized() ? "true" : "false",
(unsigned)evse_get_charging_current(),
(unsigned)evse_get_runtime_charging_current(),
(unsigned)evse_get_max_charging_current(),
(unsigned)evse_get_temp_threshold(),
evse_meter_get_instant_power(),
evse_meter_get_total_energy(),
(unsigned long)error_bits,
voltage[0], voltage[1], voltage[2],
current[0], current[1], current[2],
power[0], power[1], power[2],
has_session ? "true" : "false",
(has_session && session.is_current) ? "true" : "false",
has_session ? (unsigned long)session.session_id : 0UL,
has_session ? (unsigned long)session.duration_s : 0UL,
has_session ? (unsigned long)session.energy_wh : 0UL,
has_session ? (unsigned long)session.avg_power_w : 0UL);
hmi_link_write_line(out);
}
static void hmi_link_handle_command(const char *line)
{
cJSON *root = cJSON_Parse(line);
if (!root)
{
hmi_link_send_error("invalid_json");
return;
}
const cJSON *cmd_item = cJSON_GetObjectItemCaseSensitive(root, "cmd");
if (!cJSON_IsString(cmd_item) || cmd_item->valuestring == NULL)
{
cJSON_Delete(root);
hmi_link_send_error("missing_cmd");
return;
}
const char *cmd = cmd_item->valuestring;
if (strcmp(cmd, "get") == 0)
{
hmi_link_send_ack(cmd, true, ESP_OK);
hmi_link_send_status();
}
else if (strcmp(cmd, "ping") == 0)
{
char out[128];
snprintf(out, sizeof(out), "{\"type\":\"pong\",\"uptimeMs\":%llu}",
(unsigned long long)(esp_timer_get_time() / 1000ULL));
hmi_link_write_line(out);
}
else if (strcmp(cmd, "set_current") == 0)
{
uint16_t amps = 0;
if (!json_u16_value_any(root, &amps, "a", "currentA", "limitA"))
{
hmi_link_send_ack(cmd, false, ESP_ERR_INVALID_ARG);
}
else
{
esp_err_t err = evse_set_charging_current(amps);
hmi_link_send_ack(cmd, err == ESP_OK, err);
if (err == ESP_OK)
hmi_link_send_status();
}
}
else if (strcmp(cmd, "set_temp_threshold") == 0)
{
uint16_t temp_c = 0;
if (!json_u16_value_any(root, &temp_c, "c", "tempC", "temperatureLimit"))
{
hmi_link_send_ack(cmd, false, ESP_ERR_INVALID_ARG);
}
else
{
esp_err_t err = evse_set_temp_threshold((uint8_t)temp_c);
hmi_link_send_ack(cmd, err == ESP_OK, err);
if (err == ESP_OK)
hmi_link_send_status();
}
}
else if (strcmp(cmd, "set_enabled") == 0)
{
bool enabled = false;
if (!json_bool_value(root, "enabled", &enabled))
{
hmi_link_send_ack(cmd, false, ESP_ERR_INVALID_ARG);
}
else
{
evse_set_enabled(enabled);
hmi_link_send_ack(cmd, true, ESP_OK);
hmi_link_send_status();
}
}
else if (strcmp(cmd, "set_available") == 0)
{
bool available = false;
if (!json_bool_value(root, "available", &available))
{
hmi_link_send_ack(cmd, false, ESP_ERR_INVALID_ARG);
}
else
{
evse_set_available(available);
hmi_link_send_ack(cmd, true, ESP_OK);
hmi_link_send_status();
}
}
else
{
hmi_link_send_ack(cmd, false, ESP_ERR_NOT_SUPPORTED);
}
cJSON_Delete(root);
}
static void hmi_link_task(void *arg)
{
(void)arg;
char line[HMI_LINK_LINE_MAX];
size_t line_len = 0;
uint8_t rx[128];
TickType_t last_status = 0;
hmi_link_write_line("{\"type\":\"hello\",\"name\":\"hmi_link\",\"version\":1}");
hmi_link_send_status();
last_status = xTaskGetTickCount();
while (true)
{
int n = uart_read_bytes(HMI_LINK_UART_PORT, rx, sizeof(rx), pdMS_TO_TICKS(100));
for (int i = 0; i < n; ++i)
{
uint8_t ch = rx[i];
if (ch == '\r')
continue;
if (ch == '\n')
{
if (line_len > 0)
{
line[line_len] = '\0';
hmi_link_handle_command(line);
line_len = 0;
}
continue;
}
if (line_len < (sizeof(line) - 1))
{
line[line_len++] = (char)ch;
}
else
{
line_len = 0;
hmi_link_send_error("line_too_long");
}
}
TickType_t now = xTaskGetTickCount();
if ((now - last_status) >= pdMS_TO_TICKS(HMI_LINK_STATUS_PERIOD_MS))
{
hmi_link_send_status();
last_status = now;
}
}
}
esp_err_t hmi_link_init(void)
{
if (s_task != NULL)
{
ESP_LOGW(TAG, "Already started");
return ESP_OK;
}
const uart_config_t uart_config = {
.baud_rate = HMI_LINK_UART_BAUDRATE,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_DEFAULT,
};
esp_err_t err = uart_driver_install(HMI_LINK_UART_PORT,
HMI_LINK_RX_BUF_SIZE,
HMI_LINK_TX_BUF_SIZE,
0,
NULL,
0);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "uart_driver_install failed: %s", esp_err_to_name(err));
return err;
}
err = uart_param_config(HMI_LINK_UART_PORT, &uart_config);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "uart_param_config failed: %s", esp_err_to_name(err));
uart_driver_delete(HMI_LINK_UART_PORT);
return err;
}
err = uart_set_pin(HMI_LINK_UART_PORT,
HMI_LINK_UART_TX_GPIO,
HMI_LINK_UART_RX_GPIO,
UART_PIN_NO_CHANGE,
UART_PIN_NO_CHANGE);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "uart_set_pin failed: %s", esp_err_to_name(err));
uart_driver_delete(HMI_LINK_UART_PORT);
return err;
}
BaseType_t ok = xTaskCreate(hmi_link_task,
"hmi_link",
HMI_LINK_TASK_STACK,
NULL,
HMI_LINK_TASK_PRIO,
&s_task);
if (ok != pdPASS)
{
ESP_LOGE(TAG, "xTaskCreate failed");
s_task = NULL;
uart_driver_delete(HMI_LINK_UART_PORT);
return ESP_ERR_NO_MEM;
}
ESP_LOGI(TAG,
"Started: UART%d, TX GPIO%d, RX GPIO%d, %d baud",
(int)HMI_LINK_UART_PORT,
(int)HMI_LINK_UART_TX_GPIO,
(int)HMI_LINK_UART_RX_GPIO,
(int)HMI_LINK_UART_BAUDRATE);
return ESP_OK;
}