#!/usr/bin/env python3 # -*- coding: utf-8 -*- import sys import logging import shlex import time import threading from typing import Optional, List, Dict, Union from prompt_toolkit import PromptSession from prompt_toolkit.completion import WordCompleter from prompt_toolkit.shortcuts import print_formatted_text, clear as clear_screen from prompt_toolkit.formatted_text import HTML from prompt_toolkit.styles import Style from prompt_toolkit.patch_stdout import patch_stdout from m2m.nbiot import factory from m2m.utils import fmt_val from m2m.exceptions import SerialError, NetworkError logger = logging.getLogger('m2m.interactive') class ModemShell: """ Interactive shell for controlling IoT modules. """ def __init__(self, module_type: str, port: str, apn: str = 'iot.telefonica.de'): self.module_type = module_type self.port = port self.apn = apn try: self.module = factory(module_type, port) except Exception as e: print_formatted_text(HTML(f'Error: Could not initialize module on {port}: {e}')) sys.exit(1) # Command Dispatch Table self.commands = { 'info': self.do_info, 'status': self.do_status, 'nw_info': self.do_nw_info, 'cells': self.do_cells, 'forensics': self.do_forensics, 'scan': self.do_scan, 'connect': self.do_connect, 'disconnect': self.do_disconnect, 'operator': self.do_operator, 'ping': self.do_ping, 'dns': self.do_dns, 'http_get': self.do_http_get, 'udp_send': self.do_udp_send, 'udp_listen': self.do_udp_listen, 'tcp_client': self.do_tcp_client, 'sockets': self.do_sockets, 'close': self.do_close, 'recv': self.do_recv, 'sms_send': self.do_sms_send, 'sms_list': self.do_sms_list, 'radio': self.do_radio, 'reboot': self.do_reboot, 'psm': self.do_psm, 'mode': self.do_mode, 'edrx': self.do_edrx, 'bands': self.do_bands, 'gpio': self.do_gpio, 'ri': self.do_ri, 'urc_delay': self.do_urc_delay, 'at': self.do_at, 'error': self.do_error, 'ussd': self.do_ussd, 'clock': self.do_clock, 'temp': self.do_temp, 'ls': self.do_ls, 'power_off': self.do_power_off, 'help': self.do_help, 'clear': self.do_clear, 'exit': self.do_exit, 'quit': self.do_exit } self.completer = WordCompleter( list(self.commands.keys()) + ['on', 'off', 'loc', 'status', 'disable', 'sync', 'show', 'gsm', 'catm', 'nb', 'nb+emtc', 'all', 'opt'], ignore_case=True ) self.style = Style.from_dict({ 'prompt': '#00ff00 bold', 'command': '#ffffff', 'error': '#ff0000', 'info': '#00ffff', 'success': '#00ff00', 'header': '#ffff00 bold', 'alert': '#ff8800 bold', }) self._stop_bg = threading.Event() self._bg_thread = None # Register callbacks if hasattr(self.module.s_port, 'add_callback'): self.module.s_port.add_callback(b'+QIURC: "recv",', self._on_data_received) self.module.s_port.add_callback(b'+QIURC: "pdpdeact",', self._on_pdp_deact) def _on_data_received(self, payload: bytes): try: conn_id = payload.decode().strip() print_formatted_text(HTML(f'\n[DATA] Socket {conn_id} received data. Type "recv {conn_id}"')) except: pass def _on_pdp_deact(self, payload: bytes): print_formatted_text(HTML('\n[NETWORK] PDP Context Deactivated by network.')) def _bg_worker(self): """Polls for URCs when idle without blocking the main lock indefinitely.""" while not self._stop_bg.is_set(): try: if self.module.s_port._serial.in_waiting > 0: if self.module.s_port.lock.acquire(blocking=False): try: self.module.s_port.read_any(timeout=0.1) finally: self.module.s_port.lock.release() except: pass self._stop_bg.wait(0.5) def run(self): print_formatted_text(HTML(f'Interactive Modem Shell ({self.module_type} on {self.port})')) print_formatted_text(HTML('Type help for available commands, exit to quit.\n')) session = PromptSession(completer=self.completer, style=self.style) self._bg_thread = threading.Thread(target=self._bg_worker, daemon=True) self._bg_thread.start() with patch_stdout(): with self.module: while True: try: text = session.prompt(HTML('m2m> ')) except (KeyboardInterrupt, EOFError): break if not (stripped := text.strip()): continue try: parts = shlex.split(stripped) except ValueError as e: print_formatted_text(HTML(f'Parse error: {e}')); continue cmd, args = parts[0].lower(), parts[1:] if handler := self.commands.get(cmd): try: handler(args) except Exception as e: print_formatted_text(HTML(f'Error: {e}')) else: print_formatted_text(HTML(f'Unknown command: {cmd}')) self._stop_bg.set() def do_help(self, args): """Show available commands.""" print_formatted_text(HTML('
Available Commands:
')) for c in sorted(self.commands.keys()): doc = (self.commands[c].__doc__ or "No description.").replace('<', '<').replace('>', '>') print_formatted_text(HTML(f' {c:<12} {doc}')) def do_clear(self, _): """Clear the terminal screen.""" clear_screen() def do_info(self, _): """Read detailed module identification and configuration.""" print_formatted_text(HTML('Reading identification...')) print(f" IMEI: {self.module.read_imei() or 'N/A'}") print(f" IMSI: {self.module.read_imsi() or 'N/A'}") print(f" ICCID: {self.module.read_iccid() or 'N/A'}") print(f" FW: {self.module.read_firmware_version() or 'N/A'}") if hasattr(self.module, 'get_nw_scan_mode'): mode_map = {0: "Automatic", 1: "GSM Only", 2: "LTE Only", 3: "GSM + LTE"} print(f" ScanMode: {mode_map.get(self.module.get_nw_scan_mode(), 'Unknown')}") if hasattr(self.module, 'get_iot_op_mode_val'): op_map = {0: "eMTC", 1: "NB-IoT", 2: "eMTC + NB-IoT"} print(f" IoTMode: {op_map.get(self.module.get_iot_op_mode_val(), 'Unknown')}") def do_status(self, _): """Show registration and signal status.""" print_formatted_text(HTML('Checking status...')) rssi, ber = self.module.get_signal_quality() print(f" Signal: RSSI={rssi}, BER={ber}") print(f" Registered: {'Yes' if self.module.is_registered() else 'No'}") print(f" Operator: {self.module.get_current_operator() or 'None'}") print(f" IP Address: {self.module.get_ip_address(1) or 'None'}") def do_nw_info(self, _): """Show detailed network and serving cell info.""" if hasattr(self.module, 'get_network_info'): print(f" Network: {self.module.get_network_info()}") if hasattr(self.module, 'get_serving_cell_info'): print(f" Cell: {self.module.get_serving_cell_info()}") def do_cells(self, _): """Show serving and neighbor cell information.""" if hasattr(self.module, 'get_serving_cell_info'): print(f" Serving: {self.module.get_serving_cell_info()}") if hasattr(self.module, 'get_neighbor_cells'): for n in self.module.get_neighbor_cells(): print(f" - {n}") def do_forensics(self, _): """Full SIM card forensics report.""" print_formatted_text(HTML('
SIM CARD NETWORK FORENSICS
')) print(f"{'ICCID:':<25} {fmt_val(self.module.read_iccid())}") print(f"{'IMSI:':<25} {fmt_val(self.module.read_imsi())}") print(f"{'Home PLMN:':<25} {fmt_val(self.module.get_home_plmn())}") print(f"{'Registered PLMN:':<25} {fmt_val(self.module.get_registered_plmn())}") print(f"{'Forbidden PLMNs:':<25} {fmt_val(self.module.read_forbidden_plmns())}") print(f"{'Equivalent Home:':<25} {fmt_val(self.module.read_ehplmns())}") def do_scan(self, _): """Scan for available network operators.""" print_formatted_text(HTML('Scanning for operators...')) ops = self.module.search_operators(timeout=180) for op in (ops or []): s_map = {'0': 'Unknown', '1': 'Available', '2': 'Current', '3': 'Forbidden'} print(f" - {op['long']} ({op['mccmnc']}) [{s_map.get(op['status'], op['status'])}, Tech: {op['act']}]") def do_connect(self, args): """Connect to network. Usage: connect [apn] [--force]""" force = '--force' in args clean_args = [a for a in args if a != '--force'] apn = clean_args[0] if clean_args else self.apn print_formatted_text(HTML(f'Connecting with APN: {apn}...')) if self.module.connect_network(apn=apn, force=force): print_formatted_text(HTML('Connected!')) else: print_formatted_text(HTML('Failed.')) def do_disconnect(self, _): """Deactivate PDP context.""" if hasattr(self.module, 'deactivate_pdp_context') and self.module.deactivate_pdp_context(1): print_formatted_text(HTML('Disconnected.')) else: print_formatted_text(HTML('Failed or not supported.')) def do_operator(self, args): """Select operator manually. Usage: operator [act]""" if not args: print("Usage: operator [act]"); return plmn, act = args[0], int(args[1]) if len(args) > 1 else 8 if self.module.set_operator(plmn, act=act): print_formatted_text(HTML('Done.')) else: print_formatted_text(HTML('Failed.')) def do_ping(self, args): """Ping a host. Usage: ping [num]""" if not args: print("Usage: ping [num]"); return host, num = args[0], int(args[1]) if len(args) > 1 else 4 res = self.module.ping(host, num=num) for r in res: print(f" {r}") if (s := self.module.parse_ping_summary(res)): print_formatted_text(HTML(f'RTT avg: {s["avg"]}ms')) def do_dns(self, args): """Resolve a hostname. Usage: dns """ if not args: print("Usage: dns "); return if (ips := self.module.dns_query(args[0])): print(f" IPs: {', '.join(ips)}") else: print_formatted_text(HTML('Failed.')) def do_http_get(self, args): """Perform HTTP GET. Usage: http_get """ if not args: print("Usage: http_get "); return url = args[0] if args[0].startswith('http') else f'http://{args[0]}' if self.module.http_set_url(url) and self.module.http_get(): print("-" * 40 + f"\n{self.module.http_read_response()}\n" + "-" * 40) else: print_formatted_text(HTML('Failed.')) def do_udp_send(self, args): """Send a UDP packet. Usage: udp_send """ if len(args) < 3: print("Usage: udp_send "); return host, port, data = args[0], int(args[1]), " ".join(args[2:]) sock_id = -1 if hasattr(self.module, 'get_socket_state'): for s in self.module.get_socket_state(): if s['type'] == 'UDP' and s['remote'] == f"{host}:{port}": sock_id = int(s['id']); break if sock_id == -1: sock_id = self.module.open_socket(service_type="UDP", remote_ip=host, remote_port=port) if sock_id >= 0 and self.module.send_data_hex(sock_id, data.encode()): print_formatted_text(HTML('Sent!')) else: print_formatted_text(HTML('Failed.')) def do_udp_listen(self, args): """Open a local UDP listener. Usage: udp_listen """ if not args: print("Usage: udp_listen "); return sid = self.module.open_socket(service_type="UDP SERVICE", remote_ip="127.0.0.1", local_port=int(args[0])) if sid >= 0: print_formatted_text(HTML(f'Listening on socket {sid}.')) else: print_formatted_text(HTML('Failed.')) def do_tcp_client(self, args): """Connect to a TCP server. Usage: tcp_client """ if len(args) < 2: print("Usage: tcp_client "); return sid = self.module.open_socket(service_type="TCP", remote_ip=args[0], remote_port=int(args[1])) if sid >= 0: print_formatted_text(HTML(f'Socket {sid} connected.')) else: print_formatted_text(HTML('Failed.')) def do_sockets(self, _): """List active sockets.""" if hasattr(self.module, 'get_socket_state'): socks = self.module.get_socket_state() for s in socks: print(f" ID {s['id']}: {s['type']:<8} | {s['remote']:<20} | {s['state']}") else: print("Not supported.") def do_close(self, args): """Close a socket. Usage: close """ if args and self.module.close_socket(int(args[0])): print_formatted_text(HTML('Closed.')) else: print_formatted_text(HTML('Failed.')) def do_recv(self, args): """Read socket data. Usage: recv [len]""" if not args: print("Usage: recv [len]"); return data = self.module.receive_data(int(args[0]), int(args[1]) if len(args) > 1 else 1500) if data: print("-" * 20 + f"\n{data.decode(errors='replace')}\n" + "-" * 20) else: print(" No data.") def do_sms_send(self, args): """Send an SMS. Usage: sms_send """ if len(args) < 2: print("Usage: sms_send ") return number, message = args[0], " ".join(args[1:]) print_formatted_text(HTML(f'Sending SMS to {number}...')) if self.module.send_sms(number, message): print_formatted_text(HTML('Sent!')) else: print_formatted_text(HTML('Failed to send SMS.')) def do_sms_list(self, args): """List SMS. Usage: sms_list [ALL|REC UNREAD]""" for msg in self.module.list_sms(args[0] if args else "ALL"): print(msg) def do_radio(self, args): """Toggle radio. Usage: radio """ if not args: print(f"Radio is {'ON' if self.module.is_radio_on() else 'OFF'}"); return self.module.radio_on() if args[0].lower() == 'on' else self.module.radio_off() def do_reboot(self, _): """Reboot the module.""" self.module.reboot() def do_mode(self, args): """Set connectivity preference. Usage: mode """ if not args: print("Usage: mode ") return mode = args[0].lower() print_formatted_text(HTML(f'Setting connectivity mode to {mode}...')) if hasattr(self.module, 'set_connectivity_mode'): if self.module.set_connectivity_mode(mode): print_formatted_text(HTML('Mode set successfully.')) else: print_formatted_text(HTML('Failed to set mode.')) else: print("Mode configuration not supported on this module.") def do_psm(self, args): """Control PSM. Usage: psm """ sub = args[0].lower() if args else 'status' if sub == 'status': print(f" PSM: {self.module.read_psm_status() or 'Unknown'}") elif sub == 'off': self.module.disable_psm() elif sub == 'on' and len(args) >= 3: self.module.configure_psm_auto(int(args[1]), int(args[2])) elif sub == 'opt': self.module.configure_psm_optimization() def do_edrx(self, args): """Configure eDRX. Usage: edrx """ if len(args) >= 3 and self.module.set_edrx(int(args[0]), int(args[1]), args[2]): print_formatted_text(HTML('Done.')) def do_bands(self, args): """Show or lock frequency bands. Usage: bands [gsm|catm|nb] [add|remove] """ from m2m.utils import hex_mask_to_bands, bands_to_hex_mask if not hasattr(self.module, 'get_locked_bands'): print("Band control not supported."); return current = self.module.get_locked_bands() # Map to actual lists of integers band_lists = {k: hex_mask_to_bands(v) for k, v in current.items()} if not args: print_formatted_text(HTML('Currently Active Bands:')) for rat, blist in band_lists.items(): print(f" {rat.upper():<5}: {blist}") return if len(args) < 3: print("Usage: bands "); return rat, action, bnum = args[0].lower(), args[1].lower(), int(args[2]) if rat not in band_lists: print(f"Invalid RAT: {rat}. Use gsm, catm, or nb."); return if action == 'add': if bnum not in band_lists[rat]: band_lists[rat].append(bnum) elif action == 'remove': if bnum in band_lists[rat]: band_lists[rat].remove(bnum) else: print(f"Invalid action: {action}. Use add or remove."); return # Apply changes print_formatted_text(HTML(f'Updating {rat.upper()} bands...')) if self.module.lock_bands( gsm_bands=band_lists.get('gsm', []), catm_bands=band_lists.get('catm', []), nb_bands=band_lists.get('nb', []) ): print_formatted_text(HTML('Bands updated successfully.')) else: print_formatted_text(HTML('Failed to update bands.')) def do_gpio(self, args): """Control GPIO. Usage: gpio set | get """ if len(args) >= 2: if args[0] == 'set' and len(args) >= 3: self.module.set_gpio(3, int(args[1]), int(args[2])) elif args[0] == 'get': print(f"GPIO {args[1]}: {self.module.get_gpio(int(args[1]))}") def do_ri(self, args): """Configure Ring Indicator. Usage: ri [duration]""" if len(args) >= 1: dur = int(args[1]) if len(args) > 1 else 120 if hasattr(self.module, 'configure_ri_pin') and self.module.configure_ri_pin(args[0], dur): print_formatted_text(HTML('Done.')) def do_urc_delay(self, args): """Configure URC delay. Usage: urc_delay """ if args and hasattr(self.module, 'configure_urc_delay'): if self.module.configure_urc_delay(args[0].lower() == 'on'): print_formatted_text(HTML('Done.')) def do_at(self, args): """Send raw AT command.""" if args: cmd = " ".join(args) if not cmd.upper().startswith('AT'): cmd = f'AT{cmd}' print(self.module.send_at_command(cmd).decode(errors='ignore')) def do_error(self, _): """Show extended error report (AT+CEER).""" print(f" Report: {self.module.get_extended_error_report()}") def do_ussd(self, args): """Send USSD code. Usage: ussd """ if args: print(f" Response: {self.module.send_ussd(args[0])}") def do_clock(self, args): """Show or sync clock. Usage: clock [show|sync]""" sub = args[0].lower() if args else 'show' if sub == 'show': print(f" Clock: {self.module.get_clock()}") elif sub == 'sync': if self.module.ntp_sync(): print(f" New Clock: {self.module.get_clock()}") else: print_formatted_text(HTML('Sync failed.')) def do_temp(self, _): """Read module temperature.""" if hasattr(self.module, 'get_temperature'): print(f" Temp: {self.module.get_temperature()} C") def do_ls(self, args): """List files. Usage: ls [pattern]""" files = self.module.list_files(args[0] if args else "*") print(f"{'Filename':<30} | {'Size':>10}") for f in files: print(f"{f['name']:<30} | {f['size']:>10,}") def do_power_off(self, _): """Power off.""" self.module.power_off() def do_exit(self, _): """Exit shell.""" sys.exit(0)