rfskiosk2 0.4.20__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,30 @@
1
+ Metadata-Version: 2.1
2
+ Name: rfskiosk2
3
+ Version: 0.4.20
4
+ Summary: RFS Kiosk
5
+ License: MIT
6
+ Keywords: kiosk,tag,nfc,rfs
7
+ Author: ian
8
+ Author-email: ibowditch@gmail.com
9
+ Requires-Python: >=3.8,<4.0
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Requires-Dist: click (>=8.0.0,<9.0.0)
19
+ Requires-Dist: extended-configparser
20
+ Requires-Dist: libusb1 (>=3.1.0,<4.0.0)
21
+ Requires-Dist: logging_journald
22
+ Requires-Dist: nfcpy
23
+ Requires-Dist: py-dateutil
24
+ Requires-Dist: requests (==2.32.3)
25
+ Requires-Dist: simpleaudio
26
+ Requires-Dist: systemd-python
27
+ Requires-Dist: websocket-client
28
+ Description-Content-Type: text/markdown
29
+
30
+
File without changes
@@ -0,0 +1,37 @@
1
+ [tool.poetry]
2
+ name = "rfskiosk2"
3
+ version = "0.4.20"
4
+ description = "RFS Kiosk"
5
+ authors = ["ian <ibowditch@gmail.com>"]
6
+ readme = "README.md"
7
+ license = "MIT"
8
+ keywords = ["kiosk", "tag", "nfc", "rfs"]
9
+
10
+ # These end up in
11
+ #./.local/pipx/venvs/example-command-ib6/lib/python3.9/site-packages/rfskiosk2/envfiles
12
+ include = ["rfskiosk2/envfiles/*"]
13
+
14
+ [tool.poetry.dependencies]
15
+ python = "^3.8"
16
+ click = "^8.0.0"
17
+ requests = "2.32.3"
18
+ libusb1 = "^3.1.0"
19
+ py-dateutil="*"
20
+ logging_journald="*"
21
+ systemd-python="*"
22
+ nfcpy="*"
23
+ extended-configparser="*"
24
+ websocket-client="*"
25
+ simpleaudio="*"
26
+
27
+ [build-system]
28
+ requires = ["poetry-core"]
29
+ build-backend = "poetry.core.masonry.api"
30
+
31
+ [tool.poetry.scripts]
32
+ kiosk_beeper = "rfskiosk2.kiosk_beeper:main"
33
+ nfcreader = "rfskiosk2.nfcreader3:main"
34
+
35
+
36
+
37
+
File without changes
@@ -0,0 +1,10 @@
1
+ # Default settings for kiosk on all systems. Can be overridden in ~/.config/rfstag/local.env
2
+ NFC_INI_FILE=/home/pi/.config/rfstag/nfcreader.ini
3
+ BUSHFIRE_SERVER=rfstag.com
4
+ KIOSK_LOCATION=station
5
+ # This determines position of second HDMI screen if present.
6
+ HDMI2_X_OFFSET=1920
7
+ # Location of chrome Preferences files - used to avoid Restore? on restart
8
+ ACTIV_CHROME_PROFILE=/home/pi/Documents/Profiles/1
9
+ # Default location for ACTIV dashboard
10
+ ACTIV_URL=https://activ.rfs.nsw.gov.au/webapp/dashboard/
@@ -0,0 +1,9 @@
1
+ # This allows system defaults to be overridden if needed (rarely necessary, except in test env or multiple kiosks)
2
+ # Brigade name normally same as PI HOST name. Override here if that's not the case.
3
+ #KIOSK_BRIGADE=mybrigade
4
+ # Default location is 'station'. If multiple kiosks are present, they should all have different location names.
5
+ #KIOSK_LOCATION=home
6
+ # Production server rfstag.com is default. Anything else is for testing purposes only.
7
+ #BUSHFIRE_SERVER=rfstag.org
8
+ # Basic INI details
9
+ NFC_INI_FILE=/home/pi/.config/rfstag/nfcreader.ini
@@ -0,0 +1,21 @@
1
+ # Configuration file for bushfire nfc reader
2
+ [DEFAULT]
3
+
4
+ # KIOSK_LOCATION: station
5
+
6
+ # URI for sending signin request
7
+ TAGGING_URI: <signin>>
8
+
9
+ # URI to start up kiosk in browser
10
+ KIOSK_URI: <start kiosk>>
11
+
12
+ # URI to check server connection, and retry delay
13
+ # CHECKSERV_URI: indexaws
14
+ CHECKSERV_URI: <check server>>
15
+ SERV_RETRY: 2
16
+
17
+ # URI to lodge new event request
18
+ PAGER_URI: <incident>>
19
+
20
+ # Sub-directory of $HOME_DIR
21
+ SOUNDS: Music
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/python3
2
+
3
+ """
4
+ This connects to the bushfire server by opening a websocket to the channel used by the kiosk browser.
5
+ It scans incoming messages, and if appropriate, will play a sound associated with the message.
6
+ Current sounds supported are:
7
+ 'signin' : Member has signed in to an event
8
+ 'signout'' : Member has signed out of an event
9
+ 'noevent' : No events are available, so sign-in is not possible
10
+ 'invalid' : The tag is invalid, or may not be a member of this brigade
11
+ 'choose' ; The Member needs to select from a list of events on the kiosk
12
+ Corresponding sound files must be available for this work
13
+
14
+ This module is normally executed as part of kiosk_beeper.service, in the nfcserver3 package.
15
+ It requires environment variables set in the following files, in order or precedence:
16
+ # Set required env variables.
17
+ EnvironmentFile=/etc/profile.d/rfstag/base.env
18
+ # Allow local override of these env vars - Optional
19
+ EnvironmentFile=-/home/pi/.config/rfstag/local.env
20
+ Templates for these are provided in the nfcreader3 package.
21
+
22
+ """
23
+
24
+
25
+ import websocket
26
+ from websocket._exceptions import *
27
+ import logging
28
+ from systemd.journal import JournalHandler
29
+ import json
30
+ import socket
31
+ import os
32
+ import sys
33
+ import configparser
34
+ from dataclasses import dataclass
35
+ import simpleaudio as sa
36
+ from pathlib import Path
37
+
38
+ SOUNDS = ['signin', 'signout', 'noevent', 'invalid', 'choose']
39
+ # Map received 'stat' to a sound file
40
+ SOUNDS_MAP = {'in': 'signin', 'out': 'signout', 'choose': 'choose', 'invalid': 'invalid', 'novent': 'noevent'}
41
+
42
+ LOGGING_LEVEL = logging.DEBUG # TODO change to INFO in production
43
+
44
+ SYSLOG_ID = Path(__file__).stem # 'kiosk_beeper'
45
+ log = logging.getLogger(SYSLOG_ID)
46
+
47
+ # Maps incoming messages on kiosk_ channel to sound files.
48
+ STAT_MAP = {'tagIn': 'in', 'tagOut': 'out', 'choose': 'choose', 'invalid': 'invalid', 'none': 'none'}
49
+
50
+ class CaseSensitiveConfigParser(configparser.ConfigParser):
51
+ def optionxform(self, optionstr):
52
+ # Override the optionxform method to prevent case folding
53
+ return optionstr
54
+
55
+ # Read external settings from .ini file, using section named for the current host
56
+ def getSettings(curr_host, ini_file):
57
+ # Create a ConfigParser instance
58
+ config = CaseSensitiveConfigParser()
59
+
60
+ # Read the .ini file
61
+ config.read(ini_file)
62
+ cdict = dict(config['DEFAULT'])
63
+ return cdict
64
+
65
+ nset2 = getSettings(socket.gethostname(), os.environ.get('NFC_INI_FILE'))
66
+
67
+ @dataclass
68
+ class KioskConfig:
69
+ # APT package details
70
+ package_name: str = SYSLOG_ID
71
+ # In most cases, the host name will be the brigade/tenant name, but allow for override with env var when needed.
72
+ curr_host: str = socket.gethostname()
73
+ # Get settings from env variables (not in .ini file).
74
+ # All env vars (except KIOSK_BRIGADE) below MUST be set or the service will fail and exit. No defaults are provided.
75
+ home_dir: str = os.environ.get('HOME')
76
+ scheme: str = os.environ.get('SCHEME', 'https')
77
+ nfc_ini_file: str = os.environ.get('NFC_INI_FILE')
78
+ kiosk_brigade: str = os.environ.get('KIOSK_BRIGADE', curr_host)
79
+ bushfire_server: str = os.environ.get('BUSHFIRE_SERVER')
80
+ kiosk_location: str = os.environ.get('KIOSK_LOCATION')
81
+ # Get settings from external .ini file. If a section in the INI file is named after CURR_HOST, use that, otherwise, use DEFAULT.
82
+ kiosk_ws: str = f"ws://{kiosk_brigade}.{bushfire_server}/ws/kiosk/{kiosk_brigade}/{kiosk_location}/"
83
+ # Sounds - only needed for kiosk_beeper
84
+ sounds_dir: str = f"{home_dir}/{nset2.get('SOUNDS')}"
85
+ wav_sample_rate: int = 44100 # wav files have a sample rate of 44.1KHz
86
+ syslog_id: str = package_name # Used to tag log entries in journal
87
+
88
+ kiosk_config = KioskConfig()
89
+
90
+
91
+ def get_logger(name):
92
+ logger = logging.getLogger(name)
93
+ if not logger.handlers:
94
+ # Prevent logging from propagating to the root logger, otherwise get dups in journal
95
+ logger.propagate = 0
96
+ loghandler = JournalHandler(SYSLOG_IDENTIFIER=kiosk_config.syslog_id)
97
+ formatter = logging.Formatter(
98
+ "%(asctime)s.%(msecs)03d %(levelname)-5s [%(filename)s:%(lineno)s:%(funcName)s] %(message)s",
99
+ datefmt='%H:%M:%S')
100
+ loghandler.setFormatter(formatter)
101
+ logger.addHandler(loghandler)
102
+ return logger
103
+
104
+ logging.basicConfig(format="%(asctime)s.%(msecs)03d [%(name)s:%(levelname)-5s] %(message)s",
105
+ datefmt='%Y-%m-%d %H:%M:%S')
106
+
107
+ logger = get_logger(kiosk_config.syslog_id)
108
+ logger.setLevel(LOGGING_LEVEL)
109
+
110
+ # Set up sounds in advance so that they are loaded in memory and faster to access
111
+ slist = {}
112
+ for sn in SOUNDS:
113
+ fullfname = f"{kiosk_config.sounds_dir}/{sn}.wav"
114
+ slist[sn] = {'file': fullfname, } # 'mix': mixer.Sound(fullfname)}
115
+
116
+
117
+ def playbeep(sn):
118
+ # Load the WAV file
119
+ try:
120
+ logger.debug(f"play sound: aplay -q {sn['file']}")
121
+ wave_obj = sa.WaveObject.from_wave_file(sn['file'])
122
+
123
+ # Play the WAV file
124
+ play_obj = wave_obj.play()
125
+
126
+ # Wait for the file to finish playing
127
+ play_obj.wait_done()
128
+
129
+ except:
130
+ logger.error('play audio failed')
131
+ pass # No audio available, but ignore
132
+
133
+
134
+ def main(argv=None):
135
+ try:
136
+ ws = websocket.create_connection(kiosk_config.kiosk_ws)
137
+ except ConnectionRefusedError:
138
+ logger.error(f'server {kiosk_config.kiosk_ws} not available with {ConnectionRefusedError}')
139
+ sys.exit(1)
140
+
141
+ while True:
142
+ try:
143
+ msg = json.loads(ws.recv())
144
+ except WebSocketConnectionClosedException:
145
+ logger.error(f'Lost connection on {kiosk_config.kiosk_ws}')
146
+ sys.exit(1) # Containing systemd service should restart after a brief delay
147
+
148
+ msg_type = msg['text'].split(".")[0]
149
+ msg_brigade = msg['text'].split(".")[1]
150
+ msg_kiosk = msg.get('tagLoc', 'station')
151
+
152
+ # Check this message is for this kiosk, and if it's a beep command, do it
153
+ if msg_kiosk == kiosk_config.kiosk_location and msg_type == 'tagBeep':
154
+ stat = msg.get('stat', 'ignore')
155
+
156
+ if stat != 'ignore':
157
+ logger.info(f'message text: {msg_type}.{msg_brigade}, stat: {stat}')
158
+ snd = SOUNDS_MAP.get(stat, 'invalid')
159
+ logger.info(f'stat: {stat}, sound: {snd}')
160
+ playbeep(slist[snd])
161
+ else:
162
+ logger.debug(f'message text: {msg_type}.{msg_brigade}, stat: {stat}')
163
+
164
+ if __name__ == "__main__":
165
+ main()
@@ -0,0 +1,602 @@
1
+ #!/usr/bin/python3
2
+ # -----------------------------------------------------------------------------
3
+ # Copyright 2010, 2017 Stephen Tiedemann <stephen.tiedemann@gmail.com>
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
+ # https://joinup.ec.europa.eu/software/page/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
+ """ nfcreader3.py [-w [<brigadename>]]
23
+ Reads NFC tags and sends contents to bushfire server, and plays an appropriate tone on receipt of response from server.
24
+ Configuration details such as bushfire server name, etc. collected from ~/nfcreader.ini.
25
+ Possible responses are:
26
+ - member signed into an event
27
+ - member signed out of an event
28
+ - no events available for this member
29
+ - invalid tag - member not recognised.
30
+ Normally runs as a background service using systemd (NB: Environment="XDG_RUNTIME_DIR=/run/user/1000" must be set in .service file or sound will not work.)
31
+ Built using nfcpy library, and reader needs to be setup correctly before operating - see nfcpy documentation for details.
32
+ Currently supports Sony 380 NFC reader, though others supported by nfcpy could also be used.
33
+ Normally runs on a Raspberry PI under Raspian, but will also run on linux systems.
34
+ NFC reader is automatically detected on USB bus and type identified.
35
+ Only one process can use the reader at any time.
36
+ Central loop checks for connection to server and presence of NFC reader, and should recover from temporary loss of either.
37
+ Uses connect/sense mode to read tags. It will read a tag as soon as it is presented, with no need to wait for release of tag first.
38
+
39
+ Optional -w flag causes the program to enter a loop to write tags, rather than read them.
40
+
41
+ This module is normally executed as part of nfcserver3.service, in the nfcserver3 package.
42
+ It requires environment variables set in the following files, in order or precedence:
43
+ # Set required env variables.
44
+ EnvironmentFile=/etc/profile.d/rfstag/base.env
45
+ # Allow local override of these env vars - Optional
46
+ EnvironmentFile=-/home/pi/.config/rfstag/local.env
47
+ Templates for these are provided in the nfcreader3 package.
48
+
49
+ """
50
+
51
+ from __future__ import print_function
52
+ import logging
53
+ import struct
54
+ import sys
55
+ import time
56
+ import datetime
57
+ from time import asctime
58
+ import inspect
59
+ import socket
60
+ import os
61
+ import subprocess
62
+ import configparser
63
+ from dataclasses import dataclass # , field
64
+ from timeit import default_timer as timer
65
+ from pathlib import Path
66
+
67
+ from systemd.journal import JournalHandler
68
+ import requests
69
+ import errno
70
+ import nfc, ndef
71
+ from argparse import Namespace
72
+
73
+
74
+ LOGGING_LEVEL = logging.DEBUG # TODO change to INFO in production
75
+
76
+ if sys.version_info.major < 3:
77
+ sys.exit("This script requires Python 3")
78
+
79
+ SYSLOG_ID = Path(__file__).stem # 'nfcreader3'
80
+ log = logging.getLogger(SYSLOG_ID)
81
+
82
+ server_session = requests.Session()
83
+
84
+ ######################################################################################################################
85
+
86
+
87
+ # Read external settings from .ini file, using section named for the current host
88
+
89
+ class CaseSensitiveConfigParser(configparser.ConfigParser):
90
+ def optionxform(self, optionstr):
91
+ # Override the optionxform method to prevent case folding
92
+ return optionstr
93
+
94
+ # Read external settings from .ini file, using section named for the current host
95
+ def getSettings(curr_host, ini_file):
96
+ # Create a ConfigParser instance
97
+ config = CaseSensitiveConfigParser()
98
+
99
+ # Read the .ini file
100
+ config.read(ini_file)
101
+ cdict = dict(config['DEFAULT'])
102
+ return cdict
103
+
104
+ nset2 = getSettings(socket.gethostname(), os.environ.get('NFC_INI_FILE'))
105
+
106
+
107
+ @dataclass
108
+ class KioskConfig:
109
+ # APT package details
110
+ package_name: str = SYSLOG_ID
111
+ raw_version: bytes = subprocess.check_output(['/usr/bin/apt-show-versions', package_name])
112
+ deb_version: str = str(raw_version.split(b' ')[1], 'utf-8')
113
+ # In most cases, the host name will be the brigade/tenant name, but allow for override with env var when needed.
114
+ curr_host: str = socket.gethostname()
115
+ # Get settings from env variables (not in .ini file).
116
+ # All env vars (except KIOSK_BRIGADE) below MUST be set or the service will fail and exit. No defaults are provided.
117
+ home_dir: str = os.environ.get('HOME')
118
+ scheme: str = os.environ.get('SCHEME', 'https')
119
+ nfc_ini_file: str = os.environ.get('NFC_INI_FILE')
120
+ kiosk_brigade: str = os.environ.get('KIOSK_BRIGADE', curr_host)
121
+ bushfire_server: str = os.environ.get('BUSHFIRE_SERVER')
122
+ kiosk_location: str = os.environ.get('KIOSK_LOCATION')
123
+ kiosk_version: str = deb_version
124
+ # Get settings from external .ini file. If a section in the INI file is named after CURR_HOST, use that,
125
+ # otherwise, use DEFAULT.
126
+ tagging_uri: str = nset2.get('TAGGING_URI')
127
+ checkserv_uri: str = nset2.get('CHECKSERV_URI')
128
+ phonetag_url: str = nset2.get('PHONETAG_URI', 'bfb/phonetag')
129
+ request_url: str = f"{scheme}://{kiosk_brigade}.{bushfire_server}/{tagging_uri}"
130
+ checkconn_url: str = f"{scheme}://{kiosk_brigade}.{bushfire_server}/{checkserv_uri}"
131
+ nfc_ws: str = f"ws://{kiosk_brigade}.{bushfire_server}/ws/nfc/{kiosk_brigade}/{kiosk_location}/"
132
+ syslog_id: str = package_name # Used to tag log entries in journal
133
+ # NFC specific values
134
+ # Assume double tag and don't send if 2 tags are read less than this (s) apart
135
+ min_tag_gap: float = 3.0
136
+ start_key: bytes = b'1683249127' # Set to any arbitrary value. Will be updated on_card_connect.
137
+ write_brigade: str = kiosk_brigade # Brigade written on tags during writetags
138
+ tag_brigade: str = kiosk_brigade # Used for tag emulation when phone tagging
139
+
140
+
141
+ kiosk_config = KioskConfig()
142
+
143
+ #######################################################################################################################
144
+
145
+
146
+ class RFStag_server(object):
147
+ def __init__(self):
148
+ # This Namespace is a hangover from the original tagtool code that used argparse.
149
+ # It is used mainly in prepare_tt3_tag, but also emulate_tt3_tag (options.tt3_data)
150
+ self.options = Namespace()
151
+
152
+ self.orig_key = kiosk_config.start_key
153
+
154
+ self.disc_time = timer()
155
+ self.endt = timer()
156
+
157
+ self.rtt = None
158
+
159
+ def get_logger(name):
160
+ logger = logging.getLogger(name)
161
+ if not logger.handlers:
162
+ # Prevent logging from propagating to the root logger, otherwise get dups in journal
163
+ logger.propagate = 0
164
+ loghandler = JournalHandler(SYSLOG_IDENTIFIER=kiosk_config.syslog_id)
165
+ formatter = logging.Formatter(
166
+ "%(asctime)s.%(msecs)03d %(levelname)-5s [%(filename)s:%(lineno)s:%(funcName)s] %(message)s",
167
+ datefmt='%H:%M:%S')
168
+ loghandler.setFormatter(formatter)
169
+ logger.addHandler(loghandler)
170
+ return logger
171
+
172
+ logging.basicConfig(format="%(asctime)s.%(msecs)03d [%(name)s:%(levelname)-5s] %(message)s",
173
+ datefmt='%Y-%m-%d %H:%M:%S')
174
+
175
+ logger = get_logger(kiosk_config.syslog_id)
176
+ logger.setLevel(LOGGING_LEVEL)
177
+
178
+ self.session = server_session # don't remake this every time (can take ~300ms)
179
+
180
+ # Use this flag to indicate whether a tag or a phone has been presented
181
+ self.tag_read = False
182
+
183
+ self.tag_brigade = None
184
+ self.tag_issued = None
185
+ self.tag_member = None
186
+
187
+ self.rdwr_options = {
188
+ 'targets': ['106A', ], # Only look for passive ndef tags (type A)
189
+ 'iterations': 1, # Reduce from default of 5 to allow faster switching to phone
190
+ 'interval': 0.1, # Reduce from 0.5 to improve responsiveness
191
+ 'on-startup': self.on_startup,
192
+ 'on-discover': self.on_discover, # Required to quickly identify target
193
+ 'on-connect': self.on_connect,
194
+ 'on-release': self.on_release, # TODO try doing without this
195
+ }
196
+
197
+ self.card_options = {
198
+ 'targets': ['212F'], # F type matches phone working as a tag reader (actually not used)
199
+ 'on-startup': self.on_card_startup,
200
+ 'on-discover': self.on_card_discover,
201
+ 'on-connect': self.on_card_connect,
202
+ 'on-release': self.on_card_release,
203
+ }
204
+
205
+ self.write_options = {
206
+ 'targets': ['106A',], # Only look for passive ndef tags (type A)
207
+ 'on-startup': self.on_startupw,
208
+ 'on-discover': self.on_discoverw, # Required to quickly identify target
209
+ 'on-connect': self.on_connectw,
210
+ 'on-release': self.on_releasew,
211
+ }
212
+
213
+ def sendTag(self, memb, brigade, issued, tag_ts): # Send tag data to server
214
+ rurl = kiosk_config.request_url + str(memb)
215
+ kbrigade = kiosk_config.kiosk_brigade
216
+ kLoc = kiosk_config.kiosk_location
217
+
218
+ log.info(f"memb:{str(memb)} brigade:{brigade} issued:{issued}@loc:{kLoc}")
219
+ # TODO this should really login properly to be fully secure
220
+ try:
221
+ post = self.session.post(rurl, data={'tagBrigade': brigade, 'Issued': issued, 'kioskBrigade': kbrigade,
222
+ 'kioskLoc': kLoc, 'tread': tag_ts}, timeout=5) # verify=False)
223
+ except Exception as ex: # Catch all - may not be needed
224
+ template = "An exception of type {0} occurred. Arguments:\n{1!r}"
225
+ message = template.format(type(ex).__name__, type(ex).__qualname__, ex.args)
226
+ log.error(f"POST to {rurl} failed")
227
+ raise ValueError(f"POST to {rurl} failed")
228
+
229
+ log.debug(f"POST result:{post.request.body}")
230
+ log.debug(f"status:{str(post.status_code)}, reason:{post.reason}")
231
+
232
+ if post.status_code != 200:
233
+ log.error("Server not accepting tag")
234
+ raise ValueError("Server not accepting tag")
235
+
236
+ # Reader returned headers: only used to log result
237
+ evstat = post.headers.get('stat', 'nostat') # Action determined by server
238
+ m2 = post.headers.get('member', 'norfsID') # Member number
239
+ mname = post.headers.get('Mname', 'noname') # member name
240
+
241
+ evid = post.headers.get('event', '0') # Event number to sign in/out of
242
+ evstr = post.headers.get('evsumm', 'noevent') # Event description
243
+ evlist = post.headers.get('membAvail', 'noevents') # List of available event numbers (optional)
244
+ evflist = post.headers.get('evDetails', 'noevents') # List of available event descriptions (optional)
245
+
246
+ if evlist == 'noevents':
247
+ log.info(f"Member:{m2}={mname} evstat:{evstat} evid:{evid} evstr:{evstr}")
248
+ else:
249
+ log.info(f"Member:{m2}={mname} evstat:{evstat} evid:{evid} evstr:{evstr} evlist:{evlist} evflist:{evflist}")
250
+ return post
251
+
252
+ def getTagInfo(self, records): # Extract member details from tag data just read
253
+ mdict = {}
254
+ for index, record in enumerate(records):
255
+ rdata = repr(record.data).split('x00')[1] # Strip out prefix before x00
256
+ if record.name == 'Member':
257
+ rd2 = rdata[:-1] # Strip off last char which is spare closing quote
258
+ mdict[record.name] = int(rd2)
259
+ elif record.name == 'Issued':
260
+ rd2 = rdata[:-2]
261
+ mdict[record.name] = datetime.datetime.strptime(rd2, "%a %b %d %H:%M:%S %Y")
262
+ elif record.name == 'Brigade': # Only remaining one should be Brigade
263
+ rd2 = rdata[:-2]
264
+ mdict[record.name] = rd2
265
+ else:
266
+ log.error(f"{record.name}: invalid tag field : {rdata}")
267
+
268
+ log.info(f"Tag: {mdict}")
269
+ return mdict
270
+
271
+ # ************************ read tags
272
+ def on_startup(self, targets):
273
+ log.info("** waiting for a tag **")
274
+ log.info(inspect.currentframe().f_code.co_name)
275
+ for target in targets:
276
+ target.sensf_req = bytearray.fromhex("0012FC0000") # Not really sure what this is for
277
+ return targets
278
+
279
+ def on_discover(self, tag):
280
+ log.info("** on_discover **")
281
+ tagtest1 = tag.sel_res and tag.sel_res[0] == 0x00
282
+ tagtest2 = tag.sens_res and tag.sens_res[0] == 0x44 and tag.sens_res[1] == 0x00
283
+ result = tagtest1 and tagtest2 # If both True, this is a tag
284
+ log.info(f"on_rdrw_discover: result={result} {tag}")
285
+ self.disc_time = timer()
286
+ return result
287
+
288
+ def on_connect(self, tag):
289
+ log.info("** on_connect **")
290
+ # Tag data is available as soon as this is called
291
+ log.info(inspect.currentframe().f_code.co_name)
292
+
293
+ try:
294
+ if tag.ndef is not None:
295
+ if tag.ndef.records:
296
+ log.debug(tag)
297
+ tdata = self.getTagInfo(tag.ndef.records)
298
+ # Save the data from the tag so that it can be sent to the server.
299
+ self.tag_read = True
300
+ self.tag_member = tdata['Member']
301
+ self.tag_brigade = tdata['Brigade']
302
+ self.tag_issued = tdata['Issued']
303
+ else:
304
+ log.warning("on_connect: no ndef records")
305
+ else:
306
+ log.warning("on_connect: no ndef")
307
+ except Exception as inst:
308
+ log.warning("on_connect: tag not readable")
309
+
310
+ return False # Return False to exit as soon as the tag is read, and don't wait for release.
311
+
312
+ def on_release(self, tag):
313
+ # Comes here after data has been read in on_connect.
314
+ log.debug(inspect.currentframe().f_code.co_name)
315
+ log.info("Ready for next tag")
316
+ return True
317
+
318
+ # ************************ write tags
319
+
320
+ def on_startupw(self, targets):
321
+ print('Please place a tag to write on the reader')
322
+ return targets
323
+
324
+ def on_discoverw(self, tag):
325
+ tagtest1 = tag.sel_res and tag.sel_res[0] == 0x00
326
+ tagtest2 = tag.sens_res and tag.sens_res[0] == 0x44 and tag.sens_res[1] == 0x00
327
+
328
+ result = tagtest1 and tagtest2 # If both True, this is a useable tag
329
+ if not result:
330
+ print("Tag not writable: please replace with a suitable tag")
331
+ time.sleep(1)
332
+
333
+ log.info(f"on_rdrw_discoverw: result={result} {tag}")
334
+ return result
335
+
336
+ def on_connectw(self, tag):
337
+ print(f"Tag detected {tag}")
338
+ try:
339
+ print('To exit, press RETURN')
340
+ member = str(input("Member#: "))
341
+ except SyntaxError:
342
+ member = None
343
+
344
+ if not member or len(member) == 0: # Exit writeloop if no member number provided
345
+ print('No member number provided - finishing now.')
346
+ sys.exit(0)
347
+
348
+ brigade = kiosk_config.write_brigade
349
+ tstamp = asctime()
350
+
351
+ mrec = ndef.SmartposterRecord(member)
352
+ mrec.name = "Member"
353
+ brec = ndef.SmartposterRecord(brigade)
354
+ brec.name = "Brigade"
355
+ trec = ndef.SmartposterRecord(tstamp)
356
+ trec.name = "Issued"
357
+ tag.ndef.records = [mrec, brec, trec, ]
358
+
359
+ print('Tag written. Please remove')
360
+
361
+ return True # This forces wait for card to be removed
362
+
363
+ def on_releasew(self, tag):
364
+ print('Tag removed. Next Please')
365
+
366
+ #
367
+ #####################################################################################################################
368
+ def on_card_startup(self, target):
369
+ log.info("** waiting for a reader **")
370
+ target = self.prepare_tag(target)
371
+ return target
372
+
373
+ def on_card_discover(self, tag):
374
+ log.info(f"card discovered: result={tag.sensf_req} {tag}")
375
+ self.disc_time = timer()
376
+ return True if tag.sensf_req else False
377
+
378
+ def make_key_str(self, ts, brigade):
379
+ # Make the url key to reflect the time of connect
380
+ # Include a timestamp in the url key, so it can be checked as current/not expired.
381
+ log.info(f'make_key: ts={ts}, brigade={brigade}')
382
+ be = brigade.encode('utf-8')
383
+ brg_sum = sum(int(ch) for ch in be)
384
+ tag_key = brg_sum + ts
385
+ tag_key_str = str(tag_key)
386
+ log.info(f'make_key: brg_sum={brg_sum}, tag_key int={tag_key}, tag_key str={tag_key_str}')
387
+ return tag_key_str
388
+
389
+ def get_key_ts(self, key_str, brigade):
390
+ brg_sum = sum(int(ch) for ch in brigade.encode('utf-8'))
391
+ tag_time = int(key_str) - brg_sum
392
+ return tag_time
393
+
394
+ def on_card_connect(self, tag):
395
+ log.info("card connect")
396
+
397
+ epoch_time = int(time.time())
398
+ new_key_str = self.make_key_str(epoch_time, kiosk_config.kiosk_brigade)
399
+
400
+ tag_diff = abs(epoch_time - self.get_key_ts(new_key_str, kiosk_config.kiosk_brigade))
401
+ # assert tag_diff = 0
402
+ # log.info(f'card_connect: key_str: {new_key_str}, ts={epoch_time}, tdiff={tag_diff}')
403
+
404
+ self.options.tt3_data = self.options.tt3_data.replace(self.orig_key, new_key_str.encode('utf-8'))
405
+ log.debug(f"card connect: changed tag key from {self.orig_key} to {new_key_str}")
406
+ log.debug(f"card connect: tt3_data: {self.options.tt3_data}")
407
+
408
+ self.orig_key = new_key_str.encode('utf-8')
409
+ return self.emulate_tag_start(tag)
410
+
411
+ def on_card_release(self, tag):
412
+ log.info("card released")
413
+ self.emulate_tag_stop(tag)
414
+ return True
415
+
416
+ def terminate(self):
417
+ return False
418
+
419
+ def prepare_tag(self, target):
420
+ return self.prepare_tt3_tag(target)
421
+
422
+ def prepare_rfs_url(self):
423
+ # Set up rfstag url for inclusion in the emulated tag
424
+ tag_key = str(int(self.orig_key))
425
+
426
+ kc = kiosk_config # Shorthand only
427
+ # RFSTAG_URL = f'{KIOSK_BRIGADE}.{BUSHFIRE_SERVER}/{PHONETAG_URL}/{TAG_BRIGADE}/{KIOSK_LOCATION}/{TAG_KEY}/'
428
+ rfstag_url = f'{kc.kiosk_brigade}.{kc.bushfire_server}/{kc.phonetag_url}/{kc.tag_brigade}/{kc.kiosk_location}/{tag_key}/'
429
+
430
+ # url_text = b'kuringai.rfstag.com/bfb/phonetag/kuringai/hom3/1683249128/'
431
+ url_text = rfstag_url.encode('utf-8')
432
+ return url_text
433
+
434
+ def prepare_tt3_tag(self, target):
435
+ tag_options = self.options
436
+
437
+ # Set defaults here. This was taken from original tagtool, which set defaults in arg parser.
438
+ # In almost every case, these defaults are used below.
439
+ # It's black magic, and I don't know or want to know how it works.
440
+ if not hasattr(tag_options, "idm"): tag_options.idm = '03FEFFE011223344'
441
+ if not hasattr(tag_options, "pmm"): tag_options.pmm = '01E0000000FFFF00'
442
+ if not hasattr(tag_options, "sys"): tag_options.sys = '12FC'
443
+ if not hasattr(tag_options, "bitrate"): tag_options.bitrate = '212'
444
+ if not hasattr(tag_options, "ver"): tag_options.ver = 16
445
+ if not hasattr(tag_options, "nbr"): tag_options.nbr = 1
446
+ if not hasattr(tag_options, "nbw"): tag_options.nbw = 1
447
+ if not hasattr(tag_options, "max"): tag_options.max = None # "maximum number of blocks (default: computed)"
448
+ if not hasattr(tag_options, "rfu"): tag_options.rfu = 0
449
+ if not hasattr(tag_options, "wf"): tag_options.wf = 0
450
+ if not hasattr(tag_options, "rw"): tag_options.rw = 1
451
+ if not hasattr(tag_options, "crc"): tag_options.crc = None # "checksum attribute value (default: computed)"
452
+
453
+ if not hasattr(tag_options, "tagtype"): tag_options.tagtype = 'tt3'
454
+ if not hasattr(tag_options, "size"): tag_options.size = 1024
455
+
456
+ # Prepare the block preceding the url, including payload size (ubase2 below)
457
+ url_text = self.prepare_rfs_url()
458
+ ulen = len(url_text) + 1
459
+ ubase1 = b'\xd1\x01'
460
+ ubase2 = ulen.to_bytes(1, 'big') # Assume for now that the url length will fit in one byte
461
+ ubase3 = b'\x55\x04'
462
+ url_base = ubase1 + ubase2 + ubase3
463
+
464
+ # self.options.data = b'\xd1\x01;U\x04kuringai.rfstag.org/bfb/phonetag/kuringai/hom3/1683249128/'
465
+ tag_options.data = url_base + url_text
466
+
467
+ if not (hasattr(tag_options, "tt3_data")):
468
+ ndef_data_size = len(tag_options.data)
469
+ ndef_area_size = ((ndef_data_size + 15) // 16) * 16
470
+ ndef_area_size = max(ndef_area_size, tag_options.size)
471
+ ndef_data_area = bytearray(tag_options.data) + bytearray(ndef_area_size - ndef_data_size)
472
+
473
+ # create attribute data
474
+ attribute_data = bytearray(16)
475
+ attribute_data[0] = tag_options.ver
476
+ attribute_data[1] = tag_options.nbr
477
+ attribute_data[2] = tag_options.nbw
478
+ if tag_options.max is None:
479
+ nmaxb = len(ndef_data_area) // 16
480
+ else:
481
+ nmaxb = tag_options.max
482
+ attribute_data[3:5] = struct.pack(">H", nmaxb)
483
+ attribute_data[5:9] = 4 * [tag_options.rfu]
484
+ attribute_data[9] = tag_options.wf
485
+ attribute_data[10:14] = struct.pack(">I", len(tag_options.data))
486
+ attribute_data[10] = tag_options.rw
487
+ attribute_data[14:16] = struct.pack(">H", sum(attribute_data[:14]))
488
+ tag_options.tt3_data = attribute_data + ndef_data_area
489
+
490
+ idm = bytes.fromhex(tag_options.idm)
491
+ pmm = bytes.fromhex(tag_options.pmm)
492
+ _sys = bytes.fromhex(tag_options.sys)
493
+
494
+ target.brty = tag_options.bitrate + "F"
495
+ target.sensf_res = b"\x01" + idm + pmm + _sys
496
+
497
+ log.debug(f'prepare_tt3_tag options: {tag_options}')
498
+ log.debug(f'prepare_tt3_tag target: {target}')
499
+ return target
500
+
501
+ def emulate_tag_start(self, tag):
502
+ return self.emulate_tt3_tag(tag)
503
+
504
+ def emulate_tag_stop(self, tag):
505
+ return
506
+
507
+ def emulate_tt3_tag(self, tag):
508
+ def ndef_read(block_number, rb, re):
509
+ log.debug("tt3 read block #{0}".format(block_number))
510
+ if block_number < len(self.options.tt3_data) / 16:
511
+ first, last = block_number * 16, (block_number + 1) * 16
512
+ block_data = self.options.tt3_data[first:last]
513
+ return block_data
514
+
515
+ def ndef_write(block_number, block_data, wb, we):
516
+ log.debug("tt3 write block #{0}".format(block_number))
517
+ if block_number < len(self.options.tt3_data) / 16:
518
+ first, last = block_number * 16, (block_number + 1) * 16
519
+ self.options.tt3_data[first:last] = block_data
520
+ return True
521
+
522
+ tag.add_service(0x0009, ndef_read, ndef_write)
523
+ tag.add_service(0x000B, ndef_read, lambda: False)
524
+ return True
525
+
526
+ def run_once(self, nfc_options):
527
+ devices = ['usb']
528
+
529
+ for path in devices:
530
+ try:
531
+ clf = nfc.ContactlessFrontend(path)
532
+ except IOError as error:
533
+ if error.errno == errno.ENODEV:
534
+ log.info("no contactless reader found on " + path)
535
+ elif error.errno == errno.EACCES:
536
+ log.info("access denied for device with path " + path)
537
+ elif error.errno == errno.EBUSY:
538
+ log.info("the reader on " + path + " is busy")
539
+ else:
540
+ log.debug(repr(error) + "when trying " + path)
541
+ else:
542
+ log.debug("found a usable reader on " + path)
543
+ break
544
+ else:
545
+ log.error("no contactless reader available")
546
+ raise SystemExit(1)
547
+
548
+ try:
549
+ # Provide settings for both tag and card/phone. nfc will determine which to use base on 'targets'
550
+ # and _discover.
551
+ return clf.connect(**nfc_options)
552
+ except Exception as inst:
553
+ log.error(f"run_once failure: {inst}")
554
+ sys.exit(1)
555
+ finally:
556
+ clf.close()
557
+
558
+ def run_reader(self):
559
+ nfc_options = {'rdwr': self.rdwr_options, 'card': self.card_options}
560
+ while self.run_once(nfc_options):
561
+ # ContactlessFrontEnd connection is now closed.
562
+ # In the case of phones, the reader has emulated a tt3 style tag and sent a url to the phone,
563
+ # which emulates a tag. No further action is required.
564
+ # In the case of tags, the data has been read, but not yet sent to the server.
565
+ if self.tag_read is True: # Send the latest tag data to the server.
566
+ log.info(f"run: tag read (now send to server): {self.tag_member}, {self.tag_brigade}, {self.tag_issued}")
567
+ self.sendTag(self.tag_member, self.tag_brigade, self.tag_issued, datetime.datetime.now())
568
+ self.endt = timer()
569
+ self.rtt = self.endt - self.disc_time
570
+ log.info(f"run: tag {kiosk_config.bushfire_server} {self.tag_brigade}:{self.tag_member} rtt:{self.rtt:.3f}")
571
+ self.tag_read = False
572
+ # To avoid double tags, take a short break
573
+ time.sleep(3)
574
+ log.info("*** RESTART ***")
575
+
576
+ def run_writer(self):
577
+ nfc_options = {'rdwr': self.write_options}
578
+ while self.run_once(nfc_options):
579
+ # To avoid double tags, take a short break
580
+ time.sleep(1)
581
+ log.info("*** RESTART ***")
582
+
583
+ def main(argv=None):
584
+ if argv is not None:
585
+ writetags = '-w' in argv
586
+ else:
587
+ writetags = False
588
+
589
+ rs = RFStag_server()
590
+
591
+ try:
592
+ if not writetags:
593
+ rs.run_reader()
594
+ else:
595
+ rs.run_writer()
596
+ except Exception as inst:
597
+ log.error(f"main system failure: {inst}")
598
+ sys.exit(1)
599
+
600
+
601
+ if __name__ == '__main__':
602
+ main(sys.argv)