xref: /openbmc/dbus-sensors/src/IpmbSensor.cpp (revision 2aaf7175)
1 /*
2 // Copyright (c) 2019 Intel Corporation
3 //
4 // Licensed under the Apache License, Version 2.0 (the "License");
5 // you may not use this file except in compliance with the License.
6 // You may obtain a copy of the License at
7 //
8 //      http://www.apache.org/licenses/LICENSE-2.0
9 //
10 // Unless required by applicable law or agreed to in writing, software
11 // distributed under the License is distributed on an "AS IS" BASIS,
12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 // See the License for the specific language governing permissions and
14 // limitations under the License.
15 */
16 
17 #include "IpmbSensor.hpp"
18 
19 #include "IpmbSDRSensor.hpp"
20 #include "SensorPaths.hpp"
21 #include "Thresholds.hpp"
22 #include "Utils.hpp"
23 #include "VariantVisitors.hpp"
24 #include "sensor.hpp"
25 
26 #include <boost/asio/error.hpp>
27 #include <boost/asio/io_context.hpp>
28 #include <boost/asio/steady_timer.hpp>
29 #include <boost/container/flat_map.hpp>
30 #include <sdbusplus/asio/connection.hpp>
31 #include <sdbusplus/asio/object_server.hpp>
32 #include <sdbusplus/message.hpp>
33 #include <sdbusplus/message/native_types.hpp>
34 
35 #include <algorithm>
36 #include <array>
37 #include <chrono>
38 #include <cstddef>
39 #include <cstdint>
40 #include <iostream>
41 #include <limits>
42 #include <memory>
43 #include <stdexcept>
44 #include <string>
45 #include <tuple>
46 #include <utility>
47 #include <variant>
48 #include <vector>
49 
50 constexpr const bool debug = false;
51 
52 static constexpr double ipmbMaxReading = 0xFF;
53 static constexpr double ipmbMinReading = 0;
54 
55 static constexpr uint8_t meAddress = 1;
56 static constexpr uint8_t lun = 0;
57 static constexpr uint8_t hostSMbusIndexDefault = 0x03;
58 static constexpr uint8_t ipmbBusIndexDefault = 0;
59 static constexpr float pollRateDefault = 1; // in seconds
60 
61 static constexpr const char* sensorPathPrefix = "/xyz/openbmc_project/sensors/";
62 
IpmbSensor(std::shared_ptr<sdbusplus::asio::connection> & conn,boost::asio::io_context & io,const std::string & sensorName,const std::string & sensorConfiguration,sdbusplus::asio::object_server & objectServer,std::vector<thresholds::Threshold> && thresholdData,uint8_t deviceAddress,uint8_t hostSMbusIndex,const float pollRate,std::string & sensorTypeName)63 IpmbSensor::IpmbSensor(
64     std::shared_ptr<sdbusplus::asio::connection>& conn,
65     boost::asio::io_context& io, const std::string& sensorName,
66     const std::string& sensorConfiguration,
67     sdbusplus::asio::object_server& objectServer,
68     std::vector<thresholds::Threshold>&& thresholdData, uint8_t deviceAddress,
69     uint8_t hostSMbusIndex, const float pollRate, std::string& sensorTypeName) :
70     Sensor(escapeName(sensorName), std::move(thresholdData),
71            sensorConfiguration, "IpmbSensor", false, false, ipmbMaxReading,
72            ipmbMinReading, conn, PowerState::on),
73     deviceAddress(deviceAddress), hostSMbusIndex(hostSMbusIndex),
74     sensorPollMs(static_cast<int>(pollRate * 1000)), objectServer(objectServer),
75     waitTimer(io)
76 {
77     std::string dbusPath = sensorPathPrefix + sensorTypeName + "/" + name;
78 
79     sensorInterface = objectServer.add_interface(
80         dbusPath, "xyz.openbmc_project.Sensor.Value");
81 
82     for (const auto& threshold : thresholds)
83     {
84         std::string interface = thresholds::getInterface(threshold.level);
85         thresholdInterfaces[static_cast<size_t>(threshold.level)] =
86             objectServer.add_interface(dbusPath, interface);
87     }
88     association = objectServer.add_interface(dbusPath, association::interface);
89 }
90 
~IpmbSensor()91 IpmbSensor::~IpmbSensor()
92 {
93     waitTimer.cancel();
94     for (const auto& iface : thresholdInterfaces)
95     {
96         objectServer.remove_interface(iface);
97     }
98     objectServer.remove_interface(sensorInterface);
99     objectServer.remove_interface(association);
100 }
101 
getSubTypeUnits() const102 std::string IpmbSensor::getSubTypeUnits() const
103 {
104     switch (subType)
105     {
106         case IpmbSubType::temp:
107             return sensor_paths::unitDegreesC;
108         case IpmbSubType::curr:
109             return sensor_paths::unitAmperes;
110         case IpmbSubType::power:
111             return sensor_paths::unitWatts;
112         case IpmbSubType::volt:
113             return sensor_paths::unitVolts;
114         case IpmbSubType::util:
115             return sensor_paths::unitPercent;
116         default:
117             throw std::runtime_error("Invalid sensor type");
118     }
119 }
120 
init()121 void IpmbSensor::init()
122 {
123     loadDefaults();
124     setInitialProperties(getSubTypeUnits());
125     if (initCommand)
126     {
127         runInitCmd();
128     }
129     read();
130 }
131 
initCmdCb(const std::weak_ptr<IpmbSensor> & weakRef,const boost::system::error_code & ec,const IpmbMethodType & response)132 static void initCmdCb(const std::weak_ptr<IpmbSensor>& weakRef,
133                       const boost::system::error_code& ec,
134                       const IpmbMethodType& response)
135 {
136     std::shared_ptr<IpmbSensor> self = weakRef.lock();
137     if (!self)
138     {
139         return;
140     }
141     const int& status = std::get<0>(response);
142     if (ec || (status != 0))
143     {
144         std::cerr << "Error setting init command for device: " << self->name
145                   << "\n";
146     }
147 }
148 
runInitCmd()149 void IpmbSensor::runInitCmd()
150 {
151     if (!initCommand)
152     {
153         return;
154     }
155     dbusConnection->async_method_call(
156         [weakRef{weak_from_this()}](const boost::system::error_code& ec,
157                                     const IpmbMethodType& response) {
158             initCmdCb(weakRef, ec, response);
159         },
160         "xyz.openbmc_project.Ipmi.Channel.Ipmb",
161         "/xyz/openbmc_project/Ipmi/Channel/Ipmb", "org.openbmc.Ipmb",
162         "sendRequest", commandAddress, netfn, lun, *initCommand, initData);
163 }
164 
loadDefaults()165 void IpmbSensor::loadDefaults()
166 {
167     if (type == IpmbType::meSensor)
168     {
169         commandAddress = meAddress;
170         netfn = ipmi::sensor::netFn;
171         command = ipmi::sensor::getSensorReading;
172         commandData = {deviceAddress};
173         readingFormat = ReadingFormat::byte0;
174     }
175     else if (type == IpmbType::PXE1410CVR)
176     {
177         commandAddress = meAddress;
178         netfn = ipmi::me_bridge::netFn;
179         command = ipmi::me_bridge::sendRawPmbus;
180         initCommand = ipmi::me_bridge::sendRawPmbus;
181         // pmbus read temp
182         commandData = {0x57,          0x01, 0x00, 0x16, hostSMbusIndex,
183                        deviceAddress, 0x00, 0x00, 0x00, 0x00,
184                        0x01,          0x02, 0x8d};
185         // goto page 0
186         initData = {0x57,          0x01, 0x00, 0x14, hostSMbusIndex,
187                     deviceAddress, 0x00, 0x00, 0x00, 0x00,
188                     0x02,          0x00, 0x00, 0x00};
189         readingFormat = ReadingFormat::linearElevenBit;
190     }
191     else if (type == IpmbType::IR38363VR)
192     {
193         commandAddress = meAddress;
194         netfn = ipmi::me_bridge::netFn;
195         command = ipmi::me_bridge::sendRawPmbus;
196         // pmbus read temp
197         commandData = {0x57,          0x01, 0x00, 0x16, hostSMbusIndex,
198                        deviceAddress, 00,   0x00, 0x00, 0x00,
199                        0x01,          0x02, 0x8D};
200         readingFormat = ReadingFormat::elevenBitShift;
201     }
202     else if (type == IpmbType::ADM1278HSC)
203     {
204         commandAddress = meAddress;
205         uint8_t snsNum = 0;
206         switch (subType)
207         {
208             case IpmbSubType::temp:
209             case IpmbSubType::curr:
210                 if (subType == IpmbSubType::temp)
211                 {
212                     snsNum = 0x8d;
213                 }
214                 else
215                 {
216                     snsNum = 0x8c;
217                 }
218                 netfn = ipmi::me_bridge::netFn;
219                 command = ipmi::me_bridge::sendRawPmbus;
220                 commandData = {0x57, 0x01, 0x00, 0x86, deviceAddress,
221                                0x00, 0x00, 0x01, 0x02, snsNum};
222                 readingFormat = ReadingFormat::elevenBit;
223                 break;
224             case IpmbSubType::power:
225             case IpmbSubType::volt:
226                 netfn = ipmi::sensor::netFn;
227                 command = ipmi::sensor::getSensorReading;
228                 commandData = {deviceAddress};
229                 readingFormat = ReadingFormat::byte0;
230                 break;
231             default:
232                 throw std::runtime_error("Invalid sensor type");
233         }
234     }
235     else if (type == IpmbType::mpsVR)
236     {
237         commandAddress = meAddress;
238         netfn = ipmi::me_bridge::netFn;
239         command = ipmi::me_bridge::sendRawPmbus;
240         initCommand = ipmi::me_bridge::sendRawPmbus;
241         // pmbus read temp
242         commandData = {0x57,          0x01, 0x00, 0x16, hostSMbusIndex,
243                        deviceAddress, 0x00, 0x00, 0x00, 0x00,
244                        0x01,          0x02, 0x8d};
245         // goto page 0
246         initData = {0x57,          0x01, 0x00, 0x14, hostSMbusIndex,
247                     deviceAddress, 0x00, 0x00, 0x00, 0x00,
248                     0x02,          0x00, 0x00, 0x00};
249         readingFormat = ReadingFormat::byte3;
250     }
251     else if (type == IpmbType::SMPro)
252     {
253         // This is an Ampere SMPro reachable via a BMC.  For example,
254         // this architecture is used on ADLINK Ampere Altra systems.
255         // See the Ampere Family SoC BMC Interface Specification at
256         // https://amperecomputing.com/customer-connect/products/altra-family-software---firmware
257         // for details of the sensors.
258         commandAddress = 0;
259         netfn = 0x30;
260         command = 0x31;
261         commandData = {0x9e, deviceAddress};
262         switch (subType)
263         {
264             case IpmbSubType::temp:
265                 readingFormat = ReadingFormat::nineBit;
266                 break;
267             case IpmbSubType::power:
268                 readingFormat = ReadingFormat::tenBit;
269                 break;
270             case IpmbSubType::curr:
271             case IpmbSubType::volt:
272                 readingFormat = ReadingFormat::fifteenBit;
273                 break;
274             default:
275                 throw std::runtime_error("Invalid sensor type");
276         }
277     }
278     else
279     {
280         throw std::runtime_error("Invalid sensor type");
281     }
282 
283     if (subType == IpmbSubType::util)
284     {
285         // Utilization need to be scaled to percent
286         maxValue = 100;
287         minValue = 0;
288     }
289 }
290 
checkThresholds()291 void IpmbSensor::checkThresholds()
292 {
293     thresholds::checkThresholds(this);
294 }
295 
processReading(ReadingFormat readingFormat,uint8_t command,const std::vector<uint8_t> & data,double & resp,size_t errCount)296 bool IpmbSensor::processReading(ReadingFormat readingFormat, uint8_t command,
297                                 const std::vector<uint8_t>& data, double& resp,
298                                 size_t errCount)
299 {
300     switch (readingFormat)
301     {
302         case (ReadingFormat::byte0):
303         {
304             if (command == ipmi::sensor::getSensorReading &&
305                 !ipmi::sensor::isValid(data))
306             {
307                 return false;
308             }
309             resp = data[0];
310             return true;
311         }
312         case (ReadingFormat::byte3):
313         {
314             if (data.size() < 4)
315             {
316                 if (errCount == 0U)
317                 {
318                     std::cerr << "Invalid data length returned\n";
319                 }
320                 return false;
321             }
322             resp = data[3];
323             return true;
324         }
325         case (ReadingFormat::nineBit):
326         case (ReadingFormat::tenBit):
327         case (ReadingFormat::fifteenBit):
328         {
329             if (data.size() != 2)
330             {
331                 if (errCount == 0U)
332                 {
333                     std::cerr << "Invalid data length returned\n";
334                 }
335                 return false;
336             }
337 
338             // From the Altra Family SoC BMC Interface Specification:
339             // 0xFFFF – This sensor data is either missing or is not supported
340             // by the device.
341             if ((data[0] == 0xff) && (data[1] == 0xff))
342             {
343                 return false;
344             }
345 
346             if (readingFormat == ReadingFormat::nineBit)
347             {
348                 int16_t value = data[0];
349                 if ((data[1] & 0x1) != 0)
350                 {
351                     // Sign extend to 16 bits
352                     value |= 0xFF00;
353                 }
354                 resp = value;
355             }
356             else if (readingFormat == ReadingFormat::tenBit)
357             {
358                 uint16_t value = ((data[1] & 0x3) << 8) + data[0];
359                 resp = value;
360             }
361             else if (readingFormat == ReadingFormat::fifteenBit)
362             {
363                 uint16_t value = ((data[1] & 0x7F) << 8) + data[0];
364                 // Convert mV to V
365                 resp = value / 1000.0;
366             }
367 
368             return true;
369         }
370         case (ReadingFormat::elevenBit):
371         {
372             if (data.size() < 5)
373             {
374                 if (errCount == 0U)
375                 {
376                     std::cerr << "Invalid data length returned\n";
377                 }
378                 return false;
379             }
380 
381             int16_t value = ((data[4] << 8) | data[3]);
382             resp = value;
383             return true;
384         }
385         case (ReadingFormat::elevenBitShift):
386         {
387             if (data.size() < 5)
388             {
389                 if (errCount == 0U)
390                 {
391                     std::cerr << "Invalid data length returned\n";
392                 }
393                 return false;
394             }
395 
396             resp = ((data[4] << 8) | data[3]) >> 3;
397             return true;
398         }
399         case (ReadingFormat::linearElevenBit):
400         {
401             if (data.size() < 5)
402             {
403                 if (errCount == 0U)
404                 {
405                     std::cerr << "Invalid data length returned\n";
406                 }
407                 return false;
408             }
409 
410             int16_t value = ((data[4] << 8) | data[3]);
411             constexpr const size_t shift = 16 - 11; // 11bit into 16bit
412             value <<= shift;
413             value >>= shift;
414             resp = value;
415             return true;
416         }
417         default:
418             throw std::runtime_error("Invalid reading type");
419     }
420 }
421 
ipmbRequestCompletionCb(const boost::system::error_code & ec,const IpmbMethodType & response)422 void IpmbSensor::ipmbRequestCompletionCb(const boost::system::error_code& ec,
423                                          const IpmbMethodType& response)
424 {
425     const int& status = std::get<0>(response);
426     if (ec || (status != 0))
427     {
428         incrementError();
429         read();
430         return;
431     }
432     const std::vector<uint8_t>& data = std::get<5>(response);
433     if constexpr (debug)
434     {
435         std::cout << name << ": ";
436         for (size_t d : data)
437         {
438             std::cout << d << " ";
439         }
440         std::cout << "\n";
441     }
442     if (data.empty())
443     {
444         incrementError();
445         read();
446         return;
447     }
448 
449     double value = 0;
450 
451     if (!processReading(readingFormat, command, data, value, errCount))
452     {
453         incrementError();
454         read();
455         return;
456     }
457 
458     // rawValue only used in debug logging
459     // up to 5th byte in data are used to derive value
460     size_t end = std::min(sizeof(uint64_t), data.size());
461     uint64_t rawData = 0;
462     for (size_t i = 0; i < end; i++)
463     {
464         // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
465         reinterpret_cast<uint8_t*>(&rawData)[i] = data[i];
466     }
467     rawValue = static_cast<double>(rawData);
468 
469     /* Adjust value as per scale and offset */
470     value = (value * scaleVal) + offsetVal;
471     updateValue(value);
472     read();
473 }
474 
read()475 void IpmbSensor::read()
476 {
477     waitTimer.expires_after(std::chrono::milliseconds(sensorPollMs));
478     waitTimer.async_wait(
479         [weakRef{weak_from_this()}](const boost::system::error_code& ec) {
480             if (ec == boost::asio::error::operation_aborted)
481             {
482                 return; // we're being canceled
483             }
484             std::shared_ptr<IpmbSensor> self = weakRef.lock();
485             if (!self)
486             {
487                 return;
488             }
489             self->sendIpmbRequest();
490         });
491 }
492 
sendIpmbRequest()493 void IpmbSensor::sendIpmbRequest()
494 {
495     if (!readingStateGood())
496     {
497         updateValue(std::numeric_limits<double>::quiet_NaN());
498         read();
499         return;
500     }
501     dbusConnection->async_method_call(
502         [weakRef{weak_from_this()}](boost::system::error_code ec,
503                                     const IpmbMethodType& response) {
504             std::shared_ptr<IpmbSensor> self = weakRef.lock();
505             if (!self)
506             {
507                 return;
508             }
509             self->ipmbRequestCompletionCb(ec, response);
510         },
511         "xyz.openbmc_project.Ipmi.Channel.Ipmb",
512         "/xyz/openbmc_project/Ipmi/Channel/Ipmb", "org.openbmc.Ipmb",
513         "sendRequest", commandAddress, netfn, lun, command, commandData);
514 }
515 
sensorClassType(const std::string & sensorClass)516 bool IpmbSensor::sensorClassType(const std::string& sensorClass)
517 {
518     if (sensorClass == "PxeBridgeTemp")
519     {
520         type = IpmbType::PXE1410CVR;
521     }
522     else if (sensorClass == "IRBridgeTemp")
523     {
524         type = IpmbType::IR38363VR;
525     }
526     else if (sensorClass == "HSCBridge")
527     {
528         type = IpmbType::ADM1278HSC;
529     }
530     else if (sensorClass == "MpsBridgeTemp")
531     {
532         type = IpmbType::mpsVR;
533     }
534     else if (sensorClass == "METemp" || sensorClass == "MESensor")
535     {
536         type = IpmbType::meSensor;
537     }
538     else if (sensorClass == "SMPro")
539     {
540         type = IpmbType::SMPro;
541     }
542     else
543     {
544         std::cerr << "Invalid class " << sensorClass << "\n";
545         return false;
546     }
547     return true;
548 }
549 
sensorSubType(const std::string & sensorTypeName)550 void IpmbSensor::sensorSubType(const std::string& sensorTypeName)
551 {
552     if (sensorTypeName == "voltage")
553     {
554         subType = IpmbSubType::volt;
555     }
556     else if (sensorTypeName == "power")
557     {
558         subType = IpmbSubType::power;
559     }
560     else if (sensorTypeName == "current")
561     {
562         subType = IpmbSubType::curr;
563     }
564     else if (sensorTypeName == "utilization")
565     {
566         subType = IpmbSubType::util;
567     }
568     else
569     {
570         subType = IpmbSubType::temp;
571     }
572 }
573 
parseConfigValues(const SensorBaseConfigMap & entry)574 void IpmbSensor::parseConfigValues(const SensorBaseConfigMap& entry)
575 {
576     auto findScaleVal = entry.find("ScaleValue");
577     if (findScaleVal != entry.end())
578     {
579         scaleVal = std::visit(VariantToDoubleVisitor(), findScaleVal->second);
580     }
581 
582     auto findOffsetVal = entry.find("OffsetValue");
583     if (findOffsetVal != entry.end())
584     {
585         offsetVal = std::visit(VariantToDoubleVisitor(), findOffsetVal->second);
586     }
587 
588     readState = getPowerState(entry);
589 }
590 
createSensors(boost::asio::io_context & io,sdbusplus::asio::object_server & objectServer,boost::container::flat_map<std::string,std::shared_ptr<IpmbSensor>> & sensors,std::shared_ptr<sdbusplus::asio::connection> & dbusConnection)591 void createSensors(
592     boost::asio::io_context& io, sdbusplus::asio::object_server& objectServer,
593     boost::container::flat_map<std::string, std::shared_ptr<IpmbSensor>>&
594         sensors,
595     std::shared_ptr<sdbusplus::asio::connection>& dbusConnection)
596 {
597     if (!dbusConnection)
598     {
599         std::cerr << "Connection not created\n";
600         return;
601     }
602     dbusConnection->async_method_call(
603         [&](boost::system::error_code ec, const ManagedObjectType& resp) {
604             if (ec)
605             {
606                 std::cerr << "Error contacting entity manager\n";
607                 return;
608             }
609             for (const auto& [path, interfaces] : resp)
610             {
611                 for (const auto& [intf, cfg] : interfaces)
612                 {
613                     if (intf != configInterfaceName(sensorType))
614                     {
615                         continue;
616                     }
617                     std::string name = loadVariant<std::string>(cfg, "Name");
618 
619                     std::vector<thresholds::Threshold> sensorThresholds;
620                     if (!parseThresholdsFromConfig(interfaces,
621                                                    sensorThresholds))
622                     {
623                         std::cerr
624                             << "error populating thresholds " << name << "\n";
625                     }
626                     uint8_t deviceAddress =
627                         loadVariant<uint8_t>(cfg, "Address");
628 
629                     std::string sensorClass =
630                         loadVariant<std::string>(cfg, "Class");
631 
632                     uint8_t hostSMbusIndex = hostSMbusIndexDefault;
633                     auto findSmType = cfg.find("HostSMbusIndex");
634                     if (findSmType != cfg.end())
635                     {
636                         hostSMbusIndex = std::visit(
637                             VariantToUnsignedIntVisitor(), findSmType->second);
638                     }
639 
640                     float pollRate = getPollRate(cfg, pollRateDefault);
641 
642                     uint8_t ipmbBusIndex = ipmbBusIndexDefault;
643                     auto findBusType = cfg.find("Bus");
644                     if (findBusType != cfg.end())
645                     {
646                         ipmbBusIndex = std::visit(VariantToUnsignedIntVisitor(),
647                                                   findBusType->second);
648                         std::cerr << "Ipmb Bus Index for " << name << " is "
649                                   << static_cast<int>(ipmbBusIndex) << "\n";
650                     }
651 
652                     /* Default sensor type is "temperature" */
653                     std::string sensorTypeName = "temperature";
654                     auto findType = cfg.find("SensorType");
655                     if (findType != cfg.end())
656                     {
657                         sensorTypeName = std::visit(VariantToStringVisitor(),
658                                                     findType->second);
659                     }
660 
661                     auto& sensor = sensors[name];
662                     sensor = nullptr;
663                     sensor = std::make_shared<IpmbSensor>(
664                         dbusConnection, io, name, path, objectServer,
665                         std::move(sensorThresholds), deviceAddress,
666                         hostSMbusIndex, pollRate, sensorTypeName);
667 
668                     sensor->parseConfigValues(cfg);
669                     if (!(sensor->sensorClassType(sensorClass)))
670                     {
671                         continue;
672                     }
673                     sensor->sensorSubType(sensorTypeName);
674                     sensor->init();
675                 }
676             }
677         },
678         entityManagerName, "/xyz/openbmc_project/inventory",
679         "org.freedesktop.DBus.ObjectManager", "GetManagedObjects");
680 }
681 
interfaceRemoved(sdbusplus::message_t & message,boost::container::flat_map<std::string,std::shared_ptr<IpmbSensor>> & sensors)682 void interfaceRemoved(
683     sdbusplus::message_t& message,
684     boost::container::flat_map<std::string, std::shared_ptr<IpmbSensor>>&
685         sensors)
686 {
687     if (message.is_method_error())
688     {
689         std::cerr << "interfacesRemoved callback method error\n";
690         return;
691     }
692 
693     sdbusplus::message::object_path removedPath;
694     std::vector<std::string> interfaces;
695 
696     message.read(removedPath, interfaces);
697 
698     // If the xyz.openbmc_project.Confguration.X interface was removed
699     // for one or more sensors, delete those sensor objects.
700     auto sensorIt = sensors.begin();
701     while (sensorIt != sensors.end())
702     {
703         if ((sensorIt->second->configurationPath == removedPath) &&
704             (std::find(interfaces.begin(), interfaces.end(),
705                        configInterfaceName(sdrInterface)) != interfaces.end()))
706         {
707             sensorIt = sensors.erase(sensorIt);
708         }
709         else
710         {
711             sensorIt++;
712         }
713     }
714 }
715