完善 BLE 自启和 OTA 底座
This commit is contained in:
parent
1aaca3d83c
commit
6eca70ebb6
71
docs/ble-autostart-ota.md
Normal file
71
docs/ble-autostart-ota.md
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
# BLE 自启与 OTA 说明
|
||||||
|
|
||||||
|
## 开机自启
|
||||||
|
|
||||||
|
电脑端只需要一个常驻进程:`tools/pc-metrics/push-metrics-ble.py`。
|
||||||
|
|
||||||
|
安装开机自启:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
& "D:\AI Code\ESP-32\tools\pc-metrics\install-ble-autostart.ps1"
|
||||||
|
```
|
||||||
|
|
||||||
|
卸载开机自启:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
& "D:\AI Code\ESP-32\tools\pc-metrics\uninstall-ble-autostart.ps1"
|
||||||
|
```
|
||||||
|
|
||||||
|
计划任务名称是 `RLCD Metrics BLE`,触发条件是当前 Windows 用户登录后启动。
|
||||||
|
|
||||||
|
## BLE 状态回传
|
||||||
|
|
||||||
|
板子会通过 BLE notify 回传 `STA2` 状态:
|
||||||
|
|
||||||
|
```text
|
||||||
|
STA2,1,<battery_percent>,<battery_mv>,<temperature_c10>,<humidity_percent>,<ble_write_count>,<uptime_seconds>,<internal_heap_kb>,<psram_free_kb>
|
||||||
|
```
|
||||||
|
|
||||||
|
电脑端日志会显示成更易读的形式:
|
||||||
|
|
||||||
|
```text
|
||||||
|
BLE board: bat=98% 4095mV env=29.3C/57% writes=12 up=50s heap=140K psram=7581K
|
||||||
|
```
|
||||||
|
|
||||||
|
## BLE OTA
|
||||||
|
|
||||||
|
当前固件分区已经改成 `ota_0 / ota_1` 双应用槽。首次启用 OTA 必须先通过 USB 烧录一次新分区表和底座固件,之后可以用 BLE 发送新固件。
|
||||||
|
|
||||||
|
发送当前构建产物:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
& "D:\AI Code\ESP-32\tools\pc-metrics\send-ble-ota.ps1"
|
||||||
|
```
|
||||||
|
|
||||||
|
发送指定固件:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
& "D:\AI Code\ESP-32\tools\pc-metrics\send-ble-ota.ps1" -Firmware "D:\AI Code\ESP-32\build\rlcd_clock.bin"
|
||||||
|
```
|
||||||
|
|
||||||
|
OTA 期间需要先停止实时推送进程,避免同一时间抢 BLE 连接:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
& "D:\AI Code\ESP-32\tools\pc-metrics\stop-ble-metrics.ps1"
|
||||||
|
```
|
||||||
|
|
||||||
|
OTA 成功后板子会回传 `OTA,DONE,<bytes>` 并自动重启。重启后重新启动实时推送:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
& "D:\AI Code\ESP-32\tools\pc-metrics\start-ble-metrics.ps1"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 电量口径
|
||||||
|
|
||||||
|
电量百分比按微雪官方 ADC 示例口径换算:
|
||||||
|
|
||||||
|
- 低于 `3.0V` 显示 `0%`
|
||||||
|
- 高于 `4.12V` 显示 `100%`
|
||||||
|
- 中间按线性百分比显示
|
||||||
|
|
||||||
|
USB 充电时看到的电压通常会受充电状态影响,所以百分比仍然是估算值,不是库仑计精度。
|
||||||
@ -20,6 +20,7 @@ idf_component_register(
|
|||||||
esp_driver_usb_serial_jtag
|
esp_driver_usb_serial_jtag
|
||||||
esp_driver_i2c
|
esp_driver_i2c
|
||||||
esp_adc
|
esp_adc
|
||||||
|
app_update
|
||||||
bt
|
bt
|
||||||
nvs_flash
|
nvs_flash
|
||||||
)
|
)
|
||||||
|
|||||||
@ -18,8 +18,8 @@ static const char *TAG = "battery";
|
|||||||
#define BATTERY_ADC_CHANNEL ADC_CHANNEL_3
|
#define BATTERY_ADC_CHANNEL ADC_CHANNEL_3
|
||||||
#define BATTERY_ADC_ATTEN ADC_ATTEN_DB_12
|
#define BATTERY_ADC_ATTEN ADC_ATTEN_DB_12
|
||||||
#define BATTERY_DIVIDER 3
|
#define BATTERY_DIVIDER 3
|
||||||
#define BATTERY_EMPTY_MV 2500
|
#define BATTERY_EMPTY_MV 3000
|
||||||
#define BATTERY_FULL_MV 4200
|
#define BATTERY_FULL_MV 4120
|
||||||
|
|
||||||
static adc_oneshot_unit_handle_t s_adc_handle;
|
static adc_oneshot_unit_handle_t s_adc_handle;
|
||||||
static adc_cali_handle_t s_cali_handle;
|
static adc_cali_handle_t s_cali_handle;
|
||||||
|
|||||||
13
main/main.c
13
main/main.c
@ -3,6 +3,7 @@
|
|||||||
#include "esp_err.h"
|
#include "esp_err.h"
|
||||||
#include "esp_heap_caps.h"
|
#include "esp_heap_caps.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
|
#include "esp_system.h"
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/task.h"
|
#include "freertos/task.h"
|
||||||
#include "lvgl.h"
|
#include "lvgl.h"
|
||||||
@ -122,8 +123,18 @@ void app_main(void)
|
|||||||
TickType_t now_tick = xTaskGetTickCount();
|
TickType_t now_tick = xTaskGetTickCount();
|
||||||
if ((now_tick - last_status_notify_tick) >= pdMS_TO_TICKS(5000)) {
|
if ((now_tick - last_status_notify_tick) >= pdMS_TO_TICKS(5000)) {
|
||||||
last_status_notify_tick = now_tick;
|
last_status_notify_tick = now_tick;
|
||||||
|
uint32_t uptime_seconds = (uint32_t)(now_tick / configTICK_RATE_HZ);
|
||||||
|
uint32_t internal_free_kb = (uint32_t)(heap_caps_get_free_size(MALLOC_CAP_INTERNAL) / 1024);
|
||||||
|
uint32_t psram_free_kb = (uint32_t)(heap_caps_get_free_size(MALLOC_CAP_SPIRAM) / 1024);
|
||||||
metrics_ble_notify_status(latest_battery_percent, latest_battery_mv,
|
metrics_ble_notify_status(latest_battery_percent, latest_battery_mv,
|
||||||
latest_temperature_c10, latest_humidity_percent);
|
latest_temperature_c10, latest_humidity_percent,
|
||||||
|
uptime_seconds, internal_free_kb, psram_free_kb);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metrics_ble_take_ota_restart_request()) {
|
||||||
|
ESP_LOGW(TAG, "BLE OTA complete, restarting.");
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(2000));
|
||||||
|
esp_restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
lv_lock();
|
lv_lock();
|
||||||
|
|||||||
@ -2,8 +2,10 @@
|
|||||||
|
|
||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "esp_ota_ops.h"
|
||||||
#include "esp_err.h"
|
#include "esp_err.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
@ -27,6 +29,8 @@ static const char *DEVICE_NAME = "RLCD-Metrics";
|
|||||||
// Service: 9f4af548-1a14-4fc1-8f5a-4b9f00000001
|
// Service: 9f4af548-1a14-4fc1-8f5a-4b9f00000001
|
||||||
// Write char: 9f4af548-1a14-4fc1-8f5a-4b9f00000002
|
// Write char: 9f4af548-1a14-4fc1-8f5a-4b9f00000002
|
||||||
// Status notify char: 9f4af548-1a14-4fc1-8f5a-4b9f00000003
|
// Status notify char: 9f4af548-1a14-4fc1-8f5a-4b9f00000003
|
||||||
|
// OTA control char: 9f4af548-1a14-4fc1-8f5a-4b9f00000004
|
||||||
|
// OTA data char: 9f4af548-1a14-4fc1-8f5a-4b9f00000005
|
||||||
static const ble_uuid128_t metrics_svc_uuid =
|
static const ble_uuid128_t metrics_svc_uuid =
|
||||||
BLE_UUID128_INIT(0x01, 0x00, 0x00, 0x00, 0x9f, 0x4b, 0x5a, 0x8f,
|
BLE_UUID128_INIT(0x01, 0x00, 0x00, 0x00, 0x9f, 0x4b, 0x5a, 0x8f,
|
||||||
0xc1, 0x4f, 0x14, 0x1a, 0x48, 0xf5, 0x4a, 0x9f);
|
0xc1, 0x4f, 0x14, 0x1a, 0x48, 0xf5, 0x4a, 0x9f);
|
||||||
@ -36,19 +40,57 @@ static const ble_uuid128_t metrics_chr_uuid =
|
|||||||
static const ble_uuid128_t status_chr_uuid =
|
static const ble_uuid128_t status_chr_uuid =
|
||||||
BLE_UUID128_INIT(0x03, 0x00, 0x00, 0x00, 0x9f, 0x4b, 0x5a, 0x8f,
|
BLE_UUID128_INIT(0x03, 0x00, 0x00, 0x00, 0x9f, 0x4b, 0x5a, 0x8f,
|
||||||
0xc1, 0x4f, 0x14, 0x1a, 0x48, 0xf5, 0x4a, 0x9f);
|
0xc1, 0x4f, 0x14, 0x1a, 0x48, 0xf5, 0x4a, 0x9f);
|
||||||
|
static const ble_uuid128_t ota_control_chr_uuid =
|
||||||
|
BLE_UUID128_INIT(0x04, 0x00, 0x00, 0x00, 0x9f, 0x4b, 0x5a, 0x8f,
|
||||||
|
0xc1, 0x4f, 0x14, 0x1a, 0x48, 0xf5, 0x4a, 0x9f);
|
||||||
|
static const ble_uuid128_t ota_data_chr_uuid =
|
||||||
|
BLE_UUID128_INIT(0x05, 0x00, 0x00, 0x00, 0x9f, 0x4b, 0x5a, 0x8f,
|
||||||
|
0xc1, 0x4f, 0x14, 0x1a, 0x48, 0xf5, 0x4a, 0x9f);
|
||||||
|
|
||||||
static uint8_t own_addr_type;
|
static uint8_t own_addr_type;
|
||||||
static uint16_t metrics_chr_val_handle;
|
static uint16_t metrics_chr_val_handle;
|
||||||
static uint16_t status_chr_val_handle;
|
static uint16_t status_chr_val_handle;
|
||||||
|
static uint16_t ota_control_chr_val_handle;
|
||||||
|
static uint16_t ota_data_chr_val_handle;
|
||||||
static uint16_t s_conn_handle;
|
static uint16_t s_conn_handle;
|
||||||
static bool s_status_notify_enabled;
|
static bool s_status_notify_enabled;
|
||||||
static QueueHandle_t s_metrics_queue;
|
static QueueHandle_t s_metrics_queue;
|
||||||
static uint32_t s_ble_write_count;
|
static uint32_t s_ble_write_count;
|
||||||
|
static bool s_ota_restart_requested;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
bool active;
|
||||||
|
esp_ota_handle_t handle;
|
||||||
|
const esp_partition_t *partition;
|
||||||
|
uint32_t expected_size;
|
||||||
|
uint32_t written;
|
||||||
|
} ble_ota_state_t;
|
||||||
|
|
||||||
|
static ble_ota_state_t s_ota;
|
||||||
|
|
||||||
void ble_store_config_init(void);
|
void ble_store_config_init(void);
|
||||||
|
|
||||||
static int metrics_gap_event(struct ble_gap_event *event, void *arg);
|
static int metrics_gap_event(struct ble_gap_event *event, void *arg);
|
||||||
|
|
||||||
|
static bool metrics_notify_text(const char *line)
|
||||||
|
{
|
||||||
|
if (!s_status_notify_enabled || s_conn_handle == 0 || status_chr_val_handle == 0 || line == NULL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct os_mbuf *om = ble_hs_mbuf_from_flat(line, strlen(line));
|
||||||
|
if (om == NULL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int rc = ble_gatts_notify_custom(s_conn_handle, status_chr_val_handle, om);
|
||||||
|
if (rc != 0) {
|
||||||
|
ESP_LOGW(TAG, "status notify failed: %d", rc);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
static const char *metrics_phy_name(uint8_t phy)
|
static const char *metrics_phy_name(uint8_t phy)
|
||||||
{
|
{
|
||||||
switch (phy) {
|
switch (phy) {
|
||||||
@ -82,10 +124,51 @@ static int metrics_gatt_access(uint16_t conn_handle, uint16_t attr_handle,
|
|||||||
(void)conn_handle;
|
(void)conn_handle;
|
||||||
(void)arg;
|
(void)arg;
|
||||||
|
|
||||||
if (ctxt->op != BLE_GATT_ACCESS_OP_WRITE_CHR || attr_handle != metrics_chr_val_handle) {
|
if (ctxt->op != BLE_GATT_ACCESS_OP_WRITE_CHR) {
|
||||||
return BLE_ATT_ERR_UNLIKELY;
|
return BLE_ATT_ERR_UNLIKELY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (attr_handle == ota_data_chr_val_handle) {
|
||||||
|
if (!s_ota.active || s_ota.handle == 0) {
|
||||||
|
metrics_notify_text("OTA,ERR,NO_BEGIN");
|
||||||
|
return BLE_ATT_ERR_UNLIKELY;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t len = OS_MBUF_PKTLEN(ctxt->om);
|
||||||
|
if (len == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t data[256];
|
||||||
|
if (len > sizeof(data)) {
|
||||||
|
metrics_notify_text("OTA,ERR,CHUNK_TOO_BIG");
|
||||||
|
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t copied = 0;
|
||||||
|
int rc = ble_hs_mbuf_to_flat(ctxt->om, data, sizeof(data), &copied);
|
||||||
|
if (rc != 0 || copied != len) {
|
||||||
|
metrics_notify_text("OTA,ERR,BAD_CHUNK");
|
||||||
|
return BLE_ATT_ERR_UNLIKELY;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t ret = esp_ota_write(s_ota.handle, data, copied);
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "OTA write failed: %s", esp_err_to_name(ret));
|
||||||
|
metrics_notify_text("OTA,ERR,WRITE");
|
||||||
|
return BLE_ATT_ERR_UNLIKELY;
|
||||||
|
}
|
||||||
|
|
||||||
|
s_ota.written += copied;
|
||||||
|
if ((s_ota.written % (64 * 1024)) < copied) {
|
||||||
|
char status[48];
|
||||||
|
snprintf(status, sizeof(status), "OTA,PROG,%lu,%lu",
|
||||||
|
(unsigned long)s_ota.written, (unsigned long)s_ota.expected_size);
|
||||||
|
metrics_notify_text(status);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
char line[512] = {0};
|
char line[512] = {0};
|
||||||
uint16_t len = 0;
|
uint16_t len = 0;
|
||||||
int rc = ble_hs_mbuf_to_flat(ctxt->om, line, sizeof(line) - 1, &len);
|
int rc = ble_hs_mbuf_to_flat(ctxt->om, line, sizeof(line) - 1, &len);
|
||||||
@ -94,6 +177,93 @@ static int metrics_gatt_access(uint16_t conn_handle, uint16_t attr_handle,
|
|||||||
}
|
}
|
||||||
line[len] = '\0';
|
line[len] = '\0';
|
||||||
|
|
||||||
|
if (attr_handle == ota_control_chr_val_handle) {
|
||||||
|
if (strncmp(line, "BEGIN,", 6) == 0) {
|
||||||
|
uint32_t expected_size = (uint32_t)strtoul(line + 6, NULL, 10);
|
||||||
|
if (expected_size == 0) {
|
||||||
|
metrics_notify_text("OTA,ERR,BAD_SIZE");
|
||||||
|
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s_ota.active && s_ota.handle != 0) {
|
||||||
|
esp_ota_abort(s_ota.handle);
|
||||||
|
}
|
||||||
|
memset(&s_ota, 0, sizeof(s_ota));
|
||||||
|
s_ota.partition = esp_ota_get_next_update_partition(NULL);
|
||||||
|
if (s_ota.partition == NULL) {
|
||||||
|
metrics_notify_text("OTA,ERR,NO_PARTITION");
|
||||||
|
return BLE_ATT_ERR_UNLIKELY;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t ret = esp_ota_begin(s_ota.partition, OTA_SIZE_UNKNOWN, &s_ota.handle);
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "OTA begin failed: %s", esp_err_to_name(ret));
|
||||||
|
metrics_notify_text("OTA,ERR,BEGIN");
|
||||||
|
return BLE_ATT_ERR_UNLIKELY;
|
||||||
|
}
|
||||||
|
|
||||||
|
s_ota.active = true;
|
||||||
|
s_ota.expected_size = expected_size;
|
||||||
|
s_ota.written = 0;
|
||||||
|
metrics_notify_text("OTA,BEGIN");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strcmp(line, "END") == 0) {
|
||||||
|
if (!s_ota.active || s_ota.handle == 0) {
|
||||||
|
metrics_notify_text("OTA,ERR,NO_BEGIN");
|
||||||
|
return BLE_ATT_ERR_UNLIKELY;
|
||||||
|
}
|
||||||
|
if (s_ota.expected_size > 0 && s_ota.written != s_ota.expected_size) {
|
||||||
|
ESP_LOGW(TAG, "OTA size mismatch: written=%lu expected=%lu",
|
||||||
|
(unsigned long)s_ota.written, (unsigned long)s_ota.expected_size);
|
||||||
|
esp_ota_abort(s_ota.handle);
|
||||||
|
memset(&s_ota, 0, sizeof(s_ota));
|
||||||
|
metrics_notify_text("OTA,ERR,SIZE");
|
||||||
|
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t ret = esp_ota_end(s_ota.handle);
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "OTA end failed: %s", esp_err_to_name(ret));
|
||||||
|
memset(&s_ota, 0, sizeof(s_ota));
|
||||||
|
metrics_notify_text("OTA,ERR,END");
|
||||||
|
return BLE_ATT_ERR_UNLIKELY;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = esp_ota_set_boot_partition(s_ota.partition);
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "OTA set boot failed: %s", esp_err_to_name(ret));
|
||||||
|
memset(&s_ota, 0, sizeof(s_ota));
|
||||||
|
metrics_notify_text("OTA,ERR,BOOT");
|
||||||
|
return BLE_ATT_ERR_UNLIKELY;
|
||||||
|
}
|
||||||
|
|
||||||
|
char status[48];
|
||||||
|
snprintf(status, sizeof(status), "OTA,DONE,%lu", (unsigned long)s_ota.written);
|
||||||
|
memset(&s_ota, 0, sizeof(s_ota));
|
||||||
|
s_ota_restart_requested = true;
|
||||||
|
metrics_notify_text(status);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strcmp(line, "ABORT") == 0) {
|
||||||
|
if (s_ota.active && s_ota.handle != 0) {
|
||||||
|
esp_ota_abort(s_ota.handle);
|
||||||
|
}
|
||||||
|
memset(&s_ota, 0, sizeof(s_ota));
|
||||||
|
metrics_notify_text("OTA,ABORT");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
metrics_notify_text("OTA,ERR,BAD_CMD");
|
||||||
|
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attr_handle != metrics_chr_val_handle) {
|
||||||
|
return BLE_ATT_ERR_UNLIKELY;
|
||||||
|
}
|
||||||
|
|
||||||
ui_metrics_t metrics;
|
ui_metrics_t metrics;
|
||||||
if (!metrics_parse_line(line, &metrics)) {
|
if (!metrics_parse_line(line, &metrics)) {
|
||||||
ESP_LOGW(TAG, "bad BLE metrics: %s", line);
|
ESP_LOGW(TAG, "bad BLE metrics: %s", line);
|
||||||
@ -129,6 +299,18 @@ static const struct ble_gatt_svc_def metrics_gatt_svcs[] = {
|
|||||||
.flags = BLE_GATT_CHR_F_NOTIFY,
|
.flags = BLE_GATT_CHR_F_NOTIFY,
|
||||||
.val_handle = &status_chr_val_handle,
|
.val_handle = &status_chr_val_handle,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
.uuid = &ota_control_chr_uuid.u,
|
||||||
|
.access_cb = metrics_gatt_access,
|
||||||
|
.flags = BLE_GATT_CHR_F_WRITE,
|
||||||
|
.val_handle = &ota_control_chr_val_handle,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
.uuid = &ota_data_chr_uuid.u,
|
||||||
|
.access_cb = metrics_gatt_access,
|
||||||
|
.flags = BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_NO_RSP,
|
||||||
|
.val_handle = &ota_data_chr_val_handle,
|
||||||
|
},
|
||||||
{0},
|
{0},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -299,29 +481,27 @@ bool metrics_ble_take_latest(ui_metrics_t *metrics)
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool metrics_ble_notify_status(int battery_percent, int battery_mv,
|
bool metrics_ble_notify_status(int battery_percent, int battery_mv,
|
||||||
int temperature_c10, int humidity_percent)
|
int temperature_c10, int humidity_percent,
|
||||||
|
uint32_t uptime_seconds, uint32_t internal_free_kb,
|
||||||
|
uint32_t psram_free_kb)
|
||||||
{
|
{
|
||||||
if (!s_status_notify_enabled || s_conn_handle == 0 || status_chr_val_handle == 0) {
|
char line[128];
|
||||||
return false;
|
int len = snprintf(line, sizeof(line), "STA2,1,%d,%d,%d,%d,%lu,%lu,%lu,%lu",
|
||||||
}
|
|
||||||
|
|
||||||
char line[96];
|
|
||||||
int len = snprintf(line, sizeof(line), "STA,%d,%d,%d,%d,%lu",
|
|
||||||
battery_percent, battery_mv, temperature_c10, humidity_percent,
|
battery_percent, battery_mv, temperature_c10, humidity_percent,
|
||||||
(unsigned long)s_ble_write_count);
|
(unsigned long)s_ble_write_count, (unsigned long)uptime_seconds,
|
||||||
|
(unsigned long)internal_free_kb, (unsigned long)psram_free_kb);
|
||||||
if (len <= 0 || len >= (int)sizeof(line)) {
|
if (len <= 0 || len >= (int)sizeof(line)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct os_mbuf *om = ble_hs_mbuf_from_flat(line, len);
|
return metrics_notify_text(line);
|
||||||
if (om == NULL) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int rc = ble_gatts_notify_custom(s_conn_handle, status_chr_val_handle, om);
|
bool metrics_ble_take_ota_restart_request(void)
|
||||||
if (rc != 0) {
|
{
|
||||||
ESP_LOGW(TAG, "status notify failed: %d", rc);
|
if (!s_ota_restart_requested) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
s_ota_restart_requested = false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
#include "ui_screen.h"
|
#include "ui_screen.h"
|
||||||
|
|
||||||
@ -11,7 +12,10 @@ extern "C" {
|
|||||||
void metrics_ble_start(void);
|
void metrics_ble_start(void);
|
||||||
bool metrics_ble_take_latest(ui_metrics_t *metrics);
|
bool metrics_ble_take_latest(ui_metrics_t *metrics);
|
||||||
bool metrics_ble_notify_status(int battery_percent, int battery_mv,
|
bool metrics_ble_notify_status(int battery_percent, int battery_mv,
|
||||||
int temperature_c10, int humidity_percent);
|
int temperature_c10, int humidity_percent,
|
||||||
|
uint32_t uptime_seconds, uint32_t internal_free_kb,
|
||||||
|
uint32_t psram_free_kb);
|
||||||
|
bool metrics_ble_take_ota_restart_request(void);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
# Name, Type, SubType, Offset, Size, Flags
|
# Name, Type, SubType, Offset, Size, Flags
|
||||||
nvs, data, nvs, 0x9000, 0x6000,
|
nvs, data, nvs, 0x9000, 0x4000,
|
||||||
|
otadata, data, ota, 0xd000, 0x2000,
|
||||||
phy_init, data, phy, 0xf000, 0x1000,
|
phy_init, data, phy, 0xf000, 0x1000,
|
||||||
factory, app, factory, 0x10000, 0x400000,
|
ota_0, app, ota_0, 0x20000, 0x3f0000,
|
||||||
|
ota_1, app, ota_1, , 0x3f0000,
|
||||||
|
|||||||
|
134
tools/pc-metrics/ble-ota.py
Normal file
134
tools/pc-metrics/ble-ota.py
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from bleak import BleakClient, BleakScanner
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_DEVICE_NAME = "RLCD-Metrics"
|
||||||
|
DEFAULT_STATUS_CHAR_UUID = "9f4af548-1a14-4fc1-8f5a-4b9f00000003"
|
||||||
|
DEFAULT_OTA_CONTROL_CHAR_UUID = "9f4af548-1a14-4fc1-8f5a-4b9f00000004"
|
||||||
|
DEFAULT_OTA_DATA_CHAR_UUID = "9f4af548-1a14-4fc1-8f5a-4b9f00000005"
|
||||||
|
|
||||||
|
|
||||||
|
async def find_device(name: str, address: str | None, timeout: float):
|
||||||
|
devices = await BleakScanner.discover(timeout=timeout)
|
||||||
|
for device in devices:
|
||||||
|
if address and device.address.lower() == address.lower():
|
||||||
|
return device
|
||||||
|
if device.name == name:
|
||||||
|
return device
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def status_callback(_sender, data: bytearray):
|
||||||
|
text = bytes(data).decode("ascii", errors="replace").strip()
|
||||||
|
if text:
|
||||||
|
print(text, flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
async def send_ota(args) -> int:
|
||||||
|
firmware = Path(args.firmware).resolve()
|
||||||
|
if not firmware.is_file():
|
||||||
|
print(f"Firmware not found: {firmware}", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
size = firmware.stat().st_size
|
||||||
|
if size <= 0:
|
||||||
|
print(f"Firmware is empty: {firmware}", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
print(f"Scanning for {args.name}...", flush=True)
|
||||||
|
device = await find_device(args.name, args.address, args.scan_timeout)
|
||||||
|
if device is None:
|
||||||
|
print(f"Device not found: {args.name}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print(f"Connecting to {device.name} ({device.address})...", flush=True)
|
||||||
|
async with BleakClient(device, timeout=args.connect_timeout) as client:
|
||||||
|
await client.start_notify(args.status_char_uuid, status_callback)
|
||||||
|
print(f"Sending OTA image: {firmware} ({size} bytes)", flush=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(
|
||||||
|
client.write_gatt_char(
|
||||||
|
args.ota_control_char_uuid,
|
||||||
|
f"BEGIN,{size}".encode("ascii"),
|
||||||
|
response=True,
|
||||||
|
),
|
||||||
|
timeout=args.write_timeout,
|
||||||
|
)
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
|
sent = 0
|
||||||
|
last_percent = -1
|
||||||
|
with firmware.open("rb") as handle:
|
||||||
|
while True:
|
||||||
|
chunk = handle.read(args.chunk_size)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
await asyncio.wait_for(
|
||||||
|
client.write_gatt_char(
|
||||||
|
args.ota_data_char_uuid,
|
||||||
|
chunk,
|
||||||
|
response=args.write_with_response,
|
||||||
|
),
|
||||||
|
timeout=args.write_timeout,
|
||||||
|
)
|
||||||
|
sent += len(chunk)
|
||||||
|
percent = int(sent * 100 / size)
|
||||||
|
if percent != last_percent and (percent % 5 == 0 or percent == 100):
|
||||||
|
print(f"OTA progress: {percent}% ({sent}/{size})", flush=True)
|
||||||
|
last_percent = percent
|
||||||
|
if args.chunk_delay > 0:
|
||||||
|
await asyncio.sleep(args.chunk_delay)
|
||||||
|
|
||||||
|
await asyncio.wait_for(
|
||||||
|
client.write_gatt_char(args.ota_control_char_uuid, b"END", response=True),
|
||||||
|
timeout=args.write_timeout,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
await client.write_gatt_char(args.ota_control_char_uuid, b"ABORT", response=True)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
raise
|
||||||
|
|
||||||
|
print("OTA image sent. Waiting for board reboot notice...", flush=True)
|
||||||
|
await asyncio.sleep(args.finish_wait)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args():
|
||||||
|
default_firmware = Path(__file__).resolve().parents[2] / "build" / "rlcd_clock.bin"
|
||||||
|
parser = argparse.ArgumentParser(description="Send RLCD firmware over BLE OTA.")
|
||||||
|
parser.add_argument("--firmware", default=str(default_firmware))
|
||||||
|
parser.add_argument("--name", default=DEFAULT_DEVICE_NAME)
|
||||||
|
parser.add_argument("--address", default=None)
|
||||||
|
parser.add_argument("--status-char-uuid", default=DEFAULT_STATUS_CHAR_UUID)
|
||||||
|
parser.add_argument("--ota-control-char-uuid", default=DEFAULT_OTA_CONTROL_CHAR_UUID)
|
||||||
|
parser.add_argument("--ota-data-char-uuid", default=DEFAULT_OTA_DATA_CHAR_UUID)
|
||||||
|
parser.add_argument("--chunk-size", type=int, default=180)
|
||||||
|
parser.add_argument("--chunk-delay", type=float, default=0.01)
|
||||||
|
parser.add_argument("--write-timeout", type=float, default=10.0)
|
||||||
|
parser.add_argument("--write-with-response", action="store_true")
|
||||||
|
parser.add_argument("--scan-timeout", type=float, default=20.0)
|
||||||
|
parser.add_argument("--connect-timeout", type=float, default=20.0)
|
||||||
|
parser.add_argument("--finish-wait", type=float, default=8.0)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if os.name != "nt":
|
||||||
|
print("This helper is tuned for the current Windows BLE setup.", file=sys.stderr)
|
||||||
|
args = parse_args()
|
||||||
|
return asyncio.run(send_ota(args))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
51
tools/pc-metrics/install-ble-autostart.ps1
Normal file
51
tools/pc-metrics/install-ble-autostart.ps1
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
param(
|
||||||
|
[string]$TaskName = "RLCD Metrics BLE",
|
||||||
|
[double]$IntervalSeconds = 1,
|
||||||
|
[int]$StartupDelaySeconds = 20
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
$startScript = Join-Path $scriptDir "start-ble-metrics.ps1"
|
||||||
|
|
||||||
|
if (-not (Test-Path $startScript)) {
|
||||||
|
throw "Cannot find start script: $startScript"
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
||||||
|
$powershellExe = Join-Path $env:SystemRoot "System32\WindowsPowerShell\v1.0\powershell.exe"
|
||||||
|
$quotedStartScript = '"' + $startScript + '"'
|
||||||
|
$actionArgs = "-NoProfile -ExecutionPolicy Bypass -File $quotedStartScript -IntervalSeconds $IntervalSeconds"
|
||||||
|
|
||||||
|
$action = New-ScheduledTaskAction -Execute $powershellExe -Argument $actionArgs
|
||||||
|
$trigger = New-ScheduledTaskTrigger -AtLogOn -User $currentUser
|
||||||
|
if ($StartupDelaySeconds -gt 0) {
|
||||||
|
$trigger.Delay = "PT${StartupDelaySeconds}S"
|
||||||
|
}
|
||||||
|
|
||||||
|
$settings = New-ScheduledTaskSettingsSet `
|
||||||
|
-AllowStartIfOnBatteries `
|
||||||
|
-DontStopIfGoingOnBatteries `
|
||||||
|
-StartWhenAvailable `
|
||||||
|
-MultipleInstances IgnoreNew
|
||||||
|
|
||||||
|
$principal = New-ScheduledTaskPrincipal `
|
||||||
|
-UserId $currentUser `
|
||||||
|
-LogonType Interactive `
|
||||||
|
-RunLevel Limited
|
||||||
|
|
||||||
|
Register-ScheduledTask `
|
||||||
|
-TaskName $TaskName `
|
||||||
|
-Action $action `
|
||||||
|
-Trigger $trigger `
|
||||||
|
-Settings $settings `
|
||||||
|
-Principal $principal `
|
||||||
|
-Description "Start the RLCD BLE metrics bridge when the current Windows user logs on." `
|
||||||
|
-Force | Out-Null
|
||||||
|
|
||||||
|
Start-ScheduledTask -TaskName $TaskName
|
||||||
|
|
||||||
|
Write-Host "Installed scheduled task: $TaskName"
|
||||||
|
Write-Host "User: $currentUser"
|
||||||
|
Write-Host "Start script: $startScript"
|
||||||
@ -1059,7 +1059,33 @@ async def push_loop(args) -> int:
|
|||||||
|
|
||||||
def status_callback(_sender, data: bytearray):
|
def status_callback(_sender, data: bytearray):
|
||||||
text = bytes(data).decode("ascii", errors="replace").strip()
|
text = bytes(data).decode("ascii", errors="replace").strip()
|
||||||
if text:
|
if not text:
|
||||||
|
return
|
||||||
|
|
||||||
|
if text.startswith("STA2,"):
|
||||||
|
parts = text.split(",")
|
||||||
|
if len(parts) >= 10:
|
||||||
|
try:
|
||||||
|
battery = int(parts[2])
|
||||||
|
battery_mv = int(parts[3])
|
||||||
|
temperature_c10 = int(parts[4])
|
||||||
|
humidity = int(parts[5])
|
||||||
|
writes = int(parts[6])
|
||||||
|
uptime = int(parts[7])
|
||||||
|
internal_heap_kb = int(parts[8])
|
||||||
|
psram_kb = int(parts[9])
|
||||||
|
print(
|
||||||
|
"BLE board: "
|
||||||
|
f"bat={battery}% {battery_mv}mV "
|
||||||
|
f"env={temperature_c10 / 10:.1f}C/{humidity}% "
|
||||||
|
f"writes={writes} up={uptime}s "
|
||||||
|
f"heap={internal_heap_kb}K psram={psram_kb}K",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
print(f"BLE status: {text}", flush=True)
|
print(f"BLE status: {text}", flush=True)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
|||||||
40
tools/pc-metrics/send-ble-ota.ps1
Normal file
40
tools/pc-metrics/send-ble-ota.ps1
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
param(
|
||||||
|
[string]$Firmware = "",
|
||||||
|
[string]$Name = "RLCD-Metrics",
|
||||||
|
[int]$ChunkSize = 180,
|
||||||
|
[double]$ChunkDelay = 0.01,
|
||||||
|
[switch]$WriteWithResponse
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
$venvDir = Join-Path $scriptDir ".venv-ble"
|
||||||
|
$venvPythonExe = Join-Path $venvDir "Scripts\python.exe"
|
||||||
|
$requirements = Join-Path $scriptDir "requirements-ble.txt"
|
||||||
|
$otaScript = Join-Path $scriptDir "ble-ota.py"
|
||||||
|
|
||||||
|
if (-not $Firmware) {
|
||||||
|
$Firmware = Join-Path (Split-Path -Parent (Split-Path -Parent $scriptDir)) "build\rlcd_clock.bin"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path $venvPythonExe)) {
|
||||||
|
python -m venv $venvDir
|
||||||
|
& $venvPythonExe -m pip install --upgrade pip
|
||||||
|
& $venvPythonExe -m pip install -r $requirements
|
||||||
|
}
|
||||||
|
|
||||||
|
$args = @(
|
||||||
|
"-u",
|
||||||
|
$otaScript,
|
||||||
|
"--firmware", $Firmware,
|
||||||
|
"--name", $Name,
|
||||||
|
"--chunk-size", $ChunkSize,
|
||||||
|
"--chunk-delay", $ChunkDelay
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($WriteWithResponse) {
|
||||||
|
$args += "--write-with-response"
|
||||||
|
}
|
||||||
|
|
||||||
|
& $venvPythonExe @args
|
||||||
14
tools/pc-metrics/uninstall-ble-autostart.ps1
Normal file
14
tools/pc-metrics/uninstall-ble-autostart.ps1
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
param(
|
||||||
|
[string]$TaskName = "RLCD Metrics BLE"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
||||||
|
if ($null -eq $task) {
|
||||||
|
Write-Host "Scheduled task is not installed: $TaskName"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false
|
||||||
|
Write-Host "Removed scheduled task: $TaskName"
|
||||||
Loading…
x
Reference in New Issue
Block a user