--- title: "PTZ Control Flow" description: "Complete joystick → Pan/Tilt/Zoom → SDK command pipeline with speed analysis" --- # PTZ Control Flow ## End-to-End Pipeline ```mermaid sequenceDiagram participant HW as Joystick (HID) participant Dev as CopilotDevice participant MW as MainWindow participant VM as SegmentViewModel participant CCS as CameraControllerService participant Drv as GeViScope/GCore Driver participant SDK as Native SDK (PLC) participant Srv as Camera Server HW->>Dev: HID report (raw axis data) Dev->>Dev: Scale to -255..+255 Dev->>MW: JoystickMoved(x, y, z) MW->>MW: Dispatcher.Invoke (UI thread) MW->>VM: IJoystickHandler.OnJoystickMoved(x, y, z) alt IsCameraTelemetryActive (PTZ locked) VM->>VM: DoPtzAction(x, Pan, controller.Pan) VM->>VM: DoPtzAction(y, Tilt, controller.Tilt) VM->>VM: DoPtzAction(z, Zoom, controller.Zoom) Note over VM: DoPtzAction only sends if speed changed
(deduplication via ptzSpeeds dictionary) VM->>CCS: GetMovementController(cameraNumber) CCS->>Drv: GetMovementControllerForChannel(id) Drv-->>VM: IMovementController alt x changed (Pan) alt x > 0 VM->>Drv: PanRight(x) else x < 0 VM->>Drv: PanLeft(abs(x)) else x == 0 VM->>Drv: PanStop() end end Drv->>SDK: GscAct_PanRight(channelId, speed) SDK->>Srv: TCP PLC command VM->>VM: ResetCameraLockExpiration() else Not locked Note over VM: Joystick events ignored end ``` ## Speed Value Processing ### Layer 1: HID → Raw Value ``` Joystick physical position → HID report → GetScaledValue(-255.0, 255.0) → integer ``` The joystick reports continuously while being held. Each axis change fires independently. ### Layer 2: Deduplication (DoPtzAction) ```csharp private void DoPtzAction(int? value, PtzAction ptzActionFlag, Action ptzAction) { if (value.HasValue) { int speed = value.GetValueOrDefault(); // Only send if speed actually changed if (!ptzSpeeds.TryGetValue(ptzActionFlag, out var prevSpeed) || speed != prevSpeed) { ptzSpeeds[ptzActionFlag] = speed; ptzAction(speed); // Call Pan/Tilt/Zoom with raw value } } } ``` **Key insight:** The `ptzSpeeds` dictionary prevents flooding the SDK with duplicate commands. Only changed values are forwarded. ### Layer 3: Direction Resolution (IMovementControllerExtensions) ```csharp public static void Pan(this IMovementController controller, int speed) { if (speed > 0) controller.PanRight(speed); if (speed < 0) controller.PanLeft(Math.Abs(speed)); if (speed == 0) controller.PanStop(); } // Same pattern for Tilt and Zoom ``` ### Layer 4: SDK Command ```csharp // GeViScope example public void PanRight(int speed) { using var channelId = new GscMediaChannelID(mediaChannelId); using var action = new GscAct_PanRight(channelId, speed); plcWrapper()?.SendAction(action); } ``` The speed value (0-255) is passed directly to the native SDK with no transformation. ## Speed Range Summary ``` Joystick Position → Speed Value → SDK Parameter ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Full Left/Up → -255 → PanLeft(255) / TiltUp(255) Half Left/Up → ~-128 → PanLeft(128) / TiltUp(128) Dead Center → 0 → PanStop() / TiltStop() Half Right/Down → ~+128 → PanRight(128) / TiltDown(128) Full Right/Down → +255 → PanRight(255) / TiltDown(255) Zoom twist out → -255 → ZoomOut(255) Zoom center → 0 → ZoomStop() Zoom twist in → +255 → ZoomIn(255) ``` ## Zoom-Proportional Speed: NOT in App Code **Critical finding:** The COPILOT WPF application does NOT implement zoom-proportional pan/tilt speed adjustment. The raw joystick value (-255 to +255) passes through all layers unmodified. The "slower pan/tilt when zoomed in" behavior that operators observe is provided by one or more of: 1. **Camera hardware** — Most PTZ cameras (Axis, Bosch, Pelco) have built-in "proportional PTZ" that adjusts mechanical speed based on current zoom level 2. **GeViScope/G-Core server** — The video management software may apply speed scaling before sending commands to the physical camera 3. **PTZ protocol** — Some protocols (ONVIF, Pelco-D) support proportional speed as a protocol-level feature **Implication for Flutter rewrite:** The new system will get this behavior "for free" as long as it sends the same speed values (0-255) to the same server/camera infrastructure. ## Focus Control Focus is controlled via the `+` and `-` hardware keys (not the joystick): ``` Key Down (+) → FocusNear(128) (fixed speed) Key Down (-) → FocusFar(128) (fixed speed) Key Up → FocusStop() ``` The `DefaultFocusSpeed` constant is **128** (half of maximum). ## PTZ Lock Requirement PTZ commands are only sent when `IsCameraTelemetryActive == true`, which requires: 1. The selected camera must be `Movable` (from MediaChannel metadata) 2. The operator must acquire a camera lock via the AppServer 3. The lock has a 5-minute timeout with a 1-minute warning 4. Each PTZ action resets the lock expiration timer ```mermaid stateDiagram-v2 [*] --> Unlocked Unlocked --> LockRequested: Press Lock/Button2 LockRequested --> Locked: Lock acquired LockRequested --> ConfirmDialog: Lock held by other operator ConfirmDialog --> TakeOverRequested: Confirm takeover TakeOverRequested --> Locked: Takeover granted TakeOverRequested --> Unlocked: Takeover denied Locked --> ExpirationWarning: 4 minutes elapsed ExpirationWarning --> Locked: Any PTZ action (resets timer) ExpirationWarning --> Unlocked: 5 minutes elapsed (auto-unlock) Locked --> Unlocked: Press Lock/Button2 (manual unlock) Locked --> Unlocked: TakenOver by higher priority ```