This commit is contained in:
ImBenji
2024-02-28 18:06:22 +00:00
parent de98a2b40b
commit 8b8cb0412f
9 changed files with 520 additions and 147 deletions

View File

@@ -5,9 +5,9 @@ class ApiConstants {
static const String APPWRITE_PROJECT_ID = "65de530c1c0a7ffc0c3f"; static const String APPWRITE_PROJECT_ID = "65de530c1c0a7ffc0c3f";
static const String INFO_Q_DATABASE_ID = "65de5cab16717444527b"; static const String INFO_Q_DATABASE_ID = "65de5cab16717444527b";
static const String DEST_Q_COLLECTION_ID = "65de9f2f925562a2eda8"; static const String MANUAL_Q_COLLECTION_ID = "65de9f2f925562a2eda8";
static const String MANUAL_Q_COLLECTION_ID = "65de9f1b6282fd209bdb"; static const String INFORMATION_Q_COLLECTION_ID = "65de9f1b6282fd209bdb";
static const String BUSSTOP_Q_COLLECTION_ID = "65de9ef464bfa5a0693d"; static const String DEST_Q_COLLECTION_ID = "65de9ef464bfa5a0693d";

View File

@@ -44,7 +44,7 @@ class _ibus_displayState extends State<ibus_display> {
String _padString(String input){ String _padString(String input){
if (input.length < 40){ if (input.length < 30){
print("Input is too short"); print("Input is too short");
return input; return input;
} }

View File

@@ -3,6 +3,7 @@
import 'package:bus_infotainment/pages/components/ibus_display.dart'; import 'package:bus_infotainment/pages/components/ibus_display.dart';
import 'package:bus_infotainment/singletons/live_information.dart'; import 'package:bus_infotainment/singletons/live_information.dart';
import 'package:bus_infotainment/tfl_datasets.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:bus_infotainment/utils/delegates.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
@@ -109,79 +110,102 @@ class pages_Home extends StatelessWidget {
outlineColor: Colors.white70, outlineColor: Colors.white70,
); );
}, },
defaultBuilder: (context) {
BusRouteVariant? routeVariant = LiveInformation().getRouteVariant();
if (routeVariant == null) {
return Container(
color: Colors.grey.shade900,
child: Center(
child: Text(
"No route selected",
style: GoogleFonts.teko(
fontSize: 25,
color: Colors.white70
), ),
), ),
), ),
);
Container( } else {
return StopAnnouncementPicker(
margin: EdgeInsets.all(20), routeVariant: routeVariant,
backgroundColor: Colors.grey.shade900,
height: 300-45, outlineColor: Colors.white70,
);
child: ListView( }
scrollDirection: Axis.vertical,
children: [
ElevatedButton(
onPressed: () {
LiveInformation liveInformation = LiveInformation();
liveInformation.announceRouteVariant(liveInformation.getRouteVariant()!);
},
child: Text("Test announcement"),
),
ElevatedButton(
onPressed: () {
LiveInformation liveInformation = LiveInformation();
liveInformation.updateServer();
},
child: Text("Update server"),
),
SizedBox(
width: 100,
child: TextField(
onChanged: (String value) {
LiveInformation liveInformation = LiveInformation();
// liveInformation.documentID = value;
}, },
), ),
), ),
SizedBox(
width: 200,
child: TextField(
onSubmitted: (String value) {
LiveInformation liveInformation = LiveInformation();
liveInformation.queueAnnouncement(AnnouncementQueueEntry(
displayText: value,
audioSources: []
));
},
),
), ),
ElevatedButton( // Container(
onPressed: () { //
LiveInformation liveInformation = LiveInformation(); // margin: EdgeInsets.all(20),
liveInformation.pullServer(); //
}, // height: 300-45,
child: Text("Pull server"), //
), // child: ListView(
//
], // scrollDirection: Axis.vertical,
//
), // children: [
//
), // ElevatedButton(
// onPressed: () async {
// LiveInformation liveInformation = LiveInformation();
// liveInformation.queueAnnouncement(await liveInformation.getDestinationAnnouncement(liveInformation.getRouteVariant()!, sendToServer: false));
// },
// child: Text("Test announcement"),
// ),
//
// ElevatedButton(
// onPressed: () {
// LiveInformation liveInformation = LiveInformation();
// liveInformation.updateServer();
// },
// child: Text("Update server"),
// ),
//
// SizedBox(
//
// width: 100,
//
// child: TextField(
// onChanged: (String value) {
// LiveInformation liveInformation = LiveInformation();
// // liveInformation.documentID = value;
// },
// ),
// ),
//
// SizedBox(
//
// width: 200,
//
// child: TextField(
// onSubmitted: (String value) {
// LiveInformation liveInformation = LiveInformation();
// liveInformation.queueAnnouncement(AnnouncementQueueEntry(
// displayText: value,
// audioSources: []
// ));
// },
// ),
// ),
//
// ElevatedButton(
// onPressed: () {
// LiveInformation liveInformation = LiveInformation();
// liveInformation.pullServer();
// },
// child: Text("Pull server"),
// ),
//
// ],
//
// ),
//
// ),
], ],
) )
@@ -194,7 +218,7 @@ class ManualAnnouncementPicker extends StatefulWidget {
final Color backgroundColor; final Color backgroundColor;
final Color outlineColor; final Color outlineColor;
final List<InformationAnnouncementEntry> announcements; final List<NamedAnnouncementQueueEntry> announcements;
const ManualAnnouncementPicker({super.key, required this.backgroundColor, required this.outlineColor, required this.announcements}); const ManualAnnouncementPicker({super.key, required this.backgroundColor, required this.outlineColor, required this.announcements});
@@ -219,14 +243,16 @@ class _ManualAnnouncementPickerState extends State<ManualAnnouncementPicker> {
return; return;
} }
for (InformationAnnouncementEntry announcement in widget.announcements!) { int i = 0;
for (NamedAnnouncementQueueEntry announcement in widget.announcements!) {
announcementWidgets.add( announcementWidgets.add(
_ManualAnnouncementEntry( _ManualAnnouncementEntry(
announcement: announcement, announcement: announcement,
index: liveInformation.manualAnnouncements.indexOf(announcement), index: i,
outlineColor: Colors.white70 outlineColor: Colors.white70
) )
); );
i++;
} }
} }
@@ -506,10 +532,14 @@ class StopAnnouncementPicker extends ManualAnnouncementPicker {
outlineColor: outlineColor, outlineColor: outlineColor,
announcements: [ announcements: [
for (BusRouteStops stop in routeVariant.busStops) for (BusRouteStops stop in routeVariant.busStops)
InformationAnnouncementEntry( ManualAnnouncementEntry(
shortName: stop.formattedStopName, shortName: stop.formattedStopName,
informationText: stop.formattedStopName, informationText: stop.formattedStopName,
audioSources: [] audioSources: [
// AudioWrapperByteSource(
// LiveInformation().announcementCache[stop.getAudioFileName()]
// )
]
) )
] ]
); );
@@ -521,7 +551,7 @@ int wrap(int i, int j, int length) {
class _ManualAnnouncementEntry extends StatelessWidget { class _ManualAnnouncementEntry extends StatelessWidget {
final InformationAnnouncementEntry announcement; final NamedAnnouncementQueueEntry announcement;
final int index; final int index;
final Color outlineColor; final Color outlineColor;

View File

@@ -334,11 +334,19 @@ class _Variant extends StatelessWidget {
child: const Text("Cancel"), child: const Text("Cancel"),
), ),
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () async {
Navigator.of(context).pop(); Navigator.of(context).pop();
LiveInformation liveInformation = LiveInformation(); LiveInformation liveInformation = LiveInformation();
liveInformation.setRouteVariant(variant); liveInformation.setRouteVariant(variant);
liveInformation.queueAnnouncement(
await liveInformation.getDestinationAnnouncement(
variant,
sendToServer: true,
)
);
tfL_Dataset_TestState.setState(() {}); tfL_Dataset_TestState.setState(() {});
}, },

View File

@@ -1,6 +1,7 @@
// Singleton // Singleton
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:appwrite/appwrite.dart' as appwrite; import 'package:appwrite/appwrite.dart' as appwrite;
import 'package:appwrite/models.dart' as models; import 'package:appwrite/models.dart' as models;
@@ -10,8 +11,10 @@ import 'package:bus_infotainment/auth/auth_api.dart';
import 'package:bus_infotainment/tfl_datasets.dart'; import 'package:bus_infotainment/tfl_datasets.dart';
import 'package:bus_infotainment/utils/audio%20wrapper.dart'; import 'package:bus_infotainment/utils/audio%20wrapper.dart';
import 'package:bus_infotainment/utils/delegates.dart'; import 'package:bus_infotainment/utils/delegates.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:ntp/ntp.dart';
class LiveInformation { class LiveInformation {
@@ -61,15 +64,45 @@ class LiveInformation {
} }
Timer refreshTimer() => Timer.periodic(Duration(milliseconds: 50), (timer) { Timer refreshTimer() => Timer.periodic(const Duration(milliseconds: 100), (timer) async {
await updateNtpOffset();
_handleAnnouncementQueue(); _handleAnnouncementQueue();
}); });
int ntpOffset = -1;
DateTime lastNtpUpdate = DateTime.now().add(const Duration(seconds: -15));
/// updates the NTP offset from DateTime.now()
Future<void> updateNtpOffset() async {
// Only update the NTP offset every 10 seconds
if (DateTime.now().difference(lastNtpUpdate).inSeconds < 10) {
return;
}
var res = await http.get(Uri.parse('http://worldtimeapi.org/api/timezone/Europe/London'));
if (res.statusCode == 200) {
var json = jsonDecode(res.body);
DateTime time = DateTime.parse(json['datetime']);
ntpOffset = time.millisecondsSinceEpoch - DateTime.now().millisecondsSinceEpoch;
lastNtpUpdate = DateTime.now();
}
}
DateTime getNow() {
if (ntpOffset == -1) {
throw Exception("NTP offset not set");
}
return DateTime.now().add(Duration(milliseconds: ntpOffset));
}
AudioWrapper audioPlayer = AudioWrapper(); AudioWrapper audioPlayer = AudioWrapper();
AnnouncementCache announcementCache = AnnouncementCache(); AnnouncementCache announcementCache = AnnouncementCache();
List<AnnouncementQueueEntry> announcementQueue = []; List<AnnouncementQueueEntry> announcementQueue = [];
DateTime lastAnnouncement = DateTime.now(); AnnouncementQueueEntry? lastAnnouncement;
DateTime lastAnnouncementTimeStamp = DateTime.now().toUtc();
EventDelegate<AnnouncementQueueEntry> announcementDelegate = EventDelegate(); EventDelegate<AnnouncementQueueEntry> announcementDelegate = EventDelegate();
String _currentAnnouncement = "*** NO MESSAGE ***"; String _currentAnnouncement = "*** NO MESSAGE ***";
@@ -78,18 +111,23 @@ class LiveInformation {
_currentAnnouncement = value; _currentAnnouncement = value;
} }
bool isPlayingAnnouncement = false;
void _handleAnnouncementQueue() async { void _handleAnnouncementQueue() async {
int timerInterval = 50; int timerInterval = 100;
// print("Handling announcement queue"); // print("Handling announcement queue");
if (audioPlayer.state != AudioWrapper_State.Playing) { if (!isPlayingAnnouncement) {
if (announcementQueue.isNotEmpty) { if (announcementQueue.isNotEmpty) {
print("Handling announcement queue");
AnnouncementQueueEntry announcement = announcementQueue.first; AnnouncementQueueEntry announcement = announcementQueue.first;
print("Queue length: ${announcementQueue.length}");
{ {
DateTime now = DateTime.now(); DateTime now = getNow();
if (announcement.scheduledTime != null) { if (announcement.scheduledTime != null) {
int milisecondDifference = abs(now.millisecondsSinceEpoch - announcement.scheduledTime!.millisecondsSinceEpoch); int milisecondDifference = abs(now.millisecondsSinceEpoch - announcement.scheduledTime!.millisecondsSinceEpoch);
// print("Q Difference: ${milisecondDifference}"); // print("Q Difference: ${milisecondDifference}");
@@ -97,105 +135,178 @@ class LiveInformation {
// Account for the time lost by the periodic timer // Account for the time lost by the periodic timer
await Future.delayed(Duration(milliseconds: timerInterval - milisecondDifference)); await Future.delayed(Duration(milliseconds: timerInterval - milisecondDifference));
} else { } else {
print("Due in: ${milisecondDifference}ms");
return; return;
} }
} }
}
announcementQueue.removeAt(0);
lastAnnouncement = announcement;
isPlayingAnnouncement = true;
if (kIsWeb) {
await Future.delayed(Duration(milliseconds: 100));
} }
// announcementDelegate.trigger(announcement); print("Displaying announcement: ${announcement.displayText}");
announcementDelegate.trigger(announcement);
_currentAnnouncement = announcement.displayText; _currentAnnouncement = announcement.displayText;
lastAnnouncement = DateTime.now(); lastAnnouncementTimeStamp = getNow();
if (announcement.audioSources.isNotEmpty) {
try {
for (AudioWrapperSource source in announcement.audioSources) { for (AudioWrapperSource source in announcement.audioSources) {
Duration? duration = await audioPlayer.play(source); Duration? duration = await audioPlayer.play(source);
await Future.delayed(duration!); await Future.delayed(duration!);
await Future.delayed(Duration(milliseconds: 150)); await Future.delayed(Duration(milliseconds: 150));
} }
} finally {
audioPlayer.stop(); audioPlayer.stop();
announcementQueue.remove(announcement);
print("Queue length: ${announcementQueue.length}"); }
} else {
if (announcementQueue.isNotEmpty) {
await Future.delayed(Duration(seconds: 5));
}
}
isPlayingAnnouncement = false;
print("Popped announcement queue"); print("Popped announcement queue");
} }
} }
} }
void announceRouteVariant(BusRouteVariant routeVariant) async { Future<AnnouncementQueueEntry> _getDestinationAnnouncement(BusRouteVariant routeVariant, {bool sendToServer = false}) async {
if (routeVariant == null) {
return;
}
String display = "${routeVariant.busRoute.routeNumber} to ${routeVariant.busStops.last.formattedStopName}"; String display = "${routeVariant.busRoute.routeNumber} to ${routeVariant.busStops.last.formattedStopName}";
String audio_route = "R_${routeVariant.busRoute.routeNumber}_001.mp3"; String audio_route = "R_${routeVariant.busRoute.routeNumber}_001.mp3";
String audio_destination = routeVariant.busStops.last.getAudioFileName(); String audio_destination = routeVariant.busStops.last.getAudioFileName();
print("Audio file: $audio_route"); // Cache the audio files
await announcementCache.loadAnnouncements([audio_route, audio_destination]); await announcementCache.loadAnnouncements([audio_route, audio_destination]);
AudioWrapperSource source_route = AudioWrapperByteSource(announcementCache[audio_route]); AudioWrapperSource source_route = AudioWrapperByteSource(announcementCache[audio_route]);
AudioWrapperSource source_destination = AudioWrapperByteSource(announcementCache[audio_destination]); AudioWrapperSource source_destination = AudioWrapperByteSource(announcementCache[audio_destination]);
queueAnnouncement(AnnouncementQueueEntry( return AnnouncementQueueEntry(
sendToServer: sendToServer,
displayText: display, displayText: display,
audioSources: [source_route, AudioWrapperAssetSource("audio/to_destination.wav"), source_destination] audioSources: [source_route, AudioWrapperAssetSource("audio/to_destination.wav"), source_destination]
)); );
} }
Future<AnnouncementQueueEntry> getDestinationAnnouncement(BusRouteVariant routeVariant, {bool sendToServer = true}) async {
return DestinationAnnouncementEntry(
routeVariant: routeVariant,
audioSources: [],
sendToServer: sendToServer,
);
}
late BusSequences busSequences; late BusSequences busSequences;
BusRouteVariant? _currentRouteVariant; BusRouteVariant? _currentRouteVariant;
EventDelegate<BusRouteVariant> routeVariantDelegate = EventDelegate(); EventDelegate<BusRouteVariant> routeVariantDelegate = EventDelegate();
void setRouteVariant(BusRouteVariant routeVariant) { Future<void> setRouteVariant(BusRouteVariant routeVariant) async {
_currentRouteVariant = routeVariant; _currentRouteVariant = routeVariant;
announceRouteVariant(routeVariant);
routeVariantDelegate.trigger(routeVariant); routeVariantDelegate.trigger(routeVariant);
// cache all of the stop announcements
List<String> audioFiles = [];
for (BusRouteStops stop in routeVariant.busStops) {
audioFiles.add(stop.getAudioFileName());
print("Cached stop audio: ${stop.getAudioFileName()}");
}
await announcementCache.loadAnnouncements(audioFiles);
} }
BusRouteVariant? getRouteVariant() { BusRouteVariant? getRouteVariant() {
return _currentRouteVariant; return _currentRouteVariant;
} }
void queueAnnouncement(AnnouncementQueueEntry announcement) { void queueAnnouncement(AnnouncementQueueEntry announcement) async {
// Make sure the timestamp of the announcement is after the last announcement // Make sure the timestamp of the announcement is after the last announcement
// If so, dont queue it // If so, dont queue it
// If timestamp is null, then skip this check // If timestamp is null, then skip this check
if (announcement.timestamp != null && announcement.timestamp!.isBefore(lastAnnouncement)) { if (announcement.timestamp != null && announcement.timestamp!.toUtc().isBefore(lastAnnouncementTimeStamp)) {
print("Announcement is too old"); print("Announcement is too old");
print("LastAnnouncement: $lastAnnouncement"); print("LastAnnouncement: $lastAnnouncementTimeStamp");
print("Announcement: ${announcement.timestamp}"); print("Announcement: ${announcement.timestamp}");
int difference = announcement.timestamp!.difference(lastAnnouncement).inMilliseconds; int difference = announcement.timestamp!.difference(lastAnnouncementTimeStamp).inMilliseconds;
print("Difference: $difference"); print("Difference: $difference");
return; return;
} else if (announcement.timestamp == null) { } else if (announcement.timestamp == null) {
print("Announcement `${announcement.displayText}` does not have timestamp"); print("Announcement `${announcement.displayText}` does not have timestamp");
} }
// If there is an announcement in the queue with the same timestamp, dont queue it
if (announcementQueue.any((element) => element.timestamp == announcement.timestamp)) {
print("Announcement with same timestamp already in queue");
return;
}
if (!announcement.sendToServer) { if (!announcement.sendToServer) {
if (announcement is DestinationAnnouncementEntry) {
BusRouteVariant routeVariant = announcement.routeVariant;
if (getRouteVariant() != routeVariant) {
setRouteVariant(routeVariant);
}
announcementQueue.add(
await _getDestinationAnnouncement(
routeVariant,
sendToServer: false
)
);
print("Queued destination announcement: ${announcement.displayText}");
print("Audios: ${announcement.audioSources.length}");
return;
}
announcementQueue.add(announcement); announcementQueue.add(announcement);
print("Queued announcement: ${announcement.displayText} (no server)");
return; return;
} }
final databases = appwrite.Databases(auth.client); final databases = appwrite.Databases(auth.client);
if (announcement is InformationAnnouncementEntry) { print("Queuing announcement: ${announcement.displayText} (server)");
print("Announcement type: ${announcement.runtimeType}");
if (announcement.runtimeType == InformationAnnouncementEntry) {
announcement as InformationAnnouncementEntry;
print("Queing to InformationAnnouncementEntry");
// 5 sedonds in the future // 5 sedonds in the future
DateTime scheduledTime = DateTime.now().add(Duration(seconds: 5)); DateTime scheduledTime = (await getNow()).add(Duration(seconds: 1));
final document = databases.createDocument( final document = databases.createDocument(
documentId: appwrite.ID.unique(), documentId: appwrite.ID.unique(),
databaseId: ApiConstants.INFO_Q_DATABASE_ID, databaseId: ApiConstants.INFO_Q_DATABASE_ID,
collectionId: ApiConstants.MANUAL_Q_COLLECTION_ID, collectionId: ApiConstants.INFORMATION_Q_COLLECTION_ID,
data: { data: {
"ManualAnnouncementIndex": manualAnnouncements.indexOf(announcement), "ManualAnnouncementIndex": manualAnnouncements.indexOf(announcement),
"ScheduledTime": scheduledTime.toIso8601String(), "ScheduledTime": scheduledTime.toIso8601String(),
@@ -203,9 +314,73 @@ class LiveInformation {
} }
); );
print("Queued manual announcement: ${announcement.shortName} (server)");
} else if (announcement.runtimeType == ManualAnnouncementEntry) {
announcement as ManualAnnouncementEntry;
print("Queing to ManualAnnouncementEntry");
// 5 sedonds in the future
DateTime scheduledTime = (await getNow()).add(Duration(seconds: 1));
print("debug2");
List<String> audioFileNames = [];
for (AudioWrapperSource source in announcement.audioSources) {
if (source is AudioWrapperByteSource) {
Uint8List? bytes = await source.bytes;
String? filename = null;
for (String key in announcementCache.keys) {
if (announcementCache[key] == bytes) {
filename = key;
break;
}
}
}
}
final document = databases.createDocument(
documentId: appwrite.ID.unique(),
databaseId: ApiConstants.INFO_Q_DATABASE_ID,
collectionId: ApiConstants.MANUAL_Q_COLLECTION_ID,
data: {
"DisplayText": announcement.displayText,
"AudioFileNames": audioFileNames,
"ScheduledTime": scheduledTime.toIso8601String(),
"SessionID": sessionID,
}
);
print("Queued manual announcement: ${announcement.shortName}"); print("Queued manual announcement: ${announcement.shortName}");
} else if (announcement is AnnouncementQueueEntry) { } else if (announcement.runtimeType == DestinationAnnouncementEntry) {
announcement as DestinationAnnouncementEntry;
print("Queing to DestinationAnnouncementEntry");
// 5 sedonds in the future
DateTime scheduledTime = (getNow()).add(Duration(seconds: 2));
final document = databases.createDocument(
documentId: appwrite.ID.unique(),
databaseId: ApiConstants.INFO_Q_DATABASE_ID,
collectionId: ApiConstants.DEST_Q_COLLECTION_ID,
data: {
"RouteNumber": announcement.routeVariant.busRoute.routeNumber,
"RouteVariantIndex": announcement.routeVariant.routeVariant,
"ScheduledTime": scheduledTime.toIso8601String(),
"SessionID": sessionID,
}
);
print("Queued manual announcement: ${announcement.shortName} (server)");
} }
@@ -390,9 +565,46 @@ class LiveInformation {
return; return;
} }
List<AnnouncementQueueEntry> queue = [];
final databases = appwrite.Databases(auth.client); final databases = appwrite.Databases(auth.client);
// Pull the information queue
{
final manual_q = await databases.listDocuments(
databaseId: ApiConstants.INFO_Q_DATABASE_ID,
collectionId: ApiConstants.INFORMATION_Q_COLLECTION_ID,
queries: [
appwrite.Query.search("SessionID", sessionID),
appwrite.Query.limit(25),
appwrite.Query.offset(0),
appwrite.Query.orderDesc('\$createdAt')
]
);
for (models.Document doc in manual_q.documents) {
int index = doc.data['ManualAnnouncementIndex'];
InformationAnnouncementEntry announcement_clone =
InformationAnnouncementEntry(
shortName: manualAnnouncements[index].shortName,
informationText: manualAnnouncements[index].displayText,
audioSources: manualAnnouncements[index].audioSources,
scheduledTime: doc.data["ScheduledTime"] != null
? DateTime.parse(doc.data["ScheduledTime"])
: null,
timestamp: DateTime.parse(doc.$createdAt),
sendToServer: false,
);
// sort the queue by timestamp, so the oldest announcements are at the front
queue.add(announcement_clone);
}
}
// Pull the manual queue // Pull the manual queue
{
final manual_q = await databases.listDocuments( final manual_q = await databases.listDocuments(
databaseId: ApiConstants.INFO_Q_DATABASE_ID, databaseId: ApiConstants.INFO_Q_DATABASE_ID,
collectionId: ApiConstants.MANUAL_Q_COLLECTION_ID, collectionId: ApiConstants.MANUAL_Q_COLLECTION_ID,
@@ -404,31 +616,78 @@ class LiveInformation {
] ]
); );
List<AnnouncementQueueEntry> queue = [];
for (models.Document doc in manual_q.documents) { for (models.Document doc in manual_q.documents) {
int index = doc.data['ManualAnnouncementIndex'];
InformationAnnouncementEntry announcement_clone = InformationAnnouncementEntry( List<AudioWrapperSource> audioSources = [];
shortName: manualAnnouncements[index].shortName,
informationText: manualAnnouncements[index].displayText, for (String filename in doc.data["AudioFileNames"]) {
audioSources: manualAnnouncements[index].audioSources, audioSources.add(AudioWrapperByteSource(announcementCache[filename]));
scheduledTime: doc.data["ScheduledTime"] != null ? DateTime.parse(doc.data["ScheduledTime"]) : null, }
timestamp: DateTime.parse(doc.$createdAt),
ManualAnnouncementEntry announcement_clone =
ManualAnnouncementEntry(
sendToServer: false, sendToServer: false,
shortName: "",
informationText: doc.data["DisplayText"],
audioSources: [...audioSources],
scheduledTime: doc.data["ScheduledTime"] != null
? DateTime.parse(doc.data["ScheduledTime"])
: null,
); );
// sort the queue by timestamp, so the oldest announcements are at the front // sort the queue by timestamp, so the oldest announcements are at the front
queue.add(announcement_clone);
}
}
// pull the destination queue
{
final dest_q = await databases.listDocuments(
databaseId: ApiConstants.INFO_Q_DATABASE_ID,
collectionId: ApiConstants.DEST_Q_COLLECTION_ID,
queries: [
appwrite.Query.search("SessionID", sessionID),
appwrite.Query.limit(25),
appwrite.Query.offset(0),
appwrite.Query.orderDesc('\$createdAt')
]
);
for (models.Document doc in dest_q.documents) {
BusRoute? route = busSequences.routes[doc.data["RouteNumber"]];
BusRouteVariant? routeVariant = route!.routeVariants[doc.data["RouteVariantIndex"]];
DestinationAnnouncementEntry announcement_clone =
DestinationAnnouncementEntry(
routeVariant: routeVariant!,
scheduledTime: doc.data["ScheduledTime"] != null
? DateTime.parse(doc.data["ScheduledTime"])
: null,
timestamp: DateTime.parse(doc.$createdAt),
sendToServer: false,
audioSources: [],
);
// sort the queue by timestamp, so the oldest announcements are at the front
queue.add(announcement_clone); queue.add(announcement_clone);
} }
}
for (AnnouncementQueueEntry entry in queue) { for (AnnouncementQueueEntry entry in queue) {
// Dont queue announcements that are older than now
if (entry.scheduledTime != null && entry.scheduledTime!.isBefore(await getNow())) {
continue;
}
queueAnnouncement(entry); queueAnnouncement(entry);
} }
@@ -436,10 +695,13 @@ class LiveInformation {
appwrite.RealtimeSubscription? information_q_subscription;
appwrite.RealtimeSubscription? manual_q_subscription; appwrite.RealtimeSubscription? manual_q_subscription;
appwrite.RealtimeSubscription? destination_q_subscription;
Future<void> setupRealtime() async { Future<void> setupRealtime() async {
if (manual_q_subscription != null) { if (information_q_subscription != null) {
return; return;
} }
@@ -450,6 +712,15 @@ class LiveInformation {
// Websocket // Websocket
appwrite.Realtime realtime = appwrite.Realtime(auth.client); appwrite.Realtime realtime = appwrite.Realtime(auth.client);
information_q_subscription = realtime.subscribe(
['databases.${ApiConstants.INFO_Q_DATABASE_ID}.collections.${ApiConstants.INFORMATION_Q_COLLECTION_ID}.documents'],
);
information_q_subscription?.stream.listen((event) {
print("Manual queue entry added");
pullQueue();
});
manual_q_subscription = realtime.subscribe( manual_q_subscription = realtime.subscribe(
['databases.${ApiConstants.INFO_Q_DATABASE_ID}.collections.${ApiConstants.MANUAL_Q_COLLECTION_ID}.documents'], ['databases.${ApiConstants.INFO_Q_DATABASE_ID}.collections.${ApiConstants.MANUAL_Q_COLLECTION_ID}.documents'],
); );
@@ -459,12 +730,25 @@ class LiveInformation {
pullQueue(); pullQueue();
}); });
destination_q_subscription = realtime.subscribe(
['databases.${ApiConstants.INFO_Q_DATABASE_ID}.collections.${ApiConstants.DEST_Q_COLLECTION_ID}.documents'],
);
destination_q_subscription?.stream.listen((event) {
print("Destination queue entry added");
pullQueue();
});
print("Subscribed to servers"); print("Subscribed to servers");
await Future.delayed(Duration(seconds: 90)); await Future.delayed(Duration(seconds: 90));
information_q_subscription?.close();
information_q_subscription = null;
manual_q_subscription?.close(); manual_q_subscription?.close();
manual_q_subscription = null; manual_q_subscription = null;
destination_q_subscription?.close();
destination_q_subscription = null;
setupRealtime(); setupRealtime();
} }
@@ -483,17 +767,37 @@ class AnnouncementQueueEntry {
AnnouncementQueueEntry({required this.displayText, required this.audioSources, this.sendToServer = true, this.scheduledTime, this.timestamp}); AnnouncementQueueEntry({required this.displayText, required this.audioSources, this.sendToServer = true, this.scheduledTime, this.timestamp});
} }
class ManualAnnouncementEntry extends AnnouncementQueueEntry { class NamedAnnouncementQueueEntry extends AnnouncementQueueEntry {
final String shortName; final String shortName;
ManualAnnouncementEntry({ NamedAnnouncementQueueEntry({
required this.shortName, required this.shortName,
required String displayText,
required List<AudioWrapperSource> audioSources,
DateTime? scheduledTime,
DateTime? timestamp,
bool sendToServer = true,
}) : super(
displayText: displayText,
audioSources: audioSources,
sendToServer: sendToServer,
scheduledTime: scheduledTime,
timestamp: timestamp,
);
}
class ManualAnnouncementEntry extends NamedAnnouncementQueueEntry {
ManualAnnouncementEntry({
required String shortName,
required String informationText, required String informationText,
required List<AudioWrapperSource> audioSources, required List<AudioWrapperSource> audioSources,
DateTime? scheduledTime, DateTime? scheduledTime,
DateTime? timestamp, DateTime? timestamp,
bool sendToServer = true, bool sendToServer = true,
}) : super( }) : super(
shortName: shortName,
displayText: informationText, displayText: informationText,
audioSources: audioSources, audioSources: audioSources,
sendToServer: sendToServer, sendToServer: sendToServer,
@@ -502,17 +806,18 @@ class ManualAnnouncementEntry extends AnnouncementQueueEntry {
); );
} }
class InformationAnnouncementEntry extends AnnouncementQueueEntry { class InformationAnnouncementEntry extends NamedAnnouncementQueueEntry {
final String shortName;
InformationAnnouncementEntry({ InformationAnnouncementEntry({
required this.shortName, required String shortName,
required String informationText, required String informationText,
required List<AudioWrapperSource> audioSources, required List<AudioWrapperSource> audioSources,
DateTime? scheduledTime, DateTime? scheduledTime,
DateTime? timestamp, DateTime? timestamp,
bool sendToServer = true, bool sendToServer = true,
}) : super( }) : super(
shortName: shortName,
displayText: informationText, displayText: informationText,
audioSources: audioSources, audioSources: audioSources,
sendToServer: sendToServer, sendToServer: sendToServer,
@@ -521,4 +826,25 @@ class InformationAnnouncementEntry extends AnnouncementQueueEntry {
); );
} }
class DestinationAnnouncementEntry extends NamedAnnouncementQueueEntry {
final BusRouteVariant routeVariant;
DestinationAnnouncementEntry({
required this.routeVariant,
required List<AudioWrapperSource> audioSources,
DateTime? scheduledTime,
DateTime? timestamp,
bool sendToServer = true,
}) : super(
shortName: "Destination",
displayText: "${routeVariant.busRoute.routeNumber} to ${routeVariant.busStops.last.formattedStopName}",
audioSources: audioSources,
sendToServer: sendToServer,
scheduledTime: scheduledTime,
timestamp: timestamp,
);
}
var abs = (int value) => value < 0 ? -value : value; var abs = (int value) => value < 0 ? -value : value;

View File

@@ -72,6 +72,7 @@ class _DelegateBuilderState<T> extends State<DelegateBuilder<T>> {
}); });
}); });
} }
@override @override

View File

@@ -416,6 +416,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
ntp:
dependency: "direct main"
description:
name: ntp
sha256: "198db73e5059b334b50dbe8c626011c26576778ee9fc53f4c55c1d89d08ed2d2"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
package_info_plus: package_info_plus:
dependency: transitive dependency: transitive
description: description:

View File

@@ -43,7 +43,7 @@ dependencies:
appwrite: ^11.0.1 appwrite: ^11.0.1
shared_preferences: ^2.2.2 shared_preferences: ^2.2.2
url_launcher: ^6.2.2 url_launcher: ^6.2.2
ntp: ^2.0.0
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.