From 9f6440a8dcaa01dab5ff9d5b97628e926ab1f436 Mon Sep 17 00:00:00 2001 From: Cameron Brooks Date: Sat, 27 Jun 2026 20:27:19 -0400 Subject: [PATCH] feat(host): temperatures() query + pre-0.1.0 polish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add MarlinHost.temperatures(): query M105 and return the temperature fields. Marlin carries the M105 report on the `ok` line itself, so — unlike M114 — query() returns nothing; this reads the terminal response's fields, completing the MVP query trio (M114/M115/M105). - README: `early scaffolding` -> `MVP`, hardware-validated status. - SerialTransport: note assert_hold=0.2 is validated on RAMPS (was flagged unvalidated). Validated on real hardware (T read off the ok line); 112 hardware-free tests pass. --- README.md | 7 +++++-- marlin_host/host.py | 10 ++++++++++ marlin_host/transport.py | 5 +++-- tests/hardware/test_hardware.py | 2 +- tests/test_host.py | 14 ++++++++++++++ 5 files changed, 33 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3be7da3..8aca4ee 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,11 @@ PC/host end of the line-numbered, checksummed, `ok`-acknowledged G-code conversation that hosts like OctoPrint and Pronterface speak to a printer or CNC running Marlin. Neutral and standalone; not tied to any one application. -> **Status:** early scaffolding. Developed against the Marlin firmware source -> and official docs. Licensed Apache-2.0. +> **Status:** MVP. Connect, M115 negotiation, reliable framed send, streaming, +> queries, and out-of-band e-stop are implemented and **validated against real +> hardware** (RAMPS 1.4 / Marlin `bugfix-2.1.x`, logic level — motion/thermal +> validation under power is ongoing). Developed against the Marlin firmware +> source and official docs. Licensed Apache-2.0. ## Repository conventions diff --git a/marlin_host/host.py b/marlin_host/host.py index 3bce204..c388864 100644 --- a/marlin_host/host.py +++ b/marlin_host/host.py @@ -318,6 +318,16 @@ def capabilities(self) -> Profile: firmware, caps, _ = self._query_m115() return Profile(firmware=firmware, caps=caps) + def temperatures(self) -> Mapping[str, float]: + """Query M105 and return the reported temperature fields, e.g. + ``{'T': 20.6, 'B': 0.0, '@': 0.0}`` (hotend ``T``, bed ``B``, power ``@``). + + Unlike M114, Marlin carries the M105 report on the ``ok`` line itself, so — + unlike :meth:`query` — the data is the terminal response's fields, not a + preceding report line. Returns an empty mapping if the board reports none. + """ + return self.send("M105").fields or {} + def stream( self, program: Iterable[str], *, poll_interval: float = DEFAULT_PAUSE_POLL ) -> Iterator[StreamProgress]: diff --git a/marlin_host/transport.py b/marlin_host/transport.py index e97c705..94fe7f0 100644 --- a/marlin_host/transport.py +++ b/marlin_host/transport.py @@ -109,8 +109,9 @@ class SerialTransport: ``reset_on_open=False`` to skip the pulse (native-USB boards that ignore DTR, or to attach to a running controller). - NOTE: the DTR-pulse timing is board-specific and unvalidated against hardware; - expect to tune :meth:`reset` during the first real session — see issue #4. + NOTE: the DTR-pulse timing is board-specific. The default ``assert_hold=0.2`` + is validated on RAMPS 1.4 / Marlin ``bugfix-2.1.x`` (reliably catches ``start``); + other adapters may need tuning — see issue #4. """ def __init__( diff --git a/tests/hardware/test_hardware.py b/tests/hardware/test_hardware.py index 96933f2..7b73e6a 100644 --- a/tests/hardware/test_hardware.py +++ b/tests/hardware/test_hardware.py @@ -46,7 +46,7 @@ def test_connect_and_negotiate(rig: SimpleNamespace) -> None: def test_queries(rig: SimpleNamespace) -> None: assert rig.host.query("M114") # at least the position report line - assert rig.host.send("M105").is_ack # temperature rides on the `ok` line + assert "T" in rig.host.temperatures() # hotend temp, read off the `ok` line def test_reliable_stream(rig: SimpleNamespace) -> None: diff --git a/tests/test_host.py b/tests/test_host.py index 96e51de..dd6bb26 100644 --- a/tests/test_host.py +++ b/tests/test_host.py @@ -392,3 +392,17 @@ def test_reliable_send_recovers_from_back_to_back_resends() -> None: assert host.send("G1 X20").is_ack assert sim.expected == 3 assert t.read_line() is None + + +def test_temperatures_reads_fields_from_the_ok_line() -> None: + # Marlin answers M105 on the `ok` line itself (real RAMPS: `ok T:20.63 /0.00 @:0`), + # so the temps are terminal-response fields, not a preceding report line. + host = MarlinHost(FakeTransport(responder=lambda _line: ["ok T:20.63 /0.00 @:0"])) + temps = host.temperatures() + assert temps["T"] == pytest.approx(20.63) + assert temps["@"] == pytest.approx(0.0) + + +def test_temperatures_empty_when_board_reports_none() -> None: + host = MarlinHost(FakeTransport(responder=lambda _line: ["ok"])) + assert host.temperatures() == {}