完善 BLE 自启和 OTA 底座

This commit is contained in:
Codex 2026-06-24 21:01:33 +08:00
parent 1aaca3d83c
commit 6eca70ebb6
12 changed files with 559 additions and 25 deletions

71
docs/ble-autostart-ota.md Normal file
View 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 充电时看到的电压通常会受充电状态影响,所以百分比仍然是估算值,不是库仑计精度。

View File

@ -20,6 +20,7 @@ idf_component_register(
esp_driver_usb_serial_jtag
esp_driver_i2c
esp_adc
app_update
bt
nvs_flash
)

View File

@ -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;

View File

@ -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();

View File

@ -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;
}

View File

@ -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
}

View File

@ -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,

1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x6000 0x4000
3 otadata data ota 0xd000 0x2000
4 phy_init data phy 0xf000 0x1000
5 factory ota_0 app factory ota_0 0x10000 0x20000 0x400000 0x3f0000
6 ota_1 app ota_1 0x3f0000

134
tools/pc-metrics/ble-ota.py Normal file
View 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())

View 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"

View File

@ -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:

View 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

View 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"