diff --git a/.github/workflows/lvgl-allocator.yml b/.github/workflows/lvgl-allocator.yml new file mode 100644 index 00000000..a2962b04 --- /dev/null +++ b/.github/workflows/lvgl-allocator.yml @@ -0,0 +1,15 @@ +name: LVGL allocator checks +on: + push: + branches: [lvgl-textarea-buttonmatrix] + pull_request: + paths: [src/eez/core/alloc.h, tests/allocator-null.cpp] +permissions: + contents: read +jobs: + allocator: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - run: g++ -std=c++11 -fsanitize=address,undefined -Isrc tests/allocator-null.cpp -o /tmp/allocator-test + - run: /tmp/allocator-test diff --git a/src/eez/core/alloc.h b/src/eez/core/alloc.h index 8cb0229c..b041ba80 100644 --- a/src/eez/core/alloc.h +++ b/src/eez/core/alloc.h @@ -28,7 +28,7 @@ void free(void *ptr); template struct ObjectAllocator { static T *allocate(uint32_t id) { auto ptr = alloc(sizeof(T), id); - return new (ptr) T; + return ptr ? new (ptr) T : nullptr; } static void deallocate(T* ptr) { if (ptr) { diff --git a/src/eez/flow/components/lvgl.cpp b/src/eez/flow/components/lvgl.cpp index eea6c83c..758f8a55 100644 --- a/src/eez/flow/components/lvgl.cpp +++ b/src/eez/flow/components/lvgl.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -510,7 +511,12 @@ const char *getFullObjectName(FlowState *flowState, const char *objectName) { return; \ }\ propIndex++; \ - const char *NAME = NAME##Value.toString(0xe42b3ca2).getString(); + Value NAME##StringValue = NAME##Value.toString(0xe42b3ca2); \ + const char *NAME = NAME##StringValue.getString(); \ + if (!NAME) { \ + throwError(flowState, componentIndex, FlowError::PropertyInAction(#NAME, actionName, actionIndex)); \ + return; \ + } #define SCREEN_PROP(NAME) \ Value NAME##Value; \ @@ -1217,6 +1223,207 @@ ACTION_END //////////////////////////////////////////////////////////////////////////////// +ACTION_START(textareaGetText) + WIDGET_PROP(obj); + if (!lv_obj_check_type(obj, &lv_textarea_class)) { + throwError(flowState, componentIndex, FlowError::Plain("Expected a Textarea widget")); + return; + } + const char *text = lv_textarea_get_text(obj); + Value textCopy = Value::makeStringRef(text ? text : "", -1, 0x6f2c01a1); + if (!textCopy.isString()) { + throwError(flowState, componentIndex, FlowError::Plain("Failed to copy textarea text")); + return; + } + RESULT(result, textCopy); +ACTION_END + +ACTION_START(textareaSetText) + WIDGET_PROP(obj); + if (!lv_obj_check_type(obj, &lv_textarea_class)) { + throwError(flowState, componentIndex, FlowError::Plain("Expected a Textarea widget")); + return; + } + STR_PROP(text); + lv_textarea_set_text(obj, text); +ACTION_END + +ACTION_START(textareaSetOneLine) + WIDGET_PROP(obj); + if (!lv_obj_check_type(obj, &lv_textarea_class)) { + throwError(flowState, componentIndex, FlowError::Plain("Expected a Textarea widget")); + return; + } + BOOL_PROP(enabled); + lv_textarea_set_one_line(obj, enabled ? true : false); +ACTION_END + +ACTION_START(textareaSetPasswordMode) + WIDGET_PROP(obj); + if (!lv_obj_check_type(obj, &lv_textarea_class)) { + throwError(flowState, componentIndex, FlowError::Plain("Expected a Textarea widget")); + return; + } + BOOL_PROP(enabled); + lv_textarea_set_password_mode(obj, enabled ? true : false); +ACTION_END + +ACTION_START(textareaSetPasswordBullet) + WIDGET_PROP(obj); + if (!lv_obj_check_type(obj, &lv_textarea_class)) { + throwError(flowState, componentIndex, FlowError::Plain("Expected a Textarea widget")); + return; + } + STR_PROP(bullet); + lv_textarea_set_password_bullet(obj, bullet); +ACTION_END + +ACTION_START(textareaSetPlaceholderText) + WIDGET_PROP(obj); + if (!lv_obj_check_type(obj, &lv_textarea_class)) { + throwError(flowState, componentIndex, FlowError::Plain("Expected a Textarea widget")); + return; + } + STR_PROP(text); + lv_textarea_set_placeholder_text(obj, text); +ACTION_END + +ACTION_START(buttonMatrixGetSelectedButton) + WIDGET_PROP(obj); +#if LVGL_VERSION_MAJOR >= 9 + if (!lv_obj_check_type(obj, &lv_buttonmatrix_class)) { +#else + if (!lv_obj_check_type(obj, &lv_btnmatrix_class)) { +#endif + throwError(flowState, componentIndex, FlowError::Plain("Expected a ButtonMatrix widget")); + return; + } +#if LVGL_VERSION_MAJOR >= 9 + uint32_t selectedButton = lv_buttonmatrix_get_selected_button(obj); +#else + uint32_t selectedButton = (uint32_t)lv_btnmatrix_get_selected_btn(obj); +#endif + RESULT(result, Value((int)selectedButton, VALUE_TYPE_INT32)); +ACTION_END + +//////////////////////////////////////////////////////////////////////////////// + +ACTION_START(buttonMatrixGetButtonText) + WIDGET_PROP(obj); +#if LVGL_VERSION_MAJOR >= 9 + if (!lv_obj_check_type(obj, &lv_buttonmatrix_class)) { +#else + if (!lv_obj_check_type(obj, &lv_btnmatrix_class)) { +#endif + throwError(flowState, componentIndex, FlowError::Plain("Expected a ButtonMatrix widget")); + return; + } + INT32_PROP(buttonID); + const char *text = nullptr; + // Validate before narrowing to v8's uint16_t or calling its getter at count. + // Use public APIs; row markers do not contribute to button IDs. + if (buttonID >= 0 && buttonID < UINT16_MAX) { +#if LVGL_VERSION_MAJOR >= 9 + const char *const *map = lv_buttonmatrix_get_map(obj); +#else + const char *const *map = lv_btnmatrix_get_map(obj); +#endif + int32_t count = 0; + for (uint32_t i = 0; map && map[i] && map[i][0]; i++) { + if (strcmp(map[i], "\n") != 0) count++; + } + if (buttonID < count) { +#if LVGL_VERSION_MAJOR >= 9 + text = lv_buttonmatrix_get_button_text(obj, (uint32_t)buttonID); +#else + text = lv_btnmatrix_get_btn_text(obj, (uint16_t)buttonID); +#endif + } + } + Value textCopy = Value::makeStringRef(text ? text : "", -1, 0x6f2c01a2); + if (!textCopy.isString()) { + throwError(flowState, componentIndex, FlowError::Plain("Failed to copy button text")); + return; + } + RESULT(result, textCopy); +ACTION_END + +ACTION_START(buttonMatrixSetMap) + WIDGET_PROP(obj); +#if LVGL_VERSION_MAJOR >= 9 + if (!lv_obj_check_type(obj, &lv_buttonmatrix_class)) { +#else + if (!lv_obj_check_type(obj, &lv_btnmatrix_class)) { +#endif + throwError(flowState, componentIndex, FlowError::Plain("Expected a ButtonMatrix widget")); + return; + } + if (properties.count % 2 != 1 || properties.count / 2 >= UINT16_MAX) { + throwError(flowState, componentIndex, FlowError::Plain("Invalid ButtonMatrix map properties")); + return; + } + uint32_t count = properties.count / 2; + // Retain all evaluated strings until the map helper has copied them. + Value texts = Value::makeArrayRef(count, 0, 0x6f2c01b0); + if (!texts.isArray()) { + throwError(flowState, componentIndex, FlowError::Plain("Failed to allocate ButtonMatrix map")); + return; + } +#if LVGL_VERSION_MAJOR >= 9 + typedef lv_buttonmatrix_ctrl_t Ctrl; +#else + typedef lv_btnmatrix_ctrl_t Ctrl; +#endif + void *buffer = eez::alloc((count + 1) * sizeof(char *) + count * sizeof(Ctrl), 0x6f2c01b1); + if (!buffer) { + throwError(flowState, componentIndex, FlowError::Plain("Failed to allocate ButtonMatrix map")); + return; + } + const char **map = (const char **)buffer; + Ctrl *controls = (Ctrl *)(map + count + 1); + uint32_t buttonCount = 0; + bool valid = true; + for (uint32_t i = 0; i < count; i++) { + Value &text = texts.getArray()->values[i]; + Value control; + if (!evalExpression(flowState, componentIndex, properties[propIndex++]->evalInstructions, text, + FlowError::PropertyInAction("buttons.text", actionName, actionIndex)) || + !evalExpression(flowState, componentIndex, properties[propIndex++]->evalInstructions, control, + FlowError::PropertyInAction("buttons.width", actionName, actionIndex))) { + valid = false; + break; + } + // Match STR_PROP: expressions for string properties use Value::toString. + // Store the converted Value as well, not just a pointer to a temporary. + text = text.toString(0x6f2c01b2); + if (!text.isString() || !text.getString()) { + valid = false; + break; + } + int error; + int32_t ctrl = control.toInt32(&error); + if (error || ctrl < 0) { + valid = false; + break; + } + map[i] = text.getString()[0] ? text.getString() : " "; + if (ctrl == 0) { + if (strcmp(map[i], "\n")) { valid = false; break; } + } else { + if (!strcmp(map[i], "\n")) { valid = false; break; } + controls[buttonCount++] = (Ctrl)ctrl; + } + } + map[count] = NULL; + if (valid) valid = eez_flow_set_buttonmatrix_map(obj, map, count, buttonCount ? controls : NULL); + eez::free(buffer); + if (!valid) { + throwError(flowState, componentIndex, FlowError::Plain("Failed to set ButtonMatrix map")); + } +ACTION_END + +//////////////////////////////////////////////////////////////////////////////// + typedef void (*ActionType)(FlowState *flowState, unsigned componentIndex, const ListOfAssetsPtr &properties, uint32_t actionIndex); static ActionType actions[] = { @@ -1283,8 +1490,17 @@ static ActionType actions[] = { /* 60 */ &tabviewSetActiveTab, /* 61 */ &tabviewGetActiveTab, /* 62 */ &arcRotateObjToAngle, - /* 61 */ &objGetDisplayX, - /* 62 */ &objGetDisplayY + /* 63 */ &objGetDisplayX, + /* 64 */ &objGetDisplayY, + /* 65 */ &textareaGetText, + /* 66 */ &textareaSetText, + /* 67 */ &textareaSetOneLine, + /* 68 */ &textareaSetPasswordMode, + /* 69 */ &textareaSetPasswordBullet, + /* 70 */ &textareaSetPlaceholderText, + /* 71 */ &buttonMatrixGetSelectedButton, + /* 72 */ &buttonMatrixSetMap, + /* 73 */ &buttonMatrixGetButtonText }; //////////////////////////////////////////////////////////////////////////////// @@ -1304,6 +1520,22 @@ void executeLVGLApiComponent(FlowState *flowState, unsigned componentIndex) { for (uint32_t actionIndex = executionState ? executionState->actionIndex : 0; actionIndex < component->actions.count; actionIndex++) { // printf("actionIndex: %d\n", actionIndex + 1); auto actionType = (LVGLApiComponent_ActionType *)component->actions[actionIndex]; + if (actionType->action >= sizeof(actions) / sizeof(actions[0])) { + throwError(flowState, componentIndex, FlowError::Plain("Unknown LVGL action")); + return; + } + if (actionType->action >= 65 && actionType->action <= 71 && actionType->properties.count != 2) { + throwError(flowState, componentIndex, FlowError::Plain("Invalid LVGL action properties")); + return; + } + if (actionType->action == 72 && (actionType->properties.count % 2 != 1 || actionType->properties.count / 2 >= UINT16_MAX)) { + throwError(flowState, componentIndex, FlowError::Plain("Invalid ButtonMatrix map properties")); + return; + } + if (actionType->action == 73 && actionType->properties.count != 3) { + throwError(flowState, componentIndex, FlowError::Plain("Invalid LVGL action properties")); + return; + } (*actions[actionType->action])(flowState, componentIndex, actionType->properties, actionIndex); } diff --git a/src/eez/flow/lvgl_api.cpp b/src/eez/flow/lvgl_api.cpp index 2380678c..9e900399 100644 --- a/src/eez/flow/lvgl_api.cpp +++ b/src/eez/flow/lvgl_api.cpp @@ -441,6 +441,16 @@ EM_PORT_API(void) flowPropagateValueLVGLEvent(void *flowState, unsigned componen static char textValue[EEZ_LVGL_TEMP_STRING_BUFFER_SIZE]; +#include + +extern "C" bool eez_flow_set_buttonmatrix_text(lv_obj_t *obj, uint32_t mapIndex, const char *text) { + return eez_bm_update_bound_text(obj, mapIndex, text); +} + +extern "C" bool eez_flow_set_buttonmatrix_map(lv_obj_t *obj, const char *const *map, uint32_t count, const void *ctrl) { + return eez_bm_set_map(obj, map, count, (const eez_bm_ctrl_t *)ctrl); +} + extern "C" const char *_evalTextProperty(void *flowState, unsigned componentIndex, unsigned propertyIndex, const char *errorMessage, const char *file, int line) { eez::Value value; if (!eez::flow::evalProperty((eez::flow::FlowState *)flowState, componentIndex, propertyIndex, value, eez::flow::FlowError::Plain(errorMessage, file, line))) { diff --git a/src/eez/flow/lvgl_api.h b/src/eez/flow/lvgl_api.h index 754cabca..5ded88dc 100644 --- a/src/eez/flow/lvgl_api.h +++ b/src/eez/flow/lvgl_api.h @@ -79,6 +79,12 @@ void eez_flow_init_fonts(const ext_font_desc_t *fonts, size_t numFonts); void eez_flow_set_create_screen_func(void (*createScreenFunc)(int screenIndex)); void eez_flow_set_delete_screen_func(void (*deleteScreenFunc)(int screenIndex)); +// Updates original widget bindings until Set Map succeeds. +// Copies map text; caller retains ownership of all input buffers. +bool eez_flow_set_buttonmatrix_text(lv_obj_t *obj, uint32_t mapIndex, const char *text); +// Replaces the whole map and detaches its original widget text bindings. +bool eez_flow_set_buttonmatrix_map(lv_obj_t *obj, const char *const *map, uint32_t count, const void *ctrl); + void eez_flow_tick(); bool eez_flow_is_stopped(); diff --git a/src/eez/flow/lvgl_button_matrix.h b/src/eez/flow/lvgl_button_matrix.h new file mode 100644 index 00000000..ea02a52f --- /dev/null +++ b/src/eez/flow/lvgl_button_matrix.h @@ -0,0 +1,173 @@ +/* + * eez-framework + * MIT License + * Copyright 2026 Envox d.o.o. + * Shared by the Flow runtime and generated LVGL projects without Flow. + * Include after lvgl.h. All map strings are owned until replacement/deletion. + */ +#ifndef EEZ_LVGL_BUTTON_MATRIX_H +#define EEZ_LVGL_BUTTON_MATRIX_H + +#include +#include + +#if LVGL_VERSION_MAJOR >= 9 +#define EEZ_BM_ALLOC lv_malloc +#define EEZ_BM_FREE lv_free +#define EEZ_BM_GET_MAP lv_buttonmatrix_get_map +#define EEZ_BM_SET_MAP lv_buttonmatrix_set_map +#define EEZ_BM_SET_CTRL_MAP lv_buttonmatrix_set_ctrl_map +#define EEZ_BM_HAS_CTRL lv_buttonmatrix_has_button_ctrl +#define EEZ_BM_CLASS lv_buttonmatrix_class +typedef lv_buttonmatrix_ctrl_t eez_bm_ctrl_t; +#else +#define EEZ_BM_ALLOC lv_mem_alloc +#define EEZ_BM_FREE lv_mem_free +#define EEZ_BM_GET_MAP lv_btnmatrix_get_map +#define EEZ_BM_SET_MAP lv_btnmatrix_set_map +#define EEZ_BM_SET_CTRL_MAP lv_btnmatrix_set_ctrl_map +#define EEZ_BM_HAS_CTRL lv_btnmatrix_has_btn_ctrl +#define EEZ_BM_CLASS lv_btnmatrix_class +typedef lv_btnmatrix_ctrl_t eez_bm_ctrl_t; +#endif + +typedef struct { + const char **map; + bool bindingsDetached; +} eez_bm_state_t; + +static void eez_bm_delete(lv_event_t *event) { + /* A child's bubbled delete event must not release its parent's map. */ + if (lv_event_get_target(event) != lv_event_get_current_target(event)) return; + eez_bm_state_t *state = (eez_bm_state_t *)lv_event_get_user_data(event); + EEZ_BM_FREE((void *)state->map); + EEZ_BM_FREE(state); +} + +static eez_bm_state_t *eez_bm_state(lv_obj_t *obj) { +#if LVGL_VERSION_MAJOR >= 9 + for (uint32_t i = 0; i < lv_obj_get_event_count(obj); i++) { + lv_event_dsc_t *dsc = lv_obj_get_event_dsc(obj, i); + if (lv_event_dsc_get_cb(dsc) == eez_bm_delete) { + return (eez_bm_state_t *)lv_event_dsc_get_user_data(dsc); + } + } + return NULL; +#else + return (eez_bm_state_t *)lv_obj_get_event_user_data(obj, eez_bm_delete); +#endif +} + +/* Copies first, then publishes the new pointers. Never adopts caller memory. + * An optional text replacement preserves the map structure and control bits. + * Count is the number of map entries, including explicit newline entries. + */ +static bool eez_bm_replace(lv_obj_t *obj, const char *const *map, + uint32_t count, const eez_bm_ctrl_t *ctrl, int32_t textIndex, + const char *text) { + if (!obj || !lv_obj_check_type(obj, &EEZ_BM_CLASS) || !map || count >= UINT16_MAX) return false; + if (textIndex >= 0 && ((uint32_t)textIndex >= count || !text)) return false; + size_t bytes = ((size_t)count + 1) * sizeof(char *); + uint32_t buttonIndex = 0; + uint32_t rowUnits = 0; + uint32_t oldRowUnits = 0; + bool oldWidthsOverflow = false; + for (uint32_t i = 0; i < count; i++) { + const char *str = (int32_t)i == textIndex ? text : map[i]; + if (!str || !str[0]) return false; + if (!strcmp(str, "\n")) { + rowUnits = 0; + oldRowUnits = 0; + } else { + /* Older LVGL versions use a uint16_t row-width divisor. Also + * validate existing widths: set_map preserves them when the button + * count is unchanged, before set_ctrl_map applies the new widths. */ + uint32_t oldWidth = 0; + for (uint32_t bit = 1; bit <= 8; bit <<= 1) { + if (EEZ_BM_HAS_CTRL(obj, buttonIndex, (eez_bm_ctrl_t)bit)) oldWidth |= bit; + } + oldRowUnits += oldWidth ? oldWidth : 1; + if (oldRowUnits > UINT16_MAX) oldWidthsOverflow = true; + uint32_t width = ctrl ? ((uint32_t)ctrl[buttonIndex] & 15) : 1; + rowUnits += width ? width : 1; + if (rowUnits > UINT16_MAX) return false; + buttonIndex++; + } + size_t length = strlen(str) + 1; + if (length > SIZE_MAX - bytes) return false; + bytes += length; + } + if (oldWidthsOverflow) { + uint32_t oldButtonCount = 0; + const char *const *oldMap = EEZ_BM_GET_MAP(obj); + for (uint32_t i = 0; oldMap && i < UINT16_MAX && oldMap[i] && oldMap[i][0]; i++) { + if (strcmp(oldMap[i], "\n")) oldButtonCount++; + } + if (oldButtonCount == buttonIndex) return false; + } + const char **copy = (const char **)EEZ_BM_ALLOC(bytes); + if (!copy) return false; + char *dest = (char *)(copy + count + 1); + for (uint32_t i = 0; i < count; i++) { + const char *str = (int32_t)i == textIndex ? text : map[i]; + size_t length = strlen(str) + 1; + memcpy(dest, str, length); + copy[i] = dest; + dest += length; + } + copy[count] = NULL; + eez_bm_state_t *state = eez_bm_state(obj); + if (!state) { + state = (eez_bm_state_t *)EEZ_BM_ALLOC(sizeof(eez_bm_state_t)); + if (!state) { + EEZ_BM_FREE((void *)copy); + return false; + } + state->map = NULL; + state->bindingsDetached = false; + if (!lv_obj_add_event_cb(obj, eez_bm_delete, LV_EVENT_DELETE, state)) { + EEZ_BM_FREE(state); + EEZ_BM_FREE((void *)copy); + return false; + } + } + const char **oldMap = state->map; + state->map = copy; + EEZ_BM_SET_MAP(obj, copy); + if (ctrl && count) EEZ_BM_SET_CTRL_MAP(obj, ctrl); + EEZ_BM_FREE((void *)oldMap); + return true; +} + +/* A successful action replaces the complete map, including its text bindings. + * Failed replacements leave the current map and its bindings intact. */ +static bool eez_bm_set_map(lv_obj_t *obj, const char *const *map, + uint32_t count, const eez_bm_ctrl_t *ctrl) { + if (!eez_bm_replace(obj, map, count, ctrl, -1, NULL)) return false; + eez_bm_state(obj)->bindingsDetached = true; + return true; +} + +static bool eez_bm_set_text(lv_obj_t *obj, uint32_t mapIndex, const char *text) { + if (!obj || !lv_obj_check_type(obj, &EEZ_BM_CLASS) || !text) return false; + const char *const *map = EEZ_BM_GET_MAP(obj); + if (!map) return false; + uint32_t count = 0; + while (count < UINT16_MAX && map[count] && map[count][0]) count++; + if (count >= UINT16_MAX || mapIndex >= count || !strcmp(map[mapIndex], "\n")) return false; + /* Empty display values keep the existing button, as literal empty text does. */ + if (!text[0]) text = " "; + /* Row boundaries belong to New line, never to an expression result. */ + if (!strcmp(text, "\n")) return false; + if (!strcmp(map[mapIndex], text)) return true; + return eez_bm_replace(obj, map, count, NULL, (int32_t)mapIndex, text); +} + +static bool eez_bm_update_bound_text(lv_obj_t *obj, uint32_t mapIndex, const char *text) { + if (!obj || !lv_obj_check_type(obj, &EEZ_BM_CLASS) || !text) return false; + eez_bm_state_t *state = eez_bm_state(obj); + if (state && state->bindingsDetached) return true; + return eez_bm_set_text(obj, mapIndex, text); +} + +#endif diff --git a/tests/allocator-null.cpp b/tests/allocator-null.cpp new file mode 100644 index 00000000..355b0871 --- /dev/null +++ b/tests/allocator-null.cpp @@ -0,0 +1,22 @@ +#include +#include +#include + +static bool failAllocation; +static int constructed; +namespace eez { struct Object { Object() { constructed++; } }; } +using Object = eez::Object; +namespace eez { +void *alloc(size_t bytes, uint32_t) { return failAllocation ? nullptr : std::malloc(bytes); } +void free(void *ptr) { std::free(ptr); } +} +int main() { + failAllocation = true; + assert(eez::ObjectAllocator::allocate(0) == nullptr); + assert(constructed == 0); + failAllocation = false; + auto object = eez::ObjectAllocator::allocate(0); + assert(object && constructed == 1); + eez::ObjectAllocator::deallocate(object); + eez::ObjectAllocator::deallocate(nullptr); +}