Add STARBackend.channels_probe_z_using_ztouch for parallel force probing - #1067

Draft
BioCam wants to merge 2 commits into
PyLabRobot:mainfrom
BioCam:create-cascading-multi-channel-ztouch
Draft

Add STARBackend.channels_probe_z_using_ztouch for parallel force probing#1067
BioCam wants to merge 2 commits into
PyLabRobot:mainfrom
BioCam:create-cascading-multi-channel-ztouch

Conversation

@BioCam

Copy link
Copy Markdown
Collaborator

The Problem

PyLabRobot has a single-channel force-sensed Z probe (ztouch_probe_z_height_using_channel, firmware ZH) but no batched, parallel equivalent. Multi-channel workflows that need this measurement currently require careful and time-consuming planning.

Concrete needs that surface this gap:

  • Verifying that lids, adapters, or labware are seated at the expected Z before a workflow continues.
  • Calibrating the Z of unknown or replaceable deck features without manual teach-in.
  • Capturing per-channel Z offsets against a calibration block in one shot rather than N sequential probes.

Naively wrapping the primitive in asyncio.gather does not solve this, because of two parallelism hazards specific to force-sensed probing on the STAR head:

  • Carriage-force cross-talk. All channels share one rigid Z-drive carriage. When channels descend simultaneously, each channel's touchdown produces a force transient that perturbs the PWM-based detection threshold on the others, biasing every measurement after the first to land.

  • C0-command serialization. The single-channel primitive issues move_channel_z (firmware JZ via C0) when post_detection_dist != 0 (TODO: move to PX version), and request_tip_len_on_channel (also C0) when tip_len=None. Both route through the master controller and serialize the gather - defeating the parallelism the caller is trying to get.

A correct batched implementation has to address both of these explicitly.

PR Content / Solution

Adds channels_probe_z_using_ztouch to the STAR backend: a batched, parallel force-sensed Z probe that returns the absolute deck-frame Z of whatever solid surface each channel first touches.

Architecture

Reuses the same batched-channel orchestration skeleton that probe_liquid_heights already uses for cLLD/pLLD: plan_batches builds channel/X/Y batches, execute_batched handles inter-batch X/Y positioning and Z safety, and the per-batch callback fans out ztouch_probe_z_height_using_channel across the batch's channels under asyncio.gather. No reinvention of X/Y partitioning, no-go-zone handling, or inter-batch Z safety.

Carriage cross-talk: stagger the channel starts

Within each batch, channel starts are staggered by inter_channel_start_delay (default 0.3 s) so contact-force transients don't superpose. The stagger lives in a small _delayed(delay, factory) helper that takes a coroutine factory (not a coroutine) to avoid un-awaited-coroutine warnings if the gather is cancelled mid-sleep. Tunable to 0.0 for callers who've validated mechanically isolated geometry.

C0-avoidance: keep the gather truly parallel

The inner ZH call receives post_detection_dist=0 and an explicit tip_len so the primitive does not issue move_channel_z or request_tip_len_on_channel. Channel raising between batches and at the end is handled by the existing min_traverse_height_during_command and z_position_at_end_of_command kwargs.

Safety and aggregation

Outer try / except BaseException lifts all channels to Z safety on any exception (firmware error, KeyboardInterrupt, SystemExit, asyncio.CancelledError) before re-raising. Aggregated Z is rounded to the firmware z-drive quantum (0.01 mm) so users don't see float-averaging noise across replicates.

Design decisions

  • Returns absolute deck-frame Z, not relative to a container or any other reference. The method targets arbitrary surfaces; there's no "in this well" frame to subtract. Callers wanting relative values subtract themselves.
  • No public post_detection_dist kwarg. Using it inside the gather would issue a C0 JZ per channel and serialize the parallelism. Channel raising is already covered by min_traverse_height_during_command / z_position_at_end_of_command.
  • except BaseException, not except Exception. Catches KeyboardInterrupt, SystemExit, and asyncio.CancelledError too - all cases where channels might be pressing on a surface when control is lost. Matches the pattern already in execute_batched.
  • inter_channel_start_delay default 0.3 s. Empirical default covering a typical ~tens-of-ms contact transient with margin; tunable to 0.0 for callers who don't need the decoupling.
  • Round aggregated Z to 0.01 mm.

Batched ZH probing across multiple channels, modeled on probe_liquid_heights:
plan_batches -> execute_batched -> asyncio.gather over `ztouch_probe_z_height_using_channel`.
Channel starts within a batch are staggered by inter_channel_start_delay (default
0.3 s) to decouple contact-force transients on the shared carriage -> creating a "cascading motion".
The inner ztouch_probe_z_height_using_channel call receives post_detection_dist=0
and an explicit tip_len so no C0 commands (move_channel_z,
request_tip_len_on_channel) serialize the gather. Channel raising is handled by
the existing min_traverse_height_during_command / z_position_at_end_of_command
kwargs.
Outer try/except BaseException lifts all channels to Z safety on any exception
(firmware error, KeyboardInterrupt, CancelledError) before re-raising. Aggregated
Z is rounded to the firmware z-drive quantum (0.01 mm). Chatterbox stub
included.
@BioCam
BioCam marked this pull request as draft May 28, 2026 18:45
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@BioCam
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Add STARBackend.channels_probe_z_using_ztouch for parallel force probing - #1067

