12a40e939SJosh Lehan #include "ExternalSensor.hpp"
2*eacbfdd1SEd Tanous #include "Thresholds.hpp"
32a40e939SJosh Lehan #include "Utils.hpp"
42a40e939SJosh Lehan #include "VariantVisitors.hpp"
52a40e939SJosh Lehan
6*eacbfdd1SEd Tanous #include <boost/asio/error.hpp>
7*eacbfdd1SEd Tanous #include <boost/asio/io_context.hpp>
8*eacbfdd1SEd Tanous #include <boost/asio/post.hpp>
9*eacbfdd1SEd Tanous #include <boost/asio/steady_timer.hpp>
102a40e939SJosh Lehan #include <boost/container/flat_map.hpp>
112a40e939SJosh Lehan #include <boost/container/flat_set.hpp>
122a40e939SJosh Lehan #include <sdbusplus/asio/connection.hpp>
132a40e939SJosh Lehan #include <sdbusplus/asio/object_server.hpp>
142a40e939SJosh Lehan #include <sdbusplus/bus/match.hpp>
15*eacbfdd1SEd Tanous #include <sdbusplus/message.hpp>
16*eacbfdd1SEd Tanous #include <sdbusplus/message/native_types.hpp>
172a40e939SJosh Lehan
18*eacbfdd1SEd Tanous #include <algorithm>
192a40e939SJosh Lehan #include <array>
20*eacbfdd1SEd Tanous #include <chrono>
21*eacbfdd1SEd Tanous #include <cmath>
222a40e939SJosh Lehan #include <functional>
23*eacbfdd1SEd Tanous #include <iostream>
242a40e939SJosh Lehan #include <memory>
252a40e939SJosh Lehan #include <string>
262a40e939SJosh Lehan #include <utility>
272a40e939SJosh Lehan #include <variant>
282a40e939SJosh Lehan #include <vector>
292a40e939SJosh Lehan
302a40e939SJosh Lehan // Copied from HwmonTempSensor and inspired by
312a40e939SJosh Lehan // https://gerrit.openbmc-project.xyz/c/openbmc/dbus-sensors/+/35476
322a40e939SJosh Lehan
332a40e939SJosh Lehan // The ExternalSensor is a sensor whose value is intended to be writable
342a40e939SJosh Lehan // by something external to the BMC, so that the host (or something else)
357243217bSJosh Lehan // can write to it, perhaps by using an IPMI or Redfish connection.
362a40e939SJosh Lehan
372a40e939SJosh Lehan // Unlike most other sensors, an external sensor does not correspond
387243217bSJosh Lehan // to a hwmon file or any other kernel/hardware interface,
392a40e939SJosh Lehan // so, after initialization, this module does not have much to do,
402a40e939SJosh Lehan // but it handles reinitialization and thresholds, similar to the others.
417243217bSJosh Lehan // The main work of this module is to provide backing storage for a
427243217bSJosh Lehan // sensor that exists only virtually, and to provide an optional
437243217bSJosh Lehan // timeout service for detecting loss of timely updates.
442a40e939SJosh Lehan
452a40e939SJosh Lehan // As there is no corresponding driver or hardware to support,
462a40e939SJosh Lehan // all configuration of this sensor comes from the JSON parameters:
477243217bSJosh Lehan // MinValue, MaxValue, Timeout, PowerState, Units, Name
482a40e939SJosh Lehan
497243217bSJosh Lehan // The purpose of "Units" is to specify the physical characteristic
502a40e939SJosh Lehan // the external sensor is measuring, because with an external sensor
512a40e939SJosh Lehan // there is no other way to tell, and it will be used for the object path
527243217bSJosh Lehan // here: /xyz/openbmc_project/sensors/<Units>/<Name>
537243217bSJosh Lehan
547243217bSJosh Lehan // For more information, see external-sensor.md design document:
557243217bSJosh Lehan // https://gerrit.openbmc-project.xyz/c/openbmc/docs/+/41452
567243217bSJosh Lehan // https://github.com/openbmc/docs/tree/master/designs/
572a40e939SJosh Lehan
588a57ec09SEd Tanous static constexpr bool debug = false;
592a40e939SJosh Lehan
60054aad8fSZev Weiss static const char* sensorType = "ExternalSensor";
612a40e939SJosh Lehan
updateReaper(boost::container::flat_map<std::string,std::shared_ptr<ExternalSensor>> & sensors,boost::asio::steady_timer & timer,const std::chrono::steady_clock::time_point & now)627243217bSJosh Lehan void updateReaper(boost::container::flat_map<
637243217bSJosh Lehan std::string, std::shared_ptr<ExternalSensor>>& sensors,
647243217bSJosh Lehan boost::asio::steady_timer& timer,
657243217bSJosh Lehan const std::chrono::steady_clock::time_point& now)
667243217bSJosh Lehan {
677243217bSJosh Lehan // First pass, reap all stale sensors
6808cb50c5SZev Weiss for (const auto& [name, sensor] : sensors)
697243217bSJosh Lehan {
7008cb50c5SZev Weiss if (!sensor)
717243217bSJosh Lehan {
727243217bSJosh Lehan continue;
737243217bSJosh Lehan }
747243217bSJosh Lehan
7508cb50c5SZev Weiss if (!sensor->isAliveAndPerishable())
767243217bSJosh Lehan {
777243217bSJosh Lehan continue;
787243217bSJosh Lehan }
797243217bSJosh Lehan
8008cb50c5SZev Weiss if (!sensor->isAliveAndFresh(now))
817243217bSJosh Lehan {
827243217bSJosh Lehan // Mark sensor as dead, no longer alive
8308cb50c5SZev Weiss sensor->writeInvalidate();
847243217bSJosh Lehan }
857243217bSJosh Lehan }
867243217bSJosh Lehan
877243217bSJosh Lehan std::chrono::steady_clock::duration nextCheck;
887243217bSJosh Lehan bool needCheck = false;
897243217bSJosh Lehan
907243217bSJosh Lehan // Second pass, determine timer interval to next check
9108cb50c5SZev Weiss for (const auto& [name, sensor] : sensors)
927243217bSJosh Lehan {
9308cb50c5SZev Weiss if (!sensor)
947243217bSJosh Lehan {
957243217bSJosh Lehan continue;
967243217bSJosh Lehan }
977243217bSJosh Lehan
9808cb50c5SZev Weiss if (!sensor->isAliveAndPerishable())
997243217bSJosh Lehan {
1007243217bSJosh Lehan continue;
1017243217bSJosh Lehan }
1027243217bSJosh Lehan
10308cb50c5SZev Weiss auto expiration = sensor->ageRemaining(now);
1047243217bSJosh Lehan
1057243217bSJosh Lehan if (needCheck)
1067243217bSJosh Lehan {
1077243217bSJosh Lehan nextCheck = std::min(nextCheck, expiration);
1087243217bSJosh Lehan }
1097243217bSJosh Lehan else
1107243217bSJosh Lehan {
1117243217bSJosh Lehan // Initialization
1127243217bSJosh Lehan nextCheck = expiration;
1137243217bSJosh Lehan needCheck = true;
1147243217bSJosh Lehan }
1157243217bSJosh Lehan }
1167243217bSJosh Lehan
1177243217bSJosh Lehan if (!needCheck)
1187243217bSJosh Lehan {
1197243217bSJosh Lehan if constexpr (debug)
1207243217bSJosh Lehan {
1217243217bSJosh Lehan std::cerr << "Next ExternalSensor timer idle\n";
1227243217bSJosh Lehan }
1237243217bSJosh Lehan
1247243217bSJosh Lehan return;
1257243217bSJosh Lehan }
1267243217bSJosh Lehan
1277243217bSJosh Lehan timer.expires_at(now + nextCheck);
1287243217bSJosh Lehan
1297243217bSJosh Lehan timer.async_wait([&sensors, &timer](const boost::system::error_code& err) {
1307243217bSJosh Lehan if (err != boost::system::errc::success)
1317243217bSJosh Lehan {
1327243217bSJosh Lehan // Cancellation is normal, as timer is dynamically rescheduled
1330362738dSJosh Lehan if (err != boost::asio::error::operation_aborted)
1347243217bSJosh Lehan {
1357243217bSJosh Lehan std::cerr << "ExternalSensor timer scheduling problem: "
1367243217bSJosh Lehan << err.message() << "\n";
1377243217bSJosh Lehan }
1387243217bSJosh Lehan return;
1397243217bSJosh Lehan }
1400362738dSJosh Lehan
1417243217bSJosh Lehan updateReaper(sensors, timer, std::chrono::steady_clock::now());
1427243217bSJosh Lehan });
1437243217bSJosh Lehan
1447243217bSJosh Lehan if constexpr (debug)
1457243217bSJosh Lehan {
1467243217bSJosh Lehan std::cerr << "Next ExternalSensor timer "
1477243217bSJosh Lehan << std::chrono::duration_cast<std::chrono::microseconds>(
1487243217bSJosh Lehan nextCheck)
1497243217bSJosh Lehan .count()
1507243217bSJosh Lehan << " us\n";
1517243217bSJosh Lehan }
1527243217bSJosh Lehan }
1537243217bSJosh Lehan
createSensors(sdbusplus::asio::object_server & objectServer,boost::container::flat_map<std::string,std::shared_ptr<ExternalSensor>> & sensors,std::shared_ptr<sdbusplus::asio::connection> & dbusConnection,const std::shared_ptr<boost::container::flat_set<std::string>> & sensorsChanged,boost::asio::steady_timer & reaperTimer)1542a40e939SJosh Lehan void createSensors(
1558a17c303SEd Tanous sdbusplus::asio::object_server& objectServer,
1562a40e939SJosh Lehan boost::container::flat_map<std::string, std::shared_ptr<ExternalSensor>>&
1572a40e939SJosh Lehan sensors,
1582a40e939SJosh Lehan std::shared_ptr<sdbusplus::asio::connection>& dbusConnection,
1592a40e939SJosh Lehan const std::shared_ptr<boost::container::flat_set<std::string>>&
1607243217bSJosh Lehan sensorsChanged,
1617243217bSJosh Lehan boost::asio::steady_timer& reaperTimer)
1622a40e939SJosh Lehan {
1630362738dSJosh Lehan if constexpr (debug)
1640362738dSJosh Lehan {
1650362738dSJosh Lehan std::cerr << "ExternalSensor considering creating sensors\n";
1660362738dSJosh Lehan }
1670362738dSJosh Lehan
1682a40e939SJosh Lehan auto getter = std::make_shared<GetSensorConfiguration>(
1692a40e939SJosh Lehan dbusConnection,
1708a17c303SEd Tanous [&objectServer, &sensors, &dbusConnection, sensorsChanged,
1717243217bSJosh Lehan &reaperTimer](const ManagedObjectType& sensorConfigurations) {
1722a40e939SJosh Lehan bool firstScan = (sensorsChanged == nullptr);
1732a40e939SJosh Lehan
1742a40e939SJosh Lehan for (const std::pair<sdbusplus::message::object_path, SensorData>&
1752a40e939SJosh Lehan sensor : sensorConfigurations)
1762a40e939SJosh Lehan {
1772a40e939SJosh Lehan const std::string& interfacePath = sensor.first.str;
1782a40e939SJosh Lehan const SensorData& sensorData = sensor.second;
1792a40e939SJosh Lehan
180054aad8fSZev Weiss auto sensorBase = sensorData.find(configInterfaceName(sensorType));
1812a40e939SJosh Lehan if (sensorBase == sensorData.end())
1822a40e939SJosh Lehan {
1832a40e939SJosh Lehan std::cerr << "Base configuration not found for "
1842a40e939SJosh Lehan << interfacePath << "\n";
1852a40e939SJosh Lehan continue;
1862a40e939SJosh Lehan }
1872a40e939SJosh Lehan
1882a40e939SJosh Lehan const SensorBaseConfiguration& baseConfiguration = *sensorBase;
189bb67932aSEd Tanous const SensorBaseConfigMap& baseConfigMap = baseConfiguration.second;
1902a40e939SJosh Lehan
1912a40e939SJosh Lehan // MinValue and MinValue are mandatory numeric parameters
1922a40e939SJosh Lehan auto minFound = baseConfigMap.find("MinValue");
1932a40e939SJosh Lehan if (minFound == baseConfigMap.end())
1942a40e939SJosh Lehan {
1952a40e939SJosh Lehan std::cerr << "MinValue parameter not found for "
1962a40e939SJosh Lehan << interfacePath << "\n";
1972a40e939SJosh Lehan continue;
1982a40e939SJosh Lehan }
199779c96a2SPatrick Williams double minValue = std::visit(VariantToDoubleVisitor(),
200779c96a2SPatrick Williams minFound->second);
2012a40e939SJosh Lehan if (!std::isfinite(minValue))
2022a40e939SJosh Lehan {
2032a40e939SJosh Lehan std::cerr << "MinValue parameter not parsed for "
2042a40e939SJosh Lehan << interfacePath << "\n";
2052a40e939SJosh Lehan continue;
2062a40e939SJosh Lehan }
2072a40e939SJosh Lehan
2082a40e939SJosh Lehan auto maxFound = baseConfigMap.find("MaxValue");
2092a40e939SJosh Lehan if (maxFound == baseConfigMap.end())
2102a40e939SJosh Lehan {
2112a40e939SJosh Lehan std::cerr << "MaxValue parameter not found for "
2122a40e939SJosh Lehan << interfacePath << "\n";
2132a40e939SJosh Lehan continue;
2142a40e939SJosh Lehan }
215779c96a2SPatrick Williams double maxValue = std::visit(VariantToDoubleVisitor(),
216779c96a2SPatrick Williams maxFound->second);
2172a40e939SJosh Lehan if (!std::isfinite(maxValue))
2182a40e939SJosh Lehan {
2192a40e939SJosh Lehan std::cerr << "MaxValue parameter not parsed for "
2202a40e939SJosh Lehan << interfacePath << "\n";
2212a40e939SJosh Lehan continue;
2222a40e939SJosh Lehan }
2232a40e939SJosh Lehan
2247243217bSJosh Lehan double timeoutSecs = 0.0;
2252a40e939SJosh Lehan
2267243217bSJosh Lehan // Timeout is an optional numeric parameter
2277243217bSJosh Lehan auto timeoutFound = baseConfigMap.find("Timeout");
2287243217bSJosh Lehan if (timeoutFound != baseConfigMap.end())
2297243217bSJosh Lehan {
230779c96a2SPatrick Williams timeoutSecs = std::visit(VariantToDoubleVisitor(),
231779c96a2SPatrick Williams timeoutFound->second);
2327243217bSJosh Lehan }
2339da019ccSPatrick Williams if (!std::isfinite(timeoutSecs) || (timeoutSecs < 0.0))
2347243217bSJosh Lehan {
2357243217bSJosh Lehan std::cerr << "Timeout parameter not parsed for "
2367243217bSJosh Lehan << interfacePath << "\n";
2377243217bSJosh Lehan continue;
2387243217bSJosh Lehan }
2397243217bSJosh Lehan
2407243217bSJosh Lehan std::string sensorName;
2417243217bSJosh Lehan std::string sensorUnits;
2427243217bSJosh Lehan
2437243217bSJosh Lehan // Name and Units are mandatory string parameters
2442a40e939SJosh Lehan auto nameFound = baseConfigMap.find("Name");
2452a40e939SJosh Lehan if (nameFound == baseConfigMap.end())
2462a40e939SJosh Lehan {
247bb67932aSEd Tanous std::cerr << "Name parameter not found for " << interfacePath
248bb67932aSEd Tanous << "\n";
2492a40e939SJosh Lehan continue;
2502a40e939SJosh Lehan }
251779c96a2SPatrick Williams sensorName = std::visit(VariantToStringVisitor(),
252779c96a2SPatrick Williams nameFound->second);
2532a40e939SJosh Lehan if (sensorName.empty())
2542a40e939SJosh Lehan {
255bb67932aSEd Tanous std::cerr << "Name parameter not parsed for " << interfacePath
256bb67932aSEd Tanous << "\n";
2572a40e939SJosh Lehan continue;
2582a40e939SJosh Lehan }
2592a40e939SJosh Lehan
2607243217bSJosh Lehan auto unitsFound = baseConfigMap.find("Units");
2617243217bSJosh Lehan if (unitsFound == baseConfigMap.end())
2622a40e939SJosh Lehan {
263bb67932aSEd Tanous std::cerr << "Units parameter not found for " << interfacePath
264bb67932aSEd Tanous << "\n";
2652a40e939SJosh Lehan continue;
2662a40e939SJosh Lehan }
267779c96a2SPatrick Williams sensorUnits = std::visit(VariantToStringVisitor(),
268779c96a2SPatrick Williams unitsFound->second);
2697243217bSJosh Lehan if (sensorUnits.empty())
2702a40e939SJosh Lehan {
271bb67932aSEd Tanous std::cerr << "Units parameter not parsed for " << interfacePath
272bb67932aSEd Tanous << "\n";
2732a40e939SJosh Lehan continue;
2742a40e939SJosh Lehan }
2752a40e939SJosh Lehan
2762a40e939SJosh Lehan // on rescans, only update sensors we were signaled by
2772a40e939SJosh Lehan auto findSensor = sensors.find(sensorName);
2782a40e939SJosh Lehan if (!firstScan && (findSensor != sensors.end()))
2792a40e939SJosh Lehan {
2802a40e939SJosh Lehan std::string suffixName = "/";
2812a40e939SJosh Lehan suffixName += findSensor->second->name;
2822a40e939SJosh Lehan bool found = false;
2832a40e939SJosh Lehan for (auto it = sensorsChanged->begin();
2842a40e939SJosh Lehan it != sensorsChanged->end(); it++)
2852a40e939SJosh Lehan {
2862a40e939SJosh Lehan std::string suffixIt = "/";
2872a40e939SJosh Lehan suffixIt += *it;
2886c106d66SZev Weiss if (suffixIt.ends_with(suffixName))
2892a40e939SJosh Lehan {
2902a40e939SJosh Lehan sensorsChanged->erase(it);
2912a40e939SJosh Lehan findSensor->second = nullptr;
2922a40e939SJosh Lehan found = true;
2930362738dSJosh Lehan if constexpr (debug)
2940362738dSJosh Lehan {
2950362738dSJosh Lehan std::cerr << "ExternalSensor " << sensorName
2960362738dSJosh Lehan << " change found\n";
2970362738dSJosh Lehan }
2982a40e939SJosh Lehan break;
2992a40e939SJosh Lehan }
3002a40e939SJosh Lehan }
3012a40e939SJosh Lehan if (!found)
3022a40e939SJosh Lehan {
3032a40e939SJosh Lehan continue;
3042a40e939SJosh Lehan }
3052a40e939SJosh Lehan }
3062a40e939SJosh Lehan
3072a40e939SJosh Lehan std::vector<thresholds::Threshold> sensorThresholds;
3082a40e939SJosh Lehan if (!parseThresholdsFromConfig(sensorData, sensorThresholds))
3092a40e939SJosh Lehan {
310bb67932aSEd Tanous std::cerr << "error populating thresholds for " << sensorName
311bb67932aSEd Tanous << "\n";
3122a40e939SJosh Lehan }
3132a40e939SJosh Lehan
314a4d2768cSZev Weiss PowerState readState = getPowerState(baseConfigMap);
3152a40e939SJosh Lehan
3162a40e939SJosh Lehan auto& sensorEntry = sensors[sensorName];
3172a40e939SJosh Lehan sensorEntry = nullptr;
3182a40e939SJosh Lehan
3192a40e939SJosh Lehan sensorEntry = std::make_shared<ExternalSensor>(
3202a40e939SJosh Lehan sensorType, objectServer, dbusConnection, sensorName,
3217243217bSJosh Lehan sensorUnits, std::move(sensorThresholds), interfacePath,
3220362738dSJosh Lehan maxValue, minValue, timeoutSecs, readState);
3230362738dSJosh Lehan sensorEntry->initWriteHook(
3247243217bSJosh Lehan [&sensors, &reaperTimer](
3257243217bSJosh Lehan const std::chrono::steady_clock::time_point& now) {
3267243217bSJosh Lehan updateReaper(sensors, reaperTimer, now);
3277243217bSJosh Lehan });
3287243217bSJosh Lehan
3297243217bSJosh Lehan if constexpr (debug)
3307243217bSJosh Lehan {
331bb67932aSEd Tanous std::cerr << "ExternalSensor " << sensorName << " created\n";
3327243217bSJosh Lehan }
3332a40e939SJosh Lehan }
3342a40e939SJosh Lehan });
3352a40e939SJosh Lehan
3362a40e939SJosh Lehan getter->getConfiguration(std::vector<std::string>{sensorType});
3372a40e939SJosh Lehan }
3382a40e939SJosh Lehan
main()3392a40e939SJosh Lehan int main()
3402a40e939SJosh Lehan {
3417243217bSJosh Lehan if constexpr (debug)
3427243217bSJosh Lehan {
3437243217bSJosh Lehan std::cerr << "ExternalSensor service starting up\n";
3447243217bSJosh Lehan }
3457243217bSJosh Lehan
3461f978631SEd Tanous boost::asio::io_context io;
3472a40e939SJosh Lehan auto systemBus = std::make_shared<sdbusplus::asio::connection>(io);
34814ed5e99SEd Tanous sdbusplus::asio::object_server objectServer(systemBus, true);
34914ed5e99SEd Tanous
35014ed5e99SEd Tanous objectServer.add_manager("/xyz/openbmc_project/sensors");
3512a40e939SJosh Lehan systemBus->request_name("xyz.openbmc_project.ExternalSensor");
35214ed5e99SEd Tanous
3532a40e939SJosh Lehan boost::container::flat_map<std::string, std::shared_ptr<ExternalSensor>>
3542a40e939SJosh Lehan sensors;
3552a40e939SJosh Lehan auto sensorsChanged =
3562a40e939SJosh Lehan std::make_shared<boost::container::flat_set<std::string>>();
3577243217bSJosh Lehan boost::asio::steady_timer reaperTimer(io);
3582a40e939SJosh Lehan
35983db50caSEd Tanous boost::asio::post(io,
36083db50caSEd Tanous [&objectServer, &sensors, &systemBus, &reaperTimer]() {
3618a17c303SEd Tanous createSensors(objectServer, sensors, systemBus, nullptr, reaperTimer);
3622a40e939SJosh Lehan });
3632a40e939SJosh Lehan
3649b4a20e9SEd Tanous boost::asio::steady_timer filterTimer(io);
36592f8f515SPatrick Williams std::function<void(sdbusplus::message_t&)> eventHandler =
3668a17c303SEd Tanous [&objectServer, &sensors, &systemBus, &sensorsChanged, &filterTimer,
36792f8f515SPatrick Williams &reaperTimer](sdbusplus::message_t& message) mutable {
3682a40e939SJosh Lehan if (message.is_method_error())
3692a40e939SJosh Lehan {
3702a40e939SJosh Lehan std::cerr << "callback method error\n";
3712a40e939SJosh Lehan return;
3722a40e939SJosh Lehan }
3730362738dSJosh Lehan
3742049bd26SEd Tanous const auto* messagePath = message.get_path();
3750362738dSJosh Lehan sensorsChanged->insert(messagePath);
3760362738dSJosh Lehan if constexpr (debug)
3770362738dSJosh Lehan {
378bb67932aSEd Tanous std::cerr << "ExternalSensor change event received: " << messagePath
379bb67932aSEd Tanous << "\n";
3800362738dSJosh Lehan }
3810362738dSJosh Lehan
3822a40e939SJosh Lehan // this implicitly cancels the timer
38383db50caSEd Tanous filterTimer.expires_after(std::chrono::seconds(1));
3842a40e939SJosh Lehan
3858a17c303SEd Tanous filterTimer.async_wait(
3868a17c303SEd Tanous [&objectServer, &sensors, &systemBus, &sensorsChanged,
3878a17c303SEd Tanous &reaperTimer](const boost::system::error_code& ec) mutable {
3880362738dSJosh Lehan if (ec != boost::system::errc::success)
3892a40e939SJosh Lehan {
3902a40e939SJosh Lehan if (ec != boost::asio::error::operation_aborted)
3912a40e939SJosh Lehan {
392bb67932aSEd Tanous std::cerr << "callback error: " << ec.message() << "\n";
3932a40e939SJosh Lehan }
3942a40e939SJosh Lehan return;
3952a40e939SJosh Lehan }
3960362738dSJosh Lehan
397bb67932aSEd Tanous createSensors(objectServer, sensors, systemBus, sensorsChanged,
398bb67932aSEd Tanous reaperTimer);
3992a40e939SJosh Lehan });
4002a40e939SJosh Lehan };
4012a40e939SJosh Lehan
402214d9717SZev Weiss std::vector<std::unique_ptr<sdbusplus::bus::match_t>> matches =
403214d9717SZev Weiss setupPropertiesChangedMatches(
404214d9717SZev Weiss *systemBus, std::to_array<const char*>({sensorType}), eventHandler);
4052a40e939SJosh Lehan
4067243217bSJosh Lehan if constexpr (debug)
4077243217bSJosh Lehan {
4087243217bSJosh Lehan std::cerr << "ExternalSensor service entering main loop\n";
4097243217bSJosh Lehan }
4107243217bSJosh Lehan
4112a40e939SJosh Lehan io.run();
4122a40e939SJosh Lehan }
413