dsf-python 3.5.1rc1__py3-none-any.whl → 3.6.0__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 (36) hide show
  1. dsf/__init__.py +18 -1
  2. dsf/commands/code.py +4 -1
  3. dsf/commands/code_interception.py +1 -1
  4. dsf/commands/files.py +4 -3
  5. dsf/commands/generic.py +10 -10
  6. dsf/commands/http_endpoints.py +5 -5
  7. dsf/commands/object_model.py +3 -3
  8. dsf/commands/packages.py +2 -2
  9. dsf/commands/plugins.py +7 -9
  10. dsf/commands/user_sessions.py +4 -4
  11. dsf/connections/base_connection.py +7 -1
  12. dsf/connections/init_messages/client_init_messages.py +9 -9
  13. dsf/connections/intercept_connection.py +2 -1
  14. dsf/http.py +51 -10
  15. dsf/object_model/heat/heater.py +23 -1
  16. dsf/object_model/job/gcode_fileinfo.py +6 -0
  17. dsf/object_model/job/times_left.py +11 -0
  18. dsf/object_model/model_collection.py +15 -6
  19. dsf/object_model/move/axis.py +11 -0
  20. dsf/object_model/move/extruder.py +11 -0
  21. dsf/object_model/move/input_shaping.py +7 -20
  22. dsf/object_model/network/network_interface.py +14 -1
  23. dsf/object_model/sensors/analog_sensor.py +20 -0
  24. dsf/object_model/sensors/analog_sensor_type.py +3 -3
  25. dsf/object_model/sensors/filament_monitors/Duet3DFilamentMonitor.py +11 -0
  26. dsf/object_model/sensors/filament_monitors/pulsed_filament_monitor.py +10 -0
  27. dsf/object_model/sensors/probe_type.py +3 -0
  28. dsf/object_model/spindles/spindle_type.py +14 -0
  29. dsf/object_model/spindles/spindles.py +48 -27
  30. dsf/object_model/tools/tools.py +24 -0
  31. dsf/object_model/utils.py +8 -3
  32. {dsf_python-3.5.1rc1.dist-info → dsf_python-3.6.0.dist-info}/METADATA +18 -5
  33. {dsf_python-3.5.1rc1.dist-info → dsf_python-3.6.0.dist-info}/RECORD +36 -35
  34. {dsf_python-3.5.1rc1.dist-info → dsf_python-3.6.0.dist-info}/WHEEL +1 -1
  35. {dsf_python-3.5.1rc1.dist-info → dsf_python-3.6.0.dist-info/licenses}/LICENSE +0 -0
  36. {dsf_python-3.5.1rc1.dist-info → dsf_python-3.6.0.dist-info}/top_level.txt +0 -0
dsf/__init__.py CHANGED
@@ -1,6 +1,23 @@
1
- # path to unix socket file
1
+ __version__ = "3.6.0"
2
+
3
+ import json
4
+ import os
5
+
6
+ # Default socket file path
2
7
  SOCKET_FILE = "/run/dsf/dcs.sock"
3
8
 
9
+ # Try to read socket file path from config
10
+ config_path = "/opt/dsf/conf/config.json"
11
+ if os.path.exists(config_path):
12
+ try:
13
+ with open(config_path, 'r') as f:
14
+ config = json.load(f)
15
+ socket_dir = config.get("SocketDirectory", "/run/dsf")
16
+ socket_file = config.get("SocketFile", "dcs.sock")
17
+ SOCKET_FILE = os.path.join(socket_dir, socket_file)
18
+ except (json.JSONDecodeError, IOError):
19
+ pass # Use default if config file is invalid or inaccessible
20
+
4
21
  # allowed connection per unix server
5
22
  DEFAULT_BACKLOG = 4
6
23
 
dsf/commands/code.py CHANGED
@@ -134,7 +134,7 @@ class Code(BaseCommand):
134
134
  if self.type == CodeType.Comment:
135
135
  return "(comment)"
136
136
 