Draft
BioCam wants to merge 2 commits into
PyLabRobot:mainfrom
BioCam:create-cascading-multi-channel-ztouch
Draft

Add STARBackend.channels_probe_z_using_ztouch for parallel force probing#1067
BioCam wants to merge 2 commits into
PyLabRobot:mainfrom
BioCam:create-cascading-multi-channel-ztouch

Conversation

@BioCam

Copy link
Copy Markdown
Collaborator

The Problem

PyLabRobot has a single-channel force-sensed Z probe (ztouch_probe_z_height_using_channel, firmware ZH) but no batched, parallel equivalent. Multi-channel workflows that need this measurement currently require careful and time-consuming planning.

Concrete needs that surface this gap:

  • Verifying that lids, adapters, or labware are seated at the expected Z before a workflow continues.
  • Calibrating the Z of unknown or replaceable deck features without manual teach-in.
  • Capturing per-channel Z offsets against a calibration block in one shot rather than N sequential probes.

Naively wrapping the primitive in asyncio.gather does not solve this, because of two parallelism hazards specific to force-sensed probing on the STAR head:

  • Carriage-force cross-talk. All channels share one rigid Z-drive carriage. When channels descend simultaneously, each channel's touchdown produces a force transient that perturbs the PWM-based detection threshold on the others, biasing every measurement after the first to land.

  • C0-command serialization. The single-channel primitive issues move_channel_z (firmware JZ via C0) when post_detection_dist != 0 (TODO: move to PX version), and request_tip_len_on_channel (also C0) when tip_len=None. Both route through the master controller and serialize the gather - defeating the parallelism the caller is trying to get.

A correct batched implementation has to address both of these explicitly.

PR Content / Solution

Adds channels_probe_z_using_ztouch to the STAR backend: a batched, parallel force-sensed Z probe that returns the absolute deck-frame Z of whatever solid surface each channel first touches.

Architecture

Reuses the same batched-channel orchestration skeleton that probe_liquid_heights already uses for cLLD/pLLD: plan_batches builds channel/X/Y batches, execute_batched handles inter-batch X/Y positioning and Z safety, and the per-batch callback fans out ztouch_probe_z_height_using_channel across the batch's channels under asyncio.gather. No reinvention of X/Y partitioning, no-go-zone handling, or inter-batch Z safety.

Carriage cross-talk: stagger the channel starts

Within each batch, channel starts are staggered by inter_channel_start_delay (default 0.3 s) so contact-force transients don't superpose. The stagger lives in a small _delayed(delay, factory) helper that takes a coroutine factory (not a coroutine) to avoid un-awaited-coroutine warnings if the gather is cancelled mid-sleep. Tunable to 0.0 for callers who've validated mechanically isolated geometry.

C0-avoidance: keep the gather truly parallel

The inner ZH call receives post_detection_dist=0 and an explicit tip_len so the primitive does not issue move_channel_z or request_tip_len_on_channel. Channel raising between batches and at the end is handled by the existing min_traverse_height_during_command and z_position_at_end_of_command kwargs.

Safety and aggregation

Outer try / except BaseException lifts all channels to Z safety on any exception (firmware error, KeyboardInterrupt, SystemExit, asyncio.CancelledError) before re-raising. Aggregated Z is rounded to the firmware z-drive quantum (0.01 mm) so users don't see float-averaging noise across replicates.

Design decisions

  • Returns absolute deck-frame Z, not relative to a container or any other reference. The method targets arbitrary surfaces; there's no "in this well" frame to subtract. Callers wanting relative values subtract themselves.
  • No public post_detection_dist kwarg. Using it inside the gather would issue a C0 JZ per channel and serialize the parallelism. Channel raising is already covered by min_traverse_height_during_command / z_position_at_end_of_command.
  • except BaseException, not except Exception. Catches KeyboardInterrupt, SystemExit, and asyncio.CancelledError too - all cases where channels might be pressing on a surface when control is lost. Matches the pattern already in execute_batched.
  • inter_channel_start_delay default 0.3 s. Empirical default covering a typical ~tens-of-ms contact transient with margin; tunable to 0.0 for callers who don't need the decoupling.
  • Round aggregated Z to 0.01 mm.

Batched ZH probing across multiple channels, modeled on probe_liquid_heights:
plan_batches -> execute_batched -> asyncio.gather over `ztouch_probe_z_height_using_channel`.
Channel starts within a batch are staggered by inter_channel_start_delay (default
0.3 s) to decouple contact-force transients on the shared carriage -> creating a "cascading motion".
The inner ztouch_probe_z_height_using_channel call receives post_detection_dist=0
and an explicit tip_len so no C0 commands (move_channel_z,
request_tip_len_on_channel) serialize the gather. Channel raising is handled by
the existing min_traverse_height_during_command / z_position_at_end_of_command
kwargs.
Outer try/except BaseException lifts all channels to Z safety on any exception
(firmware error, KeyboardInterrupt, CancelledError) before re-raising. Aggregated
Z is rounded to the firmware z-drive quantum (0.01 mm). Chatterbox stub
included.
@BioCam
BioCam marked this pull request as draft May 28, 2026 18:45
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@BioCam
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add STARBackend.channels_probe_z_using_ztouch for parallel force probing - #1067

