near final

This commit is contained in:
ImBenji
2024-03-02 18:07:05 +00:00
parent 67e1cd3530
commit 429eb4ad5f
29 changed files with 7870 additions and 1345 deletions

View File

@@ -0,0 +1,330 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:bus_infotainment/audio_cache.dart';
import 'package:bus_infotainment/backend/live_information.dart';
import 'package:bus_infotainment/tfl_datasets.dart';
import 'package:bus_infotainment/utils/audio%20wrapper.dart';
import 'package:bus_infotainment/utils/delegates.dart';
import 'info_module.dart';
class AnnouncementModule extends InfoModule {
AnnouncementCache announcementCache = AnnouncementCache();
// Constructor
AnnouncementModule() {
refreshTimer();
}
// Queue
List<AnnouncementQueueEntry> queue = [];
AnnouncementQueueEntry? currentAnnouncement;
DateTime? currentAnnouncementTimeStamp;
String defaultText = "*** NO MESSAGE ***";
bool isPlaying = false;
// Audio
AudioWrapper audioPlayer = AudioWrapper();
// Events
final EventDelegate<AnnouncementQueueEntry> onAnnouncement = EventDelegate();
// Timer
Timer refreshTimer() => Timer.periodic(const Duration(milliseconds: 200), (timer) async {
if (!isPlaying) {
if (queue.isNotEmpty) {
isPlaying = true;
AnnouncementQueueEntry nextAnnouncement = queue.first;
bool proceeding = await _internalAccountForInconsistentTime(
announcement: nextAnnouncement,
timerInterval: const Duration(milliseconds: 200),
callback: () {
queue.removeAt(0);
print("Announcement proceeding");
}
);
if (!proceeding) {
isPlaying = false;
print("Announcement not proceeding");
print("Queue: ${queue.length}");
return;
}
currentAnnouncement = nextAnnouncement;
currentAnnouncementTimeStamp = liveInformation.syncedTimeModule.Now();
onAnnouncement.trigger(currentAnnouncement!);
if (currentAnnouncement!.audioSources.isNotEmpty) {
try {
for (AudioWrapperSource source in currentAnnouncement!.audioSources) {
await audioPlayer.loadSource(source);
Duration? duration = await audioPlayer.play();
await Future.delayed(duration!);
if (currentAnnouncement?.audioSources.last != source) {
await Future.delayed(const Duration(milliseconds: 100));
}
}
audioPlayer.stop();
} catch (e) {
// Do nothing
print("Error playing announcement: $e");
}
} else {
if (queue.isNotEmpty) {
await Future.delayed(const Duration(seconds: 5));
}
}
isPlaying = false;
}
}
});
// Will call the callback function if the announcement will be proceeding
Future<bool> _internalAccountForInconsistentTime({
required AnnouncementQueueEntry announcement,
required Duration timerInterval,
required Function() callback
}) async {
DateTime now = liveInformation.syncedTimeModule.Now();
if (announcement.scheduledTime != null) {
if (now.isAfter(announcement.scheduledTime!)) {
callback();
return true;
}
int milisecondDifference = abs(now.millisecondsSinceEpoch - announcement.scheduledTime!.millisecondsSinceEpoch);
if (milisecondDifference <= timerInterval.inMilliseconds) {
// Account for the time lost by the periodic timer
callback();
await Future.delayed(Duration(milliseconds: timerInterval.inMilliseconds - milisecondDifference));
return true;
} else {
return false;
}
} else {
callback();
return true;
}
}
// Configuration
int get defaultAnnouncementDelay => liveInformation.auth.isAuthenticated() ? 2 : 0;
// Methods
Future<void> queueAnnounceByAudioName({
required String displayText,
List<String> audioNames = const [],
DateTime? scheduledTime = null,
bool sendToServer = true
}) async {
if (sendToServer) {
scheduledTime ??= liveInformation.syncedTimeModule.Now().add(Duration(seconds: defaultAnnouncementDelay));
String audioNamesString = "";
for (var audioName in audioNames) {
audioNamesString += "\"$audioName\" ";
}
liveInformation.commandModule.executeCommand(
"announce manual \"$displayText\" ${audioNamesString} ${scheduledTime?.millisecondsSinceEpoch ?? ""}"
);
return;
}
// Cache the announcements
await announcementCache.loadAnnouncements(audioNames);
List<AudioWrapperSource> sources = [];
print("Audio names: $audioNames");
for (var audioName in audioNames) {
Uint8List? audioData = announcementCache[audioName];
if (audioData == null) {
continue;
}
sources.add(AudioWrapperByteSource(audioData));
}
queue.add(
AnnouncementQueueEntry(
displayText: displayText,
audioSources: sources,
scheduledTime: scheduledTime
)
);
}
void queueAnnounementByInfoIndex({
int infoIndex = -1,
DateTime? scheduledTime = null,
bool sendToServer = true
}) {
if (sendToServer) {
scheduledTime ??= liveInformation.syncedTimeModule.Now().add(Duration(seconds: defaultAnnouncementDelay));
liveInformation.commandModule.executeCommand(
"announce info $infoIndex ${scheduledTime?.millisecondsSinceEpoch ?? ""}"
);
return;
}
NamedAnnouncementQueueEntry clone = NamedAnnouncementQueueEntry(
shortName: manualAnnouncements[infoIndex].shortName,
displayText: manualAnnouncements[infoIndex].displayText,
audioSources: manualAnnouncements[infoIndex].audioSources,
scheduledTime: scheduledTime
);
queue.add(clone);
}
Future<void> queueAnnouncementByRouteVariant({
required BusRouteVariant routeVariant,
DateTime? scheduledTime = null,
bool sendToServer = true
}) async {
if (sendToServer) {
scheduledTime ??= liveInformation.syncedTimeModule.Now().add(Duration(seconds: defaultAnnouncementDelay));
liveInformation.commandModule.executeCommand(
"announce dest \"${routeVariant.busRoute.routeNumber}\" ${routeVariant.busRoute.routeVariants.values.toList().indexOf(routeVariant)} ${scheduledTime?.millisecondsSinceEpoch ?? ""}"
);
return;
}
String routeNumber = routeVariant.busRoute.routeNumber;
String destination = routeVariant.destination!.destination;
String audioRoute = "R_${routeVariant.busRoute.routeNumber}_001.mp3";
await announcementCache.loadAnnouncements([audioRoute]);
AudioWrapperSource sourceRoute = AudioWrapperByteSource(announcementCache[audioRoute]!);
AudioWrapperSource sourceDestination = AudioWrapperByteSource(await routeVariant.destination!.getAudioBytes());
AnnouncementQueueEntry announcement = AnnouncementQueueEntry(
displayText: "$routeNumber to $destination",
audioSources: [sourceRoute, AudioWrapperAssetSource("audio/to_destination.wav"), sourceDestination],
scheduledTime: scheduledTime
);
queue.add(announcement);
}
// Constants
final List<NamedAnnouncementQueueEntry> manualAnnouncements = [
NamedAnnouncementQueueEntry(
shortName: "Driver Change",
displayText: "Driver Change",
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/driverchange.mp3")],
),
NamedAnnouncementQueueEntry(
shortName: "No Standing Upr Deck",
displayText: "No standing on the upper deck",
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/nostanding.mp3")],
),
NamedAnnouncementQueueEntry(
shortName: "Face Covering",
displayText: "Please wear a face covering!",
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/facecovering.mp3")],
),
NamedAnnouncementQueueEntry(
shortName: "Seats Upstairs",
displayText: "Seats are available upstairs",
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/seatsupstairs.mp3")],
),
NamedAnnouncementQueueEntry(
shortName: "Bus Terminates Here",
displayText: "Bus terminates here. Please take your belongings with you",
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/busterminateshere.mp3")],
),
NamedAnnouncementQueueEntry(
shortName: "Bus On Diversion",
displayText: "Bus on diversion. Please listen for further announcements",
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/busondiversion.mp3")],
),
NamedAnnouncementQueueEntry(
shortName: "Destination Change",
displayText: "Destination Changed - please listen for further instructions",
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/destinationchange.mp3")],
),
NamedAnnouncementQueueEntry(
shortName: "Wheelchair Space",
displayText: "Wheelchair space requested",
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/wheelchairspace1.mp3")],
),
NamedAnnouncementQueueEntry(
shortName: "Move Down The Bus",
displayText: "Please move down the bus",
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/movedownthebus.mp3")],
),
NamedAnnouncementQueueEntry(
shortName: "Next Stop Closed",
displayText: "The next bus stop is closed",
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/nextstopclosed.wav")],
),
NamedAnnouncementQueueEntry(
shortName: "CCTV In Operation",
displayText: "CCTV is in operation on this bus",
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/cctvoperation.mp3")],
),
NamedAnnouncementQueueEntry(
shortName: "Safe Door Opening",
displayText: "Driver will open the doors when it is safe to do so",
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/safedooropening.mp3")],
),
NamedAnnouncementQueueEntry(
shortName: "Buggy Safety",
displayText: "For your child's safety, please remain with your buggy",
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/buggysafety.mp3")],
),
NamedAnnouncementQueueEntry(
shortName: "Wheelchair Space 2",
displayText: "Wheelchair priority space required",
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/wheelchairspace2.mp3")],
),
NamedAnnouncementQueueEntry(
shortName: "Service Regulation",
displayText: "Regulating service - please listen for further information",
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/serviceregulation.mp3")],
),
NamedAnnouncementQueueEntry(
shortName: "Bus Ready To Depart",
displayText: "This bus is ready to depart",
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/readytodepart.mp3")],
),
];
}

