113 lines
4.0 KiB
Python
113 lines
4.0 KiB
Python
"""
|
|
Battery guard - a worked example of a background monitor.
|
|
|
|
Watches the PMU battery level, warns once per threshold crossing, and can drop
|
|
the robot into a safe mode before it browns out mid-stride. Demonstrates
|
|
on_tick, ctx.push readouts and charted series.
|
|
"""
|
|
|
|
from backend.plugin_api import Plugin
|
|
|
|
plugin = Plugin(
|
|
id="battery_guard",
|
|
name="Battery guard",
|
|
description="Warns on low battery and can auto-safe the robot before it browns out.",
|
|
icon="\U0001F50B",
|
|
order=20,
|
|
)
|
|
|
|
|
|
@plugin.slider("warn_at", label="Warn below", min=5, max=60, step=1, default=25, unit="%")
|
|
async def warn_at(ctx, value):
|
|
ctx.storage["warn_at"] = float(value)
|
|
ctx.storage["warned"] = False
|
|
return f"Warning threshold set to {value:.0f}%"
|
|
|
|
|
|
@plugin.slider("safe_at", label="Auto-safe below", min=1, max=30, step=1, default=10, unit="%",
|
|
help="Switches to Damping so the robot settles instead of collapsing.")
|
|
async def safe_at(ctx, value):
|
|
ctx.storage["safe_at"] = float(value)
|
|
ctx.storage["safed"] = False
|
|
return f"Auto-safe threshold set to {value:.0f}%"
|
|
|
|
|
|
@plugin.toggle("auto_safe", label="Auto-safe enabled", default=False,
|
|
help="Off by default - turn on only when you want the robot to act unattended.")
|
|
async def auto_safe(ctx, value):
|
|
ctx.storage["auto_safe"] = bool(value)
|
|
return "Auto-safe armed" if value else "Auto-safe disarmed"
|
|
|
|
|
|
@plugin.action("reset_alarms", label="Reset alarms", style="default")
|
|
async def reset_alarms(ctx):
|
|
ctx.storage["warned"] = False
|
|
ctx.storage["safed"] = False
|
|
await ctx.push("status", "armed")
|
|
return "Alarms reset"
|
|
|
|
|
|
plugin.readout("level", "Battery", unit="%", chart=True, precision=1)
|
|
plugin.readout("draw", "Draw", unit="A", precision=2)
|
|
plugin.readout("status", "Guard status", format="text")
|
|
plugin.readout("est_runtime", "Estimated runtime", unit="min", precision=0)
|
|
|
|
|
|
@plugin.on_start
|
|
async def start(ctx):
|
|
ctx.storage.setdefault("warn_at", 25.0)
|
|
ctx.storage.setdefault("safe_at", 10.0)
|
|
ctx.storage.setdefault("auto_safe", False)
|
|
ctx.storage.setdefault("warned", False)
|
|
ctx.storage.setdefault("safed", False)
|
|
await ctx.push("status", "armed")
|
|
|
|
|
|
@plugin.on_tick(interval=3.0)
|
|
async def tick(ctx):
|
|
level = ctx.state.battery_pct
|
|
if level is None:
|
|
await ctx.push("status", "no PMU data")
|
|
return
|
|
|
|
current = ctx.state.battery_current
|
|
await ctx.push("level", round(level, 1))
|
|
ctx.record("level", level)
|
|
if current is not None:
|
|
await ctx.push("draw", round(abs(current), 2))
|
|
|
|
# A crude but useful projection: remaining percent over the observed drain
|
|
# rate. Only meaningful while discharging.
|
|
history = ctx.hub.series("battery_pct", limit=120)
|
|
if len(history) >= 20:
|
|
(t0, v0), (t1, v1) = history[0], history[-1]
|
|
elapsed = t1 - t0
|
|
drop = v0 - v1
|
|
if elapsed > 5 and drop > 0.01:
|
|
minutes = (level / (drop / elapsed)) / 60.0
|
|
await ctx.push("est_runtime", round(min(minutes, 9999), 0))
|
|
|
|
safe_at = ctx.storage["safe_at"]
|
|
warn_at = ctx.storage["warn_at"]
|
|
|
|
if level <= safe_at:
|
|
await ctx.push("status", "critical")
|
|
if ctx.storage.get("auto_safe") and not ctx.storage.get("safed"):
|
|
ctx.storage["safed"] = True
|
|
await ctx.bridge.stop_motion()
|
|
outcome = await ctx.bridge.set_mode("DAMPING_DEFAULT")
|
|
await ctx.log(
|
|
f"Battery {level:.1f}% - auto-safe engaged: {outcome.message}", level="error"
|
|
)
|
|
await ctx.bridge.speak("Battery critical. Entering safe mode.", priority=10)
|
|
elif level <= warn_at:
|
|
await ctx.push("status", "low")
|
|
if not ctx.storage.get("warned"):
|
|
ctx.storage["warned"] = True
|
|
await ctx.log(f"Battery low: {level:.1f}%", level="warn")
|
|
await ctx.bridge.set_led(mode=2, r=250, g=178, b=25)
|
|
else:
|
|
await ctx.push("status", "normal")
|
|
ctx.storage["warned"] = False
|
|
ctx.storage["safed"] = False
|