Vadim Yanitskiy | 29ca804 | 2020-05-09 21:23:37 +0700 | [diff] [blame] | 1 | # -*- coding: utf-8 -*- |
| 2 | |
Vadim Yanitskiy | 29ca804 | 2020-05-09 21:23:37 +0700 | [diff] [blame] | 3 | # 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 Yanitskiy | 29ca804 | 2020-05-09 21:23:37 +0700 | [diff] [blame] | 19 | import logging as log |
| 20 | import serial |
| 21 | import time |
| 22 | import re |
| 23 | |
| 24 | from pySim.transport import LinkBase |
| 25 | from pySim.exceptions import * |
| 26 | |
| 27 | # HACK: if somebody needs to debug this thing |
| 28 | # log.root.setLevel(log.DEBUG) |
| 29 | |
| 30 | class ModemATCommandLink(LinkBase): |
Harald Welte | ee3501f | 2021-04-02 13:00:18 +0200 | [diff] [blame] | 31 | """Transport Link for 3GPP TS 27.007 compliant modems.""" |
Harald Welte | eb05b2f | 2021-04-10 11:01:56 +0200 | [diff] [blame] | 32 | def __init__(self, device:str='/dev/ttyUSB0', baudrate:int=115200, **kwargs): |
| 33 | super().__init__(**kwargs) |
Vadim Yanitskiy | 29ca804 | 2020-05-09 21:23:37 +0700 | [diff] [blame] | 34 | self._sl = serial.Serial(device, baudrate, timeout=5) |
| 35 | self._device = device |
| 36 | self._atr = None |
| 37 | |
| 38 | # Trigger initial reset |
| 39 | self.reset_card() |
| 40 | |
| 41 | def __del__(self): |
| 42 | self._sl.close() |
| 43 | |
| 44 | def send_at_cmd(self, cmd): |
| 45 | # Convert from string to bytes, if needed |
| 46 | bcmd = cmd if type(cmd) is bytes else cmd.encode() |
| 47 | bcmd += b'\r' |
| 48 | |
| 49 | # Send command to the modem |
| 50 | log.debug('Sending AT command: %s' % cmd) |
| 51 | try: |
| 52 | wlen = self._sl.write(bcmd) |
| 53 | assert(wlen == len(bcmd)) |
| 54 | except: |
| 55 | raise ReaderError('Failed to send AT command: %s' % cmd) |
| 56 | |
| 57 | # Give the modem some time... |
| 58 | time.sleep(0.3) |
| 59 | |
| 60 | # Read the response |
| 61 | try: |
| 62 | # Skip characters sent back |
| 63 | self._sl.read(wlen) |
| 64 | # Read the rest |
| 65 | rsp = self._sl.read_all() |
| 66 | |
| 67 | # Strip '\r\n' |
| 68 | rsp = rsp.strip() |
| 69 | # Split into a list |
| 70 | rsp = rsp.split(b'\r\n\r\n') |
| 71 | except: |
| 72 | raise ReaderError('Failed parse response to AT command: %s' % cmd) |
| 73 | |
| 74 | log.debug('Got response from modem: %s' % rsp) |
| 75 | return rsp |
| 76 | |
| 77 | def reset_card(self): |
| 78 | # Make sure that we can talk to the modem |
| 79 | if self.send_at_cmd('AT') != [b'OK']: |
| 80 | raise ReaderError('Failed to connect to modem') |
| 81 | |
| 82 | # Reset the modem, just to be sure |
| 83 | if self.send_at_cmd('ATZ') != [b'OK']: |
| 84 | raise ReaderError('Failed to reset the modem') |
| 85 | |
| 86 | # Make sure that generic SIM access is supported |
| 87 | if self.send_at_cmd('AT+CSIM=?') != [b'OK']: |
| 88 | raise ReaderError('The modem does not seem to support SIM access') |
| 89 | |
| 90 | log.info('Modem at \'%s\' is ready!' % self._device) |
| 91 | |
| 92 | def connect(self): |
| 93 | pass # Nothing to do really ... |
| 94 | |
| 95 | def disconnect(self): |
| 96 | pass # Nothing to do really ... |
| 97 | |
| 98 | def wait_for_card(self, timeout=None, newcardonly=False): |
| 99 | pass # Nothing to do really ... |
| 100 | |
Harald Welte | c34f940 | 2021-04-10 10:55:24 +0200 | [diff] [blame] | 101 | def _send_apdu_raw(self, pdu): |
Robert Falkenberg | 8e15c18 | 2021-05-01 08:07:27 +0200 | [diff] [blame] | 102 | # Make sure pdu has upper case hex digits [A-F] |
| 103 | pdu = pdu.upper() |
| 104 | |
Vadim Yanitskiy | 29ca804 | 2020-05-09 21:23:37 +0700 | [diff] [blame] | 105 | # Prepare the command as described in 8.17 |
| 106 | cmd = 'AT+CSIM=%d,\"%s\"' % (len(pdu), pdu) |
| 107 | |
| 108 | # Send AT+CSIM command to the modem |
| 109 | # TODO: also handle +CME ERROR: <err> |
| 110 | rsp = self.send_at_cmd(cmd) |
| 111 | if len(rsp) != 2 or rsp[-1] != b'OK': |
| 112 | raise ReaderError('APDU transfer failed: %s' % str(rsp)) |
| 113 | rsp = rsp[0] # Get rid of b'OK' |
| 114 | |
| 115 | # Make sure that the response has format: b'+CSIM: %d,\"%s\"' |
| 116 | try: |
| 117 | result = re.match(b'\+CSIM: (\d+),\"([0-9A-F]+)\"', rsp) |
| 118 | (rsp_pdu_len, rsp_pdu) = result.groups() |
| 119 | except: |
| 120 | raise ReaderError('Failed to parse response from modem: %s' % rsp) |
| 121 | |
| 122 | # TODO: make sure we have at least SW |
| 123 | data = rsp_pdu[:-4].decode() |
| 124 | sw = rsp_pdu[-4:].decode() |
| 125 | return data, sw |