# 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 ```mermaid graph TB subgraph "LEGACY Architecture" direction TB L_HW["Hardware Keyboard"] --> L_App["Copilot.App.exe
(WPF .NET 7)"] L_App -->|"Native SDK DLL
(in-process)"| L_Cam["Camera Servers"] L_App -->|"SignalR
(HTTPS/WSS)"| L_AS["AppServer
(separate machine)"] L_AS --> L_DB["SQLite DB"] end subgraph "NEW Architecture" direction TB N_HW["Hardware Keyboard"] --> N_App["Flutter App
(Web/Desktop)"] N_App -->|"REST HTTP
(localhost bridges)"| N_Bridge["C# Bridges
(.NET 8)"] N_Bridge -->|"Native SDK"| N_Cam["Camera Servers"] N_App -->|"WebSocket
(:8090)"| N_Primary["PRIMARY Keyboard
(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 ```mermaid 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: 1. User selects a PTZ camera → triggers lock attempt 2. If lock acquired → PTZ telemetry enabled 3. If locked by another keyboard → dialog: "Request takeover?" 4. Lock expires after 5 minutes unless reset by PTZ movement 5. 1-minute warning before expiration 6. 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`. |