feat(rover): add drive profiles and leased remote command channel

This commit is contained in:
DCCONSTRUCTIONS
2026-09-25 16:37:56 +03:00
parent 53818230f9
commit 63cbb08ea0
18 changed files with 1845 additions and 0 deletions
+115
View File
@@ -0,0 +1,115 @@
import pytest
from k1link.fleet.rover_control import RoverControl
@pytest.fixture
def hub():
now=[10.]
h=RoverControl(lambda:now[0])
h.exchange('node',{'relay_id':'a'*32,'rover':{'supported':True,'instance':'driver','state':'observing'}})
return h,now
def arm(h):
return h.arm('node',{'standstill_confirmed':True,'current_a':30,'max_erpm':2000})['session_id']
def cmd(id,seq=1,**kw):
return {'session_id':id,'sequence':seq,'left':1,'right':1,'stop':False,**kw}
def exchange(h,**kw):
return h.exchange('node',{'relay_id':'a'*32,'rover':{'supported':True,'instance':'driver',**kw}})
def test_expired_browser_cannot_be_revived_by_late_packet(hub):
h,now=hub;id=arm(h);h.command('node',cmd(id));now[0]+=.401
assert exchange(h)['command'] is None
with pytest.raises(ValueError):h.command('node',cmd(id,2))
def test_sequence_stop_and_single_owner(hub):
h,_=hub;id=arm(h)
with pytest.raises(ValueError):arm(h)
h.command('node',cmd(id,2))
with pytest.raises(ValueError):h.command('node',cmd(id,1))
assert exchange(h)['command']['sequence']==2
h.command('node',cmd(id,3,stop=True))
with pytest.raises(ValueError):h.command('node',cmd(id,4))
assert exchange(h)['command'] is None
@pytest.mark.parametrize('snapshot',[{'state':'receiver'},{'state':'fault'},{'instance':'new-driver'}])
def test_takeover_fault_or_driver_restart_revokes(hub,snapshot):
h,_=hub;id=arm(h)
assert exchange(h,session_id=id,**snapshot)['command'] is None
def test_relay_restart_and_cross_board_do_not_inherit_authority(hub):
h,_=hub;id=arm(h)
with pytest.raises(ValueError):h.command('other',cmd(id))
assert h.exchange('node',{'relay_id':'b'*32,'rover':{}})['command'] is None
@pytest.mark.parametrize('value',[True,float('nan'),float('inf'),1.01,-1.01,'1'])
def test_invalid_demand_cannot_refresh(hub,value):
h,_=hub;id=arm(h)
with pytest.raises(ValueError):h.command('node',cmd(id,left=value))
def test_stale_telemetry_is_unavailable_and_arm_requires_current_board(hub):
h,now=hub;now[0]+=1.1
assert h.view('node')['snapshot']=={}
with pytest.raises(ValueError):arm(h)
def test_stream_keeps_absolute_deadline_and_never_renews_held_frame(hub):
h, now = hub
identifier = arm(h)
h.command('node', cmd(identifier, 1))
first = h.stream('node', 'a'*32)
now[0] += .2
repeated = h.stream('node', 'a'*32)
assert repeated['command'] == first['command']
assert repeated['control_clock']['monotonic_ms'] > first['control_clock']['monotonic_ms']
assert first['command']['expires_mono_ms'] == pytest.approx(10400)
now[0] += .201
assert h.stream('node', 'a'*32)['command'] is None
with pytest.raises(ValueError): h.command('node', cmd(identifier, 2))
def test_stream_delivers_new_intent_without_waiting_for_telemetry(hub):
h, now = hub
identifier = arm(h)
h.command('node', cmd(identifier, 1))
h.command('node', cmd(identifier, 2, left=-1))
assert h.stream('node', 'a'*32)['command']['left'] == -1
assert h.stream('node', 'b'*32)['command'] is None
h.command('node', cmd(identifier, 3, stop=True))
assert h.stream('node', 'a'*32)['command'] is None
def test_new_core_has_distinct_clock_epoch(hub):
h, _ = hub
assert h.stream('node', 'a'*32)['control_clock']['instance'] != RoverControl().clock_id
def test_stream_retires_control_if_return_telemetry_is_lost(hub):
h, now = hub
identifier = arm(h)
for seq in range(1, 13):
h.command('node', cmd(identifier, seq))
now[0] += .09
assert h.stream('node', 'a'*32)['command'] is None
with pytest.raises(ValueError): h.command('node', cmd(identifier, 13))
def test_intent_wait_wakes_on_update_and_does_not_miss_prior_update(hub):
import threading
from unittest.mock import patch
h, _ = hub
identifier = arm(h)
previous = h.stream('node', 'a'*32)['command']
entered, done = threading.Event(), threading.Event()
def waiter():
entered.set()
h.wait_for_intent('node', 'a'*32, previous, timeout=2)
done.set()
worker = threading.Thread(target=waiter)
worker.start()
assert entered.wait(1)
h.command('node', cmd(identifier, 1))
assert done.wait(.5), 'new intent waited for the periodic keepalive'
worker.join(2)
# A command arriving after socket write but before wait must not be lost.
with patch.object(h.changed, 'wait', side_effect=AssertionError('missed prior update')):
h.wait_for_intent('node', 'a'*32, previous, timeout=2)
latest = h.stream('node', 'a'*32)['command']
h.command('node', cmd(identifier, 2, stop=True))
with patch.object(h.changed, 'wait', side_effect=AssertionError('missed stop')):
h.wait_for_intent('node', 'a'*32, latest, timeout=2)