Release Vulpes OS 2.7-preview.22
This commit is contained in:
parent
200d1fb235
commit
a138f97877
294 changed files with 515786 additions and 187 deletions
201
tundra/devices/beatles/radio_status.py
Normal file
201
tundra/devices/beatles/radio_status.py
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
# SPDX-License-Identifier: MPL-2.0
|
||||
"""Read-only Android RIL status for beatles; no PIN, identity, SMS or call requests."""
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import select
|
||||
import socket
|
||||
import struct
|
||||
import time
|
||||
|
||||
REQUESTS = (1, 19, 20, 21, 22) # SIM status, signal, voice/data registration, operator.
|
||||
TECHNOLOGIES = {1:'gprs',2:'edge',3:'umts',4:'is95a',5:'is95b',6:'1xrtt',
|
||||
7:'evdo0',8:'evdoa',9:'hsdpa',10:'hsupa',11:'hspa',12:'evdob',
|
||||
13:'ehrpd',14:'lte',15:'hspa+',16:'gsm'}
|
||||
|
||||
class Parcel:
|
||||
def __init__(self, data):
|
||||
self.data, self.offset = data, 0
|
||||
def integer(self):
|
||||
if self.offset + 4 > len(self.data): raise ValueError('TRUNCATED_PARCEL')
|
||||
value, = struct.unpack_from('<i', self.data, self.offset)
|
||||
self.offset += 4
|
||||
return value
|
||||
def string(self):
|
||||
length = self.integer()
|
||||
if length == -1: return None
|
||||
if not 0 <= length <= 4096: raise ValueError('INVALID_STRING_LENGTH')
|
||||
end = self.offset + length * 2
|
||||
if end + 2 > len(self.data): raise ValueError('TRUNCATED_STRING')
|
||||
if self.data[end:end+2] != b'\0\0': raise ValueError('INVALID_TERMINATOR')
|
||||
value = self.data[self.offset:end].decode('utf-16-le')
|
||||
self.offset = (end + 2 + 3) & ~3
|
||||
if self.offset > len(self.data): raise ValueError('TRUNCATED_PADDING')
|
||||
return value
|
||||
def strings(self):
|
||||
count = self.integer()
|
||||
if not 0 <= count <= 32: raise ValueError('INVALID_ARRAY_LENGTH')
|
||||
return [self.string() for _ in range(count)]
|
||||
|
||||
|
||||
def registration(values):
|
||||
state = int(values[0]) if values and values[0] else -1
|
||||
tech = int(values[3]) if len(values)>3 and values[3] else 0
|
||||
return {'state':{0:'notSearching',1:'registered',2:'searching',3:'denied',
|
||||
4:'unknown',5:'registered',10:'notSearching',12:'searching',
|
||||
13:'denied',14:'unknown'}.get(state,'unknown'),
|
||||
'connected':state in (1,5), 'roaming':state == 5,
|
||||
'emergencyCallsOnly':state in (10,12,13,14), 'type':TECHNOLOGIES.get(tech)}
|
||||
|
||||
|
||||
def decode(request, parcel):
|
||||
if request == 1:
|
||||
card = parcel.integer(); parcel.integer()
|
||||
gsm = parcel.integer(); parcel.integer(); parcel.integer()
|
||||
count = parcel.integer()
|
||||
if not 0 <= count <= 8: raise ValueError('INVALID_SIM_APPLICATION_COUNT')
|
||||
app_state = None
|
||||
for index in range(count):
|
||||
parcel.integer(); state=parcel.integer(); parcel.integer()
|
||||
parcel.string(); parcel.string() # AID/label are deliberately discarded.
|
||||
parcel.integer(); parcel.integer(); parcel.integer()
|
||||
if index == gsm: app_state=state
|
||||
return {'sim':{'present':card == 1, 'cardState':
|
||||
'absent' if card == 0 else {2:'pinRequired',3:'pukRequired',4:'networkLocked',5:'ready'}.get(app_state,'unknown')}}
|
||||
if request in (20,21):
|
||||
state=registration(parcel.strings())
|
||||
# Packet-network registration does not prove that a data bearer is open.
|
||||
if request == 21: state['connected']=False
|
||||
return {('voice' if request == 20 else 'data'):state}
|
||||
if request == 22:
|
||||
values = parcel.strings()
|
||||
return {'operator':{'longName':(values[0] or '')[:100] if values else '',
|
||||
'shortName':(values[1] or '')[:100] if len(values)>1 else ''}}
|
||||
if request == 19:
|
||||
# GSM ASU can be unknown on LTE. RIL v6+ stores positive LTE RSRP
|
||||
# at index 8; normalize its valid 44..140 dBm magnitude for Gaia.
|
||||
values=[]
|
||||
while parcel.offset + 4 <= len(parcel.data): values.append(parcel.integer())
|
||||
if not values: raise ValueError('TRUNCATED_SIGNAL')
|
||||
asu=values[0]
|
||||
rsrp=values[8] if len(values)>8 else None
|
||||
if rsrp is not None and 44<=rsrp<=140:
|
||||
return {'signal':round((140-rsrp)*100/96)}
|
||||
return {'signal':round(asu*100/31) if 0<=asu<=31 else None}
|
||||
raise ValueError('READ_ONLY_REQUEST_REQUIRED')
|
||||
|
||||
|
||||
class Radio:
|
||||
def __init__(self, path, slot):
|
||||
self.path,self.slot=path,slot
|
||||
self.sock=None;self.buffer=b'';self.pending={};self.serial=0
|
||||
self.last_poll=0;self.retry_at=0;self.retry_delay=5
|
||||
self.state={'slot':slot,'available':False,'radioState':'unknown'}
|
||||
def close(self):
|
||||
if self.sock:self.sock.close()
|
||||
self.sock=None;self.buffer=b'';self.pending={}
|
||||
self.state={'slot':self.slot,'available':False,'radioState':'unknown'}
|
||||
self.retry_at=time.monotonic()+self.retry_delay
|
||||
self.retry_delay=min(60,self.retry_delay*2)
|
||||
def connect(self):
|
||||
connection=socket.socket(socket.AF_UNIX,socket.SOCK_STREAM)
|
||||
try:
|
||||
connection.settimeout(1);connection.connect(self.path)
|
||||
connection.setblocking(False);self.sock=connection;self.last_poll=0
|
||||
print('RIL slot',self.slot,'connected',flush=True)
|
||||
except OSError as error:
|
||||
print('RIL slot',self.slot,'connect errno',error.errno,flush=True)
|
||||
connection.close();self.close()
|
||||
def poll(self):
|
||||
now=time.monotonic()
|
||||
if not self.sock:
|
||||
if now>=self.retry_at:self.connect()
|
||||
return
|
||||
# CS fallback and network handover can outlast an individual UI request.
|
||||
# Keep the transport open for late replies instead of repeatedly filling
|
||||
# the vendor socket's accept queue with abandoned reconnects.
|
||||
if any(now-created>45 for _,created in self.pending.values()):
|
||||
print('RIL slot',self.slot,'request timeout; kinds',sorted({kind for kind,_ in self.pending.values()}),flush=True)
|
||||
self.close();return
|
||||
if now-self.last_poll<5:return
|
||||
self.last_poll=now
|
||||
for request in REQUESTS:
|
||||
if any(kind==request for kind,_ in self.pending.values()):continue
|
||||
self.serial+=1
|
||||
body=struct.pack('<ii',request,self.serial)
|
||||
self.sock.sendall(struct.pack('>I',len(body))+body)
|
||||
self.pending[self.serial]=(request,now)
|
||||
def receive(self):
|
||||
data=self.sock.recv(65536)
|
||||
if not data:raise OSError('RIL_DISCONNECTED')
|
||||
self.buffer+=data
|
||||
while len(self.buffer)>=4:
|
||||
length,=struct.unpack_from('>I',self.buffer)
|
||||
if not 4<=length<=65536:raise ValueError('INVALID_FRAME_LENGTH')
|
||||
if len(self.buffer)<length+4:return
|
||||
parcel=Parcel(self.buffer[4:4+length]);self.buffer=self.buffer[4+length:]
|
||||
kind=parcel.integer()
|
||||
if kind == 0:
|
||||
serial,error=parcel.integer(),parcel.integer()
|
||||
pending=self.pending.pop(serial,None)
|
||||
if pending is None:continue
|
||||
request,_=pending
|
||||
self.retry_delay=5
|
||||
self.response(request,error,parcel,serial)
|
||||
elif kind == 1:
|
||||
self.unsolicited(parcel.integer(),parcel)
|
||||
|
||||
def response(self,request,error,parcel,serial=None):
|
||||
self.state['available']=True
|
||||
if error:
|
||||
self.state.setdefault('errors',{})[str(request)]=error
|
||||
self.state.pop({1:'sim',19:'signal',20:'voice',21:'data',22:'operator'}[request],None)
|
||||
return
|
||||
self.state.setdefault('errors',{}).pop(str(request),None)
|
||||
self.state.update(decode(request,parcel))
|
||||
|
||||
def unsolicited(self,event,parcel):
|
||||
if event == 1000:
|
||||
self.state['radioState']={0:'disabled',1:'unavailable',10:'enabled'}.get(parcel.integer(),'unknown')
|
||||
elif event == 1034:
|
||||
count=parcel.integer()
|
||||
if count==1:self.state['rilVersion']=parcel.integer()
|
||||
elif event in (1002,1009,1019):self.last_poll=0
|
||||
# The read-only adapter never consumes or acknowledges incoming SMS.
|
||||
|
||||
|
||||
def battery(root=Path('/sys/class/power_supply/battery')):
|
||||
try:
|
||||
level=int((root/'capacity').read_text().strip())
|
||||
status=(root/'status').read_text().strip()
|
||||
if not 0<=level<=100 or status not in ('Charging','Full','Discharging','Not charging'):return None
|
||||
return {'level':level/100,'charging':status in ('Charging','Full')}
|
||||
except (OSError,ValueError):return None
|
||||
|
||||
|
||||
def main():
|
||||
os.umask(0o022)
|
||||
destination=Path('/run/tundra-hardware/status.json')
|
||||
destination.parent.mkdir(parents=True,exist_ok=True)
|
||||
destination.parent.chmod(0o755)
|
||||
radios=[Radio(os.environ.get('VULPES_RIL_SOCKET_DIR','/dev/socket')+'/'+name,index) for index,name in enumerate(('rild','rild2'))]
|
||||
published=0
|
||||
while True:
|
||||
for radio in radios:
|
||||
try:radio.poll()
|
||||
except (OSError,ValueError) as error:
|
||||
print('RIL slot',radio.slot,'poll',type(error).__name__,getattr(error,'errno',None),flush=True);radio.close()
|
||||
active={r.sock:r for r in radios if r.sock}
|
||||
readable,_,_=select.select(list(active),[],[],.25)
|
||||
for sock in readable:
|
||||
try:active[sock].receive()
|
||||
except (OSError,ValueError,UnicodeError) as error:
|
||||
print('RIL slot',active[sock].slot,'receive',type(error).__name__,getattr(error,'errno',None),flush=True);active[sock].close()
|
||||
if time.monotonic()-published<1:continue
|
||||
published=time.monotonic()
|
||||
value={'schema':1,'recordedAt':time.time(),'battery':battery(),
|
||||
'wifi':{'available':False,'state':'unavailable'},'radio':[r.state for r in radios]}
|
||||
temporary=destination.with_suffix('.tmp')
|
||||
temporary.write_text(json.dumps(value)+'\n');temporary.chmod(0o644);temporary.replace(destination)
|
||||
|
||||
if __name__=='__main__':main()
|
||||
Loading…
Reference in a new issue