With our UI in place, we're now diving into the most exciting part - the logic. This phase might be less visually stimulating since it's more theoretical, but it's intriguing to see how the logic comes together.
Understanding how we'll manage our data is crucial, especially considering we have multiple threads. Passing data around without proper security isn't an option.
Our solution is a store - a struct that holds the data accessible from all threads. We'll use a mutex for safe access, ensuring only one thread can manipulate the data at any given time. Data returned will always be a copy, ensuring no unintended modifications or nullptr manipulation.
Here's a snippet:
class Store {
std::shared_mutex mutex;
std::vector<Bus> buses;
public:
std::vector<Bus> get_buses() {
std::shared_lock lock(mutex);
return buses;
}
void set_buses(std::vector<Bus> buses) {
std::unique_lock lock(mutex);
this->buses = buses;
}
}
This design as the advantage of being simple and easy to understand. The only downside is that we need to copy the data when we want to read it. But it's not a big deal as we don't have a lot of data and we access it at very low frequency.
We're setting up a side server to write data to /tmp/tec-gps.socket
. This employs UDS for communication between processes. The client side will read and parse this data.
Pseudo-code :
Server:
Client:
It's not rocket science and it's pretty straight foward.
For static data like bus stops and lines, we'll use a SQLite database. While a remote centralized database is usually better, it's not necessary for this project.
Due to some issues with my WSL, I had to compile SQLite3 directly into the project.
afficheurs (tr: display) : (id, text, line number)
Used to display text on the screen. The text is what should be printed on the screen and the line number is the line where it should be printed.
routes : (route_id, route_short_name, route_long_name)
Used to store the route. The route_short_name is the number of the route and the route_long_name is the name of the route.
shape: (shape_id, lat, lon, seq)
Used to store the shape of the route. The shape_id is the id of the route, the lat and lon are the coordinates of the point and the seq (sequence) is the order of the point in the route.
stops (tr: stop) : (stop_id, stop_name, lat, lon)
Used to store the bus stop. The latitude and longitude are used to display the bus stop on the map and do computation.
stops_times: (trip_id, arrival_time, stop_id)
Used to store the time of the bus stop. The trip_id is the id of the route, the arrival_time is the time when the bus will arrive at the stop, the departure_time is the time when the bus will leave the stop and the stop_sequence is the order of the stop in the route.
trips: (trip_id, route_id, shape_id, afficheur_id)
Used to store the trip. The trip_id is the id of the trip, the route_id is the id of the route, the shape_id is the id of the shape and the afficheur_id is the id of the display that will display the text of the trip.
We'll use a simple wrapper around SQLite3 for database access. This wrapper is thread-safe, thanks to a mutex.
Example code for the Database class:
class Database {
sqlite3 *db;
std::mutex mutex;
public:
Database(std::string path) {
sqlite3_open(path.c_str(), &db);
}
~Database() {
sqlite3_close(db);
}
std::vector<std::vector<std::string>> execute(std::string query) {
std::lock_guard lock(mutex);
sqlite3_stmt *stmt;
sqlite3_prepare_v2(db, query.c_str(), -1, &stmt, NULL);
std::vector<std::vector<std::string>> result;
int rc = sqlite3_step(stmt);
while (rc != SQLITE_DONE) {
if (rc == SQLITE_ROW) {
std::vector<std::string> row;
int col_count = sqlite3_column_count(stmt);
for (int i = 0; i < col_count; i++) {
row.push_back((char *) sqlite3_column_text(stmt, i));
}
result.push_back(row);
}
rc = sqlite3_step(stmt);
}
sqlite3_finalize(stmt);
return result;
}
};
This is a very simple but it's just to show the idea. We could have used a library like sqlite_orm but it's not necessary for this project.
A separate thread will manage the display rotation, reading data from the store and updating the display.
Selecting a trip will retrieve relevant data from the DB and store it in the store, also updating the display.