PyVLCB 0.1.2__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.2/src/PyVLCB.egg-info → pyvlcb-0.1.3}/PKG-INFO +1 -1
- {pyvlcb-0.1.2 → pyvlcb-0.1.3}/pyproject.toml +1 -1
- {pyvlcb-0.1.2 → pyvlcb-0.1.3/src/PyVLCB.egg-info}/PKG-INFO +1 -1
- {pyvlcb-0.1.2 → pyvlcb-0.1.3}/src/pyvlcb/__init__.py +60 -7
- {pyvlcb-0.1.2 → pyvlcb-0.1.3}/src/pyvlcb/canusb.py +19 -3
- {pyvlcb-0.1.2 → pyvlcb-0.1.3}/src/pyvlcb/exceptions.py +8 -0
- {pyvlcb-0.1.2 → pyvlcb-0.1.3}/src/pyvlcb/tests/test_canusb4.py +1 -1
- {pyvlcb-0.1.2 → pyvlcb-0.1.3}/src/pyvlcb/tests/test_vlcb.py +4 -4
- {pyvlcb-0.1.2 → pyvlcb-0.1.3}/src/pyvlcb/utils.py +35 -5
- {pyvlcb-0.1.2 → pyvlcb-0.1.3}/src/pyvlcb/vlcbformat.py +24 -1
- {pyvlcb-0.1.2 → pyvlcb-0.1.3}/LICENSE +0 -0
- {pyvlcb-0.1.2 → pyvlcb-0.1.3}/README.md +0 -0
- {pyvlcb-0.1.2 → pyvlcb-0.1.3}/setup.cfg +0 -0
- {pyvlcb-0.1.2 → pyvlcb-0.1.3}/src/PyVLCB.egg-info/SOURCES.txt +0 -0
- {pyvlcb-0.1.2 → pyvlcb-0.1.3}/src/PyVLCB.egg-info/dependency_links.txt +0 -0
- {pyvlcb-0.1.2 → pyvlcb-0.1.3}/src/PyVLCB.egg-info/requires.txt +0 -0
- {pyvlcb-0.1.2 → pyvlcb-0.1.3}/src/PyVLCB.egg-info/top_level.txt +0 -0
- {pyvlcb-0.1.2 → pyvlcb-0.1.3}/src/pyvlcb/tests/run_tests.py +0 -0
|
@@ -3,7 +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
|
|
6
|
+
from .utils import num_to_1hexstr, num_to_2hexstr, num_to_4hexstr, f_to_bytes, dict_to_string
|
|
7
7
|
from .exceptions import (
|
|
8
8
|
MyLibraryError,
|
|
9
9
|
DeviceConnectionError,
|
|
@@ -123,7 +123,7 @@ class VLCB:
|
|
|
123
123
|
opcode = vlcb_entry.data[0:2]
|
|
124
124
|
opcode_string = f'{opcode} - {VLCBOpcode.opcode_mnemonic(opcode)}'
|
|
125
125
|
#data_string = f"{VLCBOpcode.parse_data(vlcb_entry.data)}"
|
|
126
|
-
data_string =
|
|
126
|
+
data_string = dict_to_string(VLCBOpcode.parse_data(vlcb_entry.data))
|
|
127
127
|
return [date_string, direction, message, str(vlcb_entry.can_id), opcode_string, data_string]
|
|
128
128
|
# Todo - error handling
|
|
129
129
|
|
|
@@ -400,12 +400,42 @@ class VLCB:
|
|
|
400
400
|
"""
|
|
401
401
|
return f"{self.make_header(opcode='23')}23{num_to_1hexstr(session_id)};"
|
|
402
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)
|
|
431
|
+
|
|
403
432
|
# Set loco speed and direction (always done together)
|
|
404
|
-
# Maximum once every 32 miliseconds (GUI configured based on non triggered so shouldn't be an issue)
|
|
405
|
-
# Could add time detection if required
|
|
406
|
-
# This uses the combined speed and direction value
|
|
407
433
|
def loco_speeddir (self, session_id: int, speeddir: int) -> str:
|
|
408
|
-
"""
|
|
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
|
|
409
439
|
|
|
410
440
|
Uses DSPD (47)
|
|
411
441
|
|
|
@@ -437,5 +467,28 @@ class VLCB:
|
|
|
437
467
|
String: A string for the request
|
|
438
468
|
"""
|
|
439
469
|
return f"{self.make_header(opcode='60')}60{num_to_1hexstr(session_id)}{num_to_1hexstr(byte1)}{num_to_1hexstr(byte2)};"
|
|
440
|
-
|
|
441
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:
|
|
@@ -57,4 +57,12 @@ class InvalidLocoError(MyLibraryError):
|
|
|
57
57
|
or short code specified but code needs long code
|
|
58
58
|
or if the packet does not contain a loco_id (eg. certain Err codes)
|
|
59
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.
|
|
67
|
+
"""
|
|
60
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."""
|
|
@@ -88,10 +88,10 @@ class TestVLCB(unittest.TestCase):
|
|
|
88
88
|
#f_to_bytes (f_num: int, function_status: List[int]) -> Tuple[bytes, bytes]
|
|
89
89
|
def test_f_to_bytes(self):
|
|
90
90
|
functions = [1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1]
|
|
91
|
-
self.assertEqual(f_to_bytes (1, functions), (
|
|
92
|
-
self.assertEqual(f_to_bytes (2, functions), (
|
|
93
|
-
self.assertEqual(f_to_bytes (7, functions), (
|
|
94
|
-
self.assertEqual(f_to_bytes (24, functions), (
|
|
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"))
|
|
95
95
|
|
|
96
96
|
|
|
97
97
|
if __name__ == "__main__":
|
|
@@ -40,7 +40,7 @@ def num_to_4hexstr (num: int) -> str:
|
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
# dict to string without {} or ""
|
|
43
|
-
def
|
|
43
|
+
def dict_to_string (dictionary: dict) -> str:
|
|
44
44
|
""" Convert a dict to a string without {} or quotes """
|
|
45
45
|
data_string = ""
|
|
46
46
|
for key, value in dictionary.items():
|
|
@@ -49,8 +49,8 @@ def _dict_to_string (dictionary: dict) -> str:
|
|
|
49
49
|
data_string += f"{key} = {value}"
|
|
50
50
|
return data_string
|
|
51
51
|
|
|
52
|
-
def f_to_bytes (f_num: int, function_status: List[int]) -> Tuple[
|
|
53
|
-
""" Convert a Fnumber (Loco function ID) to bytes
|
|
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
54
|
|
|
55
55
|
Can be used to create correct format for loco_set_dfun
|
|
56
56
|
Limited to range 0 to 28 - which is max for DFUN
|
|
@@ -61,7 +61,7 @@ def f_to_bytes (f_num: int, function_status: List[int]) -> Tuple[bytes, bytes]:
|
|
|
61
61
|
function_status: List with value of all functions (must be updated before calling)
|
|
62
62
|
|
|
63
63
|
Returns:
|
|
64
|
-
Tuple of two bytes
|
|
64
|
+
Tuple of two x 2-characer strings representing the bytes
|
|
65
65
|
|
|
66
66
|
Raises: ValueError
|
|
67
67
|
|
|
@@ -117,7 +117,11 @@ def f_to_bytes (f_num: int, function_status: List[int]) -> Tuple[bytes, bytes]:
|
|
|
117
117
|
0b1000000 * function_list[27] +
|
|
118
118
|
0b10000000 * function_list[28]
|
|
119
119
|
)
|
|
120
|
-
|
|
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)
|
|
121
125
|
|
|
122
126
|
|
|
123
127
|
# Where 2 bytes convert to addr id
|
|
@@ -148,3 +152,29 @@ def bytes_to_hexstr (byte1: bytes, byte2: bytes) -> str:
|
|
|
148
152
|
"""
|
|
149
153
|
return f"{hex(byte1).upper()[2:]:0>2}{hex(byte2).upper()[2:]:0>2}"
|
|
150
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,6 +1,6 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import warnings
|
|
3
|
-
from .utils import bytes_to_addr
|
|
3
|
+
from .utils import bytes_to_addr, bytes_to_functions
|
|
4
4
|
from typing import List, Optional, Union, Dict, Any
|
|
5
5
|
|
|
6
6
|
# Set up a null handler so nothing prints by default unless the user enables it
|
|
@@ -108,6 +108,29 @@ class VLCBFormat :
|
|
|
108
108
|
else:
|
|
109
109
|
raise InvalidLocoError(f"Opcode {self.opcode()} does not contain a loco_id")
|
|
110
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
|
+
|
|
111
134
|
|
|
112
135
|
def __str__ (self):
|
|
113
136
|
return f'{self.priority} : {self.can_id} : {self.opcode()} ({self.data[0:2]}) : {self.data} / {self.format_data()}'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|