Add runtime BMA456 tap config with NVS persistence and goTool UI.

Expose multitap parameters over UART/ESP-NOW (like deadzone), store per node in NVS, and allow get/set from CLI, REST, and the dashboard without poll-overwrite while editing.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
simon
2026-09-26 13:07:13 +02:00
co-authored by Cursor
parent f006f8b912
commit d7441cb85f
32 changed files with 1859 additions and 150 deletions
+78 -30
View File
@@ -15,31 +15,45 @@ import (
)
type MasterView struct {
Version uint32 `json:"version"`
GitHash string `json:"git_hash"`
RunningPartition string `json:"running_partition,omitempty"`
Deadzone uint32 `json:"deadzone,omitempty"`
Lipo1 lipoReadingJSON `json:"lipo1"`
Lipo2 lipoReadingJSON `json:"lipo2"`
BatteryAgeMs uint32 `json:"battery_age_ms,omitempty"`
OK bool `json:"ok"`
Error string `json:"error,omitempty"`
Version uint32 `json:"version"`
GitHash string `json:"git_hash"`
RunningPartition string `json:"running_partition,omitempty"`
Deadzone uint32 `json:"deadzone,omitempty"`
TapConfig *TapConfigView `json:"tap_config,omitempty"`
Lipo1 lipoReadingJSON `json:"lipo1"`
Lipo2 lipoReadingJSON `json:"lipo2"`
BatteryAgeMs uint32 `json:"battery_age_ms,omitempty"`
OK bool `json:"ok"`
Error string `json:"error,omitempty"`
}
type TapConfigView struct {
TapSensThres uint32 `json:"tap_sens_thres"`
MaxGestDur uint32 `json:"max_gest_dur"`
TapShockDur uint32 `json:"tap_shock_dur"`
QuiteTimeAfterGest uint32 `json:"quite_time_after_gest"`
WaitForTimeout uint32 `json:"wait_for_timeout"`
AxisSel uint32 `json:"axis_sel"`
EnableSingle bool `json:"enable_single"`
EnableDouble bool `json:"enable_double"`
EnableTriple bool `json:"enable_triple"`
}
type ClientView struct {
ID uint32 `json:"id"`
MAC string `json:"mac"`
Version uint32 `json:"version"`
Deadzone uint32 `json:"deadzone,omitempty"`
Available bool `json:"available"`
Used bool `json:"used"`
LastPing uint32 `json:"last_ping"`
LastSuccessPing uint32 `json:"last_success_ping"`
AccelValid bool `json:"accel_valid"`
AccelX int32 `json:"accel_x"`
AccelY int32 `json:"accel_y"`
AccelZ int32 `json:"accel_z"`
AccelAgeMs uint32 `json:"accel_age_ms"`
ID uint32 `json:"id"`
MAC string `json:"mac"`
Version uint32 `json:"version"`
Deadzone uint32 `json:"deadzone,omitempty"`
TapConfig *TapConfigView `json:"tap_config,omitempty"`
Available bool `json:"available"`
Used bool `json:"used"`
LastPing uint32 `json:"last_ping"`
LastSuccessPing uint32 `json:"last_success_ping"`
AccelValid bool `json:"accel_valid"`
AccelX int32 `json:"accel_x"`
AccelY int32 `json:"accel_y"`
AccelZ int32 `json:"accel_z"`
AccelAgeMs uint32 `json:"accel_age_ms"`
AccelStream bool `json:"accel_stream"`
TapNotifySingle bool `json:"tap_notify_single"`
TapNotifyDouble bool `json:"tap_notify_double"`
@@ -52,15 +66,15 @@ type ClientView struct {
}
type DashboardState struct {
UpdatedAt string `json:"updated_at"`
SerialPort string `json:"serial_port"`
UARTConnected bool `json:"uart_connected"`
SerialOK bool `json:"serial_ok"`
SerialError string `json:"serial_error,omitempty"`
UpdatedAt string `json:"updated_at"`
SerialPort string `json:"serial_port"`
UARTConnected bool `json:"uart_connected"`
SerialOK bool `json:"serial_ok"`
SerialError string `json:"serial_error,omitempty"`
/** Host: fast CACHE_STATUS poll (~16 ms) for accel + tap. */
LiveStream bool `json:"live_stream"`
Master MasterView `json:"master"`
Clients []ClientView `json:"clients"`
LiveStream bool `json:"live_stream"`
Master MasterView `json:"master"`
Clients []ClientView `json:"clients"`
}
type wsHub struct {
@@ -540,6 +554,9 @@ func pollDashboard(link *managedSerial, portName string, last *DashboardState, s
if dz, err := readDeadzonePoll(link, 0); err == nil {
st.Master.Deadzone = dz
}
if tc, err := readTapConfigPoll(link, 0); err == nil {
st.Master.TapConfig = tc
}
clients, err := link.listClientsPoll()
if err != nil {
@@ -574,6 +591,9 @@ func pollDashboard(link *managedSerial, portName string, last *DashboardState, s
if dz, err := readDeadzonePoll(link, c.GetId()); err == nil {
st.Clients[i].Deadzone = dz
}
if tc, err := readTapConfigPoll(link, c.GetId()); err == nil {
st.Clients[i].TapConfig = tc
}
}
}
if last != nil {
@@ -714,6 +734,34 @@ func readDeadzonePoll(link *managedSerial, clientID uint32) (uint32, error) {
return r.GetDeadzone(), nil
}
func tapConfigViewFromPB(r *pb.TapConfigResponse) *TapConfigView {
return &TapConfigView{
TapSensThres: r.GetTapSensThres(),
MaxGestDur: r.GetMaxGestDur(),
TapShockDur: r.GetTapShockDur(),
QuiteTimeAfterGest: r.GetQuiteTimeAfterGest(),
WaitForTimeout: r.GetWaitForTimeout(),
AxisSel: r.GetAxisSel(),
EnableSingle: r.GetEnableSingle(),
EnableDouble: r.GetEnableDouble(),
EnableTriple: r.GetEnableTriple(),
}
}
func readTapConfigPoll(link *managedSerial, clientID uint32) (*TapConfigView, error) {
r, err := link.TapConfigPoll(&pb.TapConfigRequest{
Write: false,
ClientId: clientID,
})
if err != nil {
return nil, err
}
if !r.GetSuccess() {
return nil, fmt.Errorf("tap config read failed for client %d", clientID)
}
return tapConfigViewFromPB(r), nil
}
func formatMAC(mac []byte) string {
if len(mac) == 0 {
return ""