Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
parent
93e3a48004
commit
b38dea0597
25901 changed files with 9982925 additions and 369 deletions
103
tundra/tools/usb_protocol.py
Normal file
103
tundra/tools/usb_protocol.py
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
# SPDX-License-Identifier: MPL-2.0
|
||||
"""Bounded Linux usbfs transport for the read-only Tundra diagnostic protocol."""
|
||||
import ctypes
|
||||
import fcntl
|
||||
from pathlib import Path
|
||||
import struct
|
||||
import time
|
||||
|
||||
class Bulk(ctypes.Structure):
|
||||
_fields_=[('ep',ctypes.c_uint),('length',ctypes.c_uint),('timeout',ctypes.c_uint),('data',ctypes.c_void_p)]
|
||||
|
||||
|
||||
def parse_endpoints(data, configuration):
|
||||
"""Read the active vendor interface; FunctionFS may renumber endpoints."""
|
||||
candidates = []
|
||||
current = None
|
||||
active = False
|
||||
offset = 0
|
||||
while offset < len(data):
|
||||
if len(data)-offset < 2:
|
||||
raise ValueError('Truncated USB descriptor')
|
||||
size, kind = data[offset:offset+2]
|
||||
if size < 2 or offset+size > len(data):
|
||||
raise ValueError('Invalid USB descriptor length')
|
||||
item = data[offset:offset+size]
|
||||
if kind == 2:
|
||||
if size < 9:
|
||||
raise ValueError('Invalid configuration descriptor')
|
||||
active = item[5] == configuration
|
||||
current = None
|
||||
elif kind == 4:
|
||||
if size < 9:
|
||||
raise ValueError('Invalid interface descriptor')
|
||||
current = None
|
||||
if active and item[3] == 0 and item[5:8] == bytes([255,0,0]):
|
||||
current = {'interface':item[2], 'count':item[4], 'endpoints':[]}
|
||||
candidates.append(current)
|
||||
elif kind == 5 and current is not None:
|
||||
if size < 7:
|
||||
raise ValueError('Invalid endpoint descriptor')
|
||||
current['endpoints'].append((item[2], item[3]&3))
|
||||
offset += size
|
||||
if len(candidates) != 1:
|
||||
raise ValueError('Expected one active vendor diagnostic interface')
|
||||
candidate = candidates[0]
|
||||
endpoints = candidate['endpoints']
|
||||
incoming = [ep for ep,kind in endpoints if kind == 2 and ep&0x80 and ep&0x0f]
|
||||
outgoing = [ep for ep,kind in endpoints if kind == 2 and not ep&0x80 and ep&0x0f]
|
||||
if candidate['count'] != 2 or len(endpoints) != 2 or len(incoming) != 1 or len(outgoing) != 1:
|
||||
raise ValueError('Expected one bulk IN and one bulk OUT endpoint')
|
||||
return {'interface':candidate['interface'], 'in':incoming[0], 'out':outgoing[0]}
|
||||
|
||||
|
||||
def device_layout(device, sysfs=Path('/sys/bus/usb/devices')):
|
||||
device = Path(device)
|
||||
for entry in sysfs.iterdir():
|
||||
try:
|
||||
path = Path('/dev/bus/usb')/f'{int((entry/"busnum").read_text()):03d}'/f'{int((entry/"devnum").read_text()):03d}'
|
||||
except (FileNotFoundError,NotADirectoryError):
|
||||
continue
|
||||
if path == device:
|
||||
identity = [(entry/k).read_text().strip() for k in ('idVendor','idProduct','serial')]
|
||||
if identity != ['1d6b','0104','TUNDRA-DIAG']:
|
||||
raise ValueError('Selected USB device is not the Tundra diagnostic')
|
||||
return parse_endpoints((entry/'descriptors').read_bytes(), int((entry/'bConfigurationValue').read_text()))
|
||||
raise RuntimeError('USB diagnostic disappeared; select its current device path')
|
||||
|
||||
|
||||
def exchange(device, commands=('status','version','mounts')):
|
||||
commands = tuple(commands)
|
||||
for command in commands:
|
||||
if not command.isascii() or not command.isalpha() or len(command)>60:
|
||||
raise ValueError('Invalid diagnostic command')
|
||||
layout = device_layout(device)
|
||||
libc=ctypes.CDLL(None,use_errno=True)
|
||||
libc.ioctl.argtypes = [ctypes.c_int,ctypes.c_ulong,ctypes.c_void_p]
|
||||
libc.ioctl.restype = ctypes.c_int
|
||||
ioctl_bulk=0xc0005502 | (ctypes.sizeof(Bulk)<<16)
|
||||
results={}
|
||||
with Path(device).open('r+b',buffering=0) as stream:
|
||||
fcntl.ioctl(stream,0x8004550f,struct.pack('I',layout['interface'])) # USBDEVFS_CLAIMINTERFACE
|
||||
def bulk(ep,payload=None):
|
||||
buf=ctypes.create_string_buffer(payload,len(payload)) if payload else ctypes.create_string_buffer(4096)
|
||||
request=Bulk(ep,len(buf),3000,ctypes.addressof(buf))
|
||||
count=libc.ioctl(stream.fileno(),ioctl_bulk,ctypes.byref(request))
|
||||
if count<0:raise OSError(ctypes.get_errno(),f'USB transfer endpoint 0x{ep:02x}')
|
||||
if payload is not None and count != len(payload):
|
||||
raise RuntimeError('Incomplete USB command write')
|
||||
return buf.raw[:count]
|
||||
for command in commands:
|
||||
bulk(layout['out'],(command+'\n').encode())
|
||||
response=b''
|
||||
deadline=time.monotonic()+6
|
||||
while not response.endswith(b'\nEND\n'):
|
||||
if time.monotonic() >= deadline:
|
||||
raise TimeoutError('Diagnostic response did not finish')
|
||||
chunk=bulk(layout['in'])
|
||||
if not chunk:
|
||||
raise RuntimeError('Empty USB response')
|
||||
response+=chunk
|
||||
if len(response)>8192:raise RuntimeError('Invalid oversized response')
|
||||
results[command]=response.decode()
|
||||
return results
|
||||
Loading…
Reference in a new issue