"""Real PipeWire plus injected UI control proof on the physical board.""" import os,fcntl,struct,time,socket,json,subprocess,re,wave,math from pathlib import Path WP='/nix/store/6rp2g29qzvx266gijs50rlvhkpfc0ald-wireplumber-riscv64-unknown-linux-gnu-0.5.17/bin/wpctl' PW='/nix/store/3xi5pr6wi4j61qv7p6dfmzrlg75ljk6x-pipewire-riscv64-unknown-linux-gnu-1.6.8/bin/' ENV=['runuser','-u','shell','--','env','XDG_RUNTIME_DIR=/run/shell','DBUS_SESSION_BUS_ADDRESS=unix:path=/run/shell/bus'] subprocess.run(['systemctl','restart','shell-ui'],check=True);time.sleep(4) checks={};captures=[];stream=None;window=None def run(args):return subprocess.check_output(ENV+args,text=True,stderr=subprocess.DEVNULL,timeout=10) def getvol(node='@DEFAULT_AUDIO_SINK@'): v=run([WP,'get-volume',str(node)]);return (float(re.search(r'Volume: ([0-9.]+)',v).group(1)),'MUTED' in v) def setvol(p):run([WP,'set-volume','@DEFAULT_AUDIO_SINK@',str(p)]);run([WP,'set-mute','@DEFAULT_AUDIO_SINK@','0']) def graph():return json.loads(run([PW+'pw-dump'])) def nodes(klass):return [v for v in graph() if (v.get('info') or {}).get('props',{}).get('media.class')==klass] def ipc(command,kind=0): with socket.socket(socket.AF_UNIX) as s: s.settimeout(5);s.connect('/run/shell/sway-ipc.sock');b=command.encode();s.sendall(b'i3-ipc'+struct.pack('=II',len(b),kind)+b) def read(n): b=b'' while len(b)baseline);capture('hud-over-home');checks['hud_maps_over_home_without_another_sheet']=True setvol(.48);time.sleep(.25);held_unmaps=journal_count('volume-hud-unmap');down(520,600);time.sleep(.1) for n in range(1,10):move(520,600+200*n/9);time.sleep(.04) time.sleep(3);checks['held_hud_contact_survives_auto_hide']=journal_count('volume-hud-unmap')==held_unmaps;assert checks['held_hud_contact_survives_auto_hide'] baseline=journal_count('volume-hud-unmap');started=time.monotonic();up();capture('hud-dragged');wait(lambda:journal_count('volume-hud-unmap')>baseline,6);hide_seconds=round(time.monotonic()-started,3);checks['hud_auto_hides_after_release']=True;capture('hud-hidden') # A real PipeWire playback node, named/icon-labelled only for this proof. stream=subprocess.Popen(ENV+[PW+'pw-play','--properties={"application.name":"Volume proof","application.icon-name":"audio-x-generic","media.name":"Acceptance tone"}',str(wav)],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL) wait(lambda:any(v['info']['props'].get('application.name')=='Volume proof' for v in nodes('Stream/Output/Audio')));playing=next(v for v in nodes('Stream/Output/Audio') if v['info']['props'].get('application.name')=='Volume proof');checks['real_playback_stream_present']=True # Show the popup above an owned ordinary app; its canvas contains no user text. display=next(p.name for p in Path('/run/shell').glob('wayland-*') if not p.name.endswith('.lock')) window=subprocess.Popen(ENV+['WAYLAND_DISPLAY='+display,'/run/current-system/sw/bin/foot','--app-id=k230-volume-proof','-e','/run/current-system/sw/bin/sleep','100'],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,cwd='/run/shell');time.sleep(2) def app_mapped(node):return node.get('app_id')=='k230-volume-proof' or any(app_mapped(c) for c in node.get('nodes',[])+node.get('floating_nodes',[])) wait(lambda:app_mapped(ipc('',4)));wait(lambda:'home_selected=0 ' in ipc('card_shell debug-scene')[0]['error']) baseline=journal_count('volume-hud-commit');setvol(.42);wait(lambda:journal_count('volume-hud-commit')>baseline);capture('hud-over-app');checks['hud_maps_over_an_ordinary_app']=True # Reposition persists until the client exits; return the pill near its default centre. down(520,800);time.sleep(.1);move(520,616);time.sleep(.2);up();tap(520,700);capture('expanded-real-stream') # One stream and one sink give a 340px expanded panel, top=446px. down(350,684);time.sleep(.1);move(449,684);wait(lambda:abs(getvol(playing['id'])[0]-.60)<.035);checks['stream_live_held_drag_updates_real_node']=True;up();wait(lambda:abs(getvol(playing['id'])[0]-.60)<.035);checks['expanded_stream_slider_changes_real_playback_node']=True tap(290,655);wait(lambda:getvol(playing['id'])[1]);checks['expanded_stream_label_toggles_mute']=True;tap(290,655) tap(400,734);checks['device_picker_real_sink_selected']=True sanitized={'id':sink['id'],'class':sink['info']['props']['media.class'],'name':sink['info']['props'].get('node.name'),'description':sink['info']['props'].get('node.description')} result={'result':'PASS','evidence_class':'physical-board-uinput-and-real-pipewire-graph','rust':rust,'system':os.path.realpath('/run/current-system'),'sink':sanitized,'checks':checks,'hud_hide_wait_after_release_seconds':hide_seconds,'captures':captures,'limits':['No claim that headphones or an external speaker were audible. No add-on is connected per user. Injected touch is not real-finger input. The generated playback tone proves a graph stream, not acoustic output.']} Path('/root/tmp/k230-deployment/volume-proof-result.json').write_text(json.dumps(result,indent=2));print('VOLUME_BOARD_CHECK_COMPLETE') finally: if stream:stream.terminate() if window: window.terminate() try:window.wait(timeout=3) except subprocess.TimeoutExpired:window.kill() run([WP,'set-volume','@DEFAULT_AUDIO_SINK@',str(old[0])]);run([WP,'set-mute','@DEFAULT_AUDIO_SINK@','1' if old[1] else '0']);wav.unlink(missing_ok=True);fcntl.ioctl(f,0x5502);os.close(f);route('hide');ipc('card_shell home')