# Feature Implementation Guide

> Step-by-step guide for implementing each missing feature in the Flutter app. Each section references the legacy source code and provides the Flutter implementation approach.

## Implementation Priority Order

```mermaid
gantt
    title Migration Implementation Timeline
    dateFormat YYYY-MM-DD
    axisFormat %b %d

    section Phase 2 - Critical
    Camera Lock System          :p2a, 2026-02-15, 10d
    PRIMARY/STANDBY Hub         :p2b, 2026-02-15, 14d
    Hardware Keyboard (Serial)  :p2c, after p2a, 7d
    Hardware Joystick (HID)     :p2d, after p2a, 7d

    section Phase 3 - High
    Function Buttons F1-F7      :p3a, after p2c, 3d
    Prepositions                :p3b, after p2c, 5d
    Camera Edit Timeout         :p3c, after p2c, 2d
    CrossSwitch Rules Engine    :p3d, after p2c, 5d

    section Phase 4 - Medium
    Playback Mode               :p4a, after p3b, 7d
    Sequences                   :p4b, after p3b, 10d
    Alarm History               :p4c, after p3b, 3d
    Service Menu                :p4d, after p3b, 3d
```

---

## 1. Camera Lock System

**Legacy:** `CameraLockService.cs` + `SegmentViewModel.cs:631-676`
**Complexity:** High | **Priority:** Critical

### New Files to Create

```
lib/
├── data/services/
│   └── lock_service.dart          # Lock operations via PRIMARY WebSocket
├── domain/entities/
│   └── camera_lock.dart           # Lock state entity
└── presentation/blocs/lock/
    ├── lock_bloc.dart             # Lock state management
    ├── lock_event.dart
    └── lock_state.dart
```

### Entity: CameraLock

```dart
class CameraLock extends Equatable {
  final int cameraId;
  final String heldBy;        // keyboard name
  final CameraLockPriority priority;
  final DateTime acquiredAt;
  final DateTime expiresAt;

  bool get isExpiringSoon =>
    expiresAt.difference(DateTime.now()) < Duration(minutes: 1);

  @override
  List<Object?> get props => [cameraId, heldBy, priority, acquiredAt, expiresAt];
}

enum CameraLockPriority { low, high }
enum CameraLockResult { acquired, denied, takeoverRequested }
```

### Service: LockService

```dart
class LockService {
  final CoordinationService _coordination;

  // Lock operations (send to PRIMARY via WebSocket)
  Future<CameraLockResult> tryLock(int cameraId, CameraLockPriority priority);
  Future<void> unlock(int cameraId);
  Future<void> requestTakeover(int cameraId);
  Future<void> confirmTakeover(int cameraId, bool confirm);
  Future<void> resetExpiration(int cameraId);  // Called on every PTZ movement

  // State streams
  Stream<Map<int, CameraLock>> get lockStates;  // All locks
  Stream<LockNotification> get notifications;    // Lock events for this keyboard
}
```

### BLoC: LockBloc

Key events:
- `TryLockCamera(cameraId)` → attempt lock, show dialog if denied
- `UnlockCamera(cameraId)` → release lock
- `ResetLockExpiration(cameraId)` → on PTZ movement
- `TakeoverRequested(cameraId, requestedBy)` → show confirmation dialog
- `LockStateUpdated(locks)` → from PRIMARY broadcast

Key state:
- `lockedCameras: Set<int>` — cameras locked by this keyboard
- `allLocks: Map<int, CameraLock>` — all locks across keyboards
- `pendingTakeover: int?` — camera with pending takeover dialog
- `isTelemetryActive: bool` — true when any camera locked

### Integration with Existing BLoCs

The `WallBloc` needs to check lock state before PTZ. Modify `PtzBloc`:
```dart
// Before sending PTZ command:
if (!lockBloc.state.lockedCameras.contains(cameraId)) {
  // Camera not locked, try to lock first
  lockBloc.add(TryLockCamera(cameraId));
  return;
}
// Camera locked, proceed with PTZ
bridgeService.ptzPan(camera, direction, speed);
lockBloc.add(ResetLockExpiration(cameraId));  // Reset expiry timer
```

### Lock Expiry Timer (Client-Side)

```dart
// In LockBloc: start timer when lock acquired
Timer? _expiryWarningTimer;

void _onLockAcquired(int cameraId) {
  _expiryWarningTimer?.cancel();
  _expiryWarningTimer = Timer(Duration(minutes: 4), () {
    emit(state.copyWith(expiryWarning: cameraId));
  });
}
```

