openaliro
Aliro reader: UWB/CCC core and ESP32-S3/C5/C6 port
Loading...
Searching...
No Matches
aliro_approach.h
Go to the documentation of this file.
1
7/*
8 * Copyright (c) 2026 asxeem
9 * SPDX-License-Identifier: ISC
10 *
11 * aliro_approach — predictive approach controller ("negative latency").
12 *
13 * Turns the per-block trusted UWB range stream into bolt decisions. Two paths
14 * cooperate:
15 *
16 * Presence (threshold) path — the shipped behaviour, unchanged: trusted
17 * ranges go through a median filter (rejects the metre-scale per-block
18 * spikes) and near/far dwell counters across a wide hysteresis band before
19 * the bolt moves. Slow shuffles and stand-at-door still unlock here.
20 *
21 * Prediction path — a 1-D constant-velocity Kalman filter over the same
22 * samples yields distance + closing speed. When the estimated time of
23 * arrival at the unlock radius drops inside the bolt's retraction window
24 * (motor_ms + margin_ms), retraction is started early so it COMPLETES at
25 * arrival instead of beginning there. The invariant is "open only while an
26 * authenticated credential is closing on the door": a prediction needs a
27 * converged filter, a closing speed above vmin_cm_s, and two consecutive
28 * qualifying samples. A predictively opened bolt that has not yet arrived
29 * relocks the moment the approach stops (closing speed decays) or the
30 * arrival is overdue — a stationary credential outside the unlock radius
31 * never holds the door open.
32 *
33 * Pure logic, caller-allocated state, no platform dependencies: the same unit
34 * runs on the target and in the host suite / web twin replays. Call from task
35 * context only (uses float math; never from the UWB RX/ISR path).
36 */
37#ifndef ALIRO_APPROACH_H
38#define ALIRO_APPROACH_H
39
40#include <stdbool.h>
41#include <stdint.h>
42
43#ifdef __cplusplus
44extern "C" {
45#endif
46
47/* Spike-rejecting median window over trusted ranges (odd; samples). */
48#define ALIRO_APPROACH_MEDIAN_N 5
49
50/* One decision per fed sample / tick; the caller mirrors these onto the bolt.
51 * UNLOCK/RELOCK values fire once per transition (the controller tracks the
52 * bolt state), so acting on every non-HOLD return is idempotent-safe. */
53enum aliro_approach_action {
54 ALIRO_APPROACH_HOLD = 0,
55 /* ETA inside the retraction window: start the bolt now so it finishes
56 * at arrival. */
57 ALIRO_APPROACH_UNLOCK_PREDICT,
58 /* Classic presence unlock: median inside unlock_cm for near_dwell
59 * consecutive samples. */
60 ALIRO_APPROACH_UNLOCK_THRESHOLD,
61 /* Departure: median beyond relock_cm for far_dwell consecutive
62 * samples (or the peer vanished, via aliro_approach_gone()). */
63 ALIRO_APPROACH_RELOCK_DEPART,
64 /* Predictive open aborted: the approach stopped/turned or arrival is
65 * overdue while still outside the unlock radius. */
66 ALIRO_APPROACH_RELOCK_ABORT,
67};
68
77 int32_t unlock_cm; /* presence radius; also the ETA target */
78 int32_t relock_cm; /* departure radius (hysteresis band top) */
79 int near_dwell; /* consecutive medians <= unlock_cm to unlock */
80 int far_dwell; /* consecutive medians >= relock_cm to relock */
81 int32_t motor_ms; /* bolt retraction time for this lock model */
82 int32_t margin_ms; /* scheduling slack on top of motor_ms; keep it
83 * >= one ranging block (192 ms) so the discrete
84 * sample grid cannot miss the window */
85 int32_t vmin_cm_s; /* min closing speed for a prediction to fire */
86 /*
87 * How long a silence that STARTED beyond relock_cm counts as departure.
88 *
89 * far_dwell needs consecutive far SAMPLES, and a phone being carried
90 * away stops producing them: it leaves UWB range, or iOS stops ranging,
91 * usually after one or two far readings. Measured on hardware -- 13 cm,
92 * 139 cm, 378 cm, then silence, then the link dropped 3.2 s later with
93 * far_dwell at 1 of 3. The relock then had no session left to go out
94 * on and was replayed into the NEXT approach, which the user sees as a
95 * door that stays unlocked all the way down the street and relocks as
96 * they come back.
97 *
98 * Silence alone must NOT mean departure -- a phone held still nearby
99 * also stops ranging, and relocking under the owner's hand is the worse
100 * failure. The last measurement is what separates them: last seen far
101 * and now quiet is leaving; last seen near and now quiet is standing
102 * there. 0 disables and restores the sample-only behaviour.
103 */
104 int32_t far_silence_ms;
105 bool predict_en; /* arm the prediction path at all; false leaves the
106 * presence path exactly as it shipped. Off whenever
107 * the RSSI power gate is on: the gate withholds
108 * ranging until the credential is already inside
109 * unlock_cm, so no ETA can ever arm and the two
110 * features would only pretend to cooperate. */
111};
112
122 struct aliro_approach_cfg cfg;
123
124 /* bolt mirror + presence path */
125 bool locked;
126 int32_t win[ALIRO_APPROACH_MEDIAN_N];
127 int wlen, wpos;
128 int near_dwell, far_dwell;
129
130 /* constant-velocity Kalman filter (cm, cm/s; v < 0 = closing) */
131 bool kf_init;
132 int accepted; /* samples accepted since (re)init */
133 int rejects; /* consecutive innovation-gate rejects */
134 int64_t last_ms;
135 float d, v;
136 float p00, p01, p11;
137
138 /* prediction */
139 int pred_dwell; /* consecutive in-window samples */
140 bool pred_open; /* opened predictively, not yet arrived */
141 int64_t pred_deadline_ms; /* arrive by this or RELOCK_ABORT */
142 int32_t eta_ms; /* last ETA to unlock_cm; -1 = none */
143
144 /* departure-by-silence; see aliro_approach_cfg::far_silence_ms */
145 int32_t last_cm; /* last RAW sample, not the median: the median is
146 * there to reject spikes while tracking, and the
147 * question here is what was actually measured last */
148 int64_t last_feed_ms; /* when it arrived; 0 = nothing yet */
149};
150
151/* Fill cfg with the tuned defaults (100/250 cm band, 2/3 dwells, 500 ms
152 * motor + 250 ms margin, 30 cm/s floor). */
154
155/* cfg == NULL uses the defaults. Starts locked, idle. */
156void aliro_approach_init(struct aliro_approach *ap, const struct aliro_approach_cfg *cfg);
157
158/* One trusted range sample (task context, timestamps in ms, any monotonic
159 * base). Runs both paths, returns at most one transition. */
160enum aliro_approach_action aliro_approach_feed(struct aliro_approach *ap, int64_t now_ms,
161 int32_t cm);
162
184void aliro_approach_observe_departure(struct aliro_approach *ap, int64_t now_ms, int32_t cm);
185
186/* Periodic call while no sample arrived (the controller's idle tick).
187 * Supervises an overdue predictive open, and relocks a departure whose far
188 * samples stopped before far_dwell could count them (far_silence_ms). */
189enum aliro_approach_action aliro_approach_tick(struct aliro_approach *ap, int64_t now_ms);
190
191/* Peer gone (ranging silent past the caller's timeout): reset for the next
192 * approach; returns RELOCK_DEPART if the bolt was open, else HOLD. */
193enum aliro_approach_action aliro_approach_gone(struct aliro_approach *ap);
194
195/* Trace accessors (for the ALAB walk-up report / twin overlays). */
196bool aliro_approach_locked(const struct aliro_approach *ap);
197int32_t aliro_approach_est_cm(const struct aliro_approach *ap); /* -1 = none */
198int32_t aliro_approach_vel_cm_s(const struct aliro_approach *ap); /* >0 = closing */
199int32_t aliro_approach_eta_ms(const struct aliro_approach *ap); /* -1 = none */
200
201#ifdef __cplusplus
202}
203#endif
204
205#endif /* ALIRO_APPROACH_H */
bool aliro_approach_locked(const struct aliro_approach *ap)
Return true if the door is locked, false if unlocked.
Definition aliro_approach.c:352
enum aliro_approach_action aliro_approach_feed(struct aliro_approach *ap, int64_t now_ms, int32_t cm)
Update the Kalman filter state with a new range measurement, compute estimated time-to-arrival (ETA) ...
Definition aliro_approach.c:202
int32_t aliro_approach_eta_ms(const struct aliro_approach *ap)
Return the estimated time in milliseconds until approach completes (unlock reaches the door).
Definition aliro_approach.c:387
void aliro_approach_defaults(struct aliro_approach_cfg *cfg)
Initialize an approach configuration with factory defaults: unlock at 100 cm, relock at 250 cm,...
Definition aliro_approach.c:138
void aliro_approach_init(struct aliro_approach *ap, const struct aliro_approach_cfg *cfg)
Initialize an approach controller to locked state with zero velocity and no prediction in flight.
Definition aliro_approach.c:171
enum aliro_approach_action aliro_approach_tick(struct aliro_approach *ap, int64_t now_ms)
Advance the approach state machine by one tick: handle predictive unlock abort on deadline,...
Definition aliro_approach.c:307
enum aliro_approach_action aliro_approach_gone(struct aliro_approach *ap)
Reset the approach controller to locked state while preserving its configuration.
Definition aliro_approach.c:340
int32_t aliro_approach_est_cm(const struct aliro_approach *ap)
Return the current estimated distance in centimeters.
Definition aliro_approach.c:361
void aliro_approach_observe_departure(struct aliro_approach *ap, int64_t now_ms, int32_t cm)
Record a range for the DEPARTURE decision alone, trust gate or no trust gate.
Definition aliro_approach.c:287
int32_t aliro_approach_vel_cm_s(const struct aliro_approach *ap)
Return the current velocity in centimeters per second (positive = approaching, negative = receding).
Definition aliro_approach.c:373
Configuration for approach detection: unlock_cm (presence radius and ETA target), relock_cm (departur...
Definition aliro_approach.h:76
State machine and Kalman filter for approach detection and predictive unlock.
Definition aliro_approach.h:121