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"
53 #include "config/simplified_tuning.h"
55 #include "drivers/motor.h"
56 #include "drivers/osd_symbols.h"
57 #include "drivers/system.h"
58 #include "drivers/time.h"
60 #include "fc/rc_controls.h"
61 #include "fc/runtime_config.h"
63 #include "flight/mixer.h"
65 #include "io/rcdevice_cam.h"
66 #include "io/usb_cdc_hid.h"
69 #include "pg/pg_ids.h"
76 #include "sensors/gyro.h"
78 // DisplayPort management
80 #ifndef CMS_MAX_DEVICE
81 #define CMS_MAX_DEVICE 4
84 #define CMS_MENU_STACK_LIMIT 10
86 displayPort_t
*pCurrentDisplay
;
88 static displayPort_t
*cmsDisplayPorts
[CMS_MAX_DEVICE
];
89 static unsigned cmsDeviceCount
;
90 static int cmsCurrentDevice
= -1;
92 static unsigned int osdProfileCursor
= 1;
97 bool cmsDisplayPortRegister(displayPort_t
*pDisplay
)
99 if (!pDisplay
|| cmsDeviceCount
>= CMS_MAX_DEVICE
) {
103 cmsDisplayPorts
[cmsDeviceCount
++] = pDisplay
;
108 static displayPort_t
*cmsDisplayPortSelectCurrent(void)
110 if (cmsDeviceCount
== 0) {
114 if (cmsCurrentDevice
< 0) {
115 cmsCurrentDevice
= 0;
118 return cmsDisplayPorts
[cmsCurrentDevice
];
121 static displayPort_t
*cmsDisplayPortSelectNext(void)
123 if (cmsDeviceCount
== 0) {
127 cmsCurrentDevice
= (cmsCurrentDevice
+ 1) % cmsDeviceCount
; // -1 Okay
129 return cmsDisplayPorts
[cmsCurrentDevice
];
132 bool cmsDisplayPortSelect(displayPort_t
*instance
)
134 for (unsigned i
= 0; i
< cmsDeviceCount
; i
++) {
135 if (cmsDisplayPortSelectNext() == instance
) {
142 #define CMS_POLL_INTERVAL_US 100000 // Interval of polling dynamic values (microsec)
144 // XXX LEFT_MENU_COLUMN and RIGHT_MENU_COLUMN must be adjusted
145 // dynamically depending on size of the active output device,
146 // or statically to accomodate sizes of all supported devices.
148 // Device characteristics
151 // 128x64 with 5x7 (6x8) : 21 cols x 8 rows
156 // HoTT Telemetry Screen
159 // Spektrum SRXL Telemtry Textgenerator
160 // 13 cols x 9 rows, top row printed as a Bold Heading
161 // Needs the "smallScreen" adaptions
163 #define CMS_MAX_ROWS 16
165 #define NORMAL_SCREEN_MIN_COLS 18 // Less is a small screen
166 static bool smallScreen
;
167 static uint8_t leftMenuColumn
;
168 static uint8_t rightMenuColumn
;
169 static uint8_t maxMenuItems
;
170 static uint8_t linesPerMenuItem
;
171 static cms_key_e externKey
= CMS_KEY_NONE
;
172 static bool osdElementEditing
= false;
174 bool cmsInMenu
= false;
176 typedef struct cmsCtx_s
{
177 const CMS_Menu
*menu
; // menu for this context
178 uint8_t page
; // page in the menu
179 int8_t cursorRow
; // cursorRow in the page
182 static cmsCtx_t menuStack
[CMS_MENU_STACK_LIMIT
];
183 static uint8_t menuStackIdx
= 0;
185 static int8_t pageCount
; // Number of pages in the current menu
186 static const OSD_Entry
*pageTop
; // First entry for the current page
187 static uint8_t pageMaxRow
; // Max row in the current page
189 static cmsCtx_t currentCtx
;
191 static bool saveMenuInhibited
= false;
193 #ifdef CMS_MENU_DEBUG // For external menu content creators
195 static char menuErrLabel
[21 + 1] = "RANDOM DATA";
197 static OSD_Entry menuErrEntries
[] = {
198 { "BROKEN MENU", OME_Label
, NULL
, NULL
},
199 { menuErrLabel
, OME_Label
, NULL
, NULL
},
200 { "BACK", OME_Back
, NULL
, NULL
},
201 { NULL
, OME_END
, NULL
, NULL
}
204 static CMS_Menu menuErr
= {
214 #ifdef CMS_PAGE_DEBUG
215 #define cmsPageDebug() { \
216 debug[0] = pageCount; \
217 debug[1] = currentCtx.page; \
218 debug[2] = pageMaxRow; \
219 debug[3] = currentCtx.cursorRow; } struct _dummy
222 static void cmsUpdateMaxRow(displayPort_t
*instance
)
227 for (const OSD_Entry
*ptr
= pageTop
; (ptr
->flags
& OSD_MENU_ELEMENT_MASK
) != OME_END
; ptr
++) {
231 if (pageMaxRow
> maxMenuItems
) {
232 pageMaxRow
= maxMenuItems
;
235 if (pageMaxRow
> CMS_MAX_ROWS
) {
236 pageMaxRow
= CMS_MAX_ROWS
;
242 static uint8_t cmsCursorAbsolute(displayPort_t
*instance
)
245 return currentCtx
.cursorRow
+ currentCtx
.page
* maxMenuItems
;
248 uint8_t runtimeEntryFlags
[CMS_MAX_ROWS
] = { 0 };
250 #define LOOKUP_TABLE_TICKER_START_CYCLES 20 // Task loops for start/end of ticker (1 second delay)
251 #define LOOKUP_TABLE_TICKER_SCROLL_CYCLES 3 // Task loops for each scrolling step of the ticker (150ms delay)
253 typedef struct cmsTableTicker_s
{
258 cmsTableTicker_t runtimeTableTicker
[CMS_MAX_ROWS
];
260 static void cmsPageSelect(displayPort_t
*instance
, int8_t newpage
)
262 currentCtx
.page
= (newpage
+ pageCount
) % pageCount
;
263 pageTop
= ¤tCtx
.menu
->entries
[currentCtx
.page
* maxMenuItems
];
264 cmsUpdateMaxRow(instance
);
268 for (p
= pageTop
, i
= 0; (p
<= pageTop
+ pageMaxRow
); p
++, i
++) {
269 runtimeEntryFlags
[i
] = p
->flags
;
271 displayClearScreen(instance
);
274 static void cmsPageNext(displayPort_t
*instance
)
276 cmsPageSelect(instance
, currentCtx
.page
+ 1);
279 static void cmsPagePrev(displayPort_t
*instance
)
281 cmsPageSelect(instance
, currentCtx
.page
- 1);
284 static void cmsFormatFloat(int32_t value
, char *floatString
)
289 itoa(100000 + value
, floatString
, 10); // Create string from abs of integer value
293 floatString
[0] = floatString
[1];
294 floatString
[1] = floatString
[2];
295 floatString
[2] = '.';
298 // usuwam koncowe zera i kropke
299 // Keep the first decimal place
300 for (k
= 5; k
> 3; k
--) {
301 if (floatString
[k
] == '0' || floatString
[k
] == '.') {
308 // oraz zero wiodonce
309 if (floatString
[0] == '0') {
310 floatString
[0] = ' ';
314 // CMS on OSD legacy was to use LEFT aligned values, not the RIGHT way ;-)
315 #define CMS_OSD_RIGHT_ALIGNED_VALUES
317 #ifndef CMS_OSD_RIGHT_ALIGNED_VALUES
319 // Pad buffer to the left, i.e. align left
320 static void cmsPadRightToSize(char *buf
, int size
)
324 for (i
= 0 ; i
< size
; i
++) {
330 for ( ; i
< size
; i
++) {
338 // Pad buffer to the left, i.e. align right
339 static void cmsPadLeftToSize(char *buf
, int size
)
342 int len
= strlen(buf
);
344 for (i
= size
- 1, j
= size
- len
; i
- j
>= 0 ; i
--) {
348 for ( ; i
>= 0 ; i
--) {
355 static void cmsPadToSize(char *buf
, int size
)
357 // Make absolutely sure the string terminated.
360 #ifdef CMS_OSD_RIGHT_ALIGNED_VALUES
361 cmsPadLeftToSize(buf
, size
);
363 smallScreen
? cmsPadLeftToSize(buf
, size
) : cmsPadRightToSize(buf
, size
);
367 static int cmsDisplayWrite(displayPort_t
*instance
, uint8_t x
, uint8_t y
, uint8_t attr
, const char *s
)
369 char buffer
[strlen(s
) + 1];
372 char c
= toupper(*s
++);
373 *b
++ = (c
< 0x20 || c
> 0x5F) ? ' ' : c
; // limit to alphanumeric and punctuation
377 return displayWrite(instance
, x
, y
, attr
, buffer
);
380 static int cmsDrawMenuItemValue(displayPort_t
*pDisplay
, char *buff
, uint8_t row
, uint8_t maxSize
)
385 cmsPadToSize(buff
, maxSize
);
386 #ifdef CMS_OSD_RIGHT_ALIGNED_VALUES
387 colpos
= rightMenuColumn
- maxSize
;
389 colpos
= smallScreen
? rightMenuColumn
- maxSize
: rightMenuColumn
;
391 cnt
= cmsDisplayWrite(pDisplay
, colpos
, row
, DISPLAYPORT_ATTR_NONE
, buff
);
395 static int cmsDrawMenuEntry(displayPort_t
*pDisplay
, const OSD_Entry
*p
, uint8_t row
, bool selectedRow
, uint8_t *flags
, cmsTableTicker_t
*ticker
)
397 #define CMS_DRAW_BUFFER_LEN 12
398 #define CMS_TABLE_VALUE_MAX_LEN 30
399 #define CMS_NUM_FIELD_LEN 5
400 #define CMS_CURSOR_BLINK_DELAY_MS 500
402 char buff
[CMS_DRAW_BUFFER_LEN
+1]; // Make room for null terminator.
403 char tableBuff
[CMS_TABLE_VALUE_MAX_LEN
+1];
414 switch (p
->flags
& OSD_MENU_ELEMENT_MASK
) {
416 if (IS_PRINTVALUE(*flags
) && p
->data
) {
417 strncpy(buff
, p
->data
, CMS_DRAW_BUFFER_LEN
);
418 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, CMS_DRAW_BUFFER_LEN
);
419 CLR_PRINTVALUE(*flags
);
425 if (IS_PRINTVALUE(*flags
)) {
428 if ((p
->flags
& OSD_MENU_ELEMENT_MASK
) == OME_Submenu
&& p
->func
&& *flags
& OPTSTRING
) {
430 // Special case of sub menu entry with optional value display.
432 const char *str
= p
->func(pDisplay
, p
->data
);
433 strncpy(buff
, str
, CMS_DRAW_BUFFER_LEN
);
434 } else if ((p
->flags
& OSD_MENU_ELEMENT_MASK
) == OME_Funcall
&& p
->data
) {
435 strncpy(buff
, p
->data
, CMS_DRAW_BUFFER_LEN
);
437 strncat(buff
, ">", CMS_DRAW_BUFFER_LEN
);
439 row
= smallScreen
? row
- 1 : row
;
440 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, strlen(buff
));
441 CLR_PRINTVALUE(*flags
);
446 if (IS_PRINTVALUE(*flags
) && p
->data
) {
447 if (*((uint8_t *)(p
->data
))) {
453 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, 3);
454 CLR_PRINTVALUE(*flags
);
459 if (IS_PRINTVALUE(*flags
) || IS_SCROLLINGTICKER(*flags
)) {
460 bool drawText
= false;
461 OSD_TAB_t
*ptr
= p
->data
;
462 const int labelLength
= strlen(p
->text
) + 1; // account for the space between label and display data
463 char *str
= (char *)ptr
->names
[*ptr
->val
]; // lookup table display text
464 const int displayLength
= strlen(str
);
466 // Calculate the available space to display the lookup table entry based on the
467 // screen size and the length of the label. Always display at least CMS_DRAW_BUFFER_LEN
468 // characters to prevent really long labels from overriding the data display.
469 const int availableSpace
= MAX(CMS_DRAW_BUFFER_LEN
, rightMenuColumn
- labelLength
- leftMenuColumn
- 1);
471 if (IS_PRINTVALUE(*flags
)) {
474 ticker
->loopCounter
= 0;
475 if (displayLength
> availableSpace
) { // table entry text is longer than the available space so start the ticker
476 SET_SCROLLINGTICKER(*flags
);
478 CLR_SCROLLINGTICKER(*flags
);
480 } else if (IS_SCROLLINGTICKER(*flags
)) {
481 ticker
->loopCounter
++;
482 const uint8_t loopLimit
= (ticker
->state
== 0 || ticker
->state
== (displayLength
- availableSpace
)) ? LOOKUP_TABLE_TICKER_START_CYCLES
: LOOKUP_TABLE_TICKER_SCROLL_CYCLES
;
483 if (ticker
->loopCounter
>= loopLimit
) {
484 ticker
->loopCounter
= 0;
487 if (ticker
->state
> (displayLength
- availableSpace
)) {
493 strncpy(tableBuff
, (char *)(str
+ ticker
->state
), CMS_TABLE_VALUE_MAX_LEN
);
494 cnt
= cmsDrawMenuItemValue(pDisplay
, tableBuff
, row
, availableSpace
);
496 CLR_PRINTVALUE(*flags
);
502 if (IS_PRINTVALUE(*flags
) && p
->data
) {
503 uint16_t *val
= (uint16_t *)p
->data
;
504 bool cursorBlink
= millis() % (2 * CMS_CURSOR_BLINK_DELAY_MS
) < CMS_CURSOR_BLINK_DELAY_MS
;
505 for (unsigned x
= 1; x
< OSD_PROFILE_COUNT
+ 1; x
++) {
506 if (VISIBLE_IN_OSD_PROFILE(*val
, x
)) {
507 if (osdElementEditing
&& cursorBlink
&& selectedRow
&& (x
== osdProfileCursor
)) {
508 strcpy(buff
+ x
- 1, " ");
510 strcpy(buff
+ x
- 1, "X");
513 if (osdElementEditing
&& cursorBlink
&& selectedRow
&& (x
== osdProfileCursor
)) {
514 strcpy(buff
+ x
- 1, " ");
516 strcpy(buff
+ x
- 1, "-");
520 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, 3);
521 CLR_PRINTVALUE(*flags
);
527 if (IS_PRINTVALUE(*flags
) && p
->data
) {
528 OSD_UINT8_t
*ptr
= p
->data
;
529 itoa(*ptr
->val
, buff
, 10);
530 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, CMS_NUM_FIELD_LEN
);
531 CLR_PRINTVALUE(*flags
);
536 if (IS_PRINTVALUE(*flags
) && p
->data
) {
537 OSD_INT8_t
*ptr
= p
->data
;
538 itoa(*ptr
->val
, buff
, 10);
539 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, CMS_NUM_FIELD_LEN
);
540 CLR_PRINTVALUE(*flags
);
545 if (IS_PRINTVALUE(*flags
) && p
->data
) {
546 OSD_UINT16_t
*ptr
= p
->data
;
547 itoa(*ptr
->val
, buff
, 10);
548 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, CMS_NUM_FIELD_LEN
);
549 CLR_PRINTVALUE(*flags
);
554 if (IS_PRINTVALUE(*flags
) && p
->data
) {
555 OSD_INT16_t
*ptr
= p
->data
;
556 itoa(*ptr
->val
, buff
, 10);
557 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, CMS_NUM_FIELD_LEN
);
558 CLR_PRINTVALUE(*flags
);
563 if (IS_PRINTVALUE(*flags
) && p
->data
) {
564 OSD_UINT32_t
*ptr
= p
->data
;
565 itoa(*ptr
->val
, buff
, 10);
566 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, CMS_NUM_FIELD_LEN
);
567 CLR_PRINTVALUE(*flags
);
572 if (IS_PRINTVALUE(*flags
) && p
->data
) {
573 OSD_INT32_t
*ptr
= p
->data
;
574 itoa(*ptr
->val
, buff
, 10);
575 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, CMS_NUM_FIELD_LEN
);
576 CLR_PRINTVALUE(*flags
);
581 if (IS_PRINTVALUE(*flags
) && p
->data
) {
582 OSD_FLOAT_t
*ptr
= p
->data
;
583 cmsFormatFloat(*ptr
->val
* ptr
->multipler
, buff
);
584 cnt
= cmsDrawMenuItemValue(pDisplay
, buff
, row
, CMS_NUM_FIELD_LEN
);
585 CLR_PRINTVALUE(*flags
);
590 if (IS_PRINTVALUE(*flags
) && p
->data
) {
591 // A label with optional string, immediately following text
592 cnt
= cmsDisplayWrite(pDisplay
, leftMenuColumn
+ 1 + (uint8_t)strlen(p
->text
), row
, DISPLAYPORT_ATTR_NONE
, p
->data
);
593 CLR_PRINTVALUE(*flags
);
605 #ifdef CMS_MENU_DEBUG
606 // Shouldn't happen. Notify creator of this menu content
607 #ifdef CMS_OSD_RIGHT_ALIGNED_VALUES
608 cnt
= cmsDisplayWrite(pDisplay
, rightMenuColumn
- 6, row
, DISPLAYPORT_ATTR_NONE
, "BADENT");
610 cnt
= cmsDisplayWrite(pDisplay
, rightMenuColumn
, row
, DISPLAYPORT_ATTR_NONE
, "BADENT");
619 static void cmsMenuCountPage(displayPort_t
*pDisplay
)
623 for (p
= currentCtx
.menu
->entries
; (p
->flags
& OSD_MENU_ELEMENT_MASK
) != OME_END
; p
++);
624 pageCount
= (p
- currentCtx
.menu
->entries
- 1) / maxMenuItems
+ 1;
627 STATIC_UNIT_TESTED
const void *cmsMenuBack(displayPort_t
*pDisplay
)
629 // Let onExit function decide whether to allow exit or not.
630 if (currentCtx
.menu
->onExit
) {
631 const void *result
= currentCtx
.menu
->onExit(pDisplay
, pageTop
+ currentCtx
.cursorRow
);
632 if (result
== MENU_CHAIN_BACK
) {
637 saveMenuInhibited
= false;
643 currentCtx
= menuStack
[--menuStackIdx
];
645 cmsMenuCountPage(pDisplay
);
646 cmsPageSelect(pDisplay
, currentCtx
.page
);
648 #if defined(CMS_PAGE_DEBUG)
655 // Check if overridden by slider
656 static bool rowSliderOverride(const uint16_t flags
)
661 pidSimplifiedTuningMode_e simplified_pids_mode
= currentPidProfile
->simplified_pids_mode
;
663 bool slider_flags_mode_rpy
= (simplified_pids_mode
== PID_SIMPLIFIED_TUNING_RPY
);
664 bool slider_flags_mode_rp
= slider_flags_mode_rpy
|| (simplified_pids_mode
== PID_SIMPLIFIED_TUNING_RP
);
666 bool simplified_gyro_filter
= gyroConfig()->simplified_gyro_filter
;
667 bool simplified_dterm_filter
= currentPidProfile
->simplified_dterm_filter
;
669 if (((flags
& SLIDER_RP
) && slider_flags_mode_rp
) ||
670 ((flags
& SLIDER_RPY
) && slider_flags_mode_rpy
) ||
671 ((flags
& SLIDER_GYRO
) && simplified_gyro_filter
) ||
672 ((flags
& SLIDER_DTERM
) && simplified_dterm_filter
)) {
680 // Skip read-only entries
681 static bool rowIsSkippable(const OSD_Entry
*row
)
683 OSD_MenuElement type
= row
->flags
& OSD_MENU_ELEMENT_MASK
;
685 if (type
== OME_Label
) {
689 if (type
== OME_String
) {
693 if ((type
== OME_UINT8
|| type
== OME_INT8
||
694 type
== OME_UINT16
|| type
== OME_INT16
) &&
695 ((row
->flags
== DYNAMIC
) || rowSliderOverride(row
->flags
))) {
701 static void cmsDrawMenu(displayPort_t
*pDisplay
, uint32_t currentTimeUs
)
703 if (!pageTop
|| !cmsInMenu
) {
707 const bool displayWasCleared
= pDisplay
->cleared
;
710 uint8_t top
= smallScreen
? 1 : (pDisplay
->rows
- pageMaxRow
)/2;
712 pDisplay
->cleared
= false;
714 // Polled (dynamic) value display denominator.
716 bool drawPolled
= false;
717 static uint32_t lastPolledUs
= 0;
719 if (currentTimeUs
> lastPolledUs
+ CMS_POLL_INTERVAL_US
) {
721 lastPolledUs
= currentTimeUs
;
724 uint32_t room
= displayTxBytesFree(pDisplay
);
726 if (displayWasCleared
) {
727 for (p
= pageTop
, i
= 0; (p
<= pageTop
+ pageMaxRow
); p
++, i
++) {
728 SET_PRINTLABEL(runtimeEntryFlags
[i
]);
729 SET_PRINTVALUE(runtimeEntryFlags
[i
]);
731 } else if (drawPolled
) {
732 for (p
= pageTop
, i
= 0; (p
<= pageTop
+ pageMaxRow
); p
++, i
++) {
734 SET_PRINTVALUE(runtimeEntryFlags
[i
]);
738 // Cursor manipulation
740 while (rowIsSkippable(pageTop
+ currentCtx
.cursorRow
)) { // skip labels, strings and dynamic read-only entries
741 currentCtx
.cursorRow
++;
744 #if defined(CMS_PAGE_DEBUG)
748 if (pDisplay
->cursorRow
>= 0 && currentCtx
.cursorRow
!= pDisplay
->cursorRow
) {
749 room
-= cmsDisplayWrite(pDisplay
, leftMenuColumn
, top
+ pDisplay
->cursorRow
* linesPerMenuItem
, DISPLAYPORT_ATTR_NONE
, " ");
756 if (pDisplay
->cursorRow
!= currentCtx
.cursorRow
) {
757 room
-= cmsDisplayWrite(pDisplay
, leftMenuColumn
, top
+ currentCtx
.cursorRow
* linesPerMenuItem
, DISPLAYPORT_ATTR_NONE
, ">");
758 pDisplay
->cursorRow
= currentCtx
.cursorRow
;
765 if (currentCtx
.menu
->onDisplayUpdate
) {
766 const void *result
= currentCtx
.menu
->onDisplayUpdate(pDisplay
, pageTop
+ currentCtx
.cursorRow
);
767 if (result
== MENU_CHAIN_BACK
) {
768 cmsMenuBack(pDisplay
);
775 for (i
= 0, p
= pageTop
; (p
<= pageTop
+ pageMaxRow
); i
++, p
++) {
776 if (IS_PRINTLABEL(runtimeEntryFlags
[i
])) {
777 uint8_t coloff
= leftMenuColumn
;
778 coloff
+= ((p
->flags
& OSD_MENU_ELEMENT_MASK
) == OME_Label
) ? 0 : 1;
779 room
-= cmsDisplayWrite(pDisplay
, coloff
, top
+ i
* linesPerMenuItem
, DISPLAYPORT_ATTR_NONE
, p
->text
);
780 CLR_PRINTLABEL(runtimeEntryFlags
[i
]);
787 // Highlight values overridden by sliders
788 if (rowSliderOverride(p
->flags
)) {
789 displayWriteChar(pDisplay
, leftMenuColumn
- 1, top
+ i
* linesPerMenuItem
, DISPLAYPORT_ATTR_NONE
, 'S');
794 // XXX Polled values at latter positions in the list may not be
795 // XXX printed if not enough room in the middle of the list.
797 if (IS_PRINTVALUE(runtimeEntryFlags
[i
]) || IS_SCROLLINGTICKER(runtimeEntryFlags
[i
])) {
798 bool selectedRow
= i
== currentCtx
.cursorRow
;
799 room
-= cmsDrawMenuEntry(pDisplay
, p
, top
+ i
* linesPerMenuItem
, selectedRow
, &runtimeEntryFlags
[i
], &runtimeTableTicker
[i
]);
806 // Draw the up/down page indicators if the display has space.
807 // Only draw the symbols when necessary after the screen has been cleared. Otherwise they're static.
808 // If the device supports OSD symbols then use the up/down arrows. Otherwise assume it's a
809 // simple text device and use the '^' (carat) and 'V' for arrow approximations.
810 if (displayWasCleared
&& leftMenuColumn
> 0) { // make sure there's room to draw the symbol
811 if (currentCtx
.page
> 0) {
812 const uint8_t symbol
= displaySupportsOsdSymbols(pDisplay
) ? SYM_ARROW_NORTH
: '^';
813 displayWriteChar(pDisplay
, leftMenuColumn
- 1, top
, DISPLAYPORT_ATTR_NONE
, symbol
);
815 if (currentCtx
.page
< pageCount
- 1) {
816 const uint8_t symbol
= displaySupportsOsdSymbols(pDisplay
) ? SYM_ARROW_SOUTH
: 'V';
817 displayWriteChar(pDisplay
, leftMenuColumn
- 1, top
+ pageMaxRow
, DISPLAYPORT_ATTR_NONE
, symbol
);
823 const void *cmsMenuChange(displayPort_t
*pDisplay
, const void *ptr
)
825 const CMS_Menu
*pMenu
= (const CMS_Menu
*)ptr
;
831 #ifdef CMS_MENU_DEBUG
832 if (pMenu
->GUARD_type
!= OME_MENU
) {
833 // ptr isn't pointing to a CMS_Menu.
834 if (pMenu
->GUARD_type
<= OME_MAX
) {
835 strncpy(menuErrLabel
, pMenu
->GUARD_text
, sizeof(menuErrLabel
) - 1);
837 strncpy(menuErrLabel
, "LABEL UNKNOWN", sizeof(menuErrLabel
) - 1);
843 if (pMenu
!= currentCtx
.menu
) {
844 saveMenuInhibited
= false;
846 if (currentCtx
.menu
) {
847 // If we are opening the initial top-level menu, then currentCtx.menu will be NULL and nothing to do.
848 // Otherwise stack the current menu before moving to the selected menu.
849 if (menuStackIdx
>= CMS_MENU_STACK_LIMIT
- 1) {
850 // menu stack limit reached - prevent array overflow
853 menuStack
[menuStackIdx
++] = currentCtx
;
856 currentCtx
.menu
= pMenu
;
857 currentCtx
.cursorRow
= 0;
859 if (pMenu
->onEnter
) {
860 const void *result
= pMenu
->onEnter(pDisplay
);
861 if (result
== MENU_CHAIN_BACK
) {
862 return cmsMenuBack(pDisplay
);
866 cmsMenuCountPage(pDisplay
);
867 cmsPageSelect(pDisplay
, 0);
869 // The (pMenu == curretMenu) case occurs when reopening for display cycling
870 // currentCtx.cursorRow has been saved as absolute; convert it back to page + relative
872 int8_t cursorAbs
= currentCtx
.cursorRow
;
873 currentCtx
.cursorRow
= cursorAbs
% maxMenuItems
;
874 cmsMenuCountPage(pDisplay
);
875 cmsPageSelect(pDisplay
, cursorAbs
/ maxMenuItems
);
878 #if defined(CMS_PAGE_DEBUG)
885 void cmsMenuOpen(void)
887 const CMS_Menu
*startMenu
;
890 pCurrentDisplay
= cmsDisplayPortSelectCurrent();
891 if (!pCurrentDisplay
) {
895 currentCtx
= (cmsCtx_t
){ NULL
, 0, 0 };
896 startMenu
= &cmsx_menuMain
;
898 setArmingDisabled(ARMING_DISABLED_CMS_MENU
);
899 displayLayerSelect(pCurrentDisplay
, DISPLAYPORT_LAYER_FOREGROUND
); // make sure the foreground layer is active
900 if (osdConfig()->cms_background_type
!= DISPLAY_BACKGROUND_TRANSPARENT
) {
901 displaySetBackgroundType(pCurrentDisplay
, (displayPortBackground_e
)osdConfig()->cms_background_type
); // set the background type if not transparent
905 displayPort_t
*pNextDisplay
= cmsDisplayPortSelectNext();
906 startMenu
= currentCtx
.menu
;
907 if (pNextDisplay
!= pCurrentDisplay
) {
908 // DisplayPort has been changed.
909 // Convert cursorRow to absolute value
910 currentCtx
.cursorRow
= cmsCursorAbsolute(pCurrentDisplay
);
911 displaySetBackgroundType(pCurrentDisplay
, DISPLAY_BACKGROUND_TRANSPARENT
); // reset previous displayPort to transparent
912 displayRelease(pCurrentDisplay
);
913 pCurrentDisplay
= pNextDisplay
;
914 displaySetBackgroundType(pCurrentDisplay
, (displayPortBackground_e
)osdConfig()->cms_background_type
); // set the background type if not transparent
919 displayGrab(pCurrentDisplay
); // grab the display for use by the CMS
920 // FIXME this should probably not have a dependency on the OSD or OSD slave code
925 if ( pCurrentDisplay
->cols
< NORMAL_SCREEN_MIN_COLS
) {
927 linesPerMenuItem
= 2;
929 rightMenuColumn
= pCurrentDisplay
->cols
;
930 maxMenuItems
= (pCurrentDisplay
->rows
) / linesPerMenuItem
;
933 linesPerMenuItem
= 1;
935 #ifdef CMS_OSD_RIGHT_ALIGNED_VALUES
936 rightMenuColumn
= pCurrentDisplay
->cols
- 2;
938 rightMenuColumn
= pCurrentDisplay
->cols
- CMS_DRAW_BUFFER_LEN
;
940 maxMenuItems
= pCurrentDisplay
->rows
- 2;
943 if (pCurrentDisplay
->useFullscreen
) {
945 rightMenuColumn
= pCurrentDisplay
->cols
;
946 maxMenuItems
= pCurrentDisplay
->rows
;
949 cmsMenuChange(pCurrentDisplay
, startMenu
);
952 static void cmsTraverseGlobalExit(const CMS_Menu
*pMenu
)
954 for (const OSD_Entry
*p
= pMenu
->entries
; (p
->flags
& OSD_MENU_ELEMENT_MASK
) != OME_END
; p
++) {
955 if ((p
->flags
& OSD_MENU_ELEMENT_MASK
) == OME_Submenu
) {
956 cmsTraverseGlobalExit(p
->data
);
962 const void *cmsMenuExit(displayPort_t
*pDisplay
, const void *ptr
)
964 int exitType
= (int)ptr
;
967 case CMS_EXIT_SAVEREBOOT
:
969 case CMS_POPUP_SAVEREBOOT
:
971 cmsTraverseGlobalExit(&cmsx_menuMain
);
973 if (currentCtx
.menu
->onExit
) {
974 currentCtx
.menu
->onExit(pDisplay
, (OSD_Entry
*)NULL
); // Forced exit
977 if ((exitType
== CMS_POPUP_SAVE
) || (exitType
== CMS_POPUP_SAVEREBOOT
)) {
978 // traverse through the menu stack and call their onExit functions
979 for (int i
= menuStackIdx
- 1; i
>= 0; i
--) {
980 if (menuStack
[i
].menu
->onExit
) {
981 menuStack
[i
].menu
->onExit(pDisplay
, (OSD_Entry
*)NULL
);
986 saveConfigAndNotify();
995 displaySetBackgroundType(pCurrentDisplay
, DISPLAY_BACKGROUND_TRANSPARENT
); // reset the background to transparent
997 displayRelease(pDisplay
);
998 currentCtx
.menu
= NULL
;
1000 if ((exitType
== CMS_EXIT_SAVEREBOOT
) || (exitType
== CMS_POPUP_SAVEREBOOT
) || (exitType
== CMS_POPUP_EXITREBOOT
)) {
1001 displayClearScreen(pDisplay
);
1002 cmsDisplayWrite(pDisplay
, 5, 3, DISPLAYPORT_ATTR_NONE
, "REBOOTING...");
1005 displayRedraw(pDisplay
);
1014 unsetArmingDisabled(ARMING_DISABLED_CMS_MENU
);
1019 // Stick/key detection and key codes
1021 #define IS_HI(X) (rcData[X] > 1750)
1022 #define IS_LO(X) (rcData[X] < 1250)
1023 #define IS_MID(X) (rcData[X] > 1250 && rcData[X] < 1750)
1025 #define BUTTON_TIME 250 // msec
1026 #define BUTTON_PAUSE 500 // msec
1028 STATIC_UNIT_TESTED
uint16_t cmsHandleKey(displayPort_t
*pDisplay
, cms_key_e key
)
1030 uint16_t res
= BUTTON_TIME
;
1033 if (!currentCtx
.menu
) {
1037 if (key
== CMS_KEY_MENU
) {
1039 return BUTTON_PAUSE
;
1042 if (key
== CMS_KEY_ESC
) {
1043 if (osdElementEditing
) {
1044 osdElementEditing
= false;
1046 cmsMenuBack(pDisplay
);
1048 return BUTTON_PAUSE
;
1051 if (key
== CMS_KEY_SAVEMENU
&& !saveMenuInhibited
) {
1052 osdElementEditing
= false;
1053 cmsMenuChange(pDisplay
, getSaveExitMenu());
1055 return BUTTON_PAUSE
;
1058 if ((key
== CMS_KEY_DOWN
) && (!osdElementEditing
)) {
1059 if (currentCtx
.cursorRow
< pageMaxRow
) {
1060 currentCtx
.cursorRow
++;
1062 cmsPageNext(pDisplay
);
1063 currentCtx
.cursorRow
= 0; // Goto top in any case
1067 if ((key
== CMS_KEY_UP
) && (!osdElementEditing
)) {
1068 currentCtx
.cursorRow
--;
1070 // Skip non-title labels, strings and dynamic read-only entries
1071 while ((rowIsSkippable(pageTop
+ currentCtx
.cursorRow
)) && currentCtx
.cursorRow
> 0) {
1072 currentCtx
.cursorRow
--;
1074 if (currentCtx
.cursorRow
== -1 || ((pageTop
+ currentCtx
.cursorRow
)->flags
& OSD_MENU_ELEMENT_MASK
) == OME_Label
) {
1075 // Goto previous page
1076 cmsPagePrev(pDisplay
);
1077 currentCtx
.cursorRow
= pageMaxRow
;
1081 if ((key
== CMS_KEY_DOWN
|| key
== CMS_KEY_UP
) && (!osdElementEditing
)) {
1085 p
= pageTop
+ currentCtx
.cursorRow
;
1087 switch (p
->flags
& OSD_MENU_ELEMENT_MASK
) {
1089 if (key
== CMS_KEY_RIGHT
) {
1090 cmsMenuChange(pDisplay
, p
->data
);
1097 if (p
->func
&& key
== CMS_KEY_RIGHT
) {
1098 retval
= p
->func(pDisplay
, p
->data
);
1099 if (retval
== MENU_CHAIN_BACK
) {
1100 cmsMenuBack(pDisplay
);
1102 if ((p
->flags
& REBOOT_REQUIRED
)) {
1103 setRebootRequired();
1110 if (p
->func
&& key
== CMS_KEY_RIGHT
) {
1111 p
->func(pDisplay
, p
->data
);
1117 cmsMenuBack(pDisplay
);
1119 osdElementEditing
= false;
1124 uint8_t *val
= p
->data
;
1125 const uint8_t previousValue
= *val
;
1126 *val
= (key
== CMS_KEY_RIGHT
) ? 1 : 0;
1127 SET_PRINTVALUE(runtimeEntryFlags
[currentCtx
.cursorRow
]);
1128 if ((p
->flags
& REBOOT_REQUIRED
) && (*val
!= previousValue
)) {
1129 setRebootRequired();
1132 p
->func(pDisplay
, p
->data
);
1140 uint16_t *val
= (uint16_t *)p
->data
;
1141 const uint16_t previousValue
= *val
;
1142 if ((key
== CMS_KEY_RIGHT
) && (!osdElementEditing
)) {
1143 osdElementEditing
= true;
1144 osdProfileCursor
= 1;
1145 } else if (osdElementEditing
) {
1146 #ifdef USE_OSD_PROFILES
1147 if (key
== CMS_KEY_RIGHT
) {
1148 if (osdProfileCursor
< OSD_PROFILE_COUNT
) {
1152 if (key
== CMS_KEY_LEFT
) {
1153 if (osdProfileCursor
> 1) {
1158 if (key
== CMS_KEY_UP
) {
1159 *val
|= OSD_PROFILE_FLAG(osdProfileCursor
);
1161 if (key
== CMS_KEY_DOWN
) {
1162 *val
&= ~OSD_PROFILE_FLAG(osdProfileCursor
);
1165 SET_PRINTVALUE(runtimeEntryFlags
[currentCtx
.cursorRow
]);
1166 if ((p
->flags
& REBOOT_REQUIRED
) && (*val
!= previousValue
)) {
1167 setRebootRequired();
1176 OSD_UINT8_t
*ptr
= p
->data
;
1177 const uint16_t previousValue
= *ptr
->val
;
1178 if (key
== CMS_KEY_RIGHT
) {
1179 if (*ptr
->val
< ptr
->max
) {
1180 *ptr
->val
+= ptr
->step
;
1183 if (*ptr
->val
> ptr
->min
) {
1184 *ptr
->val
-= ptr
->step
;
1187 SET_PRINTVALUE(runtimeEntryFlags
[currentCtx
.cursorRow
]);
1188 if ((p
->flags
& REBOOT_REQUIRED
) && (*ptr
->val
!= previousValue
)) {
1189 setRebootRequired();
1192 p
->func(pDisplay
, p
);
1198 if ((p
->flags
& OSD_MENU_ELEMENT_MASK
) == OME_TAB
) {
1199 OSD_TAB_t
*ptr
= p
->data
;
1200 const uint8_t previousValue
= *ptr
->val
;
1202 if (key
== CMS_KEY_RIGHT
) {
1203 if (*ptr
->val
< ptr
->max
) {
1207 if (*ptr
->val
> 0) {
1212 p
->func(pDisplay
, p
->data
);
1214 SET_PRINTVALUE(runtimeEntryFlags
[currentCtx
.cursorRow
]);
1215 if ((p
->flags
& REBOOT_REQUIRED
) && (*ptr
->val
!= previousValue
)) {
1216 setRebootRequired();
1223 OSD_INT8_t
*ptr
= p
->data
;
1224 const int8_t previousValue
= *ptr
->val
;
1225 if (key
== CMS_KEY_RIGHT
) {
1226 if (*ptr
->val
< ptr
->max
) {
1227 *ptr
->val
+= ptr
->step
;
1230 if (*ptr
->val
> ptr
->min
) {
1231 *ptr
->val
-= ptr
->step
;
1234 SET_PRINTVALUE(runtimeEntryFlags
[currentCtx
.cursorRow
]);
1235 if ((p
->flags
& REBOOT_REQUIRED
) && (*ptr
->val
!= previousValue
)) {
1236 setRebootRequired();
1239 p
->func(pDisplay
, p
);
1246 OSD_UINT16_t
*ptr
= p
->data
;
1247 const uint16_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_INT16_t
*ptr
= p
->data
;
1270 const int16_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_UINT32_t
*ptr
= p
->data
;
1293 const uint32_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_INT32_t
*ptr
= p
->data
;
1316 const int32_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
);
1350 void cmsSetExternKey(cms_key_e extKey
)
1352 if (externKey
== CMS_KEY_NONE
)
1356 uint16_t cmsHandleKeyWithRepeat(displayPort_t
*pDisplay
, cms_key_e key
, int repeatCount
)
1360 for (int i
= 0 ; i
< repeatCount
; i
++) {
1361 ret
= cmsHandleKey(pDisplay
, key
);
1367 static void cmsUpdate(uint32_t currentTimeUs
)
1369 if (IS_RC_MODE_ACTIVE(BOXPARALYZE
)
1373 #ifdef USE_USB_CDC_HID
1374 || cdcDeviceIsMayBeActive() // If this target is used as a joystick, we should leave here.
1380 static int16_t rcDelayMs
= BUTTON_TIME
;
1381 static int holdCount
= 1;
1382 static int repeatCount
= 1;
1383 static int repeatBase
= 0;
1385 static uint32_t lastCalledMs
= 0;
1386 static uint32_t lastCmsHeartBeatMs
= 0;
1388 const uint32_t currentTimeMs
= currentTimeUs
/ 1000;
1391 // Detect menu invocation
1392 if (IS_MID(THROTTLE
) && IS_LO(YAW
) && IS_HI(PITCH
) && !ARMING_FLAG(ARMED
) && !IS_RC_MODE_ACTIVE(BOXSTICKCOMMANDDISABLE
)) {
1394 rcDelayMs
= BUTTON_PAUSE
; // Tends to overshoot if BUTTON_TIME
1401 cms_key_e key
= CMS_KEY_NONE
;
1403 if (externKey
!= CMS_KEY_NONE
) {
1404 rcDelayMs
= cmsHandleKey(pCurrentDisplay
, externKey
);
1405 externKey
= CMS_KEY_NONE
;
1407 if (IS_MID(THROTTLE
) && IS_LO(YAW
) && IS_HI(PITCH
) && !ARMING_FLAG(ARMED
)) {
1409 } else if (IS_HI(PITCH
)) {
1411 } else if (IS_LO(PITCH
)) {
1413 } else if (IS_LO(ROLL
)) {
1415 } else if (IS_HI(ROLL
)) {
1416 key
= CMS_KEY_RIGHT
;
1417 } else if (IS_LO(YAW
)) {
1419 } else if (IS_HI(YAW
)) {
1420 key
= CMS_KEY_SAVEMENU
;
1423 if (key
== CMS_KEY_NONE
) {
1424 // No 'key' pressed, reset repeat control
1429 // The 'key' is being pressed; keep counting
1433 if (rcDelayMs
> 0) {
1434 rcDelayMs
-= (currentTimeMs
- lastCalledMs
);
1436 rcDelayMs
= cmsHandleKeyWithRepeat(pCurrentDisplay
, key
, repeatCount
);
1438 // Key repeat effect is implemented in two phases.
1439 // First phldase is to decrease rcDelayMs reciprocal to hold time.
1440 // When rcDelayMs reached a certain limit (scheduling interval),
1441 // repeat rate will not raise anymore, so we call key handler
1442 // multiple times (repeatCount).
1444 // XXX Caveat: Most constants are adjusted pragmatically.
1445 // XXX Rewrite this someday, so it uses actual hold time instead
1446 // of holdCount, which depends on the scheduling interval.
1448 if (((key
== CMS_KEY_LEFT
) || (key
== CMS_KEY_RIGHT
)) && (holdCount
> 20)) {
1450 // Decrease rcDelayMs reciprocally
1452 rcDelayMs
/= (holdCount
- 20);
1454 // When we reach the scheduling limit,
1456 if (rcDelayMs
<= 50) {
1458 // start calling handler multiple times.
1460 if (repeatBase
== 0) {
1461 repeatBase
= holdCount
;
1464 repeatCount
= repeatCount
+ (holdCount
- repeatBase
) / 5;
1466 if (repeatCount
> 5) {
1474 cmsDrawMenu(pCurrentDisplay
, currentTimeUs
);
1476 if (currentTimeMs
> lastCmsHeartBeatMs
+ 500) {
1477 // Heart beat for external CMS display device @ 500msec
1478 // (Timeout @ 1000msec)
1479 displayHeartbeat(pCurrentDisplay
);
1480 lastCmsHeartBeatMs
= currentTimeMs
;
1484 // Some key (command), notably flash erase, takes too long to use the
1485 // currentTimeMs to be used as lastCalledMs (freezes CMS for a minute or so
1487 lastCalledMs
= millis();
1490 void cmsHandler(timeUs_t currentTimeUs
)
1492 if (cmsDeviceCount
> 0) {
1493 cmsUpdate(currentTimeUs
);
1500 cmsCurrentDevice
= -1;
1503 void inhibitSaveMenu(void)
1505 saveMenuInhibited
= true;
1508 void cmsAddMenuEntry(OSD_Entry
*menuEntry
, char *text
, uint16_t flags
, CMSEntryFuncPtr func
, void *data
)
1510 menuEntry
->text
= text
;
1511 menuEntry
->flags
= flags
;
1512 menuEntry
->func
= func
;
1513 menuEntry
->data
= data
;