xref: /openbmc/dbus-sensors/src/external/ExternalSensorMain.cpp (revision d630b3a3904f1ad360e001937e95fb438bb8f819)
1 #include "ExternalSensor.hpp"
2 #include "Thresholds.hpp"
3 #include "Utils.hpp"
4 #include "VariantVisitors.hpp"
5 
6 #include <boost/asio/error.hpp>
7 #include <boost/asio/io_context.hpp>
8 #include <boost/asio/post.hpp>
9 #include <boost/asio/steady_timer.hpp>
10 #include <boost/container/flat_map.hpp>
11 #include <boost/container/flat_set.hpp>
12 #include <phosphor-logging/lg2.hpp>
13 #include <sdbusplus/asio/connection.hpp>
14 #include <sdbusplus/asio/object_server.hpp>
15 #include <sdbusplus/bus/match.hpp>
16 #include <sdbusplus/message.hpp>
17 #include <sdbusplus/message/native_types.hpp>
18 
19 #include <algorithm>
20 #include <array>
21 #include <chrono>
22 #include <cmath>
23 #include <functional>
24 #include <memory>
25 #include <string>
26 #include <utility>
27 #include <variant>
28 #include <vector>
29 
30 // Copied from HwmonTempSensor and inspired by
31 // https://gerrit.openbmc-project.xyz/c/openbmc/dbus-sensors/+/35476
32 
33 // The ExternalSensor is a sensor whose value is intended to be writable
34 // by something external to the BMC, so that the host (or something else)
35 // can write to it, perhaps by using an IPMI or Redfish connection.
36 
37 // Unlike most other sensors, an external sensor does not correspond
38 // to a hwmon file or any other kernel/hardware interface,
39 // so, after initialization, this module does not have much to do,
40 // but it handles reinitialization and thresholds, similar to the others.
41 // The main work of this module is to provide backing storage for a
42 // sensor that exists only virtually, and to provide an optional
43 // timeout service for detecting loss of timely updates.
44 
45 // As there is no corresponding driver or hardware to support,
46 // all configuration of this sensor comes from the JSON parameters:
47 // MinValue, MaxValue, Timeout, PowerState, Units, Name
48 
49 // The purpose of "Units" is to specify the physical characteristic
50 // the external sensor is measuring, because with an external sensor
51 // there is no other way to tell, and it will be used for the object path
52 // here: /xyz/openbmc_project/sensors/<Units>/<Name>
53 
54 // For more information, see external-sensor.md design document:
55 // https://gerrit.openbmc-project.xyz/c/openbmc/docs/+/41452
56 // https://github.com/openbmc/docs/tree/master/designs/
57 
58 static constexpr bool debug = false;
59 
60 static const char* sensorType = "ExternalSensor";
61 
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)62 void updateReaper(
63     boost::container::flat_map<std::string, std::shared_ptr<ExternalSensor>>&
64         sensors,
65     boost::asio::steady_timer& timer,
66     const std::chrono::steady_clock::time_point& now)
67 {
68     // First pass, reap all stale sensors
69     for (const auto& [name, sensor] : sensors)
70     {
71         if (!sensor)
72         {
73             continue;
74         }
75 
76         if (!sensor->isAliveAndPerishable())
77         {
78             continue;
79         }
80 
81         if (!sensor->isAliveAndFresh(now))
82         {
83             // Mark sensor as dead, no longer alive
84             sensor->writeInvalidate();
85         }
86     }
87 
88     std::chrono::steady_clock::duration nextCheck;
89     bool needCheck = false;
90 
91     // Second pass, determine timer interval to next check
92     for (const auto& [name, sensor] : sensors)
93     {
94         if (!sensor)
95         {
96             continue;
97         }
98 
99         if (!sensor->isAliveAndPerishable())
100         {
101             continue;
102         }
103 
104         auto expiration = sensor->ageRemaining(now);
105 
106         if (needCheck)
107         {
108             nextCheck = std::min(nextCheck, expiration);
109         }
110         else
111         {
112             // Initialization
113             nextCheck = expiration;
114             needCheck = true;
115         }
116     }
117 
118     if (!needCheck)
119     {
120         if constexpr (debug)
121         {
122             lg2::error("Next ExternalSensor timer idle");
123         }
124 
125         return;
126     }
127 
128     timer.expires_at(now + nextCheck);
129 
130     timer.async_wait([&sensors, &timer](const boost::system::error_code& err) {
131         if (err != boost::system::errc::success)
132         {
133             // Cancellation is normal, as timer is dynamically rescheduled
134             if (err != boost::asio::error::operation_aborted)
135             {
136                 lg2::error(
137                     "ExternalSensor timer scheduling problem: {ERROR_MESSAGE}",
138                     "ERROR_MESSAGE", err.message());
139             }
140             return;
141         }
142 
143         updateReaper(sensors, timer, std::chrono::steady_clock::now());
144     });
145 
146     if constexpr (debug)
147     {
148         lg2::error(
149             "Next ExternalSensor timer '{VALUE}' us", "VALUE",
150             std::chrono::duration_cast<std::chrono::microseconds>(nextCheck)
151                 .count());
152     }
153 }
154 
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)155 void createSensors(
156     sdbusplus::asio::object_server& objectServer,
157     boost::container::flat_map<std::string, std::shared_ptr<ExternalSensor>>&
158         sensors,
159     std::shared_ptr<sdbusplus::asio::connection>& dbusConnection,
160     const std::shared_ptr<boost::container::flat_set<std::string>>&
161         sensorsChanged,
162     boost::asio::steady_timer& reaperTimer)
163 {
164     if constexpr (debug)
165     {
166         lg2::error("ExternalSensor considering creating sensors");
167     }
168 
169     auto getter = std::make_shared<GetSensorConfiguration>(
170         dbusConnection,
171         [&objectServer, &sensors, &dbusConnection, sensorsChanged,
172          &reaperTimer](const ManagedObjectType& sensorConfigurations) {
173             bool firstScan = (sensorsChanged == nullptr);
174 
175             for (const std::pair<sdbusplus::message::object_path, SensorData>&
176                      sensor : sensorConfigurations)
177             {
178                 const std::string& interfacePath = sensor.first.str;
179                 const SensorData& sensorData = sensor.second;
180 
181                 auto sensorBase =
182                     sensorData.find(configInterfaceName(sensorType));
183                 if (sensorBase == sensorData.end())
184                 {
185                     lg2::error("Base configuration not found for '{PATH}'",
186                                "PATH", interfacePath);
187                     continue;
188                 }
189 
190                 const SensorBaseConfiguration& baseConfiguration = *sensorBase;
191                 const SensorBaseConfigMap& baseConfigMap =
192                     baseConfiguration.second;
193 
194                 // MinValue and MinValue are mandatory numeric parameters
195                 auto minFound = baseConfigMap.find("MinValue");
196                 if (minFound == baseConfigMap.end())
197                 {
198                     lg2::error("MinValue parameter not found for '{PATH}'",
199                                "PATH", interfacePath);
200                     continue;
201                 }
202                 double minValue =
203                     std::visit(VariantToDoubleVisitor(), minFound->second);
204                 if (!std::isfinite(minValue))
205                 {
206                     lg2::error("MinValue parameter not parsed for '{PATH}'",
207                                "PATH", interfacePath);
208                     continue;
209                 }
210 
211                 auto maxFound = baseConfigMap.find("MaxValue");
212                 if (maxFound == baseConfigMap.end())
213                 {
214                     lg2::error("MaxValue parameter not found for '{PATH}'",
215                                "PATH", interfacePath);
216                     continue;
217                 }
218                 double maxValue =
219                     std::visit(VariantToDoubleVisitor(), maxFound->second);
220                 if (!std::isfinite(maxValue))
221                 {
222                     lg2::error("MaxValue parameter not parsed for '{PATH}'",
223                                "PATH", interfacePath);
224                     continue;
225                 }
226 
227                 double timeoutSecs = 0.0;
228 
229                 // Timeout is an optional numeric parameter
230                 auto timeoutFound = baseConfigMap.find("Timeout");
231                 if (timeoutFound != baseConfigMap.end())
232                 {
233                     timeoutSecs = std::visit(VariantToDoubleVisitor(),
234                                              timeoutFound->second);
235                 }
236                 if (!std::isfinite(timeoutSecs) || (timeoutSecs < 0.0))
237                 {
238                     lg2::error("Timeout parameter not parsed for '{PATH}'",
239                                "PATH", interfacePath);
240                     continue;
241                 }
242 
243                 std::string sensorName;
244                 std::string sensorUnits;
245 
246                 // Name and Units are mandatory string parameters
247                 auto nameFound = baseConfigMap.find("Name");
248                 if (nameFound == baseConfigMap.end())
249                 {
250                     lg2::error("Name parameter not found for '{PATH}'", "PATH",
251                                interfacePath);
252                     continue;
253                 }
254                 sensorName =
255                     std::visit(VariantToStringVisitor(), nameFound->second);
256                 if (sensorName.empty())
257                 {
258                     lg2::error("Name parameter not parsed for '{PATH}'", "PATH",
259                                interfacePath);
260                     continue;
261                 }
262 
263                 auto unitsFound = baseConfigMap.find("Units");
264                 if (unitsFound == baseConfigMap.end())
265                 {
266                     lg2::error("Units parameter not found for '{PATH}'", "PATH",
267                                interfacePath);
268                     continue;
269                 }
270                 sensorUnits =
271                     std::visit(VariantToStringVisitor(), unitsFound->second);
272                 if (sensorUnits.empty())
273                 {
274                     lg2::error("Units parameter not parsed for '{PATH}'",
275                                "PATH", interfacePath);
276                     continue;
277                 }
278 
279                 // on rescans, only update sensors we were signaled by
280                 auto findSensor = sensors.find(sensorName);
281                 if (!firstScan && (findSensor != sensors.end()))
282                 {
283                     std::string suffixName = "/";
284                     suffixName += findSensor->second->name;
285                     bool found = false;
286                     for (auto it = sensorsChanged->begin();
287                          it != sensorsChanged->end(); it++)
288                     {
289                         std::string suffixIt = "/";
290                         suffixIt += *it;
291                         if (suffixIt.ends_with(suffixName))
292                         {
293                             sensorsChanged->erase(it);
294                             findSensor->second = nullptr;
295                             found = true;
296                             if constexpr (debug)
297                             {
298                                 lg2::error(
299                                     "ExternalSensor '{NAME}' change found",
300                                     "NAME", sensorName);
301                             }
302                             break;
303                         }
304                     }
305                     if (!found)
306                     {
307                         continue;
308                     }
309                 }
310 
311                 std::vector<thresholds::Threshold> sensorThresholds;
312                 if (!parseThresholdsFromConfig(sensorData, sensorThresholds))
313                 {
314                     lg2::error("error populating thresholds for '{NAME}'",
315                                "NAME", sensorName);
316                 }
317 
318                 PowerState readState = getPowerState(baseConfigMap);
319 
320                 auto& sensorEntry = sensors[sensorName];
321                 sensorEntry = nullptr;
322 
323                 sensorEntry = std::make_shared<ExternalSensor>(
324                     sensorType, objectServer, dbusConnection, sensorName,
325                     sensorUnits, std::move(sensorThresholds), interfacePath,
326                     maxValue, minValue, timeoutSecs, readState);
327                 sensorEntry->initWriteHook(
328                     [&sensors, &reaperTimer](
329                         const std::chrono::steady_clock::time_point& now) {
330                         updateReaper(sensors, reaperTimer, now);
331                     });
332 
333                 if constexpr (debug)
334                 {
335                     lg2::error("ExternalSensor '{NAME}' created", "NAME",
336                                sensorName);
337                 }
338             }
339         });
340 
341     getter->getConfiguration(std::vector<std::string>{sensorType});
342 }
343 
main()344 int main()
345 {
346     if constexpr (debug)
347     {
348         lg2::error("ExternalSensor service starting up");
349     }
350 
351     boost::asio::io_context io;
352     auto systemBus = std::make_shared<sdbusplus::asio::connection>(io);
353     sdbusplus::asio::object_server objectServer(systemBus, true);
354 
355     objectServer.add_manager("/xyz/openbmc_project/sensors");
356     systemBus->request_name("xyz.openbmc_project.ExternalSensor");
357 
358     boost::container::flat_map<std::string, std::shared_ptr<ExternalSensor>>
359         sensors;
360     auto sensorsChanged =
361         std::make_shared<boost::container::flat_set<std::string>>();
362     boost::asio::steady_timer reaperTimer(io);
363 
364     boost::asio::post(io, [&objectServer, &sensors, &systemBus,
365                            &reaperTimer]() {
366         createSensors(objectServer, sensors, systemBus, nullptr, reaperTimer);
367     });
368 
369     boost::asio::steady_timer filterTimer(io);
370     std::function<void(sdbusplus::message_t&)> eventHandler =
371         [&objectServer, &sensors, &systemBus, &sensorsChanged, &filterTimer,
372          &reaperTimer](sdbusplus::message_t& message) mutable {
373             if (message.is_method_error())
374             {
375                 lg2::error("callback method error");
376                 return;
377             }
378 
379             const auto* messagePath = message.get_path();
380             sensorsChanged->insert(messagePath);
381             if constexpr (debug)
382             {
383                 lg2::error("ExternalSensor change event received: '{PATH}'",
384                            "PATH", messagePath);
385             }
386 
387             // this implicitly cancels the timer
388             filterTimer.expires_after(std::chrono::seconds(1));
389 
390             filterTimer.async_wait(
391                 [&objectServer, &sensors, &systemBus, &sensorsChanged,
392                  &reaperTimer](const boost::system::error_code& ec) mutable {
393                     if (ec != boost::system::errc::success)
394                     {
395                         if (ec != boost::asio::error::operation_aborted)
396                         {
397                             lg2::error("callback error: '{ERROR_MESSAGE}'",
398                                        "ERROR_MESSAGE", ec.message());
399                         }
400                         return;
401                     }
402 
403                     createSensors(objectServer, sensors, systemBus,
404                                   sensorsChanged, reaperTimer);
405                 });
406         };
407 
408     std::vector<std::unique_ptr<sdbusplus::bus::match_t>> matches =
409         setupPropertiesChangedMatches(
410             *systemBus, std::to_array<const char*>({sensorType}), eventHandler);
411 
412     if constexpr (debug)
413     {
414         lg2::error("ExternalSensor service entering main loop");
415     }
416 
417     io.run();
418 }
419