From f81a32be2b4abb9bc3aca7b76e3b87d3a127d4ef Mon Sep 17 00:00:00 2001 From: forntoh Date: Thu, 17 Oct 2024 02:06:05 +0200 Subject: [PATCH 1/6] Integrate Fast and Efficient String Formatting Library This PR introduces a trimmed-down version of the [mpaland/printf](https://github.com/mpaland/printf) library to handle string formatting on embedded systems. The integration of this library significantly enhances the performance and efficiency of string formatting operations. Key changes include: - Addition of the `mpaland/printf` library to the project. - Refactoring string formatting in menu items to use a more efficient and flexible approach. These changes ensure that our string formatting operations are optimized for embedded systems, leading to faster and more efficient code execution. --- examples/IntFloatValues/IntFloatValues.ino | 4 +- src/ItemFloatRange.h | 56 +- src/ItemInput.h | 16 - src/ItemIntRange.h | 10 +- src/ItemRangeBase.h | 18 +- src/MenuItem.h | 16 + src/utils/printf.c | 627 +++++++++++++++++++++ src/utils/printf.h | 83 +++ 8 files changed, 744 insertions(+), 86 deletions(-) create mode 100644 src/utils/printf.c create mode 100644 src/utils/printf.h diff --git a/examples/IntFloatValues/IntFloatValues.ino b/examples/IntFloatValues/IntFloatValues.ino index f531a17e..765c8ff3 100644 --- a/examples/IntFloatValues/IntFloatValues.ino +++ b/examples/IntFloatValues/IntFloatValues.ino @@ -17,8 +17,8 @@ void callbackFloat(float value); // clang-format off MENU_SCREEN(mainScreen, mainItems, ITEM_BASIC("Con"), - ITEM_INT_RANGE("Dist", 100, 200, 100, callbackInt, (const char*) "m"), - ITEM_FLOAT_RANGE("Curr", -1.0f, 1.0f, -1.0f, callbackFloat, (const char*) "mA", 0.01f), + ITEM_INT_RANGE("Dist", 100, 200, 100, callbackInt, "%02dm"), + ITEM_FLOAT_RANGE("Curr", -1.0f, 1.0f, -1.0f, callbackFloat, "%.2fmA", 0.01f), ITEM_BASIC("Blink SOS"), ITEM_BASIC("Blink random")); // clang-format on diff --git a/src/ItemFloatRange.h b/src/ItemFloatRange.h index 9b7e7644..cf3c2f29 100644 --- a/src/ItemFloatRange.h +++ b/src/ItemFloatRange.h @@ -8,9 +8,6 @@ * The value can be incremented or decremented by a specified step. */ class ItemFloatRange : public ItemRangeBase { - private: - uint8_t decimalPlaces = 1; - public: ItemFloatRange( const char* text, @@ -18,63 +15,16 @@ class ItemFloatRange : public ItemRangeBase { const float max, float startingValue, fptrFloat callback, - const char* unit = NULL, + const char* format = "%.2f", float step = 0.1f, bool commitOnChange = false) - : ItemRangeBase(text, min, max, startingValue, callback, unit, step, commitOnChange) { - decimalPlaces = calculateDecimalPlaces(step); - } + : ItemRangeBase(text, min, max, startingValue, callback, format, step, commitOnChange) {} char* getDisplayValue() override { static char buffer[10]; - dtostrf(currentValue, calculateWidth(currentValue, decimalPlaces), decimalPlaces, buffer); - if (unit == NULL) { - return buffer; - } - concat(buffer, unit, buffer); + snprintf(buffer, sizeof(buffer), format, currentValue); return buffer; } - - /** - * @brief Calculates the width of a floating-point number when displayed with a specified number of decimal places. - * - * This function computes the width of the string representation of a floating-point number, including the decimal point - * and the specified number of decimal places. - * - * @param currentValue The floating-point number whose width is to be calculated. - * @param decimalPlaces The number of decimal places to include in the width calculation. - * @return The width of the floating-point number when displayed as a string. - */ - static inline uint8_t calculateWidth(float currentValue, uint8_t decimalPlaces) { - int intPart = static_cast(currentValue); - int intPartWidth = (intPart == 0) ? 1 : static_cast(log10(abs(intPart))) + 1; - return intPartWidth + 1 + decimalPlaces; // +1 for the decimal point - } - - /** - * @brief Calculates the number of decimal places in a given floating-point step value. - * - * This function converts the floating-point step value to a string representation, - * then determines the number of decimal places by counting the digits after the - * decimal point. Trailing zeros are not counted as decimal places. - * - * @param step The floating-point step value to analyze. - * @return The number of decimal places in the step value. - */ - static uint8_t calculateDecimalPlaces(float step) { - char buffer[10]; - dtostrf(step, 5, 5, buffer); - - char* decimalPos = strchr(buffer, '.'); - if (decimalPos == NULL) return 0; - - uint8_t numDecimalPlaces = strlen(decimalPos + 1); - while (numDecimalPlaces > 0 && buffer[strlen(buffer) - 1] == '0') { - buffer[strlen(buffer) - 1] = '\0'; - numDecimalPlaces--; - } - return numDecimalPlaces; - } }; #define ITEM_FLOAT_RANGE(...) (new ItemFloatRange(__VA_ARGS__)) diff --git a/src/ItemInput.h b/src/ItemInput.h index 56e54749..7ccdf889 100644 --- a/src/ItemInput.h +++ b/src/ItemInput.h @@ -61,22 +61,6 @@ class ItemInput : public MenuItem { * First parameter will be a `value` string. */ fptrStr callback; - /** - * @brief The number of visible characters. - * - * ``` - * visible area - * ┌───────────────┐ - * X X X X│X X X X █ X X X│X X - * ├───────────────┤ - * │<── viewSize ─>│ - * ``` - * - * Effectively const, but initialized lately when renderer is injected. - */ - inline uint8_t getViewSize(MenuRenderer* renderer) const { - return renderer->getEffectiveCols() - strlen(text) - 1 + renderer->viewShift; - }; public: /** diff --git a/src/ItemIntRange.h b/src/ItemIntRange.h index d353cacb..b45a65f7 100644 --- a/src/ItemIntRange.h +++ b/src/ItemIntRange.h @@ -15,18 +15,14 @@ class ItemIntRange : public ItemRangeBase { const int max, int startingValue, fptrInt callback, - const char* unit = NULL, + const char* format = "%d", int step = 1, bool commitOnChange = false) - : ItemRangeBase(text, min, max, startingValue, callback, unit, step, commitOnChange) {} + : ItemRangeBase(text, min, max, startingValue, callback, format, step, commitOnChange) {} char* getDisplayValue() override { static char buffer[10]; - itoa(currentValue, buffer, 10); - if (unit == NULL) { - return buffer; - } - concat(buffer, unit, buffer); + sprintf(buffer, format, currentValue); return buffer; } }; diff --git a/src/ItemRangeBase.h b/src/ItemRangeBase.h index 0f0ab63e..c520bbca 100644 --- a/src/ItemRangeBase.h +++ b/src/ItemRangeBase.h @@ -3,18 +3,20 @@ #include "LcdMenu.h" #include "MenuItem.h" +#include #include template /** * @brief Item that allows user to select a value from a range. * It can be used to display all sorts of values that can be incremented or decremented. - * You can additionally pass a unit to be displayed after the value. e.g. "%", "°C", "°F" etc. + * You can additionally pass format string to format the value. + * e.g. you can use `%.2f` to display float with 2 decimal places. * * ``` - * ┌──────────────────────────────────┐ - * │ > T E X T : V A L U E U N I T │ - * └──────────────────────────────────┘ + * ┌────────────────────────────────────────┐ + * │ > T E X T : F O R M A T E D V A L U E │ + * └────────────────────────────────────────┘ * ``` * * Additionally to `text` this item has float `currentValue`. @@ -26,7 +28,7 @@ class ItemRangeBase : public MenuItem { const T maxValue; T currentValue; void (*callback)(T); - const char* unit; + const char* format; const T step; bool commitOnChange; @@ -39,7 +41,7 @@ class ItemRangeBase : public MenuItem { * @param max The maximum value. * @param startingValue The current value. * @param callback A pointer to the callback function to execute when this menu item is selected. - * @param unit The unit e.g. "%", "°C", "°F". + * @param format The format string to format the value. * @param step The step value for increment/decrement. * @param commitOnChange If true, the callback will be called every time the value changes. */ @@ -49,10 +51,10 @@ class ItemRangeBase : public MenuItem { const T max, T startingValue, void (*callback)(T), - const char* unit = NULL, + const char* format = "%s", T step = 1, bool commitOnChange = false) - : MenuItem(text), minValue(min), maxValue(max), currentValue(startingValue), callback(callback), unit(unit), step(step), commitOnChange(commitOnChange) {} + : MenuItem(text), minValue(min), maxValue(max), currentValue(startingValue), callback(callback), format(format), step(step), commitOnChange(commitOnChange) {} /** * @brief Increments the value. diff --git a/src/MenuItem.h b/src/MenuItem.h index cb6b7a07..159fc56c 100644 --- a/src/MenuItem.h +++ b/src/MenuItem.h @@ -72,6 +72,22 @@ class MenuItem { ~MenuItem() noexcept = default; protected: + /** + * @brief The number of available columns for the potential value of the item. + * + * ``` + * visible area + * ┌───────────────┐ + * X X X X│X X X X █ X X X│X X + * ├───────────────┤ + * │<── viewSize ─>│ + * ``` + * + * Effectively const, but initialized lately when renderer is injected. + */ + inline uint8_t getViewSize(MenuRenderer* renderer) const { + return renderer->getEffectiveCols() - strlen(text) - 1 + renderer->viewShift; + }; /** * @brief Process a command decoded in 1 byte. * It can be a printable character or a control command like `ENTER` or `LEFT`. diff --git a/src/utils/printf.c b/src/utils/printf.c new file mode 100644 index 00000000..c5b2dc84 --- /dev/null +++ b/src/utils/printf.c @@ -0,0 +1,627 @@ +/////////////////////////////////////////////////////////////////////////////// +// \author (c) Marco Paland (info@paland.com) +// 2014-2019, PALANDesign Hannover, Germany +// +// \license The MIT License (MIT) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// +// \brief Tiny printf, sprintf and (v)snprintf implementation, optimized for speed on +// embedded systems with a very limited resources. These routines are thread +// safe and reentrant! +// Use this instead of the bloated standard/newlib printf cause these use +// malloc for printf (and may not be thread safe). +// +/////////////////////////////////////////////////////////////////////////////// + +#include +#include + +#include "printf.h" + +// 'ntoa' conversion buffer size, this must be big enough to hold one converted +// numeric number including padded zeros (dynamically created on stack) +// default: 32 byte +#ifndef PRINTF_NTOA_BUFFER_SIZE +#define PRINTF_NTOA_BUFFER_SIZE 32U +#endif + +// 'ftoa' conversion buffer size, this must be big enough to hold one converted +// float number including padded zeros (dynamically created on stack) +// default: 32 byte +#ifndef PRINTF_FTOA_BUFFER_SIZE +#define PRINTF_FTOA_BUFFER_SIZE 32U +#endif + +// support for the floating point type (%f) +// default: activated +#ifndef PRINTF_DISABLE_SUPPORT_FLOAT +#define PRINTF_SUPPORT_FLOAT +#endif + +// define the default floating point precision +// default: 6 digits +#ifndef PRINTF_DEFAULT_FLOAT_PRECISION +#define PRINTF_DEFAULT_FLOAT_PRECISION 6U +#endif + +// define the largest float suitable to print with %f +// default: 1e9 +#ifndef PRINTF_MAX_FLOAT +#define PRINTF_MAX_FLOAT 1e9 +#endif + +/////////////////////////////////////////////////////////////////////////////// + +// internal flag definitions +#define FLAGS_ZEROPAD (1U << 0U) +#define FLAGS_LEFT (1U << 1U) +#define FLAGS_PLUS (1U << 2U) +#define FLAGS_SPACE (1U << 3U) +#define FLAGS_HASH (1U << 4U) +#define FLAGS_UPPERCASE (1U << 5U) +#define FLAGS_CHAR (1U << 6U) +#define FLAGS_SHORT (1U << 7U) +#define FLAGS_LONG (1U << 8U) +#define FLAGS_LONG_LONG (1U << 9U) +#define FLAGS_PRECISION (1U << 10U) + +// import float.h for DBL_MAX +#if defined(PRINTF_SUPPORT_FLOAT) +#include +#endif + +// output function type +typedef void (*out_fct_type)(char character, void* buffer, size_t idx, size_t maxlen); + +// internal buffer output +static inline void _out_buffer(char character, void* buffer, size_t idx, size_t maxlen) { + if (idx < maxlen) { + ((char*)buffer)[idx] = character; + } +} + +// internal null output +static inline void _out_null(char character, void* buffer, size_t idx, size_t maxlen) { + (void)character; + (void)buffer; + (void)idx; + (void)maxlen; +} + +// internal secure strlen +// \return The length of the string (excluding the terminating 0) limited by 'maxsize' +static inline unsigned int _strnlen_s(const char* str, size_t maxsize) { + const char* s; + for (s = str; *s && maxsize--; ++s) + ; + return (unsigned int)(s - str); +} + +// internal test if char is a digit (0-9) +// \return true if char is a digit +static inline bool _is_digit(char ch) { + return (ch >= '0') && (ch <= '9'); +} + +// internal ASCII string to unsigned int conversion +static unsigned int _atoi(const char** str) { + unsigned int i = 0U; + while (_is_digit(**str)) { + i = i * 10U + (unsigned int)(*((*str)++) - '0'); + } + return i; +} + +// output the specified string in reverse, taking care of any zero-padding +static size_t _out_rev(out_fct_type out, char* buffer, size_t idx, size_t maxlen, const char* buf, size_t len, unsigned int width, unsigned int flags) { + const size_t start_idx = idx; + + // pad spaces up to given width + if (!(flags & FLAGS_LEFT) && !(flags & FLAGS_ZEROPAD)) { + for (size_t i = len; i < width; i++) { + out(' ', buffer, idx++, maxlen); + } + } + + // reverse string + while (len) { + out(buf[--len], buffer, idx++, maxlen); + } + + // append pad spaces up to given width + if (flags & FLAGS_LEFT) { + while (idx - start_idx < width) { + out(' ', buffer, idx++, maxlen); + } + } + + return idx; +} + +// internal itoa format +static size_t _ntoa_format(out_fct_type out, char* buffer, size_t idx, size_t maxlen, char* buf, size_t len, bool negative, unsigned int base, unsigned int prec, unsigned int width, unsigned int flags) { + // pad leading zeros + if (!(flags & FLAGS_LEFT)) { + if (width && (flags & FLAGS_ZEROPAD) && (negative || (flags & (FLAGS_PLUS | FLAGS_SPACE)))) { + width--; + } + while ((len < prec) && (len < PRINTF_NTOA_BUFFER_SIZE)) { + buf[len++] = '0'; + } + while ((flags & FLAGS_ZEROPAD) && (len < width) && (len < PRINTF_NTOA_BUFFER_SIZE)) { + buf[len++] = '0'; + } + } + + // handle hash + if (flags & FLAGS_HASH) { + if (!(flags & FLAGS_PRECISION) && len && ((len == prec) || (len == width))) { + len--; + if (len && (base == 16U)) { + len--; + } + } + if ((base == 16U) && !(flags & FLAGS_UPPERCASE) && (len < PRINTF_NTOA_BUFFER_SIZE)) { + buf[len++] = 'x'; + } else if ((base == 16U) && (flags & FLAGS_UPPERCASE) && (len < PRINTF_NTOA_BUFFER_SIZE)) { + buf[len++] = 'X'; + } else if ((base == 2U) && (len < PRINTF_NTOA_BUFFER_SIZE)) { + buf[len++] = 'b'; + } + if (len < PRINTF_NTOA_BUFFER_SIZE) { + buf[len++] = '0'; + } + } + + if (len < PRINTF_NTOA_BUFFER_SIZE) { + if (negative) { + buf[len++] = '-'; + } else if (flags & FLAGS_PLUS) { + buf[len++] = '+'; // ignore the space if the '+' exists + } else if (flags & FLAGS_SPACE) { + buf[len++] = ' '; + } + } + + return _out_rev(out, buffer, idx, maxlen, buf, len, width, flags); +} + +// internal itoa for 'long' type +static size_t _ntoa_long(out_fct_type out, char* buffer, size_t idx, size_t maxlen, unsigned long value, bool negative, unsigned long base, unsigned int prec, unsigned int width, unsigned int flags) { + char buf[PRINTF_NTOA_BUFFER_SIZE]; + size_t len = 0U; + + // no hash for 0 values + if (!value) { + flags &= ~FLAGS_HASH; + } + + // write if precision != 0 and value is != 0 + if (!(flags & FLAGS_PRECISION) || value) { + do { + const char digit = (char)(value % base); + buf[len++] = digit < 10 ? '0' + digit : (flags & FLAGS_UPPERCASE ? 'A' : 'a') + digit - 10; + value /= base; + } while (value && (len < PRINTF_NTOA_BUFFER_SIZE)); + } + + return _ntoa_format(out, buffer, idx, maxlen, buf, len, negative, (unsigned int)base, prec, width, flags); +} + +#if defined(PRINTF_SUPPORT_FLOAT) + +// internal ftoa for fixed decimal floating point +static size_t _ftoa(out_fct_type out, char* buffer, size_t idx, size_t maxlen, double value, unsigned int prec, unsigned int width, unsigned int flags) { + char buf[PRINTF_FTOA_BUFFER_SIZE]; + size_t len = 0U; + double diff = 0.0; + + // powers of 10 + static const double pow10[] = {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000}; + + // test for special values + if (value != value) + return _out_rev(out, buffer, idx, maxlen, "nan", 3, width, flags); + if (value < -DBL_MAX) + return _out_rev(out, buffer, idx, maxlen, "fni-", 4, width, flags); + if (value > DBL_MAX) + return _out_rev(out, buffer, idx, maxlen, (flags & FLAGS_PLUS) ? "fni+" : "fni", (flags & FLAGS_PLUS) ? 4U : 3U, width, flags); + + // test for very large values + // standard printf behavior is to print EVERY whole number digit -- which could be 100s of characters overflowing your buffers == bad + if ((value > PRINTF_MAX_FLOAT) || (value < -PRINTF_MAX_FLOAT)) { + return 0U; + } + + // test for negative + bool negative = false; + if (value < 0) { + negative = true; + value = 0 - value; + } + + // set default precision, if not set explicitly + if (!(flags & FLAGS_PRECISION)) { + prec = PRINTF_DEFAULT_FLOAT_PRECISION; + } + // limit precision to 9, cause a prec >= 10 can lead to overflow errors + while ((len < PRINTF_FTOA_BUFFER_SIZE) && (prec > 9U)) { + buf[len++] = '0'; + prec--; + } + + int whole = (int)value; + double tmp = (value - whole) * pow10[prec]; + unsigned long frac = (unsigned long)tmp; + diff = tmp - frac; + + if (diff > 0.5) { + ++frac; + // handle rollover, e.g. case 0.99 with prec 1 is 1.0 + if (frac >= pow10[prec]) { + frac = 0; + ++whole; + } + } else if (diff < 0.5) { + } else if ((frac == 0U) || (frac & 1U)) { + // if halfway, round up if odd OR if last digit is 0 + ++frac; + } + + if (prec == 0U) { + diff = value - (double)whole; + if ((!(diff < 0.5) || (diff > 0.5)) && (whole & 1)) { + // exactly 0.5 and ODD, then round up + // 1.5 -> 2, but 2.5 -> 2 + ++whole; + } + } else { + unsigned int count = prec; + // now do fractional part, as an unsigned number + while (len < PRINTF_FTOA_BUFFER_SIZE) { + --count; + buf[len++] = (char)(48U + (frac % 10U)); + if (!(frac /= 10U)) { + break; + } + } + // add extra 0s + while ((len < PRINTF_FTOA_BUFFER_SIZE) && (count-- > 0U)) { + buf[len++] = '0'; + } + if (len < PRINTF_FTOA_BUFFER_SIZE) { + // add decimal + buf[len++] = '.'; + } + } + + // do whole part, number is reversed + while (len < PRINTF_FTOA_BUFFER_SIZE) { + buf[len++] = (char)(48 + (whole % 10)); + if (!(whole /= 10)) { + break; + } + } + + // pad leading zeros + if (!(flags & FLAGS_LEFT) && (flags & FLAGS_ZEROPAD)) { + if (width && (negative || (flags & (FLAGS_PLUS | FLAGS_SPACE)))) { + width--; + } + while ((len < width) && (len < PRINTF_FTOA_BUFFER_SIZE)) { + buf[len++] = '0'; + } + } + + if (len < PRINTF_FTOA_BUFFER_SIZE) { + if (negative) { + buf[len++] = '-'; + } else if (flags & FLAGS_PLUS) { + buf[len++] = '+'; // ignore the space if the '+' exists + } else if (flags & FLAGS_SPACE) { + buf[len++] = ' '; + } + } + + return _out_rev(out, buffer, idx, maxlen, buf, len, width, flags); +} + +#endif // PRINTF_SUPPORT_FLOAT + +// internal vsnprintf +static int _vsnprintf(out_fct_type out, char* buffer, const size_t maxlen, const char* format, va_list va) { + unsigned int flags, width, precision, n; + size_t idx = 0U; + + if (!buffer) { + // use null output function + out = _out_null; + } + + while (*format) { + // format specifier? %[flags][width][.precision][length] + if (*format != '%') { + // no + out(*format, buffer, idx++, maxlen); + format++; + continue; + } else { + // yes, evaluate it + format++; + } + + // evaluate flags + flags = 0U; + do { + switch (*format) { + case '0': + flags |= FLAGS_ZEROPAD; + format++; + n = 1U; + break; + case '-': + flags |= FLAGS_LEFT; + format++; + n = 1U; + break; + case '+': + flags |= FLAGS_PLUS; + format++; + n = 1U; + break; + case ' ': + flags |= FLAGS_SPACE; + format++; + n = 1U; + break; + case '#': + flags |= FLAGS_HASH; + format++; + n = 1U; + break; + default: n = 0U; break; + } + } while (n); + + // evaluate width field + width = 0U; + if (_is_digit(*format)) { + width = _atoi(&format); + } else if (*format == '*') { + const int w = va_arg(va, int); + if (w < 0) { + flags |= FLAGS_LEFT; // reverse padding + width = (unsigned int)-w; + } else { + width = (unsigned int)w; + } + format++; + } + + // evaluate precision field + precision = 0U; + if (*format == '.') { + flags |= FLAGS_PRECISION; + format++; + if (_is_digit(*format)) { + precision = _atoi(&format); + } else if (*format == '*') { + const int prec = (int)va_arg(va, int); + precision = prec > 0 ? (unsigned int)prec : 0U; + format++; + } + } + + // evaluate length field + switch (*format) { + case 'l': + flags |= FLAGS_LONG; + format++; + if (*format == 'l') { + flags |= FLAGS_LONG_LONG; + format++; + } + break; + case 'h': + flags |= FLAGS_SHORT; + format++; + if (*format == 'h') { + flags |= FLAGS_CHAR; + format++; + } + break; + case 'j': + flags |= (sizeof(intmax_t) == sizeof(long) ? FLAGS_LONG : FLAGS_LONG_LONG); + format++; + break; + case 'z': + flags |= (sizeof(size_t) == sizeof(long) ? FLAGS_LONG : FLAGS_LONG_LONG); + format++; + break; + default: + break; + } + + // evaluate specifier + switch (*format) { + case 'd': + case 'i': + case 'u': + case 'x': + case 'X': + case 'o': + case 'b': + { + // set the base + unsigned int base; + if (*format == 'x' || *format == 'X') { + base = 16U; + } else if (*format == 'o') { + base = 8U; + } else if (*format == 'b') { + base = 2U; + } else { + base = 10U; + flags &= ~FLAGS_HASH; // no hash for dec format + } + // uppercase + if (*format == 'X') { + flags |= FLAGS_UPPERCASE; + } + + // no plus or space flag for u, x, X, o, b + if ((*format != 'i') && (*format != 'd')) { + flags &= ~(FLAGS_PLUS | FLAGS_SPACE); + } + + // ignore '0' flag when precision is given + if (flags & FLAGS_PRECISION) { + flags &= ~FLAGS_ZEROPAD; + } + + // convert the integer + if ((*format == 'i') || (*format == 'd')) { + // signed + if (flags & FLAGS_LONG) { + const long value = va_arg(va, long); + idx = _ntoa_long(out, buffer, idx, maxlen, (unsigned long)(value > 0 ? value : 0 - value), value < 0, base, precision, width, flags); + } else { + const int value = (flags & FLAGS_CHAR) ? (char)va_arg(va, int) : (flags & FLAGS_SHORT) ? (short int)va_arg(va, int) + : va_arg(va, int); + idx = _ntoa_long(out, buffer, idx, maxlen, (unsigned int)(value > 0 ? value : 0 - value), value < 0, base, precision, width, flags); + } + } else { + // unsigned + if (flags & FLAGS_LONG) { + idx = _ntoa_long(out, buffer, idx, maxlen, va_arg(va, unsigned long), false, base, precision, width, flags); + } else { + const unsigned int value = (flags & FLAGS_CHAR) ? (unsigned char)va_arg(va, unsigned int) : (flags & FLAGS_SHORT) ? (unsigned short int)va_arg(va, unsigned int) + : va_arg(va, unsigned int); + idx = _ntoa_long(out, buffer, idx, maxlen, value, false, base, precision, width, flags); + } + } + format++; + break; + } +#if defined(PRINTF_SUPPORT_FLOAT) + case 'f': + case 'F': + if (*format == 'F') flags |= FLAGS_UPPERCASE; + idx = _ftoa(out, buffer, idx, maxlen, va_arg(va, double), precision, width, flags); + format++; + break; +#endif // PRINTF_SUPPORT_FLOAT + case 'c': + { + unsigned int l = 1U; + // pre padding + if (!(flags & FLAGS_LEFT)) { + while (l++ < width) { + out(' ', buffer, idx++, maxlen); + } + } + // char output + out((char)va_arg(va, int), buffer, idx++, maxlen); + // post padding + if (flags & FLAGS_LEFT) { + while (l++ < width) { + out(' ', buffer, idx++, maxlen); + } + } + format++; + break; + } + + case 's': + { + const char* p = va_arg(va, char*); + unsigned int l = _strnlen_s(p, precision ? precision : (size_t)-1); + // pre padding + if (flags & FLAGS_PRECISION) { + l = (l < precision ? l : precision); + } + if (!(flags & FLAGS_LEFT)) { + while (l++ < width) { + out(' ', buffer, idx++, maxlen); + } + } + // string output + while ((*p != 0) && (!(flags & FLAGS_PRECISION) || precision--)) { + out(*(p++), buffer, idx++, maxlen); + } + // post padding + if (flags & FLAGS_LEFT) { + while (l++ < width) { + out(' ', buffer, idx++, maxlen); + } + } + format++; + break; + } + + case 'p': + { + width = sizeof(void*) * 2U; + flags |= FLAGS_ZEROPAD | FLAGS_UPPERCASE; + idx = _ntoa_long(out, buffer, idx, maxlen, (unsigned long)((uintptr_t)va_arg(va, void*)), false, 16U, precision, width, flags); + format++; + break; + } + + case '%': + out('%', buffer, idx++, maxlen); + format++; + break; + + default: + out(*format, buffer, idx++, maxlen); + format++; + break; + } + } + + // termination + out((char)0, buffer, idx < maxlen ? idx : maxlen - 1U, maxlen); + + // return written chars without terminating \0 + return (int)idx; +} + +/////////////////////////////////////////////////////////////////////////////// + +int sprintf_(char* buffer, const char* format, ...) { + va_list va; + va_start(va, format); + const int ret = _vsnprintf(_out_buffer, buffer, (size_t)-1, format, va); + va_end(va); + return ret; +} + +int snprintf_(char* buffer, size_t count, const char* format, ...) { + va_list va; + va_start(va, format); + const int ret = _vsnprintf(_out_buffer, buffer, count, format, va); + va_end(va); + return ret; +} + +int vsnprintf_(char* buffer, size_t count, const char* format, va_list va) { + return _vsnprintf(_out_buffer, buffer, count, format, va); +} diff --git a/src/utils/printf.h b/src/utils/printf.h new file mode 100644 index 00000000..8e8d50ac --- /dev/null +++ b/src/utils/printf.h @@ -0,0 +1,83 @@ +/////////////////////////////////////////////////////////////////////////////// +// \author (c) Marco Paland (info@paland.com) +// 2014-2019, PALANDesign Hannover, Germany +// +// \license The MIT License (MIT) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// +// \brief Tiny printf, sprintf and snprintf implementation, optimized for speed on +// embedded systems with a very limited resources. +// Use this instead of bloated standard/newlib printf. +// These routines are thread safe and reentrant. +// +/////////////////////////////////////////////////////////////////////////////// + +#ifndef _PRINTF_H_ +#define _PRINTF_H_ + +#include +#include + + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Tiny sprintf implementation + * Due to security reasons (buffer overflow) YOU SHOULD CONSIDER USING (V)SNPRINTF INSTEAD! + * \param buffer A pointer to the buffer where to store the formatted string. MUST be big enough to store the output! + * \param format A string that specifies the format of the output + * \return The number of characters that are WRITTEN into the buffer, not counting the terminating null character + */ +#define sprintf sprintf_ +int sprintf_(char* buffer, const char* format, ...); + + +/** + * Tiny snprintf/vsnprintf implementation + * \param buffer A pointer to the buffer where to store the formatted string + * \param count The maximum number of characters to store in the buffer, including a terminating null character + * \param format A string that specifies the format of the output + * \param va A value identifying a variable arguments list + * \return The number of characters that are WRITTEN into the buffer, not counting the terminating null character + * If the formatted string is truncated the buffer size (count) is returned + */ +#define snprintf snprintf_ +#define vsnprintf vsnprintf_ +int snprintf_(char* buffer, size_t count, const char* format, ...); +int vsnprintf_(char* buffer, size_t count, const char* format, va_list va); + + +/** + * Tiny vprintf implementation + * \param format A string that specifies the format of the output + * \param va A value identifying a variable arguments list + * \return The number of characters that are WRITTEN into the buffer, not counting the terminating null character + */ +#define vprintf vprintf_ +int vprintf_(const char* format, va_list va); + +#ifdef __cplusplus +} +#endif + + +#endif // _PRINTF_H_ From d66dfb19001122df66db4a996ba234402cc96ba5 Mon Sep 17 00:00:00 2001 From: forntoh Date: Thu, 17 Oct 2024 02:17:52 +0200 Subject: [PATCH 2/6] Add clang-format off directive to printf files Prevent clang-format from formatting code in printf.c and printf.h by adding the directive "clang-format off" before includes. --- src/utils/printf.c | 2 ++ src/utils/printf.h | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/utils/printf.c b/src/utils/printf.c index c5b2dc84..9c86569b 100644 --- a/src/utils/printf.c +++ b/src/utils/printf.c @@ -30,6 +30,8 @@ // /////////////////////////////////////////////////////////////////////////////// +// clang-format off + #include #include diff --git a/src/utils/printf.h b/src/utils/printf.h index 8e8d50ac..d3fa8a90 100644 --- a/src/utils/printf.h +++ b/src/utils/printf.h @@ -29,6 +29,8 @@ // /////////////////////////////////////////////////////////////////////////////// +// clang-format off + #ifndef _PRINTF_H_ #define _PRINTF_H_ From 928f1656780101e6643b0bde8bbdbeb36b064ee0 Mon Sep 17 00:00:00 2001 From: forntoh Date: Thu, 17 Oct 2024 02:25:05 +0200 Subject: [PATCH 3/6] Updated docs --- docs/source/overview/items/range.rst | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/docs/source/overview/items/range.rst b/docs/source/overview/items/range.rst index 3758600f..4fdd7995 100644 --- a/docs/source/overview/items/range.rst +++ b/docs/source/overview/items/range.rst @@ -31,7 +31,7 @@ An integer range item can be created using the following syntax: When the ``Brightness`` menu item is selected, the user can adjust the brightness level within the range of 0 to 100. -You can also optionally specify a unit string to be displayed next to the value: +You can also optionally specify formatting for the value by providing a format string argument: .. code-block:: cpp @@ -40,10 +40,10 @@ You can also optionally specify a unit string to be displayed next to the value: // Callback function to handle value change // value is the selected value within the range // Do something with the selected value - }, (const char*) "dB"), + }, "%02ddB"), // Print the value with two digits and the unit string "dB" e.g. "50dB", "05dB" // ... More menu items -When the ``Volume`` menu item is selected, the user can adjust the volume level within the range of 0 to 100, with the unit string **"dB"** displayed next to the value. +When the ``Volume`` menu item is selected, the user can adjust the volume level within the range of 0 to 100, and the value will be displayed with the format string ``"%02ddB"``. .. image:: images/item-int-range.gif :width: 400px @@ -63,15 +63,12 @@ A float range item can be created using the following syntax: // Callback function to handle value change // value is the selected value within the range // Do something with the selected value - }, (const char*) " km", 0.5f), + }, "%.02f km", 0.5f), // Print the value with two decimal places and the unit string "km" e.g. "5.00 km", "5.50 km" // ... More menu items - The last argument is the step size of the range (the increment or decrement value when changing the value). - This argument also determines the number of decimal places to display. - For example, a step size of ``0.5`` will display values with one decimal place, - while a step size of ``0.01`` will display values with two decimal places. -When the ``Dist`` menu item is selected, the user can adjust the pressure within the range of 0.0 to 100.0, with the unit string **"km"** displayed next to the value. +When the ``Dist`` menu item is selected, the user can adjust the distance within the range of 0.0 to 100.0 .. image:: images/item-float-range.png :width: 400px From 359e312d99ca1c22d866f3e91e4dd832b8b598a8 Mon Sep 17 00:00:00 2001 From: forntoh Date: Thu, 17 Oct 2024 02:27:31 +0200 Subject: [PATCH 4/6] Update code formatting by adding clang-format directives. Added clang-format off directive to printf.c and printf.h files for consistent formatting. --- src/utils/printf.c | 4 ++-- src/utils/printf.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/utils/printf.c b/src/utils/printf.c index 9c86569b..2e2cc3eb 100644 --- a/src/utils/printf.c +++ b/src/utils/printf.c @@ -1,3 +1,5 @@ +// clang-format off + /////////////////////////////////////////////////////////////////////////////// // \author (c) Marco Paland (info@paland.com) // 2014-2019, PALANDesign Hannover, Germany @@ -30,8 +32,6 @@ // /////////////////////////////////////////////////////////////////////////////// -// clang-format off - #include #include diff --git a/src/utils/printf.h b/src/utils/printf.h index d3fa8a90..406774bd 100644 --- a/src/utils/printf.h +++ b/src/utils/printf.h @@ -1,3 +1,5 @@ +// clang-format off + /////////////////////////////////////////////////////////////////////////////// // \author (c) Marco Paland (info@paland.com) // 2014-2019, PALANDesign Hannover, Germany @@ -29,8 +31,6 @@ // /////////////////////////////////////////////////////////////////////////////// -// clang-format off - #ifndef _PRINTF_H_ #define _PRINTF_H_ From 9703a388e81bb8dafa7cf21721e7cec18d2cb79a Mon Sep 17 00:00:00 2001 From: forntoh Date: Thu, 17 Oct 2024 13:50:57 +0200 Subject: [PATCH 5/6] Update distance display format to include unit symbol --- examples/SimpleRotary/SimpleRotary.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/SimpleRotary/SimpleRotary.ino b/examples/SimpleRotary/SimpleRotary.ino index d342738f..0719311a 100644 --- a/examples/SimpleRotary/SimpleRotary.ino +++ b/examples/SimpleRotary/SimpleRotary.ino @@ -23,7 +23,7 @@ MENU_SCREEN(mainScreen, mainItems, ITEM_BASIC("Connect to WiFi"), ITEM_STRING_LIST("Color", colors, 8, colorsCallback), ITEM_BASIC("Blink SOS"), - ITEM_INT_RANGE("Dist", 0, 50, 0, callback, (const char*) "m"), + ITEM_INT_RANGE("Dist", 0, 50, 0, callback, "%dm"), ITEM_TOGGLE("Backlight", toggleBacklight), ITEM_BASIC("Blink random")); // clang-format on From 43ba40daaf92a6244797796051c34f0fccbf18fa Mon Sep 17 00:00:00 2001 From: forntoh Date: Thu, 17 Oct 2024 13:57:06 +0200 Subject: [PATCH 6/6] Use printf only in item float range --- src/ItemFloatRange.h | 1 + src/ItemRangeBase.h | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ItemFloatRange.h b/src/ItemFloatRange.h index cf3c2f29..027aa4d6 100644 --- a/src/ItemFloatRange.h +++ b/src/ItemFloatRange.h @@ -2,6 +2,7 @@ #define ItemFloatRange_H #include "ItemRangeBase.h" +#include /** * @brief Item that allows user to input float information within a range. diff --git a/src/ItemRangeBase.h b/src/ItemRangeBase.h index c520bbca..b6f6db44 100644 --- a/src/ItemRangeBase.h +++ b/src/ItemRangeBase.h @@ -3,7 +3,6 @@ #include "LcdMenu.h" #include "MenuItem.h" -#include #include template