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.
116 lines
5 KiB
Python
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))
|