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>
221 lines
10 KiB
Markdown
221 lines
10 KiB
Markdown
# 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<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
|
|
|
|
```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`. |
|