blob: 9a4f0a560d5dc53a2e5f83d6499686e03cde63fd [file] [log] [blame]
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +07001# -*- coding: utf-8 -*-
2
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +07003# Copyright (C) 2020 Vadim Yanitskiy <axilirator@gmail.com>
4#
5# This program is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 2 of the License, or
8# (at your option) any later version.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program. If not, see <http://www.gnu.org/licenses/>.
17#
18
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +070019import logging as log
20import serial
21import time
22import re
Philipp Maier8c823782023-10-23 10:44:44 +020023import argparse
Philipp Maier4af63dc2023-10-26 12:17:32 +020024import os
Harald Weltef9f8d7a2023-07-09 17:06:16 +020025from typing import Optional
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +070026
Harald Weltef9f8d7a2023-07-09 17:06:16 +020027from pySim.utils import Hexstr, ResTuple
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +070028from pySim.transport import LinkBase
29from pySim.exceptions import *
30
31# HACK: if somebody needs to debug this thing
32# log.root.setLevel(log.DEBUG)
33
Harald Weltec91085e2022-02-10 18:05:45 +010034
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +070035class ModemATCommandLink(LinkBase):
Harald Weltec91085e2022-02-10 18:05:45 +010036 """Transport Link for 3GPP TS 27.007 compliant modems."""
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +070037
Harald Welte0f177c12023-12-17 12:38:29 +010038 def __init__(self, opts: argparse.Namespace = argparse.Namespace(modem_dev='/dev/ttyUSB0',
39 modem_baud=115200), **kwargs):
40 device = opts.modem_dev
41 baudrate = opts.modem_baud
Harald Weltec91085e2022-02-10 18:05:45 +010042 super().__init__(**kwargs)
Philipp Maier4af63dc2023-10-26 12:17:32 +020043 if os.environ.get('PYSIM_INTEGRATION_TEST') == "1":
44 print("Using modem for Generic SIM Access (3GPP TS 27.007)")
45 else:
46 print("Using modem for Generic SIM Access (3GPP TS 27.007) at port %s" % device)
Harald Weltec91085e2022-02-10 18:05:45 +010047 self._sl = serial.Serial(device, baudrate, timeout=5)
48 self._echo = False # this will be auto-detected by _check_echo()
49 self._device = device
50 self._atr = None
Robert Falkenberg18fb82b2021-05-02 10:21:23 +020051
Harald Weltec91085e2022-02-10 18:05:45 +010052 # Check the AT interface
53 self._check_echo()
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +070054
Harald Weltec91085e2022-02-10 18:05:45 +010055 # Trigger initial reset
56 self.reset_card()
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +070057
Harald Weltec91085e2022-02-10 18:05:45 +010058 def __del__(self):
59 if hasattr(self, '_sl'):
60 self._sl.close()
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +070061
Harald Weltec91085e2022-02-10 18:05:45 +010062 def send_at_cmd(self, cmd, timeout=0.2, patience=0.002):
63 # Convert from string to bytes, if needed
64 bcmd = cmd if type(cmd) is bytes else cmd.encode()
65 bcmd += b'\r'
Robert Falkenberg18fb82b2021-05-02 10:21:23 +020066
Harald Weltec91085e2022-02-10 18:05:45 +010067 # Clean input buffer from previous/unexpected data
68 self._sl.reset_input_buffer()
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +070069
Harald Weltec91085e2022-02-10 18:05:45 +010070 # Send command to the modem
71 log.debug('Sending AT command: %s', cmd)
72 try:
73 wlen = self._sl.write(bcmd)
74 assert(wlen == len(bcmd))
75 except:
76 raise ReaderError('Failed to send AT command: %s' % cmd)
Robert Falkenbergdddcc602021-05-06 09:55:57 +020077
Harald Weltec91085e2022-02-10 18:05:45 +010078 rsp = b''
79 its = 1
80 t_start = time.time()
81 while True:
82 rsp = rsp + self._sl.read(self._sl.in_waiting)
83 lines = rsp.split(b'\r\n')
84 if len(lines) >= 2:
85 res = lines[-2]
86 if res == b'OK':
87 log.debug('Command finished with result: %s', res)
88 break
89 if res == b'ERROR' or res.startswith(b'+CME ERROR:'):
90 log.error('Command failed with result: %s', res)
91 break
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +070092
Harald Weltec91085e2022-02-10 18:05:45 +010093 if time.time() - t_start >= timeout:
94 log.info('Command finished with timeout >= %ss', timeout)
95 break
96 time.sleep(patience)
97 its += 1
98 log.debug('Command took %0.6fs (%d cycles a %fs)',
99 time.time() - t_start, its, patience)
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +0700100
Harald Weltec91085e2022-02-10 18:05:45 +0100101 if self._echo:
102 # Skip echo chars
103 rsp = rsp[wlen:]
104 rsp = rsp.strip()
105 rsp = rsp.split(b'\r\n\r\n')
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +0700106
Harald Weltec91085e2022-02-10 18:05:45 +0100107 log.debug('Got response from modem: %s', rsp)
108 return rsp
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +0700109
Harald Weltec91085e2022-02-10 18:05:45 +0100110 def _check_echo(self):
111 """Verify the correct response to 'AT' command
112 and detect if inputs are echoed by the device
Robert Falkenberg18fb82b2021-05-02 10:21:23 +0200113
Harald Weltec91085e2022-02-10 18:05:45 +0100114 Although echo of inputs can be enabled/disabled via
115 ATE1/ATE0, respectively, we rather detect the current
116 configuration of the modem without any change.
117 """
118 # Next command shall not strip the echo from the response
119 self._echo = False
120 result = self.send_at_cmd('AT')
Robert Falkenberg18fb82b2021-05-02 10:21:23 +0200121
Harald Weltec91085e2022-02-10 18:05:45 +0100122 # Verify the response
123 if len(result) > 0:
124 if result[-1] == b'OK':
125 self._echo = False
126 return
127 elif result[-1] == b'AT\r\r\nOK':
128 self._echo = True
129 return
130 raise ReaderError(
131 'Interface \'%s\' does not respond to \'AT\' command' % self._device)
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +0700132
Harald Weltec91085e2022-02-10 18:05:45 +0100133 def reset_card(self):
134 # Reset the modem, just to be sure
135 if self.send_at_cmd('ATZ') != [b'OK']:
136 raise ReaderError('Failed to reset the modem')
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +0700137
Harald Weltec91085e2022-02-10 18:05:45 +0100138 # Make sure that generic SIM access is supported
139 if self.send_at_cmd('AT+CSIM=?') != [b'OK']:
140 raise ReaderError('The modem does not seem to support SIM access')
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +0700141
Harald Weltec91085e2022-02-10 18:05:45 +0100142 log.info('Modem at \'%s\' is ready!' % self._device)
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +0700143
Harald Weltec91085e2022-02-10 18:05:45 +0100144 def connect(self):
145 pass # Nothing to do really ...
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +0700146
Harald Weltec91085e2022-02-10 18:05:45 +0100147 def disconnect(self):
148 pass # Nothing to do really ...
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +0700149
Harald Welteab6897c2023-07-09 16:21:23 +0200150 def wait_for_card(self, timeout: Optional[int] = None, newcardonly: bool = False):
Harald Weltec91085e2022-02-10 18:05:45 +0100151 pass # Nothing to do really ...
Robert Falkenberg8e15c182021-05-01 08:07:27 +0200152
Harald Weltef9f8d7a2023-07-09 17:06:16 +0200153 def _send_apdu_raw(self, pdu: Hexstr) -> ResTuple:
Harald Weltec91085e2022-02-10 18:05:45 +0100154 # Make sure pdu has upper case hex digits [A-F]
155 pdu = pdu.upper()
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +0700156
Harald Weltec91085e2022-02-10 18:05:45 +0100157 # Prepare the command as described in 8.17
158 cmd = 'AT+CSIM=%d,\"%s\"' % (len(pdu), pdu)
159 log.debug('Sending command: %s', cmd)
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +0700160
Harald Weltec91085e2022-02-10 18:05:45 +0100161 # Send AT+CSIM command to the modem
Harald Weltec91085e2022-02-10 18:05:45 +0100162 rsp = self.send_at_cmd(cmd)
Tobias Engeld70ac222023-05-29 21:20:59 +0200163 if rsp[-1].startswith(b'+CME ERROR:'):
164 raise ProtocolError('AT+CSIM failed with: %s' % str(rsp))
Harald Weltec91085e2022-02-10 18:05:45 +0100165 if len(rsp) != 2 or rsp[-1] != b'OK':
166 raise ReaderError('APDU transfer failed: %s' % str(rsp))
167 rsp = rsp[0] # Get rid of b'OK'
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +0700168
Harald Weltec91085e2022-02-10 18:05:45 +0100169 # Make sure that the response has format: b'+CSIM: %d,\"%s\"'
170 try:
171 result = re.match(b'\+CSIM: (\d+),\"([0-9A-F]+)\"', rsp)
172 (rsp_pdu_len, rsp_pdu) = result.groups()
173 except:
174 raise ReaderError('Failed to parse response from modem: %s' % rsp)
175
176 # TODO: make sure we have at least SW
177 data = rsp_pdu[:-4].decode().lower()
178 sw = rsp_pdu[-4:].decode().lower()
179 log.debug('Command response: %s, %s', data, sw)
180 return data, sw
Philipp Maier6bfa8a82023-10-09 13:32:49 +0200181
Philipp Maier58e89eb2023-10-10 11:59:03 +0200182 def __str__(self) -> str:
Philipp Maier6bfa8a82023-10-09 13:32:49 +0200183 return "modem:%s" % self._device
Philipp Maier8c823782023-10-23 10:44:44 +0200184
185 @staticmethod
186 def argparse_add_reader_args(arg_parser: argparse.ArgumentParser):
187 modem_group = arg_parser.add_argument_group('AT Command Modem Reader')
188 modem_group.add_argument('--modem-device', dest='modem_dev', metavar='DEV', default=None,
189 help='Serial port of modem for Generic SIM Access (3GPP TS 27.007)')
190 modem_group.add_argument('--modem-baud', type=int, metavar='BAUD', default=115200,
191 help='Baud rate used for modem port')