Add runtime UV protection toggle and treat near-zero sense as external power.

Per-pod NVS enable (default off) via UART/ESP-NOW/dashboard, and skip UV latch when LiPo ADC reads ~0 mV so USB/external supply still boots.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-01 22:51:17 +01:00
co-authored by Cursor
parent c3b20887bf
commit 7f28a48a10
33 changed files with 1411 additions and 240 deletions
+9 -6
View File
@@ -429,16 +429,18 @@ Spannungen in Millivolt in Registry und `BATTERY_STATUS`-UART.
### Software-UV-Schutz (`battery_uv.c`) ### Software-UV-Schutz (`battery_uv.c`)
Aktivierbar mit `POWERPOD_BATTERY_UV_ENABLE` in `powerpod.h` (CMake-Override möglich). Aktivierbar mit `POWERPOD_BATTERY_UV_ENABLE` in `powerpod.h` (CMake-Override möglich). Runtime zusätzlich NVS `uv_en` (Default **aus**) — UART `BATTERY_UV` / Dashboard / ESP-NOW `ESPNOW_SET_BATTERY_UV`.
| Zustand | ADC-Schwelle | Logik | | Zustand | ADC-Schwelle | Logik |
|---------|--------------|-------| |---------|--------------|-------|
| UV | &lt; 2300 mV | Ein gültiger Kanal reicht | | Extern / kein Pack | &lt; 100 mV | Alle gültigen Kanäle → normaler Boot |
| Geladen | ≥ 3000 mV | Alle gültigen Kanäle | | UV | 100…2299 mV | Ein gültiger Kanal reicht |
| Geladen | ≥ 3000 mV | Alle nicht-absent gültigen Kanäle |
- NVS-Key `uv_latch` in `pod_settings.c` - NVS-Keys `uv_latch`, `uv_en` in `pod_settings.c`
- Boot: ADC früh, bei UV nur LED + ADC-Poll (kein ESP-NOW/WiFi/UART/I2C/Button) - Boot: ADC früh, bei UV nur LED + ADC-Poll (kein ESP-NOW/WiFi/UART/I2C/Button); übersprungen wenn `uv_en=0`
- Runtime: `battery_uv_poll()` im LiPo-Monitor (2 Samples Debounce) - Runtime: `battery_uv_poll()` im LiPo-Monitor (2 Samples Debounce), gated by `uv_en`
- UART `BATTERY_UV`: `client_id` / `all_clients`; Disable cleart Latch
- LED-Anzeige: Mode `6` battery-low (`led_ring_show_battery_low`) - LED-Anzeige: Mode `6` battery-low (`led_ring_show_battery_low`)
--- ---
@@ -452,6 +454,7 @@ Aktivierbar mit `POWERPOD_BATTERY_UV_ENABLE` in `powerpod.h` (CMake-Override mö
| `accel_dz` | Accel-Deadzone LSB | | `accel_dz` | Accel-Deadzone LSB |
| `tap_cfg` | BMA456 Multitap-Config (Blob) | | `tap_cfg` | BMA456 Multitap-Config (Blob) |
| `uv_latch` | Software-UV-Zustand (`1` = Energiesparmodus) | | `uv_latch` | Software-UV-Zustand (`1` = Energiesparmodus) |
| `uv_en` | Runtime-Enable UV-Schutz (`0`/`1`, fehlend = aus) |
Gespeichert bei lokalem Set (UART `client_id=0`, ESP-NOW auf Slave). Geladen nach `init_bma456()`. Gespeichert bei lokalem Set (UART `client_id=0`, ESP-NOW auf Slave). Geladen nach `init_bma456()`.
+1
View File
@@ -38,6 +38,7 @@ go run . -port /dev/ttyUSB0 clients
| `find-me` | 22 | Locate pod (`-client 0` master, `>0` slave via ESP-NOW) | | `find-me` | 22 | Locate pod (`-client 0` master, `>0` slave via ESP-NOW) |
| `restart` | 23 | Reboot master or slave (`-client 0` / `>0`) | | `restart` | 23 | Reboot master or slave (`-client 0` / `>0`) |
| `log-level` | `0x1f` | Get/set master ESP-IDF log level for tag `"*"` (`-set`, `-level` 0–5); output on UART0 debug, not host UART | | `log-level` | `0x1f` | Get/set master ESP-IDF log level for tag `"*"` (`-set`, `-level` 0–5); output on UART0 debug, not host UART |
| `battery-uv` | `0x24` | Get/set software LiPo UV protection (`-set -enable`/`-enable=false`, `-client`, `-all`) |
| `input-send-mode` | `0x23` | Get/set slave accel/tap ESP-NOW TX path (`-set -mode 0..3`, `-all` or `-client N`): best_effort/fast/wait/reliable | | `input-send-mode` | `0x23` | Get/set slave accel/tap ESP-NOW TX path (`-set -mode 0..3`, `-all` or `-client N`): best_effort/fast/wait/reliable |
`clients` requires slaves to have responded to master discover broadcasts first. `clients` requires slaves to have responded to master discover broadcasts first.
+140
View File
@@ -0,0 +1,140 @@
package main
import (
"encoding/json"
"fmt"
"net/http"
"powerpod/gotool/pb"
)
type batteryUvAPIRequest struct {
Write bool `json:"write"`
Enabled bool `json:"enabled"`
ClientID uint32 `json:"client_id"`
AllClients bool `json:"all_clients"`
// SlavesOnly: with all_clients, push to ESP-NOW slaves only (master NVS unchanged).
SlavesOnly bool `json:"slaves_only"`
}
type batteryUvAPIResponse struct {
ClientID uint32 `json:"client_id"`
Success bool `json:"success"`
Enabled bool `json:"enabled"`
Latched bool `json:"latched"`
SlavesUpdated uint32 `json:"slaves_updated"`
Error string `json:"error,omitempty"`
}
func mountBatteryUvAPI(mux *http.ServeMux, link *managedSerial) {
mux.HandleFunc("/api/battery-uv", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
serveBatteryUvGet(w, r, link)
case http.MethodPost:
serveBatteryUvPost(w, r, link)
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
})
}
func serveBatteryUvGet(w http.ResponseWriter, r *http.Request, link *managedSerial) {
clientID, err := parseUintQuery(r, "client_id", 0)
if err != nil {
writeJSON(w, http.StatusBadRequest, batteryUvAPIResponse{Error: err.Error()})
return
}
resp, err := link.BatteryUv(&pb.BatteryUvRequest{
Write: false,
ClientId: clientID,
})
if err != nil {
writeJSON(w, http.StatusServiceUnavailable, batteryUvAPIResponse{
ClientID: clientID,
Error: err.Error(),
})
return
}
writeJSON(w, http.StatusOK, batteryUvAPIResponse{
ClientID: resp.GetClientId(),
Success: resp.GetSuccess(),
Enabled: resp.GetEnabled(),
Latched: resp.GetLatched(),
SlavesUpdated: resp.GetSlavesUpdated(),
})
}
func serveBatteryUvPost(w http.ResponseWriter, r *http.Request, link *managedSerial) {
var body batteryUvAPIRequest
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeJSON(w, http.StatusBadRequest, batteryUvAPIResponse{Error: "invalid JSON"})
return
}
if body.AllClients && body.SlavesOnly {
updated, err := applyBatteryUvToSlaves(link, body.Enabled)
if err != nil {
writeJSON(w, http.StatusServiceUnavailable, batteryUvAPIResponse{
Enabled: body.Enabled,
Error: err.Error(),
})
return
}
writeJSON(w, http.StatusOK, batteryUvAPIResponse{
Success: updated > 0,
Enabled: body.Enabled,
SlavesUpdated: updated,
})
return
}
resp, err := link.BatteryUv(&pb.BatteryUvRequest{
Write: true,
Enabled: body.Enabled,
ClientId: body.ClientID,
AllClients: body.AllClients,
})
if err != nil {
writeJSON(w, http.StatusServiceUnavailable, batteryUvAPIResponse{
ClientID: body.ClientID,
Enabled: body.Enabled,
Error: err.Error(),
})
return
}
writeJSON(w, http.StatusOK, batteryUvAPIResponse{
ClientID: resp.GetClientId(),
Success: resp.GetSuccess(),
Enabled: resp.GetEnabled(),
Latched: resp.GetLatched(),
SlavesUpdated: resp.GetSlavesUpdated(),
})
}
func applyBatteryUvToSlaves(link *managedSerial, enabled bool) (uint32, error) {
clients, err := link.listClients()
if err != nil {
return 0, err
}
var updated uint32
for _, c := range clients {
resp, err := link.BatteryUv(&pb.BatteryUvRequest{
Write: true,
Enabled: enabled,
ClientId: c.GetId(),
})
if err != nil {
continue
}
if resp.GetSuccess() {
updated++
}
}
if len(clients) == 0 {
return 0, fmt.Errorf("no slaves registered")
}
if updated == 0 {
return 0, fmt.Errorf("UV protection not applied to any slave")
}
return updated, nil
}
+1
View File
@@ -105,6 +105,7 @@ func mountServeAPI(mux *http.ServeMux, link *managedSerial, hub *wsHub, streamCt
mountInputSendModeAPI(mux, link) mountInputSendModeAPI(mux, link)
mountTapAPI(mux, link, hub, tapCtl) mountTapAPI(mux, link, hub, tapCtl)
mountTapConfigAPI(mux, link) mountTapConfigAPI(mux, link)
mountBatteryUvAPI(mux, link)
mountLedRingAPI(mux, link) mountLedRingAPI(mux, link)
mountBatteryAPI(mux, link) mountBatteryAPI(mux, link)
mux.HandleFunc("/api/deadzone", func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("/api/deadzone", func(w http.ResponseWriter, r *http.Request) {
+69
View File
@@ -290,6 +290,44 @@ func (m *managedSerial) tapConfigExchange(poll bool, req *pb.TapConfigRequest) (
return r, nil return r, nil
} }
func (m *managedSerial) BatteryUv(req *pb.BatteryUvRequest) (*pb.BatteryUvResponse, error) {
return m.batteryUvExchange(false, req)
}
func (m *managedSerial) BatteryUvPoll(req *pb.BatteryUvRequest) (*pb.BatteryUvResponse, error) {
return m.batteryUvExchange(true, req)
}
func (m *managedSerial) batteryUvExchange(poll bool, req *pb.BatteryUvRequest) (*pb.BatteryUvResponse, error) {
msg := &pb.UartMessage{
Type: pb.MessageType_BATTERY_UV,
Payload: &pb.UartMessage_BatteryUvRequest{BatteryUvRequest: req},
}
body, err := proto.Marshal(msg)
if err != nil {
return nil, fmt.Errorf("encode: %w", err)
}
payload := append([]byte{byte(pb.MessageType_BATTERY_UV)}, body...)
var respPayload []byte
if poll {
respPayload, err = m.exchangePayloadPoll(payload, "BATTERY_UV")
} else {
respPayload, err = m.exchangePayload(payload, "BATTERY_UV")
}
if err != nil {
return nil, err
}
var respMsg pb.UartMessage
if err := proto.Unmarshal(respPayload[1:], &respMsg); err != nil {
return nil, fmt.Errorf("decode: %w", err)
}
r := respMsg.GetBatteryUvResponse()
if r == nil {
return nil, fmt.Errorf("missing battery_uv_response")
}
return r, nil
}
func (m *managedSerial) accelDeadzoneExchange(poll bool, req *pb.AccelDeadzoneRequest) (*pb.AccelDeadzoneResponse, error) { func (m *managedSerial) accelDeadzoneExchange(poll bool, req *pb.AccelDeadzoneRequest) (*pb.AccelDeadzoneResponse, error) {
msg := &pb.UartMessage{ msg := &pb.UartMessage{
Type: pb.MessageType_ACCEL_DEADZONE, Type: pb.MessageType_ACCEL_DEADZONE,
@@ -510,6 +548,33 @@ func (s *serialPort) tapConfig(req *pb.TapConfigRequest) (*pb.TapConfigResponse,
return r, nil return r, nil
} }
func (s *serialPort) batteryUv(req *pb.BatteryUvRequest) (*pb.BatteryUvResponse, error) {
msg := &pb.UartMessage{
Type: pb.MessageType_BATTERY_UV,
Payload: &pb.UartMessage_BatteryUvRequest{
BatteryUvRequest: req,
},
}
body, err := proto.Marshal(msg)
if err != nil {
return nil, fmt.Errorf("encode: %w", err)
}
payload := append([]byte{byte(pb.MessageType_BATTERY_UV)}, body...)
respPayload, err := s.exchangePayload(payload, "BATTERY_UV")
if err != nil {
return nil, err
}
var respMsg pb.UartMessage
if err := proto.Unmarshal(respPayload[1:], &respMsg); err != nil {
return nil, fmt.Errorf("decode: %w", err)
}
r := respMsg.GetBatteryUvResponse()
if r == nil {
return nil, fmt.Errorf("missing battery_uv_response")
}
return r, nil
}
func (s *serialPort) espnowUnicastTest(clientID, seq uint32) (*pb.EspNowUnicastTestResponse, error) { func (s *serialPort) espnowUnicastTest(clientID, seq uint32) (*pb.EspNowUnicastTestResponse, error) {
req := &pb.EspNowUnicastTestRequest{ClientId: clientID, Seq: seq} req := &pb.EspNowUnicastTestRequest{ClientId: clientID, Seq: seq}
msg := &pb.UartMessage{ msg := &pb.UartMessage{
@@ -681,6 +746,10 @@ func (s *serialPort) TapConfig(req *pb.TapConfigRequest) (*pb.TapConfigResponse,
return s.tapConfig(req) return s.tapConfig(req)
} }
func (s *serialPort) BatteryUv(req *pb.BatteryUvRequest) (*pb.BatteryUvResponse, error) {
return s.batteryUv(req)
}
func (s *serialPort) EspnowUnicastTest(clientID, seq uint32) (*pb.EspNowUnicastTestResponse, error) { func (s *serialPort) EspnowUnicastTest(clientID, seq uint32) (*pb.EspNowUnicastTestResponse, error) {
return s.espnowUnicastTest(clientID, seq) return s.espnowUnicastTest(clientID, seq)
} }
+34
View File
@@ -0,0 +1,34 @@
package main
import (
"flag"
"fmt"
"powerpod/gotool/pb"
)
func runBatteryUv(sp *serialPort, args []string) error {
fs := flag.NewFlagSet("battery-uv", flag.ExitOnError)
write := fs.Bool("set", false, "write UV protection enable (default: read)")
enabled := fs.Bool("enable", false, "enable software UV protection")
clientID := fs.Uint("client", 0, "client id (0=local master)")
all := fs.Bool("all", false, "apply to master + all registered slaves")
if err := fs.Parse(args); err != nil {
return err
}
r, err := sp.batteryUv(&pb.BatteryUvRequest{
Write: *write,
Enabled: *enabled,
ClientId: uint32(*clientID),
AllClients: *all,
})
if err != nil {
return err
}
fmt.Printf("client_id=%d success=%v enabled=%v latched=%v slaves_updated=%d\n",
r.GetClientId(), r.GetSuccess(), r.GetEnabled(), r.GetLatched(),
r.GetSlavesUpdated())
return nil
}
+23
View File
@@ -20,6 +20,8 @@ type MasterView struct {
RunningPartition string `json:"running_partition,omitempty"` RunningPartition string `json:"running_partition,omitempty"`
Deadzone uint32 `json:"deadzone,omitempty"` Deadzone uint32 `json:"deadzone,omitempty"`
TapConfig *TapConfigView `json:"tap_config,omitempty"` TapConfig *TapConfigView `json:"tap_config,omitempty"`
UvProtection *bool `json:"uv_protection,omitempty"`
UvLatched bool `json:"uv_latched,omitempty"`
Lipo1 lipoReadingJSON `json:"lipo1"` Lipo1 lipoReadingJSON `json:"lipo1"`
Lipo2 lipoReadingJSON `json:"lipo2"` Lipo2 lipoReadingJSON `json:"lipo2"`
BatteryAgeMs uint32 `json:"battery_age_ms,omitempty"` BatteryAgeMs uint32 `json:"battery_age_ms,omitempty"`
@@ -58,6 +60,7 @@ type ClientView struct {
TapNotifySingle bool `json:"tap_notify_single"` TapNotifySingle bool `json:"tap_notify_single"`
TapNotifyDouble bool `json:"tap_notify_double"` TapNotifyDouble bool `json:"tap_notify_double"`
TapNotifyTriple bool `json:"tap_notify_triple"` TapNotifyTriple bool `json:"tap_notify_triple"`
UvProtection bool `json:"uv_protection"`
LastTap string `json:"last_tap,omitempty"` LastTap string `json:"last_tap,omitempty"`
LastTapAt int64 `json:"last_tap_at,omitempty"` LastTapAt int64 `json:"last_tap_at,omitempty"`
Lipo1 lipoReadingJSON `json:"lipo1"` Lipo1 lipoReadingJSON `json:"lipo1"`
@@ -700,6 +703,11 @@ func pollDashboard(link *managedSerial, portName string, last *DashboardState, s
if tc, err := readTapConfigPoll(link, 0); err == nil { if tc, err := readTapConfigPoll(link, 0); err == nil {
st.Master.TapConfig = tc st.Master.TapConfig = tc
} }
if uv, err := readBatteryUvPoll(link, 0); err == nil {
en := uv.GetEnabled()
st.Master.UvProtection = &en
st.Master.UvLatched = uv.GetLatched()
}
clients, err := link.listClientsPoll() clients, err := link.listClientsPoll()
if err != nil { if err != nil {
@@ -726,6 +734,7 @@ func pollDashboard(link *managedSerial, portName string, last *DashboardState, s
TapNotifySingle: c.GetTapNotifySingle(), TapNotifySingle: c.GetTapNotifySingle(),
TapNotifyDouble: c.GetTapNotifyDouble(), TapNotifyDouble: c.GetTapNotifyDouble(),
TapNotifyTriple: c.GetTapNotifyTriple(), TapNotifyTriple: c.GetTapNotifyTriple(),
UvProtection: c.GetUvProtectionEnabled(),
} }
st.Clients = append(st.Clients, cv) st.Clients = append(st.Clients, cv)
} }
@@ -928,6 +937,20 @@ func tapConfigViewFromPB(r *pb.TapConfigResponse) *TapConfigView {
} }
} }
func readBatteryUvPoll(link *managedSerial, clientID uint32) (*pb.BatteryUvResponse, error) {
r, err := link.BatteryUvPoll(&pb.BatteryUvRequest{
Write: false,
ClientId: clientID,
})
if err != nil {
return nil, err
}
if !r.GetSuccess() {
return nil, fmt.Errorf("battery uv read failed for client %d", clientID)
}
return r, nil
}
func readTapConfigPoll(link *managedSerial, clientID uint32) (*TapConfigView, error) { func readTapConfigPoll(link *managedSerial, clientID uint32) (*TapConfigView, error) {
r, err := link.TapConfigPoll(&pb.TapConfigRequest{ r, err := link.TapConfigPoll(&pb.TapConfigRequest{
Write: false, Write: false,
+26
View File
@@ -400,6 +400,31 @@ go run . -port /dev/ttyUSB0 log-level -set -level 3
Dashboard: Master card → dropdown **ESP Log-Level** with **Lesen** / **Setzen**. Dashboard: Master card → dropdown **ESP Log-Level** with **Lesen** / **Setzen**.
### Battery UV protection
Runtime enable for software LiPo under-voltage protection (NVS `uv_en` per pod). Disable also clears `uv_latch`. `all_clients` applies to master + every registered slave; `slaves_only` with `all_clients` skips the master.
```http
GET /api/battery-uv?client_id=0
POST /api/battery-uv
Content-Type: application/json
{"write": true, "enabled": false, "client_id": 0}
{"write": true, "enabled": false, "all_clients": true}
{"write": true, "enabled": false, "all_clients": true, "slaves_only": true}
```
```json
{"success": true, "client_id": 0, "enabled": false, "latched": false, "slaves_updated": 0}
```
```bash
go run . -port /dev/ttyUSB0 battery-uv
go run . -port /dev/ttyUSB0 battery-uv -set -enable=false -client 0
go run . -port /dev/ttyUSB0 battery-uv -set -enable=false -all
```
Dashboard: Master **UV-Schutz** checkbox + **Alle an/aus**; per-slave **UV** column.
### Input send mode (slave accel/tap ESP-NOW TX path) ### Input send mode (slave accel/tap ESP-NOW TX path)
Debug A/B for how slaves transmit `ACCEL_SAMPLE` / `TAP_EVENT`. Default on slaves is `best_effort` (0). Master remembers the last mode for GET; write pushes via ESP-NOW. Debug A/B for how slaves transmit `ACCEL_SAMPLE` / `TAP_EVENT`. Default on slaves is `best_effort` (0). Master remembers the last mode for GET; write pushes via ESP-NOW.
@@ -486,6 +511,7 @@ Same as external API:
| Unicast test | `POST /api/unicast-test` | | Unicast test | `POST /api/unicast-test` |
| Echo ping | `POST /api/echo-ping` (per-slave latency; UI shows Host ms + ESP ms) | | Echo ping | `POST /api/echo-ping` (per-slave latency; UI shows Host ms + ESP ms) |
| Master log level | `GET` / `POST /api/log-level` or CLI `log-level` | | Master log level | `GET` / `POST /api/log-level` or CLI `log-level` |
| UV protection | `GET` / `POST /api/battery-uv` or CLI `battery-uv` |
| Input-Send mode | `GET` / `PUT /api/input-send-mode` or CLI `input-send-mode` | | Input-Send mode | `GET` / `PUT /api/input-send-mode` or CLI `input-send-mode` |
| Tap notify S/D/T | `PUT /api/clients/{id}/tap-notify` | | Tap notify S/D/T | `PUT /api/clients/{id}/tap-notify` |
| Tap receive (UI) | Live stream + tap notify; see WebSocket doc for external API | | Tap receive (UI) | Live stream + tap notify; see WebSocket doc for external API |
+8
View File
@@ -35,6 +35,7 @@ func usage() {
fmt.Fprintf(os.Stderr, " find-me blink LED ring red/green/blue (3× each, full brightness)\n") fmt.Fprintf(os.Stderr, " find-me blink LED ring red/green/blue (3× each, full brightness)\n")
fmt.Fprintf(os.Stderr, " restart reboot master or slave (ESP-NOW)\n") fmt.Fprintf(os.Stderr, " restart reboot master or slave (ESP-NOW)\n")
fmt.Fprintf(os.Stderr, " log-level get/set master ESP-IDF log level (global)\n") fmt.Fprintf(os.Stderr, " log-level get/set master ESP-IDF log level (global)\n")
fmt.Fprintf(os.Stderr, " battery-uv get/set software LiPo UV protection (per pod / all)\n")
fmt.Fprintf(os.Stderr, " input-send-mode get/set slave accel/tap ESP-NOW send path\n\n") fmt.Fprintf(os.Stderr, " input-send-mode get/set slave accel/tap ESP-NOW send path\n\n")
flag.PrintDefaults() flag.PrintDefaults()
} }
@@ -138,6 +139,13 @@ func main() {
break break
} }
runErr = runTapConfig(sp, flag.Args()[1:]) runErr = runTapConfig(sp, flag.Args()[1:])
case "battery-uv", "battery_uv":
sp, ok := h.(*serialPort)
if !ok {
runErr = fmt.Errorf("%s over BLE not wired yet; use -port", cmd)
break
}
runErr = runBatteryUv(sp, flag.Args()[1:])
case "tap-notify", "tap_notify": case "tap-notify", "tap_notify":
sp, ok := h.(*serialPort) sp, ok := h.(*serialPort)
if !ok { if !ok {
File diff suppressed because it is too large Load Diff
+134 -1
View File
@@ -383,6 +383,11 @@
<dd class="col-7" x-text="formatLipo(state.master?.lipo1)"></dd> <dd class="col-7" x-text="formatLipo(state.master?.lipo1)"></dd>
<dt class="col-5 text-muted">LiPo 2</dt> <dt class="col-5 text-muted">LiPo 2</dt>
<dd class="col-7" x-text="formatLipo(state.master?.lipo2)"></dd> <dd class="col-7" x-text="formatLipo(state.master?.lipo2)"></dd>
<dt class="col-5 text-muted">UV-Schutz</dt>
<dd class="col-7">
<span x-text="state.master?.uv_protection == null ? '—' : (state.master.uv_protection ? 'an' : 'aus')"></span>
<span class="text-warning small ms-1" x-show="state.master?.uv_latched">· gelatcht</span>
</dd>
</dl> </dl>
</template> </template>
<template x-if="state.master && !state.master.ok"> <template x-if="state.master && !state.master.ok">
@@ -512,6 +517,31 @@
Setzen Setzen
</button> </button>
</div> </div>
<h6 class="text-secondary mb-2 mt-3">UV-Schutz (LiPo)</h6>
<div class="d-flex flex-wrap gap-2 align-items-center">
<label class="tap-toggle mb-0">
<input type="checkbox"
:checked="!!state.master?.uv_protection"
@change="setBatteryUv(0, $event.target.checked)"
:disabled="busy || !hostUp()">
Master aktiv
</label>
<button type="button" class="btn btn-outline-secondary btn-sm"
@click="setBatteryUvAll(true)"
:disabled="busy || !hostUp()"
title="UV-Schutz auf Master + alle Slaves einschalten">
Alle an
</button>
<button type="button" class="btn btn-outline-warning btn-sm"
@click="setBatteryUvAll(false)"
:disabled="busy || !hostUp()"
title="UV-Schutz auf Master + alle Slaves ausschalten (cleart Latch)">
Alle aus
</button>
</div>
<p class="text-muted small mt-1 mb-0">
Runtime-NVS pro Pod. Im Energiesparmodus (gelatcht) kein Host-Zugriff.
</p>
<p class="text-muted small mt-2 mb-0" x-show="!hostUp()"> <p class="text-muted small mt-2 mb-0" x-show="!hostUp()">
Host nicht verbunden — Eingabe gesperrt. Host nicht verbunden — Eingabe gesperrt.
</p> </p>
@@ -540,6 +570,12 @@
title="Master-Tap-Config an alle Slaves (ESP-NOW)"> title="Master-Tap-Config an alle Slaves (ESP-NOW)">
Tap-Cfg alle Tap-Cfg alle
</button> </button>
<button type="button" class="btn btn-outline-warning btn-sm"
@click="setBatteryUvSlavesOnly(false)"
:disabled="busy || !hostUp() || !(state.clients || []).length"
title="UV-Schutz nur auf allen Slaves ausschalten">
UV Slaves aus
</button>
<button type="button" class="btn btn-outline-danger btn-sm" <button type="button" class="btn btn-outline-danger btn-sm"
@click="resetClientIdMap()" @click="resetClientIdMap()"
:disabled="busy || !hostUp()" :disabled="busy || !hostUp()"
@@ -611,6 +647,7 @@
<th>Tap-Cfg</th> <th>Tap-Cfg</th>
<th>Accel (LSB)</th> <th>Accel (LSB)</th>
<th>Akku</th> <th>Akku</th>
<th>UV</th>
<th>Accel</th> <th>Accel</th>
<th>Tap-Notify</th> <th>Tap-Notify</th>
<th>Tap</th> <th>Tap</th>
@@ -619,7 +656,7 @@
</thead> </thead>
<tbody> <tbody>
<template x-if="!(state.clients || []).length"> <template x-if="!(state.clients || []).length">
<tr><td colspan="12" class="text-muted text-center py-4">No clients</td></tr> <tr><td colspan="13" class="text-muted text-center py-4">No clients</td></tr>
</template> </template>
<template x-for="c in (state.clients || [])" :key="c.id + c.mac"> <template x-for="c in (state.clients || [])" :key="c.id + c.mac">
<tr> <tr>
@@ -648,6 +685,14 @@
:title="accelTitle(c)"></span> :title="accelTitle(c)"></span>
</td> </td>
<td class="small" x-text="formatLipoPair(c)" :title="lipoTitle(c)"></td> <td class="small" x-text="formatLipoPair(c)" :title="lipoTitle(c)"></td>
<td>
<label class="tap-toggle mb-0" title="Software-UV-Schutz (NVS auf Slave)">
<input type="checkbox"
:checked="!!c.uv_protection"
@change="setBatteryUv(c.id, $event.target.checked)"
:disabled="busy || !hostUp() || !c.available">
</label>
</td>
<td> <td>
<button type="button" <button type="button"
class="btn btn-sm" class="btn btn-sm"
@@ -1783,6 +1828,94 @@
this.busy = false; this.busy = false;
} }
}, },
patchBatteryUv(clientId, enabled, latched) {
if (clientId === 0) {
const master = {
...(this.state.master || {}),
uv_protection: !!enabled,
uv_latched: !!latched
};
this.state = { ...this.state, master };
return;
}
const clients = (this.state.clients || []).map((c) =>
c.id === clientId ? { ...c, uv_protection: !!enabled } : c);
this.state = { ...this.state, clients };
},
async setBatteryUv(clientId, enabled) {
this.busy = true;
try {
const r = await fetch('/api/battery-uv', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ write: true, enabled: !!enabled, client_id: clientId })
});
const data = await r.json();
if (!r.ok || !data.success) {
this.flash(data.error || `UV-Schutz ${clientId === 0 ? 'Master' : 'Slave ' + clientId} fehlgeschlagen`, false);
return;
}
this.patchBatteryUv(clientId, data.enabled, data.latched);
const who = clientId === 0 ? 'Master' : `Slave ${clientId}`;
this.flash(`${who}: UV-Schutz ${data.enabled ? 'an' : 'aus'}`, true);
} catch (e) {
this.flash(String(e), false);
} finally {
this.busy = false;
}
},
async setBatteryUvAll(enabled) {
this.busy = true;
try {
const r = await fetch('/api/battery-uv', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ write: true, enabled: !!enabled, all_clients: true })
});
const data = await r.json();
if (!r.ok || !data.success) {
this.flash(data.error || 'UV-Schutz für alle fehlgeschlagen', false);
return;
}
this.patchBatteryUv(0, data.enabled, data.latched);
for (const c of (this.state.clients || [])) {
this.patchBatteryUv(c.id, !!enabled, false);
}
this.flash(`UV-Schutz alle ${enabled ? 'an' : 'aus'} (${data.slaves_updated || 0} Slaves)`, true);
} catch (e) {
this.flash(String(e), false);
} finally {
this.busy = false;
}
},
async setBatteryUvSlavesOnly(enabled) {
this.busy = true;
try {
const r = await fetch('/api/battery-uv', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
write: true,
enabled: !!enabled,
all_clients: true,
slaves_only: true
})
});
const data = await r.json();
if (!r.ok || !data.success) {
this.flash(data.error || 'UV-Schutz Slaves fehlgeschlagen', false);
return;
}
for (const c of (this.state.clients || [])) {
this.patchBatteryUv(c.id, !!enabled, false);
}
this.flash(`UV-Schutz Slaves ${enabled ? 'an' : 'aus'} (${data.slaves_updated || 0})`, true);
} catch (e) {
this.flash(String(e), false);
} finally {
this.busy = false;
}
},
patchLiveStream(enabled) { patchLiveStream(enabled) {
let clients = this.state.clients || []; let clients = this.state.clients || [];
if (!enabled) { if (!enabled) {
+1
View File
@@ -34,6 +34,7 @@ idf_component_register(
"pod_reboot.c" "pod_reboot.c"
"cmd/cmd_led_ring.c" "cmd/cmd_led_ring.c"
"cmd/cmd_battery.c" "cmd/cmd_battery.c"
"cmd/cmd_battery_uv.c"
"cmd/cmd_set_log_level.c" "cmd/cmd_set_log_level.c"
"cmd/cmd_ota.c" "cmd/cmd_ota.c"
"cmd/cmd_ota_slave_progress.c" "cmd/cmd_ota_slave_progress.c"
+10 -4
View File
@@ -121,6 +121,7 @@ Schema: `proto/esp_now_messages.proto`. Encode/decode: `esp_now_proto.c`. The ES
| `ESPNOW_HEARTBEAT` | Slave → master | `EspNowSlavePresence` (same fields) | | `ESPNOW_HEARTBEAT` | Slave → master | `EspNowSlavePresence` (same fields) |
| `ESPNOW_SET_ACCEL_DEADZONE` | Master → slave | `EspNowAccelDeadzone` (`deadzone` LSB) | | `ESPNOW_SET_ACCEL_DEADZONE` | Master → slave | `EspNowAccelDeadzone` (`deadzone` LSB) |
| `ESPNOW_SET_TAP_CONFIG` | Master → slave | `EspNowTapConfig` (multitap sens/timing/axis + enable S/D/T) | | `ESPNOW_SET_TAP_CONFIG` | Master → slave | `EspNowTapConfig` (multitap sens/timing/axis + enable S/D/T) |
| `ESPNOW_SET_BATTERY_UV` | Master → slave | `EspNowBatteryUv` (runtime UV enable + clear latch when off) |
| `ESPNOW_UNICAST_TEST` | Master → slave | `EspNowUnicastTest` (`seq`) | | `ESPNOW_UNICAST_TEST` | Master → slave | `EspNowUnicastTest` (`seq`) |
| `ESPNOW_FIND_ME` | Master → slave | `EspNowFindMe` (`client_id` filter) — LED locate sequence | | `ESPNOW_FIND_ME` | Master → slave | `EspNowFindMe` (`client_id` filter) — LED locate sequence |
| `ESPNOW_RESTART` | Master → slave | `EspNowRestart` (`client_id` filter) — reboot slave | | `ESPNOW_RESTART` | Master → slave | `EspNowRestart` (`client_id` filter) — reboot slave |
@@ -240,6 +241,7 @@ Host and master speak nanopb-encoded `UartMessage` inside UART frames (byte 0 =
| 30 | `ESPNOW_ECHO_PING` | Implemented (`cmd/cmd_espnow_echo_ping.c`) — ESP-NOW timestamp echo (latency test) | | 30 | `ESPNOW_ECHO_PING` | Implemented (`cmd/cmd_espnow_echo_ping.c`) — ESP-NOW timestamp echo (latency test) |
| 31 | `SET_LOG_LEVEL` | Implemented (`cmd/cmd_set_log_level.c`) — get/set global `esp_log_level_set("*", …)` on master | | 31 | `SET_LOG_LEVEL` | Implemented (`cmd/cmd_set_log_level.c`) — get/set global `esp_log_level_set("*", …)` on master |
| 32 | `TAP_CONFIG` | Implemented (`cmd/cmd_tap_config.c`) — get/set BMA456H multitap params (+ NVS) | | 32 | `TAP_CONFIG` | Implemented (`cmd/cmd_tap_config.c`) — get/set BMA456H multitap params (+ NVS) |
| 36 | `BATTERY_UV` | Implemented (`cmd/cmd_battery_uv.c`) — get/set software UV enable (+ NVS / ESP-NOW) |
Regenerate C code: Regenerate C code:
@@ -473,14 +475,17 @@ Read **cached** LiPo ADC values on the **master** (master local + one entry per
### Software UV protection (LiPo) ### Software UV protection (LiPo)
When `POWERPOD_BATTERY_UV_ENABLE` is `1` (default in `powerpod.h`, disable via CMake), the firmware monitors ADC pin voltages on GPIO 1 and 11: When `POWERPOD_BATTERY_UV_ENABLE` is `1` (default in `powerpod.h`, disable via CMake), the firmware can monitor ADC pin voltages on GPIO 1 and 11. Runtime enable is NVS `uv_en` per pod (missing key → **disabled**); UART `BATTERY_UV` / dashboard toggles it on master and slaves.
| Condition | ADC threshold | Action | | Condition | ADC threshold | Action |
|-----------|---------------|--------| |-----------|---------------|--------|
| Under-voltage | any valid channel &lt; **2300 mV** (3.0 V pack) | NVS latch `uv_latch`, restart into energy-save mode | | External / absent | every valid channel &lt; **100 mV** | Normal boot (no UV); clears latch if set |
| Charged | all valid channels ≥ **3000 mV** (4.0 V pack) | Clear latch, restart into normal boot | | Under-voltage | any valid channel in **100…2299 mV** | NVS latch `uv_latch`, restart into energy-save mode |
| Charged | every non-absent valid channel ≥ **3000 mV** | Clear latch, restart into normal boot |
**Energy-save boot:** LED mode `6` (first 4 LEDs red ~10 % for 5 s), then no WiFi/ESP-NOW, UART, I2C, BMA456, or button — only LiPo ADC polling every 30 s until charged. **Energy-save boot:** LED mode `6` (first 4 LEDs red ~10 % for 5 s), then no WiFi/ESP-NOW, UART, I2C, BMA456, or button — only LiPo ADC polling every 30 s until charged. Disabling UV while latched clears the latch (requires a reachable host — not while already in energy-save).
**UART:** `BATTERY_UV` (`0x24`) — `client_id=0` master, `>0` one slave, `all_clients` master + all slaves. Disabling also clears `uv_latch`.
**Test LED pattern from host:** `led-ring -mode battery-low` (works even when UV feature is disabled). **Test LED pattern from host:** `led-ring -mode battery-low` (works even when UV feature is disabled).
@@ -614,6 +619,7 @@ Target: ESP32-S3. Close serial monitor on the UART adapter port before running `
| `pod_settings.c/h` | NVS persistence (accel deadzone, tap config, UV latch) | | `pod_settings.c/h` | NVS persistence (accel deadzone, tap config, UV latch) |
| `led_ring.c/h` | LED ring (digit display, progress bar) | | `led_ring.c/h` | LED ring (digit display, progress bar) |
| `cmd/cmd_led_ring.c` | UART `LED_RING` progress command | | `cmd/cmd_led_ring.c` | UART `LED_RING` progress command |
| `cmd/cmd_battery_uv.c` | UART `BATTERY_UV` — software UV enable + NVS / ESP-NOW |
| `cmd/cmd_set_log_level.c` | UART `SET_LOG_LEVEL` — runtime ESP-IDF log level | | `cmd/cmd_set_log_level.c` | UART `SET_LOG_LEVEL` — runtime ESP-IDF log level |
| `proto/uart_messages.proto` | UART protocol schema | | `proto/uart_messages.proto` | UART protocol schema |
| `proto/esp_now_messages.proto` | ESP-NOW protocol schema | | `proto/esp_now_messages.proto` | ESP-NOW protocol schema |
+76 -10
View File
@@ -19,15 +19,55 @@ static uint8_t s_charge_streak;
bool battery_uv_is_latched(void) { return pod_settings_is_uv_latched(); } bool battery_uv_is_latched(void) { return pod_settings_is_uv_latched(); }
static bool channel_is_absent(uint32_t mv) {
return mv < BATTERY_UV_ABSENT_ADC_MV;
}
static bool channel_is_under(uint32_t mv) {
return !channel_is_absent(mv) && mv < BATTERY_UV_ADC_MV;
}
static void log_lipo_reading(const char *ctx, const board_lipo_reading_t *reading) {
if (reading == NULL) {
return;
}
ESP_LOGI(TAG,
"%s LIPO1 %s %lu mV LIPO2 %s %lu mV", ctx,
reading->lipo1_valid ? "ok" : "n/a",
(unsigned long)reading->lipo1_mv, reading->lipo2_valid ? "ok" : "n/a",
(unsigned long)reading->lipo2_mv);
}
bool battery_uv_reading_is_external(const board_lipo_reading_t *reading) {
if (reading == NULL) {
return false;
}
bool any_valid = false;
if (reading->lipo1_valid) {
any_valid = true;
if (!channel_is_absent(reading->lipo1_mv)) {
return false;
}
}
if (reading->lipo2_valid) {
any_valid = true;
if (!channel_is_absent(reading->lipo2_mv)) {
return false;
}
}
return any_valid;
}
bool battery_uv_reading_is_under(const board_lipo_reading_t *reading) { bool battery_uv_reading_is_under(const board_lipo_reading_t *reading) {
if (reading == NULL) { if (reading == NULL) {
return false; return false;
} }
if (reading->lipo1_valid && reading->lipo1_mv < BATTERY_UV_ADC_MV) { if (reading->lipo1_valid && channel_is_under(reading->lipo1_mv)) {
return true; return true;
} }
if (reading->lipo2_valid && reading->lipo2_mv < BATTERY_UV_ADC_MV) { if (reading->lipo2_valid && channel_is_under(reading->lipo2_mv)) {
return true; return true;
} }
return false; return false;
@@ -38,25 +78,39 @@ bool battery_uv_reading_is_charged(const board_lipo_reading_t *reading) {
return false; return false;
} }
bool any_valid = false; bool any_pack = false;
if (reading->lipo1_valid) { if (reading->lipo1_valid && !channel_is_absent(reading->lipo1_mv)) {
any_valid = true; any_pack = true;
if (reading->lipo1_mv < BATTERY_CHARGE_ADC_MV) { if (reading->lipo1_mv < BATTERY_CHARGE_ADC_MV) {
return false; return false;
} }
} }
if (reading->lipo2_valid) { if (reading->lipo2_valid && !channel_is_absent(reading->lipo2_mv)) {
any_valid = true; any_pack = true;
if (reading->lipo2_mv < BATTERY_CHARGE_ADC_MV) { if (reading->lipo2_mv < BATTERY_CHARGE_ADC_MV) {
return false; return false;
} }
} }
return any_valid; return any_pack;
} }
bool battery_uv_evaluate_boot(void) { bool battery_uv_evaluate_boot(void) {
if (!pod_settings_is_uv_enabled()) {
ESP_LOGI(TAG, "UV protection disabled — normal boot");
return false;
}
board_lipo_reading_t reading; board_lipo_reading_t reading;
board_input_read_lipo(&reading); board_input_read_lipo(&reading);
log_lipo_reading("boot check", &reading);
if (battery_uv_reading_is_external(&reading)) {
ESP_LOGI(TAG, "near-zero LiPo sense — external supply, normal boot");
if (pod_settings_is_uv_latched()) {
pod_settings_clear_uv_latched();
}
return false;
}
if (pod_settings_is_uv_latched() && battery_uv_reading_is_charged(&reading)) { if (pod_settings_is_uv_latched() && battery_uv_reading_is_charged(&reading)) {
ESP_LOGI(TAG, "charged — clearing UV latch"); ESP_LOGI(TAG, "charged — clearing UV latch");
@@ -78,12 +132,19 @@ bool battery_uv_evaluate_boot(void) {
} }
void battery_uv_poll(const board_lipo_reading_t *reading) { void battery_uv_poll(const board_lipo_reading_t *reading) {
if (reading == NULL || pod_settings_is_uv_latched()) { if (reading == NULL || !pod_settings_is_uv_enabled() ||
pod_settings_is_uv_latched()) {
return;
}
if (battery_uv_reading_is_external(reading)) {
s_uv_streak = 0;
return; return;
} }
if (battery_uv_reading_is_under(reading)) { if (battery_uv_reading_is_under(reading)) {
s_uv_streak++; s_uv_streak++;
log_lipo_reading("under-voltage", reading);
ESP_LOGW(TAG, "under-voltage sample %u/%u", (unsigned)s_uv_streak, ESP_LOGW(TAG, "under-voltage sample %u/%u", (unsigned)s_uv_streak,
(unsigned)UV_DEBOUNCE_SAMPLES); (unsigned)UV_DEBOUNCE_SAMPLES);
if (s_uv_streak >= UV_DEBOUNCE_SAMPLES) { if (s_uv_streak >= UV_DEBOUNCE_SAMPLES) {
@@ -109,8 +170,13 @@ void battery_uv_run_mode(void) {
while (1) { while (1) {
board_lipo_reading_t reading; board_lipo_reading_t reading;
board_input_read_lipo(&reading); board_input_read_lipo(&reading);
log_lipo_reading("energy-save", &reading);
if (battery_uv_reading_is_charged(&reading)) { if (battery_uv_reading_is_external(&reading)) {
ESP_LOGI(TAG, "external supply — clearing UV latch and restarting");
pod_settings_clear_uv_latched();
pod_schedule_restart();
} else if (battery_uv_reading_is_charged(&reading)) {
s_charge_streak++; s_charge_streak++;
ESP_LOGI(TAG, "charge sample %u/%u", (unsigned)s_charge_streak, ESP_LOGI(TAG, "charge sample %u/%u", (unsigned)s_charge_streak,
(unsigned)UV_DEBOUNCE_SAMPLES); (unsigned)UV_DEBOUNCE_SAMPLES);
+12
View File
@@ -11,10 +11,17 @@
#define BATTERY_UV_ADC_MV 2300u #define BATTERY_UV_ADC_MV 2300u
/** ADC pin voltage (mV) at which charging is detected (4.0 V pack, rounded). */ /** ADC pin voltage (mV) at which charging is detected (4.0 V pack, rounded). */
#define BATTERY_CHARGE_ADC_MV 3000u #define BATTERY_CHARGE_ADC_MV 3000u
/**
* ADC readings below this are treated as no pack / external supply (not UV).
* Noise floor / open sense divider → near 0 mV.
*/
#define BATTERY_UV_ABSENT_ADC_MV 100u
bool battery_uv_is_latched(void); bool battery_uv_is_latched(void);
bool battery_uv_reading_is_under(const board_lipo_reading_t *reading); bool battery_uv_reading_is_under(const board_lipo_reading_t *reading);
bool battery_uv_reading_is_charged(const board_lipo_reading_t *reading); bool battery_uv_reading_is_charged(const board_lipo_reading_t *reading);
/** true when every valid channel is near 0 (externally powered / no LiPo). */
bool battery_uv_reading_is_external(const board_lipo_reading_t *reading);
/** true → enter UV energy-save path (does not return). */ /** true → enter UV energy-save path (does not return). */
bool battery_uv_evaluate_boot(void); bool battery_uv_evaluate_boot(void);
@@ -38,6 +45,11 @@ static inline bool battery_uv_reading_is_charged(const board_lipo_reading_t *rea
return false; return false;
} }
static inline bool battery_uv_reading_is_external(const board_lipo_reading_t *reading) {
(void)reading;
return false;
}
static inline bool battery_uv_evaluate_boot(void) { return false; } static inline bool battery_uv_evaluate_boot(void) { return false; }
static inline void battery_uv_poll(const board_lipo_reading_t *reading) { static inline void battery_uv_poll(const board_lipo_reading_t *reading) {
+36
View File
@@ -143,6 +143,7 @@ static client_slot_t *alloc_slot(const uint8_t mac[CLIENT_MAC_LEN],
static const bma456_tap_config_t k_tap_default = BMA456_TAP_CONFIG_DEFAULT; static const bma456_tap_config_t k_tap_default = BMA456_TAP_CONFIG_DEFAULT;
slot->info.tap_config = k_tap_default; slot->info.tap_config = k_tap_default;
} }
slot->info.uv_protection_enabled = false;
if (out_is_new != NULL) { if (out_is_new != NULL) {
*out_is_new = true; *out_is_new = true;
} }
@@ -435,6 +436,41 @@ size_t client_registry_set_tap_config_all(const bma456_tap_config_t *cfg) {
return n; return n;
} }
esp_err_t client_registry_set_uv_protection(uint32_t client_id, bool enabled) {
for (size_t i = 0; i < CLIENT_REGISTRY_MAX; i++) {
if (s_clients[i].active && s_clients[i].info.id == client_id) {
s_clients[i].info.uv_protection_enabled = enabled;
return ESP_OK;
}
}
return ESP_ERR_NOT_FOUND;
}
esp_err_t client_registry_get_uv_protection(uint32_t client_id,
bool *enabled_out) {
if (enabled_out == NULL) {
return ESP_ERR_INVALID_ARG;
}
const client_info_t *info = client_registry_find_by_id(client_id);
if (info == NULL) {
return ESP_ERR_NOT_FOUND;
}
*enabled_out = info->uv_protection_enabled;
return ESP_OK;
}
size_t client_registry_set_uv_protection_all(bool enabled) {
size_t n = 0;
for (size_t i = 0; i < CLIENT_REGISTRY_MAX; i++) {
if (!s_clients[i].active) {
continue;
}
s_clients[i].info.uv_protection_enabled = enabled;
n++;
}
return n;
}
static void clear_client_accel(client_slot_t *slot) { static void clear_client_accel(client_slot_t *slot) {
if (slot == NULL) { if (slot == NULL) {
return; return;
+7
View File
@@ -47,6 +47,8 @@ typedef struct {
uint32_t lipo1_mv; uint32_t lipo1_mv;
uint32_t lipo2_mv; uint32_t lipo2_mv;
uint32_t battery_updated_at; uint32_t battery_updated_at;
/** Master cache: software UV protection enabled on slave (default false). */
bool uv_protection_enabled;
/** Master: last ESPNOW_ASSIGN_ID send time (rate-limit resends). */ /** Master: last ESPNOW_ASSIGN_ID send time (rate-limit resends). */
uint32_t last_assign_sent_at; uint32_t last_assign_sent_at;
} client_info_t; } client_info_t;
@@ -119,6 +121,11 @@ esp_err_t client_registry_get_tap_config(uint32_t client_id,
bma456_tap_config_t *cfg_out); bma456_tap_config_t *cfg_out);
size_t client_registry_set_tap_config_all(const bma456_tap_config_t *cfg); size_t client_registry_set_tap_config_all(const bma456_tap_config_t *cfg);
esp_err_t client_registry_set_uv_protection(uint32_t client_id, bool enabled);
esp_err_t client_registry_get_uv_protection(uint32_t client_id,
bool *enabled_out);
size_t client_registry_set_uv_protection_all(bool enabled);
/** Store latest accel sample from a slave (matched by sender MAC). */ /** Store latest accel sample from a slave (matched by sender MAC). */
esp_err_t client_registry_update_accel(const uint8_t mac[CLIENT_MAC_LEN], esp_err_t client_registry_update_accel(const uint8_t mac[CLIENT_MAC_LEN],
uint32_t slave_id, int16_t x, int16_t y, uint32_t slave_id, int16_t x, int16_t y,
+118
View File
@@ -0,0 +1,118 @@
#include "cmd_battery_uv.h"
#include "client_registry.h"
#include "esp_log.h"
#include "esp_now_comm.h"
#include "pod_settings.h"
#include "uart_cmd.h"
static const char *TAG = "[BATTERY_UV_CMD]";
static void reply(uint32_t client_id, bool success, bool enabled, bool latched,
uint32_t slaves_updated) {
alox_UartMessage response;
uart_cmd_init_response(&response, alox_MessageType_BATTERY_UV,
alox_UartMessage_battery_uv_response_tag);
response.payload.battery_uv_response.client_id = client_id;
response.payload.battery_uv_response.success = success;
response.payload.battery_uv_response.enabled = enabled;
response.payload.battery_uv_response.latched = latched;
response.payload.battery_uv_response.slaves_updated = slaves_updated;
uart_cmd_send(&response, TAG);
}
static esp_err_t apply_local_uv(bool enabled) {
esp_err_t err = pod_settings_set_uv_enabled(enabled);
if (err != ESP_OK) {
return err;
}
if (!enabled) {
(void)pod_settings_clear_uv_latched();
}
return ESP_OK;
}
static esp_err_t push_uv_to_slave(const client_info_t *client, bool enabled) {
if (client == NULL) {
return ESP_ERR_INVALID_ARG;
}
esp_err_t err = client_registry_set_uv_protection(client->id, enabled);
if (err != ESP_OK) {
return err;
}
return esp_now_comm_send_battery_uv(client->mac, client->id, enabled);
}
static void handle_battery_uv(const uint8_t *data, size_t len) {
alox_UartMessage uart_msg;
alox_BatteryUvRequest req = alox_BatteryUvRequest_init_zero;
if (uart_cmd_decode(data, len, &uart_msg) != ESP_OK) {
ESP_LOGW(TAG, "decode failed");
reply(0, false, pod_settings_is_uv_enabled(), pod_settings_is_uv_latched(),
0);
return;
}
const alox_BatteryUvRequest *req_ptr = UART_CMD_REQ(
&uart_msg, alox_UartMessage_battery_uv_request_tag, battery_uv_request);
if (req_ptr != NULL) {
req = *req_ptr;
}
if (req.write) {
if (req.all_clients) {
(void)client_registry_set_uv_protection_all(req.enabled);
uint32_t sent = 0;
for (size_t i = 0; i < client_registry_count(); i++) {
const client_info_t *client = client_registry_at(i);
if (client == NULL) {
continue;
}
if (esp_now_comm_send_battery_uv(client->mac, client->id,
req.enabled) == ESP_OK) {
sent++;
}
}
bool local_ok = apply_local_uv(req.enabled) == ESP_OK;
ESP_LOGI(TAG, "UV protection %s local=%d slaves=%u",
req.enabled ? "on" : "off", local_ok, (unsigned)sent);
reply(0, local_ok || sent > 0, req.enabled,
pod_settings_is_uv_latched(), sent);
return;
}
if (req.client_id == 0) {
esp_err_t err = apply_local_uv(req.enabled);
reply(0, err == ESP_OK, pod_settings_is_uv_enabled(),
pod_settings_is_uv_latched(), 0);
return;
}
const client_info_t *client = client_registry_find_by_id(req.client_id);
if (client == NULL) {
ESP_LOGW(TAG, "client id %lu not found", (unsigned long)req.client_id);
reply(req.client_id, false, req.enabled, false, 0);
return;
}
esp_err_t err = push_uv_to_slave(client, req.enabled);
reply(req.client_id, err == ESP_OK, req.enabled, false,
err == ESP_OK ? 1 : 0);
return;
}
if (req.all_clients || req.client_id == 0) {
reply(0, true, pod_settings_is_uv_enabled(), pod_settings_is_uv_latched(),
0);
return;
}
bool enabled = true;
esp_err_t err = client_registry_get_uv_protection(req.client_id, &enabled);
reply(req.client_id, err == ESP_OK, enabled, false, 0);
}
void cmd_battery_uv_register(void) {
uart_cmd_register(alox_MessageType_BATTERY_UV, handle_battery_uv);
}
+6
View File
@@ -0,0 +1,6 @@
#ifndef CMD_BATTERY_UV_H
#define CMD_BATTERY_UV_H
void cmd_battery_uv_register(void);
#endif
+1
View File
@@ -31,6 +31,7 @@ static bool encode_clients_list(pb_ostream_t *stream, const pb_field_t *field,
proto.tap_notify_single = client->tap_notify_single; proto.tap_notify_single = client->tap_notify_single;
proto.tap_notify_double = client->tap_notify_double; proto.tap_notify_double = client->tap_notify_double;
proto.tap_notify_triple = client->tap_notify_triple; proto.tap_notify_triple = client->tap_notify_triple;
proto.uv_protection_enabled = client->uv_protection_enabled;
proto.mac.funcs.encode = uart_cmd_encode_bytes; proto.mac.funcs.encode = uart_cmd_encode_bytes;
proto.mac.arg = &mac; proto.mac.arg = &mac;
+4
View File
@@ -31,6 +31,10 @@ esp_err_t esp_now_comm_send_tap_config(const uint8_t mac[CLIENT_MAC_LEN],
uint32_t client_id, uint32_t client_id,
const bma456_tap_config_t *cfg); const bma456_tap_config_t *cfg);
/** Master: unicast software UV protection enable to one slave. */
esp_err_t esp_now_comm_send_battery_uv(const uint8_t mac[CLIENT_MAC_LEN],
uint32_t client_id, bool enabled);
/** Master: send minimal unicast test payload to verify master→slave path. */ /** Master: send minimal unicast test payload to verify master→slave path. */
esp_err_t esp_now_comm_send_unicast_test(const uint8_t mac[CLIENT_MAC_LEN], esp_err_t esp_now_comm_send_unicast_test(const uint8_t mac[CLIENT_MAC_LEN],
uint32_t seq); uint32_t seq);
+28
View File
@@ -75,6 +75,16 @@ static esp_err_t send_tap_config(const uint8_t *dest_mac, uint32_t client_id,
return esp_now_core_send(dest_mac, &msg); return esp_now_core_send(dest_mac, &msg);
} }
static esp_err_t send_battery_uv(const uint8_t *dest_mac, uint32_t client_id,
bool enabled) {
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
msg.type = alox_EspNowMessageType_ESPNOW_SET_BATTERY_UV;
msg.which_payload = alox_EspNowMessage_battery_uv_tag;
msg.payload.battery_uv.client_id = client_id;
msg.payload.battery_uv.enabled = enabled;
return esp_now_core_send(dest_mac, &msg);
}
static esp_err_t send_unicast_test(const uint8_t *dest_mac, uint32_t seq) { static esp_err_t send_unicast_test(const uint8_t *dest_mac, uint32_t seq) {
alox_EspNowMessage msg = alox_EspNowMessage_init_zero; alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
msg.type = alox_EspNowMessageType_ESPNOW_UNICAST_TEST; msg.type = alox_EspNowMessageType_ESPNOW_UNICAST_TEST;
@@ -537,6 +547,24 @@ esp_err_t esp_now_comm_send_tap_config(const uint8_t mac[CLIENT_MAC_LEN],
return err; return err;
} }
esp_err_t esp_now_comm_send_battery_uv(const uint8_t mac[CLIENT_MAC_LEN],
uint32_t client_id, bool enabled) {
if (mac == NULL || !esp_now_core_is_master()) {
return ESP_ERR_INVALID_STATE;
}
char mac_str[18];
esp_now_core_mac_to_str(mac, mac_str, sizeof(mac_str));
esp_err_t err = send_battery_uv(mac, client_id, enabled);
if (err == ESP_OK) {
ESP_LOGI(TAG, "unicast SET_BATTERY_UV to %s: enabled=%d client_id=%lu",
mac_str, enabled, (unsigned long)client_id);
} else {
ESP_LOGW(TAG, "unicast SET_BATTERY_UV to %s failed: %s", mac_str,
esp_err_to_name(err));
}
return err;
}
static void handle_accel_sample(const uint8_t mac[CLIENT_MAC_LEN], static void handle_accel_sample(const uint8_t mac[CLIENT_MAC_LEN],
const alox_EspNowAccelSample *sample) { const alox_EspNowAccelSample *sample) {
if (sample == NULL) { if (sample == NULL) {
+29
View File
@@ -640,6 +640,30 @@ static void handle_tap_config(const uint8_t *master_mac,
} }
} }
static void handle_battery_uv(const uint8_t *master_mac,
const alox_EspNowBatteryUv *msg) {
if (!client_id_matches(msg->client_id)) {
return;
}
if (s_joined && !esp_now_core_mac_equal(master_mac, s_master_mac)) {
return;
}
char mac_str[18];
esp_now_core_mac_to_str(master_mac, mac_str, sizeof(mac_str));
ESP_LOGI(TAG, "UV protection %s from master %s (id=%lu)",
msg->enabled ? "enabled" : "disabled", mac_str,
(unsigned long)my_client_id());
if (pod_settings_set_uv_enabled(msg->enabled) != ESP_OK) {
ESP_LOGW(TAG, "UV enable save failed");
return;
}
if (!msg->enabled) {
(void)pod_settings_clear_uv_latched();
}
}
static void handle_assign_id(const uint8_t *master_mac, static void handle_assign_id(const uint8_t *master_mac,
const alox_EspNowAssignId *msg) { const alox_EspNowAssignId *msg) {
if (!from_joined_master(master_mac)) { if (!from_joined_master(master_mac)) {
@@ -853,6 +877,11 @@ void esp_now_slave_on_recv(const esp_now_recv_info_t *info, const uint8_t *data,
handle_tap_config(info->src_addr, &msg.payload.tap_config); handle_tap_config(info->src_addr, &msg.payload.tap_config);
} }
break; break;
case alox_EspNowMessage_battery_uv_tag:
if (from_joined_master(info->src_addr)) {
handle_battery_uv(info->src_addr, &msg.payload.battery_uv);
}
break;
case alox_EspNowMessage_accel_stream_tag: case alox_EspNowMessage_accel_stream_tag:
if (from_joined_master(info->src_addr)) { if (from_joined_master(info->src_addr)) {
handle_accel_stream(info->src_addr, &msg.payload.accel_stream); handle_accel_stream(info->src_addr, &msg.payload.accel_stream);
+53
View File
@@ -9,6 +9,7 @@ static const char *TAG = "[SETTINGS]";
static const char *NS = "powerpod"; static const char *NS = "powerpod";
static const char *KEY_ACCEL_DZ = "accel_dz"; static const char *KEY_ACCEL_DZ = "accel_dz";
static const char *KEY_UV_LATCH = "uv_latch"; static const char *KEY_UV_LATCH = "uv_latch";
static const char *KEY_UV_EN = "uv_en";
static const char *KEY_TAP_CFG = "tap_cfg"; static const char *KEY_TAP_CFG = "tap_cfg";
static const char *KEY_ID_MAP = "id_map"; static const char *KEY_ID_MAP = "id_map";
static const char *KEY_ID_BIND = "id_bind"; static const char *KEY_ID_BIND = "id_bind";
@@ -325,6 +326,58 @@ esp_err_t pod_settings_clear_uv_latched(void) {
return pod_settings_set_uv_latched(false); return pod_settings_set_uv_latched(false);
} }
bool pod_settings_is_uv_enabled(void) {
if (!s_nvs_ready) {
return false;
}
nvs_handle_t handle;
esp_err_t err = nvs_open(NS, NVS_READONLY, &handle);
if (err != ESP_OK) {
return false;
}
uint8_t value = 0;
err = nvs_get_u8(handle, KEY_UV_EN, &value);
nvs_close(handle);
if (err == ESP_ERR_NVS_NOT_FOUND) {
return false;
}
if (err != ESP_OK) {
return false;
}
return value != 0;
}
esp_err_t pod_settings_set_uv_enabled(bool enabled) {
esp_err_t err = ensure_nvs();
if (err != ESP_OK) {
return err;
}
nvs_handle_t handle;
err = nvs_open(NS, NVS_READWRITE, &handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "nvs_open failed: %s", esp_err_to_name(err));
return err;
}
err = nvs_set_u8(handle, KEY_UV_EN, enabled ? 1u : 0u);
if (err == ESP_OK) {
err = nvs_commit(handle);
}
nvs_close(handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "save uv_en failed: %s", esp_err_to_name(err));
return err;
}
ESP_LOGI(TAG, "UV protection %s", enabled ? "enabled" : "disabled");
return ESP_OK;
}
esp_err_t pod_settings_load_id_map(pod_id_map_t *out) { esp_err_t pod_settings_load_id_map(pod_id_map_t *out) {
if (out == NULL) { if (out == NULL) {
return ESP_ERR_INVALID_ARG; return ESP_ERR_INVALID_ARG;
+7
View File
@@ -35,6 +35,13 @@ bool pod_settings_is_uv_latched(void);
esp_err_t pod_settings_set_uv_latched(bool latched); esp_err_t pod_settings_set_uv_latched(bool latched);
esp_err_t pod_settings_clear_uv_latched(void); esp_err_t pod_settings_clear_uv_latched(void);
/**
* Runtime enable for software UV protection (NVS). Missing key → disabled.
* Independent of POWERPOD_BATTERY_UV_ENABLE compile gate.
*/
bool pod_settings_is_uv_enabled(void);
esp_err_t pod_settings_set_uv_enabled(bool enabled);
/** Logical slave ids: 1…POD_CLIENT_ID_MAX (0 reserved for local master). */ /** Logical slave ids: 1…POD_CLIENT_ID_MAX (0 reserved for local master). */
#define POD_CLIENT_ID_MIN 1u #define POD_CLIENT_ID_MIN 1u
#define POD_CLIENT_ID_MAX 255u #define POD_CLIENT_ID_MAX 255u
+2
View File
@@ -18,6 +18,7 @@
#include "ota_confirm.h" #include "ota_confirm.h"
#include "cmd_led_ring.h" #include "cmd_led_ring.h"
#include "cmd_battery.h" #include "cmd_battery.h"
#include "cmd_battery_uv.h"
#include "cmd_set_log_level.h" #include "cmd_set_log_level.h"
#include "esp_now_comm.h" #include "esp_now_comm.h"
#include "powerpod.h" #include "powerpod.h"
@@ -233,6 +234,7 @@ void app_main(void) {
cmd_client_id_map_reset_register(); cmd_client_id_map_reset_register();
cmd_led_ring_register(); cmd_led_ring_register();
cmd_battery_register(); cmd_battery_register();
cmd_battery_uv_register();
cmd_ota_register(); cmd_ota_register();
cmd_ota_slave_progress_register(); cmd_ota_slave_progress_register();
cmd_set_log_level_register(); cmd_set_log_level_register();
+1 -1
View File
@@ -3,7 +3,7 @@
/** 1 = software LiPo UV latch + energy-save mode; 0 = disabled (LED mode 6 still works). */ /** 1 = software LiPo UV latch + energy-save mode; 0 = disabled (LED mode 6 still works). */
#ifndef POWERPOD_BATTERY_UV_ENABLE #ifndef POWERPOD_BATTERY_UV_ENABLE
#define POWERPOD_BATTERY_UV_ENABLE 0 #define POWERPOD_BATTERY_UV_ENABLE 1
#endif #endif
#define DIP_MASTER 4 #define DIP_MASTER 4
+3
View File
@@ -57,6 +57,9 @@ PB_BIND(alox_EspNowTapEvent, alox_EspNowTapEvent, AUTO)
PB_BIND(alox_EspNowInputSendMode, alox_EspNowInputSendMode, AUTO) PB_BIND(alox_EspNowInputSendMode, alox_EspNowInputSendMode, AUTO)
PB_BIND(alox_EspNowBatteryUv, alox_EspNowBatteryUv, AUTO)
PB_BIND(alox_EspNowBatteryReport, alox_EspNowBatteryReport, AUTO) PB_BIND(alox_EspNowBatteryReport, alox_EspNowBatteryReport, AUTO)
+30 -4
View File
@@ -38,7 +38,9 @@ typedef enum _alox_EspNowMessageType {
/* * Master → slave: forget persisted master/id binding (debug reset). */ /* * Master → slave: forget persisted master/id binding (debug reset). */
alox_EspNowMessageType_ESPNOW_CLEAR_ID_BINDING = 23, alox_EspNowMessageType_ESPNOW_CLEAR_ID_BINDING = 23,
/* * Master → slave: accel/tap ESP-NOW send path (best_effort/fast/wait/reliable). */ /* * Master → slave: accel/tap ESP-NOW send path (best_effort/fast/wait/reliable). */
alox_EspNowMessageType_ESPNOW_SET_INPUT_SEND_MODE = 24 alox_EspNowMessageType_ESPNOW_SET_INPUT_SEND_MODE = 24,
/* * Master → slave: enable/disable software LiPo UV protection (NVS). */
alox_EspNowMessageType_ESPNOW_SET_BATTERY_UV = 25
} alox_EspNowMessageType; } alox_EspNowMessageType;
/* Struct definitions */ /* Struct definitions */
@@ -161,6 +163,12 @@ typedef struct _alox_EspNowInputSendMode {
uint32_t mode; uint32_t mode;
} alox_EspNowInputSendMode; } alox_EspNowInputSendMode;
/* * Master → slave: software LiPo UV protection enable (persisted in NVS). */
typedef struct _alox_EspNowBatteryUv {
uint32_t client_id; /* 0 = all slaves; otherwise only matching slave_id applies */
bool enabled;
} alox_EspNowBatteryUv;
/* * Slave → master: LiPo voltages (periodic ~30 s and on query). */ /* * Slave → master: LiPo voltages (periodic ~30 s and on query). */
typedef struct _alox_EspNowBatteryReport { typedef struct _alox_EspNowBatteryReport {
uint32_t client_id; uint32_t client_id;
@@ -236,6 +244,7 @@ typedef struct _alox_EspNowMessage {
alox_EspNowAssignId assign_id; alox_EspNowAssignId assign_id;
alox_EspNowClearIdBinding clear_id_binding; alox_EspNowClearIdBinding clear_id_binding;
alox_EspNowInputSendMode input_send_mode; alox_EspNowInputSendMode input_send_mode;
alox_EspNowBatteryUv battery_uv;
} payload; } payload;
} alox_EspNowMessage; } alox_EspNowMessage;
@@ -246,8 +255,9 @@ extern "C" {
/* Helper constants for enums */ /* Helper constants for enums */
#define _alox_EspNowMessageType_MIN alox_EspNowMessageType_ESPNOW_UNKNOWN #define _alox_EspNowMessageType_MIN alox_EspNowMessageType_ESPNOW_UNKNOWN
#define _alox_EspNowMessageType_MAX alox_EspNowMessageType_ESPNOW_SET_INPUT_SEND_MODE #define _alox_EspNowMessageType_MAX alox_EspNowMessageType_ESPNOW_SET_BATTERY_UV
#define _alox_EspNowMessageType_ARRAYSIZE ((alox_EspNowMessageType)(alox_EspNowMessageType_ESPNOW_SET_INPUT_SEND_MODE+1)) #define _alox_EspNowMessageType_ARRAYSIZE ((alox_EspNowMessageType)(alox_EspNowMessageType_ESPNOW_SET_BATTERY_UV+1))
@@ -293,6 +303,7 @@ extern "C" {
#define alox_EspNowTapConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define alox_EspNowTapConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
#define alox_EspNowTapEvent_init_default {0, 0} #define alox_EspNowTapEvent_init_default {0, 0}
#define alox_EspNowInputSendMode_init_default {0, 0} #define alox_EspNowInputSendMode_init_default {0, 0}
#define alox_EspNowBatteryUv_init_default {0, 0}
#define alox_EspNowBatteryReport_init_default {0, 0, 0, 0, 0} #define alox_EspNowBatteryReport_init_default {0, 0, 0, 0, 0}
#define alox_EspNowLedRing_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define alox_EspNowLedRing_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
#define alox_EspNowOtaStart_init_default {0} #define alox_EspNowOtaStart_init_default {0}
@@ -317,6 +328,7 @@ extern "C" {
#define alox_EspNowTapConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define alox_EspNowTapConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
#define alox_EspNowTapEvent_init_zero {0, 0} #define alox_EspNowTapEvent_init_zero {0, 0}
#define alox_EspNowInputSendMode_init_zero {0, 0} #define alox_EspNowInputSendMode_init_zero {0, 0}
#define alox_EspNowBatteryUv_init_zero {0, 0}
#define alox_EspNowBatteryReport_init_zero {0, 0, 0, 0, 0} #define alox_EspNowBatteryReport_init_zero {0, 0, 0, 0, 0}
#define alox_EspNowLedRing_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define alox_EspNowLedRing_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
#define alox_EspNowOtaStart_init_zero {0} #define alox_EspNowOtaStart_init_zero {0}
@@ -370,6 +382,8 @@ extern "C" {
#define alox_EspNowTapEvent_kind_tag 2 #define alox_EspNowTapEvent_kind_tag 2
#define alox_EspNowInputSendMode_client_id_tag 1 #define alox_EspNowInputSendMode_client_id_tag 1
#define alox_EspNowInputSendMode_mode_tag 2 #define alox_EspNowInputSendMode_mode_tag 2
#define alox_EspNowBatteryUv_client_id_tag 1
#define alox_EspNowBatteryUv_enabled_tag 2
#define alox_EspNowBatteryReport_client_id_tag 1 #define alox_EspNowBatteryReport_client_id_tag 1
#define alox_EspNowBatteryReport_lipo1_valid_tag 2 #define alox_EspNowBatteryReport_lipo1_valid_tag 2
#define alox_EspNowBatteryReport_lipo2_valid_tag 3 #define alox_EspNowBatteryReport_lipo2_valid_tag 3
@@ -416,6 +430,7 @@ extern "C" {
#define alox_EspNowMessage_assign_id_tag 23 #define alox_EspNowMessage_assign_id_tag 23
#define alox_EspNowMessage_clear_id_binding_tag 24 #define alox_EspNowMessage_clear_id_binding_tag 24
#define alox_EspNowMessage_input_send_mode_tag 25 #define alox_EspNowMessage_input_send_mode_tag 25
#define alox_EspNowMessage_battery_uv_tag 26
/* Struct field encoding specification for nanopb */ /* Struct field encoding specification for nanopb */
#define alox_EspNowUnicastTest_FIELDLIST(X, a) \ #define alox_EspNowUnicastTest_FIELDLIST(X, a) \
@@ -531,6 +546,12 @@ X(a, STATIC, SINGULAR, UINT32, mode, 2)
#define alox_EspNowInputSendMode_CALLBACK NULL #define alox_EspNowInputSendMode_CALLBACK NULL
#define alox_EspNowInputSendMode_DEFAULT NULL #define alox_EspNowInputSendMode_DEFAULT NULL
#define alox_EspNowBatteryUv_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, client_id, 1) \
X(a, STATIC, SINGULAR, BOOL, enabled, 2)
#define alox_EspNowBatteryUv_CALLBACK NULL
#define alox_EspNowBatteryUv_DEFAULT NULL
#define alox_EspNowBatteryReport_FIELDLIST(X, a) \ #define alox_EspNowBatteryReport_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, client_id, 1) \ X(a, STATIC, SINGULAR, UINT32, client_id, 1) \
X(a, STATIC, SINGULAR, BOOL, lipo1_valid, 2) \ X(a, STATIC, SINGULAR, BOOL, lipo1_valid, 2) \
@@ -602,7 +623,8 @@ X(a, STATIC, ONEOF, MESSAGE, (payload,echo_pong,payload.echo_pong), 21) \
X(a, STATIC, ONEOF, MESSAGE, (payload,tap_config,payload.tap_config), 22) \ X(a, STATIC, ONEOF, MESSAGE, (payload,tap_config,payload.tap_config), 22) \
X(a, STATIC, ONEOF, MESSAGE, (payload,assign_id,payload.assign_id), 23) \ X(a, STATIC, ONEOF, MESSAGE, (payload,assign_id,payload.assign_id), 23) \
X(a, STATIC, ONEOF, MESSAGE, (payload,clear_id_binding,payload.clear_id_binding), 24) \ X(a, STATIC, ONEOF, MESSAGE, (payload,clear_id_binding,payload.clear_id_binding), 24) \
X(a, STATIC, ONEOF, MESSAGE, (payload,input_send_mode,payload.input_send_mode), 25) X(a, STATIC, ONEOF, MESSAGE, (payload,input_send_mode,payload.input_send_mode), 25) \
X(a, STATIC, ONEOF, MESSAGE, (payload,battery_uv,payload.battery_uv), 26)
#define alox_EspNowMessage_CALLBACK NULL #define alox_EspNowMessage_CALLBACK NULL
#define alox_EspNowMessage_DEFAULT NULL #define alox_EspNowMessage_DEFAULT NULL
#define alox_EspNowMessage_payload_discover_MSGTYPE alox_EspNowDiscover #define alox_EspNowMessage_payload_discover_MSGTYPE alox_EspNowDiscover
@@ -629,6 +651,7 @@ X(a, STATIC, ONEOF, MESSAGE, (payload,input_send_mode,payload.input_send_m
#define alox_EspNowMessage_payload_assign_id_MSGTYPE alox_EspNowAssignId #define alox_EspNowMessage_payload_assign_id_MSGTYPE alox_EspNowAssignId
#define alox_EspNowMessage_payload_clear_id_binding_MSGTYPE alox_EspNowClearIdBinding #define alox_EspNowMessage_payload_clear_id_binding_MSGTYPE alox_EspNowClearIdBinding
#define alox_EspNowMessage_payload_input_send_mode_MSGTYPE alox_EspNowInputSendMode #define alox_EspNowMessage_payload_input_send_mode_MSGTYPE alox_EspNowInputSendMode
#define alox_EspNowMessage_payload_battery_uv_MSGTYPE alox_EspNowBatteryUv
extern const pb_msgdesc_t alox_EspNowUnicastTest_msg; extern const pb_msgdesc_t alox_EspNowUnicastTest_msg;
extern const pb_msgdesc_t alox_EspNowEchoPing_msg; extern const pb_msgdesc_t alox_EspNowEchoPing_msg;
@@ -647,6 +670,7 @@ extern const pb_msgdesc_t alox_EspNowTapNotify_msg;
extern const pb_msgdesc_t alox_EspNowTapConfig_msg; extern const pb_msgdesc_t alox_EspNowTapConfig_msg;
extern const pb_msgdesc_t alox_EspNowTapEvent_msg; extern const pb_msgdesc_t alox_EspNowTapEvent_msg;
extern const pb_msgdesc_t alox_EspNowInputSendMode_msg; extern const pb_msgdesc_t alox_EspNowInputSendMode_msg;
extern const pb_msgdesc_t alox_EspNowBatteryUv_msg;
extern const pb_msgdesc_t alox_EspNowBatteryReport_msg; extern const pb_msgdesc_t alox_EspNowBatteryReport_msg;
extern const pb_msgdesc_t alox_EspNowLedRing_msg; extern const pb_msgdesc_t alox_EspNowLedRing_msg;
extern const pb_msgdesc_t alox_EspNowOtaStart_msg; extern const pb_msgdesc_t alox_EspNowOtaStart_msg;
@@ -673,6 +697,7 @@ extern const pb_msgdesc_t alox_EspNowMessage_msg;
#define alox_EspNowTapConfig_fields &alox_EspNowTapConfig_msg #define alox_EspNowTapConfig_fields &alox_EspNowTapConfig_msg
#define alox_EspNowTapEvent_fields &alox_EspNowTapEvent_msg #define alox_EspNowTapEvent_fields &alox_EspNowTapEvent_msg
#define alox_EspNowInputSendMode_fields &alox_EspNowInputSendMode_msg #define alox_EspNowInputSendMode_fields &alox_EspNowInputSendMode_msg
#define alox_EspNowBatteryUv_fields &alox_EspNowBatteryUv_msg
#define alox_EspNowBatteryReport_fields &alox_EspNowBatteryReport_msg #define alox_EspNowBatteryReport_fields &alox_EspNowBatteryReport_msg
#define alox_EspNowLedRing_fields &alox_EspNowLedRing_msg #define alox_EspNowLedRing_fields &alox_EspNowLedRing_msg
#define alox_EspNowOtaStart_fields &alox_EspNowOtaStart_msg #define alox_EspNowOtaStart_fields &alox_EspNowOtaStart_msg
@@ -691,6 +716,7 @@ extern const pb_msgdesc_t alox_EspNowMessage_msg;
#define alox_EspNowAssignId_size 6 #define alox_EspNowAssignId_size 6
#define alox_EspNowBatteryQuery_size 6 #define alox_EspNowBatteryQuery_size 6
#define alox_EspNowBatteryReport_size 22 #define alox_EspNowBatteryReport_size 22
#define alox_EspNowBatteryUv_size 8
#define alox_EspNowClearIdBinding_size 0 #define alox_EspNowClearIdBinding_size 0
#define alox_EspNowDiscover_size 19 #define alox_EspNowDiscover_size 19
#define alox_EspNowEchoPing_size 22 #define alox_EspNowEchoPing_size 22
+9
View File
@@ -33,6 +33,8 @@ enum EspNowMessageType {
ESPNOW_CLEAR_ID_BINDING = 23; ESPNOW_CLEAR_ID_BINDING = 23;
/** Master → slave: accel/tap ESP-NOW send path (best_effort/fast/wait/reliable). */ /** Master → slave: accel/tap ESP-NOW send path (best_effort/fast/wait/reliable). */
ESPNOW_SET_INPUT_SEND_MODE = 24; ESPNOW_SET_INPUT_SEND_MODE = 24;
/** Master → slave: enable/disable software LiPo UV protection (NVS). */
ESPNOW_SET_BATTERY_UV = 25;
} }
message EspNowUnicastTest { message EspNowUnicastTest {
@@ -154,6 +156,12 @@ message EspNowInputSendMode {
uint32 mode = 2; uint32 mode = 2;
} }
/** Master → slave: software LiPo UV protection enable (persisted in NVS). */
message EspNowBatteryUv {
uint32 client_id = 1; // 0 = all slaves; otherwise only matching slave_id applies
bool enabled = 2;
}
/** Slave → master: LiPo voltages (periodic ~30 s and on query). */ /** Slave → master: LiPo voltages (periodic ~30 s and on query). */
message EspNowBatteryReport { message EspNowBatteryReport {
uint32 client_id = 1; uint32 client_id = 1;
@@ -225,5 +233,6 @@ message EspNowMessage {
EspNowAssignId assign_id = 23; EspNowAssignId assign_id = 23;
EspNowClearIdBinding clear_id_binding = 24; EspNowClearIdBinding clear_id_binding = 24;
EspNowInputSendMode input_send_mode = 25; EspNowInputSendMode input_send_mode = 25;
EspNowBatteryUv battery_uv = 26;
} }
} }
+6
View File
@@ -48,6 +48,12 @@ PB_BIND(alox_InputSendModeRequest, alox_InputSendModeRequest, AUTO)
PB_BIND(alox_InputSendModeResponse, alox_InputSendModeResponse, AUTO) PB_BIND(alox_InputSendModeResponse, alox_InputSendModeResponse, AUTO)
PB_BIND(alox_BatteryUvRequest, alox_BatteryUvRequest, AUTO)
PB_BIND(alox_BatteryUvResponse, alox_BatteryUvResponse, AUTO)
PB_BIND(alox_BatteryStatusRequest, alox_BatteryStatusRequest, AUTO) PB_BIND(alox_BatteryStatusRequest, alox_BatteryStatusRequest, AUTO)
+78 -7
View File
@@ -48,7 +48,11 @@ typedef enum _alox_MessageType {
/* * /* *
Host → master: get/set slave ESP-NOW send path for accel/tap Host → master: get/set slave ESP-NOW send path for accel/tap
(best_effort / fast / wait / reliable). */ (best_effort / fast / wait / reliable). */
alox_MessageType_INPUT_SEND_MODE = 35 alox_MessageType_INPUT_SEND_MODE = 35,
/* *
Host → master: get/set software LiPo UV protection enable (NVS per pod).
client_id 0 = master; >0 = one slave; all_clients = master + every slave. */
alox_MessageType_BATTERY_UV = 36
} alox_MessageType; } alox_MessageType;
/* * /* *
@@ -98,6 +102,8 @@ typedef struct _alox_ClientInfo {
bool tap_notify_single; bool tap_notify_single;
bool tap_notify_double; bool tap_notify_double;
bool tap_notify_triple; bool tap_notify_triple;
/* * Master cache: software UV protection enabled on this slave (NVS on device). */
bool uv_protection_enabled;
} alox_ClientInfo; } alox_ClientInfo;
typedef struct _alox_ClientInfoResponse { typedef struct _alox_ClientInfoResponse {
@@ -167,6 +173,23 @@ typedef struct _alox_InputSendModeResponse {
uint32_t slaves_updated; uint32_t slaves_updated;
} alox_InputSendModeResponse; } alox_InputSendModeResponse;
/* write=false: read UV enable; write=true: persist enable (and clear latch when disabling).
client_id 0 = local master; >0 = one slave; all_clients = master + every registered slave. */
typedef struct _alox_BatteryUvRequest {
bool write;
bool enabled;
uint32_t client_id;
bool all_clients;
} alox_BatteryUvRequest;
typedef struct _alox_BatteryUvResponse {
uint32_t client_id;
bool success;
bool enabled;
bool latched;
uint32_t slaves_updated;
} alox_BatteryUvResponse;
/* * Host → master: read LiPo ADC voltages (master local and/or slaves via ESP-NOW). */ /* * Host → master: read LiPo ADC voltages (master local and/or slaves via ESP-NOW). */
typedef struct _alox_BatteryStatusRequest { typedef struct _alox_BatteryStatusRequest {
/* * 0 = master only; >0 = one slave; ignored when all_clients */ /* * 0 = master only; >0 = one slave; ignored when all_clients */
@@ -504,6 +527,8 @@ typedef struct _alox_UartMessage {
alox_ClientIdMapResetResponse client_id_map_reset_response; alox_ClientIdMapResetResponse client_id_map_reset_response;
alox_InputSendModeRequest input_send_mode_request; alox_InputSendModeRequest input_send_mode_request;
alox_InputSendModeResponse input_send_mode_response; alox_InputSendModeResponse input_send_mode_response;
alox_BatteryUvRequest battery_uv_request;
alox_BatteryUvResponse battery_uv_response;
} payload; } payload;
} alox_UartMessage; } alox_UartMessage;
@@ -514,8 +539,8 @@ extern "C" {
/* Helper constants for enums */ /* Helper constants for enums */
#define _alox_MessageType_MIN alox_MessageType_UNKNOWN #define _alox_MessageType_MIN alox_MessageType_UNKNOWN
#define _alox_MessageType_MAX alox_MessageType_INPUT_SEND_MODE #define _alox_MessageType_MAX alox_MessageType_BATTERY_UV
#define _alox_MessageType_ARRAYSIZE ((alox_MessageType)(alox_MessageType_INPUT_SEND_MODE+1)) #define _alox_MessageType_ARRAYSIZE ((alox_MessageType)(alox_MessageType_BATTERY_UV+1))
#define _alox_InputSendMode_MIN alox_InputSendMode_INPUT_SEND_BEST_EFFORT #define _alox_InputSendMode_MIN alox_InputSendMode_INPUT_SEND_BEST_EFFORT
#define _alox_InputSendMode_MAX alox_InputSendMode_INPUT_SEND_RELIABLE #define _alox_InputSendMode_MAX alox_InputSendMode_INPUT_SEND_RELIABLE
@@ -549,6 +574,8 @@ extern "C" {
#define alox_TapEvent_kind_ENUMTYPE alox_TapKind #define alox_TapEvent_kind_ENUMTYPE alox_TapKind
@@ -587,7 +614,7 @@ extern "C" {
#define alox_Ack_init_default {0} #define alox_Ack_init_default {0}
#define alox_EchoPayload_init_default {{{NULL}, NULL}} #define alox_EchoPayload_init_default {{{NULL}, NULL}}
#define alox_VersionResponse_init_default {0, {{NULL}, NULL}, {{NULL}, NULL}} #define alox_VersionResponse_init_default {0, {{NULL}, NULL}, {{NULL}, NULL}}
#define alox_ClientInfo_init_default {0, 0, 0, {{NULL}, NULL}, 0, 0, 0, 0, 0, 0, 0} #define alox_ClientInfo_init_default {0, 0, 0, {{NULL}, NULL}, 0, 0, 0, 0, 0, 0, 0, 0}
#define alox_ClientInfoResponse_init_default {{{NULL}, NULL}} #define alox_ClientInfoResponse_init_default {{{NULL}, NULL}}
#define alox_ClientInput_init_default {0, 0, 0, 0} #define alox_ClientInput_init_default {0, 0, 0, 0}
#define alox_ClientInputResponse_init_default {{{NULL}, NULL}} #define alox_ClientInputResponse_init_default {{{NULL}, NULL}}
@@ -597,6 +624,8 @@ extern "C" {
#define alox_AccelStreamResponse_init_default {0, 0, 0, 0} #define alox_AccelStreamResponse_init_default {0, 0, 0, 0}
#define alox_InputSendModeRequest_init_default {0, _alox_InputSendMode_MIN, 0, 0} #define alox_InputSendModeRequest_init_default {0, _alox_InputSendMode_MIN, 0, 0}
#define alox_InputSendModeResponse_init_default {0, _alox_InputSendMode_MIN, 0, 0} #define alox_InputSendModeResponse_init_default {0, _alox_InputSendMode_MIN, 0, 0}
#define alox_BatteryUvRequest_init_default {0, 0, 0, 0}
#define alox_BatteryUvResponse_init_default {0, 0, 0, 0, 0}
#define alox_BatteryStatusRequest_init_default {0, 0} #define alox_BatteryStatusRequest_init_default {0, 0}
#define alox_LipoReading_init_default {0, 0} #define alox_LipoReading_init_default {0, 0}
#define alox_BatterySample_init_default {0, false, alox_LipoReading_init_default, false, alox_LipoReading_init_default, 0} #define alox_BatterySample_init_default {0, false, alox_LipoReading_init_default, false, alox_LipoReading_init_default, 0}
@@ -638,7 +667,7 @@ extern "C" {
#define alox_Ack_init_zero {0} #define alox_Ack_init_zero {0}
#define alox_EchoPayload_init_zero {{{NULL}, NULL}} #define alox_EchoPayload_init_zero {{{NULL}, NULL}}
#define alox_VersionResponse_init_zero {0, {{NULL}, NULL}, {{NULL}, NULL}} #define alox_VersionResponse_init_zero {0, {{NULL}, NULL}, {{NULL}, NULL}}
#define alox_ClientInfo_init_zero {0, 0, 0, {{NULL}, NULL}, 0, 0, 0, 0, 0, 0, 0} #define alox_ClientInfo_init_zero {0, 0, 0, {{NULL}, NULL}, 0, 0, 0, 0, 0, 0, 0, 0}
#define alox_ClientInfoResponse_init_zero {{{NULL}, NULL}} #define alox_ClientInfoResponse_init_zero {{{NULL}, NULL}}
#define alox_ClientInput_init_zero {0, 0, 0, 0} #define alox_ClientInput_init_zero {0, 0, 0, 0}
#define alox_ClientInputResponse_init_zero {{{NULL}, NULL}} #define alox_ClientInputResponse_init_zero {{{NULL}, NULL}}
@@ -648,6 +677,8 @@ extern "C" {
#define alox_AccelStreamResponse_init_zero {0, 0, 0, 0} #define alox_AccelStreamResponse_init_zero {0, 0, 0, 0}
#define alox_InputSendModeRequest_init_zero {0, _alox_InputSendMode_MIN, 0, 0} #define alox_InputSendModeRequest_init_zero {0, _alox_InputSendMode_MIN, 0, 0}
#define alox_InputSendModeResponse_init_zero {0, _alox_InputSendMode_MIN, 0, 0} #define alox_InputSendModeResponse_init_zero {0, _alox_InputSendMode_MIN, 0, 0}
#define alox_BatteryUvRequest_init_zero {0, 0, 0, 0}
#define alox_BatteryUvResponse_init_zero {0, 0, 0, 0, 0}
#define alox_BatteryStatusRequest_init_zero {0, 0} #define alox_BatteryStatusRequest_init_zero {0, 0}
#define alox_LipoReading_init_zero {0, 0} #define alox_LipoReading_init_zero {0, 0}
#define alox_BatterySample_init_zero {0, false, alox_LipoReading_init_zero, false, alox_LipoReading_init_zero, 0} #define alox_BatterySample_init_zero {0, false, alox_LipoReading_init_zero, false, alox_LipoReading_init_zero, 0}
@@ -702,6 +733,7 @@ extern "C" {
#define alox_ClientInfo_tap_notify_single_tag 9 #define alox_ClientInfo_tap_notify_single_tag 9
#define alox_ClientInfo_tap_notify_double_tag 10 #define alox_ClientInfo_tap_notify_double_tag 10
#define alox_ClientInfo_tap_notify_triple_tag 11 #define alox_ClientInfo_tap_notify_triple_tag 11
#define alox_ClientInfo_uv_protection_enabled_tag 12
#define alox_ClientInfoResponse_clients_tag 1 #define alox_ClientInfoResponse_clients_tag 1
#define alox_ClientInput_id_tag 1 #define alox_ClientInput_id_tag 1
#define alox_ClientInput_lage_x_tag 2 #define alox_ClientInput_lage_x_tag 2
@@ -733,6 +765,15 @@ extern "C" {
#define alox_InputSendModeResponse_mode_tag 2 #define alox_InputSendModeResponse_mode_tag 2
#define alox_InputSendModeResponse_client_id_tag 3 #define alox_InputSendModeResponse_client_id_tag 3
#define alox_InputSendModeResponse_slaves_updated_tag 4 #define alox_InputSendModeResponse_slaves_updated_tag 4
#define alox_BatteryUvRequest_write_tag 1
#define alox_BatteryUvRequest_enabled_tag 2
#define alox_BatteryUvRequest_client_id_tag 3
#define alox_BatteryUvRequest_all_clients_tag 4
#define alox_BatteryUvResponse_client_id_tag 1
#define alox_BatteryUvResponse_success_tag 2
#define alox_BatteryUvResponse_enabled_tag 3
#define alox_BatteryUvResponse_latched_tag 4
#define alox_BatteryUvResponse_slaves_updated_tag 5
#define alox_BatteryStatusRequest_client_id_tag 1 #define alox_BatteryStatusRequest_client_id_tag 1
#define alox_BatteryStatusRequest_all_clients_tag 2 #define alox_BatteryStatusRequest_all_clients_tag 2
#define alox_LipoReading_valid_tag 1 #define alox_LipoReading_valid_tag 1
@@ -901,6 +942,8 @@ extern "C" {
#define alox_UartMessage_client_id_map_reset_response_tag 43 #define alox_UartMessage_client_id_map_reset_response_tag 43
#define alox_UartMessage_input_send_mode_request_tag 44 #define alox_UartMessage_input_send_mode_request_tag 44
#define alox_UartMessage_input_send_mode_response_tag 45 #define alox_UartMessage_input_send_mode_response_tag 45
#define alox_UartMessage_battery_uv_request_tag 46
#define alox_UartMessage_battery_uv_response_tag 47
/* Struct field encoding specification for nanopb */ /* Struct field encoding specification for nanopb */
#define alox_UartMessage_FIELDLIST(X, a) \ #define alox_UartMessage_FIELDLIST(X, a) \
@@ -944,7 +987,9 @@ X(a, STATIC, ONEOF, MESSAGE, (payload,ble_stream_tick,payload.ble_stream_t
X(a, STATIC, ONEOF, MESSAGE, (payload,client_id_map_reset_request,payload.client_id_map_reset_request), 42) \ X(a, STATIC, ONEOF, MESSAGE, (payload,client_id_map_reset_request,payload.client_id_map_reset_request), 42) \
X(a, STATIC, ONEOF, MESSAGE, (payload,client_id_map_reset_response,payload.client_id_map_reset_response), 43) \ X(a, STATIC, ONEOF, MESSAGE, (payload,client_id_map_reset_response,payload.client_id_map_reset_response), 43) \
X(a, STATIC, ONEOF, MESSAGE, (payload,input_send_mode_request,payload.input_send_mode_request), 44) \ X(a, STATIC, ONEOF, MESSAGE, (payload,input_send_mode_request,payload.input_send_mode_request), 44) \
X(a, STATIC, ONEOF, MESSAGE, (payload,input_send_mode_response,payload.input_send_mode_response), 45) X(a, STATIC, ONEOF, MESSAGE, (payload,input_send_mode_response,payload.input_send_mode_response), 45) \
X(a, STATIC, ONEOF, MESSAGE, (payload,battery_uv_request,payload.battery_uv_request), 46) \
X(a, STATIC, ONEOF, MESSAGE, (payload,battery_uv_response,payload.battery_uv_response), 47)
#define alox_UartMessage_CALLBACK NULL #define alox_UartMessage_CALLBACK NULL
#define alox_UartMessage_DEFAULT NULL #define alox_UartMessage_DEFAULT NULL
#define alox_UartMessage_payload_ack_payload_MSGTYPE alox_Ack #define alox_UartMessage_payload_ack_payload_MSGTYPE alox_Ack
@@ -987,6 +1032,8 @@ X(a, STATIC, ONEOF, MESSAGE, (payload,input_send_mode_response,payload.inp
#define alox_UartMessage_payload_client_id_map_reset_response_MSGTYPE alox_ClientIdMapResetResponse #define alox_UartMessage_payload_client_id_map_reset_response_MSGTYPE alox_ClientIdMapResetResponse
#define alox_UartMessage_payload_input_send_mode_request_MSGTYPE alox_InputSendModeRequest #define alox_UartMessage_payload_input_send_mode_request_MSGTYPE alox_InputSendModeRequest
#define alox_UartMessage_payload_input_send_mode_response_MSGTYPE alox_InputSendModeResponse #define alox_UartMessage_payload_input_send_mode_response_MSGTYPE alox_InputSendModeResponse
#define alox_UartMessage_payload_battery_uv_request_MSGTYPE alox_BatteryUvRequest
#define alox_UartMessage_payload_battery_uv_response_MSGTYPE alox_BatteryUvResponse
#define alox_Ack_FIELDLIST(X, a) \ #define alox_Ack_FIELDLIST(X, a) \
@@ -1016,7 +1063,8 @@ X(a, STATIC, SINGULAR, UINT32, version, 7) \
X(a, STATIC, SINGULAR, BOOL, accel_stream_enabled, 8) \ X(a, STATIC, SINGULAR, BOOL, accel_stream_enabled, 8) \
X(a, STATIC, SINGULAR, BOOL, tap_notify_single, 9) \ X(a, STATIC, SINGULAR, BOOL, tap_notify_single, 9) \
X(a, STATIC, SINGULAR, BOOL, tap_notify_double, 10) \ X(a, STATIC, SINGULAR, BOOL, tap_notify_double, 10) \
X(a, STATIC, SINGULAR, BOOL, tap_notify_triple, 11) X(a, STATIC, SINGULAR, BOOL, tap_notify_triple, 11) \
X(a, STATIC, SINGULAR, BOOL, uv_protection_enabled, 12)
#define alox_ClientInfo_CALLBACK pb_default_field_callback #define alox_ClientInfo_CALLBACK pb_default_field_callback
#define alox_ClientInfo_DEFAULT NULL #define alox_ClientInfo_DEFAULT NULL
@@ -1089,6 +1137,23 @@ X(a, STATIC, SINGULAR, UINT32, slaves_updated, 4)
#define alox_InputSendModeResponse_CALLBACK NULL #define alox_InputSendModeResponse_CALLBACK NULL
#define alox_InputSendModeResponse_DEFAULT NULL #define alox_InputSendModeResponse_DEFAULT NULL
#define alox_BatteryUvRequest_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, BOOL, write, 1) \
X(a, STATIC, SINGULAR, BOOL, enabled, 2) \
X(a, STATIC, SINGULAR, UINT32, client_id, 3) \
X(a, STATIC, SINGULAR, BOOL, all_clients, 4)
#define alox_BatteryUvRequest_CALLBACK NULL
#define alox_BatteryUvRequest_DEFAULT NULL
#define alox_BatteryUvResponse_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, client_id, 1) \
X(a, STATIC, SINGULAR, BOOL, success, 2) \
X(a, STATIC, SINGULAR, BOOL, enabled, 3) \
X(a, STATIC, SINGULAR, BOOL, latched, 4) \
X(a, STATIC, SINGULAR, UINT32, slaves_updated, 5)
#define alox_BatteryUvResponse_CALLBACK NULL
#define alox_BatteryUvResponse_DEFAULT NULL
#define alox_BatteryStatusRequest_FIELDLIST(X, a) \ #define alox_BatteryStatusRequest_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, client_id, 1) \ X(a, STATIC, SINGULAR, UINT32, client_id, 1) \
X(a, STATIC, SINGULAR, BOOL, all_clients, 2) X(a, STATIC, SINGULAR, BOOL, all_clients, 2)
@@ -1389,6 +1454,8 @@ extern const pb_msgdesc_t alox_AccelStreamRequest_msg;
extern const pb_msgdesc_t alox_AccelStreamResponse_msg; extern const pb_msgdesc_t alox_AccelStreamResponse_msg;
extern const pb_msgdesc_t alox_InputSendModeRequest_msg; extern const pb_msgdesc_t alox_InputSendModeRequest_msg;
extern const pb_msgdesc_t alox_InputSendModeResponse_msg; extern const pb_msgdesc_t alox_InputSendModeResponse_msg;
extern const pb_msgdesc_t alox_BatteryUvRequest_msg;
extern const pb_msgdesc_t alox_BatteryUvResponse_msg;
extern const pb_msgdesc_t alox_BatteryStatusRequest_msg; extern const pb_msgdesc_t alox_BatteryStatusRequest_msg;
extern const pb_msgdesc_t alox_LipoReading_msg; extern const pb_msgdesc_t alox_LipoReading_msg;
extern const pb_msgdesc_t alox_BatterySample_msg; extern const pb_msgdesc_t alox_BatterySample_msg;
@@ -1442,6 +1509,8 @@ extern const pb_msgdesc_t alox_OtaSlaveProgressResponse_msg;
#define alox_AccelStreamResponse_fields &alox_AccelStreamResponse_msg #define alox_AccelStreamResponse_fields &alox_AccelStreamResponse_msg
#define alox_InputSendModeRequest_fields &alox_InputSendModeRequest_msg #define alox_InputSendModeRequest_fields &alox_InputSendModeRequest_msg
#define alox_InputSendModeResponse_fields &alox_InputSendModeResponse_msg #define alox_InputSendModeResponse_fields &alox_InputSendModeResponse_msg
#define alox_BatteryUvRequest_fields &alox_BatteryUvRequest_msg
#define alox_BatteryUvResponse_fields &alox_BatteryUvResponse_msg
#define alox_BatteryStatusRequest_fields &alox_BatteryStatusRequest_msg #define alox_BatteryStatusRequest_fields &alox_BatteryStatusRequest_msg
#define alox_LipoReading_fields &alox_LipoReading_msg #define alox_LipoReading_fields &alox_LipoReading_msg
#define alox_BatterySample_fields &alox_BatterySample_msg #define alox_BatterySample_fields &alox_BatterySample_msg
@@ -1497,6 +1566,8 @@ extern const pb_msgdesc_t alox_OtaSlaveProgressResponse_msg;
#define alox_BatterySample_size 32 #define alox_BatterySample_size 32
#define alox_BatteryStatusRequest_size 8 #define alox_BatteryStatusRequest_size 8
#define alox_BatteryStatusResponse_size 580 #define alox_BatteryStatusResponse_size 580
#define alox_BatteryUvRequest_size 12
#define alox_BatteryUvResponse_size 18
#define alox_BleStreamTick_size 753 #define alox_BleStreamTick_size 753
#define alox_CacheClientAccel_size 26 #define alox_CacheClientAccel_size 26
#define alox_CacheClientStatus_size 44 #define alox_CacheClientStatus_size 44
+26
View File
@@ -47,6 +47,11 @@ enum MessageType {
* (best_effort / fast / wait / reliable). * (best_effort / fast / wait / reliable).
*/ */
INPUT_SEND_MODE = 35; INPUT_SEND_MODE = 35;
/**
* Host → master: get/set software LiPo UV protection enable (NVS per pod).
* client_id 0 = master; >0 = one slave; all_clients = master + every slave.
*/
BATTERY_UV = 36;
} }
message UartMessage { message UartMessage {
@@ -92,6 +97,8 @@ message UartMessage {
ClientIdMapResetResponse client_id_map_reset_response = 43; ClientIdMapResetResponse client_id_map_reset_response = 43;
InputSendModeRequest input_send_mode_request = 44; InputSendModeRequest input_send_mode_request = 44;
InputSendModeResponse input_send_mode_response = 45; InputSendModeResponse input_send_mode_response = 45;
BatteryUvRequest battery_uv_request = 46;
BatteryUvResponse battery_uv_response = 47;
} }
} }
@@ -122,6 +129,8 @@ message ClientInfo {
bool tap_notify_single = 9; bool tap_notify_single = 9;
bool tap_notify_double = 10; bool tap_notify_double = 10;
bool tap_notify_triple = 11; bool tap_notify_triple = 11;
/** Master cache: software UV protection enabled on this slave (NVS on device). */
bool uv_protection_enabled = 12;
} }
message ClientInfoResponse { message ClientInfoResponse {
@@ -202,6 +211,23 @@ message InputSendModeResponse {
uint32 slaves_updated = 4; uint32 slaves_updated = 4;
} }
// write=false: read UV enable; write=true: persist enable (and clear latch when disabling).
// client_id 0 = local master; >0 = one slave; all_clients = master + every registered slave.
message BatteryUvRequest {
bool write = 1;
bool enabled = 2;
uint32 client_id = 3;
bool all_clients = 4;
}
message BatteryUvResponse {
uint32 client_id = 1;
bool success = 2;
bool enabled = 3;
bool latched = 4;
uint32 slaves_updated = 5;
}
/** Host → master: read LiPo ADC voltages (master local and/or slaves via ESP-NOW). */ /** Host → master: read LiPo ADC voltages (master local and/or slaves via ESP-NOW). */
message BatteryStatusRequest { message BatteryStatusRequest {
/** 0 = master only; >0 = one slave; ignored when all_clients */ /** 0 = master only; >0 = one slave; ignored when all_clients */