152 lines
9.5 KiB
Python
152 lines
9.5 KiB
Python
"""One explicit logical-button5 selection trial; adapters own all native I/O.
|
|
|
|
Each snapshot joins native pose/input/gate and choices at the same nonce/index/
|
|
real clock. choices contains nonce,sample,real_ms,selected_id,actions; actions
|
|
contains exactly [{class,id,permitted}, ...]. This deliberately supports only
|
|
closed disabled ERR_PrisonDoorAction followed by permitted ERR_UnlockPrisonAction.
|
|
Their distinct stock/authored names cannot merge in stock action aggregation.
|
|
wheel.pulse(5,guard,deadline=...) must return button/down_monotonic/up_monotonic/
|
|
released, after actual paired release confirmation. A failure must retain its
|
|
own uncertain-button evidence and attempt safe release; this policy cannot send
|
|
another button event. No default workflow activates this module.
|
|
"""
|
|
import copy
|
|
import math
|
|
from prison_approach_proof import number, vector, length, yaw, delta, horizontal, target_for
|
|
|
|
|
|
class Refused(ValueError):
|
|
pass
|
|
|
|
|
|
def need(ok, message):
|
|
if not ok:
|
|
raise Refused(message)
|
|
|
|
|
|
def choices_for(s, geometry, *, selected_lock=False):
|
|
"""Require the exact two-entry experiment, not arbitrary raw-list navigation."""
|
|
c = s.get('choices')
|
|
need(type(c) is dict and set(c) == {'nonce','sample','real_ms','selected_id','selected_class','actions'}, 'Missing exact native choices')
|
|
need(c['nonce'] == geometry['nonce'] and type(c['sample']) is int and c['sample'] == s['sample'] and
|
|
number(c['real_ms']) == s['real_ms'], 'Choices are not joined to the native sample')
|
|
actions = c['actions']
|
|
need(type(actions) is list and len(actions) == 2, 'Selection requires exactly two native actions')
|
|
for a, name, permitted in zip(actions, ('ERR_PrisonDoorAction','ERR_UnlockPrisonAction'), (False, True)):
|
|
need(type(a) is dict and set(a) == {'class','id','permitted'} and a['class'] == name and
|
|
type(a['id']) is int and a['id'] >= 0 and a['permitted'] is permitted,
|
|
'Native action class/order/permission differs from bounded trial')
|
|
need(actions[0]['id'] != actions[1]['id'], 'Native action IDs collide')
|
|
need(type(c['selected_id']) is int and c['selected_id'] == actions[int(selected_lock)]['id'],
|
|
'Unexpected native selected action ID')
|
|
need(c['selected_class'] == actions[int(selected_lock)]['class'], 'Native selected action class disagrees')
|
|
need(s['context_selected'] is True and s['cached_performable'] is True and
|
|
s['selected'] is selected_lock and s['progress_bound'] is selected_lock,
|
|
'Native selected/context/permission binding disagrees')
|
|
return copy.deepcopy(actions)
|
|
|
|
|
|
def perform_selection(guard, require_idle, wheel, observe, geometry, calibration, facing_evidence, report, *, clock, sleep, notify):
|
|
need('action_selection_proof' not in report, 'Selection attempt already exists')
|
|
pinned = copy.deepcopy(geometry); target_for(pinned)
|
|
facing = report.get('facing_proof', {})
|
|
need(facing_evidence.get('verified') is True and facing.get('completed') is True and
|
|
facing.get('nonce') == pinned['nonce'] and facing.get('geometry') == pinned and
|
|
facing_evidence.get('last_sample') == facing.get('final_sample'), 'Independent same-run facing proof required')
|
|
prior = facing.get('samples', [])
|
|
need(prior and prior[-1]['sample'] == facing['final_sample'], 'Facing final native sample unavailable')
|
|
anchor = prior[-1]; body = vector(anchor['origin']); actions = choices_for(anchor, pinned)
|
|
started = clock(); deadline = number(facing['deadline_monotonic'])
|
|
queued = number(calibration['observer_start_real_ms']); ready = number(calibration['ready_real_ms'])
|
|
ready_at = number(calibration['ready_monotonic'])
|
|
need(calibration.get('nonce') == pinned['nonce'] and 0 <= queued and 2000 <= ready-queued < 45000 and
|
|
0 <= ready_at <= started, 'Original observation clock binding invalid')
|
|
original = ready_at+(45000-(ready-queued))/1000-.5
|
|
need(deadline <= number(facing['started_monotonic'])+8 and deadline <= original,
|
|
'Facing deadline renewed the original lease')
|
|
state = {'requested':True,'completed':False,'nonce':pinned['nonce'],'geometry':pinned,
|
|
'started_monotonic':started,'finished_monotonic':None,'deadline_monotonic':deadline,
|
|
'initial_sample':None,'final_sample':None,'selection_samples':[], 'actions':actions,
|
|
'wheel':None,'samples':[]}
|
|
report['action_selection_proof'] = state
|
|
previous = None; dispatched = False
|
|
|
|
def consume(raw):
|
|
nonlocal previous
|
|
s = copy.deepcopy(raw)
|
|
index = s['sample']; real = number(s['real_ms']); receipt = number(s['received_monotonic'])
|
|
need(type(index) is int and facing['final_sample'] <= index <= 450 and 0 <= receipt <= clock() and
|
|
queued <= real < queued+45000, 'Native selection index/clock invalid')
|
|
need(s['geometry'] == pinned and s['phase'] == 1 and s['inventory'] is False and
|
|
number(s['door_state']) == 0 and number(s['door_control']) == 0, 'Original closed prison context changed')
|
|
for k in ('origin','camera','direction','body_forward','input','velocity'): s[k] = vector(s[k])
|
|
need(math.dist(body,s['origin']) <= .05 and abs(number(s['forward'])) <= .01 and
|
|
length(s['input']) <= .01 and length(s['velocity']) <= .05, 'Body or movement changed during selection')
|
|
for k in ('selected','context_selected','cached_performable','progress_bound','performing'):
|
|
need(type(s[k]) is bool, 'Malformed native action flag')
|
|
need(number(s['pressed']) == 0 and number(s['progress']) == 0 and not s['performing'] and
|
|
number(s['duration']) == 4, 'Action began during selection-only trial')
|
|
need(.25 <= horizontal(s['camera'],pinned['context']) <= 5, 'Camera/handle bearing invalid')
|
|
bearing = math.degrees(math.atan2(pinned['context'][0]-s['camera'][0],pinned['context'][2]-s['camera'][2]))
|
|
need(abs(delta(bearing,yaw(s['direction']))) <= 3 and
|
|
abs(delta(yaw(s['direction']),yaw(anchor['direction']))) <= .2, 'Facing changed during wheel-only trial')
|
|
observed = choices_for(s,pinned,selected_lock=s['selected'])
|
|
need(observed == actions, 'Original action identities/order changed')
|
|
if not dispatched: need(not s['selected'], 'Lock selected before wheel dispatch')
|
|
if previous:
|
|
need(not previous['selected'] or s['selected'], 'Native lock selection dropped after wheel acknowledgement')
|
|
need(index >= previous['sample'] and real >= previous['real_ms'], 'Native samples reversed')
|
|
if index == previous['sample']: need(s == previous, 'Repeated native sample changed')
|
|
else: need(index == previous['sample']+1 and 0 < real-previous['real_ms'] <= 500, 'Native sample gap')
|
|
if previous is None or index != previous['sample']: state['samples'].append(copy.deepcopy(s))
|
|
previous = s
|
|
if state['initial_sample'] is None: state['initial_sample'] = index
|
|
state['final_sample'] = index
|
|
return s
|
|
|
|
def read():
|
|
need(clock() < deadline, 'Original facing/selection deadline expired')
|
|
guard.check(); require_idle()
|
|
batch = observe()
|
|
need(type(batch) is list and 1 <= len(batch) <= 450, 'Missing joined selection samples')
|
|
for raw in batch: s = consume(raw)
|
|
need(0 <= clock()-s['received_monotonic'] <= .5 and clock() < deadline, 'Native selection evidence stale or expired')
|
|
return s
|
|
|
|
try:
|
|
notify({'kind':'action-selection-pending','deadline_monotonic':deadline})
|
|
current = read()
|
|
need(clock()+1.5 <= deadline, 'Insufficient unchanged lease for one wheel trial')
|
|
# No native calls here: adapter rechecks ownership/idle/deadline immediately
|
|
# before each request and confirms release on the same connection.
|
|
before = current['sample']; request_at = clock()
|
|
state['wheel'] = {'button':5,'attempted_monotonic':request_at,'after_sample':before,'confirmed':False}
|
|
result = wheel.pulse(5,guard,deadline=min(deadline,request_at+1.5))
|
|
need(type(result) is dict and set(result) == {'button','down_monotonic','up_monotonic','released'} and
|
|
type(result['button']) is int and result['button'] == 5 and result['released'] is True,
|
|
'Wheel paired release not confirmed')
|
|
down = number(result['down_monotonic']); up = number(result['up_monotonic'])
|
|
need(request_at <= down < up <= clock() < deadline and up-request_at <= 1.5,
|
|
'Wheel attempt exceeded immutable lease')
|
|
state['wheel'].update(result, confirmed=True)
|
|
dispatched = True
|
|
stop = min(deadline,up+1.5)
|
|
while clock() < stop:
|
|
current = read()
|
|
need(clock() < stop, 'Native selection acknowledgement exceeded bound')
|
|
samples = state['samples']
|
|
if len(samples) >= 2:
|
|
a,b = samples[-2:]
|
|
if a['sample'] > before and a['received_monotonic'] >= down and a['selected'] and b['selected'] and b['sample'] == a['sample']+1 and b['real_ms']-a['real_ms'] >= 100 and abs(delta(yaw(b['direction']),yaw(a['direction']))) <= .2:
|
|
state['selection_samples'] = [a['sample'],b['sample']]
|
|
state['completed'] = True
|
|
return state
|
|
sleep(.025)
|
|
raise Refused('Single wheel did not select original lock before deadline')
|
|
except Exception as error:
|
|
state['error'] = str(error)
|
|
raise
|
|
finally:
|
|
state['finished_monotonic'] = clock()
|
|
notify({'kind':'action-selection-finished','successful':state['completed']})
|