mirror of
https://gitea.osmocom.org/sim-card/pysim.git
synced 2026-03-17 02:48:34 +03:00
When we initialize the reader, we currently tell only which type of interface we are using, but we do not print the reader number or the device path. Let's extend the messages so that the path is printed. To prevent problems with integration-tests, let's also add an environment variable that we can use to detect when pySim runs inside a integration-test. Related: OS#6210 Change-Id: Ibe296d51885b1ef5f9c9ecaf1d28da52014dcc4b
189 lines
6.6 KiB
Python
189 lines
6.6 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
# Copyright (C) 2020 Vadim Yanitskiy <axilirator@gmail.com>
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
|
|
import logging as log
|
|
import serial
|
|
import time
|
|
import re
|
|
import argparse
|
|
import os
|
|
from typing import Optional
|
|
|
|
from pySim.utils import Hexstr, ResTuple
|
|
from pySim.transport import LinkBase
|
|
from pySim.exceptions import *
|
|
|
|
# HACK: if somebody needs to debug this thing
|
|
# log.root.setLevel(log.DEBUG)
|
|
|
|
|
|
class ModemATCommandLink(LinkBase):
|
|
"""Transport Link for 3GPP TS 27.007 compliant modems."""
|
|
|
|
def __init__(self, device: str = '/dev/ttyUSB0', baudrate: int = 115200, **kwargs):
|
|
super().__init__(**kwargs)
|
|
if os.environ.get('PYSIM_INTEGRATION_TEST') == "1":
|
|
print("Using modem for Generic SIM Access (3GPP TS 27.007)")
|
|
else:
|
|
print("Using modem for Generic SIM Access (3GPP TS 27.007) at port %s" % device)
|
|
self._sl = serial.Serial(device, baudrate, timeout=5)
|
|
self._echo = False # this will be auto-detected by _check_echo()
|
|
self._device = device
|
|
self._atr = None
|
|
|
|
# Check the AT interface
|
|
self._check_echo()
|
|
|
|
# Trigger initial reset
|
|
self.reset_card()
|
|
|
|
def __del__(self):
|
|
if hasattr(self, '_sl'):
|
|
self._sl.close()
|
|
|
|
def send_at_cmd(self, cmd, timeout=0.2, patience=0.002):
|
|
# Convert from string to bytes, if needed
|
|
bcmd = cmd if type(cmd) is bytes else cmd.encode()
|
|
bcmd += b'\r'
|
|
|
|
# Clean input buffer from previous/unexpected data
|
|
self._sl.reset_input_buffer()
|
|
|
|
# Send command to the modem
|
|
log.debug('Sending AT command: %s', cmd)
|
|
try:
|
|
wlen = self._sl.write(bcmd)
|
|
assert(wlen == len(bcmd))
|
|
except:
|
|
raise ReaderError('Failed to send AT command: %s' % cmd)
|
|
|
|
rsp = b''
|
|
its = 1
|
|
t_start = time.time()
|
|
while True:
|
|
rsp = rsp + self._sl.read(self._sl.in_waiting)
|
|
lines = rsp.split(b'\r\n')
|
|
if len(lines) >= 2:
|
|
res = lines[-2]
|
|
if res == b'OK':
|
|
log.debug('Command finished with result: %s', res)
|
|
break
|
|
if res == b'ERROR' or res.startswith(b'+CME ERROR:'):
|
|
log.error('Command failed with result: %s', res)
|
|
break
|
|
|
|
if time.time() - t_start >= timeout:
|
|
log.info('Command finished with timeout >= %ss', timeout)
|
|
break
|
|
time.sleep(patience)
|
|
its += 1
|
|
log.debug('Command took %0.6fs (%d cycles a %fs)',
|
|
time.time() - t_start, its, patience)
|
|
|
|
if self._echo:
|
|
# Skip echo chars
|
|
rsp = rsp[wlen:]
|
|
rsp = rsp.strip()
|
|
rsp = rsp.split(b'\r\n\r\n')
|
|
|
|
log.debug('Got response from modem: %s', rsp)
|
|
return rsp
|
|
|
|
def _check_echo(self):
|
|
"""Verify the correct response to 'AT' command
|
|
and detect if inputs are echoed by the device
|
|
|
|
Although echo of inputs can be enabled/disabled via
|
|
ATE1/ATE0, respectively, we rather detect the current
|
|
configuration of the modem without any change.
|
|
"""
|
|
# Next command shall not strip the echo from the response
|
|
self._echo = False
|
|
result = self.send_at_cmd('AT')
|
|
|
|
# Verify the response
|
|
if len(result) > 0:
|
|
if result[-1] == b'OK':
|
|
self._echo = False
|
|
return
|
|
elif result[-1] == b'AT\r\r\nOK':
|
|
self._echo = True
|
|
return
|
|
raise ReaderError(
|
|
'Interface \'%s\' does not respond to \'AT\' command' % self._device)
|
|
|
|
def reset_card(self):
|
|
# Reset the modem, just to be sure
|
|
if self.send_at_cmd('ATZ') != [b'OK']:
|
|
raise ReaderError('Failed to reset the modem')
|
|
|
|
# Make sure that generic SIM access is supported
|
|
if self.send_at_cmd('AT+CSIM=?') != [b'OK']:
|
|
raise ReaderError('The modem does not seem to support SIM access')
|
|
|
|
log.info('Modem at \'%s\' is ready!' % self._device)
|
|
|
|
def connect(self):
|
|
pass # Nothing to do really ...
|
|
|
|
def disconnect(self):
|
|
pass # Nothing to do really ...
|
|
|
|
def wait_for_card(self, timeout: Optional[int] = None, newcardonly: bool = False):
|
|
pass # Nothing to do really ...
|
|
|
|
def _send_apdu_raw(self, pdu: Hexstr) -> ResTuple:
|
|
# Make sure pdu has upper case hex digits [A-F]
|
|
pdu = pdu.upper()
|
|
|
|
# Prepare the command as described in 8.17
|
|
cmd = 'AT+CSIM=%d,\"%s\"' % (len(pdu), pdu)
|
|
log.debug('Sending command: %s', cmd)
|
|
|
|
# Send AT+CSIM command to the modem
|
|
rsp = self.send_at_cmd(cmd)
|
|
if rsp[-1].startswith(b'+CME ERROR:'):
|
|
raise ProtocolError('AT+CSIM failed with: %s' % str(rsp))
|
|
if len(rsp) != 2 or rsp[-1] != b'OK':
|
|
raise ReaderError('APDU transfer failed: %s' % str(rsp))
|
|
rsp = rsp[0] # Get rid of b'OK'
|
|
|
|
# Make sure that the response has format: b'+CSIM: %d,\"%s\"'
|
|
try:
|
|
result = re.match(b'\+CSIM: (\d+),\"([0-9A-F]+)\"', rsp)
|
|
(rsp_pdu_len, rsp_pdu) = result.groups()
|
|
except:
|
|
raise ReaderError('Failed to parse response from modem: %s' % rsp)
|
|
|
|
# TODO: make sure we have at least SW
|
|
data = rsp_pdu[:-4].decode().lower()
|
|
sw = rsp_pdu[-4:].decode().lower()
|
|
log.debug('Command response: %s, %s', data, sw)
|
|
return data, sw
|
|
|
|
def __str__(self) -> str:
|
|
return "modem:%s" % self._device
|
|
|
|
@staticmethod
|
|
def argparse_add_reader_args(arg_parser: argparse.ArgumentParser):
|
|
modem_group = arg_parser.add_argument_group('AT Command Modem Reader')
|
|
modem_group.add_argument('--modem-device', dest='modem_dev', metavar='DEV', default=None,
|
|
help='Serial port of modem for Generic SIM Access (3GPP TS 27.007)')
|
|
modem_group.add_argument('--modem-baud', type=int, metavar='BAUD', default=115200,
|
|
help='Baud rate used for modem port')
|