---

## 2. PRIMARY/STANDBY Coordination Hub

**Legacy:** Replaces `Copilot.AppServer.dll` + `Copilot.AppServer.Client.dll`
**Complexity:** High | **Priority:** Critical

### New Files to Create

```
lib/
├── data/services/
│   └── coordination_service.dart   # WebSocket client to PRIMARY
├── coordination/
│   ├── primary_hub.dart            # WebSocket server (when this keyboard is PRIMARY)
│   ├── election_service.dart       # PRIMARY election logic
│   ├── heartbeat_service.dart      # 2-second heartbeat
│   └── messages.dart               # Shared message types
└── presentation/blocs/coordination/
    ├── coordination_bloc.dart
    ├── coordination_event.dart
    └── coordination_state.dart
```

### Message Protocol

```dart
abstract class CoordinationMessage {
  String get type;
  Map<String, dynamic> toJson();
}

// Heartbeat
class HeartbeatMessage extends CoordinationMessage {
  final String keyboardId;
  final DateTime timestamp;
  final bool isPrimary;
}

// Lock messages
class LockRequestMessage extends CoordinationMessage { ... }
class LockResponseMessage extends CoordinationMessage { ... }
class LockBroadcastMessage extends CoordinationMessage { ... }

// Sequence messages
class SequenceStartMessage extends CoordinationMessage { ... }
class SequenceStopMessage extends CoordinationMessage { ... }
class SequenceStateBroadcast extends CoordinationMessage { ... }

// State sync
class FullStateSyncMessage extends CoordinationMessage {
  final Map<int, CameraLock> locks;
  final Map<int, SequenceState> sequences;
}
```

### Election Protocol

```dart
class ElectionService {
  // 1. On startup, try to connect to configured PRIMARY
  // 2. If PRIMARY responds → become STANDBY
  // 3. If PRIMARY doesn't respond within 6s → self-promote to PRIMARY
  // 4. As PRIMARY: start WebSocket server on :8090
  // 5. As STANDBY: send heartbeat checks every 2s
  // 6. If 3 consecutive heartbeats missed (6s) → promote to PRIMARY

  Stream<KeyboardRole> get role;  // PRIMARY or STANDBY
}

enum KeyboardRole { primary, standby, standalone }
```

### Web Platform Consideration

For Flutter web, the WebSocket server (PRIMARY hub) cannot run in the browser. Options:
1. **Desktop build** on LattePanda → full support
2. **Web build** → can only be STANDBY (connects to PRIMARY via WebSocket client)
3. **Hybrid** → separate Dart CLI process for PRIMARY hub

---

## 3. Hardware Keyboard (Serial Port)

**Legacy:** `CopilotDevice.cs`, `SerialPortDataProvider.cs`
**Complexity:** Medium | **Priority:** Critical

### New Files

```
lib/
├── data/services/
│   └── keyboard_service.dart       # Serial port + HID abstraction
├── domain/entities/
│   └── virtual_key.dart            # Key enum (matches legacy VirtualKey)
```

### Serial Protocol

```
Baud rate: 115200
Data bits: 8, Stop bits: 1, Parity: None

Messages (ASCII, line-terminated):
  p<keycode>    → Key pressed  (e.g., "p48" = digit 0)
  r<keycode>    → Key released (e.g., "r48" = digit 0)
  j<value>      → Jog wheel position (-1 or +1 per detent)
  s<value>      → Shuttle wheel position (-7 to +7)
  v<version>    → Firmware version response
  h             → Heartbeat from Arduino
```

### Web Serial API Implementation

```dart
// For Flutter Web:
class WebSerialKeyboardService implements KeyboardService {
  // Uses dart:js_interop to call Web Serial API
  // navigator.serial.requestPort()
  // port.open({ baudRate: 115200 })
  // port.readable.getReader() → parse messages
}

// For Flutter Desktop:
class NativeSerialKeyboardService implements KeyboardService {
  // Uses flutter_libserialport package
  // SerialPort(portName).open(read: true)
  // Read stream → parse messages
}
```

### Key Enum (VirtualKey)

