Skip to content

feat(control): e-stop, take over and resume from the operator page - #4790

Closed
mustafab0 wants to merge 1 commit into
mustafa/feat/operator-ui-pagefrom
mustafa/feat/operator-ui-estop
Closed

mustafab0 wants to merge 1 commit into
mustafa/feat/operator-ui-pagefrom
mustafa/feat/operator-ui-estop

Conversation

@mustafab0

@mustafab0 mustafab0 commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Stacks on #4789

Why do we need this?

The operator needs a stop that halts a running move at once and stays visible, and a clear choice of how to end the hold.

What is this feature?

E-stop, Take over and Resume on the operator page.

OperatorHoldCoordinator takes operator_estop, a Bool (true stops, false clears; the page sends a bare JSON true or false through the built-in bool.json.v1 decoder), and publishes operator_estop_status, an OperatorEstopStatus (estopped, stamp in unix seconds). Stop latches E-STOP, requests a failsafe hold and cancels the trajectory; clear leaves the hold on. The blueprint declares both channels (the e-stop status paced). The page adds E-STOP, Clear e-stop, an E-STOP ACTIVE line, and Take over / Resume, which both acknowledge the hold with an intent of "takeover" or "resume".

How to manually run this

dimos run hybrid-teleop-sim --serve-dir web/hybrid-teleop, then open http://localhost:7780.

While the arm is moving, click E-STOP: the arm stops and E-STOP ACTIVE shows. Click Clear e-stop: the hold stays on. Click Resume: the hold ends.

Tested: coordinator unit tests. Headless sim with a scripted browser: idle → E-STOP during a move → clear → resume; the arm stops within about 0.3 s.

Which issue(s) does this PR close?

None.

Checklist

  • I have read and approved the CLA.

@github-actions github-actions Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

go2 replay realtime (arm64)

Details
Benchmark suite Current: 7edb281 Previous: f9c5a5c Ratio
peak memory 1774.52 MB 1795.23 MB 0.99
peak threads 359 threads 366 threads 0.98
network (transport) 2731.023 MB 2730.945 MB 1.00
disk write 1.5 MB 1.723 MB 0.87
instructions 137.827 G 137.801 G 1.00

This comment was automatically generated by workflow using github-action-benchmark.

@codecov

codecov Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
7365 1 7364 64
View the top 1 failed test(s) by shortest run time
dimos.e2e_tests.test_dimsim_spatial_memory::test_go_to_the_bed
Stack Traces | 273s run time
lcm_spy = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x728f84544a10>
start_blueprint = <function start_blueprint.<locals>.set_name_and_start at 0x728f847ad4e0>
wait_for_system_ready = <function wait_for_system_ready.<locals>.wait at 0x728f847ad580>
human_input = <function human_input.<locals>.send_human_input at 0x728f847ad6c0>
dim_sim = <dimos.e2e_tests.dim_sim_client.DimSimClient object at 0x7290f637f470>
explore_house = <function explore_house.<locals>.explore at 0x728f847adee0>

    @pytest.mark.self_hosted_large
    def test_go_to_the_bed(
        lcm_spy, start_blueprint, wait_for_system_ready, human_input, dim_sim, explore_house
    ) -> None:
        start_blueprint(
            "run",
            "unitree-go2-agentic",
            simulator="dimsim",
        )
        wait_for_system_ready(timeout=300.0)
    
        explore_house()
    
        human_input("go to the bed")
    
>       lcm_spy.wait_until_odom_position(-3.567, -1.332, threshold=2, timeout=180)

dim_sim    = <dimos.e2e_tests.dim_sim_client.DimSimClient object at 0x7290f637f470>
explore_house = <function explore_house.<locals>.explore at 0x728f847adee0>
human_input = <function human_input.<locals>.send_human_input at 0x728f847ad6c0>
lcm_spy    = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x728f84544a10>
start_blueprint = <function start_blueprint.<locals>.set_name_and_start at 0x728f847ad4e0>
wait_for_system_ready = <function wait_for_system_ready.<locals>.wait at 0x728f847ad580>

