pyflind 0.1.2__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.
- pyflind-0.1.2/LICENSE +21 -0
- pyflind-0.1.2/PKG-INFO +24 -0
- pyflind-0.1.2/README.md +5 -0
- pyflind-0.1.2/pyproject.toml +39 -0
- pyflind-0.1.2/setup.cfg +4 -0
- pyflind-0.1.2/src/pyflind/__init__.py +0 -0
- pyflind-0.1.2/src/pyflind/bidirmap.py +41 -0
- pyflind-0.1.2/src/pyflind/codec.py +156 -0
- pyflind-0.1.2/src/pyflind/device.py +1243 -0
- pyflind-0.1.2/src/pyflind/deviceio.py +277 -0
- pyflind-0.1.2/src/pyflind/group.py +285 -0
- pyflind-0.1.2/src/pyflind/recording.py +258 -0
- pyflind-0.1.2/src/pyflind/smx00.py +573 -0
- pyflind-0.1.2/src/pyflind/smx00_cli.py +185 -0
- pyflind-0.1.2/src/pyflind/util.py +69 -0
- pyflind-0.1.2/src/pyflind.egg-info/PKG-INFO +24 -0
- pyflind-0.1.2/src/pyflind.egg-info/SOURCES.txt +19 -0
- pyflind-0.1.2/src/pyflind.egg-info/dependency_links.txt +1 -0
- pyflind-0.1.2/src/pyflind.egg-info/entry_points.txt +2 -0
- pyflind-0.1.2/src/pyflind.egg-info/requires.txt +2 -0
- pyflind-0.1.2/src/pyflind.egg-info/top_level.txt +1 -0
pyflind-0.1.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 FieldLine Industries, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pyflind-0.1.2/PKG-INFO
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyflind
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: A python library for FieldLine Industries products
|
|
5
|
+
Author-email: Matt Liss <matt@fieldlineindustries.com>, Ezra Godfrey <eg@fieldlineinc.com>, Andreas Griesshammer <ag@fieldlineinc.com>
|
|
6
|
+
Maintainer-email: Matt Liss <matt@fieldlineindustries.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Homepage, https://fieldlineindustries.com
|
|
9
|
+
Project-URL: Repository, https://bitbucket.org/fl-industries/fli-python-oss
|
|
10
|
+
Keywords: FieldLine,sm200,sm300,smx00
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.8
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: numpy
|
|
17
|
+
Requires-Dist: pyserial
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# Python FieldLineIndustries
|
|
21
|
+
|
|
22
|
+
A python library for interfacing with FieldLine Industries' products.
|
|
23
|
+
|
|
24
|
+
TODO link to readthedocs
|
pyflind-0.1.2/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77.0.3", "wheel", "setuptools-git-versioning>=2.0,<3"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[tool.setuptools-git-versioning]
|
|
6
|
+
template = "{tag}.{ccount}"
|
|
7
|
+
dev_template = "{tag}.{ccount}"
|
|
8
|
+
dirty_template = "{tag}.{ccount}+g{sha}-dirty"
|
|
9
|
+
enabled = true
|
|
10
|
+
|
|
11
|
+
[project]
|
|
12
|
+
name = "pyflind"
|
|
13
|
+
description = "A python library for FieldLine Industries products"
|
|
14
|
+
keywords = ["FieldLine", "sm200", "sm300", "smx00"]
|
|
15
|
+
dynamic = ["version"]
|
|
16
|
+
dependencies = ["numpy", "pyserial"]
|
|
17
|
+
requires-python = ">=3.8"
|
|
18
|
+
authors = [
|
|
19
|
+
{ name="Matt Liss", email="matt@fieldlineindustries.com" },
|
|
20
|
+
{ name="Ezra Godfrey", email="eg@fieldlineinc.com" },
|
|
21
|
+
{ name="Andreas Griesshammer", email="ag@fieldlineinc.com" },
|
|
22
|
+
]
|
|
23
|
+
maintainers = [
|
|
24
|
+
{ name="Matt Liss", email="matt@fieldlineindustries.com" },
|
|
25
|
+
]
|
|
26
|
+
readme = "README.md"
|
|
27
|
+
classifiers = [
|
|
28
|
+
"Programming Language :: Python :: 3",
|
|
29
|
+
"Operating System :: OS Independent",
|
|
30
|
+
]
|
|
31
|
+
license="MIT"
|
|
32
|
+
license-files = ["LICEN[CS]E*"]
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
smx00-cli = "pyflind.smx00_cli:main"
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://fieldlineindustries.com"
|
|
39
|
+
Repository = "https://bitbucket.org/fl-industries/fli-python-oss"
|
pyflind-0.1.2/setup.cfg
ADDED
|
File without changes
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
|
|
2
|
+
class BidirectionalStrIntMap(object):
|
|
3
|
+
''' Dictionary with bidirectional str<->int lookup.
|
|
4
|
+
'''
|
|
5
|
+
|
|
6
|
+
def __init__(self):
|
|
7
|
+
self._dict = {}
|
|
8
|
+
|
|
9
|
+
def __getitem__(self, key):
|
|
10
|
+
if isinstance(key, int):
|
|
11
|
+
if key not in self._dict.values():
|
|
12
|
+
raise IndexError(key)
|
|
13
|
+
return list(self._dict.keys())[list(self._dict.values()).index(key)]
|
|
14
|
+
elif isinstance(key, str):
|
|
15
|
+
if key not in self._dict:
|
|
16
|
+
raise KeyError(key)
|
|
17
|
+
return self._dict[key]
|
|
18
|
+
else:
|
|
19
|
+
raise TypeError(key)
|
|
20
|
+
|
|
21
|
+
def __contains__(self, key):
|
|
22
|
+
return key in self._dict or key in self._dict.values()
|
|
23
|
+
|
|
24
|
+
def __repr__(self):
|
|
25
|
+
return self.__str__()
|
|
26
|
+
|
|
27
|
+
def __str__(self):
|
|
28
|
+
s = f'{type(self).__name__}: {{ \n'
|
|
29
|
+
for k, v in self._dict.items():
|
|
30
|
+
s += f' {v: 4d} (0x{v:02x}): {k}\n'
|
|
31
|
+
s += '}'
|
|
32
|
+
return s
|
|
33
|
+
|
|
34
|
+
def keys(self):
|
|
35
|
+
return self._dict.keys()
|
|
36
|
+
|
|
37
|
+
def values(self):
|
|
38
|
+
return self._dict.values()
|
|
39
|
+
|
|
40
|
+
def items(self):
|
|
41
|
+
return self._dict.items()
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import struct
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class V2SerialProtocolCodec:
|
|
6
|
+
'''Encoder/decoder for FLI serial protocol version 2.
|
|
7
|
+
'''
|
|
8
|
+
|
|
9
|
+
def __init__(self):
|
|
10
|
+
self.logger = logging.getLogger('V2SerialProtocolCodec')
|
|
11
|
+
self.data = b''
|
|
12
|
+
self.esc_char = False
|
|
13
|
+
self.valid_packet = False
|
|
14
|
+
self.dt_reg_read = 0x3
|
|
15
|
+
|
|
16
|
+
def push_char(self, in_char):
|
|
17
|
+
'''Push a received char to the decoder.
|
|
18
|
+
|
|
19
|
+
The decoder maintains state between calls and only returns data
|
|
20
|
+
values when the char completes a received packet to decode.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
in_char: received char
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
timestamp: device sample counter on complete packet, None otherwise
|
|
27
|
+
streams: dict of decoded stream values
|
|
28
|
+
reg_vals: list of register (address, value) tuples
|
|
29
|
+
'''
|
|
30
|
+
timestamp = None
|
|
31
|
+
streams = dict()
|
|
32
|
+
reg_vals = []
|
|
33
|
+
if not self.esc_char and in_char == 0x1B:
|
|
34
|
+
self.esc_char = True
|
|
35
|
+
elif self.esc_char:
|
|
36
|
+
self.esc_char = False
|
|
37
|
+
self.data += bytes([in_char])
|
|
38
|
+
elif in_char == 0x0A:
|
|
39
|
+
self.valid_packet = True
|
|
40
|
+
self.data = b''
|
|
41
|
+
elif self.valid_packet and in_char == 0x0D:
|
|
42
|
+
if len(self.data) < 7:
|
|
43
|
+
self.logger.error(f'not enough bytes in packet: {len(self.data)}')
|
|
44
|
+
self.data = b''
|
|
45
|
+
else:
|
|
46
|
+
timestamp = int.from_bytes(self.data[0:2], byteorder='big')
|
|
47
|
+
streams = dict()
|
|
48
|
+
self.data = self.data[2:]
|
|
49
|
+
while len(self.data):
|
|
50
|
+
datatype = int.from_bytes(self.data[0:1], byteorder='big')
|
|
51
|
+
try:
|
|
52
|
+
dataval = struct.unpack('>i', self.data[1:5])[0]
|
|
53
|
+
if datatype == self.dt_reg_read:
|
|
54
|
+
raw_reg = (dataval >> 16) & 0xFFFF
|
|
55
|
+
raw_val = dataval & 0xFFFF
|
|
56
|
+
reg_vals.append((raw_reg, raw_val))
|
|
57
|
+
else:
|
|
58
|
+
streams[datatype] = dataval
|
|
59
|
+
except struct.error as e:
|
|
60
|
+
self.logger.error(f'failed to unpack int: {e}')
|
|
61
|
+
timestamp = None
|
|
62
|
+
streams = dict()
|
|
63
|
+
reg_vals = []
|
|
64
|
+
self.data = b''
|
|
65
|
+
self.valid_packet = False
|
|
66
|
+
break
|
|
67
|
+
self.data = self.data[5:]
|
|
68
|
+
self.valid_packet = False
|
|
69
|
+
elif self.valid_packet:
|
|
70
|
+
self.data += bytes([in_char])
|
|
71
|
+
elif self.data != b'':
|
|
72
|
+
self.logger.warning(f'partial data read detected! self.data: {self.data}')
|
|
73
|
+
|
|
74
|
+
return timestamp, streams, reg_vals
|
|
75
|
+
|
|
76
|
+
def get_max_streams(self, baud, fs, crc=False):
|
|
77
|
+
'''Compute the max number of streams supported for a given baud and samplerate.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
baud: serial communications bitrate
|
|
81
|
+
fs: stream samplerate
|
|
82
|
+
crc: bool indicating state of CRC enable
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
int: max number of streams supportable
|
|
86
|
+
'''
|
|
87
|
+
bits_per_byte = 10 # 8 data, 1 start, 1 stop
|
|
88
|
+
overhead_bytes = 4 # framing and timestamp
|
|
89
|
+
if crc:
|
|
90
|
+
overhead_bytes += 2 # CRC-16
|
|
91
|
+
bytes_per_stream = 5 # 1 stream ID, 4 stream value
|
|
92
|
+
|
|
93
|
+
bytes_per_sample_avail = int(baud / bits_per_byte / fs) - overhead_bytes
|
|
94
|
+
|
|
95
|
+
return int(bytes_per_sample_avail/bytes_per_stream)
|
|
96
|
+
|
|
97
|
+
def int_to_hex_bytes(self, value, nbytes=2):
|
|
98
|
+
'''Convert integer to ascii encoded hex bytes array.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
value: integer value
|
|
102
|
+
nbytes: number of hex bytes to output (0-padded on left)
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
bytes: encoded value
|
|
106
|
+
'''
|
|
107
|
+
if type(value) != int:
|
|
108
|
+
raise TypeError(value)
|
|
109
|
+
if value < 0 or value > 2**(nbytes*4)-1:
|
|
110
|
+
raise ValueError(value)
|
|
111
|
+
return f'{value:0{nbytes}X}'.encode('ascii')
|
|
112
|
+
|
|
113
|
+
def _one_time_read(self):
|
|
114
|
+
return b'#' + self.int_to_hex_bytes(3) + b'FFFF'
|
|
115
|
+
|
|
116
|
+
def one_time_read_cmd(self, address):
|
|
117
|
+
'''Build a command for one-time-read of a register.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
address: register address to read
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
bytes: encoded command
|
|
124
|
+
'''
|
|
125
|
+
cmd = b'@' + self.int_to_hex_bytes(3)
|
|
126
|
+
cmd += self.int_to_hex_bytes(address, nbytes=4)
|
|
127
|
+
cmd += self._one_time_read()
|
|
128
|
+
return cmd
|
|
129
|
+
|
|
130
|
+
def reg_write_cmd(self, address, value):
|
|
131
|
+
'''Build a command to write a register value.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
address: register address
|
|
135
|
+
value: register value
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
bytes: encoded command
|
|
139
|
+
'''
|
|
140
|
+
cmd = b'@' + self.int_to_hex_bytes(address)
|
|
141
|
+
cmd += self.int_to_hex_bytes(value, nbytes=4)
|
|
142
|
+
return cmd
|
|
143
|
+
|
|
144
|
+
def config_stream_cmd(self, sid, enable):
|
|
145
|
+
'''Build a command to configure a stream.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
sid: stream id
|
|
149
|
+
enable: True to enable, False to disable
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
bytes: encoded command
|
|
153
|
+
'''
|
|
154
|
+
cmd = b'#' + self.int_to_hex_bytes(sid)
|
|
155
|
+
cmd += self.int_to_hex_bytes(1 if enable else 0, nbytes=4)
|
|
156
|
+
return cmd
|