Files
COPILOT/Docs/legacy-architecture/migration-business-rules.md
klas 40143734fc 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>
2026-02-12 14:57:38 +01:00

14 KiB

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

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

{
  "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