diff --git a/docs/ble-autostart-ota.md b/docs/ble-autostart-ota.md new file mode 100644 index 0000000..d041950 --- /dev/null +++ b/docs/ble-autostart-ota.md @@ -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,,,,,,,, +``` + +电脑端日志会显示成更易读的形式: + +```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,` 并自动重启。重启后重新启动实时推送: + +```powershell +& "D:\AI Code\ESP-32\tools\pc-metrics\start-ble-metrics.ps1" +``` + +## 电量口径 + +电量百分比按微雪官方 ADC 示例口径换算: + +- 低于 `3.0V` 显示 `0%` +- 高于 `4.12V` 显示 `100%` +- 中间按线性百分比显示 + +USB 充电时看到的电压通常会受充电状态影响,所以百分比仍然是估算值,不是库仑计精度。 diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 7ad48e8..33db5ce 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -20,6 +20,7 @@ idf_component_register( esp_driver_usb_serial_jtag esp_driver_i2c esp_adc + app_update bt nvs_flash ) diff --git a/main/battery_monitor.c b/main/battery_monitor.c index 20d1467..60103c9 100644 --- a/main/battery_monitor.c +++ b/main/battery_monitor.c @@ -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; diff --git a/main/main.c b/main/main.c index 598b163..26918bd 100644 --- a/main/main.c +++ b/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(); diff --git a/main/metrics_ble.c b/main/metrics_ble.c index ec2d179..48306aa 100644 --- a/main/metrics_ble.c +++ b/main/metrics_ble.c @@ -2,8 +2,10 @@ #include #include +#include #include +#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; } diff --git a/main/metrics_ble.h b/main/metrics_ble.h index e00d168..2dd8c73 100644 --- a/main/metrics_ble.h +++ b/main/metrics_ble.h @@ -1,6 +1,7 @@ #pragma once #include +#include #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 } diff --git a/partitions.csv b/partitions.csv index 163e311..a25eee7 100644 --- a/partitions.csv +++ b/partitions.csv @@ -1,4 +1,6 @@ -# Name, Type, SubType, Offset, Size, Flags -nvs, data, nvs, 0x9000, 0x6000, +# Name, Type, SubType, Offset, Size, Flags +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, diff --git a/tools/pc-metrics/ble-ota.py b/tools/pc-metrics/ble-ota.py new file mode 100644 index 0000000..d549916 --- /dev/null +++ b/tools/pc-metrics/ble-ota.py @@ -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()) diff --git a/tools/pc-metrics/install-ble-autostart.ps1 b/tools/pc-metrics/install-ble-autostart.ps1 new file mode 100644 index 0000000..f167cf6 --- /dev/null +++ b/tools/pc-metrics/install-ble-autostart.ps1 @@ -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" diff --git a/tools/pc-metrics/push-metrics-ble.py b/tools/pc-metrics/push-metrics-ble.py index 3ede5da..7389fbc 100644 --- a/tools/pc-metrics/push-metrics-ble.py +++ b/tools/pc-metrics/push-metrics-ble.py @@ -1059,8 +1059,34 @@ async def push_loop(args) -> int: def status_callback(_sender, data: bytearray): text = bytes(data).decode("ascii", errors="replace").strip() - if text: - print(f"BLE status: {text}", flush=True) + 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: print(f"Scanning for {args.name}...", flush=True) diff --git a/tools/pc-metrics/send-ble-ota.ps1 b/tools/pc-metrics/send-ble-ota.ps1 new file mode 100644 index 0000000..7d8d243 --- /dev/null +++ b/tools/pc-metrics/send-ble-ota.ps1 @@ -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 diff --git a/tools/pc-metrics/uninstall-ble-autostart.ps1 b/tools/pc-metrics/uninstall-ble-autostart.ps1 new file mode 100644 index 0000000..d9b1dca --- /dev/null +++ b/tools/pc-metrics/uninstall-ble-autostart.ps1 @@ -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"