Skip to content

a better remote body teleoperation experience#37732

Draft
stefpi wants to merge 96 commits intomasterfrom
good-remote-body
Draft

a better remote body teleoperation experience#37732
stefpi wants to merge 96 commits intomasterfrom
good-remote-body

Conversation

@stefpi
Copy link
Copy Markdown

@stefpi stefpi commented Mar 25, 2026

changes

layouts

  • new layouts and widgets specifically when attached to Body V1 or V2
    • tici and mici onroad/home screens with faces and connect buttons for easy connection
    • tici screen has a new sidebar which appears from the top, mici preserves all other screens
    • raylib dot-matrix face animations (normal, asleep, sleepy, inquisitive, wink), smooth transitions, and action-based animations (turning)
    • can now connect one-time via QR code and control device through connect app
    • tici and mici main layouts detect notCar and swap in body-specific layouts/sidebar

webrtcd

  • 1 video channel
    • camera switching via datachannel param + frame stream going into stream encoder
      • New LivestreamCamera param
    • Stream encoder bitrate bumped from 1 Mbps to 4 Mbps, GOP reduced from 15 to 5
    • Removed road camera mapping from stream encoder (only driver and wideRoad remain)
  • 1 audio channel
  • latency measurement only when "stats" menu is open on frontend with metadata injection into H264 stream before packets are sent
  • better error handling for different scenarios so end user can know why connections failed
    • other people on stream already
    • ignition is not on
  • HTTPS with auto-generated self-signed certs, /trust endpoint for cert acceptance. This is required for connecting to the device on local network but through a website hosted somewhere else which is using HTTPS.
  • CORS headers including Access-Control-Allow-Private-Network

soundd

  • sound bite requests from frontend go to /stream on webrtc and then post a message on cereal, which is then picked up by the sound daemon and then that alert sound is queued up to be played
    • this was done because soundd already takes up the audio drivers, so instead of unloading and reloading, I thought it would be easier to pass it to soundd directly.
  • New soundRequest cereal service for remote sound triggers

athena

  • New athenad getNotCar() RPC reading CarParamsPersistent
  • New athenad startJoystickStream(sdp) RPC posting to local webrtcd

debug

  • New bodyview.py debug script to simulate body UI (--big, --joystick, --monitor)
  • New body_injection_test.py for body controller testing

process manager

  • micd now also runs on body (changed from iscar to only_onroad)
  • dmonitoringmodeld and dmonitoringd disabled on body (also ignored in selfdrived)

other

  • Body enable delay reduced from 5s to 2s
  • Pandad disables IR LEDs on body (checks CarParamsPersistent every 100 frames)
  • Removed old webjoystick process
  • Entire old body teleop web UI (tools/bodyteleop/) removed
  • CONNECT_HOST / CONNECT_HOST_DISPLAY env-overridable constants used across all pairing dialogs and settings. Useful for local, preview and deployed QR code generation and link display

Related PRs that this PR depends on:

stefpi added 30 commits March 11, 2026 11:49
…webrtc negotiation for better error handling
… about being starved during cereal bridge + webrtc connection
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 26, 2026

UI Preview

mici: ⚠️ Videos differ! View Diff Report
big: ⚠️ Videos differ! View Diff Report

@stefpi stefpi force-pushed the good-remote-body branch from 61f7f7c to 4413ae6 Compare March 26, 2026 02:00
@stefpi stefpi marked this pull request as ready for review March 26, 2026 02:08
@stefpi stefpi requested a review from sshane March 26, 2026 02:08
Comment on lines +334 to +338
ssid = self._wifi_manager.connected_ssid
if ssid and len(ssid) > 20:
ssid = ssid[:17] + "..."
wifi_text = f"WiFi: {ssid}" if ssid else "wifi: not connected"
rl.draw_text_ex(self._font, wifi_text, rl.Vector2(label_x, y), 22, 0, MICI_TEXT_COLOR)
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.

you can use a UnifiedLabel for automatic eliding (the ...) and positioning, and VBoxLayout to remove a bunch of code as well

Comment on lines +388 to +389
for _ in gui_app.render():
pass
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.

