blob: ea50bc9d72af38ede25e295a91d53b13498f3124 [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
23
24from pySim.transport import LinkBase
25from pySim.exceptions import *
26
27# HACK: if somebody needs to debug this thing
28# log.root.setLevel(log.DEBUG)
29
Harald Weltec91085e2022-02-10 18:05:45 +010030
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +070031class ModemATCommandLink(LinkBase):
Harald Weltec91085e2022-02-10 18:05:45 +010032 """Transport Link for 3GPP TS 27.007 compliant modems."""
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +070033
Harald Weltec91085e2022-02-10 18:05:45 +010034 def __init__(self, device: str = '/dev/ttyUSB0', baudrate: int = 115200, **kwargs):
35 super().__init__(**kwargs)
36 self._sl = serial.Serial(device, baudrate, timeout=5)
37 self._echo = False # this will be auto-detected by _check_echo()
38 self._device = device
39 self._atr = None
Robert Falkenberg18fb82b2021-05-02 10:21:23 +020040
Harald Weltec91085e2022-02-10 18:05:45 +010041 # Check the AT interface
42 self._check_echo()
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +070043
Harald Weltec91085e2022-02-10 18:05:45 +010044 # Trigger initial reset
45 self.reset_card()
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +070046
Harald Weltec91085e2022-02-10 18:05:45 +010047 def __del__(self):
48 if hasattr(self, '_sl'):
49 self._sl.close()
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +070050
Harald Weltec91085e2022-02-10 18:05:45 +010051 def send_at_cmd(self, cmd, timeout=0.2, patience=0.002):
52 # Convert from string to bytes, if needed
53 bcmd = cmd if type(cmd) is bytes else cmd.encode()
54 bcmd += b'\r'
Robert Falkenberg18fb82b2021-05-02 10:21:23 +020055
Harald Weltec91085e2022-02-10 18:05:45 +010056 # Clean input buffer from previous/unexpected data
57 self._sl.reset_input_buffer()
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +070058
Harald Weltec91085e2022-02-10 18:05:45 +010059 # Send command to the modem
60 log.debug('Sending AT command: %s', cmd)
61 try:
62 wlen = self._sl.write(bcmd)
63 assert(wlen == len(bcmd))
64 except:
65 raise ReaderError('Failed to send AT command: %s' % cmd)
Robert Falkenbergdddcc602021-05-06 09:55:57 +020066
Harald Weltec91085e2022-02-10 18:05:45 +010067 rsp = b''
68 its = 1
69 t_start = time.time()
70 while True:
71 rsp = rsp + self._sl.read(self._sl.in_waiting)
72 lines = rsp.split(b'\r\n')
73 if len(lines) >= 2:
74 res = lines[-2]
75 if res == b'OK':
76 log.debug('Command finished with result: %s', res)
77 break
78 if res == b'ERROR' or res.startswith(b'+CME ERROR:'):
79 log.error('Command failed with result: %s', res)
80 break
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +070081
Harald Weltec91085e2022-02-10 18:05:45 +010082 if time.time() - t_start >= timeout:
83 log.info('Command finished with timeout >= %ss', timeout)
84 break
85 time.sleep(patience)
86 its += 1
87 log.debug('Command took %0.6fs (%d cycles a %fs)',
88 time.time() - t_start, its, patience)
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +070089
Harald Weltec91085e2022-02-10 18:05:45 +010090 if self._echo:
91 # Skip echo chars
92 rsp = rsp[wlen:]
93 rsp = rsp.strip()
94 rsp = rsp.split(b'\r\n\r\n')
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +070095
Harald Weltec91085e2022-02-10 18:05:45 +010096 log.debug('Got response from modem: %s', rsp)
97 return rsp
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +070098
Harald Weltec91085e2022-02-10 18:05:45 +010099 def _check_echo(self):
100 """Verify the correct response to 'AT' command
101 and detect if inputs are echoed by the device
Robert Falkenberg18fb82b2021-05-02 10:21:23 +0200102
Harald Weltec91085e2022-02-10 18:05:45 +0100103 Although echo of inputs can be enabled/disabled via
104 ATE1/ATE0, respectively, we rather detect the current
105 configuration of the modem without any change.
106 """
107 # Next command shall not strip the echo from the response
108 self._echo = False
109 result = self.send_at_cmd('AT')
Robert Falkenberg18fb82b2021-05-02 10:21:23 +0200110
Harald Weltec91085e2022-02-10 18:05:45 +0100111 # Verify the response
112 if len(result) > 0:
113 if result[-1] == b'OK':
114 self._echo = False
115 return
116 elif result[-1] == b'AT\r\r\nOK':
117 self._echo = True
118 return
119 raise ReaderError(
120 'Interface \'%s\' does not respond to \'AT\' command' % self._device)
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +0700121
Harald Weltec91085e2022-02-10 18:05:45 +0100122 def reset_card(self):
123 # Reset the modem, just to be sure
124 if self.send_at_cmd('ATZ') != [b'OK']:
125 raise ReaderError('Failed to reset the modem')
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +0700126
Harald Weltec91085e2022-02-10 18:05:45 +0100127 # Make sure that generic SIM access is supported
128 if self.send_at_cmd('AT+CSIM=?') != [b'OK']:
129 raise ReaderError('The modem does not seem to support SIM access')
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +0700130
Harald Weltec91085e2022-02-10 18:05:45 +0100131 log.info('Modem at \'%s\' is ready!' % self._device)
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +0700132
Harald Weltec91085e2022-02-10 18:05:45 +0100133 def connect(self):
134 pass # Nothing to do really ...
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +0700135
Harald Weltec91085e2022-02-10 18:05:45 +0100136 def disconnect(self):
137 pass # Nothing to do really ...
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +0700138
Harald Weltec91085e2022-02-10 18:05:45 +0100139 def wait_for_card(self, timeout=None, newcardonly=False):
140 pass # Nothing to do really ...
Robert Falkenberg8e15c182021-05-01 08:07:27 +0200141
Harald Weltec91085e2022-02-10 18:05:45 +0100142 def _send_apdu_raw(self, pdu):
143 # Make sure pdu has upper case hex digits [A-F]
144 pdu = pdu.upper()
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +0700145
Harald Weltec91085e2022-02-10 18:05:45 +0100146 # Prepare the command as described in 8.17
147 cmd = 'AT+CSIM=%d,\"%s\"' % (len(pdu), pdu)
148 log.debug('Sending command: %s', cmd)
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +0700149
Harald Weltec91085e2022-02-10 18:05:45 +0100150 # Send AT+CSIM command to the modem
Harald Weltec91085e2022-02-10 18:05:45 +0100151 rsp = self.send_at_cmd(cmd)
Tobias Engeld70ac222023-05-29 21:20:59 +0200152 if rsp[-1].startswith(b'+CME ERROR:'):
153 raise ProtocolError('AT+CSIM failed with: %s' % str(rsp))
Harald Weltec91085e2022-02-10 18:05:45 +0100154 if len(rsp) != 2 or rsp[-1] != b'OK':
155 raise ReaderError('APDU transfer failed: %s' % str(rsp))
156 rsp = rsp[0] # Get rid of b'OK'
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +0700157
Harald Weltec91085e2022-02-10 18:05:45 +0100158 # Make sure that the response has format: b'+CSIM: %d,\"%s\"'
159 try:
160 result = re.match(b'\+CSIM: (\d+),\"([0-9A-F]+)\"', rsp)
161 (rsp_pdu_len, rsp_pdu) = result.groups()
162 except:
163 raise ReaderError('Failed to parse response from modem: %s' % rsp)
164
165 # TODO: make sure we have at least SW
166 data = rsp_pdu[:-4].decode().lower()
167 sw = rsp_pdu[-4:].decode().lower()
168 log.debug('Command response: %s, %s', data, sw)
169 return data, sw