460 lines
14 KiB
Dart
460 lines
14 KiB
Dart
|
|
// Singleton
|
|
import 'dart:async';
|
|
|
|
import 'package:appwrite/appwrite.dart' as appwrite;
|
|
import 'package:appwrite/models.dart' as models;
|
|
import 'package:bus_infotainment/audio_cache.dart';
|
|
import 'package:bus_infotainment/auth/api_constants.dart';
|
|
import 'package:bus_infotainment/auth/auth_api.dart';
|
|
import 'package:bus_infotainment/tfl_datasets.dart';
|
|
import 'package:bus_infotainment/utils/audio%20wrapper.dart';
|
|
import 'package:bus_infotainment/utils/delegates.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:http/http.dart' as http;
|
|
|
|
class LiveInformation {
|
|
|
|
static final LiveInformation _singleton = LiveInformation._internal();
|
|
|
|
factory LiveInformation() {
|
|
return _singleton;
|
|
}
|
|
|
|
LiveInformation._internal();
|
|
|
|
Future<void> Initialize() async {
|
|
|
|
{
|
|
// Load the bus sequences
|
|
|
|
try {
|
|
|
|
http.Response response = await http.get(Uri.parse('https://tfl.gov.uk/bus-sequences.csv'));
|
|
|
|
busSequences = BusSequences.fromCSV(response.body);
|
|
|
|
print("Loaded bus sequences from TFL");
|
|
|
|
} catch (e) {
|
|
|
|
String csv = await rootBundle.loadString("assets/datasets/bus-sequences.csv");
|
|
|
|
busSequences = BusSequences.fromCSV(csv);
|
|
|
|
print("Loaded bus sequences from assets");
|
|
}
|
|
|
|
if (auth.isAuthenticated()){
|
|
print("Auth is authenticated");
|
|
setupRealtime();
|
|
} else {
|
|
print("Auth is not authenticated");
|
|
auth.onLogin.addListener((value) {
|
|
setupRealtime();
|
|
});
|
|
}
|
|
|
|
}
|
|
|
|
refreshTimer();
|
|
}
|
|
|
|
Timer refreshTimer() => Timer.periodic(Duration(seconds: 1), (timer) {
|
|
_handleAnnouncementQueue();
|
|
});
|
|
|
|
|
|
AudioWrapper audioPlayer = AudioWrapper();
|
|
AnnouncementCache announcementCache = AnnouncementCache();
|
|
List<AnnouncementQueueEntry> announcementQueue = [];
|
|
DateTime lastAnnouncement = DateTime.now();
|
|
EventDelegate<AnnouncementQueueEntry> announcementDelegate = EventDelegate();
|
|
String _currentAnnouncement = "*** NO MESSAGE ***";
|
|
|
|
String get currentAnnouncement => _currentAnnouncement;
|
|
void set currentAnnouncement(String value) {
|
|
_currentAnnouncement = value;
|
|
}
|
|
|
|
void _handleAnnouncementQueue() async {
|
|
print("Handling announcement queue");
|
|
if (audioPlayer.state != AudioWrapper_State.Playing) {
|
|
if (announcementQueue.isNotEmpty) {
|
|
AnnouncementQueueEntry announcement = announcementQueue.first;
|
|
announcementDelegate.trigger(announcement);
|
|
_currentAnnouncement = announcement.displayText;
|
|
|
|
lastAnnouncement = DateTime.now();
|
|
|
|
for (AudioWrapperSource source in announcement.audioSources) {
|
|
|
|
Duration? duration = await audioPlayer.play(source);
|
|
await Future.delayed(duration!);
|
|
await Future.delayed(Duration(milliseconds: 150));
|
|
}
|
|
audioPlayer.stop();
|
|
announcementQueue.removeAt(0);
|
|
print("Popped announcement queue");
|
|
}
|
|
}
|
|
}
|
|
|
|
void announceRouteVariant(BusRouteVariant routeVariant) async {
|
|
if (routeVariant == null) {
|
|
return;
|
|
}
|
|
String display = "${routeVariant.busRoute.routeNumber} to ${routeVariant.busStops.last.formattedStopName}";
|
|
|
|
String audio_route = "R_${routeVariant.busRoute.routeNumber}_001.mp3";
|
|
String audio_destination = routeVariant.busStops.last.getAudioFileName();
|
|
|
|
print("Audio file: $audio_route");
|
|
|
|
await announcementCache.loadAnnouncements([audio_route, audio_destination]);
|
|
|
|
AudioWrapperSource source_route = AudioWrapperByteSource(announcementCache[audio_route]);
|
|
AudioWrapperSource source_destination = AudioWrapperByteSource(announcementCache[audio_destination]);
|
|
|
|
queueAnnouncement(AnnouncementQueueEntry(
|
|
displayText: display,
|
|
audioSources: [source_route, AudioWrapperAssetSource("audio/to_destination.wav"), source_destination]
|
|
));
|
|
|
|
}
|
|
|
|
late BusSequences busSequences;
|
|
|
|
BusRouteVariant? _currentRouteVariant;
|
|
|
|
void setRouteVariant(BusRouteVariant routeVariant) {
|
|
_currentRouteVariant = routeVariant;
|
|
announceRouteVariant(routeVariant);
|
|
}
|
|
|
|
BusRouteVariant? getRouteVariant() {
|
|
return _currentRouteVariant;
|
|
}
|
|
|
|
void queueAnnouncement(AnnouncementQueueEntry announcement) {
|
|
announcementQueue.add(announcement);
|
|
|
|
// Make sure the timestamp of the announcement is after the last announcement
|
|
// If so, dont queue it
|
|
// If timestamp is null, then skip this check
|
|
if (announcement.timestamp != null && announcement.timestamp!.isBefore(lastAnnouncement)) {
|
|
return;
|
|
}
|
|
|
|
if (!announcement.sendToServer) {
|
|
return;
|
|
}
|
|
|
|
final databases = appwrite.Databases(auth.client);
|
|
|
|
if (announcement is ManualAnnouncementEntry) {
|
|
|
|
final document = databases.createDocument(
|
|
documentId: appwrite.ID.unique(),
|
|
databaseId: ApiConstants.INFO_Q_DATABASE_ID,
|
|
collectionId: ApiConstants.MANUAL_Q_COLLECTION_ID,
|
|
data: {
|
|
"ManualAnnouncementIndex": manualAnnouncements.indexOf(announcement),
|
|
"SessionID": sessionID,
|
|
}
|
|
);
|
|
|
|
print("Queued manual announcement: ${announcement.shortName}");
|
|
|
|
} else if (announcement is AnnouncementQueueEntry) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
List<ManualAnnouncementEntry> manualAnnouncements = [
|
|
ManualAnnouncementEntry(
|
|
shortName: "Driver Change",
|
|
informationText: "Driver Change",
|
|
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/driverchange.mp3")],
|
|
),
|
|
ManualAnnouncementEntry(
|
|
shortName: "No Standing Upr Deck",
|
|
informationText: "No standing on the upper deck",
|
|
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/nostanding.mp3")],
|
|
),
|
|
ManualAnnouncementEntry(
|
|
shortName: "Face Covering",
|
|
informationText: "Please wear a face covering!",
|
|
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/facecovering.mp3")],
|
|
),
|
|
ManualAnnouncementEntry(
|
|
shortName: "Seats Upstairs",
|
|
informationText: "Seats are available upstairs",
|
|
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/seatsupstairs.mp3")],
|
|
),
|
|
ManualAnnouncementEntry(
|
|
shortName: "Bus Terminates Here",
|
|
informationText: "Bus terminates here. Please take your belongings with you",
|
|
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/busterminateshere.mp3")],
|
|
),
|
|
ManualAnnouncementEntry(
|
|
shortName: "Bus On Diversion",
|
|
informationText: "Bus on diversion. Please listen for further announcements",
|
|
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/busondiversion.mp3")],
|
|
),
|
|
ManualAnnouncementEntry(
|
|
shortName: "Destination Change",
|
|
informationText: "Destination Changed - please listen for further instructions",
|
|
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/destinationchange.mp3")],
|
|
),
|
|
ManualAnnouncementEntry(
|
|
shortName: "Wheelchair Space",
|
|
informationText: "Wheelchair space requested",
|
|
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/wheelchairspace1.mp3")],
|
|
),
|
|
ManualAnnouncementEntry(
|
|
shortName: "Move Down The Bus",
|
|
informationText: "Please move down the bus",
|
|
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/movedownthebus.mp3")],
|
|
),
|
|
ManualAnnouncementEntry(
|
|
shortName: "Next Stop Closed",
|
|
informationText: "The next bus stop is closed",
|
|
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/nextstopclosed.wav")],
|
|
),
|
|
ManualAnnouncementEntry(
|
|
shortName: "CCTV In Operation",
|
|
informationText: "CCTV is in operation on this bus",
|
|
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/cctvoperation.mp3")],
|
|
),
|
|
ManualAnnouncementEntry(
|
|
shortName: "Safe Door Opening",
|
|
informationText: "Driver will open the doors when it is safe to do so",
|
|
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/safedooropening.mp3")],
|
|
),
|
|
ManualAnnouncementEntry(
|
|
shortName: "Buggy Safety",
|
|
informationText: "For your child's safety, please remain with your buggy",
|
|
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/buggysafety.mp3")],
|
|
),
|
|
ManualAnnouncementEntry(
|
|
shortName: "Wheelchair Space 2",
|
|
informationText: "Wheelchair priority space required",
|
|
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/wheelchairspace2.mp3")],
|
|
),
|
|
ManualAnnouncementEntry(
|
|
shortName: "Service Regulation",
|
|
informationText: "Regulating service - please listen for further information",
|
|
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/serviceregulation.mp3")],
|
|
),
|
|
ManualAnnouncementEntry(
|
|
shortName: "Bus Ready To Depart",
|
|
informationText: "This bus is ready to depart",
|
|
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/readytodepart.mp3")],
|
|
),
|
|
];
|
|
|
|
AuthAPI auth = AuthAPI();
|
|
String sessionID = "65de648aa7f44684ecce";
|
|
void updateServer() async {
|
|
|
|
|
|
|
|
final databases = appwrite.Databases(auth.client);
|
|
|
|
// final document = databases.updateDocument(
|
|
// databaseId: ApiConstants.INFO_DATABASE_ID,
|
|
// collectionId: ApiConstants.INFO_COLLECTION_ID,
|
|
// documentId: documentID,
|
|
// data: {
|
|
// "Display": _currentAnnouncement,
|
|
// }
|
|
// );
|
|
|
|
print("Updated server with announcement: $_currentAnnouncement");
|
|
|
|
}
|
|
void pullServer() async {
|
|
|
|
if (auth.status == AuthStatus.UNAUTHENTICATED) {
|
|
return;
|
|
}
|
|
|
|
final databases = appwrite.Databases(auth.client);
|
|
|
|
// final document = await databases.getDocument(
|
|
// databaseId: ApiConstants.INFO_DATABASE_ID,
|
|
// collectionId: ApiConstants.INFO_COLLECTION_ID,
|
|
// documentId: documentID,
|
|
// );
|
|
|
|
// queueAnnouncement(AnnouncementQueueEntry(
|
|
// displayText: document.data['Display'],
|
|
// audioSources: [],
|
|
// sendToServer: false, // Don't send this back to the server, else we'll get an infinite loop
|
|
// ));
|
|
|
|
// print("Pulled announcement from server: ${document.data['Display']}");
|
|
}
|
|
|
|
bool purgeRunning = false;
|
|
Future<void> deleteAllManualQueueEntries() async {
|
|
|
|
if (purgeRunning) {
|
|
return;
|
|
}
|
|
purgeRunning = true;
|
|
|
|
final databases = appwrite.Databases(auth.client);
|
|
int offset = 0;
|
|
const int limit = 25; // Maximum number of documents that can be fetched at once
|
|
|
|
while (true) {
|
|
// Fetch a page of documents from the manual queue collection
|
|
print("Deleting manual queue entries");
|
|
final manual_q = await databases.listDocuments(
|
|
databaseId: ApiConstants.INFO_Q_DATABASE_ID,
|
|
collectionId: ApiConstants.MANUAL_Q_COLLECTION_ID,
|
|
queries: [
|
|
appwrite.Query.search("SessionID", sessionID),
|
|
appwrite.Query.limit(limit),
|
|
appwrite.Query.offset(offset),
|
|
appwrite.Query.orderDesc('\$createdAt')
|
|
]
|
|
);
|
|
|
|
// If there are no documents in the fetched page, break the loop
|
|
if (manual_q.documents.isEmpty) {
|
|
break;
|
|
}
|
|
|
|
// Delete each document in the fetched page
|
|
for (models.Document doc in manual_q.documents) {
|
|
await databases.deleteDocument(
|
|
databaseId: ApiConstants.INFO_Q_DATABASE_ID,
|
|
collectionId: ApiConstants.MANUAL_Q_COLLECTION_ID,
|
|
documentId: doc.$id,
|
|
);
|
|
}
|
|
|
|
// Go to the next page
|
|
offset += limit;
|
|
}
|
|
|
|
print("Deleted all manual queue entries");
|
|
}
|
|
|
|
void pullQueue() async {
|
|
|
|
if (auth.status == AuthStatus.UNAUTHENTICATED) {
|
|
return;
|
|
}
|
|
|
|
final databases = appwrite.Databases(auth.client);
|
|
|
|
// Pull the manual queue
|
|
final manual_q = await databases.listDocuments(
|
|
databaseId: ApiConstants.INFO_Q_DATABASE_ID,
|
|
collectionId: ApiConstants.MANUAL_Q_COLLECTION_ID,
|
|
queries: [
|
|
appwrite.Query.search("SessionID", sessionID),
|
|
appwrite.Query.limit(25),
|
|
appwrite.Query.offset(0),
|
|
appwrite.Query.orderDesc('\$createdAt')
|
|
]
|
|
);
|
|
|
|
List<AnnouncementQueueEntry> queue = [];
|
|
|
|
for (models.Document doc in manual_q.documents) {
|
|
int index = doc.data['ManualAnnouncementIndex'];
|
|
|
|
ManualAnnouncementEntry announcement_clone = ManualAnnouncementEntry(
|
|
shortName: manualAnnouncements[index].shortName,
|
|
informationText: manualAnnouncements[index].displayText,
|
|
audioSources: manualAnnouncements[index].audioSources,
|
|
timestamp: DateTime.parse(doc.$createdAt),
|
|
sendToServer: false,
|
|
);
|
|
|
|
// sort the queue by timestamp, so the oldest announcements are at the front
|
|
|
|
|
|
|
|
queue.add(announcement_clone);
|
|
}
|
|
|
|
|
|
|
|
|
|
for (AnnouncementQueueEntry entry in queue) {
|
|
queueAnnouncement(entry);
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
appwrite.RealtimeSubscription? manual_q_subscription;
|
|
Future<void> setupRealtime() async {
|
|
|
|
if (manual_q_subscription != null) {
|
|
return;
|
|
}
|
|
|
|
// await deleteAllManualQueueEntries(); //todo
|
|
|
|
print("Setting up realtime");
|
|
|
|
// Websocket
|
|
appwrite.Realtime realtime = appwrite.Realtime(auth.client);
|
|
|
|
manual_q_subscription = realtime.subscribe(
|
|
['databases.${ApiConstants.INFO_Q_DATABASE_ID}.collections.${ApiConstants.MANUAL_Q_COLLECTION_ID}.documents'],
|
|
);
|
|
manual_q_subscription?.stream.listen((event) {
|
|
print("Manual queue entry added");
|
|
|
|
pullQueue();
|
|
});
|
|
|
|
print("Subscribed to servers");
|
|
|
|
await Future.delayed(Duration(seconds: 90));
|
|
|
|
manual_q_subscription?.close();
|
|
manual_q_subscription = null;
|
|
setupRealtime();
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
class AnnouncementQueueEntry {
|
|
final String displayText;
|
|
final List<AudioWrapperSource> audioSources;
|
|
bool sendToServer = true;
|
|
DateTime? timestamp;
|
|
|
|
AnnouncementQueueEntry({required this.displayText, required this.audioSources, this.sendToServer = true, this.timestamp});
|
|
}
|
|
|
|
class ManualAnnouncementEntry extends AnnouncementQueueEntry {
|
|
final String shortName;
|
|
|
|
ManualAnnouncementEntry({
|
|
required this.shortName,
|
|
required String informationText,
|
|
required List<AudioWrapperSource> audioSources,
|
|
DateTime? timestamp,
|
|
bool sendToServer = true,
|
|
}) : super(
|
|
displayText: informationText,
|
|
audioSources: audioSources,
|
|
sendToServer: sendToServer,
|
|
timestamp: timestamp,
|
|
);
|
|
} |