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>
10 KiB
10 KiB
Migration Guide: WPF → Flutter (No AppServer)
This guide maps every component of the legacy WPF COPILOT D6 system to its Flutter equivalent. The key architectural change: there is no centralized AppServer — the PRIMARY keyboard handles coordination.
Architecture Transformation
graph TB
subgraph "LEGACY Architecture"
direction TB
L_HW["Hardware Keyboard"] --> L_App["Copilot.App.exe<br/>(WPF .NET 7)"]
L_App -->|"Native SDK DLL<br/>(in-process)"| L_Cam["Camera Servers"]
L_App -->|"SignalR<br/>(HTTPS/WSS)"| L_AS["AppServer<br/>(separate machine)"]
L_AS --> L_DB["SQLite DB"]
end
subgraph "NEW Architecture"
direction TB
N_HW["Hardware Keyboard"] --> N_App["Flutter App<br/>(Web/Desktop)"]
N_App -->|"REST HTTP<br/>(localhost bridges)"| N_Bridge["C# Bridges<br/>(.NET 8)"]
N_Bridge -->|"Native SDK"| N_Cam["Camera Servers"]
N_App -->|"WebSocket<br/>(:8090)"| N_Primary["PRIMARY Keyboard<br/>(coordination)"]
end
What Replaces the AppServer?
The legacy AppServer was a centralized ASP.NET Core server providing:
| AppServer Feature | New Approach | Where It Runs |
|---|---|---|
| Camera locks (SignalR hub) | WebSocket hub + in-memory state | PRIMARY keyboard |
| Sequence execution | Sequence runner service | PRIMARY keyboard |
| Config file sync | Local JSON files + PRIMARY broadcast | Each keyboard |
| Alarm history (Camea API) | Bridge alarm endpoints + periodic sync | Each keyboard queries bridges |
| Viewer state tracking | Bridge WebSocket events | Each keyboard tracks locally |
| Auto-update | Out of scope (Phase 4+) | — |
| Admin UI (Blazor) | Not needed initially | — |
PRIMARY/STANDBY Model
sequenceDiagram
participant K1 as Keyboard 1 (PRIMARY)
participant K2 as Keyboard 2 (STANDBY)
participant B as C# Bridges
participant C as Camera Servers
Note over K1,K2: Normal operation
K1->>B: Direct commands (CrossSwitch, PTZ)
K2->>B: Direct commands (CrossSwitch, PTZ)
K1->>K2: Heartbeat every 2s via WebSocket
K1->>K2: State sync (locks, sequences)
Note over K1,K2: PRIMARY failure
K2->>K2: No heartbeat for 6s
K2->>K2: Self-promote to PRIMARY
K2->>B: Continue direct commands
Note over K2: Lock/sequence state rebuilt from bridges
Key principle: Direct commands (CrossSwitch, PTZ) always work — they go straight to bridges. Only coordination features (locks, sequences) need the PRIMARY.
Component Mapping: Legacy → Flutter
Assembly-Level Mapping
| Legacy Assembly | Flutter Equivalent | Status |
|---|---|---|
Copilot.App.dll (WPF UI + ViewModels) |
copilot_keyboard/ Flutter app |
Partially built |
Copilot.Device.dll (Serial + HID) |
Web Serial API / flutter_libserialport |
Not started |
Copilot.Drivers.GeViScope.dll |
GeViScope Bridge (:7720) REST API | Complete |
Copilot.Drivers.GCore.dll |
G-Core Bridge (:7721) REST API | Complete |
Copilot.Drivers.GeviSoft.dll |
GeViServer Bridge (:7710) REST API | Minimal |
Copilot.Drivers.Common.dll |
Bridge REST API contracts | Complete |
Copilot.Common.dll |
config/, domain/entities/ |
Partially built |
Copilot.Common.Services.dll |
data/services/ (BridgeService, StateService) |
Partially built |
Copilot.AppServer.Client.dll |
CoordinationService (WebSocket to PRIMARY) |
Not started |
Copilot.AppServer.dll |
PRIMARY keyboard coordination logic | Not started |
Copilot.AppServer.Database.dll |
In-memory state on PRIMARY | Not started |
Class-Level Mapping
| Legacy Class | Flutter Class | BLoC | Notes |
|---|---|---|---|
SegmentViewModel (1323 lines) |
Split into multiple BLoCs | WallBloc + PtzBloc + CameraBloc |
Core logic, needs decomposition |
MainWindow (217 lines) |
MainScreen + KeyboardService |
— | Input routing |
CameraControllerService (65 lines) |
BridgeService |
— | Already mapped to REST |
CameraLockService (61 lines) |
LockService (new) |
LockBloc (new) |
Via PRIMARY WebSocket |
SequenceService (77 lines) |
SequenceService (new) |
SequenceBloc (new) |
Via PRIMARY WebSocket |
ConfigurationService (100 lines) |
AppConfig + JSON files |
— | Local files, no sync |
FunctionButtonsService (78 lines) |
FunctionButtonService (new) |
WallBloc |
Config-driven |
PrepositionService (114 lines) |
PrepositionService (new) |
PrepositionBloc (new) |
Via bridge REST |
CameraAlarmService (47 lines) |
AlarmService (exists) |
AlarmBloc (exists) |
Already implemented |
PlaybackStateService (37 lines) |
PlaybackService (new) |
PlaybackBloc (new) |
Via bridge REST |
CopilotDevice |
KeyboardService (new) |
— | Web Serial + HID APIs |
NavigationService |
GoRouter |
— | Already in pubspec |
What's Already Built vs. What's Missing
Already Built in Flutter
| Feature | Files | Status |
|---|---|---|
| CrossSwitch (camera → monitor) | WallBloc, BridgeService |
Working |
| PTZ control (Pan/Tilt/Zoom) | PtzBloc, BridgeService |
Working |
| Camera number input (numpad) | WallBloc, MainScreen |
Working |
| Camera prefix selection (500/501/502) | WallBloc |
Working |
| Monitor state tracking | MonitorBloc, StateService |
Working |
| Alarm monitoring | AlarmBloc, AlarmService |
Working |
| Connection management | ConnectionBloc, BridgeService |
Working |
| Wall grid UI (5 sections) | WallOverview, SectionView |
Working |
| WebSocket event streaming | BridgeService |
Working |
| Bridge health checks | BridgeService |
Working |
| DI container (GetIt) | injection_container.dart |
Working |
Missing — Must Be Built
| Feature | Priority | Complexity | Legacy Reference |
|---|---|---|---|
| Camera lock system | Critical | High | CameraLockService, SegmentViewModel:631-676 |
| PRIMARY/STANDBY coordination | Critical | High | Replaces AppServer |
| Hardware keyboard (Serial) | Critical | Medium | CopilotDevice, SerialPortDataProvider |
| Hardware joystick (HID) | Critical | Medium | JoystickHidDataProvider |
| Function buttons (F1-F7) | High | Low | FunctionButtonsService |
| Prepositions (saved positions) | High | Medium | PrepositionService |
| Camera number edit timeout | High | Low | SegmentViewModel:829-847 |
| Playback mode (jog/shuttle) | Medium | Medium | PlaybackStateService, viewer controller |
| Sequences (camera cycling) | Medium | High | SequenceService |
| Keyboard emulation (dev mode) | Medium | Low | MainWindow:60-104 |
| Service menu | Low | Low | Long-press Backspace 3s |
| Config hot-reload | Low | Medium | ConfigurationService |
| CrossSwitch rules engine | Medium | Medium | crossswitch-rules.json |
| Alarm history view | Low | Low | CameraAlarmService.GetAlarmForCamera |
Implementation Phases
Phase 2A: Camera Lock System (Critical)
The lock system is the most complex missing piece. In the legacy app:
- User selects a PTZ camera → triggers lock attempt
- If lock acquired → PTZ telemetry enabled
- If locked by another keyboard → dialog: "Request takeover?"
- Lock expires after 5 minutes unless reset by PTZ movement
- 1-minute warning before expiration
- Priority levels: Low (default), High (override)
New implementation:
- PRIMARY keyboard manages lock state in memory
- Keyboards send lock requests via WebSocket to PRIMARY
- PRIMARY broadcasts lock state changes to all keyboards
- In degraded mode (no PRIMARY): local-only locks (no coordination)
Phase 2B: PRIMARY/STANDBY Coordination
WebSocket Hub on PRIMARY (:8090)
├── /ws/heartbeat → 2-second heartbeat
├── /ws/locks → Lock state sync
├── /ws/sequences → Sequence state sync
└── /ws/state → General state broadcast
Phase 3: Hardware Input
Serial port protocol (from CopilotDevice):
- Prefix
p= key press,r= key release - Prefix
j= jog value,s= shuttle value - Prefix
v= version,h= heartbeat
HID joystick:
- VendorId: 10959 (0x2ACF), ProductId: 257 (0x0101)
- 3 axes: X (pan), Y (tilt), Z (zoom)
- Value range: -255 to +255
Phase 4: Sequences & Playback
- Sequences run on PRIMARY keyboard (not AppServer)
- Playback via bridge viewer control endpoints
- Jog/shuttle wheel maps to playback speed (-7 to +7)
Bridge API Reference (Quick)
Both bridges expose identical endpoints:
POST /viewer/connect-live {Viewer, Channel} → CrossSwitch
POST /viewer/clear {Viewer} → Clear monitor
POST /camera/pan {Camera, Direction, Speed} → PTZ pan
POST /camera/tilt {Camera, Direction, Speed} → PTZ tilt
POST /camera/zoom {Camera, Direction, Speed} → PTZ zoom
POST /camera/stop {Camera} → Stop PTZ
POST /camera/preset {Camera, Preset} → Go to preset
GET /monitors → All monitor states
GET /alarms/active → Active alarms
GET /health → Bridge health
WS /ws/events → Real-time events
Configuration Files
| File | Purpose | Legacy Equivalent |
|---|---|---|
servers.json |
Server connections, bridge URLs, ID ranges | CameraServersConfiguration |
keyboards.json |
Keyboard identity, PRIMARY/STANDBY role | CopilotAppConfiguration |
wall-config.json |
Monitor wall layout (sections, monitors, viewers) | MonitorWallConfiguration |
function-buttons.json |
F1-F7 actions per wall | FunctionButtonsConfiguration |
prepositions.json |
Camera preset names | PrepositionsConfiguration |
crossswitch-rules.json |
Routing rules for CrossSwitch | New (was in AppServer logic) |
Risk Mitigation
| Risk | Mitigation |
|---|---|
| Joystick latency via HTTP bridge | Measure. Legacy: ~1ms (in-process). New: ~5-20ms (HTTP). Acceptable for PTZ. |
| Lock coordination without AppServer | PRIMARY WebSocket hub. Degraded mode = local-only locks. |
| Sequence failover mid-execution | PRIMARY tracks sequence state. STANDBY can resume from last known position. |
| Config drift between keyboards | PRIMARY broadcasts config changes. Startup sync on connect. |
| Browser HID/Serial support | Use Web HID API + Web Serial API. Desktop fallback via flutter_libserialport. |