From 333a85d12adba616a527ec4ac5773900d6199e38 Mon Sep 17 00:00:00 2001 From: philmoz Date: Tue, 25 Aug 2026 12:06:53 +1000 Subject: [PATCH 01/18] fix(color): manage models buttons incorrect after renaming or removing labels (#7705) (cherry picked from commit 7f6bbd1fb97bf707552877532ecfc7e3048a1e67) --- radio/src/gui/colorlcd/model/model_select.cpp | 9 ++++++++- radio/src/storage/modelslist.h | 8 -------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/radio/src/gui/colorlcd/model/model_select.cpp b/radio/src/gui/colorlcd/model/model_select.cpp index ab16bf893a0..fcab7dabf46 100644 --- a/radio/src/gui/colorlcd/model/model_select.cpp +++ b/radio/src/gui/colorlcd/model/model_select.cpp @@ -281,10 +281,15 @@ class ModelsPageBody : public Window } } - void reload() + void clearButtons() { modelButtons.clear(); clear(); + } + + void reload() + { + clearButtons(); update(); } @@ -824,6 +829,7 @@ void ModelLabelsWindow::buildBody(Window *window) }); auto labels = getLabels(); lblselector->setNames(labels); + mdlselector->clearButtons(); updateFilteredLabels(modelslabels.filteredLabels(), false); } }); @@ -848,6 +854,7 @@ void ModelLabelsWindow::buildBody(Window *window) lblselector->setSelected(newset); if (g_eeGeneral.labelSingleSelect && selected == lblselector->getActiveItem()) lblselector->setActiveItem(-1); + mdlselector->clearButtons(); updateFilteredLabels(newset); }); return 0; diff --git a/radio/src/storage/modelslist.h b/radio/src/storage/modelslist.h index b26575753f2..4e5285fc4b2 100644 --- a/radio/src/storage/modelslist.h +++ b/radio/src/storage/modelslist.h @@ -87,12 +87,6 @@ class ModelCell bool fetchRfData(); }; -typedef struct { - std::string icon; - // Anything else? -} SLabelDetail; - -typedef std::vector> ModelLabelsVector; typedef std::vector LabelsVector; typedef std::vector ModelsVector; typedef enum { @@ -250,7 +244,5 @@ class ModelsList : public ModelsVector bool loadYamlDirScanner(); }; -ModelLabelsVector getUniqueLabels(); - extern ModelsList modelslist; extern ModelMap modelslabels; From 1684aee1443541425b8cf8ee6aca0e43d7691314 Mon Sep 17 00:00:00 2001 From: philmoz Date: Tue, 25 Aug 2026 12:37:30 +1000 Subject: [PATCH 02/18] fix(color): number edit keyboard is not sized to suit display, cursor position is wrong (#7670) (cherry picked from commit 298e6e6d324aeaa56ed21e778fbb4db13aee3f2e) --- .../src/gui/colorlcd/libui/keyboard_number.cpp | 18 +++++++++--------- radio/src/gui/colorlcd/libui/numberedit.cpp | 5 ----- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/radio/src/gui/colorlcd/libui/keyboard_number.cpp b/radio/src/gui/colorlcd/libui/keyboard_number.cpp index de9efde635c..4fe6b59a453 100644 --- a/radio/src/gui/colorlcd/libui/keyboard_number.cpp +++ b/radio/src/gui/colorlcd/libui/keyboard_number.cpp @@ -20,7 +20,7 @@ #include "numberedit.h" -constexpr coord_t KEYBOARD_HEIGHT = 90; +LAYOUT_VAL_SCALED(KEYBOARD_HEIGHT, 90); NumberKeyboard* NumberKeyboard::_instance = nullptr; static const char* const number_kb_map[] = {"<<", "-", "+", ">>", "\n", @@ -71,42 +71,42 @@ void NumberKeyboard::handleEvent(const char* btn) void NumberKeyboard::decLarge() { - ((NumberEdit*)field)->onEvent(EVT_VIRTUAL_KEY_BACKWARD); + field->onEvent(EVT_VIRTUAL_KEY_BACKWARD); } void NumberKeyboard::decSmall() { - ((NumberEdit*)field)->onEvent(EVT_VIRTUAL_KEY_MINUS); + field->onEvent(EVT_VIRTUAL_KEY_MINUS); } void NumberKeyboard::incSmall() { - ((NumberEdit*)field)->onEvent(EVT_VIRTUAL_KEY_PLUS); + field->onEvent(EVT_VIRTUAL_KEY_PLUS); } void NumberKeyboard::incLarge() { - ((NumberEdit*)field)->onEvent(EVT_VIRTUAL_KEY_FORWARD); + field->onEvent(EVT_VIRTUAL_KEY_FORWARD); } void NumberKeyboard::setMIN() { - ((NumberEdit*)field)->onEvent(EVT_VIRTUAL_KEY_MIN); + field->onEvent(EVT_VIRTUAL_KEY_MIN); } void NumberKeyboard::setMAX() { - ((NumberEdit*)field)->onEvent(EVT_VIRTUAL_KEY_MAX); + field->onEvent(EVT_VIRTUAL_KEY_MAX); } void NumberKeyboard::setDEF() { - ((NumberEdit*)field)->onEvent(EVT_VIRTUAL_KEY_DEFAULT); + field->onEvent(EVT_VIRTUAL_KEY_DEFAULT); } void NumberKeyboard::changeSign() { - ((NumberEdit*)field)->onEvent(EVT_VIRTUAL_KEY_SIGN); + field->onEvent(EVT_VIRTUAL_KEY_SIGN); } #if defined(HARDWARE_KEYS) diff --git a/radio/src/gui/colorlcd/libui/numberedit.cpp b/radio/src/gui/colorlcd/libui/numberedit.cpp index ff8233820f3..d91e8098e8b 100644 --- a/radio/src/gui/colorlcd/libui/numberedit.cpp +++ b/radio/src/gui/colorlcd/libui/numberedit.cpp @@ -32,11 +32,6 @@ class NumberArea : public FormField { lv_obj_clear_flag(lvobj, LV_OBJ_FLAG_CLICK_FOCUSABLE); - if (parent->getTextFlags() & CENTERED) - etx_obj_add_style(lvobj, styles->text_align_center, LV_PART_MAIN); - else - etx_obj_add_style(lvobj, styles->text_align_right, LV_PART_MAIN); - // Allow encoder acceleration lv_obj_add_flag(lvobj, LV_OBJ_FLAG_ENCODER_ACCEL); From 8b4f781efad2965d4498872c8bc247e26e3b1586 Mon Sep 17 00:00:00 2001 From: philmoz Date: Wed, 26 Aug 2026 09:16:16 +1000 Subject: [PATCH 03/18] fix(color): in manage models, long press on selected model button clears checked state (#7707) (cherry picked from commit 94d5a89e338b18a11459fcca9fe612fb0fa309f4) --- radio/src/gui/colorlcd/model/model_select.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/radio/src/gui/colorlcd/model/model_select.cpp b/radio/src/gui/colorlcd/model/model_select.cpp index fcab7dabf46..2f1ac4c448c 100644 --- a/radio/src/gui/colorlcd/model/model_select.cpp +++ b/radio/src/gui/colorlcd/model/model_select.cpp @@ -137,7 +137,6 @@ class ModelButton : public Button return true; } } - showNoImgMsg(); } return false; @@ -266,10 +265,12 @@ class ModelsPageBody : public Window // Long Press Handler for Models button->setLongPressHandler([=]() -> uint8_t { - button->setFocused(); - focusedModel = model; + if (model != focusedModel) { + button->setFocused(); + focusedModel = model; + } openMenu(); - return 0; + return model == modelslist.getCurrentModel(); }); } From 5501690b5b12bb80547b4ee8ffaacc4466e0fcd4 Mon Sep 17 00:00:00 2001 From: philmoz Date: Thu, 27 Aug 2026 20:33:19 +1000 Subject: [PATCH 04/18] fix(color): UI issues adding and removing labels (#7715) (cherry picked from commit 4f6e0223c94676de07e115c41cb2fe749a905064) --- radio/src/gui/colorlcd/model/model_select.cpp | 14 ++++++-------- radio/src/storage/modelslist.cpp | 2 +- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/radio/src/gui/colorlcd/model/model_select.cpp b/radio/src/gui/colorlcd/model/model_select.cpp index 2f1ac4c448c..0b7ebcdc115 100644 --- a/radio/src/gui/colorlcd/model/model_select.cpp +++ b/radio/src/gui/colorlcd/model/model_select.cpp @@ -671,14 +671,8 @@ void ModelLabelsWindow::newLabel() new LabelDialog(tmpLabel, LABEL_LENGTH, STR_ENTER_LABEL, [=](std::string label) { int newlabindex = modelslabels.addLabel(label); if (newlabindex >= 0) { - std::set newset; - newset.insert(newlabindex); auto labels = getLabels(); lblselector->setNames(labels); - lblselector->setSelected(newset); - if (g_eeGeneral.labelSingleSelect) - lblselector->setActiveItem(newlabindex); - updateFilteredLabels(newset); } }); } @@ -853,8 +847,12 @@ void ModelLabelsWindow::buildBody(Window *window) std::set newset; lblselector->setNames(labels); lblselector->setSelected(newset); - if (g_eeGeneral.labelSingleSelect && selected == lblselector->getActiveItem()) - lblselector->setActiveItem(-1); + if (g_eeGeneral.labelSingleSelect) { + if (selected == lblselector->getActiveItem()) + lblselector->setActiveItem(-1); + else + newset.insert(lblselector->getActiveItem()); + } mdlselector->clearButtons(); updateFilteredLabels(newset); }); diff --git a/radio/src/storage/modelslist.cpp b/radio/src/storage/modelslist.cpp index 6feee9c8ce8..3046d7d62da 100644 --- a/radio/src/storage/modelslist.cpp +++ b/radio/src/storage/modelslist.cpp @@ -357,8 +357,8 @@ int ModelMap::addLabel(std::string lbl) if (lbl == STR_UNLABELEDMODEL) return -1; // Limit maximum label length, TODO... Truncate UTF8 Properly - lbl = lbl.substr(0, LABEL_LENGTH); removeYAMLChars(lbl); + lbl = lbl.substr(0, LABEL_LENGTH); if (lbl.size() == 0) return -1; // Add a new label if it doesn't already exist in the list From d76e7c7c2487017cbbbf0051f2d4f6ca0bc80f3f Mon Sep 17 00:00:00 2001 From: Giuseppe Rojas Date: Sun, 30 Aug 2026 03:48:47 -0500 Subject: [PATCH 05/18] fix(lua): compare float with int strictly in equality (#7611) Co-authored-by: Peter Feerick Co-authored-by: Claude Sonnet 5 (cherry picked from commit 34afae5a26e5db240aaf6b22435f2f1734db1839) --- radio/src/tests/lua.cpp | 40 ++++++++++++++++++++++++++++++ radio/src/thirdparty/Lua/src/lvm.c | 2 +- radio/src/thirdparty/Lua/src/lvm.h | 9 +++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/radio/src/tests/lua.cpp b/radio/src/tests/lua.cpp index 2b7accb094c..830f053def2 100644 --- a/radio/src/tests/lua.cpp +++ b/radio/src/tests/lua.cpp @@ -197,6 +197,46 @@ TEST(Lua, Switches) #endif } +TEST(Lua, testFloatIntegerEquality) +{ + // 0.5 is not an integer, so it must not equal 0 (regression #7587) + // both directions asserted explicitly so the intent is obvious at a glance + luaExecStr("if 0.5 == 0 then error('0.5 == 0') end"); + luaExecStr("if not (0.5 ~= 0) then error('0.5 ~= 0') end"); + luaExecStr("if 0.50 == 0 then error('0.50 == 0') end"); + luaExecStr("if not (0.50 ~= 0) then error('0.50 ~= 0') end"); + luaExecStr("if 0.50 == 0.0 then error('0.50 == 0.0') end"); + // ... even when the value comes from a variable, as in the reported issue + luaExecStr("local v = 0.50; if v == 0 then error('v == 0') end"); + luaExecStr("local v = 0.50; if not (v ~= 0) then error('v ~= 0') end"); + // negative non-integral floats must not equal integers either + luaExecStr("if -0.5 == 0 then error('-0.5 == 0') end"); + luaExecStr("if not (-0.5 ~= 0) then error('-0.5 ~= 0') end"); + luaExecStr("if not (-0.5 < 0) then error('-0.5 < 0') end"); + luaExecStr("if -0.5 > 0 then error('-0.5 > 0') end"); + // integral floats still compare equal to their integer counterpart + luaExecStr("if 1.0 ~= 1 then error('1.0 ~= 1') end"); + luaExecStr("if 0.0 ~= 0 then error('0.0 ~= 0') end"); + luaExecStr("if -1.0 ~= -1 then error('-1.0 ~= -1') end"); + // order comparisons on the same values must remain consistent + luaExecStr("if not (0.5 >= 0) then error('0.5 >= 0') end"); + luaExecStr("if 0.5 <= 0 then error('0.5 <= 0') end"); + luaExecStr("if not (0.5 > 0) then error('0.5 > 0') end"); + luaExecStr("if 0.5 < 0 then error('0.5 < 0') end"); + luaExecStr("if not (0.50 >= 0) then error('0.50 >= 0') end"); + luaExecStr("if 0.50 <= 0 then error('0.50 <= 0') end"); + luaExecStr("if not (1.0 >= 1) then error('1.0 >= 1') end"); + luaExecStr("if not (1.0 <= 1) then error('1.0 <= 1') end"); + luaExecStr("if not (0.0 >= 0) then error('0.0 >= 0') end"); + luaExecStr("if not (0.0 <= 0) then error('0.0 <= 0') end"); + // mixed arithmetic must still yield floats; the fix only affects equality + luaExecStr("if math.type(0.5 + 0) ~= 'float' then error('0.5 + 0') end"); + luaExecStr("if math.type(1.0 + 1) ~= 'float' then error('1.0 + 1') end"); + luaExecStr("if math.type(1 / 2) ~= 'float' then error('1 / 2') end"); + luaExecStr("if math.type(7.5 % 2) ~= 'float' then error('7.5 % 2') end"); + luaExecStr("if math.type(0.5 * 2) ~= 'float' then error('0.5 * 2') end"); +} + TEST(Lua, testLegacyNames) { MODEL_RESET(); diff --git a/radio/src/thirdparty/Lua/src/lvm.c b/radio/src/thirdparty/Lua/src/lvm.c index c186e5e7c26..7a2d5a6ed39 100644 --- a/radio/src/thirdparty/Lua/src/lvm.c +++ b/radio/src/thirdparty/Lua/src/lvm.c @@ -401,7 +401,7 @@ int luaV_equalobj (lua_State *L, const TValue *t1, const TValue *t2) { return 0; /* only numbers can be equal with different variants */ else { /* two numbers with different variants */ lua_Integer i1, i2; /* compare them as integers */ - return (tointeger(t1, &i1) && tointeger(t2, &i2) && i1 == i2); + return (tointegerexact(t1, &i1) && tointegerexact(t2, &i2) && i1 == i2); } } /* values have same type and same variant */ diff --git a/radio/src/thirdparty/Lua/src/lvm.h b/radio/src/thirdparty/Lua/src/lvm.h index a8f954f04ce..c066bc07f28 100644 --- a/radio/src/thirdparty/Lua/src/lvm.h +++ b/radio/src/thirdparty/Lua/src/lvm.h @@ -43,6 +43,15 @@ #define tointeger(o,i) \ (ttisinteger(o) ? (*(i) = ivalue(o), 1) : luaV_tointeger(o,i,LUA_FLOORN2I)) +/* non-soft conversion: only integral values are converted. +** This is the strict semantic used by equality ('==' and '~='), where a +** non-integral float such as 0.5 must not compare equal to an integer. +** 'tointeger' (soft, LUA_FLOORN2I) is intentionally kept for API argument +** coercion so that EdgeTX API functions accept unrounded floats; do not +** switch this macro back to the soft conversion. */ +#define tointegerexact(o,i) \ + (ttisinteger(o) ? (*(i) = ivalue(o), 1) : luaV_tointeger(o,i,0)) + #define intop(op,v1,v2) l_castU2S(l_castS2U(v1) op l_castS2U(v2)) #define luaV_rawequalobj(t1,t2) luaV_equalobj(NULL,t1,t2) From 76f0b77dd1e4481820051b020c8da9c2a13e820f Mon Sep 17 00:00:00 2001 From: fpenard <89727717+fpenard@users.noreply.github.com> Date: Sun, 30 Aug 2026 10:50:08 +0200 Subject: [PATCH 06/18] fix(taranis): restore volume cli for I2C audio targets (#6543) (cherry picked from commit bfe6cd1e7e1d2c6a1cd12c4b20bb623c1263aa4c) --- radio/src/cli.cpp | 5 +++++ radio/src/targets/taranis/hal.h | 3 +++ radio/src/targets/taranis/volume_i2c.cpp | 5 +++++ 3 files changed, 13 insertions(+) diff --git a/radio/src/cli.cpp b/radio/src/cli.cpp index 6deced89485..38d70f21000 100644 --- a/radio/src/cli.cpp +++ b/radio/src/cli.cpp @@ -1465,6 +1465,11 @@ int cliDisplay(const char ** argv) gettime(&utm); cliSerialPrint("rtc = %4d-%02d-%02d %02d:%02d:%02d.%02d0", utm.tm_year+TM_YEAR_BASE, utm.tm_mon+1, utm.tm_mday, utm.tm_hour, utm.tm_min, utm.tm_sec, g_ms100); } +#if defined(VOLUME_I2C_ADDRESS) + else if (!strcmp(argv[1], "volume")) { + cliSerialPrint("volume = %d", getVolume()); + } +#endif else if (!strcmp(argv[1], "uid")) { char str[LEN_CPU_UID+1]; getCPUUniqueID(str); diff --git a/radio/src/targets/taranis/hal.h b/radio/src/targets/taranis/hal.h index 0a155c6d6c3..64bea6a8468 100644 --- a/radio/src/targets/taranis/hal.h +++ b/radio/src/targets/taranis/hal.h @@ -2677,6 +2677,9 @@ #if !defined(SOFTWARE_VOLUME) #define VOLUME_I2C_ADDRESS 0x2E #define VOLUME_I2C_BUS I2C_Bus_1 + + #include + int32_t getVolume(); #endif #define I2C_B1_CLK_RATE 400000 diff --git a/radio/src/targets/taranis/volume_i2c.cpp b/radio/src/targets/taranis/volume_i2c.cpp index d1ed2365cf0..48b5c942e15 100644 --- a/radio/src/targets/taranis/volume_i2c.cpp +++ b/radio/src/targets/taranis/volume_i2c.cpp @@ -57,4 +57,9 @@ void audioSetVolume(uint8_t volume) write_i2c_volume(volumeScale[volume]); } +int32_t getVolume() +{ + return read_i2c_volume(); +} + #endif From 898d6e3166512e7f38143f978582ada38dc7054b Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 30 Aug 2026 10:50:57 +0200 Subject: [PATCH 07/18] chore(elrs): allow future use of unused bits in the "Arming status byte" (#7022) Co-authored-by: Peter Feerick Co-authored-by: Claude Sonnet 5 (cherry picked from commit 076d80ed9e8a9cb219f8d10919130752aa4bc0f2) --- radio/src/pulses/crossfire.cpp | 41 ++++++++++++------- radio/src/tests/crossfire.cpp | 74 +++++++++++++++++++++++++++++++++- 2 files changed, 100 insertions(+), 15 deletions(-) diff --git a/radio/src/pulses/crossfire.cpp b/radio/src/pulses/crossfire.cpp index 0e4c30e2681..58c2d71566b 100644 --- a/radio/src/pulses/crossfire.cpp +++ b/radio/src/pulses/crossfire.cpp @@ -96,20 +96,20 @@ uint8_t createCrossfireModelIDFrame(uint8_t moduleIdx, uint8_t * frame) uint8_t createCrossfireChannelsFrame(uint8_t moduleIdx, uint8_t * frame, int16_t * pulses) { // - // sends channel data and also communicates commanded armed status in arming mode Switch. - // frame len 24 -> arming mode CH5: module will use channel 5 - // frame len 25 -> arming mode Switch: send commanded armed status in extra byte after channel data - // - ModuleData *md = &g_model.moduleData[moduleIdx]; - - uint8_t armingMode = md->crsf.crsfArmingMode; // 0 = Channel mode, 1 = Switch mode - uint8_t lenAdjust = (armingMode == ARMING_MODE_SWITCH) ? 1 : 0; - + // sends channel data and also communicates status information in status byte: + // - arming status in Switch mode (bit 0) + // - arming mode Switch or CH5 (bit 1) + // - bits 2-7 spare + // uint8_t * buf = frame; *buf++ = MODULE_ADDRESS; - *buf++ = 24 + lenAdjust; // 1(ID) + 22(channel data) + (+1 extra byte if Switch mode) + 1(CRC) + *buf++ = 25; // 1(ID) + 22(channel data) + 1(extra status byte) + 1(CRC) uint8_t * crc_start = buf; *buf++ = CHANNELS_ID; + + // + // assemble channel data + // uint32_t bits = 0; uint8_t bitsavailable = 0; for (int i=0; icrsf.crsfArmingMode == ARMING_MODE_SWITCH) { swsrc_t sw = md->crsf.crsfArmingTrigger; - *buf++ = (sw != SWSRC_NONE) && getSwitch(sw, 0); // commanded armed status in Switch mode + *buf = (sw != SWSRC_NONE) && getSwitch(sw, 0); // commanded armed status in Switch mode + } else { + *buf = 0x02; // flag arming mode CH5 } + + buf++; - *buf++ = crc8(crc_start, 23 + lenAdjust); + // + // add crc + // + *buf++ = crc8(crc_start, 24); + return buf - frame; } diff --git a/radio/src/tests/crossfire.cpp b/radio/src/tests/crossfire.cpp index 18350b7f21d..b007b1bb391 100644 --- a/radio/src/tests/crossfire.cpp +++ b/radio/src/tests/crossfire.cpp @@ -22,12 +22,20 @@ #include "gtest/gtest.h" #include "gtests.h" #include "telemetry/telemetry.h" +#include "telemetry/crossfire.h" +#include "crc.h" #if defined(CROSSFIRE) uint8_t createCrossfireChannelsFrame(uint8_t moduleIdx, uint8_t * frame, int16_t * pulses); + +// Spec-defined part of the frame (0x16 RC Channels Packed): sync byte, type, +// 16 x 11-bit channel packing. Expected bytes computed independently, not +// derived from createCrossfireChannelsFrame() itself. TEST(Crossfire, createCrossfireChannelsFrame) { + MODEL_RESET(); + int16_t pulsesStart[MAX_TRAINER_CHANNELS]; uint8_t crossfire[CROSSFIRE_FRAME_MAXLEN]; @@ -38,7 +46,71 @@ TEST(Crossfire, createCrossfireChannelsFrame) createCrossfireChannelsFrame(EXTERNAL_MODULE, crossfire, pulsesStart); - // TODO check + ASSERT_EQ(crossfire[0], MODULE_ADDRESS); + ASSERT_EQ(crossfire[2], CHANNELS_ID); + + const uint8_t expectedChannelData[22] = { + 0xAD, 0xA0, 0x88, 0x5E, 0xC0, 0x73, 0xA4, 0x56, 0x51, 0x4C, 0x6F, + 0xE0, 0x33, 0x22, 0x2B, 0x27, 0x9A, 0x57, 0xF0, 0x1A, 0x99, 0xD5 + }; + ASSERT_EQ(memcmp(&crossfire[3], expectedChannelData, sizeof(expectedChannelData)), 0); +} + +// Status byte after the 0x16 payload is an ExpressLRS extension, not TBS CRSF +// spec (semantics per ExpressLRS's TXModuleEndpoint.cpp / crsf_protocol.h). +// Frame is always 25 bytes (1 ID + 22 channel data + 1 status + 1 CRC); +// bit 0 = commanded armed status (Switch mode only), bit 1 = arming mode is CH5. +TEST(Crossfire, ExpressLRSArmingExtension_CH5Mode) +{ + MODEL_RESET(); + + int16_t pulsesStart[MAX_TRAINER_CHANNELS]; + uint8_t crossfire[CROSSFIRE_FRAME_MAXLEN]; + + memset(crossfire, 0, sizeof(crossfire)); + for (int i=0; i not armed, bit 1 clear (Switch mode) + + uint8_t crc = crc8(&crossfire[2], 24); + ASSERT_EQ(crossfire[26], crc); } TEST(Crossfire, crc8) From 6ea50f5f924426bb4b88c2c4222563269bd29d12 Mon Sep 17 00:00:00 2001 From: philmoz Date: Sun, 13 Sep 2026 13:37:36 +1000 Subject: [PATCH 08/18] fix(fw): trim not applied when mix source is inverted (#7779) Co-authored-by: philmoz (cherry picked from commit b0eb2332fc352ff8be1d8df40f538708566b158d) --- radio/src/mixer.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/radio/src/mixer.cpp b/radio/src/mixer.cpp index 3a7b22f97ba..10fa1af440a 100644 --- a/radio/src/mixer.cpp +++ b/radio/src/mixer.cpp @@ -705,13 +705,13 @@ int getSourceTrimOrigin(int source) int getSourceTrimValue(int source, int stickValue=0) { - auto origin = getSourceTrimOrigin(source); + int rv = 0; + auto origin = getSourceTrimOrigin(abs(source)); if (origin >= 0) { - return getStickTrimValue(origin, stickValue); - } - else { - return 0; + rv = getStickTrimValue(origin, stickValue); + if (source < 0) rv = -rv; } + return rv; } constexpr bitfield_channels_t all_channels_dirty = (bitfield_channels_t)-1; @@ -967,7 +967,7 @@ void evalFlightModeMixes(uint8_t mode, uint8_t tick10ms) if (applyOffsetAndCurve) { bool applyTrims = !(mode & e_perout_mode_notrims); if (!applyTrims && g_model.thrTrim) { - auto origin = getSourceTrimOrigin(srcRaw); + auto origin = getSourceTrimOrigin(srcRawAbs); if (origin == g_model.getThrottleStickTrimSource() - MIXSRC_FIRST_TRIM) { applyTrims = true; } From 17cb22fc26a6aeabe0c5dd2e023aef461a72b170 Mon Sep 17 00:00:00 2001 From: philmoz Date: Sun, 13 Sep 2026 13:44:26 +1000 Subject: [PATCH 09/18] fix(bw): potential Lua stack overflow in lcd.drawCombobox (#7775) (cherry picked from commit 9bab4ec0cfa129de511255f016b4122416a7df32) --- radio/src/lua/api_stdlcd.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/radio/src/lua/api_stdlcd.cpp b/radio/src/lua/api_stdlcd.cpp index 6095bca8fd4..1fa7c46e63e 100644 --- a/radio/src/lua/api_stdlcd.cpp +++ b/radio/src/lua/api_stdlcd.cpp @@ -595,6 +595,7 @@ static int luaLcdDrawCombobox(lua_State *L) lua_rawgeti(L, 4, i+1); const char * item = luaL_checkstring(L, -1); lcdDrawText(x+2, y+2+9*i, item, 0); + lua_pop(L, 1); } lcdDrawFilledRect(x+1, y+1+9*idx, w-11, 9); lcdDrawFilledRect(x+w-10, y, 10, 11, SOLID, ERASE); @@ -606,6 +607,7 @@ static int luaLcdDrawCombobox(lua_State *L) lua_rawgeti(L, 4, idx+1); const char * item = luaL_checkstring(L, -1); lcdDrawText(x+2, y+2, item, INVERS); + lua_pop(L, 1); } else { lcdDrawFilledRect(x, y, w, 11, SOLID, ERASE); @@ -614,6 +616,7 @@ static int luaLcdDrawCombobox(lua_State *L) lua_rawgeti(L, 4, idx+1); const char * item = luaL_checkstring(L, -1); lcdDrawText(x+2, y+2, item, 0); + lua_pop(L, 1); } lcdDrawSolidHorizontalLine(x+w-8, y+3, 6); From 6dde229bc9d0b6ef526355beaa7867cee4aea1bc Mon Sep 17 00:00:00 2001 From: philmoz Date: Wed, 16 Sep 2026 15:12:41 +1000 Subject: [PATCH 10/18] fix(firmware): audio logic does not use all of the available buffers (#7689) Co-authored-by: Peter Feerick <5500713+pfeerick@users.noreply.github.com> Co-authored-by: Claude Sonnet 5 2.11 port: 2.11 already fixed the AudioBufferFifo full/empty ambiguity its own way in #7472 (a0cd30fab8d4dfb152eb87d3978ba08007973a3d, a volatile bufferFull flag) plus #7685 (53199fe3e19098efe0e4bd35d617c2dbcb96d2fa, used() when full). That flag is written by both the producer (audioPushBuffer) and the consumer (freeNextFilledBuffer), so an interrupt between the index update and the flag update can leave it wrong. This port replaces it with upstream's free-running counters, so each side only writes its own index, and also brings in the AudioFragmentFifo fix (all AUDIO_QUEUE_LENGTH slots usable), the static_asserts and the CLI output. Conflicts in the AudioBufferFifo class were resolved to the upstream version, and the bufferFull member and initialiser were removed. The cli.cpp debug output prints full() instead of bufferFull, as upstream does. The DAC underrun handling from #7472 is kept unchanged. filledAtleast() is kept, although upstream removes it as unused. 2.11's simulator audio callback (targets/simu/simpgmspace.cpp fillAudioBuffer) still uses it as a pre-buffer threshold. used() keeps returning AUDIO_BUFFER_COUNT when full, as it did after #7685. (cherry picked from commit e2968ac864eec52bd6b424cc503d75b988899fb3) Co-Authored-By: Claude Opus 5.5 --- radio/src/audio.h | 74 +++++++++++++++++++++++++++++------------------ radio/src/cli.cpp | 16 ++++++---- 2 files changed, 57 insertions(+), 33 deletions(-) diff --git a/radio/src/audio.h b/radio/src/audio.h index 9f2be4b45a2..8a750d58b09 100644 --- a/radio/src/audio.h +++ b/radio/src/audio.h @@ -98,6 +98,11 @@ constexpr uint8_t AUDIO_FILENAME_MAXLEN = (AUDIO_LUA_FILENAME_MAXLEN > AUDIO_MOD #define AUDIO_BUFFER_COUNT (3) #endif +static_assert((AUDIO_QUEUE_LENGTH & (AUDIO_QUEUE_LENGTH - 1)) == 0, + "AUDIO_QUEUE_LENGTH must be a power of 2"); +static_assert(2 * AUDIO_BUFFER_COUNT <= 255, + "AUDIO_BUFFER_COUNT free-running counter must fit in uint8_t"); + #define BEEP_MIN_FREQ (150) #define BEEP_MAX_FREQ (15000) #define BEEP_DEFAULT_FREQ (2250) @@ -300,23 +305,31 @@ class AudioBufferFifo { #endif private: + // free-running counters in [0, 2*AUDIO_BUFFER_COUNT); each written by only one side volatile uint8_t readIdx; volatile uint8_t writeIdx; - volatile bool bufferFull; - inline uint8_t nextBufferIdx(uint8_t idx) const + inline uint8_t nextIndex(uint8_t idx) const { - return (idx >= AUDIO_BUFFER_COUNT - 1 ? 0 : idx + 1); + return (idx >= 2 * AUDIO_BUFFER_COUNT - 1 ? 0 : idx + 1); } - bool full() const { return bufferFull; } - bool empty() const { return readIdx == writeIdx && !bufferFull; } - uint8_t used() const { return bufferFull ? AUDIO_BUFFER_COUNT - : (writeIdx < readIdx) ? writeIdx + AUDIO_BUFFER_COUNT - readIdx - : writeIdx - readIdx; } + inline uint8_t slot(uint8_t idx) const + { + return idx >= AUDIO_BUFFER_COUNT ? idx - AUDIO_BUFFER_COUNT : idx; + } + + uint8_t used() const + { + uint8_t w = writeIdx, r = readIdx; + return w >= r ? w - r : w + 2 * AUDIO_BUFFER_COUNT - r; + } + + bool full() const { return used() == AUDIO_BUFFER_COUNT; } + bool empty() const { return readIdx == writeIdx; } public: - AudioBufferFifo() : readIdx(0), writeIdx(0), bufferFull(false) + AudioBufferFifo() : readIdx(0), writeIdx(0) { memset(audioBuffers, 0, sizeof(audioBuffers)); } @@ -325,26 +338,19 @@ class AudioBufferFifo { // with audioPushBuffer() AudioBuffer *getEmptyBuffer() const { - return full() ? nullptr : &audioBuffers[writeIdx]; + return full() ? nullptr : &audioBuffers[slot(writeIdx)]; } // puts filled buffer into FIFO - void audioPushBuffer() - { - writeIdx = nextBufferIdx(writeIdx); - if (writeIdx == readIdx) bufferFull = true; - } + void audioPushBuffer() { writeIdx = nextIndex(writeIdx); } - void freeNextFilledBuffer() - { - readIdx = nextBufferIdx(readIdx); - bufferFull = false; - } + // frees the last played buffer + void freeNextFilledBuffer() { readIdx = nextIndex(readIdx); } // returns a pointer to the audio buffer to be played const AudioBuffer *getNextFilledBuffer() { - return empty() ? nullptr : &audioBuffers[readIdx]; + return empty() ? nullptr : &audioBuffers[slot(readIdx)]; } bool filledAtleast(int noBuffers) const { return used() >= noBuffers; } @@ -356,13 +362,25 @@ class AudioFragmentFifo friend void printAudioVars(); #endif private: + // free-running counters in [0, 2*AUDIO_QUEUE_LENGTH); each written by only one side volatile uint8_t ridx; volatile uint8_t widx; AudioFragment fragments[AUDIO_QUEUE_LENGTH]; uint8_t nextIdx(uint8_t idx) const { - return (idx + 1) & (AUDIO_QUEUE_LENGTH - 1); + return (idx + 1) & (2 * AUDIO_QUEUE_LENGTH - 1); + } + + uint8_t slot(uint8_t idx) const + { + return idx & (AUDIO_QUEUE_LENGTH - 1); + } + + uint8_t used() const + { + uint8_t w = widx, r = ridx; + return w >= r ? w - r : w + 2 * AUDIO_QUEUE_LENGTH - r; } public: @@ -372,7 +390,7 @@ class AudioFragmentFifo { uint8_t i = ridx; while (i != widx) { - AudioFragment & fragment = fragments[i]; + AudioFragment & fragment = fragments[slot(i)]; if (fragment.id == id) return true; i = nextIdx(i); } @@ -383,7 +401,7 @@ class AudioFragmentFifo { uint8_t i = ridx; while (i != widx) { - AudioFragment & fragment = fragments[i]; + AudioFragment & fragment = fragments[slot(i)]; if (fragment.id == id) fragment.clear(); i = nextIdx(i); } @@ -397,7 +415,7 @@ class AudioFragmentFifo bool full() const { - return ridx == nextIdx(widx); + return used() == AUDIO_QUEUE_LENGTH; } void clear() @@ -408,8 +426,8 @@ class AudioFragmentFifo const AudioFragment * get() { if (!empty()) { - const AudioFragment * result = &fragments[ridx]; - if (!fragments[ridx].repeat--) { + const AudioFragment * result = &fragments[slot(ridx)]; + if (!fragments[slot(ridx)].repeat--) { // repeat is done, move to the next fragment ridx = nextIdx(ridx); } @@ -422,7 +440,7 @@ class AudioFragmentFifo { if (!full()) { // TRACE("fragment %d at %d", fragment.type, widx); - fragments[widx] = fragment; + fragments[slot(widx)] = fragment; widx = nextIdx(widx); } } diff --git a/radio/src/cli.cpp b/radio/src/cli.cpp index 38d70f21000..cfa9209a649 100644 --- a/radio/src/cli.cpp +++ b/radio/src/cli.cpp @@ -1400,11 +1400,17 @@ void printAudioVars() } } - cliSerialPrint("FragmentFifo: ridx: %d, widx: %d", - audioQueue.fragmentsFifo.ridx, audioQueue.fragmentsFifo.widx); - cliSerialPrint("audioQueue: readIdx: %d, writeIdx: %d, full: %d", - audioQueue.buffersFifo.readIdx, audioQueue.buffersFifo.writeIdx, - audioQueue.buffersFifo.bufferFull); + cliSerialPrint("FragmentFifo: ridx: %d (slot %d), widx: %d (slot %d)", + audioQueue.fragmentsFifo.ridx, + audioQueue.fragmentsFifo.slot(audioQueue.fragmentsFifo.ridx), + audioQueue.fragmentsFifo.widx, + audioQueue.fragmentsFifo.slot(audioQueue.fragmentsFifo.widx)); + cliSerialPrint("audioQueue: readIdx: %d (slot %d), writeIdx: %d (slot %d), full: %d", + audioQueue.buffersFifo.readIdx, + audioQueue.buffersFifo.slot(audioQueue.buffersFifo.readIdx), + audioQueue.buffersFifo.writeIdx, + audioQueue.buffersFifo.slot(audioQueue.buffersFifo.writeIdx), + audioQueue.buffersFifo.full()); cliSerialPrint("normalContext: %u", (uint32_t)audioQueue.normalContext.fragment.type); From 538830192ee3c818e92ae07a79e5bd44d27c7682 Mon Sep 17 00:00:00 2001 From: philmoz Date: Sat, 19 Sep 2026 09:51:28 +1000 Subject: [PATCH 11/18] chore(color): fix compile error if LUA_ALLOCATOR_TRACER is defined (#7808) (cherry picked from commit 44da67ed7a4256cf1aa83fed4e1349fcbd0fc4ca) --- radio/src/gui/colorlcd/standalone_lua.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/radio/src/gui/colorlcd/standalone_lua.cpp b/radio/src/gui/colorlcd/standalone_lua.cpp index 9be6aa2faf5..f8b51fdf41c 100644 --- a/radio/src/gui/colorlcd/standalone_lua.cpp +++ b/radio/src/gui/colorlcd/standalone_lua.cpp @@ -33,7 +33,7 @@ LuaMemTracer lsStandaloneTrace; static void luaStandaloneHook(lua_State * L, lua_Debug *ar) { - else if (ar->event == LUA_HOOKLINE) { + if (ar->event == LUA_HOOKLINE) { lua_getinfo(L, "nSl", ar); LuaMemTracer * tracer = &lsStandaloneTrace; if (tracer->alloc || tracer->free) { From bdee8b0d19e47341545e45083eede52fc5fa00db Mon Sep 17 00:00:00 2001 From: philmoz Date: Thu, 24 Sep 2026 07:22:55 +1000 Subject: [PATCH 12/18] fix(color): labels for a model may get truncated in 'labels.yml' file (#7812) (cherry picked from commit 145fe0f0e0f59aaeb1c6c726e67cc823a905c99d) --- radio/src/lib_file.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/radio/src/lib_file.cpp b/radio/src/lib_file.cpp index 3523b41eff9..3c772994777 100644 --- a/radio/src/lib_file.cpp +++ b/radio/src/lib_file.cpp @@ -121,7 +121,7 @@ int f_puts(const char* s, FIL* fp) return (len == written && result == FR_OK) ? 0 : EOF; } -#define MAX_FPRINTF_LEN 100 +#define MAX_FPRINTF_LEN 128 int f_printf (FIL* fp, const TCHAR* str, ...) { From 3d8fbc591e725b73f9104c1d613b49fb51c40315 Mon Sep 17 00:00:00 2001 From: philmoz Date: Thu, 24 Sep 2026 11:56:04 +1000 Subject: [PATCH 13/18] chore(fw): replace unsafe calls to strAppend with copyToUnTerminated (#7814) Co-authored-by: Peter Feerick <5500713+pfeerick@users.noreply.github.com> 2.11 port: dropped the yaml_datastructs_funcs.cpp hunk (r_cfs_name only exists on main, from #6095; 2.11 reads switchNames via the generated YAML string node, so it has no strAppend there) and the modelslist_labels.cpp test hunk (that test doesn't exist on 2.11). The theme_manager.cpp and strhelpers.cpp changes apply unchanged. (cherry picked from commit 69d016fab09d7646cee3b017b0cbe38a86deb6e3) Co-Authored-By: Claude Opus 5.5 --- radio/src/gui/colorlcd/themes/theme_manager.cpp | 6 +++--- radio/src/strhelpers.cpp | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/radio/src/gui/colorlcd/themes/theme_manager.cpp b/radio/src/gui/colorlcd/themes/theme_manager.cpp index 6c176b40933..23eb2603ca6 100644 --- a/radio/src/gui/colorlcd/themes/theme_manager.cpp +++ b/radio/src/gui/colorlcd/themes/theme_manager.cpp @@ -385,7 +385,8 @@ void ThemePersistance::loadDefaultTheme() } for (auto theme : themes) { - if (theme->getName().compare(0, SELECTED_THEME_NAME_LEN, g_eeGeneral.selectedTheme) == 0) { + if (theme->getName().compare(0, SELECTED_THEME_NAME_LEN, + stringFromNtString(g_eeGeneral.selectedTheme)) == 0) { found = true; break; } @@ -458,8 +459,7 @@ bool ThemePersistance::createNewTheme(std::string name, ThemeFile& theme) void ThemePersistance::setDefaultTheme(int index) { if (index >= 0 && index < (int)themes.size()) { - strAppend(g_eeGeneral.selectedTheme, themes[index]->getName().c_str(), - SELECTED_THEME_NAME_LEN); + copyToUnTerminated(g_eeGeneral.selectedTheme, themes[index]->getName()); SET_DIRTY(); currentTheme = index; } diff --git a/radio/src/strhelpers.cpp b/radio/src/strhelpers.cpp index 9a756ae7dc3..502aed1b066 100644 --- a/radio/src/strhelpers.cpp +++ b/radio/src/strhelpers.cpp @@ -1172,6 +1172,7 @@ char *strAppendSigned(char *dest, int32_t value, uint8_t digits, uint8_t radix) return strAppendUnsigned(dest, (uint32_t)value, digits, radix); } +// NOTE: 'dest' buffer must be at least 'len + 1' bytes long to allow for nul terminator char *strAppend(char *dest, const char *source, int len) { if (source == nullptr) { *dest = '\0'; return dest; } From 287b90ac341befbcf6f9975dfc442202ef128bad Mon Sep 17 00:00:00 2001 From: Simon Ernst Date: Mon, 31 Oct 2022 22:12:20 +0100 Subject: [PATCH 14/18] feat(xlite): add compile time option for alternative button layout (#2662) Adds the XLITE_ALT_LAYOUT CMake option (X-Lite and X-Lite S), which swaps the key and left-trim GPIO assignments. Squash of PR #2662 (commit 39bf1c47b0; the PR's two merge-from-main commits dropped), hand-ported to 2.11: pin definitions converted to the LL style (GPIOx / LL_GPIO_PIN_n), and TRIMS_VIRTUAL_RV/RH added to the alternative trim block so the virtual right-stick trims are still generated, as in the stock X-Lite layout. Co-Authored-By: Claude Opus 5.5 --- radio/src/targets/taranis/CMakeLists.txt | 9 ++++++++ radio/src/targets/taranis/hal.h | 26 ++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/radio/src/targets/taranis/CMakeLists.txt b/radio/src/targets/taranis/CMakeLists.txt index bafba2ac3c0..eb509c7308e 100644 --- a/radio/src/targets/taranis/CMakeLists.txt +++ b/radio/src/targets/taranis/CMakeLists.txt @@ -8,6 +8,7 @@ option(AUTOUPDATE "Auto update internal chips from SD" OFF) option(BIND_KEY "Enable bind button" OFF) option(BLUETOOTH "FrSky BT module support" OFF) option(RGBLEDS "WS2812 addressable LED support" OFF) +option(XLITE_ALT_LAYOUT "Alternative layout for X-Lite button functions" OFF) set(USE_RTC_CLOCK YES) set(HARDWARE_EXTERNAL_MODULE YES) @@ -508,6 +509,10 @@ elseif(PCB STREQUAL XLITE) set(LCD_DRIVER lcd_driver_spi.cpp) set(STATUS_LEDS YES) set(BLUETOOTH YES) + if(XLITE_ALT_LAYOUT) + message("-- Using alternative layout for X-Lite button functions") + add_definitions(-DXLITE_ALT_LAYOUT) + endif() add_definitions(-DMANUFACTURER_FRSKY) elseif(PCB STREQUAL XLITES) set(DEFAULT_INTERNAL_MODULE ISRM_PXX2 CACHE STRING "Default internal module") @@ -529,6 +534,10 @@ elseif(PCB STREQUAL XLITES) set(PXX2 ON) set(BLUETOOTH YES) set(IMU YES) + if(XLITE_ALT_LAYOUT) + message("-- Using alternative layout for X-Lite button functions") + add_definitions(-DXLITE_ALT_LAYOUT) + endif() add_definitions(-DMANUFACTURER_FRSKY) endif() diff --git a/radio/src/targets/taranis/hal.h b/radio/src/targets/taranis/hal.h index 64bea6a8468..26a046377c2 100644 --- a/radio/src/targets/taranis/hal.h +++ b/radio/src/targets/taranis/hal.h @@ -53,6 +53,21 @@ #define KEYS_GPIO_PIN_PAGEDN LL_GPIO_PIN_3 // PD.03 #define KEYS_GPIO_REG_ENTER GPIOF #define KEYS_GPIO_PIN_ENTER LL_GPIO_PIN_0 // PF.00 +#elif defined(PCBXLITE) && defined(XLITE_ALT_LAYOUT) + #define KEYS_GPIO_REG_SHIFT GPIOE + #define KEYS_GPIO_PIN_SHIFT LL_GPIO_PIN_11 // PE.11 + #define KEYS_GPIO_REG_EXIT GPIOE + #define KEYS_GPIO_PIN_EXIT LL_GPIO_PIN_7 // PE.07 + #define KEYS_GPIO_REG_ENTER GPIOE + #define KEYS_GPIO_PIN_ENTER LL_GPIO_PIN_8 // PE.08 + #define KEYS_GPIO_REG_UP GPIOB + #define KEYS_GPIO_PIN_UP LL_GPIO_PIN_0 // PB.00 + #define KEYS_GPIO_REG_DOWN GPIOB + #define KEYS_GPIO_PIN_DOWN LL_GPIO_PIN_1 // PB.01 + #define KEYS_GPIO_REG_LEFT GPIOC + #define KEYS_GPIO_PIN_LEFT LL_GPIO_PIN_4 // PC.04 + #define KEYS_GPIO_REG_RIGHT GPIOC + #define KEYS_GPIO_PIN_RIGHT LL_GPIO_PIN_5 // PC.05 #elif defined(PCBXLITE) #define KEYS_GPIO_REG_SHIFT GPIOE #define KEYS_GPIO_PIN_SHIFT LL_GPIO_PIN_8 // PE.08 @@ -370,6 +385,17 @@ #define TRIMS_GPIO_PIN_RVU LL_GPIO_PIN_2 // PC.02 #define TRIMS_GPIO_REG_RHR GPIOC #define TRIMS_GPIO_PIN_RHR LL_GPIO_PIN_13 // PC.13 +#elif defined(PCBXLITE) && defined(XLITE_ALT_LAYOUT) + #define TRIMS_GPIO_REG_LHL GPIOE + #define TRIMS_GPIO_PIN_LHL LL_GPIO_PIN_12 // PE.12 + #define TRIMS_GPIO_REG_LHR GPIOE + #define TRIMS_GPIO_PIN_LHR LL_GPIO_PIN_13 // PE.13 + #define TRIMS_GPIO_REG_LVU GPIOE + #define TRIMS_GPIO_PIN_LVU LL_GPIO_PIN_10 // PE.10 + #define TRIMS_GPIO_REG_LVD GPIOE + #define TRIMS_GPIO_PIN_LVD LL_GPIO_PIN_14 // PE.14 + #define TRIMS_VIRTUAL_RV + #define TRIMS_VIRTUAL_RH #elif defined(PCBXLITE) #define TRIMS_GPIO_REG_LHL GPIOC #define TRIMS_GPIO_PIN_LHL LL_GPIO_PIN_4 // PC.04 From a67fd58bcc9c9fdb471d766ce4ea0f11f1ca5bf6 Mon Sep 17 00:00:00 2001 From: philmoz Date: Sun, 20 Sep 2026 10:42:37 +1000 Subject: [PATCH 15/18] chore(color): remove unused LV_OBJ_FLAG_ENCODER_ACCEL define. (#7809) 2.11 port: dropped the radio_mic_recorder.cpp hunk. That file was added on main by the MEMS microphone support (#7328), and the flag line in it by #7706. Neither is on 2.11: #7328 a7d730fdedae9cdd24acb2226cb49b8bcb3916d8 #7706 ccc838739baff94bfddc21763e6efc88ef0feb8f As on main, nothing on 2.11 reads the flag (unused since #3441), so the other hunks apply unchanged and there's no behaviour change. (cherry picked from commit f364c6ef630d3c3c91418ef3ce3cd8af0fcb2bd0) Co-Authored-By: Claude Opus 5.5 --- radio/src/gui/colorlcd/controls/color_editor.cpp | 2 -- radio/src/gui/colorlcd/libui/libopenui_defines.h | 2 -- radio/src/gui/colorlcd/libui/menu.cpp | 3 --- radio/src/gui/colorlcd/libui/numberedit.cpp | 3 --- 4 files changed, 10 deletions(-) diff --git a/radio/src/gui/colorlcd/controls/color_editor.cpp b/radio/src/gui/colorlcd/controls/color_editor.cpp index 13c07ce1b60..e62e0f4ea59 100644 --- a/radio/src/gui/colorlcd/controls/color_editor.cpp +++ b/radio/src/gui/colorlcd/controls/color_editor.cpp @@ -36,8 +36,6 @@ class ColorBar : public FormField uint32_t maxValue = 0) : FormField(parent, r) { - lv_obj_add_flag(lvobj, LV_OBJ_FLAG_ENCODER_ACCEL); - lv_group_add_obj((lv_group_t*)lv_group_get_default(), lvobj); lv_obj_add_event_cb(lvobj, ColorBar::pressing, LV_EVENT_PRESSING, nullptr); diff --git a/radio/src/gui/colorlcd/libui/libopenui_defines.h b/radio/src/gui/colorlcd/libui/libopenui_defines.h index daf6803563d..848a0757740 100644 --- a/radio/src/gui/colorlcd/libui/libopenui_defines.h +++ b/radio/src/gui/colorlcd/libui/libopenui_defines.h @@ -48,5 +48,3 @@ /* telemetry flags */ #define NO_UNIT 0x40u - -#define LV_OBJ_FLAG_ENCODER_ACCEL LV_OBJ_FLAG_USER_1 diff --git a/radio/src/gui/colorlcd/libui/menu.cpp b/radio/src/gui/colorlcd/libui/menu.cpp index fbe79cce8b5..83a18472faa 100644 --- a/radio/src/gui/colorlcd/libui/menu.cpp +++ b/radio/src/gui/colorlcd/libui/menu.cpp @@ -68,9 +68,6 @@ class MenuBody : public TableField MenuBody(Window* parent, const rect_t& rect) : TableField(parent, rect) { - // Allow encoder acceleration - lv_obj_add_flag(lvobj, LV_OBJ_FLAG_ENCODER_ACCEL); - setColumnWidth(0, rect.w); setAutoEdit(); diff --git a/radio/src/gui/colorlcd/libui/numberedit.cpp b/radio/src/gui/colorlcd/libui/numberedit.cpp index d91e8098e8b..acc95672161 100644 --- a/radio/src/gui/colorlcd/libui/numberedit.cpp +++ b/radio/src/gui/colorlcd/libui/numberedit.cpp @@ -32,9 +32,6 @@ class NumberArea : public FormField { lv_obj_clear_flag(lvobj, LV_OBJ_FLAG_CLICK_FOCUSABLE); - // Allow encoder acceleration - lv_obj_add_flag(lvobj, LV_OBJ_FLAG_ENCODER_ACCEL); - lv_obj_add_event_cb(lvobj, NumberArea::numberedit_cb, LV_EVENT_KEY, this); setFocusHandler([=](bool focus) { From 02845d19ea26115c487ab78f42811db547c9b6a5 Mon Sep 17 00:00:00 2001 From: Peter Feerick <5500713+pfeerick@users.noreply.github.com> Date: Thu, 24 Sep 2026 04:39:07 +0000 Subject: [PATCH 16/18] chore: bump version --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fa100b9a94f..01ce84d2e8f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,7 @@ project(EdgeTX) set(VERSION_MAJOR "2") set(VERSION_MINOR "11") -set(VERSION_REVISION "7") +set(VERSION_REVISION "8") set(CODENAME "Jolly Mon") string(TIMESTAMP BUILD_YEAR "%Y") From 6eaa48c1bdf82eac0a471f42aea27f171876c361 Mon Sep 17 00:00:00 2001 From: richardclli Date: Sat, 3 Oct 2026 22:27:36 +0000 Subject: [PATCH 17/18] fix(stm32): hangs during flashing firmware (#7415) Squash of the 11 commits on EdgeTX/edgetx#7415 (still open; head 202172f315), for release testing: * fix(stm32): disable interrupts during flash erase/program to prevent hangs on F4 * fix(stm32): replace broken HAL timeouts with DWT cycle counter during flash erase/program * fix(stm32f4): clear stale FLASH error flags before erase/program * fix(boot): report firmware write failures instead of showing success * fix(senduwing-h750): update flashWrite() declaration to return bool * fix(bootloader): treat SD read errors as a flash failure * fix(stm32f4): disable data cache during flash erase/program (ES0206 2.2.15) * fix(stm32): verify each page after writing it in flashWrite() * chore(stm32): shorten flash operation timeout to twice the datasheet maximum * fix(stm32h7): keep interrupts enabled during HAL flash erase/program * chore(stm32f4): correct reason given for clearing stale FLASH error flags Port notes for 2.11: the h750 board.h and c14/board.h hunks are dropped (those boards don't exist on 2.11), and the simulator stub change is applied to simpgmspace.cpp instead of simulib.cpp. Co-authored-by: 3djc <3djc@gh.com> Co-authored-by: Peter Feerick <5500713+pfeerick@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 --- radio/src/bootloader/bin_fw_files.cpp | 23 ++- radio/src/bootloader/boot.h | 1 + radio/src/bootloader/boot_menu.cpp | 18 +- radio/src/bootloader/firmware_files.h | 10 +- radio/src/gui/colorlcd/boot_menu.cpp | 11 +- radio/src/gui/common/stdlcd/boot_menu.cpp | 4 + radio/src/io/bootloader_flash.cpp | 14 +- .../targets/common/arm/stm32/flash_driver.cpp | 190 +++++++++++++++++- .../targets/common/arm/stm32/flash_driver.h | 2 +- radio/src/targets/pa01/board.h | 2 +- radio/src/targets/simu/simpgmspace.cpp | 3 +- radio/src/targets/st16/board.h | 2 +- radio/src/translations/bl_translations.h | 16 ++ 13 files changed, 264 insertions(+), 32 deletions(-) diff --git a/radio/src/bootloader/bin_fw_files.cpp b/radio/src/bootloader/bin_fw_files.cpp index 7580bcab741..35a07416b6a 100644 --- a/radio/src/bootloader/bin_fw_files.cpp +++ b/radio/src/bootloader/bin_fw_files.cpp @@ -53,13 +53,14 @@ static FWFileInfo fwFiles[MAX_FW_FILES]; static uint8_t Block_buffer[BLOCK_LEN]; static UINT BlockCount; -static void flashWriteBlock() +static bool flashWriteBlock() { // TODO: use some board provided driver instead uint32_t blockOffset = 0; #if !defined(SIMU) while (BlockCount) { - flashWrite((uint32_t *)firmwareAddress, (uint32_t *)&Block_buffer[blockOffset]); + if (!flashWrite((uint32_t *)firmwareAddress, (uint32_t *)&Block_buffer[blockOffset])) + return false; blockOffset += FLASH_PAGESIZE; firmwareAddress += FLASH_PAGESIZE; if (BlockCount > FLASH_PAGESIZE) { @@ -70,6 +71,7 @@ static void flashWriteBlock() } } #endif // SIMU + return true; } void sdInit(void) @@ -267,17 +269,24 @@ void firmwareInitWrite(uint32_t index) firmwareWritten = 0; } -bool firmwareWriteBlock(uint32_t* progress) +FlashWriteRes firmwareWriteBlock(uint32_t* progress) { - flashWriteBlock(); + if (!flashWriteBlock()) return FW_ERROR; + firmwareWritten += sizeof(Block_buffer); *progress = (100 * firmwareWritten) / firmwareSize; - readFirmwareFile(); + // a read error or a short file must not be reported as success + if (readFirmwareFile() != FR_OK) { + return FW_ERROR; + } + if (BlockCount == 0 && firmwareWritten < firmwareSize) { + return FW_ERROR; + } if (BlockCount == 0 || firmwareWritten >= FLASHSIZE - BOOTLOADER_SIZE) { - return true; + return FW_DONE; } - return false; + return FW_IN_PROGRESS; } diff --git a/radio/src/bootloader/boot.h b/radio/src/bootloader/boot.h index 4313e0af332..53ed498b499 100644 --- a/radio/src/bootloader/boot.h +++ b/radio/src/bootloader/boot.h @@ -53,6 +53,7 @@ enum BootloaderState { ST_FLASH_CHECK, ST_FLASHING, ST_FLASH_DONE, + ST_FLASH_ERROR, ST_RESTORE_MENU, ST_USB, #if defined(SPI_FLASH) diff --git a/radio/src/bootloader/boot_menu.cpp b/radio/src/bootloader/boot_menu.cpp index 229df8b1350..ac54145b6d8 100644 --- a/radio/src/bootloader/boot_menu.cpp +++ b/radio/src/bootloader/boot_menu.cpp @@ -96,6 +96,7 @@ void bootloaderMenu() uint8_t index = 0; FRESULT fr; uint32_t nameCount = 0; + uint32_t progress = 0; sdInit(); @@ -107,7 +108,8 @@ void bootloaderMenu() next_frame += FRAME_INTERVAL_MS; if (state != ST_USB && state != ST_FLASHING - && state != ST_FLASH_DONE && state != ST_RADIO_MENU) { + && state != ST_FLASH_DONE && state != ST_FLASH_ERROR + && state != ST_RADIO_MENU) { if (usbPlugged()) { state = ST_USB; #if !defined(SIMU) @@ -244,14 +246,16 @@ void bootloaderMenu() } else if (result == 1) { // confirmed firmwareInitWrite(vpos); + progress = 0; state = ST_FLASHING; } } else if (state == ST_FLASHING) { - uint32_t progress = 0; - bool done = firmwareWriteBlock(&progress); + FlashWriteRes res = firmwareWriteBlock(&progress); bootloaderDrawScreen(state, progress); - if(done) { + if (res == FW_DONE) { state = ST_FLASH_DONE; + } else if (res == FW_ERROR) { + state = ST_FLASH_ERROR; } #if defined(SPI_FLASH) } else if (state == ST_CLEAR_FLASH_CHECK) { @@ -282,13 +286,15 @@ void bootloaderMenu() } } - if (state == ST_FLASH_DONE) { + if (state == ST_FLASH_DONE || state == ST_FLASH_ERROR) { + BootloaderState drawn = state; + if (event == EVT_KEY_BREAK(KEY_EXIT) || event == EVT_KEY_BREAK(KEY_ENTER)) { state = ST_START; vpos = 0; } - bootloaderDrawScreen(state, 100); + bootloaderDrawScreen(drawn, drawn == ST_FLASH_DONE ? 100 : progress); } if (event == EVT_KEY_LONG(KEY_EXIT)) { diff --git a/radio/src/bootloader/firmware_files.h b/radio/src/bootloader/firmware_files.h index 4475b13d3d4..560e5135b11 100644 --- a/radio/src/bootloader/firmware_files.h +++ b/radio/src/bootloader/firmware_files.h @@ -69,7 +69,15 @@ enum FlashCheckRes { }; FlashCheckRes checkFirmwareFile(unsigned int index, FlashCheckRes res); +enum FlashWriteRes { + FW_IN_PROGRESS=0, + FW_DONE, + FW_ERROR +}; + void firmwareInitWrite(uint32_t index); bool firmwareEraseBlock(uint32_t* progress); -bool firmwareWriteBlock(uint32_t* progress); + +// Write the block currently in the buffer, then fetch the next one +FlashWriteRes firmwareWriteBlock(uint32_t* progress); diff --git a/radio/src/gui/colorlcd/boot_menu.cpp b/radio/src/gui/colorlcd/boot_menu.cpp index 24bdc1c3033..88328870886 100644 --- a/radio/src/gui/colorlcd/boot_menu.cpp +++ b/radio/src/gui/colorlcd/boot_menu.cpp @@ -344,16 +344,20 @@ void bootloaderDrawScreen(BootloaderState st, int opt, const char* str) lcd->drawText(USB_TXT_X, y + USB_PLG_TXT_YO, TR_BL_USB_CONNECTED, USB_TXT_ALIGN | BL_FOREGROUND); } else if (st == ST_FILE_LIST || st == ST_DIR_CHECK || st == ST_FLASH_CHECK || - st == ST_FLASHING || st == ST_FLASH_DONE) { + st == ST_FLASHING || st == ST_FLASH_DONE || st == ST_FLASH_ERROR) { bootloaderDrawTitle(LV_SYMBOL_SD_CARD " /FIRMWARE"); - if (st == ST_FLASHING || st == ST_FLASH_DONE) { + if (st == ST_FLASHING || st == ST_FLASH_DONE || st == ST_FLASH_ERROR) { LcdFlags color = BL_RED; if (st == ST_FLASH_DONE) { color = BL_GREEN; opt = 100; // Completed > 100% + } else if (st == ST_FLASH_ERROR) { + // the bar stays where the write gave up + lcd.drawText(LCD_W / 2, (LCD_H - PROGRESS_H) / 2 - EdgeTxStyles::STD_FONT_HEIGHT, + LV_SYMBOL_CLOSE " " TR_BL_WRITING_FAILED, CENTERED | BL_FOREGROUND); } lcd->drawRect(PROGRESS_X, (LCD_H - PROGRESS_H) / 2, PROGRESS_W, PROGRESS_H, LINE_H, SOLID, BL_SELECTED); @@ -408,6 +412,9 @@ void bootloaderDrawScreen(BootloaderState st, int opt, const char* str) else if (st == ST_FLASH_DONE) { pos = lcd->drawText(FOOTER_X1, LCD_H - FOOTER_Y1, LV_SYMBOL_CHARGE " " TR_BL_WRITING_COMPL, FOOTER_ALIGN1 | BL_FOREGROUND); } + else if (st == ST_FLASH_ERROR) { + pos = lcd.drawText(FOOTER_X1, LCD_H - FOOTER_Y1, LV_SYMBOL_CHARGE " " TR_BL_RETRY_OR_DFU, FOOTER_ALIGN1 | BL_FOREGROUND); + } } #if LANDSCAPE pos = 0; diff --git a/radio/src/gui/common/stdlcd/boot_menu.cpp b/radio/src/gui/common/stdlcd/boot_menu.cpp index 7875396ba73..3fb347df180 100644 --- a/radio/src/gui/common/stdlcd/boot_menu.cpp +++ b/radio/src/gui/common/stdlcd/boot_menu.cpp @@ -132,6 +132,10 @@ void bootloaderDrawScreen(BootloaderState st, int opt, const char *str) else if (st == ST_FLASH_DONE) { lcdDrawCenteredText(4 * FH, TR_BL_WRITING_COMPL); } + else if (st == ST_FLASH_ERROR) { + lcdDrawCenteredText(4 * FH, TR_BL_WRITING_FAILED); + lcdDrawCenteredText(6 * FH, TR_BL_RETRY_OR_DFU); + } } uint32_t bootloaderGetMenuItemCount(int baseCount) diff --git a/radio/src/io/bootloader_flash.cpp b/radio/src/io/bootloader_flash.cpp index 76ee72276f2..ce6971cc019 100644 --- a/radio/src/io/bootloader_flash.cpp +++ b/radio/src/io/bootloader_flash.cpp @@ -63,6 +63,7 @@ void BootloaderFirmwareUpdate::flashFirmware(const char * filename, ProgressHand FIL file; uint8_t buffer[1024]; UINT count; + bool success = true; pulsesStop(); @@ -80,22 +81,29 @@ void BootloaderFirmwareUpdate::flashFirmware(const char * filename, ProgressHand if (f_read(&file, buffer, sizeof(buffer), &count) != FR_OK) { POPUP_WARNING(STR_SDCARD_ERROR); + success = false; break; } if (count != sizeof(buffer) && !f_eof(&file)) { POPUP_WARNING(STR_SDCARD_ERROR); + success = false; break; } if (i == 0 && !isBootloaderStart(buffer)) { POPUP_WARNING(STR_INCOMPATIBLE); + success = false; break; } - for (UINT j = 0; j < count; j += FLASH_PAGESIZE) { + for (UINT j = 0; j < count && success; j += FLASH_PAGESIZE) { WDG_ENABLE(3000); - flashWrite(CONVERT_UINT_PTR(BOOTLOADER_ADDRESS + i + j), CONVERT_UINT_PTR(buffer + j)); + success = flashWrite(CONVERT_UINT_PTR(BOOTLOADER_ADDRESS + i + j), CONVERT_UINT_PTR(buffer + j)); WDG_ENABLE(WDG_DURATION); } + if (!success) { + POPUP_WARNING(STR_FIRMWARE_UPDATE_ERROR); + break; + } progressHandler("Bootloader", STR_WRITING, i, flash_size); // Reached end-of-file @@ -108,7 +116,7 @@ void BootloaderFirmwareUpdate::flashFirmware(const char * filename, ProgressHand #endif } - POPUP_INFORMATION(STR_FIRMWARE_UPDATE_SUCCESS); + if (success) POPUP_INFORMATION(STR_FIRMWARE_UPDATE_SUCCESS); watchdogSuspend(0); WDG_RESET(); diff --git a/radio/src/targets/common/arm/stm32/flash_driver.cpp b/radio/src/targets/common/arm/stm32/flash_driver.cpp index cc41ad11ed9..776ff2e86f6 100644 --- a/radio/src/targets/common/arm/stm32/flash_driver.cpp +++ b/radio/src/targets/common/arm/stm32/flash_driver.cpp @@ -85,6 +85,82 @@ static uint32_t stm32_flash_get_sector_size(uint32_t sector) return 128 * 1024; } +// twice the datasheet maximum for a 128 KB sector erase at x32 parallelism +// (2 s, DS9405 Table 48); the driver never issues a bank or mass erase +#define FLASH_TIMEOUT_MS 4000 + +#if defined(FLASH_FLAG_RDERR) + #define _FLASH_FLAG_RDERR FLASH_FLAG_RDERR +#else + #define _FLASH_FLAG_RDERR 0U +#endif + +// Error flags are sticky until written with 1, so one left over by an earlier +// access since reset (e.g. a stray write to flash, which sets PGSERR) would +// abort the very next erase/program. Clear them before starting an operation. +static void flash_drv_clear_errors() +{ + __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_OPERR | FLASH_FLAG_WRPERR | + FLASH_FLAG_PGAERR | FLASH_FLAG_PGPERR | + FLASH_FLAG_PGSERR | _FLASH_FLAG_RDERR); +} + +static bool flash_drv_wait_last_op() +{ + CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; + DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; + + uint32_t start = DWT->CYCCNT; + uint32_t timeout_cycles = + (uint32_t)(FLASH_TIMEOUT_MS * (SystemCoreClock / 1000UL)); + + while (__HAL_FLASH_GET_FLAG(FLASH_FLAG_BSY)) { + if ((DWT->CYCCNT - start) > timeout_cycles) { + __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_WRPERR | + FLASH_FLAG_PGAERR | FLASH_FLAG_PGPERR | + FLASH_FLAG_PGSERR); + return false; + } + } + + __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP); + + if (__HAL_FLASH_GET_FLAG(FLASH_FLAG_WRPERR | FLASH_FLAG_PGAERR | + FLASH_FLAG_PGPERR | FLASH_FLAG_PGSERR)) { + __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_WRPERR | FLASH_FLAG_PGAERR | + FLASH_FLAG_PGPERR | FLASH_FLAG_PGSERR | + FLASH_FLAG_EOP); + return false; + } + + return true; +} + +// ES0206 2.2.15: on dual bank devices the data cache may be corrupted by a +// read-while-write, so it must be disabled while flash is erased/programmed +// and reset before being enabled again. +static bool flash_drv_disable_dcache() +{ + bool enabled = (FLASH->ACR & FLASH_ACR_DCEN) != 0; + if (enabled) FLASH->ACR &= ~FLASH_ACR_DCEN; + return enabled; +} + +static void flash_drv_flush_caches(bool dcache_enabled) +{ + if (FLASH->ACR & FLASH_ACR_ICEN) { + FLASH->ACR &= ~FLASH_ACR_ICEN; + FLASH->ACR |= FLASH_ACR_ICRST; + FLASH->ACR &= ~FLASH_ACR_ICRST; + FLASH->ACR |= FLASH_ACR_ICEN; + } + if (dcache_enabled) { + FLASH->ACR |= FLASH_ACR_DCRST; + FLASH->ACR &= ~FLASH_ACR_DCRST; + FLASH->ACR |= FLASH_ACR_DCEN; + } +} + #elif defined(STM32H7) || defined(STM32H7RS) static uint32_t stm32_flash_get_sector(uint32_t address) @@ -129,6 +205,43 @@ static inline void stm32_flash_lock() { HAL_FLASH_Lock(); } static int stm32_flash_erase_sector(uint32_t address) { + int ret = 0; + +#if defined(STM32F2) || defined(STM32F4) + + uint32_t sector = stm32_flash_get_sector(address); + + __disable_irq(); + __DSB(); + + stm32_flash_unlock(); + flash_drv_clear_errors(); + + bool dcache = flash_drv_disable_dcache(); + + if (sector > 11) sector += 4; + + CLEAR_BIT(FLASH->CR, FLASH_CR_PSIZE); + FLASH->CR |= FLASH_PSIZE_WORD; + CLEAR_BIT(FLASH->CR, FLASH_CR_SNB); + FLASH->CR |= FLASH_CR_SER | (sector << FLASH_CR_SNB_Pos); + FLASH->CR |= FLASH_CR_STRT; + + if (!flash_drv_wait_last_op()) { + ret = -1; + } + + CLEAR_BIT(FLASH->CR, FLASH_CR_SER | FLASH_CR_SNB); + + __DSB(); + __enable_irq(); + + flash_drv_flush_caches(dcache); + + stm32_flash_lock(); + +#else + FLASH_EraseInitTypeDef eraseInit; eraseInit.TypeErase = FLASH_TYPEERASE_SECTORS; eraseInit.Sector = stm32_flash_get_sector(address); @@ -142,15 +255,20 @@ static int stm32_flash_erase_sector(uint32_t address) eraseInit.VoltageRange = FLASH_VOLTAGE_RANGE_3; #endif - int ret = 0; uint32_t sector_errors = 0; + // Interrupts must stay enabled here: the HAL timeout counts HAL_GetTick(), + // which only advances in the ms timer interrupt. Nothing needs masking, as + // code and vectors run from TCM/SDRAM, never from the bank being erased. stm32_flash_unlock(); if (HAL_FLASHEx_Erase(&eraseInit, §or_errors) != HAL_OK) { ret = -1; } stm32_flash_lock(); + +#endif + return ret; } @@ -162,18 +280,57 @@ static int stm32_flash_erase_sector(uint32_t address) #define FLASH_PROG_WORDS 4UL #define _FLASH_PROGRAM(address, p_data) \ HAL_FLASH_Program(FLASH_TYPEPROGRAM_QUADWORD, address, (uintptr_t)p_data) -#else - #define FLASH_PROG_WORDS 1UL - #define _FLASH_PROGRAM(address, p_data) \ - HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, address, *p_data) #endif static int stm32_flash_program(uint32_t address, void* data, uint32_t len) { + int ret = 0; + +#if defined(STM32F2) || defined(STM32F4) + uint32_t* p_data = (uint32_t*)data; uint32_t end_addr = address + len; - int ret = 0; + __disable_irq(); + __DSB(); + stm32_flash_unlock(); + flash_drv_clear_errors(); + + bool dcache = flash_drv_disable_dcache(); + + while (address < end_addr) { + CLEAR_BIT(FLASH->CR, FLASH_CR_PSIZE); + FLASH->CR |= FLASH_PSIZE_WORD; + FLASH->CR |= FLASH_CR_PG; + + *(__IO uint32_t*)address = *p_data; + + // PG must be cleared even on failure, or the next erase sees PG+SER + bool ok = flash_drv_wait_last_op(); + CLEAR_BIT(FLASH->CR, FLASH_CR_PG); + + if (!ok) { + ret = -1; + break; + } + + address += sizeof(uint32_t); + p_data++; + } + + __DSB(); + __enable_irq(); + + flash_drv_flush_caches(dcache); + + stm32_flash_lock(); + +#else + + uint32_t* p_data = (uint32_t*)data; + uint32_t end_addr = address + len; + + // interrupts stay enabled, see stm32_flash_erase_sector() stm32_flash_unlock(); while (address < end_addr) { if (_FLASH_PROGRAM(address, p_data) != HAL_OK) { @@ -186,6 +343,9 @@ static int stm32_flash_program(uint32_t address, void* data, uint32_t len) } stm32_flash_lock(); + +#endif + return ret; } @@ -211,17 +371,29 @@ const etx_flash_driver_t stm32_flash_driver = { void unlockFlash() { stm32_flash_unlock(); } void lockFlash() { stm32_flash_lock(); } -void flashWrite(uint32_t* address, const uint32_t* buffer) +bool flashWrite(uint32_t* address, const uint32_t* buffer) { // check first if the address is on a sector boundary uint32_t sector = stm32_flash_get_sector((uintptr_t)address); uint32_t bank = stm32_flash_get_bank((uintptr_t)address); if ((uintptr_t)address == _flash_sector_address(sector, bank)) { - if (stm32_flash_erase_sector((uintptr_t)address) < 0) return; + if (stm32_flash_erase_sector((uintptr_t)address) < 0) return false; } - stm32_flash_program((uintptr_t)address, (uint8_t*)buffer, FLASH_PAGESIZE); + if (stm32_flash_program((uintptr_t)address, (uint8_t*)buffer, + FLASH_PAGESIZE) < 0) + return false; + +#if defined(__DCACHE_PRESENT) && (__DCACHE_PRESENT == 1U) + // drop any lines cached before the erase/program so the read back + // below comes from flash + SCB_InvalidateDCache_by_Addr(address, FLASH_PAGESIZE); +#endif + + // verify the page was actually written, independently of what the + // flash controller reported + return memcmp(address, buffer, FLASH_PAGESIZE) == 0; } // TODO: move this somewhere else, as it depends on firmware layout diff --git a/radio/src/targets/common/arm/stm32/flash_driver.h b/radio/src/targets/common/arm/stm32/flash_driver.h index 86dc0b40107..3f107cf9fc3 100644 --- a/radio/src/targets/common/arm/stm32/flash_driver.h +++ b/radio/src/targets/common/arm/stm32/flash_driver.h @@ -44,5 +44,5 @@ extern const void* stm32_flash_dfu_media; uint32_t isFirmwareStart(const uint8_t * buffer); uint32_t isBootloaderStart(const uint8_t * buffer); -void flashWrite(uint32_t* address, const uint32_t* buffer); +bool flashWrite(uint32_t* address, const uint32_t* buffer); diff --git a/radio/src/targets/pa01/board.h b/radio/src/targets/pa01/board.h index 42a2b5bc765..4ced440aa65 100644 --- a/radio/src/targets/pa01/board.h +++ b/radio/src/targets/pa01/board.h @@ -61,7 +61,7 @@ void getCPUUniqueID(char * s); #define FLASH_PAGESIZE 256 void unlockFlash(); void lockFlash(); -void flashWrite(uint32_t * address, const uint32_t * buffer); +bool flashWrite(uint32_t * address, const uint32_t * buffer); uint32_t isFirmwareStart(const uint8_t * buffer); uint32_t isBootloaderStart(const uint8_t * buffer); diff --git a/radio/src/targets/simu/simpgmspace.cpp b/radio/src/targets/simu/simpgmspace.cpp index 0bd0c229372..5df6b3e2eb2 100644 --- a/radio/src/targets/simu/simpgmspace.cpp +++ b/radio/src/targets/simu/simpgmspace.cpp @@ -498,9 +498,10 @@ void setSelectedUsbMode(int mode) {} void delay_ms(uint32_t ms) { } void delay_us(uint16_t us) { } -void flashWrite(uint32_t *address, const uint32_t *buffer) +bool flashWrite(uint32_t *address, const uint32_t *buffer) { simuSleep(10); + return true; } uint32_t isBootloaderStart(const uint8_t * block) diff --git a/radio/src/targets/st16/board.h b/radio/src/targets/st16/board.h index 863ead25eaf..d713d304e1f 100644 --- a/radio/src/targets/st16/board.h +++ b/radio/src/targets/st16/board.h @@ -60,7 +60,7 @@ void getCPUUniqueID(char * s); #define FLASH_PAGESIZE 256 void unlockFlash(); void lockFlash(); -void flashWrite(uint32_t * address, const uint32_t * buffer); +bool flashWrite(uint32_t * address, const uint32_t * buffer); uint32_t isFirmwareStart(const uint8_t * buffer); uint32_t isBootloaderStart(const uint8_t * buffer); diff --git a/radio/src/translations/bl_translations.h b/radio/src/translations/bl_translations.h index ac45c25a6b5..9a843ebaf46 100644 --- a/radio/src/translations/bl_translations.h +++ b/radio/src/translations/bl_translations.h @@ -63,6 +63,8 @@ #define TR_BL_DIR_EMPTY "Adresar je prazdny" #define TR_BL_WRITING_FW "Nahravani firmware ..." #define TR_BL_WRITING_COMPL "Nahravani dokonceno" + #define TR_BL_WRITING_FAILED TR("Zapis selhal!", "Zapis se nezdaril!") + #define TR_BL_RETRY_OR_DFU TR("Zkuste znovu / DFU", "Zkuste znovu, nebo pouzijte USB (DFU)") #define TR_BL_ENABLE "Povoleno" #define TR_BL_DISABLE "Zakazano" @@ -109,6 +111,8 @@ #define TR_BL_DIR_EMPTY "Katalog er tomt" #define TR_BL_WRITING_FW "Installerer..." #define TR_BL_WRITING_COMPL "Installation slut" + #define TR_BL_WRITING_FAILED TR("Skrivning fejlede!", "Skrivning mislykkedes!") + #define TR_BL_RETRY_OR_DFU TR("Prov igen / DFU", "Prov igen, eller brug USB (DFU)") #define TR_BL_ENABLE "Aktiver" #define TR_BL_DISABLE "Deaktiver" @@ -155,6 +159,8 @@ #define TR_BL_DIR_EMPTY "Verzeichnis leer" #define TR_BL_WRITING_FW "Schreibe..." #define TR_BL_WRITING_COMPL TR("Schreiben fertig","Schreiben abgeschlossen") + #define TR_BL_WRITING_FAILED TR("Schreiben Fehler!", "Schreiben fehlgeschlagen!") + #define TR_BL_RETRY_OR_DFU TR("Neu vers. / DFU", "Erneut versuchen, oder USB (DFU)") #define TR_BL_ENABLE "Aktivieren" #define TR_BL_DISABLE "Deaktivieren" @@ -201,6 +207,8 @@ #define TR_BL_DIR_EMPTY "Repertoire vide" #define TR_BL_WRITING_FW "Ecriture Firmware ..." #define TR_BL_WRITING_COMPL "Ecriture terminée" + #define TR_BL_WRITING_FAILED TR("Echec ecriture!", "Echec de l'ecriture!") + #define TR_BL_RETRY_OR_DFU TR("Reessayer / DFU", "Reessayez, ou utilisez USB (DFU)") #define TR_BL_ENABLE "Activer" #define TR_BL_DISABLE "Désactiver" @@ -247,6 +255,8 @@ #define TR_BL_DIR_EMPTY "Cartella vuota" #define TR_BL_WRITING_FW "Scrittura..." #define TR_BL_WRITING_COMPL "Scrittura completata" + #define TR_BL_WRITING_FAILED TR("Scrittura fallita!", "Scrittura fallita!") + #define TR_BL_RETRY_OR_DFU TR("Riprova / DFU", "Riprova, oppure usa USB (DFU)") #define TR_BL_ENABLE "Abilita" #define TR_BL_DISABLE "Disabilita" @@ -293,6 +303,8 @@ #define TR_BL_DIR_EMPTY "Katalog jest pusty" #define TR_BL_WRITING_FW "Zapis firmware ..." #define TR_BL_WRITING_COMPL "Zapis ukonczony" + #define TR_BL_WRITING_FAILED TR("Zapis nieudany!", "Zapis nie powiodl sie!") + #define TR_BL_RETRY_OR_DFU TR("Powtorz / DFU", "Powtorz, lub uzyj USB (DFU)") #define TR_BL_ENABLE "Enable" #define TR_BL_DISABLE "Disable" @@ -340,6 +352,8 @@ #define TR_BL_DIR_EMPTY "Katalogen aer tom" #define TR_BL_WRITING_FW "Skriver..." #define TR_BL_WRITING_COMPL "Skrivning klar" + #define TR_BL_WRITING_FAILED TR("Skrivning fel!", "Skrivningen misslyckades!") + #define TR_BL_RETRY_OR_DFU TR("Forsok igen / DFU", "Forsok igen, eller anvand USB (DFU)") #define TR_BL_ENABLE "Aktivera" #define TR_BL_DISABLE "Inaktivera" @@ -386,6 +400,8 @@ #define TR_BL_DIR_EMPTY "Directory is empty" #define TR_BL_WRITING_FW "Writing..." #define TR_BL_WRITING_COMPL "Writing complete" + #define TR_BL_WRITING_FAILED TR("Writing failed!", "Writing failed!") + #define TR_BL_RETRY_OR_DFU TR("Retry or use DFU", "Retry, or flash over USB (DFU)") #define TR_BL_ENABLE "Enable" #define TR_BL_DISABLE "Disable" From 7b0bc59a23869ffdf134b9bf39cf7e2d07799a0a Mon Sep 17 00:00:00 2001 From: Peter Feerick <5500713+pfeerick@users.noreply.github.com> Date: Sat, 3 Oct 2026 22:38:48 +0000 Subject: [PATCH 18/18] fix(boot): use lcd-> in 2.11 colorlcd boot menu for #7415's flash error screen On 2.11 the colorlcd bootloader's lcd is a BitmapBuffer*, not an object as on main. Co-Authored-By: Claude Opus 5.5 --- radio/src/gui/colorlcd/boot_menu.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/radio/src/gui/colorlcd/boot_menu.cpp b/radio/src/gui/colorlcd/boot_menu.cpp index 88328870886..b9c63b0e0cb 100644 --- a/radio/src/gui/colorlcd/boot_menu.cpp +++ b/radio/src/gui/colorlcd/boot_menu.cpp @@ -356,8 +356,8 @@ void bootloaderDrawScreen(BootloaderState st, int opt, const char* str) opt = 100; // Completed > 100% } else if (st == ST_FLASH_ERROR) { // the bar stays where the write gave up - lcd.drawText(LCD_W / 2, (LCD_H - PROGRESS_H) / 2 - EdgeTxStyles::STD_FONT_HEIGHT, - LV_SYMBOL_CLOSE " " TR_BL_WRITING_FAILED, CENTERED | BL_FOREGROUND); + lcd->drawText(LCD_W / 2, (LCD_H - PROGRESS_H) / 2 - EdgeTxStyles::STD_FONT_HEIGHT, + LV_SYMBOL_CLOSE " " TR_BL_WRITING_FAILED, CENTERED | BL_FOREGROUND); } lcd->drawRect(PROGRESS_X, (LCD_H - PROGRESS_H) / 2, PROGRESS_W, PROGRESS_H, LINE_H, SOLID, BL_SELECTED); @@ -413,7 +413,7 @@ void bootloaderDrawScreen(BootloaderState st, int opt, const char* str) pos = lcd->drawText(FOOTER_X1, LCD_H - FOOTER_Y1, LV_SYMBOL_CHARGE " " TR_BL_WRITING_COMPL, FOOTER_ALIGN1 | BL_FOREGROUND); } else if (st == ST_FLASH_ERROR) { - pos = lcd.drawText(FOOTER_X1, LCD_H - FOOTER_Y1, LV_SYMBOL_CHARGE " " TR_BL_RETRY_OR_DFU, FOOTER_ALIGN1 | BL_FOREGROUND); + pos = lcd->drawText(FOOTER_X1, LCD_H - FOOTER_Y1, LV_SYMBOL_CHARGE " " TR_BL_RETRY_OR_DFU, FOOTER_ALIGN1 | BL_FOREGROUND); } } #if LANDSCAPE