feat(STAR): read recorded TADM pressure traces from a PIP channel#1139
Merged
Conversation
Add methods on PIPChannel to read back a recorded TADM (Total Aspiration and Dispense Monitoring) pressure trace into a list of floats: - request_tadm_recording_length(slot): number of recorded samples in a measurement slot via Px:QL. - request_tadm_recording(num_samples=None, ...): read the trace via Px:QN. num_samples=None queries the length and reads the whole trace, batched (<=50 samples/command, the firmware cap). - request_tadm_recording_value(index): single sample. QN sample fields are sign + 4 digits, so they parse with "qn#### (n)". Values are the raw signed firmware pressure samples cast to float (the firmware does not document a physical unit). Hardware-validated on a STAR: a 150 uL aspiration recorded 106 samples, with QL and QN agreeing on the count and the trace showing the expected underpressure dip during aspiration. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Query commands ("Q*": QN, QL, QS, QF, QC, ...) are read-only, so they take
no firmware lock and run fully in parallel with an in-flight command -- like
the existing R* requests. This lets a Px:QN TADM-buffer read be issued while
a C0 master command (e.g. an aspiration) is still executing.
Caveat: C0:QS with an `on` parameter (cover reset_output) is a write, not a
query; it is rare and never issued during pipetting, so it rides along as
lock-free with the rest.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
rickwierenga
force-pushed
the
feat/star-tadm-readback
branch
from
July 1, 2026 00:00
9dd17c5 to
1970604
Compare
rickwierenga
force-pushed
the
feat/star-tadm-readback
branch
from
July 1, 2026 01:10
010dcf8 to
aff4846
Compare
Member
Author
Usage samplesRecording a TADM trace needs only 1. Simple read-back (await the aspiration, then read the finalized trace): import asyncio
from pylabrobot.hamilton.liquid_handlers.star.star import STAR
from pylabrobot.hamilton.liquid_handlers.star.pip_backend import STARPIPBackend
from pylabrobot.resources.hamilton.tip_carriers import TIP_CAR_480_A00
from pylabrobot.resources.hamilton.tip_racks import hamilton_96_tiprack_1000uL_filter
from pylabrobot.resources.hamilton.plate_carriers import PLT_CAR_L5AC_A00
from pylabrobot.resources.corning.plates import Cor_96_wellplate_360ul_Fb
async def main():
star = STAR()
tip_carrier = TIP_CAR_480_A00(name="tip carrier")
tip_carrier[0] = tips = hamilton_96_tiprack_1000uL_filter(name="tips")
plate_carrier = PLT_CAR_L5AC_A00(name="plate carrier")
plate_carrier[0] = plate = Cor_96_wellplate_360ul_Fb(name="plate")
star.deck.assign_child_resource(tip_carrier, rails=1)
star.deck.assign_child_resource(plate_carrier, rails=15)
await star.setup()
ch = star.driver.pip.channels[0]
await star.pip.pick_up_tips(tips["A1"], use_channels=[0])
# tadm_recording_mode="all" is the whole TADM setup; BG is issued automatically.
params = STARPIPBackend.AspirateParams(tadm_recording_mode="all")
await star.pip.aspirate(plate["A1"], vols=[150.0], use_channels=[0], backend_params=params)
trace = await ch.request_tadm_recording() # list[float], one pressure sample per firmware tick
print(f"recorded {len(trace)} samples; first 8: {[round(x) for x in trace[:8]]}")
await star.pip.dispense(plate["A1"], vols=[150.0], use_channels=[0])
await star.pip.drop_tips(tips["A1"], use_channels=[0])
await star.stop()
asyncio.run(main())2. Live stream (run the aspiration as a background task and iterate import asyncio
from pylabrobot.hamilton.liquid_handlers.star.star import STAR
from pylabrobot.hamilton.liquid_handlers.star.pip_backend import STARPIPBackend
from pylabrobot.resources.hamilton.tip_carriers import TIP_CAR_480_A00
from pylabrobot.resources.hamilton.tip_racks import hamilton_96_tiprack_1000uL_filter
from pylabrobot.resources.hamilton.plate_carriers import PLT_CAR_L5AC_A00
from pylabrobot.resources.corning.plates import Cor_96_wellplate_360ul_Fb
async def main():
star = STAR()
tip_carrier = TIP_CAR_480_A00(name="tip carrier")
tip_carrier[0] = tips = hamilton_96_tiprack_1000uL_filter(name="tips")
plate_carrier = PLT_CAR_L5AC_A00(name="plate carrier")
plate_carrier[0] = plate = Cor_96_wellplate_360ul_Fb(name="plate")
star.deck.assign_child_resource(tip_carrier, rails=1)
star.deck.assign_child_resource(plate_carrier, rails=15)
await star.setup()
ch = star.driver.pip.channels[0]
await star.pip.pick_up_tips(tips["A1"], use_channels=[0])
params = STARPIPBackend.AspirateParams(tadm_recording_mode="all")
# background task so we can read while it runs (Q* queries are lock-free)
asp = asyncio.create_task(star.pip.aspirate(
plate["A1"], vols=[150.0], use_channels=[0], backend_params=params))
async for index, value in ch.stream_tadm():
print(index, value)
await asp
await star.pip.dispense(plate["A1"], vols=[150.0], use_channels=[0])
await star.pip.drop_tips(tips["A1"], use_channels=[0])
await star.stop()
asyncio.run(main())Both return identical data; #1 gets it after the command returns, #2 gets it as soon as the recording phase finalizes. Validated on a STAR. The buffer is overwritten per measurement (not appended), so consecutive aspirations don't accumulate. |
Recording a TADM trace needs only tadm_recording_mode="all" (verified on hardware: records without any limit-curve or monitoring-mode setup): - Aspirate/dispense auto-issue BG (begin monitoring) on each involved channel whenever recording is requested, so no manual arming is needed. - PIPChannel.begin_tadm_monitoring() wraps Px:BG (used by the auto path). - PIPChannel.stream_tadm(): run the op in the background and iterate this to get the trace as soon as it finalizes (mid-command, before the command returns). Gated on the QL `qm` flag; the firmware exposes no live count and the buffer can't be cleared, so per-sample streaming isn't offered. - AspirateParams/DispenseParams: `tadm_algorithm`/`recording_mode` are replaced by a single `tadm_recording_mode: Literal["off","errors_only","all"]`. The limit-curve evaluation flag (use_tadm) is dropped — it needs limit-curve management this driver doesn't provide, and recording doesn't use it. The buffer is overwritten in place per measurement (verified: two consecutive aspirations leave the sample count at a single measurement's length, not the sum), so recordings don't accumulate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
rickwierenga
force-pushed
the
feat/star-tadm-readback
branch
from
July 1, 2026 01:31
aff4846 to
bc1a4cc
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds methods on
PIPChannelto read a recorded TADM (Total Aspiration and Dispense Monitoring) pressure trace back into a list of floats.request_tadm_recording_length(slot=0)— number of recorded samples in a measurement slot viaPx:QL.request_tadm_recording(num_samples=None, start=0, batch_size=50, slot=0)— read the trace viaPx:QN.num_samples=Nonequeries the length and reads the whole trace, batched (≤50 samples/command, the firmware cap). ReturnsList[float].request_tadm_recording_value(index)— single sample.Notes
qn#### (n)(the 5-#form drops the last value and breaks single-sample reads).Testing
Hardware-validated on a STAR: a 150 µL single-channel aspiration with
recording_mode=2recorded 106 samples;QL(length) andQN(read-back) agree on the count, and the trace shows the expected underpressure dip during aspiration (0 → ~−200 → 0). Reproduced across two runs.Recording requires the firmware prelude
AQ→AY→BG→AFbefore aspirating (sotadm_algorithm=Truehas a valid limit curve); the read-back methods themselves are independent of that.🤖 Generated with Claude Code