View File

@@ -0,0 +1,224 @@
import 'dart:convert';
import 'package:bus_infotainment/auth/api_constants.dart';
import 'package:bus_infotainment/backend/live_information.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:appwrite/appwrite.dart' as appwrite;
import 'package:appwrite/models.dart' as models;
import 'package:uuid/uuid.dart';
import '../../auth/auth_api.dart';
import 'info_module.dart';
class CommandModule extends InfoModule {
final String sessionID;
late final String clientID;
List<CommandInfo> _commandHistory = [];
get commandHistory => _commandHistory;
EventDelegate<CommandInfo> onCommandReceived = EventDelegate();
CommandModule(this.sessionID){
// generate a random client ID
var uuid = Uuid();
clientID = uuid.v4();
if (liveInformation.auth.isAuthenticated()){
print("Auth is authenticated");
_setupListener();
} else {
print("Auth is not authenticated");
liveInformation.auth.onLogin.addListener((value) {
_setupListener();
});
}
}
// Will execute the command an event which is triggered when a response is received
Future<EventDelegate> executeCommand(String command) async {
EventDelegate<String> delegate = EventDelegate();
final client = liveInformation.auth.client;
final databases = appwrite.Databases(client);
if (liveInformation.auth.status == AuthStatus.AUTHENTICATED) {
final document = await databases.createDocument(
databaseId: ApiConstants.INFO_Q_DATABASE_ID,
collectionId: ApiConstants.COMMANDS_COLLECTION_ID,
documentId: appwrite.ID.unique(),
data: {
"session_id": sessionID,
"command": command,
"client_id": clientID,
}
);
}
_onCommandReceived(CommandInfo(command, clientID));
return delegate;
}
Future<void> _onCommandReceived(CommandInfo commandInfo) async {
commandHistory.add(commandInfo);
onCommandReceived.trigger(commandInfo);
print("Received command: ${commandInfo.command}");
List<String> commandParts = splitCommand(commandInfo.command);
String command = commandParts[0];
List<String> args = commandParts.sublist(1);
if (command == "Response:") {
}
else if (command == "announce") {
final displayText = args[1];
if (args[0] == "manual") {
// announce manual <DisplayText> <AudioFileName>... <ScheduledTime>
List<String> audioFileNames = args.sublist(2);
try {
if (int.parse(audioFileNames.last) != null) {
audioFileNames.removeLast();
}
} catch (e) {}
DateTime scheduledTime = LiveInformation().syncedTimeModule.Now().add(Duration(seconds: 1));
try {
if (int.parse(args.last) != null) {
scheduledTime = DateTime.fromMillisecondsSinceEpoch(int.parse(args.last));
}
} catch (e) {}
liveInformation.announcementModule.queueAnnounceByAudioName(
displayText: displayText,
audioNames: audioFileNames,
scheduledTime: scheduledTime,
sendToServer: false
);
}
else if (args[0] == "info") {
int InfoIndex = int.parse(args[1]);
DateTime scheduledTime = LiveInformation().syncedTimeModule.Now();
try {
if (int.parse(args.last) != null) {
scheduledTime = DateTime.fromMillisecondsSinceEpoch(int.parse(args.last));
}
} catch (e) {}
liveInformation.announcementModule.queueAnnounementByInfoIndex(
infoIndex: InfoIndex,
scheduledTime: scheduledTime,
sendToServer: false
);
}
else if (args[0].startsWith("dest")) {
// announce destination <RouteNumber> <RouteVariantIndex> <ScheduledTime>
String routeNumber = args[1];
int routeVariantIndex = int.parse(args[2]);
DateTime scheduledTime = LiveInformation().syncedTimeModule.Now();
try {
if (int.parse(args.last) != null) {
scheduledTime = DateTime.fromMillisecondsSinceEpoch(int.parse(args.last));
}
} catch (e) {}
BusRoute route = LiveInformation().busSequences.routes[routeNumber]!;
BusRouteVariant routeVariant = route.routeVariants.values.toList()[routeVariantIndex];
liveInformation.announcementModule.queueAnnouncementByRouteVariant(
routeVariant: routeVariant,
scheduledTime: scheduledTime,
sendToServer: false
);
}
}
else if (command == "setroute") {
// setroute <RouteNumber> <RouteVariantIndex>
LiveInformation liveInformation = LiveInformation();
String routeNumber = args[0];
int routeVariantIndex = int.parse(args[1]);
BusRoute route = liveInformation.busSequences.routes[routeNumber]!;
BusRouteVariant routeVariant = route.routeVariants.values.toList()[routeVariantIndex];
liveInformation.setRouteVariant_Internal(
routeVariant
);
executeCommand("Response: v \"Client $clientID set its route to ($routeNumber to ${routeVariant.busStops.last.formattedStopName})\"");
}
}
appwrite.RealtimeSubscription? _subscription;
Future<void> _setupListener() async {
if (_subscription != null) {
return;
}
final realtime = appwrite.Realtime(LiveInformation().auth.client);
_subscription = realtime.subscribe(
['databases.${ApiConstants.INFO_Q_DATABASE_ID}.collections.${ApiConstants.COMMANDS_COLLECTION_ID}.documents']
);
_subscription!.stream.listen((event) {
print(jsonEncode(event.payload));
// Only do something if the document was created or updated
if (!(event.events.first.contains("create") || event.events.first.contains("update"))) {
return;
}
final commandInfo = CommandInfo(event.payload['command'], event.payload['client_id']);
if (commandInfo.clientID != clientID) {
_onCommandReceived(commandInfo);
}
});
print("Listening for commands");
await Future.delayed(Duration(seconds: 90));
await _subscription!.close();
_subscription = null;
_setupListener();
}
}
class CommandInfo {
final String command;
final String clientID;
CommandInfo(this.command, this.clientID);
}
List<String> splitCommand(String command) {
var regex = RegExp(r'([^\s"]+)|"([^"]*)"');
var matches = regex.allMatches(command);
return matches.map((match) => match.group(0)!.replaceAll('"', '')).toList();
}

