feat: add output observability for device commands#886
Open
qdot wants to merge 14 commits into
Open
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
OutputObservationevents for every non-deduped output command sent to a deviceDeviceHandlethroughServerDeviceManager→ButtplugServer→ButtplugRemoteServer→FrontendasEngineMessage::DeviceOutputObservationOption<broadcast::Sender>isNone, no channels allocated, no atomic loadsDetails
New public APIs:
OutputObservationstruct (device_index,feature_index,output_type,value)ServerDeviceManagerBuilder::emit_output_observations(bool)— opt-in toggleButtplugServer::output_observation_stream()→Option<impl Stream<Item = OutputObservation>>EngineOptions::emit_output_observations/EngineOptionsBuilder::emit_output_observations()ButtplugRemoteServerEvent::OutputObservationandEngineMessage::DeviceOutputObservationvariantsObservations are emitted after the dedup check but before protocol processing — they reflect command intent, not hardware success.
Test Plan
docs/test-plans/2026-05-17-output-observability.md— end-to-end verification with real/simulated device🤖 Generated with Claude Code