# 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` - 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