Compare commits
8 Commits
5c16e6deb7
...
bugfix/ws-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0d0b4dc39 | ||
|
|
25420d57b3 | ||
|
|
18984c29a3 | ||
|
|
7555efcba9 | ||
| 379f8e105b | |||
| 3b4d61c56d | |||
| 5ae0c64ba9 | |||
|
|
7f644652bb |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,3 +3,6 @@ managed_components/
|
|||||||
sdkconfig
|
sdkconfig
|
||||||
sdkconfig.old
|
sdkconfig.old
|
||||||
dependencies.lock
|
dependencies.lock
|
||||||
|
|
||||||
|
# vscode local settings
|
||||||
|
.vscode/
|
||||||
12
.vscode/settings.json
vendored
12
.vscode/settings.json
vendored
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"idf.currentSetup": "J:\\esp\\.espressif\\v5.5.2\\esp-idf",
|
|
||||||
"idf.flashType": "UART",
|
|
||||||
"idf.portWin": "COM7",
|
|
||||||
"idf.openOcdConfigs": [
|
|
||||||
"interface/ftdi/esp_ftdi.cfg",
|
|
||||||
"target/esp32s3.cfg"
|
|
||||||
],
|
|
||||||
"idf.customExtraVars": {
|
|
||||||
"IDF_TARGET": "esp32s3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -35,6 +35,7 @@ static uint8_t s_img_buf[STATUS_IMG_BYTES];
|
|||||||
static lv_img_dsc_t s_img_dsc;
|
static lv_img_dsc_t s_img_dsc;
|
||||||
static volatile bool s_img_pending = false; /* expecting binary frame with image data */
|
static volatile bool s_img_pending = false; /* expecting binary frame with image data */
|
||||||
static volatile bool s_img_updated = false; /* new image ready for UI consumption */
|
static volatile bool s_img_updated = false; /* new image ready for UI consumption */
|
||||||
|
static volatile bool s_need_request_image = false; /* deferred image request on connect */
|
||||||
static TaskHandle_t s_img_notify_task = NULL; /* task to wake on new image */
|
static TaskHandle_t s_img_notify_task = NULL; /* task to wake on new image */
|
||||||
|
|
||||||
/* Forward declarations */
|
/* Forward declarations */
|
||||||
@@ -157,11 +158,17 @@ static void ws_event_handler(void *arg, esp_event_base_t event_base,
|
|||||||
case WEBSOCKET_EVENT_CONNECTED:
|
case WEBSOCKET_EVENT_CONNECTED:
|
||||||
ESP_LOGI(TAG, "Audio WS connected");
|
ESP_LOGI(TAG, "Audio WS connected");
|
||||||
s_state = AUDIO_CONNECTED;
|
s_state = AUDIO_CONNECTED;
|
||||||
|
s_img_pending = false;
|
||||||
|
s_need_request_image = true;
|
||||||
|
if (s_img_notify_task) {
|
||||||
|
xTaskNotifyGive(s_img_notify_task);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case WEBSOCKET_EVENT_DISCONNECTED:
|
case WEBSOCKET_EVENT_DISCONNECTED:
|
||||||
ESP_LOGW(TAG, "Audio WS disconnected");
|
ESP_LOGW(TAG, "Audio WS disconnected");
|
||||||
s_playing = false;
|
s_playing = false;
|
||||||
|
s_img_pending = false;
|
||||||
flush_queue();
|
flush_queue();
|
||||||
s_state = AUDIO_IDLE;
|
s_state = AUDIO_IDLE;
|
||||||
break;
|
break;
|
||||||
@@ -275,9 +282,25 @@ const lv_img_dsc_t *audio_client_get_status_image(bool *updated)
|
|||||||
{
|
{
|
||||||
if (updated) {
|
if (updated) {
|
||||||
*updated = s_img_updated;
|
*updated = s_img_updated;
|
||||||
if (s_img_updated) {
|
|
||||||
s_img_updated = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return &s_img_dsc;
|
return &s_img_dsc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void audio_client_ack_status_image(void)
|
||||||
|
{
|
||||||
|
s_img_updated = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool audio_client_send_pending_request(void)
|
||||||
|
{
|
||||||
|
if (!s_need_request_image || !s_client) return false;
|
||||||
|
s_need_request_image = false;
|
||||||
|
static const char REQUEST_IMG_JSON[] = "{\"type\":\"request_image\"}";
|
||||||
|
int ret = esp_websocket_client_send_text(s_client, REQUEST_IMG_JSON, strlen(REQUEST_IMG_JSON), pdMS_TO_TICKS(1000));
|
||||||
|
if (ret < 0) {
|
||||||
|
ESP_LOGE(TAG, "Failed to send image request: %d", ret);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ESP_LOGI(TAG, "Sent image request to server");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|||||||
@@ -40,11 +40,24 @@ void audio_client_set_image_notify_task(TaskHandle_t task);
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the latest status image descriptor.
|
* Get the latest status image descriptor.
|
||||||
* @param updated Set to true if a new image arrived since last call, then reset.
|
* @param updated Set to true if a new image arrived since last call.
|
||||||
* @return Pointer to the static image descriptor (always valid).
|
* @return Pointer to the static image descriptor (always valid).
|
||||||
*/
|
*/
|
||||||
const lv_img_dsc_t *audio_client_get_status_image(bool *updated);
|
const lv_img_dsc_t *audio_client_get_status_image(bool *updated);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Acknowledge that the status image was successfully rendered.
|
||||||
|
* Clears the updated flag so subsequent get_status_image calls return false.
|
||||||
|
*/
|
||||||
|
void audio_client_ack_status_image(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send any pending image request to the server.
|
||||||
|
* Call from a task context (not from an event handler).
|
||||||
|
* @return true if a request was sent, false if none pending or send failed.
|
||||||
|
*/
|
||||||
|
bool audio_client_send_pending_request(void);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -132,7 +132,9 @@ static void scroll_timer_cb(lv_timer_t *timer)
|
|||||||
cur_y = 0;
|
cur_y = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
lv_obj_scroll_to_y(tbl_services, cur_y + s_row_h, LV_ANIM_OFF);
|
// lv_obj_scroll_to_y(tbl_services, cur_y + s_row_h, LV_ANIM_OFF);
|
||||||
|
const int delta_y = s_row_h / 8;
|
||||||
|
lv_obj_scroll_to_y(tbl_services, cur_y + delta_y, LV_ANIM_ON);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Measure row height, compute visible rows, append duplicates for seamless loop */
|
/* Measure row height, compute visible rows, append duplicates for seamless loop */
|
||||||
@@ -261,7 +263,7 @@ static void create_time_bar(lv_obj_t *parent)
|
|||||||
static void create_main_section(lv_obj_t *parent)
|
static void create_main_section(lv_obj_t *parent)
|
||||||
{
|
{
|
||||||
/* === Left column: Services + Pi Vitals === */
|
/* === Left column: Services + Pi Vitals === */
|
||||||
create_label(parent, 4, MAIN_Y + 2, &InziuIosevka_Slab_CC_12px, "SERVICES");
|
create_label(parent, 4, MAIN_Y + 1, &InziuIosevka_Slab_CC_12px, "SERVICES");
|
||||||
|
|
||||||
tbl_services = lv_table_create(parent);
|
tbl_services = lv_table_create(parent);
|
||||||
lv_obj_set_pos(tbl_services, 4, MAIN_Y + 16);
|
lv_obj_set_pos(tbl_services, 4, MAIN_Y + 16);
|
||||||
@@ -281,8 +283,8 @@ static void create_main_section(lv_obj_t *parent)
|
|||||||
lv_table_set_cell_value(tbl_services, i, 1, "---");
|
lv_table_set_cell_value(tbl_services, i, 1, "---");
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Auto-scroll timer: 1 second period */
|
/* Timer for each shift of 1/8 line's height */
|
||||||
s_scroll_timer = lv_timer_create(scroll_timer_cb, 1000, NULL);
|
s_scroll_timer = lv_timer_create(scroll_timer_cb, 200, NULL);
|
||||||
|
|
||||||
/* === Left column: Pi Vitals (below services) === */
|
/* === Left column: Pi Vitals (below services) === */
|
||||||
int rx = 0;
|
int rx = 0;
|
||||||
@@ -294,7 +296,7 @@ static void create_main_section(lv_obj_t *parent)
|
|||||||
int temp_x = rx + 155; /* TEMP column, right of value labels */
|
int temp_x = rx + 155; /* TEMP column, right of value labels */
|
||||||
|
|
||||||
/* Pi Vitals header */
|
/* Pi Vitals header */
|
||||||
create_label(parent, rx + 4, 177, &InziuIosevka_Slab_CC_12px, "PI VITALS");
|
create_label(parent, rx + 4, 175, &InziuIosevka_Slab_CC_12px, "PI VITALS");
|
||||||
|
|
||||||
int ry = 192;
|
int ry = 192;
|
||||||
|
|
||||||
|
|||||||
@@ -194,12 +194,16 @@ static void sensor_task(void *arg)
|
|||||||
*/
|
*/
|
||||||
ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(1000));
|
ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(1000));
|
||||||
|
|
||||||
|
/* Send deferred image request if connect just happened */
|
||||||
|
audio_client_send_pending_request();
|
||||||
|
|
||||||
/* Check for status image updates immediately */
|
/* Check for status image updates immediately */
|
||||||
bool img_updated = false;
|
bool img_updated = false;
|
||||||
const lv_img_dsc_t *img = audio_client_get_status_image(&img_updated);
|
const lv_img_dsc_t *img = audio_client_get_status_image(&img_updated);
|
||||||
if (img_updated && Lvgl_lock(100)) {
|
if (img_updated && Lvgl_lock(100)) {
|
||||||
dashboard_ui_update_status_image(img);
|
dashboard_ui_update_status_image(img);
|
||||||
Lvgl_unlock();
|
Lvgl_unlock();
|
||||||
|
audio_client_ack_status_image();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sensor + clock updates at ~1s cadence (skip if woken early) */
|
/* Sensor + clock updates at ~1s cadence (skip if woken early) */
|
||||||
|
|||||||
2
pi/.gitignore
vendored
2
pi/.gitignore
vendored
@@ -2,4 +2,4 @@
|
|||||||
*/__pycache__
|
*/__pycache__
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyo
|
*.pyo
|
||||||
*.pyc
|
*.pyc
|
||||||
@@ -17,6 +17,7 @@ Protocol:
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -68,17 +69,17 @@ async def handler(ws):
|
|||||||
|
|
||||||
configs = load_config(_config_path)
|
configs = load_config(_config_path)
|
||||||
img_idle = load_status_image(IMG_DIR / "idle.png")
|
img_idle = load_status_image(IMG_DIR / "idle.png")
|
||||||
|
current_img = img_idle
|
||||||
|
|
||||||
try:
|
alarms = [_prepare_alarm(entry) for entry in configs] if configs else []
|
||||||
await send_status_image(ws, img_idle)
|
|
||||||
|
|
||||||
if not configs:
|
async def alarm_ticker():
|
||||||
|
nonlocal current_img
|
||||||
|
if not alarms:
|
||||||
log.info("No alarms configured — idling forever")
|
log.info("No alarms configured — idling forever")
|
||||||
await asyncio.Future()
|
await asyncio.Future()
|
||||||
return
|
return
|
||||||
|
|
||||||
alarms = [_prepare_alarm(entry) for entry in configs]
|
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
for alarm in alarms:
|
for alarm in alarms:
|
||||||
if should_fire(alarm["config"]):
|
if should_fire(alarm["config"]):
|
||||||
@@ -88,13 +89,28 @@ async def handler(ws):
|
|||||||
alarm["last_fired"] = current_minute
|
alarm["last_fired"] = current_minute
|
||||||
log.info("Alarm firing: %s at %s",
|
log.info("Alarm firing: %s at %s",
|
||||||
alarm["config"]["alarm_time"], current_minute)
|
alarm["config"]["alarm_time"], current_minute)
|
||||||
await send_status_image(ws, alarm["img"])
|
current_img = alarm["img"]
|
||||||
|
await send_status_image(ws, current_img)
|
||||||
await stream_alarm(ws, alarm["pcm"], alarm["sr"],
|
await stream_alarm(ws, alarm["pcm"], alarm["sr"],
|
||||||
alarm["ch"], alarm["bits"])
|
alarm["ch"], alarm["bits"])
|
||||||
await send_status_image(ws, img_idle)
|
current_img = img_idle
|
||||||
|
await send_status_image(ws, current_img)
|
||||||
|
|
||||||
await asyncio.sleep(TICK_INTERVAL)
|
await asyncio.sleep(TICK_INTERVAL)
|
||||||
|
|
||||||
|
async def receiver():
|
||||||
|
async for msg in ws:
|
||||||
|
try:
|
||||||
|
data = json.loads(msg)
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
continue
|
||||||
|
if data.get("type") == "request_image":
|
||||||
|
log.info("Client requested image — sending current (%d bytes)",
|
||||||
|
len(current_img))
|
||||||
|
await send_status_image(ws, current_img)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await asyncio.gather(alarm_ticker(), receiver())
|
||||||
except websockets.exceptions.ConnectionClosed:
|
except websockets.exceptions.ConnectionClosed:
|
||||||
log.info("Client disconnected: %s:%d", remote[0], remote[1])
|
log.info("Client disconnected: %s:%d", remote[0], remote[1])
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user