Spaces:
Running
Running
Gaëlle Lannuzel commited on
Commit ·
80f2124
1
Parent(s): c67cff6
Button press should also work
Browse files- app.py +35 -26
- tests/test_start_wait_camera.py +56 -0
app.py
CHANGED
|
@@ -196,11 +196,9 @@ SLAM_QUALITY_TIMEOUT_S = 10.0
|
|
| 196 |
# peer overran a 12s lead by ~0.7s (cold boot ≈10.2s, but it only had ~9.5s
|
| 197 |
# after poll delivery), so 15s gives ~2.3s margin over the observed cold boot.
|
| 198 |
GROUP_START_LEAD_COLD_S = 15.0 # OAK-D likely asleep → cover poll delivery + cold boot
|
| 199 |
-
# Warm: only command delivery
|
| 200 |
GROUP_START_LEAD_WARM_S = 1.0
|
| 201 |
-
#
|
| 202 |
-
# camera reports warm (heartbeat), rather than paying the full cold lead. Past
|
| 203 |
-
# this, give up waiting and start with the cold lead anyway.
|
| 204 |
START_WAIT_CAMERA_MAX_S = 20.0
|
| 205 |
# Fleet can't see the OAK-D power state directly; it infers "warm" from the time
|
| 206 |
# since the session's last recording stop. This window MUST stay safely below
|
|
@@ -860,10 +858,9 @@ class Session:
|
|
| 860 |
start_errors: dict[str, str] = field(default_factory=dict)
|
| 861 |
# device_id -> epoch of the last depth-camera lease sent (see DEPTH_HOLD_TTL_S).
|
| 862 |
depth_hold_sent_at: dict[str, float] = field(default_factory=dict)
|
| 863 |
-
# Epoch of a
|
| 864 |
-
# START_WAIT_CAMERA_MAX_S). None = no start waiting.
|
| 865 |
start_waiting_since: Optional[float] = None
|
| 866 |
-
# Why a waiting start
|
| 867 |
start_wait_error: str = ""
|
| 868 |
# Each entry: {"episode_id": str, "roles": {role: device_id}, "started_at": float}
|
| 869 |
episodes: list[dict[str, Any]] = field(default_factory=list)
|
|
@@ -1543,8 +1540,7 @@ async def list_devices(request: Request) -> dict[str, Any]:
|
|
| 1543 |
# Why this device refuses to record ("" = healthy). Separate from
|
| 1544 |
# activity: a device can be both faulted and busy.
|
| 1545 |
"hardware_error": d.hardware_error,
|
| 1546 |
-
#
|
| 1547 |
-
# "camera is initializing" note.
|
| 1548 |
"depth_warm": d.depth_warm,
|
| 1549 |
# id of the dataset build this device is working for ("" = none):
|
| 1550 |
# what the fleet list's Cancel button acts on.
|
|
@@ -2574,7 +2570,7 @@ def _get_open_session(owner: str, session_id: str) -> Session:
|
|
| 2574 |
|
| 2575 |
|
| 2576 |
def _start_session_episode(owner: str, s: Session) -> tuple[str, str]:
|
| 2577 |
-
"""
|
| 2578 |
s.start_waiting_since = None
|
| 2579 |
_task = _tasks_of(owner).get(s.task_id)
|
| 2580 |
target_iso = _schedule_episode_start(
|
|
@@ -2593,8 +2589,7 @@ def _start_session_episode(owner: str, s: Session) -> tuple[str, str]:
|
|
| 2593 |
|
| 2594 |
|
| 2595 |
def _should_wait_for_cameras(owner: str, s: Session) -> bool:
|
| 2596 |
-
"""
|
| 2597 |
-
warm (hold capability) — otherwise nothing would ever end the wait."""
|
| 2598 |
fleet = _fleet_of(owner)
|
| 2599 |
devs = [fleet.get(d) for d in s.members.values()]
|
| 2600 |
if any(d is None or DEPTH_HOLD_CAPABILITY not in d.capabilities for d in devs):
|
|
@@ -2603,8 +2598,7 @@ def _should_wait_for_cameras(owner: str, s: Session) -> bool:
|
|
| 2603 |
|
| 2604 |
|
| 2605 |
def _launch_waiting_start(owner: str, dev: Device) -> None:
|
| 2606 |
-
"""
|
| 2607 |
-
camera is warm, or with the cold lead once the wait has run too long."""
|
| 2608 |
s = _open_session_for_device(owner, dev.device_id)
|
| 2609 |
if s is None or s.start_waiting_since is None:
|
| 2610 |
return
|
|
@@ -2615,7 +2609,7 @@ def _launch_waiting_start(owner: str, dev: Device) -> None:
|
|
| 2615 |
_raise_if_recording_blocked(owner, s.members.values())
|
| 2616 |
_start_session_episode(owner, s)
|
| 2617 |
except HTTPException as e:
|
| 2618 |
-
#
|
| 2619 |
s.start_waiting_since = None
|
| 2620 |
detail = e.detail if isinstance(e.detail, dict) else {"message": str(e.detail)}
|
| 2621 |
s.start_wait_error = str(detail.get("message") or "start failed")
|
|
@@ -2632,7 +2626,7 @@ async def session_episode_start(session_id: str, request: Request) -> dict[str,
|
|
| 2632 |
_raise_if_recording_blocked(owner, s.members.values())
|
| 2633 |
s.start_wait_error = ""
|
| 2634 |
if _should_wait_for_cameras(owner, s):
|
| 2635 |
-
#
|
| 2636 |
_raise_if_start_refused(owner, s.members)
|
| 2637 |
s.start_waiting_since = time.time()
|
| 2638 |
return {"status": "waiting_camera", "devices": list(s.members.values())}
|
|
@@ -2646,7 +2640,7 @@ async def session_episode_stop(session_id: str, request: Request) -> dict[str, A
|
|
| 2646 |
owner = await _operator_loaded(request)
|
| 2647 |
s = _get_open_session(owner, session_id)
|
| 2648 |
if s.start_waiting_since is not None and not s.recording:
|
| 2649 |
-
#
|
| 2650 |
s.start_waiting_since = None
|
| 2651 |
return {"status": "ok", "devices": list(s.members.values())}
|
| 2652 |
ids = _dispatch_episode_stop(owner, s.members)
|
|
@@ -3543,9 +3537,17 @@ async def start_slam_check(req: SlamCheckReq, request: Request) -> dict[str, Any
|
|
| 3543 |
# device (found by membership). It behaves exactly like the operator starting
|
| 3544 |
# an episode from the dashboard: same task, same synchronized T0 across the
|
| 3545 |
# session's members. No open session → solo (the device records locally). ======
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3546 |
@app.post("/api/devices/{device_id}/sync/start")
|
| 3547 |
-
async def device_sync_start(device_id: str,
|
|
|
|
| 3548 |
owner, _token = auth
|
|
|
|
| 3549 |
if device_id not in _fleet_of(owner):
|
| 3550 |
raise HTTPException(404, "Device not registered")
|
| 3551 |
s = _open_session_for_device(owner, device_id)
|
|
@@ -3558,8 +3560,14 @@ async def device_sync_start(device_id: str, auth: tuple[str, str] = Depends(devi
|
|
| 3558 |
# busy peer refuses its own start_capture locally. A 409 travels back as
|
| 3559 |
# "refused", which the device turns into an error buzz and no recording.
|
| 3560 |
_raise_if_recording_blocked(owner, s.members.values())
|
| 3561 |
-
|
| 3562 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3563 |
s.start_waiting_since = None
|
| 3564 |
s.start_wait_error = ""
|
| 3565 |
task_name = _task_name(owner, s.task_id)
|
|
@@ -3589,7 +3597,10 @@ async def device_sync_stop(device_id: str, auth: tuple[str, str] = Depends(devic
|
|
| 3589 |
s = _open_session_for_device(owner, device_id)
|
| 3590 |
if s is None:
|
| 3591 |
return {"status": "solo"}
|
| 3592 |
-
s.start_waiting_since
|
|
|
|
|
|
|
|
|
|
| 3593 |
ids = _dispatch_episode_stop(owner, s.members, exclude_device_id=device_id)
|
| 3594 |
_begin_episode_stop(s, ids)
|
| 3595 |
return {"status": "ok", "peers": [d for d in s.members.values() if d != device_id]}
|
|
@@ -3980,7 +3991,7 @@ _INDEX = """<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">
|
|
| 3980 |
/* record circle blocked because a member is busy with a dataset job */
|
| 3981 |
.rec-toggle.blocked{cursor:not-allowed;opacity:.4;border-color:rgba(255,255,255,.3)}
|
| 3982 |
.sp-busy-note{margin-top:.6rem;text-align:center;font-size:.8rem;color:#d6c9fb}
|
| 3983 |
-
/*
|
| 3984 |
.sp-depth-note{margin-top:.6rem;display:flex;align-items:center;gap:.6rem;
|
| 3985 |
font-size:.85rem;font-weight:600;color:#fcd34d;line-height:1.4;
|
| 3986 |
background:rgba(245,158,11,.14);border:1px solid rgba(251,191,36,.45);
|
|
@@ -4467,8 +4478,7 @@ function activityBadge(id){
|
|
| 4467 |
// calibration, no angle sensors). Shown next to the activity badge rather than
|
| 4468 |
// instead of it — a faulted grabette can also be uploading, and hiding either
|
| 4469 |
// fact behind the other is how you go hunting for the wrong problem.
|
| 4470 |
-
//
|
| 4471 |
-
// cold boot (long lead). Only devices that take the hold report this.
|
| 4472 |
function deviceDepthCold(id){const d=DEVICES.find(x=>x.device_id===id);
|
| 4473 |
return !!(d&&d.online&&(d.capabilities||[]).includes('hold_depth_camera')&&d.depth_warm!==true);}
|
| 4474 |
function sessionDepthCold(s){return Object.values(s.members).some(m=>deviceDepthCold(m.device_id));}
|
|
@@ -5398,7 +5408,7 @@ async function deleteSession(id){if(!confirm('Delete this session and its manife
|
|
| 5398 |
// stopping is reported by the fleet and ends when the devices actually finish
|
| 5399 |
// (s.stopping is cleared as each stop_capture reports its result).
|
| 5400 |
function sessionPhase(s){
|
| 5401 |
-
if(s.start_waiting)return 'initializing'; // no T0 yet
|
| 5402 |
if(s.recording){
|
| 5403 |
const ep=(s.episodes&&s.episodes.length)?s.episodes[s.episodes.length-1]:null;
|
| 5404 |
const t0=ep&&ep.start_at_utc?Date.parse(ep.start_at_utc):0;
|
|
@@ -5901,7 +5911,6 @@ function renderSessionList(){
|
|
| 5901 |
const circTitle=faultBlocked?'A device cannot record — see the fault below'
|
| 5902 |
:recBlocked?`A device is busy ${busyWhat} — wait for it to finish`:M.title;
|
| 5903 |
const circle=`<div class="sp-rec"><button class="rec-toggle ${M.btn}${recBlocked?' blocked':''}" ${circDis} title="${circTitle}" onclick='episodeToggle("${s.id}")'><span class="rt-inner"></span></button></div>`;
|
| 5904 |
-
// Kept through "initializing": that is exactly when the operator waits on it.
|
| 5905 |
const depthCold=((p==='idle'&&!recBlocked)||p==='initializing')&&sessionDepthCold(s);
|
| 5906 |
const depthNote=depthCold?'<div class="sp-depth-note"><span class="sp-spinner"></span><span>Camera is currently initializing. First recording may take longer to start.</span></div>':'';
|
| 5907 |
const busyNote=(recBlocked&&!faultBlocked)?`<div class="sp-busy-note">${busyMembers.map(deviceName).join(', ')} busy ${busyWhat} — recording paused until it finishes.</div>`:'';
|
|
|
|
| 196 |
# peer overran a 12s lead by ~0.7s (cold boot ≈10.2s, but it only had ~9.5s
|
| 197 |
# after poll delivery), so 15s gives ~2.3s margin over the observed cold boot.
|
| 198 |
GROUP_START_LEAD_COLD_S = 15.0 # OAK-D likely asleep → cover poll delivery + cold boot
|
| 199 |
+
# Warm: only covers command delivery (long-poll, see _enqueue).
|
| 200 |
GROUP_START_LEAD_WARM_S = 1.0
|
| 201 |
+
# Max wait for the depth cameras before starting with the cold lead anyway.
|
|
|
|
|
|
|
| 202 |
START_WAIT_CAMERA_MAX_S = 20.0
|
| 203 |
# Fleet can't see the OAK-D power state directly; it infers "warm" from the time
|
| 204 |
# since the session's last recording stop. This window MUST stay safely below
|
|
|
|
| 858 |
start_errors: dict[str, str] = field(default_factory=dict)
|
| 859 |
# device_id -> epoch of the last depth-camera lease sent (see DEPTH_HOLD_TTL_S).
|
| 860 |
depth_hold_sent_at: dict[str, float] = field(default_factory=dict)
|
| 861 |
+
# Epoch of a start waiting for the depth cameras (no T0 yet).
|
|
|
|
| 862 |
start_waiting_since: Optional[float] = None
|
| 863 |
+
# Why a waiting start was dropped.
|
| 864 |
start_wait_error: str = ""
|
| 865 |
# Each entry: {"episode_id": str, "roles": {role: device_id}, "started_at": float}
|
| 866 |
episodes: list[dict[str, Any]] = field(default_factory=list)
|
|
|
|
| 1540 |
# Why this device refuses to record ("" = healthy). Separate from
|
| 1541 |
# activity: a device can be both faulted and busy.
|
| 1542 |
"hardware_error": d.hardware_error,
|
| 1543 |
+
# None = not reported.
|
|
|
|
| 1544 |
"depth_warm": d.depth_warm,
|
| 1545 |
# id of the dataset build this device is working for ("" = none):
|
| 1546 |
# what the fleet list's Cancel button acts on.
|
|
|
|
| 2570 |
|
| 2571 |
|
| 2572 |
def _start_session_episode(owner: str, s: Session) -> tuple[str, str]:
|
| 2573 |
+
"""Returns (T0 iso, episode_id)."""
|
| 2574 |
s.start_waiting_since = None
|
| 2575 |
_task = _tasks_of(owner).get(s.task_id)
|
| 2576 |
target_iso = _schedule_episode_start(
|
|
|
|
| 2589 |
|
| 2590 |
|
| 2591 |
def _should_wait_for_cameras(owner: str, s: Session) -> bool:
|
| 2592 |
+
"""Only members with the hold report warm, so only they can end a wait."""
|
|
|
|
| 2593 |
fleet = _fleet_of(owner)
|
| 2594 |
devs = [fleet.get(d) for d in s.members.values()]
|
| 2595 |
if any(d is None or DEPTH_HOLD_CAPABILITY not in d.capabilities for d in devs):
|
|
|
|
| 2598 |
|
| 2599 |
|
| 2600 |
def _launch_waiting_start(owner: str, dev: Device) -> None:
|
| 2601 |
+
"""On heartbeat: launch a waiting start once the cameras are warm."""
|
|
|
|
| 2602 |
s = _open_session_for_device(owner, dev.device_id)
|
| 2603 |
if s is None or s.start_waiting_since is None:
|
| 2604 |
return
|
|
|
|
| 2609 |
_raise_if_recording_blocked(owner, s.members.values())
|
| 2610 |
_start_session_episode(owner, s)
|
| 2611 |
except HTTPException as e:
|
| 2612 |
+
# A member went offline / faulted / busy meanwhile.
|
| 2613 |
s.start_waiting_since = None
|
| 2614 |
detail = e.detail if isinstance(e.detail, dict) else {"message": str(e.detail)}
|
| 2615 |
s.start_wait_error = str(detail.get("message") or "start failed")
|
|
|
|
| 2626 |
_raise_if_recording_blocked(owner, s.members.values())
|
| 2627 |
s.start_wait_error = ""
|
| 2628 |
if _should_wait_for_cameras(owner, s):
|
| 2629 |
+
# Refuse now rather than after the wait.
|
| 2630 |
_raise_if_start_refused(owner, s.members)
|
| 2631 |
s.start_waiting_since = time.time()
|
| 2632 |
return {"status": "waiting_camera", "devices": list(s.members.values())}
|
|
|
|
| 2640 |
owner = await _operator_loaded(request)
|
| 2641 |
s = _get_open_session(owner, session_id)
|
| 2642 |
if s.start_waiting_since is not None and not s.recording:
|
| 2643 |
+
# No T0 yet: nothing to stop.
|
| 2644 |
s.start_waiting_since = None
|
| 2645 |
return {"status": "ok", "devices": list(s.members.values())}
|
| 2646 |
ids = _dispatch_episode_stop(owner, s.members)
|
|
|
|
| 3537 |
# device (found by membership). It behaves exactly like the operator starting
|
| 3538 |
# an episode from the dashboard: same task, same synchronized T0 across the
|
| 3539 |
# session's members. No open session → solo (the device records locally). ======
|
| 3540 |
+
class SyncStartReq(BaseModel):
|
| 3541 |
+
task_name: str = ""
|
| 3542 |
+
# Older firmware would record solo on "waiting_camera".
|
| 3543 |
+
can_wait: bool = False
|
| 3544 |
+
|
| 3545 |
+
|
| 3546 |
@app.post("/api/devices/{device_id}/sync/start")
|
| 3547 |
+
async def device_sync_start(device_id: str, req: Optional[SyncStartReq] = None,
|
| 3548 |
+
auth: tuple[str, str] = Depends(device_auth)) -> dict[str, Any]:
|
| 3549 |
owner, _token = auth
|
| 3550 |
+
can_wait = req is not None and req.can_wait
|
| 3551 |
if device_id not in _fleet_of(owner):
|
| 3552 |
raise HTTPException(404, "Device not registered")
|
| 3553 |
s = _open_session_for_device(owner, device_id)
|
|
|
|
| 3560 |
# busy peer refuses its own start_capture locally. A 409 travels back as
|
| 3561 |
# "refused", which the device turns into an error buzz and no recording.
|
| 3562 |
_raise_if_recording_blocked(owner, s.members.values())
|
| 3563 |
+
if can_wait and (s.start_waiting_since is not None or _should_wait_for_cameras(owner, s)):
|
| 3564 |
+
# The presser gets its start_capture like a peer.
|
| 3565 |
+
if s.start_waiting_since is None:
|
| 3566 |
+
_raise_if_start_refused(owner, s.members)
|
| 3567 |
+
s.start_waiting_since = time.time()
|
| 3568 |
+
s.start_wait_error = ""
|
| 3569 |
+
return {"status": "waiting_camera"}
|
| 3570 |
+
# Older firmware needs T0 now: cold lead.
|
| 3571 |
s.start_waiting_since = None
|
| 3572 |
s.start_wait_error = ""
|
| 3573 |
task_name = _task_name(owner, s.task_id)
|
|
|
|
| 3597 |
s = _open_session_for_device(owner, device_id)
|
| 3598 |
if s is None:
|
| 3599 |
return {"status": "solo"}
|
| 3600 |
+
if s.start_waiting_since is not None and not s.recording:
|
| 3601 |
+
# No T0 yet: nothing to stop.
|
| 3602 |
+
s.start_waiting_since = None
|
| 3603 |
+
return {"status": "ok", "peers": []}
|
| 3604 |
ids = _dispatch_episode_stop(owner, s.members, exclude_device_id=device_id)
|
| 3605 |
_begin_episode_stop(s, ids)
|
| 3606 |
return {"status": "ok", "peers": [d for d in s.members.values() if d != device_id]}
|
|
|
|
| 3991 |
/* record circle blocked because a member is busy with a dataset job */
|
| 3992 |
.rec-toggle.blocked{cursor:not-allowed;opacity:.4;border-color:rgba(255,255,255,.3)}
|
| 3993 |
.sp-busy-note{margin-top:.6rem;text-align:center;font-size:.8rem;color:#d6c9fb}
|
| 3994 |
+
/* depth camera still booting */
|
| 3995 |
.sp-depth-note{margin-top:.6rem;display:flex;align-items:center;gap:.6rem;
|
| 3996 |
font-size:.85rem;font-weight:600;color:#fcd34d;line-height:1.4;
|
| 3997 |
background:rgba(245,158,11,.14);border:1px solid rgba(251,191,36,.45);
|
|
|
|
| 4478 |
// calibration, no angle sensors). Shown next to the activity badge rather than
|
| 4479 |
// instead of it — a faulted grabette can also be uploading, and hiding either
|
| 4480 |
// fact behind the other is how you go hunting for the wrong problem.
|
| 4481 |
+
// Only devices with the hold report depth_warm.
|
|
|
|
| 4482 |
function deviceDepthCold(id){const d=DEVICES.find(x=>x.device_id===id);
|
| 4483 |
return !!(d&&d.online&&(d.capabilities||[]).includes('hold_depth_camera')&&d.depth_warm!==true);}
|
| 4484 |
function sessionDepthCold(s){return Object.values(s.members).some(m=>deviceDepthCold(m.device_id));}
|
|
|
|
| 5408 |
// stopping is reported by the fleet and ends when the devices actually finish
|
| 5409 |
// (s.stopping is cleared as each stop_capture reports its result).
|
| 5410 |
function sessionPhase(s){
|
| 5411 |
+
if(s.start_waiting)return 'initializing'; // no T0 yet
|
| 5412 |
if(s.recording){
|
| 5413 |
const ep=(s.episodes&&s.episodes.length)?s.episodes[s.episodes.length-1]:null;
|
| 5414 |
const t0=ep&&ep.start_at_utc?Date.parse(ep.start_at_utc):0;
|
|
|
|
| 5911 |
const circTitle=faultBlocked?'A device cannot record — see the fault below'
|
| 5912 |
:recBlocked?`A device is busy ${busyWhat} — wait for it to finish`:M.title;
|
| 5913 |
const circle=`<div class="sp-rec"><button class="rec-toggle ${M.btn}${recBlocked?' blocked':''}" ${circDis} title="${circTitle}" onclick='episodeToggle("${s.id}")'><span class="rt-inner"></span></button></div>`;
|
|
|
|
| 5914 |
const depthCold=((p==='idle'&&!recBlocked)||p==='initializing')&&sessionDepthCold(s);
|
| 5915 |
const depthNote=depthCold?'<div class="sp-depth-note"><span class="sp-spinner"></span><span>Camera is currently initializing. First recording may take longer to start.</span></div>':'';
|
| 5916 |
const busyNote=(recBlocked&&!faultBlocked)?`<div class="sp-busy-note">${busyMembers.map(deviceName).join(', ')} busy ${busyWhat} — recording paused until it finishes.</div>`:'';
|
tests/test_start_wait_camera.py
CHANGED
|
@@ -120,3 +120,59 @@ def test_member_lost_while_waiting_drops_start_with_error():
|
|
| 120 |
assert s.start_waiting_since is None and s.recording is False
|
| 121 |
assert not starts(left)
|
| 122 |
assert app._session_dict(OWNER, s)["start_wait_error"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
assert s.start_waiting_since is None and s.recording is False
|
| 121 |
assert not starts(left)
|
| 122 |
assert app._session_dict(OWNER, s)["start_wait_error"]
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def press(device_id, can_wait=True):
|
| 126 |
+
req = app.SyncStartReq(can_wait=can_wait)
|
| 127 |
+
return asyncio.run(app.device_sync_start(device_id, req, auth=(OWNER, "token")))
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def press_stop(device_id):
|
| 131 |
+
return asyncio.run(app.device_sync_stop(device_id, auth=(OWNER, "token")))
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def test_button_waits_then_starts_every_member_including_presser():
|
| 135 |
+
left, right = fleet()
|
| 136 |
+
s = launch()
|
| 137 |
+
left.depth_warm, right.depth_warm = True, False
|
| 138 |
+
|
| 139 |
+
assert press(LEFT)["status"] == "waiting_camera"
|
| 140 |
+
assert not starts(left) and not starts(right)
|
| 141 |
+
|
| 142 |
+
beat(RIGHT, depth_warm=True)
|
| 143 |
+
assert len(starts(left)) == len(starts(right)) == 1
|
| 144 |
+
assert lead_of(left) <= app.GROUP_START_LEAD_WARM_S
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def test_old_firmware_button_keeps_cold_lead():
|
| 148 |
+
left, right = fleet()
|
| 149 |
+
s = launch()
|
| 150 |
+
left.depth_warm, right.depth_warm = True, False
|
| 151 |
+
|
| 152 |
+
res = press(LEFT, can_wait=False)
|
| 153 |
+
assert res["status"] == "scheduled"
|
| 154 |
+
assert not starts(left) # the presser self-schedules from the response
|
| 155 |
+
assert lead_of(right) > app.GROUP_START_LEAD_WARM_S
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def test_button_joins_a_waiting_fleet_start():
|
| 159 |
+
left, right = fleet()
|
| 160 |
+
s = launch()
|
| 161 |
+
left.depth_warm, right.depth_warm = True, False
|
| 162 |
+
start(s)
|
| 163 |
+
|
| 164 |
+
assert press(LEFT)["status"] == "waiting_camera"
|
| 165 |
+
assert s.start_waiting_since is not None
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def test_button_stop_while_waiting_cancels_without_stops():
|
| 169 |
+
left, right = fleet()
|
| 170 |
+
s = launch()
|
| 171 |
+
left.depth_warm, right.depth_warm = True, False
|
| 172 |
+
press(LEFT)
|
| 173 |
+
|
| 174 |
+
press_stop(LEFT)
|
| 175 |
+
assert s.start_waiting_since is None and s.stopping is False
|
| 176 |
+
beat(RIGHT, depth_warm=True)
|
| 177 |
+
assert not starts(left) and not starts(right)
|
| 178 |
+
assert not [c for c in right.queue if c.type == "stop_capture"]
|