# 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(' 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('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)