VulpesOS/tundra/tools/test-beatles-controls.py
2026-10-22 20:42:26 +02:00

104 lines
6.2 KiB
Python

#!/usr/bin/env python3
# SPDX-License-Identifier: MPL-2.0
"""Qualify Nokia controls in an already running manual Gaia diagnostic session.
Injects one volume-up/down pair, dims briefly and vibrates for 500 ms.
Restores Gaia settings afterwards. Does not call, send SMS, reboot or flash.
"""
import argparse,json,subprocess,tempfile,time,uuid
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
BASE='/data/local/vulpes-beatles/rootfs/work/'
def shell(command):
return subprocess.check_output(['adb','shell',command],text=True,timeout=10).strip()
def publish(name,value):
with tempfile.NamedTemporaryFile(mode='w') as f:
json.dump(value,f);f.flush()
subprocess.run(['adb','push',f.name,BASE+name+'.tmp'],capture_output=True,check=True)
shell('mv '+BASE+name+'.tmp '+BASE+name)
def probe(script):
identifier=uuid.uuid4().hex
publish('manual-request.json',{'id':identifier,'app':'system','script':script})
deadline=time.monotonic()+20
while time.monotonic()<deadline:
try:response=json.loads(shell('cat '+BASE+'manual-response.json'))
except json.JSONDecodeError:response={}
if response.get('id')==identifier:
if 'error' in response:raise RuntimeError(response['error'])
return response['value']
time.sleep(.1)
raise TimeoutError('Manual diagnostic did not answer')
def inject(key):
try:sequence=json.loads(shell('cat '+BASE+'keypad-response.json')).get('sequence',0)+1
except json.JSONDecodeError:sequence=1
if sequence>64:raise RuntimeError('Start a fresh manual session')
publish('keypad-request.json',{'sequence':sequence,'key':key})
deadline=time.monotonic()+5
while time.monotonic()<deadline:
try:reply=json.loads(shell('cat '+BASE+'keypad-response.json'))
except json.JSONDecodeError:reply={}
if reply.get('sequence')==sequence:return
time.sleep(.1)
raise TimeoutError('Physical input bridge did not acknowledge')
def main():
parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--output',type=Path,required=True)
args=parser.parse_args()
if shell('getprop ro.product.codename')!='beatles' or shell('getprop init.svc.b2g')!='stopped':
raise RuntimeError('Requires the Nokia manual Gaia trial')
# Gaia adjusts media volume inside an app, but notification volume on Home.
probe('dispatchEvent(new CustomEvent("home"));return true;')
time.sleep(1)
report={};initial=int(shell('cat /sys/class/leds/lcd-backlight/brightness'))
volume=probe('return (await navigator.mozSettings.createLock().get("audio.volume.notification"))["audio.volume.notification"];')
try:
probe('window.__controlCleanup?.(); window.__controlProof={events:[],hud:[]}; const buttons=e=>__controlProof.events.push(e.detail.type); const hud=()=>setTimeout(()=>__controlProof.hud.push(getComputedStyle(document.getElementById("volume")).display),100); addEventListener("softwareButtonEvent",buttons); for(const event of ["volumeup","volumedown"])addEventListener(event,hud); window.__controlCleanup=()=>{removeEventListener("softwareButtonEvent",buttons); for(const event of ["volumeup","volumedown"])removeEventListener(event,hud);}; return true;')
# Establish a middle volume so both buttons can change it.
probe('return await navigator.mozSettings.createLock().set({"audio.volume.notification":7});')
def settle(expected):
deadline=time.monotonic()+15
while time.monotonic()<deadline:
gaia=probe('return (await navigator.mozSettings.createLock().get("audio.volume.notification"))["audio.volume.notification"];')
native=shell('service call media.audio_policy 17 i32 5 i32 2')
if gaia==expected and ('%08x 00000000'%expected) in native:return gaia,native
time.sleep(.2)
raise AssertionError({'expected':expected,'gaia':gaia,'native':native})
settle(7)
inject('AudioVolumeUp')
report['volumeUp'],report['nativeUp']=settle(8)
inject('AudioVolumeDown')
report['volumeDown'],report['nativeDown']=settle(7)
report['ui']=probe('return window.__controlProof;')
probe('return await VulpesCompat.call("desktop.screen",{name:"screenBrightness",value:.2});')
report['backlight20percent']=int(shell('cat /sys/class/leds/lcd-backlight/brightness'))
with ThreadPoolExecutor(max_workers=1) as pool:
future=pool.submit(probe,'return await VulpesCompat.call("hardware.vibrate",{pattern:[500]});')
samples=[];deadline=time.monotonic()+15;completed_at=None
while time.monotonic()<deadline:
samples.append(int(shell('cat /sys/class/timed_output/vibrator/enable')));time.sleep(.05)
if future.done():
if completed_at is None:completed_at=time.monotonic()
if time.monotonic()-completed_at>1:break
future.result()
report['vibration']={'positiveSample':any(n>0 for n in samples),'final':samples[-1]}
finally:
probe('return await VulpesCompat.call("hardware.vibrate",{pattern:[]});')
probe('return await VulpesCompat.call("desktop.screen",{name:"screenBrightness",value:'+str(initial/255)+'});')
probe('return await navigator.mozSettings.createLock().set({"audio.volume.notification":'+str(volume)+'});')
probe('window.__controlCleanup?.();delete window.__controlCleanup;delete window.__controlProof;return true;')
report['restoredBacklight']=int(shell('cat /sys/class/leds/lcd-backlight/brightness'))
args.output.parent.mkdir(parents=True,exist_ok=True)
args.output.write_text(json.dumps(report,indent=2)+'\n')
assert report['volumeUp']==8 and report['volumeDown']==7,report
assert '00000008 00000000' in report['nativeUp'] and '00000007 00000000' in report['nativeDown'],report
assert 'block' in report['ui']['hud'],report
assert report['backlight20percent']==51 and report['restoredBacklight']==initial,report
assert report['vibration']['positiveSample'] and report['vibration']['final']==0,report
print('Gaia volume HUD, native audio indexes, backlight and bounded vibration: OK')
if __name__=='__main__':main()