raydriver 0.2.0__tar.gz → 0.3.0__tar.gz

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 (60) hide show
  1. {raydriver-0.2.0 → raydriver-0.3.0}/Cargo.lock +1 -1
  2. {raydriver-0.2.0 → raydriver-0.3.0}/Cargo.toml +1 -1
  3. {raydriver-0.2.0 → raydriver-0.3.0}/PKG-INFO +1 -1
  4. {raydriver-0.2.0 → raydriver-0.3.0}/python/raydriver/grbl/parser/__init__.pyi +6 -0
  5. {raydriver-0.2.0 → raydriver-0.3.0}/python/raydriver/grbl/types/__init__.pyi +19 -0
  6. {raydriver-0.2.0 → raydriver-0.3.0}/src/grbl/parser.rs +6 -0
  7. {raydriver-0.2.0 → raydriver-0.3.0}/src/python/grbl/parser.rs +9 -0
  8. {raydriver-0.2.0 → raydriver-0.3.0}/src/python/grbl/types.rs +77 -2
  9. raydriver-0.2.0/tests/test_parser.py → raydriver-0.3.0/tests/test_grbl_parser.py +11 -0
  10. raydriver-0.3.0/tests/test_grbl_types.py +171 -0
  11. {raydriver-0.2.0 → raydriver-0.3.0}/.github/workflows/ci.yml +0 -0
  12. {raydriver-0.2.0 → raydriver-0.3.0}/.github/workflows/release.yml +0 -0
  13. {raydriver-0.2.0 → raydriver-0.3.0}/.gitignore +0 -0
  14. {raydriver-0.2.0 → raydriver-0.3.0}/.pre-commit-config.yaml +0 -0
  15. {raydriver-0.2.0 → raydriver-0.3.0}/AGENTS.md +0 -0
  16. {raydriver-0.2.0 → raydriver-0.3.0}/Cargo.toml.bak +0 -0
  17. {raydriver-0.2.0 → raydriver-0.3.0}/LICENSE +0 -0
  18. {raydriver-0.2.0 → raydriver-0.3.0}/Makefile +0 -0
  19. {raydriver-0.2.0 → raydriver-0.3.0}/README.md +0 -0
  20. {raydriver-0.2.0 → raydriver-0.3.0}/env.sh +0 -0
  21. {raydriver-0.2.0 → raydriver-0.3.0}/freeze-dump.txt +0 -0
  22. {raydriver-0.2.0 → raydriver-0.3.0}/pyproject.toml +0 -0
  23. {raydriver-0.2.0 → raydriver-0.3.0}/python/raydriver/__init__.py +0 -0
  24. {raydriver-0.2.0 → raydriver-0.3.0}/python/raydriver/__init__.pyi +0 -0
  25. {raydriver-0.2.0 → raydriver-0.3.0}/python/raydriver/cli/__init__.py +0 -0
  26. {raydriver-0.2.0 → raydriver-0.3.0}/python/raydriver/cli/main.py +0 -0
  27. {raydriver-0.2.0 → raydriver-0.3.0}/python/raydriver/emulator.py +0 -0
  28. {raydriver-0.2.0 → raydriver-0.3.0}/python/raydriver/grbl/__init__.py +0 -0
  29. {raydriver-0.2.0 → raydriver-0.3.0}/python/raydriver/grbl/__init__.pyi +0 -0
  30. {raydriver-0.2.0 → raydriver-0.3.0}/python/raydriver/grbl/parser/__init__.py +0 -0
  31. {raydriver-0.2.0 → raydriver-0.3.0}/python/raydriver/grbl/parser/py.typed +0 -0
  32. {raydriver-0.2.0 → raydriver-0.3.0}/python/raydriver/grbl/py.typed +0 -0
  33. {raydriver-0.2.0 → raydriver-0.3.0}/python/raydriver/grbl/types/__init__.py +0 -0
  34. {raydriver-0.2.0 → raydriver-0.3.0}/python/raydriver/grbl/types/py.typed +0 -0
  35. {raydriver-0.2.0 → raydriver-0.3.0}/python/raydriver/py.typed +0 -0
  36. {raydriver-0.2.0 → raydriver-0.3.0}/rustfmt.toml +0 -0
  37. {raydriver-0.2.0 → raydriver-0.3.0}/src/bin/stub_gen.rs +0 -0
  38. {raydriver-0.2.0 → raydriver-0.3.0}/src/grbl/dialect.rs +0 -0
  39. {raydriver-0.2.0 → raydriver-0.3.0}/src/grbl/errors.rs +0 -0
  40. {raydriver-0.2.0 → raydriver-0.3.0}/src/grbl/flow.rs +0 -0
  41. {raydriver-0.2.0 → raydriver-0.3.0}/src/grbl/mod.rs +0 -0
  42. {raydriver-0.2.0 → raydriver-0.3.0}/src/grbl/session.rs +0 -0
  43. {raydriver-0.2.0 → raydriver-0.3.0}/src/grbl/transport/mock.rs +0 -0
  44. {raydriver-0.2.0 → raydriver-0.3.0}/src/grbl/transport/mod.rs +0 -0
  45. {raydriver-0.2.0 → raydriver-0.3.0}/src/grbl/transport/serial.rs +0 -0
  46. {raydriver-0.2.0 → raydriver-0.3.0}/src/grbl/transport/telnet.rs +0 -0
  47. {raydriver-0.2.0 → raydriver-0.3.0}/src/grbl/types.rs +0 -0
  48. {raydriver-0.2.0 → raydriver-0.3.0}/src/lib.rs +0 -0
  49. {raydriver-0.2.0 → raydriver-0.3.0}/src/python/events.rs +0 -0
  50. {raydriver-0.2.0 → raydriver-0.3.0}/src/python/grbl/mock.rs +0 -0
  51. {raydriver-0.2.0 → raydriver-0.3.0}/src/python/grbl/mod.rs +0 -0
  52. {raydriver-0.2.0 → raydriver-0.3.0}/src/python/grbl/session.rs +0 -0
  53. {raydriver-0.2.0 → raydriver-0.3.0}/src/python/mod.rs +0 -0
  54. {raydriver-0.2.0 → raydriver-0.3.0}/src/python/pyfuture.rs +0 -0
  55. {raydriver-0.2.0 → raydriver-0.3.0}/src/python/runtime.rs +0 -0
  56. {raydriver-0.2.0 → raydriver-0.3.0}/tests/conftest.py +0 -0
  57. {raydriver-0.2.0 → raydriver-0.3.0}/tests/test_cli.py +0 -0
  58. /raydriver-0.2.0/tests/test_session.py → /raydriver-0.3.0/tests/test_grbl_session.py +0 -0
  59. /raydriver-0.2.0/tests/test_streaming.py → /raydriver-0.3.0/tests/test_grbl_streaming.py +0 -0
  60. /raydriver-0.2.0/tests/test_stress.py → /raydriver-0.3.0/tests/test_grbl_stress.py +0 -0
