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>
394 lines
14 KiB
Markdown
394 lines
14 KiB
Markdown
# Business Rules Reference
|
|
|
|
> All business rules extracted from the decompiled legacy WPF source code. Each rule includes the source file and line reference for verification.
|
|
|
|
## 1. CrossSwitch Rules
|
|
|
|
**Source:** `SegmentViewModel.cs` lines 962-977
|
|
|
|
### Execution Flow
|
|
1. Stop any active playback on target viewer
|
|
2. Cancel any running sequence on target viewer
|
|
3. Call `CentralServerDriver.CrossSwitch(viewerId, cameraNumber)`
|
|
4. Log with `UserAction.CrossSwitch` for audit trail
|
|
5. Clear camera number edit state
|
|
|
|
### Preconditions
|
|
- Camera number must be valid (> 0)
|
|
- Screen must be selected
|
|
- No active alarm blocking the monitor (configurable)
|
|
- Network must be available for coordinated operations
|
|
|
|
### Camera Number Composition
|
|
```
|
|
Full Camera Number = Prefix + Entered Digits
|
|
Example: 500 (prefix) + 123 (digits) = 500123
|
|
```
|
|
- Prefix cycles through configured values (e.g., 500, 501, 502)
|
|
- Each prefix maps to a different camera server
|
|
- Prefix key (`VirtualKey.Prefix`) cycles to next prefix
|
|
|
|
### Edit Timeout
|
|
- **Source:** `SegmentViewModel.cs` lines 829-847
|
|
- Camera number entry has a configurable timeout (`CancelEditTimeout` from config)
|
|
- `DispatcherTimer` starts on first digit entry
|
|
- Timer resets on each subsequent digit
|
|
- On timeout: edit is cancelled, partial number discarded
|
|
- On Enter: edit is confirmed, CrossSwitch executed
|
|
|
|
## 2. Camera Lock Rules
|
|
|
|
**Source:** `CameraLockService.cs`, `SegmentViewModel.cs` lines 631-676
|
|
|
|
### Lock Lifecycle
|
|
```mermaid
|
|
stateDiagram-v2
|
|
[*] --> Unlocked
|
|
Unlocked --> LockRequested: User selects PTZ camera
|
|
LockRequested --> Locked: Lock acquired
|
|
LockRequested --> TakeoverDialog: Locked by another keyboard
|
|
TakeoverDialog --> TakeoverRequested: User confirms takeover
|
|
TakeoverDialog --> Unlocked: User cancels
|
|
TakeoverRequested --> Locked: Other keyboard confirms
|
|
TakeoverRequested --> Unlocked: Other keyboard rejects
|
|
Locked --> Unlocked: Manual unlock (Enter key)
|
|
Locked --> ExpiringSoon: 4 minutes elapsed
|
|
ExpiringSoon --> Locked: PTZ movement resets timer
|
|
ExpiringSoon --> Unlocked: 5 minutes elapsed (timeout)
|
|
```
|
|
|
|
### Lock Rules
|
|
| Rule | Detail | Source |
|
|
|------|--------|--------|
|
|
| Lock timeout | 5 minutes from acquisition | AppServer config |
|
|
| Expiry warning | 1 minute before timeout | `CameraLockNotificationType.ExpireSoon` |
|
|
| Reset on PTZ | Each joystick movement resets expiry timer | `SegmentViewModel:754, 941` |
|
|
| Priority levels | `Low` (default), `High` (override) | `CameraLockPriority` enum |
|
|
| Auto-unlock on CrossSwitch | Entering new camera number + Enter unlocks current | `SegmentViewModel:906-916` |
|
|
| Telemetry activation | `IsCameraTelemetryActive = true` only when locked | `SegmentViewModel:659` |
|
|
| Multiple locks | A keyboard can lock multiple cameras (HashSet) | `SegmentViewModel:631` |
|
|
| Restore on navigate | When entering segment, restore locks via `GetLockedCameraIds` | Startup logic |
|
|
|
|
### Takeover Protocol
|
|
1. Keyboard A holds lock on camera X
|
|
2. Keyboard B requests lock → gets `CameraLockResult.Acquired = false`
|
|
3. Keyboard B shows dialog: "Camera locked by [A]. Request takeover?"
|
|
4. If Yes → `RequestLock()` sends notification to Keyboard A
|
|
5. Keyboard A receives `ConfirmTakeOver` notification → shows dialog
|
|
6. If Keyboard A confirms → lock transferred to B
|
|
7. If Keyboard A rejects → B receives rejection, lock stays with A
|
|
|
|
### New Implementation (No AppServer)
|
|
- PRIMARY keyboard manages lock state in memory: `Map<int, CameraLock>`
|
|
- Lock operations via WebSocket messages to PRIMARY
|
|
- PRIMARY broadcasts lock changes to all connected keyboards
|
|
- **Degraded mode** (no PRIMARY): local-only lock tracking, no coordination
|
|
|
|
## 3. PTZ Control Rules
|
|
|
|
**Source:** `SegmentViewModel.cs` lines 739-769
|
|
|
|
### Joystick → PTZ Mapping
|
|
```
|
|
Joystick X axis → Pan (negative = left, positive = right)
|
|
Joystick Y axis → Tilt (negative = up, positive = down)
|
|
Joystick Z axis → Zoom (negative = out, positive = in)
|
|
```
|
|
|
|
### Speed Processing
|
|
| Step | Detail | Source |
|
|
|------|--------|--------|
|
|
| Raw HID value | Scaled to -255..+255 by HID driver | `CopilotDevice.HidDataProvider_DataReceived` |
|
|
| Deduplication | Only send if value changed from last sent | `ptzSpeeds` dictionary in `DoPtzAction` |
|
|
| Direction split | Negative = Left/Up/Out, Positive = Right/Down/In | Driver layer |
|
|
| Zero = Stop | Speed 0 sent as explicit stop command | Driver layer |
|
|
| No scaling | Raw value passed directly to SDK (0-255 absolute) | Confirmed in all drivers |
|
|
|
|
### Critical Finding: No Zoom-Proportional Speed
|
|
- **The app does NOT scale pan/tilt speed based on zoom level**
|
|
- Raw joystick value → directly to SDK → camera server handles proportional behavior
|
|
- This is a hardware/server feature, not an app feature
|
|
- **Flutter app should pass raw speed values unchanged**
|
|
|
|
### PTZ Preconditions
|
|
- `IsCameraTelemetryActive` must be true (camera must be locked)
|
|
- Camera must be PTZ-capable (checked via driver)
|
|
- Lock expiry resets on every joystick movement
|
|
|
|
### Bridge API for PTZ
|
|
```
|
|
POST /camera/pan {Camera: int, Direction: "left"|"right", Speed: 0-255}
|
|
POST /camera/tilt {Camera: int, Direction: "up"|"down", Speed: 0-255}
|
|
POST /camera/zoom {Camera: int, Direction: "in"|"out", Speed: 0-255}
|
|
POST /camera/stop {Camera: int}
|
|
```
|
|
|
|
## 4. Function Button Rules
|
|
|
|
**Source:** `FunctionButtonsService.cs` (78 lines)
|
|
|
|
### Configuration Structure
|
|
```json
|
|
{
|
|
"walls": {
|
|
"1": {
|
|
"F1": [
|
|
{"actionType": "CrossSwitch", "viewerId": 1001, "sourceId": 500001},
|
|
{"actionType": "CrossSwitch", "viewerId": 1002, "sourceId": 500002}
|
|
],
|
|
"F2": [
|
|
{"actionType": "SequenceStart", "viewerId": 1001, "sourceId": 5}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Execution Rules
|
|
- Buttons F1-F7 mapped per wall ID
|
|
- Each button can trigger **multiple actions** (executed sequentially)
|
|
- Action types:
|
|
- `CrossSwitch` → calls `driver.CrossSwitch(viewerId, sourceId)`
|
|
- `SequenceStart` → calls `sequenceService.Start(viewerId, sourceId)`
|
|
- After CrossSwitch action: viewer is un-maximized
|
|
- Returns `false` if no actions configured for button+wall combination
|
|
|
|
### Key Input Mapping
|
|
```
|
|
VirtualKey.F1 through VirtualKey.F7 → ExecuteFunctionButtonActions(wallId, "F1"..."F7")
|
|
```
|
|
|
|
## 5. Preposition Rules
|
|
|
|
**Source:** `PrepositionService.cs` (114 lines)
|
|
|
|
### Operations
|
|
| Operation | Method | Requires AppServer? | Bridge Endpoint |
|
|
|-----------|--------|---------------------|-----------------|
|
|
| List prepositions | `GetPrepositionNames(mediaChannelId)` | No (local config) | — |
|
|
| Call up (move to) | `CallUpPreposition(mediaChannelId, prepositionId)` | No | `POST /camera/preset` |
|
|
| Save new | `SavePrepositionToAppServer(...)` | **Yes** (legacy) | Direct via bridge (new) |
|
|
| Delete | `DeletePrepositionToAppServer(...)` | **Yes** (legacy) | Direct via bridge (new) |
|
|
| Sync from server | `UpdatePrepositionsFromAppServer()` | **Yes** (legacy) | Not needed (local config) |
|
|
|
|
### Preconditions
|
|
- Camera must be PTZ-capable
|
|
- Camera must be locked (`IsCameraTelemetryActive == true`)
|
|
- Preposition IDs are per media channel, not per camera
|
|
|
|
### New Implementation
|
|
- Preposition names stored in `prepositions.json` locally
|
|
- Call-up via bridge: `POST /camera/preset {Camera, Preset}`
|
|
- Save/delete: PRIMARY keyboard coordinates name updates and broadcasts to other keyboards
|
|
- No AppServer dependency for basic call-up
|
|
|
|
## 6. Sequence Rules
|
|
|
|
**Source:** `SequenceService.cs` (77 lines)
|
|
|
|
### Sequence Model
|
|
```
|
|
SequenceCategory → contains multiple Sequences
|
|
Sequence → runs on a specific viewer, cycling through cameras
|
|
```
|
|
|
|
### Execution Rules
|
|
- Start: `sequenceService.Start(viewerId, sequenceId)`
|
|
- Stop: `sequenceService.Stop(viewerId)`
|
|
- Only one sequence per viewer at a time
|
|
- CrossSwitch on a viewer with active sequence → **stops sequence first**
|
|
- Sequences disabled during telemetry mode (`IsCameraTelemetryActive`)
|
|
- Requires AppServer availability (legacy) → PRIMARY availability (new)
|
|
|
|
### State Events
|
|
- `ViewerSequenceStateChanged` event updates viewer state
|
|
- `GetRunningSequences()` called on startup to restore state
|
|
|
|
### New Implementation
|
|
- PRIMARY keyboard runs sequence timer logic
|
|
- Sequence definitions stored in local config
|
|
- Start/stop via WebSocket messages to PRIMARY
|
|
- STANDBY can resume sequences after promotion (query bridge for current viewer state)
|
|
|
|
## 7. Playback Rules
|
|
|
|
**Source:** `PlaybackStateService.cs`, `SegmentViewModel.cs`
|
|
|
|
### Playback States (PlayMode enum)
|
|
| Value | Mode | Description |
|
|
|-------|------|-------------|
|
|
| 0 | Unknown | — |
|
|
| 1 | PlayStop | Paused |
|
|
| 2 | PlayForward | Normal speed forward |
|
|
| 3 | PlayBackward | Normal speed backward |
|
|
| 4 | FastForward | Fast forward |
|
|
| 5 | FastBackward | Fast backward |
|
|
| 6 | StepForward | Single frame forward |
|
|
| 7 | StepBackward | Single frame backward |
|
|
| 8 | PlayBOD | Jump to beginning of day |
|
|
| 9 | PlayEOD | Jump to end of day |
|
|
| 10 | QuasiLive | Near-live with delay |
|
|
| 11 | Live | Real-time live view |
|
|
|
|
### Jog/Shuttle Mapping
|
|
**Source:** `CopilotDevice.SerialPortDataProvider`
|
|
|
|
```
|
|
Jog wheel: Single-step events (j+1 or j-1)
|
|
→ StepForward / StepBackward
|
|
|
|
Shuttle wheel: Speed values -7 to +7
|
|
-7..-1 → FastBackward (increasing speed)
|
|
0 → PlayStop (pause)
|
|
+1..+7 → FastForward (increasing speed)
|
|
```
|
|
|
|
Shuttle speed mapping (from legacy `ShuttleConverter`):
|
|
```
|
|
±1 → speed 0.5x
|
|
±2 → speed 1x
|
|
±3 → speed 2x
|
|
±4 → speed 4x
|
|
±5 → speed 8x
|
|
±6 → speed 16x
|
|
±7 → speed 32x
|
|
```
|
|
|
|
### Playback Bridge Endpoints
|
|
```
|
|
POST /viewer/set-play-mode {Viewer, PlayMode, PlaySpeed}
|
|
POST /viewer/play-from-time {Viewer, Channel, PlayMode, Time}
|
|
POST /viewer/jump-by-time {Viewer, Channel, PlayMode, TimeInSec}
|
|
```
|
|
|
|
### Playback Rules
|
|
- CrossSwitch stops active playback before switching
|
|
- Playback state tracked per viewer in `PlaybackStateService`
|
|
- Entering playback mode: navigate to `PlaybackPage`
|
|
- Plus/Minus keys in playback: not camera navigation but focus control (speed ±128)
|
|
|
|
## 8. Keyboard Input Rules
|
|
|
|
**Source:** `MainWindow.cs` (217 lines), `SegmentViewModel.cs` (771-944)
|
|
|
|
### Input Routing Architecture
|
|
```
|
|
Hardware Input → CopilotDevice → MainWindow → CurrentViewModel (if implements handler)
|
|
↓
|
|
Keyboard Emulation → PreviewKeyDown → VirtualKeyboard.Emulate → Same routing
|
|
```
|
|
|
|
### Key Mapping (SegmentViewModel)
|
|
|
|
| VirtualKey | Action | Condition |
|
|
|------------|--------|-----------|
|
|
| `Digit0-9` | Append to camera number | Begins edit mode |
|
|
| `Enter` | Confirm camera number → CrossSwitch | If editing: execute. If telemetry: unlock + execute |
|
|
| `Backspace` | Remove last digit | If editing |
|
|
| `Escape` | Cancel edit / deselect | If editing: cancel. Else: navigate back |
|
|
| `Prefix` | Cycle camera prefix | Cycles through configured prefixes |
|
|
| `Plus` | Next camera / Focus+ | If editing: ignored. If playback: focus speed +128 |
|
|
| `Minus` | Previous camera / Focus- | Similar to Plus |
|
|
| `Home` | Go to preposition / PlayLive | Context-dependent |
|
|
| `F1-F7` | Execute function button | Wall-specific actions |
|
|
| `Lock` / `JoyButton2` | Toggle camera lock | If locked: unlock. If unlocked: try lock |
|
|
| `Search` | Navigate to camera search | If no alarm on screen |
|
|
| `Prepositions` | Navigate to prepositions | If camera is PTZ + telemetry active |
|
|
| `Playback` | Navigate to playback page | If valid media channel |
|
|
| `Sequence` | Navigate to sequence categories | If screen selected + AppServer available |
|
|
| `AlarmHistory` | Navigate to alarm history | — |
|
|
| `FullScreen` | Toggle viewer maximize | If camera > 0 |
|
|
|
|
### Service Hotkey
|
|
- **Backspace held for 3 seconds** → Navigate to Service Menu
|
|
- Implemented via `DispatcherTimer` with 3000ms interval
|
|
- Timer starts on Backspace KeyDown, cancelled on KeyUp
|
|
|
|
### Keyboard Emulation Mode
|
|
- Activated when serial port not available (development)
|
|
- Uses `PreviewKeyDown` / `PreviewKeyUp` window events
|
|
- `VirtualKeyboard.Emulate()` converts `System.Windows.Input.Key` to `VirtualKey`
|
|
- Ignores key repeat (`e.IsRepeat` check)
|
|
|
|
## 9. Alarm Rules
|
|
|
|
**Source:** `CameraAlarmService.cs` (47 lines), `AlarmService` in Flutter
|
|
|
|
### Alarm States
|
|
```
|
|
vasNewAlarm → vasPresented → vasStacked → vasConfirmed → vasRemoved
|
|
```
|
|
|
|
### Alarm Blocking
|
|
- Active alarm on a camera → blocks CrossSwitch to that camera's monitor
|
|
- `HasCameraAlarms(cameraId)` returns true if unresolved alarm exists
|
|
- Alarm state displayed as red highlight on monitor tile
|
|
|
|
### Alarm Sync Strategy
|
|
1. **Startup:** Query all active alarms from bridges (`GET /alarms/active`)
|
|
2. **Real-time:** WebSocket events (`EventStarted`, `EventStopped`)
|
|
3. **Periodic:** Re-sync every 30 seconds (configurable)
|
|
4. **Hourly:** Full alarm mapping refresh (`CameraAlarmsUpdateWorker`)
|
|
|
|
### Alarm History
|
|
- `GetAlarmForCamera(cameraId, from, to)` → list of `CameraAlarm` records
|
|
- Times converted from UTC to local time for display
|
|
- Ordered by `StartTime` descending (newest first)
|
|
|
|
## 10. Monitor Wall Configuration Rules
|
|
|
|
**Source:** `MonitorWallConfiguration`, `SegmentModel`, `ScreenModel`
|
|
|
|
### Wall Structure
|
|
```
|
|
Wall → Segments → Monitors → Viewers
|
|
```
|
|
|
|
| Level | Example | Description |
|
|
|-------|---------|-------------|
|
|
| Wall | Wall 1 | The physical monitor wall installation |
|
|
| Segment | "Top Row" | A logical grouping of monitors |
|
|
| Monitor | Physical screen #1 | One physical display (can have 1 or 4 viewers) |
|
|
| Viewer | Viewer 1001 | One video stream slot on a monitor |
|
|
|
|
### Quad View
|
|
- A physical monitor can show 1 camera (single) or 4 cameras (quad)
|
|
- `PhysicalMonitor.IsQuadView` determines layout
|
|
- Quad view has 4 `viewerIds`, single has 1
|
|
|
|
### Screen Selection Rules
|
|
- Only one screen/viewer selected at a time
|
|
- Selection clears camera number edit state
|
|
- Selected viewer highlighted with cyan border
|
|
- CrossSwitch operates on selected viewer
|
|
|
|
## 11. Network Availability Rules
|
|
|
|
**Source:** `NetworkAvailabilityState`, `NetworkAvailabilityWorker`
|
|
|
|
### Availability States
|
|
- Camera server connection: tracked per server
|
|
- AppServer/PRIMARY connection: single state
|
|
- Combined state determines feature availability
|
|
|
|
### Feature Availability Matrix
|
|
|
|
| Feature | Camera Server Required | PRIMARY Required |
|
|
|---------|----------------------|------------------|
|
|
| CrossSwitch | Yes | No |
|
|
| PTZ control | Yes | No |
|
|
| Camera lock (coordinated) | No | **Yes** |
|
|
| Camera lock (local-only) | No | No |
|
|
| Preposition call-up | Yes | No |
|
|
| Preposition save/delete | Yes | **Yes** |
|
|
| Sequence start/stop | No | **Yes** |
|
|
| Alarm display | Yes | No |
|
|
| Config sync | No | **Yes** |
|
|
|
|
### Degraded Mode
|
|
When PRIMARY is unavailable:
|
|
- CrossSwitch and PTZ **always work** (direct to bridge)
|
|
- Lock shown as "local only" (no coordination between keyboards)
|
|
- Sequences cannot be started/stopped
|
|
- Config changes not synchronized
|