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>
288 lines
9.1 KiB
Markdown
288 lines
9.1 KiB
Markdown
---
|
|
title: "Data Flows"
|
|
description: "CrossSwitch, Alarms, Sequences, Playback, and Camera Lock workflows"
|
|
---
|
|
|
|
# Data Flows
|
|
|
|
## 1. CrossSwitch (Switch Camera to Monitor)
|
|
|
|
The primary operation — routing a camera feed to a physical monitor output.
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Op as Operator
|
|
participant VM as SegmentViewModel
|
|
participant CenD as CentralServerDriver
|
|
participant Srv as Camera Server
|
|
|
|
Op->>VM: Enter camera number (digits + Enter)
|
|
VM->>VM: Validate camera exists via MediaChannelService
|
|
|
|
alt Camera exists
|
|
alt Playback active on this viewer
|
|
VM->>VM: playbackStateService.SetPlaybackState(false)
|
|
end
|
|
alt Sequence active on this viewer
|
|
VM->>VM: hub.Stop(viewerId)
|
|
end
|
|
|
|
VM->>CenD: CrossSwitch(viewerId, cameraNumber)
|
|
CenD->>Srv: SDK CrossSwitch command
|
|
|
|
Note over Srv: Server switches video output<br/>and fires ViewerConnectionEvent
|
|
|
|
else Camera not found
|
|
VM->>VM: ShowError("MediaChannel does not exist", 2s)
|
|
end
|
|
```
|
|
|
|
### CrossSwitch Triggers
|
|
1. **Numeric entry** — Type camera number + Enter
|
|
2. **Plus/Minus keys** — Cycle to next/previous camera in sorted order
|
|
3. **Function buttons** — F1-F7 mapped to preconfigured CrossSwitch actions
|
|
4. **Home key** — When not in PTZ mode or playback, executes function button "Home"
|
|
|
|
### Camera Number Editing
|
|
|
|
```
|
|
State: Not Editing
|
|
│
|
|
├─ Press digit → BeginEdit(), append digit, start cancel timer
|
|
│
|
|
▼
|
|
State: Editing (showing entered digits)
|
|
│
|
|
├─ Press digit → append digit, restart cancel timer
|
|
├─ Press Backspace → remove last digit, restart cancel timer
|
|
├─ Press Enter → validate, CrossSwitch if valid, EndEdit()
|
|
└─ Cancel timer fires → CancelEdit(), revert to original camera
|
|
```
|
|
|
|
Camera number can be up to 6 digits. Configurable prefixes (e.g., "500", "501", "502") are prepended automatically based on the selected prefix.
|
|
|
|
## 2. Camera Lock Workflow
|
|
|
|
Camera locks coordinate PTZ access across multiple keyboards via the AppServer SignalR hub.
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant K1 as Keyboard 1 (Low priority)
|
|
participant Hub as AppServer Hub
|
|
participant DB as SQLite DB
|
|
participant K2 as Keyboard 2 (High priority)
|
|
|
|
K1->>Hub: TryLockCamera(camId, "K1", Low)
|
|
Hub->>DB: Check existing lock
|
|
DB-->>Hub: No lock exists
|
|
Hub->>DB: Create lock(camId, "K1", Low, expires=5min)
|
|
Hub-->>K1: LockResult(Acquired=true)
|
|
K1->>K1: IsCameraTelemetryActive = true
|
|
|
|
Note over K1: Operator controls PTZ...<br/>Each action calls ResetCameraLockExpiration()
|
|
|
|
K2->>Hub: TryLockCamera(camId, "K2", High)
|
|
Hub->>DB: Check existing lock → held by K1
|
|
Hub-->>K2: LockResult(Acquired=false, Owner="K1")
|
|
|
|
K2->>Hub: RequestCameraLock(camId, "K2", High)
|
|
Hub->>K1: CameraLockNotify(ConfirmTakeOver)
|
|
K1->>K1: Show dialog "K2 requests lock"
|
|
|
|
alt K1 confirms takeover
|
|
K1->>Hub: CameraLockConfirmTakeOver(confirm=true)
|
|
Hub->>K1: CameraLockNotify(Unlocked)
|
|
Hub->>K2: CameraLockNotify(Confirmed)
|
|
K2->>Hub: TryLockCamera(camId, "K2", High)
|
|
Hub-->>K2: LockResult(Acquired=true)
|
|
else K1 denies
|
|
K1->>Hub: CameraLockConfirmTakeOver(confirm=false)
|
|
Hub->>K2: CameraLockNotify(Rejected)
|
|
end
|
|
```
|
|
|
|
### Lock Priority Levels
|
|
Configured per keyboard in `appsettings-copilot.json`:
|
|
- `Low` — Standard operator
|
|
- `High` — Supervisor (can request takeover)
|
|
|
|
### Lock Expiration
|
|
- **Timeout:** 5 minutes (`LockExpirationConfig.LockExpirationTimeout`)
|
|
- **Warning:** 1 minute before expiration (`NotificationBeforeExpiration`)
|
|
- **Reset:** Every PTZ action or explicit reset call extends the timer
|
|
|
|
## 3. Alarm System
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Worker as CameraAlarmsUpdateWorker
|
|
participant CAS as CameraAlarmService
|
|
participant Hub as CopilotHub
|
|
participant AS as AppServer
|
|
participant Camea as Camea API
|
|
|
|
Note over Worker: Background worker (periodic)
|
|
|
|
Worker->>CAS: UpdateCameraToAlarmsMapping()
|
|
CAS->>Hub: GetCamerasWithAlarms()
|
|
Hub->>AS: Query
|
|
AS->>Camea: GET /api/alarms
|
|
Camea-->>AS: List of cameras with active alarms
|
|
AS-->>Hub: HashSet<int> cameraIds
|
|
Hub-->>CAS: Store in camerasWithAlarms
|
|
|
|
Note over CAS: UI can now check HasCameraAlarms(cameraId)
|
|
|
|
Note over Worker: Also on SegmentViewModel...
|
|
participant VM as SegmentViewModel
|
|
VM->>CAS: GetAlarmForCamera(camId, from, to)
|
|
CAS->>Hub: GetAlarmsForCamera(camId, from, to)
|
|
Hub->>AS: Query alarm history
|
|
AS-->>Hub: List<CameraAlarm>
|
|
Hub-->>CAS: Alarms (UTC → LocalTime)
|
|
CAS-->>VM: Sorted by StartTime desc
|
|
```
|
|
|
|
### Alarm-Related Behavior
|
|
- Screens with active alarms **block** camera number editing (CrossSwitch disabled)
|
|
- Alarm history page uses configurable regex to extract camera ID from alarm names: `^\D*(?<CameraId>\d{6})\D*$`
|
|
- Default search interval: 30 days
|
|
|
|
## 4. Sequence Management
|
|
|
|
Sequences automatically cycle cameras on a monitor at configurable intervals.
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Op as Operator
|
|
participant VM as SegmentViewModel
|
|
participant SeqS as SequenceService
|
|
participant Hub as SignalR Hub
|
|
participant AS as AppServer
|
|
|
|
Op->>VM: Press Sequence key
|
|
VM->>VM: Navigate to SequenceCategoriesPage
|
|
Op->>VM: Select category
|
|
VM->>SeqS: GetSequences(categoryId)
|
|
SeqS->>Hub: GetSequences(categoryId)
|
|
Hub-->>SeqS: List<SequenceMessage>
|
|
Op->>VM: Select sequence
|
|
VM->>SeqS: Start(viewerId, sequenceId)
|
|
SeqS->>Hub: Start(viewerId, sequenceId)
|
|
|
|
Note over AS: AppServer runs sequence timer
|
|
AS->>Hub: ViewerSequenceStateChanged
|
|
Hub->>SeqS: OnChanged(viewerSequenceState)
|
|
SeqS->>VM: Update UI (sequence indicator)
|
|
|
|
Note over AS: Timer fires every N seconds
|
|
AS->>AS: CrossSwitch next camera in list
|
|
```
|
|
|
|
### Sequence Configuration
|
|
```json
|
|
{
|
|
"Id": 1,
|
|
"CategoryId": 1,
|
|
"Name": "Test-A",
|
|
"Interval": 1, // seconds between switches
|
|
"MediaChannels": [500100, 500101, 500102, 500103]
|
|
}
|
|
```
|
|
|
|
Sequences are **run on the AppServer**, not on the keyboard. The keyboard only starts/stops them.
|
|
|
|
## 5. Playback Control
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Op as Operator
|
|
participant VM as SegmentViewModel
|
|
participant VC as IViewerController
|
|
participant Srv as Camera Server
|
|
|
|
Note over Op: Playback enters via Jog/Shuttle<br/>or Playback button
|
|
|
|
Op->>VM: Jog right (arrow key)
|
|
VM->>VM: IsPlaybackActive = true
|
|
VM->>VC: StepForwardAndStop()
|
|
VC->>Srv: Step forward one frame
|
|
|
|
Op->>VM: Shuttle right position 3
|
|
VM->>VM: TryGetShuttleSpeed(speeds, out speed, out forward)
|
|
Note over VM: position 3 → speeds[2] = 5
|
|
VM->>VC: FastForward(5)
|
|
VC->>Srv: Play at 5x speed
|
|
|
|
Op->>VM: Shuttle center
|
|
VM->>VC: Stop()
|
|
|
|
Op->>VM: Press Home key
|
|
VM->>VC: PlayLive()
|
|
VM->>VM: IsPlaybackActive = false
|
|
```
|
|
|
|
### Playback Controls
|
|
| Input | Action | SDK Method |
|
|
|-------|--------|------------|
|
|
| Jog Left | Step back 1 frame | `StepBackwardAndStop()` |
|
|
| Jog Right | Step forward 1 frame | `StepForwardAndStop()` |
|
|
| Shuttle Left 1-7 | Rewind at speed | `FastBackward(speed)` |
|
|
| Shuttle Right 1-7 | Fast forward at speed | `FastForward(speed)` |
|
|
| Shuttle Center | Pause | `Stop()` |
|
|
| Home | Return to live | `PlayLive()` |
|
|
|
|
## 6. Function Buttons
|
|
|
|
F1-F7 and Home keys execute preconfigured action lists per wall:
|
|
|
|
```mermaid
|
|
graph TD
|
|
Key["F1-F7 / Home Key Press"] --> FBS["FunctionButtonsService"]
|
|
FBS --> Config["Load from appsettings-function-buttons.json"]
|
|
Config --> Actions["Get actions for wallId + buttonKey"]
|
|
Actions --> ForEach["For each action:"]
|
|
|
|
ForEach -->|CrossSwitch| CS["CentralServerDriver.CrossSwitch(viewerId, sourceId)<br/>+ Minimize viewer"]
|
|
ForEach -->|SequenceStart| SS["SequenceService.Start(viewerId, sourceId)"]
|
|
```
|
|
|
|
Example: Pressing F1 on Wall 2 executes:
|
|
```json
|
|
{ "ViewerId": 12322, "ActionType": "CrossSwitch", "SourceId": 500123 }
|
|
```
|
|
This switches camera 500123 to viewer 12322.
|
|
|
|
## 7. Configuration Sync
|
|
|
|
Configuration files are managed centrally on the AppServer and synced to keyboards:
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant AS as AppServer
|
|
participant Hub as SignalR Hub
|
|
participant CfgS as ConfigurationService
|
|
participant Disk as Local JSON Files
|
|
|
|
Note over CfgS: On startup
|
|
CfgS->>Hub: GetConfigurationFile(filename) for each manager
|
|
Hub-->>CfgS: ConfigurationFile (content + hash)
|
|
CfgS->>Disk: Compare hash, write if changed
|
|
|
|
Note over AS: Admin changes config via Blazor UI
|
|
AS->>Hub: ConfigurationFileChanged event
|
|
Hub->>CfgS: OnConfigurationFileChanged(file)
|
|
CfgS->>Disk: Write updated content
|
|
|
|
Note over CfgS: Also triggered when AppServer<br/>becomes available after disconnect
|
|
```
|
|
|
|
Configuration files synced:
|
|
- `appsettings-copilot.json` — Keyboard-specific settings
|
|
- `appsettings-camera-servers.json` — Camera server connections
|
|
- `appsettings-monitor-wall.json` — Monitor wall topology
|
|
- `appsettings-function-buttons.json` — Function button actions
|
|
- `appsettings-prepositions.json` — Camera prepositions
|
|
- `appsettings-sequences.json` — Sequence definitions
|
|
- `appsettings-sequence-categories.json` — Sequence categories
|