Draft
BioCam wants to merge 2 commits into
PyLabRobot:mainfrom
BioCam:create-cascading-multi-channel-ztouch
Draft

Add STARBackend.channels_probe_z_using_ztouch for parallel force probing#1067
BioCam wants to merge 2 commits into
PyLabRobot:mainfrom
BioCam:create-cascading-multi-channel-ztouch

Conversation

@BioCam

Copy link
Copy Markdown
Collaborator

The Problem

PyLabRobot has a single-channel force-sensed Z probe (ztouch_probe_z_height_using_channel, firmware ZH) but no batched, parallel equivalent. Multi-channel workflows that need this measurement currently require careful and time-consuming planning.

Concrete needs that surface this gap:

  • Verifying that lids, adapters, or labware are seated at the expected Z before a workflow continues.
  • Calibrating the Z of unknown or replaceable deck features without manual teach-in.
  • Capturing per-channel Z offsets against a calibration block in one shot rather than N sequential probes.

Naively wrapping the primitive in asyncio.gather does not solve this, because of two parallelism hazards specific to force-sensed probing on the STAR head:

  • Carriage-force cross-talk. All channels share one rigid Z-drive carriage. When channels descend simultaneously, each channel's touchdown produces a force transient that perturbs the PWM-based detection threshold on the others, biasing every measurement after the first to land.

  • C0-command serialization. The single-channel primitive issues move_channel_z (firmware JZ via C0) when post_detection_dist != 0 (TODO: move to PX version), and request_tip_len_on_channel (also C0) when tip_len=None. Both route through the master controller and serialize the gather - defeating the parallelism the caller is trying to get.

A correct batched implementation has to address both of these explicitly.

PR Content / Solution

Adds channels_probe_z_using_ztouch to the STAR backend: a batched, parallel force-sensed Z probe that returns the absolute deck-frame Z of whatever solid surface each channel first touches.

Architecture

Reuses the same batched-channel orchestration skeleton that probe_liquid_heights already uses for cLLD/pLLD: plan_batches builds channel/X/Y batches, execute_batched handles inter-batch X/Y positioning and Z safety, and the per-batch callback fans out ztouch_probe_z_height_using_channel across the batch's channels under asyncio.gather. No reinvention of X/Y partitioning, no-go-zone handling, or inter-batch Z safety.

Carriage cross-talk: stagger the channel starts

Within each batch, channel starts are staggered by inter_channel_start_delay (default 0.3 s) so contact-force transients don't superpose. The stagger lives in a small _delayed(delay, factory) helper that takes a coroutine factory (not a coroutine) to avoid un-awaited-coroutine warnings if the gather is cancelled mid-sleep. Tunable to 0.0 for callers who've validated mechanically isolated geometry.

C0-avoidance: keep the gather truly parallel

The inner ZH call receives post_detection_dist=0 and an explicit tip_len so the primitive does not issue move_channel_z or request_tip_len_on_channel. Channel raising between batches and at the end is handled by the existing min_traverse_height_during_command and z_position_at_end_of_command kwargs.

Safety and aggregation

Outer try / except BaseException lifts all channels to Z safety on any exception (firmware error, KeyboardInterrupt, SystemExit, asyncio.CancelledError) before re-raising. Aggregated Z is rounded to the firmware z-drive quantum (0.01 mm) so users don't see float-averaging noise across replicates.

Design decisions

  • Returns absolute deck-frame Z, not relative to a container or any other reference. The method targets arbitrary surfaces; there's no "in this well" frame to subtract. Callers wanting relative values subtract themselves.
  • No public post_detection_dist kwarg. Using it inside the gather would issue a C0 JZ per channel and serialize the parallelism. Channel raising is already covered by min_traverse_height_during_command / z_position_at_end_of_command.
  • except BaseException, not except Exception. Catches KeyboardInterrupt, SystemExit, and asyncio.CancelledError too - all cases where channels might be pressing on a surface when control is lost. Matches the pattern already in execute_batched.
  • inter_channel_start_delay default 0.3 s. Empirical default covering a typical ~tens-of-ms contact transient with margin; tunable to 0.0 for callers who don't need the decoupling.
  • Round aggregated Z to 0.01 mm.

Batched ZH probing across multiple channels, modeled on probe_liquid_heights:
plan_batches -> execute_batched -> asyncio.gather over `ztouch_probe_z_height_using_channel`.
Channel starts within a batch are staggered by inter_channel_start_delay (default
0.3 s) to decouple contact-force transients on the shared carriage -> creating a "cascading motion".
The inner ztouch_probe_z_height_using_channel call receives post_detection_dist=0
and an explicit tip_len so no C0 commands (move_channel_z,
request_tip_len_on_channel) serialize the gather. Channel raising is handled by
the existing min_traverse_height_during_command / z_position_at_end_of_command
kwargs.
Outer try/except BaseException lifts all channels to Z safety on any exception
(firmware error, KeyboardInterrupt, CancelledError) before re-raising. Aggregated
Z is rounded to the firmware z-drive quantum (0.01 mm). Chatterbox stub
included.
@BioCam
BioCam marked this pull request as draft May 28, 2026 18:45
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@BioCam
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add STARBackend.channels_probe_z_using_ztouch for parallel force probing - #1067

