QCSuper can be split up into two building blocks:
-
Inputs are Python classes exposing interfaces to read and optionally send Diag protocol data. We can distinguish:
- Inputs for communicating live with devices (the
--adb
input for talking with rooted Android phones, the--usb-modem
for talking with USB modems) - Inputs for processing saved data (
--dlf-read
is able to read data in a format providing interoperability with the vendor QXDM tool)
- Inputs for communicating live with devices (the
-
Modules are Python classes using inputs to perform specific tasks using the Diag protocol. For example:
- Capturing raw 2G/3G/4G signalling traffic (
--pcap-dump
will dump to a PCAP file,--wireshark-live
will open directly Wireshark) - Gathering generation info about the device (
--info
) - Capturing raw Diag logs information into reusable formats (
--json-geo-dump
,--dlf-dump
)
- Capturing raw 2G/3G/4G signalling traffic (
QCSuper needs to deal to multiple sources of input:
-
The connected Diag device
- Which can be different kinds of descriptor:
- A pseudo-serial USB device
- A TCP socket communicating with a remote Android device
- A file to replay from
- Which can deliver different kinds of received data:
- Synchronous responses to requests
- Asynchronous logs or messages (see The Diag protocol.md)
- When the source is a real device, the device can accept only one Diag client at once
- Which can be different kinds of descriptor:
-
An optional interactive prompt (
--cli
module): needed to provide a handy way to continue capturing on the Diag client while executing other tasks
All this requires a form of concurrency to be acheived: either threading, or a way to poll on descriptors through an event loop.
The design ease/simplicity tradeoff I have chosen was to use threading (but I'm open to rework the architecture if someone has something else to propose). Polling on both a serial port and a featureful command prompt or thread queue (for example) is not doable easily in a multi-platform way, and using asyncio seemed to add some design and syntaxic overhead/external libraries to the equation.
QCSuper makes use of different threads:
- The main thread contains the loop reading from the device, and is the only place where reading is performed (it will also dispatch asynchronous messages to modules, calling the
on_log
,on_message
callbacks which may not write neither read, and calling at teardown theon_deinit
callback which may write) - A background thread is used for initializing the modules selected through command line arguments (calling the
on_init
callback which may write) - Edge case only: a background thread may be used for the optional interactive prompt (
--cli
) and initializing the modules called from it (calling theon_init
callback which may write)
A module is a Python class which may expose different methods:
__init__
: will receive the input object as its first argument, and optionally other arguments from the command line or interactive prompt (passed in sequence from the entry pointqcsuper.py
).on_init
: called when the connection to the Diag device is established. Not called when the input is not a device but a file containing recorded log data.- Callbacks triggered by a read on the input source:
on_log
: called when an asynchronous response Diag protocol raw "log" is received.on_message
: called when an asynchronous response Diag protocol text "message" structure is received.
on_deinit
: called when the connection to the Diag device ceased establishment, or the user hit Ctrl+C.
The methods composing these callbacks may perform request-response operations (using self.diag_reader.send_recv(opcode, payload)
, where self.diag_reader
is the input object).
When a request-response operation is performed, the thread for the callback is paused for the time the response is received, using a thread synchronization primitive shared with the main thread.
When using the interactive prompt (--cli
), the moment where the on_init
callbacks ends is the moment where the user is informed that the task continued to background (or is finished, in the case where there is no further callbacks).
An input is a Python class which may expose different methods:
__init__
: will optionally receive arguments from the command line or interactive prompt (passed in sequence from the entry pointqcsuper.py
).send_request
: this function will be called when a module wants to send a Diag request packet (involving possible utility functions such ashdlc_encapsulate
fromHdlcMixin
).read_loop
: Diag responses packets will be read and dispatched from here, involving the use of the privatedispatch_received_diag_packet
method inherited fromBaseInput
(and other possible utility functions, such ashdlc_decapsulate
fromHdlcMixin
).
Inputs inherit from the BaseInput
class which exposes a method called send_recv
, allowing to write a request then read the response, wrapping transparently thread synchronization primitives.
send_recv
is most likely the only method of an input to be called directly from a module.