dsf-python 3.6.0rc3__py3-none-any.whl → 3.7.0b1__py3-none-any.whl
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.
- dsf/__init__.py +2 -2
- dsf/commands/base_command.py +4 -2
- dsf/commands/code.py +34 -24
- dsf/commands/code_channel.py +7 -1
- dsf/commands/code_interception.py +0 -4
- dsf/commands/code_parameter.py +106 -62
- dsf/commands/files.py +7 -5
- dsf/commands/generic.py +4 -28
- dsf/commands/http_endpoints.py +0 -14
- dsf/commands/object_model.py +0 -12
- dsf/commands/packages.py +4 -4
- dsf/commands/plugins.py +15 -15
- dsf/commands/responses.py +27 -14
- dsf/commands/user_sessions.py +2 -8
- dsf/connections/base_command_connection.py +11 -4
- dsf/connections/base_connection.py +27 -8
- dsf/connections/command_connection.py +2 -2
- dsf/connections/exceptions.py +3 -1
- dsf/connections/init_messages/client_init_messages.py +19 -7
- dsf/connections/init_messages/server_init_message.py +9 -3
- dsf/connections/intercept_connection.py +3 -3
- dsf/connections/subscribe_connection.py +166 -12
- dsf/http.py +33 -16
- dsf/object_model/boards/accelerometer.py +11 -34
- dsf/object_model/boards/board_closed_loop.py +7 -21
- dsf/object_model/boards/boards.py +66 -244
- dsf/object_model/boards/direct_display/direct_display.py +6 -11
- dsf/object_model/boards/direct_display/direct_display_encoder.py +4 -11
- dsf/object_model/boards/direct_display/direct_display_screen.py +26 -73
- dsf/object_model/boards/direct_display/direct_display_screen_st7567.py +5 -22
- dsf/object_model/boards/driver.py +9 -18
- dsf/object_model/boards/driver_closed_loop.py +20 -56
- dsf/object_model/boards/driver_config.py +16 -0
- dsf/object_model/boards/driver_mode.py +23 -0
- dsf/object_model/boards/min_max_current.py +11 -34
- dsf/object_model/directories/directories.py +19 -73
- dsf/object_model/fans/fan_thermostatic_control.py +10 -38
- dsf/object_model/fans/fans.py +23 -99
- dsf/object_model/heat/heat.py +25 -46
- dsf/object_model/heat/heater.py +47 -167
- dsf/object_model/heat/heater_model.py +23 -98
- dsf/object_model/heat/heater_model_pid.py +11 -53
- dsf/object_model/heat/heater_monitor.py +13 -57
- dsf/object_model/inputs/input_channel.py +54 -201
- dsf/object_model/inputs/input_channel_state.py +3 -0
- dsf/object_model/inputs/inputs.py +1 -1
- dsf/object_model/job/build.py +14 -44
- dsf/object_model/job/build_object.py +15 -37
- dsf/object_model/job/gcode_fileinfo.py +38 -113
- dsf/object_model/job/job.py +33 -170
- dsf/object_model/job/layer.py +18 -48
- dsf/object_model/job/thumbnail_info.py +15 -70
- dsf/object_model/job/times_left.py +13 -44
- dsf/object_model/led_strips/led_strip.py +24 -54
- dsf/object_model/led_strips/led_strip_color_order.py +22 -0
- dsf/object_model/limits/limits.py +58 -272
- dsf/object_model/messages/messages.py +12 -45
- dsf/object_model/model_collection.py +114 -24
- dsf/object_model/model_dictionary.py +22 -7
- dsf/object_model/model_object.py +27 -19
- dsf/object_model/model_type.py +21 -0
- dsf/object_model/move/axis.py +78 -251
- dsf/object_model/move/current_move.py +27 -61
- dsf/object_model/move/driver_id.py +10 -4
- dsf/object_model/move/extruder.py +54 -174
- dsf/object_model/move/extruder_non_linear.py +8 -30
- dsf/object_model/move/extruder_pressure_advance.py +18 -0
- dsf/object_model/move/input_shaping.py +26 -53
- dsf/object_model/move/keepout_zone.py +13 -40
- dsf/object_model/move/kinematics/core_kinematics.py +7 -13
- dsf/object_model/move/kinematics/delta_kinematics.py +21 -63
- dsf/object_model/move/kinematics/delta_tower.py +17 -50
- dsf/object_model/move/kinematics/hangprinter_kinematics.py +9 -20
- dsf/object_model/move/kinematics/kinematics.py +14 -30
- dsf/object_model/move/kinematics/kinematics_name.py +8 -8
- dsf/object_model/move/kinematics/polar_kinematics.py +16 -0
- dsf/object_model/move/kinematics/scara_kinematics.py +26 -0
- dsf/object_model/move/kinematics/tilt_correction.py +21 -54
- dsf/object_model/move/kinematics/zleadscrew_kinematics.py +5 -7
- dsf/object_model/move/microstepping.py +7 -21
- dsf/object_model/move/motion_system.py +52 -0
- dsf/object_model/move/motors_idle_control.py +7 -21
- dsf/object_model/move/move.py +75 -168
- dsf/object_model/move/move_calibration.py +9 -23
- dsf/object_model/move/move_compensation.py +18 -60
- dsf/object_model/move/move_deviations.py +7 -21
- dsf/object_model/move/move_queue_item.py +7 -21
- dsf/object_model/move/move_rotation.py +8 -19
- dsf/object_model/move/move_segmentation.py +5 -20
- dsf/object_model/move/probe_grid.py +18 -41
- dsf/object_model/move/skew.py +11 -42
- dsf/object_model/network/network.py +13 -42
- dsf/object_model/network/network_interface.py +32 -177
- dsf/object_model/network/network_interface_type.py +1 -1
- dsf/object_model/network/network_state.py +3 -0
- dsf/object_model/object_model.py +23 -126
- dsf/object_model/plugins/plugin_manifest.py +53 -219
- dsf/object_model/plugins/plugins.py +17 -30
- dsf/object_model/plugins/sbc_permissions.py +2 -2
- dsf/object_model/sbc/cpu.py +6 -41
- dsf/object_model/sbc/dsf/communication_method.py +11 -0
- dsf/object_model/sbc/dsf/dsf.py +13 -64
- dsf/object_model/sbc/dsf/http_endpoint.py +14 -59
- dsf/object_model/sbc/dsf/http_endpoint_type.py +2 -2
- dsf/object_model/sbc/dsf/user_sessions/user_sessions.py +17 -65
- dsf/object_model/sbc/memory.py +4 -22
- dsf/object_model/sbc/sbc.py +12 -88
- dsf/object_model/sensors/analog_sensor.py +15 -142
- dsf/object_model/sensors/analog_sensor_type.py +22 -4
- dsf/object_model/sensors/endstop.py +11 -45
- dsf/object_model/sensors/filament_monitors/Duet3DFilamentMonitor.py +17 -66
- dsf/object_model/sensors/filament_monitors/filament_monitor.py +11 -54
- dsf/object_model/sensors/filament_monitors/laser_filament_monitor.py +30 -110
- dsf/object_model/sensors/filament_monitors/pulsed_filament_monitor.py +29 -97
- dsf/object_model/sensors/filament_monitors/rotating_magnet_filament_monitor.py +29 -97
- dsf/object_model/sensors/gp_input_port.py +4 -10
- dsf/object_model/sensors/probe.py +24 -225
- dsf/object_model/sensors/probe_touch_mode.py +14 -0
- dsf/object_model/sensors/sensors.py +17 -31
- dsf/object_model/sensors/temperature_error.py +12 -0
- dsf/object_model/spindles/spindle_type.py +0 -3
- dsf/object_model/spindles/spindles.py +28 -131
- dsf/object_model/state/beep_request.py +5 -20
- dsf/object_model/state/gp_output_port.py +7 -22
- dsf/object_model/state/message_box.py +15 -131
- dsf/object_model/state/restore_point.py +12 -74
- dsf/object_model/state/startup_error.py +9 -32
- dsf/object_model/state/state.py +27 -241
- dsf/object_model/tools/tool_retraction.py +17 -56
- dsf/object_model/tools/tools.py +23 -195
- dsf/object_model/utils.py +131 -26
- dsf/object_model/volumes/volumes.py +20 -85
- dsf/utils.py +87 -10
- dsf_python-3.7.0b1.dist-info/METADATA +303 -0
- dsf_python-3.7.0b1.dist-info/RECORD +188 -0
- {dsf_python-3.6.0rc3.dist-info → dsf_python-3.7.0b1.dist-info}/WHEEL +1 -1
- dsf_python-3.6.0rc3.dist-info/METADATA +0 -60
- dsf_python-3.6.0rc3.dist-info/RECORD +0 -180
- {dsf_python-3.6.0rc3.dist-info → dsf_python-3.7.0b1.dist-info}/licenses/LICENSE +0 -0
- {dsf_python-3.6.0rc3.dist-info → dsf_python-3.7.0b1.dist-info}/top_level.txt +0 -0
dsf/__init__.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
__version__ = "3.
|
|
1
|
+
__version__ = "3.7.0-beta.1"
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
5
|
|
|
6
6
|
# Default socket file path
|
|
7
|
-
SOCKET_FILE = "/run/dsf/dcs.sock"
|
|
7
|
+
SOCKET_FILE: str = "/run/dsf/dcs.sock"
|
|
8
8
|
|
|
9
9
|
# Try to read socket file path from config
|
|
10
10
|
config_path = "/opt/dsf/conf/config.json"
|
dsf/commands/base_command.py
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
1
3
|
class BaseCommand:
|
|
2
4
|
"""Base class of a command."""
|
|
3
5
|
|
|
4
6
|
@classmethod
|
|
5
|
-
def from_json(cls, data):
|
|
7
|
+
def from_json(cls, data: Any):
|
|
6
8
|
"""Deserialize an instance of this class from a JSON deserialized dictionary"""
|
|
7
9
|
return cls(**data)
|
|
8
10
|
|
|
9
|
-
def __init__(self, command: str, **kwargs):
|
|
11
|
+
def __init__(self, command: str, **kwargs: Any):
|
|
10
12
|
self.command = command
|
|
11
13
|
for key, value in kwargs.items():
|
|
12
14
|
self.__dict__[key] = value
|
dsf/commands/code.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import
|
|
1
|
+
from typing import Callable, Optional, cast
|
|
2
2
|
|
|
3
3
|
from .base_command import BaseCommand
|
|
4
4
|
from .code_channel import CodeChannel
|
|
@@ -13,15 +13,24 @@ class Code(BaseCommand):
|
|
|
13
13
|
"""A parsed representation of a generic G/M/T-code"""
|
|
14
14
|
|
|
15
15
|
@classmethod
|
|
16
|
-
def from_json(cls, data):
|
|
16
|
+
def from_json(cls, data: dict[str, object]) -> "Code":
|
|
17
17
|
"""Deserialize an instance of this class from JSON deserialized dictionary"""
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
# TODO refactor the types here to use JSONObj
|
|
19
|
+
message_from_json = cast(Callable[[dict[str, object]], Message], getattr(Message, "from_json"))
|
|
20
|
+
parameter_from_json = cast(Callable[[dict[str, object]], CodeParameter], getattr(CodeParameter, "from_json"))
|
|
21
|
+
|
|
22
|
+
result_raw = cast(list[dict[str, object]] | None, data["result"])
|
|
23
|
+
data["result"] = None if result_raw is None else [message_from_json(item) for item in result_raw]
|
|
24
|
+
|
|
25
|
+
parameters_raw = cast(list[dict[str, object]], data["parameters"])
|
|
26
|
+
data["parameters"] = [parameter_from_json(item) for item in parameters_raw]
|
|
20
27
|
if "channel" in data:
|
|
21
28
|
data["channel"] = CodeChannel(data["channel"])
|
|
22
29
|
return cls(**data)
|
|
23
30
|
|
|
24
|
-
def __init__(self, **kwargs):
|
|
31
|
+
def __init__(self, **kwargs: object) -> None:
|
|
32
|
+
kwargs_copy = dict(kwargs)
|
|
33
|
+
command = cast(str, kwargs_copy.pop("command"))
|
|
25
34
|
# The connection ID this code was received from. If this is 0, the code originates from an internal DCS task
|
|
26
35
|
# Usually there is no need to populate this property.
|
|
27
36
|
# It is internally overwritten by the control server on receipt
|
|
@@ -29,31 +38,31 @@ class Code(BaseCommand):
|
|
|
29
38
|
|
|
30
39
|
# Result of this code. This property is only set when the code has finished its execution.
|
|
31
40
|
# It remains None if the code has been cancelled
|
|
32
|
-
self.result: Optional[
|
|
41
|
+
self.result: Optional[list[Message]] = None
|
|
33
42
|
|
|
34
43
|
# Type of the code
|
|
35
|
-
self.type = CodeType.CodeNone
|
|
44
|
+
self.type: CodeType = CodeType.CodeNone
|
|
36
45
|
|
|
37
46
|
# Code channel to send this code to
|
|
38
|
-
self.channel = CodeChannel.DEFAULT_CHANNEL
|
|
47
|
+
self.channel: CodeChannel = CodeChannel.DEFAULT_CHANNEL
|
|
39
48
|
|
|
40
49
|
# Line number of this code
|
|
41
|
-
self.lineNumber = None
|
|
50
|
+
self.lineNumber: Optional[int] = None
|
|
42
51
|
|
|
43
52
|
# Number of whitespaces prefixing the command content
|
|
44
|
-
self.indent = 0
|
|
53
|
+
self.indent: int = 0
|
|
45
54
|
|
|
46
55
|
# Type of conditional G-code (if any)
|
|
47
56
|
self.keyword = KeywordType.KeywordNone
|
|
48
57
|
|
|
49
58
|
# Argument of the conditional G-code (if any)
|
|
50
|
-
self.keywordArgument = None
|
|
59
|
+
self.keywordArgument: Optional[str] = None
|
|
51
60
|
|
|
52
61
|
# Major code number (e.g. 28 in G28)
|
|
53
|
-
self.majorNumber = None
|
|
62
|
+
self.majorNumber: Optional[int] = None
|
|
54
63
|
|
|
55
64
|
# Minor code number (e.g. 3 in G54.3)
|
|
56
|
-
self.minorNumber = None
|
|
65
|
+
self.minorNumber: Optional[int] = None
|
|
57
66
|
|
|
58
67
|
# Flags of this code
|
|
59
68
|
self.flags = CodeFlags.CodeFlagsNone
|
|
@@ -71,16 +80,16 @@ class Code(BaseCommand):
|
|
|
71
80
|
self.length = None
|
|
72
81
|
|
|
73
82
|
# List of parsed code parameters
|
|
74
|
-
self.parameters:
|
|
83
|
+
self.parameters: list[CodeParameter] = []
|
|
75
84
|
|
|
76
|
-
super().__init__(**
|
|
85
|
+
super().__init__(command=command, **kwargs_copy)
|
|
77
86
|
|
|
78
87
|
@property
|
|
79
88
|
def is_from_file_channel(self) -> bool:
|
|
80
89
|
"""Check if this code is from a file channel"""
|
|
81
90
|
return self.channel is CodeChannel.File or self.channel is CodeChannel.File2
|
|
82
91
|
|
|
83
|
-
def parameter(self, letter: str, default=None):
|
|
92
|
+
def parameter(self, letter: str, default: Optional[object] = None) -> Optional[CodeParameter]:
|
|
84
93
|
"""Retrieve the parameter whose letter equals c or generate a default parameter"""
|
|
85
94
|
letter = letter.upper()
|
|
86
95
|
param = [param for param in self.parameters if param.letter.upper() == letter]
|
|
@@ -90,12 +99,12 @@ class Code(BaseCommand):
|
|
|
90
99
|
return CodeParameter.simple_param(letter, default)
|
|
91
100
|
return None
|
|
92
101
|
|
|
93
|
-
def get_unprecedented_string(self, quote: bool = False):
|
|
102
|
+
def get_unprecedented_string(self, quote: bool = False) -> str:
|
|
94
103
|
"""
|
|
95
104
|
Reconstruct an unprecedented string from the parameter list or
|
|
96
105
|
retrieve the parameter which does not have a letter assigned.
|
|
97
106
|
"""
|
|
98
|
-
str_list = []
|
|
107
|
+
str_list: list[str] = []
|
|
99
108
|
for param in self.parameters:
|
|
100
109
|
if quote and param.is_string:
|
|
101
110
|
str_list.append(f'{param.letter}"{param.string_value}"')
|
|
@@ -103,13 +112,14 @@ class Code(BaseCommand):
|
|
|
103
112
|
str_list.append(f"{param.letter}{param.string_value}")
|
|
104
113
|
return " ".join(str_list)
|
|
105
114
|
|
|
106
|
-
def __str__(self):
|
|
115
|
+
def __str__(self) -> str:
|
|
107
116
|
"""Convert the parsed code back to a text-based G/M/T-code"""
|
|
108
117
|
if self.keyword != KeywordType.KeywordNone:
|
|
118
|
+
keyword = cast(str, self.keyword_to_str())
|
|
109
119
|
if self.keywordArgument is not None:
|
|
110
|
-
return f"{
|
|
120
|
+
return f"{keyword} {self.keywordArgument}"
|
|
111
121
|
else:
|
|
112
|
-
return
|
|
122
|
+
return keyword
|
|
113
123
|
|
|
114
124
|
if self.type == CodeType.Comment:
|
|
115
125
|
return f";{self.comment}"
|
|
@@ -129,7 +139,7 @@ class Code(BaseCommand):
|
|
|
129
139
|
|
|
130
140
|
return "".join(str_list)
|
|
131
141
|
|
|
132
|
-
def short_str(self):
|
|
142
|
+
def short_str(self) -> str:
|
|
133
143
|
"""Convert only the command portion to a text-based G/M/T-code (e.g. G28)"""
|
|
134
144
|
if self.type == CodeType.Comment:
|
|
135
145
|
return "(comment)"
|
|
@@ -143,7 +153,7 @@ class Code(BaseCommand):
|
|
|
143
153
|
|
|
144
154
|
return f"{prefix}{self.type}"
|
|
145
155
|
|
|
146
|
-
def keyword_to_str(self):
|
|
156
|
+
def keyword_to_str(self) -> Optional[str]:
|
|
147
157
|
"""Convert the keyword to a string"""
|
|
148
158
|
return {
|
|
149
159
|
KeywordType.If: "if",
|
|
@@ -159,5 +169,5 @@ class Code(BaseCommand):
|
|
|
159
169
|
KeywordType.Global: "global",
|
|
160
170
|
}.get(self.keyword)
|
|
161
171
|
|
|
162
|
-
def is_flag_set(self, flag: CodeFlags):
|
|
172
|
+
def is_flag_set(self, flag: CodeFlags) -> bool:
|
|
163
173
|
return self.flags & flag != 0
|
dsf/commands/code_channel.py
CHANGED
|
@@ -16,6 +16,8 @@ class CodeChannel(str, Enum):
|
|
|
16
16
|
# Code channel for USB requests
|
|
17
17
|
USB = "USB"
|
|
18
18
|
|
|
19
|
+
USB2 = "USB2"
|
|
20
|
+
|
|
19
21
|
# Code channel for serial devices (e.g. PanelDue)
|
|
20
22
|
Aux = "Aux"
|
|
21
23
|
|
|
@@ -53,4 +55,8 @@ class CodeChannel(str, Enum):
|
|
|
53
55
|
|
|
54
56
|
@staticmethod
|
|
55
57
|
def list():
|
|
56
|
-
return list(map(lambda cc: cc
|
|
58
|
+
return list(map(lambda cc: cc, CodeChannel))
|
|
59
|
+
|
|
60
|
+
def get_input_index(self) -> int:
|
|
61
|
+
"""Get the index of this code channel for use in client init messages"""
|
|
62
|
+
return self.list().index(self)
|
|
@@ -24,8 +24,4 @@ def resolve_code(rtype: MessageType, content: Optional[str]):
|
|
|
24
24
|
:param rtype: Type of the resolving message
|
|
25
25
|
:param content: Content of the resolving message
|
|
26
26
|
"""
|
|
27
|
-
if not isinstance(rtype, MessageType):
|
|
28
|
-
raise TypeError("rtype must be a MessageType")
|
|
29
|
-
if content is not None and not isinstance(content, str):
|
|
30
|
-
raise TypeError("content must be None or a string")
|
|
31
27
|
return BaseCommand("Resolve", **{"type": rtype, "content": content})
|
dsf/commands/code_parameter.py
CHANGED
|
@@ -2,17 +2,40 @@
|
|
|
2
2
|
codeparameter contains all classes and methods dealing with deserialized code parameters.
|
|
3
3
|
"""
|
|
4
4
|
import json
|
|
5
|
+
from typing import Self, TypeAlias, TypedDict, cast, Optional
|
|
5
6
|
|
|
6
7
|
from ..exceptions import CodeParserException
|
|
7
8
|
from ..object_model.move.driver_id import DriverId
|
|
8
9
|
|
|
9
10
|
|
|
11
|
+
CodeParameterScalar: TypeAlias = str | int | float | DriverId
|
|
12
|
+
CodeParameterArray: TypeAlias = list[int] | list[float] | list[DriverId]
|
|
13
|
+
CodeParameterValue: TypeAlias = CodeParameterScalar | CodeParameterArray
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CodeParameterJSON(TypedDict):
|
|
17
|
+
letter: str
|
|
18
|
+
value: object
|
|
19
|
+
isString: Optional[bool]
|
|
20
|
+
isDriverId: Optional[bool]
|
|
21
|
+
|
|
22
|
+
|
|
10
23
|
class CodeParameter(json.JSONEncoder):
|
|
11
24
|
"""Represents a parsed parameter of a G/M/T-code"""
|
|
12
25
|
|
|
13
26
|
LETTER_FOR_UNPRECEDENTED_STRING = "@"
|
|
14
|
-
|
|
15
|
-
|
|
27
|
+
letter: str
|
|
28
|
+
string_value: str
|
|
29
|
+
is_string: Optional[bool]
|
|
30
|
+
is_expression: bool
|
|
31
|
+
is_driver_id: bool
|
|
32
|
+
__parsed_value: object
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def value(self) -> object:
|
|
36
|
+
return self.__parsed_value
|
|
37
|
+
|
|
38
|
+
def default(self, o: Self) -> dict[str, object]:
|
|
16
39
|
return {
|
|
17
40
|
"letter": o.letter,
|
|
18
41
|
"value": o.value,
|
|
@@ -21,16 +44,22 @@ class CodeParameter(json.JSONEncoder):
|
|
|
21
44
|
}
|
|
22
45
|
|
|
23
46
|
@classmethod
|
|
24
|
-
def from_json(cls, data):
|
|
47
|
+
def from_json(cls, data: CodeParameterJSON) -> Self:
|
|
25
48
|
"""Instantiate a new instance of this class from JSON deserialized dictionary"""
|
|
26
49
|
return cls(**data)
|
|
27
50
|
|
|
28
51
|
@classmethod
|
|
29
|
-
def simple_param(cls, letter: str, value, isDriverId: bool = False):
|
|
52
|
+
def simple_param(cls, letter: str, value: object, isDriverId: bool = False) -> Self:
|
|
30
53
|
"""Create a new simple parameter without parsing the value"""
|
|
31
54
|
return cls(letter, value, isDriverId=isDriverId)
|
|
32
55
|
|
|
33
|
-
def __init__(
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
letter: str,
|
|
59
|
+
value: object,
|
|
60
|
+
isString: Optional[bool] = None,
|
|
61
|
+
isDriverId: Optional[bool] = None,
|
|
62
|
+
) -> None:
|
|
34
63
|
"""
|
|
35
64
|
Creates a new CodeParameter instance and parses value to a native data type
|
|
36
65
|
if applicable
|
|
@@ -45,7 +74,7 @@ class CodeParameter(json.JSONEncoder):
|
|
|
45
74
|
return
|
|
46
75
|
|
|
47
76
|
self.letter = letter
|
|
48
|
-
self.string_value = value
|
|
77
|
+
self.string_value = str(value)
|
|
49
78
|
self.is_string = isString
|
|
50
79
|
self.is_expression = False
|
|
51
80
|
self.is_driver_id = isDriverId if isDriverId is not None else False
|
|
@@ -53,10 +82,10 @@ class CodeParameter(json.JSONEncoder):
|
|
|
53
82
|
self.__parsed_value = value
|
|
54
83
|
return
|
|
55
84
|
elif self.is_driver_id:
|
|
56
|
-
drivers = [DriverId(as_str=
|
|
85
|
+
drivers = [DriverId(as_str=driver_value) for driver_value in self.string_value.split(":")]
|
|
57
86
|
self.__parsed_value = drivers[0] if len(drivers) == 1 else drivers
|
|
58
87
|
|
|
59
|
-
value =
|
|
88
|
+
value = self.string_value.strip()
|
|
60
89
|
# Empty parameters are represented as integers with the value 0 (e.g. G92 XY => G92 X0 Y0)
|
|
61
90
|
if not value:
|
|
62
91
|
self.__parsed_value = 0
|
|
@@ -81,7 +110,7 @@ class CodeParameter(json.JSONEncoder):
|
|
|
81
110
|
except: # noqa
|
|
82
111
|
self.__parsed_value = value
|
|
83
112
|
|
|
84
|
-
def convert_driver_ids(self):
|
|
113
|
+
def convert_driver_ids(self) -> None:
|
|
85
114
|
"""Convert this parameter to driver id(s)"""
|
|
86
115
|
if self.is_expression:
|
|
87
116
|
return
|
|
@@ -95,23 +124,9 @@ class CodeParameter(json.JSONEncoder):
|
|
|
95
124
|
else:
|
|
96
125
|
self.__parsed_value = drivers
|
|
97
126
|
|
|
98
|
-
drivers = []
|
|
99
|
-
parameters = self.string_value.split(":")
|
|
100
|
-
for value in parameters:
|
|
101
|
-
segments = value.split(".")
|
|
102
|
-
segment_count = len(segments)
|
|
103
|
-
if segment_count == 1:
|
|
104
|
-
drivers.append(int(segments[0]))
|
|
105
|
-
elif segment_count == 2:
|
|
106
|
-
driver = (int(segments[0]) << 16) & 0xFFFF
|
|
107
|
-
driver |= int(segments[1] & 0xFFFF)
|
|
108
|
-
else:
|
|
109
|
-
raise CodeParserException(f"Driver value from {self.letter} parameter is invalid")
|
|
110
|
-
|
|
111
|
-
self.__parsed_value = drivers[0] if len(drivers) == 1 else drivers
|
|
112
127
|
self.is_driver_id = True
|
|
113
128
|
|
|
114
|
-
def as_float(self):
|
|
129
|
+
def as_float(self) -> float:
|
|
115
130
|
"""Conversion to float"""
|
|
116
131
|
if isinstance(self.__parsed_value, float):
|
|
117
132
|
return self.__parsed_value
|
|
@@ -120,16 +135,16 @@ class CodeParameter(json.JSONEncoder):
|
|
|
120
135
|
|
|
121
136
|
raise Exception(f"Cannot convert {self.letter} parameter to float (value {self.string_value})")
|
|
122
137
|
|
|
123
|
-
def as_int(self):
|
|
138
|
+
def as_int(self) -> int:
|
|
124
139
|
"""Conversion to int"""
|
|
125
140
|
if isinstance(self.__parsed_value, int):
|
|
126
141
|
return self.__parsed_value
|
|
127
142
|
if isinstance(self.__parsed_value, DriverId):
|
|
128
|
-
return self.__parsed_value.as_int()
|
|
143
|
+
return int(self.__parsed_value.as_int())
|
|
129
144
|
|
|
130
145
|
raise Exception(f"Cannot convert {self.letter} parameter to int (value {self.string_value})")
|
|
131
146
|
|
|
132
|
-
def as_driver_id(self):
|
|
147
|
+
def as_driver_id(self) -> DriverId:
|
|
133
148
|
if isinstance(self.__parsed_value, DriverId):
|
|
134
149
|
return self.__parsed_value
|
|
135
150
|
if isinstance(self.__parsed_value, int):
|
|
@@ -139,67 +154,96 @@ class CodeParameter(json.JSONEncoder):
|
|
|
139
154
|
pass
|
|
140
155
|
raise Exception(f"Cannot convert {self.letter} parameter to DriverId (value {self.string_value})")
|
|
141
156
|
|
|
142
|
-
def
|
|
143
|
-
"""
|
|
157
|
+
def _parse_expression_array(self) -> list[float]:
|
|
158
|
+
"""Parse expression-based arrays like {0, 255, 128} into a list of floats."""
|
|
159
|
+
if not self.is_expression:
|
|
160
|
+
raise Exception(
|
|
161
|
+
f"Cannot parse expression array: {self.letter} is not an expression (value {self.string_value})"
|
|
162
|
+
)
|
|
163
|
+
# Strip braces and split by comma
|
|
164
|
+
content = self.string_value[1:-1].strip() # Remove { }
|
|
165
|
+
if not content:
|
|
166
|
+
return []
|
|
167
|
+
elements = [elem.strip() for elem in content.split(",")]
|
|
144
168
|
try:
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
169
|
+
# Try to parse as floats first, then convert to ints if needed
|
|
170
|
+
return [float(elem) for elem in elements if elem]
|
|
171
|
+
except ValueError as e:
|
|
172
|
+
raise Exception(
|
|
173
|
+
f"Cannot parse expression array: failed to convert elements to numbers in {self.letter} (value {self.string_value})"
|
|
174
|
+
) from e
|
|
175
|
+
|
|
176
|
+
def as_float_array(self) -> list[float]:
|
|
177
|
+
"""Conversion to float array. Supports colon-separated (C0.5:1.0) and expression formats (C{0.5, 1.0})."""
|
|
178
|
+
try:
|
|
179
|
+
parsed_value: object = self.__parsed_value
|
|
180
|
+
if isinstance(parsed_value, list):
|
|
181
|
+
values = cast(list[int | float], parsed_value)
|
|
182
|
+
return [float(value) for value in values]
|
|
183
|
+
if isinstance(parsed_value, float):
|
|
184
|
+
return [parsed_value]
|
|
185
|
+
if isinstance(parsed_value, int):
|
|
186
|
+
return [float(parsed_value)]
|
|
187
|
+
if self.is_expression:
|
|
188
|
+
return self._parse_expression_array()
|
|
151
189
|
except: # noqa
|
|
152
190
|
pass
|
|
153
191
|
raise Exception(f"Cannot convert {self.letter} parameter to float array (value {self.string_value})")
|
|
154
192
|
|
|
155
|
-
def as_int_array(self):
|
|
156
|
-
"""Conversion to int array"""
|
|
193
|
+
def as_int_array(self) -> list[int]:
|
|
194
|
+
"""Conversion to int array. Supports colon-separated (C0:255:128) and expression formats (C{0, 255, 128})."""
|
|
157
195
|
try:
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
196
|
+
parsed_value: object = self.__parsed_value
|
|
197
|
+
if isinstance(parsed_value, list):
|
|
198
|
+
if isinstance(parsed_value[0], DriverId):
|
|
199
|
+
values = cast(list[DriverId], parsed_value)
|
|
200
|
+
return [int(value.as_int()) for value in values]
|
|
201
|
+
values = cast(list[int] | list[float], parsed_value)
|
|
202
|
+
return [int(value) for value in values]
|
|
203
|
+
if isinstance(parsed_value, int):
|
|
204
|
+
return [parsed_value]
|
|
205
|
+
if isinstance(parsed_value, DriverId):
|
|
206
|
+
return [int(parsed_value.as_int())]
|
|
207
|
+
if self.is_expression:
|
|
208
|
+
float_array = self._parse_expression_array()
|
|
209
|
+
return [int(value) for value in float_array]
|
|
166
210
|
except: # noqa
|
|
167
211
|
pass
|
|
168
|
-
raise Exception(f"Cannot convert {self.letter} parameter to
|
|
212
|
+
raise Exception(f"Cannot convert {self.letter} parameter to int array (value {self.string_value})")
|
|
169
213
|
|
|
170
|
-
def as_driver_id_array(self):
|
|
214
|
+
def as_driver_id_array(self) -> list[DriverId]:
|
|
171
215
|
try:
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if isinstance(
|
|
180
|
-
return [
|
|
216
|
+
parsed_value: object = self.__parsed_value
|
|
217
|
+
if isinstance(parsed_value, list):
|
|
218
|
+
if isinstance(parsed_value[0], DriverId):
|
|
219
|
+
return cast(list[DriverId], parsed_value)
|
|
220
|
+
if isinstance(parsed_value[0], int):
|
|
221
|
+
values = cast(list[int], parsed_value)
|
|
222
|
+
return [DriverId(as_int=value) for value in values]
|
|
223
|
+
if isinstance(parsed_value, DriverId):
|
|
224
|
+
return [parsed_value]
|
|
225
|
+
if isinstance(parsed_value, int):
|
|
226
|
+
return [DriverId(as_int=parsed_value)]
|
|
181
227
|
except: # noqa
|
|
182
228
|
pass
|
|
183
229
|
raise Exception(f"Cannot convert {self.letter} parameter to DriverId array (value {self.string_value})")
|
|
184
230
|
|
|
185
|
-
def as_bool(self):
|
|
231
|
+
def as_bool(self) -> bool:
|
|
186
232
|
"""Conversion to bool"""
|
|
187
233
|
try:
|
|
188
234
|
return float(self.string_value) > 0
|
|
189
235
|
except: # noqa
|
|
190
236
|
return False
|
|
191
237
|
|
|
192
|
-
def __eq__(self, other):
|
|
193
|
-
if self is None:
|
|
194
|
-
return other is None
|
|
238
|
+
def __eq__(self, other: object) -> bool:
|
|
195
239
|
if isinstance(other, CodeParameter):
|
|
196
240
|
return self.letter == other.letter and self.__parsed_value == other.__parsed_value
|
|
197
241
|
return self.__parsed_value == other
|
|
198
242
|
|
|
199
|
-
def __ne__(self, other):
|
|
243
|
+
def __ne__(self, other: object) -> bool:
|
|
200
244
|
return not self == other
|
|
201
245
|
|
|
202
|
-
def __str__(self):
|
|
246
|
+
def __str__(self) -> str:
|
|
203
247
|
letter = self.letter if not self.letter == CodeParameter.LETTER_FOR_UNPRECEDENTED_STRING else ""
|
|
204
248
|
if self.is_string and not self.is_expression:
|
|
205
249
|
double_quoted = self.string_value.replace('"', '""')
|
dsf/commands/files.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
1
3
|
from .base_command import BaseCommand
|
|
2
4
|
|
|
3
5
|
|
|
@@ -7,18 +9,18 @@ def get_file_info(file_name: str, read_thumbnail_content: bool = False):
|
|
|
7
9
|
:param file_name: The filename to extract information from
|
|
8
10
|
:param read_thumbnail_content: Whether thumbnail content shall be returned
|
|
9
11
|
"""
|
|
10
|
-
if not
|
|
11
|
-
raise
|
|
12
|
+
if not file_name:
|
|
13
|
+
raise ValueError("file_name must not be empty")
|
|
12
14
|
return BaseCommand("GetFileInfo", **{"fileName": file_name, "readThumbnailContent": read_thumbnail_content})
|
|
13
15
|
|
|
14
16
|
|
|
15
|
-
def resolve_path(path: str, base_directory: str = None):
|
|
17
|
+
def resolve_path(path: str, base_directory: Optional[str] = None):
|
|
16
18
|
"""
|
|
17
19
|
Resolve a RepRapFirmware-style path to an actual file path
|
|
18
20
|
:param path: Path that is RepRapFirmware-compatible
|
|
19
21
|
:param base_directory: Optional base directory to resolve the path relative to
|
|
20
22
|
:returns: The resolved path
|
|
21
23
|
"""
|
|
22
|
-
if not
|
|
23
|
-
raise
|
|
24
|
+
if not path:
|
|
25
|
+
raise ValueError("path must not be empty")
|
|
24
26
|
return BaseCommand("ResolvePath", **{"path": path, "baseDirectory": base_directory})
|
dsf/commands/generic.py
CHANGED
|
@@ -15,8 +15,8 @@ def check_password(password: str):
|
|
|
15
15
|
|
|
16
16
|
:returns: true if the password matches or is not set
|
|
17
17
|
"""
|
|
18
|
-
if not
|
|
19
|
-
raise
|
|
18
|
+
if not password:
|
|
19
|
+
raise ValueError("password must not be empty")
|
|
20
20
|
return BaseCommand("CheckPassword", **{"password": password})
|
|
21
21
|
|
|
22
22
|
|
|
@@ -29,10 +29,8 @@ def evaluate_expression(channel: CodeChannel, expression: str):
|
|
|
29
29
|
:param channel: Code channel where the expression is evaluated
|
|
30
30
|
:param expression: Expression to evaluate
|
|
31
31
|
"""
|
|
32
|
-
if not
|
|
33
|
-
raise
|
|
34
|
-
if not isinstance(expression, str) or not expression:
|
|
35
|
-
raise TypeError("expression must be a string")
|
|
32
|
+
if not expression:
|
|
33
|
+
raise ValueError("expression must not be empty")
|
|
36
34
|
return BaseCommand("EvaluateExpression", **{"channel": channel, "expression": expression})
|
|
37
35
|
|
|
38
36
|
|
|
@@ -50,12 +48,6 @@ def flush(channel: CodeChannel, sync_file_streams: bool = False, if_executing: b
|
|
|
50
48
|
|
|
51
49
|
:returns: true if the flush request is successful
|
|
52
50
|
"""
|
|
53
|
-
if not isinstance(channel, CodeChannel):
|
|
54
|
-
raise TypeError("channel must be a CodeChannel")
|
|
55
|
-
if not isinstance(sync_file_streams, bool):
|
|
56
|
-
raise TypeError("sync_file_streams must be a boolean")
|
|
57
|
-
if not isinstance(if_executing, bool):
|
|
58
|
-
raise TypeError("if_executing must be a boolean")
|
|
59
51
|
return BaseCommand("Flush",
|
|
60
52
|
**{"channel": channel, "syncFileStreams": sync_file_streams, "ifExecuting": if_executing})
|
|
61
53
|
|
|
@@ -68,8 +60,6 @@ def invalidate_channel(channel: CodeChannel):
|
|
|
68
60
|
|
|
69
61
|
:returns: true if the invalidate request is successful
|
|
70
62
|
"""
|
|
71
|
-
if not isinstance(channel, CodeChannel):
|
|
72
|
-
raise TypeError("channel must be a CodeChannel")
|
|
73
63
|
return BaseCommand("InvalidateChannel", **{"channel": channel})
|
|
74
64
|
|
|
75
65
|
|
|
@@ -79,8 +69,6 @@ def set_update_status(updating: bool):
|
|
|
79
69
|
|
|
80
70
|
:param updating: Whether an update is now in progress
|
|
81
71
|
"""
|
|
82
|
-
if not isinstance(updating, bool):
|
|
83
|
-
raise TypeError("updating must be a boolean")
|
|
84
72
|
return BaseCommand("SetUpdateStatus", **{"updating": updating})
|
|
85
73
|
|
|
86
74
|
|
|
@@ -97,10 +85,6 @@ def simple_code(code: str, channel: CodeChannel = CodeChannel.DEFAULT_CHANNEL, a
|
|
|
97
85
|
:param async_exec: Whether this code may be executed asynchronously.
|
|
98
86
|
If set, the code reply is output as a generic message
|
|
99
87
|
"""
|
|
100
|
-
if not isinstance(code, str) or not code:
|
|
101
|
-
raise TypeError("code must be a string")
|
|
102
|
-
if not isinstance(channel, CodeChannel):
|
|
103
|
-
raise TypeError("channel must be a CodeChannel")
|
|
104
88
|
return BaseCommand("SimpleCode", **{"code": code, "channel": channel, "executeAsynchronously": async_exec})
|
|
105
89
|
|
|
106
90
|
|
|
@@ -118,14 +102,6 @@ def write_message(
|
|
|
118
102
|
:param output_message: Output the message on the console and via the object model
|
|
119
103
|
:param log_level: Log level of this message
|
|
120
104
|
"""
|
|
121
|
-
if not isinstance(message_type, MessageType):
|
|
122
|
-
raise TypeError("rtype must be a MessageType")
|
|
123
|
-
if not isinstance(content, str):
|
|
124
|
-
raise TypeError("content must be a string")
|
|
125
|
-
if not isinstance(output_message, bool):
|
|
126
|
-
raise TypeError("output_message must be a boolean")
|
|
127
|
-
if log_level is not None and not isinstance(log_level, LogLevel):
|
|
128
|
-
raise TypeError("log_message must be a LogLevel")
|
|
129
105
|
return BaseCommand(
|
|
130
106
|
"WriteMessage",
|
|
131
107
|
**{
|
dsf/commands/http_endpoints.py
CHANGED
|
@@ -14,14 +14,6 @@ def add_http_endpoint(endpoint_type: HttpEndpointType, namespace: str, path: str
|
|
|
14
14
|
to whenever a matching HTTP request is received.
|
|
15
15
|
A plugin using this command has to open a new UNIX socket with the given path that DuetWebServer can connect to
|
|
16
16
|
"""
|
|
17
|
-
if not isinstance(endpoint_type, HttpEndpointType):
|
|
18
|
-
raise TypeError("endpoint_type must be a HttpEndpointType")
|
|
19
|
-
if not isinstance(namespace, str) or not namespace:
|
|
20
|
-
raise TypeError("namespace must be a string")
|
|
21
|
-
if not isinstance(path, str) or not path:
|
|
22
|
-
raise TypeError("path must be a string")
|
|
23
|
-
if not isinstance(is_upload_request, bool):
|
|
24
|
-
raise TypeError("is_upload_request must be a boolean")
|
|
25
17
|
return BaseCommand(
|
|
26
18
|
"AddHttpEndpoint",
|
|
27
19
|
**{
|
|
@@ -41,12 +33,6 @@ def remove_http_endpoint(endpoint_type: HttpEndpointType, namespace: str, path:
|
|
|
41
33
|
:param path: Path to the endpoint to unregister
|
|
42
34
|
:returns: true if the endpoint could be successfully removed
|
|
43
35
|
"""
|
|
44
|
-
if not isinstance(endpoint_type, HttpEndpointType):
|
|
45
|
-
raise TypeError("endpoint_type must be a HttpEndpointType")
|
|
46
|
-
if not isinstance(namespace, str) or not namespace:
|
|
47
|
-
raise TypeError("namespace must be a string")
|
|
48
|
-
if not isinstance(path, str) or not path:
|
|
49
|
-
raise TypeError("path must be a string")
|
|
50
36
|
return BaseCommand(
|
|
51
37
|
"RemoveHttpEndpoint",
|
|
52
38
|
**{"endpointType": endpoint_type, "namespace": namespace, "path": path},
|
dsf/commands/object_model.py
CHANGED
|
@@ -20,10 +20,6 @@ def patch_object_model(key: str, patch: str):
|
|
|
20
20
|
:param key: Key to update
|
|
21
21
|
:param patch: JSON patch to apply
|
|
22
22
|
"""
|
|
23
|
-
if not isinstance(key, str) or not key:
|
|
24
|
-
raise TypeError("key must be a string")
|
|
25
|
-
if not isinstance(patch, str) or not patch:
|
|
26
|
-
raise TypeError("patch must be a string")
|
|
27
23
|
return BaseCommand("PatchObjectModel", **{"key": key, "patch": patch})
|
|
28
24
|
|
|
29
25
|
|
|
@@ -34,10 +30,6 @@ def set_network_protocol(protocol: str, enabled: bool):
|
|
|
34
30
|
:param enabled: Whether the protocol is enabled or not
|
|
35
31
|
:returns: true if the protocol could be flagged
|
|
36
32
|
"""
|
|
37
|
-
if not isinstance(protocol, str) or not protocol:
|
|
38
|
-
raise TypeError("protocol must be a string")
|
|
39
|
-
if not isinstance(enabled, bool):
|
|
40
|
-
raise TypeError("enabled must be a boolean")
|
|
41
33
|
return BaseCommand("SetNetworkProtocol", **{"networkProtocol": protocol, "enabled": enabled})
|
|
42
34
|
|
|
43
35
|
|
|
@@ -49,10 +41,6 @@ def set_object_model(property_path: str, value: str):
|
|
|
49
41
|
:param value: String representation of the value to set
|
|
50
42
|
:returns: true if the field could be updated
|
|
51
43
|
"""
|
|
52
|
-
if not isinstance(property_path, str) or not property_path:
|
|
53
|
-
raise TypeError("property_path must be a string")
|
|
54
|
-
if not isinstance(value, str):
|
|
55
|
-
raise TypeError("value must be a string")
|
|
56
44
|
return BaseCommand("SetObjectModel", **{"propertyPath": property_path, "value": value})
|
|
57
45
|
|
|
58
46
|
|