Draft
BioCam wants to merge 2 commits into
PyLabRobot:mainfrom
BioCam:create-cascading-multi-channel-ztouch
Draft

Add STARBackend.channels_probe_z_using_ztouch for parallel force probing#1067
BioCam wants to merge 2 commits into
PyLabRobot:mainfrom
BioCam:create-cascading-multi-channel-ztouch

Conversation

@BioCam

Copy link
Copy Markdown
Collaborator

The Problem

PyLabRobot has a single-channel force-sensed Z probe (ztouch_probe_z_height_using_channel, firmware ZH) but no batched, parallel equivalent. Multi-channel workflows that need this measurement currently require careful and time-consuming planning.

Concrete needs that surface this gap:

  • Verifying that lids, adapters, or labware are seated at the expected Z before a workflow continues.
  • Calibrating the Z of unknown or replaceable deck features without manual teach-in.
  • Capturing per-channel Z offsets against a calibration block in one shot rather than N sequential probes.

Naively wrapping the primitive in asyncio.gather does not solve this, because of two parallelism hazards specific to force-sensed probing on the STAR head:

  • Carriage-force cross-talk. All channels share one rigid Z-drive carriage. When channels descend simultaneously, each channel's touchdown produces a force transient that perturbs the PWM-based detection threshold on the others, biasing every measurement after the first to land.

  • C0-command serialization. The single-channel primitive issues move_channel_z (firmware JZ via C0) when post_detection_dist != 0 (TODO: move to PX version), and request_tip_len_on_channel (also C0) when tip_len=None. Both route through the master controller and serialize the gather - defeating the parallelism the caller is trying to get.

A correct batched implementation has to address both of these explicitly.

PR Content / Solution

Adds channels_probe_z_using_ztouch to the STAR backend: a batched, parallel force-sensed Z probe that returns the absolute deck-frame Z of whatever solid surface each channel first touches.

Architecture

Reuses the same batched-channel orchestration skeleton that probe_liquid_heights already uses for cLLD/pLLD: plan_batches builds channel/X/Y batches, execute_batched handles inter-batch X/Y positioning and Z safety, and the per-batch callback fans out ztouch_probe_z_height_using_channel across the batch's channels under asyncio.gather. No reinvention of X/Y partitioning, no-go-zone handling, or inter-batch Z safety.

Carriage cross-talk: stagger the channel starts

Within each batch, channel starts are staggered by inter_channel_start_delay (default 0.3 s) so contact-force transients don't superpose. The stagger lives in a small _delayed(delay, factory) helper that takes a coroutine factory (not a coroutine) to avoid un-awaited-coroutine warnings if the gather is cancelled mid-sleep. Tunable to 0.0 for callers who've validated mechanically isolated geometry.

C0-avoidance: keep the gather truly parallel

The inner ZH call receives post_detection_dist=0 and an explicit tip_len so the primitive does not issue move_channel_z or request_tip_len_on_channel. Channel raising between batches and at the end is handled by the existing min_traverse_height_during_command and z_position_at_end_of_command kwargs.

Safety and aggregation

Outer try / except BaseException lifts all channels to Z safety on any exception (firmware error, KeyboardInterrupt, SystemExit, asyncio.CancelledError) before re-raising. Aggregated Z is rounded to the firmware z-drive quantum (0.01 mm) so users don't see float-averaging noise across replicates.

Design decisions

  • Returns absolute deck-frame Z, not relative to a container or any other reference. The method targets arbitrary surfaces; there's no "in this well" frame to subtract. Callers wanting relative values subtract themselves.
  • No public post_detection_dist kwarg. Using it inside the gather would issue a C0 JZ per channel and serialize the parallelism. Channel raising is already covered by min_traverse_height_during_command / z_position_at_end_of_command.
  • except BaseException, not except Exception. Catches KeyboardInterrupt, SystemExit, and asyncio.CancelledError too - all cases where channels might be pressing on a surface when control is lost. Matches the pattern already in execute_batched.
  • inter_channel_start_delay default 0.3 s. Empirical default covering a typical ~tens-of-ms contact transient with margin; tunable to 0.0 for callers who don't need the decoupling.
  • Round aggregated Z to 0.01 mm.

Batched ZH probing across multiple channels, modeled on probe_liquid_heights:
plan_batches -> execute_batched -> asyncio.gather over `ztouch_probe_z_height_using_channel`.
Channel starts within a batch are staggered by inter_channel_start_delay (default
0.3 s) to decouple contact-force transients on the shared carriage -> creating a "cascading motion".
The inner ztouch_probe_z_height_using_channel call receives post_detection_dist=0
and an explicit tip_len so no C0 commands (move_channel_z,
request_tip_len_on_channel) serialize the gather. Channel raising is handled by
the existing min_traverse_height_during_command / z_position_at_end_of_command
kwargs.
Outer try/except BaseException lifts all channels to Z safety on any exception
(firmware error, KeyboardInterrupt, CancelledError) before re-raising. Aggregated
Z is rounded to the firmware z-drive quantum (0.01 mm). Chatterbox stub
included.
@BioCam
BioCam marked this pull request as draft May 28, 2026 18:45
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@BioCam
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Add STARBackend.channels_probe_z_using_ztouch for parallel force probing - #1067