Suggested change
for _ in gui_app.render():
pass
gui_app.render()

you can just do this now

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.

why is this all duplicated?

Comment on lines +221 to +233
# def _draw_flag_button(self, rect: rl.Rectangle):
# if not ui_state.started:
# return

# mouse_pos = rl.get_mouse_position()
# mouse_down = self.is_pressed and rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT)

# btn_x = int(rect.x + rect.width - 100)
# btn_y = int(rect.y + 30)
# flag_rect = rl.Rectangle(btn_x, btn_y, 60, 60)
# flag_pressed = mouse_down and rl.check_collision_point_rec(mouse_pos, flag_rect)
# tint = Colors.BUTTON_PRESSED if flag_pressed else Colors.BUTTON_NORMAL
# rl.draw_texture(self._flag_img, btn_x, btn_y, tint)
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.

unused code

self._layouts = {MainState.HOME: HomeLayout(), MainState.SETTINGS: SettingsLayout(), MainState.ONROAD: AugmentedRoadView()}
if self._is_body:
self._layouts = {MainState.HOME: BodyLayout(), MainState.SETTINGS: SettingsLayout(), MainState.ONROAD: BodyLayout()}
self._sidebar.set_visible(False)
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.

you can just make the body side bar set itself not visible

Comment on lines +344 to +364
class _PairingBigButton(BigButton):
def _get_label_font_size(self):
return 64


class MiciBodyPairingScreen(NavScroller):
"""MICI pairing screen: NavScroller with BigButtons for each pairing method."""

def __init__(self):
super().__init__()

self._wifi_manager = WifiManager()
self._wifi_manager.set_active(False)

self._connect_panel = OneTimeConnectPanel()
connect_btn = _PairingBigButton("connect", "",
gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 76, 56))
connect_btn.set_click_callback(lambda: gui_app.push_widget(self._connect_panel))

self._connect_btn = connect_btn
self._scroller.add_widgets([connect_btn])
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.

wait you have two pairing screens? I don't see this one when I click connet on home screen for mici

self.color = color


class BodySidebar(Widget):
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.

just subclass Sidebar and override render?

Comment on lines +63 to +66
if self._is_body:
self._layouts[MainState.HOME].set_click_callback(self._on_onroad_clicked)
self._layouts[MainState.ONROAD].set_click_callback(self._on_onroad_clicked)

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.

This breaks tapping onroad for cars now! Why do we do this?

Comment on lines +92 to +94
# Body sidebar always starts closed; regular sidebar starts open
if not self._is_body:
self._sidebar.set_visible(True)
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.

another splittable PR, if this was using the nav stack each sidebar could manage its own show event behavior ;)

Comment on lines +119 to +130
if self._is_body: # overlay sidebar but recompute boundaries for proper click events
parent_rect = rl.Rectangle(self._rect.x, self._rect.y + BODY_SIDEBAR_HEIGHT,
self._rect.width, self._rect.height - BODY_SIDEBAR_HEIGHT) if self._sidebar.is_visible else None
self._layouts[self._current_mode].set_parent_rect(parent_rect)
self._layouts[self._current_mode].render(self._rect)
if self._sidebar.is_visible:
self._sidebar.render(self._sidebar_rect)
else:
if self._sidebar.is_visible:
self._sidebar.render(self._sidebar_rect)
content_rect = self._content_rect if self._sidebar.is_visible else self._rect
self._layouts[self._current_mode].render(content_rect)
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.

two

      if self._sidebar.is_visible:
        self._sidebar.render(self._sidebar_rect)

@sshane
Copy link
Copy Markdown
Contributor

sshane commented Mar 26, 2026

failing static analysis test too, make sure it's all in a final state, i.e. something you're happy with merging now

@sshane
Copy link
Copy Markdown
Contributor

sshane commented Mar 26, 2026

for the generic nav stack touch valid fix, I think we first need to fix the different use of enabled and touch_valid. can do that one later @stefpi

@stefpi stefpi force-pushed the good-remote-body branch from f8a23d3 to 71a2950 Compare March 26, 2026 22:27
@stefpi stefpi marked this pull request as draft March 27, 2026 23:30
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.

2 participants