View File

@@ -0,0 +1,8 @@
import 'package:bus_infotainment/backend/live_information.dart';
abstract class InfoModule {
LiveInformation get liveInformation => LiveInformation();
}

View File

@@ -0,0 +1,36 @@
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'info_module.dart';
class SyncedTimeModule extends InfoModule {
int timeOffset = -1;
DateTime lastUpdate = DateTime.now().add(const Duration(seconds: -15));
SyncedTimeModule() {
refreshTimer();
}
Timer refreshTimer() => Timer.periodic(const Duration(seconds: 10), (timer) async {
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']);
timeOffset = time.millisecondsSinceEpoch - DateTime.now().millisecondsSinceEpoch;
lastUpdate = DateTime.now();
print("Time offset: $timeOffset");
} else {
print("Failed to get time from worldtimeapi.org");
}
});
DateTime Now() {
if (timeOffset == -1) {
return DateTime.now();
}
return DateTime.now().add(Duration(milliseconds: timeOffset));
}
}

View File

@@ -0,0 +1,199 @@
import 'dart:async';
import 'dart:math';
import 'dart:typed_data';
import 'package:bus_infotainment/backend/live_information.dart';
import 'package:bus_infotainment/backend/modules/info_module.dart';
import 'package:bus_infotainment/tfl_datasets.dart';
import 'package:bus_infotainment/utils/OrdinanceSurveyUtils.dart';
import 'package:bus_infotainment/utils/audio%20wrapper.dart';
import 'package:geolocator/geolocator.dart';
import 'package:vector_math/vector_math.dart';
class TrackerModule extends InfoModule {
TrackerModule() {
locationStream();
Geolocator.getLastKnownPosition().then((Position? position) {
this._position = position;
updateNearestStop();
});
liveInformation.routeVariantDelegate.addListener((routeVariant) {
print("Route variant changed");
updateNearestStop();
});
}
Position? _position;
Position? get position => _position;
StreamSubscription<Position> locationStream() => Geolocator.getPositionStream(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.high,
distanceFilter: 1,
)
).listen((Position position) {
if (position == null) {
return;
}
this._position = position;
updateNearestStop();
});
Timer refreshTimer() => Timer.periodic(Duration(seconds: 1), (timer) async {
_position = await Geolocator.getCurrentPosition();
});
BusRouteStop? nearestStop;
bool hasArrived = false;
Future<void> updateNearestStop() async {
if (liveInformation.getRouteVariant() == null) {
return;
}
// Get the closest stop
BusRouteStop closestStop = liveInformation.getRouteVariant()!.busStops.first;
double closestDistance = OSGrid
.toNorthingEasting(_position!.latitude, _position!.longitude)
.distanceTo(Vector2(closestStop.easting.toDouble(), closestStop.northing.toDouble()));
for (BusRouteStop stop in liveInformation.getRouteVariant()!.busStops) {
double distance = OSGrid
.toNorthingEasting(_position!.latitude, _position!.longitude)
.distanceTo(Vector2(stop.easting.toDouble(), stop.northing.toDouble()));
if (distance < closestDistance) {
closestStop = stop;
closestDistance = distance;
}
}
double relativeDistance = _calculateRelativeDistance(closestStop, _position!.latitude, _position!.longitude);
if (relativeDistance < -10) {
print("Closest stop is behind us: ${closestStop.formattedStopName}");
print("Relative distance: $relativeDistance");
int stopIndex = liveInformation.getRouteVariant()!.busStops.indexOf(closestStop);
closestStop = liveInformation.getRouteVariant()!.busStops[stopIndex + 1];
print("Closest stop is now: ${closestStop.formattedStopName}");
} else {
print("Closest stop is in front of us: ${closestStop.formattedStopName}");
}
bool preExisting = true;
if (nearestStop != closestStop) {
nearestStop = closestStop;
hasArrived = false;
preExisting = false;
}
print("Closest stop is the same as before");
double distance = OSGrid
.toNorthingEasting(_position!.latitude, _position!.longitude)
.distanceTo(Vector2(nearestStop!.easting.toDouble(), nearestStop!.northing.toDouble()));
// get the speed in mph
double speed = _position!.speed * 2.23694;
print("Speed: $speed");
Duration? duration;
{
// Testing some audio stuff
Uint8List? audioBytes = liveInformation.announcementModule.announcementCache[nearestStop!.getAudioFileName()];
AudioWrapperByteSource audioSource = AudioWrapperByteSource(audioBytes!);
AudioWrapper audio = AudioWrapper();
await audio.loadSource(audioSource);
duration = await audio.getDuration();
print("Duration of audio: $duration");
}
// get the estimated distance travelled in 5 seconds, in meters
double distanceTravelled = speed * (3 + duration!.inSeconds);
// adjust for the it takes to send the announcement to other devices
distance -= distanceTravelled;
// get the time to the stop in seconds
double timeToStop = distance / speed;
print("Distance to stop: $distance");
print("Time to stop: $timeToStop");
int secondsBefore = 7;
print("Seconds before: $secondsBefore");
if ((timeToStop < secondsBefore ) && !hasArrived && relativeDistance > 0) {
print("We are at the stop");
hasArrived = true;
liveInformation.announcementModule.queueAnnounceByAudioName(
displayText: "${nearestStop!.formattedStopName}",
audioNames: [
// "A_NEXT_STOP_001.mp3",
nearestStop!.getAudioFileName()
],
sendToServer: true
);
}
if (!hasArrived && !preExisting) {
liveInformation.announcementModule.queueAnnounceByAudioName(
displayText: "${closestStop.formattedStopName}",
audioNames: [],
sendToServer: true
);
}
print("Closest stop: ${closestStop.formattedStopName} in ${closestDistance.round()} meters");
}
}
double _calculateRelativeDistance(BusRouteStop stop, double latitude, double longitude) {
List<double> toLatLong = OSGrid.toLatLong(stop.northing.toDouble(), stop.easting.toDouble());
Vector2 stopPoint = OSGrid.toNorthingEasting(toLatLong[0], toLatLong[1]);
Vector2 currentPoint = OSGrid.toNorthingEasting(latitude, longitude);
// calculate the heading from the current point to the stop point
// 0 degrees is north, 90 degrees is east, 180 degrees is south, 270 degrees is west
double toHeading = degrees(atan2(stopPoint.x - currentPoint.x, stopPoint.y - currentPoint.y));
toHeading = (toHeading + 360) % 360;
// get the dot product of the heading and the stop heading
double dotProduct = cos(radians(toHeading)) * cos(radians(stop.heading.toDouble())) + sin(radians(toHeading)) * sin(radians(stop.heading.toDouble()));
return (dotProduct.sign) * _calculateDistance(latitude, longitude, toLatLong[0], toLatLong[1]);
}
double _calculateDistance(double lat1, double lon1, double lat2, double lon2) {
// Convert to eastings and northings
Vector2 point1 = OSGrid.toNorthingEasting(lat1, lon1);
Vector2 point2 = OSGrid.toNorthingEasting(lat2, lon2);
return point1.distanceTo(point2);
}