/* * This file is part of Cleanflight and Betaflight. * * Cleanflight and Betaflight are free software. You can redistribute * this software and/or modify this software under the terms of the * GNU General Public License as published by the Free Software * Foundation, either version 3 of the License, or (at your option) * any later version. * * Cleanflight and Betaflight are distributed in the hope that they * will be useful, but WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this software. * * If not, see . */ /* Original OSD code created by Marcin Baliniak OSD-CMS separation by jflyper CMS-displayPort separation by jflyper and martinbudden */ //#define CMS_PAGE_DEBUG // For multi-page/menu debugging //#define CMS_MENU_DEBUG // For external menu content creators #include #include #include #include #include "platform.h" #ifdef USE_CMS #include "build/build_config.h" #include "build/debug.h" #include "build/version.h" #include "cms/cms.h" #include "cms/cms_menu_main.h" #include "cms/cms_menu_saveexit.h" #include "cms/cms_types.h" #include "common/maths.h" #include "common/typeconversion.h" #include "drivers/system.h" #include "drivers/time.h" #include "drivers/motor.h" // For rcData, stopAllMotors, stopPwmAllMotors #include "config/feature.h" #include "pg/pg.h" #include "pg/pg_ids.h" #include "pg/rx.h" // For 'ARM' related #include "config/config.h" #include "fc/rc_controls.h" #include "fc/runtime_config.h" #include "flight/mixer.h" // For VISIBLE* #include "io/rcdevice_cam.h" #include "osd/osd.h" #include "rx/rx.h" #ifdef USE_USB_CDC_HID #include "sensors/battery.h" #include "pg/usb.h" #endif // DisplayPort management #ifndef CMS_MAX_DEVICE #define CMS_MAX_DEVICE 4 #endif #define CMS_MENU_STACK_LIMIT 10 displayPort_t *pCurrentDisplay; static displayPort_t *cmsDisplayPorts[CMS_MAX_DEVICE]; static int cmsDeviceCount; static int cmsCurrentDevice = -1; #ifdef USE_OSD static unsigned int osdProfileCursor = 1; #endif int menuChainBack; bool cmsDisplayPortRegister(displayPort_t *pDisplay) { if (cmsDeviceCount == CMS_MAX_DEVICE) return false; cmsDisplayPorts[cmsDeviceCount++] = pDisplay; return true; } static displayPort_t *cmsDisplayPortSelectCurrent(void) { if (cmsDeviceCount == 0) return NULL; if (cmsCurrentDevice < 0) cmsCurrentDevice = 0; return cmsDisplayPorts[cmsCurrentDevice]; } static displayPort_t *cmsDisplayPortSelectNext(void) { if (cmsDeviceCount == 0) return NULL; cmsCurrentDevice = (cmsCurrentDevice + 1) % cmsDeviceCount; // -1 Okay return cmsDisplayPorts[cmsCurrentDevice]; } bool cmsDisplayPortSelect(displayPort_t *instance) { if (cmsDeviceCount == 0) { return false; } for (int i = 0; i < cmsDeviceCount; i++) { if (cmsDisplayPortSelectNext() == instance) { return true; } } return false; } #define CMS_UPDATE_INTERVAL_US 50000 // Interval of key scans (microsec) #define CMS_POLL_INTERVAL_US 100000 // Interval of polling dynamic values (microsec) // XXX LEFT_MENU_COLUMN and RIGHT_MENU_COLUMN must be adjusted // dynamically depending on size of the active output device, // or statically to accomodate sizes of all supported devices. // // Device characteristics // OLED // 21 cols x 8 rows // 128x64 with 5x7 (6x8) : 21 cols x 8 rows // MAX7456 (PAL) // 30 cols x 16 rows // MAX7456 (NTSC) // 30 cols x 13 rows // HoTT Telemetry Screen // 21 cols x 8 rows // // Spektrum SRXL Telemtry Textgenerator // 13 cols x 9 rows, top row printed as a Bold Heading // Needs the "smallScreen" adaptions #define CMS_MAX_ROWS 16 #define NORMAL_SCREEN_MIN_COLS 18 // Less is a small screen static bool smallScreen; static uint8_t leftMenuColumn; static uint8_t rightMenuColumn; static uint8_t maxMenuItems; static uint8_t linesPerMenuItem; static cms_key_e externKey = CMS_KEY_NONE; static bool osdElementEditing = false; bool cmsInMenu = false; typedef struct cmsCtx_s { const CMS_Menu *menu; // menu for this context uint8_t page; // page in the menu int8_t cursorRow; // cursorRow in the page } cmsCtx_t; static cmsCtx_t menuStack[CMS_MENU_STACK_LIMIT]; static uint8_t menuStackIdx = 0; static int8_t pageCount; // Number of pages in the current menu static const OSD_Entry *pageTop; // First entry for the current page static uint8_t pageMaxRow; // Max row in the current page static cmsCtx_t currentCtx; static bool saveMenuInhibited = false; #ifdef CMS_MENU_DEBUG // For external menu content creators static char menuErrLabel[21 + 1] = "RANDOM DATA"; static OSD_Entry menuErrEntries[] = { { "BROKEN MENU", OME_Label, NULL, NULL, 0 }, { menuErrLabel, OME_Label, NULL, NULL, 0 }, { "BACK", OME_Back, NULL, NULL, 0 }, { NULL, OME_END, NULL, NULL, 0 } }; static CMS_Menu menuErr = { "MENUERR", OME_MENU, NULL, NULL, NULL, menuErrEntries, }; #endif #ifdef CMS_PAGE_DEBUG #define cmsPageDebug() { \ debug[0] = pageCount; \ debug[1] = currentCtx.page; \ debug[2] = pageMaxRow; \ debug[3] = currentCtx.cursorRow; } struct _dummy #endif static void cmsUpdateMaxRow(displayPort_t *instance) { UNUSED(instance); pageMaxRow = 0; for (const OSD_Entry *ptr = pageTop; ptr->type != OME_END; ptr++) { pageMaxRow++; } if (pageMaxRow > maxMenuItems) { pageMaxRow = maxMenuItems; } if (pageMaxRow > CMS_MAX_ROWS) { pageMaxRow = CMS_MAX_ROWS; } pageMaxRow--; } static uint8_t cmsCursorAbsolute(displayPort_t *instance) { UNUSED(instance); return currentCtx.cursorRow + currentCtx.page * maxMenuItems; } uint8_t runtimeEntryFlags[CMS_MAX_ROWS] = { 0 }; static void cmsPageSelect(displayPort_t *instance, int8_t newpage) { currentCtx.page = (newpage + pageCount) % pageCount; pageTop = ¤tCtx.menu->entries[currentCtx.page * maxMenuItems]; cmsUpdateMaxRow(instance); const OSD_Entry *p; int i; for (p = pageTop, i = 0; (p <= pageTop + pageMaxRow); p++, i++) { runtimeEntryFlags[i] = p->flags; } displayClearScreen(instance); } static void cmsPageNext(displayPort_t *instance) { cmsPageSelect(instance, currentCtx.page + 1); } static void cmsPagePrev(displayPort_t *instance) { cmsPageSelect(instance, currentCtx.page - 1); } static void cmsFormatFloat(int32_t value, char *floatString) { uint8_t k; // np. 3450 itoa(100000 + value, floatString, 10); // Create string from abs of integer value // 103450 floatString[0] = floatString[1]; floatString[1] = floatString[2]; floatString[2] = '.'; // 03.450 // usuwam koncowe zera i kropke // Keep the first decimal place for (k = 5; k > 3; k--) if (floatString[k] == '0' || floatString[k] == '.') floatString[k] = 0; else break; // oraz zero wiodonce if (floatString[0] == '0') floatString[0] = ' '; } // CMS on OSD legacy was to use LEFT aligned values, not the RIGHT way ;-) #define CMS_OSD_RIGHT_ALIGNED_VALUES #ifndef CMS_OSD_RIGHT_ALIGNED_VALUES // Pad buffer to the left, i.e. align left static void cmsPadRightToSize(char *buf, int size) { int i; for (i = 0 ; i < size ; i++) { if (buf[i] == 0) break; } for ( ; i < size ; i++) { buf[i] = ' '; } buf[size] = 0; } #endif // Pad buffer to the left, i.e. align right static void cmsPadLeftToSize(char *buf, int size) { int i,j; int len = strlen(buf); for (i = size - 1, j = size - len ; i - j >= 0 ; i--) { buf[i] = buf[i - j]; } for ( ; i >= 0 ; i--) { buf[i] = ' '; } buf[size] = 0; } static void cmsPadToSize(char *buf, int size) { // Make absolutely sure the string terminated. buf[size] = 0x00, #ifdef CMS_OSD_RIGHT_ALIGNED_VALUES cmsPadLeftToSize(buf, size); #else smallScreen ? cmsPadLeftToSize(buf, size) : cmsPadRightToSize(buf, size); #endif } static int cmsDrawMenuItemValue(displayPort_t *pDisplay, char *buff, uint8_t row, uint8_t maxSize) { int colpos; int cnt; cmsPadToSize(buff, maxSize); #ifdef CMS_OSD_RIGHT_ALIGNED_VALUES colpos = rightMenuColumn - maxSize; #else colpos = smallScreen ? rightMenuColumn - maxSize : rightMenuColumn; #endif cnt = displayWrite(pDisplay, colpos, row, DISPLAYPORT_ATTR_NONE, buff); return cnt; } static int cmsDrawMenuEntry(displayPort_t *pDisplay, const OSD_Entry *p, uint8_t row, bool selectedRow, uint8_t *flags) { #define CMS_DRAW_BUFFER_LEN 12 #define CMS_NUM_FIELD_LEN 5 #define CMS_CURSOR_BLINK_DELAY_MS 500 char buff[CMS_DRAW_BUFFER_LEN +1]; // Make room for null terminator. int cnt = 0; #ifndef USE_OSD UNUSED(selectedRow); #endif if (smallScreen) { row++; } switch (p->type) { case OME_String: if (IS_PRINTVALUE(*flags) && p->data) { strncpy(buff, p->data, CMS_DRAW_BUFFER_LEN); cnt = cmsDrawMenuItemValue(pDisplay, buff, row, CMS_DRAW_BUFFER_LEN); CLR_PRINTVALUE(*flags); } break; case OME_Submenu: case OME_Funcall: if (IS_PRINTVALUE(*flags)) { buff[0]= 0x0; if (p->type == OME_Submenu && p->func && *flags & OPTSTRING) { // Special case of sub menu entry with optional value display. const char *str = p->func(pDisplay, p->data); strncpy(buff, str, CMS_DRAW_BUFFER_LEN); } else if (p->type == OME_Funcall && p->data) { strncpy(buff, p->data, CMS_DRAW_BUFFER_LEN); } strncat(buff, ">", CMS_DRAW_BUFFER_LEN); row = smallScreen ? row - 1 : row; cnt = cmsDrawMenuItemValue(pDisplay, buff, row, strlen(buff)); CLR_PRINTVALUE(*flags); } break; case OME_Bool: if (IS_PRINTVALUE(*flags) && p->data) { if (*((uint8_t *)(p->data))) { strcpy(buff, "YES"); } else { strcpy(buff, "NO "); } cnt = cmsDrawMenuItemValue(pDisplay, buff, row, 3); CLR_PRINTVALUE(*flags); } break; case OME_TAB: if (IS_PRINTVALUE(*flags)) { OSD_TAB_t *ptr = p->data; char * str = (char *)ptr->names[*ptr->val]; strncpy(buff, str, CMS_DRAW_BUFFER_LEN); cnt = cmsDrawMenuItemValue(pDisplay, buff, row, CMS_DRAW_BUFFER_LEN); CLR_PRINTVALUE(*flags); } break; #ifdef USE_OSD case OME_VISIBLE: if (IS_PRINTVALUE(*flags) && p->data) { uint16_t *val = (uint16_t *)p->data; bool cursorBlink = millis() % (2 * CMS_CURSOR_BLINK_DELAY_MS) < CMS_CURSOR_BLINK_DELAY_MS; for (unsigned x = 1; x < OSD_PROFILE_COUNT + 1; x++) { if (VISIBLE_IN_OSD_PROFILE(*val, x)) { if (osdElementEditing && cursorBlink && selectedRow && (x == osdProfileCursor)) { strcpy(buff + x - 1, " "); } else { strcpy(buff + x - 1, "X"); } } else { if (osdElementEditing && cursorBlink && selectedRow && (x == osdProfileCursor)) { strcpy(buff + x - 1, " "); } else { strcpy(buff + x - 1, "-"); } } } cnt = cmsDrawMenuItemValue(pDisplay, buff, row, 3); CLR_PRINTVALUE(*flags); } break; #endif case OME_UINT8: if (IS_PRINTVALUE(*flags) && p->data) { OSD_UINT8_t *ptr = p->data; itoa(*ptr->val, buff, 10); cnt = cmsDrawMenuItemValue(pDisplay, buff, row, CMS_NUM_FIELD_LEN); CLR_PRINTVALUE(*flags); } break; case OME_INT8: if (IS_PRINTVALUE(*flags) && p->data) { OSD_INT8_t *ptr = p->data; itoa(*ptr->val, buff, 10); cnt = cmsDrawMenuItemValue(pDisplay, buff, row, CMS_NUM_FIELD_LEN); CLR_PRINTVALUE(*flags); } break; case OME_UINT16: if (IS_PRINTVALUE(*flags) && p->data) { OSD_UINT16_t *ptr = p->data; itoa(*ptr->val, buff, 10); cnt = cmsDrawMenuItemValue(pDisplay, buff, row, CMS_NUM_FIELD_LEN); CLR_PRINTVALUE(*flags); } break; case OME_INT16: if (IS_PRINTVALUE(*flags) && p->data) { OSD_UINT16_t *ptr = p->data; itoa(*ptr->val, buff, 10); cnt = cmsDrawMenuItemValue(pDisplay, buff, row, CMS_NUM_FIELD_LEN); CLR_PRINTVALUE(*flags); } break; case OME_FLOAT: if (IS_PRINTVALUE(*flags) && p->data) { OSD_FLOAT_t *ptr = p->data; cmsFormatFloat(*ptr->val * ptr->multipler, buff); cnt = cmsDrawMenuItemValue(pDisplay, buff, row, CMS_NUM_FIELD_LEN); CLR_PRINTVALUE(*flags); } break; case OME_Label: if (IS_PRINTVALUE(*flags) && p->data) { // A label with optional string, immediately following text cnt = displayWrite(pDisplay, leftMenuColumn + 1 + (uint8_t)strlen(p->text), row, DISPLAYPORT_ATTR_NONE, p->data); CLR_PRINTVALUE(*flags); } break; case OME_OSD_Exit: case OME_END: case OME_Back: break; case OME_MENU: // Fall through default: #ifdef CMS_MENU_DEBUG // Shouldn't happen. Notify creator of this menu content #ifdef CMS_OSD_RIGHT_ALIGNED_VALUES cnt = displayWrite(pDisplay, rightMenuColumn - 6, row, DISPLAYPORT_ATTR_NONE, "BADENT"); #else cnt = displayWrite(pDisplay, rightMenuColumn, row, DISPLAYPORT_ATTR_NONE, "BADENT"); #endif #endif break; } return cnt; } static void cmsMenuCountPage(displayPort_t *pDisplay) { UNUSED(pDisplay); const OSD_Entry *p; for (p = currentCtx.menu->entries; p->type != OME_END; p++); pageCount = (p - currentCtx.menu->entries - 1) / maxMenuItems + 1; } STATIC_UNIT_TESTED const void *cmsMenuBack(displayPort_t *pDisplay) { // Let onExit function decide whether to allow exit or not. if (currentCtx.menu->onExit) { const void *result = currentCtx.menu->onExit(pageTop + currentCtx.cursorRow); if (result == MENU_CHAIN_BACK) { return result; } } saveMenuInhibited = false; if (!menuStackIdx) { return NULL; } currentCtx = menuStack[--menuStackIdx]; cmsMenuCountPage(pDisplay); cmsPageSelect(pDisplay, currentCtx.page); #if defined(CMS_PAGE_DEBUG) cmsPageDebug(); #endif return NULL; } // Skip read-only entries static bool rowIsSkippable(const OSD_Entry *row) { if (row->type == OME_Label) { return true; } if (row->type == OME_String) { return true; } if ((row->type == OME_UINT16 || row->type == OME_INT16) && row->flags == DYNAMIC) { return true; } return false; } static void cmsDrawMenu(displayPort_t *pDisplay, uint32_t currentTimeUs) { if (!pageTop || !cmsInMenu) { return; } uint8_t i; const OSD_Entry *p; uint8_t top = smallScreen ? 1 : (pDisplay->rows - pageMaxRow)/2; // Polled (dynamic) value display denominator. bool drawPolled = false; static uint32_t lastPolledUs = 0; if (currentTimeUs > lastPolledUs + CMS_POLL_INTERVAL_US) { drawPolled = true; lastPolledUs = currentTimeUs; } uint32_t room = displayTxBytesFree(pDisplay); if (pDisplay->cleared) { for (p = pageTop, i= 0; (p <= pageTop + pageMaxRow); p++, i++) { SET_PRINTLABEL(runtimeEntryFlags[i]); SET_PRINTVALUE(runtimeEntryFlags[i]); } pDisplay->cleared = false; } else if (drawPolled) { for (p = pageTop, i = 0; (p <= pageTop + pageMaxRow); p++, i++) { if (IS_DYNAMIC(p)) SET_PRINTVALUE(runtimeEntryFlags[i]); } } // Cursor manipulation while (rowIsSkippable(pageTop + currentCtx.cursorRow)) { // skip labels, strings and dynamic read-only entries currentCtx.cursorRow++; } #if defined(CMS_PAGE_DEBUG) cmsPageDebug(); #endif if (pDisplay->cursorRow >= 0 && currentCtx.cursorRow != pDisplay->cursorRow) { room -= displayWrite(pDisplay, leftMenuColumn, top + pDisplay->cursorRow * linesPerMenuItem, DISPLAYPORT_ATTR_NONE, " "); } if (room < 30) { return; } if (pDisplay->cursorRow != currentCtx.cursorRow) { room -= displayWrite(pDisplay, leftMenuColumn, top + currentCtx.cursorRow * linesPerMenuItem, DISPLAYPORT_ATTR_NONE, ">"); pDisplay->cursorRow = currentCtx.cursorRow; } if (room < 30) { return; } if (currentCtx.menu->onDisplayUpdate) { const void *result = currentCtx.menu->onDisplayUpdate(pageTop + currentCtx.cursorRow); if (result == MENU_CHAIN_BACK) { cmsMenuBack(pDisplay); return; } } // Print text labels for (i = 0, p = pageTop; (p <= pageTop + pageMaxRow); i++, p++) { if (IS_PRINTLABEL(runtimeEntryFlags[i])) { uint8_t coloff = leftMenuColumn; coloff += (p->type == OME_Label) ? 0 : 1; room -= displayWrite(pDisplay, coloff, top + i * linesPerMenuItem, DISPLAYPORT_ATTR_NONE, p->text); CLR_PRINTLABEL(runtimeEntryFlags[i]); if (room < 30) { return; } } // Print values // XXX Polled values at latter positions in the list may not be // XXX printed if not enough room in the middle of the list. if (IS_PRINTVALUE(runtimeEntryFlags[i])) { bool selectedRow = i == currentCtx.cursorRow; room -= cmsDrawMenuEntry(pDisplay, p, top + i * linesPerMenuItem, selectedRow, &runtimeEntryFlags[i]); if (room < 30) return; } } } const void *cmsMenuChange(displayPort_t *pDisplay, const void *ptr) { const CMS_Menu *pMenu = (const CMS_Menu *)ptr; if (!pMenu) { return NULL; } #ifdef CMS_MENU_DEBUG if (pMenu->GUARD_type != OME_MENU) { // ptr isn't pointing to a CMS_Menu. if (pMenu->GUARD_type <= OME_MAX) { strncpy(menuErrLabel, pMenu->GUARD_text, sizeof(menuErrLabel) - 1); } else { strncpy(menuErrLabel, "LABEL UNKNOWN", sizeof(menuErrLabel) - 1); } pMenu = &menuErr; } #endif if (pMenu != currentCtx.menu) { saveMenuInhibited = false; // Stack the current menu and move to a new menu. if (menuStackIdx >= CMS_MENU_STACK_LIMIT - 1) { // menu stack limit reached - prevent array overflow return NULL; } menuStack[menuStackIdx++] = currentCtx; currentCtx.menu = pMenu; currentCtx.cursorRow = 0; if (pMenu->onEnter) { const void *result = pMenu->onEnter(); if (result == MENU_CHAIN_BACK) { return cmsMenuBack(pDisplay); } } cmsMenuCountPage(pDisplay); cmsPageSelect(pDisplay, 0); } else { // The (pMenu == curretMenu) case occurs when reopening for display cycling // currentCtx.cursorRow has been saved as absolute; convert it back to page + relative int8_t cursorAbs = currentCtx.cursorRow; currentCtx.cursorRow = cursorAbs % maxMenuItems; cmsMenuCountPage(pDisplay); cmsPageSelect(pDisplay, cursorAbs / maxMenuItems); } #if defined(CMS_PAGE_DEBUG) cmsPageDebug(); #endif return NULL; } void cmsMenuOpen(void) { if (!cmsInMenu) { // New open pCurrentDisplay = cmsDisplayPortSelectCurrent(); if (!pCurrentDisplay) return; cmsInMenu = true; currentCtx = (cmsCtx_t){ &cmsx_menuMain, 0, 0 }; menuStackIdx = 0; setArmingDisabled(ARMING_DISABLED_CMS_MENU); displayLayerSelect(pCurrentDisplay, DISPLAYPORT_LAYER_FOREGROUND); // make sure the foreground layer is active } else { // Switch display displayPort_t *pNextDisplay = cmsDisplayPortSelectNext(); if (pNextDisplay != pCurrentDisplay) { // DisplayPort has been changed. // Convert cursorRow to absolute value currentCtx.cursorRow = cmsCursorAbsolute(pCurrentDisplay); displayRelease(pCurrentDisplay); pCurrentDisplay = pNextDisplay; } else { return; } } displayGrab(pCurrentDisplay); // grab the display for use by the CMS if ( pCurrentDisplay->cols < NORMAL_SCREEN_MIN_COLS) { smallScreen = true; linesPerMenuItem = 2; leftMenuColumn = 0; rightMenuColumn = pCurrentDisplay->cols; maxMenuItems = (pCurrentDisplay->rows) / linesPerMenuItem; } else { smallScreen = false; linesPerMenuItem = 1; leftMenuColumn = 2; #ifdef CMS_OSD_RIGHT_ALIGNED_VALUES rightMenuColumn = pCurrentDisplay->cols - 2; #else rightMenuColumn = pCurrentDisplay->cols - CMS_DRAW_BUFFER_LEN; #endif maxMenuItems = pCurrentDisplay->rows - 2; } if (pCurrentDisplay->useFullscreen) { leftMenuColumn = 0; rightMenuColumn = pCurrentDisplay->cols; maxMenuItems = pCurrentDisplay->rows; } cmsMenuChange(pCurrentDisplay, currentCtx.menu); } static void cmsTraverseGlobalExit(const CMS_Menu *pMenu) { for (const OSD_Entry *p = pMenu->entries; p->type != OME_END ; p++) { if (p->type == OME_Submenu) { cmsTraverseGlobalExit(p->data); } } } const void *cmsMenuExit(displayPort_t *pDisplay, const void *ptr) { int exitType = (int)ptr; switch (exitType) { case CMS_EXIT_SAVE: case CMS_EXIT_SAVEREBOOT: case CMS_POPUP_SAVE: case CMS_POPUP_SAVEREBOOT: cmsTraverseGlobalExit(&cmsx_menuMain); if (currentCtx.menu->onExit) { currentCtx.menu->onExit((OSD_Entry *)NULL); // Forced exit } if ((exitType == CMS_POPUP_SAVE) || (exitType == CMS_POPUP_SAVEREBOOT)) { // traverse through the menu stack and call their onExit functions for (int i = menuStackIdx - 1; i >= 0; i--) { if (menuStack[i].menu->onExit) { menuStack[i].menu->onExit((OSD_Entry *)NULL); } } } saveConfigAndNotify(); break; case CMS_EXIT: break; } cmsInMenu = false; displayRelease(pDisplay); currentCtx.menu = NULL; if ((exitType == CMS_EXIT_SAVEREBOOT) || (exitType == CMS_POPUP_SAVEREBOOT) || (exitType == CMS_POPUP_EXITREBOOT)) { displayClearScreen(pDisplay); displayWrite(pDisplay, 5, 3, DISPLAYPORT_ATTR_NONE, "REBOOTING..."); displayResync(pDisplay); // Was max7456RefreshAll(); why at this timing? stopMotors(); motorShutdown(); delay(200); systemReset(); } unsetArmingDisabled(ARMING_DISABLED_CMS_MENU); return NULL; } // Stick/key detection and key codes #define IS_HI(X) (rcData[X] > 1750) #define IS_LO(X) (rcData[X] < 1250) #define IS_MID(X) (rcData[X] > 1250 && rcData[X] < 1750) #define BUTTON_TIME 250 // msec #define BUTTON_PAUSE 500 // msec STATIC_UNIT_TESTED uint16_t cmsHandleKey(displayPort_t *pDisplay, cms_key_e key) { uint16_t res = BUTTON_TIME; const OSD_Entry *p; if (!currentCtx.menu) { return res; } if (key == CMS_KEY_MENU) { cmsMenuOpen(); return BUTTON_PAUSE; } if (key == CMS_KEY_ESC) { if (osdElementEditing) { osdElementEditing = false; } else { cmsMenuBack(pDisplay); } return BUTTON_PAUSE; } if (key == CMS_KEY_SAVEMENU && !saveMenuInhibited) { osdElementEditing = false; cmsMenuChange(pDisplay, getSaveExitMenu()); return BUTTON_PAUSE; } if ((key == CMS_KEY_DOWN) && (!osdElementEditing)) { if (currentCtx.cursorRow < pageMaxRow) { currentCtx.cursorRow++; } else { cmsPageNext(pDisplay); currentCtx.cursorRow = 0; // Goto top in any case } } if ((key == CMS_KEY_UP) && (!osdElementEditing)) { currentCtx.cursorRow--; // Skip non-title labels, strings and dynamic read-only entries while ((rowIsSkippable(pageTop + currentCtx.cursorRow)) && currentCtx.cursorRow > 0) { currentCtx.cursorRow--; } if (currentCtx.cursorRow == -1 || (pageTop + currentCtx.cursorRow)->type == OME_Label) { // Goto previous page cmsPagePrev(pDisplay); currentCtx.cursorRow = pageMaxRow; } } if ((key == CMS_KEY_DOWN || key == CMS_KEY_UP) && (!osdElementEditing)) { return res; } p = pageTop + currentCtx.cursorRow; switch (p->type) { case OME_Submenu: if (key == CMS_KEY_RIGHT) { cmsMenuChange(pDisplay, p->data); res = BUTTON_PAUSE; } break; case OME_Funcall:; const void *retval; if (p->func && key == CMS_KEY_RIGHT) { retval = p->func(pDisplay, p->data); if (retval == MENU_CHAIN_BACK) { cmsMenuBack(pDisplay); } res = BUTTON_PAUSE; } break; case OME_OSD_Exit: if (p->func && key == CMS_KEY_RIGHT) { p->func(pDisplay, p->data); res = BUTTON_PAUSE; } break; case OME_Back: cmsMenuBack(pDisplay); res = BUTTON_PAUSE; osdElementEditing = false; break; case OME_Bool: if (p->data) { uint8_t *val = p->data; const uint8_t previousValue = *val; *val = (key == CMS_KEY_RIGHT) ? 1 : 0; SET_PRINTVALUE(runtimeEntryFlags[currentCtx.cursorRow]); if ((p->flags & REBOOT_REQUIRED) && (*val != previousValue)) { setRebootRequired(); } if (p->func) { p->func(pDisplay, p->data); } } break; #ifdef USE_OSD case OME_VISIBLE: if (p->data) { uint16_t *val = (uint16_t *)p->data; const uint16_t previousValue = *val; if ((key == CMS_KEY_RIGHT) && (!osdElementEditing)) { osdElementEditing = true; osdProfileCursor = 1; } else if (osdElementEditing) { #ifdef USE_OSD_PROFILES if (key == CMS_KEY_RIGHT) { if (osdProfileCursor < OSD_PROFILE_COUNT) { osdProfileCursor++; } } if (key == CMS_KEY_LEFT) { if (osdProfileCursor > 1) { osdProfileCursor--; } } #endif if (key == CMS_KEY_UP) { *val |= OSD_PROFILE_FLAG(osdProfileCursor); } if (key == CMS_KEY_DOWN) { *val &= ~OSD_PROFILE_FLAG(osdProfileCursor); } } SET_PRINTVALUE(runtimeEntryFlags[currentCtx.cursorRow]); if ((p->flags & REBOOT_REQUIRED) && (*val != previousValue)) { setRebootRequired(); } } break; #endif case OME_UINT8: case OME_FLOAT: if (p->data) { OSD_UINT8_t *ptr = p->data; const uint16_t previousValue = *ptr->val; if (key == CMS_KEY_RIGHT) { if (*ptr->val < ptr->max) { *ptr->val += ptr->step; } } else { if (*ptr->val > ptr->min) { *ptr->val -= ptr->step; } } SET_PRINTVALUE(runtimeEntryFlags[currentCtx.cursorRow]); if ((p->flags & REBOOT_REQUIRED) && (*ptr->val != previousValue)) { setRebootRequired(); } if (p->func) { p->func(pDisplay, p); } } break; case OME_TAB: if (p->type == OME_TAB) { OSD_TAB_t *ptr = p->data; const uint8_t previousValue = *ptr->val; if (key == CMS_KEY_RIGHT) { if (*ptr->val < ptr->max) { *ptr->val += 1; } } else { if (*ptr->val > 0) { *ptr->val -= 1; } } if (p->func) { p->func(pDisplay, p->data); } SET_PRINTVALUE(runtimeEntryFlags[currentCtx.cursorRow]); if ((p->flags & REBOOT_REQUIRED) && (*ptr->val != previousValue)) { setRebootRequired(); } } break; case OME_INT8: if (p->data) { OSD_INT8_t *ptr = p->data; const int8_t previousValue = *ptr->val; if (key == CMS_KEY_RIGHT) { if (*ptr->val < ptr->max) { *ptr->val += ptr->step; } } else { if (*ptr->val > ptr->min) { *ptr->val -= ptr->step; } } SET_PRINTVALUE(runtimeEntryFlags[currentCtx.cursorRow]); if ((p->flags & REBOOT_REQUIRED) && (*ptr->val != previousValue)) { setRebootRequired(); } if (p->func) { p->func(pDisplay, p); } } break; case OME_UINT16: if (p->data) { OSD_UINT16_t *ptr = p->data; const uint16_t previousValue = *ptr->val; if (key == CMS_KEY_RIGHT) { if (*ptr->val < ptr->max) { *ptr->val += ptr->step; } } else { if (*ptr->val > ptr->min) { *ptr->val -= ptr->step; } } SET_PRINTVALUE(runtimeEntryFlags[currentCtx.cursorRow]); if ((p->flags & REBOOT_REQUIRED) && (*ptr->val != previousValue)) { setRebootRequired(); } if (p->func) { p->func(pDisplay, p); } } break; case OME_INT16: if (p->data) { OSD_INT16_t *ptr = p->data; const int16_t previousValue = *ptr->val; if (key == CMS_KEY_RIGHT) { if (*ptr->val < ptr->max) { *ptr->val += ptr->step; } } else { if (*ptr->val > ptr->min) { *ptr->val -= ptr->step; } } SET_PRINTVALUE(runtimeEntryFlags[currentCtx.cursorRow]); if ((p->flags & REBOOT_REQUIRED) && (*ptr->val != previousValue)) { setRebootRequired(); } if (p->func) { p->func(pDisplay, p); } } break; case OME_String: break; case OME_Label: case OME_END: break; case OME_MENU: // Shouldn't happen break; } return res; } void cmsSetExternKey(cms_key_e extKey) { if (externKey == CMS_KEY_NONE) externKey = extKey; } uint16_t cmsHandleKeyWithRepeat(displayPort_t *pDisplay, cms_key_e key, int repeatCount) { uint16_t ret = 0; for (int i = 0 ; i < repeatCount ; i++) { ret = cmsHandleKey(pDisplay, key); } return ret; } static void cmsUpdate(uint32_t currentTimeUs) { if (IS_RC_MODE_ACTIVE(BOXPARALYZE) #ifdef USE_RCDEVICE || rcdeviceInMenu #endif #ifdef USE_USB_CDC_HID || (getBatteryCellCount() == 0 && usbDevConfig()->type == COMPOSITE) #endif ) { return; } static int16_t rcDelayMs = BUTTON_TIME; static int holdCount = 1; static int repeatCount = 1; static int repeatBase = 0; static uint32_t lastCalledMs = 0; static uint32_t lastCmsHeartBeatMs = 0; const uint32_t currentTimeMs = currentTimeUs / 1000; if (!cmsInMenu) { // Detect menu invocation if (IS_MID(THROTTLE) && IS_LO(YAW) && IS_HI(PITCH) && !ARMING_FLAG(ARMED)) { cmsMenuOpen(); rcDelayMs = BUTTON_PAUSE; // Tends to overshoot if BUTTON_TIME } } else { // // Scan 'key' first // cms_key_e key = CMS_KEY_NONE; if (externKey != CMS_KEY_NONE) { rcDelayMs = cmsHandleKey(pCurrentDisplay, externKey); externKey = CMS_KEY_NONE; } else { if (IS_MID(THROTTLE) && IS_LO(YAW) && IS_HI(PITCH) && !ARMING_FLAG(ARMED)) { key = CMS_KEY_MENU; } else if (IS_HI(PITCH)) { key = CMS_KEY_UP; } else if (IS_LO(PITCH)) { key = CMS_KEY_DOWN; } else if (IS_LO(ROLL)) { key = CMS_KEY_LEFT; } else if (IS_HI(ROLL)) { key = CMS_KEY_RIGHT; } else if (IS_LO(YAW)) { key = CMS_KEY_ESC; } else if (IS_HI(YAW)) { key = CMS_KEY_SAVEMENU; } if (key == CMS_KEY_NONE) { // No 'key' pressed, reset repeat control holdCount = 1; repeatCount = 1; repeatBase = 0; } else { // The 'key' is being pressed; keep counting ++holdCount; } if (rcDelayMs > 0) { rcDelayMs -= (currentTimeMs - lastCalledMs); } else if (key) { rcDelayMs = cmsHandleKeyWithRepeat(pCurrentDisplay, key, repeatCount); // Key repeat effect is implemented in two phases. // First phldase is to decrease rcDelayMs reciprocal to hold time. // When rcDelayMs reached a certain limit (scheduling interval), // repeat rate will not raise anymore, so we call key handler // multiple times (repeatCount). // // XXX Caveat: Most constants are adjusted pragmatically. // XXX Rewrite this someday, so it uses actual hold time instead // of holdCount, which depends on the scheduling interval. if (((key == CMS_KEY_LEFT) || (key == CMS_KEY_RIGHT)) && (holdCount > 20)) { // Decrease rcDelayMs reciprocally rcDelayMs /= (holdCount - 20); // When we reach the scheduling limit, if (rcDelayMs <= 50) { // start calling handler multiple times. if (repeatBase == 0) repeatBase = holdCount; repeatCount = repeatCount + (holdCount - repeatBase) / 5; if (repeatCount > 5) { repeatCount= 5; } } } } } cmsDrawMenu(pCurrentDisplay, currentTimeUs); if (currentTimeMs > lastCmsHeartBeatMs + 500) { // Heart beat for external CMS display device @ 500msec // (Timeout @ 1000msec) displayHeartbeat(pCurrentDisplay); lastCmsHeartBeatMs = currentTimeMs; } } // Some key (command), notably flash erase, takes too long to use the // currentTimeMs to be used as lastCalledMs (freezes CMS for a minute or so // if used). lastCalledMs = millis(); } void cmsHandler(timeUs_t currentTimeUs) { if (cmsDeviceCount < 0) { return; } static timeUs_t lastCalledUs = 0; if (currentTimeUs >= lastCalledUs + CMS_UPDATE_INTERVAL_US) { lastCalledUs = currentTimeUs; cmsUpdate(currentTimeUs); } } void cmsInit(void) { cmsDeviceCount = 0; cmsCurrentDevice = -1; } void inhibitSaveMenu(void) { saveMenuInhibited = true; } #endif // CMS