Map Arduino keycodes to VirtualKey enum (matches legacy):
```dart
enum VirtualKey {
  digit0, digit1, digit2, digit3, digit4,
  digit5, digit6, digit7, digit8, digit9,
  enter, backspace, escape, prefix,
  plus, minus, home,
  f1, f2, f3, f4, f5, f6, f7,
  lock, search, prepositions, playback,
  sequence, alarmHistory, fullScreen,
  joyButton1, joyButton2,
}
```

---

## 4. Hardware Joystick (HID)

**Legacy:** `JoystickHidDataProvider.cs`
**Complexity:** Medium | **Priority:** Critical

### HID Device Identification
```
VendorId:  0x2ACF (10959)
ProductId: 0x0101 (257)
```

### Web HID API Implementation

```dart
class WebHidJoystickService implements JoystickService {
  // navigator.hid.requestDevice([{vendorId: 0x2ACF, productId: 0x0101}])
  // device.open()
  // device.addEventListener('inputreport', callback)
  // Parse input report → extract X, Y, Z axes → scale to -255..+255
}
```

### Input Report Parsing

The Arduino Leonardo sends HID reports with 3 axes:
```
Report format (depends on HID descriptor):
  Byte 0-1: X axis (16-bit signed) → Pan
  Byte 2-3: Y axis (16-bit signed) → Tilt
  Byte 4-5: Z axis (16-bit signed) → Zoom
  Byte 6:   Buttons (bitmask)
```

Scale raw values to -255..+255 range (same as legacy `GetScaledValue`).

### Deduplication

```dart
// Only emit if value changed (matches legacy DoPtzAction logic)
int _lastX = 0, _lastY = 0, _lastZ = 0;

void _onHidReport(int x, int y, int z) {
  if (x != _lastX) { _lastX = x; _ptzBloc.add(PtzPanStart(x)); }
  if (y != _lastY) { _lastY = y; _ptzBloc.add(PtzTiltStart(y)); }
  if (z != _lastZ) { _lastZ = z; _ptzBloc.add(PtzZoomStart(z)); }
}
```

---

## 5. Function Buttons (F1-F7)

**Legacy:** `FunctionButtonsService.cs` (78 lines)
**Complexity:** Low | **Priority:** High

### New Files

```
lib/
├── config/
│   └── function_buttons_config.dart   # Load function-buttons.json
├── data/services/
│   └── function_button_service.dart   # Execute button actions
```

### Implementation

```dart
class FunctionButtonService {
  final BridgeService _bridge;
  final Map<String, Map<String, List<FunctionButtonAction>>> _config;

  Future<bool> execute(String wallId, String buttonKey) async {
    final actions = _config[wallId]?[buttonKey];
    if (actions == null || actions.isEmpty) return false;

    for (final action in actions) {
      switch (action.type) {
        case ActionType.crossSwitch:
          await _bridge.viewerConnectLive(action.viewerId, action.sourceId);
        case ActionType.sequenceStart:
          await _sequenceService.start(action.viewerId, action.sourceId);
      }
    }
    return true;
  }
}
```

### Integration with WallBloc

Add to keyboard handler in `MainScreen`:
```dart
case VirtualKey.f1: functionButtonService.execute(wallId, 'F1');
case VirtualKey.f2: functionButtonService.execute(wallId, 'F2');
// ... through F7
```

---

## 6. Prepositions (Camera Presets)

**Legacy:** `PrepositionService.cs` (114 lines)
**Complexity:** Medium | **Priority:** High

### New Files

```
lib/
├── config/
│   └── prepositions_config.dart
├── data/services/
│   └── preposition_service.dart
└── presentation/
    ├── blocs/preposition/
    │   ├── preposition_bloc.dart
    │   ├── preposition_event.dart
    │   └── preposition_state.dart
    └── screens/
        ├── prepositions_screen.dart
        └── preposition_add_screen.dart
```

### Service Implementation

```dart
class PrepositionService {
  final BridgeService _bridge;
  final PrepositionsConfig _config;

  Map<int, String> getNames(int cameraId) =>
    _config.getPrepositionNames(cameraId);

  Future<void> callUp(int cameraId, int presetId) =>
    _bridge.ptzPreset(cameraId, presetId);

  Future<void> save(int cameraId, int presetId, String name) async {
    _config.setPrepositionName(cameraId, presetId, name);
    await _config.persist();
    // If PRIMARY available: broadcast to other keyboards
  }

  Future<void> delete(int cameraId, int presetId) async {
    _config.removePreposition(cameraId, presetId);
    await _config.persist();
  }
}
```

