├── .gitignore ├── nfc ├── clf.pyc ├── dep.pyc ├── __init__.pyc ├── dev │ ├── pn53x.py │ ├── pn53x.pyc │ ├── rcs380.py │ ├── __init__.pyc │ ├── transport.pyc │ ├── pn531.py │ ├── arygon.py │ ├── rcs956.py │ ├── acr122.py │ ├── __init__.py │ ├── udp.py │ └── transport.py ├── llcp │ ├── err.pyc │ ├── llc.pyc │ ├── opt.pyc │ ├── pdu.pyc │ ├── tco.pyc │ ├── socket.pyc │ ├── __init__.pyc │ ├── opt.py │ ├── __init__.py │ ├── err.py │ └── socket.py ├── tag │ ├── tag.pyc │ ├── tt1.pyc │ ├── tt2.pyc │ ├── tt3.pyc │ ├── tt4.pyc │ ├── __init__.pyc │ ├── __init__.py │ ├── tag.py │ ├── tt4.py │ ├── tt1.py │ ├── tt2.py │ └── tt3.py ├── ndef │ ├── error.pyc │ ├── record.pyc │ ├── __init__.pyc │ ├── bt_record.pyc │ ├── handover.pyc │ ├── message.pyc │ ├── text_record.pyc │ ├── uri_record.pyc │ ├── wifi_record.pyc │ ├── smart_poster.pyc │ ├── error.py │ ├── __init__.py │ ├── uri_record.py │ ├── text_record.py │ ├── message.py │ ├── smart_poster.py │ ├── record.py │ └── wifi_record.py ├── __init__.py ├── handover │ ├── __init__.py │ ├── client.py │ └── server.py └── snep │ ├── __init__.py │ ├── client.py │ └── server.py ├── README.md ├── _common.py └── songblocks.py /.gitignore: -------------------------------------------------------------------------------- 1 | .auth_data.json 2 | _common.pyc 3 | twitter-util.py 4 | -------------------------------------------------------------------------------- /nfc/clf.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnrk/songblocks/HEAD/nfc/clf.pyc -------------------------------------------------------------------------------- /nfc/dep.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnrk/songblocks/HEAD/nfc/dep.pyc -------------------------------------------------------------------------------- /nfc/__init__.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnrk/songblocks/HEAD/nfc/__init__.pyc -------------------------------------------------------------------------------- /nfc/dev/pn53x.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnrk/songblocks/HEAD/nfc/dev/pn53x.py -------------------------------------------------------------------------------- /nfc/dev/pn53x.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnrk/songblocks/HEAD/nfc/dev/pn53x.pyc -------------------------------------------------------------------------------- /nfc/dev/rcs380.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnrk/songblocks/HEAD/nfc/dev/rcs380.py -------------------------------------------------------------------------------- /nfc/llcp/err.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnrk/songblocks/HEAD/nfc/llcp/err.pyc -------------------------------------------------------------------------------- /nfc/llcp/llc.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnrk/songblocks/HEAD/nfc/llcp/llc.pyc -------------------------------------------------------------------------------- /nfc/llcp/opt.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnrk/songblocks/HEAD/nfc/llcp/opt.pyc -------------------------------------------------------------------------------- /nfc/llcp/pdu.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnrk/songblocks/HEAD/nfc/llcp/pdu.pyc -------------------------------------------------------------------------------- /nfc/llcp/tco.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnrk/songblocks/HEAD/nfc/llcp/tco.pyc -------------------------------------------------------------------------------- /nfc/tag/tag.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnrk/songblocks/HEAD/nfc/tag/tag.pyc -------------------------------------------------------------------------------- /nfc/tag/tt1.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnrk/songblocks/HEAD/nfc/tag/tt1.pyc -------------------------------------------------------------------------------- /nfc/tag/tt2.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnrk/songblocks/HEAD/nfc/tag/tt2.pyc -------------------------------------------------------------------------------- /nfc/tag/tt3.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnrk/songblocks/HEAD/nfc/tag/tt3.pyc -------------------------------------------------------------------------------- /nfc/tag/tt4.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnrk/songblocks/HEAD/nfc/tag/tt4.pyc -------------------------------------------------------------------------------- /nfc/llcp/socket.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnrk/songblocks/HEAD/nfc/llcp/socket.pyc -------------------------------------------------------------------------------- /nfc/ndef/error.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnrk/songblocks/HEAD/nfc/ndef/error.pyc -------------------------------------------------------------------------------- /nfc/ndef/record.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnrk/songblocks/HEAD/nfc/ndef/record.pyc -------------------------------------------------------------------------------- /nfc/dev/__init__.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnrk/songblocks/HEAD/nfc/dev/__init__.pyc -------------------------------------------------------------------------------- /nfc/dev/transport.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnrk/songblocks/HEAD/nfc/dev/transport.pyc -------------------------------------------------------------------------------- /nfc/llcp/__init__.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnrk/songblocks/HEAD/nfc/llcp/__init__.pyc -------------------------------------------------------------------------------- /nfc/ndef/__init__.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnrk/songblocks/HEAD/nfc/ndef/__init__.pyc -------------------------------------------------------------------------------- /nfc/ndef/bt_record.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnrk/songblocks/HEAD/nfc/ndef/bt_record.pyc -------------------------------------------------------------------------------- /nfc/ndef/handover.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnrk/songblocks/HEAD/nfc/ndef/handover.pyc -------------------------------------------------------------------------------- /nfc/ndef/message.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnrk/songblocks/HEAD/nfc/ndef/message.pyc -------------------------------------------------------------------------------- /nfc/tag/__init__.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnrk/songblocks/HEAD/nfc/tag/__init__.pyc -------------------------------------------------------------------------------- /nfc/ndef/text_record.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnrk/songblocks/HEAD/nfc/ndef/text_record.pyc -------------------------------------------------------------------------------- /nfc/ndef/uri_record.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnrk/songblocks/HEAD/nfc/ndef/uri_record.pyc -------------------------------------------------------------------------------- /nfc/ndef/wifi_record.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnrk/songblocks/HEAD/nfc/ndef/wifi_record.pyc -------------------------------------------------------------------------------- /nfc/ndef/smart_poster.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawnrk/songblocks/HEAD/nfc/ndef/smart_poster.pyc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | songblocks 2 | ========== 3 | 4 | A python script to remotely control a Sonos music player with NFC tags. For an overview of the full Song Blocks project, see [here](http://shawnrk.github.io/songblocks). 5 | 6 | The main source file is songblocks.py. 7 | 8 | I'm a mediocre developer at best and this is my first Python project. I was more interested in getting it working than doing it right, so I absolutely cut some corners. For instance, part of the nfcpy library is in this repo because I didn't have the patience to figure out how Python's module path stuff worked, so I just put the module I needed in my working directory. Also, _common.py is sourced from the example files in the TweetPony library and used to set up the connection to Twitter. 9 | 10 | Thanks to the developers of the [SoCo](https://github.com/SoCo/SoCo) (Sonos interface), [nfcpy](https://launchpad.net/nfcpy) (NFC interface), and [TweetPony](https://github.com/Mezgrman/TweetPony) (Twitter interface), libraries. All were pretty easy to use for a novice. 11 | -------------------------------------------------------------------------------- /nfc/llcp/opt.py: -------------------------------------------------------------------------------- 1 | # -*- coding: latin-1 -*- 2 | # ----------------------------------------------------------------------------- 3 | # Copyright 2009-2011 Stephen Tiedemann 4 | # 5 | # Licensed under the EUPL, Version 1.1 or - as soon they 6 | # will be approved by the European Commission - subsequent 7 | # versions of the EUPL (the "Licence"); 8 | # You may not use this work except in compliance with the 9 | # Licence. 10 | # You may obtain a copy of the Licence at: 11 | # 12 | # http://www.osor.eu/eupl 13 | # 14 | # Unless required by applicable law or agreed to in 15 | # writing, software distributed under the Licence is 16 | # distributed on an "AS IS" basis, 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 18 | # express or implied. 19 | # See the Licence for the specific language governing 20 | # permissions and limitations under the Licence. 21 | # ----------------------------------------------------------------------------- 22 | 23 | SO_SNDMIU = 1 24 | SO_RCVMIU = 2 25 | SO_SNDBUF = 3 26 | SO_RCVBUF = 4 27 | SO_SNDBSY = 5 28 | SO_RCVBSY = 6 29 | -------------------------------------------------------------------------------- /nfc/tag/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: latin-1 -*- 2 | # ----------------------------------------------------------------------------- 3 | # Copyright 2013 Stephen Tiedemann 4 | # 5 | # Licensed under the EUPL, Version 1.1 or - as soon they 6 | # will be approved by the European Commission - subsequent 7 | # versions of the EUPL (the "Licence"); 8 | # You may not use this work except in compliance with the 9 | # Licence. 10 | # You may obtain a copy of the Licence at: 11 | # 12 | # http://www.osor.eu/eupl 13 | # 14 | # Unless required by applicable law or agreed to in 15 | # writing, software distributed under the Licence is 16 | # distributed on an "AS IS" basis, 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 18 | # express or implied. 19 | # See the Licence for the specific language governing 20 | # permissions and limitations under the Licence. 21 | # ----------------------------------------------------------------------------- 22 | 23 | import logging 24 | log = logging.getLogger(__name__) 25 | 26 | from tag import activate, emulate 27 | 28 | class Error: pass 29 | class AccessError(Error): pass 30 | class CapacityError(Error): pass 31 | -------------------------------------------------------------------------------- /nfc/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: latin-1 -*- 2 | # ----------------------------------------------------------------------------- 3 | # Copyright 2009-2011 Stephen Tiedemann 4 | # 5 | # Licensed under the EUPL, Version 1.1 or - as soon they 6 | # will be approved by the European Commission - subsequent 7 | # versions of the EUPL (the "Licence"); 8 | # You may not use this work except in compliance with the 9 | # Licence. 10 | # You may obtain a copy of the Licence at: 11 | # 12 | # http://www.osor.eu/eupl 13 | # 14 | # Unless required by applicable law or agreed to in 15 | # writing, software distributed under the Licence is 16 | # distributed on an "AS IS" basis, 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 18 | # express or implied. 19 | # See the Licence for the specific language governing 20 | # permissions and limitations under the Licence. 21 | # ----------------------------------------------------------------------------- 22 | __version__ = "1.0 dev" 23 | 24 | import logging 25 | logging.getLogger(__name__).addHandler(logging.NullHandler()) 26 | logging.getLogger(__name__).setLevel(logging.INFO) 27 | 28 | from clf import ContactlessFrontend 29 | -------------------------------------------------------------------------------- /nfc/handover/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: latin-1 -*- 2 | # ----------------------------------------------------------------------------- 3 | # Copyright 2012 Stephen Tiedemann 4 | # 5 | # Licensed under the EUPL, Version 1.1 or - as soon they 6 | # will be approved by the European Commission - subsequent 7 | # versions of the EUPL (the "Licence"); 8 | # You may not use this work except in compliance with the 9 | # Licence. 10 | # You may obtain a copy of the Licence at: 11 | # 12 | # http://www.osor.eu/eupl 13 | # 14 | # Unless required by applicable law or agreed to in 15 | # writing, software distributed under the Licence is 16 | # distributed on an "AS IS" basis, 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 18 | # express or implied. 19 | # See the Licence for the specific language governing 20 | # permissions and limitations under the Licence. 21 | # ----------------------------------------------------------------------------- 22 | 23 | """ 24 | The nfc.handover module implements the NFC Forum Connection Handover 25 | 1.2 protocol as a server and client class that simplify realization of 26 | handover selector and requester functionality. 27 | 28 | """ 29 | 30 | from nfc.handover.server import HandoverServer 31 | from nfc.handover.client import HandoverClient 32 | -------------------------------------------------------------------------------- /nfc/ndef/error.py: -------------------------------------------------------------------------------- 1 | # -*- coding: latin-1 -*- 2 | # ----------------------------------------------------------------------------- 3 | # Copyright 2012 Stephen Tiedemann 4 | # 5 | # Licensed under the EUPL, Version 1.1 or - as soon they 6 | # will be approved by the European Commission - subsequent 7 | # versions of the EUPL (the "Licence"); 8 | # You may not use this work except in compliance with the 9 | # Licence. 10 | # You may obtain a copy of the Licence at: 11 | # 12 | # http://www.osor.eu/eupl 13 | # 14 | # Unless required by applicable law or agreed to in 15 | # writing, software distributed under the Licence is 16 | # distributed on an "AS IS" basis, 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 18 | # express or implied. 19 | # See the Licence for the specific language governing 20 | # permissions and limitations under the Licence. 21 | # ----------------------------------------------------------------------------- 22 | 23 | class FormatError(Exception): 24 | """NDEF Record structure error""" 25 | pass 26 | 27 | class LengthError(Exception): 28 | """NDEF structure length error""" 29 | pass 30 | 31 | class DecodeError(Exception): 32 | """NDEF payload decode error""" 33 | pass 34 | 35 | class EncodeError(Exception): 36 | """NDEF payload encode error""" 37 | pass 38 | 39 | parser_error = (FormatError, LengthError, DecodeError) 40 | -------------------------------------------------------------------------------- /nfc/llcp/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: latin-1 -*- 2 | # ----------------------------------------------------------------------------- 3 | # Copyright 2009-2013 Stephen Tiedemann 4 | # 5 | # Licensed under the EUPL, Version 1.1 or - as soon they 6 | # will be approved by the European Commission - subsequent 7 | # versions of the EUPL (the "Licence"); 8 | # You may not use this work except in compliance with the 9 | # Licence. 10 | # You may obtain a copy of the Licence at: 11 | # 12 | # http://www.osor.eu/eupl 13 | # 14 | # Unless required by applicable law or agreed to in 15 | # writing, software distributed under the Licence is 16 | # distributed on an "AS IS" basis, 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 18 | # express or implied. 19 | # See the Licence for the specific language governing 20 | # permissions and limitations under the Licence. 21 | # ----------------------------------------------------------------------------- 22 | 23 | """ 24 | The nfc.llcp module implements the NFC Forum Logical Link Control 25 | Protocol (LLCP) specification and provides a socket interface to use 26 | the connection-less and connection-mode transport facilities of LLCP. 27 | """ 28 | 29 | import logging 30 | log = logging.getLogger(__name__) 31 | 32 | from socket import Socket 33 | from llc import LOGICAL_DATA_LINK, DATA_LINK_CONNECTION 34 | from err import * 35 | from opt import * 36 | 37 | -------------------------------------------------------------------------------- /nfc/snep/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: latin-1 -*- 2 | # ----------------------------------------------------------------------------- 3 | # Copyright 2009-2011 Stephen Tiedemann 4 | # 5 | # Licensed under the EUPL, Version 1.1 or - as soon they 6 | # will be approved by the European Commission - subsequent 7 | # versions of the EUPL (the "Licence"); 8 | # You may not use this work except in compliance with the 9 | # Licence. 10 | # You may obtain a copy of the Licence at: 11 | # 12 | # http://www.osor.eu/eupl 13 | # 14 | # Unless required by applicable law or agreed to in 15 | # writing, software distributed under the Licence is 16 | # distributed on an "AS IS" basis, 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 18 | # express or implied. 19 | # See the Licence for the specific language governing 20 | # permissions and limitations under the Licence. 21 | # ----------------------------------------------------------------------------- 22 | 23 | """ 24 | The nfc.snep module implements the NFC Forum Simple NDEF Exchange 25 | Protocol (SNEP) specification and provides a server and client class 26 | for applications to easily send or receive SNEP messages. 27 | """ 28 | 29 | from nfc.snep.server import SnepServer 30 | from nfc.snep.client import SnepClient 31 | from nfc.snep.client import SnepError 32 | 33 | Success = 0x81 34 | NotFound = 0xC0 35 | ExcessData = 0xC1 36 | BadRequest = 0xC2 37 | NotImplemented = 0xE0 38 | UnsupportedVersion = 0xE1 39 | -------------------------------------------------------------------------------- /nfc/llcp/err.py: -------------------------------------------------------------------------------- 1 | # -*- coding: latin-1 -*- 2 | # ----------------------------------------------------------------------------- 3 | # Copyright 2009-2011 Stephen Tiedemann 4 | # 5 | # Licensed under the EUPL, Version 1.1 or - as soon they 6 | # will be approved by the European Commission - subsequent 7 | # versions of the EUPL (the "Licence"); 8 | # You may not use this work except in compliance with the 9 | # Licence. 10 | # You may obtain a copy of the Licence at: 11 | # 12 | # http://www.osor.eu/eupl 13 | # 14 | # Unless required by applicable law or agreed to in 15 | # writing, software distributed under the Licence is 16 | # distributed on an "AS IS" basis, 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 18 | # express or implied. 19 | # See the Licence for the specific language governing 20 | # permissions and limitations under the Licence. 21 | # ----------------------------------------------------------------------------- 22 | 23 | from os import strerror 24 | import errno 25 | 26 | class Error(IOError): 27 | def __init__(self, errno): 28 | super(Error, self).__init__(errno, strerror(errno)) 29 | 30 | def __str__(self): 31 | return "nfc.llcp.Error: [{0}] {1}".format( 32 | errno.errorcode[self.errno], self.strerror) 33 | 34 | class ConnectRefused(Error): 35 | def __init__(self, reason): 36 | super(ConnectRefused, self).__init__(errno.ECONNREFUSED) 37 | self.reason = reason 38 | 39 | def __str__(self): 40 | return "nfc.llcp.ConnectRefused: [{0}] {1} with reason {2}".format( 41 | errno.errorcode[self.errno], self.strerror, self.reason) 42 | 43 | 44 | -------------------------------------------------------------------------------- /nfc/ndef/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: latin-1 -*- 2 | # ----------------------------------------------------------------------------- 3 | # Copyright 2009-2012 Stephen Tiedemann 4 | # 5 | # Licensed under the EUPL, Version 1.1 or - as soon they 6 | # will be approved by the European Commission - subsequent 7 | # versions of the EUPL (the "Licence"); 8 | # You may not use this work except in compliance with the 9 | # Licence. 10 | # You may obtain a copy of the Licence at: 11 | # 12 | # http://www.osor.eu/eupl 13 | # 14 | # Unless required by applicable law or agreed to in 15 | # writing, software distributed under the Licence is 16 | # distributed on an "AS IS" basis, 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 18 | # express or implied. 19 | # See the Licence for the specific language governing 20 | # permissions and limitations under the Licence. 21 | # ----------------------------------------------------------------------------- 22 | # 23 | # NFC Data Exchange Format (NDEF) package 24 | # 25 | """ 26 | Support for decoding and encoding of NFC Data Exchange Format (NDEF) 27 | records and messages. 28 | """ 29 | 30 | from nfc.ndef.error import * 31 | from nfc.ndef.message import Message 32 | from nfc.ndef.record import Record 33 | from nfc.ndef.text_record import TextRecord 34 | from nfc.ndef.uri_record import UriRecord 35 | from nfc.ndef.smart_poster import SmartPosterRecord 36 | from nfc.ndef.handover import HandoverRequestMessage 37 | from nfc.ndef.handover import HandoverSelectMessage 38 | from nfc.ndef.handover import HandoverCarrierRecord 39 | from nfc.ndef.bt_record import BluetoothConfigRecord 40 | from nfc.ndef.wifi_record import WifiConfigRecord 41 | from nfc.ndef.wifi_record import WifiPasswordRecord 42 | -------------------------------------------------------------------------------- /_common.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013 Julian Metzler 2 | # See the LICENSE file for the full license. 3 | 4 | """ 5 | This file contains functions used by more than one example script. 6 | """ 7 | 8 | import json 9 | import os 10 | import tweetpony 11 | 12 | def authenticate(): 13 | try: 14 | api = tweetpony.API(tweetpony.CONSUMER_KEY, tweetpony.CONSUMER_SECRET) 15 | url = api.get_auth_url() 16 | print "Visit this URL to obtain your verification code: %s" % url 17 | verifier = raw_input("Input your code: ") 18 | api.authenticate(verifier) 19 | except tweetpony.APIError as err: 20 | print "Oh no! You could not be authenticated. Twitter returned error #%i and said: %s" % (err.code, err.description) 21 | else: 22 | auth_data = {'access_token': api.access_token, 'access_token_secret': api.access_token_secret} 23 | with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), ".auth_data.json"), 'w') as f: 24 | f.write(json.dumps(auth_data)) 25 | print "Hello, @%s! You have been authenticated. You can now run the other example scripts without having to authenticate every time." % api.user.screen_name 26 | 27 | def get_api(): 28 | if not os.path.exists(os.path.join(os.path.dirname(os.path.realpath(__file__)), ".auth_data.json")): 29 | authenticate() 30 | with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), ".auth_data.json"), 'r') as f: 31 | auth_data = json.loads(f.read()) 32 | try: 33 | api = tweetpony.API(tweetpony.CONSUMER_KEY, tweetpony.CONSUMER_SECRET, auth_data['access_token'], auth_data['access_token_secret']) 34 | except tweetpony.APIError as err: 35 | print "Oh no! You could not be authenticated. Twitter returned error #%i and said: %s" % (err.code, err.description) 36 | else: 37 | return api 38 | return False 39 | -------------------------------------------------------------------------------- /nfc/dev/pn531.py: -------------------------------------------------------------------------------- 1 | # -*- coding: latin-1 -*- 2 | # ----------------------------------------------------------------------------- 3 | # Copyright 2011-2013 Stephen Tiedemann 4 | # 5 | # Licensed under the EUPL, Version 1.1 or - as soon they 6 | # will be approved by the European Commission - subsequent 7 | # versions of the EUPL (the "Licence"); 8 | # You may not use this work except in compliance with the 9 | # Licence. 10 | # You may obtain a copy of the Licence at: 11 | # 12 | # http://www.osor.eu/eupl 13 | # 14 | # Unless required by applicable law or agreed to in 15 | # writing, software distributed under the Licence is 16 | # distributed on an "AS IS" basis, 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 18 | # express or implied. 19 | # See the Licence for the specific language governing 20 | # permissions and limitations under the Licence. 21 | # ----------------------------------------------------------------------------- 22 | # 23 | # Driver for the a NXP PN531 based contactless reader 24 | # 25 | import logging 26 | log = logging.getLogger(__name__) 27 | 28 | import time 29 | import pn53x 30 | from pn53x import ChipsetError 31 | 32 | class Chipset(pn53x.Chipset): 33 | pass 34 | 35 | class Device(pn53x.Device): 36 | def __init__(self, bus): 37 | super(Device, self).__init__(bus) 38 | 39 | def close(self): 40 | self.chipset.close() 41 | 42 | def listen_dep(self, target, timeout): 43 | # PN531 screws up in target mode, only one run is successful. 44 | # Thereafter the USB descriptor and whatever else is broken. 45 | log.warning("listen mode is disabled for this device") 46 | time.sleep(timeout) 47 | return None 48 | 49 | def init(transport): 50 | chipset = Chipset(transport) 51 | device = Device(chipset) 52 | device._vendor_name = transport.manufacturer_name 53 | device._device_name = transport.product_name 54 | return device 55 | -------------------------------------------------------------------------------- /nfc/tag/tag.py: -------------------------------------------------------------------------------- 1 | # -*- coding: latin-1 -*- 2 | # ----------------------------------------------------------------------------- 3 | # Copyright 2009-2013 Stephen Tiedemann 4 | # 5 | # Licensed under the EUPL, Version 1.1 or - as soon they 6 | # will be approved by the European Commission - subsequent 7 | # versions of the EUPL (the "Licence"); 8 | # You may not use this work except in compliance with the 9 | # Licence. 10 | # You may obtain a copy of the Licence at: 11 | # 12 | # http://www.osor.eu/eupl 13 | # 14 | # Unless required by applicable law or agreed to in 15 | # writing, software distributed under the Licence is 16 | # distributed on an "AS IS" basis, 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 18 | # express or implied. 19 | # See the Licence for the specific language governing 20 | # permissions and limitations under the Licence. 21 | # ----------------------------------------------------------------------------- 22 | 23 | import logging 24 | log = logging.getLogger(__name__) 25 | 26 | import nfc.clf 27 | from tt1 import Type1Tag 28 | from tt2 import Type2Tag 29 | from tt3 import Type3Tag, Type3TagEmulation 30 | from tt4 import Type4Tag 31 | 32 | def activate(clf, target): 33 | try: 34 | if type(target) == nfc.clf.TTA: 35 | if target.cfg[0] & 0x1F == 0 and target.cfg[1] & 0x0F == 0x0C: 36 | return Type1Tag(clf, target) 37 | if len(target.cfg) == 3: 38 | if target.cfg[2] & 0x64 == 0x00: 39 | return Type2Tag(clf, target) 40 | if target.cfg[2] & 0x24 == 0x20: 41 | return Type4Tag(clf, target) 42 | elif type(target) == nfc.clf.TTB: 43 | return Type2Tag(clf, target) 44 | elif type(target) == nfc.clf.TTF: 45 | return Type3Tag(clf, target) 46 | except nfc.clf.DigitalProtocolError: 47 | return None 48 | 49 | def emulate(clf, target): 50 | if type(target) == nfc.clf.TTA: 51 | log.debug("can't emulate TTA target'") 52 | elif type(target) == nfc.clf.TTB: 53 | log.debug("can't emulate TTB target'") 54 | elif type(target) == nfc.clf.TTF: 55 | return Type3TagEmulation(clf, target) 56 | -------------------------------------------------------------------------------- /nfc/dev/arygon.py: -------------------------------------------------------------------------------- 1 | # -*- coding: latin-1 -*- 2 | # ----------------------------------------------------------------------------- 3 | # Copyright 2009-2013 Stephen Tiedemann 4 | # 5 | # Licensed under the EUPL, Version 1.1 or - as soon they 6 | # will be approved by the European Commission - subsequent 7 | # versions of the EUPL (the "Licence"); 8 | # You may not use this work except in compliance with the 9 | # Licence. 10 | # You may obtain a copy of the Licence at: 11 | # 12 | # http://www.osor.eu/eupl 13 | # 14 | # Unless required by applicable law or agreed to in 15 | # writing, software distributed under the Licence is 16 | # distributed on an "AS IS" basis, 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 18 | # express or implied. 19 | # See the Licence for the specific language governing 20 | # permissions and limitations under the Licence. 21 | # ----------------------------------------------------------------------------- 22 | # 23 | # Driver for the Arygon contactless reader with USB serial interface 24 | # 25 | import logging 26 | log = logging.getLogger(__name__) 27 | 28 | import os 29 | import sys 30 | import time 31 | import errno 32 | 33 | import pn531 34 | from pn531 import ChipsetError 35 | 36 | class Chipset(pn531.Chipset): 37 | def __init__(self, transport): 38 | self.transport = transport 39 | for speed in (230400, 9600, 19200, 38400, 57600, 115200): 40 | log.debug("try serial baud rate {0} kbps".format(speed)) 41 | self.transport.tty = self.transport.serial.Serial( 42 | self.transport.tty.port, baudrate=speed, timeout=0.1) 43 | log.debug("read arygon firmware version") 44 | self.transport.tty.write("0av") 45 | version = self.transport.tty.readline() 46 | if version.startswith("FF0000"): 47 | log.debug("Arygon Reader {0}".format(version.strip()[-4:])) 48 | self.transport.tty.timeout = 1.0 49 | self.transport.tty.writeTimeout = 1.0 50 | log.debug("set mcu-tama speed to 230.4 kbps") 51 | self.transport.tty.write("0at05") 52 | if self.transport.tty.readline().strip() != "FF000000": 53 | log.debug("failed to set mcu-tama speed") 54 | break 55 | if self.transport.tty.baudrate != 230400: 56 | log.debug("set mcu-host speed to 230.4 kbps") 57 | self.transport.tty.write("0ah05") 58 | if self.transport.tty.readline().strip() != "FF000000": 59 | log.debug("failed to set mcu-host speed") 60 | break 61 | time.sleep(0.5) 62 | self.transport.tty.close() 63 | self.transport.tty = self.transport.serial.Serial( 64 | self.transport.tty.port, baudrate=230400, 65 | timeout=1.0, writeTimeout=1.0) 66 | return super(Chipset, self).__init__(transport) 67 | raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) 68 | 69 | def close(self): 70 | self.transport.tty.write("0ar") 71 | self.transport.tty.readline() 72 | self.transport.close() 73 | self.transport = None 74 | 75 | def write_frame(self, frame): 76 | self.transport.write("2" + frame) 77 | 78 | def read_frame(self, timeout): 79 | return self.transport.read(timeout) 80 | 81 | class Device(pn531.Device): 82 | pass 83 | 84 | def init(transport): 85 | chipset = Chipset(transport) 86 | device = Device(chipset) 87 | 88 | device._vendor_name = "Arygon" 89 | device._device_name = "APPx-ADRx" 90 | 91 | return device 92 | -------------------------------------------------------------------------------- /nfc/handover/client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: latin-1 -*- 2 | # ----------------------------------------------------------------------------- 3 | # Copyright 2009-2011 Stephen Tiedemann 4 | # 5 | # Licensed under the EUPL, Version 1.1 or - as soon they 6 | # will be approved by the European Commission - subsequent 7 | # versions of the EUPL (the "Licence"); 8 | # You may not use this work except in compliance with the 9 | # Licence. 10 | # You may obtain a copy of the Licence at: 11 | # 12 | # http://www.osor.eu/eupl 13 | # 14 | # Unless required by applicable law or agreed to in 15 | # writing, software distributed under the Licence is 16 | # distributed on an "AS IS" basis, 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 18 | # express or implied. 19 | # See the Licence for the specific language governing 20 | # permissions and limitations under the Licence. 21 | # ----------------------------------------------------------------------------- 22 | # 23 | # Negotiated Connection Handover - Client Base Class 24 | # 25 | import logging 26 | log = logging.getLogger(__name__) 27 | 28 | import nfc.llcp 29 | import time 30 | 31 | class HandoverClient(object): 32 | """ NFC Forum Connection Handover client 33 | """ 34 | def __init__(self, llc): 35 | self.socket = None 36 | self.llc = llc 37 | 38 | def connect(self, recv_miu=248, recv_buf=2): 39 | """Connect to the remote handover server if available. Raises 40 | :exc:`nfc.llcp.ConnectRefused` if the remote device does not 41 | have a handover service or the service does not accept any 42 | more connections.""" 43 | socket = nfc.llcp.Socket(self.llc, nfc.llcp.DATA_LINK_CONNECTION) 44 | socket.setsockopt(nfc.llcp.SO_RCVBUF, recv_buf) 45 | socket.setsockopt(nfc.llcp.SO_RCVMIU, recv_miu) 46 | socket.connect("urn:nfc:sn:handover") 47 | server = socket.getpeername() 48 | log.debug("handover client connected to remote sap {0}".format(server)) 49 | self.socket = socket 50 | 51 | def close(self): 52 | """Disconnect from the remote handover server.""" 53 | if self.socket: 54 | self.socket.close() 55 | self.socket = None 56 | 57 | def send(self, message): 58 | """Send a handover request message to the remote server.""" 59 | log.debug("sending '{0}' message".format(message.type)) 60 | send_miu = self.socket.getsockopt(nfc.llcp.SO_SNDMIU) 61 | try: 62 | data = str(message) 63 | except nfc.llcp.EncodeError as e: 64 | log.error("message encoding failed: {0}".format(e)) 65 | else: 66 | return self._send(data, send_miu) 67 | 68 | def _send(self, data, miu): 69 | while len(data) > 0: 70 | if self.socket.send(data[0:miu]): 71 | data = data[miu:] 72 | else: break 73 | return bool(len(data) == 0) 74 | 75 | def recv(self, timeout=None): 76 | """Receive a handover select message from the remote server.""" 77 | message = self._recv(timeout) 78 | if message and message.type == "urn:nfc:wkt:Hs": 79 | log.debug("received '{0}' message".format(message.type)) 80 | return nfc.ndef.HandoverSelectMessage(message) 81 | else: 82 | log.error("received invalid message type {0}".format(message.type)) 83 | return None 84 | 85 | def _recv(self, timeout=None): 86 | data = '' 87 | started = time.time() 88 | while self.socket.poll("recv", timeout): 89 | try: 90 | data += self.socket.recv() 91 | message = nfc.ndef.Message(data) 92 | log.debug("received message\n" + message.pretty()) 93 | return message 94 | except nfc.ndef.LengthError: 95 | elapsed = time.time() - started 96 | log.debug("message is incomplete ({0} byte)".format(len(data))) 97 | if timeout: 98 | timeout = timeout - elapsed 99 | log.debug("{0:.3f} seconds left to timeout".format(timeout)) 100 | continue # incomplete message 101 | except TypeError: 102 | log.debug("data link connection closed") 103 | break # recv() returned None 104 | 105 | def __enter__(self): 106 | self.connect() 107 | return self 108 | 109 | def __exit__(self, exc_type, exc_value, traceback): 110 | self.close() 111 | 112 | -------------------------------------------------------------------------------- /nfc/dev/rcs956.py: -------------------------------------------------------------------------------- 1 | # -*- coding: latin-1 -*- 2 | # ----------------------------------------------------------------------------- 3 | # Copyright 2009-2013 Stephen Tiedemann 4 | # 5 | # Licensed under the EUPL, Version 1.1 or - as soon they 6 | # will be approved by the European Commission - subsequent 7 | # versions of the EUPL (the "Licence"); 8 | # You may not use this work except in compliance with the 9 | # Licence. 10 | # You may obtain a copy of the Licence at: 11 | # 12 | # http://www.osor.eu/eupl 13 | # 14 | # Unless required by applicable law or agreed to in 15 | # writing, software distributed under the Licence is 16 | # distributed on an "AS IS" basis, 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 18 | # express or implied. 19 | # See the Licence for the specific language governing 20 | # permissions and limitations under the Licence. 21 | # ----------------------------------------------------------------------------- 22 | # 23 | # Device driver for Sony RC-S330/360/370 contactless reader 24 | # 25 | import logging 26 | log = logging.getLogger(__name__) 27 | 28 | import time 29 | import struct 30 | 31 | import pn53x 32 | import nfc.clf 33 | 34 | class ChipsetError(pn53x.ChipsetError): 35 | pass 36 | 37 | class Chipset(pn53x.Chipset): 38 | ACK = pn53x.Chipset.ACK 39 | 40 | def __init__(self, transport): 41 | super(Chipset, self).__init__(transport) 42 | self.CMD[0x18] = "ResetCommand" 43 | self.reset_mode() 44 | 45 | def diagnose(self, test, test_data=None): 46 | if test == "line": 47 | if test_data is None: test_data = "" 48 | data = self.command(0x00, chr(0) + test_data) 49 | if data is None: raise ChipsetError(data) 50 | return data == test_data 51 | raise ValueError("unknown diagnose test {0!r}".format(test)) 52 | 53 | def reset_mode(self): 54 | self.command(0x18, [1]) 55 | self.transport.write(Chipset.ACK) 56 | time.sleep(0.010) 57 | 58 | def read_register(self, addr): 59 | if type(addr) is int: addr = [addr] 60 | addr = ''.join([struct.pack(">H", x) for x in addr]) 61 | return self.command(0x06, addr) 62 | 63 | def in_data_exchange_tt3(self, data, timeout, *args, **kwargs): 64 | data = self.command(0x42, data, timeout) 65 | if data is None or data[0] != 0: 66 | raise ChipsetError(data) 67 | return data[1:], False 68 | 69 | class Device(pn53x.Device): 70 | def __init__(self, chipset): 71 | super(Device, self).__init__(chipset) 72 | 73 | #cfg_data = chr(self._rwt) + chr(self._wtx) + "\x08" 74 | #self.dev.rf_configuration(0x82, cfg_data) 75 | #self.dev.command(0x08, "\x63\x0d\x00") 76 | #regs = self.dev.read_register(range(0xa01b, 0xa023)) 77 | #self.dev.rf_configuration(0x0b, regs) 78 | 79 | def close(self): 80 | self.chipset.reset_mode() 81 | super(Device, self).close() 82 | 83 | def sense(self, *args, **kwargs): 84 | # RC-S956 requires to use InCommunicateThru for TT3 card commands 85 | self.chipset.reset_mode() 86 | target = super(Device, self).sense(*args, **kwargs) 87 | self.chipset.in_data_exchange = \ 88 | self.chipset.in_data_exchange_tt3 if type(target) is nfc.clf.TTF \ 89 | else super(Chipset, self.chipset).in_data_exchange 90 | return target 91 | 92 | def listen_dep(self, *args, **kwargs): 93 | # RS-S956 firmware bug requires SENS_RES="0101". This is currently 94 | # used in pn53x.py, make sure that keeps or implement it here. 95 | self.chipset.reset_mode() 96 | return super(Device, self).listen_dep(*args, **kwargs) 97 | 98 | def dep_get_data(self, timeout): 99 | if self.dev.get_general_status()[4] == 4: 100 | # except first time, initiator cmd is received in set data 101 | timeout = 100 102 | return super(Device, self).dep_get_data(timeout) 103 | 104 | def dep_set_data(self, data, timeout): 105 | return super(Device, self).dep_set_data(data, timeout) 106 | 107 | def init(transport): 108 | # write ack to perform a soft reset 109 | # raises IOError(EACCES) if we're second 110 | transport.write(Chipset.ACK) 111 | 112 | chipset = Chipset(transport) 113 | device = Device(chipset) 114 | 115 | device._vendor_name = transport.manufacturer_name 116 | device._device_name = transport.product_name 117 | if device._device_name is None: 118 | device._device_name = "RC-S330" 119 | 120 | return device 121 | -------------------------------------------------------------------------------- /songblocks.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import print_function 3 | from soco import SoCo 4 | from time import sleep 5 | import time 6 | import nfc 7 | from _common import get_api 8 | import tweetpony 9 | 10 | # this function gets called when a NFC tag is detected 11 | def touched(tag): 12 | currentHour = time.localtime()[3] 13 | isNight = 0 14 | if currentHour < daytimeRange[0] or currentHour > daytimeRange[1]: # is it nighttime? 15 | isNight = 1 16 | 17 | tag_uid = str(tag.uid).encode("hex") # get the UID of the touched tag 18 | 19 | #Look up the song to play and set the right volume depending on whether it's day or night 20 | if tag_uid in songs: 21 | print("Tag touched: #" + songs[tag_uid][0] + ", UID: " + tag_uid) 22 | print (" Song: " + songs[tag_uid][1]) 23 | if isNight: 24 | vol = int(round(nightVol * songs[tag_uid][2],0)) 25 | sonos.volume = vol 26 | print (" Nighttime song volume is " + str(vol)) 27 | else: 28 | vol = int(round(dayVol * songs[tag_uid][2],0)) 29 | sonos.volume = vol 30 | print (" Daytime song volume is " + str(vol)) 31 | 32 | sonos.play_uri(songs[tag_uid][4]) # play the song 33 | print (" Playing...") 34 | 35 | # if the song has a time offset, skip to it. 36 | if songs[tag_uid][3]: 37 | sonos.seek(songs[tag_uid][3]) 38 | print (" Skipped to " + songs[tag_uid][3]) 39 | else: 40 | print (" No record for tag UID: " + tag_uid) 41 | 42 | # Tweet the song 43 | tweet = songs[tag_uid][1] + time.strftime("\n%b %d %Y %H:%M:%S", time.localtime()) 44 | try: 45 | status = twitter_api.update_status(status = tweet) 46 | except tweetpony.APIError as err: 47 | print (" Tweet failed: ", err.description) 48 | else: 49 | print (" Tweet sent") 50 | 51 | return True 52 | 53 | 54 | # Constants 55 | dayVol = 50 # default daytime volume 56 | nightVol = 25 # default nighttime volume 57 | daytimeRange = [7,17] # daytime is 7:00a to 5:59p 58 | sonos_ip = '10.0.1.98' 59 | url = 'x-sonos-http:_t%3a%3a17790141.mp3?sid=11&flags=32' #default url 60 | time_offset = '' #time offset (to skip song intros) 61 | 62 | songs = { 63 | # block_number, title, vol % (for normalization), time_offset (to skip intros, 'HH:MM:SS'), url 64 | '04436522c52980' : ['1','Paul Simon: Diamonds on the Soles of Her Shoes',1,time_offset,'x-sonos-http:_t%3a%3a17790141.mp3?sid=11&flags=32'], 65 | '04926422c52980' : ['2','Nina Simone: To Love Somebody',1,time_offset,'x-sonos-http:_t%3a%3a2995780.mp3?sid=11&flags=32'], 66 | '04dd3a22c52980' : ['3','Bob Marley: One Cup of Coffee',0.8,time_offset,'x-sonos-http:_t%3a%3a39299573.mp3?sid=11&flags=32'], 67 | '04b56322c52980' : ['4','Adele: Best for Last',0.8,'00:00:36','x-sonos-http:_t%3a%3a2893061.mp3?sid=11&flags=32'], 68 | '04e46422c52980' : ['5','Jack Johnson: We are Going to Be Friends',0.8,time_offset,'x-sonos-http:_t%3a%3a2807102%3a%3aa%3a%3a231444.mp3?sid=11&flags=32'], 69 | '048b6322c52980' : ['6','The Hollies: The Mighty Quinn',0.8,time_offset,'x-sonos-http:_t%3a%3a2568562.mp3?sid=11&flags=32'], 70 | '04216522c52980' : ['7','Raffi: Baa Baa Black Sheep',1.15,time_offset,'x-sonos-http:_t%3a%3a5425710%3a%3aa%3a%3a441322.mp3?sid=11&flags=32'], 71 | '04df3a22c52980' : ['8','K\'naan: In the Beginning',0.75,'00:00:13','x-sonos-http:_t%3a%3a5407313.mp3?sid=11&flags=32'], 72 | '04cb6422c52980' : ['9','Tuck & Patti: Honey Pie',1.3,'00:00:41','x-sonos-http:_t%3a%3a3053744.mp3?sid=11&flags=32'], 73 | '04436422c52980' : ['10','Miriam Makeba: Pata Pata',0.9,'00:00:09','x-sonos-http:_t%3a%3a1163595.mp3?sid=11&flags=32'], 74 | '049e6422c52980' : ['11','Yo-Yo Ma & Bobby McFerrin: Flight of the Bumblebee',1.2,time_offset,'x-sonos-http:_t%3a%3a8805968.mp3?sid=11&flags=32'], 75 | '04536422c52980' : ['12','Desmond De Silva: Babi Achchee',0.8,time_offset,'x-sonos-http:_t%3a%3a43013728.mp3?sid=11&flags=32'], 76 | } 77 | 78 | # Twitter setup 79 | print("Connecting to Twitter...") 80 | twitter_api = get_api() 81 | if twitter_api: 82 | print ("Connected to Twitter") 83 | else: 84 | print ("Twitter connection error") 85 | print ("") 86 | 87 | 88 | # Sonos setup 89 | print("Connecting to Sonos...") 90 | sonos = SoCo(sonos_ip) 91 | print ("Connected to Sonos: " + sonos.player_name) 92 | 93 | # Use this section to get the URIs of new songs we want to add 94 | info = sonos.get_current_track_info() 95 | print("Currently Playing: " + info['title']) 96 | print("URI: " + info['uri']) 97 | print("---") 98 | print("") 99 | 100 | 101 | print("Setting up reader...") 102 | reader = nfc.ContactlessFrontend('tty:AMA0:pn53x') 103 | print(reader) 104 | print("Ready!") 105 | print("") 106 | 107 | while True: 108 | reader.connect(rdwr={'on-connect': touched}) 109 | print("Tag released") 110 | sonos.stop() 111 | print ("Sonos stopped") 112 | print ("---") 113 | print ("") 114 | sleep(0.1); 115 | -------------------------------------------------------------------------------- /nfc/ndef/uri_record.py: -------------------------------------------------------------------------------- 1 | # -*- coding: latin-1 -*- 2 | # ----------------------------------------------------------------------------- 3 | # Copyright 2009-2012 Stephen Tiedemann 4 | # 5 | # Licensed under the EUPL, Version 1.1 or - as soon they 6 | # will be approved by the European Commission - subsequent 7 | # versions of the EUPL (the "Licence"); 8 | # You may not use this work except in compliance with the 9 | # Licence. 10 | # You may obtain a copy of the Licence at: 11 | # 12 | # http://www.osor.eu/eupl 13 | # 14 | # Unless required by applicable law or agreed to in 15 | # writing, software distributed under the Licence is 16 | # distributed on an "AS IS" basis, 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 18 | # express or implied. 19 | # See the Licence for the specific language governing 20 | # permissions and limitations under the Licence. 21 | # ----------------------------------------------------------------------------- 22 | # 23 | # uri_record.py -- NDEF URI Record 24 | # 25 | import logging 26 | log = logging.getLogger(__name__) 27 | 28 | from record import Record 29 | 30 | class UriRecord(Record): 31 | """Wraps an NDEF URI record and provides access to the :attr:`uri` 32 | content. The URI RTD specification defines the payload of the URI 33 | record as a URI identifier code byte followed by a URI string. The 34 | URI identifier code provides one byte code points for 35 | abbreviations of commonly used URI protocol names. The 36 | :class:`UriRecord` class handles abbreviations transparently by 37 | expanding and compressing when decoding and encoding. 38 | 39 | :param uri: URI string or :class:`nfc.ndef.Record` object 40 | 41 | The `uri` argument may alternatively supply an instance of class 42 | :class:`nfc.ndef.Record`. Initialization is then done by parsing 43 | the record payload. If the record type does not match 44 | 'urn:nfc:wkt:U' a :exc:`ValueError` exception is raised. 45 | 46 | >>> nfc.ndef.UriRecord(nfc.ndef.Record()) 47 | >>> nfc.ndef.UriRecord("http://nfcpy.org") 48 | """ 49 | 50 | def __init__(self, uri=None): 51 | super(UriRecord, self).__init__('urn:nfc:wkt:U') 52 | if isinstance(uri, Record): 53 | record = uri 54 | if record.type == self.type: 55 | self.name = record.name 56 | self.data = record.data 57 | else: 58 | raise ValueError("record type mismatch") 59 | else: 60 | self.uri = uri if uri else '' 61 | 62 | def __repr__(self): 63 | s = "nfc.ndef.UriRecord(uri='{0}')" 64 | return s.format(self.uri) 65 | 66 | @property 67 | def data(self): 68 | for i, p in enumerate(protocol_strings): 69 | if i > 0 and self.uri.startswith(p): 70 | return chr(i) + self.uri[len(p):] 71 | else: 72 | return "\x00" + self.uri 73 | 74 | @data.setter 75 | def data(self, string): 76 | log.debug("decode uri record " + repr(string)) 77 | if len(string) > 0: 78 | p = min(ord(string[0]), len(protocol_strings)-1) 79 | self.uri = protocol_strings[p] + string[1:] 80 | else: log.error("nothing to parse") 81 | 82 | @property 83 | def uri(self): 84 | """The URI string, including any abbreviation that is possibly 85 | available. A :exc:`ValueError` exception is raised if the 86 | string contains non ascii characters.""" 87 | return self._uri 88 | 89 | @uri.setter 90 | def uri(self, value): 91 | try: 92 | self._uri = value.encode("ascii") 93 | except UnicodeDecodeError: 94 | raise ValueError("uri value must be an ascii string") 95 | except AttributeError: 96 | raise TypeError("uri value must be a str type") 97 | 98 | def pretty(self, indent=0): 99 | lines = list() 100 | if self.name: 101 | lines.append(("identifier", repr(self.name))) 102 | lines.append(("resource", self.uri)) 103 | 104 | indent = indent * ' ' 105 | lwidth = max([len(line[0]) for line in lines]) 106 | lines = [line[0].ljust(lwidth) + " = " + line[1] for line in lines] 107 | return ("\n").join([indent + line for line in lines]) 108 | 109 | protocol_strings = ( 110 | "", 111 | "http://www.", 112 | "https://www.", 113 | "http://", 114 | "https://", 115 | "tel:", 116 | "mailto:", 117 | "ftp://anonymous:anonymous@", 118 | "ftp://ftp.", 119 | "ftps://", 120 | "sftp://", 121 | "smb://", 122 | "nfs://", 123 | "ftp://", 124 | "dav://", 125 | "news:", 126 | "telnet://", 127 | "imap:", 128 | "rtsp://", 129 | "urn:", 130 | "pop:", 131 | "sip:", 132 | "sips:", 133 | "tftp:", 134 | "btspp://", 135 | "btl2cap://", 136 | "btgoep://", 137 | "tcpobex://", 138 | "irdaobex://", 139 | "file://", 140 | "urn:epc:id:", 141 | "urn:epc:tag:", 142 | "urn:epc:pat:", 143 | "urn:epc:raw:", 144 | "urn:epc:", 145 | "urn:nfc:", 146 | "RFU:" 147 | ) 148 | 149 | -------------------------------------------------------------------------------- /nfc/dev/acr122.py: -------------------------------------------------------------------------------- 1 | # -*- coding: latin-1 -*- 2 | # ----------------------------------------------------------------------------- 3 | # Copyright 2011-2013 Stephen Tiedemann 4 | # 5 | # Licensed under the EUPL, Version 1.1 or - as soon they 6 | # will be approved by the European Commission - subsequent 7 | # versions of the EUPL (the "Licence"); 8 | # You may not use this work except in compliance with the 9 | # Licence. 10 | # You may obtain a copy of the Licence at: 11 | # 12 | # http://www.osor.eu/eupl 13 | # 14 | # Unless required by applicable law or agreed to in 15 | # writing, software distributed under the Licence is 16 | # distributed on an "AS IS" basis, 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 18 | # express or implied. 19 | # See the Licence for the specific language governing 20 | # permissions and limitations under the Licence. 21 | # ----------------------------------------------------------------------------- 22 | # 23 | # Driver for the Arygon ACR122U contactless reader 24 | # 25 | import logging 26 | log = logging.getLogger(__name__) 27 | 28 | import os 29 | import time 30 | import errno 31 | import struct 32 | 33 | import pn53x 34 | 35 | class Chipset(pn53x.Chipset): 36 | def __init__(self, transport): 37 | self.transport = transport 38 | 39 | # read ACR122U firmware version string 40 | reader_version = self.ccid_xfr_block(bytearray.fromhex("FF00480000")) 41 | if not reader_version.startswith("ACR122U"): 42 | log.error("failed to retrieve ACR122U version string") 43 | raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) 44 | 45 | if int(chr(reader_version[7])) < 2: 46 | log.error("{0} not supported, need 2.xx".format(frame[10:])) 47 | raise IOError(errno.ENODEV, os.strerror(errno.ENODEV)) 48 | 49 | log.debug("initialize " + str(reader_version)) 50 | 51 | # set icc power on 52 | log.debug("CCID ICC-POWER-ON") 53 | frame = bytearray.fromhex("62000000000000000000") 54 | transport.write(frame); transport.read(100) 55 | 56 | # disable autodetection 57 | log.debug("Set PICC Operating Parameters") 58 | self.ccid_xfr_block(bytearray.fromhex("FF00517F00")) 59 | 60 | # switch red/green led off/on 61 | log.debug("Configure Buzzer and LED") 62 | self.ccid_xfr_block(bytearray.fromhex("FF00400E0400000000")) 63 | 64 | super(Chipset, self).__init__(transport) 65 | 66 | def close(self): 67 | self.ccid_xfr_block(bytearray.fromhex("FF00400C0400000000")) 68 | self.transport.close() 69 | self.transport = None 70 | 71 | def ccid_xfr_block(self, data, timeout=100): 72 | frame = struct.pack(" unusable 116 | log.warning("listen mode is disabled for this device") 117 | time.sleep(timeout) 118 | return None 119 | 120 | def init(transport): 121 | chipset = Chipset(transport) 122 | device = Device(chipset) 123 | device._vendor_name = transport.manufacturer_name 124 | device._device_name = transport.product_name 125 | return device 126 | -------------------------------------------------------------------------------- /nfc/handover/server.py: -------------------------------------------------------------------------------- 1 | # -*- coding: latin-1 -*- 2 | # ----------------------------------------------------------------------------- 3 | # Copyright 2009-2011 Stephen Tiedemann 4 | # 5 | # Licensed under the EUPL, Version 1.1 or - as soon they 6 | # will be approved by the European Commission - subsequent 7 | # versions of the EUPL (the "Licence"); 8 | # You may not use this work except in compliance with the 9 | # Licence. 10 | # You may obtain a copy of the Licence at: 11 | # 12 | # http://www.osor.eu/eupl 13 | # 14 | # Unless required by applicable law or agreed to in 15 | # writing, software distributed under the Licence is 16 | # distributed on an "AS IS" basis, 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 18 | # express or implied. 19 | # See the Licence for the specific language governing 20 | # permissions and limitations under the Licence. 21 | # ----------------------------------------------------------------------------- 22 | # 23 | # Negotiated Connection Handover - Server Base Class 24 | # 25 | import logging 26 | log = logging.getLogger(__name__) 27 | 28 | from threading import Thread 29 | import nfc.llcp 30 | 31 | class HandoverServer(Thread): 32 | """ NFC Forum Connection Handover server 33 | """ 34 | def __init__(self, llc, request_size_limit=0x10000, 35 | recv_miu=1984, recv_buf=15): 36 | socket = nfc.llcp.Socket(llc, nfc.llcp.DATA_LINK_CONNECTION) 37 | recv_miu = socket.setsockopt(nfc.llcp.SO_RCVMIU, recv_miu) 38 | recv_buf = socket.setsockopt(nfc.llcp.SO_RCVBUF, recv_buf) 39 | socket.bind('urn:nfc:sn:handover') 40 | log.info("handover server bound to port {0} (MIU={1}, RW={2})" 41 | .format(socket.getsockname(), recv_miu, recv_buf)) 42 | socket.listen(backlog=2) 43 | Thread.__init__(self, name='urn:nfc:sn:handover', 44 | target=self.listen, args=(llc, socket)) 45 | 46 | def listen(self, llc, socket): 47 | log.debug("handover listen thread started") 48 | try: 49 | while True: 50 | client_socket = socket.accept() 51 | client_thread = Thread(target=HandoverServer.serve, 52 | args=(client_socket, self)) 53 | client_thread.start() 54 | except nfc.llcp.Error as e: 55 | (log.debug if e.errno == nfc.llcp.errno.EPIPE else log.error)(e) 56 | finally: 57 | socket.close() 58 | log.debug("handover listen thread terminated") 59 | 60 | @staticmethod 61 | def serve(socket, handover_server): 62 | peer_sap = socket.getpeername() 63 | log.info("serving handover client on remote sap {0}".format(peer_sap)) 64 | send_miu = socket.getsockopt(nfc.llcp.SO_SNDMIU) 65 | try: 66 | while True: 67 | request_data = '' 68 | while socket.poll("recv"): 69 | data = socket.recv() 70 | if data is not None: 71 | request_data += data 72 | try: 73 | request = nfc.ndef.Message(request_data) 74 | break # message complete 75 | except nfc.ndef.LengthError: 76 | continue # need more data 77 | else: return # connection closed 78 | else: return # connection closed 79 | 80 | log.debug("<<< {0!r}".format(request_data)) 81 | response = handover_server._process_request(request) 82 | response_data = str(response) 83 | log.debug(">>> {0!r}".format(response_data)) 84 | 85 | while len(response_data) > 0: 86 | if socket.send(response_data[0:send_miu]): 87 | response_data = response_data[send_miu:] 88 | else: 89 | return # connection closed 90 | except nfc.llcp.Error as e: 91 | (log.debug if e.errno == nfc.llcp.errno.EPIPE else log.error)(e) 92 | finally: 93 | socket.close() 94 | log.debug("handover serve thread terminated") 95 | 96 | def _process_request(self, request): 97 | log.debug("rcvd handover request {0}\n{1}" 98 | .format(request.type, request.pretty())) 99 | response = nfc.ndef.Message("\xd1\x02\x01Hs\x12") 100 | if not request.type == 'urn:nfc:wkt:Hr': 101 | log.error("received message which is not a handover request") 102 | else: 103 | try: 104 | request = nfc.ndef.HandoverRequestMessage(request) 105 | except nfc.ndef.DecodeError as e: 106 | log.error("error decoding 'Hr' message: {0}".format(e)) 107 | else: 108 | response = self.process_request(request) 109 | log.debug("send handover response {0}\n{1}" 110 | .format(response.type, response.pretty())) 111 | return response 112 | 113 | def process_request(self, request): 114 | """Process a handover request message. The *request* argument 115 | is a :class:`nfc.ndef.HandoverRequestMessage` object. The 116 | return value must be a :class:`nfc.ndef.HandoverSelectMessage` 117 | object to be sent back to the client. 118 | 119 | This method should be overwritten by a subclass of 120 | :class:`HandoverServer` to customize it's behavior. The 121 | default implementation returns a version ``1.2`` 122 | :class:`nfc.ndef.HandoverSelectMessage` with no carriers. 123 | """ 124 | log.warning("default process_request method should be overwritten") 125 | return nfc.ndef.HandoverSelectMessage(version="1.2") 126 | 127 | -------------------------------------------------------------------------------- /nfc/ndef/text_record.py: -------------------------------------------------------------------------------- 1 | # -*- coding: latin-1 -*- 2 | # ----------------------------------------------------------------------------- 3 | # Copyright 2009-2012 Stephen Tiedemann 4 | # 5 | # Licensed under the EUPL, Version 1.1 or - as soon they 6 | # will be approved by the European Commission - subsequent 7 | # versions of the EUPL (the "Licence"); 8 | # You may not use this work except in compliance with the 9 | # Licence. 10 | # You may obtain a copy of the Licence at: 11 | # 12 | # http://www.osor.eu/eupl 13 | # 14 | # Unless required by applicable law or agreed to in 15 | # writing, software distributed under the Licence is 16 | # distributed on an "AS IS" basis, 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 18 | # express or implied. 19 | # See the Licence for the specific language governing 20 | # permissions and limitations under the Licence. 21 | # ----------------------------------------------------------------------------- 22 | # 23 | # text_record.py -- NDEF text record 24 | # 25 | import logging 26 | log = logging.getLogger(__name__) 27 | 28 | from record import Record 29 | 30 | class TextRecord(Record): 31 | """Wraps an NDEF Text record and provides access to the 32 | :attr:`encoding`, :attr:`language` and actual :attr:`text` 33 | content. 34 | 35 | :param text: Text string or :class:`nfc.ndef.Record` object 36 | :param language: ISO/IANA language code string 37 | :param encoding: Text encoding in binary NDEF 38 | 39 | The `text` argument may alternatively supply an instance of class 40 | :class:`nfc.ndef.Record`. Initialization is then done by parsing 41 | the record payload. If the record type does not match 42 | 'urn:nfc:wkt:T' a :exc:`ValueError` exception is raised. 43 | 44 | >>> nfc.ndef.TextRecord(nfc.ndef.Record()) 45 | >>> nfc.ndef.TextRecord("English UTF-8 encoded") 46 | >>> nfc.ndef.TextRecord("Deutsch UTF-8", language="de") 47 | >>> nfc.ndef.TextRecord("English UTF-16", encoding="UTF-16") 48 | """ 49 | 50 | def __init__(self, text=None, language='en', encoding='UTF-8'): 51 | super(TextRecord, self).__init__('urn:nfc:wkt:T') 52 | if isinstance(text, Record): 53 | record = text 54 | if record.type == self.type: 55 | self.name = record.name 56 | self.data = record.data 57 | else: 58 | raise ValueError("record type mismatch") 59 | else: 60 | self.text = text if text else '' 61 | self.language = language 62 | self.encoding = encoding 63 | 64 | def __repr__(self): 65 | s = "nfc.ndef.TextRecord(text='{0}', language='{1}', encoding='{2}')" 66 | return s.format(self.text, self.language, self.encoding) 67 | 68 | @property 69 | def data(self): 70 | sb = chr(len(self.language) | ((self.encoding == "UTF-16") << 7)) 71 | return sb + self.language + self._text.encode(self.encoding) 72 | 73 | @data.setter 74 | def data(self, string): 75 | log.debug("decode text record " + repr(string)) 76 | if len(string) > 0: 77 | status_byte = ord(string[0]) 78 | if status_byte & 0x40: 79 | log.warning("bit 6 of status byte is not zero") 80 | if status_byte & 0x3F == 0: 81 | log.warning("language code length is zero") 82 | if status_byte & 0x3F >= len(string): 83 | log.error("language code length exceeds payload") 84 | self._utfx = "UTF-16" if status_byte >> 7 else "UTF-8" 85 | self._lang = string[1:1+(status_byte & 0x3F)] 86 | self._text = string[1+len(self._lang):].decode(self._utfx) 87 | else: log.error("nothing to parse") 88 | 89 | @property 90 | def text(self): 91 | """The text content. A unicode string that specifies the TEXT 92 | record text field. Coerced into unicode when set.""" 93 | return self._text 94 | 95 | @text.setter 96 | def text(self, value): 97 | self._text = unicode(value) 98 | 99 | @property 100 | def language(self): 101 | """The text language. A string that specifies the ISO/IANA 102 | language code coded into the TEXT record. The value is not 103 | verified except that a :exc:`ValueError` exception is raised 104 | if the assigned value string exceeds 64 characters.""" 105 | return self._lang 106 | 107 | @language.setter 108 | def language(self, value): 109 | if not isinstance(value, str): 110 | raise TypeError("language must be specified as string") 111 | if len(value) > 64: 112 | raise ValueError('maximum string length is 64') 113 | self._lang = value.encode('ascii') 114 | 115 | @property 116 | def encoding(self): 117 | """The text encoding, given as a string. May be 'UTF-8' or 118 | 'UTF-16'. A :exc:`ValueError` exception is raised for 119 | anythinge else.""" 120 | return self._utfx 121 | 122 | @encoding.setter 123 | def encoding(self, value): 124 | if not value in ("UTF-8", "UTF-16"): 125 | raise ValueError('value not in ("UTF-8", "UTF-16")') 126 | self._utfx = value 127 | 128 | def pretty(self, indent=0): 129 | lines = list() 130 | if self.name: 131 | lines.append(("identifier", repr(self.name))) 132 | lines.append(("text", self.text)) 133 | lines.append(("language", self.language)) 134 | lines.append(("encoding", self.encoding)) 135 | 136 | indent = indent * ' ' 137 | lwidth = max([len(line[0]) for line in lines]) 138 | lines = [line[0].ljust(lwidth) + " = " + line[1] for line in lines] 139 | return ("\n").join([indent + line for line in lines]) 140 | -------------------------------------------------------------------------------- /nfc/snep/client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: latin-1 -*- 2 | # ----------------------------------------------------------------------------- 3 | # Copyright 2009-2011 Stephen Tiedemann 4 | # 5 | # Licensed under the EUPL, Version 1.1 or - as soon they 6 | # will be approved by the European Commission - subsequent 7 | # versions of the EUPL (the "Licence"); 8 | # You may not use this work except in compliance with the 9 | # Licence. 10 | # You may obtain a copy of the Licence at: 11 | # 12 | # http://www.osor.eu/eupl 13 | # 14 | # Unless required by applicable law or agreed to in 15 | # writing, software distributed under the Licence is 16 | # distributed on an "AS IS" basis, 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 18 | # express or implied. 19 | # See the Licence for the specific language governing 20 | # permissions and limitations under the Licence. 21 | # ----------------------------------------------------------------------------- 22 | # 23 | # Simple NDEF Exchange Protocol (SNEP) - Client Base Class 24 | # 25 | import logging 26 | log = logging.getLogger(__name__) 27 | 28 | import struct 29 | import nfc.llcp 30 | 31 | def send_request(socket, snep_request, send_miu): 32 | if len(snep_request) <= send_miu: 33 | return socket.send(snep_request) 34 | 35 | if not socket.send(snep_request[0:send_miu]): 36 | return False 37 | 38 | if socket.recv() != "\x10\x80\x00\x00\x00\x00": 39 | return False 40 | 41 | for offset in xrange(send_miu, len(snep_request), send_miu): 42 | fragment = snep_request[offset:offset+send_miu] 43 | if not socket.send(fragment): 44 | return False 45 | 46 | return True 47 | 48 | def recv_response(socket, acceptable_length, timeout): 49 | if socket.poll("recv", timeout): 50 | snep_response = socket.recv() 51 | if len(snep_response) < 6: 52 | log.debug("snep response initial fragment too short") 53 | return None 54 | version, status, length = struct.unpack(">BBL", snep_response[:6]) 55 | if length > acceptable_length: 56 | log.debug("snep response exceeds acceptable length") 57 | return None 58 | if len(snep_response) - 6 < length: 59 | # request remaining fragments 60 | socket.send("\x10\x00\x00\x00\x00\x00") 61 | while len(snep_response) - 6 < length: 62 | if socket.poll("recv", timeout): 63 | snep_response += socket.recv() 64 | else: return None 65 | return snep_response 66 | 67 | class SnepClient(object): 68 | """ Simple NDEF exchange protocol - client implementation 69 | """ 70 | def __init__(self, llc, max_ndef_msg_recv_size=1024): 71 | self.acceptable_length = max_ndef_msg_recv_size 72 | self.socket = None 73 | self.llc = llc 74 | 75 | def connect(self, service_name): 76 | """Connect to a SNEP server. This needs only be called to 77 | connect to a server other than the Default SNEP Server at 78 | `urn:nfc:sn:snep` or if the client wants to send multiple 79 | requests with a single connection. 80 | """ 81 | self.close() 82 | self.socket = nfc.llcp.Socket(self.llc, nfc.llcp.DATA_LINK_CONNECTION) 83 | self.socket.connect(service_name) 84 | self.send_miu = self.socket.getsockopt(nfc.llcp.SO_SNDMIU) 85 | 86 | def close(self): 87 | """Close the data link connection with the SNEP server. 88 | """ 89 | if self.socket: 90 | self.socket.close() 91 | self.socket = None 92 | 93 | def get(self, ndef_message=None, timeout=1.0): 94 | """Get an NDEF message from the server. Temporarily connects 95 | to the default SNEP server if the client is not yet connected. 96 | """ 97 | if ndef_message is None: 98 | ndef_message = nfc.ndef.Message(nfc.ndef.Record()) 99 | ndef_message_data = self._get(ndef_message, timeout) 100 | try: 101 | return nfc.ndef.Message(ndef_message_data) 102 | except Exception as err: 103 | log.error(repr(err)) 104 | 105 | def _get(self, ndef_message, timeout=1.0): 106 | """Get an NDEF message from the server. Temporarily connects 107 | to the default SNEP server if the client is not yet connected. 108 | """ 109 | if not self.socket: 110 | self.connect('urn:nfc:sn:snep') 111 | self.release_connection = True 112 | else: 113 | self.release_connection = False 114 | try: 115 | snep_request = '\x10\x01' 116 | snep_request += struct.pack('>L', 4 + len(str(ndef_message))) 117 | snep_request += struct.pack('>L', self.acceptable_length) 118 | snep_request += str(ndef_message) 119 | if send_request(self.socket, snep_request, self.send_miu): 120 | snep_response = recv_response( 121 | self.socket, self.acceptable_length, timeout) 122 | if snep_response is not None: 123 | response_code = ord(snep_response[1]) 124 | if response_code != 0x81: 125 | raise SnepError(response_code) 126 | return snep_response[6:] 127 | finally: 128 | if self.release_connection: 129 | self.close() 130 | 131 | def put(self, ndef_message, timeout=1.0): 132 | """Send an NDEF message to the server. Temporarily connects to 133 | the default SNEP server if the client is not yet connected. 134 | """ 135 | if not self.socket: 136 | self.connect('urn:nfc:sn:snep') 137 | self.release_connection = True 138 | else: 139 | self.release_connection = False 140 | try: 141 | ndef_msgsize = struct.pack('>L', len(str(ndef_message))) 142 | snep_request = '\x10\x02' + ndef_msgsize + str(ndef_message) 143 | if send_request(self.socket, snep_request, self.send_miu): 144 | snep_response = recv_response(self.socket, 0, timeout) 145 | if snep_response is not None: 146 | response_code = ord(snep_response[1]) 147 | if response_code == 0x81: 148 | return True 149 | else: 150 | raise SnepError(response_code) 151 | return False 152 | finally: 153 | if self.release_connection: 154 | self.close() 155 | 156 | def __enter__(self): 157 | self.connect() 158 | return self 159 | 160 | def __exit__(self, exc_type, exc_value, traceback): 161 | self.close() 162 | 163 | class SnepError(Exception): 164 | strerr = {0xC0: "resource not found", 165 | 0xC1: "resource exceeds data size limit", 166 | 0xC2: "malformed request not understood", 167 | 0xE0: "unsupported functionality requested", 168 | 0xE1: "unsupported protocol version"} 169 | 170 | def __init__(self, err): 171 | self.args = (err, SnepError.strerr.get(err, "")) 172 | 173 | def __str__(self): 174 | return "nfc.snep.SnepError: [{errno}] {info}".format( 175 | errno=self.args[0], info=self.args[1]) 176 | 177 | @property 178 | def errno(self): 179 | return self.args[0] 180 | -------------------------------------------------------------------------------- /nfc/llcp/socket.py: -------------------------------------------------------------------------------- 1 | # -*- coding: latin-1 -*- 2 | # ----------------------------------------------------------------------------- 3 | # Copyright 2009-2013 Stephen Tiedemann 4 | # 5 | # Licensed under the EUPL, Version 1.1 or - as soon they 6 | # will be approved by the European Commission - subsequent 7 | # versions of the EUPL (the "Licence"); 8 | # You may not use this work except in compliance with the 9 | # Licence. 10 | # You may obtain a copy of the Licence at: 11 | # 12 | # http://www.osor.eu/eupl 13 | # 14 | # Unless required by applicable law or agreed to in 15 | # writing, software distributed under the Licence is 16 | # distributed on an "AS IS" basis, 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 18 | # express or implied. 19 | # See the Licence for the specific language governing 20 | # permissions and limitations under the Licence. 21 | # ----------------------------------------------------------------------------- 22 | 23 | import logging 24 | log = logging.getLogger(__name__) 25 | 26 | class Socket(object): 27 | """ 28 | Create a new LLCP socket with the given socket type. The 29 | socket type should be one of: 30 | 31 | * :const:`nfc.llcp.LOGICAL_DATA_LINK` for best-effort 32 | communication using LLCP connection-less PDU exchange 33 | 34 | * :const:`nfc.llcp.DATA_LINK_CONNECTION` for reliable 35 | communication using LLCP connection-mode PDU exchange 36 | 37 | * :const:`nfc.llcp.llc.RAW_ACCESS_POINT` for unregulated LLCP PDU 38 | exchange (useful to implement test programs) 39 | """ 40 | def __init__(self, llc, sock_type): 41 | self._tco = None if sock_type is None else llc.socket(sock_type) 42 | self._llc = llc 43 | 44 | @property 45 | def llc(self): 46 | """The :class:`~nfc.llcp..llc.LogicalLinkController` instance 47 | to which this socket belongs. This attribute is read-only.""" 48 | return self._llc 49 | 50 | def resolve(self, name): 51 | """Resolve a service name into an address. This may involve 52 | conversation with the remote service discovery component if 53 | the name is hasn't yet been resolved. The return value is the 54 | service access point address that the service name is bound to 55 | at the remote device. A zero address indicates that the remote 56 | device does not know about the service name requested. The 57 | return value is None if communication with the peer device got 58 | terminated.""" 59 | return self.llc.resolve(name) 60 | 61 | def setsockopt(self, option, value): 62 | """Set the value of the given socket option and return the 63 | current value which may have been corrected if it was out of 64 | bounds.""" 65 | return self.llc.setsockopt(self._tco, option, value) 66 | 67 | def getsockopt(self, option): 68 | """Return the value of the given socket option.""" 69 | return self.llc.getsockopt(self._tco, option) 70 | 71 | def bind(self, address=None): 72 | """Bind the socket to address. The socket must not already be 73 | bound. The address may be a service name string, a service 74 | access point number, or it may be omitted. If address is a 75 | well-known service name the socket will be bound to the 76 | corresponding service access point address, otherwise the 77 | socket will be bound to the next available service access 78 | point address between 16 and 31 (inclusively). If address is a 79 | number between 32 and 63 (inclusively) the socket will be 80 | bound to that service access point address. If the address 81 | argument is omitted the socket will be bound to the next 82 | available service access point address between 32 and 63.""" 83 | return self.llc.bind(self._tco, address) 84 | 85 | def connect(self, address): 86 | """Connect to a remote socket at address. Address may be a 87 | service name string or a service access point number.""" 88 | return self.llc.connect(self._tco, address) 89 | 90 | def listen(self, backlog): 91 | """Mark a socket as a socket that will be used to accept 92 | incoming connection requests using accept(). The *backlog* 93 | defines the maximum length to which the queue of pending 94 | connections for the socket may grow. A backlog of zero 95 | disables queuing of connection requests. 96 | """ 97 | return self.llc.listen(self._tco, backlog) 98 | 99 | def accept(self): 100 | """Accept a connection. The socket must be bound to an address 101 | and listening for connections. The return value is a new 102 | socket object usable to send and receive data on the 103 | connection.""" 104 | socket = Socket(self._llc, None) 105 | socket._tco = self.llc.accept(self._tco) 106 | return socket 107 | 108 | def send(self, string): 109 | """Send data to the socket. The socket must be connected to a 110 | remote socket. Returns a boolean value that indicates success 111 | or failure. Failure to send is generally an indication that 112 | the socket or connection was closed.""" 113 | return self.llc.send(self._tco, string) 114 | 115 | def sendto(self, string, address): 116 | """Send data to the socket. The socket should not be connected 117 | to a remote socket, since the destination socket is specified 118 | by address. Returns a boolean value that indicates success 119 | or failure. Failure to send is generally an indication that 120 | the socket was closed.""" 121 | return self.llc.sendto(self._tco, string, address) 122 | 123 | def recv(self): 124 | """Receive data from the socket. The return value is a string 125 | representing the data received. The maximum amount of data 126 | that may be returned is determined by the link or connection 127 | maximum information unit size.""" 128 | return self.llc.recv(self._tco) 129 | 130 | def recvfrom(self): 131 | """Receive data from the socket. The return value is a pair 132 | (string, address) where string is a string representing the 133 | data received and address is the address of the socket sending 134 | the data.""" 135 | return self.llc.recvfrom(self._tco) 136 | 137 | def poll(self, event, timeout=None): 138 | """Wait for a socket event.""" 139 | return self.llc.poll(self._tco, event, timeout) 140 | 141 | def getsockname(self): 142 | """Obtain the address to which the socket is bound. For an 143 | unbound socket the returned value is None. 144 | """ 145 | return self.llc.getsockname(self._tco) 146 | 147 | def getpeername(self): 148 | """Obtain the address of the peer connected on the socket. For 149 | an unconnected socket the returned value is None. 150 | """ 151 | return self.llc.getpeername(self._tco) 152 | 153 | def close(self): 154 | """Close the socket. All future operations on the socket 155 | object will fail. The remote end will receive no more data 156 | Sockets are automatically closed when the logical link 157 | controller terminates (gracefully or by link disruption). A 158 | connection-mode socket will attempt to disconnect the data 159 | link connection (if in connected state).""" 160 | return self.llc.close(self._tco) 161 | -------------------------------------------------------------------------------- /nfc/ndef/message.py: -------------------------------------------------------------------------------- 1 | # -*- coding: latin-1 -*- 2 | # ----------------------------------------------------------------------------- 3 | # Copyright 2009-2012 Stephen Tiedemann 4 | # 5 | # Licensed under the EUPL, Version 1.1 or - as soon they 6 | # will be approved by the European Commission - subsequent 7 | # versions of the EUPL (the "Licence"); 8 | # You may not use this work except in compliance with the 9 | # Licence. 10 | # You may obtain a copy of the Licence at: 11 | # 12 | # http://www.osor.eu/eupl 13 | # 14 | # Unless required by applicable law or agreed to in 15 | # writing, software distributed under the Licence is 16 | # distributed on an "AS IS" basis, 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 18 | # express or implied. 19 | # See the Licence for the specific language governing 20 | # permissions and limitations under the Licence. 21 | # ----------------------------------------------------------------------------- 22 | # 23 | # message.py -- NDEF message handling 24 | # 25 | import logging 26 | log = logging.getLogger(__name__) 27 | 28 | import io 29 | import copy 30 | import nfc.ndef 31 | 32 | class Message(object): 33 | """Wraps a sequence of NDEF records and provides methods for 34 | appending, inserting and indexing. Instantiation accepts a 35 | variable number of positional arguments. A call without argument 36 | produces a Message object with no records. A single str or 37 | bytearray argument is parsed as NDEF message bytes. A single list 38 | or tuple of :class:`nfc.ndef.Record` objects produces a Message 39 | with those records in order. One or more :class:`nfc.ndef.Record` 40 | arguments produce a Message with those records in order. 41 | 42 | >>> nfc.ndef.Message(b'\\x10\\x00\\x00') # NDEF data bytes 43 | >>> nfc.ndef.Message(bytearray([16,0,0])) # NDEF data bytes 44 | >>> nfc.ndef.Message([record1, record2]) # list of records 45 | >>> nfc.ndef.Message(record1, record2) # two record args 46 | """ 47 | 48 | def __init__(self, *args): 49 | self._records = list() 50 | if len(args) == 1: 51 | if isinstance(args[0], io.BytesIO): 52 | self._read(args[0]) 53 | elif isinstance(args[0], (str, bytearray)): 54 | self._read(io.BytesIO(args[0])) 55 | elif isinstance(args[0], nfc.ndef.Record): 56 | self.append(args[0]) 57 | elif isinstance(args[0], (list, tuple)): 58 | self.extend(args[0]) 59 | else: raise TypeError("invalid argument type") 60 | elif len(args) > 1: 61 | self.extend(args) 62 | 63 | def _read(self, f): 64 | log.debug("parse ndef message at offset {0}".format(f.tell())) 65 | record = nfc.ndef.Record(data=f) 66 | if record._message_begin == False: 67 | log.error("message begin flag not set at begin of ndef") 68 | raise nfc.ndef.FormatError("message begin flag not set") 69 | self._records.append(record) 70 | while self._records[-1]._message_end == False: 71 | self._records.append(nfc.ndef.Record(data=f)) 72 | log.debug("ndef message complete at offset {0}".format(f.tell())) 73 | 74 | def _write(self, f): 75 | if len(self._records) > 0: 76 | for record in self._records: 77 | record._message_begin = record._message_end = False 78 | self._records[0]._message_begin = True 79 | self._records[-1]._message_end = True 80 | for record in self._records: 81 | record._write(f) 82 | 83 | def __repr__(self): 84 | return 'nfc.ndef.Message(' + repr(self._records) + ')' 85 | 86 | def __str__(self): 87 | stream = io.BytesIO() 88 | self._write(stream) 89 | stream.seek(0, 0) 90 | return stream.read() 91 | 92 | def __eq__(self, other): 93 | return str(self) == str(other) 94 | 95 | def __len__(self): 96 | return len(self._records) 97 | 98 | def __getitem__(self, key): 99 | return self._records[key] 100 | 101 | def __setitem__(self, key, value): 102 | if not (isinstance(value, nfc.ndef.Record) or 103 | all([isinstance(elem, nfc.ndef.Record) for elem in value])): 104 | raise TypeError("only nfc.ndef.Record objects are accepted") 105 | self._records[key] = value 106 | 107 | def __delitem__(self, key): 108 | del self._records[key] 109 | 110 | def append(self, record): 111 | """Add a record to the end of the message. The *record* 112 | argument must be an instance of :class:`nfc.ndef.Record`.""" 113 | 114 | if not isinstance(record, nfc.ndef.Record): 115 | raise TypeError("an nfc.ndef.Record object is required") 116 | self._records.append(copy.copy(record)) 117 | 118 | def extend(self, records): 119 | """Extend the message by appending all the records in the 120 | given list. The *records* argument must be a sequence of 121 | :class:`nfc.ndef.Record` elements.""" 122 | 123 | for record in records: 124 | if not isinstance(record, nfc.ndef.Record): 125 | raise TypeError("only nfc.ndef.Record objects are accepted") 126 | self._records.append(copy.copy(record)) 127 | 128 | def insert(self, i, record): 129 | """Insert a record at the given position. The first argument 130 | *i* is the index of the record before which to insert, so 131 | message.insert(0, record) inserts at the front of the message, 132 | and message.insert(len(message), record) is equivalent to 133 | message.append(record). The second argument *record* must be 134 | an instance of :class:`nfc.ndef.Record`.""" 135 | 136 | if not isinstance(record, nfc.ndef.Record): 137 | raise TypeError("an nfc.ndef.Record object is required") 138 | self._records.append(copy.copy(record)) 139 | 140 | def pop(self, i=-1): 141 | """Remove the record at the given position *i* in the message, 142 | and return it. If no position is specified, message.pop() 143 | removes and returns the last item.""" 144 | 145 | return self._records.pop(i) 146 | 147 | @property 148 | def type(self): 149 | """The message type. Corresponds to the record type of the 150 | first record in the message. None if the message has no 151 | records. This attribute is read-only.""" 152 | return self._records[0].type if len(self._records) else None 153 | 154 | @property 155 | def name(self): 156 | """The message name. Corresponds to the record name of the 157 | first record in the message. None if the message has no 158 | records. This attribute is read-only.""" 159 | return self._records[0].name if len(self._records) else None 160 | 161 | def pretty(self): 162 | """Returns a message representation that might be considered 163 | pretty-printable.""" 164 | lines = list() 165 | for index, record in enumerate(self._records): 166 | lines.append(("record {0}".format(index+1),)) 167 | lines.append((" type", repr(record.type))) 168 | lines.append((" name", repr(record.name))) 169 | lines.append((" data", repr(record.data))) 170 | lwidth = max([len(line[0]) for line in lines]) 171 | lines = [(line[0].ljust(lwidth),) + line[1:] for line in lines] 172 | lines = [" = ".join(line) for line in lines] 173 | return ("\n").join([line for line in lines]) 174 | 175 | -------------------------------------------------------------------------------- /nfc/dev/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: latin-1 -*- 2 | # ----------------------------------------------------------------------------- 3 | # Copyright 2009-2013 Stephen Tiedemann 4 | # 5 | # Licensed under the EUPL, Version 1.1 or - as soon they 6 | # will be approved by the European Commission - subsequent 7 | # versions of the EUPL (the "Licence"); 8 | # You may not use this work except in compliance with the 9 | # Licence. 10 | # You may obtain a copy of the Licence at: 11 | # 12 | # http://www.osor.eu/eupl 13 | # 14 | # Unless required by applicable law or agreed to in 15 | # writing, software distributed under the Licence is 16 | # distributed on an "AS IS" basis, 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 18 | # express or implied. 19 | # See the Licence for the specific language governing 20 | # permissions and limitations under the Licence. 21 | # ----------------------------------------------------------------------------- 22 | 23 | import logging 24 | log = logging.getLogger(__name__) 25 | 26 | import os 27 | import re 28 | import sys 29 | import time 30 | import glob 31 | import importlib 32 | 33 | import transport 34 | 35 | usb_device_map = { 36 | (0x054c, 0x0193) : "pn531", # PN531 (Sony VID/PID) 37 | (0x04cc, 0x0531) : "pn531", # PN531 (Philips VID/PID), SCM SCL3710 38 | (0x04cc, 0x2533) : "pn53x", # NXP PN533 demo board 39 | (0x04e6, 0x5591) : "pn53x", # SCM SCL3711 40 | (0x04e6, 0x5593) : "pn53x", # SCM SCL3712 41 | (0x054c, 0x02e1) : "rcs956", # Sony RC-S330/360/370 42 | (0x054c, 0x06c1) : "rcs380", # Sony RC-S380 43 | (0x054c, 0x06c3) : "rcs380", # Sony RC-S380 44 | (0x072f, 0x2200) : "acr122", # Arygon ACR122U 45 | } 46 | 47 | def connect(path): 48 | assert isinstance(path, str) and len(path) > 0 49 | 50 | def import_driver(name): 51 | name = "nfc.dev.{0}".format(name) 52 | log.debug("import {0}".format(name)) 53 | return importlib.import_module(name) 54 | 55 | found = transport.USB.find(path) 56 | if found is not None: 57 | for vid, pid, bus, dev in found: 58 | module = usb_device_map.get((vid, pid)) 59 | if module is not None: 60 | log.debug("trying usb:{0:04x}:{1:04x}".format(vid, pid)) 61 | driver = import_driver(module) 62 | try: 63 | usb = transport.USB(bus, dev) 64 | device = driver.init(usb) 65 | except IOError: 66 | continue 67 | device._path = "usb:{0:03}:{1:03}".format(int(bus), int(dev)) 68 | return device 69 | 70 | found = transport.TTY.find(path) 71 | if found is not None: 72 | port, module = found 73 | log.debug("trying {0} on '{1}'".format(module, path)) 74 | driver = import_driver(module) 75 | try: 76 | tty = transport.TTY(port) 77 | device = driver.init(tty) 78 | device._path = port 79 | return device 80 | except IOError: 81 | pass 82 | 83 | if path.startswith("udp"): 84 | path = path.split(':') 85 | host = str(path[1]) if len(path) > 1 and path[1] else 'localhost' 86 | port = int(path[2]) if len(path) > 2 and path[2] else 54321 87 | driver = import_driver("udp") 88 | device = driver.init(host, port) 89 | device._path = "udp:{0}:{1}".format(host, port) 90 | return device 91 | 92 | class Device(object): 93 | def __str__(self): 94 | return "{dev.vendor} {dev.product} at {dev.path}".format(dev=self) 95 | 96 | @property 97 | def vendor(self): 98 | return self._vendor_name if hasattr(self, "_vendor_name") else '' 99 | 100 | @property 101 | def product(self): 102 | return self._device_name if hasattr(self, "_device_name") else '' 103 | 104 | @property 105 | def path(self): 106 | return self._path 107 | 108 | @property 109 | def capabilities(self): 110 | log.warning("Driver.capabilities should be implemented.") 111 | return {} 112 | 113 | def sense(self, targets): 114 | """Send discovery and activation requests to find a 115 | target. Targets is a list of target specifications (TTA, TTB, 116 | TTF defined in clf.py). Not all drivers may support all 117 | possible target types. The return value is an activated target 118 | with a possibly updated specification (bitrate) or None. 119 | """ 120 | log.warning("Driver.sense() should be implemented.") 121 | time.sleep(1) 122 | return None 123 | 124 | def listen_tta(self, target, timeout): 125 | """Wait *timeout* seconds to become initialized as a *target* 126 | for Type A communication.""" 127 | log.warning("Driver.listen_tta() is not implemented.") 128 | time.sleep(timeout) 129 | return None 130 | 131 | def listen_ttb(self, target, timeout): 132 | """Wait *timeout* seconds to become initialized as a *target* 133 | for Type B communication.""" 134 | log.warning("Driver.listen_ttb() is not implemented.") 135 | time.sleep(timeout) 136 | return None 137 | 138 | def listen_ttf(self, target, timeout): 139 | """Wait *timeout* seconds to become initialized as a *target* 140 | for Type F communication.""" 141 | log.warning("Driver.listen_ttf() is not implemented.") 142 | time.sleep(timeout) 143 | return None 144 | 145 | def listen_dep(self, target, timeout): 146 | """Wait *timeout* seconds to become initialized as a *target* 147 | for data exchange protocol.""" 148 | log.warning("Driver.listen_dep() is not implemented.") 149 | time.sleep(timeout) 150 | return None 151 | 152 | def exchange(self, data, timeout): 153 | """Exchange data with an activated target (data is a command 154 | frame) or as an activated target (data is a response 155 | frame). Returns a target response frame (if data is send to an 156 | activated target) or a next command frame (if data is send 157 | from an activated target). Returns None if the communication 158 | link died during exchange (if data is sent as a target). The 159 | timeout is the number of seconds to wait for data to return, 160 | if the timeout expires an nfc.clf.TimeoutException is 161 | raised. Other nfc.clf.DigitalProtocolExceptions may be raised 162 | if an error is detected during communication. 163 | """ 164 | log.warning("Driver.exchange() should be implemented.") 165 | return None 166 | 167 | def set_communication_mode(self, brm, **kwargs): 168 | """Set the hardware communication mode. The effect of calling 169 | this method depends on the hardware support, some drivers may 170 | purposely ignore this function. If supported, the parameter 171 | *brm* specifies the communication mode to choose as a string 172 | composed of the bitrate and modulation type, for example 173 | '212F' shall switch to 212 kbps Type F communication. Other 174 | communication parameters may be changed with optional keyword 175 | arguments. Currently implemented by the RC-S380 driver are the 176 | parameters 'add-crc' and 'check-crc' when running as 177 | initator. It is possible to set *brm* to an empty string if 178 | bitrate and modulation shall not be changed but only optional 179 | parameters executed. 180 | """ 181 | log.warning("Driver.set_communication_mode() should be implemented.") 182 | return None 183 | -------------------------------------------------------------------------------- /nfc/snep/server.py: -------------------------------------------------------------------------------- 1 | # -*- coding: latin-1 -*- 2 | # ----------------------------------------------------------------------------- 3 | # Copyright 2009-2011 Stephen Tiedemann 4 | # 5 | # Licensed under the EUPL, Version 1.1 or - as soon they 6 | # will be approved by the European Commission - subsequent 7 | # versions of the EUPL (the "Licence"); 8 | # You may not use this work except in compliance with the 9 | # Licence. 10 | # You may obtain a copy of the Licence at: 11 | # 12 | # http://www.osor.eu/eupl 13 | # 14 | # Unless required by applicable law or agreed to in 15 | # writing, software distributed under the Licence is 16 | # distributed on an "AS IS" basis, 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 18 | # express or implied. 19 | # See the Licence for the specific language governing 20 | # permissions and limitations under the Licence. 21 | # ----------------------------------------------------------------------------- 22 | # 23 | # Simple NDEF Exchange Protocol (SNEP) - Server Base Class 24 | # 25 | import logging 26 | log = logging.getLogger(__name__) 27 | 28 | from threading import Thread 29 | from struct import pack, unpack 30 | 31 | import nfc.llcp 32 | import nfc.ndef 33 | 34 | class SnepServer(Thread): 35 | """ NFC Forum Simple NDEF Exchange Protocol server 36 | """ 37 | def __init__(self, llc, service_name="urn:nfc:sn:snep", 38 | max_acceptable_length=0x100000, 39 | recv_miu=1984, recv_buf=15): 40 | 41 | self.max_acceptable_length = min(max_acceptable_length, 0xFFFFFFFF) 42 | socket = nfc.llcp.Socket(llc, nfc.llcp.DATA_LINK_CONNECTION) 43 | recv_miu = socket.setsockopt(nfc.llcp.SO_RCVMIU, recv_miu) 44 | recv_buf = socket.setsockopt(nfc.llcp.SO_RCVBUF, recv_buf) 45 | socket.bind(service_name) 46 | log.info("snep server bound to port {0} (MIU={1}, RW={2}), " 47 | "will accept up to {3} byte NDEF messages" 48 | .format(socket.getsockname(), recv_miu, recv_buf, 49 | self.max_acceptable_length)) 50 | socket.listen(backlog=2) 51 | Thread.__init__(self, name=service_name, 52 | target=self.listen, args=(socket,)) 53 | 54 | def listen(self, socket): 55 | try: 56 | while True: 57 | client_socket = socket.accept() 58 | client_thread = Thread(target=SnepServer.serve, 59 | args=(client_socket, self)) 60 | client_thread.start() 61 | except nfc.llcp.Error as e: 62 | (log.debug if e.errno == nfc.llcp.errno.EPIPE else log.error)(e) 63 | finally: 64 | socket.close() 65 | pass 66 | 67 | @staticmethod 68 | def serve(socket, snep_server): 69 | peer_sap = socket.getpeername() 70 | log.info("serving snep client on remote sap {0}".format(peer_sap)) 71 | send_miu = socket.getsockopt(nfc.llcp.SO_SNDMIU) 72 | try: 73 | while True: 74 | data = socket.recv() 75 | if not data: 76 | break # connection closed 77 | 78 | if len(data) < 6: 79 | log.debug("snep msg initial fragment too short") 80 | break # bail out, this is a bad client 81 | 82 | version, opcode, length = unpack(">BBL", data[:6]) 83 | if (version >> 4) > 1: 84 | log.debug("unsupported version {0}".format(version>>4)) 85 | socket.send("\x10\xE1\x00\x00\x00\x00") 86 | continue 87 | 88 | if length > snep_server.max_acceptable_length: 89 | log.debug("snep msg exceeds max acceptable length") 90 | socket.send("\x10\xFF\x00\x00\x00\x00") 91 | continue 92 | 93 | snep_request = data 94 | if len(snep_request) - 6 < length: 95 | # request remaining fragments 96 | socket.send("\x10\x80\x00\x00\x00\x00") 97 | while len(snep_request) - 6 < length: 98 | data = socket.recv() 99 | if data: snep_request += data 100 | else: break # connection closed 101 | 102 | # message complete, now handle the request 103 | if opcode == 1 and len(snep_request) >= 10: 104 | snep_response = snep_server.__get(snep_request) 105 | elif opcode == 2: 106 | snep_response = snep_server.__put(snep_request) 107 | else: 108 | log.debug("bad request {0}".format(version & 0x0f)) 109 | snep_response = "\x10\xC2\x00\x00\x00\x00" 110 | 111 | # send the snep response, fragment if needed 112 | if len(snep_response) <= send_miu: 113 | socket.send(snep_response) 114 | else: 115 | socket.send(snep_response[0:send_miu]) 116 | if socket.recv() == "\x10\x00\x00\x00\x00\x00": 117 | parts = range(send_miu, len(snep_response), send_miu) 118 | for offset in parts: 119 | fragment = snep_response[offset:offset+send_miu] 120 | socket.send(fragment) 121 | 122 | except nfc.llcp.Error as e: 123 | (log.debug if e.errno == nfc.llcp.errno.EPIPE else log.error)(e) 124 | finally: 125 | socket.close() 126 | 127 | def __get(self, snep_request): 128 | acceptable_length = unpack(">L", snep_request[6:10])[0] 129 | response = self._get(acceptable_length, snep_request[10:]) 130 | if type(response) == int: 131 | response_code = chr(response) 132 | ndef_message = "" 133 | else: 134 | response_code = chr(0x81) 135 | ndef_message = response 136 | ndef_length = pack(">L", len(ndef_message)) 137 | return "\x10" + response_code + ndef_length + ndef_message 138 | 139 | def _get(self, acceptable_length, ndef_message_data): 140 | log.debug("SNEP GET ({0})".format(ndef_message_data.encode("hex"))) 141 | try: 142 | ndef_message = nfc.ndef.Message(ndef_message_data) 143 | except (nfc.ndef.LengthError, nfc.ndef.FormatError) as err: 144 | log.error(repr(err)) 145 | return 0xC2 146 | else: 147 | rsp = self.get(acceptable_length, ndef_message) 148 | return str(rsp) if isinstance(rsp, nfc.ndef.Message) else rsp 149 | 150 | def get(self, acceptable_length, ndef_message): 151 | """Handle Get requests. This method should be overwritten by a 152 | subclass of SnepServer to customize it's behavior. The default 153 | implementation simply returns Not Implemented. 154 | """ 155 | return 0xE0 156 | 157 | def __put(self, snep_request): 158 | response = self._put(snep_request[6:]) 159 | response_code = chr(response) 160 | ndef_length = "\x00\x00\x00\x00" 161 | return "\x10" + chr(response) + ndef_length 162 | 163 | def _put(self, ndef_message_data): 164 | log.debug("SNEP PUT ({0})".format(ndef_message_data.encode("hex"))) 165 | try: 166 | ndef_message = nfc.ndef.Message(ndef_message_data) 167 | except (nfc.ndef.LengthError, nfc.ndef.FormatError) as err: 168 | log.error(repr(err)) 169 | return 0xC2 170 | else: 171 | return self.put(ndef_message) 172 | 173 | def put(self, ndef_message): 174 | """Handle Put requests. This method should be overwritten by a 175 | subclass of SnepServer to customize it's behavior. The default 176 | implementation simply returns Not Implemented. 177 | """ 178 | return 0xE0 179 | -------------------------------------------------------------------------------- /nfc/dev/udp.py: -------------------------------------------------------------------------------- 1 | # -*- coding: latin-1 -*- 2 | # ----------------------------------------------------------------------------- 3 | # Copyright 2012-2013 Stephen Tiedemann 4 | # 5 | # Licensed under the EUPL, Version 1.1 or - as soon they 6 | # will be approved by the European Commission - subsequent 7 | # versions of the EUPL (the "Licence"); 8 | # You may not use this work except in compliance with the 9 | # Licence. 10 | # You may obtain a copy of the Licence at: 11 | # 12 | # http://www.osor.eu/eupl 13 | # 14 | # Unless required by applicable law or agreed to in 15 | # writing, software distributed under the Licence is 16 | # distributed on an "AS IS" basis, 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 18 | # express or implied. 19 | # See the Licence for the specific language governing 20 | # permissions and limitations under the Licence. 21 | # ----------------------------------------------------------------------------- 22 | # 23 | # udp.py - NFC link simulation over UDP 24 | # 25 | 26 | import logging 27 | log = logging.getLogger(__name__) 28 | 29 | from nfc.clf import ProtocolError, TransmissionError, TimeoutError 30 | import nfc.clf 31 | import nfc.dev 32 | 33 | import os 34 | import time 35 | import socket 36 | import select 37 | 38 | class Device(nfc.dev.Device): 39 | def __init__(self, host, port): 40 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 41 | self.addr = (host, port) 42 | 43 | def close(self): 44 | try: 45 | if self.exchange == self.send_cmd_recv_rsp: 46 | self.socket.sendto("", self.addr) 47 | except AttributeError: pass 48 | self.socket.close() 49 | 50 | @property 51 | def capabilities(self): 52 | return {} 53 | 54 | def sense(self, targets): 55 | for tg in targets: 56 | if type(tg) == nfc.clf.TTA: 57 | target = self.sense_a() 58 | if (target and 59 | (tg.cfg is None or target.cfg.startswith(tg.cfg)) and 60 | (tg.uid is None or target.uid.startswith(tg.uid))): 61 | break 62 | elif type(tg) == nfc.clf.TTB: 63 | target = self.sense_b() 64 | if target: 65 | pass 66 | elif type(tg) == nfc.clf.TTF: 67 | br, sc, rc = tg.br, tg.sys, 0 68 | if sc is None: sc, rc = bytearray('\xFF\xFF'), 1 69 | target = self.sense_f(br, sc, rc) 70 | if (target and 71 | (tg.sys is None or target.sys == tg.sys) and 72 | (tg.idm is None or target.idm.startswith(tg.idm)) and 73 | (tg.pmm is None or target.pmm.startswith(tg.pmm))): 74 | break 75 | else: 76 | return None 77 | 78 | self.exchange = self.send_cmd_recv_rsp 79 | return target 80 | 81 | def sense_a(self): 82 | return None 83 | 84 | def sense_b(self): 85 | return None 86 | 87 | def sense_f(self, br, sc, rc): 88 | cmd = "0600{sc[0]:02x}{sc[1]:02x}{rc:02x}03".format(sc=sc, rc=rc) 89 | log.debug("poll NFC-F {0} to {1}".format(cmd, self.addr)) 90 | cmd = bytearray(cmd.decode("hex")) 91 | 92 | self.socket.sendto(cmd, self.addr) 93 | if len(select.select([self.socket], [], [], 1.0)[0]) == 1: 94 | data, addr = self.socket.recvfrom(1024) 95 | log.debug("{0} {1}".format(data.encode("hex"), addr)) 96 | rsp = bytearray(data) 97 | if len(rsp) >= 18 and rsp[0] == len(rsp) and rsp[1] == 1: 98 | if len(rsp) == 18: rsp += "\xff\xff" 99 | idm, pmm, sys = rsp[2:10], rsp[10:18], rsp[18:20] 100 | return nfc.clf.TTF(br=br, idm=idm, pmm=pmm, sys=sys) 101 | 102 | def listen_ttf(self, target, timeout): 103 | assert type(target) is nfc.clf.TTF 104 | 105 | if target.br is None: 106 | target.br = 212 107 | 108 | log.debug("bind socket to {0}".format(self.addr)) 109 | try: self.socket.bind(self.addr) 110 | except socket.error: return 111 | log.debug("bound socket to {0}".format(self.addr)) 112 | 113 | while True: 114 | data, self.addr = self.socket.recvfrom(1024) 115 | log.debug("<< {0} {1}".format(data.encode("hex"), self.addr)) 116 | if data.startswith("\x06\x00"): break 117 | 118 | while True: 119 | cmd = bytearray(data) 120 | if cmd.startswith("\x06\x00"): 121 | rsp = "\x01" + target.idm + target.pmm 122 | if cmd[4] == 1: rsp += target.sys 123 | data = str(chr(len(rsp) + 1) + rsp) 124 | log.debug(">> {0} {1}".format(data.encode("hex"), self.addr)) 125 | self.socket.sendto(data, self.addr) 126 | else: break 127 | if len(select.select([self.socket], [], [], 0.1)[0]) == 1: 128 | data, self.addr = self.socket.recvfrom(1024) 129 | log.debug("<< {0} {1}".format(data.encode("hex"), self.addr)) 130 | else: return None 131 | 132 | self.exchange = self.send_rsp_recv_cmd 133 | return target, cmd 134 | 135 | def listen_dep(self, target, timeout): 136 | assert type(target) is nfc.clf.DEP 137 | 138 | target.br = 424 139 | target.idm = bytearray((0x01, 0xFE)) + os.urandom(6) 140 | target.pmm = bytearray(8) 141 | target.sys = bytearray((0xFF, 0xFF)) 142 | 143 | log.debug("bind socket to {0}".format(self.addr)) 144 | try: self.socket.bind(self.addr) 145 | except socket.error: return 146 | log.debug("bound socket to {0}".format(self.addr)) 147 | 148 | while True: 149 | data, self.addr = self.socket.recvfrom(1024) 150 | log.debug("<< {0} {1}".format(data.encode("hex"), self.addr)) 151 | if data.startswith("\x06\x00"): break 152 | 153 | while True: 154 | cmd = bytearray(data) 155 | if cmd.startswith("\x06\x00"): 156 | rsp = "\x01" + target.idm + target.pmm 157 | if cmd[4] == 1: rsp += target.sys 158 | data = str(chr(len(rsp) + 1) + rsp) 159 | log.debug(">> {0} {1}".format(data.encode("hex"), self.addr)) 160 | self.socket.sendto(data, self.addr) 161 | else: break 162 | if len(select.select([self.socket], [], [], 0.1)[0]) == 1: 163 | data, self.addr = self.socket.recvfrom(1024) 164 | log.debug("<< {0} {1}".format(data.encode("hex"), self.addr)) 165 | else: return None 166 | 167 | self.exchange = self.send_rsp_recv_cmd 168 | return target, cmd 169 | 170 | def send_cmd_recv_rsp(self, data, timeout): 171 | log.debug("send_cmd_recv_rsp with timeout {0} sec".format(timeout)) 172 | 173 | # trash data if any, as on nfc we only recv future data 174 | rfd, wfd, xfd = select.select([self.socket], [], [], 0) 175 | if rfd and rfd[0] == self.socket: 176 | self.socket.recvfrom(1024) 177 | 178 | # send data, data should normally not be none for initiator 179 | if data is not None: 180 | log.debug(">> {0}".format(str(data).encode("hex"))) 181 | self.socket.sendto(data, self.addr) 182 | 183 | # recv response data unless the timeout is zero 184 | if timeout > 0: 185 | rfd, wfd, xfd = select.select([self.socket], [], [], timeout) 186 | if rfd and rfd[0] == self.socket: 187 | data, self.addr = self.socket.recvfrom(1024) 188 | log.debug("<< {0}".format(data.encode("hex"))) 189 | return bytearray(data) if len(data) else None 190 | else: log.debug("TimeoutError"); raise TimeoutError 191 | 192 | def send_rsp_recv_cmd(self, data, timeout): 193 | log.debug("send_rsp_recv_cmd with timeout {0} sec".format(timeout)) 194 | 195 | # trash data if any, as on nfc we only recv future data 196 | rfd, wfd, xfd = select.select([self.socket], [], [], 0) 197 | if rfd and rfd[0] == self.socket: 198 | self.socket.recvfrom(1024) 199 | 200 | # send data, data may be none as target keeps silence on error 201 | if data is not None: 202 | log.debug(">> {0}".format(str(data).encode("hex"))) 203 | self.socket.sendto(data, self.addr) 204 | 205 | # recv response data unless the timeout is zero 206 | if timeout > 0: 207 | rfd, wfd, xfd = select.select([self.socket], [], [], timeout) 208 | if rfd and rfd[0] == self.socket: 209 | data, self.addr = self.socket.recvfrom(1024) 210 | log.debug("<< {0}".format(data.encode("hex"))) 211 | return bytearray(data) if len(data) else None 212 | else: log.debug("TimeoutError"); raise TimeoutError 213 | 214 | def set_communication_mode(self, brm, **kwargs): 215 | pass 216 | 217 | def init(host, port): 218 | device = Device(host, port) 219 | import platform 220 | device._vendor_name = platform.uname()[0] 221 | device._device_name = "UDP/IP" 222 | return device 223 | 224 | -------------------------------------------------------------------------------- /nfc/ndef/smart_poster.py: -------------------------------------------------------------------------------- 1 | # -*- coding: latin-1 -*- 2 | # ----------------------------------------------------------------------------- 3 | # Copyright 2009-2011 Stephen Tiedemann 4 | # 5 | # Licensed under the EUPL, Version 1.1 or - as soon they 6 | # will be approved by the European Commission - subsequent 7 | # versions of the EUPL (the "Licence"); 8 | # You may not use this work except in compliance with the 9 | # Licence. 10 | # You may obtain a copy of the Licence at: 11 | # 12 | # http://www.osor.eu/eupl 13 | # 14 | # Unless required by applicable law or agreed to in 15 | # writing, software distributed under the Licence is 16 | # distributed on an "AS IS" basis, 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 18 | # express or implied. 19 | # See the Licence for the specific language governing 20 | # permissions and limitations under the Licence. 21 | # ----------------------------------------------------------------------------- 22 | 23 | import logging 24 | log = logging.getLogger(__name__) 25 | 26 | import io 27 | import struct 28 | from record import Record 29 | from message import Message 30 | from uri_record import UriRecord 31 | from text_record import TextRecord 32 | 33 | actions = ('default', "exec", "save", "edit") 34 | 35 | class SmartPosterRecord(Record): 36 | """Wraps an NDEF SmartPoster record and provides access to the 37 | :attr:`encoding`, :attr:`language` and actual :attr:`text` 38 | content. 39 | 40 | :param uri: URI string or :class:`nfc.ndef.Record` object 41 | :param title: Smart poster title(s), assigned to :attr:`title` 42 | :param icons: Smart poster icons, assigned to :attr:`icons` 43 | :param action: Recommended action, assigned to :attr:`action` 44 | :param resource_size: Size of the referenced resource 45 | :param resource_type: Type of the referenced resource 46 | 47 | The `uri` argument may alternatively supply an instance of class 48 | :class:`nfc.ndef.Record`. Initialization is then done by parsing 49 | the record payload. If the record type does not match 50 | 'urn:nfc:wkt:Sp' a :exc:`ValueError` exception is raised. 51 | 52 | >>> nfc.ndef.SmartPosterRecord(nfc.ndef.Record()) 53 | >>> nfc.ndef.SmartPosterRecord("http://nfcpy.org", "nfcpy") 54 | >>> nfc.ndef.SmartPosterRecord("http://nfcpy.org", "nfcpy", action="save") 55 | """ 56 | def __init__(self, uri, title={}, icons={}, action='default', 57 | resource_size=None, resource_type=None): 58 | super(SmartPosterRecord, self).__init__('urn:nfc:wkt:Sp') 59 | self._title = dict() 60 | self.title = title 61 | self.icons = icons 62 | self.action = action 63 | self.resource_size = resource_size 64 | self.resource_type = resource_type 65 | if isinstance(uri, Record): 66 | record = uri 67 | if record.type == self.type: 68 | self.name = record.name 69 | self.data = record.data 70 | else: 71 | raise ValueError("record type mismatch") 72 | else: 73 | self.uri = uri 74 | 75 | @property 76 | def data(self): 77 | # encode smart poster payload as ndef message 78 | message = Message(UriRecord(self._uri)) 79 | for lang, text in self.title.iteritems(): 80 | message.append(TextRecord(text=text, language=lang)) 81 | for image_type, image_data in self.icons.iteritems(): 82 | message.append(Record("image/"+image_type, data=image_data)) 83 | if self._action >= 0: 84 | message.append(Record("urn:nfc:wkt:act", data=chr(self._action))) 85 | if self._res_size: 86 | size = struct.pack('>L', self._res_size) 87 | message.append(Record("urn:nfc:wkt:s", data=size)) 88 | return str(message) 89 | 90 | @data.setter 91 | def data(self, string): 92 | log.debug("decode smart poster record " + repr(string)) 93 | if len(string) > 0: 94 | f = io.BytesIO(string) 95 | while f.tell() < len(string): 96 | record = Record(data=f) 97 | if record.type == "urn:nfc:wkt:U": 98 | self.uri = UriRecord(record).uri 99 | elif record.type == "urn:nfc:wkt:T": 100 | record = TextRecord(record) 101 | self.title[record.language] = record.text 102 | elif record.type == "urn:nfc:wkt:act": 103 | self._action = ord(record.data) 104 | elif record.type == "urn:nfc:wkt:s": 105 | self._res_size = struct.unpack('>L', record.data) 106 | elif record.type == "urn:nfc:wkt:t": 107 | self._res_type = record.data 108 | elif record.type.startswith("image/"): 109 | image_type = record.type.replace("image/", "", 1) 110 | self.icons[image_type] = record.data 111 | else: 112 | log.error("nothing to parse") 113 | 114 | @property 115 | def uri(self): 116 | """The smart poster URI, a string of ascii characters. A 117 | :exc:`ValueError` exception is raised if non ascii characters 118 | are contained.""" 119 | return self._uri 120 | 121 | @uri.setter 122 | def uri(self, value): 123 | try: 124 | self._uri = value.encode("ascii") 125 | except UnicodeDecodeError: 126 | raise ValueError("uri value must be an ascii string") 127 | except AttributeError: 128 | raise TypeError("uri value must be a str type") 129 | 130 | @property 131 | def title(self): 132 | """A dictionary of smart poster titles with ISO/IANA language 133 | codes as keys and title strings as values. Set specific title 134 | strings with ``obj.title['en']=title``. Assigning a string 135 | value is equivalent to setting the title for language code 136 | 'en'. Titles are optional for a smart poster record""" 137 | return self._title 138 | 139 | @title.setter 140 | def title(self, value): 141 | if isinstance(value, dict): 142 | self._title = value 143 | else: 144 | self._title["en"] = value 145 | 146 | @property 147 | def icons(self): 148 | """A dictionary of smart poster icon images. The keys specify 149 | the image mime sub-type and the values are strings of image 150 | data. Icons are optional for a smart poster record.""" 151 | return self._icons 152 | 153 | @icons.setter 154 | def icons(self, value): 155 | if not isinstance(value, dict): 156 | raise TypeError("icons must be assigned a dict of images") 157 | self._icons = value 158 | 159 | @property 160 | def action(self): 161 | """The recommended action for the receiver of the smart 162 | poster. Reads as 'default', 'exec', 'save', 'edit' or a number 163 | string if RFU values were decoded. Can be set to 'exec', 164 | 'save', 'edit' or :const:`None`. The action is optional in a 165 | smart poster record.""" 166 | try: 167 | return actions[self._action + 1] 168 | except IndexError: 169 | return str(self._action) 170 | 171 | @action.setter 172 | def action(self, value): 173 | try: 174 | self._action = actions.index(value) - 1 175 | except ValueError: 176 | raise ValueError("action value not in " + repr(actions)) 177 | 178 | @property 179 | def resource_size(self): 180 | """The size of the resource referred by the URI. A 32 bit 181 | unsigned integer value or :const:`None`. The resource size is 182 | optional in a smart poster record.""" 183 | return self._res_size 184 | 185 | @resource_size.setter 186 | def resource_size(self, value): 187 | if value is not None: 188 | value = int(value) 189 | if value < 0 or value > 0xffffffff: 190 | raise ValueError("expected a 32-bit unsigned integer") 191 | self._res_size = value 192 | 193 | @property 194 | def resource_type(self): 195 | """The type of the resource referred by the URI. A UTF-8 196 | formatted string that describes an Internet media type (MIME 197 | type) or :const:`None`. The resource type is optional in a 198 | smart poster record.""" 199 | return self._res_type 200 | 201 | @resource_type.setter 202 | def resource_type(self, value): 203 | self._res_type = value 204 | 205 | def pretty(self, indent=0): 206 | lines = list() 207 | lines.append(("resource", self.uri)) 208 | if self.name: 209 | lines.append(("identifier", repr(self.name))) 210 | if self.resource_type: 211 | lines.append(("resource type", self.resource_type)) 212 | if self.resource_size: 213 | lines.append(("resource size", str(self.resource_size))) 214 | for lang in sorted(self.title): 215 | lines.append(("title[%s]" % lang, self.title[lang])) 216 | for icon in sorted(self.icons): 217 | info = "{0} ... ({1} bytes)".format( 218 | repr(self.icons[icon][:10]).strip("'"), 219 | len(self.icons[icon])) 220 | lines.append(("icon[%s]"%icon, info)) 221 | lines.append(("action", self.action)) 222 | 223 | indent = indent * ' ' 224 | lwidth = max([len(line[0]) for line in lines]) 225 | lines = [line[0].ljust(lwidth) + " = " + line[1] for line in lines] 226 | return ("\n").join([indent + line for line in lines]) 227 | -------------------------------------------------------------------------------- /nfc/tag/tt4.py: -------------------------------------------------------------------------------- 1 | # -*- coding: latin-1 -*- 2 | # ----------------------------------------------------------------------------- 3 | # Copyright 2012-2013 Stephen Tiedemann 4 | # 5 | # Licensed under the EUPL, Version 1.1 or - as soon they 6 | # will be approved by the European Commission - subsequent 7 | # versions of the EUPL (the "Licence"); 8 | # You may not use this work except in compliance with the 9 | # Licence. 10 | # You may obtain a copy of the Licence at: 11 | # 12 | # http://www.osor.eu/eupl 13 | # 14 | # Unless required by applicable law or agreed to in 15 | # writing, software distributed under the Licence is 16 | # distributed on an "AS IS" basis, 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 18 | # express or implied. 19 | # See the Licence for the specific language governing 20 | # permissions and limitations under the Licence. 21 | # ----------------------------------------------------------------------------- 22 | 23 | import logging 24 | log = logging.getLogger(__name__) 25 | 26 | import nfc.tag 27 | import nfc.clf 28 | import nfc.ndef 29 | 30 | ndef_app_file_v1 = bytearray("\xD2\x76\x00\x00\x85\x01\x00") 31 | ndef_app_file_v2 = bytearray("\xD2\x76\x00\x00\x85\x01\x01") 32 | capability_container = bytearray("\xE1\x03") 33 | 34 | class Type4TagError(BaseException): 35 | errmsg = { 36 | (0x6A, 0x80): "Incorrect parameters in the data field", 37 | (0x6A, 0x81): "Function not supported", 38 | (0x6A, 0x82): "File not found", 39 | (0x6A, 0x83): "Record not found", 40 | (0x6A, 0x84): "Not enough memory space in the file", 41 | (0x6A, 0x85): "Lc inconsistent with TLV structure", 42 | (0x6A, 0x86): "Incorrect parameters P1-P2", 43 | (0x6A, 0x87): "Lc inconsistent with P1-P2", 44 | (0x6A, 0x88): "Referenced data not found", 45 | } 46 | def __str__(self): 47 | msg = Type4TagError.errmsg.get(tuple(self.args[0]), "") 48 | return "{sw[0]:02X} {sw[1]:02X} {m}".format(sw=self.args[0], m=msg) 49 | 50 | class NDEF(object): 51 | def __init__(self, tag): 52 | self.tag = tag 53 | self.data = '' 54 | for name, le in ((ndef_app_file_v2, 0), (ndef_app_file_v1, None)): 55 | try: tag.select_file(4, 0, name, le) 56 | except Type4TagError: pass 57 | else: break 58 | else: 59 | raise Exception("ndef application file not found") 60 | 61 | try: tag.select_file(0, 0, capability_container) 62 | except Type4TagError: 63 | raise Exception("capability container not found") 64 | 65 | self._cc = tag.read_binary(offset=0, count=15) 66 | log.debug("CC = {0} ({1} bytes)".format( 67 | str(self._cc).encode("hex"), len(self._cc))) 68 | 69 | if self._cc[0] == 0 and self._cc[1] < 15: 70 | raise Exception("capability container length below 15") 71 | if not self._cc[2]>>4 in (1, 2): 72 | raise Exception("unsupported version " + self.version) 73 | if not self._cc[7] == 4 and self._cc[8] == 6: 74 | raise Exception("no ndef file control tlv") 75 | 76 | self._vmajor = self._cc[2] >> 4 77 | self._vminor = self._cc[2] & 0x0F 78 | self._max_le = self._cc[3] * 256 + self._cc[4] 79 | self._max_lc = self._cc[5] * 256 + self._cc[6] 80 | self._ndef_file_size = self._cc[11] * 256 + self._cc[12] 81 | ndef_file_id = self._cc[9:11] 82 | 83 | log.debug("Capabilities: Ver={0}.{1}, MLe={2}, MLc={3}".format( 84 | self._vmajor, self._vminor, self._max_le, self._max_lc)) 85 | 86 | if self._max_le > 255: 87 | log.warning("MLe > 255 conflicts with READ_BINARY Le encoding") 88 | self._max_le = 255 89 | 90 | if self._max_lc > 255: 91 | log.warning("MLc > 255 conflicts with READ_BINARY Le encoding") 92 | self._max_lc = 255 93 | 94 | p2 = 0 if self._vmajor == 1 else 12 95 | try: tag.select_file(0, p2, ndef_file_id) 96 | except Type4TagError: 97 | raise Exception("ndef file not found") 98 | 99 | self.changed # force initial read 100 | 101 | @property 102 | def version(self): 103 | """The version of the NDEF mapping.""" 104 | return "%d.%d" % (self._vmajor, self._vminor) 105 | 106 | @property 107 | def capacity(self): 108 | """The maximum number of user bytes on the NDEF tag.""" 109 | return self._ndef_file_size - 2 110 | 111 | @property 112 | def readable(self): 113 | """Is True if data can be read from the NDEF tag.""" 114 | return self._cc[13] == 0 115 | 116 | @property 117 | def writeable(self): 118 | """Is True if data can be written to the NDEF tag.""" 119 | return self._cc[14] == 0 120 | 121 | @property 122 | def length(self): 123 | """NDEF message data length.""" 124 | return len(self.data) 125 | 126 | @property 127 | def changed(self): 128 | """True if the message has changed since last read.""" 129 | if self.readable: 130 | old_data = self.data[:] 131 | data = self.tag.read_binary(0, self._max_le) 132 | size = data[0] * 256 + data[1] + 2 133 | tail = max(0, size - len(data)) 134 | while len(data) < size: 135 | count = min(self._max_lc, size - len(data)) 136 | data += self.tag.read_binary(len(data), count) 137 | self.data = str(data[2:size]) 138 | return self.data != old_data 139 | return False 140 | 141 | @property 142 | def message(self): 143 | """An NDEF message object (an empty record message if tag is empty).""" 144 | try: return nfc.ndef.Message(str(self.data)) 145 | except nfc.ndef.parser_error: pass 146 | return nfc.ndef.Message(nfc.ndef.Record()) 147 | 148 | @message.setter 149 | def message(self, msg): 150 | if not self.writeable: 151 | raise nfc.tag.AccessError 152 | data = bytearray([0, 0]) + bytearray(str(msg)) 153 | if len(data) > 2 + self.capacity: 154 | raise nfc.tag.CapacityError 155 | for offset in range(0, len(data), self._max_lc): 156 | part = slice(offset, offset + min(self._max_lc, len(data)-offset)) 157 | self.tag.update_binary(offset, data[part]) 158 | ndef_size = [(len(data) - 2) / 256, (len(data) - 2) % 256] 159 | self.tag.update_binary(0, bytearray(ndef_size)) 160 | 161 | class Type4Tag(object): 162 | type = "Type4Tag" 163 | 164 | def __init__(self, clf, target): 165 | log.debug("init {0}".format(target)) 166 | self.clf = clf 167 | self.atq = target.cfg[0] << 8 | target.cfg[1] 168 | self.sak = target.cfg[2] 169 | self.uid = target.uid 170 | self.ats = target.ats 171 | if self.ats is None: 172 | self.ats = self.clf.exchange('\xE0\x80', timeout=0.03) 173 | try: self.miu = (16,24,32,40,48,64,86,128,256)[self.ats[1] & 0x0F] 174 | except IndexError: 175 | log.warning("FSCI with RFU value in Type 4A Answer To Select") 176 | self.miu = 32 177 | fwi = (self.ats[3] >> 4) if (self.ats[3] >> 4 != 15) else 4 178 | self.fwt = 4096 / 13.56E6 * pow(2, fwi) 179 | if self.clf.capabilities.get('ISO-DEP') is not True: 180 | self.pni = 0 181 | self.ndef = None 182 | try: 183 | self.ndef = NDEF(self) 184 | except Exception as error: 185 | log.error("while reading ndef: {0!r}".format(error)) 186 | 187 | def __str__(self): 188 | hx = lambda x: str(x) if x is None else str(x).encode("hex") 189 | return "Type4Tag ATQ={0:04x} SAK={1:02x} UID={2} ATS={3}".format( 190 | self.atq, self.sak, hx(self.uid), hx(self.ats)) 191 | 192 | def transceive(self, command, timeout=None): 193 | if timeout is None: timeout = self.fwt + 0.01 194 | 195 | if self.clf.capabilities.get('ISO-DEP') is True: 196 | return self.clf.exchange(command, timeout) 197 | 198 | for offset in range(0, len(command), self.miu): 199 | more = len(command) - offset > self.miu 200 | pfb = (0x02 if not more else 0x12) | self.pni 201 | data = chr(pfb) + command[offset:offset+self.miu] 202 | data = self.clf.exchange(data, timeout) 203 | while data[0] & 0b11111110 == 0b11110010: # WTX 204 | log.debug("ISO-DEP waiting time extension") 205 | data = self.clf.exchange(data, timeout) 206 | if data[0] & 0x01 != self.pni: 207 | log.error("ISO-DEP protocol error: block number") 208 | raise IOError("ISO-DEP protocol error: block number") 209 | if more: 210 | if data[0] & 0b11111110 == 0b10100010: # ACK 211 | self.pni = (self.pni + 1) % 2 212 | else: 213 | log.error("ISO-DEP protocol error: expected ack") 214 | raise IOError("ISO-DEP protocol error: expected ack") 215 | else: 216 | if data[0] & 0b11101110 == 0b00000010: # INF 217 | self.pni = (self.pni + 1) % 2 218 | response = data[1:] 219 | else: 220 | log.error("ISO-DEP protocol error: expected inf") 221 | raise IOError("ISO-DEP protocol error: expected inf") 222 | 223 | while bool(data[0] & 0b00010000): 224 | data = chr(0b10100010 | self.pni) # ack 225 | data = self.clf.exchange(data, timeout) 226 | if data[0] & 0x01 != self.pni: 227 | log.error("ISO-DEP protocol error: block number") 228 | raise IOError("ISO-DEP protocol error: block number") 229 | response = response + data[1:] 230 | self.pni = (self.pni + 1) % 2 231 | 232 | return response 233 | 234 | @property 235 | def is_present(self): 236 | """True if the tag is still within communication range.""" 237 | try: 238 | self.read_binary(0, 2) 239 | return True 240 | except: 241 | return False 242 | 243 | def select_file(self, p1, p2, data, expected_response_length=None): 244 | """Select a file or directory with parameters defined in 245 | ISO/IEC 7816-4""" 246 | 247 | log.debug("select file") 248 | cmd = bytearray([0x00, 0xA4, p1, p2]) 249 | if not data is None: 250 | cmd += bytearray([len(data)]) + bytearray(data) 251 | if not expected_response_length is None: 252 | cmd += bytearray([expected_response_length]) 253 | rsp = self.transceive(cmd) 254 | if rsp[-2:] != "\x90\x00": 255 | raise Type4TagError(rsp[-2:]) 256 | 257 | def read_binary(self, offset, count): 258 | """Read *count* bytes from selected file starting at *offset*""" 259 | log.debug("read binary {0} to {1}".format(offset, offset+count)) 260 | cmd = bytearray([0x00, 0xB0, offset/256, offset%256, count]) 261 | rsp = self.transceive(cmd) 262 | if rsp[-2:] != "\x90\x00": 263 | raise Type4TagError(rsp[-2:]) 264 | return rsp[0:-2] 265 | 266 | def update_binary(self, offset, data): 267 | """Write *data* bytes to selected file starting at *offset*""" 268 | log.debug("write binary {0} to {1}".format(offset, offset+len(data))) 269 | cmd = bytearray([0x00, 0xD6, offset/256, offset%256, len(data)]) 270 | cmd = cmd + bytearray(data) 271 | rsp = self.transceive(cmd) 272 | if rsp[-2:] != "\x90\x00": 273 | raise Type4TagError(rsp[-2:]) 274 | -------------------------------------------------------------------------------- /nfc/tag/tt1.py: -------------------------------------------------------------------------------- 1 | # -*- coding: latin-1 -*- 2 | # ----------------------------------------------------------------------------- 3 | # Copyright 2011-2013 4 | # Stephen Tiedemann , 5 | # Alexander Knaub 6 | # 7 | # Licensed under the EUPL, Version 1.1 or - as soon they 8 | # will be approved by the European Commission - subsequent 9 | # versions of the EUPL (the "Licence"); 10 | # You may not use this work except in compliance with the 11 | # Licence. 12 | # You may obtain a copy of the Licence at: 13 | # 14 | # http://www.osor.eu/eupl 15 | # 16 | # Unless required by applicable law or agreed to in 17 | # writing, software distributed under the Licence is 18 | # distributed on an "AS IS" basis, 19 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 20 | # express or implied. 21 | # See the Licence for the specific language governing 22 | # permissions and limitations under the Licence. 23 | # ----------------------------------------------------------------------------- 24 | 25 | import logging 26 | log = logging.getLogger(__name__) 27 | 28 | import nfc.tag 29 | import nfc.clf 30 | import nfc.ndef 31 | 32 | class NDEF(object): 33 | def __init__(self, tag): 34 | self._tag = tag 35 | self._msg = '' 36 | self._cc = tag[8:12] 37 | log.debug("capability container " + str(self._cc).encode("hex")) 38 | self._skip = set(range(104, 120)) 39 | self.changed # force initial read 40 | 41 | def _read_tlv(self, offset): 42 | read_tlv = { 43 | 0x00: lambda x: x + 1, 44 | 0x01: self._read_lock_tlv, 45 | 0x02: self._read_memory_tlv, 46 | 0x03: self._read_ndef_tlv, 47 | 0xFE: lambda x: None 48 | }.get(self._tag[offset], self._read_unknown_tlv) 49 | return read_tlv(offset + 1) 50 | 51 | def _read_unknown_tlv(self, offset): 52 | log.debug("found unknown tlv") 53 | length, offset = self._read_tlv_length(offset) 54 | return offset + length 55 | 56 | def _read_ndef_tlv(self, offset): 57 | log.debug("ndef message tlv at 0x{0:0X}".format(offset-1)) 58 | self._ndef_tlv_offset = offset - 1 59 | length, offset = self._read_tlv_length(offset) 60 | log.debug("ndef message length is {0}".format(length)) 61 | self._capacity = (self._cc[2]+1)*8 - offset - len(self._skip) 62 | if length < 255 and self._capacity >= 255: 63 | self._capacity -= 2 # account for three byte length format 64 | self._msg = bytearray() 65 | while length > 0: 66 | if not offset in self._skip: 67 | self._msg.append(self._tag[offset]) 68 | length -= 1 69 | offset += 1 70 | return None 71 | 72 | def _read_lock_tlv(self, offset): 73 | log.debug("dynamic lock byte tlv at 0x{0:0X}".format(offset-1)) 74 | length, offset = self._read_tlv_length(offset) 75 | value = self._tag[offset:offset+length] 76 | page_offs = value[0] >> 4 77 | byte_offs = value[0] & 0x0F 78 | resv_size = ((value[1] - 1) / 8) + 1 79 | page_size = 2 ** (value[2] & 0x0F) 80 | resv_start = page_offs * page_size + byte_offs 81 | self._skip.update(range(resv_start, resv_start + resv_size)) 82 | return offset + length 83 | 84 | def _read_memory_tlv(self, offset): 85 | log.debug("memory control tlv at 0x{0:0X}".format(offset-1)) 86 | length, offset = self._read_tlv_length(offset) 87 | value = self._tag[offset:offset+length] 88 | page_offs = value[0] >> 4 89 | byte_offs = value[0] & 0x0F 90 | resv_size = value[1] 91 | page_size = 2 ** (value[2] & 0x0F) 92 | resv_start = page_offs * page_size + byte_offs 93 | self._skip.update(range(resv_start, resv_start + resv_size)) 94 | return offset + length 95 | 96 | def _read_tlv_length(self, offset): 97 | length = self._tag[offset] 98 | if length == 255: 99 | length = self._tag[offset+1] * 256 + self._tag[offset+2]; 100 | offset = offset + 2 101 | if length < 256 or length == 0xFFFF: 102 | raise ValueError("invalid tlv lenght value") 103 | return length, offset + 1 104 | 105 | @property 106 | def version(self): 107 | """The version of the NDEF mapping.""" 108 | return "%d.%d" % (self._cc[1]>>4, self._cc[1]&0x0F) 109 | 110 | @property 111 | def capacity(self): 112 | """The maximum number of user bytes on the NDEF tag.""" 113 | return self._capacity 114 | 115 | @property 116 | def readable(self): 117 | """Is True if data can be read from the NDEF tag.""" 118 | return self._cc[3] & 0xF0 == 0x00 119 | 120 | @property 121 | def writeable(self): 122 | """Is True if data can be written to the NDEF tag.""" 123 | return self._cc[3] & 0x0F == 0x00 124 | 125 | @property 126 | def length(self): 127 | """NDEF message data length.""" 128 | return len(self._msg) 129 | 130 | @property 131 | def changed(self): 132 | """True if the message has changed since the read.""" 133 | if self.readable: 134 | old_msg = self._msg[:] # make a copy 135 | offset = 12 136 | while offset is not None: 137 | offset = self._read_tlv(offset) 138 | return self._msg != old_msg 139 | return False 140 | 141 | @property 142 | def message(self): 143 | """An NDEF message object (an empty record message if tag is empty).""" 144 | if self.readable: 145 | try: return nfc.ndef.Message(str(self._msg)) 146 | except nfc.ndef.parser_error: pass 147 | return nfc.ndef.Message(nfc.ndef.Record()) 148 | 149 | @message.setter 150 | def message(self, msg): 151 | if not self.writeable: 152 | raise nfc.tag.AccessError 153 | data = bytearray(str(msg)) 154 | nlen = len(data) 155 | if nlen > self.capacity: 156 | raise nfc.tag.CapacityError 157 | if nlen < self.capacity: 158 | data = data + "\xFE" 159 | with self._tag as tag: 160 | tag[0x08] = 0x00 161 | tag[0x09] = 0x10 162 | tag[0x0B] = 0x00 163 | offset = self._ndef_tlv_offset + 1 164 | if nlen < 255: 165 | tag[offset] = nlen 166 | offset += 1 167 | else: 168 | tag[offset] = 255 169 | tag[offset+1] = nlen / 256 170 | tag[offset+2] = nlen % 256 171 | offset += 3 172 | for octet in data: 173 | while offset in self._skip: 174 | offset += 1 175 | tag[offset] = octet 176 | offset += 1 177 | with self._tag as tag: 178 | tag[8] = 0xE1 179 | 180 | class Type1Tag(object): 181 | type = "Type1Tag" 182 | 183 | def __init__(self, clf, target): 184 | self.clf = clf 185 | self.uid = target.uid 186 | self._mmap = self.read_all()[2:] 187 | self._sync = set() 188 | self.ndef = None 189 | if self[8] == 0xE1: 190 | try: self.ndef = NDEF(self) 191 | except Exception as error: 192 | log.error("while reading ndef: {0!r}".format(error)) 193 | 194 | def __str__(self): 195 | return "Type1Tag UID=" + str(self.uid).encode("hex") 196 | 197 | def __getitem__(self, key): 198 | if type(key) is int: 199 | key = slice(key, key+1) 200 | if not type(key) is slice: 201 | raise TypeError("key must be of type int or slice") 202 | if key.start > key.stop: 203 | raise ValueError("start index is greater than stop index") 204 | if key.stop > len(self._mmap): 205 | for block in range(len(self._mmap)/8, key.stop/8 + 1): 206 | self._mmap += self.read_block(block) 207 | bytes = self._mmap[key.start:key.stop] 208 | return bytes if len(bytes) > 1 else bytes[0] 209 | 210 | def __setitem__(self, key, value): 211 | if type(key) is int: 212 | key = slice(key, key+1) 213 | if type(key) is not slice: 214 | raise TypeError("key must be of type int or slice") 215 | if type(value) == int: 216 | value = bytearray([value]) 217 | else: 218 | value = bytearray(value) 219 | if len(value) != key.stop - key.start: 220 | raise ValueError("value and slice length do not match") 221 | if key.stop > len(self._mmap): 222 | self.__getitem__(key) 223 | for i in xrange(key.start, key.stop): 224 | self._mmap[i] = value[i-key.start] 225 | self._sync.add(i) 226 | 227 | def __enter__(self): 228 | return self 229 | 230 | def __exit__(self, exc_type, exc_value, traceback): 231 | if exc_type is None: 232 | if self._mmap[10] < 15: 233 | for i in sorted(self._sync): 234 | self.write_byte(i, self._mmap[i]) 235 | self._sync.clear() 236 | else: 237 | while len(self._sync) > 0: 238 | block = sorted(self._sync).pop(0) / 8 239 | self.write_block(block, self._mmap[block<<3:(block+1)<<3]) 240 | self._sync -= set(range(block<<3, (block+1)<<3)) 241 | 242 | @property 243 | def is_present(self): 244 | """Returns True if the tag is still within communication range.""" 245 | try: 246 | data = self.transceive("\x78\x00\x00"+self.uid) 247 | return data and len(data) == 6 248 | except nfc.clf.DigitalProtocolError: return False 249 | 250 | def transceive(self, data, timeout=0.1): 251 | return self.clf.exchange(data, timeout) 252 | 253 | def read_id(self): 254 | """Read header rom and all static memory bytes (blocks 0-14). 255 | """ 256 | log.debug("read all") 257 | cmd = "\x78\x00\x00\x00\x00\x00\x00" 258 | return self.transceive(cmd) 259 | 260 | def read_all(self): 261 | """Read header rom and all static memory bytes (blocks 0-14). 262 | """ 263 | log.debug("read all") 264 | cmd = "\x00\x00\x00" + self.uid 265 | return self.transceive(cmd) 266 | 267 | def read_byte(self, addr): 268 | """Read a single byte from static memory area (blocks 0-14). 269 | """ 270 | log.debug("read byte at address 0x{0:03X}".format(addr)) 271 | cmd = "\x01" + chr(addr) + "\x00" + self.uid 272 | return self.transceive(cmd)[1] 273 | 274 | def write_byte(self, addr, byte, erase=True): 275 | """Write a single byte to static memory area (blocks 0-14). 276 | The target byte is zero'd first if 'erase' is True (default). 277 | """ 278 | log.debug("write byte at address 0x{0:03X}".format(addr)) 279 | cmd = "\x53" if erase is True else "\x1A" 280 | cmd = cmd + chr(addr) + chr(byte) + self.uid 281 | return self.transceive(cmd) 282 | 283 | def read_block(self, block): 284 | """Read an 8-byte data block at address (block * 8). 285 | """ 286 | log.debug("read block at address 0x{0:03X}".format(block*8)) 287 | cmd = "\x02" + chr(block) + 8 * chr(0) + self.uid 288 | return self.transceive(cmd)[1:9] 289 | 290 | def write_block(self, block, data, erase=True): 291 | """Write an 8-byte data block at address (block * 8). 292 | The target bytes are zero'd first if 'erase' is True (default). 293 | """ 294 | log.debug("write block at address 0x{0:03X}".format(block*8)) 295 | cmd = "\x54" if erase is True else "\x1B" 296 | cmd = cmd + chr(block) + data + self.uid 297 | return self.transceive(cmd) 298 | -------------------------------------------------------------------------------- /nfc/ndef/record.py: -------------------------------------------------------------------------------- 1 | # -*- coding: latin-1 -*- 2 | # ----------------------------------------------------------------------------- 3 | # Copyright 2009-2012 Stephen Tiedemann 4 | # 5 | # Licensed under the EUPL, Version 1.1 or - as soon they 6 | # will be approved by the European Commission - subsequent 7 | # versions of the EUPL (the "Licence"); 8 | # You may not use this work except in compliance with the 9 | # Licence. 10 | # You may obtain a copy of the Licence at: 11 | # 12 | # http://www.osor.eu/eupl 13 | # 14 | # Unless required by applicable law or agreed to in 15 | # writing, software distributed under the Licence is 16 | # distributed on an "AS IS" basis, 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 18 | # express or implied. 19 | # See the Licence for the specific language governing 20 | # permissions and limitations under the Licence. 21 | # ----------------------------------------------------------------------------- 22 | # 23 | # record.py -- base class for NDEF records 24 | # 25 | # BUGS: 26 | # - does not handle chunked records 27 | 28 | import logging 29 | log = logging.getLogger(__name__) 30 | 31 | import struct 32 | import io 33 | import re 34 | 35 | import nfc.ndef 36 | from error import LengthError, FormatError 37 | 38 | type_name_prefix = ( 39 | '', 'urn:nfc:wkt:', '', '', 'urn:nfc:ext:', 'unknown', 'unchanged') 40 | 41 | class Record(object): 42 | """Wraps an NDEF record and provides getting and setting of the 43 | record type name (:attr:`type`), record identifier (:attr:`name`) 44 | and record payload (:attr:`data`). 45 | 46 | :param record_type: NDEF record type name 47 | :param record_name: NDEF record identifier 48 | :param data: NDEF record payload or NDEF record data 49 | 50 | All arguments accept a :class:`str` or :class:`bytearray` object. 51 | 52 | Interpretation of the `data` argument depends on the presence of 53 | `record_type` and `record_name`. If any of the `record_type` or 54 | `record_name` argument is present, the `data` argument is 55 | interpreted as the record payload and copied to :attr:`data`. If 56 | none of the `record_type` or `record_name` argument are present, 57 | the `data` argument is interpreted as a NDEF record bytes (NDEF 58 | header and payload) and parsed. 59 | 60 | The `record_type` argument combines the NDEF TNF (Type Name 61 | Format) and NDEF TYPE information into a single string. The TNF 62 | values 0, 5 and 6 are expressed by the strings '', 'unknown' and 63 | 'unchanged'. For TNF values 2 and 4 the `record_type` is the 64 | prefix 'urn:nfc:wkt:' and 'urn:nfc:ext:', respectively, followed 65 | by the NDEF TYPE string. TNF values 2 and 3 are not distinguished 66 | by regular expressions matching the either the media-type format 67 | 'type-name/subtype-name' or absolute URI format 'scheme:hier-part' 68 | 69 | >>> nfc.ndef.Record('urn:nfc:wkt:T', 'id', b'\x02enHello World') 70 | >>> nfc.ndef.Record('urn:nfc:wkt:T', data=b'\x02enHello World') 71 | >>> nfc.ndef.Record(data=b'\xd1\x01\x0eT\x02enHello World') 72 | """ 73 | 74 | def __init__(self, record_type=None, record_name=None, data=None): 75 | self._message_begin = self._message_end = False 76 | self._type = self._name = self._data = '' 77 | if not (record_type is None and record_name is None): 78 | self.type = record_type if record_type is not None else 'unknown' 79 | if record_name is not None: 80 | self.name = record_name 81 | if data is not None: 82 | self.data = data 83 | elif data is not None: 84 | if isinstance(data, (bytearray, str)): 85 | data = io.BytesIO(data) 86 | if isinstance(data, io.IOBase): 87 | self._read(data) 88 | else: 89 | raise TypeError("invalid data argument type") 90 | 91 | def _read(self, f): 92 | """Parse an NDEF record from a file-like object.""" 93 | 94 | try: 95 | self.header = ord(f.read(1)) 96 | except TypeError: 97 | log.debug("buffer underflow at offset {0}".format(f.tell())) 98 | raise LengthError("insufficient data to parse") 99 | 100 | mbf = bool(self.header & 0x80) 101 | mef = bool(self.header & 0x40) 102 | cff = bool(self.header & 0x20) 103 | srf = bool(self.header & 0x10) 104 | ilf = bool(self.header & 0x08) 105 | tnf = self.header & 0x07 106 | 107 | try: 108 | type_length = ord(f.read(1)) 109 | if srf: # short record 110 | data_length = ord(f.read(1)) 111 | else: # 32-bit length 112 | data_length = struct.unpack('>L', f.read(4))[0] 113 | if ilf: # id length present 114 | name_length = ord(f.read(1)) 115 | else: 116 | name_length = 0 117 | except (TypeError, struct.error): 118 | log.debug("buffer underflow at offset {0}".format(f.tell())) 119 | raise LengthError("insufficient data to parse") 120 | 121 | try: 122 | record_type = f.read(type_length) 123 | assert len(record_type) == type_length 124 | record_name = f.read(name_length) 125 | assert len(record_name) == name_length 126 | record_data = f.read(data_length) 127 | assert len(record_data) == data_length 128 | except AssertionError: 129 | log.debug("buffer underflow at offset {0}".format(f.tell())) 130 | raise LengthError("insufficient data to parse") 131 | 132 | if tnf in (0, 5, 6) and len(record_type) > 0: 133 | s = "ndef type name format {0} doesn't allow a type string" 134 | raise FormatError( s.format(tnf) ) 135 | if tnf in (1, 2, 3, 4) and len(record_type) == 0: 136 | s = "ndef type name format {0} requires a type string" 137 | raise FormatError( s.format(tnf) ) 138 | if tnf == 0 and len(record_data) > 0: 139 | s = "ndef type name format {0} doesn't allow a payload" 140 | raise FormatError( s.format(tnf) ) 141 | 142 | self._message_begin, self._message_end = mbf, mef 143 | self._type = bytearray(type_name_prefix[tnf] + record_type) 144 | self._name = bytearray(record_name) 145 | self._data = bytearray(record_data) 146 | log.debug("parsed {0}".format(repr(self))) 147 | 148 | def _write(self, f): 149 | """Serialize an NDEF record to a file-like object.""" 150 | log.debug("writing ndef record at offset {0}".format(f.tell())) 151 | 152 | record_type = self.type 153 | record_name = self.name 154 | record_data = self.data 155 | 156 | if record_type == '': 157 | header_flags = 0; record_name = ''; record_data = '' 158 | elif record_type.startswith("urn:nfc:wkt:"): 159 | header_flags = 1; record_type = record_type[12:] 160 | elif re.match(r'[a-zA-Z0-9-]+/[a-zA-Z0-9-+.]+', record_type): 161 | header_flags = 2; record_type = record_type 162 | elif re.match(r'[a-zA-Z][a-zA-Z0-9+-.]*://', record_type): 163 | header_flags = 3; record_type = record_type 164 | elif record_type.startswith("urn:nfc:ext:"): 165 | header_flags = 4; record_type = record_type[12:] 166 | elif record_type == 'unknown': 167 | header_flags = 5; record_type = '' 168 | elif record_type == 'unchanged': 169 | header_flags = 6; record_type = '' 170 | 171 | type_length = len(record_type) 172 | data_length = len(record_data) 173 | name_length = len(record_name) 174 | 175 | if self._message_begin: 176 | header_flags |= 0x80 177 | if self._message_end: 178 | header_flags |= 0x40 179 | if data_length < 256: 180 | header_flags |= 0x10 181 | if name_length > 0: 182 | header_flags |= 0x08 183 | 184 | if data_length < 256: 185 | f.write(struct.pack(">BBB", header_flags, type_length, data_length)) 186 | else: 187 | f.write(struct.pack(">BBL", header_flags, type_length, data_length)) 188 | if name_length > 0: 189 | f.write(struct.pack(">B", name_length)) 190 | 191 | f.write(record_type) 192 | f.write(record_name) 193 | f.write(record_data) 194 | 195 | @property 196 | def type(self): 197 | """The record type. A string that matches the empty string '', 198 | or the string 'unknown', or the string 'unchanged', or starts 199 | with 'urn:nfc:wkt:', or starts with 'urn:nfc:ext:', or matches 200 | the mime-type format, or matches the absolute-URI format.""" 201 | return str(self._type) 202 | 203 | @type.setter 204 | def type(self, value): 205 | value = str(value) 206 | if (value in ('', 'unknown', 'unchanged') or 207 | value.startswith("urn:nfc:wkt:") or 208 | value.startswith("urn:nfc:ext:") or 209 | re.match(r'[a-zA-Z0-9-]+/[a-zA-Z0-9-+.]+', value) or 210 | re.match(r'[a-zA-Z][a-zA-Z0-9+-.]*://', value)): 211 | self._type = bytearray(value) 212 | else: 213 | log.error("'{0}' is not an acceptable record type".format(value)) 214 | raise ValueError("invalid record type") 215 | 216 | @property 217 | def name(self): 218 | """The record identifier as an octet string. Any type that can 219 | be coverted into a sequence of characters in range(0,256) can 220 | be assigned.""" 221 | return str(self._name) 222 | 223 | @name.setter 224 | def name(self, value): 225 | self._name = bytearray(str(value)) 226 | 227 | @property 228 | def data(self): 229 | """The record payload as an octet string. Any type that can be 230 | coverted into a sequence of characters in range(0,256) can be 231 | assigned.""" 232 | return str(self._data) 233 | 234 | @data.setter 235 | def data(self, value): 236 | self._data = bytearray(str(value)) 237 | 238 | def __iter__(self): 239 | from itertools import islice 240 | return islice(str(self), None) 241 | 242 | def __str__(self): 243 | stream = io.BytesIO() 244 | self._write(stream) 245 | stream.seek(0, 0) 246 | return stream.read() 247 | 248 | def __repr__(self): 249 | return "nfc.ndef.Record('{0}', '{1}', '{2}')".format( 250 | self.type.encode('string_escape'), 251 | self.name.encode('string_escape'), 252 | self.data.encode('string_escape')) 253 | 254 | def pretty(self, indent=0): 255 | """Returns a string with a formatted representation that might 256 | be considered pretty-printable. The optional argument *indent* 257 | specifies the amount of indentation added for each level of 258 | output.""" 259 | indent = indent * ' ' 260 | lines = list() 261 | lines.append((indent + "type", repr(self.type))) 262 | lines.append((indent + "name", repr(self.name))) 263 | lines.append((indent + "data", repr(self.data))) 264 | lwidth = max([len(line[0]) for line in lines]) 265 | lines = [line[0].ljust(lwidth) + " = " + line[1] for line in lines] 266 | return ("\n").join([indent + line for line in lines]) 267 | 268 | class RecordList(list): 269 | """A specialized list type that only accepts :class:`Record` objects.""" 270 | 271 | def __init__(self, iterable=tuple()): 272 | super(RecordList, self).__init__() 273 | for item in iterable: 274 | self.append(item) 275 | 276 | def __setitem__(self, key, value): 277 | if not isinstance(value, Record): 278 | raise TypeError("RecordList only accepts Record objects") 279 | super(RecordList, self).__setitem__(key, value) 280 | 281 | def append(self, value): 282 | if not isinstance(value, Record): 283 | raise TypeError("RecordList only accepts Record objects") 284 | super(RecordList, self).append(value) 285 | 286 | def extend(self, iterable): 287 | for item in iterable: 288 | self.append(item) 289 | 290 | -------------------------------------------------------------------------------- /nfc/tag/tt2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: latin-1 -*- 2 | # ----------------------------------------------------------------------------- 3 | # Copyright 2009-2013 Stephen Tiedemann 4 | # 5 | # Licensed under the EUPL, Version 1.1 or - as soon they 6 | # will be approved by the European Commission - subsequent 7 | # versions of the EUPL (the "Licence"); 8 | # You may not use this work except in compliance with the 9 | # Licence. 10 | # You may obtain a copy of the Licence at: 11 | # 12 | # http://www.osor.eu/eupl 13 | # 14 | # Unless required by applicable law or agreed to in 15 | # writing, software distributed under the Licence is 16 | # distributed on an "AS IS" basis, 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 18 | # express or implied. 19 | # See the Licence for the specific language governing 20 | # permissions and limitations under the Licence. 21 | # ----------------------------------------------------------------------------- 22 | 23 | import logging 24 | log = logging.getLogger(__name__) 25 | 26 | import nfc.tag 27 | import nfc.clf 28 | import nfc.ndef 29 | 30 | class NDEF(object): 31 | def __init__(self, tag): 32 | self._tag = tag 33 | self._cc = tag[12:16] 34 | log.debug("capability container " + str(self._cc).encode("hex")) 35 | self._skip = set([]) 36 | self._msg = bytearray() 37 | self.changed # force initial read 38 | 39 | def _read_tlv(self, offset): 40 | read_tlv = { 41 | 0x00: lambda x: x + 1, 42 | 0x01: self._read_lock_tlv, 43 | 0x02: self._read_memory_tlv, 44 | 0x03: self._read_ndef_tlv, 45 | 0xFE: lambda x: None 46 | }.get(self._tag[offset], self._read_unknown_tlv) 47 | return read_tlv(offset + 1) 48 | 49 | def _read_unknown_tlv(self, offset): 50 | length, offset = self._read_tlv_length(offset) 51 | return offset + length 52 | 53 | def _read_ndef_tlv(self, offset): 54 | self._ndef_tlv_offset = offset - 1 55 | length, offset = self._read_tlv_length(offset) 56 | self._capacity = 16 + self._cc[2] * 8 - offset - len(self._skip) 57 | if length < 255 and self._capacity >= 255: 58 | self._capacity -= 2 # account for three byte length format 59 | self._msg = bytearray() 60 | while length > 0: 61 | if not offset in self._skip: 62 | self._msg.append(self._tag[offset]) 63 | offset += 1; length -= 1 64 | return None 65 | 66 | def _read_lock_tlv(self, offset): 67 | length, offset = self._read_tlv_length(offset) 68 | value = self._tag[offset:offset+length] 69 | page_offs = value[0] >> 4 70 | byte_offs = value[0] & 0x0F 71 | resv_size = ((value[1] - 1) / 8) + 1 72 | page_size = 2 ** (value[2] & 0x0F) 73 | resv_start = page_offs * page_size + byte_offs 74 | self._skip.update(range(resv_start, resv_start + resv_size)) 75 | return offset + length 76 | 77 | def _read_memory_tlv(self, offset): 78 | length, offset = self._read_tlv_length(offset) 79 | value = self._tag[offset:offset+length] 80 | page_offs = value[0] >> 4 81 | byte_offs = value[0] & 0x0F 82 | resv_size = value[1] 83 | page_size = 2 ** (value[2] & 0x0F) 84 | resv_start = page_offs * page_size + byte_offs 85 | self._skip.update(range(resv_start, resv_start + resv_size)) 86 | return offset + length 87 | 88 | def _read_tlv_length(self, offset): 89 | length = self._tag[offset] 90 | if length == 255: 91 | length = self._tag[offset+1] * 256 + self._tag[offset+2]; 92 | offset = offset + 2 93 | if length < 256 or length == 0xFFFF: 94 | raise ValueError("invalid tlv lenght value") 95 | return length, offset + 1 96 | 97 | @property 98 | def version(self): 99 | """The version of the NDEF mapping.""" 100 | return "%d.%d" % (self._cc[1]>>4, self._cc[1]&0x0F) 101 | 102 | @property 103 | def capacity(self): 104 | """The maximum number of user bytes on the NDEF tag.""" 105 | return self._capacity 106 | 107 | @property 108 | def readable(self): 109 | """Is True if data can be read from the NDEF tag.""" 110 | return self._cc[3] & 0xF0 == 0x00 111 | 112 | @property 113 | def writeable(self): 114 | """Is True if data can be written to the NDEF tag.""" 115 | return self._cc[3] & 0x0F == 0x00 116 | 117 | @property 118 | def length(self): 119 | """NDEF message data length.""" 120 | return len(self._msg) 121 | 122 | @property 123 | def changed(self): 124 | """True if the message has changed since the read.""" 125 | if self.readable: 126 | old_msg = self._msg[:] # make a copy 127 | offset = 16 128 | while offset is not None: 129 | offset = self._read_tlv(offset) 130 | return self._msg != old_msg 131 | return False 132 | 133 | @property 134 | def message(self): 135 | """An NDEF message object (an empty record message if tag is empty).""" 136 | if self.readable: 137 | try: return nfc.ndef.Message(str(self._msg)) 138 | except nfc.ndef.parser_error: pass 139 | return nfc.ndef.Message(nfc.ndef.Record()) 140 | 141 | @message.setter 142 | def message(self, msg): 143 | if not self.writeable: 144 | raise nfc.tag.AccessError 145 | data = bytearray(str(msg)) 146 | nlen = len(data) 147 | if nlen > self.capacity: 148 | raise nfc.tag.CapacityError 149 | if nlen < self.capacity: 150 | data = data + "\xFE" 151 | with self._tag as tag: 152 | offset = self._ndef_tlv_offset + 1 153 | tag[offset] = 0 154 | offset += 1 if len(data) < 255 else 3 155 | for octet in data: 156 | while offset in self._skip: 157 | offset += 1 158 | tag[offset] = octet 159 | offset += 1 160 | with self._tag as tag: 161 | offset = self._ndef_tlv_offset + 1 162 | if len(data) < 255: 163 | tag[offset] = nlen 164 | else: 165 | tag[offset] = 255 166 | tag[offset+1] = nlen / 256 167 | tag[offset+2] = nlen % 256 168 | 169 | class Type2Tag(object): 170 | type = "Type2Tag" 171 | 172 | def __init__(self, clf, target): 173 | clf.set_communication_mode('', check_crc='OFF') 174 | self.clf = clf 175 | self.atq = target.cfg[0] << 8 | target.cfg[1] 176 | self.sak = target.cfg[2] 177 | self.uid = target.uid 178 | self._mmap = dict() 179 | self._sync = set() 180 | self._page = 0 181 | self.ndef = None 182 | if self[12] == 0xE1: 183 | try: self.ndef = NDEF(self) 184 | except Exception as error: 185 | log.error("while reading ndef: {0!r}".format(error)) 186 | 187 | def __str__(self): 188 | s = "Type2Tag ATQ={0:04x} SAK={1:02x} UID={2}" 189 | return s.format(self.atq, self.sak, str(self.uid).encode("hex")) 190 | 191 | def __getitem__(self, key): 192 | if type(key) is int: 193 | key = slice(key, key+1) 194 | if not type(key) is slice: 195 | raise TypeError("key must be of type int or slice") 196 | octets = bytearray(key.stop - key.start) 197 | for i in xrange(key.start, key.stop): 198 | data = self._mmap.get(i/16, None) 199 | if data is None: 200 | data = self.read((i/16)*4) 201 | self._mmap[i/16] = data 202 | octets[i-key.start] = data[i%16] 203 | return octets if len(octets) > 1 else octets[0] 204 | 205 | def __setitem__(self, key, value): 206 | if type(key) is int: 207 | key = slice(key, key+1) 208 | if type(key) is not slice: 209 | raise TypeError("key must be of type int or slice") 210 | if type(value) == int: 211 | value = bytearray([value]) 212 | else: 213 | value = bytearray(value) 214 | if len(value) != key.stop - key.start: 215 | raise ValueError("value and slice length must be equal") 216 | for i in xrange(key.start, key.stop): 217 | data = self._mmap.get(i/16, None) 218 | if data is None: 219 | data = self.read((i/16)*4) 220 | self._mmap[i/16] = data 221 | data[i%16] = value[i-key.start] 222 | self._sync.add(i/4) 223 | 224 | def __enter__(self): 225 | return self 226 | 227 | def __exit__(self, exc_type, exc_value, traceback): 228 | if exc_type is None: 229 | for i in sorted(self._sync): 230 | self.write(i, self._mmap[i/4][(i*4)%16:(i*4)%16+4]) 231 | self._sync.clear() 232 | self._mmap.clear() 233 | 234 | @property 235 | def is_present(self): 236 | """Returns True if the tag is still within communication range.""" 237 | try: return bool(self.read(0)) 238 | except nfc.clf.DigitalProtocolError: return False 239 | 240 | def transceive(self, data, timeout=0.1): 241 | return self.clf.exchange(data, timeout) 242 | 243 | def read(self, block): 244 | """Read 16-byte of data from the tag. The *block* argument 245 | specifies the offset in multiples of 4 bytes (i.e. block 246 | number 1 will return bytes 4 to 19). The data returned is a 247 | byte array of length 16. 248 | """ 249 | log.debug("read block #{0}".format(block)) 250 | if self._page != block / 256: 251 | self._page = block / 256 252 | rsp = self.transceive("\xC2\xFF") 253 | if not (len(rsp) == 1 and rsp[0] == 0x0A): 254 | raise nfc.clf.ProtocolError("9.8.3.1") 255 | try: self.transceive(chr(self._page) + 3*chr(0), timeout=0.001) 256 | except nfc.clf.TimeoutError: pass 257 | else: raise nfc.clf.ProtocolError("9.8.3.3") 258 | 259 | try: 260 | rsp = self.transceive("\x30" + chr(block % 256)) 261 | except nfc.clf.TimeoutError: 262 | raise nfc.clf.TimeoutError("9.9.1.3") 263 | 264 | if len(rsp) == 16 or (len(rsp) == 18 and crca(rsp, 16) == rsp[16:18]): 265 | return rsp[0:16] 266 | if len(rsp) == 18: 267 | raise nfc.clf.TransmissionError("4.4.1.3") 268 | if len(rsp) == 1 and rsp[0] != 0x0A: 269 | raise nfc.clf.ProtocolError("9.6.2.3") 270 | raise nfc.clf.ProtocolError("9.6.2") 271 | 272 | def write(self, block, data): 273 | """Write 4-byte of data to the tag. The *block* argument 274 | specifies the offset in multiples of 4 bytes. The *data* 275 | argument must be a string or bytearray of length 4. 276 | """ 277 | log.debug("write block #{0}".format(block)) 278 | assert(len(data) == 4) 279 | if not self._page == block / 256: 280 | self._page = block / 256 281 | rsp = self.transceive("\xC2\xFF") 282 | if not (len(rsp) == 1 and rsp[0] == 0x0A): 283 | raise nfc.clf.ProtocolError("9.8.3.1") 284 | try: self.transceive(chr(self._page) + 3*chr(0), timeout=0.001) 285 | except nfc.clf.TimeoutError: pass 286 | else: raise nfc.clf.ProtocolError("9.8.3.3") 287 | 288 | try: 289 | rsp = self.transceive("\xA2" + chr(block % 256) + str(data)) 290 | except nfc.clf.TimeoutError: 291 | raise nfc.clf.TimeoutError("9.9.1.3") 292 | 293 | if (len(rsp) == 1 and rsp[0] == 0x0A) or (len(rsp) == 0): 294 | # Case 1 is for readers who return the ack/nack. 295 | # Case 2 is for readers who process the response. 296 | return True 297 | if len(rsp) == 1: 298 | raise nfc.clf.ProtocolError("9.7.2.1") 299 | raise nfc.clf.ProtocolError("9.7.2") 300 | 301 | def crca(data, size): 302 | reg = 0x6363 303 | for octet in bytearray(data[:size]): 304 | for pos in range(8): 305 | bit = (reg ^ ((octet >> pos) & 1)) & 1 306 | reg = reg >> 1 307 | if bit: reg = reg ^ 0x8408 308 | return bytearray([reg & 0xff, reg >> 8]) 309 | -------------------------------------------------------------------------------- /nfc/dev/transport.py: -------------------------------------------------------------------------------- 1 | # -*- coding: latin-1 -*- 2 | # ----------------------------------------------------------------------------- 3 | # Copyright 2012-2013 Stephen Tiedemann 4 | # 5 | # Licensed under the EUPL, Version 1.1 or - as soon they 6 | # will be approved by the European Commission - subsequent 7 | # versions of the EUPL (the "Licence"); 8 | # You may not use this work except in compliance with the 9 | # Licence. 10 | # You may obtain a copy of the Licence at: 11 | # 12 | # http://www.osor.eu/eupl 13 | # 14 | # Unless required by applicable law or agreed to in 15 | # writing, software distributed under the Licence is 16 | # distributed on an "AS IS" basis, 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 18 | # express or implied. 19 | # See the Licence for the specific language governing 20 | # permissions and limitations under the Licence. 21 | # ----------------------------------------------------------------------------- 22 | # 23 | # Transport layer for host to reader communication. 24 | # 25 | import logging 26 | log = logging.getLogger(__name__) 27 | 28 | import importlib 29 | import errno 30 | import time 31 | import sys 32 | import os 33 | import re 34 | 35 | class TTY(object): 36 | TYPE = "TTY" 37 | 38 | @classmethod 39 | def find(cls, path): 40 | if not (path.startswith("tty") or path.startswith("com")): 41 | return 42 | 43 | try: 44 | cls.serial = importlib.import_module("serial") 45 | except ImportError: 46 | log.error("python serial library not found") 47 | return None 48 | 49 | match = re.match(r"^(tty|com):([a-zA-Z0-9]+):([a-zA-Z0-9]+)$", path) 50 | 51 | if match and match.group(1) == "tty": 52 | try: 53 | port = int(match.group(2)) 54 | except ValueError: 55 | port = "/dev/tty{0}".format(match.group(2)) 56 | try: 57 | tty = cls.serial.Serial(port) 58 | return tty.port, match.group(3) 59 | except cls.serial.SerialException: 60 | log.debug("failed to open serial port '{0}'".format(port)) 61 | 62 | if match and match.group(1) == "com": 63 | try: 64 | port = int(match.group(2)) 65 | except ValueError: 66 | port = match.group(2) 67 | try: 68 | com = cls.serial.Serial(port) 69 | return com.port, match.group(3) 70 | except cls.serial.SerialException: 71 | log.debug("failed to open serial port '{0}'".format(port)) 72 | 73 | @property 74 | def manufacturer_name(self): 75 | return None 76 | 77 | @property 78 | def product_name(self): 79 | return None 80 | 81 | def __init__(self, port): 82 | self.open(port) 83 | 84 | def open(self, port): 85 | self.tty = self.serial.Serial(port, baudrate=115200, timeout=0.05) 86 | 87 | def read(self, timeout): 88 | if self.tty is not None: 89 | self.tty.timeout = max(timeout / 1000.0, 0.05) 90 | frame = bytearray(self.tty.read(6)) 91 | if frame is None or len(frame) == 0: 92 | raise IOError(errno.ETIMEDOUT, os.strerror(errno.ETIMEDOUT)) 93 | if frame.startswith("\x00\x00\xff\x00\xff\x00"): 94 | return frame 95 | LEN = frame[3] 96 | if LEN == 0xFF: 97 | frame += self.tty.read(3) 98 | LEN = frame[5]<<8 | frame[6] 99 | frame += self.tty.read(LEN + 1) 100 | log.debug("<<< " + str(frame).encode("hex")) 101 | return frame 102 | 103 | def write(self, frame): 104 | if self.tty is not None: 105 | log.debug(">>> " + str(frame).encode("hex")) 106 | self.tty.flushInput() 107 | try: 108 | self.tty.write(str(frame)) 109 | except self.serial.SerialTimeoutException: 110 | raise IOError(errno.EIO, os.strerror(errno.EIO)) 111 | 112 | def close(self): 113 | if self.tty is not None: 114 | self.tty.timeout = 0.1 115 | self.tty.read(300) 116 | self.tty.close() 117 | self.tty = None 118 | 119 | class USB(object): 120 | TYPE = "USB" 121 | 122 | @classmethod 123 | def find(cls, path): 124 | if not path.startswith("usb"): 125 | return 126 | 127 | cls.pyusb_version = None 128 | 129 | try: 130 | cls.usb_core = importlib.import_module("usb.core") 131 | cls.usb_util = importlib.import_module("usb.util") 132 | cls.pyusb_version = 1 133 | except ImportError: pass 134 | 135 | if cls.pyusb_version is None: 136 | try: 137 | cls.usb = importlib.import_module("usb") 138 | cls.pyusb_version = 0 139 | except ImportError: pass 140 | 141 | if cls.pyusb_version is None: 142 | log.error("python usb library not found") 143 | return None 144 | 145 | log.debug("using pyusb version {0}.x".format(cls.pyusb_version)) 146 | 147 | usb_or_none = re.compile(r'^(usb|)$') 148 | usb_vid_pid = re.compile(r'^usb(:[0-9a-fA-F]{4})(:[0-9a-fA-F]{4})?$') 149 | usb_bus_dev = re.compile(r'^usb(:[0-9]{1,3})(:[0-9]{1,3})?$') 150 | match = None 151 | 152 | for regex in (usb_vid_pid, usb_bus_dev, usb_or_none): 153 | m = regex.match(path) 154 | if m is not None: 155 | log.debug("path matches {0!r}".format(regex.pattern)) 156 | if regex is usb_vid_pid: 157 | match = [int(s.strip(':'), 16) for s in m.groups() if s] 158 | match = dict(zip(['idVendor', 'idProduct'], match)) 159 | if regex is usb_bus_dev: 160 | match = [int(s.strip(':'), 10) for s in m.groups() if s] 161 | match = dict(zip(['bus', 'address'], match)) 162 | if regex is usb_or_none: 163 | match = dict() 164 | break 165 | else: return None 166 | 167 | if cls.pyusb_version == 1: 168 | return [(d.idVendor, d.idProduct, d.bus, d.address) 169 | for d in cls.usb_core.find(find_all=True, **match)] 170 | 171 | if cls.pyusb_version == 0: 172 | # get all devices for all busses first, then filter 173 | devices = [(d, b) for b in cls.usb.busses() for d in b.devices] 174 | vid, pid = match.get('idVendor'), match.get('idProduct') 175 | bus, dev = match.get('bus'), match.get('address') 176 | if vid is not None: 177 | devices = [d for d in devices if d[0].idVendor == vid] 178 | if pid is not None: 179 | devices = [d for d in devices if d[0].idProduct == pid] 180 | if bus is not None: 181 | devices = [d for d in devices if int(d[1].dirname) == bus] 182 | if dev is not None: 183 | devices = [d for d in devices if int(d[0].filename) == dev] 184 | return [(d[0].idVendor, d[0].idProduct, d[1].dirname, 185 | d[0].filename) for d in devices] 186 | 187 | def __init__(self, bus_id, dev_id): 188 | self.usb_out = None 189 | self.usb_inp = None 190 | 191 | if self.pyusb_version == 0: 192 | self.open = self._PYUSB0_open 193 | self.read = self._PYUSB0_read 194 | self.write = self._PYUSB0_write 195 | self.close = self._PYUSB0_close 196 | self.get_string = self._PYUSB0_get_string 197 | elif self.pyusb_version == 1: 198 | self.open = self._PYUSB1_open 199 | self.read = self._PYUSB1_read 200 | self.write = self._PYUSB1_write 201 | self.close = self._PYUSB1_close 202 | self.get_string = self._PYUSB1_get_string 203 | else: 204 | log.error("unexpected pyusb version") 205 | raise SystemExit 206 | 207 | self.open(bus_id, dev_id) 208 | 209 | @property 210 | def manufacturer_name(self): 211 | if self.manufacturer_name_id: 212 | return self.get_string(self.manufacturer_name_id) 213 | 214 | @property 215 | def product_name(self): 216 | if self.product_name_id: 217 | return self.get_string(self.product_name_id) 218 | 219 | def _PYUSB0_get_string(self, index, langid=-1): 220 | return self.usb_dev.getString(index, 126, langid) 221 | 222 | def _PYUSB1_get_string(self, index, langid=None): 223 | # Prior to version 1.0.0b2 pyusb's' util.get_string() needed a 224 | # length parameter which has since been removed. The try/except 225 | # clause helps support older versions until pyusb 1.0.0 is 226 | # finally released and sufficiently spread. 227 | try: 228 | return self.usb_util.get_string(self.usb_dev, index, langid) 229 | except TypeError: 230 | return self.usb_util.get_string(self.usb_dev, 126, index, langid) 231 | 232 | def _PYUSB0_open(self, bus_id, dev_id): 233 | bus = [b for b in self.usb.busses() if b.dirname == bus_id][0] 234 | dev = [d for d in bus.devices if d.filename == dev_id][0] 235 | self.usb_dev = dev.open() 236 | if sys.platform.startswith("darwin"): 237 | self.usb_dev.setConfiguration(dev.configurations[0]) 238 | try: 239 | self.usb_dev.claimInterface(0) 240 | except self.usb.USBError: 241 | log.debug("device probably used by another process") 242 | raise IOError("unusable device") 243 | interface = dev.configurations[0].interfaces[0] 244 | endpoints = interface[0].endpoints 245 | bulk_inp = lambda ep: (\ 246 | (ep.type == self.usb.ENDPOINT_TYPE_BULK) and 247 | (ep.address & self.usb.ENDPOINT_DIR_MASK == self.usb.ENDPOINT_IN)) 248 | bulk_out = lambda ep: (\ 249 | (ep.type == self.usb.ENDPOINT_TYPE_BULK) and 250 | (ep.address & self.usb.ENDPOINT_DIR_MASK == self.usb.ENDPOINT_OUT)) 251 | self.usb_out = [ep for ep in endpoints if bulk_out(ep)].pop().address 252 | self.usb_inp = [ep for ep in endpoints if bulk_inp(ep)].pop().address 253 | self.manufacturer_name_id = dev.iManufacturer 254 | self.product_name_id = dev.iProduct 255 | 256 | def _PYUSB1_open(self, bus_id, dev_id): 257 | self.usb_dev = self.usb_core.find(bus=bus_id, address=dev_id) 258 | if sys.platform.startswith("darwin"): 259 | self.usb_dev.set_configuration() 260 | interface = self.usb_util.find_descriptor(self.usb_dev[0]) 261 | bulk_inp = lambda ep: (\ 262 | (self.usb_util.endpoint_type(ep.bmAttributes) == 263 | self.usb_util.ENDPOINT_TYPE_BULK) and 264 | (self.usb_util.endpoint_direction(ep.bEndpointAddress) == 265 | self.usb_util.ENDPOINT_IN)) 266 | bulk_out = lambda ep: (\ 267 | (self.usb_util.endpoint_type(ep.bmAttributes) == 268 | self.usb_util.ENDPOINT_TYPE_BULK) and 269 | (self.usb_util.endpoint_direction(ep.bEndpointAddress) == 270 | self.usb_util.ENDPOINT_OUT)) 271 | self.usb_out = [ep for ep in interface if bulk_out(ep)].pop() 272 | self.usb_inp = [ep for ep in interface if bulk_inp(ep)].pop() 273 | try: 274 | # implicitely claim interface 275 | self.usb_out.write('') 276 | except self.usb_core.USBError: 277 | raise IOError(errno.EACCES, os.strerror(errno.EACCES)) 278 | self.manufacturer_name_id = self.usb_dev.iManufacturer 279 | self.product_name_id = self.usb_dev.iProduct 280 | 281 | def _PYUSB0_read(self, timeout): 282 | if self.usb_inp is not None: 283 | try: 284 | frame = self.usb_dev.bulkRead(self.usb_inp, 300, timeout) 285 | except self.usb.USBError as error: 286 | if error.message == "Connection timed out": 287 | ETIMEDOUT = errno.ETIMEDOUT 288 | raise IOError(ETIMEDOUT, os.strerror(ETIMEDOUT)) 289 | else: 290 | log.error("{0!r}".format(error)) 291 | raise IOError(errno.EIO, os.strerror(errno.EIO)) 292 | else: 293 | frame = bytearray(frame) 294 | log.debug("<<< " + str(frame).encode("hex")) 295 | return frame 296 | 297 | def _PYUSB1_read(self, timeout): 298 | if self.usb_inp is not None: 299 | try: 300 | frame = self.usb_inp.read(300, timeout) 301 | except self.usb_core.USBError as error: 302 | if error.errno != errno.ETIMEDOUT: 303 | log.error("{0!r}".format(error)) 304 | raise error 305 | else: 306 | frame = bytearray(frame) 307 | log.debug("<<< " + str(frame).encode("hex")) 308 | return frame 309 | 310 | def _PYUSB0_write(self, frame): 311 | if self.usb_out is not None: 312 | log.debug(">>> " + str(frame).encode("hex")) 313 | try: 314 | self.usb_dev.bulkWrite(self.usb_out, frame) 315 | if len(frame) % 64 == 0: # must end bulk transfer 316 | self.usb_dev.bulkWrite(self.usb_out, '') 317 | except self.usb.USBError as error: 318 | if error.message == "Connection timed out": 319 | ETIMEDOUT = errno.ETIMEDOUT 320 | raise IOError(ETIMEDOUT, os.strerror(ETIMEDOUT)) 321 | else: 322 | log.error("{0!r}".format(error)) 323 | raise IOError(errno.EIO, os.strerror(errno.EIO)) 324 | 325 | def _PYUSB1_write(self, frame): 326 | if self.usb_out is not None: 327 | log.debug(">>> " + str(frame).encode("hex")) 328 | try: 329 | self.usb_out.write(frame) 330 | if len(frame) % self.usb_out.wMaxPacketSize == 0: 331 | self.usb_out.write('') # end bulk transfer 332 | except self.usb_core.USBError as error: 333 | if error.errno != errno.ETIMEDOUT: 334 | log.error("{0!r}".format(error)) 335 | raise error 336 | 337 | def _PYUSB0_close(self): 338 | if self.usb_dev is not None: 339 | self.usb_dev.releaseInterface() 340 | self.usb_dev = self.usb_out = self.usb_inp = None 341 | 342 | def _PYUSB1_close(self): 343 | if self.usb_dev is not None: 344 | self.usb_util.dispose_resources(self.usb_dev) 345 | self.usb_dev = self.usb_out = self.usb_inp = None 346 | 347 | -------------------------------------------------------------------------------- /nfc/ndef/wifi_record.py: -------------------------------------------------------------------------------- 1 | # -*- coding: latin-1 -*- 2 | # ----------------------------------------------------------------------------- 3 | # Copyright 2009-2012 Stephen Tiedemann 4 | # 5 | # Licensed under the EUPL, Version 1.1 or - as soon they 6 | # will be approved by the European Commission - subsequent 7 | # versions of the EUPL (the "Licence"); 8 | # You may not use this work except in compliance with the 9 | # Licence. 10 | # You may obtain a copy of the Licence at: 11 | # 12 | # http://www.osor.eu/eupl 13 | # 14 | # Unless required by applicable law or agreed to in 15 | # writing, software distributed under the Licence is 16 | # distributed on an "AS IS" basis, 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 18 | # express or implied. 19 | # See the Licence for the specific language governing 20 | # permissions and limitations under the Licence. 21 | # ----------------------------------------------------------------------------- 22 | # 23 | # WiFiSimpleConfig.py - parse or generate Wi-Fi configuration data 24 | # 25 | 26 | import logging 27 | log = logging.getLogger(__name__) 28 | 29 | import io 30 | import struct 31 | from record import Record 32 | from error import DecodeError, EncodeError 33 | 34 | VERSION1 = "\x10\x4A" 35 | CREDENTIAL = "\x10\x0e" 36 | AUTH_TYPE = "\x10\x03" 37 | CRYPT_TYPE = "\x10\x0F" 38 | MAC_ADDRESS = "\x10\x20" 39 | NETWORK_IDX = "\x10\x26" 40 | NETWORK_KEY = "\x10\x27" 41 | NETWORK_NAME = "\x10\x45" 42 | OOB_PASSWORD = "\x10\x2C" 43 | VENDOR_EXT = "\x10\x49" 44 | VENDOR_WFA = "\x00\x37\x2A" 45 | VERSION2 = "\x00" 46 | KEY_SHAREABLE = "\x02" 47 | 48 | auth_type_names = { 49 | '\x00\x01': 'Open', 50 | '\x00\x02': 'WPA-Personal', 51 | '\x00\x04': 'Shared', 52 | '\x00\x08': 'WPA-Enterprise', 53 | '\x00\x10': 'WPA2-Enterprise', 54 | '\x00\x20': 'WPA2-Personal', 55 | '\x00\x22': 'WPA/WPA2-Personal' 56 | } 57 | 58 | crypt_type_names = { 59 | '\x00\x01': 'None', 60 | '\x00\x02': 'WEP', 61 | '\x00\x04': 'TKIP', 62 | '\x00\x08': 'AES', 63 | '\x00\x0C': 'AES/TKIP' 64 | } 65 | 66 | auth_type_keys = \ 67 | dict([(v,k) for k,v in auth_type_names.iteritems()]) 68 | 69 | crypt_type_keys = \ 70 | dict([(v,k) for k,v in crypt_type_names.iteritems()]) 71 | 72 | class WifiConfigRecord(Record): 73 | def __init__(self, record=None): 74 | Record.__init__(self, 'application/vnd.wfa.wsc') 75 | self._version = '\x20' 76 | self._credentials = list() 77 | self._other = list() 78 | if record: 79 | if not record.type == self.type: 80 | raise ValueError("record type mismatch") 81 | self.name = record.name 82 | self.data = record.data 83 | else: 84 | self._credentials.append({ 85 | 'network-name': '', 86 | 'authentication' : 'Open', 87 | 'encryption' : 'None', 88 | 'network-key' : '', 89 | 'mac-address' : 'ff:ff:ff:ff:ff:ff' 90 | }) 91 | 92 | @property 93 | def data(self): 94 | f = io.BytesIO() 95 | write_attribute(f, VERSION1, '\x10') 96 | 97 | if len(self.credentials) == 0: 98 | log.warning("no credential(s) in wifi config record") 99 | 100 | for credential in self.credentials: 101 | write_attribute(f, CREDENTIAL, self._write_credential(credential)) 102 | 103 | vendor_wfa = [(VERSION2, self._version)] 104 | vendor_wfa.extend([(k, v) for k, v in self.other if len(k) == 1]) 105 | write_attribute(f, VENDOR_EXT, VENDOR_WFA + write_elements(vendor_wfa)) 106 | 107 | for k, v in [(k, v) for k, v in self.other if len(k) > 1]: 108 | write_attribute(f, k, v) 109 | 110 | f.seek(0, 0) 111 | return f.read() 112 | 113 | @data.setter 114 | def data(self, string): 115 | log.debug("parse '{0}' record".format(self.type)) 116 | if len(string) > 0: 117 | attributes = parse_attributes(string) 118 | log.debug("wifi attributes: " + repr(attributes)) 119 | for k, v in attributes: 120 | if k in (VERSION1, VERSION2): 121 | self._version = v 122 | elif k == CREDENTIAL: 123 | self._credentials.append(self._parse_credential(v)) 124 | else: 125 | self._other.append((k, v)) 126 | if len(self._credentials) == 0: 127 | raise DecodeError("missing credential attribute") 128 | 129 | @property 130 | def version(self): 131 | """The WiFi Simple Configuration version, coded as a 132 | 'major.minor' string""" 133 | version = ord(self._version) 134 | return "{0}.{1}".format(version >> 4, version & 0xF) 135 | 136 | @version.setter 137 | def version(self, value): 138 | try: 139 | major, minor = map(int, value.split('.')) 140 | except: 141 | raise TypeError("not a 'major.minor' version string") 142 | if major < 2 or major > 15: 143 | raise ValueError("major number must be in range(2,16)") 144 | if minor < 0 or minor > 15: 145 | raise ValueError("minor number must be in range(0,16)") 146 | self._version = chr((major << 4) | (minor & 0xF)) 147 | 148 | @property 149 | def credentials(self): 150 | """A list of WiFi credentials. Each credential is a dictionary 151 | with any of the possible keys ``'network-name'``, 152 | ``'network-key'``, ``'shareable'``, ``'authentication'``, 153 | ``'encryption'``, ``'mac-address'``, and ``'other'``.""" 154 | return self._credentials 155 | 156 | @property 157 | def credential(self): 158 | """The first WiFi credential. Same as 159 | ``WifiConfigRecord().credentials[0]``.""" 160 | return self.credentials[0] 161 | 162 | @property 163 | def other(self): 164 | """A list of WiFi attribute (key, value) pairs other than 165 | version and credential(s). Keys are two character strings for 166 | standard WiFi attributes, one character strings for 167 | subelements within a WFA vendor extension attribute, and three 168 | character strings for other vendor ecxtension attributes.""" 169 | return self._other 170 | 171 | def _parse_credential(self, s): 172 | attributes = parse_attributes(s) 173 | credential = dict() 174 | for k, v in attributes: 175 | if k == NETWORK_IDX: 176 | pass # attribute 'network index' is deprecated 177 | elif k == NETWORK_NAME: 178 | credential["network-name"] = v 179 | elif k == NETWORK_KEY: 180 | credential["network-key"] = v 181 | elif k == KEY_SHAREABLE: 182 | credential['shareable'] = bool(ord(v)) 183 | elif k == AUTH_TYPE: 184 | credential['authentication'] = \ 185 | auth_type_names.get(v, v.encode('hex')) 186 | elif k == CRYPT_TYPE: 187 | credential['encryption'] = \ 188 | crypt_type_names.get(v, v.encode('hex')) 189 | elif k == MAC_ADDRESS: 190 | credential['mac-address'] = \ 191 | ':'.join([c.encode('hex') for c in v]) 192 | else: 193 | credential.setdefault('other', []).append((k, v)) 194 | return credential 195 | 196 | def _write_credential(self, credential): 197 | f = io.BytesIO() 198 | try: 199 | network_name = credential['network-name'] 200 | auth_type = credential['authentication'] 201 | crypt_type = credential['encryption'] 202 | network_key = credential['network-key'] 203 | mac_address = credential['mac-address'] 204 | shareable = credential.get('shareable', None) 205 | other = credential.get('other', list()) 206 | except KeyError: 207 | raise EncodeError("missing required credential attribute") 208 | 209 | try: auth_type = auth_type_keys[auth_type] 210 | except KeyError: auth_type = auth_type.decode('hex') 211 | try: crypt_type = crypt_type_keys[crypt_type] 212 | except KeyError: crypt_type = crypt_type.decode('hex') 213 | mac_address = mac_address.replace(':', '').decode('hex') 214 | 215 | write_attribute(f, NETWORK_IDX, '\x01') 216 | write_attribute(f, NETWORK_NAME, network_name) 217 | write_attribute(f, AUTH_TYPE, auth_type) 218 | write_attribute(f, CRYPT_TYPE, crypt_type) 219 | write_attribute(f, NETWORK_KEY, network_key) 220 | write_attribute(f, MAC_ADDRESS, mac_address) 221 | 222 | vendor_wfa = [(k, v) for k, v in other if len(k) == 1] 223 | if shareable is not None: 224 | vendor_wfa = [(KEY_SHAREABLE, chr(int(shareable)))] + vendor_wfa 225 | if len(vendor_wfa) > 0: 226 | write_attribute(f, VENDOR_EXT, VENDOR_WFA + 227 | write_elements(vendor_wfa)) 228 | 229 | for k, v in [(k, v) for k, v in other if len(k) > 1]: 230 | write_attribute(f, k, v) 231 | 232 | f.seek(0, 0) 233 | return f.read() 234 | 235 | def pretty(self, indent=0): 236 | lines = list() 237 | if self.name: 238 | lines.append(("identifier", repr(self.name))) 239 | lines.append(("version", self.version)) 240 | for credential in self.credentials: 241 | shareable = str(credential.get('shareable', False)) 242 | lines.append(("network name", credential['network-name'])) 243 | lines.append(("network key", credential['network-key'])) 244 | lines.append(("authentication", credential['authentication'])) 245 | lines.append(("encryption", credential['encryption'])) 246 | lines.append(("mac address", credential['mac-address'])) 247 | lines.append(("shareable", shareable)) 248 | for key, value in self.other: 249 | lines.append((key, value)) 250 | 251 | indent = indent * ' ' 252 | lwidth = max([len(line[0]) for line in lines]) 253 | lines = [line[0].ljust(lwidth) + " = " + line[1] for line in lines] 254 | return ("\n").join([indent + line for line in lines]) 255 | 256 | class WifiPasswordRecord(Record): 257 | def __init__(self, record=None): 258 | Record.__init__(self, 'application/vnd.wfa.wsc') 259 | self._version = '\x20' 260 | self._passwords = list() 261 | self._other = list() 262 | if record: 263 | if not record.type == self.type: 264 | raise ValueError("record type mismatch") 265 | self.name = record.name 266 | self.data = record.data 267 | else: 268 | self._passwords.append({ 269 | 'public-key-hash': 20 * '\x00', 270 | 'password-id' : 0, 271 | 'password' : '', 272 | }) 273 | 274 | @property 275 | def data(self): 276 | f = io.BytesIO() 277 | write_attribute(f, VERSION1, '\x10') 278 | 279 | for password in self.passwords: 280 | write_attribute(f, OOB_PASSWORD, self._write_password(password)) 281 | 282 | vendor_wfa = [(VERSION2, self._version)] 283 | vendor_wfa.extend([(k, v) for k, v in self.other if len(k) == 1]) 284 | write_attribute(f, VENDOR_EXT, VENDOR_WFA + write_elements(vendor_wfa)) 285 | 286 | for k, v in [(k, v) for k, v in self.other if len(k) > 1]: 287 | write_attribute(f, k, v) 288 | 289 | f.seek(0, 0) 290 | return f.read() 291 | 292 | @data.setter 293 | def data(self, string): 294 | log.debug("parse '{0}' record".format(self.type)) 295 | if len(string) > 0: 296 | attributes = parse_attributes(string) 297 | log.debug("wifi attributes: " + repr(attributes)) 298 | for k, v in attributes: 299 | if k in (VERSION1, VERSION2): 300 | self._version = v 301 | elif k == OOB_PASSWORD: 302 | self._passwords.append(self._parse_password(v)) 303 | else: 304 | self._other.append((k, v)) 305 | if len(self._passwords) == 0: 306 | raise DecodeError("missing password attribute") 307 | 308 | @property 309 | def version(self): 310 | """The WiFi Simple Configuration version, coded as a 311 | 'major.minor' string""" 312 | version = ord(self._version) 313 | return "{0}.{1}".format(version >> 4, version & 0xF) 314 | 315 | @version.setter 316 | def version(self, value): 317 | try: 318 | major, minor = map(int, value.split('.')) 319 | except: 320 | raise TypeError("not a 'major.minor' version string") 321 | if major < 2 or major > 15: 322 | raise ValueError("major number must be in range(2,16)") 323 | if minor < 0 or minor > 15: 324 | raise ValueError("minor number must be in range(0,16)") 325 | self._version = chr((major << 4) | (minor & 0xF)) 326 | 327 | @property 328 | def passwords(self): 329 | """A list of WiFi out-of-band device passwords. Each password 330 | is a dictionary with the keys ``'public-key-hash'``, 331 | ``'password-id'``, and ``'password'``.""" 332 | return self._passwords 333 | 334 | @property 335 | def password(self): 336 | """The first WiFi device password. Same as 337 | ``WifiPasswordRecord().passwords[0]``.""" 338 | return self.passwords[0] 339 | 340 | @property 341 | def other(self): 342 | """A list of WiFi attribute (key, value) pairs other than 343 | version and device password. Keys are two character strings 344 | for standard WiFi attributes, one character strings for 345 | subelements within a WFA vendor extension attribute, and three 346 | character strings for other vendor extension attributes.""" 347 | return self._other 348 | 349 | def _parse_password(self, s): 350 | if len(s) < 22: 351 | raise DecodeError("wifi oob password less than 22 byte") 352 | password = dict() 353 | password['public-key-hash'] = s[0:20] 354 | password['password-id'] = struct.unpack('>H', s[20:22])[0] 355 | password['password'] = s[22:] 356 | return password 357 | 358 | def _write_password(self, password): 359 | f = io.BytesIO() 360 | try: 361 | pkhash = password['public-key-hash'] 362 | pwd_id = password['password-id'] 363 | passwd = password['password'] 364 | except KeyError: 365 | raise EncodeError("missing required attributes in oob password") 366 | if len(pkhash) != 20: 367 | raise EncodeError("public key hash must be 20 octets") 368 | f.write(pkhash + struct.pack('>H', pwd_id) + passwd) 369 | f.seek(0, 0) 370 | return f.read() 371 | 372 | def pretty(self, indent=0): 373 | lines = list() 374 | if self.name: 375 | lines.append(("identifier", repr(self.name))) 376 | lines.append(("version", self.version)) 377 | for password in self.passwords: 378 | public_key_hash = password['public-key-hash'].encode("hex") 379 | lines.append(("public key hash", public_key_hash)) 380 | lines.append(("password id", str(password['password-id']))) 381 | lines.append(("device password", password['password'])) 382 | for key, value in self.other: 383 | lines.append((key, value)) 384 | 385 | indent = indent * ' ' 386 | lwidth = max([len(line[0]) for line in lines]) 387 | lines = [line[0].ljust(lwidth) + " = " + line[1] for line in lines] 388 | return ("\n").join([indent + line for line in lines]) 389 | 390 | # -------------------------------------- helper functions for attribute parsing 391 | def parse_attribute(f): 392 | k, l = struct.unpack('>2sH', f.read(4)) 393 | v = f.read(l) 394 | if len(v) != l: 395 | raise DecodeError("wsc attribute length error") 396 | return k, v 397 | 398 | def parse_attributes(s): 399 | f = io.BytesIO(s) 400 | l = list() 401 | while f.tell() < len(s): 402 | k, v = parse_attribute(f) 403 | if k == VENDOR_EXT: 404 | k, v = v[:3], v[3:] 405 | if k == VENDOR_WFA: 406 | l.extend(parse_elements(v)) 407 | else: 408 | l.append([k, v]) 409 | return l 410 | 411 | def parse_element(f): 412 | k, l = struct.unpack(">cB", f.read(2)); v = f.read(l) 413 | if len(v) != l: 414 | raise DecodeError("wfa subelement length error") 415 | return k, v 416 | 417 | def parse_elements(s): 418 | f = io.BytesIO(s) 419 | l = list() 420 | while f.tell() < len(s): 421 | k, v = parse_element(f) 422 | l.append([k, v]) 423 | return l 424 | 425 | def write_attribute(f, k, v): 426 | f.write(struct.pack('>2sH', k, len(v)) + v) 427 | 428 | def write_element(f, k, v): 429 | f.write(struct.pack('>cB', k, len(v)) + v) 430 | 431 | def write_elements(kvl): 432 | f = io.BytesIO() 433 | for k, v in kvl: 434 | write_element(f, k, v) 435 | f.seek(0, 0) 436 | return f.read() 437 | 438 | -------------------------------------------------------------------------------- /nfc/tag/tt3.py: -------------------------------------------------------------------------------- 1 | # -*- coding: latin-1 -*- 2 | # ----------------------------------------------------------------------------- 3 | # Copyright 2009-2013 Stephen Tiedemann 4 | # 5 | # Licensed under the EUPL, Version 1.1 or - as soon they 6 | # will be approved by the European Commission - subsequent 7 | # versions of the EUPL (the "Licence"); 8 | # You may not use this work except in compliance with the 9 | # Licence. 10 | # You may obtain a copy of the Licence at: 11 | # 12 | # http://www.osor.eu/eupl 13 | # 14 | # Unless required by applicable law or agreed to in 15 | # writing, software distributed under the Licence is 16 | # distributed on an "AS IS" basis, 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 18 | # express or implied. 19 | # See the Licence for the specific language governing 20 | # permissions and limitations under the Licence. 21 | # ----------------------------------------------------------------------------- 22 | 23 | import logging 24 | log = logging.getLogger(__name__) 25 | 26 | import nfc.tag 27 | import nfc.clf 28 | import nfc.ndef 29 | 30 | ndef_read_service = 11 # service code for NDEF reading 31 | ndef_write_service = 9 # service code for NDEF writing 32 | 33 | def trace(func): 34 | def traced_func(*args, **kwargs): 35 | _args = "{0}".format(args[1:]).strip("(),") 36 | if kwargs: 37 | _args = ', '.join([_args, "{0}".format(kwargs).strip("{}")]) 38 | log.debug("{func}({args})".format(func=func.__name__, args=_args)) 39 | return func(*args, **kwargs) 40 | return traced_func 41 | 42 | class NdefAttributeData: 43 | def __init__(self, init=16): 44 | attr = bytearray(init) 45 | self.version = "{0}.{1}".format(attr[0] >> 4, attr[0] & 15) 46 | self.nbr = attr[1] 47 | self.nbw = attr[2] 48 | self.capacity = (attr[3] * 256 + attr[4]) * 16 49 | self.rfu = attr[5:9] 50 | self.writing = bool(attr[9]) 51 | self.writeable = bool(attr[10]) 52 | self.length = attr[11]<<16 | attr[12]<<8 | attr[13] 53 | self.checksum = attr[14:16] 54 | self.valid = sum(attr[0:14]) == attr[14] << 8 | attr[15] 55 | self.wf = 0x0F 56 | self.rw = 0x01 57 | 58 | def __str__(self): 59 | attr = bytearray(16) 60 | vers = map(lambda x: int(x) & 15, self.version.split('.')) 61 | maxb = ((self.capacity + 15) // 16) & 0xffff 62 | attr[0] = vers[0] << 4 | vers[1] 63 | attr[1] = self.nbr 64 | attr[2] = self.nbw 65 | attr[3] = maxb >> 8 66 | attr[4] = maxb & 0xff 67 | attr[5:9] = self.rfu 68 | attr[9] = self.wf if self.writing else 0 69 | attr[10] = self.rw if self.writeable else 0 70 | attr[11] = self.length >> 16 & 0xff 71 | attr[12] = self.length >> 8 & 0xff 72 | attr[13] = self.length & 0xff 73 | checksum = sum(attr[0:14]) 74 | attr[14] = checksum >> 8 75 | attr[15] = checksum & 0xff 76 | return str(attr) 77 | 78 | def pretty(self): 79 | return ("Ver={a.version!r} Nbr={a.nbr} Nbw={a.nbw} Nmaxb={nmaxb} " 80 | "WF={a.wf:02X}h RW={a.rw:02X}h Ln={a.length} Checksum={cs}h" 81 | .format(a=self, nmaxb=self.capacity/16, 82 | cs=str(self.checksum).encode("hex").upper())) 83 | 84 | class NDEF(object): 85 | def __init__(self, tag): 86 | self.tag = tag 87 | self._attr = None 88 | self._data = '' 89 | if not self.attr.valid: 90 | raise ValueError("invalid ndef attribute block") 91 | self.changed # force initial read 92 | 93 | @property 94 | def version(self): 95 | """The version of the NDEF mapping.""" 96 | return self.attr.version 97 | 98 | @property 99 | def capacity(self): 100 | """The maximum number of user bytes on the NDEF tag.""" 101 | return self.attr.capacity 102 | 103 | @property 104 | def readable(self): 105 | """Is True if data can be read from the NDEF tag.""" 106 | return self.attr.nbr > 0 107 | 108 | @property 109 | def writeable(self): 110 | """Is True if data can be written to the NDEF tag.""" 111 | return self.attr.writeable and self.attr.nbw > 0 112 | 113 | @property 114 | def length(self): 115 | """NDEF message data length.""" 116 | return len(self._data) 117 | 118 | @property 119 | def attr(self): 120 | if self._attr is None: 121 | self._attr = NdefAttributeData(self.tag.read(blocks=[0])) 122 | if not self._attr.valid: 123 | log.error("checksum error in ndef attribute block") 124 | return self._attr 125 | 126 | @property 127 | def changed(self): 128 | """True if the message has changed since last read.""" 129 | self._attr = None 130 | blocks = range(1, (self.attr.length + 15) / 16 + 1) 131 | data = "" 132 | while len(blocks) > self.attr.nbr: 133 | block_list = blocks[0:self.attr.nbr] 134 | data += self.tag.read(blocks[0:self.attr.nbr]) 135 | del blocks[0:self.attr.nbr] 136 | if len(blocks) > 0: 137 | data += self.tag.read(blocks) 138 | old_data, self._data = self._data, data[0:self.attr.length] 139 | return self._data != old_data 140 | 141 | @property 142 | def message(self): 143 | """An NDEF message object (an empty record message if tag is empty).""" 144 | try: return nfc.ndef.Message(str(self._data)) 145 | except nfc.ndef.parser_error: pass 146 | return nfc.ndef.Message(nfc.ndef.Record()) 147 | 148 | @message.setter 149 | def message(self, msg): 150 | if not self.writeable: 151 | raise nfc.tag.AccessError 152 | 153 | data = str(msg) 154 | if len(data) > self.capacity: 155 | raise nfc.tag.CapacityError 156 | 157 | self.attr.writing = True 158 | self.attr.length = len(data) 159 | self.tag.write(str(self.attr), [0]) 160 | 161 | blocks = range(1, (len(data)+15)/16 + 1) 162 | nb_max = self.attr.nbw # blocks to write at once 163 | length = nb_max * 16 # bytes to write at once 164 | offset = 0 165 | while len(blocks) > nb_max: 166 | self.tag.write(data[offset:offset+length], blocks[0:nb_max]) 167 | del blocks[0:nb_max] 168 | offset += length 169 | if len(blocks) > 0: 170 | data += (-len(data) % 16) * '\x00' 171 | self.tag.write(data[offset:], blocks) 172 | 173 | self.attr.writing = False # writing finished 174 | self.tag.write(str(self.attr), [0]) 175 | 176 | class Type3Tag(object): 177 | type = "Type3Tag" 178 | 179 | def __init__(self, clf, target): 180 | self.clf = clf 181 | self.idm = target.idm 182 | self.pmm = target.pmm 183 | self.sys = target.sys 184 | 185 | if self.sys != "\x12\xFC" and self.pmm[0:2] != "\x01\xE0": 186 | idm, pmm = self.poll(0x12FC) 187 | if idm is not None and pmm is not None: 188 | self.sys = bytearray([0x12, 0xFC]) 189 | self.idm, self.pmm = idm, pmm 190 | 191 | rto, wto = self.pmm[5], self.pmm[6] 192 | self.rto = ((rto&0x07)+1, (rto>>3&0x07)+1, 302E-6 * 4**(rto >> 6)) 193 | self.wto = ((wto&0x07)+1, (wto>>3&0x07)+1, 302E-6 * 4**(wto >> 6)) 194 | 195 | try: 196 | self.ndef = NDEF(self) if self.sys == "\x12\xFC" else None 197 | except Exception as error: 198 | log.error("while reading ndef: {0!r}".format(error)) 199 | self.ndef = None 200 | 201 | def __str__(self): 202 | params = list() 203 | params.append(str(self.idm).encode("hex")) 204 | params.append(str(self.pmm).encode("hex")) 205 | params.append(str(self.sys).encode("hex")) 206 | return "Type3Tag IDm=%s PMm=%s SYS=%s" % tuple(params) 207 | 208 | @property 209 | def is_present(self): 210 | """True if the tag is still within communication range.""" 211 | rto = ((self.rto[0] + self.rto[1]) * self.rto[2]) + 5E-3 212 | try: 213 | cmd = "\x04" + self.idm 214 | return bool(self.clf.exchange(chr(len(cmd)+1) + cmd, timeout=rto)) 215 | except nfc.clf.TimeoutError: pass 216 | except nfc.clf.TransmissionError: return False 217 | 218 | try: 219 | cmd = "\x00" + self.sys + "\x00\x00" 220 | return bool(self.clf.exchange(chr(len(cmd)+1) + cmd, timeout=rto)) 221 | except nfc.clf.TimeoutError: pass 222 | except nfc.clf.TransmissionError: return False 223 | 224 | return False 225 | 226 | def poll(self, system_code): 227 | """Send the polling command to recognize a system on the card. The 228 | *system_code* may be specified as a short integer or as a string or 229 | bytearray of length 2. The return value is the tuple of the two 230 | bytearrays (idm, pmm) if the requested system is present or the tuple 231 | (None, None) if not.""" 232 | 233 | if isinstance(system_code, int): 234 | system_code = bytearray([system_code/256, system_code%256]) 235 | 236 | log.debug("poll for system {0}".format(str(system_code).encode("hex"))) 237 | cmd = bytearray("\x06\x00" + system_code + "\x00\x00") 238 | 239 | try: 240 | rsp = self.clf.exchange(cmd, timeout=0.01) 241 | except nfc.clf.TimeoutError as error: 242 | return None, None 243 | except nfc.clf.DigitalProtocolError as error: 244 | raise IOError(repr(error)) 245 | if not rsp.startswith(chr(len(rsp)) + "\x01"): 246 | raise IOError("tt3 response error") 247 | 248 | log.debug("<<< {0}".format(str(rsp).encode("hex"))) 249 | return rsp[2:10], rsp[10:18] 250 | 251 | def read(self, blocks, service=ndef_read_service): 252 | """Read service data blocks from tag. The *service* argument is the 253 | tag type 3 service code to use, 0x000b for reading NDEF. The *blocks* 254 | argument holds a list of integers representing the block numbers to 255 | read. The data is returned as a character string.""" 256 | 257 | log.debug("read blocks {1} from service {0}".format(service, blocks)) 258 | cmd = "\x06" + self.idm # ReadWithoutEncryption 259 | cmd += "\x01" + ("%02X%02X" % (service%256,service/256)).decode("hex") 260 | cmd += chr(len(blocks)) 261 | for block in blocks: 262 | if block < 256: cmd += "\x80" + chr(block) 263 | else: cmd += "\x00" + chr(block%256) + chr(block/256) 264 | rto = ((self.rto[0] + self.rto[1] * len(blocks)) * self.rto[2]) + 5E-3 265 | log.debug("read timeout is {0} sec".format(rto)) 266 | try: 267 | rsp = self.clf.exchange(chr(len(cmd)+1) + cmd, timeout=rto) 268 | except nfc.clf.DigitalProtocolError as error: 269 | raise IOError(repr(error)) 270 | if not rsp.startswith(chr(len(rsp)) + "\x07" + self.idm): 271 | raise IOError("tt3 response error") 272 | if rsp[10] != 0 or rsp[11] != 0: 273 | raise IOError("tt3 cmd error {0:02x} {1:02x}".format(*rsp[10:12])) 274 | data = str(rsp[13:]) 275 | log.debug("<<< {0}".format(data.encode("hex"))) 276 | return data 277 | 278 | def write(self, data, blocks, service=ndef_write_service): 279 | """Write service data blocks to tag. The *service* argument is the 280 | tag type 3 service code to use, 0x0009 for writing NDEF. The *blocks* 281 | argument holds a list of integers representing the block numbers to 282 | write. The *data* argument must be a character string with length 283 | equal to the number of blocks times 16.""" 284 | 285 | log.debug("write blocks {1} to service {0}".format(service, blocks)) 286 | if len(data) != len(blocks) * 16: 287 | log.error("data length does not match block-count * 16") 288 | raise ValueError("invalid data length for given number of blocks") 289 | log.debug(">>> {0}".format(str(data).encode("hex"))) 290 | cmd = "\x08" + self.idm # ReadWithoutEncryption 291 | cmd += "\x01" + ("%02X%02X" % (service%256,service/256)).decode("hex") 292 | cmd += chr(len(blocks)) 293 | for block in blocks: 294 | if block < 256: cmd += "\x80" + chr(block) 295 | else: cmd += "\x00" + chr(block%256) + chr(block/256) 296 | cmd += data 297 | wto = ((self.wto[0] + self.wto[1] * len(blocks)) * self.wto[2]) + 5E-3 298 | log.debug("write timeout is {0} sec".format(wto)) 299 | try: 300 | rsp = self.clf.exchange(chr(len(cmd)+1)+cmd, timeout=wto) 301 | except nfc.clf.TimeoutError: 302 | raise IOError("communication timeout") 303 | if not rsp.startswith(chr(len(rsp)) + "\x09" + self.idm): 304 | raise IOError("tt3 response error") 305 | if rsp[10] != 0 or rsp[11] != 0: 306 | raise IOError("tt3 cmd error {0:02x} {1:02x}".format(*rsp[10:12])) 307 | 308 | class Type3TagEmulation(object): 309 | def __init__(self, clf, target): 310 | self.clf = clf 311 | self.idm = target.idm 312 | self.pmm = target.pmm 313 | self.sys = target.sys 314 | self.services = dict() 315 | 316 | def __str__(self): 317 | return "Type3TagEmulation IDm={0} PMm={1} SYS={2}".format( 318 | str(self.idm).encode("hex"), str(self.pmm).encode("hex"), 319 | str(self.sys).encode("hex")) 320 | 321 | def add_service(self, service_code, block_read_func, block_write_func): 322 | self.services[service_code] = (block_read_func, block_write_func) 323 | 324 | def process_command(self, cmd): 325 | log.debug("cmd: " + (str(cmd).encode("hex") if cmd else str(cmd))) 326 | if len(cmd) != cmd[0]: 327 | log.error("tt3 command length error") 328 | return None 329 | if tuple(cmd[0:4]) in [(6, 0, 255, 255), (6, 0) + tuple(self.sys)]: 330 | log.debug("process 'polling' command") 331 | rsp = self.polling(cmd[2:]) 332 | return bytearray([2 + len(rsp), 0x01]) + rsp 333 | if cmd[2:10] == self.idm: 334 | if cmd[1] == 0x04: 335 | log.debug("process 'request response' command") 336 | rsp = self.request_response(cmd[10:]) 337 | return bytearray([10 + len(rsp), 0x05]) + self.idm + rsp 338 | if cmd[1] == 0x06: 339 | log.debug("process 'read without encryption' command") 340 | rsp = self.read_without_encryption(cmd[10:]) 341 | return bytearray([10 + len(rsp), 0x07]) + self.idm + rsp 342 | if cmd[1] == 0x08: 343 | log.debug("process 'write without encryption' command") 344 | rsp = self.write_without_encryption(cmd[10:]) 345 | return bytearray([10 + len(rsp), 0x09]) + self.idm + rsp 346 | if cmd[1] == 0x0C: 347 | log.debug("process 'request system code' command") 348 | rsp = self.request_system_code(cmd[10:]) 349 | return bytearray([10 + len(rsp), 0x0D]) + self.idm + rsp 350 | 351 | def send_response(self, rsp, timeout): 352 | if rsp: log.debug("rsp: " + str(rsp).encode("hex")) 353 | return self.clf.exchange(rsp, timeout) 354 | 355 | def polling(self, cmd_data): 356 | if cmd_data[2] == 1: 357 | rsp = self.idm + self.pmm + self.sys 358 | else: 359 | rsp = self.idm + self.pmm 360 | return rsp 361 | 362 | def request_response(self, cmd_data): 363 | return bytearray([0]) 364 | 365 | def read_without_encryption(self, cmd_data): 366 | service_list = cmd_data.pop(0) * [[None, None]] 367 | for i in range(len(service_list)): 368 | service_code = cmd_data[1] << 8 | cmd_data[0] 369 | if not service_code in self.services.keys(): 370 | return bytearray([0xFF, 0xA1]) 371 | service_list[i] = [service_code, 0] 372 | del cmd_data[0:2] 373 | 374 | service_block_list = cmd_data.pop(0) * [None] 375 | if len(service_block_list) > 15: 376 | return bytearray([0xFF, 0xA2]) 377 | for i in range(len(service_block_list)): 378 | try: 379 | service_list_item = service_list[cmd_data[0] & 0x0F] 380 | service_code = service_list_item[0] 381 | service_list_item[1] += 1 382 | except IndexError: 383 | return bytearray([1<<(i%8), 0xA3]) 384 | if cmd_data[0] >= 128: 385 | block_number = cmd_data[1] 386 | del cmd_data[0:2] 387 | else: 388 | block_number = cmd_data[2] << 8 | cmd_data[1] 389 | del cmd_data[0:3] 390 | service_block_list[i] = [service_code, block_number, 0] 391 | 392 | service_block_count = dict(service_list) 393 | for service_block_list_item in service_block_list: 394 | service_code = service_block_list_item[0] 395 | service_block_list_item[2] = service_block_count[service_code] 396 | 397 | block_data = bytearray() 398 | for i, service_block_list_item in enumerate(service_block_list): 399 | service_code, block_number, block_count = service_block_list_item 400 | # rb (read begin) and re (read end) mark an atomic read 401 | rb = bool(block_count == service_block_count[service_code]) 402 | service_block_count[service_code] -= 1 403 | re = bool(service_block_count[service_code] == 0) 404 | read_func, write_func = self.services[service_code] 405 | one_block_data = read_func(block_number, rb, re) 406 | if one_block_data is None: 407 | return bytearray([1<<(i%8), 0xA2, 0]) 408 | block_data.extend(one_block_data) 409 | 410 | return bytearray([0, 0, len(block_data)/16]) + block_data 411 | 412 | def write_without_encryption(self, cmd_data): 413 | service_list = cmd_data.pop(0) * [[None, None]] 414 | for i in range(len(service_list)): 415 | service_code = cmd_data[1] << 8 | cmd_data[0] 416 | if not service_code in self.services.keys(): 417 | return bytearray([255, 0xA1]) 418 | service_list[i] = [service_code, 0] 419 | del cmd_data[0:2] 420 | 421 | service_block_list = cmd_data.pop(0) * [None] 422 | for i in range(len(service_block_list)): 423 | try: 424 | service_list_item = service_list[cmd_data[0] & 0x0F] 425 | service_code = service_list_item[0] 426 | service_list_item[1] += 1 427 | except IndexError: 428 | return bytearray([1<<(i%8), 0xA3]) 429 | if cmd_data[0] >= 128: 430 | block_number = cmd_data[1] 431 | del cmd_data[0:2] 432 | else: 433 | block_number = cmd_data[2] << 8 | cmd_data[1] 434 | del cmd_data[0:3] 435 | service_block_list[i] = [service_code, block_number, 0] 436 | 437 | service_block_count = dict(service_list) 438 | for service_block_list_item in service_block_list: 439 | service_code = service_block_list_item[0] 440 | service_block_list_item[2] = service_block_count[service_code] 441 | 442 | block_data = cmd_data[0:] 443 | if len(block_data) % 16 != 0: 444 | return bytearray([255, 0xA2]) 445 | 446 | for i, service_block_list_item in enumerate(service_block_list): 447 | service_code, block_number, block_count = service_block_list_item 448 | # wb (write begin) and we (write end) mark an atomic write 449 | wb = bool(block_count == service_block_count[service_code]) 450 | service_block_count[service_code] -= 1 451 | we = bool(service_block_count[service_code] == 0) 452 | read_func, write_func = self.services[service_code] 453 | if not write_func(block_number, block_data[i*16:(i+1)*16], wb, we): 454 | return bytearray([1<<(i%8), 0xA2, 0]) 455 | 456 | return bytearray([0, 0]) 457 | 458 | @trace 459 | def request_system_code(self, cmd_data): 460 | return '\x01' + self.sys 461 | --------------------------------------------------------------------------------