dsf-python 3.5.0.2rc2__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.
- dsf/__init__.py +18 -1
- dsf/commands/code.py +4 -1
- dsf/commands/code_interception.py +1 -1
- dsf/commands/files.py +4 -3
- dsf/commands/generic.py +10 -10
- dsf/commands/http_endpoints.py +5 -5
- dsf/commands/object_model.py +3 -3
- dsf/commands/packages.py +2 -2
- dsf/commands/plugins.py +7 -9
- dsf/commands/user_sessions.py +4 -4
- dsf/connections/base_connection.py +7 -1
- dsf/connections/init_messages/client_init_messages.py +14 -9
- dsf/connections/intercept_connection.py +13 -2
- dsf/http.py +51 -10
- dsf/object_model/boards/accelerometer.py +13 -0
- dsf/object_model/boards/boards.py +23 -7
- dsf/object_model/boards/inductive_sensor.py +9 -0
- dsf/object_model/fans/fans.py +11 -0
- dsf/object_model/heat/heater.py +23 -1
- dsf/object_model/job/gcode_fileinfo.py +6 -0
- dsf/object_model/job/times_left.py +11 -0
- dsf/object_model/model_collection.py +15 -6
- dsf/object_model/move/axis.py +31 -0
- dsf/object_model/move/extruder.py +11 -0
- dsf/object_model/move/input_shaping.py +7 -20
- dsf/object_model/move/move_segmentation.py +4 -4
- dsf/object_model/network/network_interface.py +14 -1
- dsf/object_model/plugins/plugin_manifest.py +8 -0
- dsf/object_model/sbc/dsf/dsf.py +10 -0
- dsf/object_model/sensors/analog_sensor.py +42 -0
- dsf/object_model/sensors/analog_sensor_type.py +3 -3
- dsf/object_model/sensors/filament_monitors/Duet3DFilamentMonitor.py +11 -0
- dsf/object_model/sensors/filament_monitors/pulsed_filament_monitor.py +10 -0
- dsf/object_model/sensors/probe.py +17 -6
- dsf/object_model/sensors/probe_type.py +3 -0
- dsf/object_model/spindles/spindle_type.py +14 -0
- dsf/object_model/spindles/spindles.py +48 -27
- dsf/object_model/tools/tools.py +24 -0
- dsf/object_model/utils.py +8 -3
- {dsf_python-3.5.0.2rc2.dist-info → dsf_python-3.6.0.dist-info}/METADATA +18 -5
- {dsf_python-3.5.0.2rc2.dist-info → dsf_python-3.6.0.dist-info}/RECORD +44 -42
- {dsf_python-3.5.0.2rc2.dist-info → dsf_python-3.6.0.dist-info}/WHEEL +1 -1
- {dsf_python-3.5.0.2rc2.dist-info → dsf_python-3.6.0.dist-info/licenses}/LICENSE +0 -0
- {dsf_python-3.5.0.2rc2.dist-info → dsf_python-3.6.0.dist-info}/top_level.txt +0 -0
dsf/__init__.py
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
|
-
|
|
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.
|
|
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", **{"
|
|
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", **{"
|
|
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", **{"
|
|
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", **{"
|
|
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", **{"
|
|
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
|
-
**{"
|
|
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", **{"
|
|
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", **{"
|
|
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", **{"
|
|
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
|
-
"
|
|
133
|
-
"
|
|
134
|
-
"
|
|
135
|
-
"
|
|
132
|
+
"type": message_type,
|
|
133
|
+
"content": content,
|
|
134
|
+
"outputMessage": output_message,
|
|
135
|
+
"logLevel": log_level,
|
|
136
136
|
},
|
|
137
137
|
)
|
dsf/commands/http_endpoints.py
CHANGED
|
@@ -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
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
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
|
-
**{"
|
|
52
|
+
**{"endpointType": endpoint_type, "namespace": namespace, "path": path},
|
|
53
53
|
)
|
dsf/commands/object_model.py
CHANGED
|
@@ -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", **{"
|
|
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", **{"
|
|
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", **{"
|
|
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", **{"
|
|
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", **{"
|
|
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", **{"
|
|
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", **{"
|
|
21
|
+
return BaseCommand("ReloadPlugin", **{"plugin": plugin})
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
def set_plugin_data(plugin: str, key: str, value
|
|
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", **{"
|
|
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", **{"
|
|
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", **{"
|
|
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", **{"
|
|
82
|
+
return BaseCommand("UninstallPlugin", **{"plugin": plugin})
|
dsf/commands/user_sessions.py
CHANGED
|
@@ -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
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
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", **{"
|
|
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.
|
|
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""
|
|
@@ -41,7 +41,8 @@ def intercept_init_message(
|
|
|
41
41
|
channels: List[CodeChannel],
|
|
42
42
|
filters: List[str],
|
|
43
43
|
priority_codes: bool,
|
|
44
|
-
auto_flush: bool = True
|
|
44
|
+
auto_flush: bool = True,
|
|
45
|
+
auto_evaluate_expression: bool = True):
|
|
45
46
|
"""
|
|
46
47
|
Enter interception mode
|
|
47
48
|
Whenever a code is received, the connection must respond with one of
|
|
@@ -62,15 +63,19 @@ def intercept_init_message(
|
|
|
62
63
|
is specified.
|
|
63
64
|
This option makes extra Flush calls in the interceptor implementation obsolete.
|
|
64
65
|
It is highly recommended to enable this in order to avoid potential deadlocks when dealing with macros!
|
|
66
|
+
:param auto_evaluate_expression: Automatically evaluate expression parameters to their final values
|
|
67
|
+
before sending it over to the client.
|
|
68
|
+
This requires auto_flush to be True and happens when the remaining codes have been processed.
|
|
65
69
|
"""
|
|
66
70
|
return ClientInitMessage(
|
|
67
71
|
ConnectionMode.INTERCEPT,
|
|
68
72
|
**{
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"
|
|
73
|
+
"interceptionMode": intercept_mode,
|
|
74
|
+
"channels": channels,
|
|
75
|
+
"autoFlush": auto_flush,
|
|
76
|
+
"autoEvaluateExpressions": auto_evaluate_expression,
|
|
77
|
+
"filters": filters,
|
|
78
|
+
"priorityCodes": priority_codes,
|
|
74
79
|
},
|
|
75
80
|
)
|
|
76
81
|
|
|
@@ -85,8 +90,8 @@ def subscribe_init_message(subscription_mode: SubscriptionMode, filter_string: s
|
|
|
85
90
|
return ClientInitMessage(
|
|
86
91
|
ConnectionMode.SUBSCRIBE,
|
|
87
92
|
**{
|
|
88
|
-
"
|
|
89
|
-
"
|
|
90
|
-
"
|
|
93
|
+
"subscriptionMode": subscription_mode,
|
|
94
|
+
"filter": filter_string,
|
|
95
|
+
"filters": filter_list,
|
|
91
96
|
},
|
|
92
97
|
)
|
|
@@ -21,6 +21,9 @@ class InterceptConnection(BaseCommandConnection):
|
|
|
21
21
|
in case a code filter is specified.
|
|
22
22
|
This option makes extra Flush calls in the interceptor implementation obsolete.
|
|
23
23
|
It is highly recommended to enable this in order to avoid potential deadlocks when dealing with macros!
|
|
24
|
+
:param auto_evaluate_expression: Automatically evaluate expression parameters to their final values
|
|
25
|
+
before sending it over to the client.
|
|
26
|
+
This requires auto_flush to be True and happens when the remaining codes have been processed.
|
|
24
27
|
:param priority_codes: Defines if priority codes may be intercepted (e.g. M122 or M999)
|
|
25
28
|
:param debug: Whether debugging output is turned on for this connection
|
|
26
29
|
"""
|
|
@@ -31,20 +34,28 @@ class InterceptConnection(BaseCommandConnection):
|
|
|
31
34
|
channels: List[CodeChannel] = None,
|
|
32
35
|
filters: List[str] = None,
|
|
33
36
|
auto_flush: bool = True,
|
|
37
|
+
auto_evaluate_expression: bool = True,
|
|
34
38
|
priority_codes: bool = False,
|
|
35
39
|
debug: bool = False,
|
|
40
|
+
timeout: int = 0
|
|
36
41
|
):
|
|
37
|
-
super().__init__(debug)
|
|
42
|
+
super().__init__(debug, timeout)
|
|
38
43
|
self.interception_mode = interception_mode
|
|
39
44
|
self.channels = channels if channels is not None else CodeChannel.list()
|
|
40
45
|
self.filters = filters
|
|
41
46
|
self.auto_flush = auto_flush
|
|
47
|
+
self.auto_evaluate_expression = auto_evaluate_expression
|
|
42
48
|
self.priority_codes = priority_codes
|
|
43
49
|
|
|
44
50
|
def connect(self, socket_file: str = SOCKET_FILE): # noqa
|
|
45
51
|
"""Establishes a connection to the given UNIX socket file"""
|
|
46
52
|
iim = client_init_messages.intercept_init_message(
|
|
47
|
-
self.interception_mode,
|
|
53
|
+
self.interception_mode,
|
|
54
|
+
self.channels,
|
|
55
|
+
self.filters,
|
|
56
|
+
self.priority_codes,
|
|
57
|
+
self.auto_flush,
|
|
58
|
+
self.auto_evaluate_expression
|
|
48
59
|
)
|
|
49
60
|
return super().connect(iim, socket_file)
|
|
50
61
|
|
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 = "
|
|
15
|
-
PlainText = "
|
|
16
|
-
JSON = "
|
|
17
|
-
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
|
-
"
|
|
71
|
-
"
|
|
72
|
-
"
|
|
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
|
|
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,
|
|
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()
|
|
@@ -5,11 +5,24 @@ class Accelerometer(ModelObject):
|
|
|
5
5
|
"""This represents an accelerometer"""
|
|
6
6
|
def __init__(self):
|
|
7
7
|
super(Accelerometer, self).__init__()
|
|
8
|
+
# Orientation of the accelerometer
|
|
9
|
+
# See https://docs.duet3d.com/en/Duet3D_hardware/Accessories/Duet3D_Accelerometer#orientation for a list of orientations
|
|
10
|
+
self._orientation = 20
|
|
8
11
|
# Number of collected data points in the last run or 0 if it failed
|
|
9
12
|
self._points = 0
|
|
10
13
|
# Number of completed sampling runs
|
|
11
14
|
self._runs = 0
|
|
12
15
|
|
|
16
|
+
@property
|
|
17
|
+
def orientation(self) -> int:
|
|
18
|
+
"""Orientation of the accelerometer
|
|
19
|
+
See https://docs.duet3d.com/en/Duet3D_hardware/Accessories/Duet3D_Accelerometer#orientation for a list of orientations"""
|
|
20
|
+
return self._orientation
|
|
21
|
+
|
|
22
|
+
@orientation.setter
|
|
23
|
+
def orientation(self, value):
|
|
24
|
+
self._orientation = int(value)
|
|
25
|
+
|
|
13
26
|
@property
|
|
14
27
|
def points(self) -> int:
|
|
15
28
|
"""Number of collected data points in the last run or 0 if it failed"""
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
|
-
from typing import List,
|
|
2
|
+
from typing import List, Optional
|
|
3
3
|
|
|
4
4
|
from .accelerometer import Accelerometer
|
|
5
5
|
from .board_closed_loop import BoardClosedLoop
|
|
6
6
|
from .direct_display import DirectDisplay
|
|
7
7
|
from .driver import Driver
|
|
8
|
+
from .inductive_sensor import InductiveSensor
|
|
8
9
|
from .min_max_current import MinMaxCurrent
|
|
9
10
|
from ..model_collection import ModelCollection
|
|
10
11
|
from ..model_object import ModelObject
|
|
@@ -42,6 +43,8 @@ class Board(ModelObject):
|
|
|
42
43
|
closed_loop = wrap_model_property('closed_loop', BoardClosedLoop)
|
|
43
44
|
# Details about a connected display or None if none is connected
|
|
44
45
|
direct_display = wrap_model_property('direct_display', DirectDisplay)
|
|
46
|
+
# Information about an inductive sensor or None if not present
|
|
47
|
+
inductive_sensor = wrap_model_property('inductive_sensor', InductiveSensor)
|
|
45
48
|
# Minimum, maximum, and current temperatures of the MCU or None if unknown
|
|
46
49
|
mcu_temp = wrap_model_property('mcu_temp', MinMaxCurrent)
|
|
47
50
|
# Minimum, maximum, and current voltages on the 12V rail or None if unknown
|
|
@@ -72,11 +75,15 @@ class Board(ModelObject):
|
|
|
72
75
|
self._firmware_name = ""
|
|
73
76
|
# Version of the firmware build
|
|
74
77
|
self._firmware_version = ""
|
|
78
|
+
# Amount of free RAM on this board (in bytes or null if unknown)
|
|
79
|
+
self._free_ram = None
|
|
75
80
|
# Filename of the IAP binary that is used for updates from the SBC or None if unsupported
|
|
76
81
|
self._iap_file_name_SBC = None
|
|
77
82
|
# Filename of the IAP binary that is used for updates from the SD card or None if unsupported
|
|
78
83
|
# This is only available for the mainboard (first board item)
|
|
79
84
|
self._iap_file_name_SD = None
|
|
85
|
+
# Information about an inductive sensor or None if not present
|
|
86
|
+
self._inductive_sensor = None
|
|
80
87
|
# Maximum number of heaters this board can control
|
|
81
88
|
self._max_heaters = 0
|
|
82
89
|
# Maximum number of motors this board can drive
|
|
@@ -104,7 +111,7 @@ class Board(ModelObject):
|
|
|
104
111
|
self._wifi_firmware_file_name = None
|
|
105
112
|
|
|
106
113
|
@property
|
|
107
|
-
def bootloader_file_name(self) ->
|
|
114
|
+
def bootloader_file_name(self) -> Optional[str]:
|
|
108
115
|
"""Filename of the bootloader binary or None if unknown"""
|
|
109
116
|
return self._bootloader_file_name
|
|
110
117
|
|
|
@@ -113,7 +120,7 @@ class Board(ModelObject):
|
|
|
113
120
|
self._bootloader_file_name = str(value) if value is not None else None
|
|
114
121
|
|
|
115
122
|
@property
|
|
116
|
-
def can_address(self) ->
|
|
123
|
+
def can_address(self) -> Optional[int]:
|
|
117
124
|
"""CAN address of this board or None if not applicable"""
|
|
118
125
|
return self._can_address
|
|
119
126
|
|
|
@@ -122,7 +129,7 @@ class Board(ModelObject):
|
|
|
122
129
|
self._can_address = int(value) if value is not None else None
|
|
123
130
|
|
|
124
131
|
@property
|
|
125
|
-
def drivers(self) ->
|
|
132
|
+
def drivers(self) -> Optional[List[Driver]]:
|
|
126
133
|
"""Drivers of this board"""
|
|
127
134
|
return self._drivers
|
|
128
135
|
|
|
@@ -167,7 +174,16 @@ class Board(ModelObject):
|
|
|
167
174
|
self._firmware_version = str(value)
|
|
168
175
|
|
|
169
176
|
@property
|
|
170
|
-
def
|
|
177
|
+
def free_ram(self) -> Optional[int]:
|
|
178
|
+
"""Amount of free RAM on this board (in bytes or null if unknown)"""
|
|
179
|
+
return self._free_ram
|
|
180
|
+
|
|
181
|
+
@free_ram.setter
|
|
182
|
+
def free_ram(self, value):
|
|
183
|
+
self._free_ram = None if value is None else int(value)
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def iap_file_name_SBC(self) -> Optional[str]:
|
|
171
187
|
"""Filename of the IAP binary that is used for updates from the SBC or None if unsupported"""
|
|
172
188
|
return self._iap_file_name_SBC
|
|
173
189
|
|
|
@@ -176,7 +192,7 @@ class Board(ModelObject):
|
|
|
176
192
|
self._iap_file_name_SBC = str(value) if value is not None else None
|
|
177
193
|
|
|
178
194
|
@property
|
|
179
|
-
def iap_file_name_SD(self) ->
|
|
195
|
+
def iap_file_name_SD(self) -> Optional[str]:
|
|
180
196
|
"""Filename of the IAP binary that is used for updates from the SD card or None if unsupported
|
|
181
197
|
This is only available for the mainboard (first board item)"""
|
|
182
198
|
return self._iap_file_name_SD
|
|
@@ -256,7 +272,7 @@ class Board(ModelObject):
|
|
|
256
272
|
self._supports_direct_display = bool(value)
|
|
257
273
|
|
|
258
274
|
@property
|
|
259
|
-
def unique_id(self) ->
|
|
275
|
+
def unique_id(self) -> Optional[str]:
|
|
260
276
|
"""Unique identifier of the board or None if unknown"""
|
|
261
277
|
return self._unique_id
|
|
262
278
|
|
dsf/object_model/fans/fans.py
CHANGED
|
@@ -23,6 +23,8 @@ class Fan(ModelObject):
|
|
|
23
23
|
self._requested_value = 0
|
|
24
24
|
# Current RPM of this fan or -1 if unknown/unset
|
|
25
25
|
self._rpm = -1
|
|
26
|
+
# Pulses per tacho revolution
|
|
27
|
+
self._tacho_ppr = 2.0
|
|
26
28
|
# Thermostatic control parameters
|
|
27
29
|
self._thermostatic = FanThermostaticControl()
|
|
28
30
|
|
|
@@ -99,6 +101,15 @@ class Fan(ModelObject):
|
|
|
99
101
|
def rpm(self, value):
|
|
100
102
|
self._rpm = int(value)
|
|
101
103
|
|
|
104
|
+
@property
|
|
105
|
+
def tacho_ppr(self) -> float:
|
|
106
|
+
"""Pulses per tacho revolution"""
|
|
107
|
+
return self._tacho_ppr
|
|
108
|
+
|
|
109
|
+
@tacho_ppr.setter
|
|
110
|
+
def tacho_ppr(self, value):
|
|
111
|
+
self._tacho_ppr = float(value)
|
|
112
|
+
|
|
102
113
|
@property
|
|
103
114
|
def thermostatic(self) -> FanThermostaticControl:
|
|
104
115
|
"""Thermostatic control parameters"""
|