Initial commit: COPILOT D6 Flutter keyboard controller
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>
This commit is contained in:
172
Docs/legacy-architecture/ptz-control.md
Normal file
172
Docs/legacy-architecture/ptz-control.md
Normal file
@@ -0,0 +1,172 @@
|
||||
---
|
||||
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
|
||||
```
|
||||
Reference in New Issue
Block a user