blob: bcd2f4425bc8d1a5b629cfcda34dac8f41d9bf01 [file] [log] [blame]
Willy Tude54f482021-01-26 15:59:09 -08001#include "dbus-sdr/sensorutils.hpp"
2
3#include <cmath>
4
5#include "gtest/gtest.h"
6
7// There is a surprising amount of slop in the math,
8// thanks to all the rounding and conversion.
9// The "x" byte value can drift by up to 2 away, I have seen.
10static constexpr int8_t expectedSlopX = 2;
11
12// Unlike expectedSlopX, this is a ratio, not an integer
13// It scales based on the range of "y"
14static constexpr double expectedSlopY = 0.01;
15
16// The algorithm here was copied from ipmitool
17// sdr_convert_sensor_reading() function
18// https://github.com/ipmitool/ipmitool/blob/42a023ff0726c80e8cc7d30315b987fe568a981d/lib/ipmi_sdr.c#L360
19double ipmitool_y_from_x(uint8_t x, int m, int k2_rExp, int b, int k1_bExp,
20 bool bSigned)
21{
22 double result;
23
24 // Rename to exactly match names and types (except analog) from ipmitool
25 uint8_t val = x;
26 double k1 = k1_bExp;
27 double k2 = k2_rExp;
28 int analog = bSigned ? 2 : 0;
29
30 // Begin paste here
31 // Only change is to comment out complicated structure in switch statement
32
33 switch (/*sensor->cmn.unit.*/ analog)
34 {
35 case 0:
36 result = (double)(((m * val) + (b * pow(10, k1))) * pow(10, k2));
37 break;
38 case 1:
39 if (val & 0x80)
40 val++;
41 /* Deliberately fall through to case 2. */
Willy Tu11d68892022-01-20 10:37:34 -080042 [[fallthrough]];
Willy Tude54f482021-01-26 15:59:09 -080043 case 2:
44 result =
45 (double)(((m * (int8_t)val) + (b * pow(10, k1))) * pow(10, k2));
46 break;
47 default:
48 /* Oops! This isn't an analog sensor. */
49 return 0.0;
50 }
51
52 // End paste here
53 // Ignoring linearization curves and postprocessing that follows,
54 // assuming all sensors are perfectly linear
55 return result;
56}
57
58void testValue(int x, double y, int16_t M, int8_t rExp, int16_t B, int8_t bExp,
59 bool bSigned, double yRange)
60{
61 double yRoundtrip;
62 int result;
63
64 // There is intentionally no exception catching here,
65 // because if getSensorAttributes() returned true,
66 // it is a promise that all of these should work.
67 if (bSigned)
68 {
69 int8_t expect = x;
Patrick Williams1318a5e2024-08-16 15:19:54 -040070 int8_t actual =
71 ipmi::scaleIPMIValueFromDouble(y, M, rExp, B, bExp, bSigned);
Willy Tude54f482021-01-26 15:59:09 -080072
73 result = actual;
74 yRoundtrip = ipmitool_y_from_x(actual, M, rExp, B, bExp, bSigned);
75
76 EXPECT_NEAR(actual, expect, expectedSlopX);
77 }
78 else
79 {
80 uint8_t expect = x;
Patrick Williams1318a5e2024-08-16 15:19:54 -040081 uint8_t actual =
82 ipmi::scaleIPMIValueFromDouble(y, M, rExp, B, bExp, bSigned);
Willy Tude54f482021-01-26 15:59:09 -080083
84 result = actual;
85 yRoundtrip = ipmitool_y_from_x(actual, M, rExp, B, bExp, bSigned);
86
87 EXPECT_NEAR(actual, expect, expectedSlopX);
88 }
89
90 // Scale the amount of allowed slop in y based on range, so ratio similar
91 double yTolerance = yRange * expectedSlopY;
92 // double yError = std::abs(y - yRoundtrip);
93
94 EXPECT_NEAR(y, yRoundtrip, yTolerance);
95
96 char szFormat[1024];
97 sprintf(szFormat,
98 "Value | xExpect %4d | xResult %4d "
99 "| M %5d | rExp %3d "
100 "| B %5d | bExp %3d | bSigned %1d | y %18.3f | yRoundtrip %18.3f\n",
101 x, result, M, (int)rExp, B, (int)bExp, (int)bSigned, y, yRoundtrip);
102 std::cout << szFormat;
103}
104
105void testBounds(double yMin, double yMax, bool bExpectedOutcome = true)
106{
107 int16_t mValue;
108 int8_t rExp;
109 int16_t bValue;
110 int8_t bExp;
111 bool bSigned;
112 bool result;
113
114 result = ipmi::getSensorAttributes(yMax, yMin, mValue, rExp, bValue, bExp,
115 bSigned);
116 EXPECT_EQ(result, bExpectedOutcome);
117
118 if (!result)
119 {
120 return;
121 }
122
123 char szFormat[1024];
124 sprintf(szFormat,
125 "Bounds | yMin %18.3f | yMax %18.3f | M %5d"
126 " | rExp %3d | B %5d | bExp %3d | bSigned %1d\n",
127 yMin, yMax, mValue, (int)rExp, bValue, (int)bExp, (int)bSigned);
128 std::cout << szFormat;
129
130 double y50p = (yMin + yMax) / 2.0;
131
132 // Average the average
133 double y25p = (yMin + y50p) / 2.0;
134 double y75p = (y50p + yMax) / 2.0;
135
136 // This range value is only used for tolerance checking, not computation
137 double yRange = yMax - yMin;
138
139 if (bSigned)
140 {
141 int8_t xMin = -128;
142 int8_t x25p = -64;
143 int8_t x50p = 0;
144 int8_t x75p = 64;
145 int8_t xMax = 127;
146
147 testValue(xMin, yMin, mValue, rExp, bValue, bExp, bSigned, yRange);
148 testValue(x25p, y25p, mValue, rExp, bValue, bExp, bSigned, yRange);
149 testValue(x50p, y50p, mValue, rExp, bValue, bExp, bSigned, yRange);
150 testValue(x75p, y75p, mValue, rExp, bValue, bExp, bSigned, yRange);
151 testValue(xMax, yMax, mValue, rExp, bValue, bExp, bSigned, yRange);
152 }
153 else
154 {
155 uint8_t xMin = 0;
156 uint8_t x25p = 64;
157 uint8_t x50p = 128;
158 uint8_t x75p = 192;
159 uint8_t xMax = 255;
160
161 testValue(xMin, yMin, mValue, rExp, bValue, bExp, bSigned, yRange);
162 testValue(x25p, y25p, mValue, rExp, bValue, bExp, bSigned, yRange);
163 testValue(x50p, y50p, mValue, rExp, bValue, bExp, bSigned, yRange);
164 testValue(x75p, y75p, mValue, rExp, bValue, bExp, bSigned, yRange);
165 testValue(xMax, yMax, mValue, rExp, bValue, bExp, bSigned, yRange);
166 }
167}
168
169void testRanges(void)
170{
171 // The ranges from the main TEST function
172 testBounds(0x0, 0xFF);
173 testBounds(-128, 127);
174 testBounds(0, 16000);
175 testBounds(0, 20);
176 testBounds(8000, 16000);
177 testBounds(-10, 10);
178 testBounds(0, 277);
179 testBounds(0, 0, false);
180 testBounds(10, 12);
181
182 // Additional test cases recommended to me by hardware people
183 testBounds(-40, 150);
184 testBounds(0, 1);
185 testBounds(0, 2);
186 testBounds(0, 4);
187 testBounds(0, 8);
188 testBounds(35, 65);
189 testBounds(0, 18);
190 testBounds(0, 25);
191 testBounds(0, 80);
192 testBounds(0, 500);
193
194 // Additional sanity checks
195 testBounds(0, 255);
196 testBounds(-255, 0);
197 testBounds(-255, 255);
198 testBounds(0, 1000);
199 testBounds(-1000, 0);
200 testBounds(-1000, 1000);
201 testBounds(0, 255000);
202 testBounds(-128000000, 127000000);
203 testBounds(-50000, 0);
204 testBounds(-40000, 10000);
205 testBounds(-30000, 20000);
206 testBounds(-20000, 30000);
207 testBounds(-10000, 40000);
208 testBounds(0, 50000);
209 testBounds(-1e3, 1e6);
210 testBounds(-1e6, 1e3);
211
212 // Extreme ranges are now possible
213 testBounds(0, 1e10);
214 testBounds(0, 1e11);
215 testBounds(0, 1e12);
216 testBounds(0, 1e13, false);
217 testBounds(-1e10, 0);
218 testBounds(-1e11, 0);
219 testBounds(-1e12, 0);
220 testBounds(-1e13, 0, false);
221 testBounds(-1e9, 1e9);
222 testBounds(-1e10, 1e10);
223 testBounds(-1e11, 1e11);
224 testBounds(-1e12, 1e12, false);
225
226 // Large multiplier but small offset
227 testBounds(1e4, 1e4 + 255);
228 testBounds(1e5, 1e5 + 255);
229 testBounds(1e6, 1e6 + 255);
230 testBounds(1e7, 1e7 + 255);
231 testBounds(1e8, 1e8 + 255);
232 testBounds(1e9, 1e9 + 255);
233 testBounds(1e10, 1e10 + 255, false);
234
235 // Input validation against garbage
236 testBounds(0, INFINITY, false);
237 testBounds(-INFINITY, 0, false);
238 testBounds(-INFINITY, INFINITY, false);
239 testBounds(0, NAN, false);
240 testBounds(NAN, 0, false);
241 testBounds(NAN, NAN, false);
242
243 // Noteworthy binary integers
244 testBounds(0, std::pow(2.0, 32.0) - 1.0);
245 testBounds(0, std::pow(2.0, 32.0));
246 testBounds(0.0 - std::pow(2.0, 31.0), std::pow(2.0, 31.0));
247 testBounds((0.0 - std::pow(2.0, 31.0)) - 1.0, std::pow(2.0, 31.0));
248
249 // Similar but negative (note additional commented-out below)
250 testBounds(-1e1, (-1e1) + 255);
251 testBounds(-1e2, (-1e2) + 255);
252
253 // Ranges of negative numbers (note additional commented-out below)
254 testBounds(-10400, -10000);
255 testBounds(-15000, -14000);
256 testBounds(-10000, -9000);
257 testBounds(-1000, -900);
258 testBounds(-1000, -800);
259 testBounds(-1000, -700);
260 testBounds(-1000, -740);
261
262 // Very small ranges (note additional commented-out below)
263 testBounds(0, 0.1);
264 testBounds(0, 0.01);
265 testBounds(0, 0.001);
266 testBounds(0, 0.0001);
267 testBounds(0, 0.000001, false);
268
269#if 0
270 // TODO(): The algorithm in this module is better than it was before,
271 // but the resulting value of X is still wrong under certain conditions,
272 // such as when the range between min and max is around 255,
273 // and the offset is fairly extreme compared to the multiplier.
274 // Not sure why this is, but these ranges are contrived,
275 // and real-world examples would most likely never be this way.
276 testBounds(-10290, -10000);
277 testBounds(-10280, -10000);
278 testBounds(-10275,-10000);
279 testBounds(-10270,-10000);
280 testBounds(-10265,-10000);
281 testBounds(-10260,-10000);
282 testBounds(-10255,-10000);
283 testBounds(-10250,-10000);
284 testBounds(-10245,-10000);
285 testBounds(-10256,-10000);
286 testBounds(-10512, -10000);
287 testBounds(-11024, -10000);
288
289 // TODO(): This also fails, due to extreme small range, loss of precision
290 testBounds(0, 0.00001);
291
292 // TODO(): Interestingly, if bSigned is forced false,
293 // causing "x" to have range of (0,255) instead of (-128,127),
294 // these test cases change from failing to passing!
295 // Not sure why this is, perhaps a mathematician might know.
296 testBounds(-10300, -10000);
297 testBounds(-1000,-750);
298 testBounds(-1e3, (-1e3) + 255);
299 testBounds(-1e4, (-1e4) + 255);
300 testBounds(-1e5, (-1e5) + 255);
301 testBounds(-1e6, (-1e6) + 255);
302#endif
303}
304
305TEST(sensorutils, TranslateToIPMI)
306{
307 /*bool getSensorAttributes(double maxValue, double minValue, int16_t
308 &mValue, int8_t &rExp, int16_t &bValue, int8_t &bExp, bool &bSigned); */
309 // normal unsigned sensor
310 double maxValue = 0xFF;
311 double minValue = 0x0;
312 int16_t mValue;
313 int8_t rExp;
314 int16_t bValue;
315 int8_t bExp;
316 bool bSigned;
317 bool result;
318
319 uint8_t scaledVal;
320
321 result = ipmi::getSensorAttributes(maxValue, minValue, mValue, rExp, bValue,
322 bExp, bSigned);
323 EXPECT_EQ(result, true);
324 if (result)
325 {
326 EXPECT_EQ(bSigned, false);
327 EXPECT_EQ(mValue, 1);
328 EXPECT_EQ(rExp, 0);
329 EXPECT_EQ(bValue, 0);
330 EXPECT_EQ(bExp, 0);
331 }
332 double expected = 0x50;
333 scaledVal = ipmi::scaleIPMIValueFromDouble(0x50, mValue, rExp, bValue, bExp,
334 bSigned);
335 EXPECT_NEAR(scaledVal, expected, expected * 0.01);
336
337 // normal signed sensor
338 maxValue = 127;
339 minValue = -128;
340
341 result = ipmi::getSensorAttributes(maxValue, minValue, mValue, rExp, bValue,
342 bExp, bSigned);
343 EXPECT_EQ(result, true);
344
345 if (result)
346 {
347 EXPECT_EQ(bSigned, true);
348 EXPECT_EQ(mValue, 1);
349 EXPECT_EQ(rExp, 0);
350 EXPECT_EQ(bValue, 0);
351 EXPECT_EQ(bExp, 0);
352 }
353
354 // check negative values
355 expected = 236; // 2s compliment -20
356 scaledVal = ipmi::scaleIPMIValueFromDouble(-20, mValue, rExp, bValue, bExp,
357 bSigned);
358 EXPECT_NEAR(scaledVal, expected, expected * 0.01);
359
360 // fan example
361 maxValue = 16000;
362 minValue = 0;
363
364 result = ipmi::getSensorAttributes(maxValue, minValue, mValue, rExp, bValue,
365 bExp, bSigned);
366 EXPECT_EQ(result, true);
367 if (result)
368 {
369 EXPECT_EQ(bSigned, false);
370 EXPECT_EQ(mValue, floor((16000.0 / 0xFF) + 0.5));
371 EXPECT_EQ(rExp, 0);
372 EXPECT_EQ(bValue, 0);
373 EXPECT_EQ(bExp, 0);
374 }
375
376 // voltage sensor example
377 maxValue = 20;
378 minValue = 0;
379
380 result = ipmi::getSensorAttributes(maxValue, minValue, mValue, rExp, bValue,
381 bExp, bSigned);
382 EXPECT_EQ(result, true);
383 if (result)
384 {
385 EXPECT_EQ(bSigned, false);
386 EXPECT_EQ(mValue, floor(((20.0 / 0xFF) / std::pow(10, rExp)) + 0.5));
387 EXPECT_EQ(rExp, -3);
388 EXPECT_EQ(bValue, 0);
389 EXPECT_EQ(bExp, 0);
390 }
391 scaledVal = ipmi::scaleIPMIValueFromDouble(12.2, mValue, rExp, bValue, bExp,
392 bSigned);
393
394 expected = 12.2 / (mValue * std::pow(10, rExp));
395 EXPECT_NEAR(scaledVal, expected, expected * 0.01);
396
397 // shifted fan example
398 maxValue = 16000;
399 minValue = 8000;
400
401 result = ipmi::getSensorAttributes(maxValue, minValue, mValue, rExp, bValue,
402 bExp, bSigned);
403 EXPECT_EQ(result, true);
404
405 if (result)
406 {
407 EXPECT_EQ(bSigned, false);
408 EXPECT_EQ(mValue, floor(((8000.0 / 0xFF) / std::pow(10, rExp)) + 0.5));
409 EXPECT_EQ(rExp, -1);
410 EXPECT_EQ(bValue, 8);
411 EXPECT_EQ(bExp, 4);
412 }
413
414 // signed voltage sensor example
415 maxValue = 10;
416 minValue = -10;
417
418 result = ipmi::getSensorAttributes(maxValue, minValue, mValue, rExp, bValue,
419 bExp, bSigned);
420 EXPECT_EQ(result, true);
421 if (result)
422 {
423 EXPECT_EQ(bSigned, true);
424 EXPECT_EQ(mValue, floor(((20.0 / 0xFF) / std::pow(10, rExp)) + 0.5));
425 EXPECT_EQ(rExp, -3);
426 // Although this seems like a weird magic number,
427 // it is because the range (-128,127) is not symmetrical about zero,
428 // unlike the range (-10,10), so this introduces some distortion.
429 EXPECT_EQ(bValue, 392);
430 EXPECT_EQ(bExp, -1);
431 }
432
Patrick Williams1318a5e2024-08-16 15:19:54 -0400433 scaledVal =
434 ipmi::scaleIPMIValueFromDouble(5, mValue, rExp, bValue, bExp, bSigned);
Willy Tude54f482021-01-26 15:59:09 -0800435
436 expected = 5 / (mValue * std::pow(10, rExp));
437 EXPECT_NEAR(scaledVal, expected, expected * 0.01);
438
439 // reading = max example
440 maxValue = 277;
441 minValue = 0;
442
443 result = ipmi::getSensorAttributes(maxValue, minValue, mValue, rExp, bValue,
444 bExp, bSigned);
445 EXPECT_EQ(result, true);
446 if (result)
447 {
448 EXPECT_EQ(bSigned, false);
449 }
450
451 scaledVal = ipmi::scaleIPMIValueFromDouble(maxValue, mValue, rExp, bValue,
452 bExp, bSigned);
453
454 expected = 0xFF;
455 EXPECT_NEAR(scaledVal, expected, expected * 0.01);
456
457 // 0, 0 failure
458 maxValue = 0;
459 minValue = 0;
460 result = ipmi::getSensorAttributes(maxValue, minValue, mValue, rExp, bValue,
461 bExp, bSigned);
462 EXPECT_EQ(result, false);
463
464 // too close *success* (was previously failure!)
465 maxValue = 12;
466 minValue = 10;
467 result = ipmi::getSensorAttributes(maxValue, minValue, mValue, rExp, bValue,
468 bExp, bSigned);
469 EXPECT_EQ(result, true);
470 if (result)
471 {
472 EXPECT_EQ(bSigned, false);
473 EXPECT_EQ(mValue, floor(((2.0 / 0xFF) / std::pow(10, rExp)) + 0.5));
474 EXPECT_EQ(rExp, -4);
475 EXPECT_EQ(bValue, 1);
476 EXPECT_EQ(bExp, 5);
477 }
478}
479
480TEST(sensorUtils, TestRanges)
481{
482 // Additional test ranges, each running through a series of values,
483 // to make sure the values of "x" and "y" go together and make sense,
484 // for the resulting scaling attributes from each range.
485 // Unlike the TranslateToIPMI test, exact matches of the
486 // getSensorAttributes() results (the coefficients) are not required,
487 // because they are tested through actual use, relating "x" to "y".
488 testRanges();
489}