2 * This file is part of Cleanflight and Betaflight.
4 * Cleanflight and Betaflight are free software. You can redistribute
5 * this software and/or modify this software under the terms of the
6 * GNU General Public License as published by the Free Software
7 * Foundation, either version 3 of the License, or (at your option)
10 * Cleanflight and Betaflight are distributed in the hope that they
11 * will be useful, but WITHOUT ANY WARRANTY; without even the implied
12 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
13 * See the GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this software.
18 * If not, see <http://www.gnu.org/licenses/>.
22 Original OSD code created by Marcin Baliniak
23 OSD-CMS separation by jflyper
24 CMS-displayPort separation by jflyper and martinbudden
27 //#define CMS_PAGE_DEBUG // For multi-page/menu debugging
28 //#define CMS_MENU_DEBUG // For external menu content creators
39 #include "build/build_config.h"
40 #include "build/debug.h"
41 #include "build/version.h"
44 #include "cms/cms_menu_main.h"
45 #include "cms/cms_menu_saveexit.h"
46 #include "cms/cms_menu_quick.h"
47 #include "cms/cms_types.h"
49 #include "common/maths.h"
50 #include "common/typeconversion.h"
52 #include "config/config.h"
53 #include "config/feature.h"
54 #include "config/simplified_tuning.h"
56 #include "drivers/motor.h"
57 #include "drivers/osd_symbols.h"
58 #include "drivers/system.h"
59 #include "drivers/time.h"
61 #include "fc/rc_controls.h"
62 #include "fc/runtime_config.h"
64 #include "flight/mixer.h"
66 #include "io/rcdevice_cam.h"
67 #include "io/usb_cdc_hid.h"
70 #include "pg/pg_ids.h"
77 #include "sensors/gyro.h"
79 // DisplayPort management
81 #ifndef CMS_MAX_DEVICE
82 #define CMS_MAX_DEVICE 4
85 #define CMS_MENU_STACK_LIMIT 10
87 displayPort_t
*pCurrentDisplay
;
89 static displayPort_t
*cmsDisplayPorts
[CMS_MAX_DEVICE
];
90 static unsigned cmsDeviceCount
;
91 static int cmsCurrentDevice
= -1;
93 static unsigned int osdProfileCursor
= 1;
98 bool cmsDisplayPortRegister(displayPort_t
*pDisplay
)
100 if (!pDisplay
|| cmsDeviceCount
>= CMS_MAX_DEVICE
) {
104 cmsDisplayPorts
[cmsDeviceCount
++] = pDisplay
;
109 static displayPort_t
*cmsDisplayPortSelectCurrent(void)
111 if (cmsDeviceCount
== 0) {
115 if (cmsCurrentDevice
< 0) {
116 cmsCurrentDevice
= 0;
119 return cmsDisplayPorts
[cmsCurrentDevice
];
122 static displayPort_t
*cmsDisplayPortSelectNext(void)
124 if (cmsDeviceCount
== 0) {
128 cmsCurrentDevice
= (cmsCurrentDevice
+ 1) % cmsDeviceCount
; // -1 Okay
130 return cmsDisplayPorts
[cmsCurrentDevice
];
133 bool cmsDisplayPortSelect(displayPort_t
*instance
)
135 for (unsigned i
= 0; i
< cmsDeviceCount
; i
++) {
136 if (cmsDisplayPortSelectNext() == instance
) {
143 #define CMS_POLL_INTERVAL_US 100000 // Interval of polling dynamic values (microsec)
145 // XXX LEFT_MENU_COLUMN and RIGHT_MENU_COLUMN must be adjusted
146 // dynamically depending on size of the active output device,
147 // or statically to accomodate sizes of all supported devices.
149 // Device characteristics
152 // 128x64 with 5x7 (6x8) : 21 cols x 8 rows
157 // HoTT Telemetry Screen
161 // Spektrum SRXL Telemtry Textgenerator
162 // 13 cols x 9 rows, top row printed as a Bold Heading
163 // Needs the "smallScreen" adaptions
165 #define CMS_MAX_ROWS 31
167 #define NORMAL_SCREEN_MIN_COLS 18 // Less is a small screen
168 #define NORMAL_SCREEN_MAX_COLS 30 // More is a large screen
169 static bool smallScreen
;
170 static uint8_t leftMenuColumn
;
171 static uint8_t rightMenuColumn
;
172 static uint8_t maxMenuItems
;
173 static uint8_t linesPerMenuItem
;
174 static cms_key_e externKey
= CMS_KEY_NONE
;
175 static bool osdElementEditing
= false;
177 bool cmsInMenu
= false;
179 typedef struct cmsCtx_s
{
180 const CMS_Menu
*menu
; // menu for this context
181 uint8_t page
; // page in the menu
182 int8_t cursorRow
; // cursorRow in the page
185 static cmsCtx_t menuStack
[CMS_MENU_STACK_LIMIT
];
186 static uint8_t menuStackIdx
= 0;
188 static int8_t pageCount
; // Number of pages in the current menu
189 static const OSD_Entry
*pageTop
; // First entry for the current page
190 static uint8_t pageMaxRow
; // Max row in the current page
192 static cmsCtx_t currentCtx
;
194 static bool saveMenuInhibited
= false;
196 #ifdef CMS_MENU_DEBUG // For external menu content creators
198 static char menuErrLabel
[21 + 1] = "RANDOM DATA";
200 static OSD_Entry menuErrEntries
[] = {
201 { "BROKEN MENU", OME_Label
, NULL
, NULL
},
202 { menuErrLabel
, OME_Label
, NULL
, NULL
},
203 { "BACK", OME_Back
, NULL
, NULL
},
204 { NULL
, OME_END
, NULL
, NULL
}
207 static CMS_Menu menuErr
= {
217 #ifdef CMS_PAGE_DEBUG
218 #define cmsPageDebug() { \
219 debug[0] = pageCount; \
220 debug[1] = currentCtx.page; \
221 debug[2] = pageMaxRow; \
222 debug[3] = currentCtx.cursorRow; } struct _dummy
225 static void cmsUpdateMaxRow(displayPort_t
*instance
)
230 for (const OSD_Entry
*ptr
= pageTop
; (ptr
->flags
& OSD_MENU_ELEMENT_MASK
) != OME_END
; ptr
++) {
234 if (pageMaxRow
> maxMenuItems
) {
235 pageMaxRow
= maxMenuItems
;
238 if (pageMaxRow
> CMS_MAX_ROWS
) {
239 pageMaxRow
= CMS_MAX_ROWS
;
245 static uint8_t cmsCursorAbsolute(displayPort_t
*instance
)
248 return currentCtx
.cursorRow
+ currentCtx
.page
* maxMenuItems
;
251 uint8_t runtimeEntryFlags
[CMS_MAX_ROWS
] = { 0 };
253 #define LOOKUP_TABLE_TICKER_START_CYCLES 20 // Task loops for start/end of ticker (1 second delay)
254 #define LOOKUP_TABLE_TICKER_SCROLL_CYCLES 3 // Task loops for each scrolling step of the ticker (150ms delay)
256 typedef struct cmsTableTicker_s
{
261 cmsTableTicker_t runtimeTableTicker
[CMS_MAX_ROWS
];
263 static void cmsPageSelect(displayPort_t
*instance
, int8_t newpage
)
265 currentCtx
.page
= (newpage
+ pageCount
) % pageCount
;
266 pageTop
= ¤tCtx
.menu
->entries
[currentCtx
.page
* maxMenuItems
];
267 cmsUpdateMaxRow(instance
);
271 for (p
= pageTop
, i
= 0; (p
<= pageTop
+ pageMaxRow
); p
++, i
++) {
272 runtimeEntryFlags
[i
] = p
->flags
;
274 displayClearScreen(instance
, DISPLAY_CLEAR_WAIT
);
277 static void cmsPageNext(displayPort_t
*instance
)
279 cmsPageSelect(instance
, currentCtx
.page
+ 1);
282 static void cmsPagePrev(displayPort_t
*instance
)
284 cmsPageSelect(instance
, currentCtx
.page
- 1);
287 static void cmsFormatFloat(int32_t value
, char *floatString
)
292 itoa(100000 + value
, floatString
, 10); // Create string from abs of integer value
296 floatString
[0] = floatString
[1];
297 floatString
[1] = floatString
[2];
298 floatString
[2] = '.';
301 // usuwam koncowe zera i kropke
302 // Keep the first decimal place
303 for (k
= 5; k
> 3; k
--) {
304 if (floatString
[k
] == '0' || floatString
[k
] == '.') {
311 // oraz zero wiodonce
312 if (floatString
[0] == '0') {
313 floatString
[0] = ' ';
317 // CMS on OSD legacy was to use LEFT aligned values, not the RIGHT way ;-)
318 #define CMS_OSD_RIGHT_ALIGNED_VALUES
320 #ifndef CMS_OSD_RIGHT_ALIGNED_VALUES
322 // Pad buffer to the left, i.e. align left
323 static void cmsPadRightToSize(char *buf
, int size
)
327 for (i
= 0 ; i
< size
; i
++) {
333 for ( ; i
< size
; i
++) {
341 // Pad buffer to the left, i.e. align right
342 static void cmsPadLeftToSize(char *buf
, int size
)
345 int len
= strlen(buf
);
347 for (i
= size
- 1, j
= size
- len
; i
- j
>= 0 ; i
--) {
351 for ( ; i
>= 0 ; i
--) {
358 static void cmsPadToSize(char *buf
, int size
)
360 // Make absolutely sure the string terminated.
363 #ifdef CMS_OSD_RIGHT_ALIGNED_VALUES
364 cmsPadLeftToSize(buf
, size
);
366 smallScreen
? cmsPadLeftToSize(buf
, size
) : cmsPadRightToSize(buf
, size
);
370 static int cmsDisplayWrite(displayPort_t
*instance
, uint8_t x
, uint8_t y
, uint8_t attr
, const char *s
)
372 char buffer
[strlen(s
) + 1];
375 char c
= toupper(*s
++);
376 *b
++ = (c
< 0x20 || c
> 0x5F) ? ' ' : c
; // limit to alphanumeric and punctuation
380 return displayWrite(instance
, x
, y
, attr
, buffer
);
383 static int cmsDrawMenuItemValue(displayPort_t
*pDisplay
, char *buff
, uint8_t row
, uint8_t maxSize
)
388 cmsPadToSize(buff
, maxSize
);
389 #ifdef CMS_OSD_RIGHT_ALIGNED_VALUES
390 colpos
= rightMenuColumn
- maxSize
;
392 colpos
= smallScreen
? rightMenuColumn
- maxSize
: rightMenuColumn
;
394 cnt
= cmsDisplayWrite(pDisplay
, colpos
, row
, DISPLAYPORT_SEVERITY_NORMAL
, buff
);
398 static int cmsDrawMenuEntry(displayPort_t
*pDisplay
, const OSD_Entry
*p
, uint8_t row
, bool selectedRow
, uint8_t *flags
, cmsTableTicker_t
*ticker
)
400 #define CMS_DRAW_BUFFER_LEN 12
401 #define CMS_TABLE_VALUE_MAX_LEN 30
402 #define CMS_NUM_FIELD_LEN 5
403 #define CMS_CURSOR_BLINK_DELAY_MS 500
405 char buff
[CMS_DRAW_BUFFER_LEN
+1]; // Make room for null terminator.
406 char tableBuff
[CMS_TABLE_VALUE_MAX_LEN
+1];
417 switch (p
->flags
& OSD_MENU_ELEMENT_MASK
) {
419 if (IS_PRINTVALUE(*flags
) && p
->data
) {
420 strncpy(buff
, p
->data
, CMS_DRAW_BUFFER_LEN
);
421 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, CMS_DRAW_BUFFER_LEN
);
422 CLR_PRINTVALUE(*flags
);
428 if (IS_PRINTVALUE(*flags
)) {
431 if ((p
->flags
& OSD_MENU_ELEMENT_MASK
) == OME_Submenu
&& p
->func
&& *flags
& OPTSTRING
) {
433 // Special case of sub menu entry with optional value display.
435 const char *str
= p
->func(pDisplay
, p
->data
);
436 strncpy(buff
, str
, CMS_DRAW_BUFFER_LEN
);
437 } else if ((p
->flags
& OSD_MENU_ELEMENT_MASK
) == OME_Funcall
&& p
->data
) {
438 strncpy(buff
, p
->data
, CMS_DRAW_BUFFER_LEN
);
440 strncat(buff
, ">", CMS_DRAW_BUFFER_LEN
);
442 row
= smallScreen
? row
- 1 : row
;
443 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, strlen(buff
));
444 CLR_PRINTVALUE(*flags
);
449 if (IS_PRINTVALUE(*flags
) && p
->data
) {
450 if (*((uint8_t *)(p
->data
))) {
456 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, 3);
457 CLR_PRINTVALUE(*flags
);
462 if (IS_PRINTVALUE(*flags
) || IS_SCROLLINGTICKER(*flags
)) {
463 bool drawText
= false;
464 OSD_TAB_t
*ptr
= p
->data
;
465 const int labelLength
= strlen(p
->text
) + 1; // account for the space between label and display data
466 char *str
= (char *)ptr
->names
[*ptr
->val
]; // lookup table display text
467 const int displayLength
= strlen(str
);
469 // Calculate the available space to display the lookup table entry based on the
470 // screen size and the length of the label. Always display at least CMS_DRAW_BUFFER_LEN
471 // characters to prevent really long labels from overriding the data display.
472 const int availableSpace
= MAX(CMS_DRAW_BUFFER_LEN
, rightMenuColumn
- labelLength
- leftMenuColumn
- 1);
474 if (IS_PRINTVALUE(*flags
)) {
477 ticker
->loopCounter
= 0;
478 if (displayLength
> availableSpace
) { // table entry text is longer than the available space so start the ticker
479 SET_SCROLLINGTICKER(*flags
);
481 CLR_SCROLLINGTICKER(*flags
);
483 } else if (IS_SCROLLINGTICKER(*flags
)) {
484 ticker
->loopCounter
++;
485 const uint8_t loopLimit
= (ticker
->state
== 0 || ticker
->state
== (displayLength
- availableSpace
)) ? LOOKUP_TABLE_TICKER_START_CYCLES
: LOOKUP_TABLE_TICKER_SCROLL_CYCLES
;
486 if (ticker
->loopCounter
>= loopLimit
) {
487 ticker
->loopCounter
= 0;
490 if (ticker
->state
> (displayLength
- availableSpace
)) {
496 strncpy(tableBuff
, (char *)(str
+ ticker
->state
), CMS_TABLE_VALUE_MAX_LEN
);
497 cnt
= cmsDrawMenuItemValue(pDisplay
, tableBuff
, row
, availableSpace
);
499 CLR_PRINTVALUE(*flags
);
505 if (IS_PRINTVALUE(*flags
) && p
->data
) {
506 uint16_t *val
= (uint16_t *)p
->data
;
507 bool cursorBlink
= millis() % (2 * CMS_CURSOR_BLINK_DELAY_MS
) < CMS_CURSOR_BLINK_DELAY_MS
;
508 for (unsigned x
= 1; x
< OSD_PROFILE_COUNT
+ 1; x
++) {
509 if (VISIBLE_IN_OSD_PROFILE(*val
, x
)) {
510 if (osdElementEditing
&& cursorBlink
&& selectedRow
&& (x
== osdProfileCursor
)) {
511 strcpy(buff
+ x
- 1, " ");
513 strcpy(buff
+ x
- 1, "X");
516 if (osdElementEditing
&& cursorBlink
&& selectedRow
&& (x
== osdProfileCursor
)) {
517 strcpy(buff
+ x
- 1, " ");
519 strcpy(buff
+ x
- 1, "-");
523 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, 3);
524 CLR_PRINTVALUE(*flags
);
530 if (IS_PRINTVALUE(*flags
) && p
->data
) {
531 OSD_UINT8_t
*ptr
= p
->data
;
532 itoa(*ptr
->val
, buff
, 10);
533 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, CMS_NUM_FIELD_LEN
);
534 CLR_PRINTVALUE(*flags
);
539 if (IS_PRINTVALUE(*flags
) && p
->data
) {
540 OSD_INT8_t
*ptr
= p
->data
;
541 itoa(*ptr
->val
, buff
, 10);
542 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, CMS_NUM_FIELD_LEN
);
543 CLR_PRINTVALUE(*flags
);
548 if (IS_PRINTVALUE(*flags
) && p
->data
) {
549 OSD_UINT16_t
*ptr
= p
->data
;
550 itoa(*ptr
->val
, buff
, 10);
551 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, CMS_NUM_FIELD_LEN
);
552 CLR_PRINTVALUE(*flags
);
557 if (IS_PRINTVALUE(*flags
) && p
->data
) {
558 OSD_INT16_t
*ptr
= p
->data
;
559 itoa(*ptr
->val
, buff
, 10);
560 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, CMS_NUM_FIELD_LEN
);
561 CLR_PRINTVALUE(*flags
);
566 if (IS_PRINTVALUE(*flags
) && p
->data
) {
567 OSD_UINT32_t
*ptr
= p
->data
;
568 itoa(*ptr
->val
, buff
, 10);
569 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, CMS_NUM_FIELD_LEN
);
570 CLR_PRINTVALUE(*flags
);
575 if (IS_PRINTVALUE(*flags
) && p
->data
) {
576 OSD_INT32_t
*ptr
= p
->data
;
577 itoa(*ptr
->val
, buff
, 10);
578 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, CMS_NUM_FIELD_LEN
);
579 CLR_PRINTVALUE(*flags
);
584 if (IS_PRINTVALUE(*flags
) && p
->data
) {
585 OSD_FLOAT_t
*ptr
= p
->data
;
586 cmsFormatFloat(*ptr
->val
* ptr
->multipler
, buff
);
587 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, CMS_NUM_FIELD_LEN
);
588 CLR_PRINTVALUE(*flags
);
593 if (IS_PRINTVALUE(*flags
) && p
->data
) {
594 // A label with optional string, immediately following text
595 cnt
= cmsDisplayWrite(pDisplay
, leftMenuColumn
+ 1 + (uint8_t)strlen(p
->text
), row
, DISPLAYPORT_SEVERITY_NORMAL
, p
->data
);
596 CLR_PRINTVALUE(*flags
);
608 #ifdef CMS_MENU_DEBUG
609 // Shouldn't happen. Notify creator of this menu content
610 #ifdef CMS_OSD_RIGHT_ALIGNED_VALUES
611 cnt
= cmsDisplayWrite(pDisplay
, rightMenuColumn
- 6, row
, DISPLAYPORT_SEVERITY_NORMAL
, "BADENT");
613 cnt
= cmsDisplayWrite(pDisplay
, rightMenuColumn
, row
, DISPLAYPORT_SEVERITY_NORMAL
, "BADENT");
622 static void cmsMenuCountPage(displayPort_t
*pDisplay
)
626 for (p
= currentCtx
.menu
->entries
; (p
->flags
& OSD_MENU_ELEMENT_MASK
) != OME_END
; p
++);
627 pageCount
= (p
- currentCtx
.menu
->entries
- 1) / maxMenuItems
+ 1;
630 STATIC_UNIT_TESTED
const void *cmsMenuBack(displayPort_t
*pDisplay
)
632 // Let onExit function decide whether to allow exit or not.
633 if (currentCtx
.menu
->onExit
) {
634 const void *result
= currentCtx
.menu
->onExit(pDisplay
, pageTop
+ currentCtx
.cursorRow
);
635 if (result
== MENU_CHAIN_BACK
) {
640 saveMenuInhibited
= false;
646 currentCtx
= menuStack
[--menuStackIdx
];
648 cmsMenuCountPage(pDisplay
);
649 cmsPageSelect(pDisplay
, currentCtx
.page
);
651 #if defined(CMS_PAGE_DEBUG)
658 // Check if overridden by slider
659 static bool rowSliderOverride(const uint16_t flags
)
664 pidSimplifiedTuningMode_e simplified_pids_mode
= currentPidProfile
->simplified_pids_mode
;
666 bool slider_flags_mode_rpy
= (simplified_pids_mode
== PID_SIMPLIFIED_TUNING_RPY
);
667 bool slider_flags_mode_rp
= slider_flags_mode_rpy
|| (simplified_pids_mode
== PID_SIMPLIFIED_TUNING_RP
);
669 bool simplified_gyro_filter
= gyroConfig()->simplified_gyro_filter
;
670 bool simplified_dterm_filter
= currentPidProfile
->simplified_dterm_filter
;
672 if (((flags
& SLIDER_RP
) && slider_flags_mode_rp
) ||
673 ((flags
& SLIDER_RPY
) && slider_flags_mode_rpy
) ||
674 ((flags
& SLIDER_GYRO
) && simplified_gyro_filter
) ||
675 ((flags
& SLIDER_DTERM
) && simplified_dterm_filter
)) {
683 // Skip read-only entries
684 static bool rowIsSkippable(const OSD_Entry
*row
)
686 OSD_MenuElement type
= row
->flags
& OSD_MENU_ELEMENT_MASK
;
688 if (type
== OME_Label
) {
692 if (type
== OME_String
) {
696 if ((type
== OME_UINT8
|| type
== OME_INT8
||
697 type
== OME_UINT16
|| type
== OME_INT16
) &&
698 ((row
->flags
== DYNAMIC
) || rowSliderOverride(row
->flags
))) {
704 static void cmsDrawMenu(displayPort_t
*pDisplay
, uint32_t currentTimeUs
)
706 if (!pageTop
|| !cmsInMenu
) {
710 const bool displayWasCleared
= pDisplay
->cleared
;
713 uint8_t top
= smallScreen
? 1 : (pDisplay
->rows
- pageMaxRow
)/2;
715 pDisplay
->cleared
= false;
717 // Polled (dynamic) value display denominator.
719 bool drawPolled
= false;
720 static uint32_t lastPolledUs
= 0;
722 if (currentTimeUs
> lastPolledUs
+ CMS_POLL_INTERVAL_US
) {
724 lastPolledUs
= currentTimeUs
;
727 uint32_t room
= displayTxBytesFree(pDisplay
);
729 if (displayWasCleared
) {
730 for (p
= pageTop
, i
= 0; (p
<= pageTop
+ pageMaxRow
); p
++, i
++) {
731 SET_PRINTLABEL(runtimeEntryFlags
[i
]);
732 SET_PRINTVALUE(runtimeEntryFlags
[i
]);
734 } else if (drawPolled
) {
735 for (p
= pageTop
, i
= 0; (p
<= pageTop
+ pageMaxRow
); p
++, i
++) {
737 SET_PRINTVALUE(runtimeEntryFlags
[i
]);
741 // Cursor manipulation
743 while (rowIsSkippable(pageTop
+ currentCtx
.cursorRow
)) { // skip labels, strings and dynamic read-only entries
744 currentCtx
.cursorRow
++;
747 #if defined(CMS_PAGE_DEBUG)
751 if (pDisplay
->cursorRow
>= 0 && currentCtx
.cursorRow
!= pDisplay
->cursorRow
) {
752 room
-= cmsDisplayWrite(pDisplay
, leftMenuColumn
, top
+ pDisplay
->cursorRow
* linesPerMenuItem
, DISPLAYPORT_SEVERITY_NORMAL
, " ");
759 if (pDisplay
->cursorRow
!= currentCtx
.cursorRow
) {
760 room
-= cmsDisplayWrite(pDisplay
, leftMenuColumn
, top
+ currentCtx
.cursorRow
* linesPerMenuItem
, DISPLAYPORT_SEVERITY_NORMAL
, ">");
761 pDisplay
->cursorRow
= currentCtx
.cursorRow
;
768 if (currentCtx
.menu
->onDisplayUpdate
) {
769 const void *result
= currentCtx
.menu
->onDisplayUpdate(pDisplay
, pageTop
+ currentCtx
.cursorRow
);
770 if (result
== MENU_CHAIN_BACK
) {
771 cmsMenuBack(pDisplay
);
778 for (i
= 0, p
= pageTop
; (p
<= pageTop
+ pageMaxRow
); i
++, p
++) {
779 if (IS_PRINTLABEL(runtimeEntryFlags
[i
])) {
780 uint8_t coloff
= leftMenuColumn
;
781 coloff
+= ((p
->flags
& OSD_MENU_ELEMENT_MASK
) == OME_Label
) ? 0 : 1;
782 room
-= cmsDisplayWrite(pDisplay
, coloff
, top
+ i
* linesPerMenuItem
, DISPLAYPORT_SEVERITY_NORMAL
, p
->text
);
783 CLR_PRINTLABEL(runtimeEntryFlags
[i
]);
790 // Highlight values overridden by sliders
791 if (rowSliderOverride(p
->flags
)) {
792 displayWriteChar(pDisplay
, leftMenuColumn
- 1, top
+ i
* linesPerMenuItem
, DISPLAYPORT_SEVERITY_NORMAL
, 'S');
797 // XXX Polled values at latter positions in the list may not be
798 // XXX printed if not enough room in the middle of the list.
800 if (IS_PRINTVALUE(runtimeEntryFlags
[i
]) || IS_SCROLLINGTICKER(runtimeEntryFlags
[i
])) {
801 bool selectedRow
= i
== currentCtx
.cursorRow
;
802 room
-= cmsDrawMenuEntry(pDisplay
, p
, top
+ i
* linesPerMenuItem
, selectedRow
, &runtimeEntryFlags
[i
], &runtimeTableTicker
[i
]);
809 // Draw the up/down page indicators if the display has space.
810 // Only draw the symbols when necessary after the screen has been cleared. Otherwise they're static.
811 // If the device supports OSD symbols then use the up/down arrows. Otherwise assume it's a
812 // simple text device and use the '^' (carat) and 'V' for arrow approximations.
813 if (displayWasCleared
&& leftMenuColumn
> 0) { // make sure there's room to draw the symbol
814 if (currentCtx
.page
> 0) {
815 const uint8_t symbol
= displaySupportsOsdSymbols(pDisplay
) ? SYM_ARROW_SMALL_UP
: '^';
816 displayWriteChar(pDisplay
, leftMenuColumn
- 1, top
, DISPLAYPORT_SEVERITY_NORMAL
, symbol
);
818 if (currentCtx
.page
< pageCount
- 1) {
819 const uint8_t symbol
= displaySupportsOsdSymbols(pDisplay
) ? SYM_ARROW_SMALL_DOWN
: 'v';
820 displayWriteChar(pDisplay
, leftMenuColumn
- 1, top
+ pageMaxRow
, DISPLAYPORT_SEVERITY_NORMAL
, symbol
);
826 const void *cmsMenuChange(displayPort_t
*pDisplay
, const void *ptr
)
828 const CMS_Menu
*pMenu
= (const CMS_Menu
*)ptr
;
834 #ifdef CMS_MENU_DEBUG
835 if (pMenu
->GUARD_type
!= OME_MENU
) {
836 // ptr isn't pointing to a CMS_Menu.
837 if (pMenu
->GUARD_type
<= OME_MAX
) {
838 strncpy(menuErrLabel
, pMenu
->GUARD_text
, sizeof(menuErrLabel
) - 1);
840 strncpy(menuErrLabel
, "LABEL UNKNOWN", sizeof(menuErrLabel
) - 1);
846 if (pMenu
!= currentCtx
.menu
) {
847 saveMenuInhibited
= false;
849 if (currentCtx
.menu
&& pMenu
!= &cmsx_menuMain
) {
850 // If we are opening the initial top-level menu, then currentCtx.menu will be NULL and nothing to do.
851 // Otherwise stack the current menu before moving to the selected menu.
852 if (menuStackIdx
>= CMS_MENU_STACK_LIMIT
- 1) {
853 // menu stack limit reached - prevent array overflow
856 menuStack
[menuStackIdx
++] = currentCtx
;
859 currentCtx
.menu
= pMenu
;
860 currentCtx
.cursorRow
= 0;
862 if (pMenu
->onEnter
) {
863 const void *result
= pMenu
->onEnter(pDisplay
);
864 if (result
== MENU_CHAIN_BACK
) {
865 return cmsMenuBack(pDisplay
);
869 cmsMenuCountPage(pDisplay
);
870 cmsPageSelect(pDisplay
, 0);
872 // The (pMenu == curretMenu) case occurs when reopening for display cycling
873 // currentCtx.cursorRow has been saved as absolute; convert it back to page + relative
875 int8_t cursorAbs
= currentCtx
.cursorRow
;
876 currentCtx
.cursorRow
= cursorAbs
% maxMenuItems
;
877 cmsMenuCountPage(pDisplay
);
878 cmsPageSelect(pDisplay
, cursorAbs
/ maxMenuItems
);
881 #if defined(CMS_PAGE_DEBUG)
888 void cmsMenuOpen(void)
890 const CMS_Menu
*startMenu
;
893 pCurrentDisplay
= cmsDisplayPortSelectCurrent();
894 if (!pCurrentDisplay
) {
898 currentCtx
= (cmsCtx_t
){ NULL
, 0, 0 };
899 startMenu
= &cmsx_menuMain
;
901 #ifdef USE_OSD_QUICK_MENU
902 if (osdConfig()->osd_use_quick_menu
) {
903 startMenu
= &cmsx_menuQuick
;
905 #endif // USE_OSD_QUICK_MENU
908 setArmingDisabled(ARMING_DISABLED_CMS_MENU
);
909 displayLayerSelect(pCurrentDisplay
, DISPLAYPORT_LAYER_FOREGROUND
); // make sure the foreground layer is active
911 if (osdConfig()->cms_background_type
!= DISPLAY_BACKGROUND_TRANSPARENT
) {
912 displaySetBackgroundType(pCurrentDisplay
, (displayPortBackground_e
)osdConfig()->cms_background_type
); // set the background type if not transparent
917 displayPort_t
*pNextDisplay
= cmsDisplayPortSelectNext();
918 startMenu
= currentCtx
.menu
;
919 if (pNextDisplay
!= pCurrentDisplay
) {
920 // DisplayPort has been changed.
921 // Convert cursorRow to absolute value
922 currentCtx
.cursorRow
= cmsCursorAbsolute(pCurrentDisplay
);
923 displaySetBackgroundType(pCurrentDisplay
, DISPLAY_BACKGROUND_TRANSPARENT
); // reset previous displayPort to transparent
924 displayRelease(pCurrentDisplay
);
925 pCurrentDisplay
= pNextDisplay
;
927 displaySetBackgroundType(pCurrentDisplay
, (displayPortBackground_e
)osdConfig()->cms_background_type
); // set the background type if not transparent
933 displayGrab(pCurrentDisplay
); // grab the display for use by the CMS
934 // FIXME this should probably not have a dependency on the OSD or OSD slave code
939 if ( pCurrentDisplay
->cols
< NORMAL_SCREEN_MIN_COLS
) {
941 linesPerMenuItem
= 2;
943 rightMenuColumn
= pCurrentDisplay
->cols
;
944 maxMenuItems
= (pCurrentDisplay
->rows
) / linesPerMenuItem
;
947 linesPerMenuItem
= 1;
948 if (pCurrentDisplay
->cols
<= NORMAL_SCREEN_MAX_COLS
) {
950 #ifdef CMS_OSD_RIGHT_ALIGNED_VALUES
951 rightMenuColumn
= pCurrentDisplay
->cols
- 2;
953 rightMenuColumn
= pCurrentDisplay
->cols
- CMS_DRAW_BUFFER_LEN
;
956 leftMenuColumn
= (pCurrentDisplay
->cols
/ 2) - 13;
957 #ifdef CMS_OSD_RIGHT_ALIGNED_VALUES
958 rightMenuColumn
= (pCurrentDisplay
->cols
/ 2) + 13;
960 rightMenuColumn
= pCurrentDisplay
->cols
- CMS_DRAW_BUFFER_LEN
;
963 maxMenuItems
= pCurrentDisplay
->rows
- 2;
966 if (pCurrentDisplay
->useFullscreen
) {
968 rightMenuColumn
= pCurrentDisplay
->cols
;
969 maxMenuItems
= pCurrentDisplay
->rows
;
972 cmsMenuChange(pCurrentDisplay
, startMenu
);
975 static void cmsTraverseGlobalExit(const CMS_Menu
*pMenu
)
977 for (const OSD_Entry
*p
= pMenu
->entries
; (p
->flags
& OSD_MENU_ELEMENT_MASK
) != OME_END
; p
++) {
978 if ((p
->flags
& OSD_MENU_ELEMENT_MASK
) == OME_Submenu
) {
979 cmsTraverseGlobalExit(p
->data
);
985 const void *cmsMenuExit(displayPort_t
*pDisplay
, const void *ptr
)
987 int exitType
= (intptr_t)ptr
;
990 case CMS_EXIT_SAVEREBOOT
:
992 case CMS_POPUP_SAVEREBOOT
:
994 cmsTraverseGlobalExit(&cmsx_menuMain
);
996 if (currentCtx
.menu
->onExit
) {
997 currentCtx
.menu
->onExit(pDisplay
, (OSD_Entry
*)NULL
); // Forced exit
1000 if ((exitType
== CMS_POPUP_SAVE
) || (exitType
== CMS_POPUP_SAVEREBOOT
)) {
1001 // traverse through the menu stack and call their onExit functions
1002 for (int i
= menuStackIdx
- 1; i
>= 0; i
--) {
1003 if (menuStack
[i
].menu
->onExit
) {
1004 menuStack
[i
].menu
->onExit(pDisplay
, (OSD_Entry
*)NULL
);
1009 saveConfigAndNotify();
1018 displaySetBackgroundType(pCurrentDisplay
, DISPLAY_BACKGROUND_TRANSPARENT
); // reset the background to transparent
1020 displayRelease(pDisplay
);
1021 currentCtx
.menu
= NULL
;
1023 if ((exitType
== CMS_EXIT_SAVEREBOOT
) || (exitType
== CMS_POPUP_SAVEREBOOT
) || (exitType
== CMS_POPUP_EXITREBOOT
)) {
1024 displayClearScreen(pDisplay
, DISPLAY_CLEAR_WAIT
);
1025 cmsDisplayWrite(pDisplay
, 5, 3, DISPLAYPORT_SEVERITY_NORMAL
, "REBOOTING...");
1028 displayRedraw(pDisplay
);
1037 unsetArmingDisabled(ARMING_DISABLED_CMS_MENU
);
1042 // Stick/key detection and key codes
1044 #define IS_HI(X) (rcData[X] > 1750)
1045 #define IS_LO(X) (rcData[X] < 1250)
1046 #define IS_MID(X) (rcData[X] > 1250 && rcData[X] < 1750)
1048 #define BUTTON_TIME 250 // msec
1049 #define BUTTON_PAUSE 500 // msec
1051 STATIC_UNIT_TESTED
uint16_t cmsHandleKey(displayPort_t
*pDisplay
, cms_key_e key
)
1053 uint16_t res
= BUTTON_TIME
;
1056 if (!currentCtx
.menu
) {
1060 if (key
== CMS_KEY_MENU
) {
1062 return BUTTON_PAUSE
;
1065 if (key
== CMS_KEY_ESC
) {
1066 if (osdElementEditing
) {
1067 osdElementEditing
= false;
1069 cmsMenuBack(pDisplay
);
1071 return BUTTON_PAUSE
;
1074 if (key
== CMS_KEY_SAVEMENU
&& !saveMenuInhibited
) {
1075 osdElementEditing
= false;
1076 cmsMenuChange(pDisplay
, getSaveExitMenu());
1078 return BUTTON_PAUSE
;
1081 if ((key
== CMS_KEY_DOWN
) && (!osdElementEditing
)) {
1082 if (currentCtx
.cursorRow
< pageMaxRow
) {
1083 currentCtx
.cursorRow
++;
1085 cmsPageNext(pDisplay
);
1086 currentCtx
.cursorRow
= 0; // Goto top in any case
1090 if ((key
== CMS_KEY_UP
) && (!osdElementEditing
)) {
1091 currentCtx
.cursorRow
--;
1093 // Skip non-title labels, strings and dynamic read-only entries
1094 while ((rowIsSkippable(pageTop
+ currentCtx
.cursorRow
)) && currentCtx
.cursorRow
> 0) {
1095 currentCtx
.cursorRow
--;
1097 if (currentCtx
.cursorRow
== -1 || ((pageTop
+ currentCtx
.cursorRow
)->flags
& OSD_MENU_ELEMENT_MASK
) == OME_Label
) {
1098 // Goto previous page
1099 cmsPagePrev(pDisplay
);
1100 currentCtx
.cursorRow
= pageMaxRow
;
1104 if ((key
== CMS_KEY_DOWN
|| key
== CMS_KEY_UP
) && (!osdElementEditing
)) {
1108 p
= pageTop
+ currentCtx
.cursorRow
;
1110 switch (p
->flags
& OSD_MENU_ELEMENT_MASK
) {
1112 if (key
== CMS_KEY_RIGHT
) {
1113 cmsMenuChange(pDisplay
, p
->data
);
1120 if (p
->func
&& key
== CMS_KEY_RIGHT
) {
1121 retval
= p
->func(pDisplay
, p
->data
);
1122 if (retval
== MENU_CHAIN_BACK
) {
1123 cmsMenuBack(pDisplay
);
1125 if ((p
->flags
& REBOOT_REQUIRED
)) {
1126 setRebootRequired();
1133 if (p
->func
&& key
== CMS_KEY_RIGHT
) {
1134 p
->func(pDisplay
, p
->data
);
1140 cmsMenuBack(pDisplay
);
1142 osdElementEditing
= false;
1147 uint8_t *val
= p
->data
;
1148 const uint8_t previousValue
= *val
;
1149 *val
= (key
== CMS_KEY_RIGHT
) ? 1 : 0;
1150 SET_PRINTVALUE(runtimeEntryFlags
[currentCtx
.cursorRow
]);
1151 if ((p
->flags
& REBOOT_REQUIRED
) && (*val
!= previousValue
)) {
1152 setRebootRequired();
1155 p
->func(pDisplay
, p
->data
);
1163 uint16_t *val
= (uint16_t *)p
->data
;
1164 const uint16_t previousValue
= *val
;
1165 if ((key
== CMS_KEY_RIGHT
) && (!osdElementEditing
)) {
1166 osdElementEditing
= true;
1167 osdProfileCursor
= 1;
1168 } else if (osdElementEditing
) {
1169 #ifdef USE_OSD_PROFILES
1170 if (key
== CMS_KEY_RIGHT
) {
1171 if (osdProfileCursor
< OSD_PROFILE_COUNT
) {
1175 if (key
== CMS_KEY_LEFT
) {
1176 if (osdProfileCursor
> 1) {
1181 if (key
== CMS_KEY_UP
) {
1182 *val
|= OSD_PROFILE_FLAG(osdProfileCursor
);
1184 if (key
== CMS_KEY_DOWN
) {
1185 *val
&= ~OSD_PROFILE_FLAG(osdProfileCursor
);
1188 SET_PRINTVALUE(runtimeEntryFlags
[currentCtx
.cursorRow
]);
1189 if ((p
->flags
& REBOOT_REQUIRED
) && (*val
!= previousValue
)) {
1190 setRebootRequired();
1199 OSD_UINT8_t
*ptr
= p
->data
;
1200 const uint16_t previousValue
= *ptr
->val
;
1201 if (key
== CMS_KEY_RIGHT
) {
1202 if (*ptr
->val
< ptr
->max
) {
1203 *ptr
->val
+= ptr
->step
;
1206 if (*ptr
->val
> ptr
->min
) {
1207 *ptr
->val
-= ptr
->step
;
1210 SET_PRINTVALUE(runtimeEntryFlags
[currentCtx
.cursorRow
]);
1211 if ((p
->flags
& REBOOT_REQUIRED
) && (*ptr
->val
!= previousValue
)) {
1212 setRebootRequired();
1215 p
->func(pDisplay
, p
);
1221 if ((p
->flags
& OSD_MENU_ELEMENT_MASK
) == OME_TAB
) {
1222 OSD_TAB_t
*ptr
= p
->data
;
1223 const uint8_t previousValue
= *ptr
->val
;
1225 if (key
== CMS_KEY_RIGHT
) {
1226 if (*ptr
->val
< ptr
->max
) {
1230 if (*ptr
->val
> 0) {
1235 p
->func(pDisplay
, p
->data
);
1237 SET_PRINTVALUE(runtimeEntryFlags
[currentCtx
.cursorRow
]);
1238 if ((p
->flags
& REBOOT_REQUIRED
) && (*ptr
->val
!= previousValue
)) {
1239 setRebootRequired();
1246 OSD_INT8_t
*ptr
= p
->data
;
1247 const int8_t previousValue
= *ptr
->val
;
1248 if (key
== CMS_KEY_RIGHT
) {
1249 if (*ptr
->val
< ptr
->max
) {
1250 *ptr
->val
+= ptr
->step
;
1253 if (*ptr
->val
> ptr
->min
) {
1254 *ptr
->val
-= ptr
->step
;
1257 SET_PRINTVALUE(runtimeEntryFlags
[currentCtx
.cursorRow
]);
1258 if ((p
->flags
& REBOOT_REQUIRED
) && (*ptr
->val
!= previousValue
)) {
1259 setRebootRequired();
1262 p
->func(pDisplay
, p
);
1269 OSD_UINT16_t
*ptr
= p
->data
;
1270 const uint16_t previousValue
= *ptr
->val
;
1271 if (key
== CMS_KEY_RIGHT
) {
1272 if (*ptr
->val
< ptr
->max
) {
1273 *ptr
->val
+= ptr
->step
;
1276 if (*ptr
->val
> ptr
->min
) {
1277 *ptr
->val
-= ptr
->step
;
1280 SET_PRINTVALUE(runtimeEntryFlags
[currentCtx
.cursorRow
]);
1281 if ((p
->flags
& REBOOT_REQUIRED
) && (*ptr
->val
!= previousValue
)) {
1282 setRebootRequired();
1285 p
->func(pDisplay
, p
);
1292 OSD_INT16_t
*ptr
= p
->data
;
1293 const int16_t previousValue
= *ptr
->val
;
1294 if (key
== CMS_KEY_RIGHT
) {
1295 if (*ptr
->val
< ptr
->max
) {
1296 *ptr
->val
+= ptr
->step
;
1299 if (*ptr
->val
> ptr
->min
) {
1300 *ptr
->val
-= ptr
->step
;
1303 SET_PRINTVALUE(runtimeEntryFlags
[currentCtx
.cursorRow
]);
1304 if ((p
->flags
& REBOOT_REQUIRED
) && (*ptr
->val
!= previousValue
)) {
1305 setRebootRequired();
1308 p
->func(pDisplay
, p
);
1315 OSD_UINT32_t
*ptr
= p
->data
;
1316 const uint32_t previousValue
= *ptr
->val
;
1317 if (key
== CMS_KEY_RIGHT
) {
1318 if (*ptr
->val
< ptr
->max
) {
1319 *ptr
->val
+= ptr
->step
;
1322 if (*ptr
->val
> ptr
->min
) {
1323 *ptr
->val
-= ptr
->step
;
1326 SET_PRINTVALUE(runtimeEntryFlags
[currentCtx
.cursorRow
]);
1327 if ((p
->flags
& REBOOT_REQUIRED
) && (*ptr
->val
!= previousValue
)) {
1328 setRebootRequired();
1331 p
->func(pDisplay
, p
);
1338 OSD_INT32_t
*ptr
= p
->data
;
1339 const int32_t previousValue
= *ptr
->val
;
1340 if (key
== CMS_KEY_RIGHT
) {
1341 if (*ptr
->val
< ptr
->max
) {
1342 *ptr
->val
+= ptr
->step
;
1345 if (*ptr
->val
> ptr
->min
) {
1346 *ptr
->val
-= ptr
->step
;
1349 SET_PRINTVALUE(runtimeEntryFlags
[currentCtx
.cursorRow
]);
1350 if ((p
->flags
& REBOOT_REQUIRED
) && (*ptr
->val
!= previousValue
)) {
1351 setRebootRequired();
1354 p
->func(pDisplay
, p
);
1373 void cmsSetExternKey(cms_key_e extKey
)
1375 if (externKey
== CMS_KEY_NONE
)
1379 uint16_t cmsHandleKeyWithRepeat(displayPort_t
*pDisplay
, cms_key_e key
, int repeatCount
)
1383 for (int i
= 0 ; i
< repeatCount
; i
++) {
1384 ret
= cmsHandleKey(pDisplay
, key
);
1390 static uint16_t cmsScanKeys(timeMs_t currentTimeMs
, timeMs_t lastCalledMs
, int16_t rcDelayMs
)
1392 static int holdCount
= 1;
1393 static int repeatCount
= 1;
1394 static int repeatBase
= 0;
1400 cms_key_e key
= CMS_KEY_NONE
;
1402 if (externKey
!= CMS_KEY_NONE
) {
1403 rcDelayMs
= cmsHandleKey(pCurrentDisplay
, externKey
);
1404 externKey
= CMS_KEY_NONE
;
1406 if (IS_MID(THROTTLE
) && IS_LO(YAW
) && IS_HI(PITCH
) && !ARMING_FLAG(ARMED
)) {
1408 } else if (IS_HI(PITCH
)) {
1410 } else if (IS_LO(PITCH
)) {
1412 } else if (IS_LO(ROLL
)) {
1414 } else if (IS_HI(ROLL
)) {
1415 key
= CMS_KEY_RIGHT
;
1416 } else if (IS_LO(YAW
)) {
1418 } else if (IS_HI(YAW
)) {
1419 key
= CMS_KEY_SAVEMENU
;
1422 if (key
== CMS_KEY_NONE
) {
1423 // No 'key' pressed, reset repeat control
1428 // The 'key' is being pressed; keep counting
1432 if (rcDelayMs
> 0) {
1433 rcDelayMs
-= (currentTimeMs
- lastCalledMs
);
1435 rcDelayMs
= cmsHandleKeyWithRepeat(pCurrentDisplay
, key
, repeatCount
);
1437 // Key repeat effect is implemented in two phases.
1438 // First phase is to decrease rcDelayMs reciprocal to hold time.
1439 // When rcDelayMs reached a certain limit (scheduling interval),
1440 // repeat rate will not raise anymore, so we call key handler
1441 // multiple times (repeatCount).
1443 // XXX Caveat: Most constants are adjusted pragmatically.
1444 // XXX Rewrite this someday, so it uses actual hold time instead
1445 // of holdCount, which depends on the scheduling interval.
1447 if (((key
== CMS_KEY_LEFT
) || (key
== CMS_KEY_RIGHT
)) && (holdCount
> 20)) {
1449 // Decrease rcDelayMs reciprocally
1451 rcDelayMs
/= (holdCount
- 20);
1453 // When we reach the scheduling limit,
1455 if (rcDelayMs
<= 50) {
1457 // start calling handler multiple times.
1459 if (repeatBase
== 0) {
1460 repeatBase
= holdCount
;
1463 repeatCount
= repeatCount
+ (holdCount
- repeatBase
) / 5;
1465 if (repeatCount
> 5) {
1476 static void cmsUpdate(uint32_t currentTimeUs
)
1478 if (IS_RC_MODE_ACTIVE(BOXPARALYZE
)
1482 #ifdef USE_USB_CDC_HID
1483 || cdcDeviceIsMayBeActive() // If this target is used as a joystick, we should leave here.
1489 static int16_t rcDelayMs
= BUTTON_TIME
;
1491 static uint32_t lastCalledMs
= 0;
1492 static uint32_t lastCmsHeartBeatMs
= 0;
1494 const uint32_t currentTimeMs
= currentTimeUs
/ 1000;
1497 // Detect menu invocation
1498 if (IS_MID(THROTTLE
) && IS_LO(YAW
) && IS_HI(PITCH
) && !ARMING_FLAG(ARMED
) && !IS_RC_MODE_ACTIVE(BOXSTICKCOMMANDDISABLE
)) {
1500 rcDelayMs
= BUTTON_PAUSE
; // Tends to overshoot if BUTTON_TIME
1503 displayBeginTransaction(pCurrentDisplay
, DISPLAY_TRANSACTION_OPT_RESET_DRAWING
);
1505 rcDelayMs
= cmsScanKeys(currentTimeMs
, lastCalledMs
, rcDelayMs
);
1507 cmsDrawMenu(pCurrentDisplay
, currentTimeUs
);
1509 if (currentTimeMs
> lastCmsHeartBeatMs
+ 500) {
1510 // Heart beat for external CMS display device @ 500msec
1511 // (Timeout @ 1000msec)
1512 displayHeartbeat(pCurrentDisplay
);
1513 lastCmsHeartBeatMs
= currentTimeMs
;
1516 displayCommitTransaction(pCurrentDisplay
);
1519 // Some key (command), notably flash erase, takes too long to use the
1520 // currentTimeMs to be used as lastCalledMs (freezes CMS for a minute or so
1522 lastCalledMs
= millis();
1525 void cmsHandler(timeUs_t currentTimeUs
)
1527 if (cmsDeviceCount
> 0) {
1528 cmsUpdate(currentTimeUs
);
1535 cmsCurrentDevice
= -1;
1538 void inhibitSaveMenu(void)
1540 saveMenuInhibited
= true;
1543 void cmsAddMenuEntry(OSD_Entry
*menuEntry
, char *text
, uint16_t flags
, CMSEntryFuncPtr func
, void *data
)
1545 menuEntry
->text
= text
;
1546 menuEntry
->flags
= flags
;
1547 menuEntry
->func
= func
;
1548 menuEntry
->data
= data
;