2 * This file is part of Cleanflight.
4 * Cleanflight is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
9 * Cleanflight is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with Cleanflight. If not, see <http://www.gnu.org/licenses/>.
19 Original OSD code created by Marcin Baliniak
20 OSD-CMS separation by jflyper
21 CMS-displayPort separation by jflyper and martinbudden
24 //#define CMS_PAGE_DEBUG // For multi-page/menu debugging
25 //#define CMS_MENU_DEBUG // For external menu content creators
35 #include "build/build_config.h"
36 #include "build/debug.h"
37 #include "build/version.h"
40 #include "cms/cms_menu_builtin.h"
41 #include "cms/cms_menu_saveexit.h"
42 #include "cms/cms_menu_osd.h"
43 #include "cms/cms_types.h"
45 #include "common/maths.h"
46 #include "common/printf.h"
47 #include "common/typeconversion.h"
48 #include "common/utils.h"
50 #include "drivers/system.h"
51 #include "drivers/time.h"
53 // For rcData, stopAllMotors, stopPwmAllMotors
54 #include "config/feature.h"
55 #include "config/parameter_group.h"
56 #include "config/parameter_group_ids.h"
59 #include "fc/fc_core.h"
60 #include "fc/config.h"
61 #include "fc/rc_controls.h"
62 #include "fc/runtime_config.h"
63 #include "fc/settings.h"
65 #include "flight/mixer.h"
66 #include "flight/servos.h"
70 #include "io/rcdevice_cam.h"
74 // DisplayPort management
76 #ifndef CMS_MAX_DEVICE
77 #define CMS_MAX_DEVICE 4
80 // Should be as big as the maximum number of rows displayed
81 // simultaneously in the tallest supported screen.
82 static uint8_t entry_flags
[32];
84 #define IS_PRINTVALUE(p, row) (entry_flags[row] & PRINT_VALUE)
85 #define SET_PRINTVALUE(p, row) { entry_flags[row] |= PRINT_VALUE; }
86 #define CLR_PRINTVALUE(p, row) { entry_flags[row] &= ~PRINT_VALUE; }
88 #define IS_PRINTLABEL(p, row) (entry_flags[row] & PRINT_LABEL)
89 #define SET_PRINTLABEL(p, row) { entry_flags[row] |= PRINT_LABEL; }
90 #define CLR_PRINTLABEL(p, row) { entry_flags[row] &= ~PRINT_LABEL; }
92 #define IS_DYNAMIC(p) ((p)->flags & DYNAMIC)
93 #define IS_READONLY(p) ((p)->flags & READONLY)
95 #define SETTING_INVALID_VALUE_NAME "INVALID"
97 static displayPort_t
*pCurrentDisplay
;
99 static displayPort_t
*cmsDisplayPorts
[CMS_MAX_DEVICE
];
100 static int cmsDeviceCount
;
101 static int cmsCurrentDevice
= -1;
102 static timeMs_t cmsYieldUntil
= 0;
104 bool cmsDisplayPortRegister(displayPort_t
*pDisplay
)
106 if (cmsDeviceCount
== CMS_MAX_DEVICE
)
109 cmsDisplayPorts
[cmsDeviceCount
++] = pDisplay
;
114 static displayPort_t
*cmsDisplayPortSelectCurrent(void)
116 if (cmsDeviceCount
== 0)
119 if (cmsCurrentDevice
< 0)
120 cmsCurrentDevice
= 0;
122 return cmsDisplayPorts
[cmsCurrentDevice
];
125 static displayPort_t
*cmsDisplayPortSelectNext(void)
127 if (cmsDeviceCount
== 0)
130 cmsCurrentDevice
= (cmsCurrentDevice
+ 1) % cmsDeviceCount
; // -1 Okay
132 return cmsDisplayPorts
[cmsCurrentDevice
];
135 bool cmsDisplayPortSelect(displayPort_t
*instance
)
137 if (cmsDeviceCount
== 0) {
140 for (int i
= 0; i
< cmsDeviceCount
; i
++) {
141 if (cmsDisplayPortSelectNext() == instance
) {
148 displayPort_t
*cmsDisplayPortGetCurrent(void)
150 return pCurrentDisplay
;
153 #define CMS_UPDATE_INTERVAL_US 50000 // Interval of key scans (microsec)
154 #define CMS_POLL_INTERVAL_US 100000 // Interval of polling dynamic values (microsec)
156 // XXX LEFT_MENU_COLUMN and RIGHT_MENU_COLUMN must be adjusted
157 // dynamically depending on size of the active output device,
158 // or statically to accomodate sizes of all supported devices.
160 // Device characteristics
163 // 128x64 with 5x7 (6x8) : 21 cols x 8 rows
168 // HoTT Telemetry Screen
176 #define NORMAL_SCREEN_MIN_COLS 18 // Less is a small screen
177 #define NORMAL_SCREEN_MAX_COLS 30 // More is a big screen
178 static bool smallScreen
;
179 static uint8_t leftMenuColumn
;
180 static uint8_t rightMenuColumn
;
181 static uint8_t maxMenuItems
;
182 static uint8_t linesPerMenuItem
;
183 static cms_key_e externKey
= CMS_KEY_NONE
;
185 bool cmsInMenu
= false;
187 typedef struct cmsCtx_s
{
188 const CMS_Menu
*menu
; // menu for this context
189 uint8_t page
; // page in the menu
190 int8_t cursorRow
; // cursorRow in the page
193 static cmsCtx_t menuStack
[10];
194 static uint8_t menuStackIdx
= 0;
196 static int8_t pageCount
; // Number of pages in the current menu
197 static const OSD_Entry
*pageTop
; // First entry for the current page
198 static uint8_t pageMaxRow
; // Max row in the current page
200 static cmsCtx_t currentCtx
;
202 #ifdef CMS_MENU_DEBUG // For external menu content creators
204 static char menuErrLabel
[21 + 1] = "RANDOM DATA";
206 static const OSD_Entry menuErrEntries
[] = {
207 { "BROKEN MENU", OME_Label
, NULL
, NULL
, 0 },
208 { menuErrLabel
, OME_Label
, NULL
, NULL
, 0 },
214 static const CMS_Menu menuErr
= {
224 #ifdef CMS_PAGE_DEBUG
225 #define cmsPageDebug() { \
226 debug[0] = pageCount; \
227 debug[1] = currentCtx.page; \
228 debug[2] = pageMaxRow; \
229 debug[3] = currentCtx.cursorRow; } struct _dummy
231 #define cmsPageDebug()
234 static void cmsUpdateMaxRow(displayPort_t
*instance
)
239 for (const OSD_Entry
*ptr
= pageTop
; ptr
->type
!= OME_END
; ptr
++) {
241 if (ptr
->type
== OME_BACK_AND_END
) {
246 if (pageMaxRow
> maxMenuItems
) {
247 pageMaxRow
= maxMenuItems
;
253 static uint8_t cmsCursorAbsolute(displayPort_t
*instance
)
256 return currentCtx
.cursorRow
+ currentCtx
.page
* maxMenuItems
;
259 static void cmsPageSelect(displayPort_t
*instance
, int8_t newpage
)
261 currentCtx
.page
= (newpage
+ pageCount
) % pageCount
;
262 pageTop
= ¤tCtx
.menu
->entries
[currentCtx
.page
* maxMenuItems
];
263 cmsUpdateMaxRow(instance
);
264 displayClearScreen(instance
);
267 static void cmsPageNext(displayPort_t
*instance
)
269 cmsPageSelect(instance
, currentCtx
.page
+ 1);
272 static void cmsPagePrev(displayPort_t
*instance
)
274 cmsPageSelect(instance
, currentCtx
.page
- 1);
277 static bool cmsElementIsLabel(OSD_MenuElement element
)
279 return element
== OME_Label
|| element
== OME_LabelFunc
|| element
== OME_Label_PAGE2_DATA
;
282 static void cmsFormatFloat(int32_t value
, char *floatString
)
287 itoa(100000 + value
, floatString
, 10); // Create string from abs of integer value
291 floatString
[0] = floatString
[1];
292 floatString
[1] = floatString
[2];
293 floatString
[2] = '.';
296 // usuwam koncowe zera i kropke
297 // Keep the first decimal place
298 for (k
= 5; k
> 3; k
--)
299 if (floatString
[k
] == '0' || floatString
[k
] == '.')
304 // oraz zero wiodonce
305 if (floatString
[0] == '0')
306 floatString
[0] = ' ';
309 // Pad buffer to the left, i.e. align right
310 static void cmsPadLeftToSize(char *buf
, int size
)
313 int len
= strlen(buf
);
315 for (i
= size
- 1, j
= size
- len
; i
- j
>= 0; i
--) {
319 for (; i
>= 0; i
--) {
326 static void cmsPadToSize(char *buf
, int size
)
328 // Make absolutely sure the string terminated.
331 cmsPadLeftToSize(buf
, size
);
334 static int cmsDrawMenuItemValue(displayPort_t
*pDisplay
, char *buff
, uint8_t row
, uint8_t maxSize
)
339 cmsPadToSize(buff
, maxSize
);
340 colpos
= rightMenuColumn
- maxSize
;
341 cnt
= displayWrite(pDisplay
, colpos
, row
, buff
);
345 static int cmsDrawMenuEntry(displayPort_t
*pDisplay
, const OSD_Entry
*p
, uint8_t row
, uint8_t screenRow
)
347 #define CMS_DRAW_BUFFER_LEN 12
348 #define CMS_NUM_FIELD_LEN 5
349 #define CMS_CURSOR_BLINK_DELAY_MS 500
351 char buff
[CMS_DRAW_BUFFER_LEN
+ 1]; // Make room for null terminator.
360 if (IS_PRINTVALUE(p
, screenRow
) && p
->data
) {
361 strncpy(buff
, p
->data
, CMS_DRAW_BUFFER_LEN
);
362 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, CMS_DRAW_BUFFER_LEN
);
363 CLR_PRINTVALUE(p
, screenRow
);
369 if (IS_PRINTVALUE(p
, screenRow
)) {
371 if ((p
->type
== OME_Submenu
) && p
->func
&& (p
->flags
& OPTSTRING
)) {
373 // Special case of sub menu entry with optional value display.
374 char *str
= p
->menufunc();
375 strncpy(buff
, str
, CMS_DRAW_BUFFER_LEN
);
377 strncat(buff
, ">", CMS_DRAW_BUFFER_LEN
);
379 row
= smallScreen
? row
- 1 : row
;
380 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, strlen(buff
));
381 CLR_PRINTVALUE(p
, screenRow
);
386 if (IS_PRINTVALUE(p
, screenRow
) && p
->data
) {
387 if (*((uint8_t *)(p
->data
))) {
393 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, 3);
394 CLR_PRINTVALUE(p
, screenRow
);
399 if (IS_PRINTVALUE(p
, screenRow
) && p
->data
) {
400 bool (*func
)(bool *arg
) = p
->data
;
407 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, 3);
408 CLR_PRINTVALUE(p
, screenRow
);
413 if (IS_PRINTVALUE(p
, screenRow
)) {
414 const OSD_TAB_t
*ptr
= p
->data
;
415 char * str
= (char *)ptr
->names
[*ptr
->val
];
416 strncpy(buff
, str
, CMS_DRAW_BUFFER_LEN
);
417 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, CMS_DRAW_BUFFER_LEN
);
418 CLR_PRINTVALUE(p
, screenRow
);
423 if (IS_PRINTVALUE(p
, screenRow
) && p
->data
) {
425 if (IS_READONLY(p
)) {
428 const OSD_UINT8_t
*ptr
= p
->data
;
431 itoa(*val
, buff
, 10);
432 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, CMS_NUM_FIELD_LEN
);
433 CLR_PRINTVALUE(p
, screenRow
);
438 if (IS_PRINTVALUE(p
, screenRow
) && p
->data
) {
440 if (IS_READONLY(p
)) {
443 const OSD_INT8_t
*ptr
= p
->data
;
446 itoa(*val
, buff
, 10);
447 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, CMS_NUM_FIELD_LEN
);
448 CLR_PRINTVALUE(p
, screenRow
);
453 if (IS_PRINTVALUE(p
, screenRow
) && p
->data
) {
455 if (IS_READONLY(p
)) {
458 const OSD_UINT16_t
*ptr
= p
->data
;
461 itoa(*val
, buff
, 10);
462 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, CMS_NUM_FIELD_LEN
);
463 CLR_PRINTVALUE(p
, screenRow
);
468 if (IS_PRINTVALUE(p
, screenRow
) && p
->data
) {
470 if (IS_READONLY(p
)) {
473 const OSD_INT16_t
*ptr
= p
->data
;
476 itoa(*val
, buff
, 10);
477 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, CMS_NUM_FIELD_LEN
);
478 CLR_PRINTVALUE(p
, screenRow
);
483 if (IS_PRINTVALUE(p
, screenRow
) && p
->data
) {
484 const OSD_FLOAT_t
*ptr
= p
->data
;
485 cmsFormatFloat(*ptr
->val
* ptr
->multipler
, buff
);
486 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, CMS_NUM_FIELD_LEN
);
487 CLR_PRINTVALUE(p
, screenRow
);
492 if (IS_PRINTVALUE(p
, screenRow
) && p
->data
) {
493 uint8_t maxSize
= CMS_NUM_FIELD_LEN
;
495 const OSD_SETTING_t
*ptr
= p
->data
;
496 const setting_t
*var
= settingGet(ptr
->val
);
498 const void *valuePointer
= settingGetValuePointer(var
);
499 switch (SETTING_TYPE(var
)) {
501 value
= *(uint8_t *)valuePointer
;
504 value
= *(int8_t *)valuePointer
;
507 value
= *(uint16_t *)valuePointer
;
510 value
= *(int16_t *)valuePointer
;
513 value
= *(uint32_t *)valuePointer
;
516 // XXX: This bypasses the data types. However, we
517 // don't have any VAR_FLOAT settings which require
519 ftoa(*(float *)valuePointer
, buff
);
522 strncpy(buff
, valuePointer
, sizeof(buff
));
525 if (buff
[0] == '\0') {
526 const char *suffix
= NULL
;
527 switch (CMS_DATA_TYPE(p
)) {
528 case CMS_DATA_TYPE_ANGULAR_RATE
:
529 // Setting is in degrees/10 per second
534 switch (SETTING_MODE(var
)) {
536 if (SETTING_TYPE(var
) == VAR_UINT32
) {
537 tfp_sprintf(buff
, "%u", (unsigned)value
);
539 tfp_sprintf(buff
, "%d", (int)value
);
544 const char *str
= settingLookupValueName(var
, value
);
545 strncpy(buff
, str
? str
: SETTING_INVALID_VALUE_NAME
, sizeof(buff
) - 1);
546 maxSize
= MAX(settingGetValueNameMaxSize(var
), strlen(SETTING_INVALID_VALUE_NAME
));
551 strcat(buff
, suffix
);
554 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, maxSize
);
555 CLR_PRINTVALUE(p
, screenRow
);
561 if (IS_PRINTVALUE(p
, screenRow
)) {
562 // A label with optional string, immediately following text
563 const char *text
= p
->data
;
564 if (p
->type
== OME_LabelFunc
) {
565 // Label is generated by a function
566 bool (*label_func
)(char *buf
, unsigned size
) = p
->data
;
567 if (label_func(buff
, sizeof(buff
))) {
574 cnt
= displayWrite(pDisplay
,
575 leftMenuColumn
+ 1 + (uint8_t) strlen(p
->text
), row
, text
);
577 CLR_PRINTVALUE(p
, screenRow
);
580 case OME_Label_PAGE2_DATA
:
584 case OME_BACK_AND_END
:
590 #ifdef CMS_MENU_DEBUG
591 // Shouldn't happen. Notify creator of this menu content.
592 cnt
= displayWrite(pDisplay
, rightMenuColumn
- 6), row
, "BADENT");
600 static void cmsDrawMenu(displayPort_t
*pDisplay
, uint32_t currentTimeUs
)
607 uint8_t top
= smallScreen
? 1 : (pDisplay
->rows
- pageMaxRow
) / 2;
609 // Polled (dynamic) value display denominator.
611 bool drawPolled
= false;
612 static uint32_t lastPolledUs
= 0;
614 if (currentTimeUs
> lastPolledUs
+ CMS_POLL_INTERVAL_US
) {
616 lastPolledUs
= currentTimeUs
;
619 uint32_t room
= displayTxBytesFree(pDisplay
);
621 if (pDisplay
->cleared
) {
622 // Mark all labels and values for printing
623 memset(entry_flags
, PRINT_LABEL
| PRINT_VALUE
, sizeof(entry_flags
));
624 pDisplay
->cleared
= false;
625 } else if (drawPolled
) {
626 for (p
= pageTop
, i
= 0; p
<= pageTop
+ pageMaxRow
; p
++, i
++) {
628 SET_PRINTVALUE(p
, i
);
632 // Cursor manipulation
634 while (cmsElementIsLabel((pageTop
+ currentCtx
.cursorRow
)->type
)) // skip label
635 currentCtx
.cursorRow
++;
639 if (pDisplay
->cursorRow
>= 0 && currentCtx
.cursorRow
!= pDisplay
->cursorRow
) {
640 room
-= displayWrite(pDisplay
, leftMenuColumn
, top
+ pDisplay
->cursorRow
* linesPerMenuItem
, " ");
646 if (pDisplay
->cursorRow
!= currentCtx
.cursorRow
) {
647 room
-= displayWrite(pDisplay
, leftMenuColumn
, top
+ currentCtx
.cursorRow
* linesPerMenuItem
, ">");
648 pDisplay
->cursorRow
= currentCtx
.cursorRow
;
655 for (i
= 0, p
= pageTop
; i
< maxMenuItems
&& p
->type
!= OME_END
; i
++, p
++) {
656 if (IS_PRINTLABEL(p
, i
)) {
657 uint8_t coloff
= leftMenuColumn
;
658 coloff
+= cmsElementIsLabel(p
->type
) ? 0 : 1;
660 if (p
->type
== OME_Label_PAGE2_DATA
) {
661 #ifdef USE_CMS_FONT_PREVIEW
662 // A label with immediately following text in page2
665 size_t textLen
= strlen(p
->text
);
666 for(size_t k
= 0; k
< textLen
; k
++) {
667 displayWriteChar(pDisplay
,
668 coloff
+ printed
, top
+ i
* linesPerMenuItem
, p
->text
[k
]);
673 const char *p2text
= (const char *)p
->data
;
674 for (size_t k
= 0; k
< strlen(p2text
); ++k
) {
675 displayWriteChar(pDisplay
,
676 coloff
+ printed
+ k
, top
+ i
* linesPerMenuItem
, (p2text
[k
] | (1 << 8)));
681 room
-= displayWrite(pDisplay
, coloff
, top
+ i
* linesPerMenuItem
, p
->text
);
683 CLR_PRINTLABEL(p
, i
);
688 if (p
->type
== OME_BACK_AND_END
) {
694 // XXX Polled values at latter positions in the list may not be
695 // XXX printed if not enough room in the middle of the list.
696 for (i
= 0, p
= pageTop
; i
< maxMenuItems
&& p
->type
!= OME_END
; i
++, p
++) {
697 if (IS_PRINTVALUE(p
, i
)) {
698 room
-= cmsDrawMenuEntry(pDisplay
, p
, top
+ i
* linesPerMenuItem
, i
);
703 if (p
->type
== OME_BACK_AND_END
) {
709 static void cmsMenuCountPage(displayPort_t
*pDisplay
)
713 for (p
= currentCtx
.menu
->entries
; p
->type
!= OME_END
; p
++) {
714 if (p
->type
== OME_BACK_AND_END
) {
719 pageCount
= (p
- currentCtx
.menu
->entries
- 1) / maxMenuItems
+ 1;
722 STATIC_UNIT_TESTED
long cmsMenuBack(displayPort_t
*pDisplay
); // Forward; will be resolved after merging
724 long cmsMenuChange(displayPort_t
*pDisplay
, const CMS_Menu
*pMenu
, const OSD_Entry
*from
)
730 #ifdef CMS_MENU_DEBUG
731 if (pMenu
->GUARD_type
!= OME_MENU
) {
732 // ptr isn't pointing to a CMS_Menu.
733 if (pMenu
->GUARD_type
<= OME_MAX
) {
734 strncpy(menuErrLabel
, pMenu
->GUARD_text
, sizeof(menuErrLabel
) - 1);
736 strncpy(menuErrLabel
, "LABEL UNKNOWN", sizeof(menuErrLabel
) - 1);
742 if (pMenu
!= currentCtx
.menu
) {
743 // Stack the current menu and move to a new menu.
745 menuStack
[menuStackIdx
++] = currentCtx
;
747 currentCtx
.menu
= pMenu
;
748 currentCtx
.cursorRow
= 0;
750 if (pMenu
->onEnter
&& (pMenu
->onEnter(from
) == MENU_CHAIN_BACK
)) {
751 return cmsMenuBack(pDisplay
);
754 cmsMenuCountPage(pDisplay
);
755 cmsPageSelect(pDisplay
, 0);
757 // The (pMenu == curretMenu) case occurs when reopening for display cycling
758 // currentCtx.cursorRow has been saved as absolute; convert it back to page + relative
760 int8_t cursorAbs
= currentCtx
.cursorRow
;
761 currentCtx
.cursorRow
= cursorAbs
% maxMenuItems
;
762 cmsMenuCountPage(pDisplay
);
763 cmsPageSelect(pDisplay
, cursorAbs
/ maxMenuItems
);
771 STATIC_UNIT_TESTED
long cmsMenuBack(displayPort_t
*pDisplay
)
773 // Let onExit function decide whether to allow exit or not.
775 if (currentCtx
.menu
->onExit
&& currentCtx
.menu
->onExit(pageTop
+ currentCtx
.cursorRow
) < 0) {
783 currentCtx
= menuStack
[--menuStackIdx
];
785 cmsMenuCountPage(pDisplay
);
786 cmsPageSelect(pDisplay
, currentCtx
.page
);
793 void cmsMenuOpen(void)
797 setServoOutputEnabled(false);
798 pCurrentDisplay
= cmsDisplayPortSelectCurrent();
799 if (!pCurrentDisplay
)
802 currentCtx
= (cmsCtx_t
){ &menuMain
, 0, 0 };
803 ENABLE_ARMING_FLAG(ARMING_DISABLED_CMS_MENU
);
806 displayPort_t
*pNextDisplay
= cmsDisplayPortSelectNext();
807 if (pNextDisplay
!= pCurrentDisplay
) {
808 // DisplayPort has been changed.
809 // Convert cursorRow to absolute value
810 currentCtx
.cursorRow
= cmsCursorAbsolute(pCurrentDisplay
);
811 displayRelease(pCurrentDisplay
);
812 pCurrentDisplay
= pNextDisplay
;
817 displayGrab(pCurrentDisplay
); // grab the display for use by the CMS
819 if (pCurrentDisplay
->cols
< NORMAL_SCREEN_MIN_COLS
) {
821 linesPerMenuItem
= 2;
823 rightMenuColumn
= pCurrentDisplay
->cols
;
824 maxMenuItems
= (pCurrentDisplay
->rows
) / linesPerMenuItem
;
827 linesPerMenuItem
= 1;
828 maxMenuItems
= pCurrentDisplay
->rows
- 2;
829 if (pCurrentDisplay
->cols
> NORMAL_SCREEN_MAX_COLS
) {
831 rightMenuColumn
= pCurrentDisplay
->cols
- 7;
834 rightMenuColumn
= pCurrentDisplay
->cols
- 2;
838 if (pCurrentDisplay
->useFullscreen
) {
840 rightMenuColumn
= pCurrentDisplay
->cols
;
841 maxMenuItems
= pCurrentDisplay
->rows
;
844 cmsMenuChange(pCurrentDisplay
, currentCtx
.menu
, NULL
);
847 static void cmsTraverseGlobalExit(const CMS_Menu
*pMenu
)
849 for (const OSD_Entry
*p
= pMenu
->entries
; p
->type
!= OME_END
; p
++) {
850 if (p
->type
== OME_Submenu
) {
851 cmsTraverseGlobalExit(p
->data
);
853 if (p
->type
== OME_BACK_AND_END
) {
858 if (pMenu
->onGlobalExit
) {
859 pMenu
->onGlobalExit(NULL
);
863 long cmsMenuExit(displayPort_t
*pDisplay
, const void *ptr
)
865 #if defined(SITL_BUILD)
866 unsigned long exitType
= (uintptr_t)ptr
;
868 int exitType
= (int)ptr
;
872 case CMS_EXIT_SAVEREBOOT
:
874 case CMS_POPUP_SAVEREBOOT
:
876 cmsTraverseGlobalExit(&menuMain
);
878 if (currentCtx
.menu
->onExit
)
879 currentCtx
.menu
->onExit((OSD_Entry
*)NULL
); // Forced exit
881 if ((exitType
== CMS_POPUP_SAVE
) || (exitType
== CMS_POPUP_SAVEREBOOT
)) {
882 // traverse through the menu stack and call their onExit functions
883 for (int i
= menuStackIdx
- 1; i
>= 0; i
--) {
884 if (menuStack
[i
].menu
->onExit
) {
885 menuStack
[i
].menu
->onExit((OSD_Entry
*) NULL
);
890 saveConfigAndNotify();
899 displayRelease(pDisplay
);
900 currentCtx
.menu
= NULL
;
902 setServoOutputEnabled(true);
904 if ((exitType
== CMS_EXIT_SAVEREBOOT
) || (exitType
== CMS_POPUP_SAVEREBOOT
)) {
905 processDelayedSave();
906 displayClearScreen(pDisplay
);
907 displayWrite(pDisplay
, 5, 3, "REBOOTING...");
909 displayResync(pDisplay
); // Was max7456RefreshAll(); why at this timing?
914 DISABLE_ARMING_FLAG(ARMING_DISABLED_CMS_MENU
);
919 void cmsYieldDisplay(displayPort_t
*pPort
, timeMs_t duration
)
921 // Check if we're already yielding, in that case just extend
922 // the yield time without releasing the display again, otherwise
923 // the yield/grab become unbalanced.
924 if (cmsYieldUntil
== 0) {
925 displayRelease(pPort
);
927 cmsYieldUntil
= millis() + duration
;
930 // Stick/key detection and key codes
932 #define IS_HI(X) (rxGetChannelValue(X) > 1750)
933 #define IS_LO(X) (rxGetChannelValue(X) < 1250)
934 #define IS_MID(X) (rxGetChannelValue(X) > 1250 && rxGetChannelValue(X) < 1750)
936 #define BUTTON_TIME 250 // msec
937 #define BUTTON_PAUSE 500 // msec
939 STATIC_UNIT_TESTED
uint16_t cmsHandleKey(displayPort_t
*pDisplay
, uint8_t key
)
941 uint16_t res
= BUTTON_TIME
;
944 if (!currentCtx
.menu
)
947 if (key
== CMS_KEY_MENU
) {
952 if (key
== CMS_KEY_ESC
) {
953 cmsMenuBack(pDisplay
);
957 if (key
== CMS_KEY_SAVEMENU
) {
958 cmsMenuChange(pDisplay
, &cmsx_menuSaveExit
, NULL
);
962 if (key
== CMS_KEY_DOWN
) {
963 if (currentCtx
.cursorRow
< pageMaxRow
) {
964 currentCtx
.cursorRow
++;
966 cmsPageNext(pDisplay
);
967 currentCtx
.cursorRow
= 0; // Goto top in any case
971 if (key
== CMS_KEY_UP
) {
972 currentCtx
.cursorRow
--;
974 // Skip non-title labels
975 if (cmsElementIsLabel((pageTop
+ currentCtx
.cursorRow
)->type
) && currentCtx
.cursorRow
> 0)
976 currentCtx
.cursorRow
--;
978 if (currentCtx
.cursorRow
== -1 || cmsElementIsLabel((pageTop
+ currentCtx
.cursorRow
)->type
)) {
979 // Goto previous page
980 cmsPagePrev(pDisplay
);
981 currentCtx
.cursorRow
= pageMaxRow
;
985 if (key
== CMS_KEY_DOWN
|| key
== CMS_KEY_UP
)
988 p
= pageTop
+ currentCtx
.cursorRow
;
992 if (key
== CMS_KEY_RIGHT
) {
993 cmsMenuChange(pDisplay
, p
->data
, p
);
999 if (p
->func
&& key
== CMS_KEY_RIGHT
) {
1000 long retval
= p
->func(pDisplay
, p
->data
);
1001 if (retval
== MENU_CHAIN_BACK
)
1002 cmsMenuBack(pDisplay
);
1008 if (p
->func
&& key
== CMS_KEY_RIGHT
) {
1009 p
->func(pDisplay
, p
->data
);
1015 case OME_BACK_AND_END
:
1016 cmsMenuBack(pDisplay
);
1022 uint8_t *val
= (uint8_t *)p
->data
;
1023 if (key
== CMS_KEY_RIGHT
)
1027 SET_PRINTVALUE(p
, currentCtx
.cursorRow
);
1029 p
->func(pDisplay
, p
);
1036 bool (*func
)(bool *arg
) = p
->data
;
1037 bool val
= key
== CMS_KEY_RIGHT
;
1039 SET_PRINTVALUE(p
, currentCtx
.cursorRow
);
1045 if (IS_READONLY(p
)) {
1049 const OSD_UINT8_t
*ptr
= p
->data
;
1050 if (key
== CMS_KEY_RIGHT
) {
1051 if (*ptr
->val
< ptr
->max
)
1052 *ptr
->val
+= ptr
->step
;
1054 if (*ptr
->val
> ptr
->min
)
1055 *ptr
->val
-= ptr
->step
;
1057 SET_PRINTVALUE(p
, currentCtx
.cursorRow
);
1059 p
->func(pDisplay
, p
);
1065 if (p
->type
== OME_TAB
) {
1066 const OSD_TAB_t
*ptr
= p
->data
;
1068 if (key
== CMS_KEY_RIGHT
) {
1069 if (*ptr
->val
< ptr
->max
)
1076 p
->func(pDisplay
, p
->data
);
1078 SET_PRINTVALUE(p
, currentCtx
.cursorRow
);
1083 if (IS_READONLY(p
)) {
1087 const OSD_INT8_t
*ptr
= p
->data
;
1088 if (key
== CMS_KEY_RIGHT
) {
1089 if (*ptr
->val
< ptr
->max
)
1090 *ptr
->val
+= ptr
->step
;
1092 if (*ptr
->val
> ptr
->min
)
1093 *ptr
->val
-= ptr
->step
;
1095 SET_PRINTVALUE(p
, currentCtx
.cursorRow
);
1097 p
->func(pDisplay
, p
);
1103 if (IS_READONLY(p
)) {
1107 const OSD_UINT16_t
*ptr
= p
->data
;
1108 if (key
== CMS_KEY_RIGHT
) {
1109 if (*ptr
->val
< ptr
->max
)
1110 *ptr
->val
+= ptr
->step
;
1112 if (*ptr
->val
> ptr
->min
)
1113 *ptr
->val
-= ptr
->step
;
1115 SET_PRINTVALUE(p
, currentCtx
.cursorRow
);
1117 p
->func(pDisplay
, p
);
1123 if (IS_READONLY(p
)) {
1127 const OSD_INT16_t
*ptr
= p
->data
;
1128 if (key
== CMS_KEY_RIGHT
) {
1129 if (*ptr
->val
< ptr
->max
)
1130 *ptr
->val
+= ptr
->step
;
1132 if (*ptr
->val
> ptr
->min
)
1133 *ptr
->val
-= ptr
->step
;
1135 SET_PRINTVALUE(p
, currentCtx
.cursorRow
);
1137 p
->func(pDisplay
, p
);
1144 const OSD_SETTING_t
*ptr
= p
->data
;
1145 const setting_t
*var
= settingGet(ptr
->val
);
1146 setting_min_t min
= settingGetMin(var
);
1147 setting_max_t max
= settingGetMax(var
);
1148 float step
= ptr
->step
?: 1;
1149 if (key
!= CMS_KEY_RIGHT
) {
1152 const void *valuePointer
= settingGetValuePointer(var
);
1153 switch (SETTING_TYPE(var
)) {
1156 uint8_t val
= *(uint8_t *)valuePointer
;
1157 val
= MIN(MAX(val
+ step
, (uint8_t)min
), (uint8_t)max
);
1158 *(uint8_t *)valuePointer
= val
;
1163 int8_t val
= *(int8_t *)valuePointer
;
1164 val
= MIN(MAX(val
+ step
, (int8_t)min
), (int8_t)max
);
1165 *(int8_t *)valuePointer
= val
;
1170 uint16_t val
= *(uint16_t *)valuePointer
;
1171 val
= MIN(MAX(val
+ step
, (uint16_t)min
), (uint16_t)max
);
1172 *(uint16_t *)valuePointer
= val
;
1177 int16_t val
= *(int16_t *)valuePointer
;
1178 val
= MIN(MAX(val
+ step
, (int16_t)min
), (int16_t)max
);
1179 *(int16_t *)valuePointer
= val
;
1184 uint32_t val
= *(uint32_t *)valuePointer
;
1185 val
= MIN(MAX(val
+ step
, (uint32_t)min
), (uint32_t)max
);
1186 *(uint32_t *)valuePointer
= val
;
1191 float val
= *(float *)valuePointer
;
1192 val
= MIN(MAX(val
+ step
, (float)min
), (float)max
);
1193 *(float *)valuePointer
= val
;
1199 SET_PRINTVALUE(p
, currentCtx
.cursorRow
);
1201 p
->func(pDisplay
, p
);
1221 void cmsSetExternKey(cms_key_e extKey
)
1223 if (externKey
== CMS_KEY_NONE
)
1227 uint16_t cmsHandleKeyWithRepeat(displayPort_t
*pDisplay
, uint8_t key
,
1232 for (int i
= 0; i
< repeatCount
; i
++) {
1233 ret
= cmsHandleKey(pDisplay
, key
);
1239 static uint16_t cmsScanKeys(timeMs_t currentTimeMs
, timeMs_t lastCalledMs
, int16_t rcDelayMs
)
1241 static int holdCount
= 1;
1242 static int repeatCount
= 1;
1243 static int repeatBase
= 0;
1249 uint8_t key
= CMS_KEY_NONE
;
1251 if (externKey
!= CMS_KEY_NONE
) {
1252 rcDelayMs
= cmsHandleKey(pCurrentDisplay
, externKey
);
1253 externKey
= CMS_KEY_NONE
;
1255 if (IS_MID(THROTTLE
) && IS_LO(YAW
) && IS_HI(PITCH
) && !ARMING_FLAG(ARMED
)) {
1257 } else if (IS_HI(PITCH
)) {
1259 } else if (IS_LO(PITCH
)) {
1261 } else if (IS_LO(ROLL
)) {
1263 } else if (IS_HI(ROLL
)) {
1264 key
= CMS_KEY_RIGHT
;
1265 } else if (IS_LO(YAW
)) {
1267 } else if (IS_HI(YAW
)) {
1268 key
= CMS_KEY_SAVEMENU
;
1271 if (key
== CMS_KEY_NONE
) {
1272 // No 'key' pressed, reset repeat control
1277 // The 'key' is being pressed; keep counting
1281 if (rcDelayMs
> 0) {
1282 rcDelayMs
-= (currentTimeMs
- lastCalledMs
);
1284 rcDelayMs
= cmsHandleKeyWithRepeat(pCurrentDisplay
, key
,
1287 // Key repeat effect is implemented in two phases.
1288 // First phldase is to decrease rcDelayMs reciprocal to hold time.
1289 // When rcDelayMs reached a certain limit (scheduling interval),
1290 // repeat rate will not raise anymore, so we call key handler
1291 // multiple times (repeatCount).
1293 // XXX Caveat: Most constants are adjusted pragmatically.
1294 // XXX Rewrite this someday, so it uses actual hold time instead
1295 // of holdCount, which depends on the scheduling interval.
1297 if (((key
== CMS_KEY_LEFT
) || (key
== CMS_KEY_RIGHT
)) && (holdCount
> 20)) {
1299 // Decrease rcDelayMs reciprocally
1301 rcDelayMs
/= (holdCount
- 20);
1303 // When we reach the scheduling limit,
1305 if (rcDelayMs
<= 50) {
1307 // start calling handler multiple times.
1309 if (repeatBase
== 0)
1310 repeatBase
= holdCount
;
1312 if (holdCount
< 100) {
1313 repeatCount
= repeatCount
1314 + (holdCount
- repeatBase
) / 5;
1316 if (repeatCount
> 5) {
1320 repeatCount
= repeatCount
+ holdCount
- repeatBase
;
1322 if (repeatCount
> 50) {
1333 void cmsUpdate(uint32_t currentTimeUs
)
1336 if(rcdeviceInMenu
) {
1341 static int16_t rcDelayMs
= BUTTON_TIME
;
1343 static timeMs_t lastCalledMs
= 0;
1344 static uint32_t lastCmsHeartBeatMs
= 0;
1346 const timeMs_t currentTimeMs
= currentTimeUs
/ 1000;
1349 // Detect menu invocation
1350 if (IS_MID(THROTTLE
) && IS_LO(YAW
) && IS_HI(PITCH
) && !ARMING_FLAG(ARMED
)) {
1352 rcDelayMs
= BUTTON_PAUSE
; // Tends to overshoot if BUTTON_TIME
1355 displayBeginTransaction(pCurrentDisplay
, DISPLAY_TRANSACTION_OPT_RESET_DRAWING
);
1357 // Check if we're yielding and its's time to stop it
1358 if (cmsYieldUntil
> 0 && currentTimeMs
> cmsYieldUntil
) {
1360 displayGrab(pCurrentDisplay
);
1361 displayClearScreen(pCurrentDisplay
);
1364 // Only scan keys and draw if we're not yielding
1365 if (cmsYieldUntil
== 0) {
1366 // XXX: Note that one call to cmsScanKeys() might generate multiple keypresses
1367 // when repeating, that's why cmsYieldDisplay() has to check for multiple calls.
1368 rcDelayMs
= cmsScanKeys(currentTimeMs
, lastCalledMs
, rcDelayMs
);
1369 // Check again, the keypress might have produced a yield
1370 if (cmsYieldUntil
== 0) {
1371 cmsDrawMenu(pCurrentDisplay
, currentTimeUs
);
1375 if (currentTimeMs
> lastCmsHeartBeatMs
+ 500) {
1376 // Heart beat for external CMS display device @ 500msec
1377 // (Timeout @ 1000msec)
1378 displayHeartbeat(pCurrentDisplay
);
1379 lastCmsHeartBeatMs
= currentTimeMs
;
1381 displayCommitTransaction(pCurrentDisplay
);
1384 // Some key (command), notably flash erase, takes too long to use the
1385 // currentTimeMs to be used as lastCalledMs (freezes CMS for a minute or so
1387 lastCalledMs
= millis();
1390 void cmsHandler(timeUs_t currentTimeUs
)
1392 if (cmsDeviceCount
< 0)
1395 static timeUs_t lastCalledUs
= 0;
1397 if (currentTimeUs
>= lastCalledUs
+ CMS_UPDATE_INTERVAL_US
) {
1398 lastCalledUs
= currentTimeUs
;
1399 cmsUpdate(currentTimeUs
);
1406 cmsCurrentDevice
= -1;