137
- prefix = "G53 " if self.flags & CodeFlags.EnforceAbsolutePosition != 0 else ""
137
+ prefix = "G53 " if self.is_flag_set(CodeFlags.EnforceAbsolutePosition) else ""
138
138
  if self.majorNumber is not None:
139
139
  if self.minorNumber is not None:
140
140
  return f"{prefix}{self.type}{self.majorNumber}.{self.minorNumber}"
@@ -158,3 +158,6 @@ class Code(BaseCommand):
158
158
  KeywordType.Echo: "echo",
159
159
  KeywordType.Global: "global",
160
160
  }.get(self.keyword)
161
+
162
+ def is_flag_set(self, flag: CodeFlags):
163
+ return self.flags & flag != 0
@@ -28,4 +28,4 @@ def resolve_code(rtype: MessageType, content: Optional[str]):
28
28
  raise TypeError("rtype must be a MessageType")
29
29
  if content is not None and not isinstance(content, str):
30
30
  raise TypeError("content must be None or a string")
31
- return BaseCommand("Resolve", **{"Type": rtype, "Content": content})
31
+ return BaseCommand("Resolve", **{"type": rtype, "content": content})
dsf/commands/files.py CHANGED
@@ -9,15 +9,16 @@ def get_file_info(file_name: str, read_thumbnail_content: bool = False):
9
9
  """
10
10
  if not isinstance(file_name, str) or not file_name:
11
11
  raise TypeError("file_name must be a string")
12
- return BaseCommand("GetFileInfo", **{"FileName": file_name, "ReadThumbnailContent": read_thumbnail_content})
12
+ return BaseCommand("GetFileInfo", **{"fileName": file_name, "readThumbnailContent": read_thumbnail_content})
13
13
 
14
14
 
15
- def resolve_path(path: str):
15
+ def resolve_path(path: str, base_directory: str = None):
16
16
  """
17
17
  Resolve a RepRapFirmware-style path to an actual file path
18
18
  :param path: Path that is RepRapFirmware-compatible
19
+ :param base_directory: Optional base directory to resolve the path relative to
19
20
  :returns: The resolved path
20
21
  """
21
22
  if not isinstance(path, str) or not path:
22
23
  raise TypeError("path must be a string")
23
- return BaseCommand("ResolvePath", **{"Path": path})
24
+ return BaseCommand("ResolvePath", **{"path": path, "baseDirectory": base_directory})
dsf/commands/generic.py CHANGED
@@ -17,7 +17,7 @@ def check_password(password: str):
17
17
  """
18
18
  if not isinstance(password, str) or not password:
19
19
  raise TypeError("password must be a string")
20
- return BaseCommand("CheckPassword", **{"Password": password})
20
+ return BaseCommand("CheckPassword", **{"password": password})
21
21
 
22
22
 
23
23
  def evaluate_expression(channel: CodeChannel, expression: str):
@@ -33,7 +33,7 @@ def evaluate_expression(channel: CodeChannel, expression: str):
33
33
  raise TypeError("channel must be a CodeChannel")
34
34
  if not isinstance(expression, str) or not expression:
35
35
  raise TypeError("expression must be a string")
36
- return BaseCommand("EvaluateExpression", **{"Channel": channel, "Expression": expression})
36
+ return BaseCommand("EvaluateExpression", **{"channel": channel, "expression": expression})
37
37
 
38
38
 
39
39
  def flush(channel: CodeChannel, sync_file_streams: bool = False, if_executing: bool = True):
@@ -57,7 +57,7 @@ def flush(channel: CodeChannel, sync_file_streams: bool = False, if_executing: b
57
57
  if not isinstance(if_executing, bool):
58
58
  raise TypeError("if_executing must be a boolean")
59
59
  return BaseCommand("Flush",
60
- **{"Channel": channel, "SyncFileStreams": sync_file_streams, "IfExecuting": if_executing})
60
+ **{"channel": channel, "syncFileStreams": sync_file_streams, "ifExecuting": if_executing})
61
61
 
62
62
 
63
63
  def invalidate_channel(channel: CodeChannel):
