完善 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_i2c
|
||||
esp_adc
|
||||
app_update
|
||||
bt
|
||||
nvs_flash
|
||||
)
|
||||
|
||||
@ -18,8 +18,8 @@ static const char *TAG = "battery";
|
||||
#define BATTERY_ADC_CHANNEL ADC_CHANNEL_3
|
||||
#define BATTERY_ADC_ATTEN ADC_ATTEN_DB_12
|
||||
#define BATTERY_DIVIDER 3
|
||||
#define BATTERY_EMPTY_MV 2500
|
||||
#define BATTERY_FULL_MV 4200
|
||||
#define BATTERY_EMPTY_MV 3000
|
||||
#define BATTERY_FULL_MV 4120
|
||||
|
||||
static adc_oneshot_unit_handle_t s_adc_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_heap_caps.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_system.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "lvgl.h"
|
||||
@ -122,8 +123,18 @@ void app_main(void)
|
||||
TickType_t now_tick = xTaskGetTickCount();
|
||||
if ((now_tick - last_status_notify_tick) >= pdMS_TO_TICKS(5000)) {
|
||||
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,
|
||||
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();
|
||||
|
||||
@ -2,8 +2,10 @@
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "esp_ota_ops.h"
|
||||
#include "esp_err.h"
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
@ -27,6 +29,8 @@ static const char *DEVICE_NAME = "RLCD-Metrics";
|
||||
// Service: 9f4af548-1a14-4fc1-8f5a-4b9f00000001
|
||||
// Write char: 9f4af548-1a14-4fc1-8f5a-4b9f00000002
|
||||
// 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 =
|
||||
BLE_UUID128_INIT(0x01, 0x00, 0x00, 0x00, 0x9f, 0x4b, 0x5a, 0x8f,
|
||||
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 =
|
||||
BLE_UUID128_INIT(0x03, 0x00, 0x00, 0x00, 0x9f, 0x4b, 0x5a, 0x8f,
|
||||
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 uint16_t metrics_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 bool s_status_notify_enabled;
|
||||
static QueueHandle_t s_metrics_queue;
|
||||
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);
|
||||
|
||||
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)
|
||||
{
|
||||
switch (phy) {
|
||||
@ -82,10 +124,51 @@ static int metrics_gatt_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
(void)conn_handle;
|
||||
(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;
|
||||
}
|
||||
|
||||
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};
|
||||
uint16_t len = 0;
|
||||
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';
|
||||
|
||||
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;
|
||||
if (!metrics_parse_line(line, &metrics)) {
|
||||
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,
|
||||
.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},
|
||||
},
|
||||
},
|
||||
@ -299,29 +481,27 @@ bool metrics_ble_take_latest(ui_metrics_t *metrics)
|
||||
}
|
||||
|
||||
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) {
|
||||
return false;
|
||||
}
|
||||
|
||||
char line[96];
|
||||
int len = snprintf(line, sizeof(line), "STA,%d,%d,%d,%d,%lu",
|
||||
char line[128];
|
||||
int len = snprintf(line, sizeof(line), "STA2,1,%d,%d,%d,%d,%lu,%lu,%lu,%lu",
|
||||
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)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
struct os_mbuf *om = ble_hs_mbuf_from_flat(line, len);
|
||||
if (om == NULL) {
|
||||
return false;
|
||||
}
|
||||
return metrics_notify_text(line);
|
||||
}
|
||||
|
||||
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);
|
||||
bool metrics_ble_take_ota_restart_request(void)
|
||||
{
|
||||
if (!s_ota_restart_requested) {
|
||||
return false;
|
||||
}
|
||||
s_ota_restart_requested = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "ui_screen.h"
|
||||
|
||||
@ -11,7 +12,10 @@ extern "C" {
|
||||
void metrics_ble_start(void);
|
||||
bool metrics_ble_take_latest(ui_metrics_t *metrics);
|
||||
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
|
||||
}
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
# 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,
|
||||
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):
|
||||
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)
|
||||
|
||||
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