Draft
BioCam wants to merge 2 commits into
PyLabRobot:mainfrom
BioCam:create-cascading-multi-channel-ztouch
Draft

Add STARBackend.channels_probe_z_using_ztouch for parallel force probing#1067
BioCam wants to merge 2 commits into
PyLabRobot:mainfrom
BioCam:create-cascading-multi-channel-ztouch

Conversation

@BioCam

Copy link
Copy Markdown
Collaborator

The Problem

PyLabRobot has a single-channel force-sensed Z probe (ztouch_probe_z_height_using_channel, firmware ZH) but no batched, parallel equivalent. Multi-channel workflows that need this measurement currently require careful and time-consuming planning.

Concrete needs that surface this gap:

  • Verifying that lids, adapters, or labware are seated at the expected Z before a workflow continues.
  • Calibrating the Z of unknown or replaceable deck features without manual teach-in.
  • Capturing per-channel Z offsets against a calibration block in one shot rather than N sequential probes.

Naively wrapping the primitive in asyncio.gather does not solve this, because of two parallelism hazards specific to force-sensed probing on the STAR head:

  • Carriage-force cross-talk. All channels share one rigid Z-drive carriage. When channels descend simultaneously, each channel's touchdown produces a force transient that perturbs the PWM-based detection threshold on the others, biasing every measurement after the first to land.

  • C0-command serialization. The single-channel primitive issues move_channel_z (firmware JZ via C0) when post_detection_dist != 0 (TODO: move to PX version), and request_tip_len_on_channel (also C0) when tip_len=None. Both route through the master controller and serialize the gather - defeating the parallelism the caller is trying to get.

A correct batched implementation has to address both of these explicitly.

PR Content / Solution

Adds channels_probe_z_using_ztouch to the STAR backend: a batched, parallel force-sensed Z probe that returns the absolute deck-frame Z of whatever solid surface each channel first touches.

Architecture

Reuses the same batched-channel orchestration skeleton that probe_liquid_heights already uses for cLLD/pLLD: plan_batches builds channel/X/Y batches, execute_batched handles inter-batch X/Y positioning and Z safety, and the per-batch callback fans out ztouch_probe_z_height_using_channel across the batch's channels under asyncio.gather. No reinvention of X/Y partitioning, no-go-zone handling, or inter-batch Z safety.

Carriage cross-talk: stagger the channel starts

Within each batch, channel starts are staggered by inter_channel_start_delay (default 0.3 s) so contact-force transients don't superpose. The stagger lives in a small _delayed(delay, factory) helper that takes a coroutine factory (not a coroutine) to avoid un-awaited-coroutine warnings if the gather is cancelled mid-sleep. Tunable to 0.0 for callers who've validated mechanically isolated geometry.

C0-avoidance: keep the gather truly parallel

The inner ZH call receives post_detection_dist=0 and an explicit tip_len so the primitive does not issue move_channel_z or request_tip_len_on_channel. Channel raising between batches and at the end is handled by the existing min_traverse_height_during_command and z_position_at_end_of_command kwargs.

Safety and aggregation

Outer try / except BaseException lifts all channels to Z safety on any exception (firmware error, KeyboardInterrupt, SystemExit, asyncio.CancelledError) before re-raising. Aggregated Z is rounded to the firmware z-drive quantum (0.01 mm) so users don't see float-averaging noise across replicates.

Design decisions

  • Returns absolute deck-frame Z, not relative to a container or any other reference. The method targets arbitrary surfaces; there's no "in this well" frame to subtract. Callers wanting relative values subtract themselves.
  • No public post_detection_dist kwarg. Using it inside the gather would issue a C0 JZ per channel and serialize the parallelism. Channel raising is already covered by min_traverse_height_during_command / z_position_at_end_of_command.
  • except BaseException, not except Exception. Catches KeyboardInterrupt, SystemExit, and asyncio.CancelledError too - all cases where channels might be pressing on a surface when control is lost. Matches the pattern already in execute_batched.
  • inter_channel_start_delay default 0.3 s. Empirical default covering a typical ~tens-of-ms contact transient with margin; tunable to 0.0 for callers who don't need the decoupling.
  • Round aggregated Z to 0.01 mm.

Batched ZH probing across multiple channels, modeled on probe_liquid_heights:
plan_batches -> execute_batched -> asyncio.gather over `ztouch_probe_z_height_using_channel`.
Channel starts within a batch are staggered by inter_channel_start_delay (default
0.3 s) to decouple contact-force transients on the shared carriage -> creating a "cascading motion".
The inner ztouch_probe_z_height_using_channel call receives post_detection_dist=0
and an explicit tip_len so no C0 commands (move_channel_z,
request_tip_len_on_channel) serialize the gather. Channel raising is handled by
the existing min_traverse_height_during_command / z_position_at_end_of_command
kwargs.
Outer try/except BaseException lifts all channels to Z safety on any exception
(firmware error, KeyboardInterrupt, CancelledError) before re-raising. Aggregated
Z is rounded to the firmware z-drive quantum (0.01 mm). Chatterbox stub
included.
@BioCam
BioCam marked this pull request as draft May 28, 2026 18:45
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@BioCam
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add STARBackend.channels_probe_z_using_ztouch for parallel force probing - #1067

