From e795ce604aca700b2350e734a59ebc3ca0cfea4d Mon Sep 17 00:00:00 2001
From: John <ebusd@ebusd.eu>
Date: Mon, 6 Jan 2025 22:08:49 +0100
Subject: [PATCH] add value range and step support, fix value list validation

---
 src/lib/ebus/contrib/tem.cpp    |   5 +-
 src/lib/ebus/data.cpp           |  70 ++++++-
 src/lib/ebus/datatype.cpp       | 353 +++++++++++++++++++++-----------
 src/lib/ebus/datatype.h         |  59 +++++-
 src/lib/ebus/test/test_data.cpp |  68 +++++-
 5 files changed, 417 insertions(+), 138 deletions(-)

diff --git a/src/lib/ebus/contrib/tem.cpp b/src/lib/ebus/contrib/tem.cpp
index ff1e22b1..991905cd 100755
--- a/src/lib/ebus/contrib/tem.cpp
+++ b/src/lib/ebus/contrib/tem.cpp
@@ -135,8 +135,9 @@ result_t TemParamDataType::writeSymbols(const size_t offset, const size_t length
       value = (grp << 7) | num;  // grp in bits 7...11, num in bits 0...6
     }
   }
-  if (value < getMinValue() || value > getMaxValue()) {
-    return RESULT_ERR_OUT_OF_RANGE;  // value out of range
+  result_t ret = checkValueRange(value);
+  if (ret != RESULT_OK) {
+    return ret;
   }
   return writeRawValue(value, offset, length, output, usedLength);
 }
