Run automatic modem diagnostics
This script runs the annotated modem diagnostics automatically and saves results to a text file.1
Download the scripts
Download
modem_diagnostics.py and modem_interface.py to the same directory.2
Run the script
python3 modem_diagnostics.py --port /dev/ttyUSB2 --baud 115200
Note:
--port and --baud are optional. If omitted, the script will prompt for available serial ports and default to 115200 baud.3
Save and share results
The output prints in your terminal and saves to
ModemDiagnostics.txt in the same directory. Attach this file when opening a support ticket.ModemManager variant
If you use ModemManager, run the alternative helper script:python3 mmcli-diagnostics.py
Python scripts
mmcli-diagnostics.py
#!/usr/bin/env python3
###
### 2023-12-06 | Authors: Leonardo Gurria
### The project is the automated process of running modem diagnostics on Linux using ModemManager
### License: hologram.io
###
import subprocess
import logging
import time
import json
# Sanity checks for ModemManager
def check_mmcli(log):
# Check if mmcli is installed (should be installed by default)
log.debug('Checking if mmcli is installed')
installed = subprocess.getoutput('which mmcli')
# Check if mmcli is running (should be running on boot)
log.debug('Checking if mmcli is active and running')
running = subprocess.getoutput('sudo systemctl status ModemManager')
if len(installed) == 0 or 'active (running)' not in running:
return False
return True
# Sanity checks for debug mode
def check_debug_mode(log):
# Check for service file
log.debug('Checking if ModemManager.service file exists')
service = subprocess.getoutput('ls -lrt /lib/systemd/system/ModemManager.service')
if len(service) == 0:
log.warning('File /lib/systemd/system/ModemManager.service does not exist')
return False
# Check for debug flag
log.debug('Checking if debug flag is present')
flag = subprocess.getoutput('grep "debug" /lib/systemd/system/ModemManager.service')
if len(flag) == 0:
log.debug('Debug flag is not present')
return False
return True
# Ask user how to proceed
def user_selection(log):
# Print user menu
print('\nDebug mode is not enabled on ModemManager')
print('How would you like to proceed?')
print(' 0 - Basic Diagnostics')
print(' 1 - Show me how to enable it')
user_input = int(input('Your selection: '))
while user_input != 0 and user_input != 1:
log.error('Please enter a valid option')
user_input = int(input('Your selection: '))
if user_input == 0:
log.info('Proceeding with basic diagnostics! Please note this might take around 5 mins')
# Do basic diagnostics
b, p = basic_diagnostics(log)
del p
log.info(json.dumps(b, indent=4))
else:
log.debug('Showing instructions to enable debug mode')
# Show instructions
print('\nStep 1) Add --debug at the end of line: ExecStart=/usr/sbin/ModemManager in /lib/systemd/system/ModemManager.service file')
print(' Line should look like this: ExecStart=/usr/sbin/ModemManager --debug')
print('Step 2) Restart your RPi/Linux machine and run this script again!')
exit(0)
# Basic diagnostics
def basic_diagnostics(log):
# Check that modem is plugged in
log.debug('Checking if modem is plugged in')
plugged = subprocess.getoutput('mmcli -L')
if 'No modems were found' in plugged:
log.error('No modem is plugged in')
exit(0)
plugged = subprocess.getoutput('mmcli -L -J')
plugged = json.loads(plugged)
plugged = plugged['modem-list'][0]
# Check for modem info
log.debug('Checking for modem information in slot ' + plugged)
modem = subprocess.getoutput('mmcli -m ' + plugged)
if "error: couldn't find modem" in modem:
log.error('Unable to check modem information')
exit(0)
log.debug(modem)
# Get information on JSON format
modem = subprocess.getoutput('mmcli -m ' + plugged + ' -J')
log.debug('Modem JSON file')
log.debug(modem)
m = json.loads(modem)
s = m['modem']['generic']['sim']
b = m['modem']['generic']['bearers'][0] if len(m['modem']['generic']['bearers']) > 0 else 'No bearer'
# Check that SIM is present
log.debug('Checking for SIM in slot ' + s)
sim = subprocess.getoutput('mmcli -i ' + s)
if "error: couldn't find SIM" in sim:
log.error('SIM is missing')
exit(0)
log.debug(sim)
# Get information on JSON format
sim = subprocess.getoutput('mmcli -i ' + s + ' -J')
log.debug('SIM JSON file')
log.debug(sim)
# Check for bearer
log.debug('Checking for connection in slot ' + b)
bearer = subprocess.getoutput('mmcli -b ' + b)
if "error: couldn't find bearer" in bearer or b == 'No bearer':
log.warning('Connection is not in use')
bearer = 'No bearer'
else:
log.debug(bearer)
# Get information on JSON format
bearer = subprocess.getoutput('mmcli -b ' + b + ' -J')
log.debug('Bearer JSON file')
log.debug(bearer)
# Parse JSON information
return parse_basic_diagnostics(log, modem, sim, bearer, plugged)
# Parse basic diagnsotics from JSON format
def parse_basic_diagnostics(log, modem, sim, bearer, plugged):
log.debug('Basic Diagnostics')
m = json.loads(modem)
s = json.loads(sim)
if bearer != 'No bearer':
bea = json.loads(bearer)
b = bea['bearer']['properties']['apn']
else:
b = bearer
# Networks
networks = subprocess.getoutput('sudo mmcli -m ' + plugged + ' --3gpp-scan --timeout=300 -J')
if 'error' not in networks:
net = json.loads(networks)
n = net['modem']['3gpp']['scan-networks']
else:
n = networks
log.warning(n)
# Extended signal
ext_signal = subprocess.getoutput('mmcli -m ' + plugged + ' --signal-get')
ext_signal = ext_signal.replace('error rate threshold', '')
if 'error' not in ext_signal:
# Setup extended signal
ext_signal = subprocess.getoutput('sudo mmcli -m ' + plugged + ' --signal-setup=10')
# Wait 10 + 1 secs
time.sleep(11)
# Get actual extended signal
ext_signal = subprocess.getoutput('mmcli -m ' + plugged + ' --signal-get -J')
es = json.loads(ext_signal)
if es['modem']['signal']['lte']['rssi'] == '--':
log.debug('Fetching all RAT extended signal')
sig = es['modem']['signal']
else:
log.debug('Fetching LTE extended signal')
sig = es['modem']['signal']['lte']
else:
sig = ext_signal
log.warning(f'Unable to get extended signal due to: {ext_signal}')
basic_diagnostics = {
'module': m['modem']['generic']['model'],
'manufacturer': m['modem']['generic']['manufacturer'],
'firmware': m['modem']['generic']['revision'],
'iccid': s['sim']['properties']['iccid'],
'signal': m['modem']['generic']['signal-quality'],
'extended_signal': sig,
'registration_status': m['modem']['generic']['state'] + ' | ' + m['modem']['3gpp']['registration-state'],
'apn': b,
'current_operator': m['modem']['3gpp']['operator-code'] + ' | ' + m['modem']['3gpp']['operator-name'],
'networks_in_reach': n,
'cpin': '',
'fplmn': '',
'csq': '',
'cgact': '',
'creg': '',
'cgreg': '',
'cereg': '',
'cgdcont': ''
}
log.debug(json.dumps(basic_diagnostics, indent=4))
return basic_diagnostics, plugged
# Full diagnostics
def full_diagnostics(log):
# Run basic diagnsotics first
full_diagnostics, plugged = basic_diagnostics(log)
log.debug('Full diagnostics')
# Run remaining commands for full diagnostics
cpin = subprocess.getoutput("sudo mmcli -m " + plugged + " --command='+cpin?'")
fplmn = subprocess.getoutput("sudo mmcli -m " + plugged + " --command='+crsm=176,28539,0,0,12'")
csq = subprocess.getoutput("sudo mmcli -m " + plugged + " --command='+csq'")
cgact = subprocess.getoutput("sudo mmcli -m " + plugged + " --command='+cgact?'")
cgdcont = subprocess.getoutput("sudo mmcli -m " + plugged + " --command='+cgdcont?'")
# These need to be checked in /var/log/syslog for their output
creg = subprocess.getoutput("sudo mmcli -m " + plugged + " --command='+creg?'")
# creg_out = subprocess.getoutput('cat /var/log/syslog | grep -i "+creg:" | tail -1')
creg_out = subprocess.getoutput('journalctl -u ModemManager | grep -i "+creg:" | tail -1')
cgreg = subprocess.getoutput("sudo mmcli -m " + plugged + " --command='+cgreg?'")
# cgreg_out = subprocess.getoutput('cat /var/log/syslog | grep -i "+cgreg:" | tail -1')
cgreg_out = subprocess.getoutput('journalctl -u ModemManager | grep -i "+cgreg:" | tail -1')
cereg = subprocess.getoutput("sudo mmcli -m " + plugged + " --command='+cereg?'")
# cereg_out = subprocess.getoutput('cat /var/log/syslog | grep -i "+cereg:" | tail -1')
cereg_out = subprocess.getoutput('journalctl -u ModemManager | grep -i "+cereg:" | tail -1')
# Sanity checks for outputs
if 'error' in cpin or cpin == "response: ''":
log.warning(f'Unable to run +cpin command due to: {cpin}')
else:
full_diagnostics['cpin'] = cpin
if 'error' in fplmn or fplmn == "response: ''":
log.warning(f'Unable to run +crsm command due to: {fplmn}')
else:
full_diagnostics['fplmn'] = fplmn
if 'error' in csq or csq == "response: ''":
log.warning(f'Unable to run +csq command due to: {csq}')
else:
full_diagnostics['csq'] = csq
if 'error' in cgact or cgact == "response: ''":
log.warning(f'Unable to run +cgact command due to: {cgact}')
else:
full_diagnostics['cgact'] = cgact
if 'error' in cgdcont or cgdcont == "response: ''":
log.warning(f'Unable to run +cgdcont command due to: {cgdcont}')
else:
full_diagnostics['cgdcont'] = cgdcont
if 'error' in creg or len(creg_out) == 0:
log.warning(f'Unable to run +creg command due to: {creg}')
else:
find = creg_out.find('<-- ')
parse = creg_out[find:]
full_diagnostics['creg'] = parse.replace('<-- ', '').replace("'", '').replace('<CR><LF>', '').replace('OK', '')
if 'error' in cgreg or len(cgreg_out) == 0:
log.warning(f'Unable to run +cgreg command due to: {cgreg}')
else:
find = cgreg_out.find('<-- ')
parse = cgreg_out[find:]
full_diagnostics['cgreg'] = parse.replace('<-- ', '').replace("'", '').replace('<CR><LF>', '').replace('OK', '')
if 'error' in cereg or len(cereg_out) == 0:
log.warning(f'Unable to run +cereg command due to: {cereg}')
else:
find = cereg_out.find('<-- ')
parse = cereg_out[find:]
full_diagnostics['cereg'] = parse.replace('<-- ', '').replace("'", '').replace('<CR><LF>', '').replace('OK', '')
log.info(json.dumps(full_diagnostics, indent=4))
# Main
def main():
# Measure execution time
start_time = time.time()
# Log info
log = logging.getLogger('mmcli-diagnostics')
logging.basicConfig(level=logging.DEBUG, filename='mmcli-diagnostics.log', filemode='w', format='%(asctime)s | %(name)s | %(levelname)s | %(message)s')
# Console
con = logging.StreamHandler()
con.setLevel(logging.INFO)
con.setFormatter(logging.Formatter('%(asctime)s | %(name)s | %(levelname)s | %(message)s'))
log.addHandler(con)
log.info('Starting script!')
# Run sanity checks for ModemManager
mmcli_ready = check_mmcli(log)
if mmcli_ready is False:
log.error('Your system does not have ModemManager ready for diagnostics')
exit(0)
# Run sanity checks for debug mode
debug_mode_ready = check_debug_mode(log)
if debug_mode_ready is False:
log.debug('Debug mode is not enabled')
# Check for user selection
user_selection(log)
else:
log.info('Debug flag is present. Proceeding with full diagnostics! Please note this might take around 5 mins')
full_diagnostics(log)
log.info('Script completed!')
# Measure execution time
end_time = time.time()
log.info('Execution duration: ' + str(end_time - start_time) + ' secs')
# Python stuff
if __name__ == '__main__':
main()
modem_interface.py
#!/usr/bin/env python3
#
# This code provide a robust serial interface to a modem.
# Cory Dixon, Hologram
#
#from tkinter import W
import serial
import time
import sys
import glob
import os
import re
# helper function to perform sort
def atoi(text):
return int(text) if text.isdigit() else text
def natural_keys(text):
'''
alist.sort(key=natural_keys) sorts in human order
http://nedbatchelder.com/blog/200712/human_sorting.html
(See Toothy's implementation in the comments)
'''
return [ atoi(c) for c in re.split(r'(\d+)', text) ]
""" Lists serial port names
:raises EnvironmentError:
On unsupported or unknown platforms
:returns:
A list of the serial ports available on the system
"""
def serial_ports():
# Windows OS
if sys.platform.startswith('win'):
ports = ['COM%s' % (i + 1) for i in range(256)]
# Linux OS
elif sys.platform.startswith('linux') or sys.platform.startswith('cygwin'):
# this excludes your current terminal "/dev/tty"
ports = glob.glob('/dev/tty[A-Za-z]*')
# Mac OS
elif sys.platform.startswith('darwin'):
ports = glob.glob('/dev/tty.*')
# Some other OS
else:
raise EnvironmentError('Unsupported platform')
# Mainly for MacOS
user = os.environ.get('USER').lower()
if user:
user = user.lower()
# Store port names
result = []
for port in ports:
if user in port.lower(): continue
elif 'bluetooth' in port.lower(): continue
try:
s = serial.Serial(port)
s.close()
result.append(port)
except (OSError, serial.SerialException):
pass
result.sort(key=natural_keys)
return result
""" Modem AT Command Manager Class """
class ATManager:
def __init__(self, serial_timeout=5, msg_timeout=1, print_debug = False):
self.print_debug = print_debug
self.ser = None
self.init_time = time.time()
self.sent_cmd = None
self.last_rx = time.time()
self.serial_timeout = serial_timeout
self.msg_timeout = msg_timeout
self.msg_timeout_def = msg_timeout
self.send_at = None
self.last_sms = [] # message from last urc code
self.have_sms = False
self.ping_count = 0
self.have_ping = False
self.last_ping = 0
# The default message timeout is 1 second. However, some commands like the AT+COPS=?
# can take much longer. You should cal set_msg_timeout(big_number) for those commands
# and then it is advised to set the timeout back to 1 set_msg_timeout(1)
def set_msg_timeout(self, val):
if val is not None:
self.msg_timeout = val
self.msg_timeout_def = val
# Assign an AT command to the command manager
def set_cmd(self, val):
self.send_at = val
#print("setting cmd {}".format(self.send_at))
# Open serial port
def open(self, port, baud):
self.ser = serial.Serial(port, baudrate = baud, timeout=self.serial_timeout)
#self.ser.flushInput()
self.ser.reset_input_buffer()
self.ser.reset_output_buffer()
self.ser.flush()
self.ser.read_all()
# Close serial port
def close(self):
if self.ser:
self.ser.reset_input_buffer()
self.ser.reset_output_buffer()
self.ser.flush()
self.ser.read_all()
self.ser.close()
self.sent_cmd = None
self.ser = None
# DO NOT CALL THIS DIRECTLY. Use set_cmd
def __write(self,val):
cmd = 'AT' if not (val.startswith('AT') or val.startswith('at')) else ''
dt = (time.time() - self.init_time)
if self.print_debug: print("TX [{:.2f}]: {}".format(dt, cmd+val))
cmd = cmd + val + '\r\n'
cmd = cmd.encode()
self.ser.write(cmd)
self.ser.flush()
self.sent_cmd = time.time()
# Check is serial port is open
def isOpen(self):
return self.ser.isOpen()
# Wait for modem to respond to AT command sent
def wait_for_rx(self, wait_time=None, log_file=None, exit_flag = False):
if wait_time is not None:
self.msg_timeout = wait_time
full_rep = []
while self.sent_cmd or self.send_at and exit_flag != True:
rep = self.update(log_file)
if rep is None:
return full_rep
elif rep == '':
time.sleep(0.01)
else:
full_rep.append(rep)
self.msg_timeout = self.msg_timeout_def
return full_rep
# Update output/log_file
def update(self, log_file=None):
#print("{} {} {} {}".format(time.time(), self.send_at, self.last_rx, self.sent_cmd ))
if not self.ser.isOpen():
if self.print_debug: print("Serial port is not open")
return None
n = self.ser.inWaiting()
dt = (time.time() - self.init_time)
if n > 0:
self.last_rx = time.time()
try:
reply = self.ser.readline()
reply = reply.decode().strip()
except:
return ''
# URC detection
#if reply.startswith('+')
self.parse_urc( reply, log_file)
if self.sent_cmd:
if self.print_debug: print("RX [{:.2f}]: {}".format(dt, reply))
if reply.startswith('OK') or 'ERROR' in reply or 'ABORTED' in reply:
self.sent_cmd = None
else:
if self.print_debug: print("UX [{:.2f}]: {}".format(dt, reply))
#print('[{:.2f}] {}'.format(dt, reply))
if log_file:
log_file.write('[{:.2f}] '.format(dt))
log_file.write(reply)
log_file.write('\r\n')
#print("have reply: {}".format(reply))
return reply
elif self.sent_cmd:
#print("waiting on send")
if (time.time() - self.sent_cmd) >= self.msg_timeout:
if self.print_debug: print("ERROR: timed out waiting for response")
self.sent_cmd = None
return 'TIMEOUT'
# Don't send a new command faster than 200 ms
elif self.send_at and (time.time() - self.last_rx) >= 0.2 and (time.time() - (self.sent_cmd if self.sent_cmd else 0)) >= .2:
#print("next send")
if log_file:
log_file.write('[{:.2f}] '.format(dt))
log_file.write(self.send_at)
log_file.write('\r\n')
self.__write(self.send_at)
self.send_at = None
return ''
# Parse URC values we may care about
def parse_urc(self, rsp, log_file=None):
#print(f'parse_urc {rsp}')
# +UUSORD: 0,17
if '+UUSORD' in rsp:
val = rsp.rsplit(':', 1)[-1]
cmd = 'AT+USORD=' + val
self.__write(cmd)
elif '+CMT' in rsp:
# Send SMS ack
#self.set_cmd('AT+CNMA')
self.have_sms = True
self.last_sms = rsp
elif '+QPING' in rsp:
# Ping ack
# +QPING: 0,"8.8.8.8",32,295,255
# +QPING: 0,1,1,0,295,295,295
val = rsp.rsplit(':', 1)[-1]
ms = val.split(',')
self.ping_count += 1
if len(ms) > 2 and ms[1].isdigit():
count = int(ms[1])
if count + 1 == self.ping_count:
self.have_ping = True
self.last_ping = int(ms[-1])
self.sent_cmd = None
# get sms
def get_sms(self):
if self.have_sms:
self.have_sms = False
sms = self.last_sms
self.last_sms = []
return sms
return None
# get ping
def get_ping(self):
if self.have_ping:
self.have_ping = False
self.ping_count = 0
ping = self.last_ping
self.last_ping = []
return ping
return None
# Get the current carrier
def get_carrier(self, log_file=None):
# send command
self.set_cmd('AT+COPS?')
# wait for response
reply = self.wait_for_rx(log_file=log_file)
# parse response
if reply is not None and len(reply) == 2 and 'OK' in reply[-1]:
vals = reply[0].split(',')
if len(vals) >= 3:
return vals[2]
return ''
# See if the modem will respond to AT command
def connected_state(self, log_file=None):
# send command
self.set_cmd('AT+CEREG?')
# wait for response
reply = self.wait_for_rx(log_file=log_file)
# parse response
if reply is not None and len(reply) == 2 and 'OK' in reply[-1]:
vals = re.findall('[0-9]+', reply[0])
if len(vals) >= 2:
try:
v = int(vals[1])
return v
except:
return 4
return 4
# Ping a server
def ping(self, server="8.8.8.8", num=4, log_file=None):
timeout = 1
self.have_ping = False
self.ping_count = 0
# format command
cmd = f'AT+QPING=1,"{server}",{timeout},{str(num)}'
# send command
self.set_cmd(cmd)
# wait for response
reply = self.wait_for_rx(wait_time=5, log_file=log_file)
if reply is None or reply[-1] != 'OK':
return None
for i in range(20):
self.update()
if self.have_ping:
return self.get_ping()
time.sleep(.1)
return None
modem_diagnostics.py
#!/usr/bin/env python3
import sys
import signal
import argparse
# Fails on Windows unless X-server is used
# If this line is commented, though, the script runs fine + W is not used anywhere
# from tkinter import W
from modem_interface import *
import re
# setup handler for control-c to exit cleanly
def ctrl_handler(signum, frame):
print("Ctrl-c was pressed.")
atm.close()
if log_file:
log_file.write("\nCtrl-c was pressed.\n")
log_file.close()
sys.exit(0)
# Main
def main():
# SERIAL_PORT = "/dev/ttyACM0" # Raspberry Pi 2
# BAUD = 115200
SERIAL_PORT = None
BAUD = None
# get inputs
parser=argparse.ArgumentParser()
parser.add_argument('--port', default='', type=str, help='Serial port to use')
parser.add_argument('--baud', default=115200, type=int, help='Baud rate to use')
args=parser.parse_args()
if args.baud:
BAUD = args.baud
if args.port:
SERIAL_PORT = args.port
else:
sp = serial_ports()
print('Select port:')
for (i,s) in enumerate(sp):
print('{} {}'.format(i,s))
ind = int(input('> '))
SERIAL_PORT = sp[ind]
# Diagnostic Commands
#atCommands = ['ATE0','ATI','AT+CGMI','AT+CGMM','AT+GMM','AT+CMEE=2', 'AT+CREG=2', 'AT+CGREG=2', 'AT+CEREG=3', 'AT+CGEREP=2,1', 'AT+CPIN?','AT+CCID','AT+CRSM=176,28539,0,0,12','AT+CFUN?','AT+CSQ','AT+CESQ','AT+CREG?','AT+CGREG?','AT+CEREG?','AT+CGDCONT?','AT+CGACT?','AT+COPS?','AT+COPS=?']
atCommands = ['ATE0','ATI','AT+CGMI','AT+CGMM','AT+GMM','AT+CMEE=2', 'AT+CREG=2', 'AT+CGREG=2', 'AT+CEREG=3', 'AT+CGEREP=2,1', 'AT+CPIN?','AT+CCID','AT+CRSM=176,28539,0,0,12','AT+CFUN?','AT+CSQ','AT+CESQ','AT+CREG?','AT+CGREG?','AT+CEREG?','AT+CGDCONT?','AT+CGACT?','AT+COPS?']
# Create and open modem interface
atm = ATManager(print_debug=True)
# Save to file
file_name = 'ModemDiagnostics.txt'
log_file = open(file_name, 'w')
signal.signal(signal.SIGINT, ctrl_handler)
print('\n---------------\nConnecting to modem ...')
while True:
try:
atm.open(SERIAL_PORT, BAUD)
break
except:
time.sleep(.1)
pass
while atm.isOpen():
atm.set_cmd('AT')
reply = atm.wait_for_rx(log_file=log_file)
#print(reply)
#print(reply[-1])
#print('OK' in reply)
if reply is None or len(reply) == 0:
time.sleep(1)
elif 'OK' in reply[-1]:
break
# the COPS=? command can take several minutes
atm.set_msg_timeout( 300 )
for cmd in atCommands:
atm.set_cmd(cmd)
atm.wait_for_rx(log_file=log_file)
log_file.write('-----\r\n')
print('-----\r\n')
# How you can parse out ICCID
atm.set_cmd('AT+QCCID')
reply = atm.wait_for_rx(log_file=log_file)
print('----')
if len(reply) >= 2 and reply[1] == 'OK':
x = re.findall('[0-9]+', reply[0])
print(x)
else:
print('Invalid reponse!')
# close interfaces
log_file.close()
atm.close()
print("Good bye!")
# Python stuff
if __name__ == '__main__':
main()