blob: d7fee6d54776a3a817c3568ec862c0ce75818b1d [file] [log] [blame]
Jason M. Bills5e049d32018-10-19 12:59:38 -07001/*
2// Copyright (c) 2017 2018 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#pragma once
Jeff Linaaffc122021-02-18 15:03:16 +080018#include <algorithm>
Jason M. Bills5e049d32018-10-19 12:59:38 -070019#include <cmath>
20#include <iostream>
Jason M. Bills5e049d32018-10-19 12:59:38 -070021
22namespace ipmi
23{
24/** @struct VariantToDoubleVisitor
25 * @brief Visitor to convert variants to doubles
26 * @details Performs a static cast on the underlying type
27 */
28struct VariantToDoubleVisitor
29{
Zhikui Ren672bdfc2020-07-14 11:37:01 -070030 template <typename T>
31 double operator()(const T& t) const
Jason M. Bills5e049d32018-10-19 12:59:38 -070032 {
33 static_assert(std::is_arithmetic_v<T>,
34 "Cannot translate type to double");
35 return static_cast<double>(t);
36 }
37};
38
39static constexpr int16_t maxInt10 = 0x1FF;
40static constexpr int16_t minInt10 = -0x200;
41static constexpr int8_t maxInt4 = 7;
42static constexpr int8_t minInt4 = -8;
43
Jeff Linaaffc122021-02-18 15:03:16 +080044// Helper function to avoid repeated complicated expression
45// TODO(): Refactor to add a proper sensorutils.cpp file,
46// instead of putting everything in this header as it is now,
47// so that helper functions can be correctly hidden from callers.
48static inline bool baseInRange(double base)
49{
50 auto min10 = static_cast<double>(minInt10);
51 auto max10 = static_cast<double>(maxInt10);
52
53 return ((base >= min10) && (base <= max10));
54}
55
56// Helper function for internal use by getSensorAttributes()
57// Ensures floating-point "base" is within bounds,
58// and adjusts integer exponent "expShift" accordingly.
59// To minimize data loss when later truncating to integer,
60// the floating-point "base" will be as large as possible,
61// but still within the bounds (minInt10,maxInt10).
62// The bounds of "expShift" are (minInt4,maxInt4).
63// Consider this equation: n = base * (10.0 ** expShift)
64// This function will try to maximize "base",
65// adjusting "expShift" to keep the value "n" unchanged,
66// while keeping base and expShift within bounds.
67// Returns true if successful, modifies values in-place
68static inline bool scaleFloatExp(double& base, int8_t& expShift)
69{
70 // Comparing with zero should be OK, zero is special in floating-point
71 // If base is exactly zero, no adjustment of the exponent is necessary
72 if (base == 0.0)
73 {
74 return true;
75 }
76
77 // As long as base value is within allowed range, expand precision
78 // This will help to avoid loss when later rounding to integer
79 while (baseInRange(base))
80 {
81 if (expShift <= minInt4)
82 {
83 // Already at the minimum expShift, can not decrement it more
84 break;
85 }
86
87 // Multiply by 10, but shift decimal point to the left, no net change
88 base *= 10.0;
89 --expShift;
90 }
91
92 // As long as base value is *not* within range, shrink precision
93 // This will pull base value closer to zero, thus within range
94 while (!(baseInRange(base)))
95 {
96 if (expShift >= maxInt4)
97 {
98 // Already at the maximum expShift, can not increment it more
99 break;
100 }
101
102 // Divide by 10, but shift decimal point to the right, no net change
103 base /= 10.0;
104 ++expShift;
105 }
106
107 // If the above loop was not able to pull it back within range,
108 // the base value is beyond what expShift can represent, return false.
109 return baseInRange(base);
110}
111
112// Helper function for internal use by getSensorAttributes()
113// Ensures integer "ibase" is no larger than necessary,
114// by normalizing it so that the decimal point shift is in the exponent,
115// whenever possible.
116// This provides more consistent results,
117// as many equivalent solutions are collapsed into one consistent solution.
118// If integer "ibase" is a clean multiple of 10,
119// divide it by 10 (this is lossless), so it is closer to zero.
120// Also modify floating-point "dbase" at the same time,
121// as both integer and floating-point base share the same expShift.
122// Example: (ibase=300, expShift=2) becomes (ibase=3, expShift=4)
123// because the underlying value is the same: 200*(10**2) == 2*(10**4)
124// Always successful, modifies values in-place
125static inline void normalizeIntExp(int16_t& ibase, int8_t& expShift,
126 double& dbase)
127{
128 for (;;)
129 {
130 // If zero, already normalized, ensure exponent also zero
131 if (ibase == 0)
132 {
133 expShift = 0;
134 break;
135 }
136
137 // If not cleanly divisible by 10, already normalized
138 if ((ibase % 10) != 0)
139 {
140 break;
141 }
142
143 // If exponent already at max, already normalized
144 if (expShift >= maxInt4)
145 {
146 break;
147 }
148
149 // Bring values closer to zero, correspondingly shift exponent,
150 // without changing the underlying number that this all represents,
151 // similar to what is done by scaleFloatExp().
152 // The floating-point base must be kept in sync with the integer base,
153 // as both floating-point and integer share the same exponent.
154 ibase /= 10;
155 dbase /= 10.0;
156 ++expShift;
157 }
158}
159
160// The IPMI equation:
161// y = (Mx + (B * 10^(bExp))) * 10^(rExp)
162// Section 36.3 of this document:
163// https://www.intel.com/content/dam/www/public/us/en/documents/product-briefs/ipmi-second-gen-interface-spec-v2-rev1-1.pdf
164//
165// The goal is to exactly match the math done by the ipmitool command,
166// at the other side of the interface:
167// https://github.com/ipmitool/ipmitool/blob/42a023ff0726c80e8cc7d30315b987fe568a981d/lib/ipmi_sdr.c#L360
168//
169// To use with Wolfram Alpha, make all variables single letters
170// bExp becomes E, rExp becomes R
171// https://www.wolframalpha.com/input/?i=y%3D%28%28M*x%29%2B%28B*%2810%5EE%29%29%29*%2810%5ER%29
Jason M. Bills5e049d32018-10-19 12:59:38 -0700172static inline bool getSensorAttributes(const double max, const double min,
173 int16_t& mValue, int8_t& rExp,
174 int16_t& bValue, int8_t& bExp,
175 bool& bSigned)
176{
Jeff Linaaffc122021-02-18 15:03:16 +0800177 if (!(std::isfinite(min)))
Jason M. Bills5e049d32018-10-19 12:59:38 -0700178 {
Jeff Linaaffc122021-02-18 15:03:16 +0800179 std::cerr << "getSensorAttributes: Min value is unusable\n";
180 return false;
181 }
182 if (!(std::isfinite(max)))
183 {
184 std::cerr << "getSensorAttributes: Max value is unusable\n";
Jason M. Bills5e049d32018-10-19 12:59:38 -0700185 return false;
186 }
187
Jeff Linaaffc122021-02-18 15:03:16 +0800188 // Because NAN has already been tested for, this comparison works
189 if (max <= min)
Jason M. Bills5e049d32018-10-19 12:59:38 -0700190 {
Jeff Linaaffc122021-02-18 15:03:16 +0800191 std::cerr << "getSensorAttributes: Max must be greater than min\n";
192 return false;
193 }
194
195 // Given min and max, we must solve for M, B, bExp, rExp
196 // y comes in from D-Bus (the actual sensor reading)
197 // x is calculated from y by scaleIPMIValueFromDouble() below
198 // If y is min, x should equal = 0 (or -128 if signed)
199 // If y is max, x should equal 255 (or 127 if signed)
200 double fullRange = max - min;
201 double lowestX;
202
203 rExp = 0;
204 bExp = 0;
205
206 // TODO(): The IPMI document is ambiguous, as to whether
207 // the resulting byte should be signed or unsigned,
208 // essentially leaving it up to the caller.
209 // The document just refers to it as "raw reading",
210 // or "byte of reading", without giving further details.
211 // Previous code set it signed if min was less than zero,
212 // so I'm sticking with that, until I learn otherwise.
213 if (min < 0.0)
214 {
215 // TODO(): It would be worth experimenting with the range (-127,127),
216 // instead of the range (-128,127), because this
217 // would give good symmetry around zero, and make results look better.
218 // Divide by 254 instead of 255, and change -128 to -127 elsewhere.
Jason M. Bills5e049d32018-10-19 12:59:38 -0700219 bSigned = true;
Jeff Linaaffc122021-02-18 15:03:16 +0800220 lowestX = -128.0;
Jason M. Bills5e049d32018-10-19 12:59:38 -0700221 }
222 else
223 {
224 bSigned = false;
Jeff Linaaffc122021-02-18 15:03:16 +0800225 lowestX = 0.0;
Jason M. Bills5e049d32018-10-19 12:59:38 -0700226 }
227
Jeff Linaaffc122021-02-18 15:03:16 +0800228 // Step 1: Set y to (max - min), set x to 255, set B to 0, solve for M
229 // This works, regardless of signed or unsigned,
230 // because total range is the same.
231 double dM = fullRange / 255.0;
Jason M. Bills5e049d32018-10-19 12:59:38 -0700232
Jeff Linaaffc122021-02-18 15:03:16 +0800233 // Step 2: Constrain M, and set rExp accordingly
234 if (!(scaleFloatExp(dM, rExp)))
Jason M. Bills5e049d32018-10-19 12:59:38 -0700235 {
Jeff Linaaffc122021-02-18 15:03:16 +0800236 std::cerr << "getSensorAttributes: Multiplier range exceeds scale (M="
237 << dM << ", rExp=" << (int)rExp << ")\n";
238 return false;
Jason M. Bills5e049d32018-10-19 12:59:38 -0700239 }
240
Jeff Linaaffc122021-02-18 15:03:16 +0800241 mValue = static_cast<int16_t>(std::round(dM));
242
243 normalizeIntExp(mValue, rExp, dM);
244
245 // The multiplier can not be zero, for obvious reasons
246 if (mValue == 0)
Jason M. Bills5e049d32018-10-19 12:59:38 -0700247 {
Jeff Linaaffc122021-02-18 15:03:16 +0800248 std::cerr << "getSensorAttributes: Multiplier range below scale\n";
249 return false;
Jason M. Bills5e049d32018-10-19 12:59:38 -0700250 }
251
Jeff Linaaffc122021-02-18 15:03:16 +0800252 // Step 3: set y to min, set x to min, keep M and rExp, solve for B
253 // If negative, x will be -128 (the most negative possible byte), not 0
Jason M. Bills5e049d32018-10-19 12:59:38 -0700254
Jeff Linaaffc122021-02-18 15:03:16 +0800255 // Solve the IPMI equation for B, instead of y
256 // https://www.wolframalpha.com/input/?i=solve+y%3D%28%28M*x%29%2B%28B*%2810%5EE%29%29%29*%2810%5ER%29+for+B
257 // B = 10^(-rExp - bExp) (y - M 10^rExp x)
258 // TODO(): Compare with this alternative solution from SageMathCell
259 // https://sagecell.sagemath.org/?z=eJyrtC1LLNJQr1TX5KqAMCuATF8I0xfIdIIwnYDMIteKAggPxAIKJMEFkiACxfk5Zaka0ZUKtrYKGhq-CloKFZoK2goaTkCWhqGBgpaWAkilpqYmQgBklmasjoKTJgDAECTH&lang=sage&interacts=eJyLjgUAARUAuQ==
260 double dB = std::pow(10.0, ((-rExp) - bExp)) *
261 (min - ((dM * std::pow(10.0, rExp) * lowestX)));
262
263 // Step 4: Constrain B, and set bExp accordingly
264 if (!(scaleFloatExp(dB, bExp)))
Jason M. Bills5e049d32018-10-19 12:59:38 -0700265 {
Jeff Linaaffc122021-02-18 15:03:16 +0800266 std::cerr << "getSensorAttributes: Offset (B=" << dB
267 << ", bExp=" << (int)bExp
268 << ") exceeds multiplier scale (M=" << dM
269 << ", rExp=" << (int)rExp << ")\n";
270 return false;
Jason M. Bills5e049d32018-10-19 12:59:38 -0700271 }
272
Jeff Linaaffc122021-02-18 15:03:16 +0800273 bValue = static_cast<int16_t>(std::round(dB));
Jason M. Bills5e049d32018-10-19 12:59:38 -0700274
Jeff Linaaffc122021-02-18 15:03:16 +0800275 normalizeIntExp(bValue, bExp, dB);
Jason M. Bills5e049d32018-10-19 12:59:38 -0700276
Jeff Linaaffc122021-02-18 15:03:16 +0800277 // Unlike the multiplier, it is perfectly OK for bValue to be zero
Jason M. Bills5e049d32018-10-19 12:59:38 -0700278 return true;
279}
280
281static inline uint8_t
Jeff Linaaffc122021-02-18 15:03:16 +0800282 scaleIPMIValueFromDouble(const double value, const int16_t mValue,
283 const int8_t rExp, const int16_t bValue,
Jason M. Bills5e049d32018-10-19 12:59:38 -0700284 const int8_t bExp, const bool bSigned)
285{
Jeff Linaaffc122021-02-18 15:03:16 +0800286 // Avoid division by zero below
287 if (mValue == 0)
288 {
289 throw std::out_of_range("Scaling multiplier is uninitialized");
290 }
Jason M. Bills5e049d32018-10-19 12:59:38 -0700291
Jeff Linaaffc122021-02-18 15:03:16 +0800292 auto dM = static_cast<double>(mValue);
293 auto dB = static_cast<double>(bValue);
294
295 // Solve the IPMI equation for x, instead of y
296 // https://www.wolframalpha.com/input/?i=solve+y%3D%28%28M*x%29%2B%28B*%2810%5EE%29%29%29*%2810%5ER%29+for+x
297 // x = (10^(-rExp) (y - B 10^(rExp + bExp)))/M and M 10^rExp!=0
298 // TODO(): Compare with this alternative solution from SageMathCell
299 // https://sagecell.sagemath.org/?z=eJyrtC1LLNJQr1TX5KqAMCuATF8I0xfIdIIwnYDMIteKAggPxAIKJMEFkiACxfk5Zaka0ZUKtrYKGhq-CloKFZoK2goaTkCWhqGBgpaWAkilpqYmQgBklmasDlAlAMB8JP0=&lang=sage&interacts=eJyLjgUAARUAuQ==
300 double dX =
301 (std::pow(10.0, -rExp) * (value - (dB * std::pow(10.0, rExp + bExp)))) /
302 dM;
303
304 auto scaledValue = static_cast<int32_t>(std::round(dX));
305
306 int32_t minClamp;
307 int32_t maxClamp;
308
309 // Because of rounding and integer truncation of scaling factors,
310 // sometimes the resulting byte is slightly out of range.
311 // Still allow this, but clamp the values to range.
Jason M. Bills5e049d32018-10-19 12:59:38 -0700312 if (bSigned)
313 {
Jeff Linaaffc122021-02-18 15:03:16 +0800314 minClamp = std::numeric_limits<int8_t>::lowest();
315 maxClamp = std::numeric_limits<int8_t>::max();
Jason M. Bills5e049d32018-10-19 12:59:38 -0700316 }
317 else
318 {
Jeff Linaaffc122021-02-18 15:03:16 +0800319 minClamp = std::numeric_limits<uint8_t>::lowest();
320 maxClamp = std::numeric_limits<uint8_t>::max();
Jason M. Bills5e049d32018-10-19 12:59:38 -0700321 }
Jeff Linaaffc122021-02-18 15:03:16 +0800322
323 auto clampedValue = std::clamp(scaledValue, minClamp, maxClamp);
324
325 // This works for both signed and unsigned,
326 // because it is the same underlying byte storage.
327 return static_cast<uint8_t>(clampedValue);
Jason M. Bills5e049d32018-10-19 12:59:38 -0700328}
329
330static inline uint8_t getScaledIPMIValue(const double value, const double max,
331 const double min)
332{
333 int16_t mValue = 0;
334 int8_t rExp = 0;
335 int16_t bValue = 0;
336 int8_t bExp = 0;
Jeff Linaaffc122021-02-18 15:03:16 +0800337 bool bSigned = false;
Jason M. Bills5e049d32018-10-19 12:59:38 -0700338
Jeff Linaaffc122021-02-18 15:03:16 +0800339 bool result =
340 getSensorAttributes(max, min, mValue, rExp, bValue, bExp, bSigned);
Jason M. Bills5e049d32018-10-19 12:59:38 -0700341 if (!result)
342 {
343 throw std::runtime_error("Illegal sensor attributes");
344 }
Jeff Linaaffc122021-02-18 15:03:16 +0800345
Jason M. Bills5e049d32018-10-19 12:59:38 -0700346 return scaleIPMIValueFromDouble(value, mValue, rExp, bValue, bExp, bSigned);
347}
348
Jeff Linaaffc122021-02-18 15:03:16 +0800349} // namespace ipmi