flashforge-python-api 1.0.2__py3-none-any.whl → 1.2.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.
- flashforge/__init__.py +70 -29
- flashforge/api/__init__.py +1 -0
- flashforge/api/constants/__init__.py +1 -0
- flashforge/api/constants/commands.py +1 -0
- flashforge/api/constants/endpoints.py +7 -0
- flashforge/api/controls/__init__.py +1 -0
- flashforge/api/controls/control.py +64 -61
- flashforge/api/controls/files.py +28 -26
- flashforge/api/controls/info.py +84 -85
- flashforge/api/controls/job_control.py +120 -82
- flashforge/api/controls/temp_control.py +14 -11
- flashforge/api/misc/__init__.py +1 -1
- flashforge/api/network/__init__.py +2 -1
- flashforge/api/network/utils.py +7 -8
- flashforge/client.py +185 -64
- flashforge/discovery/__init__.py +30 -3
- flashforge/discovery/discovery.py +641 -308
- flashforge/models/__init__.py +11 -10
- flashforge/models/machine_info.py +225 -100
- flashforge/models/responses.py +103 -43
- flashforge/tcp/__init__.py +30 -17
- flashforge/tcp/a3_client.py +408 -0
- flashforge/tcp/a4_client.py +300 -0
- flashforge/tcp/ff_client.py +93 -90
- flashforge/tcp/gcode/__init__.py +4 -2
- flashforge/tcp/gcode/a3_gcode_controller.py +109 -0
- flashforge/tcp/gcode/gcode_controller.py +48 -36
- flashforge/tcp/gcode/gcodes.py +7 -1
- flashforge/tcp/parsers/__init__.py +11 -11
- flashforge/tcp/parsers/endstop_status.py +103 -132
- flashforge/tcp/parsers/location_info.py +26 -13
- flashforge/tcp/parsers/print_status.py +49 -56
- flashforge/tcp/parsers/printer_info.py +38 -48
- flashforge/tcp/parsers/temp_info.py +46 -47
- flashforge/tcp/parsers/thumbnail_info.py +65 -40
- flashforge/tcp/tcp_client.py +296 -293
- flashforge_python_api-1.2.0.dist-info/METADATA +133 -0
- flashforge_python_api-1.2.0.dist-info/RECORD +46 -0
- {flashforge_python_api-1.0.2.dist-info → flashforge_python_api-1.2.0.dist-info}/WHEEL +1 -1
- flashforge_python_api-1.0.2.dist-info/METADATA +0 -284
- flashforge_python_api-1.0.2.dist-info/RECORD +0 -43
- {flashforge_python_api-1.0.2.dist-info → flashforge_python_api-1.2.0.dist-info}/entry_points.txt +0 -0
- {flashforge_python_api-1.0.2.dist-info → flashforge_python_api-1.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -6,6 +6,7 @@ X, Y, Z coordinates of the printer's print head.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import logging
|
|
9
|
+
import re
|
|
9
10
|
from typing import Optional
|
|
10
11
|
|
|
11
12
|
logger = logging.getLogger(__name__)
|
|
@@ -14,7 +15,7 @@ logger = logging.getLogger(__name__)
|
|
|
14
15
|
class LocationInfo:
|
|
15
16
|
"""
|
|
16
17
|
Represents the current X, Y, and Z coordinates of the printer's print head.
|
|
17
|
-
|
|
18
|
+
|
|
18
19
|
This information is typically parsed from the response of an M114 G-code command,
|
|
19
20
|
which reports the current position.
|
|
20
21
|
"""
|
|
@@ -30,29 +31,41 @@ class LocationInfo:
|
|
|
30
31
|
self.z: str = ""
|
|
31
32
|
"""The current Z-axis coordinate as a string (e.g., "5.25")."""
|
|
32
33
|
|
|
33
|
-
def from_replay(self, replay: str) -> Optional[
|
|
34
|
+
def from_replay(self, replay: str) -> Optional["LocationInfo"]:
|
|
34
35
|
"""
|
|
35
36
|
Parse a raw string replay from M114 command to populate coordinate info.
|
|
36
|
-
|
|
37
|
+
|
|
37
38
|
The parsing logic assumes the replay is a multi-line string where the second line
|
|
38
39
|
(data[1]) contains the coordinate data in a format like "X:10.00 Y:20.50 Z:5.25 ...".
|
|
39
40
|
It splits this line by spaces and then extracts the values for X, Y, and Z by
|
|
40
41
|
removing the prefixes "X:", "Y:", and "Z:".
|
|
41
|
-
|
|
42
|
+
|
|
42
43
|
Args:
|
|
43
44
|
replay: The raw multi-line string response from the printer
|
|
44
|
-
|
|
45
|
+
|
|
45
46
|
Returns:
|
|
46
47
|
The populated LocationInfo instance, or None if parsing fails
|
|
47
48
|
"""
|
|
48
49
|
try:
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
lines = [line.strip() for line in replay.replace("\r", "\n").split("\n") if line.strip()]
|
|
51
|
+
coordinate_line = next(
|
|
52
|
+
(line for line in lines if "X:" in line and "Y:" in line and "Z:" in line),
|
|
53
|
+
"",
|
|
54
|
+
)
|
|
55
|
+
if not coordinate_line:
|
|
56
|
+
logger.error("LocationInfo replay has bad/null data")
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
x_match = re.search(r"X:\s*([^\s]+)", coordinate_line)
|
|
60
|
+
y_match = re.search(r"Y:\s*([^\s]+)", coordinate_line)
|
|
61
|
+
z_match = re.search(r"Z:\s*([^\s]+)", coordinate_line)
|
|
62
|
+
if not x_match or not y_match or not z_match:
|
|
63
|
+
logger.error("LocationInfo replay has bad/null data")
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
self.x = x_match.group(1)
|
|
67
|
+
self.y = y_match.group(1)
|
|
68
|
+
self.z = z_match.group(1)
|
|
56
69
|
return self
|
|
57
70
|
except Exception:
|
|
58
71
|
logger.error("LocationInfo replay has bad/null data")
|
|
@@ -61,7 +74,7 @@ class LocationInfo:
|
|
|
61
74
|
def __str__(self) -> str:
|
|
62
75
|
"""
|
|
63
76
|
Return a string representation of the location information.
|
|
64
|
-
|
|
77
|
+
|
|
65
78
|
Returns:
|
|
66
79
|
A string in the format "X: [X_value] Y: [Y_value] Z: [Z_value]"
|
|
67
80
|
"""
|
|
@@ -3,39 +3,40 @@ FlashForge Python API - Print Status Parser
|
|
|
3
3
|
|
|
4
4
|
Parses print progress information from M27 command responses.
|
|
5
5
|
"""
|
|
6
|
+
|
|
6
7
|
from typing import Optional
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
class PrintStatus:
|
|
10
11
|
"""
|
|
11
12
|
Represents the status of an ongoing print job, including SD card byte progress and layer progress.
|
|
12
|
-
|
|
13
|
+
|
|
13
14
|
This information is typically parsed from the response of an M27 G-code command,
|
|
14
15
|
which reports the print progress from the SD card.
|
|
15
16
|
"""
|
|
16
17
|
|
|
17
|
-
def __init__(self):
|
|
18
|
+
def __init__(self) -> None:
|
|
18
19
|
"""Initialize a new PrintStatus instance."""
|
|
19
20
|
self.sd_current: str = ""
|
|
20
21
|
self.sd_total: str = ""
|
|
21
22
|
self.layer_current: str = ""
|
|
22
23
|
self.layer_total: str = ""
|
|
23
24
|
|
|
24
|
-
def from_replay(self, replay: str) -> Optional[
|
|
25
|
+
def from_replay(self, replay: str) -> Optional["PrintStatus"]:
|
|
25
26
|
"""
|
|
26
27
|
Parses a raw string replay (typically from an M27 command) to populate
|
|
27
28
|
the print status properties of this instance.
|
|
28
|
-
|
|
29
|
+
|
|
29
30
|
The parsing logic expects a multi-line string:
|
|
30
31
|
- Line 1 (data[0]): Usually a command echo, ignored
|
|
31
32
|
- Line 2 (data[1]): Contains SD card progress, e.g., "SD printing byte 12345/67890"
|
|
32
33
|
It extracts the current and total bytes
|
|
33
34
|
- Line 3 (data[2]): Contains layer progress, e.g., "Layer: 10/250"
|
|
34
35
|
It extracts the current and total layers
|
|
35
|
-
|
|
36
|
+
|
|
36
37
|
Args:
|
|
37
38
|
replay: The raw multi-line string response from the printer
|
|
38
|
-
|
|
39
|
+
|
|
39
40
|
Returns:
|
|
40
41
|
The populated PrintStatus instance, or None if parsing fails
|
|
41
42
|
"""
|
|
@@ -43,44 +44,31 @@ class PrintStatus:
|
|
|
43
44
|
return None
|
|
44
45
|
|
|
45
46
|
try:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if len(data) > 2:
|
|
63
|
-
try:
|
|
64
|
-
# Example: "Layer: 10/250"
|
|
65
|
-
layer_progress = data[2].replace("Layer: ", "").strip()
|
|
66
|
-
except Exception:
|
|
67
|
-
print("PrintStatus: Bad layer progress")
|
|
68
|
-
print(f"Raw printer replay: {replay}")
|
|
69
|
-
return None
|
|
70
|
-
|
|
71
|
-
try:
|
|
72
|
-
lp_data = layer_progress.split('/')
|
|
47
|
+
self.sd_current = ""
|
|
48
|
+
self.sd_total = ""
|
|
49
|
+
self.layer_current = ""
|
|
50
|
+
self.layer_total = ""
|
|
51
|
+
lines = [line.strip() for line in replay.replace("\r", "\n").split("\n") if line.strip()]
|
|
52
|
+
|
|
53
|
+
for line in lines:
|
|
54
|
+
if "SD printing byte " in line:
|
|
55
|
+
sd_progress = line.split("SD printing byte ", 1)[1].strip().strip('"')
|
|
56
|
+
sd_progress_data = sd_progress.split("/")
|
|
57
|
+
if len(sd_progress_data) >= 2:
|
|
58
|
+
self.sd_current = sd_progress_data[0].strip()
|
|
59
|
+
self.sd_total = sd_progress_data[1].strip()
|
|
60
|
+
elif line.startswith("Layer:"):
|
|
61
|
+
layer_progress = line.replace("Layer:", "", 1).strip()
|
|
62
|
+
lp_data = layer_progress.split("/")
|
|
73
63
|
if len(lp_data) >= 2:
|
|
74
64
|
self.layer_current = lp_data[0].strip()
|
|
75
65
|
self.layer_total = lp_data[1].strip()
|
|
76
66
|
else:
|
|
77
|
-
print("PrintStatus: Invalid layer progress format")
|
|
78
|
-
print(f"layerProgress: {layer_progress}")
|
|
79
67
|
return None
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
68
|
+
|
|
69
|
+
if not self.sd_current or not self.sd_total:
|
|
70
|
+
print("PrintStatus: Invalid SD progress format")
|
|
71
|
+
return None
|
|
84
72
|
|
|
85
73
|
return self
|
|
86
74
|
|
|
@@ -92,7 +80,7 @@ class PrintStatus:
|
|
|
92
80
|
"""
|
|
93
81
|
Calculates the print progress percentage based on the current and total layers.
|
|
94
82
|
The result is clamped between 0 and 100.
|
|
95
|
-
|
|
83
|
+
|
|
96
84
|
Returns:
|
|
97
85
|
The print progress percentage (0-100), rounded to the nearest integer.
|
|
98
86
|
Returns NaN if layer information is not available or invalid.
|
|
@@ -102,18 +90,18 @@ class PrintStatus:
|
|
|
102
90
|
total_layers = int(self.layer_total)
|
|
103
91
|
|
|
104
92
|
if total_layers == 0:
|
|
105
|
-
return float(
|
|
93
|
+
return float("nan")
|
|
106
94
|
|
|
107
95
|
perc = (current_layer / total_layers) * 100
|
|
108
96
|
return round(min(100, max(0, perc))) # Clamp between 0 and 100
|
|
109
97
|
|
|
110
98
|
except (ValueError, TypeError):
|
|
111
|
-
return
|
|
99
|
+
return self.get_sd_percent()
|
|
112
100
|
|
|
113
101
|
def get_layer_progress(self) -> str:
|
|
114
102
|
"""
|
|
115
103
|
Gets the layer progress as a string.
|
|
116
|
-
|
|
104
|
+
|
|
117
105
|
Returns:
|
|
118
106
|
A string in the format "currentLayer/totalLayers"
|
|
119
107
|
"""
|
|
@@ -122,7 +110,7 @@ class PrintStatus:
|
|
|
122
110
|
def get_sd_progress(self) -> str:
|
|
123
111
|
"""
|
|
124
112
|
Gets the SD card byte progress as a string.
|
|
125
|
-
|
|
113
|
+
|
|
126
114
|
Returns:
|
|
127
115
|
A string in the format "currentBytes/totalBytes"
|
|
128
116
|
"""
|
|
@@ -131,7 +119,7 @@ class PrintStatus:
|
|
|
131
119
|
def get_sd_percent(self) -> float:
|
|
132
120
|
"""
|
|
133
121
|
Calculates the SD card progress percentage based on current and total bytes.
|
|
134
|
-
|
|
122
|
+
|
|
135
123
|
Returns:
|
|
136
124
|
The SD progress percentage (0-100), or NaN if data is invalid
|
|
137
125
|
"""
|
|
@@ -140,18 +128,18 @@ class PrintStatus:
|
|
|
140
128
|
total_bytes = int(self.sd_total)
|
|
141
129
|
|
|
142
130
|
if total_bytes == 0:
|
|
143
|
-
return float(
|
|
131
|
+
return float("nan")
|
|
144
132
|
|
|
145
133
|
perc = (current_bytes / total_bytes) * 100
|
|
146
134
|
return round(min(100, max(0, perc))) # Clamp between 0 and 100
|
|
147
135
|
|
|
148
136
|
except (ValueError, TypeError):
|
|
149
|
-
return float(
|
|
137
|
+
return float("nan")
|
|
150
138
|
|
|
151
139
|
def is_complete(self) -> bool:
|
|
152
140
|
"""
|
|
153
141
|
Checks if the print is complete based on layer progress.
|
|
154
|
-
|
|
142
|
+
|
|
155
143
|
Returns:
|
|
156
144
|
True if current layer equals total layers, False otherwise
|
|
157
145
|
"""
|
|
@@ -160,7 +148,8 @@ class PrintStatus:
|
|
|
160
148
|
total = int(self.layer_total)
|
|
161
149
|
return current >= total and total > 0
|
|
162
150
|
except (ValueError, TypeError):
|
|
163
|
-
|
|
151
|
+
sd_percent = self.get_sd_percent()
|
|
152
|
+
return sd_percent == 100 if sd_percent == sd_percent else False
|
|
164
153
|
|
|
165
154
|
def __str__(self) -> str:
|
|
166
155
|
"""String representation of the print status."""
|
|
@@ -170,13 +159,17 @@ class PrintStatus:
|
|
|
170
159
|
layer_str = "nan%" if layer_perc != layer_perc else f"{layer_perc}%" # Check for NaN
|
|
171
160
|
sd_str = "nan%" if sd_perc != sd_perc else f"{sd_perc}%" # Check for NaN
|
|
172
161
|
|
|
173
|
-
return (
|
|
174
|
-
|
|
162
|
+
return (
|
|
163
|
+
f"PrintStatus(layer={self.get_layer_progress()} [{layer_str}], "
|
|
164
|
+
f"sd={self.get_sd_progress()} [{sd_str}])"
|
|
165
|
+
)
|
|
175
166
|
|
|
176
167
|
def __repr__(self) -> str:
|
|
177
168
|
"""Detailed string representation for debugging."""
|
|
178
|
-
return (
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
169
|
+
return (
|
|
170
|
+
f"PrintStatus("
|
|
171
|
+
f"sd_current='{self.sd_current}', "
|
|
172
|
+
f"sd_total='{self.sd_total}', "
|
|
173
|
+
f"layer_current='{self.layer_current}', "
|
|
174
|
+
f"layer_total='{self.layer_total}')"
|
|
175
|
+
)
|
|
@@ -6,6 +6,7 @@ like model, firmware version, serial number, and capabilities.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import logging
|
|
9
|
+
import re
|
|
9
10
|
from typing import Optional
|
|
10
11
|
|
|
11
12
|
logger = logging.getLogger(__name__)
|
|
@@ -14,7 +15,7 @@ logger = logging.getLogger(__name__)
|
|
|
14
15
|
class PrinterInfo:
|
|
15
16
|
"""
|
|
16
17
|
Represents general information about the FlashForge 3D printer.
|
|
17
|
-
|
|
18
|
+
|
|
18
19
|
This information is typically parsed from the response of an M115 G-code command,
|
|
19
20
|
which provides details about the printer's firmware and capabilities.
|
|
20
21
|
"""
|
|
@@ -42,13 +43,13 @@ class PrinterInfo:
|
|
|
42
43
|
self.tool_count: str = ""
|
|
43
44
|
"""The number of tools (extruders) the printer has."""
|
|
44
45
|
|
|
45
|
-
def from_replay(self, replay: str) -> Optional[
|
|
46
|
+
def from_replay(self, replay: str) -> Optional["PrinterInfo"]:
|
|
46
47
|
"""
|
|
47
48
|
Parse a raw string replay from M115 command to populate printer info.
|
|
48
|
-
|
|
49
|
+
|
|
49
50
|
The M115 response is expected to be a multi-line string where each line
|
|
50
51
|
provides a piece of information in a "Key: Value" format.
|
|
51
|
-
|
|
52
|
+
|
|
52
53
|
Expected format:
|
|
53
54
|
- Line 1: Command echo/header (ignored)
|
|
54
55
|
- Line 2: "Machine Type: [TypeName]"
|
|
@@ -58,10 +59,10 @@ class PrinterInfo:
|
|
|
58
59
|
- Line 6: Dimensions string (e.g., "X:220 Y:220 Z:220")
|
|
59
60
|
- Line 7: "Tool count: [ToolCount]"
|
|
60
61
|
- Line 8: "Mac Address:[MacAddress]"
|
|
61
|
-
|
|
62
|
+
|
|
62
63
|
Args:
|
|
63
64
|
replay: The raw multi-line string response from the M115 command
|
|
64
|
-
|
|
65
|
+
|
|
65
66
|
Returns:
|
|
66
67
|
The populated PrinterInfo instance, or None if parsing fails
|
|
67
68
|
"""
|
|
@@ -69,51 +70,40 @@ class PrinterInfo:
|
|
|
69
70
|
return None
|
|
70
71
|
|
|
71
72
|
try:
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
lines = [line.strip() for line in replay.replace("\r", "\n").split("\n") if line.strip()]
|
|
74
|
+
|
|
75
|
+
for line in lines:
|
|
76
|
+
if line.startswith("Machine Type:"):
|
|
77
|
+
self.type_name = line.replace("Machine Type:", "", 1).strip()
|
|
78
|
+
elif line.startswith("Machine Name:"):
|
|
79
|
+
self.name = line.replace("Machine Name:", "", 1).strip()
|
|
80
|
+
elif line.startswith("Firmware:"):
|
|
81
|
+
self.firmware_version = line.replace("Firmware:", "", 1).strip()
|
|
82
|
+
elif line.startswith("SN:"):
|
|
83
|
+
self.serial_number = line.replace("SN:", "", 1).strip()
|
|
84
|
+
elif line.startswith("Serial Number:"):
|
|
85
|
+
self.serial_number = line.replace("Serial Number:", "", 1).strip()
|
|
86
|
+
elif line.startswith("Tool count:") or line.startswith("Tool Count:"):
|
|
87
|
+
tool_count = self._get_right(line)
|
|
88
|
+
if tool_count is not None:
|
|
89
|
+
self.tool_count = tool_count
|
|
90
|
+
elif line.startswith("Mac Address:"):
|
|
91
|
+
self.mac_address = line.replace("Mac Address:", "", 1).strip()
|
|
92
|
+
elif re.search(r"X:\s*\d+.*Y:\s*\d+.*Z:\s*\d+", line, re.IGNORECASE):
|
|
93
|
+
self.dimensions = line
|
|
94
|
+
|
|
95
|
+
if not self.type_name:
|
|
77
96
|
logger.error("PrinterInfo replay has null Machine Type")
|
|
78
97
|
return None
|
|
79
|
-
self.
|
|
80
|
-
|
|
81
|
-
# Parse machine name (line 3)
|
|
82
|
-
nick = self._get_right(data[2]) # Expected: "Machine Name: MyPrinter"
|
|
83
|
-
if nick is None:
|
|
98
|
+
if not self.name:
|
|
84
99
|
logger.error("PrinterInfo replay has null Machine Name")
|
|
85
100
|
return None
|
|
86
|
-
self.
|
|
87
|
-
|
|
88
|
-
# Parse firmware version (line 4)
|
|
89
|
-
fw = self._get_right(data[3]) # Expected: "Firmware: V1.2.3"
|
|
90
|
-
if fw is None:
|
|
101
|
+
if not self.firmware_version:
|
|
91
102
|
logger.error("PrinterInfo replay has null firmware version")
|
|
92
103
|
return None
|
|
93
|
-
self.
|
|
94
|
-
|
|
95
|
-
# Parse serial number (line 5)
|
|
96
|
-
sn = self._get_right(data[4]) # Expected: "SN: SN12345"
|
|
97
|
-
if sn is None:
|
|
104
|
+
if not self.serial_number:
|
|
98
105
|
logger.error("PrinterInfo replay has null serial number")
|
|
99
106
|
return None
|
|
100
|
-
self.serial_number = sn
|
|
101
|
-
|
|
102
|
-
# Parse dimensions (line 6) - direct string
|
|
103
|
-
if len(data) > 5:
|
|
104
|
-
self.dimensions = data[5].strip() # Expected: "X:220 Y:220 Z:220"
|
|
105
|
-
|
|
106
|
-
# Parse tool count (line 7)
|
|
107
|
-
if len(data) > 6:
|
|
108
|
-
tcs = self._get_right(data[6]) # Expected: "Tool count: 1"
|
|
109
|
-
if tcs is None:
|
|
110
|
-
logger.error("PrinterInfo replay has null tool count")
|
|
111
|
-
return None
|
|
112
|
-
self.tool_count = tcs
|
|
113
|
-
|
|
114
|
-
# Parse MAC address (line 8)
|
|
115
|
-
if len(data) > 7:
|
|
116
|
-
self.mac_address = data[7].replace("Mac Address:", "").strip()
|
|
117
107
|
|
|
118
108
|
return self
|
|
119
109
|
|
|
@@ -121,25 +111,25 @@ class PrinterInfo:
|
|
|
121
111
|
logger.error(f"Error creating PrinterInfo instance from replay: {e}")
|
|
122
112
|
return None
|
|
123
113
|
|
|
124
|
-
def _get_right(self, rp_data: str) ->
|
|
114
|
+
def _get_right(self, rp_data: str) -> str | None:
|
|
125
115
|
"""
|
|
126
116
|
Helper function to extract the value part of a "Key: Value" string.
|
|
127
|
-
|
|
117
|
+
|
|
128
118
|
Args:
|
|
129
119
|
rp_data: The input string (e.g., "Machine Type: Adventurer 5M Pro")
|
|
130
|
-
|
|
120
|
+
|
|
131
121
|
Returns:
|
|
132
122
|
The extracted value string (e.g., "Adventurer 5M Pro"), or None if parsing fails
|
|
133
123
|
"""
|
|
134
124
|
try:
|
|
135
|
-
return rp_data.split(
|
|
125
|
+
return rp_data.split(":", 1)[1].strip()
|
|
136
126
|
except (IndexError, AttributeError):
|
|
137
127
|
return None
|
|
138
128
|
|
|
139
129
|
def __str__(self) -> str:
|
|
140
130
|
"""
|
|
141
131
|
Return a string representation of the printer information.
|
|
142
|
-
|
|
132
|
+
|
|
143
133
|
Returns:
|
|
144
134
|
A multi-line string detailing the printer's properties
|
|
145
135
|
"""
|