### UI Flow

```mermaid
stateDiagram-v2
    [*] --> PrepositionList: User presses Prepositions key
    PrepositionList --> CallUp: Select preset
    CallUp --> PrepositionList: Camera moves to position
    PrepositionList --> AddPreposition: Press Add button
    AddPreposition --> EnterName: Move camera to position
    EnterName --> PrepositionList: Save with name
    PrepositionList --> [*]: Press Back
```

---

## 7. Camera Number Edit Timeout

**Legacy:** `SegmentViewModel.cs:829-847`
**Complexity:** Low | **Priority:** High

### Add to WallBloc

```dart
// In WallBloc
Timer? _editTimeoutTimer;
static const editTimeout = Duration(seconds: 5); // from config

void _onAddCameraDigit(AddCameraDigit event, Emitter<WallState> emit) {
  _editTimeoutTimer?.cancel();
  _editTimeoutTimer = Timer(editTimeout, () {
    add(ClearCameraInput());  // Auto-cancel on timeout
  });
  // ... existing digit logic
}

void _onExecuteCrossSwitch(ExecuteCrossSwitch event, Emitter<WallState> emit) {
  _editTimeoutTimer?.cancel();
  // ... existing CrossSwitch logic
}
```

---

## 8. Playback Mode

**Legacy:** `PlaybackStateService.cs`, viewer controller
**Complexity:** Medium | **Priority:** Medium

### New Files

```
lib/
├── data/services/
│   └── playback_service.dart
├── presentation/
│   ├── blocs/playback/
│   │   ├── playback_bloc.dart
│   │   ├── playback_event.dart
│   │   └── playback_state.dart
│   └── screens/
│       └── playback_screen.dart
```

### Shuttle Speed Mapping

```dart
const shuttleSpeedMap = {
  -7: -32.0,  -6: -16.0,  -5: -8.0,  -4: -4.0,
  -3: -2.0,   -2: -1.0,   -1: -0.5,   0: 0.0,
   1:  0.5,    2:  1.0,    3:  2.0,    4:  4.0,
   5:  8.0,    6: 16.0,    7: 32.0,
};
```

### Bridge Endpoints

```dart
class PlaybackService {
  Future<void> setPlayMode(int viewer, PlayMode mode, double speed) =>
    _bridge.post('/viewer/set-play-mode', {
      'Viewer': viewer, 'PlayMode': mode.name, 'PlaySpeed': speed
    });

  Future<void> playFromTime(int viewer, int channel, PlayMode mode, DateTime time) =>
    _bridge.post('/viewer/play-from-time', {
      'Viewer': viewer, 'Channel': channel,
      'PlayMode': mode.name, 'Time': time.toIso8601String()
    });

  Future<void> jumpByTime(int viewer, int channel, PlayMode mode, int seconds) =>
    _bridge.post('/viewer/jump-by-time', {
      'Viewer': viewer, 'Channel': channel,
      'PlayMode': mode.name, 'TimeInSec': seconds
    });
}
```

---

## 9. Sequence Execution

**Legacy:** `SequenceService.cs` (77 lines)
**Complexity:** High | **Priority:** Medium

### Architecture Decision

Sequences run on the PRIMARY keyboard (replaces AppServer):

```
Flutter App (any keyboard)
  → WebSocket message to PRIMARY
    → PRIMARY runs sequence timer
      → PRIMARY sends CrossSwitch commands to bridge
      → PRIMARY broadcasts state to all keyboards
```

### New Files

```
lib/
├── data/services/
│   └── sequence_service.dart          # Client (sends to PRIMARY)
├── coordination/
│   └── sequence_runner.dart           # Server (runs on PRIMARY)
├── presentation/
│   ├── blocs/sequence/
│   │   ├── sequence_bloc.dart
│   │   ├── sequence_event.dart
│   │   └── sequence_state.dart
│   └── screens/
│       ├── sequence_categories_screen.dart
│       └── sequences_screen.dart
```

### Sequence Runner (PRIMARY only)

