diff --git a/src/controls.py b/src/controls.py index 9afaef5..c0d185e 100644 --- a/src/controls.py +++ b/src/controls.py @@ -225,11 +225,14 @@ def _create_axis_column(index: int, axis: Any) -> None: axis_state, ctr_cfg, mtr_cfg, enc_cfg, trp_cfg = mirrors cc = axis.motor.current_control - async def sync_mirrors() -> None: - for mirror in mirrors: - await mirror.sync() + async def sync_state() -> None: + await axis_state.sync() show_state(axis_state.current_state) + async def sync_config() -> None: + for mirror in mirrors[1:]: + await mirror.sync() + async def update_power() -> None: if (watts := await _on_usb(axis, lambda: cc.Iq_measured * cc.v_current_control_integral_q)) is not None: power.set_text(f'{watts:.1f} W') @@ -239,9 +242,11 @@ async def update_error() -> None: button.set_enabled(error != 0) # every property read is a USB round-trip, so keep the 10 Hz path to the two values - # that must feel live; the error flag only enables a button and can follow at 1 Hz. + # that must feel live. The error flag only enables a button, and config values only + # change when someone edits them (an edit here shows at once), so they follow at 1 Hz. ui.timer(0.1, update_power) - ui.timer(0.1, sync_mirrors) + ui.timer(0.1, sync_state) + ui.timer(1.0, sync_config) ui.timer(1.0, update_error) def request_state(state: int) -> None: diff --git a/tests/test_main.py b/tests/test_main.py index 65bce0a..f289b7e 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -174,7 +174,7 @@ async def test_gain_edits_reach_the_device_and_device_changes_reach_the_ui(user: assert field.value == 42.0 config.pos_gain = 7.0 # changed behind the UI's back (odrivetool, another client) - await asyncio.sleep(1.2) + await asyncio.sleep(2.0) # config values are refreshed at 1 Hz assert field.value == 7.0 diff --git a/tests/test_render.py b/tests/test_render.py index bfab7e7..be41c96 100644 --- a/tests/test_render.py +++ b/tests/test_render.py @@ -81,6 +81,18 @@ async def test_live_values_are_not_read_on_the_event_loop(user: User) -> None: assert READS['odrive-usb_0'] > before['odrive-usb_0'] # the live values are still read, on the worker +async def test_config_values_are_not_polled_at_10_hz(user: User) -> None: + """Every worker read delays the writes and commands queued behind it. Only the axis + state and the power label must feel live; the ~22 bound config values per axis are + refreshed at 1 Hz (two calibrated axes: ~105 reads per second instead of ~480).""" + await user.open('/') + await user.should_see('Axis 1') + await asyncio.sleep(0.5) # past the reads of the panel build + before = READS['odrive-usb_0'] + await asyncio.sleep(2.0) + assert (READS['odrive-usb_0'] - before) / 2.0 < 150 + + async def test_a_refresh_that_read_before_an_edit_does_not_undo_it(user: User, mock_odrive) -> None: """A refresh reads on the USB worker while the user may edit the same value: the read sees the old device value, and applying it would snap the widget back -- whose change