Flutter web app replacing legacy WPF CCTV surveillance keyboard controller. Includes wall overview, section view with monitor grid, camera input, PTZ control, alarm/lock/sequence BLoCs, and legacy-matching UI styling. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
173 lines
6.1 KiB
Markdown
173 lines
6.1 KiB
Markdown
---
|
|
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<br/>(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<int> 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
|
|
```
|