blob: cc95cf51723f694e47abec784319dc2f437f4ee1 [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
Patrick Williams5a18f102024-08-16 15:20:38 -0400172static inline bool getSensorAttributes(
173 const double max, const double min, int16_t& mValue, int8_t& rExp,
174 int16_t& bValue, int8_t& bExp, bool& bSigned)
Jason M. Bills5e049d32018-10-19 12:59:38 -0700175{
Jeff Linaaffc122021-02-18 15:03:16 +0800176 if (!(std::isfinite(min)))
Jason M. Bills5e049d32018-10-19 12:59:38 -0700177 {
Jeff Linaaffc122021-02-18 15:03:16 +0800178 std::cerr << "getSensorAttributes: Min value is unusable\n";
179 return false;
180 }
181 if (!(std::isfinite(max)))
182 {
183 std::cerr << "getSensorAttributes: Max value is unusable\n";
Jason M. Bills5e049d32018-10-19 12:59:38 -0700184 return false;
185 }
186
Jeff Linaaffc122021-02-18 15:03:16 +0800187 // Because NAN has already been tested for, this comparison works
188 if (max <= min)
Jason M. Bills5e049d32018-10-19 12:59:38 -0700189 {
Jeff Linaaffc122021-02-18 15:03:16 +0800190 std::cerr << "getSensorAttributes: Max must be greater than min\n";
191 return false;
192 }
193
194 // Given min and max, we must solve for M, B, bExp, rExp
195 // y comes in from D-Bus (the actual sensor reading)
196 // x is calculated from y by scaleIPMIValueFromDouble() below
197 // If y is min, x should equal = 0 (or -128 if signed)
198 // If y is max, x should equal 255 (or 127 if signed)
199 double fullRange = max - min;
200 double lowestX;
201
202 rExp = 0;
203 bExp = 0;
204
205 // TODO(): The IPMI document is ambiguous, as to whether
206 // the resulting byte should be signed or unsigned,
207 // essentially leaving it up to the caller.
208 // The document just refers to it as "raw reading",
209 // or "byte of reading", without giving further details.
210 // Previous code set it signed if min was less than zero,
211 // so I'm sticking with that, until I learn otherwise.
212 if (min < 0.0)
213 {
214 // TODO(): It would be worth experimenting with the range (-127,127),
215 // instead of the range (-128,127), because this
216 // would give good symmetry around zero, and make results look better.
217 // Divide by 254 instead of 255, and change -128 to -127 elsewhere.
Jason M. Bills5e049d32018-10-19 12:59:38 -0700218 bSigned = true;
Jeff Linaaffc122021-02-18 15:03:16 +0800219 lowestX = -128.0;
Jason M. Bills5e049d32018-10-19 12:59:38 -0700220 }
221 else
222 {
223 bSigned = false;
Jeff Linaaffc122021-02-18 15:03:16 +0800224 lowestX = 0.0;
Jason M. Bills5e049d32018-10-19 12:59:38 -0700225 }
226
Jeff Linaaffc122021-02-18 15:03:16 +0800227 // Step 1: Set y to (max - min), set x to 255, set B to 0, solve for M
228 // This works, regardless of signed or unsigned,
229 // because total range is the same.
230 double dM = fullRange / 255.0;
Jason M. Bills5e049d32018-10-19 12:59:38 -0700231
Jeff Linaaffc122021-02-18 15:03:16 +0800232 // Step 2: Constrain M, and set rExp accordingly
233 if (!(scaleFloatExp(dM, rExp)))
Jason M. Bills5e049d32018-10-19 12:59:38 -0700234 {
Jeff Linaaffc122021-02-18 15:03:16 +0800235 std::cerr << "getSensorAttributes: Multiplier range exceeds scale (M="
236 << dM << ", rExp=" << (int)rExp << ")\n";
237 return false;
Jason M. Bills5e049d32018-10-19 12:59:38 -0700238 }
239
Jeff Linaaffc122021-02-18 15:03:16 +0800240 mValue = static_cast<int16_t>(std::round(dM));
241
242 normalizeIntExp(mValue, rExp, dM);
243
244 // The multiplier can not be zero, for obvious reasons
245 if (mValue == 0)
Jason M. Bills5e049d32018-10-19 12:59:38 -0700246 {
Jeff Linaaffc122021-02-18 15:03:16 +0800247 std::cerr << "getSensorAttributes: Multiplier range below scale\n";
248 return false;
Jason M. Bills5e049d32018-10-19 12:59:38 -0700249 }
250
Jeff Linaaffc122021-02-18 15:03:16 +0800251 // Step 3: set y to min, set x to min, keep M and rExp, solve for B
252 // If negative, x will be -128 (the most negative possible byte), not 0
Jason M. Bills5e049d32018-10-19 12:59:38 -0700253
Jeff Linaaffc122021-02-18 15:03:16 +0800254 // Solve the IPMI equation for B, instead of y
255 // 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
256 // B = 10^(-rExp - bExp) (y - M 10^rExp x)
257 // TODO(): Compare with this alternative solution from SageMathCell
258 // https://sagecell.sagemath.org/?z=eJyrtC1LLNJQr1TX5KqAMCuATF8I0xfIdIIwnYDMIteKAggPxAIKJMEFkiACxfk5Zaka0ZUKtrYKGhq-CloKFZoK2goaTkCWhqGBgpaWAkilpqYmQgBklmasjoKTJgDAECTH&lang=sage&interacts=eJyLjgUAARUAuQ==
259 double dB = std::pow(10.0, ((-rExp) - bExp)) *
260 (min - ((dM * std::pow(10.0, rExp) * lowestX)));
261
262 // Step 4: Constrain B, and set bExp accordingly
263 if (!(scaleFloatExp(dB, bExp)))
Jason M. Bills5e049d32018-10-19 12:59:38 -0700264 {
Patrick Williams5a18f102024-08-16 15:20:38 -0400265 std::cerr << "getSensorAttributes: Offset (B=" << dB << ", bExp="
266 << (int)bExp << ") exceeds multiplier scale (M=" << dM
Jeff Linaaffc122021-02-18 15:03:16 +0800267 << ", rExp=" << (int)rExp << ")\n";
268 return false;
Jason M. Bills5e049d32018-10-19 12:59:38 -0700269 }
270
Jeff Linaaffc122021-02-18 15:03:16 +0800271 bValue = static_cast<int16_t>(std::round(dB));
Jason M. Bills5e049d32018-10-19 12:59:38 -0700272
Jeff Linaaffc122021-02-18 15:03:16 +0800273 normalizeIntExp(bValue, bExp, dB);
Jason M. Bills5e049d32018-10-19 12:59:38 -0700274
Jeff Linaaffc122021-02-18 15:03:16 +0800275 // Unlike the multiplier, it is perfectly OK for bValue to be zero
Jason M. Bills5e049d32018-10-19 12:59:38 -0700276 return true;
277}
278
Patrick Williams5a18f102024-08-16 15:20:38 -0400279static inline uint8_t scaleIPMIValueFromDouble(
280 const double value, const int16_t mValue, const int8_t rExp,
281 const int16_t bValue, const int8_t bExp, const bool bSigned)
Jason M. Bills5e049d32018-10-19 12:59:38 -0700282{
Jeff Linaaffc122021-02-18 15:03:16 +0800283 // Avoid division by zero below
284 if (mValue == 0)
285 {
286 throw std::out_of_range("Scaling multiplier is uninitialized");
287 }
Jason M. Bills5e049d32018-10-19 12:59:38 -0700288
Jeff Linaaffc122021-02-18 15:03:16 +0800289 auto dM = static_cast<double>(mValue);
290 auto dB = static_cast<double>(bValue);
291
292 // Solve the IPMI equation for x, instead of y
293 // 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
294 // x = (10^(-rExp) (y - B 10^(rExp + bExp)))/M and M 10^rExp!=0
295 // TODO(): Compare with this alternative solution from SageMathCell
296 // https://sagecell.sagemath.org/?z=eJyrtC1LLNJQr1TX5KqAMCuATF8I0xfIdIIwnYDMIteKAggPxAIKJMEFkiACxfk5Zaka0ZUKtrYKGhq-CloKFZoK2goaTkCWhqGBgpaWAkilpqYmQgBklmasDlAlAMB8JP0=&lang=sage&interacts=eJyLjgUAARUAuQ==
297 double dX =
298 (std::pow(10.0, -rExp) * (value - (dB * std::pow(10.0, rExp + bExp)))) /
299 dM;
300
301 auto scaledValue = static_cast<int32_t>(std::round(dX));
302
303 int32_t minClamp;
304 int32_t maxClamp;
305
306 // Because of rounding and integer truncation of scaling factors,
307 // sometimes the resulting byte is slightly out of range.
308 // Still allow this, but clamp the values to range.
Jason M. Bills5e049d32018-10-19 12:59:38 -0700309 if (bSigned)
310 {
Jeff Linaaffc122021-02-18 15:03:16 +0800311 minClamp = std::numeric_limits<int8_t>::lowest();
312 maxClamp = std::numeric_limits<int8_t>::max();
Jason M. Bills5e049d32018-10-19 12:59:38 -0700313 }
314 else
315 {
Jeff Linaaffc122021-02-18 15:03:16 +0800316 minClamp = std::numeric_limits<uint8_t>::lowest();
317 maxClamp = std::numeric_limits<uint8_t>::max();
Jason M. Bills5e049d32018-10-19 12:59:38 -0700318 }
Jeff Linaaffc122021-02-18 15:03:16 +0800319
320 auto clampedValue = std::clamp(scaledValue, minClamp, maxClamp);
321
322 // This works for both signed and unsigned,
323 // because it is the same underlying byte storage.
324 return static_cast<uint8_t>(clampedValue);
Jason M. Bills5e049d32018-10-19 12:59:38 -0700325}
326
327static inline uint8_t getScaledIPMIValue(const double value, const double max,
328 const double min)
329{
330 int16_t mValue = 0;
331 int8_t rExp = 0;
332 int16_t bValue = 0;
333 int8_t bExp = 0;
Jeff Linaaffc122021-02-18 15:03:16 +0800334 bool bSigned = false;
Jason M. Bills5e049d32018-10-19 12:59:38 -0700335
Patrick Williams5a18f102024-08-16 15:20:38 -0400336 bool result =
337 getSensorAttributes(max, min, mValue, rExp, bValue, bExp, bSigned);
Jason M. Bills5e049d32018-10-19 12:59:38 -0700338 if (!result)
339 {
340 throw std::runtime_error("Illegal sensor attributes");
341 }
Jeff Linaaffc122021-02-18 15:03:16 +0800342
Jason M. Bills5e049d32018-10-19 12:59:38 -0700343 return scaleIPMIValueFromDouble(value, mValue, rExp, bValue, bExp, bSigned);
344}
345
Jeff Linaaffc122021-02-18 15:03:16 +0800346} // namespace ipmi