Skip to content

fix: track press-and-hold per device action and make it thread-safe - #1493

Merged
ndorin merged 2 commits into
releasefrom
feature/demo-room
Sep 29, 2026
Merged

ndorin merged 2 commits into
releasefrom
feature/demo-room

Conversation

@ndorin

@ndorin ndorin commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes #1489.

  • PressAndHoldHandler keyed holds by device only, in an unlocked static dictionary. Two holds on one device at once (e.g. cameraLeft + cameraUp for a diagonal PTZ move) collided: a duplicate-key exception, a second action that never started, and an axis that stopped about 1 s late.
  • Holds are now keyed by device and action path via a new HandlePressAndHold(deviceKey, actionPath, content, action) overload. All 47 call sites pass their own path, and CameraControlMessenger keys its holds per action too.
  • Start, heartbeat reset, release and timeout run under one lock, so each action starts once and stops once, in order. A timeout only stops its action if that hold is still current.
  • The original device-only overload is kept for plugins that call it.

Not in this PR: SIMPLCameraMessenger, SIMPLVtcMessenger, IHasCodecCamerasMessenger and MobileControlSIMPLRoomBridge call GetPressAndHoldHandler with the message value (pressed/held/released) as the hold key, so heartbeats and releases never find the hold. That's a separate bug in those callers.

Test plan

  • Messengers and MobileControl projects build.
  • Harness against the real class: cameraLeft/cameraUp pressed at the same moment from two threads, held, released, 200 runs: each axis started and stopped exactly once every time.
  • Release stops immediately; a hold with no heartbeat times out once; a late release after timeout does nothing; the device-only overload behaves as before.
  • Diagonal PTZ on a real camera through Mobile Control.

🤖 Generated with Claude Code

Holds were keyed by device only in an unlocked static dictionary, so two
simultaneous holds on one device (e.g. pan + tilt for a diagonal PTZ move)
collided: a duplicate-key exception, a second action that never started,
and an axis that stopped ~1s late. Holds are now keyed by device and action
path, and start/reset/stop/expiry run under a lock so each action starts
and stops exactly once. The device-only overload is kept for plugins.

Fixes #1489

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

Timer-generation races, failed-start cleanup, and global callback serialization remain unresolved.

Review effort: Balanced
Findings: 3 Medium severity

Open (3)
What changed in this PR

Fixes #1489 by tracking press-and-hold state per device action and synchronizing timer access.

Changes:

  • Adds action-specific hold keys across 47 call sites.
  • Synchronizes hold lifecycle operations.
  • Preserves the legacy device-only overload.
File Description
MobileControlEssentialsRoomBridge.cs Keys master-volume holds per direction.
PressAndHoldHandler.cs Adds action keys and synchronized timer management.
ILevelControlsMessenger.cs Separates level-control holds.
IBasicVolumeControlsMessenger.cs Separates volume holds.
CameraControlMessenger.cs Separates PTZ holds per action.
ITransportMessenger.cs Adds transport action keys.
ISetTopBoxControlsMessenger.cs Adds set-top-box action keys.
INumericMessenger.cs Adds numeric-button action keys.
IDvrMessenger.cs Adds DVR action keys.
IDPadMessenger.cs Adds directional-pad action keys.
IColorMessenger.cs Adds color-button action keys.
IChannelMessenger.cs Adds channel-control action keys.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Addresses review on #1493: device callbacks now run under a per-hold lock
instead of one global lock, a start that throws no longer leaves the key
held, and each heartbeat replaces the hold's timer so an expiry already
queued for the old timer can't end a hold that was just extended. A hold
stays registered until its stop has run, keeping start/stop ordered per key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ndorin
ndorin merged commit f28148c into release Sep 29, 2026
3 checks passed
@ndorin
ndorin deleted the feature/demo-room branch September 29, 2026 21:02
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