| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | /* | ||
| 2 | * This file is part of the Falcon Player (FPP) and is Copyright (C) | ||
| 3 | * 2013-2022 by the Falcon Player Developers. | ||
| 4 | * | ||
| 5 | * The Falcon Player (FPP) is free software, and is covered under | ||
| 6 | * multiple Open Source licenses. Please see the included 'LICENSES' | ||
| 7 | * file for descriptions of what files are covered by each license. | ||
| 8 | * | ||
| 9 | * This source file is covered under the LGPL v2.1 as described in the | ||
| 10 | * included LICENSE.LGPL file. | ||
| 11 | */ | ||
| 12 | |||
| 13 | #include "fpp-pch.h" | ||
| 14 | |||
| 15 | #include "fpp-json.h" | ||
| 16 | |||
| 17 | #include <sys/ioctl.h> | ||
| 18 | #include <sys/stat.h> | ||
| 19 | #include <fcntl.h> | ||
| 20 | #include <fstream> | ||
| 21 | #include <iomanip> | ||
| 22 | #include <set> | ||
| 23 | #include <thread> | ||
| 24 | #include <unistd.h> | ||
| 25 | |||
| 26 | #include "../Warnings.h" | ||
| 27 | #include "../common.h" | ||
| 28 | #include "../settings.h" | ||
| 29 | |||
| 30 | #include "ADS7828.h" | ||
| 31 | #include "IIOSensorSource.h" | ||
| 32 | #include "MultiSensorSource.h" | ||
| 33 | #include "MuxSensorSource.h" | ||
| 34 | #include "util/I2CUtils.h" | ||
| 35 | |||
| 36 | #include "Sensors.h" | ||
| 37 | |||
| 38 | #if defined(PLATFORM_BBB) || defined(PLATFORM_BB64) | ||
| 39 | #define I2C_DEV 2 | ||
| 40 | #else | ||
| 41 | #define I2C_DEV 1 | ||
| 42 | #endif | ||
| 43 | |||
| 44 | 2 | static std::string exec(const std::string& cmd) { | |
| 45 | std::array<char, 128> buffer; | ||
| 46 | 2 | std::string result; | |
| 47 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | struct PipeCloser { void operator()(FILE* f) const { if (f) pclose(f); } }; |
| 48 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | std::unique_ptr<FILE, PipeCloser> pipe(popen(cmd.c_str(), "r")); |
| 49 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
|
2 | if (!pipe) { |
| 50 | ✗ | throw std::runtime_error("popen() failed!"); | |
| 51 | } | ||
| 52 |
3/4✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 2 times.
|
12 | while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) { |
| 53 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | result += buffer.data(); |
| 54 | } | ||
| 55 | 2 | return result; | |
| 56 | 2 | } | |
| 57 | |||
| 58 | 15 | SensorSource::SensorSource(Json::Value& s) { | |
| 59 |
2/4✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
|
15 | if (s.isMember("id")) { |
| 60 |
2/4✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
|
15 | _id = s["id"].asString(); |
| 61 | ✗ | } else if (s.isMember("type")) { | |
| 62 | ✗ | _id = s["type"].asString(); | |
| 63 | } | ||
| 64 | 15 | } | |
| 65 | |||
| 66 | class Sensor { | ||
| 67 | public: | ||
| 68 | 364 | explicit Sensor(Json::Value& s) : | |
| 69 | 364 | label(s["label"].asString()) { | |
| 70 |
2/4✓ Branch 0 taken 363 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 363 times.
|
363 | if (s.isMember("prefix")) { |
| 71 | ✗ | prefix = s["prefix"].asString(); | |
| 72 | } | ||
| 73 |
3/4✓ Branch 0 taken 363 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 62 times.
✓ Branch 3 taken 301 times.
|
363 | if (s.isMember("postfix")) { |
| 74 |
2/4✓ Branch 0 taken 62 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 62 times.
✗ Branch 3 not taken.
|
62 | postfix = s["postfix"].asString(); |
| 75 | } | ||
| 76 |
2/4✓ Branch 0 taken 363 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 363 times.
|
363 | if (s.isMember("precision")) { |
| 77 | ✗ | precision = s["precision"].asInt(); | |
| 78 | } | ||
| 79 |
3/4✓ Branch 0 taken 363 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 361 times.
✓ Branch 3 taken 2 times.
|
363 | if (s.isMember("valueType")) { |
| 80 |
2/4✓ Branch 0 taken 361 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 361 times.
✗ Branch 3 not taken.
|
361 | valueType = s["valueType"].asString(); |
| 81 | } | ||
| 82 | 363 | } | |
| 83 | 726 | virtual ~Sensor() {} | |
| 84 | |||
| 85 | Sensor(Sensor const&) = delete; | ||
| 86 | void operator=(Sensor const& x) = delete; | ||
| 87 | |||
| 88 | virtual double getValue() = 0; | ||
| 89 | 4189 | virtual SensorSource* getSensorSource() { | |
| 90 | 4189 | return nullptr; | |
| 91 | } | ||
| 92 | |||
| 93 | 6644 | void report(Json::Value& s) { | |
| 94 |
1/2✓ Branch 0 taken 6644 times.
✗ Branch 1 not taken.
|
6644 | std::unique_lock<std::mutex> lock(sensorLock); |
| 95 |
1/2✓ Branch 0 taken 6644 times.
✗ Branch 1 not taken.
|
6644 | Json::Value v; |
| 96 |
2/4✓ Branch 0 taken 6644 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6644 times.
✗ Branch 3 not taken.
|
6644 | v["label"] = label; |
| 97 |
2/4✓ Branch 0 taken 6644 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6644 times.
✗ Branch 3 not taken.
|
6644 | v["prefix"] = prefix; |
| 98 |
2/4✓ Branch 0 taken 6644 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6644 times.
✗ Branch 3 not taken.
|
6644 | v["postfix"] = postfix; |
| 99 |
1/2✓ Branch 0 taken 6644 times.
✗ Branch 1 not taken.
|
6644 | double d = getValue(); |
| 100 |
2/4✓ Branch 0 taken 6644 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6644 times.
✗ Branch 3 not taken.
|
6644 | v["value"] = d; |
| 101 |
2/4✓ Branch 0 taken 6644 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6644 times.
✗ Branch 3 not taken.
|
6644 | v["valueType"] = valueType; |
| 102 | |||
| 103 |
1/2✓ Branch 0 taken 6644 times.
✗ Branch 1 not taken.
|
6644 | std::ostringstream stringStream; |
| 104 |
1/2✓ Branch 0 taken 6644 times.
✗ Branch 1 not taken.
|
6644 | stringStream << prefix |
| 105 |
1/2✓ Branch 0 taken 6644 times.
✗ Branch 1 not taken.
|
6644 | << std::fixed << std::setprecision(precision) |
| 106 |
1/2✓ Branch 0 taken 6644 times.
✗ Branch 1 not taken.
|
6644 | << d |
| 107 |
1/2✓ Branch 0 taken 6644 times.
✗ Branch 1 not taken.
|
6644 | << postfix; |
| 108 |
3/6✓ Branch 0 taken 6644 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6644 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 6644 times.
✗ Branch 5 not taken.
|
6644 | v["formatted"] = stringStream.str(); |
| 109 |
1/2✓ Branch 0 taken 6644 times.
✗ Branch 1 not taken.
|
6644 | s.append(v); |
| 110 | 6644 | } | |
| 111 | std::string label; | ||
| 112 | std::string prefix; | ||
| 113 | std::string postfix; | ||
| 114 | std::string valueType; | ||
| 115 | int precision = 1; | ||
| 116 | std::mutex sensorLock; | ||
| 117 | }; | ||
| 118 | |||
| 119 | // I2C sensor addresses are hex with an optional 0x prefix (e.g. "0x48"). | ||
| 120 | // Manual checks (no new headers): anything else cannot be an address and must | ||
| 121 | // never reach the shell command built in HasI2CDevice() below. | ||
| 122 | 4 | static bool isValidI2CAddress(const std::string& a) { | |
| 123 | 4 | size_t i = 0; | |
| 124 |
4/10✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 4 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✓ Branch 8 taken 4 times.
✗ Branch 9 not taken.
|
4 | if (a.size() > 2 && a[0] == '0' && (a[1] == 'x' || a[1] == 'X')) { |
| 125 | 4 | i = 2; | |
| 126 | } | ||
| 127 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
|
4 | if (i == a.size()) { |
| 128 | ✗ | return false; | |
| 129 | } | ||
| 130 |
2/2✓ Branch 0 taken 10 times.
✓ Branch 1 taken 2 times.
|
12 | for (; i < a.size(); i++) { |
| 131 | 10 | char c = a[i]; | |
| 132 |
5/12✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 8 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 2 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 2 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
|
10 | if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) { |
| 133 | 2 | return false; | |
| 134 | } | ||
| 135 | } | ||
| 136 | 2 | return true; | |
| 137 | } | ||
| 138 | |||
| 139 | class I2CSensor : public Sensor { | ||
| 140 | public: | ||
| 141 | 2 | explicit I2CSensor(Json::Value& s) : | |
| 142 | Sensor(s), | ||
| 143 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | address(s["address"].asString()), |
| 144 |
2/4✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
|
2 | path(s["path"].asString()), |
| 145 |
2/4✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
|
2 | driver(s["driver"].asString()), |
| 146 |
1/2✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
|
2 | file(-1) { |
| 147 |
2/4✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
|
2 | if (s.isMember("multiplier")) { |
| 148 | ✗ | multiplier = s["multiplier"].asDouble(); | |
| 149 | } | ||
| 150 | 2 | int bus = I2C_DEV; | |
| 151 |
2/4✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
|
2 | if (!CheckForFile(path)) { |
| 152 | // path doesn't exist, need to enable | ||
| 153 |
2/8✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 2 times.
|
2 | if (bus == 2 && !HasI2CDevice(bus)) { |
| 154 | // wasn't found on the default bus of the BBB, we can | ||
| 155 | // try the other i2c bus | ||
| 156 | ✗ | bus = 1; | |
| 157 | ✗ | size_t start_pos = path.find("/i2c-2"); | |
| 158 | ✗ | if (start_pos != std::string::npos) { | |
| 159 | ✗ | path.replace(start_pos, 6, "/i2c-1"); | |
| 160 | } | ||
| 161 | } | ||
| 162 |
2/4✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
|
2 | if (HasI2CDevice(bus)) { |
| 163 | ✗ | std::ofstream out("/sys/bus/i2c/devices/i2c-" + std::to_string(bus) + "/new_device"); | |
| 164 | ✗ | out << driver << " " << address; | |
| 165 | ✗ | out.close(); | |
| 166 | ✗ | } | |
| 167 | } | ||
| 168 | 2 | int count = 0; | |
| 169 |
6/8✓ Branch 0 taken 10 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 10 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 10 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 10 times.
✓ Branch 7 taken 2 times.
|
12 | while (count < 5 && !CheckForFile(path)) { |
| 170 |
1/2✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
|
10 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); |
| 171 | 10 | count++; | |
| 172 | } | ||
| 173 | // after 0.5 seconds, still doesn't exist | ||
| 174 |
2/4✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
|
2 | if (CheckForFile(path)) { |
| 175 | ✗ | file = open(path.c_str(), O_RDONLY); | |
| 176 |
3/10✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 2 times.
|
2 | } else if (HasI2CDevice(bus) && driver == "lm75") { |
| 177 | // Kernel 4.19 on Pi seems to not like the lm75 chips, we'll need to read the values directly | ||
| 178 | // switch to rawIO | ||
| 179 | ✗ | int add = strtol(address.c_str(), nullptr, 16); | |
| 180 | ✗ | i2cUtils = new I2CUtils(bus, add); | |
| 181 | } | ||
| 182 | 2 | } | |
| 183 | 14 | bool CheckForFile(std::string& path) { | |
| 184 |
1/2✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
|
14 | if (path.find("hwmon0") == std::string::npos) { |
| 185 |
1/2✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
|
14 | return FileExists(path); |
| 186 | } | ||
| 187 | // if it's a hwmon path, it MAY be on a different hwmon depending on the order that hwmon | ||
| 188 | // loads the drivers | ||
| 189 | ✗ | std::string base = path.substr(0, path.find("hwmon0")); | |
| 190 | ✗ | std::string next = path.substr(path.find("hwmon0") + 6); | |
| 191 | ✗ | if (!DirectoryExists(base.c_str())) { | |
| 192 | ✗ | return false; | |
| 193 | } | ||
| 194 | ✗ | for (int x = 0; x < 10; x++) { | |
| 195 | ✗ | std::string npath = base + "hwmon" + std::to_string(x) + next; | |
| 196 | ✗ | if (FileExists(npath)) { | |
| 197 | ✗ | path = npath; | |
| 198 | ✗ | return true; | |
| 199 | } | ||
| 200 | ✗ | } | |
| 201 | ✗ | return false; | |
| 202 | ✗ | } | |
| 203 | |||
| 204 | 8 | virtual ~I2CSensor() { | |
| 205 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
|
4 | if (file != -1) { |
| 206 | ✗ | close(file); | |
| 207 | } | ||
| 208 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
|
4 | if (i2cUtils) { |
| 209 | ✗ | delete i2cUtils; | |
| 210 | } | ||
| 211 | 8 | } | |
| 212 | 4 | bool HasI2CDevice(int i2cBus) { | |
| 213 | // address comes from operator-editable sensors.json and is | ||
| 214 | // interpolated unquoted into a shell below. I2C addresses are hex | ||
| 215 | // (e.g. "0x48"), so anything else is rejected here; a rejection reads | ||
| 216 | // as "no device", the same outcome as a genuinely absent chip, and | ||
| 217 | // the bus-fallback/rawIO branches keep working off that signal. | ||
| 218 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
|
4 | if (!isValidI2CAddress(address)) { |
| 219 | 2 | return false; | |
| 220 | } | ||
| 221 | char buf[256]; | ||
| 222 | 2 | snprintf(buf, sizeof(buf), "i2cdetect -y -r %d %s %s", i2cBus, address.c_str(), address.c_str()); | |
| 223 |
2/4✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
|
2 | std::string result = exec(buf); |
| 224 | 2 | return result.find("--") == std::string::npos; | |
| 225 | 2 | } | |
| 226 | |||
| 227 | 3 | virtual double getValue() override { | |
| 228 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
|
3 | if (i2cUtils) { |
| 229 | ✗ | int ret = i2cUtils->readWordData(0); | |
| 230 | ✗ | if (ret >= 0) { | |
| 231 | ✗ | int t = (ret >> 8) & 0xFF; | |
| 232 | ✗ | t |= (ret << 8) & 0xFF00; | |
| 233 | ✗ | if (t & 0x8000) { | |
| 234 | // negative temperature | ||
| 235 | ✗ | t |= 0xFFFF0000; | |
| 236 | } | ||
| 237 | ✗ | t = t >> 5; | |
| 238 | ✗ | double d = t; | |
| 239 | ✗ | d *= 0.125; | |
| 240 | ✗ | return d; | |
| 241 | } | ||
| 242 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
|
3 | } else if (file != -1) { |
| 243 | ✗ | lseek(file, 0, SEEK_SET); | |
| 244 | char buffer[20]; | ||
| 245 | // Leave room for the terminator; a failed read must not index at | ||
| 246 | // [-1] and returns the same 0.0 as a missing file below. | ||
| 247 | ✗ | int i = read(file, buffer, sizeof(buffer) - 1); | |
| 248 | ✗ | if (i <= 0) { | |
| 249 | ✗ | return 0.0; | |
| 250 | } | ||
| 251 | ✗ | buffer[i] = 0; | |
| 252 | ✗ | double d = atof(buffer); | |
| 253 | ✗ | d *= multiplier; | |
| 254 | ✗ | return d; | |
| 255 | } | ||
| 256 | 3 | return 0.0; | |
| 257 | } | ||
| 258 | |||
| 259 | std::string address; | ||
| 260 | std::string path; | ||
| 261 | std::string driver; | ||
| 262 | double multiplier = 1.0; | ||
| 263 | |||
| 264 | I2CUtils* i2cUtils = nullptr; | ||
| 265 | volatile int file; | ||
| 266 | }; | ||
| 267 | |||
| 268 | class SensorSourceSensor : public Sensor { | ||
| 269 | public: | ||
| 270 | 22 | explicit SensorSourceSensor(Json::Value& s) : | |
| 271 | 22 | Sensor(s) { | |
| 272 |
2/4✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 21 times.
|
21 | if (s.isMember("scale")) { |
| 273 | ✗ | scale = s["scale"].asDouble(); | |
| 274 | } | ||
| 275 |
2/4✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 21 times.
|
21 | if (s.isMember("offset")) { |
| 276 | ✗ | offset = s["offset"].asDouble(); | |
| 277 | } | ||
| 278 |
2/4✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 21 times.
|
21 | if (s.isMember("min")) { |
| 279 | ✗ | min = s["min"].asDouble(); | |
| 280 | ✗ | isMinMax = true; | |
| 281 | } | ||
| 282 |
2/4✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 21 times.
|
21 | if (s.isMember("max")) { |
| 283 | ✗ | max = s["max"].asDouble(); | |
| 284 | ✗ | isMinMax = true; | |
| 285 | } | ||
| 286 |
2/4✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 21 times.
✗ Branch 3 not taken.
|
21 | if (s.isMember("channel")) { |
| 287 |
2/4✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 21 times.
✗ Branch 3 not taken.
|
21 | channel = s["channel"].asInt(); |
| 288 | } | ||
| 289 |
3/6✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 21 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 21 times.
✗ Branch 5 not taken.
|
21 | source = Sensors::INSTANCE.getSensorSource(s["sensor"].asString()); |
| 290 |
2/2✓ Branch 0 taken 18 times.
✓ Branch 1 taken 3 times.
|
21 | if (source) { |
| 291 |
1/2✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
|
18 | source->enable(channel); |
| 292 | } | ||
| 293 | 21 | } | |
| 294 | 84 | virtual ~SensorSourceSensor() { | |
| 295 | 84 | } | |
| 296 | 2455 | virtual SensorSource* getSensorSource() override { | |
| 297 | 2455 | return source; | |
| 298 | } | ||
| 299 | 2455 | virtual double getValue() override { | |
| 300 |
2/2✓ Branch 0 taken 2449 times.
✓ Branch 1 taken 6 times.
|
2455 | if (source) { |
| 301 | 2449 | double d = source->getValue(channel); | |
| 302 | 2449 | d -= offset; | |
| 303 | 2449 | d *= scale; | |
| 304 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 2449 times.
|
2449 | if (isMinMax) { |
| 305 | ✗ | d *= (max - min + 1); | |
| 306 | ✗ | d += min; | |
| 307 | } | ||
| 308 | 2449 | return d; | |
| 309 | } | ||
| 310 | 6 | return 0.0f; | |
| 311 | } | ||
| 312 | SensorSource* source = nullptr; | ||
| 313 | double scale = 1.0; | ||
| 314 | double offset = 0.0; | ||
| 315 | double min = 0.0; | ||
| 316 | double max = 0.0; | ||
| 317 | bool isMinMax = false; | ||
| 318 | int channel = 0; | ||
| 319 | }; | ||
| 320 | |||
| 321 | class AINSensor : public Sensor { | ||
| 322 | public: | ||
| 323 | ✗ | explicit AINSensor(Json::Value& s) : | |
| 324 | Sensor(s), | ||
| 325 | ✗ | errcount(0), | |
| 326 | ✗ | address(s["address"].asString()), | |
| 327 | ✗ | path(s["path"].asString()), | |
| 328 | ✗ | file(-1) { | |
| 329 | ✗ | if (s.isMember("max")) { | |
| 330 | ✗ | max = s["max"].asDouble(); | |
| 331 | } | ||
| 332 | ✗ | if (s.isMember("min")) { | |
| 333 | ✗ | min = s["min"].asDouble(); | |
| 334 | } | ||
| 335 | ✗ | if (s.isMember("vScale")) { | |
| 336 | ✗ | vScale = s["vScale"].asFloat(); | |
| 337 | } else { | ||
| 338 | #ifdef PLATFORM_BB64 | ||
| 339 | ✗ | std::string vs = GetFileContents("/sys/bus/iio/devices/iio:device0/in_voltage_scale"); | |
| 340 | ✗ | vScale = std::atof(vs.c_str()); | |
| 341 | // original reference is 1.8V which for 12bit would be 0.439453125. | ||
| 342 | // If the cape doesn't specify a reference scale, we'll adjust and assume | ||
| 343 | // the params are based on 1.8 ref, but we're reading to 3.3V | ||
| 344 | ✗ | vScale = vScale / 0.439453125; | |
| 345 | #else | ||
| 346 | ✗ | vScale = 1.0f; | |
| 347 | #endif | ||
| 348 | ✗ | } | |
| 349 | ✗ | } | |
| 350 | ✗ | virtual ~AINSensor() { | |
| 351 | ✗ | close(file); | |
| 352 | ✗ | } | |
| 353 | |||
| 354 | ✗ | virtual double getValue() override { | |
| 355 | ✗ | if (file == -1 && errcount < 10) { | |
| 356 | char path[256]; | ||
| 357 | ✗ | snprintf(path, sizeof(path), "/sys/bus/iio/devices/iio:device0/in_voltage%s_raw", address.c_str()); | |
| 358 | // need to use low level calls for reading from sysfs | ||
| 359 | // to avoid any buffering that ifstream does | ||
| 360 | ✗ | if (FileExists(path)) { | |
| 361 | ✗ | file = open(path, O_RDONLY); | |
| 362 | ✗ | errcount = errcount + 1; | |
| 363 | } | ||
| 364 | } | ||
| 365 | |||
| 366 | ✗ | if (file != -1) { | |
| 367 | ✗ | lseek(file, 0, SEEK_SET); | |
| 368 | char buffer[20]; | ||
| 369 | // Leave room for the terminator: a full-size read leaves no room | ||
| 370 | // for buffer[i] = 0, and a failed read must not index at [-1]. | ||
| 371 | ✗ | int i = read(file, buffer, sizeof(buffer) - 1); | |
| 372 | ✗ | if (i <= 0) { | |
| 373 | ✗ | return 0.0; | |
| 374 | } | ||
| 375 | ✗ | buffer[i] = 0; | |
| 376 | ✗ | double d = atof(buffer); | |
| 377 | ✗ | d *= vScale; | |
| 378 | |||
| 379 | ✗ | d /= 4096; // 12 bit a2d | |
| 380 | ✗ | d *= (max - min + 1); | |
| 381 | ✗ | d += min; | |
| 382 | ✗ | return d; | |
| 383 | } | ||
| 384 | |||
| 385 | ✗ | return 0.0; | |
| 386 | } | ||
| 387 | |||
| 388 | std::string address; | ||
| 389 | std::string path; | ||
| 390 | std::string driver; | ||
| 391 | double min = 0.0; | ||
| 392 | double max = 100.0; | ||
| 393 | double vScale = 1.0; | ||
| 394 | |||
| 395 | volatile int file; | ||
| 396 | volatile int errcount; | ||
| 397 | }; | ||
| 398 | |||
| 399 | class ThermalSensor : public Sensor { | ||
| 400 | public: | ||
| 401 | 278 | explicit ThermalSensor(Json::Value& s) : | |
| 402 | Sensor(s), | ||
| 403 |
2/4✓ Branch 0 taken 278 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 278 times.
✗ Branch 3 not taken.
|
278 | path(s["path"].asString()) { |
| 404 |
1/2✓ Branch 0 taken 278 times.
✗ Branch 1 not taken.
|
278 | file = open(path.c_str(), O_RDONLY); |
| 405 | 278 | } | |
| 406 | 1112 | virtual ~ThermalSensor() { | |
| 407 | 556 | close(file); | |
| 408 | 1112 | } | |
| 409 | |||
| 410 | 3314 | virtual double getValue() override { | |
| 411 |
1/2✓ Branch 0 taken 3314 times.
✗ Branch 1 not taken.
|
3314 | if (file != -1) { |
| 412 | 3314 | lseek(file, 0, SEEK_SET); | |
| 413 | char buffer[20]; | ||
| 414 |
1/2✓ Branch 0 taken 3314 times.
✗ Branch 1 not taken.
|
3314 | int i = read(file, buffer, sizeof(buffer) - 1); |
| 415 | 3314 | double d = 0; | |
| 416 |
1/2✓ Branch 0 taken 3314 times.
✗ Branch 1 not taken.
|
3314 | if (i > 0) { |
| 417 | 3314 | buffer[i] = 0; | |
| 418 | 3314 | d = std::atof(buffer); | |
| 419 | 3314 | d /= 1000; | |
| 420 | } | ||
| 421 | 3314 | return d; | |
| 422 | } | ||
| 423 | |||
| 424 | ✗ | return 0.0; | |
| 425 | } | ||
| 426 | |||
| 427 | volatile int file; | ||
| 428 | std::string path; | ||
| 429 | }; | ||
| 430 | |||
| 431 | class FanSensor : public Sensor { | ||
| 432 | public: | ||
| 433 | 62 | explicit FanSensor(Json::Value& s) : | |
| 434 | Sensor(s), | ||
| 435 |
2/4✓ Branch 0 taken 62 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 62 times.
✗ Branch 3 not taken.
|
62 | path(s["path"].asString()) { |
| 436 |
1/2✓ Branch 0 taken 62 times.
✗ Branch 1 not taken.
|
62 | file = open(path.c_str(), O_RDONLY); |
| 437 | 62 | } | |
| 438 | 248 | virtual ~FanSensor() { | |
| 439 |
1/2✓ Branch 0 taken 62 times.
✗ Branch 1 not taken.
|
124 | if (file != -1) close(file); |
| 440 | 248 | } | |
| 441 | |||
| 442 | 872 | virtual double getValue() override { | |
| 443 |
1/2✓ Branch 0 taken 872 times.
✗ Branch 1 not taken.
|
872 | if (file != -1) { |
| 444 | 872 | lseek(file, 0, SEEK_SET); | |
| 445 | char buffer[20]; | ||
| 446 |
1/2✓ Branch 0 taken 872 times.
✗ Branch 1 not taken.
|
872 | int i = read(file, buffer, sizeof(buffer) - 1); |
| 447 | 872 | double d = 0; | |
| 448 |
1/2✓ Branch 0 taken 872 times.
✗ Branch 1 not taken.
|
872 | if (i > 0) { |
| 449 | 872 | buffer[i] = 0; | |
| 450 | 872 | d = std::atof(buffer); | |
| 451 | } | ||
| 452 | 872 | return d; | |
| 453 | } | ||
| 454 | |||
| 455 | ✗ | return 0.0; | |
| 456 | } | ||
| 457 | |||
| 458 | volatile int file; | ||
| 459 | std::string path; | ||
| 460 | }; | ||
| 461 | |||
| 462 | Sensors Sensors::INSTANCE; | ||
| 463 | |||
| 464 | 2346 | Sensors::Sensors() { | |
| 465 | 2346 | } | |
| 466 | // sensorSources is walked on the OutputMonitor and string tester threads and | ||
| 467 | // appended to by getSensorSource() during an output reload (the default | ||
| 468 | // "iio" source, created on first use). Sources are never removed while | ||
| 469 | // fppd runs. Recursive: a source's constructor looks up the one it reads. | ||
| 470 | static std::recursive_mutex sensorSourcesLock; | ||
| 471 | |||
| 472 | 458 | Sensors::~Sensors() { | |
| 473 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 458 times.
|
458 | for (auto x : sensorSources) { |
| 474 | ✗ | delete x; | |
| 475 | } | ||
| 476 | 458 | sensorSources.clear(); | |
| 477 | 458 | } | |
| 478 | 1885 | void Sensors::Init(std::map<int, std::function<bool(int)>>& callbacks) { | |
| 479 |
2/2✓ Branch 0 taken 7 times.
✓ Branch 1 taken 1885 times.
|
1892 | for (auto it = sensorSources.rbegin(); it != sensorSources.rend(); ++it) { |
| 480 |
1/2✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
|
7 | (*it)->Init(callbacks); |
| 481 | } | ||
| 482 | 1885 | } | |
| 483 | |||
| 484 | 7 | void Sensors::addSensorSources(Json::Value& config) { | |
| 485 |
1/2✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
|
7 | std::lock_guard<std::recursive_mutex> lk(sensorSourcesLock); |
| 486 |
2/4✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 7 times.
|
7 | if (!config.isArray()) { |
| 487 | ✗ | return; | |
| 488 | } | ||
| 489 |
3/4✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 18 times.
✓ Branch 3 taken 7 times.
|
25 | for (int x = 0; x < config.size(); x++) { |
| 490 | // Read from the cape's config at startup: a source with a value of the | ||
| 491 | // wrong JSON type is skipped, as addSensors() does, not thrown on. | ||
| 492 | 18 | SensorSource* ss = nullptr; | |
| 493 | try { | ||
| 494 |
2/4✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 18 times.
✗ Branch 3 not taken.
|
18 | Json::Value v = config[x]; |
| 495 |
4/4✓ Branch 0 taken 16 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 15 times.
✓ Branch 3 taken 1 times.
|
18 | std::string type = v["type"].asString(); |
| 496 |
7/10✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✓ Branch 3 taken 3 times.
✓ Branch 4 taken 12 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 12 times.
✓ Branch 8 taken 3 times.
✓ Branch 9 taken 12 times.
|
15 | if (type == "ads7828" || type == "ads7830") { |
| 497 |
5/8✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 2 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 1 times.
✓ Branch 6 taken 2 times.
✗ Branch 7 not taken.
|
3 | ss = new ADS7828Sensor(v); |
| 498 |
3/4✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✓ Branch 3 taken 6 times.
|
12 | } else if (type == "iio") { |
| 499 |
5/8✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 1 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 5 times.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
|
6 | ss = new IIOSensorSource(v); |
| 500 |
3/4✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 1 times.
|
6 | } else if (type == "mux") { |
| 501 |
5/8✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 1 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 4 times.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
|
5 | ss = new MuxSensorSource(v); |
| 502 |
2/4✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
|
1 | } else if (type == "multi") { |
| 503 |
3/8✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
|
1 | ss = new MultiSensorSource(v); |
| 504 | } | ||
| 505 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
|
31 | } catch (const std::exception& e) { |
| 506 |
3/6✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 8 times.
✗ Branch 5 not taken.
|
24 | WarningHolder::InvalidConfigEntry("sensorSources", e.what()); |
| 507 | 8 | continue; | |
| 508 | 8 | } | |
| 509 |
6/8✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 7 times.
✓ Branch 5 taken 3 times.
✓ Branch 6 taken 7 times.
✓ Branch 7 taken 3 times.
|
10 | if (ss && ss->isOK()) { |
| 510 |
1/2✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
|
7 | sensorSources.push_back(ss); |
| 511 |
1/2✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
|
3 | } else if (ss) { |
| 512 |
3/6✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
|
3 | WarningHolder::AddWarning(53, "Could not create SensorSource: " + ss->getID()); |
| 513 |
1/2✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
|
3 | delete ss; |
| 514 | } | ||
| 515 | } | ||
| 516 |
1/2✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
|
7 | } |
| 517 | 1574 | void Sensors::updateSensorSources(bool forceInstant) { | |
| 518 |
1/2✓ Branch 0 taken 1574 times.
✗ Branch 1 not taken.
|
1574 | std::lock_guard<std::recursive_mutex> lk(sensorSourcesLock); |
| 519 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1574 times.
|
1574 | for (auto& ss : sensorSources) { |
| 520 | ✗ | ss->update(forceInstant); | |
| 521 | } | ||
| 522 | 1574 | } | |
| 523 | 25 | SensorSource* Sensors::getSensorSource(const std::string& name) { | |
| 524 |
1/2✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
|
25 | std::lock_guard<std::recursive_mutex> lk(sensorSourcesLock); |
| 525 |
2/2✓ Branch 0 taken 33 times.
✓ Branch 1 taken 4 times.
|
37 | for (auto ss : sensorSources) { |
| 526 |
3/4✓ Branch 0 taken 33 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 21 times.
✓ Branch 3 taken 12 times.
|
33 | if (ss->getID() == name) { |
| 527 | 21 | return ss; | |
| 528 | } | ||
| 529 | } | ||
| 530 |
2/4✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 4 times.
|
4 | if (name == "iio") { |
| 531 | // default the iio name if requested | ||
| 532 | ✗ | Json::Value v; | |
| 533 | ✗ | v["type"] = "iio"; | |
| 534 | ✗ | v["id"] = "iio"; | |
| 535 | ✗ | SensorSource* ss = new IIOSensorSource(v); | |
| 536 | ✗ | if (ss->isOK()) { | |
| 537 | ✗ | sensorSources.push_back(ss); | |
| 538 | } else { | ||
| 539 | ✗ | WarningHolder::AddWarning(53, "Could not create SensorSource: " + ss->getID()); | |
| 540 | ✗ | delete ss; | |
| 541 | ✗ | ss = nullptr; | |
| 542 | } | ||
| 543 | ✗ | return ss; | |
| 544 | ✗ | } | |
| 545 | 4 | return nullptr; | |
| 546 | 25 | } | |
| 547 | 6 | void Sensors::lockToGroup(int i) { | |
| 548 |
1/2✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
|
6 | std::lock_guard<std::recursive_mutex> lk(sensorSourcesLock); |
| 549 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
|
6 | for (auto ss : sensorSources) { |
| 550 | ✗ | ss->lockToGroup(i); | |
| 551 | } | ||
| 552 | 6 | } | |
| 553 | |||
| 554 | 1887 | void Sensors::DetectHWSensors() { | |
| 555 | 1887 | int i = 0; | |
| 556 | 1887 | char path[256] = { 0 }; | |
| 557 | 1887 | snprintf(path, sizeof(path), "/sys/class/thermal/thermal_zone%d/temp", i); | |
| 558 |
3/4✓ Branch 0 taken 2165 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 278 times.
✓ Branch 3 taken 1887 times.
|
2165 | while (FileExists(path)) { |
| 559 |
1/2✓ Branch 0 taken 278 times.
✗ Branch 1 not taken.
|
278 | Json::Value v; |
| 560 |
2/4✓ Branch 0 taken 278 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 278 times.
✗ Branch 3 not taken.
|
278 | v["path"] = path; |
| 561 |
2/4✓ Branch 0 taken 278 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 278 times.
✗ Branch 3 not taken.
|
278 | v["valueType"] = "Temperature"; |
| 562 | |||
| 563 | 278 | snprintf(path, sizeof(path), "/sys/class/thermal/thermal_zone%d/type", i); | |
| 564 |
2/4✓ Branch 0 taken 278 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 278 times.
✗ Branch 3 not taken.
|
278 | if (FileExists(path)) { |
| 565 |
1/2✓ Branch 0 taken 278 times.
✗ Branch 1 not taken.
|
278 | int file = open(path, O_RDONLY); |
| 566 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 278 times.
|
278 | if (file == -1) { |
| 567 | ✗ | v["label"] = "CPU: "; | |
| 568 | } else { | ||
| 569 | // Cap the read below the buffer so there is always room for | ||
| 570 | // the terminator; short/failed reads fall back like a missing | ||
| 571 | // type file instead of indexing out of bounds. | ||
| 572 |
1/2✓ Branch 0 taken 278 times.
✗ Branch 1 not taken.
|
278 | int r = read(file, path, sizeof(path) - 1); |
| 573 |
1/2✓ Branch 0 taken 278 times.
✗ Branch 1 not taken.
|
278 | close(file); |
| 574 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 278 times.
|
278 | if (r <= 0) { |
| 575 | ✗ | v["label"] = "CPU: "; | |
| 576 | } else { | ||
| 577 | 278 | path[r] = 0; | |
| 578 |
1/2✓ Branch 0 taken 278 times.
✗ Branch 1 not taken.
|
278 | if (path[r - 1] == '\n') { |
| 579 | 278 | path[r - 1] = 0; // remove trailing newline | |
| 580 | 278 | --r; | |
| 581 | } | ||
| 582 |
1/2✓ Branch 0 taken 278 times.
✗ Branch 1 not taken.
|
278 | std::string label = path; |
| 583 | // Each strip removes the suffix plus its separator, so only | ||
| 584 | // strip when a non-empty prefix is left: an exact-length | ||
| 585 | // match would otherwise index before the buffer. | ||
| 586 |
18/32✓ Branch 0 taken 278 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 278 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 277 times.
✓ Branch 5 taken 1 times.
✓ Branch 6 taken 277 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 277 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1 times.
✓ Branch 11 taken 276 times.
✓ Branch 12 taken 1 times.
✓ Branch 13 taken 1 times.
✓ Branch 14 taken 277 times.
✓ Branch 15 taken 1 times.
✓ Branch 16 taken 277 times.
✓ Branch 17 taken 1 times.
✓ Branch 18 taken 278 times.
✗ Branch 19 not taken.
✓ Branch 20 taken 278 times.
✗ Branch 21 not taken.
✓ Branch 22 taken 1 times.
✓ Branch 23 taken 277 times.
✗ Branch 24 not taken.
✗ Branch 25 not taken.
✗ Branch 26 not taken.
✗ Branch 27 not taken.
✗ Branch 28 not taken.
✗ Branch 29 not taken.
✗ Branch 30 not taken.
✗ Branch 31 not taken.
|
1388 | if ((endsWith(label, "thermal_zone") || endsWith(label, "thermal-zone")) && (int)label.size() > 13) { |
| 587 | 1 | path[r - 13] = 0; // remove the "thermal_zone" suffix | |
| 588 |
9/18✓ Branch 0 taken 277 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 277 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 276 times.
✓ Branch 5 taken 1 times.
✓ Branch 6 taken 276 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 277 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 277 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 276 times.
✓ Branch 13 taken 1 times.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
|
831 | } else if (endsWith(label, "thermal") && (int)label.size() > 8) { |
| 589 | 276 | path[r - 8] = 0; // remove the "thermal" suffix | |
| 590 | } | ||
| 591 | 278 | bool hasUpper = false; | |
| 592 |
2/2✓ Branch 0 taken 1361 times.
✓ Branch 1 taken 278 times.
|
1639 | for (int x = 0; path[x]; x++) { |
| 593 |
2/2✓ Branch 0 taken 148 times.
✓ Branch 1 taken 1213 times.
|
1361 | if (isupper(path[x])) { |
| 594 | 148 | hasUpper = true; | |
| 595 | } | ||
| 596 | } | ||
| 597 |
2/2✓ Branch 0 taken 204 times.
✓ Branch 1 taken 74 times.
|
278 | if (!hasUpper) { |
| 598 | // convert to lower case | ||
| 599 |
2/2✓ Branch 0 taken 917 times.
✓ Branch 1 taken 204 times.
|
1121 | for (int x = 0; path[x]; x++) { |
| 600 | 917 | path[x] = toupper(path[x]); | |
| 601 | } | ||
| 602 | } | ||
| 603 |
1/2✓ Branch 0 taken 278 times.
✗ Branch 1 not taken.
|
278 | label = path; |
| 604 |
3/6✓ Branch 0 taken 278 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 278 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 278 times.
|
556 | if (endsWith(label, "_THERMAL_ZONE")) { |
| 605 | ✗ | label = label.substr(0, label.size() - 13); | |
| 606 |
3/6✓ Branch 0 taken 278 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 278 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 278 times.
|
556 | } else if (endsWith(label, "_THERMAL")) { |
| 607 | ✗ | label = label.substr(0, label.size() - 8); | |
| 608 | } | ||
| 609 |
1/2✓ Branch 0 taken 278 times.
✗ Branch 1 not taken.
|
278 | label += ": "; |
| 610 |
2/4✓ Branch 0 taken 278 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 278 times.
✗ Branch 3 not taken.
|
278 | v["label"] = label; |
| 611 | 278 | } | |
| 612 | } | ||
| 613 | } else { | ||
| 614 | ✗ | v["label"] = "CPU: "; | |
| 615 | } | ||
| 616 |
4/10✓ Branch 0 taken 278 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 278 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 278 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 278 times.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
|
278 | sensors.push_back(new ThermalSensor(v)); |
| 617 | 278 | i++; | |
| 618 | 278 | snprintf(path, sizeof(path), "/sys/class/thermal/thermal_zone%d/temp", i); | |
| 619 | 278 | } | |
| 620 |
1/2✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
|
1887 | DetectFanSensors(); |
| 621 | 1887 | } | |
| 622 | 1887 | void Sensors::DetectFanSensors() { | |
| 623 | // A pwm-fan hwmon node exposes fanN_input even when no working fan is wired | ||
| 624 | // to it -- it just reads 0, which looks like a stalled fan. fppinit's | ||
| 625 | // boot-time probe (probeFanPresence in FPPINIT_Config.cpp) forces each fan on | ||
| 626 | // and records whether its tachometer actually reported RPM into | ||
| 627 | // media/tmp/fan_probe.json, keyed by "<hwmon name>:<fan index>". Skip the RPM | ||
| 628 | // sensor for any fan that probed as absent -- there is nothing useful to | ||
| 629 | // show. A missing file or entry means "unknown", so we register as before. | ||
| 630 | // fppinit only writes the probe file when it finds a fanN_input to probe, so | ||
| 631 | // a board with no tachometer-capable fan node -- the overwhelming majority -- | ||
| 632 | // never has one. That is the "unknown" case handled above, not a problem, so | ||
| 633 | // skip the load entirely rather than have it report the file as missing. | ||
| 634 |
1/2✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
|
1887 | Json::Value fanProbe; |
| 635 |
2/4✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1887 times.
✗ Branch 3 not taken.
|
1887 | std::string fanProbeFile = FPP_DIR_MEDIA("/tmp/fan_probe.json"); |
| 636 |
3/4✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 1886 times.
|
1887 | if (FileExists(fanProbeFile)) { |
| 637 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | LoadJsonFromFile(fanProbeFile, fanProbe, JsonRoot::Object); |
| 638 | } | ||
| 639 | |||
| 640 |
1/2✓ Branch 0 taken 3809 times.
✗ Branch 1 not taken.
|
3809 | for (int h = 0; h < 32; h++) { |
| 641 | char hwmonPath[256]; | ||
| 642 | 3809 | snprintf(hwmonPath, sizeof(hwmonPath), "/sys/class/hwmon/hwmon%d", h); | |
| 643 |
3/4✓ Branch 0 taken 3809 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1887 times.
✓ Branch 3 taken 1922 times.
|
3809 | if (!DirectoryExists(hwmonPath)) { |
| 644 | 1887 | break; | |
| 645 | } | ||
| 646 | |||
| 647 | char namePath[256]; | ||
| 648 | 1922 | snprintf(namePath, sizeof(namePath), "%s/name", hwmonPath); | |
| 649 |
1/2✓ Branch 0 taken 1922 times.
✗ Branch 1 not taken.
|
1922 | std::string deviceName = "hwmon" + std::to_string(h); |
| 650 |
2/4✓ Branch 0 taken 1922 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1922 times.
✗ Branch 3 not taken.
|
1922 | if (FileExists(namePath)) { |
| 651 |
1/2✓ Branch 0 taken 1922 times.
✗ Branch 1 not taken.
|
1922 | int f = open(namePath, O_RDONLY); |
| 652 |
1/2✓ Branch 0 taken 1922 times.
✗ Branch 1 not taken.
|
1922 | if (f != -1) { |
| 653 | 1922 | char buf[64] = {0}; | |
| 654 |
1/2✓ Branch 0 taken 1922 times.
✗ Branch 1 not taken.
|
1922 | int r = read(f, buf, 63); |
| 655 |
1/2✓ Branch 0 taken 1922 times.
✗ Branch 1 not taken.
|
1922 | if (r > 0) { |
| 656 |
1/2✓ Branch 0 taken 1922 times.
✗ Branch 1 not taken.
|
1922 | if (buf[r - 1] == '\n') buf[r - 1] = 0; |
| 657 |
1/2✓ Branch 0 taken 1922 times.
✗ Branch 1 not taken.
|
1922 | deviceName = buf; |
| 658 | } | ||
| 659 |
1/2✓ Branch 0 taken 1922 times.
✗ Branch 1 not taken.
|
1922 | close(f); |
| 660 | } | ||
| 661 | } | ||
| 662 | |||
| 663 | // Pick the nicest available base name for the label: | ||
| 664 | // 1. a cape-supplied DT override: of_node/fpp,fan-label (e.g. "K16-Pro Fan") | ||
| 665 | // 2. the DT node name: of_node/name (e.g. Pi5 "cooling_fan") | ||
| 666 | // 3. the hwmon device name: e.g. "pwmfan" | ||
| 667 | 3843 | auto readDTString = [](const std::string& path) { | |
| 668 |
6/10✓ Branch 0 taken 313 times.
✓ Branch 1 taken 3530 times.
✓ Branch 2 taken 313 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3530 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 3530 times.
✓ Branch 7 taken 313 times.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
|
7373 | std::string s = FileExists(path) ? GetFileContents(path) : ""; |
| 669 | // device-tree strings are NUL-terminated; trim NUL/whitespace | ||
| 670 |
5/8✓ Branch 0 taken 313 times.
✓ Branch 1 taken 3530 times.
✓ Branch 2 taken 313 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 313 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 3843 times.
|
3843 | while (!s.empty() && (s.back() == '\0' || isspace((unsigned char)s.back()))) |
| 671 | ✗ | s.pop_back(); | |
| 672 | 3843 | return s; | |
| 673 | }; | ||
| 674 |
3/6✓ Branch 0 taken 1922 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1922 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1922 times.
✗ Branch 5 not taken.
|
1922 | std::string baseName = readDTString(std::string(hwmonPath) + "/of_node/fpp,fan-label"); |
| 675 |
2/2✓ Branch 0 taken 1921 times.
✓ Branch 1 taken 1 times.
|
1922 | if (baseName.empty()) { |
| 676 |
3/6✓ Branch 0 taken 1921 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1921 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1921 times.
✗ Branch 5 not taken.
|
1921 | std::string nodeName = readDTString(std::string(hwmonPath) + "/of_node/name"); |
| 677 |
9/14✓ Branch 0 taken 1921 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1846 times.
✓ Branch 3 taken 75 times.
✓ Branch 4 taken 1846 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1846 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1846 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1846 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 1846 times.
✓ Branch 13 taken 75 times.
|
1921 | if (nodeName != "pwm-fan" && nodeName != "pwm_fan" && nodeName != "fan") { |
| 678 | // prettify, e.g. "cooling_fan" -> "Cooling Fan" | ||
| 679 | 1846 | bool up = true; | |
| 680 |
2/2✓ Branch 0 taken 1717 times.
✓ Branch 1 taken 1846 times.
|
3563 | for (auto& c : nodeName) { |
| 681 |
3/4✓ Branch 0 taken 1662 times.
✓ Branch 1 taken 55 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 1662 times.
|
1717 | if (c == '_' || c == '-') { c = ' '; up = true; } |
| 682 |
3/4✓ Branch 0 taken 292 times.
✓ Branch 1 taken 1370 times.
✓ Branch 2 taken 292 times.
✗ Branch 3 not taken.
|
1662 | else if (up && islower((unsigned char)c)) { c = toupper((unsigned char)c); up = false; } |
| 683 | 1370 | else up = false; | |
| 684 | } | ||
| 685 |
1/2✓ Branch 0 taken 1846 times.
✗ Branch 1 not taken.
|
1846 | baseName = nodeName; |
| 686 | } | ||
| 687 | 1921 | } | |
| 688 |
2/2✓ Branch 0 taken 1684 times.
✓ Branch 1 taken 238 times.
|
1922 | if (baseName.empty()) |
| 689 |
1/2✓ Branch 0 taken 1684 times.
✗ Branch 1 not taken.
|
1684 | baseName = deviceName; |
| 690 | |||
| 691 | // count fans on this device so we only append an index when there's more than one | ||
| 692 | 1922 | int fanCount = 0; | |
| 693 |
2/2✓ Branch 0 taken 15376 times.
✓ Branch 1 taken 1922 times.
|
17298 | for (int f = 1; f <= 8; f++) { |
| 694 | char fp[256]; | ||
| 695 | 15376 | snprintf(fp, sizeof(fp), "%s/fan%d_input", hwmonPath, f); | |
| 696 |
3/4✓ Branch 0 taken 15376 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 63 times.
✓ Branch 3 taken 15313 times.
|
15376 | if (FileExists(fp)) fanCount++; |
| 697 | } | ||
| 698 | |||
| 699 |
2/2✓ Branch 0 taken 15376 times.
✓ Branch 1 taken 1922 times.
|
17298 | for (int fan = 1; fan <= 8; fan++) { |
| 700 | char fanPath[256]; | ||
| 701 | 15376 | snprintf(fanPath, sizeof(fanPath), "%s/fan%d_input", hwmonPath, fan); | |
| 702 |
3/4✓ Branch 0 taken 15376 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 63 times.
✓ Branch 3 taken 15313 times.
|
15376 | if (FileExists(fanPath)) { |
| 703 |
1/2✓ Branch 0 taken 63 times.
✗ Branch 1 not taken.
|
63 | Json::Value v; |
| 704 |
2/4✓ Branch 0 taken 63 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 63 times.
✗ Branch 3 not taken.
|
63 | v["path"] = fanPath; |
| 705 |
2/4✓ Branch 0 taken 63 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 63 times.
✗ Branch 3 not taken.
|
63 | v["valueType"] = "FanSpeed"; |
| 706 | // "<base> Fan", skipping a redundant trailing "Fan", and adding the | ||
| 707 | // index only when the device actually has more than one fan | ||
| 708 |
1/2✓ Branch 0 taken 63 times.
✗ Branch 1 not taken.
|
63 | std::string label = baseName; |
| 709 |
16/44✓ Branch 0 taken 63 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 63 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 56 times.
✓ Branch 5 taken 7 times.
✓ Branch 6 taken 56 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 56 times.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✓ Branch 11 taken 56 times.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✓ Branch 19 taken 63 times.
✗ Branch 20 not taken.
✓ Branch 21 taken 63 times.
✓ Branch 22 taken 56 times.
✓ Branch 23 taken 7 times.
✓ Branch 24 taken 56 times.
✓ Branch 25 taken 7 times.
✓ Branch 26 taken 63 times.
✗ Branch 27 not taken.
✓ Branch 28 taken 63 times.
✗ Branch 29 not taken.
✗ Branch 30 not taken.
✓ Branch 31 taken 63 times.
✗ Branch 32 not taken.
✗ Branch 33 not taken.
✗ Branch 34 not taken.
✗ Branch 35 not taken.
✗ Branch 36 not taken.
✗ Branch 37 not taken.
✗ Branch 38 not taken.
✗ Branch 39 not taken.
✗ Branch 40 not taken.
✗ Branch 41 not taken.
✗ Branch 42 not taken.
✗ Branch 43 not taken.
|
301 | if (!(endsWith(label, "fan") || endsWith(label, "Fan") || endsWith(label, "FAN"))) |
| 710 | ✗ | label += " Fan"; | |
| 711 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 61 times.
|
63 | if (fanCount > 1) |
| 712 |
2/4✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
|
2 | label += " " + std::to_string(fan); |
| 713 |
3/6✓ Branch 0 taken 63 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 63 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 63 times.
✗ Branch 5 not taken.
|
63 | v["label"] = label + ": "; |
| 714 |
2/4✓ Branch 0 taken 63 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 63 times.
✗ Branch 3 not taken.
|
63 | v["postfix"] = " RPM"; |
| 715 | |||
| 716 |
2/4✓ Branch 0 taken 63 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 63 times.
✗ Branch 3 not taken.
|
63 | std::string probeKey = deviceName + ":" + std::to_string(fan); |
| 717 |
10/14✓ Branch 0 taken 63 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 61 times.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1 times.
✓ Branch 11 taken 1 times.
✓ Branch 12 taken 1 times.
✓ Branch 13 taken 62 times.
|
63 | if (fanProbe.isMember(probeKey) && !fanProbe[probeKey]["present"].asBool()) { |
| 718 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | LogInfo(VB_GENERAL, "Fan '%s' (%s) reported no tachometer signal when forced on at boot; suppressing RPM display\n", |
| 719 | label.c_str(), probeKey.c_str()); | ||
| 720 | 1 | continue; | |
| 721 | } | ||
| 722 |
4/10✓ Branch 0 taken 62 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 62 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 62 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 62 times.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
|
62 | sensors.push_back(new FanSensor(v)); |
| 723 |
6/6✓ Branch 0 taken 62 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 62 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 62 times.
✓ Branch 5 taken 1 times.
|
65 | } |
| 724 | } | ||
| 725 | 1922 | } | |
| 726 | 1887 | } | |
| 727 | 1867 | void Sensors::Close() { | |
| 728 |
2/2✓ Branch 0 taken 363 times.
✓ Branch 1 taken 1867 times.
|
2230 | for (auto x : sensors) { |
| 729 |
1/2✓ Branch 0 taken 363 times.
✗ Branch 1 not taken.
|
363 | delete x; |
| 730 | } | ||
| 731 | 1867 | sensors.clear(); | |
| 732 | 1867 | } | |
| 733 | |||
| 734 | 24 | Sensor* Sensors::createSensor(Json::Value& s) { | |
| 735 |
5/8✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 24 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 24 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✓ Branch 7 taken 22 times.
|
24 | if (s["type"].asString() == "i2c") { |
| 736 |
2/6✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
|
2 | return new I2CSensor(s); |
| 737 |
4/8✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 22 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 22 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 22 times.
|
22 | } else if (s["type"].asString() == "ain") { |
| 738 | ✗ | return new AINSensor(s); | |
| 739 |
4/8✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 22 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 22 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 22 times.
✗ Branch 7 not taken.
|
22 | } else if (s["type"].asString() == "sensor") { |
| 740 |
4/6✓ Branch 0 taken 21 times.
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 21 times.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
|
22 | return new SensorSourceSensor(s); |
| 741 | } | ||
| 742 | ✗ | return nullptr; | |
| 743 | } | ||
| 744 | |||
| 745 | 11 | void Sensors::addSensors(Json::Value& config) { | |
| 746 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 10 times.
|
11 | if (!config.isArray()) { |
| 747 | 1 | return; | |
| 748 | } | ||
| 749 |
2/2✓ Branch 0 taken 24 times.
✓ Branch 1 taken 10 times.
|
34 | for (int x = 0; x < config.size(); x++) { |
| 750 | // A sensor with a value of the wrong JSON type threw out of its | ||
| 751 | // constructor at startup, to std::terminate; skip that one. The | ||
| 752 | // constructors read all their fields before doing anything. | ||
| 753 | try { | ||
| 754 |
2/4✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 24 times.
✗ Branch 3 not taken.
|
24 | Json::Value v = config[x]; |
| 755 |
2/2✓ Branch 0 taken 23 times.
✓ Branch 1 taken 1 times.
|
24 | Sensor* sensor = createSensor(v); |
| 756 |
1/2✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
|
23 | if (sensor) { |
| 757 |
1/2✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
|
23 | sensors.push_back(sensor); |
| 758 | } | ||
| 759 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
|
25 | } catch (const std::exception& e) { |
| 760 |
3/6✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
|
3 | WarningHolder::InvalidConfigEntry("sensors", e.what()); |
| 761 | 1 | } | |
| 762 | } | ||
| 763 | } | ||
| 764 | |||
| 765 | 137639 | void Sensors::reportSensors(Json::Value& root) { | |
| 766 |
2/2✓ Branch 0 taken 2010 times.
✓ Branch 1 taken 135629 times.
|
137639 | if (!sensors.empty()) { |
| 767 | 2010 | std::set<SensorSource*> sensorSources; | |
| 768 |
1/2✓ Branch 0 taken 2010 times.
✗ Branch 1 not taken.
|
2010 | Json::Value& s = root["sensors"]; |
| 769 |
2/2✓ Branch 0 taken 6644 times.
✓ Branch 1 taken 2010 times.
|
8654 | for (auto a : sensors) { |
| 770 |
1/2✓ Branch 0 taken 6644 times.
✗ Branch 1 not taken.
|
6644 | SensorSource* ss = a->getSensorSource(); |
| 771 |
2/2✓ Branch 0 taken 2449 times.
✓ Branch 1 taken 4195 times.
|
6644 | if (ss) { |
| 772 | // only trigger updates on the sensor sources that | ||
| 773 | // actually report via the normal reporting and not | ||
| 774 | // all the sensor sources that may be used for | ||
| 775 | // current monitoring or other things | ||
| 776 |
3/4✓ Branch 0 taken 2449 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 321 times.
✓ Branch 3 taken 2128 times.
|
2449 | if (!sensorSources.contains(ss)) { |
| 777 |
1/2✓ Branch 0 taken 321 times.
✗ Branch 1 not taken.
|
321 | ss->update(false); |
| 778 |
1/2✓ Branch 0 taken 321 times.
✗ Branch 1 not taken.
|
321 | sensorSources.emplace(ss); |
| 779 | } | ||
| 780 | } | ||
| 781 |
1/2✓ Branch 0 taken 6644 times.
✗ Branch 1 not taken.
|
6644 | a->report(s); |
| 782 | } | ||
| 783 | 2010 | } | |
| 784 | 137639 | } | |
| 785 |