Draft
BioCam wants to merge 2 commits into
PyLabRobot:mainfrom
BioCam:create-cascading-multi-channel-ztouch
Draft

Add STARBackend.channels_probe_z_using_ztouch for parallel force probing#1067
BioCam wants to merge 2 commits into
PyLabRobot:mainfrom
BioCam:create-cascading-multi-channel-ztouch

Conversation

@BioCam

Copy link
Copy Markdown
Collaborator

The Problem

PyLabRobot has a single-channel force-sensed Z probe (ztouch_probe_z_height_using_channel, firmware ZH) but no batched, parallel equivalent. Multi-channel workflows that need this measurement currently require careful and time-consuming planning.

Concrete needs that surface this gap:

  • Verifying that lids, adapters, or labware are seated at the expected Z before a workflow continues.
  • Calibrating the Z of unknown or replaceable deck features without manual teach-in.
  • Capturing per-channel Z offsets against a calibration block in one shot rather than N sequential probes.

Naively wrapping the primitive in asyncio.gather does not solve this, because of two parallelism hazards specific to force-sensed probing on the STAR head:

  • Carriage-force cross-talk. All channels share one rigid Z-drive carriage. When channels descend simultaneously, each channel's touchdown produces a force transient that perturbs the PWM-based detection threshold on the others, biasing every measurement after the first to land.

  • C0-command serialization. The single-channel primitive issues move_channel_z (firmware JZ via C0) when post_detection_dist != 0 (TODO: move to PX version), and request_tip_len_on_channel (also C0) when tip_len=None. Both route through the master controller and serialize the gather - defeating the parallelism the caller is trying to get.

A correct batched implementation has to address both of these explicitly.

PR Content / Solution

Adds channels_probe_z_using_ztouch to the STAR backend: a batched, parallel force-sensed Z probe that returns the absolute deck-frame Z of whatever solid surface each channel first touches.

Architecture

Reuses the same batched-channel orchestration skeleton that probe_liquid_heights already uses for cLLD/pLLD: plan_batches builds channel/X/Y batches, execute_batched handles inter-batch X/Y positioning and Z safety, and the per-batch callback fans out ztouch_probe_z_height_using_channel across the batch's channels under asyncio.gather. No reinvention of X/Y partitioning, no-go-zone handling, or inter-batch Z safety.

Carriage cross-talk: stagger the channel starts

Within each batch, channel starts are staggered by inter_channel_start_delay (default 0.3 s) so contact-force transients don't superpose. The stagger lives in a small _delayed(delay, factory) helper that takes a coroutine factory (not a coroutine) to avoid un-awaited-coroutine warnings if the gather is cancelled mid-sleep. Tunable to 0.0 for callers who've validated mechanically isolated geometry.

C0-avoidance: keep the gather truly parallel

The inner ZH call receives post_detection_dist=0 and an explicit tip_len so the primitive does not issue move_channel_z or request_tip_len_on_channel. Channel raising between batches and at the end is handled by the existing min_traverse_height_during_command and z_position_at_end_of_command kwargs.

Safety and aggregation

Outer try / except BaseException lifts all channels to Z safety on any exception (firmware error, KeyboardInterrupt, SystemExit, asyncio.CancelledError) before re-raising. Aggregated Z is rounded to the firmware z-drive quantum (0.01 mm) so users don't see float-averaging noise across replicates.

Design decisions

  • Returns absolute deck-frame Z, not relative to a container or any other reference. The method targets arbitrary surfaces; there's no "in this well" frame to subtract. Callers wanting relative values subtract themselves.
  • No public post_detection_dist kwarg. Using it inside the gather would issue a C0 JZ per channel and serialize the parallelism. Channel raising is already covered by min_traverse_height_during_command / z_position_at_end_of_command.
  • except BaseException, not except Exception. Catches KeyboardInterrupt, SystemExit, and asyncio.CancelledError too - all cases where channels might be pressing on a surface when control is lost. Matches the pattern already in execute_batched.
  • inter_channel_start_delay default 0.3 s. Empirical default covering a typical ~tens-of-ms contact transient with margin; tunable to 0.0 for callers who don't need the decoupling.
  • Round aggregated Z to 0.01 mm.

Batched ZH probing across multiple channels, modeled on probe_liquid_heights:
plan_batches -> execute_batched -> asyncio.gather over `ztouch_probe_z_height_using_channel`.
Channel starts within a batch are staggered by inter_channel_start_delay (default
0.3 s) to decouple contact-force transients on the shared carriage -> creating a "cascading motion".
The inner ztouch_probe_z_height_using_channel call receives post_detection_dist=0
and an explicit tip_len so no C0 commands (move_channel_z,
request_tip_len_on_channel) serialize the gather. Channel raising is handled by
the existing min_traverse_height_during_command / z_position_at_end_of_command
kwargs.
Outer try/except BaseException lifts all channels to Z safety on any exception
(firmware error, KeyboardInterrupt, CancelledError) before re-raising. Aggregated
Z is rounded to the firmware z-drive quantum (0.01 mm). Chatterbox stub
included.
@BioCam
BioCam marked this pull request as draft May 28, 2026 18:45
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@BioCam
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add STARBackend.channels_probe_z_using_ztouch for parallel force probing - #1067

