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.
Files changed (140) hide show
  1. dsf/__init__.py +2 -2
  2. dsf/commands/base_command.py +4 -2
  3. dsf/commands/code.py +34 -24
  4. dsf/commands/code_channel.py +7 -1
  5. dsf/commands/code_interception.py +0 -4
  6. dsf/commands/code_parameter.py +106 -62
  7. dsf/commands/files.py +7 -5
  8. dsf/commands/generic.py +4 -28
  9. dsf/commands/http_endpoints.py +0 -14
  10. dsf/commands/object_model.py +0 -12
  11. dsf/commands/packages.py +4 -4
  12. dsf/commands/plugins.py +15 -15
  13. dsf/commands/responses.py +27 -14
  14. dsf/commands/user_sessions.py +2 -8
  15. dsf/connections/base_command_connection.py +11 -4
  16. dsf/connections/base_connection.py +27 -8
  17. dsf/connections/command_connection.py +2 -2
  18. dsf/connections/exceptions.py +3 -1
  19. dsf/connections/init_messages/client_init_messages.py +19 -7
  20. dsf/connections/init_messages/server_init_message.py +9 -3
  21. dsf/connections/intercept_connection.py +3 -3
  22. dsf/connections/subscribe_connection.py +166 -12
  23. dsf/http.py +33 -16
  24. dsf/object_model/boards/accelerometer.py +11 -34
  25. dsf/object_model/boards/board_closed_loop.py +7 -21
  26. dsf/object_model/boards/boards.py +66 -244
  27. dsf/object_model/boards/direct_display/direct_display.py +6 -11
  28. dsf/object_model/boards/direct_display/direct_display_encoder.py +4 -11
  29. dsf/object_model/boards/direct_display/direct_display_screen.py +26 -73
  30. dsf/object_model/boards/direct_display/direct_display_screen_st7567.py +5 -22
  31. dsf/object_model/boards/driver.py +9 -18
  32. dsf/object_model/boards/driver_closed_loop.py +20 -56
  33. dsf/object_model/boards/driver_config.py +16 -0
  34. dsf/object_model/boards/driver_mode.py +23 -0
  35. dsf/object_model/boards/min_max_current.py +11 -34
  36. dsf/object_model/directories/directories.py +19 -73
  37. dsf/object_model/fans/fan_thermostatic_control.py +10 -38
  38. dsf/object_model/fans/fans.py +23 -99
  39. dsf/object_model/heat/heat.py +25 -46
  40. dsf/object_model/heat/heater.py +47 -167
  41. dsf/object_model/heat/heater_model.py +23 -98
  42. dsf/object_model/heat/heater_model_pid.py +11 -53
  43. dsf/object_model/heat/heater_monitor.py +13 -57
  44. dsf/object_model/inputs/input_channel.py +54 -201
  45. dsf/object_model/inputs/input_channel_state.py +3 -0
  46. dsf/object_model/inputs/inputs.py +1 -1
  47. dsf/object_model/job/build.py +14 -44
  48. dsf/object_model/job/build_object.py +15 -37
  49. dsf/object_model/job/gcode_fileinfo.py +38 -113
  50. dsf/object_model/job/job.py +33 -170
  51. dsf/object_model/job/layer.py +18 -48
  52. dsf/object_model/job/thumbnail_info.py +15 -70
  53. dsf/object_model/job/times_left.py +13 -44
  54. dsf/object_model/led_strips/led_strip.py +24 -54
  55. dsf/object_model/led_strips/led_strip_color_order.py +22 -0
  56. dsf/object_model/limits/limits.py +58 -272
  57. dsf/object_model/messages/messages.py +12 -45
  58. dsf/object_model/model_collection.py +114 -24
  59. dsf/object_model/model_dictionary.py +22 -7
  60. dsf/object_model/model_object.py +27 -19
  61. dsf/object_model/model_type.py +21 -0
  62. dsf/object_model/move/axis.py +78 -251
  63. dsf/object_model/move/current_move.py +27 -61
  64. dsf/object_model/move/driver_id.py +10 -4
  65. dsf/object_model/move/extruder.py +54 -174
  66. dsf/object_model/move/extruder_non_linear.py +8 -30
  67. dsf/object_model/move/extruder_pressure_advance.py +18 -0
  68. dsf/object_model/move/input_shaping.py +26 -53
  69. dsf/object_model/move/keepout_zone.py +13 -40
  70. dsf/object_model/move/kinematics/core_kinematics.py +7 -13
  71. dsf/object_model/move/kinematics/delta_kinematics.py +21 -63
  72. dsf/object_model/move/kinematics/delta_tower.py +17 -50
  73. dsf/object_model/move/kinematics/hangprinter_kinematics.py +9 -20
  74. dsf/object_model/move/kinematics/kinematics.py +14 -30
  75. dsf/object_model/move/kinematics/kinematics_name.py +8 -8
  76. dsf/object_model/move/kinematics/polar_kinematics.py +16 -0
  77. dsf/object_model/move/kinematics/scara_kinematics.py +26 -0
  78. dsf/object_model/move/kinematics/tilt_correction.py +21 -54
  79. dsf/object_model/move/kinematics/zleadscrew_kinematics.py +5 -7
  80. dsf/object_model/move/microstepping.py +7 -21
  81. dsf/object_model/move/motion_system.py +52 -0
  82. dsf/object_model/move/motors_idle_control.py +7 -21
  83. dsf/object_model/move/move.py +75 -168
  84. dsf/object_model/move/move_calibration.py +9 -23
  85. dsf/object_model/move/move_compensation.py +18 -60
  86. dsf/object_model/move/move_deviations.py +7 -21
  87. dsf/object_model/move/move_queue_item.py +7 -21
  88. dsf/object_model/move/move_rotation.py +8 -19
  89. dsf/object_model/move/move_segmentation.py +5 -20
  90. dsf/object_model/move/probe_grid.py +18 -41
  91. dsf/object_model/move/skew.py +11 -42
  92. dsf/object_model/network/network.py +13 -42
  93. dsf/object_model/network/network_interface.py +32 -177
  94. dsf/object_model/network/network_interface_type.py +1 -1
  95. dsf/object_model/network/network_state.py +3 -0
  96. dsf/object_model/object_model.py +23 -126
  97. dsf/object_model/plugins/plugin_manifest.py +53 -219
  98. dsf/object_model/plugins/plugins.py +17 -30
  99. dsf/object_model/plugins/sbc_permissions.py +2 -2
  100. dsf/object_model/sbc/cpu.py +6 -41
  101. dsf/object_model/sbc/dsf/communication_method.py +11 -0
  102. dsf/object_model/sbc/dsf/dsf.py +13 -64
  103. dsf/object_model/sbc/dsf/http_endpoint.py +14 -59
  104. dsf/object_model/sbc/dsf/http_endpoint_type.py +2 -2
  105. dsf/object_model/sbc/dsf/user_sessions/user_sessions.py +17 -65
  106. dsf/object_model/sbc/memory.py +4 -22
  107. dsf/object_model/sbc/sbc.py +12 -88
  108. dsf/object_model/sensors/analog_sensor.py +15 -142
  109. dsf/object_model/sensors/analog_sensor_type.py +22 -4
  110. dsf/object_model/sensors/endstop.py +11 -45
  111. dsf/object_model/sensors/filament_monitors/Duet3DFilamentMonitor.py +17 -66
  112. dsf/object_model/sensors/filament_monitors/filament_monitor.py +11 -54
  113. dsf/object_model/sensors/filament_monitors/laser_filament_monitor.py +30 -110
  114. dsf/object_model/sensors/filament_monitors/pulsed_filament_monitor.py +29 -97
  115. dsf/object_model/sensors/filament_monitors/rotating_magnet_filament_monitor.py +29 -97
  116. dsf/object_model/sensors/gp_input_port.py +4 -10
  117. dsf/object_model/sensors/probe.py +24 -225
  118. dsf/object_model/sensors/probe_touch_mode.py +14 -0
  119. dsf/object_model/sensors/sensors.py +17 -31
  120. dsf/object_model/sensors/temperature_error.py +12 -0
  121. dsf/object_model/spindles/spindle_type.py +0 -3
  122. dsf/object_model/spindles/spindles.py +28 -131
  123. dsf/object_model/state/beep_request.py +5 -20
  124. dsf/object_model/state/gp_output_port.py +7 -22
  125. dsf/object_model/state/message_box.py +15 -131
  126. dsf/object_model/state/restore_point.py +12 -74
  127. dsf/object_model/state/startup_error.py +9 -32
  128. dsf/object_model/state/state.py +27 -241
  129. dsf/object_model/tools/tool_retraction.py +17 -56
  130. dsf/object_model/tools/tools.py +23 -195
  131. dsf/object_model/utils.py +131 -26
  132. dsf/object_model/volumes/volumes.py +20 -85
  133. dsf/utils.py +87 -10
  134. dsf_python-3.7.0b1.dist-info/METADATA +303 -0
  135. dsf_python-3.7.0b1.dist-info/RECORD +188 -0
  136. {dsf_python-3.6.0rc3.dist-info → dsf_python-3.7.0b1.dist-info}/WHEEL +1 -1
  137. dsf_python-3.6.0rc3.dist-info/METADATA +0 -60
  138. dsf_python-3.6.0rc3.dist-info/RECORD +0 -180
  139. {dsf_python-3.6.0rc3.dist-info → dsf_python-3.7.0b1.dist-info}/licenses/LICENSE +0 -0
  140. {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.6.0-rc.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"
@@ -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 List, Optional
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
- data["result"] = None if data["result"] is None else list(map(Message.from_json, data["result"]))
19
- data["parameters"] = list(map(CodeParameter.from_json, data["parameters"]))
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[List[Message]] = None
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: List[CodeParameter] = []
83
+ self.parameters: list[CodeParameter] = []
75
84
 
76
- super().__init__(**kwargs)
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"{self.keyword_to_str()} {self.keywordArgument}"
120
+ return f"{keyword} {self.keywordArgument}"
111
121
  else:
112
- return self.keyword_to_str()
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
@@ -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.value, CodeChannel))
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})
@@ -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
- def default(self, o):
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__(self, letter: str, value, isString: bool = None, isDriverId: bool = None):
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=value) for value in self.string_value.split(":")]
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 = value.strip()
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 as_float_array(self):
143
- """Conversion to float array"""
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
- if isinstance(self.__parsed_value, list):
146
- return list(map(float, self.__parsed_value))
147
- if isinstance(self.__parsed_value, float):
148
- return [self.__parsed_value]
149
- if isinstance(self.__parsed_value, int):
150
- return [float(self.__parsed_value)]
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
- if isinstance(self.__parsed_value, list):
159
- if isinstance(self.__parsed_value[0], DriverId):
160
- return [d.as_int() for d in self.__parsed_value]
161
- return list(map(int, self.__parsed_value))
162
- if isinstance(self.__parsed_value, int):
163
- return [self.__parsed_value]
164
- if isinstance(self.__parsed_value, DriverId):
165
- return [self.__parsed_value.as_int()]
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 float array (value {self.string_value})")
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
- if isinstance(self.__parsed_value, list):
173
- if isinstance(self.__parsed_value[0], DriverId):
174
- return self.__parsed_value
175
- if isinstance(self.__parsed_value[0], int):
176
- return list(map(DriverId, self.__parsed_value))
177
- if isinstance(self.__parsed_value, DriverId):
178
- return [self.__parsed_value]
179
- if isinstance(self.__parsed_value, int):
180
- return [DriverId(as_int=self.__parsed_value)]
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 isinstance(file_name, str) or not file_name:
11
- raise TypeError("file_name must be a string")
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 isinstance(path, str) or not path:
23
- raise TypeError("path must be a string")
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 isinstance(password, str) or not password:
19
- raise TypeError("password must be a string")
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 isinstance(channel, CodeChannel):
33
- raise TypeError("channel must be a CodeChannel")
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
  **{
@@ -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},
@@ -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