13f7c5e40SJason M. Bills /*
23f7c5e40SJason M. Bills // Copyright (c) 2017 2018 Intel Corporation
33f7c5e40SJason M. Bills //
43f7c5e40SJason M. Bills // Licensed under the Apache License, Version 2.0 (the "License");
53f7c5e40SJason M. Bills // you may not use this file except in compliance with the License.
63f7c5e40SJason M. Bills // You may obtain a copy of the License at
73f7c5e40SJason M. Bills //
83f7c5e40SJason M. Bills //      http://www.apache.org/licenses/LICENSE-2.0
93f7c5e40SJason M. Bills //
103f7c5e40SJason M. Bills // Unless required by applicable law or agreed to in writing, software
113f7c5e40SJason M. Bills // distributed under the License is distributed on an "AS IS" BASIS,
123f7c5e40SJason M. Bills // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
133f7c5e40SJason M. Bills // See the License for the specific language governing permissions and
143f7c5e40SJason M. Bills // limitations under the License.
153f7c5e40SJason M. Bills */
163f7c5e40SJason M. Bills 
173f7c5e40SJason M. Bills #pragma once
18*17e21c20SJosh Lehan #include <algorithm>
1972867debSJason M. Bills #include <cmath>
2072867debSJason M. Bills #include <iostream>
213f7c5e40SJason M. Bills 
223f7c5e40SJason M. Bills namespace ipmi
233f7c5e40SJason M. Bills {
2472867debSJason M. Bills static constexpr int16_t maxInt10 = 0x1FF;
2539417c7bSJames Feist static constexpr int16_t minInt10 = -0x200;
2672867debSJason M. Bills static constexpr int8_t maxInt4 = 7;
2772867debSJason M. Bills static constexpr int8_t minInt4 = -8;
2872867debSJason M. Bills 
29*17e21c20SJosh Lehan // Helper function to avoid repeated complicated expression
30*17e21c20SJosh Lehan // TODO(): Refactor to add a proper sensorutils.cpp file,
31*17e21c20SJosh Lehan // instead of putting everything in this header as it is now,
32*17e21c20SJosh Lehan // so that helper functions can be correctly hidden from callers.
33*17e21c20SJosh Lehan static inline bool baseInRange(double base)
34*17e21c20SJosh Lehan {
35*17e21c20SJosh Lehan     auto min10 = static_cast<double>(minInt10);
36*17e21c20SJosh Lehan     auto max10 = static_cast<double>(maxInt10);
37*17e21c20SJosh Lehan 
38*17e21c20SJosh Lehan     return ((base >= min10) && (base <= max10));
39*17e21c20SJosh Lehan }
40*17e21c20SJosh Lehan 
41*17e21c20SJosh Lehan // Helper function for internal use by getSensorAttributes()
42*17e21c20SJosh Lehan // Ensures floating-point "base" is within bounds,
43*17e21c20SJosh Lehan // and adjusts integer exponent "expShift" accordingly.
44*17e21c20SJosh Lehan // To minimize data loss when later truncating to integer,
45*17e21c20SJosh Lehan // the floating-point "base" will be as large as possible,
46*17e21c20SJosh Lehan // but still within the bounds (minInt10,maxInt10).
47*17e21c20SJosh Lehan // The bounds of "expShift" are (minInt4,maxInt4).
48*17e21c20SJosh Lehan // Consider this equation: n = base * (10.0 ** expShift)
49*17e21c20SJosh Lehan // This function will try to maximize "base",
50*17e21c20SJosh Lehan // adjusting "expShift" to keep the value "n" unchanged,
51*17e21c20SJosh Lehan // while keeping base and expShift within bounds.
52*17e21c20SJosh Lehan // Returns true if successful, modifies values in-place
53*17e21c20SJosh Lehan static inline bool scaleFloatExp(double& base, int8_t& expShift)
54*17e21c20SJosh Lehan {
55*17e21c20SJosh Lehan     auto min10 = static_cast<double>(minInt10);
56*17e21c20SJosh Lehan     auto max10 = static_cast<double>(maxInt10);
57*17e21c20SJosh Lehan 
58*17e21c20SJosh Lehan     // Comparing with zero should be OK, zero is special in floating-point
59*17e21c20SJosh Lehan     // If base is exactly zero, no adjustment of the exponent is necessary
60*17e21c20SJosh Lehan     if (base == 0.0)
61*17e21c20SJosh Lehan     {
62*17e21c20SJosh Lehan         return true;
63*17e21c20SJosh Lehan     }
64*17e21c20SJosh Lehan 
65*17e21c20SJosh Lehan     // As long as base value is within allowed range, expand precision
66*17e21c20SJosh Lehan     // This will help to avoid loss when later rounding to integer
67*17e21c20SJosh Lehan     while (baseInRange(base))
68*17e21c20SJosh Lehan     {
69*17e21c20SJosh Lehan         if (expShift <= minInt4)
70*17e21c20SJosh Lehan         {
71*17e21c20SJosh Lehan             // Already at the minimum expShift, can not decrement it more
72*17e21c20SJosh Lehan             break;
73*17e21c20SJosh Lehan         }
74*17e21c20SJosh Lehan 
75*17e21c20SJosh Lehan         // Multiply by 10, but shift decimal point to the left, no net change
76*17e21c20SJosh Lehan         base *= 10.0;
77*17e21c20SJosh Lehan         --expShift;
78*17e21c20SJosh Lehan     }
79*17e21c20SJosh Lehan 
80*17e21c20SJosh Lehan     // As long as base value is *not* within range, shrink precision
81*17e21c20SJosh Lehan     // This will pull base value closer to zero, thus within range
82*17e21c20SJosh Lehan     while (!(baseInRange(base)))
83*17e21c20SJosh Lehan     {
84*17e21c20SJosh Lehan         if (expShift >= maxInt4)
85*17e21c20SJosh Lehan         {
86*17e21c20SJosh Lehan             // Already at the maximum expShift, can not increment it more
87*17e21c20SJosh Lehan             break;
88*17e21c20SJosh Lehan         }
89*17e21c20SJosh Lehan 
90*17e21c20SJosh Lehan         // Divide by 10, but shift decimal point to the right, no net change
91*17e21c20SJosh Lehan         base /= 10.0;
92*17e21c20SJosh Lehan         ++expShift;
93*17e21c20SJosh Lehan     }
94*17e21c20SJosh Lehan 
95*17e21c20SJosh Lehan     // If the above loop was not able to pull it back within range,
96*17e21c20SJosh Lehan     // the base value is beyond what expShift can represent, return false.
97*17e21c20SJosh Lehan     return baseInRange(base);
98*17e21c20SJosh Lehan }
99*17e21c20SJosh Lehan 
100*17e21c20SJosh Lehan // Helper function for internal use by getSensorAttributes()
101*17e21c20SJosh Lehan // Ensures integer "ibase" is no larger than necessary,
102*17e21c20SJosh Lehan // by normalizing it so that the decimal point shift is in the exponent,
103*17e21c20SJosh Lehan // whenever possible.
104*17e21c20SJosh Lehan // This provides more consistent results,
105*17e21c20SJosh Lehan // as many equivalent solutions are collapsed into one consistent solution.
106*17e21c20SJosh Lehan // If integer "ibase" is a clean multiple of 10,
107*17e21c20SJosh Lehan // divide it by 10 (this is lossless), so it is closer to zero.
108*17e21c20SJosh Lehan // Also modify floating-point "dbase" at the same time,
109*17e21c20SJosh Lehan // as both integer and floating-point base share the same expShift.
110*17e21c20SJosh Lehan // Example: (ibase=300, expShift=2) becomes (ibase=3, expShift=4)
111*17e21c20SJosh Lehan // because the underlying value is the same: 200*(10**2) == 2*(10**4)
112*17e21c20SJosh Lehan // Always successful, modifies values in-place
113*17e21c20SJosh Lehan static inline void normalizeIntExp(int16_t& ibase, int8_t& expShift,
114*17e21c20SJosh Lehan                                    double& dbase)
115*17e21c20SJosh Lehan {
116*17e21c20SJosh Lehan     for (;;)
117*17e21c20SJosh Lehan     {
118*17e21c20SJosh Lehan         // If zero, already normalized, ensure exponent also zero
119*17e21c20SJosh Lehan         if (ibase == 0)
120*17e21c20SJosh Lehan         {
121*17e21c20SJosh Lehan             expShift = 0;
122*17e21c20SJosh Lehan             break;
123*17e21c20SJosh Lehan         }
124*17e21c20SJosh Lehan 
125*17e21c20SJosh Lehan         // If not cleanly divisible by 10, already normalized
126*17e21c20SJosh Lehan         if ((ibase % 10) != 0)
127*17e21c20SJosh Lehan         {
128*17e21c20SJosh Lehan             break;
129*17e21c20SJosh Lehan         }
130*17e21c20SJosh Lehan 
131*17e21c20SJosh Lehan         // If exponent already at max, already normalized
132*17e21c20SJosh Lehan         if (expShift >= maxInt4)
133*17e21c20SJosh Lehan         {
134*17e21c20SJosh Lehan             break;
135*17e21c20SJosh Lehan         }
136*17e21c20SJosh Lehan 
137*17e21c20SJosh Lehan         // Bring values closer to zero, correspondingly shift exponent,
138*17e21c20SJosh Lehan         // without changing the underlying number that this all represents,
139*17e21c20SJosh Lehan         // similar to what is done by scaleFloatExp().
140*17e21c20SJosh Lehan         // The floating-point base must be kept in sync with the integer base,
141*17e21c20SJosh Lehan         // as both floating-point and integer share the same exponent.
142*17e21c20SJosh Lehan         ibase /= 10;
143*17e21c20SJosh Lehan         dbase /= 10.0;
144*17e21c20SJosh Lehan         ++expShift;
145*17e21c20SJosh Lehan     }
146*17e21c20SJosh Lehan }
147*17e21c20SJosh Lehan 
148*17e21c20SJosh Lehan // The IPMI equation:
149*17e21c20SJosh Lehan // y = (Mx + (B * 10^(bExp))) * 10^(rExp)
150*17e21c20SJosh Lehan // Section 36.3 of this document:
151*17e21c20SJosh Lehan // https://www.intel.com/content/dam/www/public/us/en/documents/product-briefs/ipmi-second-gen-interface-spec-v2-rev1-1.pdf
152*17e21c20SJosh Lehan //
153*17e21c20SJosh Lehan // The goal is to exactly match the math done by the ipmitool command,
154*17e21c20SJosh Lehan // at the other side of the interface:
155*17e21c20SJosh Lehan // https://github.com/ipmitool/ipmitool/blob/42a023ff0726c80e8cc7d30315b987fe568a981d/lib/ipmi_sdr.c#L360
156*17e21c20SJosh Lehan //
157*17e21c20SJosh Lehan // To use with Wolfram Alpha, make all variables single letters
158*17e21c20SJosh Lehan // bExp becomes E, rExp becomes R
159*17e21c20SJosh Lehan // https://www.wolframalpha.com/input/?i=y%3D%28%28M*x%29%2B%28B*%2810%5EE%29%29%29*%2810%5ER%29
16072867debSJason M. Bills static inline bool getSensorAttributes(const double max, const double min,
16172867debSJason M. Bills                                        int16_t& mValue, int8_t& rExp,
16272867debSJason M. Bills                                        int16_t& bValue, int8_t& bExp,
16372867debSJason M. Bills                                        bool& bSigned)
16472867debSJason M. Bills {
165*17e21c20SJosh Lehan     if (!(std::isfinite(min)))
166*17e21c20SJosh Lehan     {
167*17e21c20SJosh Lehan         std::cerr << "getSensorAttributes: Min value is unusable\n";
168*17e21c20SJosh Lehan         return false;
169*17e21c20SJosh Lehan     }
170*17e21c20SJosh Lehan     if (!(std::isfinite(max)))
171*17e21c20SJosh Lehan     {
172*17e21c20SJosh Lehan         std::cerr << "getSensorAttributes: Max value is unusable\n";
173*17e21c20SJosh Lehan         return false;
174*17e21c20SJosh Lehan     }
175*17e21c20SJosh Lehan 
176*17e21c20SJosh Lehan     // Because NAN has already been tested for, this comparison works
17739417c7bSJames Feist     if (max <= min)
17872867debSJason M. Bills     {
17953870d73SVernon Mauery         std::cerr << "getSensorAttributes: Max must be greater than min\n";
18072867debSJason M. Bills         return false;
18172867debSJason M. Bills     }
18239417c7bSJames Feist 
183*17e21c20SJosh Lehan     // Given min and max, we must solve for M, B, bExp, rExp
184*17e21c20SJosh Lehan     // y comes in from D-Bus (the actual sensor reading)
185*17e21c20SJosh Lehan     // x is calculated from y by scaleIPMIValueFromDouble() below
186*17e21c20SJosh Lehan     // If y is min, x should equal = 0 (or -128 if signed)
187*17e21c20SJosh Lehan     // If y is max, x should equal 255 (or 127 if signed)
188*17e21c20SJosh Lehan     double fullRange = max - min;
189*17e21c20SJosh Lehan     double lowestX;
19072867debSJason M. Bills 
191*17e21c20SJosh Lehan     rExp = 0;
192*17e21c20SJosh Lehan     bExp = 0;
193*17e21c20SJosh Lehan 
194*17e21c20SJosh Lehan     // TODO(): The IPMI document is ambiguous, as to whether
195*17e21c20SJosh Lehan     // the resulting byte should be signed or unsigned,
196*17e21c20SJosh Lehan     // essentially leaving it up to the caller.
197*17e21c20SJosh Lehan     // The document just refers to it as "raw reading",
198*17e21c20SJosh Lehan     // or "byte of reading", without giving further details.
199*17e21c20SJosh Lehan     // Previous code set it signed if min was less than zero,
200*17e21c20SJosh Lehan     // so I'm sticking with that, until I learn otherwise.
201*17e21c20SJosh Lehan     if (min < 0.0)
20272867debSJason M. Bills     {
203*17e21c20SJosh Lehan         // TODO(): It would be worth experimenting with the range (-127,127),
204*17e21c20SJosh Lehan         // instead of the range (-128,127), because this
205*17e21c20SJosh Lehan         // would give good symmetry around zero, and make results look better.
206*17e21c20SJosh Lehan         // Divide by 254 instead of 255, and change -128 to -127 elsewhere.
20772867debSJason M. Bills         bSigned = true;
208*17e21c20SJosh Lehan         lowestX = -128.0;
20972867debSJason M. Bills     }
21072867debSJason M. Bills     else
21172867debSJason M. Bills     {
21272867debSJason M. Bills         bSigned = false;
213*17e21c20SJosh Lehan         lowestX = 0.0;
21472867debSJason M. Bills     }
21572867debSJason M. Bills 
216*17e21c20SJosh Lehan     // Step 1: Set y to (max - min), set x to 255, set B to 0, solve for M
217*17e21c20SJosh Lehan     // This works, regardless of signed or unsigned,
218*17e21c20SJosh Lehan     // because total range is the same.
219*17e21c20SJosh Lehan     double dM = fullRange / 255.0;
22072867debSJason M. Bills 
221*17e21c20SJosh Lehan     // Step 2: Constrain M, and set rExp accordingly
222*17e21c20SJosh Lehan     if (!(scaleFloatExp(dM, rExp)))
22372867debSJason M. Bills     {
224*17e21c20SJosh Lehan         std::cerr << "getSensorAttributes: Multiplier range exceeds scale (M="
225*17e21c20SJosh Lehan                   << dM << ", rExp=" << (int)rExp << ")\n";
22672867debSJason M. Bills         return false;
22772867debSJason M. Bills     }
22872867debSJason M. Bills 
229*17e21c20SJosh Lehan     mValue = static_cast<int16_t>(std::round(dM));
230*17e21c20SJosh Lehan 
231*17e21c20SJosh Lehan     normalizeIntExp(mValue, rExp, dM);
232*17e21c20SJosh Lehan 
233*17e21c20SJosh Lehan     // The multiplier can not be zero, for obvious reasons
234*17e21c20SJosh Lehan     if (mValue == 0)
23572867debSJason M. Bills     {
236*17e21c20SJosh Lehan         std::cerr << "getSensorAttributes: Multiplier range below scale\n";
23772867debSJason M. Bills         return false;
23872867debSJason M. Bills     }
239*17e21c20SJosh Lehan 
240*17e21c20SJosh Lehan     // Step 3: set y to min, set x to min, keep M and rExp, solve for B
241*17e21c20SJosh Lehan     // If negative, x will be -128 (the most negative possible byte), not 0
242*17e21c20SJosh Lehan 
243*17e21c20SJosh Lehan     // Solve the IPMI equation for B, instead of y
244*17e21c20SJosh Lehan     // 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
245*17e21c20SJosh Lehan     // B = 10^(-rExp - bExp) (y - M 10^rExp x)
246*17e21c20SJosh Lehan     // TODO(): Compare with this alternative solution from SageMathCell
247*17e21c20SJosh Lehan     // https://sagecell.sagemath.org/?z=eJyrtC1LLNJQr1TX5KqAMCuATF8I0xfIdIIwnYDMIteKAggPxAIKJMEFkiACxfk5Zaka0ZUKtrYKGhq-CloKFZoK2goaTkCWhqGBgpaWAkilpqYmQgBklmasjoKTJgDAECTH&lang=sage&interacts=eJyLjgUAARUAuQ==
248*17e21c20SJosh Lehan     double dB = std::pow(10.0, ((-rExp) - bExp)) *
249*17e21c20SJosh Lehan                 (min - ((dM * std::pow(10.0, rExp) * lowestX)));
250*17e21c20SJosh Lehan 
251*17e21c20SJosh Lehan     // Step 4: Constrain B, and set bExp accordingly
252*17e21c20SJosh Lehan     if (!(scaleFloatExp(dB, bExp)))
25372867debSJason M. Bills     {
254*17e21c20SJosh Lehan         std::cerr << "getSensorAttributes: Offset (B=" << dB
255*17e21c20SJosh Lehan                   << ", bExp=" << (int)bExp
256*17e21c20SJosh Lehan                   << ") exceeds multiplier scale (M=" << dM
257*17e21c20SJosh Lehan                   << ", rExp=" << (int)rExp << ")\n";
25872867debSJason M. Bills         return false;
25972867debSJason M. Bills     }
26072867debSJason M. Bills 
261*17e21c20SJosh Lehan     bValue = static_cast<int16_t>(std::round(dB));
26272867debSJason M. Bills 
263*17e21c20SJosh Lehan     normalizeIntExp(bValue, bExp, dB);
26472867debSJason M. Bills 
265*17e21c20SJosh Lehan     // Unlike the multiplier, it is perfectly OK for bValue to be zero
26672867debSJason M. Bills     return true;
26772867debSJason M. Bills }
26872867debSJason M. Bills 
26972867debSJason M. Bills static inline uint8_t
270*17e21c20SJosh Lehan     scaleIPMIValueFromDouble(const double value, const int16_t mValue,
271*17e21c20SJosh Lehan                              const int8_t rExp, const int16_t bValue,
27272867debSJason M. Bills                              const int8_t bExp, const bool bSigned)
27372867debSJason M. Bills {
274*17e21c20SJosh Lehan     // Avoid division by zero below
275*17e21c20SJosh Lehan     if (mValue == 0)
276*17e21c20SJosh Lehan     {
277*17e21c20SJosh Lehan         throw std::out_of_range("Scaling multiplier is uninitialized");
278*17e21c20SJosh Lehan     }
27939417c7bSJames Feist 
280*17e21c20SJosh Lehan     auto dM = static_cast<double>(mValue);
281*17e21c20SJosh Lehan     auto dB = static_cast<double>(bValue);
282*17e21c20SJosh Lehan 
283*17e21c20SJosh Lehan     // Solve the IPMI equation for x, instead of y
284*17e21c20SJosh Lehan     // 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
285*17e21c20SJosh Lehan     // x = (10^(-rExp) (y - B 10^(rExp + bExp)))/M and M 10^rExp!=0
286*17e21c20SJosh Lehan     // TODO(): Compare with this alternative solution from SageMathCell
287*17e21c20SJosh Lehan     // https://sagecell.sagemath.org/?z=eJyrtC1LLNJQr1TX5KqAMCuATF8I0xfIdIIwnYDMIteKAggPxAIKJMEFkiACxfk5Zaka0ZUKtrYKGhq-CloKFZoK2goaTkCWhqGBgpaWAkilpqYmQgBklmasDlAlAMB8JP0=&lang=sage&interacts=eJyLjgUAARUAuQ==
288*17e21c20SJosh Lehan     double dX =
289*17e21c20SJosh Lehan         (std::pow(10.0, -rExp) * (value - (dB * std::pow(10.0, rExp + bExp)))) /
290*17e21c20SJosh Lehan         dM;
291*17e21c20SJosh Lehan 
292*17e21c20SJosh Lehan     auto scaledValue = static_cast<int32_t>(std::round(dX));
293*17e21c20SJosh Lehan 
294*17e21c20SJosh Lehan     int32_t minClamp;
295*17e21c20SJosh Lehan     int32_t maxClamp;
296*17e21c20SJosh Lehan 
297*17e21c20SJosh Lehan     // Because of rounding and integer truncation of scaling factors,
298*17e21c20SJosh Lehan     // sometimes the resulting byte is slightly out of range.
299*17e21c20SJosh Lehan     // Still allow this, but clamp the values to range.
300f6a07837SJames Feist     if (bSigned)
301f6a07837SJames Feist     {
302*17e21c20SJosh Lehan         minClamp = std::numeric_limits<int8_t>::lowest();
303*17e21c20SJosh Lehan         maxClamp = std::numeric_limits<int8_t>::max();
304f6a07837SJames Feist     }
305f6a07837SJames Feist     else
306f6a07837SJames Feist     {
307*17e21c20SJosh Lehan         minClamp = std::numeric_limits<uint8_t>::lowest();
308*17e21c20SJosh Lehan         maxClamp = std::numeric_limits<uint8_t>::max();
30939417c7bSJames Feist     }
310*17e21c20SJosh Lehan 
311*17e21c20SJosh Lehan     auto clampedValue = std::clamp(scaledValue, minClamp, maxClamp);
312*17e21c20SJosh Lehan 
313*17e21c20SJosh Lehan     // This works for both signed and unsigned,
314*17e21c20SJosh Lehan     // because it is the same underlying byte storage.
315*17e21c20SJosh Lehan     return static_cast<uint8_t>(clampedValue);
31672867debSJason M. Bills }
31772867debSJason M. Bills 
31872867debSJason M. Bills static inline uint8_t getScaledIPMIValue(const double value, const double max,
31972867debSJason M. Bills                                          const double min)
32072867debSJason M. Bills {
32172867debSJason M. Bills     int16_t mValue = 0;
32272867debSJason M. Bills     int8_t rExp = 0;
32372867debSJason M. Bills     int16_t bValue = 0;
32472867debSJason M. Bills     int8_t bExp = 0;
325*17e21c20SJosh Lehan     bool bSigned = false;
32672867debSJason M. Bills 
327*17e21c20SJosh Lehan     bool result =
328*17e21c20SJosh Lehan         getSensorAttributes(max, min, mValue, rExp, bValue, bExp, bSigned);
32972867debSJason M. Bills     if (!result)
33072867debSJason M. Bills     {
33139417c7bSJames Feist         throw std::runtime_error("Illegal sensor attributes");
33272867debSJason M. Bills     }
333*17e21c20SJosh Lehan 
33472867debSJason M. Bills     return scaleIPMIValueFromDouble(value, mValue, rExp, bValue, bExp, bSigned);
33572867debSJason M. Bills }
33672867debSJason M. Bills 
3373f7c5e40SJason M. Bills } // namespace ipmi
338