Draft
BioCam wants to merge 2 commits into
PyLabRobot:mainfrom
BioCam:create-cascading-multi-channel-ztouch
Draft

Add STARBackend.channels_probe_z_using_ztouch for parallel force probing#1067
BioCam wants to merge 2 commits into
PyLabRobot:mainfrom
BioCam:create-cascading-multi-channel-ztouch

Conversation

@BioCam

Copy link
Copy Markdown
Collaborator

The Problem

PyLabRobot has a single-channel force-sensed Z probe (ztouch_probe_z_height_using_channel, firmware ZH) but no batched, parallel equivalent. Multi-channel workflows that need this measurement currently require careful and time-consuming planning.

Concrete needs that surface this gap:

  • Verifying that lids, adapters, or labware are seated at the expected Z before a workflow continues.
  • Calibrating the Z of unknown or replaceable deck features without manual teach-in.
  • Capturing per-channel Z offsets against a calibration block in one shot rather than N sequential probes.

Naively wrapping the primitive in asyncio.gather does not solve this, because of two parallelism hazards specific to force-sensed probing on the STAR head:

  • Carriage-force cross-talk. All channels share one rigid Z-drive carriage. When channels descend simultaneously, each channel's touchdown produces a force transient that perturbs the PWM-based detection threshold on the others, biasing every measurement after the first to land.

  • C0-command serialization. The single-channel primitive issues move_channel_z (firmware JZ via C0) when post_detection_dist != 0 (TODO: move to PX version), and request_tip_len_on_channel (also C0) when tip_len=None. Both route through the master controller and serialize the gather - defeating the parallelism the caller is trying to get.

A correct batched implementation has to address both of these explicitly.

PR Content / Solution

Adds channels_probe_z_using_ztouch to the STAR backend: a batched, parallel force-sensed Z probe that returns the absolute deck-frame Z of whatever solid surface each channel first touches.

Architecture

Reuses the same batched-channel orchestration skeleton that probe_liquid_heights already uses for cLLD/pLLD: plan_batches builds channel/X/Y batches, execute_batched handles inter-batch X/Y positioning and Z safety, and the per-batch callback fans out ztouch_probe_z_height_using_channel across the batch's channels under asyncio.gather. No reinvention of X/Y partitioning, no-go-zone handling, or inter-batch Z safety.

Carriage cross-talk: stagger the channel starts

Within each batch, channel starts are staggered by inter_channel_start_delay (default 0.3 s) so contact-force transients don't superpose. The stagger lives in a small _delayed(delay, factory) helper that takes a coroutine factory (not a coroutine) to avoid un-awaited-coroutine warnings if the gather is cancelled mid-sleep. Tunable to 0.0 for callers who've validated mechanically isolated geometry.

C0-avoidance: keep the gather truly parallel

The inner ZH call receives post_detection_dist=0 and an explicit tip_len so the primitive does not issue move_channel_z or request_tip_len_on_channel. Channel raising between batches and at the end is handled by the existing min_traverse_height_during_command and z_position_at_end_of_command kwargs.

Safety and aggregation

Outer try / except BaseException lifts all channels to Z safety on any exception (firmware error, KeyboardInterrupt, SystemExit, asyncio.CancelledError) before re-raising. Aggregated Z is rounded to the firmware z-drive quantum (0.01 mm) so users don't see float-averaging noise across replicates.

Design decisions

  • Returns absolute deck-frame Z, not relative to a container or any other reference. The method targets arbitrary surfaces; there's no "in this well" frame to subtract. Callers wanting relative values subtract themselves.
  • No public post_detection_dist kwarg. Using it inside the gather would issue a C0 JZ per channel and serialize the parallelism. Channel raising is already covered by min_traverse_height_during_command / z_position_at_end_of_command.
  • except BaseException, not except Exception. Catches KeyboardInterrupt, SystemExit, and asyncio.CancelledError too - all cases where channels might be pressing on a surface when control is lost. Matches the pattern already in execute_batched.
  • inter_channel_start_delay default 0.3 s. Empirical default covering a typical ~tens-of-ms contact transient with margin; tunable to 0.0 for callers who don't need the decoupling.
  • Round aggregated Z to 0.01 mm.

Batched ZH probing across multiple channels, modeled on probe_liquid_heights:
plan_batches -> execute_batched -> asyncio.gather over `ztouch_probe_z_height_using_channel`.
Channel starts within a batch are staggered by inter_channel_start_delay (default
0.3 s) to decouple contact-force transients on the shared carriage -> creating a "cascading motion".
The inner ztouch_probe_z_height_using_channel call receives post_detection_dist=0
and an explicit tip_len so no C0 commands (move_channel_z,
request_tip_len_on_channel) serialize the gather. Channel raising is handled by
the existing min_traverse_height_during_command / z_position_at_end_of_command
kwargs.
Outer try/except BaseException lifts all channels to Z safety on any exception
(firmware error, KeyboardInterrupt, CancelledError) before re-raising. Aggregated
Z is rounded to the firmware z-drive quantum (0.01 mm). Chatterbox stub
included.
@BioCam
BioCam marked this pull request as draft May 28, 2026 18:45
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@BioCam
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Add STARBackend.channels_probe_z_using_ztouch for parallel force probing - #1067

