VulpesOS/tundra/tools/diagnose-telephony.py
OverSu 1311bed5e9 Release Preview 20 shared desktop, Android and Sargo updates
Restore Gaia settings and media workflows, extend native network adapters, and align the APK payload. Generate French/English changelogs and website entries from the simplified JSON source.

Validation: shared JS and Tundra unit tests; desktop workflows and persistence; signed APK payload parity; Sargo RAM and installed startup with boot readback. Android runtime qualification is recorded separately.
2026-10-13 00:19:45 +02:00

116 lines
5 KiB
Python

#!/usr/bin/env python3
# SPDX-License-Identifier: MPL-2.0
"""Report native telephony health without dialing or exposing subscriber data."""
import datetime
import json
from pathlib import Path
import re
import subprocess
FIELDS = {
'Modem': ('Powered', 'Online', 'Emergency', 'Revision'),
'SimManager': ('Present', 'PinRequired', 'LockedPins'),
'NetworkRegistration': ('Status', 'Mode', 'Technology', 'Strength'),
'RadioSettings': ('TechnologyPreference', 'AvailableTechnologies'),
'ConnectionManager': ('Attached', 'Powered', 'RoamingAllowed'),
'IpMultimediaSystem': ('Registered', 'VoiceCapable', 'SmsCapable'),
'CallVolume': ('Muted', 'SpeakerVolume', 'MicrophoneVolume'),
}
def select_properties(interface, properties):
return {key: properties[key].get('data') for key in FIELDS[interface]
if isinstance(properties.get(key), dict) and 'data' in properties[key]}
def registration_events(text):
"""Keep RAT and denial reason; never copy cell location or raw log lines."""
events = []
pattern = re.compile(
r'binder_network_poll_(voice|data)_state_cb\(\).*?slot\d+ '
r'(registered|roaming|searching|unregistered|denied|unknown),'
r'([^,]*),(-?\d+),(-?\d+),(-?\d+),(-?\d+)')
for line in text.splitlines():
match = pattern.search(line)
if not match:
continue
domain, status, technology, rat, _lac, _cell, reason = match.groups()
event = {'domain': domain, 'status': status,
'technology': technology, 'radioTechnology': int(rat),
'denialReason': int(reason)}
if not events or event != events[-1]:
events.append(event)
return events[-40:]
def run(command):
return subprocess.run(command, capture_output=True, text=True, timeout=12)
def modem_crashes(text):
signals = []
for line in text.splitlines():
if 'qcrild' not in line:
continue
match = re.search(r'Fatal signal (\d+) \((SIG[A-Z]+)\)', line)
if match:
event = {'signal': int(match[1]), 'name': match[2]}
timestamp = re.match(r'\s*(\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+)\s', line)
if timestamp:
event['logTime'] = timestamp[1]
signals.append(event)
return {'signals': signals,
'initialAttachTimeoutInBacktrace': 'qcril_qmi_nas_initial_attach_timeout_handler' in text}
def collect(command=run, root=Path('/')):
report = {'recordedAt': datetime.datetime.now(datetime.timezone.utc).isoformat(),
'properties': {}, 'services': {}, 'errors': {}}
for interface in FIELDS:
try:
result = command(['busctl', '--system', '--timeout=8', '--json=short',
'call', 'org.ofono', '/ril_0',
'org.ofono.' + interface, 'GetProperties'])
if result.returncode:
report['errors'][interface] = 'unavailable'
continue
data = json.loads(result.stdout)['data'][0]
report['properties'][interface] = select_properties(interface, data)
except (OSError, subprocess.TimeoutExpired, ValueError, KeyError, IndexError, TypeError):
report['errors'][interface] = 'unavailable'
for service in ('ofono', 'tundra-radio', 'tundra-sms', 'tundra-audio', 'tundra-call-audio'):
try:
result = command(['systemctl', 'is-active', service])
state = result.stdout.strip()
report['services'][service] = state if state in (
'active', 'inactive', 'failed', 'activating', 'deactivating') else 'unknown'
except (OSError, subprocess.TimeoutExpired):
report['services'][service] = 'unknown'
try:
result = command(['journalctl', '-u', 'ofono', '--since', '-10 min',
'--no-pager', '-n', '2000'])
report['registrationEvents'] = registration_events(result.stdout)
report['registrationLogAvailable'] = result.returncode == 0
except (OSError, subprocess.TimeoutExpired):
report['registrationEvents'] = []
report['registrationLogAvailable'] = False
try:
result = command(['lxc-attach', '-n', 'tundra-hal', '--', '/system/bin/logcat',
'-b', 'crash', '-d', '-t', '300'])
report['qualcommCrashes'] = modem_crashes(result.stdout)
report['qualcommCrashes']['logAvailable'] = result.returncode == 0
except (OSError, subprocess.TimeoutExpired):
report['qualcommCrashes'] = {'logAvailable': False}
try:
data = json.loads((root/'run/tundra-call-audio/status.json').read_text())
report['audio'] = {key: data[key] for key in (
'active', 'routed', 'speaker', 'muted', 'volume', 'recordedAt') if key in data}
report['audio']['hasError'] = bool(data.get('error'))
except (OSError, ValueError, TypeError):
report['audio'] = {'available': False}
return report
if __name__ == '__main__':
print(json.dumps(collect(), indent=2))