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_types.h"
48 #include "common/maths.h"
49 #include "common/typeconversion.h"
51 #include "config/config.h"
52 #include "config/feature.h"
54 #include "drivers/motor.h"
55 #include "drivers/osd_symbols.h"
56 #include "drivers/system.h"
57 #include "drivers/time.h"
59 #include "fc/rc_controls.h"
60 #include "fc/runtime_config.h"
62 #include "flight/mixer.h"
64 #include "io/rcdevice_cam.h"
65 #include "io/usb_cdc_hid.h"
68 #include "pg/pg_ids.h"
75 // DisplayPort management
77 #ifndef CMS_MAX_DEVICE
78 #define CMS_MAX_DEVICE 4
81 #define CMS_MENU_STACK_LIMIT 10
83 displayPort_t
*pCurrentDisplay
;
85 static displayPort_t
*cmsDisplayPorts
[CMS_MAX_DEVICE
];
86 static unsigned cmsDeviceCount
;
87 static int cmsCurrentDevice
= -1;
89 static unsigned int osdProfileCursor
= 1;
94 bool cmsDisplayPortRegister(displayPort_t
*pDisplay
)
96 if (!pDisplay
|| cmsDeviceCount
>= CMS_MAX_DEVICE
) {
100 cmsDisplayPorts
[cmsDeviceCount
++] = pDisplay
;
105 static displayPort_t
*cmsDisplayPortSelectCurrent(void)
107 if (cmsDeviceCount
== 0) {
111 if (cmsCurrentDevice
< 0) {
112 cmsCurrentDevice
= 0;
115 return cmsDisplayPorts
[cmsCurrentDevice
];
118 static displayPort_t
*cmsDisplayPortSelectNext(void)
120 if (cmsDeviceCount
== 0) {
124 cmsCurrentDevice
= (cmsCurrentDevice
+ 1) % cmsDeviceCount
; // -1 Okay
126 return cmsDisplayPorts
[cmsCurrentDevice
];
129 bool cmsDisplayPortSelect(displayPort_t
*instance
)
131 for (unsigned i
= 0; i
< cmsDeviceCount
; i
++) {
132 if (cmsDisplayPortSelectNext() == instance
) {
139 #define CMS_POLL_INTERVAL_US 100000 // Interval of polling dynamic values (microsec)
141 // XXX LEFT_MENU_COLUMN and RIGHT_MENU_COLUMN must be adjusted
142 // dynamically depending on size of the active output device,
143 // or statically to accomodate sizes of all supported devices.
145 // Device characteristics
148 // 128x64 with 5x7 (6x8) : 21 cols x 8 rows
153 // HoTT Telemetry Screen
156 // Spektrum SRXL Telemtry Textgenerator
157 // 13 cols x 9 rows, top row printed as a Bold Heading
158 // Needs the "smallScreen" adaptions
160 #define CMS_MAX_ROWS 16
162 #define NORMAL_SCREEN_MIN_COLS 18 // Less is a small screen
163 static bool smallScreen
;
164 static uint8_t leftMenuColumn
;
165 static uint8_t rightMenuColumn
;
166 static uint8_t maxMenuItems
;
167 static uint8_t linesPerMenuItem
;
168 static cms_key_e externKey
= CMS_KEY_NONE
;
169 static bool osdElementEditing
= false;
171 bool cmsInMenu
= false;
173 typedef struct cmsCtx_s
{
174 const CMS_Menu
*menu
; // menu for this context
175 uint8_t page
; // page in the menu
176 int8_t cursorRow
; // cursorRow in the page
179 static cmsCtx_t menuStack
[CMS_MENU_STACK_LIMIT
];
180 static uint8_t menuStackIdx
= 0;
182 static int8_t pageCount
; // Number of pages in the current menu
183 static const OSD_Entry
*pageTop
; // First entry for the current page
184 static uint8_t pageMaxRow
; // Max row in the current page
186 static cmsCtx_t currentCtx
;
188 static bool saveMenuInhibited
= false;
190 #ifdef CMS_MENU_DEBUG // For external menu content creators
192 static char menuErrLabel
[21 + 1] = "RANDOM DATA";
194 static OSD_Entry menuErrEntries
[] = {
195 { "BROKEN MENU", OME_Label
, NULL
, NULL
, 0 },
196 { menuErrLabel
, OME_Label
, NULL
, NULL
, 0 },
197 { "BACK", OME_Back
, NULL
, NULL
, 0 },
198 { NULL
, OME_END
, NULL
, NULL
, 0 }
201 static CMS_Menu menuErr
= {
211 #ifdef CMS_PAGE_DEBUG
212 #define cmsPageDebug() { \
213 debug[0] = pageCount; \
214 debug[1] = currentCtx.page; \
215 debug[2] = pageMaxRow; \
216 debug[3] = currentCtx.cursorRow; } struct _dummy
219 static void cmsUpdateMaxRow(displayPort_t
*instance
)
224 for (const OSD_Entry
*ptr
= pageTop
; ptr
->type
!= OME_END
; ptr
++) {
228 if (pageMaxRow
> maxMenuItems
) {
229 pageMaxRow
= maxMenuItems
;
232 if (pageMaxRow
> CMS_MAX_ROWS
) {
233 pageMaxRow
= CMS_MAX_ROWS
;
239 static uint8_t cmsCursorAbsolute(displayPort_t
*instance
)
242 return currentCtx
.cursorRow
+ currentCtx
.page
* maxMenuItems
;
245 uint8_t runtimeEntryFlags
[CMS_MAX_ROWS
] = { 0 };
247 #define LOOKUP_TABLE_TICKER_START_CYCLES 20 // Task loops for start/end of ticker (1 second delay)
248 #define LOOKUP_TABLE_TICKER_SCROLL_CYCLES 3 // Task loops for each scrolling step of the ticker (150ms delay)
250 typedef struct cmsTableTicker_s
{
255 cmsTableTicker_t runtimeTableTicker
[CMS_MAX_ROWS
];
257 static void cmsPageSelect(displayPort_t
*instance
, int8_t newpage
)
259 currentCtx
.page
= (newpage
+ pageCount
) % pageCount
;
260 pageTop
= ¤tCtx
.menu
->entries
[currentCtx
.page
* maxMenuItems
];
261 cmsUpdateMaxRow(instance
);
265 for (p
= pageTop
, i
= 0; (p
<= pageTop
+ pageMaxRow
); p
++, i
++) {
266 runtimeEntryFlags
[i
] = p
->flags
;
268 displayClearScreen(instance
);
271 static void cmsPageNext(displayPort_t
*instance
)
273 cmsPageSelect(instance
, currentCtx
.page
+ 1);
276 static void cmsPagePrev(displayPort_t
*instance
)
278 cmsPageSelect(instance
, currentCtx
.page
- 1);
281 static void cmsFormatFloat(int32_t value
, char *floatString
)
286 itoa(100000 + value
, floatString
, 10); // Create string from abs of integer value
290 floatString
[0] = floatString
[1];
291 floatString
[1] = floatString
[2];
292 floatString
[2] = '.';
295 // usuwam koncowe zera i kropke
296 // Keep the first decimal place
297 for (k
= 5; k
> 3; k
--) {
298 if (floatString
[k
] == '0' || floatString
[k
] == '.') {
305 // oraz zero wiodonce
306 if (floatString
[0] == '0') {
307 floatString
[0] = ' ';
311 // CMS on OSD legacy was to use LEFT aligned values, not the RIGHT way ;-)
312 #define CMS_OSD_RIGHT_ALIGNED_VALUES
314 #ifndef CMS_OSD_RIGHT_ALIGNED_VALUES
316 // Pad buffer to the left, i.e. align left
317 static void cmsPadRightToSize(char *buf
, int size
)
321 for (i
= 0 ; i
< size
; i
++) {
327 for ( ; i
< size
; i
++) {
335 // Pad buffer to the left, i.e. align right
336 static void cmsPadLeftToSize(char *buf
, int size
)
339 int len
= strlen(buf
);
341 for (i
= size
- 1, j
= size
- len
; i
- j
>= 0 ; i
--) {
345 for ( ; i
>= 0 ; i
--) {
352 static void cmsPadToSize(char *buf
, int size
)
354 // Make absolutely sure the string terminated.
357 #ifdef CMS_OSD_RIGHT_ALIGNED_VALUES
358 cmsPadLeftToSize(buf
, size
);
360 smallScreen
? cmsPadLeftToSize(buf
, size
) : cmsPadRightToSize(buf
, size
);
364 static int cmsDisplayWrite(displayPort_t
*instance
, uint8_t x
, uint8_t y
, uint8_t attr
, const char *s
)
366 char buffer
[strlen(s
) + 1];
369 char c
= toupper(*s
++);
370 *b
++ = (c
< 0x20 || c
> 0x5F) ? ' ' : c
; // limit to alphanumeric and punctuation
374 return displayWrite(instance
, x
, y
, attr
, buffer
);
377 static int cmsDrawMenuItemValue(displayPort_t
*pDisplay
, char *buff
, uint8_t row
, uint8_t maxSize
)
382 cmsPadToSize(buff
, maxSize
);
383 #ifdef CMS_OSD_RIGHT_ALIGNED_VALUES
384 colpos
= rightMenuColumn
- maxSize
;
386 colpos
= smallScreen
? rightMenuColumn
- maxSize
: rightMenuColumn
;
388 cnt
= cmsDisplayWrite(pDisplay
, colpos
, row
, DISPLAYPORT_ATTR_NONE
, buff
);
392 static int cmsDrawMenuEntry(displayPort_t
*pDisplay
, const OSD_Entry
*p
, uint8_t row
, bool selectedRow
, uint8_t *flags
, cmsTableTicker_t
*ticker
)
394 #define CMS_DRAW_BUFFER_LEN 12
395 #define CMS_TABLE_VALUE_MAX_LEN 30
396 #define CMS_NUM_FIELD_LEN 5
397 #define CMS_CURSOR_BLINK_DELAY_MS 500
399 char buff
[CMS_DRAW_BUFFER_LEN
+1]; // Make room for null terminator.
400 char tableBuff
[CMS_TABLE_VALUE_MAX_LEN
+1];
413 if (IS_PRINTVALUE(*flags
) && p
->data
) {
414 strncpy(buff
, p
->data
, CMS_DRAW_BUFFER_LEN
);
415 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, CMS_DRAW_BUFFER_LEN
);
416 CLR_PRINTVALUE(*flags
);
422 if (IS_PRINTVALUE(*flags
)) {
425 if (p
->type
== OME_Submenu
&& p
->func
&& *flags
& OPTSTRING
) {
427 // Special case of sub menu entry with optional value display.
429 const char *str
= p
->func(pDisplay
, p
->data
);
430 strncpy(buff
, str
, CMS_DRAW_BUFFER_LEN
);
431 } else if (p
->type
== OME_Funcall
&& p
->data
) {
432 strncpy(buff
, p
->data
, CMS_DRAW_BUFFER_LEN
);
434 strncat(buff
, ">", CMS_DRAW_BUFFER_LEN
);
436 row
= smallScreen
? row
- 1 : row
;
437 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, strlen(buff
));
438 CLR_PRINTVALUE(*flags
);
443 if (IS_PRINTVALUE(*flags
) && p
->data
) {
444 if (*((uint8_t *)(p
->data
))) {
450 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, 3);
451 CLR_PRINTVALUE(*flags
);
456 if (IS_PRINTVALUE(*flags
) || IS_SCROLLINGTICKER(*flags
)) {
457 bool drawText
= false;
458 OSD_TAB_t
*ptr
= p
->data
;
459 const int labelLength
= strlen(p
->text
) + 1; // account for the space between label and display data
460 char *str
= (char *)ptr
->names
[*ptr
->val
]; // lookup table display text
461 const int displayLength
= strlen(str
);
463 // Calculate the available space to display the lookup table entry based on the
464 // screen size and the length of the label. Always display at least CMS_DRAW_BUFFER_LEN
465 // characters to prevent really long labels from overriding the data display.
466 const int availableSpace
= MAX(CMS_DRAW_BUFFER_LEN
, rightMenuColumn
- labelLength
- leftMenuColumn
- 1);
468 if (IS_PRINTVALUE(*flags
)) {
471 ticker
->loopCounter
= 0;
472 if (displayLength
> availableSpace
) { // table entry text is longer than the available space so start the ticker
473 SET_SCROLLINGTICKER(*flags
);
475 CLR_SCROLLINGTICKER(*flags
);
477 } else if (IS_SCROLLINGTICKER(*flags
)) {
478 ticker
->loopCounter
++;
479 const uint8_t loopLimit
= (ticker
->state
== 0 || ticker
->state
== (displayLength
- availableSpace
)) ? LOOKUP_TABLE_TICKER_START_CYCLES
: LOOKUP_TABLE_TICKER_SCROLL_CYCLES
;
480 if (ticker
->loopCounter
>= loopLimit
) {
481 ticker
->loopCounter
= 0;
484 if (ticker
->state
> (displayLength
- availableSpace
)) {
490 strncpy(tableBuff
, (char *)(str
+ ticker
->state
), CMS_TABLE_VALUE_MAX_LEN
);
491 cnt
= cmsDrawMenuItemValue(pDisplay
, tableBuff
, row
, availableSpace
);
493 CLR_PRINTVALUE(*flags
);
499 if (IS_PRINTVALUE(*flags
) && p
->data
) {
500 uint16_t *val
= (uint16_t *)p
->data
;
501 bool cursorBlink
= millis() % (2 * CMS_CURSOR_BLINK_DELAY_MS
) < CMS_CURSOR_BLINK_DELAY_MS
;
502 for (unsigned x
= 1; x
< OSD_PROFILE_COUNT
+ 1; x
++) {
503 if (VISIBLE_IN_OSD_PROFILE(*val
, x
)) {
504 if (osdElementEditing
&& cursorBlink
&& selectedRow
&& (x
== osdProfileCursor
)) {
505 strcpy(buff
+ x
- 1, " ");
507 strcpy(buff
+ x
- 1, "X");
510 if (osdElementEditing
&& cursorBlink
&& selectedRow
&& (x
== osdProfileCursor
)) {
511 strcpy(buff
+ x
- 1, " ");
513 strcpy(buff
+ x
- 1, "-");
517 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, 3);
518 CLR_PRINTVALUE(*flags
);
524 if (IS_PRINTVALUE(*flags
) && p
->data
) {
525 OSD_UINT8_t
*ptr
= p
->data
;
526 itoa(*ptr
->val
, buff
, 10);
527 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, CMS_NUM_FIELD_LEN
);
528 CLR_PRINTVALUE(*flags
);
533 if (IS_PRINTVALUE(*flags
) && p
->data
) {
534 OSD_INT8_t
*ptr
= p
->data
;
535 itoa(*ptr
->val
, buff
, 10);
536 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, CMS_NUM_FIELD_LEN
);
537 CLR_PRINTVALUE(*flags
);
542 if (IS_PRINTVALUE(*flags
) && p
->data
) {
543 OSD_UINT16_t
*ptr
= p
->data
;
544 itoa(*ptr
->val
, buff
, 10);
545 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, CMS_NUM_FIELD_LEN
);
546 CLR_PRINTVALUE(*flags
);
551 if (IS_PRINTVALUE(*flags
) && p
->data
) {
552 OSD_INT16_t
*ptr
= p
->data
;
553 itoa(*ptr
->val
, buff
, 10);
554 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, CMS_NUM_FIELD_LEN
);
555 CLR_PRINTVALUE(*flags
);
560 if (IS_PRINTVALUE(*flags
) && p
->data
) {
561 OSD_UINT32_t
*ptr
= p
->data
;
562 itoa(*ptr
->val
, buff
, 10);
563 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, CMS_NUM_FIELD_LEN
);
564 CLR_PRINTVALUE(*flags
);
569 if (IS_PRINTVALUE(*flags
) && p
->data
) {
570 OSD_INT32_t
*ptr
= p
->data
;
571 itoa(*ptr
->val
, buff
, 10);
572 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, CMS_NUM_FIELD_LEN
);
573 CLR_PRINTVALUE(*flags
);
578 if (IS_PRINTVALUE(*flags
) && p
->data
) {
579 OSD_FLOAT_t
*ptr
= p
->data
;
580 cmsFormatFloat(*ptr
->val
* ptr
->multipler
, buff
);
581 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, CMS_NUM_FIELD_LEN
);
582 CLR_PRINTVALUE(*flags
);
587 if (IS_PRINTVALUE(*flags
) && p
->data
) {
588 // A label with optional string, immediately following text
589 cnt
= cmsDisplayWrite(pDisplay
, leftMenuColumn
+ 1 + (uint8_t)strlen(p
->text
), row
, DISPLAYPORT_ATTR_NONE
, p
->data
);
590 CLR_PRINTVALUE(*flags
);
602 #ifdef CMS_MENU_DEBUG
603 // Shouldn't happen. Notify creator of this menu content
604 #ifdef CMS_OSD_RIGHT_ALIGNED_VALUES
605 cnt
= cmsDisplayWrite(pDisplay
, rightMenuColumn
- 6, row
, DISPLAYPORT_ATTR_NONE
, "BADENT");
607 cnt
= cmsDisplayWrite(pDisplay
, rightMenuColumn
, row
, DISPLAYPORT_ATTR_NONE
, "BADENT");
616 static void cmsMenuCountPage(displayPort_t
*pDisplay
)
620 for (p
= currentCtx
.menu
->entries
; p
->type
!= OME_END
; p
++);
621 pageCount
= (p
- currentCtx
.menu
->entries
- 1) / maxMenuItems
+ 1;
624 STATIC_UNIT_TESTED
const void *cmsMenuBack(displayPort_t
*pDisplay
)
626 // Let onExit function decide whether to allow exit or not.
627 if (currentCtx
.menu
->onExit
) {
628 const void *result
= currentCtx
.menu
->onExit(pDisplay
, pageTop
+ currentCtx
.cursorRow
);
629 if (result
== MENU_CHAIN_BACK
) {
634 saveMenuInhibited
= false;
640 currentCtx
= menuStack
[--menuStackIdx
];
642 cmsMenuCountPage(pDisplay
);
643 cmsPageSelect(pDisplay
, currentCtx
.page
);
645 #if defined(CMS_PAGE_DEBUG)
652 // Skip read-only entries
653 static bool rowIsSkippable(const OSD_Entry
*row
)
655 if (row
->type
== OME_Label
) {
659 if (row
->type
== OME_String
) {
663 if ((row
->type
== OME_UINT16
|| row
->type
== OME_INT16
) && row
->flags
== DYNAMIC
) {
669 static void cmsDrawMenu(displayPort_t
*pDisplay
, uint32_t currentTimeUs
)
671 if (!pageTop
|| !cmsInMenu
) {
675 const bool displayWasCleared
= pDisplay
->cleared
;
678 uint8_t top
= smallScreen
? 1 : (pDisplay
->rows
- pageMaxRow
)/2;
680 pDisplay
->cleared
= false;
682 // Polled (dynamic) value display denominator.
684 bool drawPolled
= false;
685 static uint32_t lastPolledUs
= 0;
687 if (currentTimeUs
> lastPolledUs
+ CMS_POLL_INTERVAL_US
) {
689 lastPolledUs
= currentTimeUs
;
692 uint32_t room
= displayTxBytesFree(pDisplay
);
694 if (displayWasCleared
) {
695 for (p
= pageTop
, i
= 0; (p
<= pageTop
+ pageMaxRow
); p
++, i
++) {
696 SET_PRINTLABEL(runtimeEntryFlags
[i
]);
697 SET_PRINTVALUE(runtimeEntryFlags
[i
]);
699 } else if (drawPolled
) {
700 for (p
= pageTop
, i
= 0; (p
<= pageTop
+ pageMaxRow
); p
++, i
++) {
702 SET_PRINTVALUE(runtimeEntryFlags
[i
]);
706 // Cursor manipulation
708 while (rowIsSkippable(pageTop
+ currentCtx
.cursorRow
)) { // skip labels, strings and dynamic read-only entries
709 currentCtx
.cursorRow
++;
712 #if defined(CMS_PAGE_DEBUG)
716 if (pDisplay
->cursorRow
>= 0 && currentCtx
.cursorRow
!= pDisplay
->cursorRow
) {
717 room
-= cmsDisplayWrite(pDisplay
, leftMenuColumn
, top
+ pDisplay
->cursorRow
* linesPerMenuItem
, DISPLAYPORT_ATTR_NONE
, " ");
724 if (pDisplay
->cursorRow
!= currentCtx
.cursorRow
) {
725 room
-= cmsDisplayWrite(pDisplay
, leftMenuColumn
, top
+ currentCtx
.cursorRow
* linesPerMenuItem
, DISPLAYPORT_ATTR_NONE
, ">");
726 pDisplay
->cursorRow
= currentCtx
.cursorRow
;
733 if (currentCtx
.menu
->onDisplayUpdate
) {
734 const void *result
= currentCtx
.menu
->onDisplayUpdate(pDisplay
, pageTop
+ currentCtx
.cursorRow
);
735 if (result
== MENU_CHAIN_BACK
) {
736 cmsMenuBack(pDisplay
);
743 for (i
= 0, p
= pageTop
; (p
<= pageTop
+ pageMaxRow
); i
++, p
++) {
744 if (IS_PRINTLABEL(runtimeEntryFlags
[i
])) {
745 uint8_t coloff
= leftMenuColumn
;
746 coloff
+= (p
->type
== OME_Label
) ? 0 : 1;
747 room
-= cmsDisplayWrite(pDisplay
, coloff
, top
+ i
* linesPerMenuItem
, DISPLAYPORT_ATTR_NONE
, p
->text
);
748 CLR_PRINTLABEL(runtimeEntryFlags
[i
]);
756 // XXX Polled values at latter positions in the list may not be
757 // XXX printed if not enough room in the middle of the list.
759 if (IS_PRINTVALUE(runtimeEntryFlags
[i
]) || IS_SCROLLINGTICKER(runtimeEntryFlags
[i
])) {
760 bool selectedRow
= i
== currentCtx
.cursorRow
;
761 room
-= cmsDrawMenuEntry(pDisplay
, p
, top
+ i
* linesPerMenuItem
, selectedRow
, &runtimeEntryFlags
[i
], &runtimeTableTicker
[i
]);
768 // Draw the up/down page indicators if the display has space.
769 // Only draw the symbols when necessary after the screen has been cleared. Otherwise they're static.
770 // If the device supports OSD symbols then use the up/down arrows. Otherwise assume it's a
771 // simple text device and use the '^' (carat) and 'V' for arrow approximations.
772 if (displayWasCleared
&& leftMenuColumn
> 0) { // make sure there's room to draw the symbol
773 if (currentCtx
.page
> 0) {
774 const uint8_t symbol
= displaySupportsOsdSymbols(pDisplay
) ? SYM_ARROW_NORTH
: '^';
775 displayWriteChar(pDisplay
, leftMenuColumn
- 1, top
, DISPLAYPORT_ATTR_NONE
, symbol
);
777 if (currentCtx
.page
< pageCount
- 1) {
778 const uint8_t symbol
= displaySupportsOsdSymbols(pDisplay
) ? SYM_ARROW_SOUTH
: 'V';
779 displayWriteChar(pDisplay
, leftMenuColumn
- 1, top
+ pageMaxRow
, DISPLAYPORT_ATTR_NONE
, symbol
);
785 const void *cmsMenuChange(displayPort_t
*pDisplay
, const void *ptr
)
787 const CMS_Menu
*pMenu
= (const CMS_Menu
*)ptr
;
793 #ifdef CMS_MENU_DEBUG
794 if (pMenu
->GUARD_type
!= OME_MENU
) {
795 // ptr isn't pointing to a CMS_Menu.
796 if (pMenu
->GUARD_type
<= OME_MAX
) {
797 strncpy(menuErrLabel
, pMenu
->GUARD_text
, sizeof(menuErrLabel
) - 1);
799 strncpy(menuErrLabel
, "LABEL UNKNOWN", sizeof(menuErrLabel
) - 1);
805 if (pMenu
!= currentCtx
.menu
) {
806 saveMenuInhibited
= false;
808 if (currentCtx
.menu
) {
809 // If we are opening the initial top-level menu, then currentCtx.menu will be NULL and nothing to do.
810 // Otherwise stack the current menu before moving to the selected menu.
811 if (menuStackIdx
>= CMS_MENU_STACK_LIMIT
- 1) {
812 // menu stack limit reached - prevent array overflow
815 menuStack
[menuStackIdx
++] = currentCtx
;
818 currentCtx
.menu
= pMenu
;
819 currentCtx
.cursorRow
= 0;
821 if (pMenu
->onEnter
) {
822 const void *result
= pMenu
->onEnter(pDisplay
);
823 if (result
== MENU_CHAIN_BACK
) {
824 return cmsMenuBack(pDisplay
);
828 cmsMenuCountPage(pDisplay
);
829 cmsPageSelect(pDisplay
, 0);
831 // The (pMenu == curretMenu) case occurs when reopening for display cycling
832 // currentCtx.cursorRow has been saved as absolute; convert it back to page + relative
834 int8_t cursorAbs
= currentCtx
.cursorRow
;
835 currentCtx
.cursorRow
= cursorAbs
% maxMenuItems
;
836 cmsMenuCountPage(pDisplay
);
837 cmsPageSelect(pDisplay
, cursorAbs
/ maxMenuItems
);
840 #if defined(CMS_PAGE_DEBUG)
847 void cmsMenuOpen(void)
849 const CMS_Menu
*startMenu
;
852 pCurrentDisplay
= cmsDisplayPortSelectCurrent();
853 if (!pCurrentDisplay
) {
857 currentCtx
= (cmsCtx_t
){ NULL
, 0, 0 };
858 startMenu
= &cmsx_menuMain
;
860 setArmingDisabled(ARMING_DISABLED_CMS_MENU
);
861 displayLayerSelect(pCurrentDisplay
, DISPLAYPORT_LAYER_FOREGROUND
); // make sure the foreground layer is active
862 if (osdConfig()->cms_background_type
!= DISPLAY_BACKGROUND_TRANSPARENT
) {
863 displaySetBackgroundType(pCurrentDisplay
, (displayPortBackground_e
)osdConfig()->cms_background_type
); // set the background type if not transparent
867 displayPort_t
*pNextDisplay
= cmsDisplayPortSelectNext();
868 startMenu
= currentCtx
.menu
;
869 if (pNextDisplay
!= pCurrentDisplay
) {
870 // DisplayPort has been changed.
871 // Convert cursorRow to absolute value
872 currentCtx
.cursorRow
= cmsCursorAbsolute(pCurrentDisplay
);
873 displaySetBackgroundType(pCurrentDisplay
, DISPLAY_BACKGROUND_TRANSPARENT
); // reset previous displayPort to transparent
874 displayRelease(pCurrentDisplay
);
875 pCurrentDisplay
= pNextDisplay
;
876 displaySetBackgroundType(pCurrentDisplay
, (displayPortBackground_e
)osdConfig()->cms_background_type
); // set the background type if not transparent
881 displayGrab(pCurrentDisplay
); // grab the display for use by the CMS
882 // FIXME this should probably not have a dependency on the OSD or OSD slave code
887 if ( pCurrentDisplay
->cols
< NORMAL_SCREEN_MIN_COLS
) {
889 linesPerMenuItem
= 2;
891 rightMenuColumn
= pCurrentDisplay
->cols
;
892 maxMenuItems
= (pCurrentDisplay
->rows
) / linesPerMenuItem
;
895 linesPerMenuItem
= 1;
897 #ifdef CMS_OSD_RIGHT_ALIGNED_VALUES
898 rightMenuColumn
= pCurrentDisplay
->cols
- 2;
900 rightMenuColumn
= pCurrentDisplay
->cols
- CMS_DRAW_BUFFER_LEN
;
902 maxMenuItems
= pCurrentDisplay
->rows
- 2;
905 if (pCurrentDisplay
->useFullscreen
) {
907 rightMenuColumn
= pCurrentDisplay
->cols
;
908 maxMenuItems
= pCurrentDisplay
->rows
;
911 cmsMenuChange(pCurrentDisplay
, startMenu
);
914 static void cmsTraverseGlobalExit(const CMS_Menu
*pMenu
)
916 for (const OSD_Entry
*p
= pMenu
->entries
; p
->type
!= OME_END
; p
++) {
917 if (p
->type
== OME_Submenu
) {
918 cmsTraverseGlobalExit(p
->data
);
924 const void *cmsMenuExit(displayPort_t
*pDisplay
, const void *ptr
)
926 int exitType
= (int)ptr
;
929 case CMS_EXIT_SAVEREBOOT
:
931 case CMS_POPUP_SAVEREBOOT
:
933 cmsTraverseGlobalExit(&cmsx_menuMain
);
935 if (currentCtx
.menu
->onExit
) {
936 currentCtx
.menu
->onExit(pDisplay
, (OSD_Entry
*)NULL
); // Forced exit
939 if ((exitType
== CMS_POPUP_SAVE
) || (exitType
== CMS_POPUP_SAVEREBOOT
)) {
940 // traverse through the menu stack and call their onExit functions
941 for (int i
= menuStackIdx
- 1; i
>= 0; i
--) {
942 if (menuStack
[i
].menu
->onExit
) {
943 menuStack
[i
].menu
->onExit(pDisplay
, (OSD_Entry
*)NULL
);
948 saveConfigAndNotify();
957 displaySetBackgroundType(pCurrentDisplay
, DISPLAY_BACKGROUND_TRANSPARENT
); // reset the background to transparent
959 displayRelease(pDisplay
);
960 currentCtx
.menu
= NULL
;
962 if ((exitType
== CMS_EXIT_SAVEREBOOT
) || (exitType
== CMS_POPUP_SAVEREBOOT
) || (exitType
== CMS_POPUP_EXITREBOOT
)) {
963 displayClearScreen(pDisplay
);
964 cmsDisplayWrite(pDisplay
, 5, 3, DISPLAYPORT_ATTR_NONE
, "REBOOTING...");
967 displayRedraw(pDisplay
);
976 unsetArmingDisabled(ARMING_DISABLED_CMS_MENU
);
981 // Stick/key detection and key codes
983 #define IS_HI(X) (rcData[X] > 1750)
984 #define IS_LO(X) (rcData[X] < 1250)
985 #define IS_MID(X) (rcData[X] > 1250 && rcData[X] < 1750)
987 #define BUTTON_TIME 250 // msec
988 #define BUTTON_PAUSE 500 // msec
990 STATIC_UNIT_TESTED
uint16_t cmsHandleKey(displayPort_t
*pDisplay
, cms_key_e key
)
992 uint16_t res
= BUTTON_TIME
;
995 if (!currentCtx
.menu
) {
999 if (key
== CMS_KEY_MENU
) {
1001 return BUTTON_PAUSE
;
1004 if (key
== CMS_KEY_ESC
) {
1005 if (osdElementEditing
) {
1006 osdElementEditing
= false;
1008 cmsMenuBack(pDisplay
);
1010 return BUTTON_PAUSE
;
1013 if (key
== CMS_KEY_SAVEMENU
&& !saveMenuInhibited
) {
1014 osdElementEditing
= false;
1015 cmsMenuChange(pDisplay
, getSaveExitMenu());
1017 return BUTTON_PAUSE
;
1020 if ((key
== CMS_KEY_DOWN
) && (!osdElementEditing
)) {
1021 if (currentCtx
.cursorRow
< pageMaxRow
) {
1022 currentCtx
.cursorRow
++;
1024 cmsPageNext(pDisplay
);
1025 currentCtx
.cursorRow
= 0; // Goto top in any case
1029 if ((key
== CMS_KEY_UP
) && (!osdElementEditing
)) {
1030 currentCtx
.cursorRow
--;
1032 // Skip non-title labels, strings and dynamic read-only entries
1033 while ((rowIsSkippable(pageTop
+ currentCtx
.cursorRow
)) && currentCtx
.cursorRow
> 0) {
1034 currentCtx
.cursorRow
--;
1036 if (currentCtx
.cursorRow
== -1 || (pageTop
+ currentCtx
.cursorRow
)->type
== OME_Label
) {
1037 // Goto previous page
1038 cmsPagePrev(pDisplay
);
1039 currentCtx
.cursorRow
= pageMaxRow
;
1043 if ((key
== CMS_KEY_DOWN
|| key
== CMS_KEY_UP
) && (!osdElementEditing
)) {
1047 p
= pageTop
+ currentCtx
.cursorRow
;
1051 if (key
== CMS_KEY_RIGHT
) {
1052 cmsMenuChange(pDisplay
, p
->data
);
1059 if (p
->func
&& key
== CMS_KEY_RIGHT
) {
1060 retval
= p
->func(pDisplay
, p
->data
);
1061 if (retval
== MENU_CHAIN_BACK
) {
1062 cmsMenuBack(pDisplay
);
1064 if ((p
->flags
& REBOOT_REQUIRED
)) {
1065 setRebootRequired();
1072 if (p
->func
&& key
== CMS_KEY_RIGHT
) {
1073 p
->func(pDisplay
, p
->data
);
1079 cmsMenuBack(pDisplay
);
1081 osdElementEditing
= false;
1086 uint8_t *val
= p
->data
;
1087 const uint8_t previousValue
= *val
;
1088 *val
= (key
== CMS_KEY_RIGHT
) ? 1 : 0;
1089 SET_PRINTVALUE(runtimeEntryFlags
[currentCtx
.cursorRow
]);
1090 if ((p
->flags
& REBOOT_REQUIRED
) && (*val
!= previousValue
)) {
1091 setRebootRequired();
1094 p
->func(pDisplay
, p
->data
);
1102 uint16_t *val
= (uint16_t *)p
->data
;
1103 const uint16_t previousValue
= *val
;
1104 if ((key
== CMS_KEY_RIGHT
) && (!osdElementEditing
)) {
1105 osdElementEditing
= true;
1106 osdProfileCursor
= 1;
1107 } else if (osdElementEditing
) {
1108 #ifdef USE_OSD_PROFILES
1109 if (key
== CMS_KEY_RIGHT
) {
1110 if (osdProfileCursor
< OSD_PROFILE_COUNT
) {
1114 if (key
== CMS_KEY_LEFT
) {
1115 if (osdProfileCursor
> 1) {
1120 if (key
== CMS_KEY_UP
) {
1121 *val
|= OSD_PROFILE_FLAG(osdProfileCursor
);
1123 if (key
== CMS_KEY_DOWN
) {
1124 *val
&= ~OSD_PROFILE_FLAG(osdProfileCursor
);
1127 SET_PRINTVALUE(runtimeEntryFlags
[currentCtx
.cursorRow
]);
1128 if ((p
->flags
& REBOOT_REQUIRED
) && (*val
!= previousValue
)) {
1129 setRebootRequired();
1138 OSD_UINT8_t
*ptr
= p
->data
;
1139 const uint16_t previousValue
= *ptr
->val
;
1140 if (key
== CMS_KEY_RIGHT
) {
1141 if (*ptr
->val
< ptr
->max
) {
1142 *ptr
->val
+= ptr
->step
;
1145 if (*ptr
->val
> ptr
->min
) {
1146 *ptr
->val
-= ptr
->step
;
1149 SET_PRINTVALUE(runtimeEntryFlags
[currentCtx
.cursorRow
]);
1150 if ((p
->flags
& REBOOT_REQUIRED
) && (*ptr
->val
!= previousValue
)) {
1151 setRebootRequired();
1154 p
->func(pDisplay
, p
);
1160 if (p
->type
== OME_TAB
) {
1161 OSD_TAB_t
*ptr
= p
->data
;
1162 const uint8_t previousValue
= *ptr
->val
;
1164 if (key
== CMS_KEY_RIGHT
) {
1165 if (*ptr
->val
< ptr
->max
) {
1169 if (*ptr
->val
> 0) {
1174 p
->func(pDisplay
, p
->data
);
1176 SET_PRINTVALUE(runtimeEntryFlags
[currentCtx
.cursorRow
]);
1177 if ((p
->flags
& REBOOT_REQUIRED
) && (*ptr
->val
!= previousValue
)) {
1178 setRebootRequired();
1185 OSD_INT8_t
*ptr
= p
->data
;
1186 const int8_t previousValue
= *ptr
->val
;
1187 if (key
== CMS_KEY_RIGHT
) {
1188 if (*ptr
->val
< ptr
->max
) {
1189 *ptr
->val
+= ptr
->step
;
1192 if (*ptr
->val
> ptr
->min
) {
1193 *ptr
->val
-= ptr
->step
;
1196 SET_PRINTVALUE(runtimeEntryFlags
[currentCtx
.cursorRow
]);
1197 if ((p
->flags
& REBOOT_REQUIRED
) && (*ptr
->val
!= previousValue
)) {
1198 setRebootRequired();
1201 p
->func(pDisplay
, p
);
1208 OSD_UINT16_t
*ptr
= p
->data
;
1209 const uint16_t previousValue
= *ptr
->val
;
1210 if (key
== CMS_KEY_RIGHT
) {
1211 if (*ptr
->val
< ptr
->max
) {
1212 *ptr
->val
+= ptr
->step
;
1215 if (*ptr
->val
> ptr
->min
) {
1216 *ptr
->val
-= ptr
->step
;
1219 SET_PRINTVALUE(runtimeEntryFlags
[currentCtx
.cursorRow
]);
1220 if ((p
->flags
& REBOOT_REQUIRED
) && (*ptr
->val
!= previousValue
)) {
1221 setRebootRequired();
1224 p
->func(pDisplay
, p
);
1231 OSD_INT16_t
*ptr
= p
->data
;
1232 const int16_t previousValue
= *ptr
->val
;
1233 if (key
== CMS_KEY_RIGHT
) {
1234 if (*ptr
->val
< ptr
->max
) {
1235 *ptr
->val
+= ptr
->step
;
1238 if (*ptr
->val
> ptr
->min
) {
1239 *ptr
->val
-= ptr
->step
;
1242 SET_PRINTVALUE(runtimeEntryFlags
[currentCtx
.cursorRow
]);
1243 if ((p
->flags
& REBOOT_REQUIRED
) && (*ptr
->val
!= previousValue
)) {
1244 setRebootRequired();
1247 p
->func(pDisplay
, p
);
1254 OSD_UINT32_t
*ptr
= p
->data
;
1255 const uint32_t previousValue
= *ptr
->val
;
1256 if (key
== CMS_KEY_RIGHT
) {
1257 if (*ptr
->val
< ptr
->max
) {
1258 *ptr
->val
+= ptr
->step
;
1261 if (*ptr
->val
> ptr
->min
) {
1262 *ptr
->val
-= ptr
->step
;
1265 SET_PRINTVALUE(runtimeEntryFlags
[currentCtx
.cursorRow
]);
1266 if ((p
->flags
& REBOOT_REQUIRED
) && (*ptr
->val
!= previousValue
)) {
1267 setRebootRequired();
1270 p
->func(pDisplay
, p
);
1277 OSD_INT32_t
*ptr
= p
->data
;
1278 const int32_t previousValue
= *ptr
->val
;
1279 if (key
== CMS_KEY_RIGHT
) {
1280 if (*ptr
->val
< ptr
->max
) {
1281 *ptr
->val
+= ptr
->step
;
1284 if (*ptr
->val
> ptr
->min
) {
1285 *ptr
->val
-= ptr
->step
;
1288 SET_PRINTVALUE(runtimeEntryFlags
[currentCtx
.cursorRow
]);
1289 if ((p
->flags
& REBOOT_REQUIRED
) && (*ptr
->val
!= previousValue
)) {
1290 setRebootRequired();
1293 p
->func(pDisplay
, p
);
1312 void cmsSetExternKey(cms_key_e extKey
)
1314 if (externKey
== CMS_KEY_NONE
)
1318 uint16_t cmsHandleKeyWithRepeat(displayPort_t
*pDisplay
, cms_key_e key
, int repeatCount
)
1322 for (int i
= 0 ; i
< repeatCount
; i
++) {
1323 ret
= cmsHandleKey(pDisplay
, key
);
1329 static void cmsUpdate(uint32_t currentTimeUs
)
1331 if (IS_RC_MODE_ACTIVE(BOXPARALYZE
)
1335 #ifdef USE_USB_CDC_HID
1336 || cdcDeviceIsMayBeActive() // If this target is used as a joystick, we should leave here.
1342 static int16_t rcDelayMs
= BUTTON_TIME
;
1343 static int holdCount
= 1;
1344 static int repeatCount
= 1;
1345 static int repeatBase
= 0;
1347 static uint32_t lastCalledMs
= 0;
1348 static uint32_t lastCmsHeartBeatMs
= 0;
1350 const uint32_t currentTimeMs
= currentTimeUs
/ 1000;
1353 // Detect menu invocation
1354 if (IS_MID(THROTTLE
) && IS_LO(YAW
) && IS_HI(PITCH
) && !ARMING_FLAG(ARMED
) && !IS_RC_MODE_ACTIVE(BOXSTICKCOMMANDDISABLE
)) {
1356 rcDelayMs
= BUTTON_PAUSE
; // Tends to overshoot if BUTTON_TIME
1363 cms_key_e key
= CMS_KEY_NONE
;
1365 if (externKey
!= CMS_KEY_NONE
) {
1366 rcDelayMs
= cmsHandleKey(pCurrentDisplay
, externKey
);
1367 externKey
= CMS_KEY_NONE
;
1369 if (IS_MID(THROTTLE
) && IS_LO(YAW
) && IS_HI(PITCH
) && !ARMING_FLAG(ARMED
)) {
1371 } else if (IS_HI(PITCH
)) {
1373 } else if (IS_LO(PITCH
)) {
1375 } else if (IS_LO(ROLL
)) {
1377 } else if (IS_HI(ROLL
)) {
1378 key
= CMS_KEY_RIGHT
;
1379 } else if (IS_LO(YAW
)) {
1381 } else if (IS_HI(YAW
)) {
1382 key
= CMS_KEY_SAVEMENU
;
1385 if (key
== CMS_KEY_NONE
) {
1386 // No 'key' pressed, reset repeat control
1391 // The 'key' is being pressed; keep counting
1395 if (rcDelayMs
> 0) {
1396 rcDelayMs
-= (currentTimeMs
- lastCalledMs
);
1398 rcDelayMs
= cmsHandleKeyWithRepeat(pCurrentDisplay
, key
, repeatCount
);
1400 // Key repeat effect is implemented in two phases.
1401 // First phldase is to decrease rcDelayMs reciprocal to hold time.
1402 // When rcDelayMs reached a certain limit (scheduling interval),
1403 // repeat rate will not raise anymore, so we call key handler
1404 // multiple times (repeatCount).
1406 // XXX Caveat: Most constants are adjusted pragmatically.
1407 // XXX Rewrite this someday, so it uses actual hold time instead
1408 // of holdCount, which depends on the scheduling interval.
1410 if (((key
== CMS_KEY_LEFT
) || (key
== CMS_KEY_RIGHT
)) && (holdCount
> 20)) {
1412 // Decrease rcDelayMs reciprocally
1414 rcDelayMs
/= (holdCount
- 20);
1416 // When we reach the scheduling limit,
1418 if (rcDelayMs
<= 50) {
1420 // start calling handler multiple times.
1422 if (repeatBase
== 0) {
1423 repeatBase
= holdCount
;
1426 repeatCount
= repeatCount
+ (holdCount
- repeatBase
) / 5;
1428 if (repeatCount
> 5) {
1436 cmsDrawMenu(pCurrentDisplay
, currentTimeUs
);
1438 if (currentTimeMs
> lastCmsHeartBeatMs
+ 500) {
1439 // Heart beat for external CMS display device @ 500msec
1440 // (Timeout @ 1000msec)
1441 displayHeartbeat(pCurrentDisplay
);
1442 lastCmsHeartBeatMs
= currentTimeMs
;
1446 // Some key (command), notably flash erase, takes too long to use the
1447 // currentTimeMs to be used as lastCalledMs (freezes CMS for a minute or so
1449 lastCalledMs
= millis();
1452 void cmsHandler(timeUs_t currentTimeUs
)
1454 if (cmsDeviceCount
> 0) {
1455 cmsUpdate(currentTimeUs
);
1462 cmsCurrentDevice
= -1;
1465 void inhibitSaveMenu(void)
1467 saveMenuInhibited
= true;
1470 void cmsAddMenuEntry(OSD_Entry
*menuEntry
, char *text
, OSD_MenuElement type
, CMSEntryFuncPtr func
, void *data
, uint8_t flags
)
1472 menuEntry
->text
= text
;
1473 menuEntry
->type
= type
;
1474 menuEntry
->func
= func
;
1475 menuEntry
->data
= data
;
1476 menuEntry
->flags
= flags
;