diff --git a/src/lib/ebus/data.cpp b/src/lib/ebus/data.cpp
index 36856978..2f745ad7 100644
--- a/src/lib/ebus/data.cpp
+++ b/src/lib/ebus/data.cpp
@@ -273,6 +273,7 @@ result_t DataField::create(bool isWriteMessage, bool isTemplate, bool isBroadcas
 
     string divisorStr = pluck("divisor", &row);
     string valuesStr = pluck("values", &row);
+    string rangeStr = pluck("range", &row);
     if (divisorStr.empty() && valuesStr.empty()) {
       divisorStr = pluck("divisor/values", &row);  // [divisor|values]
       if (divisorStr.find('=') != string::npos) {
@@ -365,6 +366,57 @@ result_t DataField::create(bool isWriteMessage, bool isTemplate, bool isBroadcas
         }
         transform(typeName.begin(), typeName.end(), typeName.begin(), ::toupper);
         const DataType* dataType = DataTypeList::getInstance()->get(typeName, length == REMAIN_LEN ? 0 : length);
+        if (dataType && dataType->isNumeric() && !rangeStr.empty()) {
+          const NumberDataType* numType = reinterpret_cast<const NumberDataType*>(dataType);
+          if (divisor != 1 && divisor != 0) {
+            result = numType->derive(divisor, numType->getBitCount(), &numType);
+            divisor = 1;
+          }
+          // either from-to or from-to:step
+          size_t sepPos = rangeStr.find('-', 1);
+          string part = rangeStr.substr(0, sepPos);
+          FileReader::trim(&part);
+          unsigned int from;
+          if (result == RESULT_OK) {
+            result = numType->parseInput(part, &from);
+          }
+          unsigned int to = from;
+          unsigned int inc = 0;
+          if (result == RESULT_OK) {
+            part = rangeStr.substr(sepPos+1);
+            FileReader::trim(&part);
+            sepPos = part.find(':');
+            if (sepPos != string::npos) {
+              string incStr = part.substr(sepPos + 1);
+              FileReader::trim(&incStr);
+              part = part.substr(0, sepPos);
+              FileReader::trim(&part);
+              result = numType->parseInput(incStr, &inc);
+            }
+            if (result == RESULT_OK) {
+              result = numType->parseInput(part, &to);
+            }
+          }
+          float ffrom = 0, fto = 0;
+          if (result == RESULT_OK) {
+            result = numType->getFloatFromRawValue(from, &ffrom);
+          }
+          if (result == RESULT_OK) {
+            result = numType->getFloatFromRawValue(to, &fto);
+          }
+          if (result == RESULT_OK && ffrom > fto) {
+            result = RESULT_ERR_INVALID_LIST;
+          }
+          if (result == RESULT_OK) {
+            result = numType->derive(from, to, inc, &numType);
+          };
+          if (result != RESULT_OK) {
+            *errorDescription = "\""+rangeStr+"\" in field "+formatInt(fieldIndex);
+            result = RESULT_ERR_OUT_OF_RANGE;
+            break;
+          }
+          dataType = numType;
+        }
         if (!dataType) {
           result = RESULT_ERR_NOTFOUND;
           *errorDescription = "field type "+typeName+" in field "+formatInt(fieldIndex);
@@ -512,8 +564,11 @@ result_t SingleDataField::create(const string& name, const map<string, string>&
       *returnField = new SingleDataField(name, attributes, numType, partType, byteCount);
       return RESULT_OK;
     }
-    if (values->begin()->first < numType->getMinValue() || values->rbegin()->first > numType->getMaxValue()) {
-      return RESULT_ERR_OUT_OF_RANGE;
+    for (auto& it : *values) {
+      result_t ret = numType->checkValueRange(it.first);
+      if (ret != RESULT_OK) {
+        return ret;
+      }
     }
     *returnField = new ValueListDataField(name, attributes, numType, partType, byteCount, *values);
     return RESULT_OK;
@@ -775,8 +830,11 @@ result_t ValueListDataField::derive(const string& name, PartType partType, int d
   }
   const NumberDataType* num = reinterpret_cast<const NumberDataType*>(m_dataType);
   if (!values.empty()) {
-    if (values.begin()->first < num->getMinValue() || values.rbegin()->first > num->getMaxValue()) {
-      return RESULT_ERR_INVALID_ARG;  // cannot use divisor != 1 for value list field
+    for (auto& it : values) {
+      result_t ret = num->checkValueRange(it.first);
+      if (ret != RESULT_OK) {
+        return RESULT_ERR_INVALID_ARG;
+      }
     }
     fields->push_back(new ValueListDataField(useName, *attributes,
         num, partType, m_length, values));
@@ -922,8 +980,8 @@ result_t ConstantDataField::readSymbols(const SymbolString& input, size_t offset
   if (result != RESULT_OK) {
     return result;
   }
-    string value = coutput.str();
-    FileReader::trim(&value);
+  string value = coutput.str();
+  FileReader::trim(&value);
   if (m_verify) {
     if (value != m_value) {
       return RESULT_ERR_OUT_OF_RANGE;
diff --git a/src/lib/ebus/datatype.cpp b/src/lib/ebus/datatype.cpp
index 6a5c62ff..a617bb39 100755
--- a/src/lib/ebus/datatype.cpp
+++ b/src/lib/ebus/datatype.cpp
@@ -700,8 +700,16 @@ bool NumberDataType::dump(OutputFormat outputFormat, size_t length, AppendDiviso
       ret = true;
     }
   }
-  if (ret && (outputFormat & OF_JSON) && (outputFormat & OF_ALL_ATTRS)) {
-    *output << ", \"precision\": " << static_cast<unsigned>(getPrecision());
+  if ((outputFormat & OF_JSON) && (outputFormat & OF_ALL_ATTRS)) {
+    if (ret) {
+      *output << ", \"precision\": " << static_cast<unsigned>(getPrecision());
+    }
+    *output << ", \"min\": ";
+    getMinMax(false, OF_JSON, output);
+    *output << ", \"max\": ";
+    getMinMax(true, OF_JSON, output);
+    *output << ", \"step\": ";
+    getStep(OF_JSON, output);
   }
   return ret;
 }
@@ -747,23 +755,119 @@ result_t NumberDataType::derive(int divisor, size_t bitCount, const NumberDataTy
   } else {
     return RESULT_ERR_INVALID_ARG;
   }
+  ostringstream str;
+  str << m_id << ',' << static_cast<unsigned>(bitCount) << ',' << static_cast<signed>(divisor);
+  string key = str.str();
+  *derived = static_cast<const NumberDataType*>(DataTypeList::getInstance()->get(key));
+  if (*derived == nullptr) {
+    if (m_bitCount < 8) {
+      *derived = new NumberDataType(m_id, bitCount, m_flags, m_replacement,
+                                    m_firstBit, divisor, m_baseType ? m_baseType : this);
+    } else {
+      *derived = new NumberDataType(m_id, bitCount, m_flags, m_replacement,
+                                    m_minValue, m_maxValue, divisor, m_baseType ? m_baseType : this);
+    }
+    DataTypeList::getInstance()->add(*derived, key);
+  }
+  return RESULT_OK;
+}
+
+result_t NumberDataType::derive(unsigned int min, unsigned int max, unsigned int inc, const NumberDataType** derived)
+const {
   if (m_bitCount < 8) {
-    *derived = new NumberDataType(m_id, bitCount, m_flags, m_replacement,
-                                  m_firstBit, divisor, m_baseType ? m_baseType : this);
-  } else {
-    *derived = new NumberDataType(m_id, bitCount, m_flags, m_replacement,
-                                  m_minValue, m_maxValue, divisor, m_baseType ? m_baseType : this);
+    return RESULT_ERR_INVALID_ARG;
+  }
+  if (min == m_minValue && max == m_maxValue && (inc == 0 || inc == m_incValue)) {
+    *derived = this;
+    return RESULT_OK;
+  }
+  if (checkValueRange(min) != RESULT_OK || checkValueRange(max) != RESULT_OK) {
+    return RESULT_ERR_OUT_OF_RANGE;
+  }
+  ostringstream str;
+  str << m_id << ',' << static_cast<unsigned>(m_bitCount) << ',' << static_cast<signed>(m_divisor)
+  << ',' << static_cast<unsigned>(min)<< ',' << static_cast<unsigned>(max)<< ',' << static_cast<unsigned>(inc);
+  string key = str.str();
+  *derived = static_cast<const NumberDataType*>(DataTypeList::getInstance()->get(key));
+  if (*derived == nullptr) {
+    *derived = new NumberDataType(m_id, m_bitCount, m_flags, m_replacement,
+                                  min, max, inc, m_divisor, m_baseType ? m_baseType : this);
+    DataTypeList::getInstance()->add(*derived, key);
   }
-  DataTypeList::getInstance()->addCleanup(*derived);
   return RESULT_OK;
 }
 
 result_t NumberDataType::getMinMax(bool getMax, const OutputFormat outputFormat, ostream* output) const {
-  return readFromRawValue(getMax ? m_maxValue : m_minValue, outputFormat, output);
+  return readFromRawValue(getMax ? m_maxValue : m_minValue, outputFormat, output, true);
 }
 
 result_t NumberDataType::getStep(const OutputFormat outputFormat, ostream* output) const {
-  return readFromRawValue(hasFlag(EXP) ? floatToUint(1.0f) : 1, outputFormat, output);
+  return readFromRawValue(m_incValue ? m_incValue : hasFlag(EXP) ? floatToUint(1.0f) : 1, outputFormat, output, true);
+}
+
+result_t NumberDataType::checkValueRange(unsigned int value, bool* pnegative) const {
+  bool negative;
+  if (hasFlag(SIG)) {  // signed value
+    unsigned int negBit = 1 << (m_bitCount - 1);
+    negative = (value & negBit) != 0;
+    if (hasFlag(EXP)) {
+      float fval = uintToFloat(value, negative);
+      if (!isfinite(fval)) {
+        return RESULT_EMPTY;
+      }
+      float cval = uintToFloat(m_minValue, (m_minValue & negBit) != 0);
+      if (!isfinite(cval)) {
+        return RESULT_EMPTY;
+      }
+      if (fval < cval) {
+        return RESULT_ERR_OUT_OF_RANGE;
+      }
+      cval = uintToFloat(m_maxValue, (m_maxValue & negBit) != 0);
+      if (!isfinite(cval)) {
+        return RESULT_EMPTY;
+      }
+      if (fval > cval) {
+        return RESULT_ERR_OUT_OF_RANGE;
+      }
+    } else {
+      if (m_minValue & negBit) {
+        // negative min
+        if (negative && value < m_minValue) {
+          // e.g. SCH val=0xfc=-4 min=0xff=-1
+          return RESULT_ERR_OUT_OF_RANGE;
+        }
+      } else {
+        // positive min
+        if (negative || value < m_minValue) {
+          // e.g. SCH val=0xfc=-4 min=0x01=+1
+          // e.g. SCH val=0x00=0 min=0x01=+1
+          return RESULT_ERR_OUT_OF_RANGE;
+        }
+      }
+      if (m_maxValue & negBit) {
+        // negative max
+        if (!negative || value > m_maxValue) {
+          // e.g. SCH val=0x00=0 max=0xff=-1
+          // e.g. SCH val=0xff=-1 max=0xfe=-2
+          return RESULT_ERR_OUT_OF_RANGE;
+        }
+      } else {
+        // positive max
+        if (!negative && value > m_maxValue) {
+          // e.g. SCH val=0x04=+4 max=0x01=+1
+          return RESULT_ERR_OUT_OF_RANGE;
+        }
+      }
+    }
+  } else if (value < m_minValue || value > m_maxValue) {
+    return RESULT_ERR_OUT_OF_RANGE;
+  } else {
+    negative = false;
+  }
+  if (pnegative) {
+    *pnegative = negative;
+  }
+  return RESULT_OK;
 }
 
 result_t NumberDataType::readRawValue(size_t offset, size_t length, const SymbolString& input,
@@ -830,22 +934,10 @@ result_t NumberDataType::getFloatFromRawValue(unsigned int value, float* output)
     return RESULT_EMPTY;
   }
 
-  bool negative;
-  if (hasFlag(SIG)) {  // signed value
-    negative = (value & (1 << (m_bitCount - 1))) != 0;
-    if (!hasFlag(EXP)) {
-      if (negative) {  // negative signed value
-        if (value < m_minValue) {
-          return RESULT_ERR_OUT_OF_RANGE;  // value out of range
-        }
-      } else if (value > m_maxValue) {
-        return RESULT_ERR_OUT_OF_RANGE;  // value out of range
-      }
-    }
-  } else if (value < m_minValue || value > m_maxValue) {
-    return RESULT_ERR_OUT_OF_RANGE;  // value out of range
-  } else {
-    negative = false;
+  bool negative = false;
+  result_t ret = checkValueRange(value, &negative);
+  if (ret != RESULT_OK) {
+    return ret;
   }
   int signedValue;
   if (m_bitCount == 32) {
@@ -865,7 +957,7 @@ result_t NumberDataType::getFloatFromRawValue(unsigned int value, float* output)
           val /= static_cast<float>(m_divisor);
         }
       }
-      *output = static_cast<float>(val);
+      *output = val;
       return RESULT_OK;
     }
     if (!negative) {
@@ -895,7 +987,7 @@ result_t NumberDataType::getFloatFromRawValue(unsigned int value, float* output)
 }
 
 result_t NumberDataType::readFromRawValue(unsigned int value,
-                                          OutputFormat outputFormat, ostream* output) const {
+                                          OutputFormat outputFormat, ostream* output, bool skipRangeCheck) const {
   size_t length = (m_bitCount < 8) ? 1 : (m_bitCount/8);
   // initialize output
   *output << setw(0) << std::resetiosflags(output->flags()) << dec << std::skipws << setprecision(6);
@@ -909,22 +1001,10 @@ result_t NumberDataType::readFromRawValue(unsigned int value,
     return RESULT_OK;
   }
 
-  bool negative;
-  if (hasFlag(SIG)) {  // signed value
-    negative = (value & (1 << (m_bitCount - 1))) != 0;
-    if (!hasFlag(EXP)) {
-      if (negative) {  // negative signed value
-        if (value < m_minValue) {
-          return RESULT_ERR_OUT_OF_RANGE;  // value out of range
-        }
-      } else if (value > m_maxValue) {
-        return RESULT_ERR_OUT_OF_RANGE;  // value out of range
-      }
-    }
-  } else if (value < m_minValue || value > m_maxValue) {
-    return RESULT_ERR_OUT_OF_RANGE;  // value out of range
-  } else {
-    negative = false;
+  bool negative = false;
+  result_t ret = checkValueRange(value, &negative);
+  if (!skipRangeCheck && ret != RESULT_OK) {
+    return ret;
   }
   int signedValue;
   if (m_bitCount == 32) {
@@ -1055,7 +1135,7 @@ result_t NumberDataType::getRawValueFromFloat(float val, unsigned int* output) c
   } else {
     if (m_divisor == 1) {
       if (hasFlag(SIG)) {
-        long signedValue = static_cast<long>(val);  // TODO static_c?
+        long signedValue = static_cast<long>(val);
         if (signedValue < 0 && m_bitCount != 32) {
           value = (unsigned int)(signedValue + (1 << m_bitCount));
         } else {
@@ -1091,106 +1171,108 @@ result_t NumberDataType::getRawValueFromFloat(float val, unsigned int* output) c
         value = (unsigned int)dvalue;
       }
     }
-
-    if (hasFlag(SIG)) {  // signed value
-      if ((value & (1 << (m_bitCount - 1))) != 0) {  // negative signed value
-        if (value < m_minValue) {
-          return RESULT_ERR_OUT_OF_RANGE;  // value out of range
-        }
-      } else if (value > m_maxValue) {
-        return RESULT_ERR_OUT_OF_RANGE;  // value out of range
-      }
-    } else if (value < m_minValue || value > m_maxValue) {
-      return RESULT_ERR_OUT_OF_RANGE;  // value out of range
-    }
+  }
+  result_t ret = checkValueRange(value);
+  if (ret != RESULT_OK) {
+    return ret;
   }
   *output = value;
   return RESULT_OK;
 }
 
-result_t NumberDataType::writeSymbols(size_t offset, size_t length, istringstream* input,
-                                      SymbolString* output, size_t* usedLength) const {
+result_t NumberDataType::parseInput(const string inputStr, unsigned int* parsedValue) const {
   unsigned int value;
 
-  const string inputStr = input->str();
   if (!hasFlag(REQ) && (isIgnored() || inputStr == NULL_VALUE)) {
     value = m_replacement;  // replacement value
   } else if (inputStr.empty()) {
     return RESULT_ERR_EOF;  // input too short
-  } else if (hasFlag(EXP)) {  // IEEE 754 binary32
-    const char* str = inputStr.c_str();
-    char* strEnd = nullptr;
-    double dvalue = strtod(str, &strEnd);
-    if (strEnd == nullptr || strEnd == str || *strEnd != 0) {
-      return RESULT_ERR_INVALID_NUM;  // invalid value
-    }
-    if (m_divisor < 0) {
-      dvalue /= -m_divisor;
-    } else if (m_divisor > 1) {
-      dvalue *= m_divisor;
-    }
-    value = floatToUint(static_cast<float>(dvalue));
-    if (value == 0xffffffff) {
-      return RESULT_ERR_INVALID_NUM;
-    }
   } else {
-    const char* str = inputStr.c_str();
-    char* strEnd = nullptr;
-    if (m_divisor == 1) {
-      if (hasFlag(SIG)) {
-        long signedValue = strtol(str, &strEnd, 10);
-        if (signedValue < 0 && m_bitCount != 32) {
-          value = (unsigned int)(signedValue + (1 << m_bitCount));
-        } else {
-          value = (unsigned int)signedValue;
-        }
-      } else {
-        value = (unsigned int)strtoul(str, &strEnd, 10);
-      }
-      if (strEnd == nullptr || strEnd == str || (*strEnd != 0 && *strEnd != '.')) {
-        return RESULT_ERR_INVALID_NUM;  // invalid value
-      }
-    } else {
+    if (hasFlag(EXP)) {  // IEEE 754 binary32
+      const char* str = inputStr.c_str();
+      char* strEnd = nullptr;
       double dvalue = strtod(str, &strEnd);
-      if (strEnd == nullptr || strEnd == str || *strEnd != 0) {
+      if (errno == ERANGE || strEnd == nullptr || strEnd == str || *strEnd != 0) {
         return RESULT_ERR_INVALID_NUM;  // invalid value
       }
       if (m_divisor < 0) {
-        dvalue = round(dvalue / -m_divisor);
-      } else {
-        dvalue = round(dvalue * m_divisor);
+        dvalue /= -m_divisor;
+      } else if (m_divisor > 1) {
+        dvalue *= m_divisor;
       }
-      if (hasFlag(SIG)) {
-        if (dvalue < -exp2((8 * static_cast<double>(length)) - 1)
-            || dvalue >= exp2((8 * static_cast<double>(length)) - 1)) {
-          return RESULT_ERR_OUT_OF_RANGE;  // value out of range
-        }
-        if (dvalue < 0 && m_bitCount != 32) {
-          value = static_cast<int>(dvalue + (1 << m_bitCount));
+      value = floatToUint(static_cast<float>(dvalue));
+      if (value == 0xffffffff) {
+        return RESULT_ERR_INVALID_NUM;
+      }
+    } else {
+      unsigned int maxBit = m_bitCount != 32 ? 1 << m_bitCount : 0;
+      const char* str = inputStr.c_str();
+      char* strEnd = nullptr;
+      if (m_divisor == 1) {
+        if (hasFlag(SIG)) {
+          long signedValue = strtol(str, &strEnd, 0);
+          if (errno == ERANGE || (maxBit && (signedValue < -(maxBit/2L) || signedValue >= maxBit/2L))) {
+            return RESULT_ERR_OUT_OF_RANGE;  // value out of range
+          }
+          if (signedValue < 0 && m_bitCount != 32) {
+            value = (unsigned int)(signedValue + maxBit);
+          } else {
+            value = (unsigned int)signedValue;
+          }
         } else {
-          value = static_cast<int>(dvalue);
+          value = (unsigned int)strtoul(str, &strEnd, 0);
+          if (errno == ERANGE || (maxBit && value >= maxBit)) {
+            return RESULT_ERR_OUT_OF_RANGE;
+          }
+        }
+        if (strEnd == nullptr || strEnd == str || (*strEnd != 0 && *strEnd != '.')) {
+          return RESULT_ERR_INVALID_NUM;  // invalid value
         }
       } else {
-        if (dvalue < 0.0 || dvalue >= exp2(8 * static_cast<double>(length))) {
-          return RESULT_ERR_OUT_OF_RANGE;  // value out of range
+        double dvalue = strtod(str, &strEnd);
+        if (errno == ERANGE || strEnd == nullptr || strEnd == str || *strEnd != 0) {
+          return RESULT_ERR_INVALID_NUM;  // invalid value
         }
-        value = (unsigned int)dvalue;
-      }
-    }
-
-    if (hasFlag(SIG)) {  // signed value
-      if ((value & (1 << (m_bitCount - 1))) != 0) {  // negative signed value
-        if (value < m_minValue) {
-          return RESULT_ERR_OUT_OF_RANGE;  // value out of range
+        if (m_divisor < 0) {
+          dvalue = round(dvalue / -m_divisor);
+        } else {
+          dvalue = round(dvalue * m_divisor);
+        }
+        if (hasFlag(SIG)) {
+          double max = exp2(m_bitCount - 1);
+          if (dvalue < -max || dvalue >= max) {
+            return RESULT_ERR_OUT_OF_RANGE;  // value out of range
+          }
+          if (dvalue < 0 && m_bitCount != 32) {
+            value = static_cast<int>(dvalue + (1 << m_bitCount));
+          } else {
+            value = static_cast<int>(dvalue);
+          }
+        } else {
+          if (dvalue < 0.0 || dvalue >= exp2(m_bitCount)) {
+            return RESULT_ERR_OUT_OF_RANGE;  // value out of range
+          }
+          value = (unsigned int)dvalue;
         }
-      } else if (value > m_maxValue) {
-        return RESULT_ERR_OUT_OF_RANGE;  // value out of range
       }
-    } else if (value < m_minValue || value > m_maxValue) {
-      return RESULT_ERR_OUT_OF_RANGE;  // value out of range
+    }
+    result_t ret = checkValueRange(value);
+    if (ret != RESULT_OK) {
+      return ret;
     }
   }
+  *parsedValue = value;
+  return RESULT_OK;
+}
 
+result_t NumberDataType::writeSymbols(size_t offset, size_t length, istringstream* input,
+                                      SymbolString* output, size_t* usedLength) const {
+  unsigned int value;
+  const string inputStr = input->str();
+  result_t ret = parseInput(inputStr, &value);
+  if (ret != RESULT_OK) {
+    return ret;
+  }
   return writeRawValue(value, offset, length, output, usedLength);
 }
 
@@ -1207,6 +1289,7 @@ DataTypeList::DataTypeList() {
   // unsigned decimal in BCD, 0000 - 9999 (fixed length)
   add(new NumberDataType("PIN", 16, FIX|BCD|REV, 0xffff, 0, 0x9999, 1));
   add(new NumberDataType("UCH", 8, 0, 0xff, 0, 0xfe, 1));  // unsigned integer, 0 - 254
+  add(new NumberDataType("U1L", 8, REQ, 0, 0, 0xff, 1));  // unsigned 1-byte, 0 - 255 (no replacement)
   add(new StringDataType("IGN", MAX_LEN*8, IGN|ADJ, 0));  // >= 1 byte ignored data
   // >= 1 byte character string filled up with 0x00 (null terminated string)
   add(new StringDataType("NTS", MAX_LEN*8, ADJ, 0));
@@ -1264,6 +1347,7 @@ DataTypeList::DataTypeList() {
   add(new NumberDataType("HCD:2", 16, HCD|BCD|REQ, 0, 0, 9999, 1));  // unsigned decimal in HCD, 0 - 9999
   add(new NumberDataType("HCD:3", 24, HCD|BCD|REQ, 0, 0, 999999, 1));  // unsigned decimal in HCD, 0 - 999999
   add(new NumberDataType("SCH", 8, SIG, 0x80, 0x81, 0x7f, 1));  // signed integer, -127 - +127
+  add(new NumberDataType("S1L", 8, SIG|REQ, 0, 0x80, 0x7f, 1));  // signed integer, -128 - +127 (no replacement)
   add(new NumberDataType("D1B", 8, SIG, 0x80, 0x81, 0x7f, 1));  // signed integer, -127 - +127
   // unsigned number (fraction 1/2), 0 - 100 (0x00 - 0xc8, replacement 0xff)
   add(new NumberDataType("D1C", 8, 0, 0xff, 0x00, 0xc8, 2));
@@ -1283,26 +1367,50 @@ DataTypeList::DataTypeList() {
   add(new NumberDataType("UIN", 16, 0, 0xffff, 0, 0xfffe, 1));
   // unsigned integer, 0 - 65534, big endian
   add(new NumberDataType("UIR", 16, REV, 0xffff, 0, 0xfffe, 1));
+  // unsigned integer, 0 - 65535, little endian (no replacement)
+  add(new NumberDataType("U2L", 16, REQ, 0, 0, 0xffff, 1));
+  // unsigned integer, 0 - 65535, big endian (no replacement)
+  add(new NumberDataType("U2B", 16, REQ|REV, 0, 0, 0xffff, 1));
   // signed integer, -32767 - +32767, little endian
   add(new NumberDataType("SIN", 16, SIG, 0x8000, 0x8001, 0x7fff, 1));
   // signed integer, -32767 - +32767, big endian
   add(new NumberDataType("SIR", 16, SIG|REV, 0x8000, 0x8001, 0x7fff, 1));
+  // signed integer, -32768 - +32767, little endian (no replacement)
+  add(new NumberDataType("S2L", 16, SIG|REQ, 0, 0x8000, 0x7fff, 1));
+  // signed integer, -32768 - +32767, big endian (no replacement)
+  add(new NumberDataType("S2B", 16, SIG|REQ|REV, 0, 0x8000, 0x7fff, 1));
   // unsigned 3 bytes int, 0 - 16777214, little endian
   add(new NumberDataType("U3N", 24, 0, 0xffffff, 0, 0xfffffe, 1));
   // unsigned 3 bytes int, 0 - 16777214, big endian
   add(new NumberDataType("U3R", 24, REV, 0xffffff, 0, 0xfffffe, 1));
+  // unsigned 3 bytes int, 0 - 16777215, little endian (no replacement)
+  add(new NumberDataType("U3L", 24, REQ, 0, 0, 0xffffff, 1));
+  // unsigned 3 bytes int, 0 - 16777215, big endian (no replacement)
+  add(new NumberDataType("U3B", 24, REQ|REV, 0, 0, 0xffffff, 1));
   // signed 3 bytes int, -8388607 - +8388607, little endian
   add(new NumberDataType("S3N", 24, SIG, 0x800000, 0x800001, 0x7fffff, 1));
   // signed 3 bytes int, -8388607 - +8388607, big endian
   add(new NumberDataType("S3R", 24, SIG|REV, 0x800000, 0x800001, 0x7fffff, 1));
+  // signed 3 bytes int, -8388608 - +8388607, little endian (no replacement)
+  add(new NumberDataType("S3L", 24, SIG|REQ, 0, 0x800000, 0x7fffff, 1));
+  // signed 3 bytes int, -8388608 - +8388607, big endian (no replacement)
+  add(new NumberDataType("S3B", 24, SIG|REQ|REV, 0, 0x800000, 0x7fffff, 1));
   // unsigned integer, 0 - 4294967294, little endian
   add(new NumberDataType("ULG", 32, 0, 0xffffffff, 0, 0xfffffffe, 1));
   // unsigned integer, 0 - 4294967294, big endian
   add(new NumberDataType("ULR", 32, REV, 0xffffffff, 0, 0xfffffffe, 1));
   // signed integer, -2147483647 - +2147483647, little endian
+  // unsigned integer, 0 - 4294967295, little endian (no replacement)
+  add(new NumberDataType("U4L", 32, REQ, 0, 0, 0xffffffff, 1));
+  // unsigned integer, 0 - 4294967295, big endian (no replacement)
+  add(new NumberDataType("U4B", 32, REQ|REV, 0, 0, 0xffffffff, 1));
   add(new NumberDataType("SLG", 32, SIG, 0x80000000, 0x80000001, 0x7fffffff, 1));
   // signed integer, -2147483647 - +2147483647, big endian
   add(new NumberDataType("SLR", 32, SIG|REV, 0x80000000, 0x80000001, 0x7fffffff, 1));
+  // signed integer, -2147483648 - +2147483647, little endian (no replacement)
+  add(new NumberDataType("S4L", 32, SIG|REQ, 0, 0x80000000, 0x7fffffff, 1));
+  // signed integer, -2147483648 - +2147483647, big endian (no replacement)
+  add(new NumberDataType("S4B", 32, SIG|REQ|REV, 0, 0x80000000, 0x7fffffff, 1));
   add(new NumberDataType("BI0", 7, ADJ|REQ, 0, 0, 1));  // bit 0 (up to 7 bits until bit 6)
   add(new NumberDataType("BI1", 7, ADJ|REQ, 0, 1, 1));  // bit 1 (up to 7 bits until bit 7)
   add(new NumberDataType("BI2", 6, ADJ|REQ, 0, 2, 1));  // bit 2 (up to 6 bits until bit 7)
@@ -1350,11 +1458,12 @@ void DataTypeList::clear() {
   m_typesById.clear();
 }
 
-result_t DataTypeList::add(const DataType* dataType) {
-  if (m_typesById.find(dataType->getId()) != m_typesById.end()) {
+result_t DataTypeList::add(const DataType* dataType, const string derivedKey) {
+  string key = derivedKey.empty() ? dataType->getId() : derivedKey;
+  if (m_typesById.find(key) != m_typesById.end()) {
     return RESULT_ERR_DUPLICATE_NAME;  // duplicate key
   }
-  m_typesById[dataType->getId()] = dataType;
+  m_typesById[key] = dataType;
   m_cleanupTypes.push_back(dataType);
   return RESULT_OK;
 }
diff --git a/src/lib/ebus/datatype.h b/src/lib/ebus/datatype.h
index c5e77a46..9fb2f3a5 100755
--- a/src/lib/ebus/datatype.h
+++ b/src/lib/ebus/datatype.h
@@ -481,7 +481,25 @@ class NumberDataType : public DataType {
   NumberDataType(const string& id, size_t bitCount, uint16_t flags, unsigned int replacement,
       unsigned int minValue, unsigned int maxValue, int divisor,
       const NumberDataType* baseType = nullptr)
-    : DataType(id, bitCount, flags|NUM, replacement), m_minValue(minValue), m_maxValue(maxValue),
+    : DataType(id, bitCount, flags|NUM, replacement), m_minValue(minValue), m_maxValue(maxValue), m_incValue(0),
+      m_divisor(divisor == 0 ? 1 : divisor), m_precision(calcPrecision(divisor)), m_firstBit(0), m_baseType(baseType) {}
+
+  /**
+   * Constructs a new instance for multiple of 8 bits with increment value.
+   * @param id the type identifier.
+   * @param bitCount the number of bits (maximum length if #ADJ flag is set).
+   * @param flags the combination of flags (like #BCD).
+   * @param replacement the replacement value (no replacement if equal to minValue).
+   * @param minValue the minimum raw value.
+   * @param maxValue the maximum raw value.
+   * @param incValue the smallest step value for increment/decrement, or 0 for auto.
+   * @param divisor the divisor (negative for reciprocal).
+   * @param baseType the base @a NumberDataType for derived instances, or nullptr.
+   */
+  NumberDataType(const string& id, size_t bitCount, uint16_t flags, unsigned int replacement,
+      unsigned int minValue, unsigned int maxValue, unsigned int incValue, int divisor,
+      const NumberDataType* baseType = nullptr)
+    : DataType(id, bitCount, flags|NUM, replacement), m_minValue(minValue), m_maxValue(maxValue), m_incValue(incValue),
       m_divisor(divisor == 0 ? 1 : divisor), m_precision(calcPrecision(divisor)), m_firstBit(0), m_baseType(baseType) {}
 
   /**
@@ -496,7 +514,7 @@ class NumberDataType : public DataType {
    */
   NumberDataType(const string& id, size_t bitCount, uint16_t flags, unsigned int replacement,
       int16_t firstBit, int divisor, const NumberDataType* baseType = nullptr)
-    : DataType(id, bitCount, flags|NUM, replacement), m_minValue(0), m_maxValue((1 << bitCount)-1),
+    : DataType(id, bitCount, flags|NUM, replacement), m_minValue(0), m_maxValue((1 << bitCount)-1), m_incValue(0),
       m_divisor(divisor == 0 ? 1 : divisor), m_precision(0), m_firstBit(firstBit), m_baseType(baseType) {}
 
   /**
@@ -527,6 +545,18 @@ class NumberDataType : public DataType {
    */
   virtual result_t derive(int divisor, size_t bitCount, const NumberDataType** derived) const;
 
+  /**
+   * Derive a new @a NumberDataType from this.
+   * @param min the minimum raw value.
+   * @param max the minimum raw value.
+   * @param inc the smallest step value for increment/decrement, or 0 to keep the current increment (or calculate
+   * automatically).
+   * @param derived the derived @a NumberDataType, or this if derivation is
+   * not necessary.
+   * @return @a RESULT_OK on success, or an error code.
+   */
+  virtual result_t derive(unsigned int min, unsigned int max, unsigned int inc, const NumberDataType** derived) const;
+
   /**
    * @return the minimum raw value.
    */
@@ -546,6 +576,14 @@ class NumberDataType : public DataType {
    */
   result_t getMinMax(bool getMax, const OutputFormat outputFormat, ostream* output) const;
 
+  /**
+   * Check the value against the minimum and maximum value.
+   * @param value the raw value.
+   * @param negative optional variable in which to store the negative flag.
+   * @return @a RESULT_OK on success, or an error code.
+   */
+  result_t checkValueRange(unsigned int value, bool* negative = nullptr) const;
+
   /**
    * Get the smallest step value for increment/decrement.
    * @param outputFormat the @a OutputFormat options to use.
@@ -598,10 +636,19 @@ class NumberDataType : public DataType {
    * @param value the numeric raw value.
    * @param outputFormat the @a OutputFormat options to use.
    * @param output the ostream to append the formatted value to.
+   * @param skipRangeCheck whether to skip the value range check.
    * @return @a RESULT_OK on success, or an error code.
    */
   result_t readFromRawValue(unsigned int value,
-                            OutputFormat outputFormat, ostream* output) const;
+                            OutputFormat outputFormat, ostream* output, bool skipRangeCheck = false) const;
+
+  /**
+   * Internal method for parsing an input string to the coorresponding raw value.
+   * @param inputStr the input string to parse the formatted value from.
+   * @param parsedValue the variable in which to store the parsed raw value.
+   * @return @a RESULT_OK on success, or an error code.
+   */
+  result_t parseInput(const string inputStr, unsigned int* parsedValue) const;
 
   /**
    * Internal method for writing the numeric raw value to a @a SymbolString.
@@ -628,6 +675,9 @@ class NumberDataType : public DataType {
   /** the maximum raw value. */
   const unsigned int m_maxValue;
 
+  /** the smallest step value for increment/decrement, or 0 for auto. */
+  const unsigned int m_incValue;
+
   /** the divisor (negative for reciprocal). */
   const int m_divisor;
 
@@ -680,10 +730,11 @@ class DataTypeList {
   /**
    * Adds a @a DataType instance to this map.
    * @param dataType the @a DataType instance to add.
+   * @param derivedKey optional speicla key for derived instances.
    * @return @a RESULT_OK on success, or an error code.
    * Note: the caller may not free the added instance on success.
    */
-  result_t add(const DataType* dataType);
+  result_t add(const DataType* dataType, const string derivedKey = "");
 
   /**
    * Adds a @a DataType instance for later cleanup.
diff --git a/src/lib/ebus/test/test_data.cpp b/src/lib/ebus/test/test_data.cpp
index 1cfb5626..6fce4b91 100755
--- a/src/lib/ebus/test/test_data.cpp
+++ b/src/lib/ebus/test/test_data.cpp
@@ -53,15 +53,18 @@ void verify(bool expectFailMatch, string type, string input,
 
 class TestReader : public MappedFileReader {
  public:
-  TestReader(DataFieldTemplates* templates, bool isSet, bool isMasterDest)
+  TestReader(DataFieldTemplates* templates, bool isSet, bool isMasterDest, bool withRange)
       : MappedFileReader::MappedFileReader(true), m_templates(templates), m_isSet(isSet), m_isMasterDest(isMasterDest),
-        m_fields(nullptr) {}
+        m_withRange(withRange), m_fields(nullptr) {}
   result_t getFieldMap(const string& preferLanguage, vector<string>* row, string* errorDescription) const override {
     if (row->empty()) {
       row->push_back("*name");
       row->push_back("part");
       row->push_back("type");
       row->push_back("divisor/values");
+      if (m_withRange) {
+        row->push_back("range");
+      }
       row->push_back("unit");
       row->push_back("comment");
       return RESULT_OK;
@@ -86,6 +89,7 @@ class TestReader : public MappedFileReader {
   const DataFieldTemplates* m_templates;
   const bool m_isSet;
   const bool m_isMasterDest;
+  const bool m_withRange;
  public:
   const DataField* m_fields;
 };
@@ -101,7 +105,7 @@ int main() {
   // entry: definition, decoded value, master data, slave data, flags
   // definition: name,part,type[:len][,[divisor|values][,[unit][,[comment]]]]
   unsigned int baseLine = __LINE__+1;
-  string checks[][5] = {
+  string checks[][6] = {
       {"x,,ign:10",  "",                              "10fe07000a00000000000000000000", "00", ""},
       {"x,,ign:*",   "",                              "10fe07000a00000000000000000000", "00", "W"},
       {"x,,ign,2",   "",                              "",                               "",   "c"},
@@ -344,6 +348,13 @@ int main() {
       {"x,,uch,==48", "",    "10feffff01ab", "00", "rW"},
       {"x,,uch,=48", "",     "10feffff0130", "00", ""},
       {"x,,uch,==48", "",    "10feffff0130", "00", ""},
+      {"x,,uch,,1-3", "2",    "10feffff0102", "00", "-"},
+      {"x,,uch,,1-3", "4",    "10feffff0102", "00", "-Rw:ERR: argument value out of valid range"},
+      {"x,,uch,,1-3", "2",    "10feffff0104", "00", "-rW:ERR: argument value out of valid range"},
+      {"x,,uch,,0x1-0x3", "4","10feffff0102", "00", "-Rw:ERR: argument value out of valid range"},
+      {"x,,uch,,0x1-0x3", "2","10feffff0104", "00", "-rW:ERR: argument value out of valid range"},
+      {"x,,uch", "\n     \"x\": {\"value\": 2}",    "10feffff0102", "00", "-jV", "\n     { \"name\": \"x\", \"slave\": false, \"type\": \"UCH\", \"isbits\": false, \"isadjustable\": false, \"isignored\": false, \"isreverse\": false, \"length\": 1, \"result\": \"number\", \"min\": 0, \"max\": 254, \"step\": 1, \"unit\": \"\", \"comment\": \"\"}"},
+      {"x,,uch,,1-3:2", "\n     \"x\": {\"value\": 2}",    "10feffff0102", "00", "-jV", "\n     { \"name\": \"x\", \"slave\": false, \"type\": \"UCH\", \"isbits\": false, \"isadjustable\": false, \"isignored\": false, \"isreverse\": false, \"length\": 1, \"result\": \"number\", \"min\": 1, \"max\": 3, \"step\": 2, \"unit\": \"\", \"comment\": \"\"}"},
       {"x,,sch", "-90",      "10feffff01a6", "00", ""},
       {"x,,sch", "0",        "10feffff0100", "00", ""},
       {"x,,sch", "-1",       "10feffff01ff", "00", ""},
@@ -352,6 +363,15 @@ int main() {
       {"x,,sch", "127",      "10feffff017f", "00", ""},
       {"x,,sch,10", "-9.0",  "10feffff01a6", "00", ""},
       {"x,,sch,-10", "-900", "10feffff01a6", "00", ""},
+      {"x,,sch,,1-3", "2",    "10feffff0102", "00", "-"},
+      {"x,,sch,,1-500", "-",    "10feffff0180", "00", "-c"},
+      {"x,,sch,,-130-1", "-",   "10feffff0180", "00", "-c"},
+      {"x,,sch,,-127-127", "-",   "10feffff0180", "00", "-"},
+      {"x,,sch,,-127-128", "-",   "10feffff0180", "00", "-c"},
+      {"x,,sch,,-128-127", "-",   "10feffff0180", "00", "-c"},
+      {"x,,sch,,1-3", "4",    "10feffff0102", "00", "-Rw:ERR: argument value out of valid range"},
+      {"x,,sch,,1-3", "2",    "10feffff0104", "00", "-rW:ERR: argument value out of valid range"},
+      {"x,,sch,,-3--1", "-4", "10feffff01fe", "00", "-Rw:ERR: argument value out of valid range"},
       {"x,,d1b", "-90",      "10feffff01a6", "00", ""},
       {"x,,d1b", "0",        "10feffff0100", "00", ""},
       {"x,,d1b", "-1",       "10feffff01ff", "00", ""},
@@ -429,6 +449,12 @@ int main() {
       {"x,,flt", "-",      "10feffff020080", "00", ""},
       {"x,,flt", "-32.767", "10feffff020180", "00", ""},
       {"x,,flt", "32.767", "10feffff02ff7f", "00", ""},
+      {"x,,flt,,1-3", "2.000",    "10feffff02d007", "00", "-"},
+      {"x,,flt,,1-3", "4.000",    "10feffff02d007", "00", "-Rw:ERR: argument value out of valid range"},
+      {"x,,flt,,1-3", "2.000",    "10feffff02a00f", "00", "-rW:ERR: argument value out of valid range"},
+      {"x,,flt,,-3--1", "-4", "10feffff0230f8", "00", "-Rw:ERR: argument value out of valid range"}, // -4:60f0, -2:30f8
+      {"x,,flt,,-3.1--1.0", "-4", "10feffff0230f8", "00", "-Rw:ERR: argument value out of valid range"},
+      {"x,,flt,,-3.1--1.0", "-2", "10feffff0260f0", "00", "-rW:ERR: argument value out of valid range"},
       {"x,,flr", "-0.090", "10feffff02ffa6", "00", ""},
       {"x,,flr", "0.000",  "10feffff020000", "00", ""},
       {"x,,flr", "-0.001", "10feffff02ffff", "00", ""},
@@ -445,6 +471,11 @@ int main() {
       {"x,,exp", "0.25",  "10feffff040000803e", "00", ""},
       {"x,,exp", "0.95",  "10feffff043333733f", "00", ""},
       {"x,,exp", "0.65",  "10feffff046666263f", "00", ""},
+      {"x,,exp", "0.065", "10feffff04b81e853d", "00", ""},
+      {"x,,exp,,0-0.65", "0.65",  "10feffff046666263f", "00", "-"},
+      {"x,,exp,,0-0.5", "0.65",  "10feffff046666263f", "00", "-rw:ERR: argument value out of valid range"},
+      {"x,,exp,10,0-0.065", "0.0650000",  "10feffff046666263f", "00", "-"},
+      {"x,,exp,10,0-0.05", "0.0650000",  "10feffff046666263f", "00", "-rw:ERR: argument value out of valid range"},
       {"x,,exr", "-0.09",  "10feffff04bdb851ec", "00", ""},
       {"x,,exr", "0.0",    "10feffff0400000000", "00", ""},
       {"x,,exr", "-0.001", "10feffff04ba83126f", "00", ""},
@@ -577,6 +608,12 @@ int main() {
       continue;
     }
     string flags = check[4];
+    size_t colon = flags.find(':');
+    string errStr;
+    if (colon != string::npos) {
+      errStr = flags.substr(colon+1);
+      flags = flags.substr(0, colon);
+    }
     bool isSet = flags.find('s') != string::npos;
     bool testFields = flags.find('F') != string::npos;
     bool failedCreate = flags.find('c') != string::npos;
@@ -584,6 +621,7 @@ int main() {
     bool failedReadMatch = flags.find('R') != string::npos;
     bool failedWrite = flags.find('w') != string::npos;
     bool failedWriteMatch = flags.find('W') != string::npos;
+    string withDump = check[5];  // optional
     const char* findName = flags.find('I') == string::npos ? nullptr : "x";
     ssize_t findIndex = -1;
     if (flags.find('i') != string::npos) {
@@ -599,6 +637,9 @@ int main() {
     if (flags.find("vvv") != string::npos) {
       verbosity |= OF_COMMENTS;
     }
+    if (flags.find("V") != string::npos) {
+      verbosity |= OF_NAMES|OF_UNITS|OF_COMMENTS|OF_ALL_ATTRS;
+    }
     if (flags.find('j') != string::npos) {
       verbosity |= OF_JSON;
     }
@@ -620,7 +661,8 @@ int main() {
       }
       continue;
     }
-    TestReader reader{templates, isSet, mstr[1] == BROADCAST || isMaster(mstr[1])};
+    bool withRange = flags.find('-') != string::npos;
+    TestReader reader{templates, isSet, mstr[1] == BROADCAST || isMaster(mstr[1]), withRange};
     lineNo = 0;
     dummystr.clear();
     dummystr.str("#");
@@ -638,6 +680,9 @@ int main() {
       if (result == RESULT_OK) {
         cout << "\"" << check[0] << "\": failed create error: unexpectedly succeeded" << endl;
         error = true;
+      } else if (!errStr.empty() && errorDescription != errStr) {
+        cout << "\"" << check[0] << "\": failed create error: unexpected result \"" << errorDescription << "\" instead of \"" << errStr << "\"" << endl;
+        error = true;
       } else {
         cout << "\"" << check[0] << "\": failed create OK" << endl;
       }
@@ -680,6 +725,10 @@ int main() {
           cout << "  failed read " << fields->getName(-1) << " >" << check[2] << " " << check[3]
                << "< error: unexpectedly succeeded" << endl;
           error = true;
+        } else if (!errStr.empty() && getResultCode(result) != errStr) {
+          cout << "  failed read " << fields->getName(-1) << " >" << check[2] << " " << check[3]
+               << "< error: unexpected result \"";
+          cout << getResultCode(result) << "\" instead of \"" << errStr << "\"" << endl;
         } else {
           cout << "  failed read " << fields->getName(-1) << " >" << check[2] << " " << check[3]
                << "< OK" << endl;
@@ -744,6 +793,10 @@ int main() {
           cout << "  failed write " << fields->getName(-1) << " >"
                << expectStr << "< error: unexpectedly succeeded" << endl;
           error = true;
+        } else if (!errStr.empty() && getResultCode(result) != errStr) {
+          cout << "  failed write " << fields->getName(-1) << " >"
+               << "< error: unexpected result \"";
+          cout << getResultCode(result) << "\" instead of \"" << errStr << "\"" << endl;
         } else {
           cout << "  failed write " << fields->getName(-1) << " >"
                << expectStr << "< OK" << endl;
@@ -760,6 +813,13 @@ int main() {
                writeMstr.getStr() + " " + writeSstr.getStr());
       }
     }
+    if (!withDump.empty()) {
+      output.clear();
+      output.str("");
+      fields->dump(false, verbosity, &output);
+      bool match = output.str() == withDump;
+      verify(false, "dump", withDump, match, withDump, output.str());
+    }
     delete fields;
     fields = nullptr;
   }