@@ -70,7 +70,7 @@ def invalidate_channel(channel: CodeChannel):
70
70
  """
71
71
  if not isinstance(channel, CodeChannel):
72
72
  raise TypeError("channel must be a CodeChannel")
73
- return BaseCommand("InvalidateChannel", **{"Channel": channel})
73
+ return BaseCommand("InvalidateChannel", **{"channel": channel})
74
74
 
75
75
 
76
76
  def set_update_status(updating: bool):
@@ -81,7 +81,7 @@ def set_update_status(updating: bool):
81
81
  """
82
82
  if not isinstance(updating, bool):
83
83
  raise TypeError("updating must be a boolean")
84
- return BaseCommand("SetUpdateStatus", **{"Updating": updating})
84
+ return BaseCommand("SetUpdateStatus", **{"updating": updating})
85
85
 
86
86
 
87
87
  def simple_code(code: str, channel: CodeChannel = CodeChannel.DEFAULT_CHANNEL, async_exec: bool = False):
@@ -101,7 +101,7 @@ def simple_code(code: str, channel: CodeChannel = CodeChannel.DEFAULT_CHANNEL, a
101
101
  raise TypeError("code must be a string")
102
102
  if not isinstance(channel, CodeChannel):
103
103
  raise TypeError("channel must be a CodeChannel")
104
- return BaseCommand("SimpleCode", **{"Code": code, "Channel": channel, "ExecuteAsynchronously": async_exec})
104
+ return BaseCommand("SimpleCode", **{"code": code, "channel": channel, "executeAsynchronously": async_exec})
105
105
 
106
106
 
107
107
  def write_message(
@@ -129,9 +129,9 @@ def write_message(
129
129
  return BaseCommand(
130
130
  "WriteMessage",
131
131
  **{
132
- "Type": message_type,
133
- "Content": content,
134
- "OutputMessage": output_message,
135
- "LogLevel": log_level,
132
+ "type": message_type,
133
+ "content": content,
134
+ "outputMessage": output_message,
135
+ "logLevel": log_level,
136
136
  },
137
137
  )
@@ -25,10 +25,10 @@ def add_http_endpoint(endpoint_type: HttpEndpointType, namespace: str, path: str
25
25
  return BaseCommand(
26
26
  "AddHttpEndpoint",
27
27
  **{
28
- "EndpointType": endpoint_type,
29
- "Namespace": namespace,
30
- "Path": path,
31
- "IsUploadRequest": is_upload_request,
28
+ "endpointType": endpoint_type,
29
+ "namespace": namespace,
30
+ "path": path,
31
+ "isUploadRequest": is_upload_request,
32
32
  },
33
33
  )
34
34
 
@@ -49,5 +49,5 @@ def remove_http_endpoint(endpoint_type: HttpEndpointType, namespace: str, path:
49
49
  raise TypeError("path must be a string")
50
50
  return BaseCommand(
51
51
  "RemoveHttpEndpoint",
52
- **{"EndpointType": endpoint_type, "Namespace": namespace, "Path": path},
52
+ **{"endpointType": endpoint_type, "namespace": namespace, "path": path},
53
53
  )
@@ -24,7 +24,7 @@ def patch_object_model(key: str, patch: str):
24
24
  raise TypeError("key must be a string")
25
25
  if not isinstance(patch, str) or not patch:
26
26
  raise TypeError("patch must be a string")
27
- return BaseCommand("PatchObjectModel", **{"Key": key, "Patch": patch})
27
+ return BaseCommand("PatchObjectModel", **{"key": key, "patch": patch})
28
28
 
29
29
 
30
30
  def set_network_protocol(protocol: str, enabled: bool):
@@ -38,7 +38,7 @@ def set_network_protocol(protocol: str, enabled: bool):
38
38
  raise TypeError("protocol must be a string")
39
39
  if not isinstance(enabled, bool):
40
40
  raise TypeError("enabled must be a boolean")
41
- return BaseCommand("SetNetworkProtocol", **{"NetworkProtocol": protocol, "Enabled": enabled})
41
+ return BaseCommand("SetNetworkProtocol", **{"networkProtocol": protocol, "enabled": enabled})
42
42
 
43
43
 
44
44
  def set_object_model(property_path: str, value: str):
@@ -53,7 +53,7 @@ def set_object_model(property_path: str, value: str):
53
53
  raise TypeError("property_path must be a string")
54
54
  if not isinstance(value, str):
55
55
  raise TypeError("value must be a string")
56
- return BaseCommand("SetObjectModel", **{"PropertyPath": property_path, "Value": value})
56
+ return BaseCommand("SetObjectModel", **{"propertyPath": property_path, "value": value})
57
57
 
58
58
 
59
59
  def sync_object_model():
dsf/commands/packages.py CHANGED
@@ -7,7 +7,7 @@ def install_system_package(package_file: str):
7
7
  """
8
8
  if not isinstance(package_file, str) or not package_file:
9
9
  raise TypeError("package_file must be a string")
10
- return BaseCommand("InstallSystemPackage", **{"PackageFile": package_file})
10
+ return BaseCommand("InstallSystemPackage", **{"packageFile": package_file})
11
11
 
12
12
 
13
13
  def uninstall_system_package(package: str):
@@ -16,4 +16,4 @@ def uninstall_system_package(package: str):
16
16
  """
17
17
  if not isinstance(package, str) or not package:
18
18
  raise TypeError("package must be a string")
19
- return BaseCommand("UninstallSystemPackage", **{"Package": package})
19
+ return BaseCommand("UninstallSystemPackage", **{"package": package})
dsf/commands/plugins.py CHANGED
@@ -8,7 +8,7 @@ def install_plugin(plugin_file: str):
8
8
  """
9
9
  if not isinstance(plugin_file, str) or not plugin_file:
10
10
  raise TypeError("plugin_file must be a string")
11
- return BaseCommand("InstallPlugin", **{"PluginFile": plugin_file})
11
+ return BaseCommand("InstallPlugin", **{"pluginFile": plugin_file})
12
12
 
13
13
 
14
14
  def reload_plugin(plugin: str):
@@ -18,10 +18,10 @@ def reload_plugin(plugin: str):
18
18
  """
19
19
  if not isinstance(plugin, str) or not plugin:
20
20
  raise TypeError("plugin must be a string")
21
- return BaseCommand("ReloadPlugin", **{"Plugin": plugin})
21
+ return BaseCommand("ReloadPlugin", **{"plugin": plugin})
22
22
 
23
23
 
24
- def set_plugin_data(plugin: str, key: str, value: str):
24
+ def set_plugin_data(plugin: str, key: str, value):
25
25
  """
26
26
  Update custom plugin data in the object model
27
27
  May be used to update only the own plugin data unless the plugin has the ManagePlugins permission.
@@ -34,10 +34,8 @@ def set_plugin_data(plugin: str, key: str, value: str):
34
34
  raise TypeError("plugin must be a string")
35
35
  if not isinstance(key, str) or not key:
36
36
  raise TypeError("key must be a string")
37
- if not isinstance(value, str):
38
- raise TypeError("value must be a string")
39
37
  return BaseCommand(
40
- "SetPluginData", **{"Plugin": plugin, "Key": key, "Value": value}
38
+ "SetPluginData", **{"plugin": plugin, "key": key, "value": value}
41
39
  )
42
40
 
43
41
 
@@ -49,7 +47,7 @@ def start_plugin(plugin: str, save_state: bool = True):
49
47
  """
50
48
  if not isinstance(plugin, str) or not plugin:
51
49
  raise TypeError("plugin must be a string")
52
- return BaseCommand("StartPlugin", **{"Plugin": plugin, "SaveState": save_state})
50
+ return BaseCommand("StartPlugin", **{"plugin": plugin, "saveState": save_state})
53
51
 
54
52
 
55
53
  def start_plugins():
@@ -65,7 +63,7 @@ def stop_plugin(plugin: str, save_state: bool = True):
65
63
  """
66
64
  if not isinstance(plugin, str) or not plugin:
67
65
  raise TypeError("plugin must be a string")
68
- return BaseCommand("StopPlugin", **{"Plugin": plugin, "SaveState": save_state})
66
+ return BaseCommand("StopPlugin", **{"plugin": plugin, "saveState": save_state})
69
67
 
70
68
 
71
69
  def stop_plugins():
@@ -81,4 +79,4 @@ def uninstall_plugin(plugin: str):
81
79
  """
82
80
  if not isinstance(plugin, str) or not plugin:
83
81
  raise TypeError("plugin must be a string")
84
- return BaseCommand("UninstallPlugin", **{"Plugin": plugin})
82
+ return BaseCommand("UninstallPlugin", **{"plugin": plugin})
@@ -19,9 +19,9 @@ def add_user_session(access_level: AccessLevel, session_type: SessionType, origi
19
19
  return BaseCommand(
20
20
  "AddUserSession",
21
21
  **{
22
- "AccessLevel": access_level,
23
- "SessionType": session_type,
24
- "Origin": origin,
22
+ "accessLevel": access_level,
23
+ "sessionType": session_type,
24
+ "origin": origin,
25
25
  },
26
26
  )
27
27
 
@@ -33,4 +33,4 @@ def remove_user_session(session_id: int):
33
33
  """
34
34
  if not isinstance(session_id, int):
35
35
  raise TypeError("session_id must be an integer")
36
- return BaseCommand("RemoveUserSession", **{"Id": session_id})
36
+ return BaseCommand("RemoveUserSession", **{"id": session_id})
@@ -1,5 +1,6 @@
1
1
  import json
2
2
  import socket
3
+ import time
3
4
  from typing import Optional
4
5
 
5
6
  from .exceptions import IncompatibleVersionException, InternalServerException, TaskCanceledException
@@ -25,7 +26,8 @@ class BaseConnection:
25
26
 
26
27
  self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
27
28
  self.socket.connect(socket_file)
28
- self.socket.setblocking(True)
29
+ self.socket.settimeout(self.timeout if self.timeout > 0 else None)
30
+ # self.socket.setblocking(True)
29
31
  server_init_msg = server_init_message.ServerInitMessage.from_json(
30
32
  json.loads(self.socket.recv(50).decode("utf8"))
31
33
  )
@@ -98,7 +100,11 @@ class BaseConnection:
98
100
  json_string = json_string[:end_index]
99
101
  else:
100
102
  found = False
103
+ start_time = time.time()
101
104
  while not found:
105
+ if (self.timeout > 0) and (time.time() - start_time > self.timeout):
106
+ raise TimeoutError("Timeout while waiting for JSON response")
107
+
102
108
  # Refill the buffer and check again
103
109
  BUFF_SIZE = 4096 # 4 KiB
104
110
  data = b""
@@ -70,12 +70,12 @@ def intercept_init_message(
70
70
  return ClientInitMessage(
71
71
  ConnectionMode.INTERCEPT,
72
72
  **{
73
- "InterceptionMode": intercept_mode,
74
- "Channels": channels,
75
- "AutoFlush": auto_flush,
76
- "AutoEvaluateExpressions": auto_evaluate_expression,
77
- "Filters": filters,
78
- "PriorityCodes": priority_codes,
73
+ "interceptionMode": intercept_mode,
74
+ "channels": channels,
75
+ "autoFlush": auto_flush,
76
+ "autoEvaluateExpressions": auto_evaluate_expression,
77
+ "filters": filters,
78
+ "priorityCodes": priority_codes,
79
79
  },
80
80
  )
81
81
 
@@ -90,8 +90,8 @@ def subscribe_init_message(subscription_mode: SubscriptionMode, filter_string: s
90
90
  return ClientInitMessage(
91
91
  ConnectionMode.SUBSCRIBE,
92
92
  **{
93
- "SubscriptionMode": subscription_mode,
94
- "Filter": filter_string,
95
- "Filters": filter_list,
93
+ "subscriptionMode": subscription_mode,
94
+ "filter": filter_string,
95
+ "filters": filter_list,
96
96
  },
97
97
  )
@@ -37,8 +37,9 @@ class InterceptConnection(BaseCommandConnection):
37
37
  auto_evaluate_expression: bool = True,
38
38
  priority_codes: bool = False,
39
39
  debug: bool = False,
40
+ timeout: int = 0
40
41
  ):
41
- super().__init__(debug)
42
+ super().__init__(debug, timeout)
42
43
  self.interception_mode = interception_mode
43
44
  self.channels = channels if channels is not None else CodeChannel.list()
44
45
  self.filters = filters
dsf/http.py CHANGED
@@ -1,5 +1,8 @@
1
1
  import asyncio
2
2
  import json
3
+ import socket
4
+ import stat
5
+ import errno
3
6
  import os
4
7
  from concurrent.futures import ThreadPoolExecutor
5
8
  from enum import Enum
@@ -11,10 +14,11 @@ from .object_model import HttpEndpointType
11
14
  class HttpResponseType(str, Enum):
12
15
  """Enumeration of supported HTTP responses"""
13
16
 
14
- StatusCode = "StatusCode"
15
- PlainText = "PlainText"
16
- JSON = "JSON"
17
- File = "File"
17
+ StatusCode = "statuscode"
18
+ PlainText = "plainText"
19
+ JSON = "json"
20
+ File = "file"
21
+ URI = "uri"
18
22
 
19
23
 
20
24
  class ReceivedHttpRequest:
@@ -47,7 +51,7 @@ class HttpEndpointConnection:
47
51
  """Close the connection"""
48
52
  self.writer.close()
49
53
 
50
- async def read_request(self):
54
+ async def read_request(self) -> ReceivedHttpRequest:
51
55
  """
52
56
  Read information about the last HTTP request.
53
57
  Note that a call to this method may fail!
@@ -67,9 +71,9 @@ class HttpEndpointConnection:
67
71
  try:
68
72
  await self.send(
69
73
  {
70
- "StatusCode": status_code,
71
- "Response": response,
72
- "ResponseType": response_type,
74
+ "statusCode": status_code,
75
+ "response": response,
76
+ "responseType": response_type,
73
77
  }
74
78
  )
75
79
  finally:
@@ -136,7 +140,8 @@ class HttpEndpointUnixSocket:
136
140
  if self._loop is not None:
137
141
  # TODO: this enables correctly ending the loop. Why?
138
142
  self._loop.set_debug(True)
139
- self._server.close()
143
+ if self._server is not None:
144
+ self._server.close()
140
145
  self._loop.stop()
141
146
  self.event_loop.cancel()
142
147
  self.executor.shutdown(wait=False)
@@ -149,11 +154,47 @@ class HttpEndpointUnixSocket:
149
154
  """Set the handler to handle client connections"""
150
155
  self.handler = handler
151
156
 
157
+ def _create_socket(self, path: str):
158
+ path = os.fspath(path)
159
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
160
+
161
+ # Check for abstract socket. `str` and `bytes` paths are supported.
162
+ if path[0] not in (0, '\x00'):
163
+ try:
164
+ if stat.S_ISSOCK(os.stat(path).st_mode):
165
+ os.remove(path)
166
+ except FileNotFoundError:
167
+ pass
168
+ except OSError as err:
169
+ # Directory may have permissions only to create socket.
170
+ # logger.error('Unable to check or remove stale UNIX socket '
171
+ # '%r: %r', path, err)
172
+ pass
173
+
174
+ try:
175
+ sock.bind(path)
176
+ os.chmod(path, os.stat(path).st_mode | stat.S_IWGRP | stat.S_IRGRP)
177
+ except OSError as exc:
178
+ sock.close()
179
+ if exc.errno == errno.EADDRINUSE:
180
+ # Let's improve the error message by adding
181
+ # with what exact address it occurs.
182
+ msg = f'Address {path!r} is already in use'
183
+ raise OSError(errno.EADDRINUSE, msg) from None
184
+ else:
185
+ raise
186
+ except:
187
+ sock.close()
188
+ raise
189
+
190
+ return sock
191
+
152
192
  def start_connection_listener(self):
153
193
  try:
154
194
  self._loop = asyncio.new_event_loop()
195
+ sock = self._create_socket(self.socket_file)
155
196
  self._server = asyncio.start_unix_server(
156
- self.handle_connection, self.socket_file, backlog=self.backlog
197
+ self.handle_connection, sock=sock, backlog=self.backlog
157
198
  )
158
199
  self._loop.create_task(self._server)
159
200
  self._loop.run_forever()
@@ -1,5 +1,5 @@
1
1
  from enum import Enum
2
- from typing import List
2
+ from typing import List, Union
3
3
 
4
4
  from .heater_model import HeaterModel
5
5
  from .heater_monitor import HeaterMonitor
@@ -39,6 +39,10 @@ class Heater(ModelObject):
39
39
  self._avg_pwm = 0
40
40
  # Current temperature of the heater (in C)
41
41
  self._current = -273.15
42
+ # Current feedforward PWM boost applied to the heater
43
+ self._extr_pwm_boost = None
44
+ # Current temperature boost applied to the heater
45
+ self._extr_temp_boost = None
42
46
  # Maximum temperature allowed for this heater (in C)
43
47
  # This is only temporary and should be replaced by a representation of the heater protection as in RRF
44
48
  self._max = 285
@@ -89,6 +93,24 @@ class Heater(ModelObject):
89
93
  def current(self, value: float):
90
94
  self._current = float(value)
91
95
 
96
+ @property
97
+ def extr_pwm_boost(self) -> Union[float, None]:
98
+ """Current feedforward PWM boost applied to the heater"""
99
+ return self._extr_pwm_boost
100
+
101
+ @extr_pwm_boost.setter
102
+ def extr_pwm_boost(self, value: Union[float, None]):
103
+ self._extr_pwm_boost = float(value) if value is not None else None
104
+
105
+ @property
106
+ def extr_temp_boost(self) -> Union[float, None]:
107
+ """Current temperature boost applied to the heater"""
108
+ return self._extr_temp_boost
109
+
110
+ @extr_temp_boost.setter
111
+ def extr_temp_boost(self, value: Union[float, None]):
112
+ self._extr_temp_boost = float(value) if value is not None else None
113
+
92
114
  @property
93
115
  def max(self) -> float:
94
116
  """Maximum temperature allowed for this heater (in C)
@@ -11,6 +11,7 @@ class GCodeFileInfo(ModelObject):
11
11
 
12
12
  def __init__(self):
13
13
  super().__init__()
14
+ self._custom_info = {}
14
15
  self._filament = []
15
16
  self._file_name = ""
16
17
  self._generated_by = ""
@@ -23,6 +24,11 @@ class GCodeFileInfo(ModelObject):
23
24
  self._size = 0
24
25
  self._thumbnails = ModelCollection(ThumbnailInfo)
25
26
 
27
+ @property
28
+ def custom_info(self) -> dict:
29
+ """Custom information extracted from the G-code file"""
30
+ return self._custom_info
31
+
26
32
  @property
27
33
  def filament(self) -> List[float]:
28
34
  """Filament consumption per extruder drive (in mm)"""
@@ -14,6 +14,8 @@ class TimesLeft(ModelObject):
14
14
  self._file = None
15
15
  # Time left based on the slicer reports (see M73, in s or null)
16
16
  self._slicer = None
17
+ # Time left before the next colour change is expected (see M73 C, in s or null)
18
+ self._to_pause = None
17
19
 
18
20
  @property
19
21
  def filament(self) -> Union[int, None]:
@@ -41,3 +43,12 @@ class TimesLeft(ModelObject):
41
43
  @slicer.setter
42
44
  def slicer(self, value):
43
45
  self._slicer = int(value) if value is not None else None
46
+
47
+ @property
48
+ def to_pause(self) -> Union[int, None]:
49
+ """Time left before the next colour change is expected (see M73 C, in s or null)"""
50
+ return self._to_pause
51
+
52
+ @to_pause.setter
53
+ def to_pause(self, value):
54
+ self._to_pause = int(value) if value is not None else None
@@ -1,25 +1,34 @@
1
+ from typing import Generic, TypeVar, Type, List, Dict, Any, Union, Optional
1
2
  from .utils import is_model_object
2
3
 
4
+ T = TypeVar('T')
3
5
 
4
- class ModelCollection(list):
6
+
7
+ class ModelCollection(Generic[T], list):
5
8
  """
6
9
  Class for storing model object items in a list
7
10
  Useful for updating model object items from JSON data (patches)
8
11
  """
9
12
 
10
- def __init__(self, item_constructor, value=None):
13
+ def __init__(self, item_constructor: Type[T], value: Optional[List[T]] = None) -> None:
11
14
  """
12
-
13
15
  :param item_constructor: Item constructor type that items must derive from
14
16
  :param value: Value used to initialize the list from
15
17
  """
16
18
  super().__init__()
17
- self._item_constructor = item_constructor
19
+ self._item_constructor: Type[T] = item_constructor
18
20
 
19
21
  if value is not None:
20
- self[:] = value
22
+ self[:] = []
23
+ for (i, item) in enumerate(value):
24
+ if isinstance(item, self._item_constructor):
25
+ self.append(item)
26
+ else:
27
+ ref_item = self._item_constructor()
28
+ ref_item.update_from_json(item)
29
+ self.append(ref_item)
21
30
 
22
- def update_from_json(self, json_element):
31
+ def update_from_json(self, json_element: List[Any]) -> 'ModelCollection[T]':
23
32
  """
24
33
  Update this instance from the given data
25
34
  :param json_element: JSON data to upgrade this instance from
@@ -86,6 +86,8 @@ class Axis(ModelObject):
86
86
  self._percent_current = 100
87
87
  # Percentage applied to the motor current during standstill (0..100 or None if not supported)
88
88
  self._percent_stst_current = None
89
+ # Motor jerk during the current print only (in mm/s)
90
+ self._printing_jerk = None
89
91
  # Reduced accelerations used by Z probing and stall homing moves (in mm/s^2)
90
92
  self._reduced_acceleration = 0
91
93
  # Maximum speed (in mm/min)
@@ -242,6 +244,15 @@ class Axis(ModelObject):
242
244
  def percent_stst_current(self, value):
243
245
  self._percent_stst_current = int(value) if value is not None else None
244
246
 
247
+ @property
248
+ def printing_jerk(self) -> Union[float, None]:
249
+ """Motor jerk during the current print only (in mm/s)"""
250
+ return self._printing_jerk
251
+
252
+ @printing_jerk.setter
253
+ def printing_jerk(self, value):
254
+ self._printing_jerk = float(value) if value is not None else None
255
+
245
256
  @property
246
257
  def reduced_acceleration(self) -> float:
247
258
  """Reduced accelerations used by Z probing and stall homing moves (in mm/s^2)"""
@@ -41,6 +41,8 @@ class Extruder(ModelObject):
41
41
  self._position = 0
42
42
  # Pressure advance
43
43
  self._pressure_advance = 0
44
+ # Motor jerk during the current print only (in mm/s)
45
+ self._printing_jerk = None
44
46
  # Raw extruder position as commanded by the slicer without extrusion factor applied (in mm)
45
47
  self._raw_position = 0
46
48
  # Maximum speed (in mm/s)
@@ -148,6 +150,15 @@ class Extruder(ModelObject):
148
150
  def pressure_advance(self, value):
149
151
  self._pressure_advance = float(value)
150
152
 
153
+ @property
154
+ def printing_jerk(self) -> Union[float, None]:
155
+ """Motor jerk during the current print only (in mm/s)"""
156
+ return self._printing_jerk
157
+
158
+ @printing_jerk.setter
159
+ def printing_jerk(self, value):
160
+ self._printing_jerk = float(value) if value is not None else None
161
+
151
162
  @property
152
163
  def raw_position(self) -> float:
153
164
  """Raw extruder position as commanded by the slicer without extrusion factor applied (in mm)"""