dimos/e2e_tests/test_dimsim_spatial_memory.py:33: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
dimos/e2e_tests/lcm_spy.py:143: in wait_until_odom_position
    self.wait_for_message_result(
        predicate  = <function LcmSpy.wait_until_odom_position.<locals>.predicate at 0x728fad30c220>
        self       = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x728f84544a10>
        threshold  = 2
        timeout    = 180
        x          = -3.567
        y          = -1.332
dimos/e2e_tests/lcm_spy.py:129: in wait_for_message_result
    wait_until(
        event      = <threading.Event at 0x728fad0d2810: unset>
        fail_message = 'Failed to get to position x=-3.567, y=-1.332'
        listener   = <function LcmSpy.wait_for_message_result.<locals>.listener at 0x728fac0fb240>
        predicate  = <function LcmSpy.wait_until_odom_position.<locals>.predicate at 0x728fad30c220>
        self       = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x728f84544a10>
        timeout    = 180
        topic      = '/odom#geometry_msgs.PoseStamped'
        type       = <class 'dimos.msgs.geometry_msgs.PoseStamped.PoseStamped'>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

predicate = <bound method Event.is_set of <threading.Event at 0x728fad0d2810: unset>>

    def wait_until(
        predicate: Callable[[], bool],
        *,
        timeout: float,
        interval: float = 0.1,
        message: str | None = None,
    ) -> None:
        """Poll ``predicate`` until it returns truthy or ``timeout`` elapses."""
        deadline = time.monotonic() + timeout
        while time.monotonic() < deadline:
            if predicate():
                return
            time.sleep(interval)
>       raise TimeoutError(message or f"Timed out after {timeout}s waiting for condition")
E       TimeoutError: Failed to get to position x=-3.567, y=-1.332

deadline   = 9269744.940725291
interval   = 0.1
message    = 'Failed to get to position x=-3.567, y=-1.332'
predicate  = <bound method Event.is_set of <threading.Event at 0x728fad0d2810: unset>>
timeout    = 180

.../utils/testing/waiting.py:35: TimeoutError

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@mustafab0
mustafab0 force-pushed the mustafa/feat/operator-ui-page branch from e419ec4 to df2a447 Compare October 23, 2026 01:00
@mustafab0
mustafab0 force-pushed the mustafa/feat/operator-ui-estop branch from 7a21ea9 to b2eb928 Compare October 23, 2026 01:00
@mustafab0
mustafab0 added this pull request to stack #4809 October 23, 2026 19:07
@mustafab0
mustafab0 force-pushed the mustafa/feat/operator-ui-estop branch from b2eb928 to 2d6cbd7 Compare October 23, 2026 19:30
@mustafab0
mustafab0 force-pushed the mustafa/feat/operator-ui-page branch from df2a447 to 24e0d00 Compare October 23, 2026 19:30
@mustafab0
mustafab0 force-pushed the mustafa/feat/operator-ui-estop branch from 2d6cbd7 to c2d6a89 Compare October 23, 2026 21:56
@mustafab0
mustafab0 force-pushed the mustafa/feat/operator-ui-page branch from 24e0d00 to ce47c47 Compare October 23, 2026 21:56
OperatorHoldCoordinator takes an e-stop on a new operator_estop stream of
Bool. True latches set_estop(True), requests the operator hold (route
failsafe, reason e-stop) so every joint is frozen where it is, and
cancels the running trajectory. False only clears the latch; the hold
stays on until the operator acknowledges it. The latch state goes out
on operator_estop_status as an OperatorEstopStatus (estopped, stamp in
unix seconds) at start and on every set_estop. OperatorEstopStatus sits
beside OperatorHoldCommand so modules outside the blueprint can read it.

hybrid-teleop-sim exposes both as web channels: operator_estop with the
built-in bool.json.v1 decoder (the page sends a bare true or false) and
operator_estop_status paced, so a second change right after the first
still reaches the page. web/hybrid-teleop gets an E-STOP button, Clear
e-stop, an E-STOP ACTIVE line, and Take over / Resume in place of
Acknowledge. Both send action "acknowledge" with a new OperatorHoldCommand
intent field ("takeover" or "resume") that the hold task does not read.

This branch was previously deployed

1 inactive deployment
cachix — 7edb2810 Deployed Oct 23, 2026 by mustafab0 via cachix-build #14174
Sign up for free to 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