# 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
