pyllrp 3.1.0__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.
- pyllrp-3.1.0/License.txt +17 -0
- pyllrp-3.1.0/PKG-INFO +105 -0
- pyllrp-3.1.0/README.txt +63 -0
- pyllrp-3.1.0/pyllrp/AutoDetect.py +100 -0
- pyllrp-3.1.0/pyllrp/LLRPConnector.py +183 -0
- pyllrp-3.1.0/pyllrp/ParseDef.py +151 -0
- pyllrp-3.1.0/pyllrp/TagInventory.py +204 -0
- pyllrp-3.1.0/pyllrp/TagWriter.py +181 -0
- pyllrp-3.1.0/pyllrp/TinyExample.py +132 -0
- pyllrp-3.1.0/pyllrp/__init__.py +0 -0
- pyllrp-3.1.0/pyllrp/llrpdef.py +2578 -0
- pyllrp-3.1.0/pyllrp/pyllrp.py +1040 -0
- pyllrp-3.1.0/pyllrp/pypi.py +182 -0
- pyllrp-3.1.0/pyllrp.egg-info/PKG-INFO +105 -0
- pyllrp-3.1.0/pyllrp.egg-info/SOURCES.txt +19 -0
- pyllrp-3.1.0/pyllrp.egg-info/dependency_links.txt +1 -0
- pyllrp-3.1.0/pyllrp.egg-info/requires.txt +1 -0
- pyllrp-3.1.0/pyllrp.egg-info/top_level.txt +1 -0
- pyllrp-3.1.0/pyproject.toml +32 -0
- pyllrp-3.1.0/setup.cfg +4 -0
- pyllrp-3.1.0/setup.py +15 -0
pyllrp-3.1.0/License.txt
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
|
|
2
|
+
Copyright (C) 2013-2019 Edward Sitarski
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
|
5
|
+
and associated documentation files (the "Software"), to deal in the Software without
|
|
6
|
+
restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
|
8
|
+
Software is furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all copies or
|
|
11
|
+
substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
|
14
|
+
BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
15
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
16
|
+
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
17
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
pyllrp-3.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pyllrp
|
|
3
|
+
Version: 3.1.0
|
|
4
|
+
Summary: A complete implementation of LLRP (Low Level Reader Protocol) in pure Python.
|
|
5
|
+
Home-page: http://www.sites.google.com/site/crossmgrsoftware/
|
|
6
|
+
Author: Edward Sitarski
|
|
7
|
+
Author-email: Edward Sitarski <edward.sitarski@gmail.com>
|
|
8
|
+
License:
|
|
9
|
+
Copyright (C) 2013-2019 Edward Sitarski
|
|
10
|
+
|
|
11
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
|
12
|
+
and associated documentation files (the "Software"), to deal in the Software without
|
|
13
|
+
restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
|
14
|
+
distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
|
15
|
+
Software is furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all copies or
|
|
18
|
+
substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
|
21
|
+
BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
22
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
23
|
+
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
25
|
+
|
|
26
|
+
Project-URL: Homepage, https://github.com/esitarski/pyllrp
|
|
27
|
+
Project-URL: Repository, https://github.com/esitarski/pyllrp
|
|
28
|
+
Project-URL: Issues, https://github.com/esitarski/pyllrp/issues
|
|
29
|
+
Keywords: LLRP,RFID,Impinj Reader
|
|
30
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
31
|
+
Classifier: Intended Audience :: Developers
|
|
32
|
+
Classifier: Topic :: System :: Hardware :: Hardware Drivers
|
|
33
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
34
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
39
|
+
Requires-Python: >=3.8
|
|
40
|
+
Description-Content-Type: text/plain
|
|
41
|
+
Requires-Dist: bitstring>=3.1.1
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
======
|
|
45
|
+
pyllrp
|
|
46
|
+
======
|
|
47
|
+
|
|
48
|
+
A pure Python implementation of LLRP (Low Level Reader Protocol)
|
|
49
|
+
used to communicate with LLRP-compliant RFID readers including Impinj, Alien and ThingMagic.
|
|
50
|
+
See the LLRP spec ) for details:
|
|
51
|
+
https://www.gs1.org/sites/default/files/docs/epc/llrp_1_0_1-standard-20070813.pdf
|
|
52
|
+
|
|
53
|
+
pyllrp enables quick-and-easy scripting in portable pure Python to
|
|
54
|
+
create LLRP applications.
|
|
55
|
+
|
|
56
|
+
It is used extensively in CrossMgr, RaceDB, TagReadWrite for RFID and race timing.
|
|
57
|
+
|
|
58
|
+
The implementation is 100% complete. All LLRP features and full Impinj custom extensions are
|
|
59
|
+
supported.
|
|
60
|
+
|
|
61
|
+
* Message and Parameters are full Python classes.
|
|
62
|
+
* Full validation of all LLRP Messages and Parameters including data types, data ranges, parameter sequence and count.
|
|
63
|
+
* Full support for enumerated values - no hardcoded values.
|
|
64
|
+
* "Impossible" to pass incorrect or malformed LLRP messages to the reader.
|
|
65
|
+
|
|
66
|
+
See TinyExample.py for how to use.
|
|
67
|
+
|
|
68
|
+
A reader connection manager is also included that can connect to a reader, transact commands,
|
|
69
|
+
then start/stop a thread to listen for tag reads.
|
|
70
|
+
A message handler can be configured to respond to any reader message.
|
|
71
|
+
See wxExample.py for a simple method to show reader messages in
|
|
72
|
+
a wxPython application with a Queue (requires wxPython install).
|
|
73
|
+
|
|
74
|
+
The module also supports reading and writing messages in XML format.
|
|
75
|
+
|
|
76
|
+
Unlike other implementations, pyllrp generates the interfaces from the LLRP XML spec itself.
|
|
77
|
+
It is 100% compliant with the LLRP spec and Impinj extensions.
|
|
78
|
+
|
|
79
|
+
pyllrp is also 100% pedantic regarding LLRP Messages and Parameters.
|
|
80
|
+
For example, pyllrp is strict about bool and ints: passing 1/0 for True/False doesn't work.
|
|
81
|
+
The names of all fields must be specified, with the exception of Parameters which have only one field.
|
|
82
|
+
In this case, the single parameter is taken to be the value.
|
|
83
|
+
|
|
84
|
+
How to use it:
|
|
85
|
+
|
|
86
|
+
The only files you need are pyllrp.py and llrpdef.py.
|
|
87
|
+
TinyExample.py shows how to formulate LLRP specs, messages and parameters.
|
|
88
|
+
To run TinyExample, from the top pyllrp directory, enter "python3 -m pyllrp.TinyExample".
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
How it works:
|
|
92
|
+
|
|
93
|
+
ParseDef.py reads the LLRP XML spec files and generates the llrpdef.py file.
|
|
94
|
+
During this process, it translates the LLRP data types into bitstream representation and processes the XML into Python-friendly structures.
|
|
95
|
+
ParseDef must be run every time the XML files change.
|
|
96
|
+
|
|
97
|
+
At run time, pyllrp.py imports llrpdef and does a 1-time augmentation on the Messages, Parameters and Enums into classes.
|
|
98
|
+
It adds the binary pack/unpack, validation and human-readable formating functions.
|
|
99
|
+
Startup time is minimal due to the llrpdef.py pre-processing.
|
|
100
|
+
|
|
101
|
+
Stuff to do:
|
|
102
|
+
|
|
103
|
+
Because the classes are created dynamically, python linters (eg. flake8) do not understand pyllrp classes.
|
|
104
|
+
The only way I can think of to migitate this is to code-generate the full signatures for the class functions,
|
|
105
|
+
but this would be messier than dynamically creating the classes from the llrpdef.py info.
|
pyllrp-3.1.0/README.txt
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
|
|
2
|
+
======
|
|
3
|
+
pyllrp
|
|
4
|
+
======
|
|
5
|
+
|
|
6
|
+
A pure Python implementation of LLRP (Low Level Reader Protocol)
|
|
7
|
+
used to communicate with LLRP-compliant RFID readers including Impinj, Alien and ThingMagic.
|
|
8
|
+
See the LLRP spec ) for details:
|
|
9
|
+
https://www.gs1.org/sites/default/files/docs/epc/llrp_1_0_1-standard-20070813.pdf
|
|
10
|
+
|
|
11
|
+
pyllrp enables quick-and-easy scripting in portable pure Python to
|
|
12
|
+
create LLRP applications.
|
|
13
|
+
|
|
14
|
+
It is used extensively in CrossMgr, RaceDB, TagReadWrite for RFID and race timing.
|
|
15
|
+
|
|
16
|
+
The implementation is 100% complete. All LLRP features and full Impinj custom extensions are
|
|
17
|
+
supported.
|
|
18
|
+
|
|
19
|
+
* Message and Parameters are full Python classes.
|
|
20
|
+
* Full validation of all LLRP Messages and Parameters including data types, data ranges, parameter sequence and count.
|
|
21
|
+
* Full support for enumerated values - no hardcoded values.
|
|
22
|
+
* "Impossible" to pass incorrect or malformed LLRP messages to the reader.
|
|
23
|
+
|
|
24
|
+
See TinyExample.py for how to use.
|
|
25
|
+
|
|
26
|
+
A reader connection manager is also included that can connect to a reader, transact commands,
|
|
27
|
+
then start/stop a thread to listen for tag reads.
|
|
28
|
+
A message handler can be configured to respond to any reader message.
|
|
29
|
+
See wxExample.py for a simple method to show reader messages in
|
|
30
|
+
a wxPython application with a Queue (requires wxPython install).
|
|
31
|
+
|
|
32
|
+
The module also supports reading and writing messages in XML format.
|
|
33
|
+
|
|
34
|
+
Unlike other implementations, pyllrp generates the interfaces from the LLRP XML spec itself.
|
|
35
|
+
It is 100% compliant with the LLRP spec and Impinj extensions.
|
|
36
|
+
|
|
37
|
+
pyllrp is also 100% pedantic regarding LLRP Messages and Parameters.
|
|
38
|
+
For example, pyllrp is strict about bool and ints: passing 1/0 for True/False doesn't work.
|
|
39
|
+
The names of all fields must be specified, with the exception of Parameters which have only one field.
|
|
40
|
+
In this case, the single parameter is taken to be the value.
|
|
41
|
+
|
|
42
|
+
How to use it:
|
|
43
|
+
|
|
44
|
+
The only files you need are pyllrp.py and llrpdef.py.
|
|
45
|
+
TinyExample.py shows how to formulate LLRP specs, messages and parameters.
|
|
46
|
+
To run TinyExample, from the top pyllrp directory, enter "python3 -m pyllrp.TinyExample".
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
How it works:
|
|
50
|
+
|
|
51
|
+
ParseDef.py reads the LLRP XML spec files and generates the llrpdef.py file.
|
|
52
|
+
During this process, it translates the LLRP data types into bitstream representation and processes the XML into Python-friendly structures.
|
|
53
|
+
ParseDef must be run every time the XML files change.
|
|
54
|
+
|
|
55
|
+
At run time, pyllrp.py imports llrpdef and does a 1-time augmentation on the Messages, Parameters and Enums into classes.
|
|
56
|
+
It adds the binary pack/unpack, validation and human-readable formating functions.
|
|
57
|
+
Startup time is minimal due to the llrpdef.py pre-processing.
|
|
58
|
+
|
|
59
|
+
Stuff to do:
|
|
60
|
+
|
|
61
|
+
Because the classes are created dynamically, python linters (eg. flake8) do not understand pyllrp classes.
|
|
62
|
+
The only way I can think of to migitate this is to code-generate the full signatures for the class functions,
|
|
63
|
+
but this would be messier than dynamically creating the classes from the llrpdef.py info.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import socket
|
|
2
|
+
import threading
|
|
3
|
+
from queue import Queue, Empty
|
|
4
|
+
try:
|
|
5
|
+
from .pyllrp import UnpackMessageFromSocket, ConnectionAttemptEvent_Parameter, ConnectionAttemptStatusType
|
|
6
|
+
except ImportError:
|
|
7
|
+
from pyllrp import UnpackMessageFromSocket, ConnectionAttemptEvent_Parameter, ConnectionAttemptStatusType
|
|
8
|
+
|
|
9
|
+
def is_impinj_reader( ip, port ):
|
|
10
|
+
# Connect to the socket.
|
|
11
|
+
readerSocket = socket.socket( socket.AF_INET, socket.SOCK_STREAM )
|
|
12
|
+
readerSocket.settimeout( 4.0 )
|
|
13
|
+
try:
|
|
14
|
+
readerSocket.connect( (ip, port) )
|
|
15
|
+
except Exception as e:
|
|
16
|
+
return False
|
|
17
|
+
|
|
18
|
+
# Get the reader's default response.
|
|
19
|
+
try:
|
|
20
|
+
response = UnpackMessageFromSocket( readerSocket )
|
|
21
|
+
except Exception as e:
|
|
22
|
+
return False
|
|
23
|
+
finally:
|
|
24
|
+
readerSocket.close()
|
|
25
|
+
readerSocket = None
|
|
26
|
+
|
|
27
|
+
# Check if the response is valid.
|
|
28
|
+
try:
|
|
29
|
+
connectionAttemptEvent = response.getFirstParameterByClass(ConnectionAttemptEvent_Parameter)
|
|
30
|
+
return connectionAttemptEvent and connectionAttemptEvent.Status == ConnectionAttemptStatusType.Success
|
|
31
|
+
except Exception as e:
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
def connection_tester(job_q, results_q, port):
|
|
35
|
+
while results_q.empty(): # Quit as soon as we detect a reader.
|
|
36
|
+
ip = job_q.get()
|
|
37
|
+
if ip is None:
|
|
38
|
+
break
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
if is_impinj_reader(ip, port):
|
|
42
|
+
results_q.put(ip)
|
|
43
|
+
except Exception as e:
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
def get_my_ip():
|
|
47
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
48
|
+
try:
|
|
49
|
+
# doesn't have to be reachable
|
|
50
|
+
s.connect(('10.255.255.255', 1))
|
|
51
|
+
IP = s.getsockname()[0]
|
|
52
|
+
except Exception as e:
|
|
53
|
+
IP = '127.0.0.1'
|
|
54
|
+
finally:
|
|
55
|
+
s.close()
|
|
56
|
+
return IP
|
|
57
|
+
|
|
58
|
+
def check_network(pool_size=255, port=5084):
|
|
59
|
+
""" Check the network for readers. """
|
|
60
|
+
""" Multithread the calls for performance. """
|
|
61
|
+
|
|
62
|
+
# get my IP and compose a base like 192.168.1.xxx
|
|
63
|
+
ip_parts = get_my_ip().split('.')
|
|
64
|
+
base_ip = ip_parts[0] + '.' + ip_parts[1] + '.' + ip_parts[2] + '.'
|
|
65
|
+
|
|
66
|
+
# prepare the jobs queue
|
|
67
|
+
jobs = Queue()
|
|
68
|
+
results = Queue()
|
|
69
|
+
|
|
70
|
+
pool = [threading.Thread(target=connection_tester, args=(jobs, results, port)) for i in range(pool_size)]
|
|
71
|
+
for p in pool:
|
|
72
|
+
p.start()
|
|
73
|
+
|
|
74
|
+
# cue the ping processes
|
|
75
|
+
my_low_order = int( ip_parts[3] )
|
|
76
|
+
low_orders = sorted( (i for i in range(1,255) if i != my_low_order), key=lambda v: abs(v-my_low_order) )
|
|
77
|
+
for i in low_orders:
|
|
78
|
+
jobs.put('{}{}'.format(base_ip, i))
|
|
79
|
+
|
|
80
|
+
for p in pool:
|
|
81
|
+
jobs.put(None)
|
|
82
|
+
|
|
83
|
+
for p in pool:
|
|
84
|
+
p.join()
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
return results.get(False)
|
|
88
|
+
except Empty:
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
GetDefaultHost = get_my_ip
|
|
92
|
+
|
|
93
|
+
def findImpinjHost( impinjPort=5084 ):
|
|
94
|
+
return check_network(port=impinjPort)
|
|
95
|
+
|
|
96
|
+
def AutoDetect( impinjPort=5084 ):
|
|
97
|
+
return findImpinjHost(impinjPort), GetDefaultHost()
|
|
98
|
+
|
|
99
|
+
if __name__ == '__main__':
|
|
100
|
+
print( AutoDetect() )
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
from queue import Queue, Empty
|
|
3
|
+
import socket
|
|
4
|
+
import datetime
|
|
5
|
+
import threading
|
|
6
|
+
try:
|
|
7
|
+
from .pyllrp import *
|
|
8
|
+
except Exception as e:
|
|
9
|
+
from pyllrp import *
|
|
10
|
+
|
|
11
|
+
class LLRPConnector:
|
|
12
|
+
#--------------------------------------------------------------------------
|
|
13
|
+
#
|
|
14
|
+
# A simple LLRP reader connection manager.
|
|
15
|
+
#
|
|
16
|
+
# Supports connecting to the reader, transacting commands, message handlers,
|
|
17
|
+
# and asynchronous/synchronous monitoring of the reader socket.
|
|
18
|
+
#
|
|
19
|
+
def __init__( self ):
|
|
20
|
+
self.TimeoutSecs = 6 # Time for the reader to respond.
|
|
21
|
+
self._reset()
|
|
22
|
+
self.handlers = {}
|
|
23
|
+
|
|
24
|
+
def _reset( self ):
|
|
25
|
+
''' Reset all internal fields. '''
|
|
26
|
+
self.host = None
|
|
27
|
+
self.port = None
|
|
28
|
+
self.readerSocket = None
|
|
29
|
+
self.thread = None
|
|
30
|
+
self.shutdownQ = None # Used to shutdown the thread.
|
|
31
|
+
self.keepGoing = False
|
|
32
|
+
self.timeCorrection = None
|
|
33
|
+
|
|
34
|
+
def connect( self, host, port = 5084 ):
|
|
35
|
+
''' Connect to a reader. '''
|
|
36
|
+
self._reset()
|
|
37
|
+
self.readerSocket = socket.socket( socket.AF_INET, socket.SOCK_STREAM )
|
|
38
|
+
|
|
39
|
+
# Set a timeout for the socket. This is also the maximum time it will take to shut down the listener.
|
|
40
|
+
self.readerSocket.settimeout( self.TimeoutSecs )
|
|
41
|
+
self.readerSocket.connect( (host, port) )
|
|
42
|
+
|
|
43
|
+
self.host = host
|
|
44
|
+
self.port = port
|
|
45
|
+
|
|
46
|
+
# Expecting READER_EVENT_NOTIFICATION message.
|
|
47
|
+
response = UnpackMessageFromSocket( self.readerSocket )
|
|
48
|
+
tNow = datetime.datetime.now() # Get the time here to minimize latency.
|
|
49
|
+
|
|
50
|
+
# Check if the connection succeeded.
|
|
51
|
+
connectionAttemptEvent = response.getFirstParameterByClass(ConnectionAttemptEvent_Parameter)
|
|
52
|
+
if connectionAttemptEvent and connectionAttemptEvent.Status != ConnectionAttemptStatusType.Success:
|
|
53
|
+
self.disconnect()
|
|
54
|
+
raise EnvironmentError(
|
|
55
|
+
connectionAttemptEvent.Status,
|
|
56
|
+
ConnectionAttemptStatusType.getName(connectionAttemptEvent.Status).replace('_',' ')
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
self.keepGoing = True
|
|
60
|
+
|
|
61
|
+
# Compute a correction between the reader's time and the computer's time.
|
|
62
|
+
self.timeCorrection = None
|
|
63
|
+
try:
|
|
64
|
+
microseconds = response.getFirstParameterByClass(UTCTimestamp_Parameter).Microseconds
|
|
65
|
+
readerTime = datetime.datetime.utcfromtimestamp( microseconds / 1000000.0 )
|
|
66
|
+
self.timeCorrection = tNow - readerTime
|
|
67
|
+
except Exception as e:
|
|
68
|
+
self.disconnect()
|
|
69
|
+
raise ValueError('Missing Timestamp: ' + response.__repr__())
|
|
70
|
+
|
|
71
|
+
return response
|
|
72
|
+
|
|
73
|
+
def tagTimeToComputerTime( self, tagTime ):
|
|
74
|
+
# Time is in microseconds from Jan 1, 1970.
|
|
75
|
+
return datetime.datetime.utcfromtimestamp( tagTime / 1000000.0 ) + self.timeCorrection
|
|
76
|
+
|
|
77
|
+
def disconnect( self ):
|
|
78
|
+
''' Disconnect from a reader. Also stops the listener. '''
|
|
79
|
+
self.timeCorrection = None
|
|
80
|
+
if not self.readerSocket:
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
if self.isListening():
|
|
84
|
+
self.stopListener()
|
|
85
|
+
|
|
86
|
+
# Send the reader a disconnect message.
|
|
87
|
+
response = None
|
|
88
|
+
try:
|
|
89
|
+
response = self.transact( CLOSE_CONNECTION_Message() )
|
|
90
|
+
except Exception:
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
self.readerSocket.close()
|
|
94
|
+
self.readerSocket = None
|
|
95
|
+
return response
|
|
96
|
+
|
|
97
|
+
def addHandler( self, messageClass, handlerFunc ):
|
|
98
|
+
''' Add a handler for a specific message type. '''
|
|
99
|
+
''' Support multiple handlers for the same message type. '''
|
|
100
|
+
self.handlers.setdefault( messageClass, [] ).append( handlerFunc )
|
|
101
|
+
|
|
102
|
+
def removeHandler( self, messageClass, handlerFunc = None ):
|
|
103
|
+
''' Remove a handler for a specific message type from the reader.
|
|
104
|
+
If handlerFunc is None, all handlers for the given messageClass will be removed.
|
|
105
|
+
If handlerFunc is not None, only the specific handler is removed.
|
|
106
|
+
'''
|
|
107
|
+
if handlerFunc is None:
|
|
108
|
+
try:
|
|
109
|
+
del self.handlers[messageClass]
|
|
110
|
+
except KeyError:
|
|
111
|
+
pass
|
|
112
|
+
else:
|
|
113
|
+
while True:
|
|
114
|
+
try:
|
|
115
|
+
self.handlers[messageClass].remove( handlerFunc )
|
|
116
|
+
except (KeyError, ValueError):
|
|
117
|
+
break
|
|
118
|
+
|
|
119
|
+
def removeAllHandlers( self ):
|
|
120
|
+
self.handlers = {}
|
|
121
|
+
|
|
122
|
+
def transact( self, message ):
|
|
123
|
+
''' Send a message to the reader and wait for the response. '''
|
|
124
|
+
assert not self.isListening(), 'Cannot perform transact() while listen thread is active. Stop it first with stopListener().'
|
|
125
|
+
message.send( self.readerSocket )
|
|
126
|
+
response = WaitForMessage( message.MessageID, self.readerSocket, self.callHandler )
|
|
127
|
+
return response
|
|
128
|
+
|
|
129
|
+
def checkKeepGoing( self ):
|
|
130
|
+
''' Check if we should continue the reader thread. '''
|
|
131
|
+
if not self.keepGoing:
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
# Check the shutdown queue for a message. If there is one, shutdown.
|
|
136
|
+
self.shutdownQ.get( False )
|
|
137
|
+
self.shutdownQ.task_done()
|
|
138
|
+
self.keepGoing = False
|
|
139
|
+
return False
|
|
140
|
+
except (AttributeError, Empty):
|
|
141
|
+
return True
|
|
142
|
+
|
|
143
|
+
def callHandler( self, message ):
|
|
144
|
+
''' Call all the handlers for this message. '''
|
|
145
|
+
for cb in (self.handlers.get(message.__class__, None) or self.handlers.get('default', [])):
|
|
146
|
+
cb( self, message )
|
|
147
|
+
|
|
148
|
+
def listen( self ):
|
|
149
|
+
''' Listen for messages from the reader. '''
|
|
150
|
+
# Calling this by itself will block.
|
|
151
|
+
# Recommended usage is to use startListener and stopListener.
|
|
152
|
+
while self.checkKeepGoing():
|
|
153
|
+
try:
|
|
154
|
+
response = UnpackMessageFromSocket( self.readerSocket )
|
|
155
|
+
except socket.timeout:
|
|
156
|
+
continue
|
|
157
|
+
self.callHandler( response )
|
|
158
|
+
|
|
159
|
+
def startListener( self ):
|
|
160
|
+
''' Starts a thread to listen to the reader. '''
|
|
161
|
+
assert self.readerSocket, 'Cannot start listener without a successful connection.'
|
|
162
|
+
assert not self.thread, 'Listener is already running. Stop it first with stopListener().'
|
|
163
|
+
self.shutdownQ = Queue()
|
|
164
|
+
self.keepGoing = True
|
|
165
|
+
self.thread = threading.Thread( target = self.listen, name='LLRP Listener' )
|
|
166
|
+
self.thread.daemon = True
|
|
167
|
+
self.thread.start()
|
|
168
|
+
|
|
169
|
+
def stopListener( self ):
|
|
170
|
+
''' Stops the thread listening to the reader. '''
|
|
171
|
+
self.shutdownQ.put( 'Shutdown' )
|
|
172
|
+
self.thread.join() # Wait for the thread to terminate.
|
|
173
|
+
self.shutdownQ = None
|
|
174
|
+
self.thread = None
|
|
175
|
+
|
|
176
|
+
def isListening( self ):
|
|
177
|
+
return self.thread and self.thread.is_alive()
|
|
178
|
+
|
|
179
|
+
def __enter__( self ):
|
|
180
|
+
return self
|
|
181
|
+
|
|
182
|
+
def __exit__(self, type, value, traceback):
|
|
183
|
+
self.disconnect()
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#---------------------------------------------------------------------------
|
|
2
|
+
#
|
|
3
|
+
# Reformats the llrpdef.xml file into a Python-formatted description llrpdef.py
|
|
4
|
+
#
|
|
5
|
+
# The reason we so this is so we avoid a dependency on the xml parser in deployment.
|
|
6
|
+
# It is also much faster to use the .pyc file rather than parsing XML again
|
|
7
|
+
# on startup.
|
|
8
|
+
#
|
|
9
|
+
import sys
|
|
10
|
+
import pprint
|
|
11
|
+
from xml.dom.minidom import parse
|
|
12
|
+
|
|
13
|
+
# Map the xml field types to our types (bitstring standard, and our custom ones).
|
|
14
|
+
fieldMap = {
|
|
15
|
+
'u1': 'bool',
|
|
16
|
+
'u2': 'bits:2',
|
|
17
|
+
'u8': 'uintbe:8',
|
|
18
|
+
'u16': 'uintbe:16',
|
|
19
|
+
'u32': 'uintbe:32',
|
|
20
|
+
'u64': 'uintbe:64',
|
|
21
|
+
'u96': 'uintbe:96',
|
|
22
|
+
|
|
23
|
+
's8': 'intbe:8',
|
|
24
|
+
's16': 'intbe:16',
|
|
25
|
+
's32': 'intbe:32',
|
|
26
|
+
's64': 'intbe:64',
|
|
27
|
+
|
|
28
|
+
'utf8v': 'string',
|
|
29
|
+
|
|
30
|
+
'u1v': 'bitarray',
|
|
31
|
+
'u8v': 'array:8',
|
|
32
|
+
'u16v': 'array:16',
|
|
33
|
+
'u32v': 'array:32',
|
|
34
|
+
|
|
35
|
+
'bytesToEnd': 'bytesToEnd', # Used for the Custom message type.
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
def toAscii( s ):
|
|
39
|
+
return s.encode('ascii', 'ignore').decode( 'ascii' )
|
|
40
|
+
|
|
41
|
+
def getEnum( e ):
|
|
42
|
+
Name = toAscii(e.attributes['name'].value)
|
|
43
|
+
Choices = [ (int(toAscii(c.attributes['value'].value)), toAscii(c.attributes['name'].value))
|
|
44
|
+
for c in e.childNodes if c.nodeName == 'entry' ]
|
|
45
|
+
return {'name':Name, 'choices':tuple(Choices) }
|
|
46
|
+
|
|
47
|
+
def getParameterMessage( n, isMessage ):
|
|
48
|
+
Name = toAscii(n.attributes['name'].value)
|
|
49
|
+
|
|
50
|
+
Fields = []
|
|
51
|
+
Parameters = []
|
|
52
|
+
|
|
53
|
+
sys.stdout.write( '{} {}\n'.format(Name, 'Message' if isMessage else 'Parameter') )
|
|
54
|
+
try:
|
|
55
|
+
TypeNum = int(n.attributes['typeNum'].value)
|
|
56
|
+
except KeyError:
|
|
57
|
+
# This is a custom parameter or message.
|
|
58
|
+
TypeNum = 1023 # Code for custom Message and Parameter.
|
|
59
|
+
Fields.append( {'name': 'VendorIdentifier', 'type': 'uintbe:32', 'default': vendors[toAscii(n.attributes['vendor'].value)]} )
|
|
60
|
+
if isMessage:
|
|
61
|
+
Fields.append( {'name': 'MessageSubtype', 'type': 'uintbe:8', 'default': int(n.attributes['subtype'].value)} )
|
|
62
|
+
else:
|
|
63
|
+
Fields.append( {'name': 'ParameterSubtype', 'type': 'uintbe:32', 'default': int(n.attributes['subtype'].value)} )
|
|
64
|
+
|
|
65
|
+
for c in n.childNodes:
|
|
66
|
+
if c.nodeName == 'field':
|
|
67
|
+
fieldInfo = { 'name': toAscii(c.attributes['name'].value), 'type': fieldMap[toAscii(c.attributes['type'].value)] }
|
|
68
|
+
try:
|
|
69
|
+
fieldInfo['enumeration'] = toAscii(c.attributes['enumeration'].value)
|
|
70
|
+
except Exception:
|
|
71
|
+
pass
|
|
72
|
+
try:
|
|
73
|
+
fieldInfo['format'] = toAscii(c.attributes['format'].value)
|
|
74
|
+
except Exception:
|
|
75
|
+
pass
|
|
76
|
+
Fields.append( fieldInfo )
|
|
77
|
+
elif c.nodeName == 'reserved':
|
|
78
|
+
bitCount = int(toAscii(c.attributes['bitCount'].value))
|
|
79
|
+
code = 'skip:%d' % bitCount
|
|
80
|
+
Fields.append( {'name':code, 'type':code} )
|
|
81
|
+
elif c.nodeName == 'parameter' or c.nodeName == 'choice':
|
|
82
|
+
repeat = toAscii(c.attributes['repeat'].value)
|
|
83
|
+
minMax = repeat.split('-')
|
|
84
|
+
if len(minMax) == 1:
|
|
85
|
+
rMin = rMax = int(repeat)
|
|
86
|
+
else:
|
|
87
|
+
rMin = int(minMax[0])
|
|
88
|
+
rMax = int(minMax[1]) if minMax[1] != 'N' else 99999
|
|
89
|
+
Parameter = toAscii(c.attributes['type'].value)
|
|
90
|
+
Parameters.append( {'parameter':Parameter, 'repeat': (rMin, rMax), 'choice': int(c.nodeName == 'choice')} )
|
|
91
|
+
pm = {'name':Name, 'typeNum':TypeNum}
|
|
92
|
+
if Fields:
|
|
93
|
+
pm['fields'] = tuple(Fields)
|
|
94
|
+
if Parameters:
|
|
95
|
+
pm['parameters'] = tuple(Parameters)
|
|
96
|
+
return pm
|
|
97
|
+
|
|
98
|
+
def getVendorCode( n ):
|
|
99
|
+
name = toAscii(n.attributes['name'].value)
|
|
100
|
+
id = int(n.attributes['vendorID'].value)
|
|
101
|
+
return (name, id)
|
|
102
|
+
|
|
103
|
+
def getChoiceDefinition( n ):
|
|
104
|
+
choices = {}
|
|
105
|
+
name = toAscii(n.attributes['name'].value)
|
|
106
|
+
for c in n.childNodes:
|
|
107
|
+
if c.nodeName == 'parameter':
|
|
108
|
+
choices[toAscii(c.attributes['type'].value)] = name
|
|
109
|
+
return choices
|
|
110
|
+
|
|
111
|
+
enums, parameters, messages = [], [], []
|
|
112
|
+
vendors = {}
|
|
113
|
+
choiceDefinitions = {}
|
|
114
|
+
|
|
115
|
+
llrpDefXml = '../xml/llrp-1x0-def.xml'
|
|
116
|
+
dom = parse( llrpDefXml )
|
|
117
|
+
|
|
118
|
+
vendors.update( dict(getVendorCode(v) for v in dom.getElementsByTagName('vendorDefinition')) )
|
|
119
|
+
enums = [getEnum(e) for e in dom.getElementsByTagName('enumerationDefinition')]
|
|
120
|
+
parameters = [getParameterMessage(p, False) for p in dom.getElementsByTagName('parameterDefinition')]
|
|
121
|
+
messages = [getParameterMessage(m, True) for m in dom.getElementsByTagName('messageDefinition')]
|
|
122
|
+
for c in dom.getElementsByTagName('choiceDefinition'):
|
|
123
|
+
choiceDefinitions.update( getChoiceDefinition(c) )
|
|
124
|
+
del choiceDefinitions['Custom']
|
|
125
|
+
|
|
126
|
+
dom.unlink()
|
|
127
|
+
dom = None
|
|
128
|
+
|
|
129
|
+
llrpCustomDefXml = '../xml/Impinjdef-1.18-private.xml'
|
|
130
|
+
dom = parse( llrpCustomDefXml )
|
|
131
|
+
|
|
132
|
+
vendors.update( dict(getVendorCode(v) for v in dom.getElementsByTagName('vendorDefinition')) )
|
|
133
|
+
enums.extend( getEnum(e) for e in dom.getElementsByTagName('customEnumerationDefinition') )
|
|
134
|
+
parameters.extend( getParameterMessage(p, False) for p in dom.getElementsByTagName('customParameterDefinition') )
|
|
135
|
+
messages.extend( getParameterMessage(m, True) for m in dom.getElementsByTagName('customMessageDefinition') )
|
|
136
|
+
for p in dom.getElementsByTagName('customParameterDefinition'):
|
|
137
|
+
choiceDefinitions[toAscii(p.attributes['name'].value)] = 'Custom'
|
|
138
|
+
|
|
139
|
+
with open('llrpdef.py', 'w', encoding='utf8') as fp:
|
|
140
|
+
fp.write( '#-----------------------------------------------------------\n' )
|
|
141
|
+
fp.write( '# DO NOT EDIT!\n' )
|
|
142
|
+
fp.write( '# MACHINE GENERATED by ParseDef.py\n' )
|
|
143
|
+
fp.write( '# XML sources:\n' )
|
|
144
|
+
fp.write( '# "{}"\n'.format(llrpDefXml) )
|
|
145
|
+
fp.write( '# "{}"\n'.format(llrpCustomDefXml) )
|
|
146
|
+
fp.write( '# see https://github.com/esitarski/pyllrp for details\n' )
|
|
147
|
+
fp.write( '#-----------------------------------------------------------\n' )
|
|
148
|
+
for n in ['vendors', 'enums', 'parameters', 'messages', 'choiceDefinitions']:
|
|
149
|
+
fp.write( '{}='.format(n) )
|
|
150
|
+
pprint.pprint( globals()[n], fp, compact=True )
|
|
151
|
+
fp.write( '\n' )
|