sedbgmux/sedbgmux-shell.py

246 lines
10 KiB
Python
Executable File

#!/usr/bin/env python3
# This file is a part of sedbgmux, an open source DebugMux client.
# Copyright (c) 2022-2023 Vadim Yanitskiy <fixeria@osmocom.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
import argparse
import cmd2
import sys
from typing import List
from sedbgmux.io import DbgMuxIOModem
from sedbgmux.io import DumpIONative
from sedbgmux import DbgMuxPeer
from sedbgmux import DbgMuxClient
from sedbgmux.ch import DbgMuxConnTerminal
from sedbgmux.ch import DbgMuxConnFileLogger
from sedbgmux.ch import DbgMuxConnUdpProxy
from sedbgmux.ch import DbgMuxConnWalker
# local logger for this module
log = logging.getLogger(__name__)
class SEDbgMuxApp(cmd2.Cmd):
DESC = 'DebugMux client for [Sony] Ericsson phones and modems'
# Command categories
CATEGORY_CONN = 'Connection management commands'
CATEGORY_DBGMUX = 'DebugMux specific commands'
def __init__(self, argv):
super().__init__(allow_cli_args=False)
if argv.verbose > 0:
logging.root.setLevel(logging.DEBUG)
self.debug = True
self.intro = cmd2.style('Welcome to %s!' % self.DESC, fg=cmd2.Fg.RED)
self.prompt = 'DebugMux (\'%s\')> ' % argv.serial_port
self.default_category = 'Built-in commands'
self.argv = argv
# Init the I/O layer, DebugMux peer and client
self.io = DbgMuxIOModem(self.argv)
self.peer = DbgMuxPeer(self.io)
self.client = DbgMuxClient(self.peer)
# Optionally dump DebugMux frames to a file
if argv.dump_file is not None:
dump = DumpIONative(argv.dump_file, readonly=False)
self.peer.enable_dump(dump)
# Modem connection state
self.set_connected(False)
def _tab_data_providers(self) -> List[cmd2.CompletionItem]:
''' Generate a list of DPRef values for tab-completion '''
return [cmd2.CompletionItem('0x%02x' % DPRef, DPName)
for DPRef, DPName in self.client.data_providers.items()]
def _tab_connections(self) -> List[cmd2.CompletionItem]:
''' Generate a list of ConnRef values for tab-completion '''
return [cmd2.CompletionItem('0x%02x' % ConnRef, 'DPRef=%02x %s' % ConnInfo)
for ConnRef, ConnInfo in self.client.active_conn.items()]
def set_connected(self, state: bool) -> None:
self.connected: bool = state
if self.connected:
self.enable_category(self.CATEGORY_DBGMUX)
else:
msg = 'You must be connected to use this command'
self.disable_category(self.CATEGORY_DBGMUX, msg)
@cmd2.with_category(CATEGORY_CONN)
def do_connect(self, opts) -> None:
''' Connect to the modem and switch it to DebugMux mode '''
self.io.connect()
self.peer.start()
self.client.start()
self.set_connected(True)
@cmd2.with_category(CATEGORY_CONN)
def do_disconnect(self, opts) -> None:
''' Disconnect from the modem '''
self.client.stop()
self.peer.stop()
self.io.disconnect()
self.set_connected(False)
@cmd2.with_category(CATEGORY_CONN)
def do_status(self, opts) -> None:
''' Print connection info and statistics '''
if not self.connected:
self.poutput('Not connected')
return
self.poutput('Connected to \'%s\'' % self.argv.serial_port)
self.poutput('Baudrate: %d' % self.argv.serial_baudrate)
self.poutput('TxCount (Ns): %d' % self.peer.tx_count)
self.poutput('RxCount (Nr): %d' % self.peer.rx_count)
show_parser = cmd2.Cmd2ArgumentParser()
show_sparser = show_parser.add_subparsers(dest='command', required=True)
show_sparser.add_parser('target-info')
show_sparser.add_parser('data-providers')
show_sparser.add_parser('connections')
@cmd2.with_argparser(show_parser)
@cmd2.with_category(CATEGORY_CONN)
def do_show(self, opts) -> None:
''' Show various information '''
if opts.command == 'target-info':
self.poutput('Name: ' + (self.client.target_name or '(unknown)'))
self.poutput('IMEI: ' + (self.client.target_imei or '(unknown)'))
elif opts.command == 'data-providers':
for (DPRef, DPName) in self.client.data_providers.items():
self.poutput('Data Provider (DPRef=0x%02x): %s' % (DPRef, DPName))
elif opts.command == 'connections':
for (ConnRef, ConnInfo) in self.client.active_conn.items():
(DPRef, ch) = ConnInfo
self.poutput('Connection (DPRef=0x%02x, ConnRef=0x%02x): %s'
% (DPRef, ConnRef, str(ch)))
for (DPRef, ch) in self.client.pending_conn.items():
self.poutput('Pending Connection (DPRef=0x%02x): %s' % (DPRef, str(ch)))
@cmd2.with_category(CATEGORY_DBGMUX)
def do_enquiry(self, opts) -> None:
''' Enquiry target identifier and available Data Providers '''
self.client.enquiry()
ping_parser = cmd2.Cmd2ArgumentParser()
ping_parser.add_argument('-p', '--payload',
type=str, default='Knock, knock!',
help='Ping payload')
@cmd2.with_argparser(ping_parser)
@cmd2.with_category(CATEGORY_DBGMUX)
def do_ping(self, opts) -> None:
''' Send a Ping to the target, expect Pong '''
self.client.ping(opts.payload)
establish_parser = cmd2.Cmd2ArgumentParser()
establish_parser.add_argument('DPRef',
type=lambda v: int(v, 16),
choices_provider=_tab_data_providers,
help='DPRef of a Data Provider in hex')
establish_sparser = establish_parser.add_subparsers(dest='handler', required=True,
help='Connection handler')
ch_terminal = establish_sparser.add_parser('terminal',
help=DbgMuxConnTerminal.__doc__)
ch_walker = establish_sparser.add_parser('walker',
help=DbgMuxConnWalker.__doc__)
ch_file_logger = establish_sparser.add_parser('file-logger',
help=DbgMuxConnFileLogger.__doc__)
ch_file_logger.add_argument('FILE', type=argparse.FileType('ab', 0),
completer=cmd2.Cmd.path_complete,
help='File name or \'-\' for stdout')
ch_udp_proxy = establish_sparser.add_parser('udp-proxy',
help=DbgMuxConnUdpProxy.__doc__)
ch_udp_proxy.add_argument('-la', '--local-addr', dest='laddr', type=str,
default=DbgMuxConnUdpProxy.LADDR_DEF[0],
help='Local address (default: %(default)s)')
ch_udp_proxy.add_argument('-lp', '--local-port', dest='lport', type=int,
default=DbgMuxConnUdpProxy.LADDR_DEF[1],
help='Local port (default: %(default)s)')
ch_udp_proxy.add_argument('-ra', '--remote-addr', dest='raddr', type=str,
default=DbgMuxConnUdpProxy.RADDR_DEF[0],
help='Remote address (default: %(default)s)')
ch_udp_proxy.add_argument('-rp', '--remote-port', dest='rport', type=int,
default=DbgMuxConnUdpProxy.RADDR_DEF[1],
help='Remote port (default: %(default)s)')
@cmd2.with_argparser(establish_parser)
@cmd2.with_category(CATEGORY_DBGMUX)
def do_establish(self, opts) -> None:
''' Establish connections with Data Providers '''
if opts.handler == 'terminal':
ch = DbgMuxConnTerminal()
elif opts.handler == 'walker':
ch = DbgMuxConnWalker()
elif opts.handler == 'file-logger':
ch = DbgMuxConnFileLogger(opts.FILE)
elif opts.handler == 'udp-proxy':
ch = DbgMuxConnUdpProxy(laddr=(opts.laddr, opts.lport),
raddr=(opts.raddr, opts.rport))
self.client.conn_establish(opts.DPRef, ch)
if opts.handler == 'terminal':
ch.attach() # blocking until Ctrl + [CD]
ch.terminate()
elif opts.handler == 'walker':
ch.walk() # blocking
ch.terminate()
terminate_parser = cmd2.Cmd2ArgumentParser()
terminate_parser.add_argument('ConnRef',
type=lambda v: int(v, 16),
choices_provider=_tab_connections,
help='ConnRef in hex')
@cmd2.with_argparser(terminate_parser)
@cmd2.with_category(CATEGORY_DBGMUX)
def do_terminate(self, opts) -> None:
''' Terminate connection with a Data Provider '''
self.client.conn_terminate(opts.ConnRef)
ap = argparse.ArgumentParser(prog='sedbgmux-shell', description=SEDbgMuxApp.DESC)
ap.add_argument('-v', '--verbose', action='count', default=0,
help='print debug logging')
group = ap.add_argument_group('connection parameters')
group.add_argument('-p', '--serial-port', metavar='PORT', type=str, default='/dev/ttyACM0',
help='serial port path (default %(default)s)')
group.add_argument('--serial-baudrate', metavar='BAUDRATE', type=int, default=115200,
help='serial port speed (default %(default)s)')
group.add_argument('--serial-timeout', metavar='TIMEOUT', type=float, default=0.5,
help='serial port read timeout (default %(default)s)')
group.add_argument('--dump-file', metavar='FILE', type=str,
help='save Rx/Tx DebugMux frames to a file')
logging.basicConfig(
format='\r[%(levelname)s] %(filename)s:%(lineno)d %(message)s', level=logging.INFO)
if __name__ == '__main__':
argv = ap.parse_args()
app = SEDbgMuxApp(argv)
sys.exit(app.cmdloop())