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