Draft
BioCam wants to merge 2 commits into
PyLabRobot:mainfrom
BioCam:create-cascading-multi-channel-ztouch
Draft

Add STARBackend.channels_probe_z_using_ztouch for parallel force probing#1067
BioCam wants to merge 2 commits into
PyLabRobot:mainfrom
BioCam:create-cascading-multi-channel-ztouch

Conversation

@BioCam

Copy link
Copy Markdown
Collaborator

The Problem

PyLabRobot has a single-channel force-sensed Z probe (ztouch_probe_z_height_using_channel, firmware ZH) but no batched, parallel equivalent. Multi-channel workflows that need this measurement currently require careful and time-consuming planning.

Concrete needs that surface this gap:

  • Verifying that lids, adapters, or labware are seated at the expected Z before a workflow continues.
  • Calibrating the Z of unknown or replaceable deck features without manual teach-in.
  • Capturing per-channel Z offsets against a calibration block in one shot rather than N sequential probes.

Naively wrapping the primitive in asyncio.gather does not solve this, because of two parallelism hazards specific to force-sensed probing on the STAR head:

  • Carriage-force cross-talk. All channels share one rigid Z-drive carriage. When channels descend simultaneously, each channel's touchdown produces a force transient that perturbs the PWM-based detection threshold on the others, biasing every measurement after the first to land.

  • C0-command serialization. The single-channel primitive issues move_channel_z (firmware JZ via C0) when post_detection_dist != 0 (TODO: move to PX version), and request_tip_len_on_channel (also C0) when tip_len=None. Both route through the master controller and serialize the gather - defeating the parallelism the caller is trying to get.

A correct batched implementation has to address both of these explicitly.

PR Content / Solution

Adds channels_probe_z_using_ztouch to the STAR backend: a batched, parallel force-sensed Z probe that returns the absolute deck-frame Z of whatever solid surface each channel first touches.

Architecture

Reuses the same batched-channel orchestration skeleton that probe_liquid_heights already uses for cLLD/pLLD: plan_batches builds channel/X/Y batches, execute_batched handles inter-batch X/Y positioning and Z safety, and the per-batch callback fans out ztouch_probe_z_height_using_channel across the batch's channels under asyncio.gather. No reinvention of X/Y partitioning, no-go-zone handling, or inter-batch Z safety.

Carriage cross-talk: stagger the channel starts

Within each batch, channel starts are staggered by inter_channel_start_delay (default 0.3 s) so contact-force transients don't superpose. The stagger lives in a small _delayed(delay, factory) helper that takes a coroutine factory (not a coroutine) to avoid un-awaited-coroutine warnings if the gather is cancelled mid-sleep. Tunable to 0.0 for callers who've validated mechanically isolated geometry.

C0-avoidance: keep the gather truly parallel

The inner ZH call receives post_detection_dist=0 and an explicit tip_len so the primitive does not issue move_channel_z or request_tip_len_on_channel. Channel raising between batches and at the end is handled by the existing min_traverse_height_during_command and z_position_at_end_of_command kwargs.

Safety and aggregation

Outer try / except BaseException lifts all channels to Z safety on any exception (firmware error, KeyboardInterrupt, SystemExit, asyncio.CancelledError) before re-raising. Aggregated Z is rounded to the firmware z-drive quantum (0.01 mm) so users don't see float-averaging noise across replicates.

Design decisions

  • Returns absolute deck-frame Z, not relative to a container or any other reference. The method targets arbitrary surfaces; there's no "in this well" frame to subtract. Callers wanting relative values subtract themselves.
  • No public post_detection_dist kwarg. Using it inside the gather would issue a C0 JZ per channel and serialize the parallelism. Channel raising is already covered by min_traverse_height_during_command / z_position_at_end_of_command.
  • except BaseException, not except Exception. Catches KeyboardInterrupt, SystemExit, and asyncio.CancelledError too - all cases where channels might be pressing on a surface when control is lost. Matches the pattern already in execute_batched.
  • inter_channel_start_delay default 0.3 s. Empirical default covering a typical ~tens-of-ms contact transient with margin; tunable to 0.0 for callers who don't need the decoupling.
  • Round aggregated Z to 0.01 mm.

Batched ZH probing across multiple channels, modeled on probe_liquid_heights:
plan_batches -> execute_batched -> asyncio.gather over `ztouch_probe_z_height_using_channel`.
Channel starts within a batch are staggered by inter_channel_start_delay (default
0.3 s) to decouple contact-force transients on the shared carriage -> creating a "cascading motion".
The inner ztouch_probe_z_height_using_channel call receives post_detection_dist=0
and an explicit tip_len so no C0 commands (move_channel_z,
request_tip_len_on_channel) serialize the gather. Channel raising is handled by
the existing min_traverse_height_during_command / z_position_at_end_of_command
kwargs.
Outer try/except BaseException lifts all channels to Z safety on any exception
(firmware error, KeyboardInterrupt, CancelledError) before re-raising. Aggregated
Z is rounded to the firmware z-drive quantum (0.01 mm). Chatterbox stub
included.
@BioCam
BioCam marked this pull request as draft May 28, 2026 18:45
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@BioCam