PyVLCB 0.1.1__tar.gz → 0.1.3__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.
- {pyvlcb-0.1.1/src/PyVLCB.egg-info → pyvlcb-0.1.3}/PKG-INFO +42 -20
- pyvlcb-0.1.3/README.md +80 -0
- {pyvlcb-0.1.1 → pyvlcb-0.1.3}/pyproject.toml +1 -1
- {pyvlcb-0.1.1 → pyvlcb-0.1.3/src/PyVLCB.egg-info}/PKG-INFO +42 -20
- {pyvlcb-0.1.1 → pyvlcb-0.1.3}/src/PyVLCB.egg-info/SOURCES.txt +1 -0
- {pyvlcb-0.1.1 → pyvlcb-0.1.3}/src/pyvlcb/__init__.py +79 -169
- {pyvlcb-0.1.1 → pyvlcb-0.1.3}/src/pyvlcb/canusb.py +19 -3
- {pyvlcb-0.1.1 → pyvlcb-0.1.3}/src/pyvlcb/exceptions.py +9 -0
- {pyvlcb-0.1.1 → pyvlcb-0.1.3}/src/pyvlcb/tests/test_canusb4.py +1 -1
- {pyvlcb-0.1.1 → pyvlcb-0.1.3}/src/pyvlcb/tests/test_vlcb.py +10 -8
- pyvlcb-0.1.3/src/pyvlcb/utils.py +180 -0
- {pyvlcb-0.1.1 → pyvlcb-0.1.3}/src/pyvlcb/vlcbformat.py +72 -1
- pyvlcb-0.1.1/README.md +0 -58
- {pyvlcb-0.1.1 → pyvlcb-0.1.3}/LICENSE +0 -0
- {pyvlcb-0.1.1 → pyvlcb-0.1.3}/setup.cfg +0 -0
- {pyvlcb-0.1.1 → pyvlcb-0.1.3}/src/PyVLCB.egg-info/dependency_links.txt +0 -0
- {pyvlcb-0.1.1 → pyvlcb-0.1.3}/src/PyVLCB.egg-info/requires.txt +0 -0
- {pyvlcb-0.1.1 → pyvlcb-0.1.3}/src/PyVLCB.egg-info/top_level.txt +0 -0
- {pyvlcb-0.1.1 → pyvlcb-0.1.3}/src/pyvlcb/tests/run_tests.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyVLCB
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: A library for CBUS / VLCB communication
|
|
5
5
|
Author-email: "Stewart Watkiss (penguintutor)" <5587863+penguintutor@users.noreply.github.com>
|
|
6
6
|
License: MIT License
|
|
@@ -35,30 +35,41 @@ License-File: LICENSE
|
|
|
35
35
|
Requires-Dist: pyserial>=3.4
|
|
36
36
|
Dynamic: license-file
|
|
37
37
|
|
|
38
|
-
# PyVLCB Python Software Library for
|
|
38
|
+
# PyVLCB Python Software Library for VLCB
|
|
39
39
|
|
|
40
|
-
This is a
|
|
40
|
+
This is a Python library for VLCB (Versatile Layout Control Bus). It implements the core
|
|
41
|
+
communication protocols defined by the CBUS® protocol and includes extensions for the newer
|
|
42
|
+
VLCB specifications.
|
|
41
43
|
|
|
42
|
-
It
|
|
44
|
+
It is designed for use with the MERG CANUSB4 CBUS adapter and other compatible hardware.
|
|
43
45
|
|
|
46
|
+
---
|
|
44
47
|
|
|
45
48
|
## Install
|
|
46
49
|
|
|
47
50
|
|
|
48
51
|
For most systems including a Raspberry Pi then Python is managed using virtual environment.
|
|
49
|
-
To setup a virtual environment enter the following commands
|
|
50
52
|
|
|
51
|
-
|
|
52
|
-
python3 -m venv ~/venv/pyvlcb --system-site-packages
|
|
53
|
+
**Setup a virtual environment:**
|
|
53
54
|
|
|
54
|
-
|
|
55
|
+
```bash
|
|
56
|
+
mkdir ~/venv
|
|
57
|
+
python3 -m venv ~/venv/pyvlcb --system-site-packages
|
|
58
|
+
```
|
|
55
59
|
|
|
56
|
-
|
|
60
|
+
**Activate the virtual environment:**
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
source ~/venv/pyvlcb/bin/activate
|
|
64
|
+
```
|
|
57
65
|
|
|
58
|
-
|
|
66
|
+
**Install the latest release:**
|
|
59
67
|
|
|
60
|
-
|
|
68
|
+
```bash
|
|
69
|
+
pip install pyvlcb
|
|
70
|
+
```
|
|
61
71
|
|
|
72
|
+
---
|
|
62
73
|
|
|
63
74
|
## Development
|
|
64
75
|
|
|
@@ -69,12 +80,14 @@ The latest source code is available from GitHub. [PyVLCB on GitHub](https://gith
|
|
|
69
80
|
|
|
70
81
|
To install the library, as a submodule in your own git project repository, then enter your project directory and run:
|
|
71
82
|
|
|
72
|
-
|
|
73
|
-
|
|
83
|
+
```bash
|
|
84
|
+
git submodule add https://github.com/penguintutor/pyvlcb.git lib/pyvlcb
|
|
85
|
+
pip install lib/pyvlcb
|
|
86
|
+
```
|
|
74
87
|
|
|
75
|
-
|
|
88
|
+
*Note: To use as a submodule, you must create your own git project first and install within the project folder. To update, run `git pull` and repeat the pip install command.*
|
|
76
89
|
|
|
77
|
-
|
|
90
|
+
---
|
|
78
91
|
|
|
79
92
|
## Library Reference
|
|
80
93
|
|
|
@@ -84,12 +97,21 @@ See the link below for the library reference documentation
|
|
|
84
97
|
|
|
85
98
|
## Demo examples
|
|
86
99
|
|
|
87
|
-
Example code is stored within the demo folder available
|
|
88
|
-
|
|
89
|
-
|
|
100
|
+
Example code is stored within the demo folder available on GitHub. Copy these into your project folder to test the library and connectivity.
|
|
101
|
+
|
|
102
|
+
* The demos are created for a Raspberry Pi or other Linux computers.
|
|
103
|
+
* The USB port is hard-coded as /dev/ttyACM0.
|
|
104
|
+
* For other computers or USB ports, edit the Python file and update the port statement.Example code is stored within the demo folder available from GitHub. Copy these into your project folder to test the library and connectivity.
|
|
90
105
|
|
|
91
106
|
## More Details
|
|
92
107
|
|
|
93
|
-
|
|
108
|
+
* Project page: [PenguinTutor PyVLCB library page](https://www.penguintutor.com/projects/pyvlcb)
|
|
109
|
+
* Application example: [PenguinTutor Pi SignalBox project page](https://www.penguintutor.com/projects/pisignalbox)
|
|
110
|
+
|
|
111
|
+
## Legal and Trademarks
|
|
112
|
+
|
|
113
|
+
* **CBUS®** is a registered trademark of Dr. Michael Bolton.
|
|
114
|
+
* This library is an independent implementation based on publicly available protocol documentation and is not officially affiliated with or endorsed by the trademark holder.
|
|
115
|
+
* All other trademarks are the property of their respective owners.
|
|
116
|
+
|
|
94
117
|
|
|
95
|
-
For an example of a program using this library see the [PenguinTutor Pi SignalBox project page](https://www.penguintutor.com/projects/pisignalbox)
|
pyvlcb-0.1.3/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# PyVLCB Python Software Library for VLCB
|
|
2
|
+
|
|
3
|
+
This is a Python library for VLCB (Versatile Layout Control Bus). It implements the core
|
|
4
|
+
communication protocols defined by the CBUS® protocol and includes extensions for the newer
|
|
5
|
+
VLCB specifications.
|
|
6
|
+
|
|
7
|
+
It is designed for use with the MERG CANUSB4 CBUS adapter and other compatible hardware.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
For most systems including a Raspberry Pi then Python is managed using virtual environment.
|
|
15
|
+
|
|
16
|
+
**Setup a virtual environment:**
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
mkdir ~/venv
|
|
20
|
+
python3 -m venv ~/venv/pyvlcb --system-site-packages
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**Activate the virtual environment:**
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
source ~/venv/pyvlcb/bin/activate
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Install the latest release:**
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install pyvlcb
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Development
|
|
38
|
+
|
|
39
|
+
The latest source code is available from GitHub. [PyVLCB on GitHub](https://github.com/penguintutor/pyvlcb).
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
### Installing as a git submodule
|
|
43
|
+
|
|
44
|
+
To install the library, as a submodule in your own git project repository, then enter your project directory and run:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
git submodule add https://github.com/penguintutor/pyvlcb.git lib/pyvlcb
|
|
48
|
+
pip install lib/pyvlcb
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
*Note: To use as a submodule, you must create your own git project first and install within the project folder. To update, run `git pull` and repeat the pip install command.*
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Library Reference
|
|
56
|
+
|
|
57
|
+
See the link below for the library reference documentation
|
|
58
|
+
[PyVLCB Library Reference Documentation](https://penguintutor.github.io/pyvlcb/reference/)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
## Demo examples
|
|
62
|
+
|
|
63
|
+
Example code is stored within the demo folder available on GitHub. Copy these into your project folder to test the library and connectivity.
|
|
64
|
+
|
|
65
|
+
* The demos are created for a Raspberry Pi or other Linux computers.
|
|
66
|
+
* The USB port is hard-coded as /dev/ttyACM0.
|
|
67
|
+
* For other computers or USB ports, edit the Python file and update the port statement.Example code is stored within the demo folder available from GitHub. Copy these into your project folder to test the library and connectivity.
|
|
68
|
+
|
|
69
|
+
## More Details
|
|
70
|
+
|
|
71
|
+
* Project page: [PenguinTutor PyVLCB library page](https://www.penguintutor.com/projects/pyvlcb)
|
|
72
|
+
* Application example: [PenguinTutor Pi SignalBox project page](https://www.penguintutor.com/projects/pisignalbox)
|
|
73
|
+
|
|
74
|
+
## Legal and Trademarks
|
|
75
|
+
|
|
76
|
+
* **CBUS®** is a registered trademark of Dr. Michael Bolton.
|
|
77
|
+
* This library is an independent implementation based on publicly available protocol documentation and is not officially affiliated with or endorsed by the trademark holder.
|
|
78
|
+
* All other trademarks are the property of their respective owners.
|
|
79
|
+
|
|
80
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyVLCB
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: A library for CBUS / VLCB communication
|
|
5
5
|
Author-email: "Stewart Watkiss (penguintutor)" <5587863+penguintutor@users.noreply.github.com>
|
|
6
6
|
License: MIT License
|
|
@@ -35,30 +35,41 @@ License-File: LICENSE
|
|
|
35
35
|
Requires-Dist: pyserial>=3.4
|
|
36
36
|
Dynamic: license-file
|
|
37
37
|
|
|
38
|
-
# PyVLCB Python Software Library for
|
|
38
|
+
# PyVLCB Python Software Library for VLCB
|
|
39
39
|
|
|
40
|
-
This is a
|
|
40
|
+
This is a Python library for VLCB (Versatile Layout Control Bus). It implements the core
|
|
41
|
+
communication protocols defined by the CBUS® protocol and includes extensions for the newer
|
|
42
|
+
VLCB specifications.
|
|
41
43
|
|
|
42
|
-
It
|
|
44
|
+
It is designed for use with the MERG CANUSB4 CBUS adapter and other compatible hardware.
|
|
43
45
|
|
|
46
|
+
---
|
|
44
47
|
|
|
45
48
|
## Install
|
|
46
49
|
|
|
47
50
|
|
|
48
51
|
For most systems including a Raspberry Pi then Python is managed using virtual environment.
|
|
49
|
-
To setup a virtual environment enter the following commands
|
|
50
52
|
|
|
51
|
-
|
|
52
|
-
python3 -m venv ~/venv/pyvlcb --system-site-packages
|
|
53
|
+
**Setup a virtual environment:**
|
|
53
54
|
|
|
54
|
-
|
|
55
|
+
```bash
|
|
56
|
+
mkdir ~/venv
|
|
57
|
+
python3 -m venv ~/venv/pyvlcb --system-site-packages
|
|
58
|
+
```
|
|
55
59
|
|
|
56
|
-
|
|
60
|
+
**Activate the virtual environment:**
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
source ~/venv/pyvlcb/bin/activate
|
|
64
|
+
```
|
|
57
65
|
|
|
58
|
-
|
|
66
|
+
**Install the latest release:**
|
|
59
67
|
|
|
60
|
-
|
|
68
|
+
```bash
|
|
69
|
+
pip install pyvlcb
|
|
70
|
+
```
|
|
61
71
|
|
|
72
|
+
---
|
|
62
73
|
|
|
63
74
|
## Development
|
|
64
75
|
|
|
@@ -69,12 +80,14 @@ The latest source code is available from GitHub. [PyVLCB on GitHub](https://gith
|
|
|
69
80
|
|
|
70
81
|
To install the library, as a submodule in your own git project repository, then enter your project directory and run:
|
|
71
82
|
|
|
72
|
-
|
|
73
|
-
|
|
83
|
+
```bash
|
|
84
|
+
git submodule add https://github.com/penguintutor/pyvlcb.git lib/pyvlcb
|
|
85
|
+
pip install lib/pyvlcb
|
|
86
|
+
```
|
|
74
87
|
|
|
75
|
-
|
|
88
|
+
*Note: To use as a submodule, you must create your own git project first and install within the project folder. To update, run `git pull` and repeat the pip install command.*
|
|
76
89
|
|
|
77
|
-
|
|
90
|
+
---
|
|
78
91
|
|
|
79
92
|
## Library Reference
|
|
80
93
|
|
|
@@ -84,12 +97,21 @@ See the link below for the library reference documentation
|
|
|
84
97
|
|
|
85
98
|
## Demo examples
|
|
86
99
|
|
|
87
|
-
Example code is stored within the demo folder available
|
|
88
|
-
|
|
89
|
-
|
|
100
|
+
Example code is stored within the demo folder available on GitHub. Copy these into your project folder to test the library and connectivity.
|
|
101
|
+
|
|
102
|
+
* The demos are created for a Raspberry Pi or other Linux computers.
|
|
103
|
+
* The USB port is hard-coded as /dev/ttyACM0.
|
|
104
|
+
* For other computers or USB ports, edit the Python file and update the port statement.Example code is stored within the demo folder available from GitHub. Copy these into your project folder to test the library and connectivity.
|
|
90
105
|
|
|
91
106
|
## More Details
|
|
92
107
|
|
|
93
|
-
|
|
108
|
+
* Project page: [PenguinTutor PyVLCB library page](https://www.penguintutor.com/projects/pyvlcb)
|
|
109
|
+
* Application example: [PenguinTutor Pi SignalBox project page](https://www.penguintutor.com/projects/pisignalbox)
|
|
110
|
+
|
|
111
|
+
## Legal and Trademarks
|
|
112
|
+
|
|
113
|
+
* **CBUS®** is a registered trademark of Dr. Michael Bolton.
|
|
114
|
+
* This library is an independent implementation based on publicly available protocol documentation and is not officially affiliated with or endorsed by the trademark holder.
|
|
115
|
+
* All other trademarks are the property of their respective owners.
|
|
116
|
+
|
|
94
117
|
|
|
95
|
-
For an example of a program using this library see the [PenguinTutor Pi SignalBox project page](https://www.penguintutor.com/projects/pisignalbox)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
from .vlcbformat import VLCBFormat, VLCBOpcode
|
|
5
5
|
from .canusb import CanUSB4
|
|
6
|
+
from .utils import num_to_1hexstr, num_to_2hexstr, num_to_4hexstr, f_to_bytes, dict_to_string
|
|
6
7
|
from .exceptions import (
|
|
7
8
|
MyLibraryError,
|
|
8
9
|
DeviceConnectionError,
|
|
@@ -122,162 +123,17 @@ class VLCB:
|
|
|
122
123
|
opcode = vlcb_entry.data[0:2]
|
|
123
124
|
opcode_string = f'{opcode} - {VLCBOpcode.opcode_mnemonic(opcode)}'
|
|
124
125
|
#data_string = f"{VLCBOpcode.parse_data(vlcb_entry.data)}"
|
|
125
|
-
data_string =
|
|
126
|
+
data_string = dict_to_string(VLCBOpcode.parse_data(vlcb_entry.data))
|
|
126
127
|
return [date_string, direction, message, str(vlcb_entry.can_id), opcode_string, data_string]
|
|
127
128
|
# Todo - error handling
|
|
128
129
|
|
|
129
|
-
# dict to string without {} or ""
|
|
130
|
-
@staticmethod
|
|
131
|
-
def _dict_to_string (dictionary: dict) -> str:
|
|
132
|
-
""" Convert a dict to a string without {} or quotes """
|
|
133
|
-
data_string = ""
|
|
134
|
-
for key, value in dictionary.items():
|
|
135
|
-
if data_string != "":
|
|
136
|
-
data_string += " , "
|
|
137
|
-
data_string += f"{key} = {value}"
|
|
138
|
-
return data_string
|
|
139
|
-
|
|
140
|
-
@staticmethod
|
|
141
|
-
def f_to_bytes (f_num: int, function_status: List[int]) -> Tuple[bytes, bytes]:
|
|
142
|
-
""" Convert a Fnumber (Loco function ID) to bytes
|
|
143
|
-
|
|
144
|
-
Can be used to create correct format for loco_set_dfun
|
|
145
|
-
Limited to range 0 to 28 - which is max for DFUN
|
|
146
|
-
Higher would need to use DFNON/DFOFF
|
|
147
|
-
|
|
148
|
-
Args:
|
|
149
|
-
f_num: Function number
|
|
150
|
-
function_status: List with value of all functions (must be updated before calling)
|
|
151
|
-
|
|
152
|
-
Returns:
|
|
153
|
-
Tuple of two bytes
|
|
154
|
-
|
|
155
|
-
Raises: ValueError
|
|
156
|
-
|
|
157
|
-
"""
|
|
158
|
-
# Must be an F number between 0 and 28
|
|
159
|
-
if f_num < 0 or f_num > 28:
|
|
160
|
-
raise ValueError (f"Fnumber needs to be between 0 and 28. Number provided {f_num}")
|
|
161
|
-
|
|
162
|
-
# Extend function_status to 29 entries
|
|
163
|
-
function_list = function_status + [0] * (29 - len(function_status))
|
|
164
|
-
|
|
165
|
-
if f_num <= 4:
|
|
166
|
-
byte1 = 1
|
|
167
|
-
byte2 = (0b10000 * function_list[0] + # fn0 is higher nibble (bit 5)
|
|
168
|
-
0b0001 * function_list[1] +
|
|
169
|
-
0b0010 * function_list[2] +
|
|
170
|
-
0b0100 * function_list[3] +
|
|
171
|
-
0b1000 * function_list[4]
|
|
172
|
-
)
|
|
173
|
-
elif f_num <= 8:
|
|
174
|
-
byte1 = 2
|
|
175
|
-
byte2 = (0b0001 * function_list[5] +
|
|
176
|
-
0b0010 * function_list[6] +
|
|
177
|
-
0b0100 * function_list[7] +
|
|
178
|
-
0b1000 * function_list[8]
|
|
179
|
-
)
|
|
180
|
-
elif f_num <= 12:
|
|
181
|
-
byte1 = 3
|
|
182
|
-
byte2 = (0b0001 * function_list[9] +
|
|
183
|
-
0b0010 * function_list[10] +
|
|
184
|
-
0b0100 * function_list[11] +
|
|
185
|
-
0b1000 * function_list[12]
|
|
186
|
-
)
|
|
187
|
-
elif f_num <= 20:
|
|
188
|
-
byte1 = 4
|
|
189
|
-
byte2 = (0b0001 * function_list[13] +
|
|
190
|
-
0b0010 * function_list[14] +
|
|
191
|
-
0b0100 * function_list[15] +
|
|
192
|
-
0b1000 * function_list[16] +
|
|
193
|
-
0b10000 * function_list[17] +
|
|
194
|
-
0b100000 * function_list[18] +
|
|
195
|
-
0b1000000 * function_list[19] +
|
|
196
|
-
0b10000000 * function_list[20]
|
|
197
|
-
)
|
|
198
|
-
elif f_num <= 28:
|
|
199
|
-
byte1 = 5
|
|
200
|
-
byte2 = (0b0001 * function_list[21] +
|
|
201
|
-
0b0010 * function_list[22] +
|
|
202
|
-
0b0100 * function_list[23] +
|
|
203
|
-
0b1000 * function_list[24] +
|
|
204
|
-
0b10000 * function_list[25] +
|
|
205
|
-
0b100000 * function_list[26] +
|
|
206
|
-
0b1000000 * function_list[27] +
|
|
207
|
-
0b10000000 * function_list[28]
|
|
208
|
-
)
|
|
209
|
-
return (byte1, byte2)
|
|
210
130
|
|
|
131
|
+
# Static Methods moved to utils
|
|
132
|
+
#num_to_1hexstr = staticmethod(num_to_1hexstr)
|
|
133
|
+
#num_to_2hexstr = staticmethod(num_to_2hexstr)
|
|
134
|
+
#num_to_4hexstr = staticmethod(num_to_4hexstr)
|
|
211
135
|
|
|
212
|
-
@staticmethod
|
|
213
|
-
# Where 1 x bytes (2 chars)
|
|
214
|
-
def num_to_1hexstr (num: int) -> str:
|
|
215
|
-
"""Convert number to a byte
|
|
216
|
-
|
|
217
|
-
Args:
|
|
218
|
-
num (int): Number to convert
|
|
219
|
-
|
|
220
|
-
Returns:
|
|
221
|
-
String: A hex representation of the number (2 chars)
|
|
222
|
-
"""
|
|
223
|
-
return f"{hex(num).upper()[2:]:0>2}"
|
|
224
|
-
|
|
225
|
-
@staticmethod
|
|
226
|
-
# Where 2 x bytes (4 chars)
|
|
227
|
-
def num_to_2hexstr (num: int) -> str:
|
|
228
|
-
"""Convert number to 2 bytes
|
|
229
|
-
|
|
230
|
-
Args:
|
|
231
|
-
num (int): Number to convert
|
|
232
|
-
|
|
233
|
-
Returns:
|
|
234
|
-
String: A hex representation of the number (4 chars)
|
|
235
|
-
"""
|
|
236
|
-
return f"{hex(num).upper()[2:]:0>4}"
|
|
237
|
-
|
|
238
|
-
@staticmethod
|
|
239
|
-
# Where 4 x bytes (8 chars)
|
|
240
|
-
def num_to_4hexstr (num: int) -> str:
|
|
241
|
-
"""Convert number to 4 bytes
|
|
242
|
-
|
|
243
|
-
Args:
|
|
244
|
-
num (int): Number to convert
|
|
245
|
-
|
|
246
|
-
Returns:
|
|
247
|
-
String: A hex representation of the number (8 chars)
|
|
248
|
-
"""
|
|
249
|
-
return f"{hex(num).upper()[2:]:0>8}"
|
|
250
|
-
|
|
251
|
-
@staticmethod
|
|
252
|
-
# Where 2 bytes convert to addr id
|
|
253
|
-
def bytes_to_addr (byte1: bytes, byte2: bytes) -> int:
|
|
254
|
-
"""Convert 2 byte values into an address id sring
|
|
255
|
-
|
|
256
|
-
Args:
|
|
257
|
-
byte1 (bytes): Most significant byte
|
|
258
|
-
byte2 (bytes): Least significant byte
|
|
259
|
-
|
|
260
|
-
Returns:
|
|
261
|
-
Int: The address id value
|
|
262
|
-
"""
|
|
263
|
-
msb = int(byte1)
|
|
264
|
-
lsb = int(byte2)
|
|
265
|
-
return ((msb << 8) + lsb)
|
|
266
|
-
|
|
267
|
-
@staticmethod
|
|
268
|
-
def bytes_to_hexstr (byte1: bytes, byte2: bytes) -> str:
|
|
269
|
-
"""Convert 2 bytes to a hex string
|
|
270
|
-
|
|
271
|
-
Args:
|
|
272
|
-
byte1 (bytes): Most significant byte
|
|
273
|
-
byte2 (bytes): Least significant byte
|
|
274
136
|
|
|
275
|
-
Returns:
|
|
276
|
-
String: A hex representation of the number
|
|
277
|
-
"""
|
|
278
|
-
return f"{hex(byte1).upper()[2:]:0>2}{hex(byte2).upper()[2:]:0>2}"
|
|
279
|
-
|
|
280
|
-
|
|
281
137
|
# Create header using low priority and can_id (or self.can_id)
|
|
282
138
|
# If opcode provided, but no priority then appropriate min code looked up
|
|
283
139
|
# MajPri would be based on packet aging - needs to be managed outside of this
|
|
@@ -312,6 +168,7 @@ class VLCB:
|
|
|
312
168
|
return header_string
|
|
313
169
|
#return header_string.encode('utf-8')
|
|
314
170
|
|
|
171
|
+
|
|
315
172
|
# Discover nodes
|
|
316
173
|
def discover (self) -> str:
|
|
317
174
|
"""Create a discovery string
|
|
@@ -336,7 +193,7 @@ class VLCB:
|
|
|
336
193
|
Returns:
|
|
337
194
|
String: A string for the request
|
|
338
195
|
"""
|
|
339
|
-
return f"{self.make_header(opcode='58')}58{
|
|
196
|
+
return f"{self.make_header(opcode='58')}58{num_to_2hexstr(node_id)};"
|
|
340
197
|
|
|
341
198
|
# Discover number of events available
|
|
342
199
|
def discover_nevn (self, node_id: int) -> str:
|
|
@@ -350,7 +207,7 @@ class VLCB:
|
|
|
350
207
|
Returns:
|
|
351
208
|
String: A string for the request
|
|
352
209
|
"""
|
|
353
|
-
return f"{self.make_header(opcode='56')}56{
|
|
210
|
+
return f"{self.make_header(opcode='56')}56{num_to_2hexstr(node_id)};"
|
|
354
211
|
|
|
355
212
|
# Discover stored events NERD
|
|
356
213
|
def discover_nerd (self, node_id: int) -> str:
|
|
@@ -364,7 +221,7 @@ class VLCB:
|
|
|
364
221
|
Returns:
|
|
365
222
|
String: A string for the request
|
|
366
223
|
"""
|
|
367
|
-
return f"{self.make_header(opcode='57')}57{
|
|
224
|
+
return f"{self.make_header(opcode='57')}57{num_to_2hexstr(node_id)};"
|
|
368
225
|
|
|
369
226
|
# Emergency stop all locos
|
|
370
227
|
# RESTP
|
|
@@ -419,10 +276,10 @@ class VLCB:
|
|
|
419
276
|
# Turn on
|
|
420
277
|
if state == True or state == "on":
|
|
421
278
|
# ASON
|
|
422
|
-
return f"{self.make_header(opcode='98')}98{
|
|
279
|
+
return f"{self.make_header(opcode='98')}98{num_to_2hexstr(node_id)}{num_to_2hexstr(ev_id)};"
|
|
423
280
|
# Turn off = ASOFF
|
|
424
281
|
else:
|
|
425
|
-
return f"{self.make_header(opcode='99')}99{
|
|
282
|
+
return f"{self.make_header(opcode='99')}99{num_to_2hexstr(node_id)}{num_to_2hexstr(ev_id)};"
|
|
426
283
|
|
|
427
284
|
def accessory_long_command (self, node_id: int, ev_id: int, state: Union[str, bool]) -> str:
|
|
428
285
|
"""Create an accessory long command
|
|
@@ -440,10 +297,10 @@ class VLCB:
|
|
|
440
297
|
# Turn on
|
|
441
298
|
if state == True or state == "on":
|
|
442
299
|
# ASON
|
|
443
|
-
return f"{self.make_header(opcode='90')}90{
|
|
300
|
+
return f"{self.make_header(opcode='90')}90{num_to_4hexstr(ev_id)};"
|
|
444
301
|
# Turn off = ASOFF
|
|
445
302
|
else:
|
|
446
|
-
return f"{self.make_header(opcode='91')}91{
|
|
303
|
+
return f"{self.make_header(opcode='91')}91{num_to_4hexstr(ev_id)};"
|
|
447
304
|
|
|
448
305
|
# RLOC (Allocate loco) :SB040N40D446;
|
|
449
306
|
# Short address upper address all zeros, only 6 bits of the lower byte are used (1 to 127) 0 is decoderless
|
|
@@ -468,7 +325,7 @@ class VLCB:
|
|
|
468
325
|
raise ValueError ("Invalid short code. Loco ID {loco_id} is larger than 127")
|
|
469
326
|
if long == True:
|
|
470
327
|
loco_id = loco_id | 0xC000
|
|
471
|
-
return f"{self.make_header(opcode='40')}40{
|
|
328
|
+
return f"{self.make_header(opcode='40')}40{num_to_2hexstr(loco_id)};"
|
|
472
329
|
|
|
473
330
|
def release_loco (self, session_id: int) -> str:
|
|
474
331
|
"""Create a release loco request
|
|
@@ -481,7 +338,7 @@ class VLCB:
|
|
|
481
338
|
Returns:
|
|
482
339
|
String: A string for the request
|
|
483
340
|
"""
|
|
484
|
-
return f"{self.make_header(opcode='21')}21{
|
|
341
|
+
return f"{self.make_header(opcode='21')}21{num_to_1hexstr(session_id)};"
|
|
485
342
|
|
|
486
343
|
def steal_loco (self, loco_id: int, long: Optional[bool] = True) -> str:
|
|
487
344
|
"""Create an steal loco request
|
|
@@ -504,7 +361,7 @@ class VLCB:
|
|
|
504
361
|
raise InvalidLocoError(f"Invalid short code {loco_id}")
|
|
505
362
|
if long == True:
|
|
506
363
|
loco_id = loco_id | 0xC000
|
|
507
|
-
return f"{self.make_header(opcode='61')}61{
|
|
364
|
+
return f"{self.make_header(opcode='61')}61{num_to_2hexstr(loco_id)}01;"
|
|
508
365
|
|
|
509
366
|
def share_loco (self, loco_id: int, long: Optional[bool] = True) -> str:
|
|
510
367
|
"""Create an share loco request
|
|
@@ -527,7 +384,7 @@ class VLCB:
|
|
|
527
384
|
raise InvalidLocoError(f"Invalid short code {loco_id}")
|
|
528
385
|
if long == True:
|
|
529
386
|
loco_id = loco_id | 0xC000
|
|
530
|
-
return f"{self.make_header(opcode='61')}61{
|
|
387
|
+
return f"{self.make_header(opcode='61')}61{num_to_2hexstr(loco_id)}02;"
|
|
531
388
|
|
|
532
389
|
def keep_alive (self, session_id: int) -> str:
|
|
533
390
|
"""Create an keep alive request
|
|
@@ -541,14 +398,44 @@ class VLCB:
|
|
|
541
398
|
Returns:
|
|
542
399
|
String: A string for the request
|
|
543
400
|
"""
|
|
544
|
-
return f"{self.make_header(opcode='23')}23{
|
|
401
|
+
return f"{self.make_header(opcode='23')}23{num_to_1hexstr(session_id)};"
|
|
402
|
+
|
|
403
|
+
def loco_speed_dir (self, session_id: int, speed: int, direction: int) -> str:
|
|
404
|
+
"""Set loco speed and direction based on separate arguments
|
|
405
|
+
|
|
406
|
+
Same as loco_speeddir but this takes 2 arguments, whereas loco_speeddir needs a combined value
|
|
407
|
+
Maximum call this once every 32 milliseconds
|
|
408
|
+
|
|
409
|
+
Uses DSPD (47)
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
session_id: Session ID
|
|
413
|
+
speed: 0 to 127 (1 is increased to 2 to avoid emergency stop)
|
|
414
|
+
direction: 1 = forward, 0 = reverse
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
String: A string for the request
|
|
418
|
+
|
|
419
|
+
Raises:
|
|
420
|
+
ValueError is speed is out of range, or invalid direction
|
|
421
|
+
"""
|
|
422
|
+
if speed < 0 or speed > 127:
|
|
423
|
+
raise ValueError ("Invalid speed specified. Must be in range 0 to 127")
|
|
424
|
+
if direction <0 or direction > 1:
|
|
425
|
+
raise ValueError ("Direction is not valid. Use 1 for forward, 0 for reverse")
|
|
426
|
+
# special case - ignore emergency stop
|
|
427
|
+
if speed == 1:
|
|
428
|
+
speed = 2
|
|
429
|
+
speeddir = (direction * 0x80) + speed
|
|
430
|
+
return self.loco_speeddir (session_id, speeddir)
|
|
545
431
|
|
|
546
432
|
# Set loco speed and direction (always done together)
|
|
547
|
-
# Maximum once every 32 miliseconds (GUI configured based on non triggered so shouldn't be an issue)
|
|
548
|
-
# Could add time detection if required
|
|
549
|
-
# This uses the combined speed and direction value
|
|
550
433
|
def loco_speeddir (self, session_id: int, speeddir: int) -> str:
|
|
551
|
-
"""
|
|
434
|
+
"""Set loco speed and direction
|
|
435
|
+
|
|
436
|
+
Maximum call this once every 32 milliseconds
|
|
437
|
+
Needs combined speed and direction value.
|
|
438
|
+
If speed is set to 1 then that is considered an emergency stop
|
|
552
439
|
|
|
553
440
|
Uses DSPD (47)
|
|
554
441
|
|
|
@@ -559,7 +446,7 @@ class VLCB:
|
|
|
559
446
|
Returns:
|
|
560
447
|
String: A string for the request
|
|
561
448
|
"""
|
|
562
|
-
return f"{self.make_header(opcode='47')}47{
|
|
449
|
+
return f"{self.make_header(opcode='47')}47{num_to_1hexstr(session_id)}{num_to_1hexstr(speeddir)};"
|
|
563
450
|
|
|
564
451
|
# Set function using DFUN - needs to be provided with the two bytes
|
|
565
452
|
# First byte is group (1 = F1 to F4, 2 = F5 to F8, 3 = F9 to F12)
|
|
@@ -579,6 +466,29 @@ class VLCB:
|
|
|
579
466
|
Returns:
|
|
580
467
|
String: A string for the request
|
|
581
468
|
"""
|
|
582
|
-
return f"{self.make_header(opcode='60')}60{
|
|
583
|
-
|
|
469
|
+
return f"{self.make_header(opcode='60')}60{num_to_1hexstr(session_id)}{num_to_1hexstr(byte1)}{num_to_1hexstr(byte2)};"
|
|
584
470
|
|
|
471
|
+
def loco_set_function (self, session_id: int, function_num, function_list) -> str:
|
|
472
|
+
"""Create a set function request using the function list
|
|
473
|
+
Sends the entire group of functions where the function_num resides
|
|
474
|
+
This is an alternative to loco_set_dfun as this calculates the bytes
|
|
475
|
+
this method can only be used for functions 0 to 27
|
|
476
|
+
|
|
477
|
+
Uses DFUN (60)
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
session_id: Session ID
|
|
481
|
+
byte1: Function number
|
|
482
|
+
byte2: List of current function statuses
|
|
483
|
+
|
|
484
|
+
Returns:
|
|
485
|
+
String: A string for the request
|
|
486
|
+
|
|
487
|
+
Raises:
|
|
488
|
+
ValueError: Typically raised from f_to_bytes
|
|
489
|
+
"""
|
|
490
|
+
byte1_2 = f_to_bytes(function_num, function_list)
|
|
491
|
+
return f"{self.make_header(opcode='60')}60{num_to_1hexstr(session_id)}{byte1_2[0]}{byte1_2[1]};"
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
|
|
@@ -22,7 +22,8 @@ class CanUSB4 ():
|
|
|
22
22
|
def __init__ (self,
|
|
23
23
|
port: str,
|
|
24
24
|
baud: Optional[int] = 115200,
|
|
25
|
-
timeout: Optional[float] = 0.01
|
|
25
|
+
timeout: Optional[float] = 0.01,
|
|
26
|
+
exclusive: Optional[bool] = True) -> None:
|
|
26
27
|
"""Inits CanUSB4 with a USB port
|
|
27
28
|
|
|
28
29
|
Args:
|
|
@@ -41,6 +42,10 @@ class CanUSB4 ():
|
|
|
41
42
|
self.timeout = timeout
|
|
42
43
|
self.max_retry = 30 # How many times to attempt on get_data must be at least as long as frame
|
|
43
44
|
# Timeout for a request could be max_rety x timeout
|
|
45
|
+
self.exclusive = exclusive # Exclusive determines if check for exclusive use of the USB port
|
|
46
|
+
# If this is set to false then if another application is already using the port then the application
|
|
47
|
+
# will run, but appear to hang if there is no response because another thread has already
|
|
48
|
+
# taken the incoming data
|
|
44
49
|
|
|
45
50
|
if not port:
|
|
46
51
|
raise InvalidConfigurationError("Port name cannot be empty")
|
|
@@ -53,7 +58,11 @@ class CanUSB4 ():
|
|
|
53
58
|
|
|
54
59
|
|
|
55
60
|
# Optional arguments override existing
|
|
56
|
-
def connect(self,
|
|
61
|
+
def connect(self,
|
|
62
|
+
port: Optional[str] = None,
|
|
63
|
+
baud: Optional[int] = None,
|
|
64
|
+
timeout: Optional[float] = None,
|
|
65
|
+
exclusive: Optional[bool] = None) -> None:
|
|
57
66
|
"""Inits CanUSB4 with a USB port
|
|
58
67
|
|
|
59
68
|
Args:
|
|
@@ -74,8 +83,15 @@ class CanUSB4 ():
|
|
|
74
83
|
self.baud = baud
|
|
75
84
|
if timeout != None:
|
|
76
85
|
self.timeout = timeout
|
|
86
|
+
if exclusive != None:
|
|
87
|
+
self.exclusive = exclusive
|
|
77
88
|
try:
|
|
78
|
-
self.ser = serial.Serial(
|
|
89
|
+
self.ser = serial.Serial(
|
|
90
|
+
self.port,
|
|
91
|
+
self.baud,
|
|
92
|
+
timeout=self.timeout,
|
|
93
|
+
exclusive = exclusive
|
|
94
|
+
)
|
|
79
95
|
except serial.SerialException as e:
|
|
80
96
|
raise DeviceConnectionError(f"Could not open port {self.port}") from e
|
|
81
97
|
if self.ser:
|
|
@@ -55,5 +55,14 @@ class InvalidLocoError(MyLibraryError):
|
|
|
55
55
|
Raised for an invalid loco definition
|
|
56
56
|
Includes if the ID is out of range
|
|
57
57
|
or short code specified but code needs long code
|
|
58
|
+
or if the packet does not contain a loco_id (eg. certain Err codes)
|
|
59
|
+
"""
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
class InvalidFunctionError(MyLibraryError):
|
|
63
|
+
"""
|
|
64
|
+
Raised for an invalid function requests
|
|
65
|
+
For example asking for function information
|
|
66
|
+
from a response which doesn't include the function information.
|
|
58
67
|
"""
|
|
59
68
|
pass
|
|
@@ -34,7 +34,7 @@ class TestCanUSB4(unittest.TestCase):
|
|
|
34
34
|
def test_init_valid(self):
|
|
35
35
|
"""Test that initialization opens the serial port with correct settings."""
|
|
36
36
|
# Check if serial.Serial was called with correct args
|
|
37
|
-
self.mock_serial_class.assert_called_with(self.port, 115200, timeout=0.01)
|
|
37
|
+
self.mock_serial_class.assert_called_with(self.port, 115200, timeout=0.01, exclusive=None)
|
|
38
38
|
|
|
39
39
|
def test_init_empty_port(self):
|
|
40
40
|
"""Test that empty port raises InvalidConfigurationError."""
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import unittest
|
|
2
2
|
from typing import Optional
|
|
3
3
|
from pyvlcb import VLCB
|
|
4
|
+
# Import utils
|
|
5
|
+
from pyvlcb import num_to_1hexstr, num_to_2hexstr, num_to_4hexstr, f_to_bytes
|
|
4
6
|
from pyvlcb.vlcbformat import VLCBFormat
|
|
5
7
|
|
|
6
8
|
class TestVLCB(unittest.TestCase):
|
|
@@ -13,14 +15,14 @@ class TestVLCB(unittest.TestCase):
|
|
|
13
15
|
def test_num_to_hex_conversions(self):
|
|
14
16
|
"""Test that numbers are correctly padded and converted to hex strings."""
|
|
15
17
|
# Test 1 byte (2 chars)
|
|
16
|
-
self.assertEqual(
|
|
17
|
-
self.assertEqual(
|
|
18
|
+
self.assertEqual(num_to_1hexstr(10), "0A")
|
|
19
|
+
self.assertEqual(num_to_1hexstr(255), "FF")
|
|
18
20
|
|
|
19
21
|
# Test 2 bytes (4 chars)
|
|
20
|
-
self.assertEqual(
|
|
22
|
+
self.assertEqual(num_to_2hexstr(10), "000A")
|
|
21
23
|
|
|
22
24
|
# Test 4 bytes (8 chars)
|
|
23
|
-
self.assertEqual(
|
|
25
|
+
self.assertEqual(num_to_4hexstr(10), "0000000A")
|
|
24
26
|
|
|
25
27
|
## Tests for Packet Parsing
|
|
26
28
|
def test_parse_input_valid_1(self):
|
|
@@ -86,10 +88,10 @@ class TestVLCB(unittest.TestCase):
|
|
|
86
88
|
#f_to_bytes (f_num: int, function_status: List[int]) -> Tuple[bytes, bytes]
|
|
87
89
|
def test_f_to_bytes(self):
|
|
88
90
|
functions = [1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1]
|
|
89
|
-
self.assertEqual(
|
|
90
|
-
self.assertEqual(
|
|
91
|
-
self.assertEqual(
|
|
92
|
-
self.assertEqual(
|
|
91
|
+
self.assertEqual(f_to_bytes (1, functions), ("01", "19"))
|
|
92
|
+
self.assertEqual(f_to_bytes (2, functions), ("01", "19"))
|
|
93
|
+
self.assertEqual(f_to_bytes (7, functions), ("02", "06"))
|
|
94
|
+
self.assertEqual(f_to_bytes (24, functions), ("05", "00"))
|
|
93
95
|
|
|
94
96
|
|
|
95
97
|
if __name__ == "__main__":
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
""" Utils for use by other parts of the library """
|
|
2
|
+
|
|
3
|
+
from typing import Optional, Union, Tuple, List
|
|
4
|
+
|
|
5
|
+
# Where 1 x bytes (2 chars)
|
|
6
|
+
def num_to_1hexstr (num: int) -> str:
|
|
7
|
+
"""Convert number to a byte
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
num (int): Number to convert
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
String: A hex representation of the number (2 chars)
|
|
14
|
+
"""
|
|
15
|
+
return f"{hex(num).upper()[2:]:0>2}"
|
|
16
|
+
|
|
17
|
+
# Where 2 x bytes (4 chars)
|
|
18
|
+
def num_to_2hexstr (num: int) -> str:
|
|
19
|
+
"""Convert number to 2 bytes
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
num (int): Number to convert
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
String: A hex representation of the number (4 chars)
|
|
26
|
+
"""
|
|
27
|
+
return f"{hex(num).upper()[2:]:0>4}"
|
|
28
|
+
|
|
29
|
+
# Where 4 x bytes (8 chars)
|
|
30
|
+
def num_to_4hexstr (num: int) -> str:
|
|
31
|
+
"""Convert number to 4 bytes
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
num (int): Number to convert
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
String: A hex representation of the number (8 chars)
|
|
38
|
+
"""
|
|
39
|
+
return f"{hex(num).upper()[2:]:0>8}"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# dict to string without {} or ""
|
|
43
|
+
def dict_to_string (dictionary: dict) -> str:
|
|
44
|
+
""" Convert a dict to a string without {} or quotes """
|
|
45
|
+
data_string = ""
|
|
46
|
+
for key, value in dictionary.items():
|
|
47
|
+
if data_string != "":
|
|
48
|
+
data_string += " , "
|
|
49
|
+
data_string += f"{key} = {value}"
|
|
50
|
+
return data_string
|
|
51
|
+
|
|
52
|
+
def f_to_bytes (f_num: int, function_status: List[int]) -> Tuple[str, str]:
|
|
53
|
+
""" Convert a Fnumber (Loco function ID) to bytes as string representation
|
|
54
|
+
|
|
55
|
+
Can be used to create correct format for loco_set_dfun
|
|
56
|
+
Limited to range 0 to 28 - which is max for DFUN
|
|
57
|
+
Higher would need to use DFNON/DFOFF
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
f_num: Function number
|
|
61
|
+
function_status: List with value of all functions (must be updated before calling)
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Tuple of two x 2-characer strings representing the bytes
|
|
65
|
+
|
|
66
|
+
Raises: ValueError
|
|
67
|
+
|
|
68
|
+
"""
|
|
69
|
+
# Must be an F number between 0 and 28
|
|
70
|
+
if f_num < 0 or f_num > 28:
|
|
71
|
+
raise ValueError (f"Fnumber needs to be between 0 and 28. Number provided {f_num}")
|
|
72
|
+
|
|
73
|
+
# Extend function_status to 29 entries
|
|
74
|
+
function_list = function_status + [0] * (29 - len(function_status))
|
|
75
|
+
|
|
76
|
+
if f_num <= 4:
|
|
77
|
+
byte1 = 1
|
|
78
|
+
byte2 = (0b10000 * function_list[0] + # fn0 is higher nibble (bit 5)
|
|
79
|
+
0b0001 * function_list[1] +
|
|
80
|
+
0b0010 * function_list[2] +
|
|
81
|
+
0b0100 * function_list[3] +
|
|
82
|
+
0b1000 * function_list[4]
|
|
83
|
+
)
|
|
84
|
+
elif f_num <= 8:
|
|
85
|
+
byte1 = 2
|
|
86
|
+
byte2 = (0b0001 * function_list[5] +
|
|
87
|
+
0b0010 * function_list[6] +
|
|
88
|
+
0b0100 * function_list[7] +
|
|
89
|
+
0b1000 * function_list[8]
|
|
90
|
+
)
|
|
91
|
+
elif f_num <= 12:
|
|
92
|
+
byte1 = 3
|
|
93
|
+
byte2 = (0b0001 * function_list[9] +
|
|
94
|
+
0b0010 * function_list[10] +
|
|
95
|
+
0b0100 * function_list[11] +
|
|
96
|
+
0b1000 * function_list[12]
|
|
97
|
+
)
|
|
98
|
+
elif f_num <= 20:
|
|
99
|
+
byte1 = 4
|
|
100
|
+
byte2 = (0b0001 * function_list[13] +
|
|
101
|
+
0b0010 * function_list[14] +
|
|
102
|
+
0b0100 * function_list[15] +
|
|
103
|
+
0b1000 * function_list[16] +
|
|
104
|
+
0b10000 * function_list[17] +
|
|
105
|
+
0b100000 * function_list[18] +
|
|
106
|
+
0b1000000 * function_list[19] +
|
|
107
|
+
0b10000000 * function_list[20]
|
|
108
|
+
)
|
|
109
|
+
elif f_num <= 28:
|
|
110
|
+
byte1 = 5
|
|
111
|
+
byte2 = (0b0001 * function_list[21] +
|
|
112
|
+
0b0010 * function_list[22] +
|
|
113
|
+
0b0100 * function_list[23] +
|
|
114
|
+
0b1000 * function_list[24] +
|
|
115
|
+
0b10000 * function_list[25] +
|
|
116
|
+
0b100000 * function_list[26] +
|
|
117
|
+
0b1000000 * function_list[27] +
|
|
118
|
+
0b10000000 * function_list[28]
|
|
119
|
+
)
|
|
120
|
+
# convert to strings before returning
|
|
121
|
+
# and with 0xFF guarentees it doesn't overflow (although not really neccessary for this method)
|
|
122
|
+
str1 = f"{ (byte1 & 0xFF) :02x}"
|
|
123
|
+
str2 = f"{ (byte2 & 0xFF) :02x}"
|
|
124
|
+
return (str1, str2)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# Where 2 bytes convert to addr id
|
|
128
|
+
def bytes_to_addr (byte1: bytes, byte2: bytes) -> int:
|
|
129
|
+
"""Convert 2 byte values into an address id sring
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
byte1 (bytes): Most significant byte
|
|
133
|
+
byte2 (bytes): Least significant byte
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Int: The address id value
|
|
137
|
+
"""
|
|
138
|
+
msb = int(byte1)
|
|
139
|
+
lsb = int(byte2)
|
|
140
|
+
return ((msb << 8) + lsb)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def bytes_to_hexstr (byte1: bytes, byte2: bytes) -> str:
|
|
144
|
+
"""Convert 2 bytes to a hex string
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
byte1 (bytes): Most significant byte
|
|
148
|
+
byte2 (bytes): Least significant byte
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
String: A hex representation of the number
|
|
152
|
+
"""
|
|
153
|
+
return f"{hex(byte1).upper()[2:]:0>2}{hex(byte2).upper()[2:]:0>2}"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def bytes_to_functions (fn1: bytes, fn2: bytes, fn3: bytes) -> List[int]:
|
|
157
|
+
""" Sets the value of the functions from a PLOC message
|
|
158
|
+
Only returns values for F0 to F12 (others not included in PLOC)
|
|
159
|
+
Provides as a list for inclusion in menus etc.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
fn1: Function byte F0 to F4 - bit 5 = dir lighting (F0), bit 6 = direction, bit 7 = res, bit 8 = 0
|
|
163
|
+
fn2: Function byte F5 to F8 - bit 5 upwards reserved
|
|
164
|
+
fn3: Function byte F9 to F12
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
List of function values as 0 or 1 for each function as off and on
|
|
168
|
+
|
|
169
|
+
"""
|
|
170
|
+
data_in = [fn1, fn2, fn3]
|
|
171
|
+
mask = [0b0001, 0b0010, 0b0100, 0b1000]
|
|
172
|
+
function_status = [0] * 29
|
|
173
|
+
# Handle 0 separately as it's in the upper nibble
|
|
174
|
+
function_status[0] = data_in[0] & 0b10000
|
|
175
|
+
# Create a list of 12 entries
|
|
176
|
+
for i in range (0, 3):
|
|
177
|
+
for j in range (0, 4):
|
|
178
|
+
function_status[(i*4)+j+1] = 1 if (data_in[i] & mask[j]) > 0 else 0
|
|
179
|
+
|
|
180
|
+
return function_status
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import warnings
|
|
3
|
+
from .utils import bytes_to_addr, bytes_to_functions
|
|
3
4
|
from typing import List, Optional, Union, Dict, Any
|
|
4
5
|
|
|
5
6
|
# Set up a null handler so nothing prints by default unless the user enables it
|
|
@@ -49,7 +50,18 @@ class VLCBFormat :
|
|
|
49
50
|
return VLCBOpcode.opcodes[str_value]['opc']
|
|
50
51
|
else:
|
|
51
52
|
raise ValueError(f"Opcode {str_value} is not defined.")
|
|
52
|
-
|
|
53
|
+
|
|
54
|
+
def get_data (self) -> OpcodeData:
|
|
55
|
+
"""Returns the opcode associated with the data string as a dict
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
OpcodeData: Dict from the VLCBOpcode
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
ValueError: If opcode not found
|
|
62
|
+
"""
|
|
63
|
+
return VLCBOpcode.parse_data(self.data)
|
|
64
|
+
|
|
53
65
|
def format_data (self) -> OpcodeData:
|
|
54
66
|
"""Returns the opcode associated with the data string
|
|
55
67
|
|
|
@@ -60,7 +72,66 @@ class VLCBFormat :
|
|
|
60
72
|
ValueError: If opcode not found
|
|
61
73
|
"""
|
|
62
74
|
return VLCBOpcode.parse_data(self.data)
|
|
75
|
+
|
|
76
|
+
def get_loco_id (self) -> int:
|
|
77
|
+
"""Converts AddrHigh and AddrLow into a loco_id
|
|
78
|
+
|
|
79
|
+
Only valid with certain VLCBFormat packets associated with Locos.
|
|
80
|
+
If packet is does not contain the AddrHigh & AddrLow values (which
|
|
81
|
+
may be formatted differently) then raises a InvalidLocoError
|
|
63
82
|
|
|
83
|
+
Returns:
|
|
84
|
+
loco_id: Loco number
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
InvalidLocoError: If AddrHigh / AddrLow are not in the packet
|
|
88
|
+
"""
|
|
89
|
+
loco_id = None
|
|
90
|
+
if self.opcode() == "PLOC":
|
|
91
|
+
# Get data
|
|
92
|
+
data_dict = VLCBOpcode.parse_data(self.data)
|
|
93
|
+
loco_id = data_dict['AddrHigh_AddrLow'] & 0x3FFF
|
|
94
|
+
elif self.opcode() == "ERR":
|
|
95
|
+
# also check it's one of the Error codes associated with allocate loco etc.
|
|
96
|
+
# 1 = loco stack full 2 = loco taken, 7 = invalid request
|
|
97
|
+
# The following are not supported as data bytes contain session / consist ID and not loco_id
|
|
98
|
+
# 3 = no session, 4 consist empty, 5 loco not found, 6 can bus error
|
|
99
|
+
data_dict = VLCBOpcode.parse_data(self.data)
|
|
100
|
+
if data_dict["ErrCode"] in [1, 2, 7]:
|
|
101
|
+
loco_id = bytes_to_addr(data_dict['Byte1'],data_dict['Byte2']) & 0x3FFF
|
|
102
|
+
else:
|
|
103
|
+
raise InvalidLocoError (f"Error code {data_dict['ErrCode']} does not contain a loco_id")
|
|
104
|
+
|
|
105
|
+
# If loco_id not updated then raise error
|
|
106
|
+
if loco_id != None:
|
|
107
|
+
return loco_id
|
|
108
|
+
else:
|
|
109
|
+
raise InvalidLocoError(f"Opcode {self.opcode()} does not contain a loco_id")
|
|
110
|
+
|
|
111
|
+
def get_function_list (self) -> List[int]:
|
|
112
|
+
"""Where packet contains Fn1, Fn2, Fn3 (eg. PLOC)
|
|
113
|
+
returns
|
|
114
|
+
|
|
115
|
+
Only valid with certain VLCBFormat packets associated with Locos.
|
|
116
|
+
If packet is does not contain the 3 byte values (which
|
|
117
|
+
may be formatted differently) then raises a InvalidFunctionError
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
List of function values as 0 or 1 for each function as off and on
|
|
121
|
+
|
|
122
|
+
Raises:
|
|
123
|
+
InvalidFunctionError: If Function Bytes are not in the packet
|
|
124
|
+
"""
|
|
125
|
+
if self.opcode() == "PLOC":
|
|
126
|
+
# Get data
|
|
127
|
+
data_dict = VLCBOpcode.parse_data(self.data)
|
|
128
|
+
return bytes_to_functions (data_dict['Fn1'], data_dict['Fn2'], data_dict['Fn3'])
|
|
129
|
+
else:
|
|
130
|
+
raise InvalidLocoError(f"Opcode {self.opcode()} does not contain a loco_id")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
|
|
64
135
|
def __str__ (self):
|
|
65
136
|
return f'{self.priority} : {self.can_id} : {self.opcode()} ({self.data[0:2]}) : {self.data} / {self.format_data()}'
|
|
66
137
|
|
pyvlcb-0.1.1/README.md
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
# PyVLCB Python Software Library for CBUS / VLCB
|
|
2
|
-
|
|
3
|
-
This is a small software library for CBUS and VLCB using Python.
|
|
4
|
-
|
|
5
|
-
It's designed for use with the MERG CANUSB4 CBUS adapter.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
## Install
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
For most systems including a Raspberry Pi then Python is managed using virtual environment.
|
|
12
|
-
To setup a virtual environment enter the following commands
|
|
13
|
-
|
|
14
|
-
mkdir ~/venv
|
|
15
|
-
python3 -m venv ~/venv/pyvlcb --system-site-packages
|
|
16
|
-
|
|
17
|
-
You will then need to run the following command to activate the virtual environment
|
|
18
|
-
|
|
19
|
-
source ~/venv/pyvlcb/bin/activate
|
|
20
|
-
|
|
21
|
-
To instally the latest release use
|
|
22
|
-
|
|
23
|
-
pip install pyvlcb
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
## Development
|
|
27
|
-
|
|
28
|
-
The latest source code is available from GitHub. [PyVLCB on GitHub](https://github.com/penguintutor/pyvlcb).
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
### Installing as a git submodule
|
|
32
|
-
|
|
33
|
-
To install the library, as a submodule in your own git project repository, then enter your project directory and run:
|
|
34
|
-
|
|
35
|
-
git submodule add https://github.com/penguintutor/pyvlcb.git lib/pyvlcb
|
|
36
|
-
pip install lib/pyvlcb
|
|
37
|
-
|
|
38
|
-
If you subsequently want to get the latest version of the library run a git pull then use the pip install command again.
|
|
39
|
-
|
|
40
|
-
_Important_ To use as a submodule you must create your own git project first and install within the project folder.
|
|
41
|
-
|
|
42
|
-
## Library Reference
|
|
43
|
-
|
|
44
|
-
See the link below for the library reference documentation
|
|
45
|
-
[PyVLCB Library Reference Documentation](https://penguintutor.github.io/pyvlcb/reference/)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
## Demo examples
|
|
49
|
-
|
|
50
|
-
Example code is stored within the demo folder available from GitHub. Copy these into your project folder to test the library and connectivity.
|
|
51
|
-
The demos are created for a Raspberry Pi or other Linux computer. The USB port is hard-coded as /dev/ttyACM0
|
|
52
|
-
For other computers / USB ports then edit the python file directory and update the port statement.
|
|
53
|
-
|
|
54
|
-
## More Details
|
|
55
|
-
|
|
56
|
-
For more details see: [PenguinTutor PyVLCB library page](https://www.penguintutor.com/projects/pyvlcb)
|
|
57
|
-
|
|
58
|
-
For an example of a program using this library see the [PenguinTutor Pi SignalBox project page](https://www.penguintutor.com/projects/pisignalbox)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|