```dart
class SequenceRunner {
  final Map<int, Timer> _activeSequences = {};  // viewerId → timer

  void start(int viewerId, Sequence sequence) {
    stop(viewerId);  // Stop existing sequence on this viewer
    int stepIndex = 0;

    _activeSequences[viewerId] = Timer.periodic(
      Duration(seconds: sequence.dwellTime),
      (_) {
        final camera = sequence.cameras[stepIndex % sequence.cameras.length];
        bridgeService.viewerConnectLive(viewerId, camera);
        stepIndex++;
        _broadcastState();
      },
    );
  }

  void stop(int viewerId) {
    _activeSequences[viewerId]?.cancel();
    _activeSequences.remove(viewerId);
    _broadcastState();
  }
}
```

---

## 10. Service Menu

**Legacy:** `MainWindow.cs` — Backspace held 3 seconds
**Complexity:** Low | **Priority:** Low

### Implementation

```dart
// In MainScreen or KeyboardService
Timer? _serviceKeyTimer;

void _onKeyDown(VirtualKey key) {
  if (key == VirtualKey.backspace) {
    _serviceKeyTimer = Timer(Duration(seconds: 3), () {
      GoRouter.of(context).push('/service-menu');
    });
  }
}

void _onKeyUp(VirtualKey key) {
  if (key == VirtualKey.backspace) {
    _serviceKeyTimer?.cancel();
  }
}
```

### Service Menu Options
- Restart application
- Restart hardware (LattePanda reboot)
- Shutdown hardware
- Show firmware version
- Show app version
- Network diagnostics

---

## Testing Strategy

### Unit Tests (per feature)

| Feature | Test Focus |
|---------|-----------|
| Lock system | Lock/unlock/takeover/expiry logic |
| Coordination | Election, heartbeat, failover |
| Keyboard input | Key mapping, edit timeout, service hotkey |
| Function buttons | Config loading, action execution |
| Prepositions | CRUD operations, name mapping |
| Playback | Shuttle speed mapping, mode transitions |
| Sequences | Start/stop/timer/failover |

### Integration Tests

| Test | Description |
|------|------------|
| CrossSwitch + Lock | Lock camera → PTZ → CrossSwitch → auto-unlock |
| Failover | PRIMARY stops → STANDBY promotes → locks preserved |
| Alarm blocking | Active alarm → CrossSwitch blocked → alarm resolved → CrossSwitch works |
| Sequence + CrossSwitch | Running sequence → CrossSwitch → sequence stopped |
| Playback + PTZ | Enter playback → switch to PTZ → playback stopped |

### Hardware Tests (Manual)

| Test | Verify |
|------|--------|
| Serial keyboard | All keys mapped correctly, no missed events |
| HID joystick | Axes correct direction, full range, smooth PTZ |
| Shuttle wheel | Speed mapping matches legacy behavior |
| Jog wheel | Single-step forward/backward |
| Long-press Backspace | Service menu opens after 3 seconds |

---

## File Summary: What to Create

| File | Type | Lines (est.) | Phase |
|------|------|-------------|-------|
| `lock_service.dart` | Service | 120 | 2A |
| `camera_lock.dart` | Entity | 40 | 2A |
| `lock_bloc.dart` + events/state | BLoC | 200 | 2A |
| `coordination_service.dart` | Service | 150 | 2B |
| `primary_hub.dart` | Server | 200 | 2B |
| `election_service.dart` | Service | 100 | 2B |
| `heartbeat_service.dart` | Service | 60 | 2B |
| `messages.dart` | Models | 120 | 2B |
| `coordination_bloc.dart` + events/state | BLoC | 150 | 2B |
| `keyboard_service.dart` | Service | 150 | 3 |
| `virtual_key.dart` | Enum | 50 | 3 |
| `joystick_service.dart` | Service | 100 | 3 |
| `function_button_service.dart` | Service | 60 | 3 |
| `function_buttons_config.dart` | Config | 40 | 3 |
| `preposition_service.dart` | Service | 80 | 3 |
| `prepositions_config.dart` | Config | 40 | 3 |
| `preposition_bloc.dart` + events/state | BLoC | 120 | 3 |
| `prepositions_screen.dart` | Screen | 100 | 3 |
| `playback_service.dart` | Service | 80 | 4 |
| `playback_bloc.dart` + events/state | BLoC | 120 | 4 |
| `playback_screen.dart` | Screen | 150 | 4 |
| `sequence_service.dart` | Service | 60 | 4 |
| `sequence_runner.dart` | Server | 100 | 4 |
| `sequence_bloc.dart` + events/state | BLoC | 120 | 4 |
| `sequence_screens.dart` | Screens | 150 | 4 |
| **Total** | | **~2,560** | |
