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.
- rfskiosk2-0.4.20/PKG-INFO +30 -0
- rfskiosk2-0.4.20/README.md +0 -0
- rfskiosk2-0.4.20/pyproject.toml +37 -0
- rfskiosk2-0.4.20/rfskiosk2/__init__.py +0 -0
- rfskiosk2-0.4.20/rfskiosk2/envfiles/base-example.env +10 -0
- rfskiosk2-0.4.20/rfskiosk2/envfiles/local-example.env +9 -0
- rfskiosk2-0.4.20/rfskiosk2/envfiles/nfcreader-example.ini +21 -0
- rfskiosk2-0.4.20/rfskiosk2/kiosk_beeper.py +165 -0
- rfskiosk2-0.4.20/rfskiosk2/nfcreader3.py +602 -0
|
@@ -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)
|