blob: 2018ac2347e453b52865a0e7383458937bcd27eb [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
30class ModemATCommandLink(LinkBase):
Harald Welteee3501f2021-04-02 13:00:18 +020031 """Transport Link for 3GPP TS 27.007 compliant modems."""
Harald Welteeb05b2f2021-04-10 11:01:56 +020032 def __init__(self, device:str='/dev/ttyUSB0', baudrate:int=115200, **kwargs):
33 super().__init__(**kwargs)
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +070034 self._sl = serial.Serial(device, baudrate, timeout=5)
Robert Falkenberg18fb82b2021-05-02 10:21:23 +020035 self._echo = False # this will be auto-detected by _check_echo()
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +070036 self._device = device
37 self._atr = None
38
Robert Falkenberg18fb82b2021-05-02 10:21:23 +020039 # Check the AT interface
40 self._check_echo()
41
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +070042 # Trigger initial reset
43 self.reset_card()
44
45 def __del__(self):
Vadim Yanitskiy52efc032021-05-02 23:07:38 +020046 if hasattr(self, '_sl'):
47 self._sl.close()
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +070048
Robert Falkenberg18fb82b2021-05-02 10:21:23 +020049 def send_at_cmd(self, cmd, timeout=0.2, patience=0.002):
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +070050 # Convert from string to bytes, if needed
51 bcmd = cmd if type(cmd) is bytes else cmd.encode()
52 bcmd += b'\r'
53
Robert Falkenberg18fb82b2021-05-02 10:21:23 +020054 # Clean input buffer from previous/unexpected data
55 self._sl.reset_input_buffer()
56
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +070057 # Send command to the modem
Robert Falkenberg18fb82b2021-05-02 10:21:23 +020058 log.debug('Sending AT command: %s', cmd)
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +070059 try:
60 wlen = self._sl.write(bcmd)
61 assert(wlen == len(bcmd))
62 except:
63 raise ReaderError('Failed to send AT command: %s' % cmd)
64
Robert Falkenberg18fb82b2021-05-02 10:21:23 +020065 rsp = b''
66 its = 1
67 t_start = time.time()
68 while True:
69 rsp = rsp + self._sl.read(self._sl.in_waiting)
70 if rsp.endswith(b'OK\r\n'):
71 log.debug('Command finished with result: OK')
72 break
73 if rsp.endswith(b'ERROR\r\n'):
74 log.debug('Command finished with result: ERROR')
75 break
76 if time.time() - t_start >= timeout:
77 log.debug('Command finished with timeout >= %ss', timeout)
78 break
79 time.sleep(patience)
80 its += 1
81 log.debug('Command took %0.6fs (%d cycles a %fs)', time.time() - t_start, its, patience)
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +070082
Robert Falkenberg18fb82b2021-05-02 10:21:23 +020083 if self._echo:
84 # Skip echo chars
85 rsp = rsp[wlen:]
86 rsp = rsp.strip()
87 rsp = rsp.split(b'\r\n\r\n')
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +070088
Robert Falkenberg18fb82b2021-05-02 10:21:23 +020089 log.debug('Got response from modem: %s', rsp)
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +070090 return rsp
91
Robert Falkenberg18fb82b2021-05-02 10:21:23 +020092 def _check_echo(self):
93 """Verify the correct response to 'AT' command
94 and detect if inputs are echoed by the device
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +070095
Robert Falkenberg18fb82b2021-05-02 10:21:23 +020096 Although echo of inputs can be enabled/disabled via
97 ATE1/ATE0, respectively, we rather detect the current
98 configuration of the modem without any change.
99 """
100 # Next command shall not strip the echo from the response
101 self._echo = False
102 result = self.send_at_cmd('AT')
103
104 # Verify the response
105 if len(result) > 0:
106 if result[-1] == b'OK':
107 self._echo = False
108 return
109 elif result[-1] == b'AT\r\r\nOK':
110 self._echo = True
111 return
112 raise ReaderError('Interface \'%s\' does not respond to \'AT\' command' % self._device)
113
114 def reset_card(self):
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +0700115 # Reset the modem, just to be sure
116 if self.send_at_cmd('ATZ') != [b'OK']:
117 raise ReaderError('Failed to reset the modem')
118
119 # Make sure that generic SIM access is supported
120 if self.send_at_cmd('AT+CSIM=?') != [b'OK']:
121 raise ReaderError('The modem does not seem to support SIM access')
122
123 log.info('Modem at \'%s\' is ready!' % self._device)
124
125 def connect(self):
126 pass # Nothing to do really ...
127
128 def disconnect(self):
129 pass # Nothing to do really ...
130
131 def wait_for_card(self, timeout=None, newcardonly=False):
132 pass # Nothing to do really ...
133
Harald Weltec34f9402021-04-10 10:55:24 +0200134 def _send_apdu_raw(self, pdu):
Robert Falkenberg8e15c182021-05-01 08:07:27 +0200135 # Make sure pdu has upper case hex digits [A-F]
136 pdu = pdu.upper()
137
Vadim Yanitskiy29ca8042020-05-09 21:23:37 +0700138 # Prepare the command as described in 8.17
139 cmd = 'AT+CSIM=%d,\"%s\"' % (len(pdu), pdu)
140
141 # Send AT+CSIM command to the modem
142 # TODO: also handle +CME ERROR: <err>
143 rsp = self.send_at_cmd(cmd)
144 if len(rsp) != 2 or rsp[-1] != b'OK':
145 raise ReaderError('APDU transfer failed: %s' % str(rsp))
146 rsp = rsp[0] # Get rid of b'OK'
147
148 # Make sure that the response has format: b'+CSIM: %d,\"%s\"'
149 try:
150 result = re.match(b'\+CSIM: (\d+),\"([0-9A-F]+)\"', rsp)
151 (rsp_pdu_len, rsp_pdu) = result.groups()
152 except:
153 raise ReaderError('Failed to parse response from modem: %s' % rsp)
154
155 # TODO: make sure we have at least SW
156 data = rsp_pdu[:-4].decode()
157 sw = rsp_pdu[-4:].decode()
158 return data, sw