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:
393
Docs/legacy-architecture/migration-business-rules.md
Normal file
393
Docs/legacy-architecture/migration-business-rules.md
Normal file
@@ -0,0 +1,393 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user