Skip to content

feat: add output observability for device commands#886

Open
qdot wants to merge 14 commits into
masterfrom
output-observability
Open

feat: add output observability for device commands#886
qdot wants to merge 14 commits into
masterfrom
output-observability

Conversation

@qdot
Copy link
Copy Markdown
Member

@qdot qdot commented May 18, 2026

Summary

  • Adds opt-in output observation channel that broadcasts OutputObservation events for every non-deduped output command sent to a device
  • Wires observations from DeviceHandle through ServerDeviceManagerButtplugServerButtplugRemoteServerFrontend as EngineMessage::DeviceOutputObservation
  • Zero overhead when disabled: Option<broadcast::Sender> is None, no channels allocated, no atomic loads

Details

New public APIs:

  • OutputObservation struct (device_index, feature_index, output_type, value)
  • ServerDeviceManagerBuilder::emit_output_observations(bool) — opt-in toggle
  • ButtplugServer::output_observation_stream()Option<impl Stream<Item = OutputObservation>>
  • EngineOptions::emit_output_observations / EngineOptionsBuilder::emit_output_observations()
  • ButtplugRemoteServerEvent::OutputObservation and EngineMessage::DeviceOutputObservation variants

Observations are emitted after the dedup check but before protocol processing — they reflect command intent, not hardware success.

Test Plan

  • 7 integration tests covering all acceptance criteria (AC2.1-2.3, AC3.1-3.3, AC5.1)
  • Human test plan at docs/test-plans/2026-05-17-output-observability.md — end-to-end verification with real/simulated device
  • Multi-device StopAllDevices manual verification (automated test covers single device)

🤖 Generated with Claude Code

qdot and others added 14 commits May 17, 2026 21:19
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a new OutputObservation struct to track output commands as they are emitted
from the dedup tap point in the device manager. This type is the foundation for
observability of device output commands.

- Create crates/buttplug_server/src/device/output_observation.rs
- Add module declaration and public re-export from device/mod.rs
…r through to DeviceHandle

- Add emit_output_observations flag and builder method to ServerDeviceManagerBuilder (Task 2)
- Create observation channel conditionally based on flag in finish()
- Pass observation sender to ServerDeviceManagerEventLoop constructor (Task 3)
- Thread observation sender through event loop struct and pass to build_device_handle()
- Add output_observation_sender field to DeviceHandle (Task 4)
- Emit OutputObservation from handle_outputcmd_v4() after dedup check
- Update build_device_handle signature to accept observation sender
- Zero overhead when disabled: None stored instead of Some(sender) when flag is false

Implements output-observability AC2.1-AC2.3 and AC5.1-AC5.2 structurally.
- Add emit_output_observations bool field to EngineOptions struct
- Add emit_output_observations bool field to EngineOptionsExternal struct
- Update From<EngineOptionsExternal> impl to include new field
- Add emit_output_observations() builder method to EngineOptionsBuilder
- Field defaults to false via Default derive on both structs
- Verifies output-observability.AC1.1 and AC1.2
…rontend

Implements Tasks 3-5 from Phase 2 of output observability feature:

Task 3: Add OutputObservation variant to ButtplugRemoteServerEvent enum with
inline fields (device_index, feature_index, output_type, value). Spawn a
parallel task in ButtplugRemoteServer::new() that subscribes to the observation
stream and forwards events through the broadcast channel.

Task 4: Add DeviceOutputObservation variant to EngineMessage enum with same
fields, making observations serializable for frontend transmission.

Task 5: Handle ButtplugRemoteServerEvent::OutputObservation in
frontend_server_event_loop() by converting to EngineMessage::DeviceOutputObservation
and forwarding to frontend. Uses trace!-level logging since observations are
high-frequency (device updates every frame).

Enables observation flow: DeviceHandle -> broadcast channel -> RemoteServer ->
Frontend, completing the wiring when em_output_observations flag is enabled.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove unnecessary `use tracing::{info, trace}` import from frontend/mod.rs
  The crate already has `#[macro_use] extern crate log;` in lib.rs which
  brings log macros into scope for all modules. The explicit tracing import
  was redundant and inconsistent with the rest of the codebase.

- Add trace log when output observation stream terminates in remote_server.rs
  The observation forwarding task now logs when the stream ends, consistent
  with the pattern used in run_device_event_stream.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add comprehensive integration tests for output observation functionality,
verifying all acceptance criteria:

- AC2.1: Output commands emit observations with correct fields
- AC2.2: Deduped commands (same value) don't emit observations
- AC2.3: Observations reflect intent (emitted before protocol processing)
- AC3.1: Stop commands emit zero-value observations
- AC3.2: StopAll targets all devices and emits zero-value observations
- AC3.3: Dedup applies to stop-generated zero-value commands
- AC5.1: No observation stream when disabled (zero overhead)

All 7 tests passing; no regressions in existing tests.
Critical fix #1: test_ac3_3_stop_dedup - Proper assertion for AC3.3 stop dedup behavior
- Changed test from accepting all outcomes (match with no assertions) to strict timeout assertion
- Fixed stream consumption issue: Test 2 was only consuming 1 of 2 observations from stop command
  (device has 2 vibrate features, so stop generates 2 zero-value observations)
- Added loop to consume all stop observations before proceeding to Test 3
- Now correctly asserts that second stop with no assertion produces no new observation (dedup works)
- AC3.3 requirement: 'Stop dedup: no observation if already at zero'

Minor fix #1: Remove unused import ButtplugServerMessageVariant
- Cleaned up import at line 21 to only include ButtplugClientMessageVariant
- Resolves compiler warning for unused import

Verification: All 7 observation tests pass, full test suite passes with no regressions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Document the new output observation broadcast channel architecture,
public API surface, and integration tests in CLAUDE.md files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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