desktop push

This commit is contained in:
ImBenji
2024-05-20 09:06:38 +01:00
parent e5a8d78bf1
commit 639faddfc8
19 changed files with 2455 additions and 996 deletions

View File

@@ -10,6 +10,7 @@ import 'package:bus_infotainment/auth/api_constants.dart';
import 'package:bus_infotainment/auth/auth_api.dart';
import 'package:bus_infotainment/backend/modules/announcement.dart';
import 'package:bus_infotainment/backend/modules/commands.dart';
import 'package:bus_infotainment/backend/modules/networking.dart';
import 'package:bus_infotainment/backend/modules/synced_time.dart';
import 'package:bus_infotainment/backend/modules/tracker.dart';
import 'package:bus_infotainment/backend/modules/tube_info.dart';
@@ -75,6 +76,8 @@ class LiveInformation {
if (!auth.isAuthenticated()) {
auth.loadAnonymousUser();
}
networkingModule = NetworkingModule();
}
Future<void> initTrackerModule() async {
@@ -90,9 +93,12 @@ class LiveInformation {
String? roomCode;
String? roomDocumentID;
bool isHost = false;
bool inRoom = false;
appwrite.RealtimeSubscription? _subscription;
RealtimeKeepAliveConnection? _keepAliveConnection; // This is a workaround for a bug in the appwrite SDK
// Local room stuff
ListenerReceipt<String>? _listenerReciept;
// Modules
// late CommandModule commandModule; This needs to be deprecated
@@ -101,6 +107,7 @@ class LiveInformation {
late SyncedTimeModule syncedTimeModule;
late TrackerModule trackerModule;
late TubeStations tubeStations;
late NetworkingModule networkingModule;
// Important variables
BusRouteVariant? _currentRouteVariant;
@@ -114,7 +121,7 @@ class LiveInformation {
Future<void> setRouteVariant(BusRouteVariant? routeVariant) async {
Future<void> setRouteVariant(BusRouteVariant? routeVariant, {bool sendToServer = false}) async {
if (routeVariant == null) {
_currentRouteVariant = null;
@@ -160,6 +167,11 @@ class LiveInformation {
print("Failed to update route on server");
}
}
if (inRoom && sendToServer) {
SendCommand("setroute ${routeVariant.busRoute.routeNumber} ${routeVariant.busRoute.routeVariants.values.toList().indexOf(routeVariant)}");
}
Continuation:
@@ -200,12 +212,13 @@ class LiveInformation {
return _currentRouteVariant;
}
Future<void> setRouteVariantQuery(String routeNumber, int routeVariantIndex) async {
Future<void> setRouteVariantQuery(String routeNumber, int routeVariantIndex, {bool sendToServer = false}) async {
BusRoute route = busSequences.routes[routeNumber]!;
BusRouteVariant routeVariant = route.routeVariants.values.toList()[routeVariantIndex];
await setRouteVariant(
routeVariant
routeVariant,
sendToServer: sendToServer
);
}
@@ -213,174 +226,225 @@ class LiveInformation {
// Multi device support
Future<void> createRoom(String roomCode) async {
print("Creating room with code $roomCode");
// Update the room code
this.roomCode = roomCode;
// Enable host mode
isHost = true;
// Access the database
final client = auth.client;
final databases = appwrite.Databases(client);
// Remove any existing documents
final existingDocuments = await databases.listDocuments(
databaseId: "6633e85400036415ab0f",
collectionId: "6633e85d0020f52f3771",
queries: [
appwrite.Query.search("SessionID", roomCode)
]
);
for (var document in existingDocuments.documents) {
await databases.deleteDocument(
databaseId: "6633e85400036415ab0f",
collectionId: "6633e85d0020f52f3771",
documentId: document.$id
{
// Local Room
await networkingModule.startWebSocketServer();
inRoom = true;
_listenerReciept = networkingModule.onMessageReceived?.addListener(
(p0) {
print("Received local command: $p0");
ExecuteCommand(p0);
}
);
}
// Create the document
final document = await databases.createDocument(
databaseId: "6633e85400036415ab0f",
collectionId: "6633e85d0020f52f3771",
documentId: appwrite.ID.unique(),
data: {
"SessionID": roomCode,
"LastUpdater": auth.userID,
}
);
{
// Cloud Room
print("Creating room with code $roomCode");
// Listen for changes
// { Breaks due to bug in appwrite
// final realtime = appwrite.Realtime(client);
//
// if (_subscription != null) {
// _subscription!.close();
// }
//
// _subscription = realtime.subscribe(
// ['databases.6633e85400036415ab0f.collections.6633e85d0020f52f3771.documents.${document.$id}']
// );
// _subscription!.stream.listen(ServerListener);
// }
// Listen for changes
if (_keepAliveConnection != null) {
try {
_keepAliveConnection!.close();
} catch (e) {
print("Failed to close connection... oh well");
// Update the room code
this.roomCode = roomCode;
// Enable host mode
isHost = true;
inRoom = true;
// Access the database
final client = auth.client;
final databases = appwrite.Databases(client);
// Remove any existing documents
final existingDocuments = await databases.listDocuments(
databaseId: "6633e85400036415ab0f",
collectionId: "6633e85d0020f52f3771",
queries: [
appwrite.Query.search("SessionID", roomCode)
]
);
for (var document in existingDocuments.documents) {
await databases.deleteDocument(
databaseId: "6633e85400036415ab0f",
collectionId: "6633e85d0020f52f3771",
documentId: document.$id
);
}
// Create the document
final document = await databases.createDocument(
databaseId: "6633e85400036415ab0f",
collectionId: "6633e85d0020f52f3771",
documentId: appwrite.ID.unique(),
data: {
"SessionID": roomCode,
"LastUpdater": auth.userID,
}
);
// Listen for changes
if (_keepAliveConnection != null) {
try {
_keepAliveConnection!.close();
} catch (e) {
print("Failed to close connection... oh well");
}
}
String APPWRITE_ENDPOINT_URL = "https://cloud.appwrite.io/v1";
String domain = APPWRITE_ENDPOINT_URL.replaceAll("https://", "").trim();
_keepAliveConnection = RealtimeKeepAliveConnection(
channels: ['databases.6633e85400036415ab0f.collections.6633e85d0020f52f3771.documents.${document.$id}'],
onData: ServerListener,
domain: domain,
client: auth.client,
onError: (e) {
print("Workarround Error: $e");
},
);
_keepAliveConnection!.initialize();
// Update the room document ID
roomDocumentID = document.$id;
print("Created room with code $roomCode");
}
String APPWRITE_ENDPOINT_URL = "https://cloud.appwrite.io/v1";
String domain = APPWRITE_ENDPOINT_URL.replaceAll("https://", "").trim();
_keepAliveConnection = RealtimeKeepAliveConnection(
channels: ['databases.6633e85400036415ab0f.collections.6633e85d0020f52f3771.documents.${document.$id}'],
onData: ServerListener,
domain: domain,
client: auth.client,
onError: (e) {
print("Workarround Error: $e");
},
);
_keepAliveConnection!.initialize();
// Update the room document ID
roomDocumentID = document.$id;
print("Created room with code $roomCode");
}
Future<void> joinRoom(String roomCode) async {
print("Joining room with code $roomCode");
Future<void> joinRoom(String infoJson) async {
// Disable host mode
isHost = false;
try {
{
// sync
String routeNumber = jsonDecode(infoJson)["sync"]["route"];
int routeVariantIndex = jsonDecode(infoJson)["sync"]["routeVariant"];
// Update the room code
this.roomCode = roomCode;
setRouteVariantQuery(routeNumber, routeVariantIndex);
// Access the database
final client = auth.client;
final databases = appwrite.Databases(client);
// Get the document
final response = await databases.listDocuments(
databaseId: "6633e85400036415ab0f",
collectionId: "6633e85d0020f52f3771",
queries: [
appwrite.Query.search("SessionID", roomCode)
]
);
if (response.documents.isEmpty) {
throw Exception("Room not found");
LiveInformation().announcementModule.queueAnnouncementByRouteVariant(routeVariant: _currentRouteVariant!, sendToServer: false);
}
} catch (e) {
print("Failed to sync route");
}
final document = response.documents.first;
{
// Local Room
// Listen for changes
// {
// final realtime = appwrite.Realtime(client);
//
// if (_subscription != null) {
// _subscription!.close();
// }
//
// _subscription = realtime.subscribe([
// 'databases.6633e85400036415ab0f.collections.6633e85d0020f52f3771.documents.${document.$id}'
// ]);
//
// _subscription!.stream.listen(ServerListener);
// }
// Listen for changes
if (_keepAliveConnection != null) {
try {
_keepAliveConnection!.close();
} catch (e) {
print("Failed to close connection... oh well");
String host = jsonDecode(infoJson)["local"]["host"];
if (await networkingModule.connectToWebSocketServer(host)){
print("Connected to local room at $host");
_listenerReciept = networkingModule.onMessageReceived?.addListener(
(p0) {
print("Received local command: $p0");
ExecuteCommand(p0);
}
);
inRoom = true;
return; // We dont need to connect to the cloud room if we are connected to the local room.
} else {
print("Failed to connect to local room at $host");
print("Falling back to cloud room");
}
}
String APPWRITE_ENDPOINT_URL = "https://cloud.appwrite.io/v1";
String domain = APPWRITE_ENDPOINT_URL.replaceAll("https://", "").trim();
_keepAliveConnection = RealtimeKeepAliveConnection(
channels: ['databases.6633e85400036415ab0f.collections.6633e85d0020f52f3771.documents.${document.$id}'],
onData: ServerListener,
domain: domain,
client: auth.client,
onError: (e) {
print("Workarround Error: $e");
},
);
_keepAliveConnection!.initialize();
{
// Cloud Room
// Update the room document ID
roomDocumentID = document.$id;
String roomCode = jsonDecode(infoJson)["cloud"]["roomCode"];
// Get the current route
try {
String routeNumber = document.data["CurrentRoute"];
int routeVariantIndex = document.data["CurrentRouteVariant"];
print("Joining cloud room with code $roomCode");
await setRouteVariantQuery(routeNumber, routeVariantIndex);
print("Set route to $routeNumber $routeVariantIndex");
} catch (e) {
print("Failed to set route");
// Disable host mode
isHost = false;
// Update the room code
this.roomCode = roomCode;
// Access the database
final client = auth.client;
final databases = appwrite.Databases(client);
// Get the document
final response = await databases.listDocuments(
databaseId: "6633e85400036415ab0f",
collectionId: "6633e85d0020f52f3771",
queries: [
appwrite.Query.search("SessionID", roomCode)
]
);
if (response.documents.isEmpty) {
throw Exception("Room not found");
}
final document = response.documents.first;
// Listen for changes
if (_keepAliveConnection != null) {
try {
_keepAliveConnection!.close();
} catch (e) {
print("Failed to close connection... oh well");
}
}
String APPWRITE_ENDPOINT_URL = "https://cloud.appwrite.io/v1";
String domain = APPWRITE_ENDPOINT_URL.replaceAll("https://", "").trim();
_keepAliveConnection = RealtimeKeepAliveConnection(
channels: ['databases.6633e85400036415ab0f.collections.6633e85d0020f52f3771.documents.${document.$id}'],
onData: ServerListener,
domain: domain,
client: auth.client,
onError: (e) {
print("Workarround Error: $e");
},
);
_keepAliveConnection!.initialize();
// Update the room document ID
roomDocumentID = document.$id;
// Get the current route
try {
String routeNumber = document.data["CurrentRoute"];
int routeVariantIndex = document.data["CurrentRouteVariant"];
await setRouteVariantQuery(routeNumber, routeVariantIndex);
print("Set route to $routeNumber $routeVariantIndex");
} catch (e) {
print("Failed to set route");
}
inRoom = true;
print("Joined cloud room with code $roomCode");
}
print("Joined room with code $roomCode");
}
Future<void> leaveRoom() async {
if (roomCode == null) {
throw Exception("Not in a room");
if (!inRoom) {
return;
}
{
// Local Room
networkingModule.stopWebSocketServer();
inRoom = false;
networkingModule.onMessageReceived?.removeListener(_listenerReciept!);
}
{
// Cloud Room
if (_keepAliveConnection != null) {
_keepAliveConnection!.close();
_keepAliveConnection = null;
}
}
inRoom = false;
if (isHost) {
// Access the database
final client = auth.client;
@@ -406,7 +470,7 @@ class LiveInformation {
roomCode = null;
roomDocumentID = null;
isHost = false;
inRoom = false;
_keepAliveConnection?.close();
_keepAliveConnection = null;
@@ -414,6 +478,42 @@ class LiveInformation {
setRouteVariant(null);
}
String generateRoomInfo() {
// Room Info Example
/*
{
"cloud": {
"roomCode": "6633e85d0020f52f3771"
},
"local":
{
"host": "ws://192.168.0.123:8080"
},
"sync":
{
"route": "W11",
"routeVariant": 1
}
}
*/
return jsonEncode({
"cloud": {
"roomCode": roomCode,
},
"local": {
"host": "ws://${networkingModule.localIP}:8080"
},
if (_currentRouteVariant != null)
"sync": {
"route": _currentRouteVariant!.busRoute.routeNumber,
"routeVariant": _currentRouteVariant!.busRoute.routeVariants.values.toList().indexOf(_currentRouteVariant!),
}
});
}
String? lastCommand;
Future<void> ServerListener(appwrite.RealtimeMessage response) async {
print("Session update");
@@ -454,8 +554,11 @@ class LiveInformation {
// Execute the command
List<String> commands = response.payload["Commands"].cast<String>();
String? command = commands.last;
ExecuteCommand(commands.last);
}
void ExecuteCommand(String command) {
if (command == lastCommand) {
return;
}
@@ -516,6 +619,12 @@ class LiveInformation {
);
}
} else if (commandName == "setroute") {
print("Set route command received");
String routeNumber = commandParts[1];
int routeVariantIndex = int.parse(commandParts[2]);
setRouteVariantQuery(routeNumber, routeVariantIndex, sendToServer: false);
}
}
@@ -527,34 +636,48 @@ class LiveInformation {
Future<void> SendCommand(String command) async {
final client = auth.client;
final databases = appwrite.Databases(client);
{
// Local Commands
final response = await databases.listDocuments(
databaseId: "6633e85400036415ab0f",
collectionId: "6633e85d0020f52f3771",
queries: [
appwrite.Query.search("SessionID", roomCode!)
]
);
networkingModule.sendMessage(command);
}
List<String> pastCommands = [];
response.documents.first.data["Commands"].forEach((element) {
pastCommands.add(element);
});
{
// Cloud Commands
final client = auth.client;
final databases = appwrite.Databases(client);
final response = await databases.listDocuments(
databaseId: "6633e85400036415ab0f",
collectionId: "6633e85d0020f52f3771",
queries: [
appwrite.Query.search("SessionID", roomCode!)
]
);
List<String> pastCommands = [];
response.documents.first.data["Commands"].forEach((element) {
pastCommands.add(element);
});
pastCommands.add(command);
final document = await databases.updateDocument(
databaseId: "6633e85400036415ab0f",
collectionId: "6633e85d0020f52f3771",
documentId: roomDocumentID!,
data: {
"Commands": pastCommands,
"LastUpdater": auth.userID,
}
);
}
pastCommands.add(command);
final document = await databases.updateDocument(
databaseId: "6633e85400036415ab0f",
collectionId: "6633e85d0020f52f3771",
documentId: roomDocumentID!,
data: {
"Commands": pastCommands,
"LastUpdater": auth.userID,
}
);
}
List<String> _splitCommand(String command) {

View File

@@ -27,7 +27,7 @@ class AnnouncementModule extends InfoModule {
// Files
String _bundleLocation = "assets/ibus_recordings.zip";
Uint8List? _bundleBytes;
void setBundleBytes(Uint8List bytes) {
void setBundleBytes(Uint8List? bytes) {
_bundleBytes = bytes;
}
Future<Uint8List> getBundleBytes() async {
@@ -35,7 +35,6 @@ class AnnouncementModule extends InfoModule {
if (_bundleBytes != null) {
return _bundleBytes!;
} else {
// Try to load them from shared preferences
try {
SharedPreferences prefs = await SharedPreferences.getInstance();
@@ -47,17 +46,7 @@ class AnnouncementModule extends InfoModule {
} catch (e) {
throw Exception("Loading announcements from assets has been deprecated.");
}
// if (kIsWeb) {
// throw Exception("Cannot load bundle bytes on web");
// }
//
// final bytes = await rootBundle.load(_bundleLocation);
// return bytes.buffer.asUint8List();
}
}
// Queue
@@ -165,7 +154,14 @@ class AnnouncementModule extends InfoModule {
}
// Configuration
int get defaultAnnouncementDelay => liveInformation.auth.isAuthenticated() ? 1 : 0;
Duration get defaultAnnouncementDelay {
if (liveInformation.inRoom) {
return Duration(milliseconds: 500);
} else {
print("Not in room");
return Duration.zero;
}
}
// Methods
Future<void> queueAnnounceByAudioName({
@@ -177,7 +173,9 @@ class AnnouncementModule extends InfoModule {
if (sendToServer && _shouldSendToServer()) {
scheduledTime ??= liveInformation.syncedTimeModule.Now().add(Duration(seconds: defaultAnnouncementDelay));
scheduledTime ??= liveInformation.syncedTimeModule.Now().add(defaultAnnouncementDelay);
String audioNamesString = "";
@@ -232,7 +230,7 @@ class AnnouncementModule extends InfoModule {
if (sendToServer && _shouldSendToServer()) {
scheduledTime ??= liveInformation.syncedTimeModule.Now().add(Duration(seconds: defaultAnnouncementDelay));
scheduledTime ??= liveInformation.syncedTimeModule.Now().add(defaultAnnouncementDelay);
liveInformation.SendCommand("announce info $infoIndex ${scheduledTime.millisecondsSinceEpoch}");
queueAnnouncementByInfoIndex(
@@ -262,7 +260,9 @@ class AnnouncementModule extends InfoModule {
if (sendToServer && _shouldSendToServer()) {
scheduledTime ??= liveInformation.syncedTimeModule.Now().add(Duration(seconds: defaultAnnouncementDelay));
print("Sending route announcement to server");
scheduledTime ??= liveInformation.syncedTimeModule.Now().add(defaultAnnouncementDelay);
String routeNumber = routeVariant.busRoute.routeNumber;
int routeVariantIndex = routeVariant.busRoute.routeVariants.values.toList().indexOf(routeVariant);
@@ -326,7 +326,7 @@ class AnnouncementModule extends InfoModule {
// Server check
bool _shouldSendToServer() {
bool condition = liveInformation.roomCode != null;
bool condition = liveInformation.inRoom;
print("Should send to server? " + (condition.toString()));
return condition;

View File

@@ -0,0 +1,196 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:network_info_plus/network_info_plus.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as io;
import 'package:shelf_web_socket/shelf_web_socket.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:bus_infotainment/backend/modules/info_module.dart';
import 'package:bus_infotainment/utils/delegates.dart';
class NetworkingModule extends InfoModule {
// Host websocket server
String host = "ws://0.0.0.0:8080";
HttpServer? _server;
WebSocketChannel? _channel;
// Store connected WebSocket channels
final List<WebSocketChannel> _connectedClients = [];
EventDelegate<String>? onMessageReceived = EventDelegate();
NetworkingModule() {
_refresh();
refreshTimer();
}
Future<bool> startWebSocketServer() async {
try {
var handler = webSocketHandler((WebSocketChannel webSocket) {
_connectedClients.add(webSocket); // Add the client to the list
print('Client connected: ${webSocket}'); // Log client connection
webSocket.stream.listen((message) {
// Handle messages from the client here
print('Received message: $message');
// Forward message to all clients except the sender
for (var client in _connectedClients) {
if (client != webSocket) {
client.sink.add(message);
}
}
_onMessageReceived(message);
}, onDone: () {
_connectedClients.remove(webSocket); // Remove client on disconnect
print('Client disconnected: ${webSocket}'); // Log client disconnection
});
});
_server = await io.serve(handler, InternetAddress.anyIPv4, 8080);
print('WebSocket server started at ${_server?.address.address}:${_server?.port}');
return true;
} catch (e) {
print('Failed to start WebSocket server: $e');
return false;
}
}
bool stopWebSocketServer() {
if (_server == null) {
throw Exception('WebSocket server is not running');
}
try {
for (var client in _connectedClients) {
client.sink.close();
}
_connectedClients.clear();
_server?.close(force: true);
_server = null;
print('WebSocket server stopped');
return true;
} catch (e) {
print('Failed to stop WebSocket server: $e');
return false;
}
}
Future<bool> connectToWebSocketServer(String url) async {
try {
_channel = await WebSocketChannel.connect(Uri.parse(url));
_channel?.stream.listen((message) {
// Handle messages from the server here
print('Received message from server: $message');
_onMessageReceived(message);
});
print('Connected to WebSocket server at $url');
return true;
} catch (e) {
print('Failed to connect to WebSocket server: $e');
return false;
}
}
bool disconnectFromWebSocketServer() {
if (_channel == null) {
throw Exception('No active WebSocket connection');
}
try {
_channel?.sink.close();
_channel = null;
print('Disconnected from WebSocket server');
return true;
} catch (e) {
print('Failed to disconnect from WebSocket server: $e');
return false;
}
}
bool sendMessage(String message) {
// If hosting a server, send message to all clients
if (_server != null) {
return sendMessageToClients(message);
}
if (_channel == null) {
throw Exception('No active WebSocket connection');
}
try {
_channel?.sink.add(message);
print('Sent message: $message');
return true;
} catch (e) {
print('Failed to send message: $e');
return false;
}
}
bool sendMessageToClients(String message) {
if (_connectedClients.isEmpty) {
print('No clients connected');
return false;
}
try {
for (var client in _connectedClients) {
client.sink.add(message);
}
print('Sent message to all clients: $message');
return true;
} catch (e) {
print('Failed to send message to clients: $e');
return false;
}
}
void _onMessageReceived(String message) {
// Notify all listeners that a message has been received.
onMessageReceived?.trigger(message);
}
// Useful boilerplate
String _localIP = "";
String get localIP => _localIP;
Timer refreshTimer() => Timer.periodic(const Duration(seconds: 10), (timer) {
if (kIsWeb) return;
_refresh();
});
Future<void> _refresh() async {
print("Refreshing network info...");
{
// Update the local IP address
// First try NetworkInfo
_localIP = (await NetworkInfo().getWifiIP()) ?? "";
// If null, try NetworkInterface
// Only look for ethernet. Wifi would have been found by NetworkInfo
if (_localIP.isEmpty) {
for (var interface in await NetworkInterface.list()) {
if (!interface.name.toLowerCase().contains("eth") || interface.name.contains(" ")) {
continue;
}
for (var addr in interface.addresses) {
print('Interface ${interface.name} has address ${addr.address}');
if (addr.type == InternetAddressType.IPv4 && !addr.isLoopback) {
_localIP = addr.address;
break;
}
}
}
}
}
}
}