nuotec-sent2usb 0.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.
- nuotec_sent2usb-0.1.0/LICENSE.txt +21 -0
- nuotec_sent2usb-0.1.0/PKG-INFO +188 -0
- nuotec_sent2usb-0.1.0/README.md +176 -0
- nuotec_sent2usb-0.1.0/pyproject.toml +29 -0
- nuotec_sent2usb-0.1.0/pyproject.toml.orig +29 -0
- nuotec_sent2usb-0.1.0/src/nuotec_sent2usb/__init__.py +19 -0
- nuotec_sent2usb-0.1.0/src/nuotec_sent2usb/cli.py +191 -0
- nuotec_sent2usb-0.1.0/src/nuotec_sent2usb/decoder.py +61 -0
- nuotec_sent2usb-0.1.0/src/nuotec_sent2usb/device.py +192 -0
- nuotec_sent2usb-0.1.0/src/nuotec_sent2usb/device_version.py +40 -0
- nuotec_sent2usb-0.1.0/src/nuotec_sent2usb/exceptions.py +10 -0
- nuotec_sent2usb-0.1.0/src/nuotec_sent2usb/frame.py +65 -0
- nuotec_sent2usb-0.1.0/src/nuotec_sent2usb/nuotec-sent2usb.bin +0 -0
- nuotec_sent2usb-0.1.0/src/nuotec_sent2usb/optionbytes.bin +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nuotec GmbH
|
|
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.
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nuotec-sent2usb
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Driver for Nuotec SENT2USB Converter
|
|
5
|
+
Author: Nuotec GmbH
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE.txt
|
|
8
|
+
Requires-Dist: cobs>=1.2.2,<2
|
|
9
|
+
Requires-Dist: pyserial>=3.5,<4
|
|
10
|
+
Requires-Python: >=3.8
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# Nuotec SENT2USB Python Driver
|
|
14
|
+
|
|
15
|
+
This is an alternative Python driver for the
|
|
16
|
+
[SENT (SAE J2716) to USB converter from uCANDevices](https://ucandevices.github.io/sentusb.html).
|
|
17
|
+
|
|
18
|
+
> [!IMPORTANT]
|
|
19
|
+
> This driver requires the firmware from
|
|
20
|
+
> [nuotec-sent2usb-firmware](https://gitlab.com/nuotec/sent2usb/nuotec-sent2usb-firmware)
|
|
21
|
+
> to be running on the device. See the instructions below to replace the
|
|
22
|
+
> original firmware with this firmware.
|
|
23
|
+
|
|
24
|
+
> [!NOTE]
|
|
25
|
+
> For more information about the advantages and disadvantages of this
|
|
26
|
+
> alternative firmware and Python driver compared to the original product, see
|
|
27
|
+
> details at
|
|
28
|
+
> [nuotec-sent2usb-firmware](https://gitlab.com/nuotec/sent2usb/nuotec-sent2usb-firmware).
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
Install the package from PyPI:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install nuotec-sent2usb
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## CLI Usage
|
|
39
|
+
|
|
40
|
+
The easiest way to use this driver is through its command-line interface:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
$ nuotec-sent2usb --help
|
|
44
|
+
|
|
45
|
+
usage: nuotec-sent2usb [-h] [-l] [-a] [-b] [-o] [-f] [-u] [-p PORT] [-s SEND] [-i INTERVAL] [-m] [-t TICK_TIME] [-w WIDTH]
|
|
46
|
+
|
|
47
|
+
options:
|
|
48
|
+
-h, --help show this help message and exit
|
|
49
|
+
-l, --list List connected devices
|
|
50
|
+
-a, --all List all available serial ports
|
|
51
|
+
-b, --bootloader Enter DFU bootloader
|
|
52
|
+
-o, --flash-option-bytes
|
|
53
|
+
Flash option bytes with dfu-util
|
|
54
|
+
-f, --flash Flash firmware with dfu-util
|
|
55
|
+
-u, --update Check & update firmware on device
|
|
56
|
+
-p, --port PORT Serial port
|
|
57
|
+
-s, --send SEND Send nibbles (e.g. '0abc')
|
|
58
|
+
-i, --interval INTERVAL
|
|
59
|
+
Send data periodically in this interval [s]
|
|
60
|
+
-m, --monitor Monitor and print received SENT frames
|
|
61
|
+
-t, --tick-time TICK_TIME
|
|
62
|
+
Clock tick time in microseconds
|
|
63
|
+
-w, --width WIDTH Low pulse length in ticks
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Flash Firmware
|
|
67
|
+
|
|
68
|
+
After you have purchased an original
|
|
69
|
+
[SENTToUSB device from uCANDevices](https://ucandevices.github.io/sentusb.html),
|
|
70
|
+
you need to replace its firmware to make it work with this Python driver.
|
|
71
|
+
|
|
72
|
+
> [!CAUTION]
|
|
73
|
+
> This step may be risky. After flashing our firmware, your device won't be
|
|
74
|
+
> compatible with the tools from uCANDevices anymore. It is recommended to
|
|
75
|
+
> back up the original firmware on the device first, to allow restoring it
|
|
76
|
+
> at any time.
|
|
77
|
+
|
|
78
|
+
First, instruct the original firmware to enter the bootloader (adjust
|
|
79
|
+
`/dev/ttyACM0` to match your serial port):
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
python -c "import serial,time; s=serial.Serial('/dev/ttyACM0',115200,timeout=1); s.write(b'boot\r'); time.sleep(0.3); s.close()"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Then flash the new firmware to it. This will require
|
|
86
|
+
[dfu-util](https://dfu-util.sourceforge.net/) to be available in `PATH`:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
nuotec-sent2usb -f
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Update Firmware
|
|
93
|
+
|
|
94
|
+
If the device is already running our custom firmware, it can be updated with
|
|
95
|
+
a single command (the firmware binary is contained in the Python driver):
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
nuotec-sent2usb -u
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Example: List Devices
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
$ nuotec-sent2usb -l
|
|
105
|
+
/dev/ttyACM0 Nuotec SENT2USB - CDC Interface 002E002F393043022033344E
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Example: Send Frame (90us Tick Time, Data=123456)
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
$ nuotec-sent2usb -t 90 -s 123456
|
|
112
|
+
Firmware: 1.0.0 (9b287c72ce4c48e7c11475cbb7acde5df17e562a)
|
|
113
|
+
Tick Time: 90 μs
|
|
114
|
+
TX: status=0x1 data=[0x2, 0x3, 0x4, 0x5, 0x6] crc=0x0
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Example: Monitor Received Frames (90us Tick Time)
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
$ nuotec-sent2usb -t 90 -m
|
|
121
|
+
Firmware: 1.0.0 (9b287c72ce4c48e7c11475cbb7acde5df17e562a)
|
|
122
|
+
Tick Time: 90 μs
|
|
123
|
+
RX: status=0x0 data=[0x1, 0x0, 0xC, 0x3, 0xC, 0x0] crc=0xB
|
|
124
|
+
RX: status=0x0 data=[0x1, 0x0, 0xC, 0x3, 0xC, 0x0] crc=0xB
|
|
125
|
+
RX: status=0x0 data=[0x1, 0x0, 0xC, 0x3, 0xC, 0x0] crc=0xB
|
|
126
|
+
RX: status=0x0 data=[0x1, 0x0, 0xC, 0x3, 0xC, 0x0] crc=0xB
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Press Ctrl+C to cancel.
|
|
130
|
+
|
|
131
|
+
## API Usage
|
|
132
|
+
|
|
133
|
+
The driver can also be used from Python directly:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
import time
|
|
137
|
+
from nuotec_sent2usb import Sent2UsbDevice, SentFrame, SentDecoder
|
|
138
|
+
|
|
139
|
+
with Sent2UsbDevice() as device:
|
|
140
|
+
print(f"Firmware: {device.get_version()}")
|
|
141
|
+
|
|
142
|
+
# Send frame
|
|
143
|
+
frame = SentFrame(status=0x0, data=[0x1, 0x2, 0x3, 0x4, 0x5, 0x6])
|
|
144
|
+
device.send_pulses(frame.to_pulses(5, 90)) # width [ticks] & tick time [us]
|
|
145
|
+
print(f"TX: {frame}")
|
|
146
|
+
|
|
147
|
+
# Receive frames
|
|
148
|
+
decoder = SentDecoder(90) # tick time [us]
|
|
149
|
+
while True:
|
|
150
|
+
for period, width in device.fetch_pulses():
|
|
151
|
+
frame = decoder.process(period, width)
|
|
152
|
+
if frame is not None:
|
|
153
|
+
print(f"RX: {frame}")
|
|
154
|
+
time.sleep(0.05)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
For more details, see the Python files in this package.
|
|
158
|
+
|
|
159
|
+
## Disclaimer
|
|
160
|
+
|
|
161
|
+
This project is provided as-is without any guarantee for correct behavior.
|
|
162
|
+
It was a side project, without the goal of creating a highly reliable
|
|
163
|
+
production device. So far it seems to work fine, but the code is not perfect
|
|
164
|
+
at all and would need some deeper review, rework and more testing to get more
|
|
165
|
+
confidence about the quality and reliability of this product.
|
|
166
|
+
|
|
167
|
+
## Contributions
|
|
168
|
+
|
|
169
|
+
Generally, this project has been published just in case it could be useful for
|
|
170
|
+
other people. However, it may or may not fit your use-case. In case you are
|
|
171
|
+
interested in adding more functionality, bugfixing, etc., we are happy to
|
|
172
|
+
receive contributions (though without any guarantee that we find the time to
|
|
173
|
+
review & merge it - it's recommended to contact us in advance about your
|
|
174
|
+
intentions).
|
|
175
|
+
|
|
176
|
+
## Credits
|
|
177
|
+
|
|
178
|
+
The development of this project is kindly sponsored by
|
|
179
|
+
[Nuotec GmbH](https://nuotec.ch) and
|
|
180
|
+
[Sensirion Automotive Solutions AG](https://sensirion-automotive.com/).
|
|
181
|
+
|
|
182
|
+
Many thanks to [uCANDevices](https://ucandevices.github.io/) for developing
|
|
183
|
+
the hardware this firmware runs on, which is available for purchase through
|
|
184
|
+
their website.
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
Released under MIT, see [LICENSE.txt](LICENSE.txt).
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Nuotec SENT2USB Python Driver
|
|
2
|
+
|
|
3
|
+
This is an alternative Python driver for the
|
|
4
|
+
[SENT (SAE J2716) to USB converter from uCANDevices](https://ucandevices.github.io/sentusb.html).
|
|
5
|
+
|
|
6
|
+
> [!IMPORTANT]
|
|
7
|
+
> This driver requires the firmware from
|
|
8
|
+
> [nuotec-sent2usb-firmware](https://gitlab.com/nuotec/sent2usb/nuotec-sent2usb-firmware)
|
|
9
|
+
> to be running on the device. See the instructions below to replace the
|
|
10
|
+
> original firmware with this firmware.
|
|
11
|
+
|
|
12
|
+
> [!NOTE]
|
|
13
|
+
> For more information about the advantages and disadvantages of this
|
|
14
|
+
> alternative firmware and Python driver compared to the original product, see
|
|
15
|
+
> details at
|
|
16
|
+
> [nuotec-sent2usb-firmware](https://gitlab.com/nuotec/sent2usb/nuotec-sent2usb-firmware).
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
Install the package from PyPI:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install nuotec-sent2usb
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## CLI Usage
|
|
27
|
+
|
|
28
|
+
The easiest way to use this driver is through its command-line interface:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
$ nuotec-sent2usb --help
|
|
32
|
+
|
|
33
|
+
usage: nuotec-sent2usb [-h] [-l] [-a] [-b] [-o] [-f] [-u] [-p PORT] [-s SEND] [-i INTERVAL] [-m] [-t TICK_TIME] [-w WIDTH]
|
|
34
|
+
|
|
35
|
+
options:
|
|
36
|
+
-h, --help show this help message and exit
|
|
37
|
+
-l, --list List connected devices
|
|
38
|
+
-a, --all List all available serial ports
|
|
39
|
+
-b, --bootloader Enter DFU bootloader
|
|
40
|
+
-o, --flash-option-bytes
|
|
41
|
+
Flash option bytes with dfu-util
|
|
42
|
+
-f, --flash Flash firmware with dfu-util
|
|
43
|
+
-u, --update Check & update firmware on device
|
|
44
|
+
-p, --port PORT Serial port
|
|
45
|
+
-s, --send SEND Send nibbles (e.g. '0abc')
|
|
46
|
+
-i, --interval INTERVAL
|
|
47
|
+
Send data periodically in this interval [s]
|
|
48
|
+
-m, --monitor Monitor and print received SENT frames
|
|
49
|
+
-t, --tick-time TICK_TIME
|
|
50
|
+
Clock tick time in microseconds
|
|
51
|
+
-w, --width WIDTH Low pulse length in ticks
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Flash Firmware
|
|
55
|
+
|
|
56
|
+
After you have purchased an original
|
|
57
|
+
[SENTToUSB device from uCANDevices](https://ucandevices.github.io/sentusb.html),
|
|
58
|
+
you need to replace its firmware to make it work with this Python driver.
|
|
59
|
+
|
|
60
|
+
> [!CAUTION]
|
|
61
|
+
> This step may be risky. After flashing our firmware, your device won't be
|
|
62
|
+
> compatible with the tools from uCANDevices anymore. It is recommended to
|
|
63
|
+
> back up the original firmware on the device first, to allow restoring it
|
|
64
|
+
> at any time.
|
|
65
|
+
|
|
66
|
+
First, instruct the original firmware to enter the bootloader (adjust
|
|
67
|
+
`/dev/ttyACM0` to match your serial port):
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
python -c "import serial,time; s=serial.Serial('/dev/ttyACM0',115200,timeout=1); s.write(b'boot\r'); time.sleep(0.3); s.close()"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Then flash the new firmware to it. This will require
|
|
74
|
+
[dfu-util](https://dfu-util.sourceforge.net/) to be available in `PATH`:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
nuotec-sent2usb -f
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Update Firmware
|
|
81
|
+
|
|
82
|
+
If the device is already running our custom firmware, it can be updated with
|
|
83
|
+
a single command (the firmware binary is contained in the Python driver):
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
nuotec-sent2usb -u
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Example: List Devices
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
$ nuotec-sent2usb -l
|
|
93
|
+
/dev/ttyACM0 Nuotec SENT2USB - CDC Interface 002E002F393043022033344E
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Example: Send Frame (90us Tick Time, Data=123456)
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
$ nuotec-sent2usb -t 90 -s 123456
|
|
100
|
+
Firmware: 1.0.0 (9b287c72ce4c48e7c11475cbb7acde5df17e562a)
|
|
101
|
+
Tick Time: 90 μs
|
|
102
|
+
TX: status=0x1 data=[0x2, 0x3, 0x4, 0x5, 0x6] crc=0x0
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Example: Monitor Received Frames (90us Tick Time)
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
$ nuotec-sent2usb -t 90 -m
|
|
109
|
+
Firmware: 1.0.0 (9b287c72ce4c48e7c11475cbb7acde5df17e562a)
|
|
110
|
+
Tick Time: 90 μs
|
|
111
|
+
RX: status=0x0 data=[0x1, 0x0, 0xC, 0x3, 0xC, 0x0] crc=0xB
|
|
112
|
+
RX: status=0x0 data=[0x1, 0x0, 0xC, 0x3, 0xC, 0x0] crc=0xB
|
|
113
|
+
RX: status=0x0 data=[0x1, 0x0, 0xC, 0x3, 0xC, 0x0] crc=0xB
|
|
114
|
+
RX: status=0x0 data=[0x1, 0x0, 0xC, 0x3, 0xC, 0x0] crc=0xB
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Press Ctrl+C to cancel.
|
|
118
|
+
|
|
119
|
+
## API Usage
|
|
120
|
+
|
|
121
|
+
The driver can also be used from Python directly:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
import time
|
|
125
|
+
from nuotec_sent2usb import Sent2UsbDevice, SentFrame, SentDecoder
|
|
126
|
+
|
|
127
|
+
with Sent2UsbDevice() as device:
|
|
128
|
+
print(f"Firmware: {device.get_version()}")
|
|
129
|
+
|
|
130
|
+
# Send frame
|
|
131
|
+
frame = SentFrame(status=0x0, data=[0x1, 0x2, 0x3, 0x4, 0x5, 0x6])
|
|
132
|
+
device.send_pulses(frame.to_pulses(5, 90)) # width [ticks] & tick time [us]
|
|
133
|
+
print(f"TX: {frame}")
|
|
134
|
+
|
|
135
|
+
# Receive frames
|
|
136
|
+
decoder = SentDecoder(90) # tick time [us]
|
|
137
|
+
while True:
|
|
138
|
+
for period, width in device.fetch_pulses():
|
|
139
|
+
frame = decoder.process(period, width)
|
|
140
|
+
if frame is not None:
|
|
141
|
+
print(f"RX: {frame}")
|
|
142
|
+
time.sleep(0.05)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
For more details, see the Python files in this package.
|
|
146
|
+
|
|
147
|
+
## Disclaimer
|
|
148
|
+
|
|
149
|
+
This project is provided as-is without any guarantee for correct behavior.
|
|
150
|
+
It was a side project, without the goal of creating a highly reliable
|
|
151
|
+
production device. So far it seems to work fine, but the code is not perfect
|
|
152
|
+
at all and would need some deeper review, rework and more testing to get more
|
|
153
|
+
confidence about the quality and reliability of this product.
|
|
154
|
+
|
|
155
|
+
## Contributions
|
|
156
|
+
|
|
157
|
+
Generally, this project has been published just in case it could be useful for
|
|
158
|
+
other people. However, it may or may not fit your use-case. In case you are
|
|
159
|
+
interested in adding more functionality, bugfixing, etc., we are happy to
|
|
160
|
+
receive contributions (though without any guarantee that we find the time to
|
|
161
|
+
review & merge it - it's recommended to contact us in advance about your
|
|
162
|
+
intentions).
|
|
163
|
+
|
|
164
|
+
## Credits
|
|
165
|
+
|
|
166
|
+
The development of this project is kindly sponsored by
|
|
167
|
+
[Nuotec GmbH](https://nuotec.ch) and
|
|
168
|
+
[Sensirion Automotive Solutions AG](https://sensirion-automotive.com/).
|
|
169
|
+
|
|
170
|
+
Many thanks to [uCANDevices](https://ucandevices.github.io/) for developing
|
|
171
|
+
the hardware this firmware runs on, which is available for purchase through
|
|
172
|
+
their website.
|
|
173
|
+
|
|
174
|
+
## License
|
|
175
|
+
|
|
176
|
+
Released under MIT, see [LICENSE.txt](LICENSE.txt).
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "nuotec-sent2usb"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Driver for Nuotec SENT2USB Converter"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE.txt"]
|
|
8
|
+
requires-python = ">=3.8"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"cobs>=1.2.2,<2",
|
|
11
|
+
"pyserial>=3.5,<4",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[[project.authors]]
|
|
15
|
+
name = "Nuotec GmbH"
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
nuotec-sent2usb = "nuotec_sent2usb.cli:main"
|
|
19
|
+
sent2usb = "nuotec_sent2usb.cli:main"
|
|
20
|
+
|
|
21
|
+
[dependency-groups]
|
|
22
|
+
dev = [
|
|
23
|
+
"pytest>=8.0,<8.4",
|
|
24
|
+
"ruff>=0.16.2,<0.17",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[build-system]
|
|
28
|
+
requires = ["uv_build>=0.12.3,<0.13"]
|
|
29
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "nuotec-sent2usb"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Driver for Nuotec SENT2USB Converter"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE.txt"]
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Nuotec GmbH" },
|
|
10
|
+
]
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"cobs>=1.2.2,<2",
|
|
14
|
+
"pyserial>=3.5,<4",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[dependency-groups]
|
|
18
|
+
dev = [
|
|
19
|
+
"pytest>=8.0,<8.4",
|
|
20
|
+
"ruff>=0.16.2,<0.17",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
nuotec-sent2usb = "nuotec_sent2usb.cli:main"
|
|
25
|
+
sent2usb = "nuotec_sent2usb.cli:main"
|
|
26
|
+
|
|
27
|
+
[build-system]
|
|
28
|
+
requires = ["uv_build>=0.12.3,<0.13"]
|
|
29
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .decoder import SentDecoder
|
|
2
|
+
from .device import Sent2UsbDevice
|
|
3
|
+
from .device_version import Sent2UsbDeviceVersion
|
|
4
|
+
from .exceptions import (
|
|
5
|
+
Sent2UsbCommunicationError,
|
|
6
|
+
Sent2UsbDeviceError,
|
|
7
|
+
Sent2UsbError,
|
|
8
|
+
)
|
|
9
|
+
from .frame import SentFrame
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"Sent2UsbCommunicationError",
|
|
13
|
+
"Sent2UsbDevice",
|
|
14
|
+
"Sent2UsbDeviceError",
|
|
15
|
+
"Sent2UsbDeviceVersion",
|
|
16
|
+
"Sent2UsbError",
|
|
17
|
+
"SentDecoder",
|
|
18
|
+
"SentFrame",
|
|
19
|
+
]
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
from importlib import resources
|
|
6
|
+
from struct import unpack
|
|
7
|
+
|
|
8
|
+
from .decoder import SentDecoder
|
|
9
|
+
from .device import Sent2UsbDevice
|
|
10
|
+
from .device_version import Sent2UsbDeviceVersion
|
|
11
|
+
from .frame import SentFrame
|
|
12
|
+
|
|
13
|
+
FIRMWARE_RESOURCE = ("nuotec_sent2usb", "nuotec-sent2usb.bin")
|
|
14
|
+
OPTIONBYTES_RESOURCE = ("nuotec_sent2usb", "optionbytes.bin")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main():
|
|
18
|
+
parser = argparse.ArgumentParser()
|
|
19
|
+
parser.add_argument(
|
|
20
|
+
"-l", "--list", action="store_true", help="List connected devices"
|
|
21
|
+
)
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"-a", "--all", action="store_true", help="List all available serial ports"
|
|
24
|
+
)
|
|
25
|
+
parser.add_argument(
|
|
26
|
+
"-b", "--bootloader", action="store_true", help="Enter DFU bootloader"
|
|
27
|
+
)
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
"-o",
|
|
30
|
+
"--flash-option-bytes",
|
|
31
|
+
action="store_true",
|
|
32
|
+
help="Flash option bytes with dfu-util",
|
|
33
|
+
)
|
|
34
|
+
parser.add_argument(
|
|
35
|
+
"-f", "--flash", action="store_true", help="Flash firmware with dfu-util"
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"-u", "--update", action="store_true", help="Check & update firmware on device"
|
|
39
|
+
)
|
|
40
|
+
parser.add_argument("-p", "--port", type=str, help="Serial port")
|
|
41
|
+
parser.add_argument(
|
|
42
|
+
"-s",
|
|
43
|
+
"--send",
|
|
44
|
+
type=str,
|
|
45
|
+
help="Send nibbles (e.g. '0abc')",
|
|
46
|
+
)
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
"-i",
|
|
49
|
+
"--interval",
|
|
50
|
+
type=float,
|
|
51
|
+
help="Send data periodically in this interval [s]",
|
|
52
|
+
)
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
"-m",
|
|
55
|
+
"--monitor",
|
|
56
|
+
action="store_true",
|
|
57
|
+
help="Monitor and print received SENT frames",
|
|
58
|
+
)
|
|
59
|
+
parser.add_argument(
|
|
60
|
+
"-t", "--tick-time", type=int, default=3, help="Clock tick time in microseconds"
|
|
61
|
+
)
|
|
62
|
+
parser.add_argument(
|
|
63
|
+
"-w", "--width", type=int, default=5, help="Low pulse length in ticks"
|
|
64
|
+
)
|
|
65
|
+
args = parser.parse_args()
|
|
66
|
+
|
|
67
|
+
# --list
|
|
68
|
+
if args.list:
|
|
69
|
+
try:
|
|
70
|
+
ports = Sent2UsbDevice.list_ports(list_all=args.all, as_objects=True)
|
|
71
|
+
for p in ports:
|
|
72
|
+
print(f"{p.device} {p.manufacturer} {p.description} {p.serial_number}")
|
|
73
|
+
if len(ports) == 0:
|
|
74
|
+
print(
|
|
75
|
+
"No device found. Make sure the nuotec-sent2usb firmware is flashed and running."
|
|
76
|
+
)
|
|
77
|
+
except Exception as e: # noqa: BLE001
|
|
78
|
+
print(f"ERROR: {e!s}")
|
|
79
|
+
sys.exit(1)
|
|
80
|
+
|
|
81
|
+
# --update
|
|
82
|
+
if args.update:
|
|
83
|
+
try:
|
|
84
|
+
with resources.path(*FIRMWARE_RESOURCE) as p, open(p, "rb") as f:
|
|
85
|
+
content = f.read()
|
|
86
|
+
pattern, major, minor, patch, candidate, name_len, desc_len = unpack(
|
|
87
|
+
"<IBBBBII", content[0x100:0x110]
|
|
88
|
+
)
|
|
89
|
+
if pattern != 0x6E758001:
|
|
90
|
+
raise ValueError("Invalid firmware binary (magic pattern not found).")
|
|
91
|
+
latest_version = Sent2UsbDeviceVersion(
|
|
92
|
+
major,
|
|
93
|
+
minor,
|
|
94
|
+
patch,
|
|
95
|
+
candidate,
|
|
96
|
+
content[0x114 + name_len : 0x114 + name_len + desc_len],
|
|
97
|
+
)
|
|
98
|
+
print(f"Latest version: {latest_version}")
|
|
99
|
+
with Sent2UsbDevice(args.port) as device:
|
|
100
|
+
version = device.get_version()
|
|
101
|
+
print(f"Device version: {version}")
|
|
102
|
+
if latest_version.build_id == version.build_id:
|
|
103
|
+
print("Firmware is up-to-date.")
|
|
104
|
+
else:
|
|
105
|
+
args.bootloader = True
|
|
106
|
+
args.flash = True
|
|
107
|
+
except Exception as e: # noqa: BLE001
|
|
108
|
+
print(f"ERROR: {e!s}")
|
|
109
|
+
sys.exit(1)
|
|
110
|
+
|
|
111
|
+
# --bootloader
|
|
112
|
+
if args.bootloader:
|
|
113
|
+
try:
|
|
114
|
+
print("Enter bootloader...")
|
|
115
|
+
with Sent2UsbDevice(args.port) as device:
|
|
116
|
+
device.enter_bootloader()
|
|
117
|
+
print("Waiting for reset...")
|
|
118
|
+
time.sleep(4.0)
|
|
119
|
+
except Exception as e: # noqa: BLE001
|
|
120
|
+
print(f"ERROR: {e!s}")
|
|
121
|
+
sys.exit(1)
|
|
122
|
+
|
|
123
|
+
# --flash-option-bytes
|
|
124
|
+
if args.flash_option_bytes:
|
|
125
|
+
try:
|
|
126
|
+
with resources.path(*OPTIONBYTES_RESOURCE) as p:
|
|
127
|
+
command = [
|
|
128
|
+
"dfu-util",
|
|
129
|
+
"-a1",
|
|
130
|
+
"-s0x1ffff800",
|
|
131
|
+
f"-D{p}",
|
|
132
|
+
]
|
|
133
|
+
print(f"Run command: {' '.join(command)}")
|
|
134
|
+
subprocess.check_call(command, stdout=sys.stdout, stderr=sys.stderr)
|
|
135
|
+
time.sleep(2.0)
|
|
136
|
+
except Exception as e: # noqa: BLE001
|
|
137
|
+
print(f"ERROR: {e!s}")
|
|
138
|
+
sys.exit(1)
|
|
139
|
+
|
|
140
|
+
# --flash
|
|
141
|
+
if args.flash:
|
|
142
|
+
try:
|
|
143
|
+
with resources.path(*FIRMWARE_RESOURCE) as p:
|
|
144
|
+
command = [
|
|
145
|
+
"dfu-util",
|
|
146
|
+
"-a0",
|
|
147
|
+
"-s0x08000000:leave",
|
|
148
|
+
f"-D{p}",
|
|
149
|
+
]
|
|
150
|
+
print(f"Run command: {' '.join(command)}")
|
|
151
|
+
subprocess.check_call(command, stdout=sys.stdout, stderr=sys.stderr)
|
|
152
|
+
time.sleep(2.0)
|
|
153
|
+
except Exception as e: # noqa: BLE001
|
|
154
|
+
print(f"ERROR: {e!s}")
|
|
155
|
+
sys.exit(1)
|
|
156
|
+
|
|
157
|
+
if args.send or args.monitor:
|
|
158
|
+
with Sent2UsbDevice(args.port) as device:
|
|
159
|
+
print(f"Firmware: {device.get_version()}")
|
|
160
|
+
print(f"Tick Time: {args.tick_time} μs")
|
|
161
|
+
|
|
162
|
+
last_tx_time = None
|
|
163
|
+
decoder = SentDecoder(args.tick_time)
|
|
164
|
+
while True:
|
|
165
|
+
if args.send and (
|
|
166
|
+
last_tx_time is None
|
|
167
|
+
or (
|
|
168
|
+
args.interval is not None
|
|
169
|
+
and (time.time() - last_tx_time > args.interval)
|
|
170
|
+
)
|
|
171
|
+
):
|
|
172
|
+
last_tx_time = time.time()
|
|
173
|
+
nibbles = [int(c, 16) for c in args.send]
|
|
174
|
+
frame = SentFrame(status=nibbles[0], data=nibbles[1:])
|
|
175
|
+
print(f"TX: {frame}")
|
|
176
|
+
device.send_pulses(frame.to_pulses(args.width, args.tick_time))
|
|
177
|
+
|
|
178
|
+
if args.monitor:
|
|
179
|
+
for period, width in device.fetch_pulses():
|
|
180
|
+
frame = decoder.process(period, width)
|
|
181
|
+
if frame is not None:
|
|
182
|
+
print(f"RX: {frame}")
|
|
183
|
+
|
|
184
|
+
if not args.monitor and args.interval is None:
|
|
185
|
+
break
|
|
186
|
+
|
|
187
|
+
time.sleep(0.05)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
if __name__ == "__main__":
|
|
191
|
+
main()
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from .frame import SentFrame
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SentDecoder:
|
|
5
|
+
"""Decodes SENT pulse pairs into frames."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, tick_time_us: int, max_deviation: float = 0.25):
|
|
8
|
+
"""Initialize the decoder.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
tick_time_us: SENT clock tick time [microseconds].
|
|
12
|
+
max_deviation: Allowed calibration pulse deviation [1].
|
|
13
|
+
"""
|
|
14
|
+
self._time_base_ticks = 48 * tick_time_us
|
|
15
|
+
self._min_calib_ticks = 56 * (1 - max_deviation)
|
|
16
|
+
self._max_calib_ticks = 56 * (1 + max_deviation)
|
|
17
|
+
self._calib_factor = None
|
|
18
|
+
self._received_nibbles = []
|
|
19
|
+
|
|
20
|
+
def process(self, period: int, width: int):
|
|
21
|
+
"""Process a received pulse pair.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
period: Pulse period in device ticks.
|
|
25
|
+
width: Pulse low time in device ticks.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
A decoded frame, or `None` until one is complete.
|
|
29
|
+
"""
|
|
30
|
+
period_ticks = period / self._time_base_ticks
|
|
31
|
+
|
|
32
|
+
is_complete = width != 0
|
|
33
|
+
is_calib_pulse = (
|
|
34
|
+
is_complete
|
|
35
|
+
and (period_ticks >= self._min_calib_ticks)
|
|
36
|
+
and (period_ticks <= self._max_calib_ticks)
|
|
37
|
+
)
|
|
38
|
+
is_timeout = period_ticks > self._max_calib_ticks
|
|
39
|
+
|
|
40
|
+
result = None
|
|
41
|
+
if (
|
|
42
|
+
self._calib_factor is not None
|
|
43
|
+
and len(self._received_nibbles) > 2
|
|
44
|
+
and (is_calib_pulse or is_timeout)
|
|
45
|
+
):
|
|
46
|
+
result = SentFrame(
|
|
47
|
+
self._received_nibbles[0],
|
|
48
|
+
self._received_nibbles[1:-1],
|
|
49
|
+
self._received_nibbles[-1],
|
|
50
|
+
)
|
|
51
|
+
self._calib_factor = None
|
|
52
|
+
if is_calib_pulse:
|
|
53
|
+
self._calib_factor = period_ticks / 56
|
|
54
|
+
self._received_nibbles = []
|
|
55
|
+
elif is_complete and self._calib_factor is not None:
|
|
56
|
+
value = round((period_ticks / self._calib_factor) - 12)
|
|
57
|
+
if (value >= 0) and (value <= 15):
|
|
58
|
+
self._received_nibbles.append(value)
|
|
59
|
+
else:
|
|
60
|
+
self._calib_factor = None
|
|
61
|
+
return result
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
from struct import pack, unpack
|
|
2
|
+
|
|
3
|
+
from cobs import cobs
|
|
4
|
+
from serial import Serial
|
|
5
|
+
from serial.tools import list_ports
|
|
6
|
+
|
|
7
|
+
from .device_version import Sent2UsbDeviceVersion
|
|
8
|
+
from .exceptions import Sent2UsbCommunicationError, Sent2UsbDeviceError, Sent2UsbError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Sent2UsbDevice:
|
|
12
|
+
"""Represents a SENT2USB device."""
|
|
13
|
+
|
|
14
|
+
USB_VID = 0x0483
|
|
15
|
+
USB_PID = 0x5740
|
|
16
|
+
|
|
17
|
+
def __init__(self, port=None):
|
|
18
|
+
"""Initialize the device (not connecting yet).
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
port: Serial port. Detected automatically when omitted.
|
|
22
|
+
"""
|
|
23
|
+
self._port = port or Sent2UsbDevice.find_port()
|
|
24
|
+
self._serial = None
|
|
25
|
+
|
|
26
|
+
def __enter__(self):
|
|
27
|
+
"""Connect to and return the device."""
|
|
28
|
+
self.connect()
|
|
29
|
+
return self
|
|
30
|
+
|
|
31
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
32
|
+
"""Disconnect from the device."""
|
|
33
|
+
self.disconnect()
|
|
34
|
+
|
|
35
|
+
def connect(self):
|
|
36
|
+
"""Open the serial connection."""
|
|
37
|
+
self._serial = Serial(port=self._port, timeout=1)
|
|
38
|
+
|
|
39
|
+
def disconnect(self):
|
|
40
|
+
"""Close the serial connection."""
|
|
41
|
+
if self._serial and self._serial.is_open:
|
|
42
|
+
self._serial.close()
|
|
43
|
+
self._serial = None
|
|
44
|
+
|
|
45
|
+
def get_version(self) -> Sent2UsbDeviceVersion:
|
|
46
|
+
"""Read the firmware version.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
The device firmware version.
|
|
50
|
+
"""
|
|
51
|
+
rx_data = self._transceive(0x00, min_len=4, max_len=None)
|
|
52
|
+
return Sent2UsbDeviceVersion.from_raw(rx_data)
|
|
53
|
+
|
|
54
|
+
def send_pulses(self, pattern):
|
|
55
|
+
"""Send SENT pulse pairs.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
pattern: `(period, low_time)` pairs ending with period `0xFFFF`.
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
ValueError: If the pattern is invalid.
|
|
62
|
+
"""
|
|
63
|
+
if len(pattern) < 1:
|
|
64
|
+
raise ValueError("Pattern must contain minimum 1 pulse.")
|
|
65
|
+
if any(x[0] < 2 or x[0] > 0xFFFF for x in pattern):
|
|
66
|
+
raise ValueError("Period must be in range 2..0xFFFF.")
|
|
67
|
+
if any(x[1] < 1 or x[1] >= x[0] for x in pattern):
|
|
68
|
+
raise ValueError("Low-time must be in range 1..period-1.")
|
|
69
|
+
if pattern[-1][0] != 0xFFFF:
|
|
70
|
+
raise ValueError("The last period must be 0xFFFF.")
|
|
71
|
+
data = pack(f">{len(pattern) * 2}H", *[v for pair in pattern for v in pair])
|
|
72
|
+
self._transceive(0x10, data)
|
|
73
|
+
|
|
74
|
+
def fetch_pulses(self):
|
|
75
|
+
"""Fetch received SENT pulse pairs.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
A list of `(period, low_time)` pairs.
|
|
79
|
+
"""
|
|
80
|
+
data = self._transceive(0x20, max_len=None)
|
|
81
|
+
values = unpack(f">{len(data) // 4}I", data)
|
|
82
|
+
return [(values[i * 2], values[i * 2 + 1]) for i in range(len(values) // 2)]
|
|
83
|
+
|
|
84
|
+
def enter_bootloader(self):
|
|
85
|
+
"""Instruct the device to enter its bootloader."""
|
|
86
|
+
self._transceive(0xFF)
|
|
87
|
+
|
|
88
|
+
def _transceive(self, cmd, data=None, min_len=0, max_len=-1):
|
|
89
|
+
if max_len is not None and max_len < 0:
|
|
90
|
+
max_len = min_len
|
|
91
|
+
self._transmit(cmd, data)
|
|
92
|
+
rx_cmd, rx_result, rx_data = self._receive()
|
|
93
|
+
if rx_cmd != cmd:
|
|
94
|
+
raise Sent2UsbCommunicationError(
|
|
95
|
+
f"Received command '0x{rx_cmd:02X}', expected '0x{cmd:02X}'."
|
|
96
|
+
)
|
|
97
|
+
if rx_result != 0x00:
|
|
98
|
+
raise Sent2UsbDeviceError(
|
|
99
|
+
f"Command '0x{cmd:02X}' failed with error '0x{rx_result:02X}'."
|
|
100
|
+
)
|
|
101
|
+
rx_len = len(rx_data)
|
|
102
|
+
if rx_len < min_len or (max_len is not None and rx_len > max_len):
|
|
103
|
+
raise Sent2UsbCommunicationError(
|
|
104
|
+
f"Received {rx_len} bytes, expected {min_len}..{max_len}."
|
|
105
|
+
)
|
|
106
|
+
return rx_data
|
|
107
|
+
|
|
108
|
+
def _transmit(self, cmd, data=None):
|
|
109
|
+
frame = bytearray()
|
|
110
|
+
frame.append(cmd)
|
|
111
|
+
if data is not None:
|
|
112
|
+
frame.extend(data)
|
|
113
|
+
frame.extend(pack(">H", self._calc_crc(frame)))
|
|
114
|
+
self._serial.reset_input_buffer()
|
|
115
|
+
self._serial.write(cobs.encode(frame) + b"\x00")
|
|
116
|
+
|
|
117
|
+
def _receive(self):
|
|
118
|
+
frame = self._serial.read_until(expected=b"\x00")
|
|
119
|
+
if not frame.endswith(b"\x00"):
|
|
120
|
+
raise TimeoutError()
|
|
121
|
+
frame = cobs.decode(frame[:-1])
|
|
122
|
+
if len(frame) < 4:
|
|
123
|
+
raise Sent2UsbCommunicationError(
|
|
124
|
+
f"Received too short response frame: {frame}"
|
|
125
|
+
)
|
|
126
|
+
cmd = frame[0]
|
|
127
|
+
result = frame[1]
|
|
128
|
+
payload = frame[2:-2]
|
|
129
|
+
crc = unpack(">H", frame[-2:])[0]
|
|
130
|
+
if crc != self._calc_crc(frame[:-2]):
|
|
131
|
+
raise Sent2UsbCommunicationError(f"Received invalid CRC: {frame}")
|
|
132
|
+
return cmd, result, payload
|
|
133
|
+
|
|
134
|
+
def _calc_crc(self, data: bytes) -> int:
|
|
135
|
+
crc = 0xFFFF
|
|
136
|
+
for byte in data:
|
|
137
|
+
crc ^= byte << 8
|
|
138
|
+
for _ in range(8):
|
|
139
|
+
if crc & 0x8000:
|
|
140
|
+
crc = ((crc << 1) ^ 0x1021) & 0xFFFF
|
|
141
|
+
else:
|
|
142
|
+
crc = (crc << 1) & 0xFFFF
|
|
143
|
+
return crc
|
|
144
|
+
|
|
145
|
+
@staticmethod
|
|
146
|
+
def list_ports(list_all: bool = False, as_objects: bool = False):
|
|
147
|
+
"""List serial ports.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
list_all: Include ports not matching the SENT2USB identifiers.
|
|
151
|
+
as_objects: Return pyserial port objects instead of device names.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Available serial ports.
|
|
155
|
+
"""
|
|
156
|
+
ports = list_ports.comports()
|
|
157
|
+
if not list_all:
|
|
158
|
+
# Filter by VID/PID
|
|
159
|
+
ports = [
|
|
160
|
+
p
|
|
161
|
+
for p in ports
|
|
162
|
+
if p.vid == Sent2UsbDevice.USB_VID and p.pid == Sent2UsbDevice.USB_PID
|
|
163
|
+
]
|
|
164
|
+
# Try to find only devices which match the manufacturer & description.
|
|
165
|
+
# However, this does not seem to work on Windows so this is optional.
|
|
166
|
+
ports = [
|
|
167
|
+
p
|
|
168
|
+
for p in ports
|
|
169
|
+
if "nuotec" in (p.manufacturer or "").lower()
|
|
170
|
+
and "sent2usb" in (p.description or "").lower()
|
|
171
|
+
] or ports
|
|
172
|
+
return ports if as_objects else [p.device for p in ports]
|
|
173
|
+
|
|
174
|
+
@staticmethod
|
|
175
|
+
def find_port():
|
|
176
|
+
"""Find the only connected SENT2USB device.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
The device serial port.
|
|
180
|
+
|
|
181
|
+
Raises:
|
|
182
|
+
Sent2UsbError: If no device or multiple devices are found.
|
|
183
|
+
"""
|
|
184
|
+
ports = Sent2UsbDevice.list_ports()
|
|
185
|
+
if len(ports) == 0:
|
|
186
|
+
raise Sent2UsbError("No device found, check USB connection.")
|
|
187
|
+
elif len(ports) > 1:
|
|
188
|
+
raise Sent2UsbError(
|
|
189
|
+
f"Multiple devices found, specify serial port manually: {ports}"
|
|
190
|
+
)
|
|
191
|
+
else:
|
|
192
|
+
return ports[0]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
class Sent2UsbDeviceVersion:
|
|
2
|
+
"""Represents a SENT2USB firmware version."""
|
|
3
|
+
|
|
4
|
+
def __init__(
|
|
5
|
+
self, major: int, minor: int, patch: int, candidate: int, build_id: str
|
|
6
|
+
):
|
|
7
|
+
"""Initialize a firmware version.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
major: Major version number.
|
|
11
|
+
minor: Minor version number.
|
|
12
|
+
patch: Patch version number.
|
|
13
|
+
candidate: Release candidate number.
|
|
14
|
+
build_id: Firmware build identifier.
|
|
15
|
+
"""
|
|
16
|
+
self.major = major
|
|
17
|
+
self.minor = minor
|
|
18
|
+
self.patch = patch
|
|
19
|
+
self.candidate = candidate
|
|
20
|
+
self.build_id = build_id
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def from_raw(data: bytes) -> "Sent2UsbDeviceVersion":
|
|
24
|
+
"""Parse a raw version response.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
data: Raw version response bytes.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
The parsed firmware version.
|
|
31
|
+
"""
|
|
32
|
+
return Sent2UsbDeviceVersion(data[0], data[1], data[2], data[3], data[4:])
|
|
33
|
+
|
|
34
|
+
def __str__(self) -> str:
|
|
35
|
+
s = f"{self.major}.{self.minor}.{self.patch}"
|
|
36
|
+
if self.candidate != 0:
|
|
37
|
+
s += f"-rc{self.candidate}"
|
|
38
|
+
if len(self.build_id) > 0:
|
|
39
|
+
s += f" ({self.build_id.hex()})"
|
|
40
|
+
return s
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
class Sent2UsbError(Exception):
|
|
2
|
+
"""Base class for errors raised by this driver."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Sent2UsbCommunicationError(Sent2UsbError):
|
|
6
|
+
"""An invalid or unexpected response received from the device."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Sent2UsbDeviceError(Sent2UsbError):
|
|
10
|
+
"""A non-success result returned from the device."""
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
class SentFrame:
|
|
2
|
+
"""Represents a SENT frame."""
|
|
3
|
+
|
|
4
|
+
def __init__(self, status: int, data: list, crc=None):
|
|
5
|
+
"""Initialize a SENT frame.
|
|
6
|
+
|
|
7
|
+
Args:
|
|
8
|
+
status: Status nibble.
|
|
9
|
+
data: Frame data nibbles.
|
|
10
|
+
crc: CRC nibble. Calculated from data when omitted.
|
|
11
|
+
"""
|
|
12
|
+
self.status = status
|
|
13
|
+
self.data = data
|
|
14
|
+
self.crc = SentFrame.calc_crc(self.data) if crc is None else crc
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def valid(self) -> bool:
|
|
18
|
+
"""Whether the frame CRC is valid."""
|
|
19
|
+
return self.crc == SentFrame.calc_crc(self.data)
|
|
20
|
+
|
|
21
|
+
def to_pulses(self, low_time: int, tick_time_us: int):
|
|
22
|
+
"""Convert the frame to SENT pulse pairs.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
low_time: Pulse low time [clock ticks].
|
|
26
|
+
tick_time_us: Clock tick time [microseconds].
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
A list of `(period, low_time)` pairs.
|
|
30
|
+
"""
|
|
31
|
+
pulses = []
|
|
32
|
+
pulses.append(56) # calibration pulse
|
|
33
|
+
pulses.append(self.status + 12)
|
|
34
|
+
pulses.extend([data + 12 for data in self.data])
|
|
35
|
+
pulses.append(self.crc + 12)
|
|
36
|
+
return [(p * tick_time_us, low_time * tick_time_us) for p in pulses] + [
|
|
37
|
+
(0xFFFF, low_time * tick_time_us)
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
def __str__(self) -> str:
|
|
41
|
+
data = ", ".join([f"0x{d:X}" for d in self.data])
|
|
42
|
+
s = f"status=0x{self.status:X} data=[{data}] crc=0x{self.crc:X}"
|
|
43
|
+
if not self.valid:
|
|
44
|
+
s += " [CRC ERROR]"
|
|
45
|
+
return s
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def calc_crc(data: bytes) -> int:
|
|
49
|
+
"""Calculate the SENT CRC nibble.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
data: Frame data nibbles.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
The CRC nibble.
|
|
56
|
+
"""
|
|
57
|
+
crc = 0x03
|
|
58
|
+
for byte in data:
|
|
59
|
+
crc ^= byte
|
|
60
|
+
for _ in range(4):
|
|
61
|
+
if crc & 0x08:
|
|
62
|
+
crc = ((crc << 1) ^ 0x0D) & 0x0F
|
|
63
|
+
else:
|
|
64
|
+
crc = (crc << 1) & 0x0F
|
|
65
|
+
return crc
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
�U�������������
|