diff --git a/tutorials/application_design/c++11/common.hpp b/tutorials/application_design/c++11/common.hpp index e244dc00f..1efbe5baf 100644 --- a/tutorials/application_design/c++11/common.hpp +++ b/tutorials/application_design/c++11/common.hpp @@ -20,7 +20,7 @@ using CoordSequence = rti::core::bounded_sequence; -namespace { // Coord namespace +// Coord namespace std::string to_string(const Coord &coord) { @@ -29,7 +29,7 @@ std::string to_string(const Coord &coord) return ss.str(); } -} // namespace +// namespace namespace rti::core { // bounded_sequence namespace @@ -60,11 +60,6 @@ static std::mt19937 gen { rd() }; }; // namespace details -void set_random_seed(unsigned seed) -{ - details::gen.seed(seed); -} - double random_range(double min, double max) { return std::uniform_real_distribution<>(min, max)(details::gen); diff --git a/tutorials/application_design/c++11/publisher.cxx b/tutorials/application_design/c++11/publisher.cxx index 7555c650e..7da8ca100 100644 --- a/tutorials/application_design/c++11/publisher.cxx +++ b/tutorials/application_design/c++11/publisher.cxx @@ -23,13 +23,17 @@ class PublisherSimulation { explicit PublisherSimulation( dds::pub::DataWriter metrics_writer, dds::pub::DataWriter transit_writer) - : metrics_writer_(metrics_writer), - transit_writer_(transit_writer), - vehicle_vin_(utils::new_vin()), - vehicle_fuel_(100.0), - vehicle_route_(utils::new_route()), - vehicle_position_(vehicle_route_[0]) + : metrics_writer_(metrics_writer), transit_writer_(transit_writer) { + auto new_vin = utils::new_vin(); + auto new_route = utils::new_route(); + + metrics_.vehicle_vin(new_vin); + metrics_.fuel_level(100.0); + + transit_.vehicle_vin(new_vin); + transit_.current_route(new_route); + transit_.current_position(new_route.front()); } bool has_ended() const @@ -39,74 +43,75 @@ class PublisherSimulation { void run(); - friend std::string to_string(const PublisherSimulation &sim); + std::string to_string() const; private: dds::pub::DataWriter metrics_writer_; dds::pub::DataWriter transit_writer_; - std::string vehicle_vin_; - double vehicle_fuel_; - CoordSequence vehicle_route_; - Coord vehicle_position_; + VehicleMetrics metrics_; + VehicleTransit transit_; bool is_out_of_fuel() const { - return vehicle_fuel_ <= 0.0; + return metrics_.fuel_level() <= 0.0; } bool is_on_standby() const { - return vehicle_route_.empty(); + return !transit_.current_route().has_value() + || transit_.current_route().value().empty(); } }; void PublisherSimulation::run() { while (!has_ended()) { - metrics_writer_.write(VehicleMetrics { vehicle_vin_, vehicle_fuel_ }); + metrics_writer_.write(metrics_); - transit_writer_.write(VehicleTransit { vehicle_vin_, - vehicle_position_, - vehicle_route_ }); + transit_writer_.write(transit_); std::this_thread::sleep_for(std::chrono::seconds(1)); if (is_on_standby()) { - std::cout << "Vehicle '" << vehicle_vin_ + std::cout << "Vehicle '" << metrics_.vehicle_vin() << "' has reached its destination, now moving to a " "new location..." << std::endl; - vehicle_route_ = utils::new_route(); - vehicle_route_[0] = vehicle_position_; + auto new_route = utils::new_route(); + new_route.front() = transit_.current_position(); + + transit_.current_route(new_route); } - vehicle_fuel_ -= 10 * utils::random_stduniform(); - vehicle_position_ = vehicle_route_.front(); - vehicle_route_.erase(vehicle_route_.begin()); + metrics_.fuel_level() -= 10 * utils::random_stduniform(); + transit_.current_position(transit_.current_route().value().front()); + transit_.current_route().value().erase( + transit_.current_route().value().begin()); if (is_out_of_fuel()) { - vehicle_fuel_ = 0.0; - std::cout << "Vehicle '" << vehicle_vin_ << "' ran out of fuel!" - << std::endl; + metrics_.fuel_level(0.0); + std::cout << "Vehicle '" << metrics_.vehicle_vin() + << "' ran out of fuel!" << std::endl; } } } std::string to_string(const PublisherSimulation &sim) +{ + return sim.to_string(); +} + +std::string PublisherSimulation::to_string() const { std::ostringstream ss; - ss << "PublisherSimulation(vehicle_vin: " << sim.vehicle_vin_; - ss << ", vehicle_fuel: " << sim.vehicle_fuel_; - ss << ", vehicle_route: " << to_string(sim.vehicle_route_); - ss << ", vehicle_position: " << to_string(sim.vehicle_position_) << ")"; + ss << "PublisherSimulation(metrics: " << metrics_; + ss << ", transit: " << transit_ << ")"; return ss.str(); } int main(int argc, char **argv) { - utils::set_random_seed(std::time(nullptr)); - rti::domain::register_type(); rti::domain::register_type(); diff --git a/tutorials/application_design/c++11/subscriber.cxx b/tutorials/application_design/c++11/subscriber.cxx index 900202f09..2a747e7ea 100644 --- a/tutorials/application_design/c++11/subscriber.cxx +++ b/tutorials/application_design/c++11/subscriber.cxx @@ -23,9 +23,7 @@ struct DashboardItem { std::string vin; bool is_historical; std::vector fuel_history; - int completed_routes; rti::core::optional current_destination; - std::vector reached_destinations; }; class SubscriberDashboard { @@ -39,95 +37,101 @@ class SubscriberDashboard { void run(); - friend std::string to_string(const SubscriberDashboard &dashboard); + std::string to_string() const; private: dds::sub::DataReader metrics_reader_; dds::sub::DataReader transit_reader_; - dds::core::cond::WaitSet waitset; - std::unordered_map - dashboard_data_; - - void display_app(); - void metrics_app(); - void transit_app(); - std::vector online_vehicles() const - { - std::vector online; - for (const auto &item : dashboard_data_) { - if (!item.second.is_historical) { - online.push_back(item.second); - } - } - return online; - } + std::string new_position_string(dds::sub::cond::ReadCondition &condition); + std::string dashboard_string(); - std::vector offline_vehicles() const - { - std::vector offline; - for (const auto &item : dashboard_data_) { - if (item.second.is_historical) { - offline.push_back(item.second); - } - } - return offline; - } + std::unordered_map + dashboard_data(); }; void SubscriberDashboard::run() { - std::mutex mutex; - - dds::sub::cond::ReadCondition metrics_condition( - metrics_reader_, - dds::sub::status::DataState::any(), - [this]() { metrics_app(); }); - - dds::sub::cond::ReadCondition transit_condition( + dds::sub::cond::ReadCondition new_position_condition( transit_reader_, - dds::sub::status::DataState::any(), - [this]() { transit_app(); }); + dds::sub::status::DataState::new_data(), + [this, &new_position_condition]() { + std::cout << new_position_string(new_position_condition) + << std::endl; + }); - dds::core::cond::GuardCondition display_condition; - display_condition.extensions().handler([this]() { display_app(); }); + dds::core::cond::GuardCondition dashboard_condition; + dashboard_condition.extensions().handler( + [this]() { std::cout << dashboard_string() << std::endl; }); - std::thread display_thread([&display_condition, &mutex]() { + std::mutex mutex; + std::thread display_thread([&dashboard_condition, &mutex]() { for (;;) { - std::this_thread::sleep_for(std::chrono::milliseconds(500)); + std::this_thread::sleep_for(std::chrono::seconds(5)); std::lock_guard lock(mutex); - display_condition.trigger_value(true); + dashboard_condition.trigger_value(true); } }); - waitset.attach_condition(metrics_condition); - waitset.attach_condition(transit_condition); - waitset.attach_condition(display_condition); + dds::core::cond::WaitSet waitset; + waitset += new_position_condition; + waitset += dashboard_condition; for (;;) { waitset.dispatch(); std::lock_guard lock(mutex); - display_condition.trigger_value(false); + dashboard_condition.trigger_value(false); } } -void SubscriberDashboard::display_app() +std::string SubscriberDashboard::new_position_string( + dds::sub::cond::ReadCondition &condition) { using ::to_string; using std::to_string; + std::stringstream ss; + auto transit_samples = transit_reader_.select().condition(condition).read(); + for (const auto &sample : transit_samples) { + if (!sample.info().valid()) { + continue; + } + ss << "[INFO] Vehicle " << sample.data().vehicle_vin(); + auto ¤t_route = sample.data().current_route(); + if (current_route.has_value() && !current_route->empty()) { + ss << " is enroute to " << to_string(current_route->back()) + << " from " << to_string(sample.data().current_position()); + } else { + ss << " has arrived at its destination in " + << to_string(sample.data().current_position()); + } + ss << "\n"; + } + return ss.str(); +} + +std::string SubscriberDashboard::dashboard_string() +{ + using ::to_string; + using std::to_string; std::stringstream ss; auto now = std::chrono::system_clock::now(); ss << "[[ DASHBOARD: " << now.time_since_epoch().count() << " ]]\n"; + auto data = dashboard_data(); { - auto online = online_vehicles(); + std::vector online; + for (const auto &item : data) { + if (!item.second.is_historical) { + online.push_back(item.second); + } + } ss << "Online vehicles: " << online.size() << "\n"; for (auto &item : online) { - ss << "- Vehicle " << item.vin << ":\n"; - ss << " Fuel updates: " << item.fuel_history.size() << "\n"; + ss << "- Vehicle " << item.vin << "\n"; + ss << " Known fuel updates: " << item.fuel_history.size() << "\n"; ss << " Last known destination: " << (item.current_destination - ? to_string(*item.current_destination) + ? to_string(item.current_destination.value()) : "None") << "\n"; ss << " Last known fuel level: " @@ -138,98 +142,97 @@ void SubscriberDashboard::display_app() } } { - auto offline = offline_vehicles(); + std::vector offline; + for (const auto &item : data) { + if (item.second.is_historical) { + offline.push_back(item.second); + } + } ss << "Offline vehicles: " << offline.size() << "\n"; for (auto &item : offline) { - ss << "- Vehicle " << item.vin << ":\n"; - ss << " Mean fuel consumption: " - << std::accumulate( - item.fuel_history.begin(), - item.fuel_history.end(), - 0.0) - / item.fuel_history.size() - << "\n"; - ss << " Known reached destinations: " - << item.reached_destinations.size() << "\n"; - for (auto &destination : item.reached_destinations) { - ss << " - " << to_string(destination) << "\n"; - } + ss << "- Vehicle " << item.vin << "\n"; } } - std::cout << ss.str() << std::endl; + return ss.str(); } -void SubscriberDashboard::metrics_app() +std::unordered_map +SubscriberDashboard::dashboard_data() { - for (const auto &sample : metrics_reader_.take()) { - auto it = dashboard_data_.find(sample.info().instance_handle()); - // If not a tracked vehicle, track it. - if (it == dashboard_data_.end()) { - if (!sample.info().valid()) - continue; + { + std::unordered_map data; + auto metric_samples = metrics_reader_.read(); + auto transit_samples = transit_reader_.read(); + + for (const auto &sample : metric_samples) { + auto it = data.find(sample.info().instance_handle()); + // If not a tracked vehicle, track it. + if (it == data.end()) { + if (!sample.info().valid()) + continue; + + auto new_handle = sample.info().instance_handle(); + auto new_data = DashboardItem { sample.data().vehicle_vin() }; + it = data.emplace(new_handle, new_data).first; + } - auto new_handle = sample.info().instance_handle(); - auto new_data = DashboardItem { sample.data().vehicle_vin() }; - it = dashboard_data_.emplace(new_handle, new_data).first; - } + auto &item = it->second; + item.is_historical = sample.info().state().instance_state() + != dds::sub::status::InstanceState::alive(); - auto &item = it->second; - item.is_historical = sample.info().state().instance_state() - != dds::sub::status::InstanceState::alive(); + if (!sample.info().valid() && item.is_historical) { + continue; + } - if (!sample.info().valid() && item.is_historical) { - continue; + item.fuel_history.push_back(sample.data().fuel_level()); } + for (const auto &sample : transit_samples) { + auto it = data.find(sample.info().instance_handle()); + // If not a tracked vehicle, track it. + if (it == data.end()) { + if (!sample.info().valid()) + continue; + + auto new_handle = sample.info().instance_handle(); + auto new_data = DashboardItem { sample.data().vehicle_vin() }; + it = data.emplace(new_handle, new_data).first; + } - item.fuel_history.push_back(sample.data().fuel_level()); - } -} + auto &item = it->second; + item.is_historical = sample.info().state().instance_state() + != dds::sub::status::InstanceState::alive(); -void SubscriberDashboard::transit_app() -{ - for (const auto &sample : transit_reader_.take()) { - auto it = dashboard_data_.find(sample.info().instance_handle()); - // If not a tracked vehicle, track it. - if (it == dashboard_data_.end()) { - if (!sample.info().valid()) + if (!sample.info().valid() && item.is_historical) { continue; + } - auto new_handle = sample.info().instance_handle(); - auto new_data = DashboardItem { sample.data().vehicle_vin() }; - it = dashboard_data_.emplace(new_handle, new_data).first; - } - - auto &item = it->second; - item.is_historical = sample.info().state().instance_state() - != dds::sub::status::InstanceState::alive(); - - if (!sample.info().valid() && item.is_historical) { - continue; + auto ¤t_route = sample.data().current_route(); + if (current_route->size() > 0) { + item.current_destination = current_route->back(); + } else { + item.current_destination.reset(); + } } - auto ¤t_route = sample.data().current_route(); - if (current_route->size() > 0) { - item.current_destination = current_route->back(); - } else { - item.reached_destinations.push_back(*item.current_destination); - item.current_destination.reset(); - item.completed_routes++; - } + return data; } } -std::string to_string(const SubscriberDashboard &dashboard) +std::string SubscriberDashboard::to_string() const { std::ostringstream ss; ss << "Dashboard()"; return ss.str(); } -int main(int argc, char **argv) +std::string to_string(const SubscriberDashboard &dashboard) { - utils::set_random_seed(std::time(nullptr)); + return dashboard.to_string(); +} +int main(int argc, char **argv) +{ rti::domain::register_type(); rti::domain::register_type(); diff --git a/tutorials/application_design/py/publisher.py b/tutorials/application_design/py/publisher.py index 62770c5c9..f613f7e14 100644 --- a/tutorials/application_design/py/publisher.py +++ b/tutorials/application_design/py/publisher.py @@ -18,20 +18,20 @@ from VehicleModeling import Coord, VehicleMetrics, VehicleTransit -def new_route( +def create_new_route( n: int = 5, start: typing.Optional[Coord] = None, end: typing.Optional[Coord] = None, ): - def new_random_coord(): + def create_new_random_coord(): return Coord( (0.5 - random.random()) * 100, (0.5 - random.random()) * 100, ) - start = start or new_random_coord() - intermediate = (new_random_coord() for _ in range(n)) - end = end or new_random_coord() + start = start or create_new_random_coord() + intermediate = (create_new_random_coord() for _ in range(n)) + end = end or create_new_random_coord() return [start, *intermediate, end] @@ -39,27 +39,30 @@ def new_random_coord(): class PublisherSimulation: def __init__( self, - metrics_writer: "dds.DataWriter", - transit_writer: "dds.DataWriter", - ): + metrics_writer: dds.DataWriter, + transit_writer: dds.DataWriter, + ) -> None: self._metrics_writer = metrics_writer self._transit_writer = transit_writer - self._vehicle_vin: str = "".join( + + vehicle_vin = "".join( random.choices("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", k=17) ) - self._vehicle_fuel = 100.0 - self._vehicle_route = new_route() - self._vehicle_position = self._vehicle_route.pop(0) + vehicle_route = create_new_route() + vehicle_position = vehicle_route.pop(0) + + self._metrics = VehicleMetrics(vehicle_vin, 100.0) + self._transit = VehicleTransit( + vehicle_vin, vehicle_position, vehicle_route + ) def __repr__(self): return ( f"Simulation(" f"{self._metrics_writer=}, " f"{self._transit_writer=}, " - f"{self._vehicle_vin=}, " - f"{self._vehicle_fuel=}, " - f"{self._vehicle_route=}, " - f"{self._vehicle_position=})" + f"{self._metrics=}, " + f"{self._transit=})" ) @property @@ -68,44 +71,36 @@ def has_ended(self): @property def _is_out_of_fuel(self): - return self._vehicle_fuel <= 0.0 + return self._metrics.fuel_level <= 0.0 @property def _is_on_standby(self): - return not self._vehicle_route + return not self._transit.current_route def run(self): while not self.has_ended: - self._metrics_writer.write( - VehicleMetrics( - self._vehicle_vin, - self._vehicle_fuel, - ) - ) - - self._transit_writer.write( - VehicleTransit( - self._vehicle_vin, - current_route=self._vehicle_route, - current_position=self._vehicle_position, - ) - ) + self._metrics_writer.write(self._metrics) + self._transit_writer.write(self._transit) time.sleep(1) if self._is_on_standby: print( - f"Vehicle '{self._vehicle_vin}' has reached its destination, now moving to a new location..." + f"Vehicle '{self._metrics.vehicle_vin}' has reached its destination, now moving to a new location..." + ) + self._transit.current_route = create_new_route( + start=self._transit.current_position ) - self._vehicle_route = new_route(start=self._vehicle_position) - self._vehicle_position = self._vehicle_route.pop(0) - self._vehicle_fuel -= 10 * random.random() + self._transit.current_position = self._transit.current_route.pop(0) + self._metrics.fuel_level -= 10 * random.random() if self._is_out_of_fuel: - self._vehicle_fuel = 0.0 + self._metrics.fuel_level = 0.0 - print(f"Vehicle '{self._vehicle_vin}' ran out of fuel!") + print( + f"Vehicle '{self._metrics.vehicle_vin}' ran out of fuel!" + ) def main(): diff --git a/tutorials/application_design/py/subscriber.py b/tutorials/application_design/py/subscriber.py index f12c75485..859e8628b 100644 --- a/tutorials/application_design/py/subscriber.py +++ b/tutorials/application_design/py/subscriber.py @@ -9,14 +9,11 @@ # damages arising out of the use or inability to use the software. # -import asyncio import dataclasses -import datetime -import statistics +import threading import typing from datetime import datetime -import rti.asyncio import rti.connextdds as dds from VehicleModeling import Coord, VehicleMetrics, VehicleTransit @@ -30,86 +27,122 @@ class DashboardItem: fuel_history: typing.List[float] = dataclasses.field( default_factory=lambda: [100.0] ) - completed_routes: int = 0 current_destination: typing.Optional[Coord] = None - reached_destinations: typing.List[Coord] = dataclasses.field( - default_factory=list - ) class SubscriberDashboard: def __init__( self, - metrics_reader: "dds.DataReader", - transit_reader: "dds.DataReader", - ): + metrics_reader: dds.DataReader, + transit_reader: dds.DataReader, + ) -> None: self._metrics_reader = metrics_reader self._transit_reader = transit_reader - self._dashboard_data: typing.Dict[ - dds.InstanceHandle, DashboardItem - ] = dict() def __repr__(self): - return f"Dashboard({self._metrics_reader=}, {self._transit_reader=}, {self._dashboard_data=})" + return f"Dashboard({self._metrics_reader=}, {self._transit_reader=})" + + def run(self): + + # Create a new handler for newly-received data. + # This handler will be called as data comes by, by dispatching + # the WaitSet that has attached this ReadCondition. + def new_position_handler(_): + print(self._create_new_position_string(new_position_condition)) + + new_position_condition = dds.ReadCondition( + self._transit_reader, dds.DataState.new_data, new_position_handler + ) + + # Create a new handler for printing the dashboard. + # This handler will be called whenever the GuardCondition is triggered, + # which we're periodically doing on a background thread. + def dashboard_handler(_): + print(self._create_dashboard_string()) + + dashboard_condition = dds.GuardCondition() + dashboard_condition.set_handler(dashboard_handler) + + # Create a background thread that will trigger the dashboard condition. + def display_handler(): + dashboard_condition.trigger_value = True + + display_thread = threading.Thread(target=display_handler) + + # Create a WaitSet and attach the conditions to it. + waitset = dds.WaitSet() + waitset.attach_condition(new_position_condition) + waitset.attach_condition(dashboard_condition) + + # Start the thread now that the WaitSet has been created. + display_thread.start() + + try: + while True: + waitset.dispatch() + dashboard_condition.trigger_value = False + except KeyboardInterrupt: + pass + + def _create_new_position_string(self, condition): + string = "" + for sample, info in ( + self._transit_reader.select().condition(condition).read() + ): + if not info.valid: + continue - async def run(self): - await asyncio.gather( - self._display_app(), - self._metrics_app(), - self._transit_app(), + string += f"[INFO] Vehicle {sample.vehicle_vin}" + if sample.current_route and len(sample.current_route): + string += f" is en route to {sample.current_route[-1]} from {sample.current_position}" + else: + string += f" has arrived at its destination in {sample.current_position}" + string += "\n" + return string + + def _create_dashboard_string(self): + dashboard_data = self._build_dashboard_data() + online_vehicles = [ + data for data in dashboard_data.values() if not data.is_historical + ] + offline_vehicles = [ + data for data in dashboard_data.values() if data.is_historical + ] + + online_str = "\n".join( + f"Vehicle {data.vin}:\n" + f" Fuel updates: {len(data.fuel_history)}\n" + f" Last known destination: {data.current_destination}\n" + f" Last known fuel level: {data.fuel_history[-1]}\n" + for data in online_vehicles + ) + offline_str = "\n".join( + f"Vehicle {data.vin}" for data in offline_vehicles + ) + + return "\n".join( + [ + f"[[ DASHBOARD: {datetime.now()} ]]", + f"Online vehicles: {len(online_vehicles)}", + online_str, + f"Offline vehicles: {len(offline_vehicles)}", + offline_str, + ] ) - @property - def online_vehicles(self): - return { - handle: data - for handle, data in self._dashboard_data.items() - if not data.is_historical - } - - @property - def offline_vehicles(self): - return { - handle: data - for handle, data in self._dashboard_data.items() - if data.is_historical - } - - async def _display_app(self): - while True: - print(f"[[ DASHBOARD: {datetime.now()} ]]") - print(f"Online vehicles: {len(self.online_vehicles)}") - for data in self.online_vehicles.values(): - print(f"- Vehicle {data.vin}:") - print(f" Fuel updates: {len(data.fuel_history)}") - print(f" Last known destination: {data.current_destination}") - print(f" Last known fuel level: {data.fuel_history[-1]}") - print(f"Offline vehicles: {len(self.offline_vehicles.keys())}") - for data in self.offline_vehicles.values(): - mean_full_consumption = statistics.mean( - data.fuel_history - ) / len(data.fuel_history) - - print(f"- Vehicle {data.vin}:") - print(f" Mean fuel consumption: {mean_full_consumption}") - print( - f" Known reached destinations: {len(data.reached_destinations)}" - ) - for coord in data.reached_destinations: - print(f" - {coord}") - print() - await asyncio.sleep(0.5) - - async def _metrics_app(self): - async for sample, info in self._metrics_reader.take_async(): - if info.instance_handle not in self._dashboard_data: + def _build_dashboard_data(self): + data: typing.Dict[dds.InstanceHandle, DashboardItem] = {} + + metrics = self._metrics_reader.read() + transit = self._transit_reader.read() + + for sample, info in metrics: + if info.instance_handle not in data: if sample is None: continue - self._dashboard_data[info.instance_handle] = DashboardItem( - sample.vehicle_vin - ) + data[info.instance_handle] = DashboardItem(sample.vehicle_vin) - instance_data = self._dashboard_data[info.instance_handle] + instance_data = data[info.instance_handle] instance_data.is_historical = ( info.state.instance_state != dds.InstanceState.ALIVE ) @@ -119,18 +152,13 @@ async def _metrics_app(self): instance_data.fuel_history.append(sample.fuel_level) - print("metrics ended") - - async def _transit_app(self): - async for sample, info in self._transit_reader.take_async(): - if info.instance_handle not in self._dashboard_data: + for sample, info in transit: + if info.instance_handle not in data: if sample is None: continue - self._dashboard_data[info.instance_handle] = DashboardItem( - sample.vehicle_vin - ) + data[info.instance_handle] = DashboardItem(sample.vehicle_vin) - instance_data = self._dashboard_data[info.instance_handle] + instance_data = data[info.instance_handle] instance_data.is_historical = ( info.state.instance_state != dds.InstanceState.ALIVE ) @@ -144,11 +172,8 @@ async def _transit_app(self): else: # Vehicle has finished its route instance_data.current_destination = None - instance_data.reached_destinations.append( - sample.current_position - ) - print("transit ended") + return data def main(): @@ -172,7 +197,7 @@ def main(): ) print(f"Running dashboard: {dashboard=}") - asyncio.run(dashboard.run()) + dashboard.run() if __name__ == "__main__":