Initial commit: COPILOT D6 Flutter keyboard controller
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>
This commit is contained in:
189
copilot_keyboard/lib/presentation/blocs/ptz/ptz_bloc.dart
Normal file
189
copilot_keyboard/lib/presentation/blocs/ptz/ptz_bloc.dart
Normal file
@@ -0,0 +1,189 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../data/services/bridge_service.dart';
|
||||
import '../../../data/services/coordination_service.dart';
|
||||
import 'ptz_event.dart';
|
||||
import 'ptz_state.dart';
|
||||
|
||||
class PtzBloc extends Bloc<PtzEvent, PtzState> {
|
||||
final BridgeService _bridgeService;
|
||||
final CoordinationService _coordinationService;
|
||||
|
||||
Timer? _lockResetTimer;
|
||||
|
||||
PtzBloc({
|
||||
required BridgeService bridgeService,
|
||||
required CoordinationService coordinationService,
|
||||
}) : _bridgeService = bridgeService,
|
||||
_coordinationService = coordinationService,
|
||||
super(const PtzState()) {
|
||||
on<PtzPanStart>(_onPanStart);
|
||||
on<PtzTiltStart>(_onTiltStart);
|
||||
on<PtzZoomStart>(_onZoomStart);
|
||||
on<PtzStop>(_onStop);
|
||||
on<PtzGoToPreset>(_onGoToPreset);
|
||||
on<PtzSetCamera>(_onSetCamera);
|
||||
}
|
||||
|
||||
/// Ensure we have a lock on the camera before PTZ movement.
|
||||
/// Returns true if lock was acquired or already held.
|
||||
Future<bool> _ensureLock(int cameraId, Emitter<PtzState> emit) async {
|
||||
// Already locked by us
|
||||
if (_coordinationService.isCameraLockedByMe(cameraId)) {
|
||||
emit(state.copyWith(lockStatus: PtzLockStatus.locked));
|
||||
return true;
|
||||
}
|
||||
|
||||
emit(state.copyWith(lockStatus: PtzLockStatus.acquiring));
|
||||
|
||||
final result = await _coordinationService.tryLock(cameraId);
|
||||
if (result.acquired) {
|
||||
emit(state.copyWith(lockStatus: PtzLockStatus.locked));
|
||||
_startLockResetTimer(cameraId);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Lock denied — someone else has it
|
||||
emit(state.copyWith(
|
||||
lockStatus: PtzLockStatus.denied,
|
||||
lockedBy: result.lock?.ownerName,
|
||||
error: 'Camera locked by ${result.lock?.ownerName ?? "another keyboard"}',
|
||||
));
|
||||
return false;
|
||||
}
|
||||
|
||||
void _startLockResetTimer(int cameraId) {
|
||||
_lockResetTimer?.cancel();
|
||||
_lockResetTimer = Timer.periodic(const Duration(minutes: 2), (_) {
|
||||
if (_coordinationService.isCameraLockedByMe(cameraId)) {
|
||||
_coordinationService.resetExpiration(cameraId);
|
||||
} else {
|
||||
_lockResetTimer?.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _onPanStart(
|
||||
PtzPanStart event,
|
||||
Emitter<PtzState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(activeCameraId: event.cameraId));
|
||||
|
||||
if (!await _ensureLock(event.cameraId, emit)) return;
|
||||
|
||||
final direction =
|
||||
event.direction == 'left' ? PtzDirection.left : PtzDirection.right;
|
||||
emit(state.copyWith(
|
||||
currentDirection: direction,
|
||||
isMoving: true,
|
||||
error: null,
|
||||
));
|
||||
|
||||
try {
|
||||
await _bridgeService.ptzPan(event.cameraId, event.direction, event.speed);
|
||||
} catch (e) {
|
||||
emit(state.copyWith(error: e.toString(), isMoving: false));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onTiltStart(
|
||||
PtzTiltStart event,
|
||||
Emitter<PtzState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(activeCameraId: event.cameraId));
|
||||
|
||||
if (!await _ensureLock(event.cameraId, emit)) return;
|
||||
|
||||
final direction =
|
||||
event.direction == 'up' ? PtzDirection.up : PtzDirection.down;
|
||||
emit(state.copyWith(
|
||||
currentDirection: direction,
|
||||
isMoving: true,
|
||||
error: null,
|
||||
));
|
||||
|
||||
try {
|
||||
await _bridgeService.ptzTilt(
|
||||
event.cameraId, event.direction, event.speed);
|
||||
} catch (e) {
|
||||
emit(state.copyWith(error: e.toString(), isMoving: false));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onZoomStart(
|
||||
PtzZoomStart event,
|
||||
Emitter<PtzState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(activeCameraId: event.cameraId));
|
||||
|
||||
if (!await _ensureLock(event.cameraId, emit)) return;
|
||||
|
||||
final direction =
|
||||
event.direction == 'in' ? PtzDirection.zoomIn : PtzDirection.zoomOut;
|
||||
emit(state.copyWith(
|
||||
currentDirection: direction,
|
||||
isMoving: true,
|
||||
error: null,
|
||||
));
|
||||
|
||||
try {
|
||||
await _bridgeService.ptzZoom(
|
||||
event.cameraId, event.direction, event.speed);
|
||||
} catch (e) {
|
||||
emit(state.copyWith(error: e.toString(), isMoving: false));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onStop(
|
||||
PtzStop event,
|
||||
Emitter<PtzState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(
|
||||
currentDirection: PtzDirection.none,
|
||||
isMoving: false,
|
||||
error: null,
|
||||
));
|
||||
|
||||
try {
|
||||
await _bridgeService.ptzStop(event.cameraId);
|
||||
} catch (e) {
|
||||
emit(state.copyWith(error: e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onGoToPreset(
|
||||
PtzGoToPreset event,
|
||||
Emitter<PtzState> emit,
|
||||
) async {
|
||||
if (!await _ensureLock(event.cameraId, emit)) return;
|
||||
|
||||
emit(state.copyWith(error: null));
|
||||
|
||||
try {
|
||||
await _bridgeService.ptzPreset(event.cameraId, event.preset);
|
||||
} catch (e) {
|
||||
emit(state.copyWith(error: e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
void _onSetCamera(
|
||||
PtzSetCamera event,
|
||||
Emitter<PtzState> emit,
|
||||
) {
|
||||
if (event.cameraId == null) {
|
||||
_lockResetTimer?.cancel();
|
||||
emit(state.copyWith(
|
||||
clearCamera: true, lockStatus: PtzLockStatus.none));
|
||||
} else {
|
||||
emit(state.copyWith(activeCameraId: event.cameraId));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_lockResetTimer?.cancel();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user