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
1817e21c20SJosh Lehan #include <algorithm>
1972867debSJason M. Bills #include <cmath>
20ae339045SVernon Mauery #include <cstdint>
2172867debSJason M. Bills #include <iostream>
223f7c5e40SJason M. Bills 
233f7c5e40SJason M. Bills namespace ipmi
243f7c5e40SJason M. Bills {
2572867debSJason M. Bills static constexpr int16_t maxInt10 = 0x1FF;
2639417c7bSJames Feist static constexpr int16_t minInt10 = -0x200;
2772867debSJason M. Bills static constexpr int8_t maxInt4 = 7;
2872867debSJason M. Bills static constexpr int8_t minInt4 = -8;
2972867debSJason M. Bills 
3017e21c20SJosh Lehan // Helper function to avoid repeated complicated expression
3117e21c20SJosh Lehan // TODO(): Refactor to add a proper sensorutils.cpp file,
3217e21c20SJosh Lehan // instead of putting everything in this header as it is now,
3317e21c20SJosh Lehan // so that helper functions can be correctly hidden from callers.
baseInRange(double base)3417e21c20SJosh Lehan static inline bool baseInRange(double base)
3517e21c20SJosh Lehan {
3617e21c20SJosh Lehan     auto min10 = static_cast<double>(minInt10);
3717e21c20SJosh Lehan     auto max10 = static_cast<double>(maxInt10);
3817e21c20SJosh Lehan 
3917e21c20SJosh Lehan     return ((base >= min10) && (base <= max10));
4017e21c20SJosh Lehan }
4117e21c20SJosh Lehan 
4217e21c20SJosh Lehan // Helper function for internal use by getSensorAttributes()
4317e21c20SJosh Lehan // Ensures floating-point "base" is within bounds,
4417e21c20SJosh Lehan // and adjusts integer exponent "expShift" accordingly.
4517e21c20SJosh Lehan // To minimize data loss when later truncating to integer,
4617e21c20SJosh Lehan // the floating-point "base" will be as large as possible,
4717e21c20SJosh Lehan // but still within the bounds (minInt10,maxInt10).
4817e21c20SJosh Lehan // The bounds of "expShift" are (minInt4,maxInt4).
4917e21c20SJosh Lehan // Consider this equation: n = base * (10.0 ** expShift)
5017e21c20SJosh Lehan // This function will try to maximize "base",
5117e21c20SJosh Lehan // adjusting "expShift" to keep the value "n" unchanged,
5217e21c20SJosh Lehan // while keeping base and expShift within bounds.
5317e21c20SJosh Lehan // Returns true if successful, modifies values in-place
scaleFloatExp(double & base,int8_t & expShift)5417e21c20SJosh Lehan static inline bool scaleFloatExp(double& base, int8_t& expShift)
5517e21c20SJosh Lehan {
5617e21c20SJosh Lehan     // Comparing with zero should be OK, zero is special in floating-point
5717e21c20SJosh Lehan     // If base is exactly zero, no adjustment of the exponent is necessary
5817e21c20SJosh Lehan     if (base == 0.0)
5917e21c20SJosh Lehan     {
6017e21c20SJosh Lehan         return true;
6117e21c20SJosh Lehan     }
6217e21c20SJosh Lehan 
6317e21c20SJosh Lehan     // As long as base value is within allowed range, expand precision
6417e21c20SJosh Lehan     // This will help to avoid loss when later rounding to integer
6517e21c20SJosh Lehan     while (baseInRange(base))
6617e21c20SJosh Lehan     {
6717e21c20SJosh Lehan         if (expShift <= minInt4)
6817e21c20SJosh Lehan         {
6917e21c20SJosh Lehan             // Already at the minimum expShift, can not decrement it more
7017e21c20SJosh Lehan             break;
7117e21c20SJosh Lehan         }
7217e21c20SJosh Lehan 
7317e21c20SJosh Lehan         // Multiply by 10, but shift decimal point to the left, no net change
7417e21c20SJosh Lehan         base *= 10.0;
7517e21c20SJosh Lehan         --expShift;
7617e21c20SJosh Lehan     }
7717e21c20SJosh Lehan 
7817e21c20SJosh Lehan     // As long as base value is *not* within range, shrink precision
7917e21c20SJosh Lehan     // This will pull base value closer to zero, thus within range
8017e21c20SJosh Lehan     while (!(baseInRange(base)))
8117e21c20SJosh Lehan     {
8217e21c20SJosh Lehan         if (expShift >= maxInt4)
8317e21c20SJosh Lehan         {
8417e21c20SJosh Lehan             // Already at the maximum expShift, can not increment it more
8517e21c20SJosh Lehan             break;
8617e21c20SJosh Lehan         }
8717e21c20SJosh Lehan 
8817e21c20SJosh Lehan         // Divide by 10, but shift decimal point to the right, no net change
8917e21c20SJosh Lehan         base /= 10.0;
9017e21c20SJosh Lehan         ++expShift;
9117e21c20SJosh Lehan     }
9217e21c20SJosh Lehan 
9317e21c20SJosh Lehan     // If the above loop was not able to pull it back within range,
9417e21c20SJosh Lehan     // the base value is beyond what expShift can represent, return false.
9517e21c20SJosh Lehan     return baseInRange(base);
9617e21c20SJosh Lehan }
9717e21c20SJosh Lehan 
9817e21c20SJosh Lehan // Helper function for internal use by getSensorAttributes()
9917e21c20SJosh Lehan // Ensures integer "ibase" is no larger than necessary,
10017e21c20SJosh Lehan // by normalizing it so that the decimal point shift is in the exponent,
10117e21c20SJosh Lehan // whenever possible.
10217e21c20SJosh Lehan // This provides more consistent results,
10317e21c20SJosh Lehan // as many equivalent solutions are collapsed into one consistent solution.
10417e21c20SJosh Lehan // If integer "ibase" is a clean multiple of 10,
10517e21c20SJosh Lehan // divide it by 10 (this is lossless), so it is closer to zero.
10617e21c20SJosh Lehan // Also modify floating-point "dbase" at the same time,
10717e21c20SJosh Lehan // as both integer and floating-point base share the same expShift.
10817e21c20SJosh Lehan // Example: (ibase=300, expShift=2) becomes (ibase=3, expShift=4)
10917e21c20SJosh Lehan // because the underlying value is the same: 200*(10**2) == 2*(10**4)
11017e21c20SJosh Lehan // Always successful, modifies values in-place
normalizeIntExp(int16_t & ibase,int8_t & expShift,double & dbase)11117e21c20SJosh Lehan static inline void normalizeIntExp(int16_t& ibase, int8_t& expShift,
11217e21c20SJosh Lehan                                    double& dbase)
11317e21c20SJosh Lehan {
11417e21c20SJosh Lehan     for (;;)
11517e21c20SJosh Lehan     {
11617e21c20SJosh Lehan         // If zero, already normalized, ensure exponent also zero
11717e21c20SJosh Lehan         if (ibase == 0)
11817e21c20SJosh Lehan         {
11917e21c20SJosh Lehan             expShift = 0;
12017e21c20SJosh Lehan             break;
12117e21c20SJosh Lehan         }
12217e21c20SJosh Lehan 
12317e21c20SJosh Lehan         // If not cleanly divisible by 10, already normalized
12417e21c20SJosh Lehan         if ((ibase % 10) != 0)
12517e21c20SJosh Lehan         {
12617e21c20SJosh Lehan             break;
12717e21c20SJosh Lehan         }
12817e21c20SJosh Lehan 
12917e21c20SJosh Lehan         // If exponent already at max, already normalized
13017e21c20SJosh Lehan         if (expShift >= maxInt4)
13117e21c20SJosh Lehan         {
13217e21c20SJosh Lehan             break;
13317e21c20SJosh Lehan         }
13417e21c20SJosh Lehan 
13517e21c20SJosh Lehan         // Bring values closer to zero, correspondingly shift exponent,
13617e21c20SJosh Lehan         // without changing the underlying number that this all represents,
13717e21c20SJosh Lehan         // similar to what is done by scaleFloatExp().
13817e21c20SJosh Lehan         // The floating-point base must be kept in sync with the integer base,
13917e21c20SJosh Lehan         // as both floating-point and integer share the same exponent.
14017e21c20SJosh Lehan         ibase /= 10;
14117e21c20SJosh Lehan         dbase /= 10.0;
14217e21c20SJosh Lehan         ++expShift;
14317e21c20SJosh Lehan     }
14417e21c20SJosh Lehan }
14517e21c20SJosh Lehan 
14617e21c20SJosh Lehan // The IPMI equation:
14717e21c20SJosh Lehan // y = (Mx + (B * 10^(bExp))) * 10^(rExp)
14817e21c20SJosh Lehan // Section 36.3 of this document:
14917e21c20SJosh Lehan // https://www.intel.com/content/dam/www/public/us/en/documents/product-briefs/ipmi-second-gen-interface-spec-v2-rev1-1.pdf
15017e21c20SJosh Lehan //
15117e21c20SJosh Lehan // The goal is to exactly match the math done by the ipmitool command,
15217e21c20SJosh Lehan // at the other side of the interface:
15317e21c20SJosh Lehan // https://github.com/ipmitool/ipmitool/blob/42a023ff0726c80e8cc7d30315b987fe568a981d/lib/ipmi_sdr.c#L360
15417e21c20SJosh Lehan //
15517e21c20SJosh Lehan // To use with Wolfram Alpha, make all variables single letters
15617e21c20SJosh Lehan // bExp becomes E, rExp becomes R
15717e21c20SJosh Lehan // https://www.wolframalpha.com/input/?i=y%3D%28%28M*x%29%2B%28B*%2810%5EE%29%29%29*%2810%5ER%29
getSensorAttributes(const double max,const double min,int16_t & mValue,int8_t & rExp,int16_t & bValue,int8_t & bExp,bool & bSigned)158*1bcced08SPatrick Williams static inline bool getSensorAttributes(
159*1bcced08SPatrick Williams     const double max, const double min, int16_t& mValue, int8_t& rExp,
160*1bcced08SPatrick Williams     int16_t& bValue, int8_t& bExp, bool& bSigned)
16172867debSJason M. Bills {
16217e21c20SJosh Lehan     if (!(std::isfinite(min)))
16317e21c20SJosh Lehan     {
16417e21c20SJosh Lehan         std::cerr << "getSensorAttributes: Min value is unusable\n";
16517e21c20SJosh Lehan         return false;
16617e21c20SJosh Lehan     }
16717e21c20SJosh Lehan     if (!(std::isfinite(max)))
16817e21c20SJosh Lehan     {
16917e21c20SJosh Lehan         std::cerr << "getSensorAttributes: Max value is unusable\n";
17017e21c20SJosh Lehan         return false;
17117e21c20SJosh Lehan     }
17217e21c20SJosh Lehan 
17317e21c20SJosh Lehan     // Because NAN has already been tested for, this comparison works
17439417c7bSJames Feist     if (max <= min)
17572867debSJason M. Bills     {
17653870d73SVernon Mauery         std::cerr << "getSensorAttributes: Max must be greater than min\n";
17772867debSJason M. Bills         return false;
17872867debSJason M. Bills     }
17939417c7bSJames Feist 
18017e21c20SJosh Lehan     // Given min and max, we must solve for M, B, bExp, rExp
18117e21c20SJosh Lehan     // y comes in from D-Bus (the actual sensor reading)
18217e21c20SJosh Lehan     // x is calculated from y by scaleIPMIValueFromDouble() below
18317e21c20SJosh Lehan     // If y is min, x should equal = 0 (or -128 if signed)
18417e21c20SJosh Lehan     // If y is max, x should equal 255 (or 127 if signed)
18517e21c20SJosh Lehan     double fullRange = max - min;
18617e21c20SJosh Lehan     double lowestX;
18772867debSJason M. Bills 
18817e21c20SJosh Lehan     rExp = 0;
18917e21c20SJosh Lehan     bExp = 0;
19017e21c20SJosh Lehan 
19117e21c20SJosh Lehan     // TODO(): The IPMI document is ambiguous, as to whether
19217e21c20SJosh Lehan     // the resulting byte should be signed or unsigned,
19317e21c20SJosh Lehan     // essentially leaving it up to the caller.
19417e21c20SJosh Lehan     // The document just refers to it as "raw reading",
19517e21c20SJosh Lehan     // or "byte of reading", without giving further details.
19617e21c20SJosh Lehan     // Previous code set it signed if min was less than zero,
19717e21c20SJosh Lehan     // so I'm sticking with that, until I learn otherwise.
19817e21c20SJosh Lehan     if (min < 0.0)
19972867debSJason M. Bills     {
20017e21c20SJosh Lehan         // TODO(): It would be worth experimenting with the range (-127,127),
20117e21c20SJosh Lehan         // instead of the range (-128,127), because this
20217e21c20SJosh Lehan         // would give good symmetry around zero, and make results look better.
20317e21c20SJosh Lehan         // Divide by 254 instead of 255, and change -128 to -127 elsewhere.
20472867debSJason M. Bills         bSigned = true;
20517e21c20SJosh Lehan         lowestX = -128.0;
20672867debSJason M. Bills     }
20772867debSJason M. Bills     else
20872867debSJason M. Bills     {
20972867debSJason M. Bills         bSigned = false;
21017e21c20SJosh Lehan         lowestX = 0.0;
21172867debSJason M. Bills     }
21272867debSJason M. Bills 
21317e21c20SJosh Lehan     // Step 1: Set y to (max - min), set x to 255, set B to 0, solve for M
21417e21c20SJosh Lehan     // This works, regardless of signed or unsigned,
21517e21c20SJosh Lehan     // because total range is the same.
21617e21c20SJosh Lehan     double dM = fullRange / 255.0;
21772867debSJason M. Bills 
21817e21c20SJosh Lehan     // Step 2: Constrain M, and set rExp accordingly
21917e21c20SJosh Lehan     if (!(scaleFloatExp(dM, rExp)))
22072867debSJason M. Bills     {
22117e21c20SJosh Lehan         std::cerr << "getSensorAttributes: Multiplier range exceeds scale (M="
22217e21c20SJosh Lehan                   << dM << ", rExp=" << (int)rExp << ")\n";
22372867debSJason M. Bills         return false;
22472867debSJason M. Bills     }
22572867debSJason M. Bills 
22617e21c20SJosh Lehan     mValue = static_cast<int16_t>(std::round(dM));
22717e21c20SJosh Lehan 
22817e21c20SJosh Lehan     normalizeIntExp(mValue, rExp, dM);
22917e21c20SJosh Lehan 
23017e21c20SJosh Lehan     // The multiplier can not be zero, for obvious reasons
23117e21c20SJosh Lehan     if (mValue == 0)
23272867debSJason M. Bills     {
23317e21c20SJosh Lehan         std::cerr << "getSensorAttributes: Multiplier range below scale\n";
23472867debSJason M. Bills         return false;
23572867debSJason M. Bills     }
23617e21c20SJosh Lehan 
23717e21c20SJosh Lehan     // Step 3: set y to min, set x to min, keep M and rExp, solve for B
23817e21c20SJosh Lehan     // If negative, x will be -128 (the most negative possible byte), not 0
23917e21c20SJosh Lehan 
24017e21c20SJosh Lehan     // Solve the IPMI equation for B, instead of y
24117e21c20SJosh 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
24217e21c20SJosh Lehan     // B = 10^(-rExp - bExp) (y - M 10^rExp x)
24317e21c20SJosh Lehan     // TODO(): Compare with this alternative solution from SageMathCell
24417e21c20SJosh Lehan     // https://sagecell.sagemath.org/?z=eJyrtC1LLNJQr1TX5KqAMCuATF8I0xfIdIIwnYDMIteKAggPxAIKJMEFkiACxfk5Zaka0ZUKtrYKGhq-CloKFZoK2goaTkCWhqGBgpaWAkilpqYmQgBklmasjoKTJgDAECTH&lang=sage&interacts=eJyLjgUAARUAuQ==
24517e21c20SJosh Lehan     double dB = std::pow(10.0, ((-rExp) - bExp)) *
24617e21c20SJosh Lehan                 (min - ((dM * std::pow(10.0, rExp) * lowestX)));
24717e21c20SJosh Lehan 
24817e21c20SJosh Lehan     // Step 4: Constrain B, and set bExp accordingly
24917e21c20SJosh Lehan     if (!(scaleFloatExp(dB, bExp)))
25072867debSJason M. Bills     {
251*1bcced08SPatrick Williams         std::cerr << "getSensorAttributes: Offset (B=" << dB << ", bExp="
252*1bcced08SPatrick Williams                   << (int)bExp << ") exceeds multiplier scale (M=" << dM
25317e21c20SJosh Lehan                   << ", rExp=" << (int)rExp << ")\n";
25472867debSJason M. Bills         return false;
25572867debSJason M. Bills     }
25672867debSJason M. Bills 
25717e21c20SJosh Lehan     bValue = static_cast<int16_t>(std::round(dB));
25872867debSJason M. Bills 
25917e21c20SJosh Lehan     normalizeIntExp(bValue, bExp, dB);
26072867debSJason M. Bills 
26117e21c20SJosh Lehan     // Unlike the multiplier, it is perfectly OK for bValue to be zero
26272867debSJason M. Bills     return true;
26372867debSJason M. Bills }
26472867debSJason M. Bills 
scaleIPMIValueFromDouble(const double value,const int16_t mValue,const int8_t rExp,const int16_t bValue,const int8_t bExp,const bool bSigned)265*1bcced08SPatrick Williams static inline uint8_t scaleIPMIValueFromDouble(
266*1bcced08SPatrick Williams     const double value, const int16_t mValue, const int8_t rExp,
267*1bcced08SPatrick Williams     const int16_t bValue, const int8_t bExp, const bool bSigned)
26872867debSJason M. Bills {
26917e21c20SJosh Lehan     // Avoid division by zero below
27017e21c20SJosh Lehan     if (mValue == 0)
27117e21c20SJosh Lehan     {
27217e21c20SJosh Lehan         throw std::out_of_range("Scaling multiplier is uninitialized");
27317e21c20SJosh Lehan     }
27439417c7bSJames Feist 
27517e21c20SJosh Lehan     auto dM = static_cast<double>(mValue);
27617e21c20SJosh Lehan     auto dB = static_cast<double>(bValue);
27717e21c20SJosh Lehan 
27817e21c20SJosh Lehan     // Solve the IPMI equation for x, instead of y
27917e21c20SJosh 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
28017e21c20SJosh Lehan     // x = (10^(-rExp) (y - B 10^(rExp + bExp)))/M and M 10^rExp!=0
28117e21c20SJosh Lehan     // TODO(): Compare with this alternative solution from SageMathCell
28217e21c20SJosh Lehan     // https://sagecell.sagemath.org/?z=eJyrtC1LLNJQr1TX5KqAMCuATF8I0xfIdIIwnYDMIteKAggPxAIKJMEFkiACxfk5Zaka0ZUKtrYKGhq-CloKFZoK2goaTkCWhqGBgpaWAkilpqYmQgBklmasDlAlAMB8JP0=&lang=sage&interacts=eJyLjgUAARUAuQ==
28317e21c20SJosh Lehan     double dX =
28417e21c20SJosh Lehan         (std::pow(10.0, -rExp) * (value - (dB * std::pow(10.0, rExp + bExp)))) /
28517e21c20SJosh Lehan         dM;
28617e21c20SJosh Lehan 
28717e21c20SJosh Lehan     auto scaledValue = static_cast<int32_t>(std::round(dX));
28817e21c20SJosh Lehan 
28917e21c20SJosh Lehan     int32_t minClamp;
29017e21c20SJosh Lehan     int32_t maxClamp;
29117e21c20SJosh Lehan 
29217e21c20SJosh Lehan     // Because of rounding and integer truncation of scaling factors,
29317e21c20SJosh Lehan     // sometimes the resulting byte is slightly out of range.
29417e21c20SJosh Lehan     // Still allow this, but clamp the values to range.
295f6a07837SJames Feist     if (bSigned)
296f6a07837SJames Feist     {
29717e21c20SJosh Lehan         minClamp = std::numeric_limits<int8_t>::lowest();
29817e21c20SJosh Lehan         maxClamp = std::numeric_limits<int8_t>::max();
299f6a07837SJames Feist     }
300f6a07837SJames Feist     else
301f6a07837SJames Feist     {
30217e21c20SJosh Lehan         minClamp = std::numeric_limits<uint8_t>::lowest();
30317e21c20SJosh Lehan         maxClamp = std::numeric_limits<uint8_t>::max();
30439417c7bSJames Feist     }
30517e21c20SJosh Lehan 
30617e21c20SJosh Lehan     auto clampedValue = std::clamp(scaledValue, minClamp, maxClamp);
30717e21c20SJosh Lehan 
30817e21c20SJosh Lehan     // This works for both signed and unsigned,
30917e21c20SJosh Lehan     // because it is the same underlying byte storage.
31017e21c20SJosh Lehan     return static_cast<uint8_t>(clampedValue);
31172867debSJason M. Bills }
31272867debSJason M. Bills 
getScaledIPMIValue(const double value,const double max,const double min)31372867debSJason M. Bills static inline uint8_t getScaledIPMIValue(const double value, const double max,
31472867debSJason M. Bills                                          const double min)
31572867debSJason M. Bills {
31672867debSJason M. Bills     int16_t mValue = 0;
31772867debSJason M. Bills     int8_t rExp = 0;
31872867debSJason M. Bills     int16_t bValue = 0;
31972867debSJason M. Bills     int8_t bExp = 0;
32017e21c20SJosh Lehan     bool bSigned = false;
32172867debSJason M. Bills 
322*1bcced08SPatrick Williams     bool result =
323*1bcced08SPatrick Williams         getSensorAttributes(max, min, mValue, rExp, bValue, bExp, bSigned);
32472867debSJason M. Bills     if (!result)
32572867debSJason M. Bills     {
32639417c7bSJames Feist         throw std::runtime_error("Illegal sensor attributes");
32772867debSJason M. Bills     }
32817e21c20SJosh Lehan 
32972867debSJason M. Bills     return scaleIPMIValueFromDouble(value, mValue, rExp, bValue, bExp, bSigned);
33072867debSJason M. Bills }
33172867debSJason M. Bills 
3323f7c5e40SJason M. Bills } // namespace ipmi
333