@@ -893,7 +893,7 @@ checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
893
893
 
894
894
  [[package]]
895
895
  name = "raydriver"
896
- version = "0.2.0"
896
+ version = "0.3.0"
897
897
  dependencies = [
898
898
  "async-trait",
899
899
  "env_filter 0.1.4",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "raydriver"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  edition = "2021"
5
5
  description = "Rust-native machine drivers for Rayforge, with Python bindings"
6
6
  readme = "README.md"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: raydriver
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Python :: Implementation :: PyPy
@@ -19,6 +19,7 @@ __all__ = [
19
19
  "parse_grbl_parser_state",
20
20
  "parse_grbl_settings",
21
21
  "parse_msg",
22
+ "parse_opt_flags",
22
23
  "parse_opt_info",
23
24
  "parse_probe_line",
24
25
  "parse_setting_pairs",
@@ -87,6 +88,11 @@ def parse_msg(line: builtins.str) -> typing.Optional[tuple[builtins.str, builtin
87
88
  Parse a `[MSG:key:value]` line into `(key, value)`.
88
89
  """
89
90
 
91
+ def parse_opt_flags(line: builtins.str) -> typing.Optional[builtins.str]:
92
+ r"""
93
+ Extract the compile option flags from an `[OPT:...]` line.
94
+ """
95
+
90
96
  def parse_opt_info(line: builtins.str) -> typing.Optional[builtins.int]:
91
97
  r"""
92
98
  Extract the RX buffer size from an `[OPT:...]` line.
@@ -25,6 +25,7 @@ class DeviceError:
25
25
  @property
26
26
  def description(self) -> builtins.str: ...
27
27
  def __eq__(self, other: builtins.object, /) -> builtins.bool: ...
28
+ def __new__(cls, code: builtins.int, title: builtins.str, description: builtins.str) -> DeviceError: ...
28
29
  def __repr__(self) -> builtins.str: ...
29
30
 
30
31
  @typing.final
@@ -34,31 +35,49 @@ class DeviceState:
34
35
  """
35
36
  @property
36
37
  def status(self) -> DeviceStatus: ...
38
+ @status.setter
39
+ def status(self, value: DeviceStatus) -> None: ...
37
40
  @property
38
41
  def error(self) -> typing.Optional[DeviceError]: ...
42
+ @error.setter
43
+ def error(self, value: typing.Optional[DeviceError]) -> None: ...
39
44
  @property
40
45
  def machine_pos(self) -> builtins.list[typing.Optional[builtins.float]]:
41
46
  r"""
42
47
  Machine position in mm; entries may be `None`.
43
48
  """
49
+ @machine_pos.setter
50
+ def machine_pos(self, value: typing.Sequence[typing.Optional[builtins.float]]) -> None: ...
44
51
  @property
45
52
  def work_pos(self) -> builtins.list[typing.Optional[builtins.float]]:
46
53
  r"""
47
54
  Work position in mm; entries may be `None`.
48
55
  """
56
+ @work_pos.setter
57
+ def work_pos(self, value: typing.Sequence[typing.Optional[builtins.float]]) -> None: ...
49
58
  @property
50
59
  def wco(self) -> builtins.list[typing.Optional[builtins.float]]:
51
60
  r"""
52
61
  Work coordinate offset in mm; entries may be `None`.
53
62
  """
63
+ @wco.setter
64
+ def wco(self, value: typing.Sequence[typing.Optional[builtins.float]]) -> None: ...
54
65
  @property
55
66
  def feed_rate(self) -> typing.Optional[builtins.int]: ...
67
+ @feed_rate.setter
68
+ def feed_rate(self, value: typing.Optional[builtins.int]) -> None: ...
56
69
  @property
57
70
  def spindle_speed(self) -> typing.Optional[builtins.int]: ...
71
+ @spindle_speed.setter
72
+ def spindle_speed(self, value: typing.Optional[builtins.int]) -> None: ...
58
73
  @property
59
74
  def buffer_available(self) -> typing.Optional[builtins.int]: ...
75
+ @buffer_available.setter
76
+ def buffer_available(self, value: typing.Optional[builtins.int]) -> None: ...
60
77
  @property
61
78
  def buffer_rx_available(self) -> typing.Optional[builtins.int]: ...
79
+ @buffer_rx_available.setter
80
+ def buffer_rx_available(self, value: typing.Optional[builtins.int]) -> None: ...
62
81
  def __eq__(self, other: builtins.object, /) -> builtins.bool: ...
63
82
  def __new__(cls) -> DeviceState: ...
64
83
  def __repr__(self) -> builtins.str: ...
@@ -262,6 +262,12 @@ pub fn parse_opt_info(line: &str) -> Option<i64> {
262
262
  .and_then(|caps| caps[3].parse::<i64>().ok())
263
263
  }
264
264
 
265
+ /// Extract the compile option flags from an
266
+ /// `[OPT:<flags>,<planner_buffer_blocks>,<rx_buffer_size>]` line.
267
+ pub fn parse_opt_flags(line: &str) -> Option<String> {
268
+ GRBL_OPT_RE.captures(line).map(|caps| caps[1].to_string())
269
+ }
270
+
265
271
  /// Parses the response from a '$G' command to find the active WCS.
266
272
  /// Example response: `[G54 G17 G21 G90 G94 M5 M9 T0 F0 S0]`
267
273
  pub fn parse_grbl_parser_state(response_lines: &[String]) -> Option<String> {
@@ -60,6 +60,13 @@ fn parse_opt_info(line: &str) -> Option<i64> {
60
60
  parser::parse_opt_info(line)
61
61
  }
62
62
 
63
+ /// Extract the compile option flags from an `[OPT:...]` line.
64
+ #[gen_stub_pyfunction(module = "raydriver.grbl.parser")]
65
+ #[pyfunction]
66
+ fn parse_opt_flags(line: &str) -> Option<String> {
67
+ parser::parse_opt_flags(line)
68
+ }
69
+
63
70
  /// Parse a `[MSG:key:value]` line into `(key, value)`.
64
71
  #[gen_stub_pyfunction(module = "raydriver.grbl.parser")]
65
72
  #[pyfunction]
@@ -202,6 +209,7 @@ pub(crate) fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
202
209
  "parse_version",
203
210
  "parse_ver",
204
211
  "parse_opt_info",
212
+ "parse_opt_flags",
205
213
  "parse_msg",
206
214
  "extract_device_name",
207
215
  "extract_device_name_from_output",
@@ -225,6 +233,7 @@ pub(crate) fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
225
233
  m.add_function(pyo3::wrap_pyfunction!(parse_version, m.clone())?)?;
226
234
  m.add_function(pyo3::wrap_pyfunction!(parse_ver, m.clone())?)?;
227
235
  m.add_function(pyo3::wrap_pyfunction!(parse_opt_info, m.clone())?)?;
236
+ m.add_function(pyo3::wrap_pyfunction!(parse_opt_flags, m.clone())?)?;
228
237
  m.add_function(pyo3::wrap_pyfunction!(parse_msg, m.clone())?)?;
229
238
  m.add_function(pyo3::wrap_pyfunction!(extract_device_name, m.clone())?)?;
230
239
  m.add_function(pyo3::wrap_pyfunction!(
@@ -18,7 +18,7 @@ pyo3_stub_gen::module_doc!("raydriver.grbl.types", "{}", MODULE_DOC);
18
18
 
19
19
  /// Machine state as reported in Grbl status reports.
20
20
  #[gen_stub_pyclass_enum]
21
- #[pyclass(module = "raydriver.grbl.types", eq, eq_int, skip_from_py_object)]
21
+ #[pyclass(module = "raydriver.grbl.types", eq, eq_int, from_py_object)]
22
22
  #[derive(Clone, Copy, PartialEq, Eq)]
23
23
  pub enum DeviceStatus {
24
24
  #[pyo3(name = "UNKNOWN")]
@@ -78,6 +78,29 @@ impl From<core::DeviceStatus> for DeviceStatus {
78
78
  }
79
79
  }
80
80
 
81
+ impl From<DeviceStatus> for core::DeviceStatus {
82
+ fn from(status: DeviceStatus) -> Self {
83
+ match status {
84
+ DeviceStatus::Unknown => Self::Unknown,
85
+ DeviceStatus::Idle => Self::Idle,
86
+ DeviceStatus::Run => Self::Run,
87
+ DeviceStatus::Hold => Self::Hold,
88
+ DeviceStatus::Jog => Self::Jog,
89
+ DeviceStatus::Alarm => Self::Alarm,
90
+ DeviceStatus::Door => Self::Door,
91
+ DeviceStatus::Check => Self::Check,
92
+ DeviceStatus::Home => Self::Home,
93
+ DeviceStatus::Sleep => Self::Sleep,
94
+ DeviceStatus::Tool => Self::Tool,
95
+ DeviceStatus::Queue => Self::Queue,
96
+ DeviceStatus::Lock => Self::Lock,
97
+ DeviceStatus::Unlock => Self::Unlock,
98
+ DeviceStatus::Cycle => Self::Cycle,
99
+ DeviceStatus::Test => Self::Test,
100
+ }
101
+ }
102
+ }
103
+
81
104
  #[gen_stub_pymethods]
82
105
  #[pymethods]
83
106
  impl DeviceStatus {
@@ -110,7 +133,7 @@ impl DeviceStatus {
110
133
 
111
134
  /// An error or alarm with code, title and description.
112
135
  #[gen_stub_pyclass]
113
- #[pyclass(module = "raydriver.grbl.types", eq, skip_from_py_object)]
136
+ #[pyclass(module = "raydriver.grbl.types", eq, from_py_object)]
114
137
  #[derive(Clone, PartialEq)]
115
138
  pub struct DeviceError {
116
139
  pub(crate) inner: core::DeviceError,
@@ -125,6 +148,13 @@ impl DeviceError {
125
148
  #[gen_stub_pymethods]
126
149
  #[pymethods]
127
150
  impl DeviceError {
151
+ #[new]
152
+ fn new_py(code: i32, title: String, description: String) -> Self {
153
+ Self {
154
+ inner: core::DeviceError::new(code, &title, &description),
155
+ }
156
+ }
157
+
128
158
  #[getter]
129
159
  fn code(&self) -> i32 {
130
160
  self.inner.code
@@ -174,49 +204,94 @@ impl DeviceState {
174
204
  self.inner.status.into()
175
205
  }
176
206
 
207
+ #[setter]
208
+ fn set_status(&mut self, status: DeviceStatus) {
209
+ self.inner.status = status.into();
210
+ }
211
+
177
212
  #[getter]
178
213
  fn error(&self) -> Option<DeviceError> {
179
214
  self.inner.error.clone().map(DeviceError::new)
180
215
  }
181
216
 
217
+ #[setter]
218
+ fn set_error(&mut self, error: Option<&DeviceError>) {
219
+ self.inner.error = error.map(|e| e.inner.clone());
220
+ }
221
+
182
222
  /// Machine position in mm; entries may be `None`.
183
223
  #[getter]
184
224
  fn machine_pos(&self) -> Vec<Option<f64>> {
185
225
  self.inner.machine_pos.clone()
186
226
  }
187
227
 
228
+ #[setter]
229
+ fn set_machine_pos(&mut self, pos: Vec<Option<f64>>) {
230
+ self.inner.machine_pos = pos;
231
+ }
232
+
188
233
  /// Work position in mm; entries may be `None`.
189
234
  #[getter]
190
235
  fn work_pos(&self) -> Vec<Option<f64>> {
191
236
  self.inner.work_pos.clone()
192
237
  }
193
238
 
239
+ #[setter]
240
+ fn set_work_pos(&mut self, pos: Vec<Option<f64>>) {
241
+ self.inner.work_pos = pos;
242
+ }
243
+
194
244
  /// Work coordinate offset in mm; entries may be `None`.
195
245
  #[getter]
196
246
  fn wco(&self) -> Vec<Option<f64>> {
197
247
  self.inner.wco.clone()
198
248
  }
199
249
 
250
+ #[setter]
251
+ fn set_wco(&mut self, wco: Vec<Option<f64>>) {
252
+ self.inner.wco = wco;
253
+ }
254
+
200
255
  #[getter]
201
256
  fn feed_rate(&self) -> Option<i64> {
202
257
  self.inner.feed_rate
203
258
  }
204
259
 
260
+ #[setter]
261
+ fn set_feed_rate(&mut self, feed_rate: Option<i64>) {
262
+ self.inner.feed_rate = feed_rate;
263
+ }
264
+
205
265
  #[getter]
206
266
  fn spindle_speed(&self) -> Option<i64> {
207
267
  self.inner.spindle_speed
208
268
  }
209
269
 
270
+ #[setter]
271
+ fn set_spindle_speed(&mut self, spindle_speed: Option<i64>) {
272
+ self.inner.spindle_speed = spindle_speed;
273
+ }
274
+
210
275
  #[getter]
211
276
  fn buffer_available(&self) -> Option<i64> {
212
277
  self.inner.buffer_available
213
278
  }
214
279
 
280
+ #[setter]
281
+ fn set_buffer_available(&mut self, buffer_available: Option<i64>) {
282
+ self.inner.buffer_available = buffer_available;
283
+ }
284
+
215
285
  #[getter]
216
286
  fn buffer_rx_available(&self) -> Option<i64> {
217
287
  self.inner.buffer_rx_available
218
288
  }
219
289
 
290
+ #[setter]
291
+ fn set_buffer_rx_available(&mut self, buffer_rx_available: Option<i64>) {
292
+ self.inner.buffer_rx_available = buffer_rx_available;
293
+ }
294
+
220
295
  fn __repr__(&self) -> String {
221
296
  format!(
222
297
  "DeviceState(status={}, feed_rate={:?})",
@@ -14,6 +14,7 @@ from raydriver.grbl.parser import (
14
14
  parse_grbl_parser_state,
15
15
  parse_grbl_settings,
16
16
  parse_msg,
17
+ parse_opt_flags,
17
18
  parse_opt_info,
18
19
  parse_probe_line,
19
20
  parse_setting_pairs,
@@ -336,6 +337,16 @@ class TestOptInfo:
336
337
  assert parse_opt_info("ok") is None
337
338
 
338
339
 
340
+ class TestOptFlags:
341
+ def test_valid(self):
342
+ assert parse_opt_flags("[OPT:V,15,127]") == "V"
343
+ assert parse_opt_flags("[OPT:VMPH,63,511]") == "VMPH"
344
+
345
+ def test_invalid(self):
346
+ assert parse_opt_flags("[VER:1.1h:]") is None
347
+ assert parse_opt_flags("ok") is None
348
+
349
+
339
350
  class TestParseMsg:
340
351
  def test_valid(self):
341
352
  assert parse_msg("[MSG:machine:Ortur]") == ("machine", "Ortur")
@@ -0,0 +1,171 @@
1
+ """Tests for the shared value types: DeviceState, DeviceStatus and
2
+ DeviceError.
3
+
4
+ Covers the Python-facing construction surface (setters and the
5
+ DeviceError constructor) and the merge/round-trip contract that
6
+ embedders rely on when they keep their own state object and feed it
7
+ back to ``parse_state`` as the default.
8
+ """
9
+
10
+ from raydriver.grbl.parser import parse_state
11
+ from raydriver.grbl.types import DeviceError, DeviceState, DeviceStatus
12
+
13
+
14
+ def build_state(**overrides) -> DeviceState:
15
+ """A fully populated DeviceState, built through the Python
16
+ setters, with optional per-field overrides."""
17
+ state = DeviceState()
18
+ state.status = overrides.get("status", DeviceStatus.RUN)
19
+ state.error = overrides.get("error", None)
20
+ state.machine_pos = overrides.get("machine_pos", [11.0, 22.0, 33.0])
21
+ state.work_pos = overrides.get("work_pos", [10.0, 20.0, 30.0])
22
+ state.wco = overrides.get("wco", [1.0, 2.0, 3.0])
23
+ state.feed_rate = overrides.get("feed_rate", 500)
24
+ state.spindle_speed = overrides.get("spindle_speed", 1000)
25
+ state.buffer_available = overrides.get("buffer_available", 62)
26
+ state.buffer_rx_available = overrides.get("buffer_rx_available", 100)
27
+ return state
28
+
29
+
30
+ class TestDeviceStateDefaults:
31
+ def test_fresh_state_defaults(self):
32
+ state = DeviceState()
33
+ assert state.status == DeviceStatus.UNKNOWN
34
+ assert state.error is None
35
+ assert state.machine_pos == [None, None, None]
36
+ assert state.work_pos == [None, None, None]
37
+ assert state.wco == [0.0, 0.0, 0.0]
38
+ assert state.feed_rate is None
39
+ assert state.spindle_speed is None
40
+ assert state.buffer_available is None
41
+ assert state.buffer_rx_available is None
42
+
43
+ def test_status_name(self):
44
+ assert DeviceStatus.ALARM.name == "ALARM"
45
+ assert parse_state("<Idle|MPos:1,2,3>").status.name == "IDLE"
46
+
47
+
48
+ class TestDeviceStateSetters:
49
+ def test_set_all_fields(self):
50
+ state = build_state()
51
+ assert state.status == DeviceStatus.RUN
52
+ assert state.machine_pos == [11.0, 22.0, 33.0]
53
+ assert state.work_pos == [10.0, 20.0, 30.0]
54
+ assert state.wco == [1.0, 2.0, 3.0]
55
+ assert state.feed_rate == 500
56
+ assert state.spindle_speed == 1000
57
+ assert state.buffer_available == 62
58
+ assert state.buffer_rx_available == 100
59
+
60
+ def test_set_error(self):
61
+ state = DeviceState()
62
+ error = DeviceError(20, "Unsupported Command", "details")
63
+ state.error = error
64
+ assert state.error == error
65
+ assert state.error.code == 20
66
+ assert state.error.title == "Unsupported Command"
67
+ assert state.error.description == "details"
68
+
69
+ def test_clear_error_with_none(self):
70
+ state = build_state(error=DeviceError(1, "t", "d"))
71
+ state.error = None
72
+ assert state.error is None
73
+
74
+ def test_unknown_entries_survive_setter(self):
75
+ state = DeviceState()
76
+ state.machine_pos = [1.0, None, 3.0]
77
+ assert state.machine_pos == [1.0, None, 3.0]
78
+
79
+ def test_status_from_member_lookup(self):
80
+ state = DeviceState()
81
+ state.status = getattr(DeviceStatus, "HOLD")
82
+ assert state.status == DeviceStatus.HOLD
83
+
84
+
85
+ class TestDeviceStateEquality:
86
+ def test_equal_states_compare_equal(self):
87
+ assert build_state() == build_state()
88
+
89
+ def test_status_difference_compares_unequal(self):
90
+ assert build_state() != build_state(status=DeviceStatus.IDLE)
91
+
92
+ def test_error_difference_compares_unequal(self):
93
+ plain = build_state()
94
+ failing = build_state(error=DeviceError(9, "t", "d"))
95
+ assert plain != failing
96
+
97
+ def test_position_difference_compares_unequal(self):
98
+ assert build_state() != build_state(machine_pos=[1.0, 2.0, 3.0])
99
+
100
+
101
+ class TestDeviceError:
102
+ def test_constructor_and_getters(self):
103
+ error = DeviceError(9, "Commands Locked", "Clear the alarm state.")
104
+ assert error.code == 9
105
+ assert error.title == "Commands Locked"
106
+ assert error.description == "Clear the alarm state."
107
+
108
+ def test_equality(self):
109
+ assert DeviceError(1, "a", "b") == DeviceError(1, "a", "b")
110
+ assert DeviceError(1, "a", "b") != DeviceError(2, "a", "b")
111
+
112
+ def test_repr_contains_code_and_title(self):
113
+ text = repr(DeviceError(7, "Memory Error", "d"))
114
+ assert "7" in text
115
+ assert "Memory Error" in text
116
+
117
+
118
+ class TestParseStateWithBuiltDefault:
119
+ """parse_state merges the report into a caller-provided default;
120
+ the default may be a state constructed through the Python
121
+ setters (the round-trip embedders depend on)."""
122
+
123
+ def test_report_updates_status_of_built_state(self):
124
+ state = parse_state(
125
+ "<Idle|MPos:1.0,2.0,3.0>", build_state(status=DeviceStatus.ALARM)
126
+ )
127
+ assert state.status == DeviceStatus.IDLE
128
+ assert state.error is None
129
+
130
+ def test_report_keeps_unreported_fields_of_built_state(self):
131
+ state = parse_state(
132
+ "<Idle|WPos:10.0,20.0,30.0>",
133
+ build_state(feed_rate=500, wco=[1.0, 2.0, 3.0]),
134
+ )
135
+ assert state.feed_rate == 500
136
+ assert state.machine_pos == [11.0, 22.0, 33.0]
137
+
138
+ def test_alarm_report_sets_error_on_built_state(self):
139
+ state = parse_state("<Alarm:1|FS:500,0>", build_state(feed_rate=100))
140
+ assert state.status == DeviceStatus.ALARM
141
+ assert state.error is not None
142
+ assert state.error.code == 1
143
+ assert state.feed_rate == 500
144
+
145
+ def test_repeated_merge_is_stable(self):
146
+ merged = build_state()
147
+ for _ in range(3):
148
+ merged = parse_state("<Hold|MPos:11.0,22.0,33.0>", merged)
149
+ assert merged.status == DeviceStatus.HOLD
150
+ assert merged.machine_pos == [11.0, 22.0, 33.0]
151
+ assert merged.feed_rate == 500
152
+
153
+
154
+ class TestStateRoundTrip:
155
+ """Copying a parsed state field-by-field through the setters
156
+ reproduces it exactly — the contract the Rayforge conversion
157
+ helpers implement."""
158
+
159
+ def test_copy_via_setters_reproduces_parsed_state(self):
160
+ parsed = parse_state("<Run|MPos:1.0,2.0,3.0|FS:800,50|Bf:14,120>")
161
+ copy = DeviceState()
162
+ copy.status = parsed.status
163
+ copy.error = parsed.error
164
+ copy.machine_pos = parsed.machine_pos
165
+ copy.work_pos = parsed.work_pos
166
+ copy.wco = parsed.wco
167
+ copy.feed_rate = parsed.feed_rate
168
+ copy.spindle_speed = parsed.spindle_speed
169
+ copy.buffer_available = parsed.buffer_available
170
+ copy.buffer_rx_available = parsed.buffer_rx_available
171
+ assert copy == parsed
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes