Files
mirror-os-flutter/apps/display/lib/services/ws_server.dart
T
2026-07-11 21:51:34 -05:00

124 lines
3.7 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:shelf_web_socket/shelf_web_socket.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:mirror_protocol/mirror_protocol.dart';
import '../providers/display_state_provider.dart';
import '../providers/layout_provider.dart';
import 'layout_persistence.dart';
final wsServerProvider = Provider<WsServer>((ref) => WsServer(ref));
class WsServer {
final Ref _ref;
final Set<WebSocketChannel> _clients = {};
HttpServer? _server;
final int port;
final LayoutPersistence _layoutPersistence = LayoutPersistence();
DashboardLayoutConfig? _currentLayout;
WsServer(this._ref, {this.port = 9090});
Future<void> start() async {
_currentLayout = await _layoutPersistence.load();
final handler = webSocketHandler((WebSocketChannel channel) {
_clients.add(channel);
// Send current state on connect
final state = _ref.read(displayStateProvider);
final sync = StateSync(state: state);
channel.sink.add(sync.encode());
channel.stream.listen(
(data) => _handleMessage(channel, data as String),
onDone: () => _clients.remove(channel),
onError: (_) => _clients.remove(channel),
);
});
_server = await shelf_io.serve(handler, InternetAddress.anyIPv4, port);
print('WebSocket server running on :$port');
}
void _handleMessage(WebSocketChannel sender, String raw) {
try {
final message = MirrorMessage.decode(raw);
_routeMessage(sender, message);
} catch (e) {
print('Invalid message: $e');
}
}
void _routeMessage(WebSocketChannel sender, MirrorMessage message) {
final notifier = _ref.read(displayStateProvider.notifier);
switch (message) {
case NavigateCommand(:final target):
notifier.navigate(target);
case PlaybackCommand(:final action, :final value):
notifier.handlePlayback(action, value);
case WorkoutCommand(:final action, :final payload):
notifier.handleWorkout(action, payload);
case ProgramCommand(:final action, :final payload):
notifier.handleProgram(action, payload);
case MediaCommand(:final action, :final payload):
notifier.handleMedia(action, payload);
case RegisterMessage():
// Already handled on connect
break;
case DashboardConfigRequest():
_handleConfigRequest(sender);
case DashboardConfigUpdate(:final layout):
_handleConfigUpdate(layout);
default:
break;
}
}
void _handleConfigRequest(WebSocketChannel sender) {
final layout = _currentLayout ?? DashboardLayoutConfig.defaultLayout();
final response = DashboardConfigResponse(layout: layout);
try {
sender.sink.add(response.encode());
} catch (_) {
_clients.remove(sender);
}
}
void _handleConfigUpdate(DashboardLayoutConfig layout) {
_currentLayout = layout;
_layoutPersistence.save(layout);
// Update the layout provider so dashboard screen rebuilds
_ref.read(layoutProvider.notifier).updateLayout(layout);
}
void broadcast(MirrorMessage message) {
final encoded = message.encode();
for (final client in _clients.toList()) {
try {
client.sink.add(encoded);
} catch (_) {
_clients.remove(client);
}
}
}
void broadcastState(DisplayState state) {
broadcast(StateSync(state: state));
}
DashboardLayoutConfig get currentLayout =>
_currentLayout ?? DashboardLayoutConfig.defaultLayout();
Future<void> stop() async {
for (final client in _clients) {
await client.sink.close();
}
_clients.clear();
await _server?.close();
}
}